Skip to content
This repository was archived by the owner on Jan 13, 2025. It is now read-only.

Commit b92a170

Browse files
committed
WIP: Add unit tests for announce()
1 parent c924dd5 commit b92a170

File tree

4 files changed

+161
-6
lines changed

4 files changed

+161
-6
lines changed

packages/mdc-snackbar/index.js

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
import {MDCComponent} from '@material/base/index';
2525
import MDCSnackbarFoundation from './foundation';
2626
import {strings} from './constants';
27-
import {announce} from './util';
27+
import * as util from './util';
2828
import * as ponyfill from '@material/dom/ponyfill';
2929

3030
const {
@@ -49,13 +49,23 @@ class MDCSnackbar extends MDCComponent {
4949
/** @type {!HTMLElement} */
5050
this.actionButtonEl_;
5151

52+
/** @type {function(!HTMLElement, !HTMLElement=): void} */
53+
this.announce_;
54+
5255
/** @private {!Function} */
5356
this.handleKeyDown_;
5457

5558
/** @private {!Function} */
5659
this.handleSurfaceClick_;
5760
}
5861

62+
/**
63+
* @param {function(): function(!HTMLElement, !HTMLElement=):void} announceFactory
64+
*/
65+
initialize(announceFactory = () => util.announce) {
66+
this.announce_ = announceFactory();
67+
}
68+
5969
initialSyncWithDOM() {
6070
this.surfaceEl_ = /** @type {!HTMLElement} */ (this.root_.querySelector(SURFACE_SELECTOR));
6171
this.labelEl_ = /** @type {!HTMLElement} */ (this.root_.querySelector(LABEL_SELECTOR));
@@ -101,7 +111,7 @@ class MDCSnackbar extends MDCComponent {
101111
return new MDCSnackbarFoundation({
102112
addClass: (className) => this.root_.classList.add(className),
103113
removeClass: (className) => this.root_.classList.remove(className),
104-
announce: () => announce(this.labelEl_),
114+
announce: () => this.announce_(this.labelEl_),
105115
notifyOpening: () => this.emit(OPENING_EVENT, {}),
106116
notifyOpened: () => this.emit(OPENED_EVENT, {}),
107117
notifyClosing: (reason) => this.emit(CLOSING_EVENT, reason ? {reason} : {}),

test/unit/mdc-snackbar/foundation.test.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,13 @@ test('#destroy cancels all timers', () => {
9292
td.verify(foundation.close(strings.REASON_DISMISS), {times: 0});
9393
});
9494

95+
test('#open announces text to screen readers', () => {
96+
const {foundation, mockAdapter} = setupTest();
97+
98+
foundation.open();
99+
td.verify(mockAdapter.announce(), {times: 1});
100+
});
101+
95102
test('#open adds CSS classes after rAF', () => {
96103
const {foundation, mockAdapter} = setupTest();
97104
const clock = installClock();

test/unit/mdc-snackbar/mdc-snackbar.test.js

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,9 @@ function setupTest(fixture = getFixture()) {
6666
const actions = fixture.querySelector('.mdc-snackbar__actions');
6767
const actionButton = fixture.querySelector('.mdc-snackbar__action-button');
6868
const actionIcon = fixture.querySelector('.mdc-snackbar__action-icon');
69-
const component = new MDCSnackbar(root);
70-
return {component, root, surface, label, actions, actionButton, actionIcon};
69+
const announce = td.func('announce');
70+
const component = new MDCSnackbar(root, undefined, () => announce);
71+
return {component, announce, root, surface, label, actions, actionButton, actionIcon};
7172
}
7273

7374
/**
@@ -91,10 +92,12 @@ function setupTestWithMocks(fixture = getFixture()) {
9192
/** @type {!MDCSnackbarFoundation} */
9293
const mockFoundation = new MockFoundationCtor();
9394

95+
const announce = td.func('announce');
96+
9497
/** @type {!MDCSnackbar} */
95-
const component = new MDCSnackbar(root, mockFoundation);
98+
const component = new MDCSnackbar(root, mockFoundation, () => announce);
9699

97-
return {component, mockFoundation, root, surface, label, actions, actionButton, actionIcon};
100+
return {component, mockFoundation, announce, root, surface, label, actions, actionButton, actionIcon};
98101
}
99102

100103
suite('MDCSnackbar');
@@ -149,6 +152,13 @@ test('clicking on surface does nothing', () => {
149152
component.destroy();
150153
});
151154

155+
test('#open announces to screen readers', () => {
156+
const {component, announce, label} = setupTest();
157+
158+
component.open();
159+
td.verify(announce(label), {times: 1});
160+
});
161+
152162
test('#open forwards to MDCSnackbarFoundation#open', () => {
153163
const {component, mockFoundation} = setupTestWithMocks();
154164

test/unit/mdc-snackbar/util.test.js

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
/**
2+
* @license
3+
* Copyright 2017 Google Inc.
4+
*
5+
* Permission is hereby granted, free of charge, to any person obtaining a copy
6+
* of this software and associated documentation files (the "Software"), to deal
7+
* in the Software without restriction, including without limitation the rights
8+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
* copies of the Software, and to permit persons to whom the Software is
10+
* furnished to do so, subject to the following conditions:
11+
*
12+
* The above copyright notice and this permission notice shall be included in
13+
* all copies or substantial portions of the Software.
14+
*
15+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21+
* THE SOFTWARE.
22+
*/
23+
24+
import bel from 'bel';
25+
import {assert} from 'chai';
26+
import {install as installClock} from '../helpers/clock';
27+
import * as util from '../../../packages/mdc-snackbar/util';
28+
import {strings, numbers} from '../../../packages/mdc-snackbar/constants';
29+
30+
const {ARIA_LIVE_DELAY_MS} = numbers;
31+
const {ARIA_LIVE_LABEL_TEXT_ATTR} = strings;
32+
33+
suite('MDCSnackbar - util');
34+
35+
test('#announce temporarily disables ARIA attributes and then restores them', () => {
36+
const fixture = bel`
37+
<div>
38+
<div class="aria" role="status" aria-live="polite">
39+
<div class="label"></div>
40+
</div>
41+
</div>`;
42+
const clock = installClock();
43+
44+
const ariaEl = fixture.querySelector('.aria');
45+
const labelEl = fixture.querySelector('.label');
46+
47+
const labelText = 'Foo';
48+
labelEl.textContent = labelText;
49+
50+
util.announce(ariaEl, labelEl);
51+
52+
// Trim to remove `&nbsp;` (see comment in util.js)
53+
assert.equal(labelEl.textContent.trim(), '');
54+
assert.equal(ariaEl.getAttribute('aria-live'), 'off');
55+
56+
clock.tick(ARIA_LIVE_DELAY_MS);
57+
58+
assert.equal(labelEl.textContent, labelText);
59+
assert.equal(ariaEl.getAttribute('aria-live'), 'polite');
60+
});
61+
62+
test('#announce prevents flicker by displaying label text on ::before pseudo-element and then removing it', () => {
63+
const fixture = bel`
64+
<div>
65+
<div class="aria" role="status" aria-live="polite">
66+
<div class="label"></div>
67+
</div>
68+
</div>`;
69+
const clock = installClock();
70+
71+
const ariaEl = fixture.querySelector('.aria');
72+
const labelEl = fixture.querySelector('.label');
73+
74+
const labelText = 'Foo';
75+
labelEl.textContent = labelText;
76+
77+
util.announce(ariaEl, labelEl);
78+
79+
assert.equal(labelEl.getAttribute(ARIA_LIVE_LABEL_TEXT_ATTR), labelText);
80+
81+
clock.tick(ARIA_LIVE_DELAY_MS);
82+
83+
assert.equal(labelEl.getAttribute(ARIA_LIVE_LABEL_TEXT_ATTR), null);
84+
});
85+
86+
test('#announce second argument is optional', () => {
87+
const fixture = bel`
88+
<div>
89+
<div class="aria label" role="status" aria-live="polite"></div>
90+
</div>`;
91+
const clock = installClock();
92+
93+
const ariaEl = fixture.querySelector('.aria');
94+
95+
const labelText = 'Foo';
96+
ariaEl.textContent = labelText;
97+
98+
util.announce(ariaEl);
99+
100+
// Trim to remove `&nbsp;` (see comment in util.js)
101+
assert.equal(ariaEl.textContent.trim(), '');
102+
assert.equal(ariaEl.getAttribute(ARIA_LIVE_LABEL_TEXT_ATTR), labelText);
103+
assert.equal(ariaEl.getAttribute('aria-live'), 'off');
104+
105+
clock.tick(ARIA_LIVE_DELAY_MS);
106+
107+
assert.equal(ariaEl.textContent, labelText);
108+
assert.equal(ariaEl.getAttribute(ARIA_LIVE_LABEL_TEXT_ATTR), null);
109+
assert.equal(ariaEl.getAttribute('aria-live'), 'polite');
110+
});
111+
112+
test('#announce does nothing if textContent is empty', () => {
113+
const fixture = bel`
114+
<div>
115+
<div class="aria" role="status" aria-live="polite">
116+
<div class="label"></div>
117+
</div>
118+
</div>`;
119+
120+
const ariaEl = fixture.querySelector('.aria');
121+
const labelEl = fixture.querySelector('.label');
122+
123+
util.announce(ariaEl, labelEl);
124+
125+
assert.equal(labelEl.textContent.trim(), '');
126+
assert.equal(labelEl.getAttribute(ARIA_LIVE_LABEL_TEXT_ATTR), null);
127+
assert.equal(ariaEl.getAttribute('aria-live'), 'polite');
128+
});

0 commit comments

Comments
 (0)