From c8be3d37bdc2c6422bd3a21624e800775607959d Mon Sep 17 00:00:00 2001 From: Beverly Nguyen Date: Mon, 15 Sep 2025 00:44:07 -0700 Subject: [PATCH] rm updateContent.js --- app/assets/javascripts/updateContent.js | 148 ------- gulpfile.js | 1 - tests/javascripts/updateContent.test.js | 513 ------------------------ 3 files changed, 662 deletions(-) delete mode 100644 app/assets/javascripts/updateContent.js delete mode 100644 tests/javascripts/updateContent.test.js diff --git a/app/assets/javascripts/updateContent.js b/app/assets/javascripts/updateContent.js deleted file mode 100644 index d488cd9a2..000000000 --- a/app/assets/javascripts/updateContent.js +++ /dev/null @@ -1,148 +0,0 @@ -(function(global) { - "use strict"; - - var queues = {}; - var morphdom = global.GOVUK.vendor.morphdom; - var defaultInterval = 2000; - var interval = 0; - - var calculateBackoff = responseTime => parseInt(Math.max( - (250 * Math.sqrt(responseTime)) - 1000, - 1000 - )); - - // Methods to ensure the DOM fragment is clean of classes added by JS before diffing - // and that they are replaced afterwards. - // - // Added to allow the use of JS, in main.js, to apply styles which in future could be - // achieved with the :has pseudo-class. If :has is available in our supported browsers, - // this can be removed in favour of a CSS-only solution. - var ClassesPersister = function ($contents) { - this._$contents = $contents; - this._classNames = []; - this._classesTo$ElsMap = {}; - }; - ClassesPersister.prototype.addClassName = function (className) { - if (this._classNames.indexOf(className) === -1) { - this._classNames.push(className); - } - }; - ClassesPersister.prototype.remove = function () { - // Store references to any elements with class names to persist - this._classNames.forEach(className => { - var $elsWithClassName = $('.' + className, this._$contents).removeClass(className); - - if ($elsWithClassName.length > 0) { - this._classesTo$ElsMap[className] = $elsWithClassName; - } - }); - }; - ClassesPersister.prototype.replace = function () { - var replaceClasses = (idx, el) => { - - // Avoid updating elements that are no longer present. - // elements removed will still exist in memory but won't be attached to the DOM any more - if (global.document.body.contains(el)) { - $(el).addClass(className); - } - - }; - var className; - - for (className in this._classesTo$ElsMap) { - this._classesTo$ElsMap[className].each(replaceClasses); - } - - // remove references to elements - this._classesTo$ElsMap = {}; - }; - - var getRenderer = ($contents, key, classesPersister) => response => { - classesPersister.remove(); - morphdom( - $contents.get(0), - $(response[key]).get(0) - ); - classesPersister.replace(); - }; - - var getQueue = resource => ( - queues[resource] = queues[resource] || [] - ); - - var flushQueue = function(queue, response) { - while(queue.length) queue.shift()(response); - }; - - var clearQueue = queue => (queue.length = 0); - - var poll = function(renderer, resource, queue, form) { - - let startTime = Date.now(); - - if (document.visibilityState !== "hidden" && queue.push(renderer) === 1) { - $.ajax( - resource, - { - 'method': form ? 'post' : 'get', - 'data': form ? $('#' + form).serialize() : {} - } - ).done( - response => { - flushQueue(queue, response); - if (response.stop === 1) { - poll = function(){}; - } - interval = calculateBackoff(Date.now() - startTime); - } - ).fail( - () => poll = function(){} - ); - } - - setTimeout( - () => poll.apply(window, arguments), interval - ); - }; - - global.GOVUK.Modules.UpdateContent = function() { - - this.start = component => { - var $component = $(component); - var $contents = $component.children().eq(0); - var key = $component.data('key'); - var resource = $component.data('resource'); - var form = $component.data('form'); - var classesPersister = new ClassesPersister($contents); - - // Replace component with contents. - // The renderer does this anyway when diffing against the first response - $component.replaceWith($contents); - - // Store any classes that should persist through updates - // - // Added to allow the use of JS, in main.js, to apply styles which in future could be - // achieved with the :has pseudo-class. If :has is available in our supported browsers, - // this can be removed in favour of a CSS-only solution. - if ($contents.data('classesToPersist') !== undefined) { - $contents.data('classesToPersist') - .split(' ') - .forEach(className => classesPersister.addClassName(className)); - } - - setTimeout( - () => poll( - getRenderer($contents, key, classesPersister), - resource, - getQueue(resource), - form - ), - defaultInterval - ); - }; - - }; - - global.GOVUK.Modules.UpdateContent.calculateBackoff = calculateBackoff; - -})(window); diff --git a/gulpfile.js b/gulpfile.js index 78b76640f..a49f7d1ad 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -60,7 +60,6 @@ const javascripts = () => { paths.src + 'javascripts/enhancedTextbox.js', paths.src + 'javascripts/fileUpload.js', paths.src + 'javascripts/radioSelect.js', - paths.src + 'javascripts/updateContent.js', paths.src + 'javascripts/listEntry.js', paths.src + 'javascripts/liveSearch.js', paths.src + 'javascripts/errorTracking.js', diff --git a/tests/javascripts/updateContent.test.js b/tests/javascripts/updateContent.test.js deleted file mode 100644 index f8229f63a..000000000 --- a/tests/javascripts/updateContent.test.js +++ /dev/null @@ -1,513 +0,0 @@ -const each = require('jest-each').default; - -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 => { - // The server takes 1 second to respond - jest.advanceTimersByTime(1000); - callback(responseObj); - return jqueryAJAXReturnObj; - }, - fail: () => {} - }; - - $.ajax.mockImplementation(() => jqueryAJAXReturnObj); - - // RollupJS assigns our bundled module code, including morphdom, to window.GOVUK. - // morphdom is assigned to its vendor property so we need to copy that here for the updateContent - // code to pick it up. - window.GOVUK.vendor = { - morphdom: require('morphdom') - }; - require('../../app/assets/javascripts/updateContent.js'); - -}); - -afterAll(() => { - require('./support/teardown.js'); -}); - -describe('Update content', () => { - - const getInitialHTMLString = partial => ` -
- ${partial} -
`; - - describe("All variations", () => { - - beforeEach(() => { - - // Intentionally basic example because we're not testing changes to the partial - document.body.innerHTML = getInitialHTMLString(`

Sending

`); - - // default the response to match the content inside div[data-module] - responseObj[updateKey] = `

Sending

`; - - }); - - 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(() => { - - // Add a form to the page - document.body.innerHTML += ` -
- - -
`; - - // Link the component to the form - 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] - ])); - - }) - - }); - - }); - - describe('When updating the contents of DOM nodes', () => { - - let partialData; - - const getPartial = items => { - let pillsHTML = ''; - - items.forEach(item => { - pillsHTML += ` -
  • -
    -
    -
    ${item.count}
    -
    -
    ${item.label}
    -
    -
  • `; - }); - - return ` -
    - -
    `; - }; - - beforeEach(() => { - - partialData = [ - { - count: 0, - label: 'total', - selected: true - }, - { - count: 0, - label: 'sending', - selected: false - }, - { - count: 0, - label: 'delivered', - selected: false - }, - { - count: 0, - label: 'failed', - selected: false - } - ]; - - document.body.innerHTML = getInitialHTMLString(getPartial(partialData)); - - }); - - test("It should replace the original HTML with that of the partial, to match that returned from AJAX responses", () => { - - // default the response to match the content inside div[data-module] - responseObj[updateKey] = getPartial(partialData); - - // start the module - window.GOVUK.modules.start(); - - expect(document.querySelector('.ajax-block-container').parentNode.hasAttribute('data-resource')).toBe(false); - - }); - - test("It should make requests to the URL specified in the data-resource attribute", () => { - - // default the response to match the content inside div[data-module] - responseObj[updateKey] = getPartial(partialData); - - // 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] = getPartial(partialData); - - // start the module - window.GOVUK.modules.start(); - jest.advanceTimersByTime(2000); - - // check a sample DOM node is unchanged - expect(document.querySelectorAll('.big-number-number')[0].textContent.trim()).toEqual("0"); - - }); - - test("If the response contains changes, it should update the DOM with them", () => { - - partialData[0].count = 1; - - // send the done callback a response with updates included - responseObj[updateKey] = getPartial(partialData); - - // 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("When adding or removing DOM nodes", () => { - - let partialData; - - const getPartial = items => { - - const getItemHTMLString = content => { - var areas = ''; - - content.areas.forEach(area => - areas += "\n" + `
  • ${area}
  • ` - ); - - return ` -
    -
    -

    - ${content.title} -

    -
    -
    - - ${content.hint} - -
    -
    -

    - ${content.status} -

    -
    -
    -
      - ${areas} -
    -
    -
    `; - }; - - var itemsHTMLString = ''; - - items.forEach(item => itemsHTMLString += "\n" + getItemHTMLString(item)); - - return `
    - ${itemsHTMLString}; -
    -
    `; - - }; - - beforeEach(() => { - - partialData = [ - { - 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" - ] - } - ]; - - }); - - test("If the response contains no changes, the DOM should stay the same", () => { - - document.body.innerHTML = getInitialHTMLString(getPartial(partialData)); - - // make a response with no changes - responseObj[updateKey] = getPartial(partialData); - - // 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", () => { - - document.body.innerHTML = getInitialHTMLString(getPartial(partialData)); - - partialData.push({ - 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] = getPartial(partialData); - - // 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", () => { - - // add another item so we start with 2 - partialData.push({ - 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" - ] - }); - - document.body.innerHTML = getInitialHTMLString(getPartial(partialData)); - - // remove the last item - partialData.pop(); - - // default the response to match the content inside div[data-module] - responseObj[updateKey] = getPartial(partialData); - - // 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 to a single component", () => { - - document.body.innerHTML = getInitialHTMLString(getPartial(partialData)); - - // 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'); - - // Add an item to trigger an update - partialData.push({ - 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] = getPartial(partialData); - - // 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); - - }); - - test("If other scripts have added classes to the DOM, they should persist through updates to multiple components", () => { - - // Create duplicate components in the page - document.body.innerHTML = getInitialHTMLString(getPartial(partialData)) + "\n" + getInitialHTMLString(getPartial(partialData)); - - var partialsInPage = document.querySelectorAll('.ajax-block-container'); - - // Mark classes to persist on the partials (2nd is made up) - partialsInPage[0].setAttribute('data-classes-to-persist', 'js-child-has-focus'); - partialsInPage[1].setAttribute('data-classes-to-persist', 'js-2nd-child-has-focus'); - - // Add examples of those classes on each partial (2nd is made up) - partialsInPage[0].querySelectorAll('.file-list h2')[0].classList.add('js-child-has-focus'); - partialsInPage[1].querySelectorAll('.file-list h2')[0].classList.add('js-2nd-child-has-focus'); - - // Add an item to trigger an update - partialData.push({ - 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 all responses have an extra item - responseObj[updateKey] = getPartial(partialData); - - // start the module - window.GOVUK.modules.start(); - jest.advanceTimersByTime(2000); - - // re-select in case nodes in partialsInPage have changed - partialsInPage = document.querySelectorAll('.ajax-block-container'); - - // check the classes are still there - expect(partialsInPage[0].querySelectorAll('.file-list h2')[0].classList.contains('js-child-has-focus')).toBe(true); - expect(partialsInPage[1].querySelectorAll('.file-list h2')[0].classList.contains('js-2nd-child-has-focus')).toBe(true); - - // check each heading only has the classes assigned to it before updates occurred - expect(partialsInPage[0].querySelectorAll('.file-list h2')[0].classList.contains('js-2nd-child-has-focus')).toBe(false); - expect(partialsInPage[1].querySelectorAll('.file-list h2')[0].classList.contains('js-child-has-focus')).toBe(false); - - }); - - }); - - 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(); - - }); - -});