From bbb1ca33e96df12979078e108f305d562893af69 Mon Sep 17 00:00:00 2001 From: Tom Byers Date: Mon, 2 Sep 2019 09:47:18 +0100 Subject: [PATCH 1/4] Add utility helper for making form data sendable Useful for assertions where the data you're comparing is already in this format. --- tests/javascripts/support/helpers.js | 2 ++ .../javascripts/support/helpers/utilities.js | 22 +++++++++++++++++++ 2 files changed, 24 insertions(+) create mode 100644 tests/javascripts/support/helpers/utilities.js diff --git a/tests/javascripts/support/helpers.js b/tests/javascripts/support/helpers.js index fe5adf08e..7fef27e98 100644 --- a/tests/javascripts/support/helpers.js +++ b/tests/javascripts/support/helpers.js @@ -4,6 +4,7 @@ const html = require('./helpers/html.js'); const elements = require('./helpers/elements.js'); const rendering = require('./helpers/rendering.js'); const forms = require('./helpers/forms.js'); +const utilities = require('./helpers/utilities.js'); exports.triggerEvent = events.triggerEvent; exports.clickElementWithMouse = events.clickElementWithMouse; @@ -18,3 +19,4 @@ exports.element = elements.element; exports.WindowMock = rendering.WindowMock; exports.ScreenMock = rendering.ScreenMock; exports.spyOnFormSubmit = forms.spyOnFormSubmit; +exports.getFormDataFromPairs = utilities.getFormDataFromPairs; diff --git a/tests/javascripts/support/helpers/utilities.js b/tests/javascripts/support/helpers/utilities.js new file mode 100644 index 000000000..9ec0f8015 --- /dev/null +++ b/tests/javascripts/support/helpers/utilities.js @@ -0,0 +1,22 @@ +// general helpers, not related to the DOM and usable in different contexts + +// turn a list of key=value pairs (like tuples) into data that can be sent via AJAX +// taken from https://developer.mozilla.org/en-US/docs/Learn/HTML/Forms/Sending_forms_through_JavaScript +// but requiring an array as input rather than a hash, to preserve order of pairs +function getFormDataFromPairs (pairs) { + + const urlEncodedDataPairs = []; + + pairs.forEach(pair => { + + urlEncodedDataPairs.push(`${window.encodeURIComponent(pair[0])}=${window.encodeURIComponent(pair[1])}`); + + }); + + // Combine the pairs into a single string and replace all %-encoded spaces to + // the '+' character; matches the behaviour of browser form submissions. + return urlEncodedDataPairs.join('&').replace(/%20/g, '+'); + +}; + +exports.getFormDataFromPairs = getFormDataFromPairs; From a0d39496b9eff59658f20d5a3e8f6ff80f27167c Mon Sep 17 00:00:00 2001 From: Tom Byers Date: Mon, 2 Sep 2019 14:24:02 +0100 Subject: [PATCH 2/4] Make global explicit in module scope This is mainly because tests don't inferr that global variables are just properties of the window object, as browsers do, but it also makes this more explicit. --- app/assets/javascripts/updateContent.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/assets/javascripts/updateContent.js b/app/assets/javascripts/updateContent.js index 204e1c9ce..6297e3854 100644 --- a/app/assets/javascripts/updateContent.js +++ b/app/assets/javascripts/updateContent.js @@ -1,8 +1,8 @@ -(function(Modules) { +(function(global) { "use strict"; var queues = {}; - var dd = new diffDOM(); + var dd = new global.diffDOM(); var getRenderer = $component => response => dd.apply( $component.get(0), @@ -43,7 +43,7 @@ ); }; - Modules.UpdateContent = function() { + global.GOVUK.Modules.UpdateContent = function() { this.start = component => poll( getRenderer($(component)), @@ -55,4 +55,4 @@ }; -})(window.GOVUK.Modules); +})(window); From c241d1eb5eb445bea234f05a5b194d264c030865 Mon Sep 17 00:00:00 2001 From: Tom Byers Date: Mon, 2 Sep 2019 14:27:46 +0100 Subject: [PATCH 3/4] Add tests for updateContent module --- tests/javascripts/updateContent.test.js | 223 ++++++++++++++++++++++++ 1 file changed, 223 insertions(+) create mode 100644 tests/javascripts/updateContent.test.js diff --git a/tests/javascripts/updateContent.test.js b/tests/javascripts/updateContent.test.js new file mode 100644 index 000000000..fdc620b74 --- /dev/null +++ b/tests/javascripts/updateContent.test.js @@ -0,0 +1,223 @@ +const helpers = require('./support/helpers.js'); + +const serviceNumber = '6658542f-0cad-491f-bec8-ab8457700ead'; +const resourceURL = `/services/${serviceNumber}/notifications/email.json?status=sending%2Cdelivered%2Cfailed`; +const updateKey = 'counts'; + +let responseObj = {}; +let jqueryAJAXReturnObj; + +beforeAll(() => { + + // ensure all timers go through Jest + jest.useFakeTimers(); + + // mock the bits of jQuery used + jest.spyOn(window.$, 'ajax'); + + // set up the object returned from $.ajax so it responds with whatever responseObj is set to + jqueryAJAXReturnObj = { + done: callback => { + callback(responseObj); + return jqueryAJAXReturnObj; + }, + fail: () => {} + }; + + $.ajax.mockImplementation(() => jqueryAJAXReturnObj); + + // because we're running in node, diffDOM executes as a module + // in the normal browser environment it will attach to window so we replicate that here + window.diffDOM = require('../../node_modules/diff-dom/diffDOM.js'); + require('../../app/assets/javascripts/updateContent.js'); + +}); + +afterAll(() => { + require('./support/teardown.js'); +}); + +describe('Update content', () => { + + beforeEach(() => { + + // store HTML in string to allow use in AJAX responses + HTMLString = ` +
+
+ +
+
`; + + document.body.innerHTML = HTMLString; + + // default the response to match the existing content + responseObj[updateKey] = HTMLString; + + }); + + afterEach(() => { + + document.body.innerHTML = ''; + + // tidy up record of mocked AJAX calls + $.ajax.mockClear(); + + // ensure any timers set by continually starting the module are cleared + jest.clearAllTimers(); + + }); + + test("It should make requests to the URL specified in the data-resource attribute", () => { + + // start the module + window.GOVUK.modules.start(); + + expect($.ajax.mock.calls[0][0]).toEqual(resourceURL); + + }); + + test("If the response contains no changes, the DOM should stay the same", () => { + + // send the done callback a response with updates included + responseObj[updateKey] = HTMLString; + + // start the module + window.GOVUK.modules.start(); + + // check the right DOM node is updated + expect(document.querySelectorAll('.big-number-number')[0].textContent.trim()).toEqual("0"); + + }); + + test("If the response contains changes, it should update the DOM with them", () => { + + // send the done callback a response with updates included + responseObj[updateKey] = HTMLString.replace(/
0<\/div>{1}/, '
1
'); + + // start the module + window.GOVUK.modules.start(); + + // check the right DOM node is updated + expect(document.querySelectorAll('.big-number-number')[0].textContent.trim()).toEqual("1"); + + }); + + test("If an interval between requests is specified, using the data-interval-seconds attribute, requests should happen at that frequency", () => { + + document.querySelector('[data-module=update-content]').setAttribute('data-interval-seconds', '0.5'); + + // start the module + window.GOVUK.modules.start(); + + expect($.ajax).toHaveBeenCalledTimes(1); + + jest.advanceTimersByTime(500); + jest.advanceTimersByTime(500); + jest.advanceTimersByTime(500); + + expect($.ajax).toHaveBeenCalledTimes(4); + + }); + + describe("By default", () => { + + beforeEach(() => { + + // start the module + window.GOVUK.modules.start(); + + }); + + test("It should use the GET HTTP method", () => { + + expect($.ajax.mock.calls[0][1].method).toEqual('get'); + + }); + + test("It shouldn't send any data as part of the requests", () => { + + expect($.ajax.mock.calls[0][1].data).toEqual({}); + + }); + + test("It should request updates every 1.5 seconds", () => { + + expect($.ajax).toHaveBeenCalledTimes(1); + + jest.advanceTimersByTime(1500); + + expect($.ajax).toHaveBeenCalledTimes(2); + + }); + + }); + + describe("If a form is used as a source for data, referenced in the data-form attribute", () => { + + beforeEach(() => { + + document.body.innerHTML += ` +
+ + +
`; + + document.querySelector('[data-module=update-content]').setAttribute('data-form', 'service'); + + // start the module + window.GOVUK.modules.start(); + + }); + + test("requests should use the same HTTP method as the form", () => { + + expect($.ajax.mock.calls[0][1].method).toEqual('post'); + + }) + + test("requests should use the data from the form", () => { + + expect($.ajax.mock.calls[0][1].data).toEqual(helpers.getFormDataFromPairs([ + ['serviceName', 'Buckhurst surgery'], + ['serviceNumber', serviceNumber] + ])); + + }) + + }); + +}); From 5df4864743178c8fdebb0465fa0ff73a864041df Mon Sep 17 00:00:00 2001 From: Tom Byers Date: Thu, 5 Sep 2019 09:43:18 +0100 Subject: [PATCH 4/4] Add note about `advanceTimersByTime` units --- tests/javascripts/updateContent.test.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/javascripts/updateContent.test.js b/tests/javascripts/updateContent.test.js index fdc620b74..0ea2d8d2d 100644 --- a/tests/javascripts/updateContent.test.js +++ b/tests/javascripts/updateContent.test.js @@ -145,6 +145,7 @@ describe('Update content', () => { expect($.ajax).toHaveBeenCalledTimes(1); + // units are milliseconds jest.advanceTimersByTime(500); jest.advanceTimersByTime(500); jest.advanceTimersByTime(500); @@ -178,6 +179,7 @@ describe('Update content', () => { expect($.ajax).toHaveBeenCalledTimes(1); + // units are milliseconds jest.advanceTimersByTime(1500); expect($.ajax).toHaveBeenCalledTimes(2);