Merge branch 'main' of https://github.com/GSA/notifications-admin into 2123-back-button-context

This commit is contained in:
Jonathan Bobel
2025-04-25 10:27:25 -04:00
30 changed files with 553 additions and 192 deletions
+1
View File
@@ -14,6 +14,7 @@
*.XLSX *.XLSX
## Non user files allowed to be commited ## Non user files allowed to be commited
!app/assets/pdf/wa_case_study.pdf
!app/assets/pdf/best-practices-for-texting-the-public.pdf !app/assets/pdf/best-practices-for-texting-the-public.pdf
!app/assets/pdf/investing-in-notifications-tts-public-benefits-studio-decision-memo.pdf !app/assets/pdf/investing-in-notifications-tts-public-benefits-studio-decision-memo.pdf
!app/assets/pdf/best-practices-section-outline.pdf !app/assets/pdf/best-practices-section-outline.pdf
+59 -24
View File
@@ -1,34 +1,69 @@
function announceUploadStatusFromElement() {
const srRegion = document.getElementById('upload-status-live');
const success = document.getElementById('upload-success');
const error = document.getElementById('upload-error');
if (!srRegion) return;
const message = error?.textContent || success?.textContent;
if (message) {
srRegion.textContent = '';
setTimeout(() => {
srRegion.textContent = message + '\u00A0'; // add a non-breaking space
srRegion.focus(); // Optional
}, 300);
}
}
// Exported for use in tests
function initUploadStatusAnnouncer() {
document.addEventListener('DOMContentLoaded', () => {
announceUploadStatusFromElement();
});
}
(function(Modules) { (function(Modules) {
"use strict"; "use strict";
Modules.FileUpload = function() { Modules.FileUpload = function() {
this.submit = () => this.$form.trigger('submit'); this.submit = () => this.$form.trigger('submit');
this.showCancelButton = () => $('.file-upload-button', this.$form).replaceWith(` this.showCancelButton = () => {
<a href="" class='usa-button usa-button--secondary'>Cancel upload</a> $('.file-upload-button', this.$form).replaceWith(`
`); <button class='usa-button uploading-button' aria-disabled="true" tabindex="0">
Uploading<span class="dot-anim" aria-hidden="true"></span>
this.start = function(component) { </button>
`);
this.$form = $(component);
// The label gets styled like a button and is used to hide the native file upload control. This is so that
// users see a button that looks like the others on the site.
this.$form.find('label.file-upload-button').addClass('usa-button margin-bottom-1').attr( {role: 'button', tabindex: '0'} );
// Clear the form if the user navigates back to the page
$(window).on("pageshow", () => this.$form[0].reset());
// Need to put the event on the container, not the input for it to work properly
this.$form.on(
'change', '.file-upload-field',
() => this.submit() && this.showCancelButton()
);
}; };
}; this.start = function(component) {
this.$form = $(component);
this.$form.on('click', '[data-module="upload-trigger"]', function () {
const inputId = $(this).data('file-input-id');
const fileInput = document.getElementById(inputId);
if (fileInput) fileInput.click();
});
$(window).on("pageshow", () => this.$form[0].reset());
this.$form.on('change', '.file-upload-field', () => {
this.submit();
this.showCancelButton();
});
};
};
})(window.GOVUK.Modules); })(window.GOVUK.Modules);
if (typeof module !== 'undefined' && module.exports) {
module.exports = {
announceUploadStatusFromElement,
initUploadStatusAnnouncer
};
}
if (typeof window !== 'undefined') {
initUploadStatusAnnouncer();
}
+2 -3
View File
@@ -22,7 +22,6 @@
this.$scrollableTable this.$scrollableTable
.on('scroll', this.toggleShadows) .on('scroll', this.toggleShadows)
.on('scroll', this.maintainHeight) .on('scroll', this.maintainHeight)
.on('focus blur', () => this.$component.toggleClass('js-focus-style'));
if ( if (
window.GOVUK.stickAtBottomWhenScrolling && window.GOVUK.stickAtBottomWhenScrolling &&
@@ -37,11 +36,11 @@
this.insertShims = () => { this.insertShims = () => {
const attributesForFocus = 'role aria-labelledby tabindex'; const attributesForFocus = 'role aria-labelledby';
let captionId = this.$table.find('caption').text().toLowerCase().replace(/[^A-Za-z]+/g, ''); let captionId = this.$table.find('caption').text().toLowerCase().replace(/[^A-Za-z]+/g, '');
this.$table.find('caption').attr('id', captionId); this.$table.find('caption').attr('id', captionId);
this.$table.wrap(`<div class="fullscreen-scrollable-table" role="region" aria-labelledby="${captionId}" tabindex="0"/>`); this.$table.wrap(`<div class="fullscreen-scrollable-table" role="region" aria-labelledby="${captionId}"/>`);
this.$component this.$component
.append( .append(
Binary file not shown.
@@ -344,3 +344,17 @@ h2.recipient-list {
} }
} }
} }
// Button ellipses loading
.dot-anim::after {
content: '.';
animation: dotPulse 1.5s steps(3, end) infinite;
}
@keyframes dotPulse {
0% { content: ''; }
33% { content: '.'; }
66% { content: '..'; }
100% { content: '...'; }
}
@@ -289,6 +289,7 @@ td.table-empty-message {
margin-top: units(1); margin-top: units(1);
width: 100%; width: 100%;
border: 1px solid color('gray-60'); border: 1px solid color('gray-60');
height: 40px;
} }
} }
+1 -1
View File
@@ -1,5 +1,5 @@
@forward "uswds-theme"; @forward "uswds-theme";
@forward "uswds"; @forward "uswds";
@forward "uswds-theme-custom-styles"; @forward "uswds-theme-custom-styles";
@forward "legacy-styles"; @forward "main";
@forward "data-visualization"; @forward "data-visualization";
+1
View File
@@ -481,6 +481,7 @@ def send_one_off_step(service_id, template_id, step_index):
), ),
template=template, template=template,
form=form, form=form,
current_placeholder=current_placeholder,
skip_link=get_skip_link(step_index, template), skip_link=get_skip_link(step_index, template),
back_link=back_link, back_link=back_link,
link_to_upload=( link_to_upload=(
+6
View File
@@ -11,6 +11,12 @@
{% include "components/head.html" %} {% include "components/head.html" %}
</head> </head>
<body class="usa-template__body {{ bodyClasses }}"> <body class="usa-template__body {{ bodyClasses }}">
<div id="upload-status-live"
class="usa-sr-only"
aria-live="assertive"
tabindex="-1"
role="status">
</div>
<script nonce="{{ csp_nonce() }}">document.body.className = ((document.body.className) ? document.body.className + ' js-enabled' : 'js-enabled');</script> <script nonce="{{ csp_nonce() }}">document.body.className = ((document.body.className) ? document.body.className + ' js-enabled' : 'js-enabled');</script>
{% block bodyStart %} {% block bodyStart %}
{% block extra_javascripts_before_body %} {% block extra_javascripts_before_body %}
-4
View File
@@ -4,10 +4,6 @@
{% macro banner(body, type=None, with_tick=False, delete_button=None, subhead=None, context=None, action=None, id=None, thing=None) %} {% macro banner(body, type=None, with_tick=False, delete_button=None, subhead=None, context=None, action=None, id=None, thing=None) %}
<div <div
class='banner{% if type %}-{{ type }}{% endif %}{% if with_tick %}-with-tick{% endif %}' class='banner{% if type %}-{{ type }}{% endif %}{% if with_tick %}-with-tick{% endif %}'
{% if type == 'dangerous' %}
role='group'
tabindex='-1'
{% endif %}
{% if id %} {% if id %}
id={{ id }} id={{ id }}
{% endif %} {% endif %}
+8 -4
View File
@@ -27,17 +27,21 @@
</label> </label>
{{ field(**{ {{ field(**{
'class': 'file-upload-field', 'class': 'file-upload-field',
'id': field.name,
'accept': allowed_file_extensions|format_list_items('.{item}')|join(',')|e 'accept': allowed_file_extensions|format_list_items('.{item}')|join(',')|e
}) }} }) }}
<label class="file-upload-button usa-button" for="{{ field.name }}"> <button type="button"
{{ button_text }} class="usa-button file-upload-button"
</label> data-module="upload-trigger"
data-file-input-id="{{ field.name }}">
{{ button_text }}
</button>
{% if alternate_link and alternate_link_text %} {% if alternate_link and alternate_link_text %}
<span class="file-upload-alternate-link"> <span class="file-upload-alternate-link">
or <a class="usa-link" href="{{ alternate_link }}">{{ alternate_link_text }}</a> or <a class="usa-link" href="{{ alternate_link }}">{{ alternate_link_text }}</a>
</span> </span>
{% endif %} {% endif %}
<label class="file-upload-filename" for="{{ field.name }}"></label> <span class="file-upload-filename" aria-live="polite"></span>
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" /> <input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
{{ usaButton({ "text": "Submit", "classes": "file-upload-submit" }) }} {{ usaButton({ "text": "Submit", "classes": "file-upload-submit" }) }}
</form> </form>
+54 -31
View File
@@ -5,7 +5,6 @@
highlight_placeholders=False, highlight_placeholders=False,
autofocus=False, autofocus=False,
autosize=False, autosize=False,
colour_preview=False,
help_link=None, help_link=None,
help_link_text=None, help_link_text=None,
width='2-3', width='2-3',
@@ -13,58 +12,82 @@
safe_error_message=False, safe_error_message=False,
rows=8, rows=8,
extra_form_group_classes='', extra_form_group_classes='',
placeholder='' placeholder='',
input_id=None
) %} ) %}
{% set field_id = kwargs.id if kwargs.id is defined else field.id %}
{% set described_by = '' %}
{% if hint %}{% set described_by = described_by + field_id ~ '-hint' %}{% endif %}
{% if field.errors %}
{% if described_by %}{% set described_by = described_by + ' ' %}{% endif %}
{% set described_by = described_by + field_id ~ '-error' %}
{% endif %}
<div <div
class="usa-form-group{% if field.errors %} usa-form-group--error{% endif %} {{ extra_form_group_classes }}" class="usa-form-group{% if field.errors %} usa-form-group--error{% endif %} {{ extra_form_group_classes }}"
data-module="{% if autofocus %}autofocus{% elif colour_preview %}colour-preview{% endif %}" data-module="{% if autofocus %}autofocus{% elif colour_preview %}colour-preview{% endif %}"
> >
<label class="usa-label" for="{{ field.name }}"> <label class="usa-label" for="{{ field_id }}">
{% if label %} {{ label or field.label.text }}
{{ label }}
{% else %}
{{ field.label.text }}
{% endif %}
</label> </label>
{% if hint %} {% if hint %}
<div class="usa-hint"> <div class="usa-hint" id="{{ field_id }}-hint">
{{ hint }} {{ hint }}
</div> </div>
{% endif %} {% endif %}
{% if field.errors %} {% if field.errors %}
<span id="{{ field.name}}-error" class="error-message usa-error-message" data-module="track-error" data-error-type="{{ field.errors[0] }}" data-error-label="{{ field.name }}" tabindex="-1" aria-live="assertive" role="alert"> <span id="{{ field_id }}-error" class="error-message usa-error-message" data-module="track-error" data-error-type="{{ field.errors[0] }}" data-error-label="{{ field.name }}" tabindex="-1" aria-live="assertive" role="alert">
<span class="usa-sr-only">Error:</span> <span class="usa-sr-only">Error:</span>
{% if not safe_error_message %}{{ field.errors[0] }}{% else %}{{ field.errors[0]|safe }}{% endif %} {% if not safe_error_message %}
{{ field.errors[0] }}
{% else %}
{{ field.errors[0] | safe }}
{% endif %}
</span> </span>
{% endif %} {% endif %}
{%
if highlight_placeholders or autosize {% if highlight_placeholders or autosize %}
%}
{% set field_class = 'usa-textarea form-control-{} textbox-highlight-textbox'.format(width) %} {% set field_class = 'usa-textarea form-control-{} textbox-highlight-textbox'.format(width) %}
{% else %} {% else %}
{% set field_class = 'form-control-{} {}'.format(width, 'textbox-right-aligned' if suffix else '') %} {% set field_class = 'usa-input' %}
{% endif %} {% endif %}
{% {% set field_class = 'form-control ' ~ field_class ~ (' form-control-error' if field.errors else '') %}
set field_class = 'form-control ' + field_class + (
' form-control-error' if field.errors else '' {% if autosize or highlight_placeholders %}
) <textarea
%} id="{{ field_id }}"
{{ field( name="{{ field.name }}"
class=field_class, class="{{ field_class }}"
data_module='enhanced-textbox' if highlight_placeholders or autosize else '', rows="{{ rows }}"
data_highlight_placeholders='true' if highlight_placeholders else 'false', placeholder="{{ placeholder }}"
rows=rows|string, aria-describedby="{{ described_by.strip() }}"
placeholder=placeholder, {% if field.flags.required %}required{% endif %}
aria_describedby=field.name+"-error", data-module="enhanced-textbox"
required='required' if required else None, data-highlight-placeholders="true"
**kwargs >{{ field._value() }}</textarea>
) }} {% else %}
{% set input_type = 'tel' if 'phone' in field.label.text else 'text' %}
<input
id="{{ field_id }}"
name="{{ field.name }}"
type="{{ input_type }}"
class="{{ field_class }}"
value="{{ field._value() }}"
placeholder="{{ placeholder }}"
aria-describedby="{{ described_by.strip() }}"
{% if field.flags.required %}required{% endif %}
/>
{% endif %}
{% if suffix %} {% if suffix %}
<span>{{ suffix }}</span> <span>{{ suffix }}</span>
{% endif %} {% endif %}
{% if help_link and help_link_text %} {% if help_link and help_link_text %}
<p class="textbox-help-link"> <p class="textbox-help-link">
<a class="usa-link" href='{{ help_link }}'>{{ help_link_text }}</a> <a class="usa-link" href="{{ help_link }}">{{ help_link_text }}</a>
</p> </p>
{% endif %} {% endif %}
</div> </div>
+5 -1
View File
@@ -20,7 +20,11 @@ Error
{% block maincolumn_content %} {% block maincolumn_content %}
<div class=""> <div class="">
{# Alert for users of AT #}
<span id="upload-error" class="usa-sr-only">File upload failed</span>
{% call banner_wrapper(type='dangerous') %} {% call banner_wrapper(type='dangerous') %}
{% if recipients.too_many_rows %} {% if recipients.too_many_rows %}
@@ -150,7 +154,7 @@ Error
) }} ) }}
{% endif %} {% endif %}
</div> </div>
<a href="#content" class="usa-link back-to-top-link">Back to top</a> <a href="#content" class="usa-link back-to-top-link display-block margin-top-2">Back to top</a>
</div> </div>
{% endif %} {% endif %}
+2
View File
@@ -21,6 +21,8 @@
{{ template|string }} {{ template|string }}
<div class="bottom-gutter-3-2"> <div class="bottom-gutter-3-2">
{# Alert for users of AT #}
<span id="upload-success" class="usa-sr-only">File uploaded successfully</span>
<form method="post" enctype="multipart/form-data" action="{{url_for('main.preview_job', service_id=current_service.id, template_id=template_id, upload_id=upload_id)}}" class='page-footer'> <form method="post" enctype="multipart/form-data" action="{{url_for('main.preview_job', service_id=current_service.id, template_id=template_id, upload_id=upload_id)}}" class='page-footer'>
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" /> <input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
{% if choose_time_form %} {% if choose_time_form %}
@@ -16,6 +16,8 @@
{% block maincolumn_content %} {% block maincolumn_content %}
<div class=""> <div class="">
{# Alert for users of AT #}
<span id="upload-failed" class="usa-sr-only">File upload failed</span>
{% call banner_wrapper(type='dangerous') %} {% call banner_wrapper(type='dangerous') %}
{% if row_errors|length == 1 %} {% if row_errors|length == 1 %}
<div class="usa-alert usa-alert--error" role="alert"> <div class="usa-alert usa-alert--error" role="alert">
+14 -4
View File
@@ -1,6 +1,7 @@
{% extends "withnav_template.html" %} {% extends "withnav_template.html" %}
{% from "components/page-header.html" import page_header %} {% from "components/page-header.html" import page_header %}
{% from "components/page-footer.html" import page_footer %} {% from "components/page-footer.html" import page_footer %}
{% from "components/textbox.html" import textbox %}
{% from "components/form.html" import form_wrapper %} {% from "components/form.html" import form_wrapper %}
{% from "components/components/back-link/macro.njk" import usaBackLink %} {% from "components/components/back-link/macro.njk" import usaBackLink %}
@@ -38,10 +39,19 @@
data_force_focus=True data_force_focus=True
) %} ) %}
<div class="grid-row"> <div class="grid-row">
{% set extra_class = "extra-tracking" if form.placeholder_value.label.text == "phone number" else "" %} {% set is_phone_number = form.placeholder_value.label.text == "phone number" %}
{% set placeholder_id = "phone number" if form.placeholder_value.label.text == "phone number" else "" %} {% set label_text = "Country code and phone number" if current_placeholder == "phone number" else current_placeholder %}
<div class="grid-col-12 {% if form.placeholder_value.label.text == 'phone number' %}extra-tracking{% endif %}"> {% set hint_text = "For example: +18885551234" if is_phone_number else None %}
{{ form.placeholder_value(param_extensions={"classes": "", "id": "phone-number"}) }} {% set extra_class = "extra-tracking" if is_phone_number else "" %}
<div class="grid-col-12 {{ extra_class }}">
{{ textbox(
form.placeholder_value,
label=label_text,
hint=hint_text,
extra_form_group_classes=extra_class,
param_extensions={"id": "phone-number", "classes": ""}
) }}
</div> </div>
{% if skip_link or link_to_upload %} {% if skip_link or link_to_upload %}
<div class="grid-col-12 margin-top-1"> <div class="grid-col-12 margin-top-1">
+2 -1
View File
@@ -71,13 +71,14 @@
show_errors=False show_errors=False
)}} )}}
</div> </div>
<span id="upload-status-live" class="usa-sr-only" role="status" aria-live="polite" aria-atomic="true"></span>
<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" data-module="fullscreen-table">
{% call(item, row_number) list_table( {% call(item, row_number) list_table(
example, example,
caption="Example", caption="Example spreadsheet",
caption_visible=False, caption_visible=False,
field_headings=[''] + column_headings field_headings=[''] + column_headings
) %} ) %}
+1
View File
@@ -7,3 +7,4 @@ cloud_dot_gov_route: notify-demo.app.cloud.gov
redis_enabled: 1 redis_enabled: 1
nr_agent_id: '1134302465' nr_agent_id: '1134302465'
nr_app_id: '1083160688' nr_app_id: '1083160688'
FEATURE_SOCKET_ENABLED: true
+1
View File
@@ -7,3 +7,4 @@ cloud_dot_gov_route: notify.app.cloud.gov
redis_enabled: 1 redis_enabled: 1
nr_agent_id: '1050708682' nr_agent_id: '1050708682'
nr_app_id: '1050708682' nr_app_id: '1050708682'
FEATURE_SOCKET_ENABLED: false
+1
View File
@@ -9,6 +9,7 @@ ADMIN_CLIENT_USERNAME: notify-admin
ADMIN_CLIENT_SECRET: sandbox-notify-secret-key ADMIN_CLIENT_SECRET: sandbox-notify-secret-key
DANGEROUS_SALT: sandbox-notify-salt DANGEROUS_SALT: sandbox-notify-salt
SECRET_KEY: sandbox-notify-secret-key SECRET_KEY: sandbox-notify-secret-key
FEATURE_SOCKET_ENABLED: true
nr_agent_id: '' nr_agent_id: ''
nr_app_id: '' nr_app_id: ''
NR_BROWSER_KEY: '' NR_BROWSER_KEY: ''
+1
View File
@@ -7,3 +7,4 @@ cloud_dot_gov_route: notify-staging.app.cloud.gov
redis_enabled: 1 redis_enabled: 1
nr_agent_id: '1134291385' nr_agent_id: '1134291385'
nr_app_id: '1031640326' nr_app_id: '1031640326'
FEATURE_SOCKET_ENABLED: false
+1
View File
@@ -62,3 +62,4 @@ applications:
LOGIN_DOT_GOV_CERTS_URL: ((LOGIN_DOT_GOV_CERTS_URL)) LOGIN_DOT_GOV_CERTS_URL: ((LOGIN_DOT_GOV_CERTS_URL))
# feature flagging # feature flagging
FEATURE_SOCKET_ENABLED: ((FEATURE_SOCKET_ENABLED))
Generated
+234 -86
View File
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -24,7 +24,7 @@ itsdangerous = "~=2.2"
jinja2 = "^3.1.6" jinja2 = "^3.1.6"
newrelic = "*" newrelic = "*"
notifications-python-client = "==10.0.1" notifications-python-client = "==10.0.1"
pyexcel = "==0.7.2" pyexcel = "==0.7.3"
pyexcel-io = "==0.6.7" pyexcel-io = "==0.6.7"
pyexcel-ods3 = "==0.6.1" pyexcel-ods3 = "==0.6.1"
pyexcel-xls = "==0.7.1" pyexcel-xls = "==0.7.1"
@@ -48,7 +48,7 @@ cryptography = "^44.0.2"
flask-redis = "^0.4.0" flask-redis = "^0.4.0"
geojson = "^3.1.0" geojson = "^3.1.0"
jmespath = "^1.0.1" jmespath = "^1.0.1"
numpy = "^2.2.4" numpy = "^2.2.5"
ordered-set = "^4.1.0" ordered-set = "^4.1.0"
phonenumbers = "^9.0.3" phonenumbers = "^9.0.3"
pycparser = "^2.22" pycparser = "^2.22"
+1 -1
View File
@@ -1264,7 +1264,7 @@ def test_send_one_off_has_correct_page_title(
( (
0, 0,
{}, {},
"phone number", "Country code and phone number",
), ),
( (
1, 1,
+1 -1
View File
@@ -945,7 +945,7 @@ def test_load_edit_template_with_copy_of_template(
assert page.select_one("form")["method"] == "post" assert page.select_one("form")["method"] == "post"
assert page.select_one("input")["value"] == (expected_name) assert page.select_one("input")["value"] == (expected_name)
assert page.select_one("textarea").text == ("\r\nYour ((thing)) is due soon") assert page.select_one("textarea").text.strip() == ("Your ((thing)) is due soon")
mock_get_service_email_template.assert_called_once_with( mock_get_service_email_template.assert_called_once_with(
SERVICE_TWO_ID, SERVICE_TWO_ID,
TEMPLATE_ONE_ID, TEMPLATE_ONE_ID,
+2 -2
View File
@@ -164,12 +164,12 @@ def test_should_show_empty_text_box(
) )
textbox = page.select_one( textbox = page.select_one(
"[data-module=autofocus][data-force-focus=True] .usa-input" ".usa-input"
) )
assert "value" not in textbox assert "value" not in textbox
assert textbox["name"] == "placeholder_value" assert textbox["name"] == "placeholder_value"
assert textbox["class"] == [ assert textbox["class"] == [
"usa-input", "form-control", "usa-input",
] ]
# data-module=autofocus is set on a containing element so it # data-module=autofocus is set on a containing element so it
# shouldnt also be set on the textbox itself # shouldnt also be set on the textbox itself
@@ -0,0 +1,26 @@
// This will use the real function and listener
const {
initUploadStatusAnnouncer
} = require('../../app/assets/javascripts/fileUpload.js');
jest.useFakeTimers();
test('writes upload message to the live region on DOMContentLoaded', () => {
// Setup the DOM
document.body.innerHTML = `
<div id="upload-status-live" aria-live="assertive" role="status" class="usa-sr-only">Old message</div>
<span id="upload-success" class="usa-sr-only">File upload successful</span>
`;
// Register the listener (same as page load does)
initUploadStatusAnnouncer();
// Simulate the page load event
document.dispatchEvent(new Event('DOMContentLoaded'));
// Live region will be cleared first, then updated
jest.advanceTimersByTime(300);
const srRegion = document.getElementById('upload-status-live');
expect(srRegion.textContent).toBe('File upload successful\u00A0');
});
+110 -7
View File
@@ -1,13 +1,12 @@
const helpers = require('./support/helpers.js'); const helpers = require('./support/helpers.js');
beforeAll(() => { const { announceUploadStatusFromElement } = require('../../app/assets/javascripts/fileUpload.js');
require('../../app/assets/javascripts/fileUpload.js');
});
afterAll(() => { afterAll(() => {
require('./support/teardown.js'); require('./support/teardown.js');
}); });
describe('File upload', () => { describe('File upload', () => {
let form; let form;
@@ -72,12 +71,116 @@ describe('File upload', () => {
}); });
test("It should add a link to cancel the upload by reloading the page", () => { test("It should display a disabled Uploading button", () => {
const uploadingButton = form.querySelector("button.uploading-button");
expect(form.querySelector("a[href='']")).not.toBeNull(); expect(uploadingButton).not.toBeNull();
expect(uploadingButton.textContent).toMatch(/Uploading/);
expect(uploadingButton.getAttribute('aria-disabled')).toBe("true");
}); });
}); });
}); });
describe('File upload "upload-trigger" click handler', () => {
let form;
beforeEach(() => {
document.body.innerHTML = `
<form method="post" enctype="multipart/form-data" data-module="file-upload">
<button type="button" data-module="upload-trigger" data-file-input-id="test-file-input">Upload your file</button>
<input type="file" id="test-file-input" style="display:none;">
</form>
`;
form = document.querySelector('form');
// Register the module
window.GOVUK.modules.start();
});
afterEach(() => {
document.body.innerHTML = '';
});
test('clicking upload trigger simulates file input click', () => {
const uploadButton = form.querySelector('[data-module="upload-trigger"]');
const fileInput = document.getElementById('test-file-input');
// Spy on fileInput.click
const clickSpy = jest.spyOn(fileInput, 'click').mockImplementation(() => {});
// Trigger the click
helpers.triggerEvent(uploadButton, 'click');
expect(clickSpy).toHaveBeenCalled();
clickSpy.mockRestore();
});
});
describe('announceUploadStatusFromElement', () => {
beforeEach(() => {
jest.useFakeTimers();
document.body.innerHTML = `
<div id="upload-status-live" aria-live="assertive" role="status" class="usa-sr-only"></div>
`;
});
afterEach(() => {
jest.useRealTimers();
document.body.innerHTML = '';
});
test('announces error message from #upload-error', () => {
document.body.innerHTML += `
<span id="upload-error" class="usa-sr-only">File upload failed</span>
`;
const srRegion = document.getElementById('upload-status-live');
// Call the function
announceUploadStatusFromElement();
// Confirm it clears first
expect(srRegion.textContent).toBe('');
// Fast-forward the timer
jest.advanceTimersByTime(300);
// Confirm it updates after delay
expect(srRegion.textContent).toBe('File upload failed\u00A0');
});
test('announces success message from #upload-success if no error is present', () => {
document.body.innerHTML += `
<span id="upload-success" class="usa-sr-only">File upload successful</span>
`;
const srRegion = document.getElementById('upload-status-live');
announceUploadStatusFromElement();
expect(srRegion.textContent).toBe('');
jest.advanceTimersByTime(301);
expect(srRegion.textContent).toBe('File upload successful\u00A0');
});
test('does nothing if neither success nor error is present', () => {
const srRegion = document.getElementById('upload-status-live');
srRegion.textContent = 'Old message';
announceUploadStatusFromElement();
// Should not clear or update if no message element is found
expect(srRegion.textContent).toBe('Old message');
jest.advanceTimersByTime(300);
// Still unchanged
expect(srRegion.textContent).toBe('Old message');
});
});
-20
View File
@@ -347,24 +347,4 @@ describe('FullscreenTable', () => {
}); });
describe("when the table is focused", () => {
beforeEach(() => {
// start module
window.GOVUK.modules.start();
tableFrame = document.querySelector('.fullscreen-scrollable-table');
tableFrame.focus();
});
test("it should make the parent frame a focus style", () => {
expect(container.classList.contains('js-focus-style')).toBe(true);
});
});
}); });