Attribute added/removed on event

I have made some code that adds(or removes) a name attribute on an input and it is fired depending on some events taking place:
fiddle line 163-179.
Click edit,try to edit one of the values in the input boxes and see a name attribute being added to the input or removed if the user chooses to type in the original value.

There is one more case when the original values are restored…that is when he clicks the cancel button…the code that does that is at lines 60-64(it only applies for the services input).

My problem is that once the original values are restored(with the aforementioned way) the name attribute is not removed as it should be and as it is the case if the user does manually(by typing the original value).

What is wrong here?Is the cause which event comes first?And if yes how to fix it?

The name attr is removed when the input value is equal to the data-value value…it seems that when cancel is clicked the comparison is made at a point where the input value is not restored(yet) to it’s original.

What do you think?

I think that too much complexity is occurring here.

The solution to that is to dumb things down, get the interface working with no javascript, so that the backend gets reliably developed, and then use javascript to improve the user experience, which allows you to then easily improve the backend so that you can request information as needed.

the complexity will be there anyway…backend or frontend…now I must find a solution with the code given though.

This isn’t really an answer but the kind of thing you are trying to achieve is what JavaScript frameworks are made for.

There are lots of them out there but I personally recommend Vue.js. The learning curve is very shallow and it’s easy to get started.

The general idea with these frameworks is you have your data and you have your view (HTML). You describe how the view renders based on the data. What the framework does it update the view when the data changes. no more having to prepend() and append() raw HTML. Ever.

Honestly, spend a few hours with Vue.js and you’ll soon stop using jQuery for the kind of thing. Don’t get me wrong, it has its uses but it’s a headache trying to make this work. With a framework you’ll need less code and get things done much more quickly.

your suggestion is right…
The only problem is that I have spent considerable time and effort to code this specific feature(adding user services) “manually”…I am not sure how good idea is to abandon this work now and use a Framework.

I will surely consider a framework for the next UI feature…

1 Like

Definitely give it a try next time; it will save you lots of time in future. Vue.js is also growing rapidly in popularity (had about 10k stars on github when I started using it, now has over 88k) so it is very useful to know.

I wasn’t happy about just saying “It’s too complex” so I’m trying to make the code simpler and easier to work with.

After using JSBeautifier and JSLint I had code that was easier to think about.

After naming all of the anonymous functions, I’ve moved them up which gives a very simple set of event listeners at the end of the code:

    $('.editservices').on("click", editServices);
    $('#addser').on("click", addService);
    $('#cancelserv').on("click", cancelService);
    $('#saveserv').on("click", saveService);
    $('body').on('click', '#remser', removeService);
    $(".services").on("change paste keyup", servicesChangeHandler);

Some commented code has been replaced by easier to understand code instead:

I’ve also translated the Greek to English, so that others around here will have an easier time helping out too.

