Compare commits

..

42 Commits

Author SHA1 Message Date
Andrew Shumway
386b3f8348 Merge pull request #2505 from GSA/dependabot/pip/pyexcel-0.7.3
Bump pyexcel from 0.7.2 to 0.7.3
2025-04-23 15:28:07 -06:00
dependabot[bot]
8bf58cdca9 Bump pyexcel from 0.7.2 to 0.7.3
Bumps [pyexcel](https://github.com/pyexcel/pyexcel) from 0.7.2 to 0.7.3.
- [Release notes](https://github.com/pyexcel/pyexcel/releases)
- [Changelog](https://github.com/pyexcel/pyexcel/blob/dev/CHANGELOG.rst)
- [Commits](https://github.com/pyexcel/pyexcel/compare/v0.7.2...v0.7.3)

---
updated-dependencies:
- dependency-name: pyexcel
  dependency-version: 0.7.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-23 21:21:33 +00:00
Andrew Shumway
6b92941156 Merge pull request #2527 from GSA/dependabot/pip/moto-5.1.4
Bump moto from 5.1.3 to 5.1.4
2025-04-23 15:20:17 -06:00
Andrew Shumway
0ccbe17b3b Merge pull request #2528 from GSA/dependabot/pip/numpy-2.2.5
Bump numpy from 2.2.4 to 2.2.5
2025-04-23 15:19:33 -06:00
Beverly Nguyen
ae305227bb Merge pull request #2440 from GSA/2138-cancel-button-CSV
2138 - Change cancel button to uploading button for CSV
2025-04-23 12:01:06 -07:00
Beverly Nguyen
7130925b9a Merge pull request #2524 from GSA/2510-add-phone-number-instruction
2510 - Adding additional context to phone numbers
2025-04-23 11:56:05 -07:00
ccostino
6f6402e36a Merge pull request #2499 from GSA/2481-landing-page-standalone-without-api
homepage can stand on it's own without api
2025-04-22 16:49:29 -04:00
Beverly Nguyen
6bfa9ede76 isort and remove poetry 2025-04-22 12:09:00 -07:00
Beverly Nguyen
57023b3a3a Merge branch 'main' into 2481-landing-page-standalone-without-api 2025-04-22 12:08:34 -07:00
Jonathan Bobel
0e31d34672 Update fullscreenTable.js
typo
2025-04-22 12:48:03 -04:00
Jonathan Bobel
1b9e19072e Updating some of the full screen table code 2025-04-22 10:45:11 -04:00
Beverly Nguyen
431dbb10dd remove header banner when site is down 2025-04-21 16:26:07 -07:00
dependabot[bot]
25524324c5 Bump numpy from 2.2.4 to 2.2.5
Bumps [numpy](https://github.com/numpy/numpy) from 2.2.4 to 2.2.5.
- [Release notes](https://github.com/numpy/numpy/releases)
- [Changelog](https://github.com/numpy/numpy/blob/main/doc/RELEASE_WALKTHROUGH.rst)
- [Commits](https://github.com/numpy/numpy/compare/v2.2.4...v2.2.5)

---
updated-dependencies:
- dependency-name: numpy
  dependency-version: 2.2.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-21 23:17:11 +00:00
dependabot[bot]
5abc0a4c8e Bump moto from 5.1.3 to 5.1.4
Bumps [moto](https://github.com/getmoto/moto) from 5.1.3 to 5.1.4.
- [Release notes](https://github.com/getmoto/moto/releases)
- [Changelog](https://github.com/getmoto/moto/blob/master/CHANGELOG.md)
- [Commits](https://github.com/getmoto/moto/compare/5.1.3...5.1.4)

---
updated-dependencies:
- dependency-name: moto
  dependency-version: 5.1.4
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-21 23:16:04 +00:00
Jonathan Bobel
b6a6ffb2ab Merge branch 'main' of https://github.com/GSA/notifications-admin into 2138-cancel-button-CSV 2025-04-21 16:03:43 -04:00
Jonathan Bobel
b01a62225e Fixing a test, updating content 2025-04-21 13:57:29 -04:00
Jonathan Bobel
522340f52b Update send-test.html
Fixing the case of the labels
2025-04-21 13:45:51 -04:00
Jonathan Bobel
fe42f5ef64 Allowing for a custom label on the first step 2025-04-21 12:12:09 -04:00
Jonathan Bobel
b4afcb07aa Fixing tests 2025-04-21 11:42:24 -04:00
Jonathan Bobel
5d3364a114 Adjusting input types 2025-04-18 15:11:07 -04:00
Jonathan Bobel
70b149476c 2510 - adding additional context to phone numbers 2025-04-18 14:51:22 -04:00
Beverly Nguyen
10c61440c9 fix testing 2025-04-18 11:27:07 -07:00
Beverly Nguyen
34fdf7a1ca fixed testing 2025-04-18 11:17:51 -07:00
Jonathan Bobel
20cbb0a8db Removing top alert, creating if/else for the larger alert on the home page 2025-04-18 10:42:17 -04:00
Jonathan Bobel
14024a28ca Merge branch '2481-landing-page-standalone-without-api' of https://github.com/GSA/notifications-admin into 2481-landing-page-standalone-without-api 2025-04-18 10:11:49 -04:00
Beverly Nguyen
77e9475ed8 fixing templates 2025-04-17 15:04:50 -07:00
Beverly Nguyen
b4e239c9bb creating error page for tech difficulties 2025-04-17 15:01:22 -07:00
Jonathan Bobel
34b39acf38 Merge branch 'main' of https://github.com/GSA/notifications-admin into 2481-landing-page-standalone-without-api 2025-04-16 11:23:07 -04:00
Jonathan Bobel
3e0e290dc0 Updated error message and imagery 2025-04-15 15:50:42 -04:00
Beverly Nguyen
d4a2584dfb update logging 2025-04-14 14:51:55 -07:00
Beverly Nguyen
638a4f20da update logging 2025-04-14 14:49:19 -07:00
Beverly Nguyen
da695557b0 status banner added 2025-04-11 13:28:42 -07:00
Beverly Nguyen
3e18c8f595 keep landing page independent from api 2025-04-11 13:03:40 -07:00
Jonathan Bobel
f8c7f6b817 Merge conflict - poetry.lock 2025-04-02 11:44:39 -04:00
Jonathan Bobel
193184983d Changing alert back to a focus and fixing tests 2025-04-02 11:39:07 -04:00
Jonathan Bobel
38451d77ad Test updates, js fixes 2025-04-01 16:22:46 -04:00
Jonathan Bobel
032b9927aa Test updates 2025-04-01 13:03:12 -04:00
Jonathan Bobel
06c238269f Update send.html
Fixing the back link
2025-04-01 11:02:08 -04:00
Jonathan Bobel
2064fc864a Added an sr-only alert to the site for upload errors (and any other errors we might want to only announce to the screen reader) 2025-04-01 10:46:56 -04:00
Jonathan Bobel
f55d47e3d1 Update column-errors.html 2025-03-28 11:00:13 -04:00
Jonathan Bobel
7813ce7914 Accessibility updates 2025-03-28 10:36:17 -04:00
Jonathan Bobel
d79ece2d01 2138 - Change cancel button to uploading button for CSV 2025-03-26 14:49:11 -04:00
31 changed files with 842 additions and 408 deletions

View File

@@ -109,6 +109,7 @@ from app.notify_client.template_statistics_api_client import template_statistics
from app.notify_client.upload_api_client import upload_api_client
from app.notify_client.user_api_client import user_api_client
from app.url_converters import SimpleDateTypeConverter, TemplateTypeConverter
from app.utils.api_health import is_api_down
from app.utils.govuk_frontend_jinja.flask_ext import init_govuk_frontend
from notifications_utils import logging, request_helper
from notifications_utils.formatters import (
@@ -189,6 +190,10 @@ def create_app(application):
FEATURE_SOCKET_ENABLED=feature_socket_enabled,
)
@application.context_processor
def inject_is_api_down():
return {"is_api_down": is_api_down()}
@application.context_processor
def inject_initial_signin_url():
ttl = 24 * 60 * 60

View File

@@ -0,0 +1 @@
<svg id="Capa_1" enable-background="new 0 0 512 512" height="512" viewBox="0 0 512 512" width="512" xmlns="http://www.w3.org/2000/svg"><g><g><path d="m468.547 144.82h-63.047v-121.32c0-12.958-10.542-23.5-23.5-23.5h-324c-12.958 0-23.5 10.542-23.5 23.5v84.833c.344 9.931 14.659 9.923 15 0v-33.333h341v69.82h-121.998c-14.312 0-25.954 11.574-25.954 25.8v83.174l-28.866 19.221c-5.511 3.444-5.51 12.168.001 15.611l28.865 19.22v83.174c0 14.226 11.643 25.799 25.954 25.799h121.998v71.68c0 4.687-3.813 8.5-8.5 8.5h-324c-4.687 0-8.5-3.813-8.5-8.5v-20h131.5c7.444 0 13.5-6.056 13.5-13.5v-127.999c0-7.444-6.056-13.5-13.5-13.5h-131.5v-168.021c-.344-9.931-14.659-9.923-15 0v168.021h-3.5c-7.444 0-13.5 6.056-13.5 13.5v128c0 7.444 6.056 13.5 13.5 13.5h3.5v20c0 12.958 10.542 23.5 23.5 23.5h324c12.958 0 23.5-10.542 23.5-23.5v-71.68h11.5c9.931-.344 9.923-14.659 0-15h-148.498c-6.04 0-10.954-4.845-10.954-10.799v-85.173c0-3.779-1.882-7.288-5.031-9.384l-23.492-15.643 23.492-15.644c3.15-2.098 5.031-5.606 5.031-9.384v-85.173c0-5.955 4.914-10.8 10.954-10.8h200.045c6.04 0 10.953 4.845 10.953 10.8v220.401c0 5.955-4.913 10.799-10.953 10.799h-16.436c-9.931.344-9.923 14.659 0 15h16.436c14.311 0 25.953-11.574 25.953-25.799v-220.401c0-14.226-11.643-25.8-25.953-25.8zm-256.047-129.82v9.5c-.353 9.929-14.65 9.922-15 0v-9.5zm-55.333 0v9.5c-.353 9.929-14.65 9.922-15 0v-9.5zm-55.334 0v9.5c-.353 9.929-14.65 9.922-15 0v-9.5zm-52.333 45v-36.5c0-4.687 3.813-8.5 8.5-8.5h13.833v9.5c1.135 29.816 43.876 29.793 45 0v-9.5h10.334v9.5c1.135 29.816 43.876 29.793 45 0v-9.5h10.333v9.5c1.135 29.816 43.876 29.793 45 0v-9.5h154.5c4.687 0 8.5 3.813 8.5 8.5v36.5zm-17 268.5h147v125h-147z"/><path d="m451.356 344.514c2.439-4.507 2.214-9.973-.589-14.264l-69.093-105.771c-5.242-8.361-18.107-8.359-23.35 0l-69.092 105.771c-2.803 4.291-3.028 9.756-.589 14.263s7.139 7.307 12.264 7.307h138.186c5.125 0 9.824-2.799 12.263-7.306zm-148.499-7.694 67.143-102.786 67.143 102.786z"/><path d="m362.5 266.32v31.445c.344 9.931 14.659 9.923 15 0v-31.445c-.344-9.931-14.659-9.923-15 0z"/><path d="m370 310.82c-4.143 0-7.5 3.358-7.5 7.5v2.5c.344 9.931 14.659 9.923 15 0v-2.5c0-4.142-3.357-7.5-7.5-7.5z"/><path d="m91.375 123.094h36.375c9.931-.344 9.923-14.659 0-15h-36.375c-9.931.344-9.923 14.66 0 15z"/><path d="m91.375 149.316h90c9.931-.344 9.923-14.659 0-15h-90c-9.931.344-9.923 14.66 0 15z"/><path d="m91.375 175.539h77.625c9.931-.344 9.923-14.659 0-15h-77.625c-9.931.344-9.923 14.659 0 15z"/><path d="m91.375 225.76h36.375c9.931-.344 9.923-14.659 0-15h-36.375c-9.931.344-9.923 14.66 0 15z"/><path d="m91.375 251.982h90c9.931-.344 9.923-14.659 0-15h-90c-9.931.344-9.923 14.66 0 15z"/><path d="m91.375 278.205h77.625c9.931-.344 9.923-14.659 0-15h-77.625c-9.931.344-9.923 14.659 0 15z"/><path d="m139.354 356.038c-2.93-2.929-7.678-2.929-10.607 0l-22.747 22.747-22.747-22.747c-2.93-2.929-7.678-2.929-10.607 0s-2.929 7.678 0 10.606l22.748 22.748-22.748 22.748c-2.929 2.929-2.929 7.678 0 10.606 2.93 2.929 7.677 2.929 10.607 0l22.747-22.747 22.747 22.747c2.93 2.929 7.677 2.929 10.607 0 2.929-2.929 2.929-7.678 0-10.606l-22.748-22.748 22.748-22.748c2.929-2.928 2.929-7.677 0-10.606z"/></g></g></svg>

After

Width:  |  Height:  |  Size: 3.1 KiB

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) {
"use strict";
Modules.FileUpload = function() {
this.submit = () => this.$form.trigger('submit');
this.showCancelButton = () => $('.file-upload-button', this.$form).replaceWith(`
<a href="" class='usa-button usa-button--secondary'>Cancel upload</a>
`);
this.start = function(component) {
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.showCancelButton = () => {
$('.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>
</button>
`);
};
};
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);
if (typeof module !== 'undefined' && module.exports) {
module.exports = {
announceUploadStatusFromElement,
initUploadStatusAnnouncer
};
}
if (typeof window !== 'undefined') {
initUploadStatusAnnouncer();
}

View File

@@ -22,7 +22,6 @@
this.$scrollableTable
.on('scroll', this.toggleShadows)
.on('scroll', this.maintainHeight)
.on('focus blur', () => this.$component.toggleClass('js-focus-style'));
if (
window.GOVUK.stickAtBottomWhenScrolling &&
@@ -37,11 +36,11 @@
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, '');
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
.append(

View File

@@ -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: '...'; }
}

View File

@@ -289,6 +289,7 @@ td.table-empty-message {
margin-top: units(1);
width: 100%;
border: 1px solid color('gray-60');
height: 40px;
}
}

View File

@@ -1,5 +1,5 @@
@forward "uswds-theme";
@forward "uswds";
@forward "uswds-theme-custom-styles";
@forward "legacy-styles";
@forward "main";
@forward "data-visualization";

View File

@@ -1,3 +1,5 @@
import logging
from flask import (
abort,
current_app,
@@ -19,6 +21,8 @@ from app.main.views.sub_navigation_dictionaries import (
)
from app.utils.user import user_is_logged_in
logger = logging.getLogger(__name__)
# Hook to check for feature flags
@main.before_request
@@ -45,7 +49,7 @@ def index():
return render_template(
"views/signedout.html",
sms_rate=CURRENT_SMS_RATE,
counts=status_api_client.get_count_of_live_services_and_organizations(),
counts=status_api_client.get_count_of_live_services_and_organizations()
)

View File

@@ -481,6 +481,7 @@ def send_one_off_step(service_id, template_id, step_index):
),
template=template,
form=form,
current_placeholder=current_placeholder,
skip_link=get_skip_link(step_index, template),
back_link=back_link,
link_to_upload=(

View File

@@ -11,6 +11,12 @@
{% include "components/head.html" %}
</head>
<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>
{% block bodyStart %}
{% block extra_javascripts_before_body %}
@@ -29,29 +35,31 @@
{% block header %}
{% include 'components/usa_banner.html' %}
{% if current_user.is_authenticated or current_service or current_user.platform_admin %}
<section class="usa-site-alert usa-site-alert--info" aria-label="Site alert,,,,">
<div class="usa-alert">
<div class="usa-alert__body">
<p class="usa-alert__heading text-bold">Notify.gov Service Ending</p>
<p class="usa-alert__text">
GSA will no longer offer the Notify.gov service after June 8th, 2025. Visit
<a class="usa-link" href="/notify-service-ending">Notify.gov Service Ending</a> for more information.
</p>
{% if not is_api_down %}
{% if current_user.is_authenticated or current_service or current_user.platform_admin %}
<section class="usa-site-alert usa-site-alert--info" aria-label="Site alert,,,,">
<div class="usa-alert">
<div class="usa-alert__body">
<p class="usa-alert__heading text-bold">Notify.gov Service Ending</p>
<p class="usa-alert__text">
GSA will no longer offer the Notify.gov service after June 8th, 2025. Visit
<a class="usa-link" href="/notify-service-ending">Notify.gov Service Ending</a> for more information.
</p>
</div>
</div>
</div>
</section>
{% else %}
<section class="usa-site-alert usa-site-alert--emergency usa-site-alert--no-heading" aria-label="Site alert,,,,">
<div class="usa-alert">
<div class="usa-alert__body">
<p class="usa-alert__heading text-bold">Notify.gov Service Ending</p>
<p class="usa-alert__text">
Notify.gov is no longer accepting new partners.
</p>
</section>
{% else %}
<section class="usa-site-alert usa-site-alert--emergency usa-site-alert--no-heading" aria-label="Site alert,,,,">
<div class="usa-alert">
<div class="usa-alert__body">
<p class="usa-alert__heading text-bold">Notify.gov Service Ending</p>
<p class="usa-alert__text">
Notify.gov is no longer accepting new partners.
</p>
</div>
</div>
</div>
</section>
</section>
{% endif %}
{% endif %}
{% include 'components/header.html' %}
{% endblock %}

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) %}
<div
class='banner{% if type %}-{{ type }}{% endif %}{% if with_tick %}-with-tick{% endif %}'
{% if type == 'dangerous' %}
role='group'
tabindex='-1'
{% endif %}
{% if id %}
id={{ id }}
{% endif %}

View File

@@ -27,17 +27,21 @@
</label>
{{ field(**{
'class': 'file-upload-field',
'id': field.name,
'accept': allowed_file_extensions|format_list_items('.{item}')|join(',')|e
}) }}
<label class="file-upload-button usa-button" for="{{ field.name }}">
{{ button_text }}
</label>
<button type="button"
class="usa-button file-upload-button"
data-module="upload-trigger"
data-file-input-id="{{ field.name }}">
{{ button_text }}
</button>
{% if alternate_link and alternate_link_text %}
<span class="file-upload-alternate-link">
or <a class="usa-link" href="{{ alternate_link }}">{{ alternate_link_text }}</a>
</span>
{% 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() }}" />
{{ usaButton({ "text": "Submit", "classes": "file-upload-submit" }) }}
</form>

View File

@@ -5,7 +5,6 @@
highlight_placeholders=False,
autofocus=False,
autosize=False,
colour_preview=False,
help_link=None,
help_link_text=None,
width='2-3',
@@ -13,58 +12,82 @@
safe_error_message=False,
rows=8,
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
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 %}"
>
<label class="usa-label" for="{{ field.name }}">
{% if label %}
{{ label }}
{% else %}
{{ field.label.text }}
{% endif %}
<label class="usa-label" for="{{ field_id }}">
{{ label or field.label.text }}
</label>
{% if hint %}
<div class="usa-hint">
<div class="usa-hint" id="{{ field_id }}-hint">
{{ hint }}
</div>
{% endif %}
{% 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>
{% 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>
{% endif %}
{%
if highlight_placeholders or autosize
%}
{% if highlight_placeholders or autosize %}
{% set field_class = 'usa-textarea form-control-{} textbox-highlight-textbox'.format(width) %}
{% else %}
{% set field_class = 'form-control-{} {}'.format(width, 'textbox-right-aligned' if suffix else '') %}
{% set field_class = 'usa-input' %}
{% endif %}
{%
set field_class = 'form-control ' + field_class + (
' form-control-error' if field.errors else ''
)
%}
{{ field(
class=field_class,
data_module='enhanced-textbox' if highlight_placeholders or autosize else '',
data_highlight_placeholders='true' if highlight_placeholders else 'false',
rows=rows|string,
placeholder=placeholder,
aria_describedby=field.name+"-error",
required='required' if required else None,
**kwargs
) }}
{% set field_class = 'form-control ' ~ field_class ~ (' form-control-error' if field.errors else '') %}
{% if autosize or highlight_placeholders %}
<textarea
id="{{ field_id }}"
name="{{ field.name }}"
class="{{ field_class }}"
rows="{{ rows }}"
placeholder="{{ placeholder }}"
aria-describedby="{{ described_by.strip() }}"
{% if field.flags.required %}required{% endif %}
data-module="enhanced-textbox"
data-highlight-placeholders="true"
>{{ 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 %}
<span>{{ suffix }}</span>
{% endif %}
{% if help_link and help_link_text %}
<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>
{% endif %}
</div>

View File

@@ -0,0 +1,17 @@
<section class="grid-container usa-section margin-top-5 margin-bottom-5">
<div class="grid-row flex-justify-center">
<div class="desktop:grid-col-3 tablet:grid-col-4 mobile:grid-col-12 flex-align-center">
<img
class="width-tablet tablet:width-full"
src="{{ asset_url('images/api-error.svg') }}"
alt="Mobile phone showing error screen"/>
</div>
<div class="desktop:grid-col-6 tablet:grid-col-12 padding-left-0 desktop:padding-left-5">
<h1 class="font-body-3xl line-height-sans-2 text-bold">
There's currently a technical issue.
</h1>
<p class="font-body-lg line-height-sans-2">Thank you for your patience while we work on it. Notify will be back soon.</p>
</div>
</div>
</div>
</section>

View File

@@ -20,7 +20,11 @@ Error
{% block maincolumn_content %}
<div class="">
{# Alert for users of AT #}
<span id="upload-error" class="usa-sr-only">File upload failed</span>
{% call banner_wrapper(type='dangerous') %}
{% if recipients.too_many_rows %}
@@ -150,7 +154,7 @@ Error
) }}
{% endif %}
</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>
{% endif %}

View File

@@ -21,6 +21,8 @@
{{ template|string }}
<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'>
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
{% if choose_time_form %}

View File

@@ -16,6 +16,8 @@
{% block maincolumn_content %}
<div class="">
{# Alert for users of AT #}
<span id="upload-failed" class="usa-sr-only">File upload failed</span>
{% call banner_wrapper(type='dangerous') %}
{% if row_errors|length == 1 %}
<div class="usa-alert usa-alert--error" role="alert">

View File

@@ -1,6 +1,7 @@
{% extends "withnav_template.html" %}
{% from "components/page-header.html" import page_header %}
{% from "components/page-footer.html" import page_footer %}
{% from "components/textbox.html" import textbox %}
{% from "components/form.html" import form_wrapper %}
{% from "components/components/back-link/macro.njk" import usaBackLink %}
@@ -38,10 +39,19 @@
data_force_focus=True
) %}
<div class="grid-row">
{% set extra_class = "extra-tracking" if form.placeholder_value.label.text == "phone number" else "" %}
{% set placeholder_id = "phone number" if form.placeholder_value.label.text == "phone number" else "" %}
<div class="grid-col-12 {% if form.placeholder_value.label.text == 'phone number' %}extra-tracking{% endif %}">
{{ form.placeholder_value(param_extensions={"classes": "", "id": "phone-number"}) }}
{% set is_phone_number = form.placeholder_value.label.text == "phone number" %}
{% set label_text = "Country code and phone number" if current_placeholder == "phone number" else current_placeholder %}
{% set hint_text = "For example: +18885551234" if is_phone_number else None %}
{% 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>
{% if skip_link or link_to_upload %}
<div class="grid-col-12 margin-top-1">

View File

@@ -12,7 +12,9 @@
{% block backLink %}
{{ usaBackLink(params) }}
{{ usaBackLink({
"href": url_for('main.send_one_off_step', service_id=current_service.id, template_id=template.id, step_index=0)
}) }}
{% endblock %}
{% set phone_numbers = [
@@ -68,13 +70,14 @@
show_errors=False
)}}
</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>
<div class="spreadsheet" data-module="fullscreen-table">
{% call(item, row_number) list_table(
example,
caption="Example",
caption="Example spreadsheet",
caption_visible=False,
field_headings=[''] + column_headings
) %}

View File

@@ -4,199 +4,206 @@ import usaButton %} {% block meta %}
name="description"
content="Notify.gov lets you send text messages to your users. Try it now if you work in federal, state, or local government."
/>
{% endblock %} {% block pageTitle %} Notify.gov {% endblock %} {% block main %}
{% endblock %}
{% block pageTitle %} Notify.gov
{% if is_api_down %} - There's currently a technical issue {% endif %}
{% endblock %}
{% block main %}
{% block beforeContent %}{% endblock %}
<main id="main-content" role="main">
{% block content %}
<section class="usa-section--dark usa-hero" aria-label="Introduction">
<div class="grid-container padding-y-4">
<div class="grid-row grid-gap display-flex flex-align-center">
<div class="desktop:grid-col-8 tablet:grid-col-12">
<h1 class="font-sans-2xl usa-hero__heading">
Reach people where they are with government-powered text messages
</h1>
<p class="font-sans-lg">
Notify.gov is a text messaging service that helps federal, state,
local, tribal and territorial governments more effectively
communicate with the people they serve.
</p>
<div class="usa-button-group margin-bottom-5 flex-align-center">
<a
class="usa-button usa-button login-button login-button--primary margin-right-2"
href="{{ initial_signin_url }}"
>Sign in with
{% if is_api_down %}
{% include "error/technical_difficulties.html" %}
{% else %}
<section class="usa-section--dark usa-hero" aria-label="Introduction">
<div class="grid-container padding-y-4">
<div class="grid-row grid-gap display-flex flex-align-center">
<div class="desktop:grid-col-8 tablet:grid-col-12">
<h1 class="font-sans-2xl usa-hero__heading">
Reach people where they are with government-powered text messages
</h1>
<p class="font-sans-lg">
Notify.gov is a text messaging service that helps federal, state,
local, tribal and territorial governments more effectively
communicate with the people they serve.
</p>
<div class="usa-button-group margin-bottom-5 flex-align-center">
<a
class="usa-button usa-button login-button login-button--primary margin-right-2"
href="{{ initial_signin_url }}"
>Sign in with
<img
src="{{ asset_url('images/logo-login.svg') }}"
alt="Login.gov logo"
/>
</a>
if you are an existing partner
</div>
</div>
<div
class="desktop:grid-col-3 grid-offset-1 desktop:display-block display-none margin-x-5"
>
<img
src="{{ asset_url('images/logo-login.svg') }}"
alt="Login.gov logo"
src="{{ asset_url('images/product/phone-text.png') }}"
alt="Illustration of a mobile phone displaying a text message and number on its screen, with a speech bubble coming out from outside the phone, symbolizing communication or notification."
/>
</a>
if you are an existing partner
</div>
</div>
</div>
<div
class="desktop:grid-col-3 grid-offset-1 desktop:display-block display-none margin-x-5"
>
<img
src="{{ asset_url('images/product/phone-text.png') }}"
alt="Illustration of a mobile phone displaying a text message and number on its screen, with a speech bubble coming out from outside the phone, symbolizing communication or notification."
/>
</div>
</div>
</div>
</section>
<section
class="grid-container usa-section usa-section__home border-top border-base-lighter usa-prose padding-bottom-1"
>
<h2 class="font-body-xl margin-top-0 margin-bottom-3">
Government texting made easy
</h2>
<p class="usa-body">
Notify.gov is a text messaging platform built for government agencies.
With minimal set-up and secure, personalized messaging, you can make
one-way texting a part of your outreach program.
</p>
</section>
<section
class="grid-container usa-section usa-section__home usa-prose padding-bottom-1"
>
<h2 class="font-sans-xl margin-top-0">Key features</h2>
<div class="home-cards margin-top-5">
<ul class="usa-card-group display-flex margin-bottom-4">
<li class="usa-card tablet:grid-col-4 mobile-lg:grid-col-12">
<div class="usa-card__container">
<div class="blue-bar"></div>
<img
src="{{ asset_url('images/internet.svg') }}"
alt="Globe on top of a web browser"
/>
<div class="usa-card__header">
<h3 class="font-heading-md">Web-based</h3>
</div>
<div class="usa-card__body">
<p>Nothing to download or install</p>
</div>
</div>
</li>
<li class="usa-card tablet:grid-col-4 mobile-lg:grid-col-12">
<div class="usa-card__container">
<div class="blue-bar"></div>
<img
src="{{ asset_url('images/fast.svg') }}"
alt="Stopwatch with a notification speech bubble with a star inside"
/>
<div class="usa-card__header">
<h3 class="font-heading-md">Fast and easy</h3>
</div>
<div class="usa-card__body">
<p>No technical expertise required</p>
</div>
</div>
</li>
<li class="usa-card tablet:grid-col-4 mobile-lg:grid-col-12">
<div class="usa-card__container">
<div class="blue-bar"></div>
<img
src="{{ asset_url('images/status.svg') }}"
alt="3 status messages, 2 successes and one failure"
/>
<div class="usa-card__header">
<h3 class="font-heading-md">Track message delivery</h3>
</div>
<div class="usa-card__body">
<p>See which messages were received</p>
</div>
</div>
</li>
</ul>
<ul class="usa-card-group">
<li class="usa-card tablet:grid-col-4 mobile-lg:grid-col-12">
<div class="usa-card__container">
<div class="blue-bar"></div>
<img
src="{{ asset_url('images/translation.svg') }}"
alt="Speech bubbles with the letter A and the Chinese character for language"
/>
<div class="usa-card__header">
<h3 class="font-heading-md">
Send in recipients' preferred language
</h3>
</div>
<div class="usa-card__body">
<p>Notify.gov has support for more than 30 character sets</p>
</div>
</div>
</li>
<li class="usa-card tablet:grid-col-4 mobile-lg:grid-col-12">
<div class="usa-card__container">
<div class="blue-bar"></div>
<img
src="{{ asset_url('images/security.svg') }}"
alt="Lock with code icon inside on top of a web browser"
/>
<div class="usa-card__header">
<h3 class="font-heading-md">Security and privacy</h3>
</div>
<div class="usa-card__body">
<p>
Limited data retention, encryption, and multi-factor
authentication protect user data and manage risk with <br><a href="/about/security">our security efforts</a>
</p>
</div>
</div>
</li>
<li class="usa-card tablet:grid-col-4 mobile-lg:grid-col-12">
<div class="usa-card__container">
<div class="blue-bar"></div>
<img
src="{{ asset_url('images/send.svg') }}"
alt="Paper airplane and a notification icon with the number 1 inside"
/>
<div class="usa-card__header">
<h3 class="font-heading-md">
Send bulk, customized, one-way messages
</h3>
</div>
<div class="usa-card__body">
<p>
Send hundreds or thousands of individually customized messages
with just a few clicks
</p>
</div>
</div>
</li>
</ul>
</div>
</section>
<section
class="grid-container usa-section usa-section__home usa-prose grid-container padding-bottom-10">
<div class="grid-row grid-gap-3">
<a
class="text-no-underline tablet:grid-col-4 mobile-lg:grid-col-12"
href="mailto:tts-notify@gsa.gov"
</section>
<section
class="grid-container usa-section usa-section__home border-top border-base-lighter usa-prose padding-bottom-1"
>
<div class="contact-us-card">
<div class="grid-row flex-align-center grid-gap-2">
<div class="grid-col-auto">
<img
src="{{ asset_url('images/contact.svg') }}"
alt=""
class="height-7 width-7"
/>
</div>
<div class="grid-col">
<p class="margin-0">
<strong>Contact us</strong><br />
</p>
</div>
</div>
</div>
</a>
</div>
</section>
<h2 class="font-body-xl margin-top-0 margin-bottom-3">
Government texting made easy
</h2>
<p class="usa-body">
Notify.gov is a text messaging platform built for government agencies.
With minimal set-up and secure, personalized messaging, you can make
one-way texting a part of your outreach program.
</p>
</section>
<section
class="grid-container usa-section usa-section__home usa-prose padding-bottom-1"
>
<h2 class="font-sans-xl margin-top-0">Key features</h2>
<div class="home-cards margin-top-5">
<ul class="usa-card-group display-flex margin-bottom-4">
<li class="usa-card tablet:grid-col-4 mobile-lg:grid-col-12">
<div class="usa-card__container">
<div class="blue-bar"></div>
<img
src="{{ asset_url('images/internet.svg') }}"
alt="Globe on top of a web browser"
/>
<div class="usa-card__header">
<h3 class="font-heading-md">Web-based</h3>
</div>
<div class="usa-card__body">
<p>Nothing to download or install</p>
</div>
</div>
</li>
<li class="usa-card tablet:grid-col-4 mobile-lg:grid-col-12">
<div class="usa-card__container">
<div class="blue-bar"></div>
<img
src="{{ asset_url('images/fast.svg') }}"
alt="Stopwatch with a notification speech bubble with a star inside"
/>
<div class="usa-card__header">
<h3 class="font-heading-md">Fast and easy</h3>
</div>
<div class="usa-card__body">
<p>No technical expertise required</p>
</div>
</div>
</li>
<li class="usa-card tablet:grid-col-4 mobile-lg:grid-col-12">
<div class="usa-card__container">
<div class="blue-bar"></div>
<img
src="{{ asset_url('images/status.svg') }}"
alt="3 status messages, 2 successes and one failure"
/>
<div class="usa-card__header">
<h3 class="font-heading-md">Track message delivery</h3>
</div>
<div class="usa-card__body">
<p>See which messages were received</p>
</div>
</div>
</li>
</ul>
<ul class="usa-card-group">
<li class="usa-card tablet:grid-col-4 mobile-lg:grid-col-12">
<div class="usa-card__container">
<div class="blue-bar"></div>
<img
src="{{ asset_url('images/translation.svg') }}"
alt="Speech bubbles with the letter A and the Chinese character for language"
/>
<div class="usa-card__header">
<h3 class="font-heading-md">
Send in recipients' preferred language
</h3>
</div>
<div class="usa-card__body">
<p>Notify.gov has support for more than 30 character sets</p>
</div>
</div>
</li>
<li class="usa-card tablet:grid-col-4 mobile-lg:grid-col-12">
<div class="usa-card__container">
<div class="blue-bar"></div>
<img
src="{{ asset_url('images/security.svg') }}"
alt="Lock with code icon inside on top of a web browser"
/>
<div class="usa-card__header">
<h3 class="font-heading-md">Security and privacy</h3>
</div>
<div class="usa-card__body">
<p>
Limited data retention, encryption, and multi-factor
authentication protect user data and manage risk with <br><a href="/about/security">our security efforts</a>
</p>
</div>
</div>
</li>
<li class="usa-card tablet:grid-col-4 mobile-lg:grid-col-12">
<div class="usa-card__container">
<div class="blue-bar"></div>
<img
src="{{ asset_url('images/send.svg') }}"
alt="Paper airplane and a notification icon with the number 1 inside"
/>
<div class="usa-card__header">
<h3 class="font-heading-md">
Send bulk, customized, one-way messages
</h3>
</div>
<div class="usa-card__body">
<p>
Send hundreds or thousands of individually customized messages
with just a few clicks
</p>
</div>
</div>
</li>
</ul>
</div>
</section>
<section
class="grid-container usa-section usa-section__home usa-prose grid-container padding-bottom-10">
<div class="grid-row grid-gap-3">
<a
class="text-no-underline tablet:grid-col-4 mobile-lg:grid-col-12"
href="mailto:tts-notify@gsa.gov"
>
<div class="contact-us-card">
<div class="grid-row flex-align-center grid-gap-2">
<div class="grid-col-auto">
<img
src="{{ asset_url('images/contact.svg') }}"
alt=""
class="height-7 width-7"
/>
</div>
<div class="grid-col">
<p class="margin-0">
<strong>Contact us</strong><br />
</p>
</div>
</div>
</div>
</a>
</div>
</section>
{% endif %}
{% endblock %}
</main>
{% endblock %}

20
app/utils/api_health.py Normal file
View File

@@ -0,0 +1,20 @@
import logging
import os
import requests
from requests.exceptions import RequestException
logger = logging.getLogger(__name__)
def is_api_down():
api_base_url = os.getenv("API_HOST_NAME")
try:
response = requests.get(api_base_url, timeout=2)
is_down = response.status_code != 200
if is_down:
logger.warning(f"API responded with status {response.status_code} at {api_base_url}")
return is_down
except RequestException as e:
logger.error(f"API down when loading homepage {e}")
return True

View File

@@ -1,5 +1,4 @@
import logging
import logging.handlers
import re
import sys
from itertools import product
@@ -10,6 +9,21 @@ from flask.ctx import has_app_context, has_request_context
from flask.logging import default_handler
from pythonjsonlogger.jsonlogger import JsonFormatter as BaseJSONFormatter
class ColorFormatter(logging.Formatter):
COLOR_MAP = {
"WARNING": "\033[93m",
"ERROR": "\033[91m",
"CRITICAL": "\033[95m",
}
RESET = "\033[0m"
def format(self, record: logging.LogRecord) -> str:
color = self.COLOR_MAP.get(record.levelname, self.RESET)
base_message = super().format(record)
return f"{color}{base_message}{self.RESET}"
LOG_FORMAT = (
"%(asctime)s %(app_name)s %(name)s %(levelname)s "
'%(request_id)s %(service_id)s "%(message)s" [in %(pathname)s:%(lineno)d]'
@@ -79,7 +93,6 @@ def init_app(app):
def get_handlers(app):
handlers = []
standard_formatter = logging.Formatter(LOG_FORMAT, TIME_FORMAT)
json_formatter = JSONFormatter(LOG_FORMAT, TIME_FORMAT)
stream_handler = logging.StreamHandler(sys.stdout)
@@ -92,9 +105,9 @@ def get_handlers(app):
return not ("GET /static/" in msg and " 200 " in msg)
logging.getLogger("werkzeug").addFilter(is_200_static_log)
color_formatter = ColorFormatter(LOG_FORMAT, TIME_FORMAT)
# human readable stdout logs
handlers.append(configure_handler(stream_handler, app, standard_formatter))
handlers.append(configure_handler(stream_handler, app, color_formatter))
return handlers

322
poetry.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -24,7 +24,7 @@ itsdangerous = "~=2.2"
jinja2 = "^3.1.6"
newrelic = "*"
notifications-python-client = "==10.0.1"
pyexcel = "==0.7.2"
pyexcel = "==0.7.3"
pyexcel-io = "==0.6.7"
pyexcel-ods3 = "==0.6.1"
pyexcel-xls = "==0.7.1"
@@ -48,7 +48,7 @@ cryptography = "^44.0.2"
flask-redis = "^0.4.0"
geojson = "^3.1.0"
jmespath = "^1.0.1"
numpy = "^2.2.4"
numpy = "^2.2.5"
ordered-set = "^4.1.0"
phonenumbers = "^9.0.3"
pycparser = "^2.22"

View File

@@ -16,16 +16,22 @@ def test_non_logged_in_user_can_see_homepage(
client_request.logout()
page = client_request.get("main.index", _test_page_title=False)
assert page.h1.text.strip() == (
"Reach people where they are with government-powered text messages"
)
heading = page.h1.text.strip()
assert heading in [
"Reach people where they are with government-powered text messages",
"There's currently a technical issue.",
]
# Assert the entire HTML of the button to include the image
button = page.select_one(
"a.usa-button.login-button.login-button--primary.margin-right-2"
)
assert "Sign in with" in button.text.strip() # Assert button text
assert button.find("img")["alt"] == "Login.gov logo" # Assert image presence
if heading == "There's currently a technical issue.":
assert button is None
else:
assert button is not None
assert "Sign in with" in button.text.strip()
assert button.find("img")["alt"] == "Login.gov logo"
assert page.select_one("meta[name=description]") is not None
assert page.select_one("#whos-using-notify a") is None

View File

@@ -1264,7 +1264,7 @@ def test_send_one_off_has_correct_page_title(
(
0,
{},
"phone number",
"Country code and phone number",
),
(
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("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(
SERVICE_TWO_ID,
TEMPLATE_ONE_ID,

View File

@@ -164,12 +164,12 @@ def test_should_show_empty_text_box(
)
textbox = page.select_one(
"[data-module=autofocus][data-force-focus=True] .usa-input"
".usa-input"
)
assert "value" not in textbox
assert textbox["name"] == "placeholder_value"
assert textbox["class"] == [
"usa-input",
"form-control", "usa-input",
]
# data-module=autofocus is set on a containing element so it
# shouldnt also be set on the textbox itself

View File

@@ -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');
});

View File

@@ -1,13 +1,12 @@
const helpers = require('./support/helpers.js');
beforeAll(() => {
require('../../app/assets/javascripts/fileUpload.js');
});
const { announceUploadStatusFromElement } = require('../../app/assets/javascripts/fileUpload.js');
afterAll(() => {
require('./support/teardown.js');
});
describe('File upload', () => {
let form;
@@ -72,12 +71,116 @@ describe('File upload', () => {
});
test("It should add a link to cancel the upload by reloading the page", () => {
expect(form.querySelector("a[href='']")).not.toBeNull();
test("It should display a disabled Uploading button", () => {
const uploadingButton = form.querySelector("button.uploading-button");
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');
});
});

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);
});
});
});