diff --git a/app/assets/javascripts/updateContent.js b/app/assets/javascripts/updateContent.js index 2e4bc4758..d6d678e08 100644 --- a/app/assets/javascripts/updateContent.js +++ b/app/assets/javascripts/updateContent.js @@ -11,10 +11,37 @@ 1000 )); - var getRenderer = $component => response => morphdom( - $component.get(0), - $(response[$component.data('key')]).get(0) - ); + // Methods to ensure the DOM fragment is clean of classes added by JS before diffing + // and that they are replaced afterwards. + var classesToPersist = { + classNames: [], + $els: [], + remove: function () { + this.classNames.forEach(className => { + var $elsWithClassName = $('.' + className).removeClass(className); + + // store elements for that className at the same index + this.$els.push($elsWithClassName); + }); + }, + replace: function () { + this.classNames.forEach((className, index) => { + this.$els[index].addClass(className); + }); + + // remove references to elements + this.$els = []; + } + }; + + var getRenderer = $component => response => { + classesToPersist.remove(); + morphdom( + $component.get(0), + $(response[$component.data('key')]).get(0) + ); + classesToPersist.replace(); + }; var getQueue = resource => ( queues[resource] = queues[resource] || [] @@ -55,15 +82,26 @@ global.GOVUK.Modules.UpdateContent = function() { - this.start = component => setTimeout( - () => poll( - getRenderer($(component)), - $(component).data('resource'), - getQueue($(component).data('resource')), - $(component).data('form') - ), - defaultInterval - ); + this.start = component => { + var $component = $(component); + + // store any classes that should persist through updates + if ($contents.data('classesToPersist') !== undefined) { + $contents.data('classesToPersist') + .split(' ') + .forEach(className => classesToPersist.classNames.push(className)); + } + + setTimeout( + () => poll( + getRenderer($component), + $component.data('resource'), + getQueue($component.data('resource')), + $component.data('form') + ), + defaultInterval + ); + }; }; diff --git a/tests/javascripts/updateContent.test.js b/tests/javascripts/updateContent.test.js index d965906b8..aff63d767 100644 --- a/tests/javascripts/updateContent.test.js +++ b/tests/javascripts/updateContent.test.js @@ -50,56 +50,426 @@ describe('Update content', () => { let HTMLString; let initialHTMLString; - beforeEach(() => { + describe('When updating the contents of DOM nodes', () => { - // store HTML in string to allow use in AJAX responses - HTMLString = ` -
- -
`; + + + `; + }; + var getHTMLString = items => { - initialHTMLString = `
- ${HTMLString} -
`; + var itemsHTMLString = ''; - document.body.innerHTML = initialHTMLString; + items.forEach(item => itemsHTMLString += "\n" + getItemHTMLString(item)); - // default the response to match the content inside div[data-module] - responseObj[updateKey] = HTMLString; + return `
+ ${itemsHTMLString}; +
+
`; + + }; + + test("If the response contains no changes, the DOM should stay the same", () => { + + // store HTML in string to allow use in AJAX responses + HTMLString = getHTMLString([ + { + title: "Gas leak", + hint: "There's a gas leak in the local area. Residents should vacate until further notice.", + status: "Waiting for approval", + areas: [ + "Santa Claus Village, Rovaniemi B", + "Santa Claus Village, Rovaniemi C" + ] + } + ]); + + initialHTMLString = `
+ ${HTMLString} +
`; + + document.body.innerHTML = initialHTMLString; + + // make the response have an extra item + responseObj[updateKey] = HTMLString; + + // start the module + window.GOVUK.modules.start(); + jest.advanceTimersByTime(2000); + + // check it has the same number of items + expect(document.querySelectorAll('.file-list').length).toEqual(1); + expect(document.querySelectorAll('.file-list h2 a')[0].textContent.trim()).toEqual("Gas leak"); + + }); + + test("If the response adds a node, the DOM should contain that node", () => { + + // store HTML in string to allow use in AJAX responses + HTMLString = getHTMLString([ + { + title: "Gas leak", + hint: "There's a gas leak in the local area. Residents should vacate until further notice.", + status: "Waiting for approval", + areas: [ + "Santa Claus Village, Rovaniemi B", + "Santa Claus Village, Rovaniemi C" + ] + } + ]); + + initialHTMLString = `
+ ${HTMLString} +
`; + + document.body.innerHTML = initialHTMLString; + + var updatedHTMLString = getHTMLString([ + { + title: "Gas leak", + hint: "There's a gas leak in the local area. Residents should vacate until further notice.", + status: "Waiting for approval", + areas: [ + "Santa Claus Village, Rovaniemi B", + "Santa Claus Village, Rovaniemi C" + ] + }, + { + title: "Reservoir flooding template", + hint: "The local reservoir has flooded. All people within 5 miles should move to a safer location.", + status: "Waiting for approval", + areas: [ + "Santa Claus Village, Rovaniemi A", + "Santa Claus Village, Rovaniemi D" + ] + } + ]); + + // make the response have an extra item + responseObj[updateKey] = updatedHTMLString; + + // start the module + window.GOVUK.modules.start(); + jest.advanceTimersByTime(2000); + + // check the node has been added + expect(document.querySelectorAll('.file-list').length).toEqual(2); + expect(document.querySelectorAll('.file-list h2 a')[0].textContent.trim()).toEqual("Gas leak"); + expect(document.querySelectorAll('.file-list h2 a')[1].textContent.trim()).toEqual("Reservoir flooding template"); + + }); + + test("If the response removes a node, the DOM should not contain that node", () => { + + // store HTML in string to allow use in AJAX responses + HTMLString = getHTMLString([ + { + title: "Gas leak", + hint: "There's a gas leak in the local area. Residents should vacate until further notice.", + status: "Waiting for approval", + areas: [ + "Santa Claus Village, Rovaniemi B", + "Santa Claus Village, Rovaniemi C" + ] + }, + { + title: "Reservoir flooding template", + hint: "The local reservoir has flooded. All people within 5 miles should move to a safer location.", + status: "Waiting for approval", + areas: [ + "Santa Claus Village, Rovaniemi A", + "Santa Claus Village, Rovaniemi D" + ] + } + ]); + + initialHTMLString = `
+ ${HTMLString} +
`; + + document.body.innerHTML = initialHTMLString; + + var updatedHTMLString = getHTMLString([ + { + title: "Gas leak", + hint: "There's a gas leak in the local area. Residents should vacate until further notice.", + status: "Waiting for approval", + areas: [ + "Santa Claus Village, Rovaniemi B", + "Santa Claus Village, Rovaniemi C" + ] + } + ]); + + // default the response to match the content inside div[data-module] + responseObj[updateKey] = updatedHTMLString; + + // start the module + window.GOVUK.modules.start(); + jest.advanceTimersByTime(2000); + + // check the node has been removed + expect(document.querySelectorAll('.file-list').length).toEqual(1); + expect(document.querySelectorAll('.file-list h2 a')[0].textContent.trim()).toEqual("Gas leak"); + + }); + + test("If other scripts have added classes to the DOM, they should persist through updates", () => { + + // store HTML in string to allow use in AJAX responses + HTMLString = getHTMLString([ + { + title: "Gas leak", + hint: "There's a gas leak in the local area. Residents should vacate until further notice.", + status: "Waiting for approval", + areas: [ + "Santa Claus Village, Rovaniemi B", + "Santa Claus Village, Rovaniemi C" + ] + } + ]); + + initialHTMLString = `
+ ${HTMLString} +
`; + + document.body.innerHTML = initialHTMLString; + + // mark classes to persist on the partial + document.querySelector('.ajax-block-container').setAttribute('data-classes-to-persist', 'js-child-has-focus'); + + // Add class to indicate focus state of link on parent heading + document.querySelectorAll('.file-list h2')[0].classList.add('js-child-has-focus'); + + // make the response match the initial HTML to emulate a response with no changes + responseObj[updateKey] = HTMLString; + + // start the module + window.GOVUK.modules.start(); + jest.advanceTimersByTime(2000); + + // check the class is still there + expect(document.querySelectorAll('.file-list h2')[0].classList.contains('js-child-has-focus')).toBe(true); + + }); }); @@ -115,138 +485,4 @@ describe('Update content', () => { }); - test("It should make requests to the URL specified in the data-resource attribute", () => { - - // start the module - window.GOVUK.modules.start(); - jest.advanceTimersByTime(2000); - - 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(); - jest.advanceTimersByTime(2000); - - // 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(); - jest.advanceTimersByTime(2000); - - // check the right DOM node is updated - expect(document.querySelectorAll('.big-number-number')[0].textContent.trim()).toEqual("1"); - - }); - - describe("By default", () => { - - beforeEach(() => { - - // start the module - window.GOVUK.modules.start(); - - }); - - test("It should use the GET HTTP method", () => { - - jest.advanceTimersByTime(2000); - expect($.ajax.mock.calls[0][1].method).toEqual('get'); - - }); - - test("It shouldn't send any data as part of the requests", () => { - - jest.advanceTimersByTime(2000); - expect($.ajax.mock.calls[0][1].data).toEqual({}); - - }); - - test("It should request updates with a dynamic interval", () => { - - // First call doesn’t happen in the first 2000ms - jest.advanceTimersByTime(1999); - expect($.ajax).toHaveBeenCalledTimes(0); - - // But it happens after 2000ms by default - jest.advanceTimersByTime(1); - expect($.ajax).toHaveBeenCalledTimes(1); - - // It took the server 1000ms to respond to the first call so we - // will back off – the next call shouldn’t happen in the next 6904ms - jest.advanceTimersByTime(6904); - expect($.ajax).toHaveBeenCalledTimes(1); - - // But it should happen after 6905ms - jest.advanceTimersByTime(1); - expect($.ajax).toHaveBeenCalledTimes(2); - - }); - - each([ - [1000, 0], - [1500, 100], - [4590, 500], - [6905, 1000], - [24000, 10000], - ]).test('It calculates a delay of %dms if the API responds in %dms', (waitTime, responseTime) => { - expect( - window.GOVUK.Modules.UpdateContent.calculateBackoff(responseTime) - ).toBe( - waitTime - ); - }); - - }); - - 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", () => { - - jest.advanceTimersByTime(2000); - expect($.ajax.mock.calls[0][1].method).toEqual('post'); - - }) - - test("requests should use the data from the form", () => { - - jest.advanceTimersByTime(2000); - expect($.ajax.mock.calls[0][1].data).toEqual(helpers.getFormDataFromPairs([ - ['serviceName', 'Buckhurst surgery'], - ['serviceNumber', serviceNumber] - ])); - - }) - - }); - });