And among other improvements, I’ve also reworked the start of the removeService function, making it much easier to understand what’s going on there too.

    function removeService() {
        if ($(".services:visible:last").val() !== "") {
            if (window.confirm("Are you sure you want to delete this service?")) {
                deleteService();
                return;
            }
        }
        $(".wrapper_servp:visible:last").remove();
        var i = 0;
        while (i < originals.ser_input_lgth) {
        // $(".show_price")
        //     // fetch only those sections that have a sub-element with the .value class
        //     .filter((ignore, e) => $(".value", e).length === 1)
        var hasValueClass = (ignore, e) => $(".value", e).length === 1;
        $(".show_price").filter(hasValueClass)

Here’s the updated code that’s much easier to work with:

/*jslint browser, es6 */
/*global jQuery, window */
jQuery(function ($) {
    "use strict";
    var originals = {};
    var services = {};
    var prices = {};
    var show_pr_val = $(".show_pr_value");
    var IDs = [];

    function priceInfo(i, el) {
        var value = $(".value", this).data("value");
        var fieldsetCount = $("#serv").length;
        var index = fieldsetCount + i;
        var yesChecked = (value === 1)
            ? "checked"
            : "";
        var noChecked = (value === 0)
            ? "checked"
            : "";
        return "<div class='show_price'><p id='show_p_msg'>Price visibility</p></div>" +
                "<div class='show_p_inpts'>" +
                "<input class='price_show' " + yesChecked + "type='radio' name='form[" + index + "][price_show]' value='1'>yes" +
                "<input class='price_show' " + noChecked + "type='radio' name='form[" + index + "][price_show]' value='0'>no" +
                "</div>";
    }
    function editServices() {
        originals.ser_input_lgth = $(".services").length;

        $(".openservices")
            .removeClass("hide")
            .find("[readonly]")
            .prop("readonly", false);
        services = $(".services[data-value]").map(function elValue(ignore, el) {
            return $(this).val();
        }).get();

        console.log(originals.ser_input_lgth);

        originals.ser_input_lgth = $(".services").length;

        var hasValueClass = (ignore, e) => $(".value", e).length === 1;
        $(".show_price").filter(hasValueClass)
            .replaceWith(priceInfo);
        $("#buttons").removeClass("prfbuttons");
        $("#saveserv").addClass("hideb");
        $(".actionsserv").removeClass("actionsserv");
        // priceavail = $(".price_show:input").serializeArray();
    }
    function addService() {
        console.log("add handler");
        $("#saveserv").css("border", "2px solid none");

        var serviceCount = $("input.services").length + 1;
        var serv_inputs = "<div class='wrapper_servp'><div class='serv_contain'>\n" +
                "<input placeholder='service' class='services text' name='form[" + serviceCount + "][service]' type='text' size='40'> \n" +
                "<input placeholder='price' class='price text' name='form[" + serviceCount + "][price]' type='text' size='3'></div>";
        var p_show = "<div class='show_p'>" +
                "<p id='show_p_msg'>Price visibility;</p>" +
                "<span id='err_show_p'></span><br>" +
                "</div>";
        var inputs = "<div class='show_p_inpts'>" +
                "<input class='price_show' type='radio' name='form[" + serviceCount + "][price_show]' value='1'>yes" +
                "<input class='price_show' type='radio' name='form[" + serviceCount + "][price_show]' value='0'>no" +
                "</div></div>";
        $(".wrapper_servp").last().after(serv_inputs + p_show + inputs);
        $("#saveserv").removeClass("hideb");
        $("#remser").css("display", "inline");
    }
    function cancelService(e) {
        e.preventDefault();
        var i = 0;
        while (i < originals.ser_input_lgth) {
            $("input[data-name='service" + i + "']").val(services[i]);
            i += 1;
        }
        $(".show_p_inpts")
            .filter((ignore, e) => $(".price_show:checked", e).length === 1)
            .replaceWith(function showPrice() {
                var value = $(".price_show:checked").attr("value");
                var show = (value === 1)
                    ? "yes"
                    : "no";
                return "<span data-value='" + value + "' class='value'>" + show + "</span>";
            });
    }

    function groupHasCheckedBox(ignore, el) {
        return $(this).find("input").filter(function hasCheckbox() {
            return $(el).prop("checked");
        }).length === 0;
    }
    function inputHasValue(ignore, el) {
        return $(this).val() === "";
    }
    function saveService(e) {
        e.preventDefault();
        //from here
        // var $radioGroups = $(".show_p_inpts");
        $(".show_p_inpts").filter(groupHasCheckedBox).closest("div").addClass("error");
        $(".services, .price").filter(inputHasValue).addClass("error");
        //to here

        var $errorInputs = $("input.services").filter((ignore, e) => !e.value.trim());
        if ($errorInputs.length >= 1) {
            console.log($errorInputs);
            $("#err_message").html("you have to fill in the service");
            return;
        }
        if ($("input.price").filter((ignore, e) => !e.value.trim()).length >= 1) {
            $("#err_message").html("you have to fill in the price");
            return;
        }
    }
    function deleteService() {
        IDs.push($(".services:visible:last").data("service"));
        //       originals.dataID;
        console.log(IDs);

        $(".wrapper_servp:visible:last").addClass("btypehide");
        if ($(".serv_contain").length === 1) {
            $("#remser").css("display", "none");
        }
        $("#saveserv").removeClass("hideb").css("border", "5px solid red");

        //originals.servrem=true;
    }
    function removeService() {
        if ($(".services:visible:last").val() !== "") {
            if (window.confirm("Are you sure you want to delete this service?")) {
                deleteService();
                return;
            }
        }
        $(".wrapper_servp:visible:last").remove();
        var i = 0;
        while (i < originals.ser_input_lgth) {
            $("input[name='service" + i + "']").val(services[i].value);
            $("input[name='price" + i + "']").val(prices[i].value);
            i += 1;
        }
        // remove input boxes ... see how it will work after we put a second service
        $(".services").slice(originals.ser_input_lgth).remove();
        $(".price").slice(originals.ser_input_lgth).remove();
        $(".openservices").addClass("hide").find(".services,.price").prop("readonly", true);
        var prValue = (show_pr_val.value === 1)
            ? "Yes"
            : "No";
        var text = "<p class='show_price'>You want the price to appear;<span data-value='" + show_pr_val.value + "' class='value'>" + (prValue) + "</span></p>";

        $(".show_p_inpts").remove();
        $(".show_price").replaceWith(text);
    }

    function servicesChangeHandler(evt) {
        var el = evt.target;
        console.log("change detected");

        $("#saveserv").removeClass("hideb").addClass("higl");
        if ($(this).val() === $(this).attr("data-value")) {

            $(this).removeAttr("name");
        } else {
            var digit = ($(this).attr("data-name")).slice(-1),
                name = ($(this).attr("data-name")).slice(0, -1);
            $(this).attr("name", "form[" + digit + "][" + name + "]");
        }
    }

    $(".editservices").on("click", editServices);
    $("#addser").on("click", addService);
    $("#cancelserv").on("click", cancelService);
    $("#saveserv").on("click", saveService);
    $("body").on("click", "#remser", removeService);
    $(".services").on("change paste keyup", servicesChangeHandler);
});
Edit:

The linter doesn’t like the this keyword, but that hasn’t been touched yet.

3 Likes

I really appreciate for the time you spent revising this whole code…
Many thanks.

1 Like

The next thing to do is to attempt to understand the code based on the issue that you’re facing.
That seems to be too much like hard work, so I’ve been procrastinating with other improvements to the code.

    function showPriceInput(index, isChecked) {
        var checked = (isChecked === true)
            ? "checked "
            : "";
        return "<input class='price_show' " + checked + "type='radio' name='form[" + index + "][price_show]' value='1'>";
    }
    function priceInfo(i) {
        var dataValue = $(".value", this).data("value");
        var fieldsetCount = $("#serv").length;
        var index = fieldsetCount + i;
        return "<div class='show_price'><p id='show_p_msg'>Price visibility</p></div>" +
                "<div class='show_p_inpts'>" +
                showPriceInput(index, dataValue === "1") + "yes" +
                showPriceInput(index, dataValue !== "1") + "no" +
                "</div>";
    }

But that doesn’t help much towards the desired goal.
I’m feeling lost without tests to help, but <wails>I don’t wanna write tests!</wails> Especially after the code has already been written.

All of the code doesn’t need to have tests right now, but I can write tests for the parts that we currently care about.
I can work towards it in little steps, starting with the Edit button where changes to a field results in the name attribute being changed.

To help keep track of progress towards my goal, I’ll list the steps that are taken towards it.
Keeping the goal crystal clear up-front helps to prevent distractions from derailing things.

Goal: Find out and fix what’s wrong with restoring name attribute values.

  • Create tests for existing relevant code
    • Get and install Jasmine
  • Create test to exercise the existing problem
  • Fix the problem, using the test to confirm that it’s fixed

I have the code saved to some local files (index.html, style.css, script.js) in a service manager folder on my computer.
I can then use getting started with Jasmine as a test harness, and set things up by adding the following to the index.html page:

<link rel="shortcut icon" type="image/png" href="jasmine/lib/jasmine-3.1.0/jasmine_favicon.png">
<link rel="stylesheet" type="text/css" href="jasmine/lib/jasmine-3.1.0/jasmine.css">

<script type="text/javascript" src="jasmine/lib/jasmine-3.1.0/jasmine.js"></script>
<script type="text/javascript" src="jasmine/lib/jasmine-3.1.0/jasmine-html.js"></script>
<script type="text/javascript" src="jasmine/lib/jasmine-3.1.0/boot.js"></script>

and after loading the script, we load in a spec for testing it too:

<script src="src/lib/jquery-3.3.1.min.js"></script>
<script src="src/script.js"></script>
<script src="spec/script.spec.js"></script>

Goal: Find out and fix what’s wrong with restoring name attribute values.

  • Create tests for existing relevant code
    • Get and install Jasmine
    • Test the edit section
  • Create test to exercise the existing problem
  • Fix the problem, using the test to confirm that it’s fixed

And I can now put in a basic structure for testing the edit section:

/*jslint browser*/
/*global describe, it*/
describe("service manager", function () {
    "use strict";
    describe("edit", function () {
        it("", function () {
            
        });
    });
});

Goal: Find out and fix what’s wrong with restoring name attribute values.

  • Create tests for existing relevant code
    • Get and install Jasmine
    • Test the edit section
      • Test clicking the edit button
  • Create test to exercise the existing problem
  • Fix the problem, using the test to confirm that it’s fixed

When we click the Edit button, we expect something to happen, in this case that the buttons become visible.

    function isVisible(el) {
        return (el.offsetParent !== null);
    }
    describe("edit", function () {
        it("shows the edit buttons", function () {
            var editButton = document.querySelector(".editservices");
            var buttons = document.querySelector("#buttons");
            expect(isVisible(buttons)).toBe(false);
            editButton.click();
            expect(isVisible(buttons)).toBe(true);
        });
    });

We are now in a good place to add more tests to the edit section.

Goal: Find out and fix what’s wrong with restoring name attribute values.

  • Create tests for existing relevant code
    • Get and install Jasmine
    • Protect the HTML code from effects of the tests
    • Test the edit section
      • Test clicking the edit button
  • Create test to exercise the existing problem
  • Fix the problem, using the test to confirm that it’s fixed

After the test finishes, we need to revert the HTML code back to how it began, so that other tests will have a consistent starting point to work with.

Sometimes you want to do things before each test, but in this case because I want the visual presentation of the HTML code to be no different when the tests end, I’m going to use afterEach here instead to reset the HTML code back to how it began.

    var openServices = document.querySelector(".openservices");
    var initialHTML = openServices.innerHTML;
    afterEach(function () {
        openServices.innerHTML = initialHTML;
    });

But that’s not enough, because none of the events exist on the replaced HTML code, so we need to initialize those once again.

Do you remember how in earlier code I gathered the event listeners together at the bottom of the code?
We can put them inside an init function and return it from the function, so that we can run that init function at any time that we need.

// jQuery(function ($) {
var serviceManager = (function ($) {
  ...
    function initServices() {
        $(".editservices").on("click", editServices);
        $("#addser").on("click", addService);
        $("#cancelserv").on("click", cancelService);
        $("#saveserv").on("click", saveService);
        $("body").on("click", "#remser", removeService);
        $(".services").on("change paste keyup", servicesChangeHandler);
    }
    return {
        init: initServices
    };
// });
}(jQuery));
serviceManager.init();

That lets us use serviceManager.init() from within the test code too.

    afterEach(function () {
        openServices.innerHTML = initialHTML;
        serviceManager.init();
    });

Goal: Find out and fix what’s wrong with restoring name attribute values.

  • Create tests for existing relevant code
    • Get and install Jasmine
    • Localise images to improve testing speed
    • Protect the HTML code from effects of the tests
    • Test the edit section
      • Test clicking the edit button
  • Create test to exercise the existing problem
  • Fix the problem, using the test to confirm that it’s fixed

Each time I reload the test page there’s a delay before the test begins, which on further investigation is because it’s loading some image files from dropbox. Saving those files locally lets the test occur immediately. It doesn’t have much other impact than making me feel better because I’m not waiting half a second for the test to start, but that’s a good enough reason to do so.

    ...<img src="img/edit_btn.png">...
...
      <img ... src="img/add_service.png">
      <img ... src="img/remove_serv.png">

We are now in a good place to add more tests to the edit section.

Here’s the existing test script:

/*jslint browser*/
/*global serviceManager, afterEach, describe, it, expect*/
describe("service manager", function () {
    "use strict";
    var openServices = document.querySelector(".openservices");
    var initialHTML = openServices.innerHTML;
    afterEach(function () {
        openServices.innerHTML = initialHTML;
        serviceManager.init();
    });
    function isVisible(el) {
        return (el.offsetParent !== null);
    }
    describe("edit", function () {
        it("shows the edit buttons", function () {
            var editButton = document.querySelector(".editservices");
            var buttons = document.querySelector("#buttons");
            expect(isVisible(buttons)).toBe(false);
            editButton.click();
            expect(isVisible(buttons)).toBe(true);
        });
    });
});

And the existing scripting code:

/*jslint browser, es6 */
/*global jQuery, window */
var serviceManager = (function ($) {
    "use strict";
    var originals = {};
    var services = {};
    var prices = {};
    var show_pr_val = $(".show_pr_value");
    var IDs = [];

    function showPriceInput(index, isChecked) {
        var checked = (isChecked === true)
            ? "checked "
            : "";
        return "<input class='price_show' " + checked + "type='radio' name='form[" + index + "][price_show]' value='1'>";
    }
    function priceInfo(i) {
        var dataValue = $(".value", this).data("value");
        var fieldsetCount = $("#serv").length;
        var index = fieldsetCount + i;
        return "<div class='show_price'><p id='show_p_msg'>Price visibility</p></div>" +
                "<div class='show_p_inpts'>" +
                showPriceInput(index, dataValue === "1") + "yes" +
                showPriceInput(index, dataValue !== "1") + "no" +
                "</div>";
    }
    function editServices() {
        originals.ser_input_lgth = $(".services").length;

        $(".openservices")
            .removeClass("hide")
            .find("[readonly]")
            .prop("readonly", false);
        services = $(".services[data-value]").map(function elValue(ignore) {
            return $(this).val();
        }).get();

        console.log(originals.ser_input_lgth);

        originals.ser_input_lgth = $(".services").length;

        var hasValueClass = (ignore, e) => $(".value", e).length === 1;
        $(".show_price")
            .filter(hasValueClass)
            .replaceWith(priceInfo);
        $("#buttons").removeClass("prfbuttons");
        $("#saveserv").addClass("hideb");
        $(".actionsserv").removeClass("actionsserv");
        // priceavail = $(".price_show:input").serializeArray();
    }
    function addService() {
        console.log("add handler");
        $("#saveserv").css("border", "2px solid none");

        var serviceCount = $("input.services").length + 1;
        var serv_inputs = "<div class='wrapper_servp'><div class='serv_contain'>\n" +
                "<input placeholder='service' class='services text' name='form[" + serviceCount + "][service]' type='text' size='40'> \n" +
                "<input placeholder='price' class='price text' name='form[" + serviceCount + "][price]' type='text' size='3'></div>";
        var p_show = "<div class='show_p'>" +
                "<p id='show_p_msg'>Price visibility;</p>" +
                "<span id='err_show_p'></span><br>" +
                "</div>";
        var inputs = "<div class='show_p_inpts'>" +
                "<input class='price_show' type='radio' name='form[" + serviceCount + "][price_show]' value='1'>yes" +
                "<input class='price_show' type='radio' name='form[" + serviceCount + "][price_show]' value='0'>no" +
                "</div></div>";
        $(".wrapper_servp").last().after(serv_inputs + p_show + inputs);
        $("#saveserv").removeClass("hideb");
        $("#remser").css("display", "inline");
    }
    function cancelService(e) {
        e.preventDefault();
        var i = 0;
        while (i < originals.ser_input_lgth) {
            $("input[data-name='service" + i + "']").val(services[i]);
            i += 1;
        }
        $(".show_p_inpts")
            .filter((ignore, e) => $(".price_show:checked", e).length === 1)
            .replaceWith(function showPriceSpan() {
                var value = $(".price_show:checked").attr("value");
                var show = (value === 1)
                    ? "yes"
                    : "no";
                return "<span data-value='" + value + "' class='value'>" + show + "</span>";
            });
    }

    function groupHasCheckedBox(ignore, el) {
        return $(this).find("input").filter(function hasCheckbox() {
            return $(el).prop("checked");
        }).length === 0;
    }
    function inputHasValue() {
        return $(this).val() === "";
    }
    function saveService(e) {
        e.preventDefault();
        //from here
        // var $radioGroups = $(".show_p_inpts");
        $(".show_p_inpts").filter(groupHasCheckedBox).closest("div").addClass("error");
        $(".services, .price").filter(inputHasValue).addClass("error");
        //to here

        var $errorInputs = $("input.services").filter((ignore, e) => !e.value.trim());
        if ($errorInputs.length >= 1) {
            console.log($errorInputs);
            $("#err_message").html("you have to fill in the service");
            return;
        }
        if ($("input.price").filter((ignore, e) => !e.value.trim()).length >= 1) {
            $("#err_message").html("you have to fill in the price");
            return;
        }
    }
    function deleteService() {
        IDs.push($(".services:visible:last").data("service"));
        //       originals.dataID;
        console.log(IDs);

        $(".wrapper_servp:visible:last").addClass("btypehide");
        if ($(".serv_contain").length === 1) {
            $("#remser").css("display", "none");
        }
        $("#saveserv").removeClass("hideb").css("border", "5px solid red");

        //originals.servrem=true;
    }
    function removeService() {
        if ($(".services:visible:last").val() !== "") {
            if (window.confirm("Are you sure you want to delete this service?")) {
                deleteService();
                return;
            }
        }
        $(".wrapper_servp:visible:last").remove();
        var i = 0;
        while (i < originals.ser_input_lgth) {
            $("input[name='service" + i + "']").val(services[i].value);
            $("input[name='price" + i + "']").val(prices[i].value);
            i += 1;
        }
        // remove input boxes ... see how it will work after we put a second service
        $(".services").slice(originals.ser_input_lgth).remove();
        $(".price").slice(originals.ser_input_lgth).remove();
        $(".openservices").addClass("hide").find(".services,.price").prop("readonly", true);
        var prValue = (show_pr_val.value === 1)
            ? "Yes"
            : "No";
        var text = "<p class='show_price'>You want the price to appear;<span data-value='" + show_pr_val.value + "' class='value'>" + (prValue) + "</span></p>";

        $(".show_p_inpts").remove();
        $(".show_price").replaceWith(text);
    }

    function servicesChangeHandler(evt) {
        var el = evt.target;
        console.log("change detected");

        $("#saveserv").removeClass("hideb").addClass("higl");
        if ($(this).val() === $(this).attr("data-value")) {

            $(this).removeAttr("name");
        } else {
            var digit = ($(this).attr("data-name")).slice(-1),
                name = ($(this).attr("data-name")).slice(0, -1);
            $(this).attr("name", "form[" + digit + "][" + name + "]");
        }
    }
    function initServices() {
        $(".editservices").on("click", editServices);
        $("#addser").on("click", addService);
        $("#cancelserv").on("click", cancelService);
        $("#saveserv").on("click", saveService);
        $("body").on("click", "#remser", removeService);
        $(".services").on("change paste keyup", servicesChangeHandler);
    }
    return {
        init: initServices
    };
}(jQuery));
serviceManager.init();

I’ll follow up with more tests on the next post.

2 Likes

We can now start adding tests for the different things that the OP says that are relevant to the problem wanting to be solved.

[quote=“designtrooper, post:1, topic:292711, full:true”]
Click edit,try to edit one of the values in the input boxes and see a name attribute being added to the input or removed if the user chooses to type in the original value.[/quote]

We have already tested for clicking the Edit button.

Testing that the name attribute changes means triggering the change event on the input field.

    function triggerEvent(name, el) {
        var event = document.createEvent('Event');
        event.initEvent(name);
        el.dispatchEvent(event);
    }
    ...
        it("adds a name attribute when the value is changed", function () {
            var editButton = document.querySelector(".editservices");
            var input = openServices.querySelector("input");
            editButton.click();
            expect(input.name).toBe("");
            input.value = "test";
            triggerEvent("change", input);
            expect(input.name).toBe("form[0][service]");
        });

It looks like the editButton variable will be needed by pretty much all of the tests, so we can declare it at the top, and assign that in the beforeEach() function instead:

    var editButton;
    beforeEach(function () {
        editButton = document.querySelector(".editservices");
    });
    ...
        it("shows the edit buttons", function () {
            // var editButton = document.querySelector(".editservices");
            var buttons = document.querySelector("#buttons");
            ...
        });
        it("adds a name attribute when the value is changed", function () {
            // var editButton = document.querySelector(".editservices");
            var input = openServices.querySelector("input");
            ...
        });

And returning it back to the original value should result in the name attribute being empty.

        it("removes the name attribute when the value is returned back to how it started", function () {
            var input = openServices.querySelector("input");
            editButton.click();
            input.value = "test";
            triggerEvent("change", input);
            input.value = input.defaultValue;
            triggerEvent("change", input);
            expect(input.name).toBe("");
        });

A strange bug occurs because Jasmine does the tests in a random order. When the last test is the first one to occur, the input value is “test” instead of “hair”. Moving the afterEach code into the beforeEach function fixes that strange oddity.

This goes to show that it’s important that each test has the same initial starting conditions.

    beforeEach(function () {
        openServices.innerHTML = initialHTML;
        serviceManager.init();
        editButton = document.querySelector(".editservices");
    });
    afterEach(function () {
        // openServices.innerHTML = initialHTML;
        // serviceManager.init();
    });

Instead of using afterEach, I’ll put it in afterAll so that the page is ready for us to use if we want to do anything manually with it too.

    afterAll(function () {
        // openServices.innerHTML = initialHTML;
        // serviceManager.init();
    });

The next test involves the cancel button.

[quote=“designtrooper, post:1, topic:292711, full:true”]
There is one more case when the original values are restored…that is when he clicks the cancel button…the code that does that is at lines 60-64(it only applies for the services input).[/quote]

The test for doing this is:

        it("removes the name attribute when cancelled", function () {
            var input = openServices.querySelector("input");
            var cancelButton = openServices.querySelector("#cancelserv");
            editButton.click();
            input.value = "test";
            triggerEvent("change", input);
            triggerEvent("click", cancelButton);
            expect(input.name).toBe("");
        });

But, the test doesn’t pass because the name attribute is still form[0][service] from when the field was changed.

[quote=“designtrooper, post:1, topic:292711, full:true”]
My problem is that once the original values are restored(with the aforementioned way) the name attribute is not removed as it should be and as it is the case if the user does manually(by typing the original value).[/quote]

Aha - so the test is correctly showing the problem.

We can now return back to the list of tasks being done:

  • Create tests for existing relevant code
    • Get and install Jasmine
    • Localise images to improve testing speed
    • Protect the HTML code from effects of the tests
    • Test the edit section
      • Test clicking the edit button
  • Create test to exercise the existing problem
  • Fix the problem, using the test to confirm that it’s fixed

[quote=“designtrooper, post:1, topic:292711, full:true”]
What is wrong here?Is the cause which event comes first?And if yes how to fix it?[/quote]

Well let’s find out. We can add some console.log statements to show in what order things occur.

        it("removes the name attribute when cancelled", function () {
            var input = openServices.querySelector("input");
            var cancelButton = openServices.querySelector("#cancelserv");
            editButton.click();
            input.value = "test";
            console.log("1. trigger change event");
            triggerEvent("change", input);
            console.log("2. trigger cancel event");
            triggerEvent("click", cancelButton);
            console.log("3. expect input.name");
            expect(input.name).toBe("");
        });

and we can add another console.log in the cancelService function:

    function cancelService(e) {
        console.log("cancel service");
        ...
    }

We can now examine the console to find out where the “cancel service” occurs, and it’s always shown in the correct place, between 2. and 3.

1. trigger change event
change detected
2. trigger cancel event
cancel service
3. expect input.name

In the cancelService function the while loop resets the input values:

        while (i < originals.ser_input_lgth) {
            $("input[data-name='service" + i + "']").val(services[i]);
            i += 1;
        }

But, I see nowhere in there at the name attribute is changed. We can add that to the while loop too:

        while (i < originals.ser_input_lgth) {
            $("input[data-name='service" + i + "']").val(services[i]);
            $("input[data-name='service" + i + "']").attr("name", "");
            i += 1;
        }

And running the test confirms that things are now working.

  • Create tests for existing relevant code
    • Get and install Jasmine
    • Localise images to improve testing speed
    • Protect the HTML code from effects of the tests
    • Test the edit section
      • Test clicking the edit button
  • Create test to exercise the existing problem
  • Fix the problem, using the test to confirm that it’s fixed

There’s no need for the console.log statements now, and they can be removed, leaving us with the following test code:

        it("removes the name attribute when cancelled", function () {
            var input = openServices.querySelector("input");
            var cancelButton = openServices.querySelector("#cancelserv");
            editButton.click();
            input.value = "test";
            triggerEvent("change", input);
            triggerEvent("click", cancelButton);
            expect(input.name).toBe("");
        });

The test performed an important aspect here. It doesn’t just document how the code is supposed to work, it does more than that.

Before making the code change the test was clearly exposing the problem. After the code change the very same test was confirming that the code is works properly.

With any future changes to the code, the same tests will also help to warn us if any regressions occur too.

The test code that we currently have is:

/*jslint browser*/
/*global serviceManager, beforeEach, afterAll, describe, it, expect*/
describe("service manager", function () {
    "use strict";
    var openServices = document.querySelector(".openservices");
    var initialHTML = openServices.innerHTML;
    var editButton;
    beforeEach(function () {
        openServices.innerHTML = initialHTML;
        serviceManager.init();
        editButton = document.querySelector(".editservices");
    });
    afterAll(function () {
        openServices.innerHTML = initialHTML;
        serviceManager.init();
    });
    function triggerEvent(name, el) {
        var event = document.createEvent('Event');
        event.initEvent(name);
        el.dispatchEvent(event);
    }
    function isVisible(el) {
        return (el.offsetParent !== null);
    }
    describe("edit", function () {
        it("shows the edit buttons", function () {
            var buttons = document.querySelector("#buttons");
            expect(isVisible(buttons)).toBe(false);
            editButton.click();
            expect(isVisible(buttons)).toBe(true);
        });
        it("adds a name attribute when the value is changed", function () {
            var input = openServices.querySelector("input");
            editButton.click();
            expect(input.name).toBe("");
            input.value = "test";
            triggerEvent("change", input);
            expect(input.name).toBe("form[0][service]");
        });
        it("removes the name attribute when the value is returned back to how it started", function () {
            var input = openServices.querySelector("input");
            editButton.click();
            input.value = "test";
            triggerEvent("change", input);
            input.value = input.defaultValue;
            triggerEvent("change", input);
            expect(input.name).toBe("");
        });
        it("removes the name attribute when cancelled", function () {
            var input = openServices.querySelector("input");
            var cancelButton = openServices.querySelector("#cancelserv");
            editButton.click();
            input.value = "test";
            triggerEvent("change", input);
            triggerEvent("click", cancelButton);
            expect(input.name).toBe("");
        });
    });
});

The full code can be downloaded from https://www.dropbox.com/s/yn0x14jbcho3efk/service%20manager.zip?dl=0

1 Like

This topic was automatically closed 91 days after the last reply. New replies are no longer allowed.