From 900aa19bd3717fc92c85babd797485ff9a5d62ef Mon Sep 17 00:00:00 2001 From: Tom Byers Date: Tue, 31 Dec 2019 16:49:52 +0000 Subject: [PATCH] Add JS tests for analytics & cookies JS Includes: - tests for the analytics interface ported from GOVUK Frontend Toolkit - tests for the cookie banner that appears on all pages except the cookies page - tests for the cookies page JS - tests for the hasConsentFor function - adding a deleteCookie helper to remove cookies during tests - polyfill for insertAdjacentText The last one is because JSDOM doesn't support insertAdjacentText but our target browsers do. This polyfill also includes one for insertAdjacentHTML. --- tests/javascripts/analytics/analytics.test.js | 97 +++++++ tests/javascripts/analytics/init.test.js | 123 +++++++++ tests/javascripts/consent.test.js | 55 ++++ tests/javascripts/cookieMessage.test.js | 230 ++++++++++++++++ tests/javascripts/cookieSettings.test.js | 255 ++++++++++++++++++ tests/javascripts/support/helpers.js | 3 + tests/javascripts/support/helpers/cookies.js | 22 ++ tests/javascripts/support/polyfills.js | 53 ++++ tests/javascripts/support/setup.js | 3 + 9 files changed, 841 insertions(+) create mode 100644 tests/javascripts/analytics/analytics.test.js create mode 100644 tests/javascripts/analytics/init.test.js create mode 100644 tests/javascripts/consent.test.js create mode 100644 tests/javascripts/cookieMessage.test.js create mode 100644 tests/javascripts/cookieSettings.test.js create mode 100644 tests/javascripts/support/helpers/cookies.js create mode 100644 tests/javascripts/support/polyfills.js diff --git a/tests/javascripts/analytics/analytics.test.js b/tests/javascripts/analytics/analytics.test.js new file mode 100644 index 000000000..6b357a1e8 --- /dev/null +++ b/tests/javascripts/analytics/analytics.test.js @@ -0,0 +1,97 @@ +const helpers = require('../support/helpers'); + +beforeAll(() => { + + // add the script GA looks for in the document + document.body.appendChild(document.createElement('script')); + + require('../../../app/assets/javascripts/govuk/cookie-functions.js'); + require('../../../app/assets/javascripts/analytics/analytics.js'); + require('../../../app/assets/javascripts/analytics/init.js'); + +}); + +afterAll(() => { + + require('../support/teardown.js'); + +}); + +describe("Analytics", () => { + + let analytics; + + beforeEach(() => { + + window.ga = jest.fn(); + + analytics = new GOVUK.Analytics({ + trackingId: 'UA-75215134-1', + cookieDomain: 'auto', + anonymizeIp: true, + displayFeaturesTask: null, + transport: 'beacon' + }); + + }); + + afterEach(() => { + + window.ga.mockReset(); + + }); + + describe("When created", () => { + + test("It configures a tracker", () => { + + setUpArguments = window.ga.mock.calls; + + expect(setUpArguments[0]).toEqual(['create', 'UA-75215134-1', 'auto']); + expect(setUpArguments[1]).toEqual(['set', 'anonymizeIp', true]); + expect(setUpArguments[2]).toEqual(['set', 'displayFeaturesTask', null]); + expect(setUpArguments[3]).toEqual(['set', 'transport', 'beacon']); + + }); + + }); + + describe("When tracking pageviews", () => { + + test("It sends the right URL for the page if no arguments", () => { + + window.ga.mockClear(); + + jest.spyOn(window, 'location', 'get').mockImplementation(() => { + return { + 'pathname': '/privacy', + 'search': '' + }; + }); + + analytics.trackPageview(); + + expect(window.ga.mock.calls[0]).toEqual(['send', 'pageview', '/privacy']); + + }); + + test("It strips the UUIDs from URLs", () => { + + window.ga.mockClear(); + + jest.spyOn(window, 'location', 'get').mockImplementation(() => { + return { + 'pathname': '/services/6658542f-0cad-491f-bec8-ab8457700ead', + 'search': '' + }; + }); + + analytics.trackPageview(); + + expect(window.ga.mock.calls[0]).toEqual(['send', 'pageview', '/services/…']); + + }); + + }); + +}); diff --git a/tests/javascripts/analytics/init.test.js b/tests/javascripts/analytics/init.test.js new file mode 100644 index 000000000..3f3563b5c --- /dev/null +++ b/tests/javascripts/analytics/init.test.js @@ -0,0 +1,123 @@ +const helpers = require('../support/helpers'); + +beforeAll(() => { + + // add the script GA looks for in the document + document.body.appendChild(document.createElement('script')); + + require('../../../app/assets/javascripts/govuk/cookie-functions.js'); + require('../../../app/assets/javascripts/analytics/analytics.js'); + require('../../../app/assets/javascripts/analytics/init.js'); + +}); + +afterAll(() => { + + require('../support/teardown.js'); + +}); + +describe("Analytics init", () => { + + beforeAll(() => { + + window.ga = jest.fn(); + jest.spyOn(window.GOVUK.Analytics, 'load'); + + // pretend we're on the /privacy page + jest.spyOn(window, 'location', 'get').mockImplementation(() => { + return { + 'pathname': '/privacy', + 'search': '' + }; + }); + + }); + + afterEach(() => { + + window.GOVUK.Analytics.load.mockClear(); + window.ga.mockClear(); + + }); + + test("After the init.js script has been loaded, Google Analytics will be disabled", () => { + + expect(window['ga-disable-UA-26179049-1']).toBe(true); + + }); + + describe("If initAnalytics has already been called", () => { + + beforeAll(() => { + + // Fake a tracker instance + window.GOVUK.analytics = {}; + + }); + + beforeEach(() => { + + window.GOVUK.initAnalytics(); + + }); + + afterAll(() => { + + delete window.GOVUK.analytics; + + }); + + test("The Google Analytics libraries will not be loaded", () => { + + expect(window.GOVUK.Analytics.load).not.toHaveBeenCalled(); + + }); + + }); + + describe("If initAnalytics has not been called", () => { + + beforeEach(() => { + + window.GOVUK.initAnalytics(); + + }); + + afterEach(() => { + + // window.GOVUK.initAnalytics sets up a new window.GOVUK.analytics which needs clearing + delete window.GOVUK.analytics; + + }); + + test("Google Analytics will not be disabled", () => { + + expect(window['ga-disable-UA-26179049-1']).toBe(false); + + }); + + test("The Google Analytics libraries will have been loaded", () => { + + expect(window.GOVUK.Analytics.load).toHaveBeenCalled(); + + }); + + test("There will be an interface with the Google Analytics API", () => { + + expect(window.GOVUK.analytics).toBeDefined(); + + }); + + test("A pageview will be registered", () => { + + expect(window.ga.mock.calls.length).toEqual(5); + + // The first 4 calls configure the analytics tracker. All subsequent calls send data + expect(window.ga.mock.calls[4]).toEqual(['send', 'pageview', '/privacy']); + + }); + + }); + +}); diff --git a/tests/javascripts/consent.test.js b/tests/javascripts/consent.test.js new file mode 100644 index 000000000..9217bd81c --- /dev/null +++ b/tests/javascripts/consent.test.js @@ -0,0 +1,55 @@ +const helpers = require('./support/helpers'); + +beforeAll(() => { + + require('../../app/assets/javascripts/govuk/cookie-functions.js'); + require('../../app/assets/javascripts/consent.js'); + +}); + +afterAll(() => { + + require('./support/teardown.js'); + +}); + +describe("Cookie consent", () => { + + describe("hasConsentFor", () => { + + afterEach(() => { + + // remove cookie set by tests + helpers.deleteCookie('cookies_policy'); + + }); + + test("If there is no consent cookie, return false", () => { + + expect(window.GOVUK.hasConsentFor('analytics')).toBe(false); + + }); + + describe("If a consent cookie is set", () => { + + test("If the category is not saved in the cookie, return false", () => { + + window.GOVUK.setConsentCookie({ 'usage': true }); + + expect(window.GOVUK.hasConsentFor('analytics')).toBe(false); + + }); + + test("If the category is saved in the cookie, return its value", () => { + + window.GOVUK.setConsentCookie({ 'analytics': true }); + + expect(window.GOVUK.hasConsentFor('analytics')).toBe(true); + + }); + + }); + + }); + +}); diff --git a/tests/javascripts/cookieMessage.test.js b/tests/javascripts/cookieMessage.test.js new file mode 100644 index 000000000..37e5f3bdf --- /dev/null +++ b/tests/javascripts/cookieMessage.test.js @@ -0,0 +1,230 @@ +const helpers = require('./support/helpers'); + +beforeAll(() => { + + require('../../app/assets/javascripts/govuk/cookie-functions.js'); + require('../../app/assets/javascripts/analytics/analytics.js'); + require('../../app/assets/javascripts/analytics/init.js'); + require('../../app/assets/javascripts/cookieMessage.js'); + +}); + +afterAll(() => { + + require('./support/teardown.js'); + +}); + +describe("Cookie message", () => { + + let cookieMessage; + + beforeAll(() => { + + helpers.deleteCookie('cookies-policy'); + + }); + + beforeEach(() => { + + // add the script GA looks for in the document + document.body.appendChild(document.createElement('script')); + + jest.spyOn(window.GOVUK, 'initAnalytics'); + + cookieMessage = ` + `; + + document.body.innerHTML += cookieMessage; + + }); + + afterEach(() => { + + document.body.innerHTML = ''; + + // remove cookie set by tests + helpers.deleteCookie('cookies_policy'); + + // reset spies + window.GOVUK.initAnalytics.mockClear(); + + // remove analytics tracker + delete window.GOVUK.analytics; + + // reset global variable to state when init.js loaded + window['ga-disable-UA-26179049-1'] = true; + + }); + + /* + Note: If no JS, the cookie banner shows a button to take you to the cookies page for more information. + + This works through CSS, based on the presence of the `js-enabled` class on the so is not tested here. + */ + + test("If the cookies set by the old banner still exist, they can be cleared with the `clearOldCookies` method", () => { + + helpers.setCookie('seen_cookie_message', 'true', { 'days': 365 }); + helpers.setCookie('_ga', 'GA1.1.123.123', { 'days': 365 }); + helpers.setCookie('_gid', 'GA1.1.456.456', { 'days': 1 }); + + window.GOVUK.Modules.CookieBanner.clearOldCookies(); + + expect(window.GOVUK.cookie('seen_cookie_message')).toBeNull(); + expect(window.GOVUK.cookie('_ga')).toBeNull(); + expect(window.GOVUK.cookie('_gid')).toBeNull(); + + }); + + test("If user has made a choice to give their consent or not, the cookie banner should be hidden", () => { + + window.GOVUK.setConsentCookie({ 'analytics': false }); + + window.GOVUK.modules.start() + + expect(helpers.element(document.querySelector('.notify-cookie-banner')).is('hidden')).toBe(true); + + }); + + describe("If user hasn't made a choice to give their consent or not", () => { + + beforeEach(() => { + + window.GOVUK.modules.start(); + + }); + + test("The cookie banner should show", () => { + + const banner = helpers.element(document.querySelector('.notify-cookie-banner')); + + expect(banner.is('hidden')).toBe(false); + + }); + + test("No analytics should run", () => { + + expect(window.GOVUK.initAnalytics).not.toHaveBeenCalled(); + + }); + + describe("If the user clicks the button to accept analytics", () => { + + beforeEach(() => { + + const acceptButton = document.querySelector('.notify-cookie-banner__button-accept button'); + + helpers.triggerEvent(acceptButton, 'click'); + + }); + + test("the banner should confirm your choice and link to the cookies page as a way to change your mind", () => { + + confirmation = helpers.element(document.querySelector('.notify-cookie-banner__confirmation')); + + expect(confirmation.is('hidden')).toBe(false); + expect(confirmation.el.textContent.trim()).toEqual(expect.stringMatching(/^You’ve accepted analytics cookies/)); + + }); + + test("If the user clicks the 'hide' button, the banner should be hidden", () => { + + const hideButton = document.querySelector('.notify-cookie-banner__hide-button'); + const banner = helpers.element(document.querySelector('.notify-cookie-banner')); + + helpers.triggerEvent(hideButton, 'click'); + + expect(banner.is('hidden')).toBe(true); + + }); + + test("The consent cookie should be set, with analytics set to 'true'", () => { + + expect(window.GOVUK.getConsentCookie()).toEqual({ 'analytics': true }); + + }); + + test("The analytics should be set up", () => { + + expect(window.GOVUK.analytics).toBeDefined(); + + }); + + }); + + describe("If the user clicks the button to reject analytics", () => { + + beforeEach(() => { + + const rejectButton = document.querySelector('.notify-cookie-banner__button-reject button'); + + helpers.triggerEvent(rejectButton, 'click'); + + }); + + test("the banner should confirm your choice and link to the cookies page as a way to change your mind", () => { + + confirmation = helpers.element(document.querySelector('.notify-cookie-banner__confirmation')); + + expect(confirmation.is('hidden')).toBe(false); + expect(confirmation.el.textContent.trim()).toEqual(expect.stringMatching(/^You told us not to use analytics cookies/)); + + }); + + test("If the user clicks the 'hide' button, the banner should be hidden", () => { + + const hideButton = document.querySelector('.notify-cookie-banner__hide-button'); + const banner = helpers.element(document.querySelector('.notify-cookie-banner')); + + helpers.triggerEvent(hideButton, 'click'); + + expect(banner.is('hidden')).toBe(true); + + }); + + test("The consent cookie should be set, with analytics set to 'true'", () => { + + expect(window.GOVUK.getConsentCookie()).toEqual({ 'analytics': false }); + + }); + + test("The analytics should not be set up", () => { + + expect(window.GOVUK.analytics).not.toBeDefined(); + + }); + + }); + + }); + +}); diff --git a/tests/javascripts/cookieSettings.test.js b/tests/javascripts/cookieSettings.test.js new file mode 100644 index 000000000..a6c6f1344 --- /dev/null +++ b/tests/javascripts/cookieSettings.test.js @@ -0,0 +1,255 @@ +const helpers = require('./support/helpers'); + +beforeAll(() => { + + require('../../app/assets/javascripts/govuk/cookie-functions.js'); + require('../../app/assets/javascripts/consent.js'); + require('../../app/assets/javascripts/analytics/analytics.js'); + require('../../app/assets/javascripts/analytics/init.js'); + require('../../app/assets/javascripts/cookieSettings.js'); + +}); + +afterAll(() => { + + require('./support/teardown.js'); + +}); + +describe("Cookie settings", () => { + + let cookiesPageContent; + let yesRadio; + let noRadio; + let saveButton; + + beforeEach(() => { + + // add the script GA looks for in the document + document.body.appendChild(document.createElement('script')); + + window.ga = jest.fn(); + jest.spyOn(window.GOVUK, 'initAnalytics'); + + cookiesPageContent = ` + +

Cookies

+

+ Cookies are small files saved on your phone, tablet or computer when you visit a website. +

+

We use cookies to make GOV.UK Notify work and collect information about how you use our service.

+ +

Analytics cookies (optional)

+ `; + + document.body.innerHTML += cookiesPageContent; + + yesRadio = document.querySelector('#cookies-analytics-yes'); + noRadio = document.querySelector('#cookies-analytics-no'); + saveButton = document.querySelector('.govuk-button'); + + }); + + afterEach(() => { + + document.body.innerHTML = ''; + + // remove cookie set by tests + helpers.deleteCookie('cookies_policy'); + + // reset spies + window.ga.mockClear(); + window.GOVUK.initAnalytics.mockClear(); + + // remove analytics tracker + delete window.GOVUK.analytics; + + // reset global variable to state when init.js loaded + window['ga-disable-UA-26179049-1'] = true; + + }); + + /* + Note: If no JS, the cookies page contains content to explain why JS is required to set analytics cookies. + This is hidden if JS is available when the page loads. + + The message displayed to confirm any selection made is also in the page but hidden on load. + + Both of these work through CSS, based on the presence of the `js-enabled` class on the so are not tested here. + */ + + describe("When the page loads", () => { + + test("If user has not chosen to accept or reject analytics, the radios for making that choice should be set to unchecked", () => { + + window.GOVUK.modules.start(); + + expect(yesRadio.checked).toBe(false); + expect(noRadio.checked).toBe(false); + + }); + + test("If analytics are accepted, the radio for 'accept analytics' should be set to checked", () => { + + window.GOVUK.setConsentCookie({ 'analytics': true }); + + window.GOVUK.modules.start(); + + expect(yesRadio.checked).toBe(true); + expect(noRadio.checked).toBe(false); + + }); + + test("If analytics are rejected, the radio for 'reject analytics' should be set to checked", () => { + + window.GOVUK.setConsentCookie({ 'analytics': false }); + + window.GOVUK.modules.start(); + + expect(yesRadio.checked).toBe(false); + expect(noRadio.checked).toBe(true); + + }); + + }); + + describe("When the 'Save cookie settings' button is clicked", () => { + + beforeEach(() => { + + window.GOVUK.modules.start(); + + }); + + test("If no selection is made, set consent to reject analytics", () => { + + helpers.triggerEvent(saveButton, 'click'); + + expect(window.GOVUK.getConsentCookie()).toEqual({ 'analytics': false }); + + }); + + test("If a selection is made, save this as consent", () => { + + yesRadio.checked = true; + + helpers.triggerEvent(saveButton, 'click'); + + expect(window.GOVUK.getConsentCookie()).toEqual({ 'analytics': true }); + + }); + + describe("The message confirming your choice", () => { + + let confirmationMessage; + + beforeEach(() => { + + confirmationMessage = document.querySelector('.cookie-settings__confirmation'); + helpers.triggerEvent(saveButton, 'click'); + + }); + + test("Should be shown when the 'Save cookie settings' button is clicked", () => { + + expect(helpers.element(confirmationMessage).is('hidden')).toBe(false); + + }); + + test("Should include a link to the last page visited, if information on the referrer is available", () => { + + jest.spyOn(document, 'referrer', 'get').mockReturnValue('https://notifications.service.gov.uk/privacy'); + + helpers.triggerEvent(saveButton, 'click'); + + expect(confirmationMessage.querySelector('.cookie-settings__prev-page').getAttribute('href')).toEqual('/privacy'); + + }); + + }); + + describe("Analytics code", () => { + + beforeAll(() => { + + jest.spyOn(window, 'location', 'get').mockImplementation(() => { + + return { + 'pathname': '/privacy', + 'search': '' + } + + }); + + }); + + test("if user accepted analytics, the analytics code should initialise and register a pageview", () => { + + window.GOVUK.modules.start(); + + yesRadio.checked = true; + + helpers.triggerEvent(saveButton, 'click'); + + expect(window.GOVUK.initAnalytics).toHaveBeenCalled(); + + expect(window.ga).toHaveBeenCalled(); + // the first 4 calls are configuration + expect(window.ga.mock.calls[4]).toEqual(['send', 'pageview', '/privacy']); + + }); + + test("if user rejected analytics, the analytics code should not run", () => { + + window.GOVUK.modules.start(); + + noRadio.checked = true; + + helpers.triggerEvent(saveButton, 'click'); + + expect(window.GOVUK.initAnalytics).not.toHaveBeenCalled(); + + }); + + }); + + }); + +}); diff --git a/tests/javascripts/support/helpers.js b/tests/javascripts/support/helpers.js index 90c253306..9135ed245 100644 --- a/tests/javascripts/support/helpers.js +++ b/tests/javascripts/support/helpers.js @@ -1,6 +1,7 @@ const globals = require('./helpers/globals.js'); const events = require('./helpers/events.js'); const domInterfaces = require('./helpers/dom_interfaces.js'); +const cookies = require('./helpers/cookies.js'); const html = require('./helpers/html.js'); const elements = require('./helpers/elements.js'); const rendering = require('./helpers/rendering.js'); @@ -14,6 +15,8 @@ exports.moveSelectionToRadio = events.moveSelectionToRadio; exports.activateRadioWithSpace = events.activateRadioWithSpace; exports.RangeMock = domInterfaces.RangeMock; exports.SelectionMock = domInterfaces.SelectionMock; +exports.deleteCookie = cookies.deleteCookie; +exports.setCookie = cookies.setCookie; exports.getRadioGroup = html.getRadioGroup; exports.getRadios = html.getRadios; exports.templatesAndFoldersCheckboxes = html.templatesAndFoldersCheckboxes; diff --git a/tests/javascripts/support/helpers/cookies.js b/tests/javascripts/support/helpers/cookies.js new file mode 100644 index 000000000..2ef2c0ebd --- /dev/null +++ b/tests/javascripts/support/helpers/cookies.js @@ -0,0 +1,22 @@ +// Helper for deleting a cookie +function deleteCookie (cookieName) { + + document.cookie = cookieName + '=; path=/; expires=' + (new Date()); + +}; + +function setCookie (name, value, options) { + if (typeof options === 'undefined') { + options = {}; + } + var cookieString = name + '=' + value + '; path=/;domain=' + window.location.hostname; + if (options.days) { + var date = new Date(); + date.setTime(date.getTime() + (options.days * 24 * 60 * 60 * 1000)); + cookieString = cookieString + '; expires=' + date.toGMTString(); + } + document.cookie = cookieString; +}; + +exports.deleteCookie = deleteCookie; +exports.setCookie = setCookie; diff --git a/tests/javascripts/support/polyfills.js b/tests/javascripts/support/polyfills.js new file mode 100644 index 000000000..0809eb763 --- /dev/null +++ b/tests/javascripts/support/polyfills.js @@ -0,0 +1,53 @@ +// Polyfills for any parts of the DOM API available in browsers but not JSDOM + +// From: https://gist.github.com/eligrey/1276030 +HTMLElement.prototype.insertAdjacentHTML = function(position, html) { + "use strict"; + + var + ref = this + , container = ref.ownerDocument.createElementNS("http://www.w3.org/1999/xhtml", "_") + , ref_parent = ref.parentNode + , node, first_child, next_sibling + ; + + container.innerHTML = html; + + switch (position.toLowerCase()) { + case "beforebegin": + while ((node = container.firstChild)) { + ref_parent.insertBefore(node, ref); + } + break; + case "afterbegin": + first_child = ref.firstChild; + while ((node = container.lastChild)) { + first_child = ref.insertBefore(node, first_child); + } + break; + case "beforeend": + while ((node = container.firstChild)) { + ref.appendChild(node); + } + break; + case "afterend": + next_sibling = ref.nextSibling; + while ((node = container.lastChild)) { + next_sibling = ref_parent.insertBefore(node, next_sibling); + } + break; + } + +}; + +// from: https://developer.mozilla.org/en-US/docs/Web/API/Element/insertAdjacentText#Polyfill +if (!Element.prototype.insertAdjacentText) { + Element.prototype.insertAdjacentText = function(type, txt){ + this.insertAdjacentHTML( + type, + (txt+'') // convert to string + .replace(/&/g, '&') // embed ampersand symbols + .replace(/