// The order of the requires bellow is important //= require jquery-1.10.2.min.js //= require jquery-ui-1.10.3.custom.min.js //= require jquery.json-2.4.min.js //= require jquery.simulate.js //= require jquery.simulate-ext.js //= require jquery.simulate.drag-n-drop.js describe("Ballonizer", function () { /* jshint strict: false */ // We disable the strict mode here because the libs included // with "//= require" break the tests if with strict mode // Variables pre-set before each test var actionFormURL, imageToBallonizeCSSSelector, initialBallonText, instance; beforeEach(function () { actionFormURL = "/action/path/to/submit"; imageToBallonizeCSSSelector = ".ballonizer_image_container"; initialBallonText = "double click to edit"; instance = null; this.addMatchers({ toHaveBallonizerForm: function (actionFormURL) { var lastBodyChild = this.actual.children().last(); expect(lastBodyChild).toBe("form.ballonizer_page_form" + "[method='post'][action='" + actionFormURL + "']"); expect(lastBodyChild).toContain("input[type='submit']" + "[name='ballonizer_submit']"); expect(lastBodyChild).toContain("input[type='hidden']" + "[name='ballonizer_data']"); return true; }, toBeAnEmptyObject: function () { var obj = this.actual; for (var p in obj) { if (obj.hasOwnProperty(p)) { this.message = "expected object to be empty but" + "found the key '" + p + "'"; return false; } } return true; } }); }); afterEach(function () { // Remove left-overs created out of the #jasmine-fixtures container $(".ballonizer_page_form").remove(); $(".ballonizer_image_form").remove(); }); it("it's defined", function () { expect(Ballonizer).toBeDefined(); }); describe("when exist images to ballonize in the document", function () { describe("but none in the context", function () { it("doesn't create a form", function () { loadFixtures("ballonized-xkcd-without-ballons.html"); Ballonizer(actionFormURL, imageToBallonizeCSSSelector, $("#bottom"), $); expect($("form.ballonizer_page_form")).not.toExist(); }); it(".getForm return null", function () { instance = Ballonizer(actionFormURL, imageToBallonizeCSSSelector, $("#bottom"), $); expect(instance.getForm()).toEqual(null); }); }); describe("and they are in the context", function () { it("create a hidden form as the last child of the context", function () { loadFixtures("ballonized-xkcd-without-ballons.html"); Ballonizer(actionFormURL, imageToBallonizeCSSSelector, $("#jasmine-fixtures"), $); expect($("#jasmine-fixtures")).toHaveBallonizerForm(actionFormURL); expect($("form.ballonizer_page_form")).toBeHidden(); }); it("changes in .getForm result affect the node in the DOM", function () { loadFixtures("ballonized-xkcd-without-ballons.html"); instance = Ballonizer(actionFormURL, imageToBallonizeCSSSelector, $("#jasmine-fixtures"), $); instance.getForm().attr("method", "get"); expect($("form.ballonizer_page_form")).toHaveAttr("method", "get"); }); }); describe("when the ballonized image is double clicked", function () { it("a new ballon will be created", function () { loadFixtures("ballonized-xkcd-without-ballons.html"); instance = Ballonizer(actionFormURL, imageToBallonizeCSSSelector, $("#jasmine-fixtures"), $); var ballonizedImg = $(".ballonizer_image_container img"); ballonizedImg.trigger("dblclick"); expect($(".ballonizer_ballon")).toExist(); }); it("the default action of clicking the image (if any) will not trigger", function () { loadFixtures("ballonized-xkcd-with-anchor-in-image.html"); instance = Ballonizer(actionFormURL, imageToBallonizeCSSSelector, $("#jasmine-fixtures"), $); var spyAnchor = spyOnEvent("#comic a", "click"); var ballonizedImg = $(".ballonizer_image_container img"); // In a real browser the dblclick event will be generated with one or // two clicks events before it (http://api.jquery.com/dblclick/) ballonizedImg.trigger("click"); expect(spyAnchor).toHaveBeenPrevented(); }); }); }); describe(".getBallonizedImageContainers", function () { describe("when there's no image to ballonize", function () { it("return an object without any keys", function () { instance = Ballonizer(actionFormURL, imageToBallonizeCSSSelector, $("#jasmine-fixtures"), $); var ballonizedImageContainers = instance.getBallonizedImageContainers(); expect(ballonizedImageContainers).toBeAnEmptyObject(); }); }); describe("when there's a image to ballonize", function () { it("return an object with the img src as key and the object as value", function () { loadFixtures("ballonized-xkcd-without-ballons.html"); instance = Ballonizer(actionFormURL, imageToBallonizeCSSSelector, $("#jasmine-fixtures"), $); var ballonizedImageContainers = instance.getBallonizedImageContainers(); expect(ballonizedImageContainers.hasOwnProperty("http://imgs.xkcd.com/comics/cells.png")).toBeTruthy(); }); }); }); describe("BallonizedImageContainer", function () { it("adds a hidden form for the ballon edition in the body", function () { loadFixtures("ballonized-xkcd-without-ballons.html"); instance = Ballonizer(actionFormURL, imageToBallonizeCSSSelector, $("#jasmine-fixtures"), $); expect($("body")).toContain("form.ballonizer_image_form"); // What the value of action is not important, but the action attribute // is required (http://www.w3.org/TR/html401/interact/forms.html#h-17.3) expect($("form.ballonizer_image_form")).toHaveAttr("action"); expect($("form.ballonizer_image_form")).toBeHidden(); }); describe("when there's a image with ballons", function () { it("getBallons return they", function () { loadFixtures("ballonized-xkcd-with-ballons.html"); instance = Ballonizer(actionFormURL, imageToBallonizeCSSSelector, $("#jasmine-fixtures"), $); var ballonizedImageContainers = instance.getBallonizedImageContainers(); var ballons = ballonizedImageContainers["http://imgs.xkcd.com/comics/cells.png"].getBallons(); expect(ballons.length).toEqual(2); expect(ballons[0].getText()).toEqual("When you see a claim that a common drug or vitamin \"kills cancer cells in a petri dish\", keep in mind:"); expect(ballons[1].getText()).toEqual("So does a handgun."); expect(ballons[0].getPositionAndSize()).toEqual( { left: 0, top: 0, width: 218, height: 82 } ); expect(ballons[1].getPositionAndSize()).toEqual( { left: 21, top: 319, width: 170, height: 19 } ); }); }); }); describe("InterfaceBallon", function () { var htmlUnescape = function (value) { return (value) .replace(/"/g, '"') .replace(/'/g, "'") .replace(/</g, '<') .replace(/>/g, '>') .replace(/&/g, '&'); }; var containerWidth, containerHeight; var getBallons = function () { loadFixtures("ballonized-xkcd-with-ballons.html"); instance = Ballonizer(actionFormURL, imageToBallonizeCSSSelector, $("#jasmine-fixtures"), $); var imageContainer = instance.getBallonizedImageContainers()["http://imgs.xkcd.com/comics/cells.png"], ballons = imageContainer.getBallons(), containerNode = imageContainer.getContainerNode(); containerWidth = containerNode.width(); containerHeight = containerNode.height(); return ballons; }; var realWorldEvent = function (eventName, obj) { switch (eventName) { case "dblclick": obj.mousedown().mouseup().click().mousedown().mouseup().click().dblclick(); break; case "click": obj.mousedown().mouseup().click(); break; } return obj; }; var ballons = null; it("creates a hidden edit ballon for each ballon", function () { ballons = getBallons(); var imgContainer = ballons[0].getBallonizedImageContainer(); var imgFormInnerContainer = imgContainer.getFormInnerContainer(); expect(imgFormInnerContainer.children().length).toEqual(2); expect(imgFormInnerContainer.children()).toBeHidden(); expect(imgFormInnerContainer).toContain( "textarea.ballonizer_edition_ballon" ); var textfields = imgFormInnerContainer.children().toArray(); var textfieldTexts = textfields.map(function (e, ix) { /* jshint unused: false */ return $(e).val(); }).sort(); var ballonTexts = jQuery.map(ballons, function (e, ix) { /* jshint unused: false */ return e.getText(); }).sort(); expect(textfieldTexts).toEqual(ballonTexts); }); it("initial state is 'initial'", function () { ballons = getBallons(); expect(ballons[0].getState()).toEqual("initial"); expect(ballons[1].getState()).toEqual("initial"); }); describe("when in initial state", function () { it("the ballon can be dragged (and this update the getBounds return)", function () { ballons = getBallons(); // We are using the second ballon because the first // can't be dragged in the x axis, it already occupy // all the x axis var oldBounds = ballons[1].getPositionAndSize(); ballons[1].getNode().simulate("drag-n-drop", { dx: -20, dy: -20 }); expect(ballons[1].getPositionAndSize()).toEqual({ left: (oldBounds.left - 20), top: (oldBounds.top - 20), width: oldBounds.width, height: oldBounds.height }); }); it("the ballon can be resized (and this update the getBounds return)", function () { ballons = getBallons(); var oldBounds = ballons[0].getPositionAndSize(); var resizeHandle = $(".ui-icon-gripsmall-diagonal-se", ballons[0].getNode()); // The handle is hidden when not hovering over the ballon // (which includes the handle) resizeHandle.mouseover().simulate("drag-n-drop", { dx: -20, dy: -20 }).mouseout(); expect(ballons[0].getPositionAndSize()).toEqual({ left: oldBounds.left, top: oldBounds.top, width: oldBounds.width - 20, height: oldBounds.height - 20 }); }); }); describe("when dblclicked 2x(mousedown + mouseup + click) + dblclick", function () { it("alternate to edit mode initial -> edit)", function () { ballons = getBallons(); realWorldEvent("dblclick", ballons[0].getNode()); expect(ballons[0].getState()).toEqual("edit"); }); it("hide the normal ballon and put the textarea (focused) in the place", function () { ballons = getBallons(); var normalNode = ballons[0].getNormalNode(); var editionNode = ballons[0].getEditionNode(); var normalNodeBounds = normalNode.offset(); normalNodeBounds.width = normalNode.css('width'); normalNodeBounds.height = normalNode.css('height'); // alternate to the edition mode realWorldEvent("dblclick", normalNode); expect(normalNode).toBeHidden(); expect(editionNode).toBeVisible(); expect(editionNode).toBeFocused(); var editionNodeBounds = editionNode.offset(); editionNodeBounds.width = editionNode.css('width'); editionNodeBounds.height = editionNode.css('height'); expect(editionNodeBounds).toEqual(normalNodeBounds); }); describe("when the container is inside a element", function () { it("prevent the default action of changing the page", function () { // first create a ballon loadFixtures("ballonized-xkcd-with-anchor-in-image.html"); instance = Ballonizer(actionFormURL, imageToBallonizeCSSSelector, $("#jasmine-fixtures"), $); var ballonizedImg = $(".ballonizer_image_container img"); realWorldEvent("dblclick", ballonizedImg); var ballon = $(".ballonizer_ballon"); // and then we simulate a dblclick over the ballon var spyAnchor = spyOnEvent("#comic a", "click"); realWorldEvent("dblclick", ballon); expect(spyAnchor).toHaveBeenPrevented(); }); }); }); describe("when the focus is lost in edit mode", function () { it("the ballon return to the initial mode with the new text", function () { ballons = getBallons(); // Edit the text of the first ballon realWorldEvent("dblclick", ballons[0].getNode()); var firstNewText = "This is the first ballon text"; ballons[0].getNode().val(firstNewText); // simulate focus loss ballons[0].getNode().blur(); expect(ballons[0].getNormalNode()).toBeVisible(); expect(ballons[0].getText()).toEqual(firstNewText); expect(ballons[0].getNormalNode().text()).toEqual(firstNewText); expect(ballons[0].getEditionNode()).toBeHidden(); }); describe("and the ballon is empty (or only with spaces)", function () { it("the ballon is removed", function () { ballons = getBallons(); var imgContainer = ballons[0].getBallonizedImageContainer(); realWorldEvent("dblclick", ballons[0].getNode()); ballons[0].getNode().val(""); ballons[0].getNode().blur(); ballons = imgContainer.getBallons(); expect(ballons.length).toEqual(1); expect($(".ballonizer_ballon")).toHaveLength(1); expect($("form.ballonizer_image_form " + ".ballonizer_edition_ballon")).toHaveLength(1); }); }); }); describe("when the ballons change", function () { describe("position", function () { var changePosition = function (ballon, dx, dy) { ballon.getNode().simulate("drag-n-drop", { dx: dx, dy: dy }); return ballon; }; // We are using the second ballon in the tests because // the first can't be dragged in the x axis, it already // occupy all the x axis it("the button to submit change appears", function () { ballons = getBallons(); changePosition(ballons[1], -20, -20); expect($("form.ballonizer_page_form input[name='ballonizer_submit']")).toBeVisible(); }); it("the serialize methods shows the new values", function () { ballons = getBallons(); var ballon = ballons[1], image = ballon.getBallonizedImageContainer(), ballonizer = image.getBallonizerInstance(), bounds = ballon.getPositionAndSize(), expectedLeft = (bounds.left - 20) / containerWidth, expectedTop = (bounds.top - 20) / containerHeight; changePosition(ballons[1], -20, -20); expect(ballon.serialize().left).toEqual(expectedLeft); expect(ballon.serialize().top).toEqual(expectedTop); expect(image.serialize()[1].left).toEqual(expectedLeft); expect(image.serialize()[1].top).toEqual(expectedTop); expect(ballonizer.serialize()["http://imgs.xkcd.com/comics/cells.png"][1].left).toEqual(expectedLeft); expect(ballonizer.serialize()["http://imgs.xkcd.com/comics/cells.png"][1].top).toEqual(expectedTop); }); it("the page form data update", function () { /* jshint camelcase: false */ // font_size is in ruby style because is sent to the ruby ballons = getBallons(); var formData = $("form.ballonizer_page_form input[name='ballonizer_data']"); expect(formData).toHaveValue(""); changePosition(ballons[1], -20, -20); expect(jQuery.parseJSON(htmlUnescape(formData.val()))).toEqual({ "http://imgs.xkcd.com/comics/cells.png": [ { left: 0, top: 0, width: 1, height: 0.24188790560471976, font_size: 15, text: 'When you see a claim that a common drug or vitamin "kills cancer cells in a petri dish", keep in mind:' }, { left: 0.0045871559633027525, top: 0.8820058997050148, width: 0.7798165137614679, height: 0.05604719764011799, font_size: 14, text: 'So does a handgun.' } ] }); }); }); describe("size", function () { var changeSize = function (ballon, dx, dy) { var resizeHandle = $(".ui-icon-gripsmall-diagonal-se", ballon.getNode()); // The handle is hidden when not hovering over the ballon (which includes the handle) resizeHandle.mouseover().simulate("drag-n-drop", { dx: dx, dy: dy }).mouseout(); return ballon; }; it("the button to submit change appears", function () { ballons = getBallons(); changeSize(ballons[0], -20, -20); expect($("form.ballonizer_page_form input[name='ballonizer_submit']")).toBeVisible(); }); it("the serialize methods shows the new values (to size and font-size)", function () { /* jshint camelcase: false */ // font_size is in ruby style because is sent to the ruby ballons = getBallons(); var ballon = ballons[0], image = ballon.getBallonizedImageContainer(), ballonizer = image.getBallonizerInstance(), bounds = ballon.getPositionAndSize(), expectedWidth = (bounds.width - 20) / containerWidth, expectedHeight = (bounds.height - 20) / containerHeight, expectedFontSize = 11; // verified by observation changeSize(ballons[0], -20, -20); // check the serialize of the ballon expect(ballon.serialize().width).toEqual(expectedWidth); expect(ballon.serialize().height).toEqual(expectedHeight); expect(ballon.serialize().font_size).toEqual(expectedFontSize); // check the serialize of the image expect(image.serialize()[0].width).toEqual(expectedWidth); expect(image.serialize()[0].height).toEqual(expectedHeight); expect(image.serialize()[0].font_size).toEqual(expectedFontSize); // check the serialize of the ballonizer expect(ballonizer.serialize()["http://imgs.xkcd.com/comics/cells.png"][0].width).toEqual(expectedWidth); expect(ballonizer.serialize()["http://imgs.xkcd.com/comics/cells.png"][0].height).toEqual(expectedHeight); expect(ballonizer.serialize()["http://imgs.xkcd.com/comics/cells.png"][0].font_size).toEqual(expectedFontSize); }); it("the page form data update (size and font-size)", function () { /* jshint camelcase: false */ // font_size is in ruby style because is sent to the ruby ballons = getBallons(); var formData = $("form.ballonizer_page_form input[name='ballonizer_data']"); expect(formData).toHaveValue(""); changeSize(ballons[0], -20, -20); expect(jQuery.parseJSON(htmlUnescape(formData.val()))).toEqual({ "http://imgs.xkcd.com/comics/cells.png": [ { left: 0, top: 0, width: 0.908256880733945, height: 0.18289085545722714, // font-size is changed by the change of the ballon size font_size: 11, text: 'When you see a claim that a common drug or vitamin "kills cancer cells in a petri dish", keep in mind:' }, { left: 0.0963302752293578, top: 0.9410029498525073, width: 0.7798165137614679, height: 0.05604719764011799, font_size: 14, text: 'So does a handgun.' } ] }); }); }); describe("text", function () { var changeText = function (ballon, text) { realWorldEvent("dblclick", ballon.getNode()); ballon.getNode().val(text); ballon.getNode().blur(); return ballon; }; it("the button to submit change appears", function () { ballons = getBallons(); changeText(ballons[0], "Ballon new text"); expect($("form.ballonizer_page_form input[name='ballonizer_submit']")).toBeVisible(); }); it("the serialize methods shows the new values (for text and font-size)", function () { /* jshint camelcase: false */ // font_size is in ruby style because is sent to the ruby ballons = getBallons(); var ballon = ballons[0], text = "Ballon new text", image = ballon.getBallonizedImageContainer(), ballonizer = image.getBallonizerInstance(), expectedFontSize = 32; // verified by observation changeText(ballon, text); // ballon serialize expect(ballon.serialize().text).toEqual(text); expect(ballon.serialize().font_size).toEqual(expectedFontSize); // image serialize expect(image.serialize()[0].text).toEqual(text); expect(image.serialize()[0].font_size).toEqual(expectedFontSize); // ballonizer serialize expect(ballonizer.serialize()["http://imgs.xkcd.com/comics/cells.png"][0].text).toEqual(text); expect(ballonizer.serialize()["http://imgs.xkcd.com/comics/cells.png"][0].font_size).toEqual(expectedFontSize); }); it("the page form data update (text and font-size)", function () { /* jshint camelcase: false */ // font_size is in ruby style because is sent to the ruby ballons = getBallons(); var formData = $("form.ballonizer_page_form input[name='ballonizer_data']"); expect(formData).toHaveValue(""); changeText(ballons[0], "Ballon new text"); expect(jQuery.parseJSON(htmlUnescape(formData.val()))).toEqual({ "http://imgs.xkcd.com/comics/cells.png": [ { left: 0, top: 0, width: 1, height: 0.24188790560471976, // the font-size increment as the ballon size // is the same but have lesser text font_size: 32, text: 'Ballon new text' }, { left: 0.0963302752293578, top: 0.9410029498525073, width: 0.7798165137614679, height: 0.05604719764011799, font_size: 14, text: 'So does a handgun.' } ] }); }); }); }); describe("when a ballon is added", function () { var addBallon = function () { var offset = $(".ballonizer_image_container").offset(); $(".ballonizer_image_container img").trigger( jQuery.Event("dblclick", { pageX: offset.left + 100, pageY: offset.top + 109 }) ); }; it("the button to submit change appears", function () { getBallons(); addBallon(); expect($("form.ballonizer_page_form input[name='ballonizer_submit']")).toBeVisible(); }); it("the serialize methods shows the new values", function () { ballons = getBallons(); addBallon(); var image = ballons[0].getBallonizedImageContainer(), imageData = image.serialize(), ballonizer = image.getBallonizerInstance(), ballonizerData = ballonizer.serialize(); expect(imageData.length).toEqual(3); expect(ballonizerData["http://imgs.xkcd.com/comics/cells.png"].length).toEqual(3); }); it("the page form data update", function () { /* jshint camelcase: false */ // font_size is in ruby style because is sent to the ruby getBallons(); var formData = $("form.ballonizer_page_form input[name='ballonizer_data']"); expect(formData).toHaveValue(""); addBallon(); expect(jQuery.parseJSON(htmlUnescape(formData.val()))).toEqual({ "http://imgs.xkcd.com/comics/cells.png": [ { left: 0, top: 0, width: 1, height: 0.24188790560471976, font_size: 15, text: 'When you see a claim that a common drug or vitamin "kills cancer cells in a petri dish", keep in mind:' }, { left: 0.0963302752293578, top: 0.9410029498525073, width: 0.7798165137614679, height: 0.05604719764011799, font_size: 14, text: 'So does a handgun.' }, { left: 0.40825688073394495, top: 0.3215339233038348, width: 0.591743119266055, height: 0.12094395280235988, font_size: 15, text: 'double click to edit ballon text' } ] }); }); }); describe("when a ballon is removed", function () { var removeBallon = function (ballon) { realWorldEvent("dblclick", ballon.getNode()); ballon.getNode().val(""); ballon.getNode().blur(); return ballon; }; it("the button to submit change appears", function () { ballons = getBallons(); removeBallon(ballons[0]); expect($("form.ballonizer_page_form input[name='ballonizer_submit']")).toBeVisible(); return ballons; }); it("the serialize methods shows the new values", function () { ballons = getBallons(); var image = ballons[0].getBallonizedImageContainer(), ballonizer = image.getBallonizerInstance(); removeBallon(ballons[0]); var imageData = image.serialize(), ballonizerData = ballonizer.serialize(); expect(imageData.length).toEqual(1); expect(ballonizerData["http://imgs.xkcd.com/comics/cells.png"].length).toEqual(1); }); it("the page form data update", function () { /* jshint camelcase: false */ // font_size is in ruby style because is sent to the ruby ballons = getBallons(); var formData = $("form.ballonizer_page_form input[name='ballonizer_data']"); expect(formData).toHaveValue(""); removeBallon(ballons[0]); expect(jQuery.parseJSON(htmlUnescape(formData.val()))).toEqual({ "http://imgs.xkcd.com/comics/cells.png": [ { left: 0.0963302752293578, top: 0.9410029498525073, width: 0.7798165137614679, height: 0.05604719764011799, font_size: 14, text: 'So does a handgun.' } ] }); }); }); }); });