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

@@ -239,10 +239,10 @@
} }
const data = await response.json(); const data = await response.json();
labels = []; let labels = [];
deliveredData = []; let deliveredData = [];
failedData = []; let failedData = [];
pendingData = []; let pendingData = [];
let totalMessages = 0; let totalMessages = 0;
for (var dateString in data) { for (var dateString in data) {

View File

@@ -1,10 +1,7 @@
(function (window) { export function initCurrentYear() {
"use strict"; const currentYearElement = document.getElementById("current-year");
// Show the current year if (currentYearElement) {
const currentYearElement = document.getElementById("current-year"); currentYearElement.textContent = new Date().getFullYear();
}
if (currentYearElement) { }
currentYearElement.textContent = new Date().getFullYear();
}
})(window);

View File

@@ -1,16 +1,7 @@
(function (window) { export const ErrorBanner = {
"use strict"; hideBanner: () => $('.banner-dangerous').addClass('display-none'),
showBanner: () => $('.banner-dangerous').removeClass('display-none')
};
/* window.NotifyModules = window.NotifyModules || {};
This module is intended to be used to show and hide an error banner based on a javascript trigger. You should make window.NotifyModules.ErrorBanner = ErrorBanner;
sure the banner has an appropriate aria-live attribute, and a tabindex of -1 so that screenreaders and keyboard users
are alerted to the change respectively.
This may behave in unexpected ways if you have more than one element with the `banner-dangerous` class on your page.
*/
window.NotifyModules.ErrorBanner = {
hideBanner: () => $('.banner-dangerous').addClass('display-none'),
showBanner: () => $('.banner-dangerous')
.removeClass('display-none')
};
})(window);

View File

@@ -1,123 +0,0 @@
(function(window) {
"use strict";
window.NotifyModules['fullscreen-table'] = function() {
this.start = function(component) {
this.$component = $(component);
this.$table = this.$component.find('table');
this.nativeHeight = this.$component.innerHeight() + 20; // 20px to allow room for scrollbar
this.topOffset = this.$component.offset().top;
this.insertShims();
this.maintainWidth();
this.maintainHeight();
this.toggleShadows();
$(window)
.on('scroll resize', this.maintainHeight)
.on('resize', this.maintainWidth);
this.$scrollableTable
.on('scroll', this.toggleShadows)
.on('scroll', this.maintainHeight)
if (
window.NotifyModules.stickAtBottomWhenScrolling &&
window.NotifyModules.stickAtBottomWhenScrolling.recalculate
) {
window.NotifyModules.stickAtBottomWhenScrolling.recalculate();
}
this.maintainWidth();
};
this.insertShims = () => {
const attributesForFocus = 'role aria-labelledby';
let captionId = this.$table.find('caption').text().toLowerCase().replace(/[^A-Za-z]+/g, '');
this.$table.find('caption').attr('id', captionId);
this.$table.wrap(`<div class="fullscreen-scrollable-table" role="region" aria-labelledby="${captionId}"/>`);
this.$component
.append(
this.$component.find('.fullscreen-scrollable-table')
.clone()
.addClass('fullscreen-fixed-table')
.removeClass('fullscreen-scrollable-table')
.removeAttr(attributesForFocus)
.attr('aria-hidden', true)
.find('caption')
.removeAttr('id')
.closest('.fullscreen-fixed-table')
)
.append(
'<div class="fullscreen-right-shadow" />'
)
.after(
$("<div class='fullscreen-shim'/>").css({
'height': this.nativeHeight,
'top': this.topOffset
})
)
.css('position', 'absolute');
this.$scrollableTable = this.$component.find('.fullscreen-scrollable-table');
this.$fixedTable = this.$component.find('.fullscreen-fixed-table');
};
this.maintainHeight = () => {
let height = Math.min(
$(window).height() - this.topOffset + $(window).scrollTop(),
this.nativeHeight
);
this.$scrollableTable.outerHeight(height);
this.$fixedTable.outerHeight(height);
};
this.maintainWidth = () => {
let indexColumnWidth = this.$fixedTable.find('.table-field-index').outerWidth();
this.$scrollableTable
.css({
'width': this.$component.parent('main').width() - indexColumnWidth,
'margin-left': indexColumnWidth
});
this.$fixedTable
.width(indexColumnWidth + 4);
};
this.toggleShadows = () => {
this.$fixedTable
.toggleClass(
'fullscreen-scrolled-table',
this.$scrollableTable.scrollLeft() > 0
);
this.$component.find('.fullscreen-right-shadow')
.toggleClass(
'visible',
this.$scrollableTable.scrollLeft() < (this.$table.width() - this.$scrollableTable.width())
);
setTimeout(
() => this.$component.find('.fullscreen-right-shadow').addClass('with-transition'),
3000
);
};
};
})(window);

View File

@@ -0,0 +1,84 @@
// Webpack Entry Point
// Vendor libraries
import $ from 'jquery';
window.jQuery = window.$ = $;
import './jquery-expose.js';
import 'query-command-supported';
import 'textarea-caret';
import * as cbor from 'cbor-js';
// D3 - selective imports for tree shaking
import {
select,
scaleLinear,
scaleBand,
scaleSymlog,
scaleOrdinal,
axisBottom,
axisLeft,
stack,
stackOrderNone,
stackOffsetNone,
sum,
max,
format,
interpolate
} from 'd3';
window.CBOR = cbor;
window.d3 = {
select,
scaleLinear,
scaleBand,
scaleSymlog,
scaleOrdinal,
axisBottom,
axisLeft,
stack,
stackOrderNone,
stackOffsetNone,
sum,
max,
format,
interpolate
};
window.NotifyModules = window.NotifyModules || {};
// Core modules
import './modules/init.js';
import './modules/uswds-modules.js';
import './modules/show-hide-content.js';
import { registerModule, initModules } from './moduleRegistry.js';
// Local modules
import './radioSelect.js';
import './liveSearch.js';
import './preventDuplicateFormSubmissions.js';
import { ErrorBanner } from './errorBanner.js';
import './notifyModal.js';
import './timeoutPopup.js';
import { initCurrentYear } from './date.js';
import './sidenav.js';
import './validation.js';
import './scrollPosition.js';
initCurrentYear();
// NotifyModules
import './copyToClipboard.js';
import './enhancedTextbox.js';
import './fileUpload.js';
import './errorTracking.js';
import './templateFolderForm.js';
import './collapsibleCheckboxes.js';
import './radioSlider.js';
import './updateStatus.js';
import './main.js';
import './listEntry.js';
import './stick-to-window-when-scrolling.js';
import './totalMessagesChart.js';
import './activityChart.js';
import './job-polling.js';

View File

@@ -1,41 +0,0 @@
(function (window) {
// Set the target date (10 days before March 15th, 2024)
const targetDate = new Date("April 16, 2024 00:00:00").getTime();
// Set a const for the container to write a conditional statement and only run if it exists
const countdownContainer = document.getElementById("countdown-container");
// Function to update the countdown display
function updateCountdown() {
if (countdownContainer) {
const now = new Date().getTime();
const difference = targetDate - now;
// Time calculations for days only
const days = Math.floor(difference / (1000 * 60 * 60 * 24));
// Visibility logic
if (days < 0 || days > 10) {
// Hide if more than 10 days away OR if already past the date
countdownContainer.style.display = "none";
} else {
// Show if 10 days or less remaining
countdownContainer.style.display = "block";
document.getElementById("countdown").innerHTML = days + " days ";
}
}
}
// Expose the updateCountdown function to the outside world
window.updateCountdown = updateCountdown;
// Initial display update
updateCountdown();
// Update the countdown every second (inside the IIFE)
setInterval(updateCountdown, 1000);
})(window);

View File

@@ -0,0 +1,40 @@
const modules = new Map();
export function registerModule(name, ModuleClass) {
modules.set(name, ModuleClass);
window.NotifyModules = window.NotifyModules || {};
window.NotifyModules[name] = ModuleClass;
}
export function initModules() {
const moduleElements = document.querySelectorAll('[data-module]');
moduleElements.forEach(element => {
const moduleName = element.getAttribute('data-module');
const moduleStarted = element.getAttribute('data-module-started');
if (moduleStarted) return;
const ModuleClass = modules.get(moduleName);
if (!ModuleClass) {
console.warn(`Module "${moduleName}" not found in registry`);
return;
}
try {
const moduleInstance = new ModuleClass();
if (moduleInstance.start && typeof moduleInstance.start === 'function') {
moduleInstance.start(element);
}
element.setAttribute('data-module-started', 'true');
} catch (error) {
console.error(`Error initializing module "${moduleName}":`, error);
}
});
}
window.NotifyModules = window.NotifyModules || {};
window.NotifyModules.start = initModules;

View File

@@ -12,6 +12,11 @@
this.$liveRegionCounter = this.$form.find('.selection-counter'); this.$liveRegionCounter = this.$form.find('.selection-counter');
// Get single channel data from DOM (must happen after DOM is ready)
const addNewTemplateForm = document.querySelector('div[id=add_new_template_form]');
this.$singleNotificationChannel = addNewTemplateForm ? addNewTemplateForm.getAttribute("data-channel") : null;
this.$singleChannelService = addNewTemplateForm ? addNewTemplateForm.getAttribute("data-service") : null;
this.$liveRegionCounter.before(this.nothingSelectedButtons); this.$liveRegionCounter.before(this.nothingSelectedButtons);
this.$liveRegionCounter.before(this.itemsSelectedButtons); this.$liveRegionCounter.before(this.itemsSelectedButtons);
@@ -188,9 +193,6 @@
return changed; return changed;
}; };
this.$singleNotificationChannel = (document.querySelector('div[id=add_new_template_form]')).getAttribute("data-channel");
this.$singleChannelService = (document.querySelector('div[id=add_new_template_form]')).getAttribute("data-service");
this.actionButtonClicked = function(event) { this.actionButtonClicked = function(event) {
this.currentState = $(event.currentTarget).val(); this.currentState = $(event.currentTarget).val();
@@ -215,6 +217,12 @@
} }
} }
} else { } else {
// If state is not changing, this is a submit button - allow form submission
if (this.currentState === this._lastState) {
return true;
}
// Otherwise, show the form UI
event.preventDefault(); event.preventDefault();
if (this.stateChanged()) { if (this.stateChanged()) {
this.render(); this.render();

View File

@@ -12,7 +12,7 @@ window.NotifyModules.TimeoutPopup = window.NotifyModules.TimeoutPopup || {};
var difference = timeTillSessionEnd - now; var difference = timeTillSessionEnd - now;
var minutes = Math.floor((difference % (1000 * 60 * 60)) / (1000 * 60)); var minutes = Math.floor((difference % (1000 * 60 * 60)) / (1000 * 60));
var seconds = Math.floor((difference % (1000 * 60)) / 1000); var seconds = Math.floor((difference % (1000 * 60)) / 1000);
document.getElementById("timeLeft").innerHTML = + minutes + "m " + seconds + "s"; document.getElementById("timeLeft").textContent = minutes + "m " + seconds + "s";
showTimer(); showTimer();
document.getElementById("logOutTimer").addEventListener("click", signoutUser); document.getElementById("logOutTimer").addEventListener("click", signoutUser);
document.getElementById("extendSessionTimer").addEventListener("click", extendSession); document.getElementById("extendSessionTimer").addEventListener("click", extendSession);

View File

@@ -304,12 +304,19 @@ td.table-empty-message {
.usa-hint { .usa-hint {
display: block; display: block;
color: color('gray-70');
} }
.usa-table { .usa-table {
th { th {
border-bottom: 0 !important; border-bottom: 0 !important;
} }
td {
min-height: units(5);
div:empty::before {
content: '\00a0';
}
}
} }
.js-stick-at-bottom-when-scrolling { .js-stick-at-bottom-when-scrolling {
@@ -330,9 +337,32 @@ td.table-empty-message {
margin: 0; margin: 0;
border: 1px solid color('gray-cool-10'); border: 1px solid color('gray-cool-10');
padding: units(2); padding: units(2);
margin-bottom: units(2);
&:focus { &:focus {
outline-offset: units(1); outline-offset: units(1);
} }
.js-stick-at-bottom-when-scrolling {
display: block;
.usa-form-group {
margin-bottom: 0;
}
fieldset {
margin-bottom: 0;
}
.usa-fieldset {
margin-top: 0;
}
.radios-nested {
.usa-radio {
margin-top: units(1);
max-width: units('mobile');
}
}
.page-footer {
margin-top: units(3);
margin-bottom: 0;
}
}
} }
.spark-bar-bar { .spark-bar-bar {
@@ -732,10 +762,6 @@ details form {
// Login page // Login page
#countdown-container {
display: none; // Hide the countdown timer
}
.placeholder, .placeholder,
.placeholder-conditional { .placeholder-conditional {
background-color: #face00; background-color: #face00;

View File

@@ -9,6 +9,8 @@ in the form $setting: value,
@use "uswds-core" with ( @use "uswds-core" with (
$theme-font-type-sans: "public-sans", $theme-font-type-sans: "public-sans",
$theme-font-path: "../fonts",
$theme-image-path: "../img",
$theme-card-border-radius: 0, $theme-card-border-radius: 0,
$theme-card-gap: 4, $theme-card-gap: 4,
$theme-card-border-width: 2px, $theme-card-border-width: 2px,

View File

@@ -53,7 +53,6 @@ $path: '/static/images/';
@import 'components/list-entry'; @import 'components/list-entry';
@import 'components/live-search'; @import 'components/live-search';
@import 'components/stick-at-top-when-scrolling'; @import 'components/stick-at-top-when-scrolling';
@import 'components/fullscreen-table';
@import 'components/conditional-radios'; @import 'components/conditional-radios';
@import 'components/vendor/breadcrumbs'; @import 'components/vendor/breadcrumbs';
@import 'components/vendor/responsive-embed'; @import 'components/vendor/responsive-embed';

View File

@@ -1,5 +1,5 @@
{% macro mapping_table(caption='', field_headings=[], field_headings_visible=True, caption_visible=True, equal_length=False, border_visible=False) -%} {% macro mapping_table(caption='', field_headings=[], field_headings_visible=True, caption_visible=True, equal_length=False, border_visible=False, striped=False) -%}
<table class="usa-table{{' usa-table--borderless' if not border_visible}} width-full"> <table class="usa-table{{' usa-table--borderless' if not border_visible}}{{' usa-table--striped' if striped}} width-full">
<caption class="font-body-lg table-heading{{ ' usa-sr-only' if not caption_visible}}"> <caption class="font-body-lg table-heading{{ ' usa-sr-only' if not caption_visible}}">
{{ caption }} {{ caption }}
</caption> </caption>
@@ -22,10 +22,10 @@
</table> </table>
{%- endmacro %} {%- endmacro %}
{% macro list_table(items, caption='', empty_message='', field_headings=[], field_headings_visible=True, caption_visible=True, equal_length=False, border_visible=False) -%} {% macro list_table(items, caption='', empty_message='', field_headings=[], field_headings_visible=True, caption_visible=True, equal_length=False, border_visible=False, striped=False) -%}
{% set parent_caller = caller %} {% set parent_caller = caller %}
{% call mapping_table(caption, field_headings, field_headings_visible, caption_visible, equal_length, border_visible) %} {% call mapping_table(caption, field_headings, field_headings_visible, caption_visible, equal_length, border_visible, striped) %}
{% for item in items %} {% for item in items %}
{% call row(item.id) %} {% call row(item.id) %}
{{ parent_caller(item, loop.index + 1) }} {{ parent_caller(item, loop.index + 1) }}

View File

@@ -165,7 +165,7 @@ recipients.column_headers %}
<h2 class="font-body-lg" id="file-preview">{{ original_file_name }}</h2> <h2 class="font-body-lg" id="file-preview">{{ original_file_name }}</h2>
<div class="fullscreen-content" data-module="fullscreen-table"> <div class="spreadsheet usa-table-container--scrollable" tabindex="0">
{% call(item, row_number) list_table( {% call(item, row_number) list_table(
recipients.displayed_rows, recipients.displayed_rows,
caption=original_file_name, caption=original_file_name,

View File

@@ -57,7 +57,7 @@
</div> </div>
</div> </div>
<div class="fullscreen-content" data-module="fullscreen-table"> <div class="spreadsheet usa-table-container--scrollable" tabindex="0">
{% call(item, row_number) mapping_table( {% call(item, row_number) mapping_table(
caption="Errors in " + original_file_name, caption="Errors in " + original_file_name,
caption_visible=False, caption_visible=False,

View File

@@ -75,7 +75,7 @@
<h2 class="font-body-lg">A spreadsheet is available to use</h2> <h2 class="font-body-lg">A spreadsheet is available to use</h2>
<div class="spreadsheet" data-module="fullscreen-table"> <div class="spreadsheet usa-table-container--scrollable" tabindex="0">
{% call(item, row_number) list_table( {% call(item, row_number) list_table(
example, example,
caption="Example spreadsheet", caption="Example spreadsheet",

View File

@@ -39,16 +39,9 @@
{% call row() %} {% call row() %}
{{ text_field('Send text messages') }} {{ text_field('Send text messages') }}
{{ boolean_field('sms' in current_service.permissions) }} {{ boolean_field('sms' in current_service.permissions) }}
{# {{ edit_field( {% call field(align='right') %}
'Change', <span class="table-field-status-default">Set automatically</span>
url_for( {% endcall %}
'.service_set_channel',
service_id=current_service.id,
channel='sms'
),
permissions=[ServicePermission.MANAGE_SERVICE],
suffix='your settings for sending text messages',
)}} #}
{% endcall %} {% endcall %}
{% if current_user.platform_admin %} {% if current_user.platform_admin %}
@@ -218,6 +211,9 @@
{{ text_field('Live')}} {{ text_field('Live')}}
{% if current_service.trial_mode and not current_service.organization_id %} {% if current_service.trial_mode and not current_service.organization_id %}
{{ text_field('No (organization must be set first)') }} {{ text_field('No (organization must be set first)') }}
{% call field(align='right') %}
<span class="table-field-status-default">Set automatically</span>
{% endcall %}
{% else %} {% else %}
{{ boolean_field(not current_service.trial_mode) }} {{ boolean_field(not current_service.trial_mode) }}
{{ edit_field('Change', url_for('.service_switch_live', service_id=current_service.id), suffix='service status') }} {{ edit_field('Change', url_for('.service_switch_live', service_id=current_service.id), suffix='service status') }}

View File

@@ -1,3 +1,9 @@
module.exports = { module.exports = {
presets: ['@babel/preset-env'], presets: [
['@babel/preset-env', {
targets: {
node: 'current'
}
}]
]
}; };

Binary file not shown.

Before

Width:  |  Height:  |  Size: 125 KiB

After

Width:  |  Height:  |  Size: 125 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 120 KiB

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 340 KiB

After

Width:  |  Height:  |  Size: 339 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 90 KiB

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 119 KiB

After

Width:  |  Height:  |  Size: 119 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 160 KiB

After

Width:  |  Height:  |  Size: 160 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 104 KiB

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 393 KiB

After

Width:  |  Height:  |  Size: 524 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 220 KiB

After

Width:  |  Height:  |  Size: 220 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 112 KiB

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 126 KiB

After

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 160 KiB

After

Width:  |  Height:  |  Size: 141 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 129 KiB

After

Width:  |  Height:  |  Size: 128 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 MiB

After

Width:  |  Height:  |  Size: 6.6 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 146 KiB

After

Width:  |  Height:  |  Size: 198 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 211 KiB

After

Width:  |  Height:  |  Size: 214 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 211 KiB

After

Width:  |  Height:  |  Size: 214 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 172 KiB

After

Width:  |  Height:  |  Size: 161 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 393 KiB

After

Width:  |  Height:  |  Size: 524 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 155 KiB

After

Width:  |  Height:  |  Size: 197 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 124 KiB

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 393 KiB

After

Width:  |  Height:  |  Size: 524 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 177 KiB

After

Width:  |  Height:  |  Size: 177 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 166 KiB

After

Width:  |  Height:  |  Size: 166 KiB

View File

@@ -28,7 +28,6 @@ const javascripts = () => {
paths.src + 'javascripts/errorBanner.js', paths.src + 'javascripts/errorBanner.js',
paths.src + 'javascripts/notifyModal.js', paths.src + 'javascripts/notifyModal.js',
paths.src + 'javascripts/date.js', paths.src + 'javascripts/date.js',
paths.src + 'javascripts/loginAlert.js',
paths.src + 'javascripts/sidenav.js', paths.src + 'javascripts/sidenav.js',
paths.src + 'javascripts/validation.js', paths.src + 'javascripts/validation.js',
paths.src + 'javascripts/scrollPosition.js', paths.src + 'javascripts/scrollPosition.js',

5382
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -3,14 +3,19 @@
"version": "0.0.1", "version": "0.0.1",
"description": "Admin front end for Notify", "description": "Admin front end for Notify",
"engines": { "engines": {
"node": ">=10.15.3" "node": ">=22.0.0"
}, },
"scripts": { "scripts": {
"lint": "gulp lint", "lint": "jshint app/assets/javascripts",
"test": "jest --config tests/javascripts/jest.config.js tests/javascripts", "test": "jest --config tests/javascripts/jest.config.js tests/javascripts",
"test-watch": "jest --watch --config tests/javascripts/jest.config.js tests/javascripts", "test-watch": "jest --watch --config tests/javascripts/jest.config.js tests/javascripts",
"build": "gulp", "build": "NODE_ENV=production npx webpack --config webpack.config.js",
"watch": "gulp watch", "build:dev": "npx webpack --config webpack.config.js",
"watch": "npx webpack --watch --config webpack.config.js",
"backstop:test": "npx backstop test --configPath=backstop.config.js",
"backstop:reference": "npx backstop reference --configPath=backstop.config.js",
"backstop:approve": "npx backstop approve --configPath=backstop.config.js",
"backstop:open": "npx backstop openReport --configPath=backstop.config.js",
"audit": "better-npm-audit audit --production --level low", "audit": "better-npm-audit audit --production --level low",
"pa11y-ci": "pa11y-ci" "pa11y-ci": "pa11y-ci"
}, },
@@ -31,41 +36,34 @@
"@uswds/uswds": "^3.13.0", "@uswds/uswds": "^3.13.0",
"cbor-js": "0.1.0", "cbor-js": "0.1.0",
"d3": "^7.9.0", "d3": "^7.9.0",
"gulp-merge": "^0.1.1",
"jquery": "3.7.1", "jquery": "3.7.1",
"playwright": "^1.56.0", "playwright": "^1.56.0",
"python": "^0.0.4", "python": "^0.0.4",
"query-command-supported": "1.0.0", "query-command-supported": "1.0.0",
"sass-embedded": "^1.93.2", "sass-embedded": "^1.93.2",
"textarea-caret": "3.1.0", "textarea-caret": "3.1.0"
"vinyl-buffer": "^1.0.1",
"vinyl-source-stream": "^2.0.0"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.28.4", "@babel/core": "^7.28.4",
"@babel/preset-env": "^7.28.3", "@babel/preset-env": "^7.28.3",
"@uswds/compile": "^1.3.1", "autoprefixer": "^10.4.21",
"babel-loader": "^10.0.0",
"backstopjs": "^6.3.25", "backstopjs": "^6.3.25",
"better-npm-audit": "^3.11.0", "better-npm-audit": "^3.11.0",
"gulp": "^5.0.1", "copy-webpack-plugin": "^13.0.1",
"gulp-add-src": "^1.0.0", "css-loader": "^7.1.2",
"gulp-babel": "8.0.0",
"gulp-clean-css": "4.3.0",
"gulp-concat": "^2.6.1",
"gulp-if": "^3.0.0",
"gulp-include": "2.4.1",
"gulp-jshint": "2.1.0",
"gulp-load-plugins": "^2.0.8",
"gulp-prettyerror": "2.0.0",
"gulp-uglify": "3.0.2",
"jest": "^30.2.0", "jest": "^30.2.0",
"jest-each": "^30.1.0", "jest-each": "^30.1.0",
"jest-environment-jsdom": "^30.2.0", "jest-environment-jsdom": "^30.2.0",
"jshint": "2.13.6", "jshint": "2.13.6",
"jshint-stylish": "2.2.1", "jshint-stylish": "2.2.1",
"merge-stream": "^2.0.0", "merge-stream": "^2.0.0",
"rollup": "^4.52.4", "mini-css-extract-plugin": "^2.9.4",
"rollup-plugin-commonjs": "10.1.0", "postcss-loader": "^8.2.0",
"rollup-plugin-node-resolve": "5.2.0" "sass": "^1.93.2",
"sass-loader": "^16.0.5",
"terser-webpack-plugin": "^5.3.14",
"webpack": "^5.102.1",
"webpack-cli": "^6.0.1"
} }
} }

View File

@@ -53,7 +53,7 @@ def _mock_get_service_settings_page_common(
[ [
"", "",
"Service name Test Service Change service name", "Service name Test Service Change service name",
"Send text messages On", "Send text messages On Set automatically",
"Start text messages with service name On Change your settings " "Start text messages with service name On Change your settings "
"for starting text messages with service name", "for starting text messages with service name",
], ],
@@ -63,7 +63,7 @@ def _mock_get_service_settings_page_common(
[ [
"", "",
"Service name Test Service Change service name", "Service name Test Service Change service name",
"Send text messages On", "Send text messages On Set automatically",
"Text message senders (Only visible to Platform Admins) USGOV Manage text message senders", "Text message senders (Only visible to Platform Admins) USGOV Manage text message senders",
"Start text messages with service name On Change your settings " "Start text messages with service name On Change your settings "
"for starting text messages with service name", "for starting text messages with service name",
@@ -191,7 +191,7 @@ def test_send_files_by_email_row_on_settings_page(
["email", "sms", "international_sms"], ["email", "sms", "international_sms"],
[ [
"Service name service one Change service name", "Service name service one Change service name",
"Send text messages On", "Send text messages On Set automatically",
"Start text messages with service name On Change your settings " "Start text messages with service name On Change your settings "
"for starting text messages with service name", "for starting text messages with service name",
], ],
@@ -200,7 +200,7 @@ def test_send_files_by_email_row_on_settings_page(
["email", "sms", ServicePermission.EMAIL_AUTH], ["email", "sms", ServicePermission.EMAIL_AUTH],
[ [
"Service name service one Change service name", "Service name service one Change service name",
"Send text messages On", "Send text messages On Set automatically",
"Start text messages with service name On Change your settings " "Start text messages with service name On Change your settings "
"for starting text messages with service name", "for starting text messages with service name",
], ],

View File

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

View File

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

View File

@@ -1,5 +1,6 @@
beforeAll(() => { beforeAll(() => {
require('../../app/assets/javascripts/errorBanner.js') // ErrorBanner module sets window.NotifyModules.ErrorBanner for backward compatibility
require('../../app/assets/javascripts/errorBanner.js');
}); });
afterAll(() => { afterAll(() => {
@@ -34,4 +35,10 @@ describe("Error Banner", () => {
expect(document.querySelector('.banner-dangerous').classList).not.toContain('display-none') 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 form;
let fileUpload; let fileUpload;
let uploadControl;
beforeEach(() => { 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', url: 'https://beta.notify.gov',
}, },
transform: { transform: {
'^.+\\.js$': 'babel-jest', '^.+\\.js$': ['babel-jest', { rootMode: 'upward' }],
}, },
transformIgnorePatterns: [ 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)/)' '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' 'va.gov'
]; ];
let inputList; let inputList;
let name_value_pairs;
let fields;
const triggerEvent = (el, evtType) => { const triggerEvent = (el, evtType) => {
const evt = new Event(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]; 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 originalOptionsForAllCategories;
let originalOptionsForCategory;
let selectedOption;
let categoryButtons;
const getDataFromOption = (option) => { const getDataFromOption = (option) => {
return { return {

View File

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

View File

@@ -220,7 +220,7 @@ describe('TemplateFolderForm', () => {
}); });
function getTemplateFolderCheckboxes () { function getTemplateFolderCheckboxes () {
return templateFolderForm.querySelectorAll('.usa-checkbox__label'); return templateFolderForm.querySelectorAll('.usa-checkbox__input');
}; };
function getVisibleCounter () { function getVisibleCounter () {
@@ -832,6 +832,9 @@ describe('TemplateFolderForm', () => {
describe("Additional selection counter scenarios", () => { describe("Additional selection counter scenarios", () => {
let templateFolderCheckboxes; let templateFolderCheckboxes;
let visibleCounterText;
let hiddenCounterText;
let formControls;
beforeEach(() => { beforeEach(() => {
@@ -855,19 +858,40 @@ describe('TemplateFolderForm', () => {
test("the content of both visible and hidden counters should match", () => { 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[1], 'click');
helpers.triggerEvent(templateFolderCheckboxes[2], 'click'); helpers.triggerEvent(templateFolderCheckboxes[2], 'click');
visibleCounterText = getVisibleCounter().textContent.trim();
hiddenCounterText = getHiddenCounter().textContent.trim();
expect(visibleCounterText).toEqual(hiddenCounterText); expect(visibleCounterText).toEqual(hiddenCounterText);
}); });
test("the content of the counter should reflect the selection", () => { test("the content of the counter should reflect the selection", () => {
helpers.triggerEvent(templateFolderCheckboxes[1], 'click'); // Re-query checkboxes to ensure we have fresh references
helpers.triggerEvent(templateFolderCheckboxes[2], 'click'); 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", () => { 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'); helpers.triggerEvent(templateFolderCheckboxes[0], 'click');
visibleCounterText = getVisibleCounter().textContent.trim();
hiddenCounterText = getHiddenCounter().textContent.trim();
expect(visibleCounterText).toEqual(hiddenCounterText); expect(visibleCounterText).toEqual(hiddenCounterText);
}); });
test("the content of the counter should reflect the selection", () => { 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"); 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.");
});
}); });

162
webpack.config.js Normal file
View File

@@ -0,0 +1,162 @@
const path = require('path');
const webpack = require('webpack');
const TerserPlugin = require('terser-webpack-plugin');
const CopyPlugin = require('copy-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const isProduction = process.env.NODE_ENV === 'production';
module.exports = {
mode: isProduction ? 'production' : 'development',
entry: {
all: path.resolve(__dirname, 'app/assets/javascripts/index.js'),
styles: path.resolve(__dirname, 'app/assets/sass/uswds/styles.scss')
},
output: {
path: path.resolve(__dirname, 'app/static/javascripts'),
filename: '[name].js',
clean: true
},
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env']
}
}
},
{
test: /\.s[ac]ss$/i,
use: [
MiniCssExtractPlugin.loader,
{
loader: 'css-loader',
options: {
url: false
}
},
{
loader: 'postcss-loader',
options: {
postcssOptions: {
plugins: [require('autoprefixer')]
}
}
},
{
loader: 'sass-loader',
options: {
sassOptions: {
loadPaths: [
path.resolve(__dirname, 'app/assets/sass/uswds'),
path.resolve(__dirname, 'node_modules/@uswds'),
path.resolve(__dirname, 'node_modules/@uswds/uswds/packages'),
path.resolve(__dirname, 'node_modules/@uswds/uswds/dist/scss/stylesheets/packages')
]
}
}
}
]
}
]
},
resolve: {
extensions: ['.js'],
alias: {
'@javascripts': path.resolve(__dirname, 'app/assets/javascripts'),
'@modules': path.resolve(__dirname, 'app/assets/javascripts/modules')
}
},
plugins: [
new webpack.ProvidePlugin({
$: 'jquery',
jQuery: 'jquery',
'window.jQuery': 'jquery'
}),
new MiniCssExtractPlugin({
filename: '../css/styles.css'
}),
new CopyPlugin({
patterns: [
{
from: path.resolve(__dirname, 'app/assets/images'),
to: path.resolve(__dirname, 'app/static/images'),
noErrorOnMissing: true
},
{
from: path.resolve(__dirname, 'app/assets/pdf'),
to: path.resolve(__dirname, 'app/static/pdf'),
noErrorOnMissing: true
},
{
from: path.resolve(__dirname, 'app/assets/js/gtm_head.js'),
to: path.resolve(__dirname, 'app/static/js/gtm_head.js'),
noErrorOnMissing: true
},
{
from: path.resolve(__dirname, 'app/assets/js/setTimezone.js'),
to: path.resolve(__dirname, 'app/static/js/setTimezone.js'),
noErrorOnMissing: true
},
{
from: path.resolve(__dirname, 'node_modules/@uswds/uswds/dist/js/uswds.min.js'),
to: path.resolve(__dirname, 'app/static/js/uswds.min.js')
},
{
from: path.resolve(__dirname, 'node_modules/@uswds/uswds/dist/fonts'),
to: path.resolve(__dirname, 'app/static/fonts')
},
{
from: path.resolve(__dirname, 'node_modules/@uswds/uswds/dist/img'),
to: path.resolve(__dirname, 'app/static/img')
}
]
})
],
optimization: {
minimize: isProduction,
minimizer: [
new TerserPlugin({
terserOptions: {
compress: {
drop_console: isProduction,
drop_debugger: true,
pure_funcs: isProduction ? ['console.info', 'console.debug', 'console.warn', 'console.error'] : [],
},
mangle: true,
format: {
comments: false,
},
},
extractComments: false,
}),
],
splitChunks: false,
},
devtool: isProduction ? false : 'source-map',
stats: {
colors: true,
modules: false,
children: false,
chunks: false,
chunkModules: false
},
performance: {
hints: isProduction ? 'warning' : false,
maxEntrypointSize: 512000,
maxAssetSize: 512000,
}
};