Skip to content

Commit 81c93ac

Browse files
committed
Fix #1757: announce settings save status
1 parent d733aa9 commit 81c93ac

7 files changed

Lines changed: 214 additions & 2 deletions

File tree

admin/class-enqueue-admin.php

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,9 +83,13 @@ public static function maybe_enqueue_admin_and_editor_app_scripts() {
8383
// let extensions supply the correct ID (e.g. a Pro virtual-page ID).
8484
$post_id = apply_filters( 'edac_filter_admin_post_id', $post_id );
8585

86-
wp_enqueue_script( 'edac', plugin_dir_url( EDAC_PLUGIN_FILE ) . 'build/admin.bundle.js', [ 'jquery' ], EDAC_VERSION, false );
87-
wp_set_script_translations( 'edac', 'accessibility-checker', plugin_dir_path( EDAC_PLUGIN_FILE ) . 'languages' );
86+
$admin_script_dependencies = [ 'jquery' ];
87+
if ( 'accessibility_checker_settings' === $page ) {
88+
$admin_script_dependencies[] = 'wp-a11y';
89+
}
8890

91+
wp_enqueue_script( 'edac', plugin_dir_url( EDAC_PLUGIN_FILE ) . 'build/admin.bundle.js', $admin_script_dependencies, EDAC_VERSION, false );
92+
wp_set_script_translations( 'edac', 'accessibility-checker', plugin_dir_path( EDAC_PLUGIN_FILE ) . 'languages' );
8993
wp_localize_script(
9094
'edac',
9195
'edac_script_vars',

partials/settings-page.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,8 @@ function ( $a, $b ) {
110110

111111
<h1><?php echo esc_html( get_admin_page_title() ); ?></h1>
112112

113+
<?php settings_errors(); ?>
114+
113115
<?php
114116
if ( $edac_settings_tab_items ) {
115117
echo '<nav class="nav-tab-wrapper" aria-label="' . esc_attr__( 'Accessibility Checker Settings', 'accessibility-checker' ) . '">';

src/admin/index.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88

99
import { initFixesInputStateHandler } from './fixes-page/conditional-disable-settings';
1010
import { initRequiredSetup } from './fixes-page/conditional-required-settings';
11+
import { announceSettingsSaveStatus } from './settings/announce-settings-save-status';
1112
import { inlineSettingsProUpsell } from '../common/settings-pro-callout';
1213

1314
// eslint-disable-next-line camelcase
@@ -17,6 +18,8 @@ const edacScriptVars = edac_script_vars;
1718
'use strict';
1819

1920
jQuery( function() {
21+
announceSettingsSaveStatus( window.wp?.a11y );
22+
2023
if ( document.getElementById( 'edac-fixes-page' ) ) {
2124
initFixesInputStateHandler();
2225
initRequiredSetup();
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/**
2+
* Announce the result of saving Accessibility Checker settings.
3+
*
4+
* @param {Object} a11y WordPress accessibility utilities.
5+
* @param {Window} browserWindow Browser window used to schedule the announcement.
6+
*/
7+
export const announceSettingsSaveStatus = ( a11y, browserWindow = window ) => {
8+
const notice = document.getElementById( 'setting-error-settings_updated' );
9+
10+
if ( ! notice || ! a11y || typeof a11y.speak !== 'function' ) {
11+
return;
12+
}
13+
14+
const message = notice.textContent.trim();
15+
if ( ! message ) {
16+
return;
17+
}
18+
19+
const politeness = notice.classList.contains( 'notice-error' ) ? 'assertive' : 'polite';
20+
const speakAfterPageLoad = () => {
21+
// Give assistive technology time to finish announcing the newly loaded page.
22+
browserWindow.setTimeout( () => {
23+
a11y.speak( message, politeness );
24+
}, 1000 );
25+
};
26+
27+
if ( document.readyState === 'complete' ) {
28+
speakAfterPageLoad();
29+
return;
30+
}
31+
32+
browserWindow.addEventListener( 'load', speakAfterPageLoad, { once: true } );
33+
};
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import { announceSettingsSaveStatus } from '../../../src/admin/settings/announce-settings-save-status';
2+
3+
describe( 'announceSettingsSaveStatus', () => {
4+
let a11y;
5+
let readyStateSpy;
6+
7+
beforeEach( () => {
8+
jest.useFakeTimers();
9+
document.body.innerHTML = '';
10+
a11y = { speak: jest.fn() };
11+
readyStateSpy = jest.spyOn( document, 'readyState', 'get' ).mockReturnValue( 'interactive' );
12+
} );
13+
14+
afterEach( () => {
15+
readyStateSpy.mockRestore();
16+
jest.useRealTimers();
17+
} );
18+
19+
test( 'announces a successful save politely after the page finishes loading', () => {
20+
document.body.innerHTML = `
21+
<div id="setting-error-settings_updated" class="notice notice-success settings-error">
22+
<p><strong>Settings saved.</strong></p>
23+
</div>
24+
`;
25+
26+
announceSettingsSaveStatus( a11y );
27+
28+
window.dispatchEvent( new Event( 'load' ) );
29+
expect( a11y.speak ).not.toHaveBeenCalled();
30+
31+
jest.advanceTimersByTime( 1000 );
32+
expect( a11y.speak ).toHaveBeenCalledWith( 'Settings saved.', 'polite' );
33+
} );
34+
35+
test( 'announces a save error assertively', () => {
36+
document.body.innerHTML = `
37+
<div id="setting-error-settings_updated" class="notice notice-error settings-error">
38+
<p><strong>Settings save failed.</strong></p>
39+
</div>
40+
`;
41+
42+
announceSettingsSaveStatus( a11y );
43+
44+
window.dispatchEvent( new Event( 'load' ) );
45+
jest.advanceTimersByTime( 1000 );
46+
expect( a11y.speak ).toHaveBeenCalledWith( 'Settings save failed.', 'assertive' );
47+
} );
48+
49+
test( 'announces when the page has already finished loading', () => {
50+
readyStateSpy.mockReturnValue( 'complete' );
51+
document.body.innerHTML = `
52+
<div id="setting-error-settings_updated" class="notice notice-success settings-error">
53+
<p><strong>Settings saved.</strong></p>
54+
</div>
55+
`;
56+
57+
announceSettingsSaveStatus( a11y );
58+
59+
jest.advanceTimersByTime( 1000 );
60+
expect( a11y.speak ).toHaveBeenCalledWith( 'Settings saved.', 'polite' );
61+
} );
62+
63+
test( 'does not announce when the settings notice is missing', () => {
64+
announceSettingsSaveStatus( a11y );
65+
66+
window.dispatchEvent( new Event( 'load' ) );
67+
jest.runAllTimers();
68+
expect( a11y.speak ).not.toHaveBeenCalled();
69+
} );
70+
71+
test( 'does not announce an empty settings notice', () => {
72+
document.body.innerHTML = '<div id="setting-error-settings_updated"></div>';
73+
74+
announceSettingsSaveStatus( a11y );
75+
76+
window.dispatchEvent( new Event( 'load' ) );
77+
jest.runAllTimers();
78+
expect( a11y.speak ).not.toHaveBeenCalled();
79+
} );
80+
81+
test( 'does not announce when the accessibility utility is unavailable', () => {
82+
document.body.innerHTML = `
83+
<div id="setting-error-settings_updated">
84+
<p><strong>Settings saved.</strong></p>
85+
</div>
86+
`;
87+
88+
announceSettingsSaveStatus();
89+
90+
window.dispatchEvent( new Event( 'load' ) );
91+
jest.runAllTimers();
92+
expect( a11y.speak ).not.toHaveBeenCalled();
93+
} );
94+
} );

tests/phpunit/Admin/EnqueueAdminTest.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ protected function tearDown(): void {
7373

7474
global $wp_scripts, $wp_styles;
7575
unset( $wp_scripts, $wp_styles, $GLOBALS['current_screen'] );
76+
unset( $_GET['page'] );
7677

7778
unset( $this->enqueue_admin );
7879
}
@@ -89,13 +90,31 @@ public function testEnqueueBaseScriptInAdminNonEditorPage() {
8990

9091
$this->assertTrue( wp_script_is( 'edac', 'enqueued' ) );
9192
$this->assertFalse( wp_script_is( 'edac-editor-app', 'enqueued' ) );
93+
$this->assertNotContains( 'wp-a11y', $wp_scripts->registered['edac']->deps );
9294

9395
$localized_data = $wp_scripts->get_data( 'edac', 'data' );
9496
$this->assertIsString( $localized_data );
9597
$this->assertStringContainsString( 'utm_content=__name__', $localized_data );
9698
$this->assertStringNotContainsString( 'utm-content=__name__', $localized_data );
9799
}
98100

101+
/**
102+
* Test that the base script loads the accessibility utility on the settings page.
103+
*
104+
* @return void
105+
*/
106+
public function testEnqueueBaseScriptWithWpA11yOnSettingsPage() {
107+
108+
global $wp_scripts;
109+
110+
$_GET['page'] = 'accessibility_checker_settings';
111+
112+
$this->enqueue_admin::maybe_enqueue_admin_and_editor_app_scripts();
113+
114+
$this->assertTrue( wp_script_is( 'edac', 'enqueued' ) );
115+
$this->assertContains( 'wp-a11y', $wp_scripts->registered['edac']->deps );
116+
}
117+
99118
/**
100119
* Test localized pro URL includes the expected underscore UTM content key.
101120
*
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
<?php
2+
/**
3+
* Accessibility Checker
4+
*
5+
* @package AccessibilityChecker
6+
*/
7+
8+
/**
9+
* Test the settings page partial.
10+
*
11+
* @package AccessibilityChecker
12+
*/
13+
class SettingsPagePartialTest extends WP_UnitTestCase {
14+
15+
/**
16+
* Prepare settings errors for each test.
17+
*
18+
* @return void
19+
*/
20+
protected function setUp(): void {
21+
parent::setUp();
22+
23+
$GLOBALS['wp_settings_errors'] = [];
24+
$GLOBALS['title'] = 'Accessibility Checker Settings';
25+
}
26+
27+
/**
28+
* Clean up settings errors after each test.
29+
*
30+
* @return void
31+
*/
32+
protected function tearDown(): void {
33+
unset( $GLOBALS['wp_settings_errors'], $GLOBALS['title'] );
34+
35+
parent::tearDown();
36+
}
37+
38+
/**
39+
* Test that the settings page renders the standard saved notice.
40+
*/
41+
public function testPartialRendersSettingsSavedNotice() {
42+
add_settings_error(
43+
'general',
44+
'settings_updated',
45+
'Settings saved.',
46+
'success'
47+
);
48+
49+
ob_start();
50+
include dirname( __DIR__, 3 ) . '/partials/settings-page.php';
51+
$output = ob_get_clean();
52+
53+
$this->assertStringContainsString( "id='setting-error-settings_updated'", $output );
54+
$this->assertStringContainsString( 'notice-success', $output );
55+
$this->assertStringContainsString( 'Settings saved.', $output );
56+
}
57+
}

0 commit comments

Comments
 (0)