Feat/webpack (#2998)

* First commit

* Removed govuk for webpack. Modernized javascript importing. Removed dead js

* Fixed tests, a few styling bugs

* Fixed some table errors and regenerated backstop ref images

* Updated tests for coverage

* Changes from carlo suggestions
This commit is contained in:
Alex Janousek
2025-10-14 13:20:58 -04:00
committed by GitHub
parent 3b3191b337
commit 3a39d910b3
62 changed files with 1657 additions and 4861 deletions

View File

@@ -15,7 +15,7 @@ afterAll(() => {
describe('Collapsible fieldset', () => {
const _checkboxes = (start, end) => {
result = '';
let result = '';
for (let num = start; num <= end; num++) {
let id = `folder-permissions-${num}`;

View File

@@ -16,6 +16,7 @@ describe('copy to clipboard', () => {
let component;
let selectionMock;
let rangeMock;
let screenMock;
const setUpDOM = function (options) {

View File

@@ -1,5 +1,6 @@
beforeAll(() => {
require('../../app/assets/javascripts/errorBanner.js')
// ErrorBanner module sets window.NotifyModules.ErrorBanner for backward compatibility
require('../../app/assets/javascripts/errorBanner.js');
});
afterAll(() => {
@@ -34,4 +35,10 @@ describe("Error Banner", () => {
expect(document.querySelector('.banner-dangerous').classList).not.toContain('display-none')
});
});
test("Module exports ErrorBanner to window.NotifyModules", () => {
expect(window.NotifyModules.ErrorBanner).toBeDefined();
expect(window.NotifyModules.ErrorBanner.hideBanner).toBeDefined();
expect(window.NotifyModules.ErrorBanner.showBanner).toBeDefined();
});
});

View File

@@ -11,6 +11,7 @@ describe('File upload', () => {
let form;
let fileUpload;
let uploadControl;
beforeEach(() => {

View File

@@ -1,350 +0,0 @@
const helpers = require('./support/helpers');
beforeAll(() => {
// TODO: remove this when tests for sticky JS are written
require('../../app/assets/javascripts/stick-to-window-when-scrolling.js');
require('../../app/assets/javascripts/fullscreenTable.js');
});
afterAll(() => {
require('./support/teardown.js');
});
describe('FullscreenTable', () => {
let screenMock;
let container;
let tableFrame;
let table;
let numberColumnFrame;
beforeEach(() => {
const tableHeadings = () => {
let result = '';
const headings = ['1', 'name', 'email address', 'age', 'building number', 'address line 1', 'address line 2', 'postcode'];
headings.forEach((heading, idx) => {
if (idx === 0) {
result += `<th scope="col" class="table-field-heading-first">
<span class="visually-hidden">Row in file</span><span aria-hidden="true" class="table-field-invisible-error">${heading}</span>
</th>`;
} else {
result += `<th scope="col" class="table-field-heading">
${heading}
</th>`;
}
});
return result;
}
const rowCells = (cells) => {
let result = '';
Object.keys(cells).forEach((key, idx) => {
if (idx === 0) {
result += `<td class="table-field-index">
<span class="table-field-error">
${key}
</span>
</td>`;
} else {
result += `<td class="table-field-left-aligned ">
<div class="table-field-status-default">
${key}
</div>
</td>`;
}
});
return result;
};
const tableRows = () => {
let result = '';
const rows = [
['John Logie Baird', 'johnlbaird@gmail.com', '37', '22', 'Frith Street', 'Soho, London', 'W1D 4RF'],
['Guglielmo Marconi', 'gmarconi@hotmail.com', '21', 'Pontecchio Marconi', 'Via Celestini 1', 'Bologna', ''],
['Louis Braille', 'louisbraille@yahoo.co.uk', '', '56', 'Boulevard des Invalides', 'Paris', '75007'],
['Ray Tomlinson', 'hedy.lamarr@msn.com', '25', '870', '870 Winter Street', 'Waltham', 'MA 02451']
];
rows.forEach(row => {
result += `<tr class="table-row">${rowCells(row)}</tr>`;
});
return result;
}
screenMock = new helpers.ScreenMock(jest);
screenMock.setWindow({
width: 1990,
height: 940,
scrollTop: 0
});
// set up DOM
document.body.innerHTML =
`<main>
<div class="fullscreen-content" data-module="fullscreen-table">
<table class="table table-font-xsmall">
<caption class="heading-medium table-heading visuallyhidden">
people.csv
</caption>
<thead class="table-field-headings-visible">
<tr>
${tableHeadings()}
</tr>
</thead>
<tbody>
${tableRows()}
</tbody>
</table>
</div>
</main>`;
container = document.querySelector('.fullscreen-content');
});
afterEach(() => {
document.body.innerHTML = '';
});
describe("when it loads", () => {
test("it fixes the number column for each row without changing the semantics", () => {
// start module
window.NotifyModules.start();
tableFrame = document.querySelector('.fullscreen-scrollable-table');
numberColumnFrame = document.querySelector('.fullscreen-fixed-table');
expect(tableFrame).not.toBeNull();
expect(numberColumnFrame).not.toBeNull();
expect(numberColumnFrame.getAttribute('aria-hidden')).toEqual('true');
});
test("it calls the sticky JS to update any cached dimensions", () => {
const stickyJSSpy = jest.spyOn(window.NotifyModules.stickAtBottomWhenScrolling, 'recalculate');
// start module
window.NotifyModules.start();
expect(stickyJSSpy.mock.calls.length).toBe(1);
stickyJSSpy.mockClear();
});
test("the scrolling section is focusable and has an accessible name matching the table caption", () => {
// start module
window.NotifyModules.start();
tableFrame = document.querySelector('.fullscreen-scrollable-table');
caption = tableFrame.querySelector('caption');
expect(tableFrame.hasAttribute('role')).toBe(true);
expect(tableFrame.getAttribute('role')).toEqual('region');
expect(tableFrame.hasAttribute('aria-labelledby')).toBe(true);
expect(tableFrame.getAttribute('aria-labelledby')).toEqual(caption.getAttribute('id'));
});
test("the section providing the fixed row headers is not focusable and is hidden from assistive tech'", () => {
// start module
window.NotifyModules.start();
fixedRowHeaders = document.querySelector('.fullscreen-fixed-table');
expect(fixedRowHeaders.hasAttribute('role')).toBe(false);
expect(fixedRowHeaders.hasAttribute('aria-labelledby')).toBe(false);
expect(fixedRowHeaders.hasAttribute('tabindex')).toBe(false);
expect(fixedRowHeaders.hasAttribute('aria-hidden')).toBe(true);
expect(fixedRowHeaders.getAttribute('aria-hidden')).toEqual('true');
});
});
describe("the height of the table should fit the vertical space available to it", () => {
let containerBoundingClientRectSpy;
let containerClientRectsSpy;
beforeEach(() => {
// set the height and offset of the window and table container from the top of the document
// so just the top 268px of it appears on-screen
screenMock.window.setHeightTo(768);
screenMock.mockPositionAndDimension('container', container, {
'offsetHeight': 1000,
'offsetWidth': 641,
'offsetTop': 500
});
// start module
window.NotifyModules.start();
tableFrame = document.querySelector('.fullscreen-scrollable-table');
numberColumnFrame = document.querySelector('.fullscreen-fixed-table');
});
afterEach(() => {
screenMock.reset();
});
test("when the page has loaded", () => {
// the frames should crop to the top 268px of the table that is visible
expect(window.getComputedStyle(tableFrame)['height']).toEqual('268px');
expect(window.getComputedStyle(numberColumnFrame)['height']).toEqual('268px');
});
test("when the page has scrolled", () => {
// scroll the window so the table fills the height of the window (768px)
screenMock.window.scrollTo(500);
// the frames should crop to the window height
expect(window.getComputedStyle(tableFrame)['height']).toEqual('768px');
expect(window.getComputedStyle(numberColumnFrame)['height']).toEqual('768px');
});
test("when the page has resized", () => {
// resize the window by 232px (from 768px to 1000px)
screenMock.window.resizeTo({ height: 1000, width: 1024 });
// the frames should crop to the top 500px of the table now visible
expect(window.getComputedStyle(tableFrame)['height']).toEqual('500px');
expect(window.getComputedStyle(numberColumnFrame)['height']).toEqual('500px');
});
});
describe("the width of the table should fit the horizontal space available to it", () => {
let rowNumberColumnCell;
beforeEach(() => {
rowNumberColumnCell = container.querySelector('.table-field-index');
// set main content column width (used as module as gauge for table width)
screenMock.window.setWidthTo(1024);
document.querySelector('main').setAttribute('style', 'width: 742px');
// set total width of column for row numbers in table to 40px
rowNumberColumnCell.setAttribute('style', 'width: 40px');
// start module
window.NotifyModules.start();
tableFrame = document.querySelector('.fullscreen-scrollable-table');
numberColumnFrame = document.querySelector('.fullscreen-fixed-table');
});
afterEach(() => {
screenMock.reset();
});
test("when the page has loaded", () => {
// table should set its width to be that of `<main>`, minus margin-left for the row numbers column
expect(window.getComputedStyle(tableFrame)['width']).toEqual('702px'); // width of content column - numbers column
expect(window.getComputedStyle(tableFrame)['margin-left']).toEqual('40px'); // width of numbers column
// table for number column has 4px extra to allow space for drop shadow
expect(window.getComputedStyle(numberColumnFrame)['width']).toEqual('44px');
});
test("when the page has resized", () => {
// resize window and content column
document.querySelector('main').setAttribute('style', 'width: 720px');
screenMock.window.resizeTo({ height: 768, width: 960 });
// table should set its width to be that of `<main>`, minus margin-left for the row numbers column
expect(window.getComputedStyle(tableFrame)['width']).toEqual('680px'); // width of content column - numbers column
expect(window.getComputedStyle(tableFrame)['margin-left']).toEqual('40px'); // width of numbers column
// table for number column has 4px extra to allow space for drop shadow
expect(window.getComputedStyle(numberColumnFrame)['width']).toEqual('44px');
});
});
describe("when the table scrolls horizontally", () => {
let rightEdgeShadow;
beforeEach(() => {
// start module
window.NotifyModules.start();
tableFrame = document.querySelector('.fullscreen-scrollable-table');
table = tableFrame.querySelector('table');
numberColumnFrame = document.querySelector('.fullscreen-fixed-table');
rightEdgeShadow = container.querySelector('.fullscreen-right-shadow');
tableFrame.setAttribute('style', 'width: 742px');
table.setAttribute('style', 'width: 1000px');
});
test("the right edge of the table scroll area should have a drop-shadow if it isn't scrolled", () => {
tableFrame.scrollLeft = 0;
helpers.triggerEvent(tableFrame, 'scroll');
expect(numberColumnFrame.classList.contains('fullscreen-scrolled-table')).toBe(false);
expect(rightEdgeShadow.classList.contains('visible')).toBe(true);
});
test("the left edge of the table scroll area should have a drop-shadow if the table is scrolled to 100%", () => {
// scroll to end of table
tableFrame.scrollLeft = 258;
helpers.triggerEvent(tableFrame, 'scroll');
expect(numberColumnFrame.classList.contains('fullscreen-scrolled-table')).toBe(true);
expect(rightEdgeShadow.classList.contains('visible')).toBe(false);
});
test("both edges of the table scroll area should have a drop-shadow if the table is scrolled between 0% and 100%", () => {
// scroll to middle of table
tableFrame.scrollLeft = 129;
helpers.triggerEvent(tableFrame, 'scroll');
expect(numberColumnFrame.classList.contains('fullscreen-scrolled-table')).toBe(true);
expect(rightEdgeShadow.classList.contains('visible')).toBe(true);
});
});
});

View File

@@ -18,7 +18,7 @@ module.exports = {
url: 'https://beta.notify.gov',
},
transform: {
'^.+\\.js$': 'babel-jest',
'^.+\\.js$': ['babel-jest', { rootMode: 'upward' }],
},
transformIgnorePatterns: [
'node_modules/(?!(d3|d3-array|d3-axis|d3-brush|d3-chord|d3-collection|d3-color|d3-contour|d3-dispatch|d3-drag|d3-dsv|d3-ease|d3-fetch|d3-force|d3-format|d3-geo|d3-hierarchy|d3-interpolate|d3-path|d3-polygon|d3-quadtree|d3-random|d3-scale|d3-scale-chromatic|d3-selection|d3-shape|d3-tile|d3-time|d3-time-format|d3-timer|d3-transition|d3-voronoi|d3-zoom)/)'

View File

@@ -20,6 +20,8 @@ describe("List entry", () => {
'va.gov'
];
let inputList;
let name_value_pairs;
let fields;
const triggerEvent = (el, evtType) => {
const evt = new Event(evtType, {

View File

@@ -1,52 +0,0 @@
beforeAll(() => {
jest.spyOn(global, 'setTimeout');
document.body.innerHTML = `
<div id="countdown-container" class="usa-alert usa-alert--warning width-full margin-bottom-4">
<div class="usa-alert__body">
<h4 class="usa-alert__heading">Login.gov is required by April 16, 2024</h4>
<p class="usa-alert__text">
You have <span id="countdown"></span> left to use Login.gov to sign in
</p>
</div>
</div>
`
const sessionTimerModule = require('../../app/assets/javascripts/loginAlert.js');
window.NotifyModules.start();
});
jest.useFakeTimers();
const targetDate = new Date("April 16, 2024 00:00:00"); // Reference point
test('Hides the countdown if more than 10 days away', () => {
jest.setSystemTime(targetDate.getTime() - 12 * 24 * 60 * 60 * 1000); // 12 days before
window.updateCountdown(); // Update the countdown display
expect(document.getElementById("countdown-container").style.display).toBe("none");
});
test('Shows the countdown if 10 days or less away', () => {
jest.setSystemTime(targetDate.getTime() - 8 * 24 * 60 * 60 * 1000);
window.updateCountdown();
expect(document.getElementById("countdown-container").style.display).toBe("block");
});
test('Displays the correct number of days', () => {
jest.setSystemTime(targetDate.getTime() - 5 * 24 * 60 * 60 * 1000);
window.updateCountdown();
expect(document.getElementById("countdown").textContent).toBe("5 days ");
});
test('Hides the countdown if the target date has passed', () => {
jest.setSystemTime(targetDate.getTime() + 2 * 24 * 60 * 60 * 1000);
window.updateCountdown();
expect(document.getElementById("countdown-container").style.display).toBe("none");
});

View File

@@ -17,6 +17,9 @@ describe('RadioSelect', () => {
];
const HOURS = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24];
let originalOptionsForAllCategories;
let originalOptionsForCategory;
let selectedOption;
let categoryButtons;
const getDataFromOption = (option) => {
return {

View File

@@ -715,7 +715,7 @@ describe("Stick to top/bottom of window when scrolling", () => {
describe("If intending to stick to the bottom", () => {
let header;
let heading;
let content;
let pageFooter;
let windowHeight;

View File

@@ -220,7 +220,7 @@ describe('TemplateFolderForm', () => {
});
function getTemplateFolderCheckboxes () {
return templateFolderForm.querySelectorAll('.usa-checkbox__label');
return templateFolderForm.querySelectorAll('.usa-checkbox__input');
};
function getVisibleCounter () {
@@ -832,6 +832,9 @@ describe('TemplateFolderForm', () => {
describe("Additional selection counter scenarios", () => {
let templateFolderCheckboxes;
let visibleCounterText;
let hiddenCounterText;
let formControls;
beforeEach(() => {
@@ -855,19 +858,40 @@ describe('TemplateFolderForm', () => {
test("the content of both visible and hidden counters should match", () => {
// Ensure all checkboxes start unchecked
templateFolderCheckboxes.forEach(checkbox => checkbox.checked = false);
helpers.triggerEvent(templateFolderCheckboxes[1], 'click');
helpers.triggerEvent(templateFolderCheckboxes[2], 'click');
visibleCounterText = getVisibleCounter().textContent.trim();
hiddenCounterText = getHiddenCounter().textContent.trim();
expect(visibleCounterText).toEqual(hiddenCounterText);
});
test("the content of the counter should reflect the selection", () => {
helpers.triggerEvent(templateFolderCheckboxes[1], 'click');
helpers.triggerEvent(templateFolderCheckboxes[2], 'click');
// Re-query checkboxes to ensure we have fresh references
const checkboxes = getTemplateFolderCheckboxes();
expect(visibleCounterText).toEqual('2 templates selected');
// Ensure all checkboxes start unchecked
checkboxes.forEach(checkbox => {
checkbox.checked = false;
helpers.triggerEvent(checkbox, 'change');
});
// Click templates (indices 1 and 2)
checkboxes[1].checked = true;
helpers.triggerEvent(checkboxes[1], 'change');
checkboxes[2].checked = true;
helpers.triggerEvent(checkboxes[2], 'change');
visibleCounterText = getVisibleCounter().textContent.trim();
// Application counts the content, not just the number of items selected
expect(visibleCounterText).toEqual('1 template, 1 folder selected');
});
@@ -877,17 +901,37 @@ describe('TemplateFolderForm', () => {
test("the content of both visible and hidden counters should match", () => {
// Ensure all checkboxes start unchecked
templateFolderCheckboxes.forEach(checkbox => checkbox.checked = false);
helpers.triggerEvent(templateFolderCheckboxes[0], 'click');
visibleCounterText = getVisibleCounter().textContent.trim();
hiddenCounterText = getHiddenCounter().textContent.trim();
expect(visibleCounterText).toEqual(hiddenCounterText);
});
test("the content of the counter should reflect the selection", () => {
helpers.triggerEvent(templateFolderCheckboxes[0], 'click');
// Re-query checkboxes to ensure we have fresh references
const checkboxes = getTemplateFolderCheckboxes();
expect(visibleCounterText).toEqual('1 folder selected');
// Ensure all checkboxes start unchecked
checkboxes.forEach(checkbox => {
checkbox.checked = false;
helpers.triggerEvent(checkbox, 'change');
});
// Click folder (index 0)
checkboxes[0].checked = true;
helpers.triggerEvent(checkboxes[0], 'change');
visibleCounterText = getVisibleCounter().textContent.trim();
// Application counts the content within the folder
expect(visibleCounterText).toEqual('1 template, 1 folder selected');
});

View File

@@ -110,4 +110,22 @@ describe("Form Validation", () => {
expect(errorElement.style.display).toBe("none");
});
test("Uses 'This field' as label when no label element exists", async () => {
document.body.innerHTML = `
<form data-force-focus="True">
<input id="unlabeled-input" name="unlabeled" type="text" />
<button type="submit">Submit</button>
</form>
`;
form = document.querySelector('form[data-force-focus="True"]');
attachValidation();
form.dispatchEvent(new Event("submit", { bubbles: true }));
await new Promise(resolve => setTimeout(resolve, 20));
const errorMessage = document.getElementById("unlabeled-input-error");
expect(errorMessage.textContent).toBe("Error: This field is required.");
});
});