diff --git a/tests/javascripts/apiKey.test.js b/tests/javascripts/apiKey.test.js
new file mode 100644
index 000000000..34f7509c8
--- /dev/null
+++ b/tests/javascripts/apiKey.test.js
@@ -0,0 +1,212 @@
+const helpers = require('./support/helpers');
+
+afterAll(() => {
+
+ require('./support/teardown.js');
+
+ // clear up methods in the global space
+ document.queryCommandSupported = undefined;
+
+});
+
+describe('API key', () => {
+
+ let apiKey;
+ let thing;
+ let component;
+
+ beforeEach(() => {
+
+ apiKey = 'admin-service-6658542f-0cad-491f-bec8-ab8457700ead-53c0c274-8e83-48f1-8448-598657bb39af';
+ thing = 'API key';
+
+ // mock sticky JS
+ window.GOVUK.stickAtBottomWhenScrolling = {
+ recalculate: jest.fn(() => {})
+ }
+
+ });
+
+ test("If copy command isn't available, nothing should happen", () => {
+
+ // fake support for the copy command not being available
+ document.queryCommandSupported = jest.fn(command => false);
+
+ require('../../app/assets/javascripts/apiKey.js');
+
+ // set up DOM
+ document.body.innerHTML =`
+
+ ${thing}
+
+
+ ${apiKey}
+
`;
+
+ component = document.querySelector('[data-module=api-key]');
+
+ // start the module
+ window.GOVUK.modules.start();
+
+ expect(component.querySelector('input[type=button]')).toBeNull();
+
+ });
+
+ describe("If copy command is available", () => {
+
+ let componentHeightOnLoad;
+
+ beforeAll(() => {
+
+ // assume copy command is available
+ document.queryCommandSupported = jest.fn(command => command === 'copy');
+
+ // force module require to not come from cache
+ jest.resetModules();
+
+ require('../../app/assets/javascripts/apiKey.js');
+
+ });
+
+ beforeEach(() => {
+
+ // set up DOM
+ document.body.innerHTML =`
+
+ ${thing}
+
+
+ ${apiKey}
+
`;
+
+ component = document.querySelector('[data-module=api-key]');
+
+ // mock DOM API called for element height
+ componentHeightOnLoad = 50;
+ jest.spyOn(component, 'offsetHeight', 'get').mockImplementation(() => componentHeightOnLoad);
+
+ // start the module
+ window.GOVUK.modules.start();
+
+ });
+
+ describe("On page load", () => {
+
+ test("It should add a button for copying the key to the clipboard", () => {
+
+ expect(component.querySelector('input[type=button]')).not.toBeNull();
+
+ });
+
+ test("It should add the 'api-key' class", () => {
+
+ expect(component.classList.contains('api-key')).toBe(true);
+
+ });
+
+ test("It should change aria-live to 'polite'", () => {
+
+ expect(component.getAttribute('aria-live')).toEqual('polite');
+
+ });
+
+ test("It should tell any sticky JS present the page has changed", () => {
+
+ // recalculate forces the sticky JS to recalculate any stored DOM position/dimensions
+ expect(window.GOVUK.stickAtBottomWhenScrolling.recalculate).toHaveBeenCalled();
+
+ });
+
+ test("It should set the component's minimum height based on its height when the page loads", () => {
+
+ // to prevent the position of the button moving when the state changes
+ expect(window.getComputedStyle(component)['min-height']).toEqual(`${componentHeightOnLoad}px`);
+
+ });
+
+ });
+
+ describe("If you click the 'Copy API key to clipboard' button", () => {
+
+ let selectionMock;
+ let rangeMock;
+ let keyEl;
+ let copyButton;
+
+ beforeEach(() => {
+
+ keyEl = component.querySelector('span');
+ copyButton = component.querySelector('input[type=button]');
+
+ // mock objects used to manipulate the page selection
+ selectionMock = new helpers.SelectionMock(jest);
+ rangeMock = new helpers.RangeMock(jest);
+
+ // plug gaps in JSDOM's API for manipulation of selections
+ window.getSelection = jest.fn(() => selectionMock);
+ document.createRange = jest.fn(() => rangeMock);
+
+ // plug JSDOM not having execCommand
+ document.execCommand = jest.fn(() => {});
+
+ helpers.triggerEvent(copyButton, 'click');
+
+ });
+
+ test("It should change the text to confirm the copy action", () => {
+
+ expect(component.querySelector('span').textContent.trim()).toEqual('Copied to clipboard');
+
+ });
+
+ test("It should swap the button for one to show the API key", () => {
+
+ expect(component.querySelector('input[type=button]').getAttribute('value')).toEqual('Show API key');
+
+ });
+
+ test("It should copy the key to the clipboard", () => {
+
+ // it should make a selection (a range) from the contents of the element containing the API key
+ expect(rangeMock.selectNodeContents.mock.calls[0]).toEqual([keyEl]);
+
+ // that selection (a range) should be added to that for the page (a selection)
+ expect(selectionMock.addRange.mock.calls[0]).toEqual([rangeMock]);
+
+ expect(document.execCommand).toHaveBeenCalled();
+ expect(document.execCommand.mock.calls[0]).toEqual(['copy']);
+
+ // reset any methods in the global space
+ window.queryCommandSupported = undefined;
+ window.getSelection = undefined;
+ document.createRange = undefined;
+
+ });
+
+ describe("If you then click the 'Show API key'", () => {
+
+ beforeEach(() => {
+
+ helpers.triggerEvent(component.querySelector('input[type=button]'), 'click');
+
+ });
+
+ test("It should change the text to show the API key", () => {
+
+ expect(component.querySelector('span').textContent.trim()).toEqual(apiKey);
+
+ });
+
+ test("It should swap the button for one to copy the key to the clipboard", () => {
+
+ expect(component.querySelector('input[type=button]').getAttribute('value')).toEqual('Copy API key to clipboard');
+
+ })
+
+ });
+
+ });
+
+ });
+
+});
diff --git a/tests/javascripts/support/helpers.js b/tests/javascripts/support/helpers.js
index 9e7b056a8..ee56ae375 100644
--- a/tests/javascripts/support/helpers.js
+++ b/tests/javascripts/support/helpers.js
@@ -227,6 +227,50 @@ class WindowMock {
}
}
+// Base class for mocking an DOM APi interfaces not in JSDOM
+class DOMInterfaceMock {
+
+ constructor (jest, spec) {
+
+ // set up methods so their calls can be tracked
+ // leave implementation/return values to the test
+ spec.methods.forEach(method => this[method] = jest.fn(() => {}));
+
+ // set up props
+ // any spies should be relative to the test so not set here
+ spec.props.forEach(prop => {
+
+ Object.defineProperty(this, prop, {
+ get: () => this[prop],
+ set: value => this[prop] = value
+ });
+
+ });
+
+ }
+
+}
+
+// Very basic class for stubbing the Range interface
+// Only contains methods required for current tests
+class RangeMock extends DOMInterfaceMock {
+
+ constructor (jest) {
+ super(jest, { props: [], methods: ['selectNodeContents'] });
+ }
+
+}
+
+// Very basic class for stubbing the Selection interface
+// Only contains methods required for current tests
+class SelectionMock extends DOMInterfaceMock {
+
+ constructor (jest) {
+ super(jest, { props: [], methods: ['removeAllRanges', 'addRange'] });
+ }
+
+}
+
// function to ask certain questions of a DOM Element
const element = function (el) {
return new ElementQuery(el);
@@ -236,5 +280,7 @@ exports.triggerEvent = triggerEvent;
exports.clickElementWithMouse = clickElementWithMouse;
exports.moveSelectionToRadio = moveSelectionToRadio;
exports.activateRadioWithSpace = activateRadioWithSpace;
+exports.RangeMock = RangeMock;
+exports.SelectionMock = SelectionMock;
exports.element = element;
exports.WindowMock = WindowMock;