`;
+ `;
};
beforeEach(() => {
- // 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(() => {});
+ // Reset the clipboard mock for each test
+ if (clipboardWriteTextMock) {
+ clipboardWriteTextMock.mockClear();
+ }
// mock sticky JS
window.NotifyModules.stickAtBottomWhenScrolling = {
@@ -52,10 +37,10 @@ describe('copy to clipboard', () => {
});
- test("If copy command isn't available, nothing should happen", () => {
+ test("If Clipboard API isn't available, nothing should happen", () => {
- // fake support for the copy command not being available
- document.queryCommandSupported = jest.fn(command => false);
+ // Remove clipboard API
+ navigator.clipboard = undefined;
require('../../app/assets/javascripts/copyToClipboard.js');
@@ -70,14 +55,15 @@ describe('copy to clipboard', () => {
});
- describe("If copy command is available", () => {
-
- let componentHeightOnLoad;
+ describe("If Clipboard API is available", () => {
beforeAll(() => {
- // assume copy command is available
- document.queryCommandSupported = jest.fn(command => command === 'copy');
+ // Mock modern Clipboard API BEFORE loading the module
+ clipboardWriteTextMock = jest.fn(() => Promise.resolve());
+ navigator.clipboard = {
+ writeText: clipboardWriteTextMock
+ };
// force module require to not come from cache
jest.resetModules();
@@ -86,10 +72,6 @@ describe('copy to clipboard', () => {
});
- afterEach(() => {
- screenMock.reset();
- });
-
describe("On page load", () => {
describe("For all variations of the initial HTML", () => {
@@ -100,26 +82,6 @@ describe('copy to clipboard', () => {
component = document.querySelector('[data-module=copy-to-clipboard]');
- // set default style for component height (queried by jQuery before checking DOM APIs)
- const stylesheet = document.createElement('style');
- stylesheet.innerHTML = '[data-module=copy-to-clipboard] { height: auto; }'; // set to browser default
- document.getElementsByTagName('head')[0].appendChild(stylesheet);
-
- componentHeightOnLoad = 50;
-
- // mock the DOM APIs called for the position & dimension of the component
- screenMock = new helpers.ScreenMock(jest);
- screenMock.setWindow({
- width: 1990,
- height: 940,
- scrollTop: 0
- });
- screenMock.mockPositionAndDimension('component', component, {
- offsetHeight: componentHeightOnLoad,
- offsetWidth: 641,
- offsetTop: 0
- });
-
// start the module
window.NotifyModules.start();
@@ -128,6 +90,7 @@ describe('copy to clipboard', () => {
test("It should add a button for copying the thing to the clipboard", () => {
expect(component.querySelector('button')).not.toBeNull();
+ expect(component.querySelector('button').textContent.trim()).toContain('Copy Some Thing');
});
@@ -139,21 +102,23 @@ describe('copy to clipboard', () => {
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.NotifyModules.stickAtBottomWhenScrolling.recalculate).toHaveBeenCalled();
});
- test("It should set the component's minimum height based on its height when the page loads", () => {
+ test("It should display the value", () => {
- // to prevent the position of the button moving when the state changes
- expect(window.getComputedStyle(component)['min-height']).toEqual(`${componentHeightOnLoad}px`);
+ expect(component.querySelector('.copy-to-clipboard__value')).not.toBeNull();
+ expect(component.querySelector('.copy-to-clipboard__value').textContent).toBe(apiKey);
});
- test("It should render the 'thing' without extra whitespace", () => {
+ test("It should have an aria-live region for screen reader announcements", () => {
- expect(component.querySelector('.copy-to-clipboard__value').textContent).toBe('00000000-0000-0000-0000-000000000000');
+ const liveRegion = component.querySelector('[aria-live]');
+ expect(liveRegion).not.toBeNull();
+ expect(liveRegion.getAttribute('aria-live')).toBe('polite');
+ expect(liveRegion.getAttribute('aria-atomic')).toBe('true');
});
@@ -163,8 +128,6 @@ describe('copy to clipboard', () => {
beforeEach(() => {
- // If 'thing' (what the id is) and 'name' (its specific idenifier on the page) are
- // different, it will be one of others called the same 'thing'.
setUpDOM({ 'thing': 'ID', 'name': 'Default' });
component = document.querySelector('[data-module=copy-to-clipboard]');
@@ -174,19 +137,12 @@ describe('copy to clipboard', () => {
});
- // Because it is not the only 'thing' on the page, the id will not have a heading
- // and so needs some prefix text to label it
- test("The id should have a hidden prefix to label what it is", () => {
+ test("the button should have shorter label and hidden suffix", () => {
- const value = component.querySelector('.copy-to-clipboard__value .usa-sr-only');
- expect(value).not.toBeNull();
- expect(value.textContent).toEqual('ID: ');
+ const button = component.querySelector('button');
+ expect(button.textContent).toContain('Copy ID');
- });
-
- test("the button should have a hidden suffix naming the id it is for", () => {
-
- const buttonSuffix = component.querySelector('button .usa-sr-only');
+ const buttonSuffix = button.querySelector('.usa-sr-only');
expect(buttonSuffix).not.toBeNull();
expect(buttonSuffix.textContent).toEqual(' for Default');
@@ -198,9 +154,6 @@ describe('copy to clipboard', () => {
beforeEach(() => {
- // The heading is added if 'thing' (what the id is) has the same value as 'name'
- // (its specific identifier on the page) because this means it can assume it is
- // the only one of its type there
setUpDOM({ 'thing': 'Some Thing', 'name': 'Some Thing' });
component = document.querySelector('[data-module=copy-to-clipboard]');
@@ -210,12 +163,11 @@ describe('copy to clipboard', () => {
});
- test("Its button and id shouldn't have extra hidden text to identify them", () => {
+ test("the button should have full label without extra suffix", () => {
- const value = component.querySelector('.copy-to-clipboard__value .usa-sr-only');
- const buttonSuffix = component.querySelector('button .usa-sr-only');
- expect(value).toBeNull();
- expect(buttonSuffix).toBeNull();
+ const button = component.querySelector('button');
+ expect(button.textContent).toContain('Copy Some Thing to clipboard');
+ expect(button.textContent).not.toContain(' for ');
})
@@ -223,187 +175,84 @@ describe('copy to clipboard', () => {
});
- describe("If you click the 'Copy Some Thing to clipboard' button", () => {
+ describe("If you click the 'Copy' button", () => {
- describe("For all variations of the initial HTML", () => {
+ beforeEach(async () => {
- let keyEl;
+ jest.useFakeTimers();
- beforeEach(() => {
+ setUpDOM({ 'thing': 'API key', 'name': 'API key' });
- setUpDOM({ 'thing': 'Some Thing', 'name': 'Some Thing' });
+ component = document.querySelector('[data-module=copy-to-clipboard]');
- // start the module
- window.NotifyModules.start();
+ // start the module
+ window.NotifyModules.start();
- component = document.querySelector('[data-module=copy-to-clipboard]');
- keyEl = component.querySelector('.copy-to-clipboard__value');
+ const button = component.querySelector('button');
+ helpers.triggerEvent(button, 'click');
- helpers.triggerEvent(component.querySelector('button'), 'click');
-
- });
-
- test("The live-region should be shown and its text should confirm the copy action", () => {
-
- const liveRegion = component.querySelector('.copy-to-clipboard__notice');
-
- expect(liveRegion.classList.contains('usa-sr-only')).toBe(false);
- expect(liveRegion.textContent.trim()).toEqual(
- expect.stringContaining('Copied to clipboard')
- );
-
- });
-
- // The button also says this but its text after being changed is not announced due to being
- // lower priority than the live-region
- test("The live-region should contain some hidden text giving context to the statement shown", () => {
-
- const liveRegionHiddenText = component.querySelectorAll('.copy-to-clipboard__notice .usa-sr-only');
-
- expect(liveRegionHiddenText.length).toEqual(2);
- expect(liveRegionHiddenText[0].textContent).toEqual('Some Thing ');
- expect(liveRegionHiddenText[1].textContent).toEqual(', press button to show in page');
-
- });
-
- test("It should swap the button for one to show the Some Thing", () => {
-
- expect(component.querySelector('button').textContent.trim()).toEqual(
- expect.stringContaining('Show Some Thing')
- );
-
- });
-
- test("It should remove the id from the page", () => {
-
- expect(component.querySelector('.copy-to-clipboard__value')).toBeNull();
-
- });
-
- test("It should copy the thing to the clipboard", () => {
-
- // it should make a selection (a range) from the contents of the element containing the Some Thing
- 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 Some Thing'", () => {
-
- beforeEach(() => {
-
- helpers.triggerEvent(component.querySelector('button'), 'click');
-
- });
-
- test("It should change the text to show the Some Thing", () => {
-
- expect(component.querySelector('.copy-to-clipboard__value')).not.toBeNull();
-
- });
-
- test("It should swap the button for one to copy the thing to the clipboard", () => {
-
- expect(component.querySelector('button').textContent.trim()).toEqual(
- expect.stringContaining('Copy Some Thing to clipboard')
- );
-
- })
-
- });
+ // Wait for async clipboard operation
+ await Promise.resolve();
});
- describe("If it's one of many in the page", () => {
+ afterEach(() => {
+ jest.useRealTimers();
+ });
- beforeEach(() => {
+ test("The value should still be visible", () => {
- // If 'thing' (what the id is) and 'name' (its specific idenifier on the page) are
- // different, it will be one of others called the same 'thing'.
- setUpDOM({ 'thing': 'ID', 'name': 'Default' });
-
- // start the module
- window.NotifyModules.start();
-
- component = document.querySelector('[data-module=copy-to-clipboard]');
-
- helpers.triggerEvent(component.querySelector('button'), 'click');
-
- });
-
- test("the button should have a hidden suffix naming the id it is for", () => {
-
- const buttonSuffix = component.querySelector('button .usa-sr-only');
- expect(buttonSuffix).not.toBeNull();
- expect(buttonSuffix.textContent).toEqual(' for Default');
-
- });
-
- test("the copied selection (range) should start after visually hidden prefix", () => {
-
- // that selection (a range) should have a startOffset of 1:
- // index 0: the visually hidden prefix node, for example "Template ID: " or "API key: "
- // index 1: the value node
- expect(rangeMock.setStart).toHaveBeenCalled();
- expect(rangeMock.setStart.mock.calls[0][1]).toEqual(1);
-
- // reset any methods in the global space
- window.queryCommandSupported = undefined;
- window.getSelection = undefined;
- document.createRange = undefined;
-
- });
+ expect(component.querySelector('.copy-to-clipboard__value')).not.toBeNull();
+ expect(component.querySelector('.copy-to-clipboard__value').textContent).toBe(apiKey);
});
- describe("If it's the only one on the page", () => {
+ test("The button text should change to 'Copied!'", () => {
- beforeEach(() => {
+ const button = component.querySelector('button');
+ expect(button.textContent).toContain('Copied!');
- // The heading is added if 'thing' (what the id is) has the same value as 'name'
- // (its specific identifier on the page) because this means it can assume it is
- // the only one of its type there
- setUpDOM({ 'thing': 'Some Thing', 'name': 'Some Thing' });
+ });
- // start the module
- window.NotifyModules.start();
+ test("The button should be disabled", () => {
- component = document.querySelector('[data-module=copy-to-clipboard]');
+ const button = component.querySelector('button');
+ expect(button.disabled).toBe(true);
- helpers.triggerEvent(component.querySelector('button'), 'click');
+ });
- });
+ test("Screen reader announcement should confirm the copy", () => {
- test("Its button and id shouldn't have extra hidden text to identify them", () => {
+ const liveRegion = component.querySelector('[aria-live]');
+ expect(liveRegion.textContent).toBe('API key copied to clipboard');
- const prefix = component.querySelector('.copy-to-clipboard__value .usa-sr-only');
- const buttonSuffix = component.querySelector('button .usa-sr-only');
- expect(prefix).toBeNull();
- expect(buttonSuffix).toBeNull();
+ });
- })
+ test("It should copy the value to clipboard using Clipboard API", () => {
- test("the copied selection (range) should start at the default position", () => {
+ expect(clipboardWriteTextMock).toHaveBeenCalledWith(apiKey);
- // that selection (a range) shouldn't call setStart to avoid the prefix:
- expect(rangeMock.setStart).not.toHaveBeenCalled();
+ });
- // reset any methods in the global space
- window.queryCommandSupported = undefined;
- window.getSelection = undefined;
- document.createRange = undefined;
+ test("After 2 seconds, button should reset to original text", () => {
- });
+ const button = component.querySelector('button');
+
+ jest.advanceTimersByTime(2000);
+
+ expect(button.textContent).toContain('Copy API key');
+ expect(button.textContent).not.toContain('Copied!');
+ expect(button.disabled).toBe(false);
+
+ });
+
+ test("After 2 seconds, screen reader announcement should clear", () => {
+
+ const liveRegion = component.querySelector('[aria-live]');
+
+ jest.advanceTimersByTime(2000);
+
+ expect(liveRegion.textContent).toBe('');
});
diff --git a/tests/javascripts/jest.config.js b/tests/javascripts/jest.config.js
index a3be0bb1d..d56f4c146 100644
--- a/tests/javascripts/jest.config.js
+++ b/tests/javascripts/jest.config.js
@@ -2,7 +2,8 @@ module.exports = {
collectCoverage: true,
coverageDirectory: './coverage',
coveragePathIgnorePatterns: [
- 'support/polyfills.js'
+ 'support/polyfills.js',
+ 'support/helpers/'
],
coverageThreshold: {
global: {