ngwr 11.1.0 → 11.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/circular-text/styles/_index.scss +11 -0
- package/command-palette/styles/_index.scss +12 -0
- package/fesm2022/ngwr-action-sheet-testing.mjs +313 -0
- package/fesm2022/ngwr-action-sheet-testing.mjs.map +1 -0
- package/fesm2022/ngwr-bar-chart-testing.mjs +89 -0
- package/fesm2022/ngwr-bar-chart-testing.mjs.map +1 -0
- package/fesm2022/ngwr-blur-text-testing.mjs +203 -0
- package/fesm2022/ngwr-blur-text-testing.mjs.map +1 -0
- package/fesm2022/ngwr-blur-text.mjs +15 -1
- package/fesm2022/ngwr-blur-text.mjs.map +1 -1
- package/fesm2022/ngwr-border-glow-testing.mjs +325 -0
- package/fesm2022/ngwr-border-glow-testing.mjs.map +1 -0
- package/fesm2022/ngwr-calendar-heatmap-testing.mjs +95 -0
- package/fesm2022/ngwr-calendar-heatmap-testing.mjs.map +1 -0
- package/fesm2022/ngwr-calendar-testing.mjs +368 -0
- package/fesm2022/ngwr-calendar-testing.mjs.map +1 -0
- package/fesm2022/ngwr-calendar.mjs +43 -3
- package/fesm2022/ngwr-calendar.mjs.map +1 -1
- package/fesm2022/ngwr-cascader.mjs +12 -0
- package/fesm2022/ngwr-cascader.mjs.map +1 -1
- package/fesm2022/ngwr-checkbox.mjs +8 -0
- package/fesm2022/ngwr-checkbox.mjs.map +1 -1
- package/fesm2022/ngwr-circular-text-testing.mjs +230 -0
- package/fesm2022/ngwr-circular-text-testing.mjs.map +1 -0
- package/fesm2022/ngwr-circular-text.mjs +2 -2
- package/fesm2022/ngwr-circular-text.mjs.map +1 -1
- package/fesm2022/ngwr-color-picker-testing.mjs +432 -0
- package/fesm2022/ngwr-color-picker-testing.mjs.map +1 -0
- package/fesm2022/ngwr-color-picker.mjs +29 -3
- package/fesm2022/ngwr-color-picker.mjs.map +1 -1
- package/fesm2022/ngwr-context-menu.mjs +25 -5
- package/fesm2022/ngwr-context-menu.mjs.map +1 -1
- package/fesm2022/ngwr-date-picker-testing.mjs +10 -79
- package/fesm2022/ngwr-date-picker-testing.mjs.map +1 -1
- package/fesm2022/ngwr-decrypt-text-testing.mjs +261 -0
- package/fesm2022/ngwr-decrypt-text-testing.mjs.map +1 -0
- package/fesm2022/ngwr-decrypt-text.mjs +6 -1
- package/fesm2022/ngwr-decrypt-text.mjs.map +1 -1
- package/fesm2022/ngwr-donut-chart-testing.mjs +93 -0
- package/fesm2022/ngwr-donut-chart-testing.mjs.map +1 -0
- package/fesm2022/ngwr-drawer.mjs +36 -7
- package/fesm2022/ngwr-drawer.mjs.map +1 -1
- package/fesm2022/ngwr-dropdown.mjs +32 -2
- package/fesm2022/ngwr-dropdown.mjs.map +1 -1
- package/fesm2022/ngwr-event-calendar-testing.mjs +446 -0
- package/fesm2022/ngwr-event-calendar-testing.mjs.map +1 -0
- package/fesm2022/ngwr-event-calendar.mjs +43 -18
- package/fesm2022/ngwr-event-calendar.mjs.map +1 -1
- package/fesm2022/ngwr-falling-text-testing.mjs +218 -0
- package/fesm2022/ngwr-falling-text-testing.mjs.map +1 -0
- package/fesm2022/ngwr-fuzzy-text-testing.mjs +134 -0
- package/fesm2022/ngwr-fuzzy-text-testing.mjs.map +1 -0
- package/fesm2022/ngwr-gauge-testing.mjs +86 -0
- package/fesm2022/ngwr-gauge-testing.mjs.map +1 -0
- package/fesm2022/ngwr-glitch-text-testing.mjs +181 -0
- package/fesm2022/ngwr-glitch-text-testing.mjs.map +1 -0
- package/fesm2022/ngwr-gradient-text-testing.mjs +223 -0
- package/fesm2022/ngwr-gradient-text-testing.mjs.map +1 -0
- package/fesm2022/ngwr-i18n-en.mjs +14 -0
- package/fesm2022/ngwr-i18n-en.mjs.map +1 -1
- package/fesm2022/ngwr-i18n-ru.mjs +14 -0
- package/fesm2022/ngwr-i18n-ru.mjs.map +1 -1
- package/fesm2022/ngwr-image-cropper-testing.mjs +156 -0
- package/fesm2022/ngwr-image-cropper-testing.mjs.map +1 -0
- package/fesm2022/ngwr-image-cropper.mjs +10 -3
- package/fesm2022/ngwr-image-cropper.mjs.map +1 -1
- package/fesm2022/ngwr-input-number.mjs +73 -6
- package/fesm2022/ngwr-input-number.mjs.map +1 -1
- package/fesm2022/ngwr-knob-testing.mjs +220 -0
- package/fesm2022/ngwr-knob-testing.mjs.map +1 -0
- package/fesm2022/ngwr-lightbox-testing.mjs +223 -0
- package/fesm2022/ngwr-lightbox-testing.mjs.map +1 -0
- package/fesm2022/ngwr-lightbox.mjs +9 -2
- package/fesm2022/ngwr-lightbox.mjs.map +1 -1
- package/fesm2022/ngwr-line-chart-testing.mjs +119 -0
- package/fesm2022/ngwr-line-chart-testing.mjs.map +1 -0
- package/fesm2022/ngwr-marquee-testing.mjs +336 -0
- package/fesm2022/ngwr-marquee-testing.mjs.map +1 -0
- package/fesm2022/ngwr-meter-group-testing.mjs +80 -0
- package/fesm2022/ngwr-meter-group-testing.mjs.map +1 -0
- package/fesm2022/ngwr-overlay.mjs +28 -1
- package/fesm2022/ngwr-overlay.mjs.map +1 -1
- package/fesm2022/ngwr-popover.mjs +14 -0
- package/fesm2022/ngwr-popover.mjs.map +1 -1
- package/fesm2022/ngwr-rotating-text-testing.mjs +220 -0
- package/fesm2022/ngwr-rotating-text-testing.mjs.map +1 -0
- package/fesm2022/ngwr-rotating-text.mjs +2 -2
- package/fesm2022/ngwr-rotating-text.mjs.map +1 -1
- package/fesm2022/ngwr-select.mjs +70 -11
- package/fesm2022/ngwr-select.mjs.map +1 -1
- package/fesm2022/ngwr-shiny-text-testing.mjs +237 -0
- package/fesm2022/ngwr-shiny-text-testing.mjs.map +1 -0
- package/fesm2022/ngwr-slider.mjs +44 -6
- package/fesm2022/ngwr-slider.mjs.map +1 -1
- package/fesm2022/ngwr-sparkline-testing.mjs +76 -0
- package/fesm2022/ngwr-sparkline-testing.mjs.map +1 -0
- package/fesm2022/ngwr-speed-dial-testing.mjs +279 -0
- package/fesm2022/ngwr-speed-dial-testing.mjs.map +1 -0
- package/fesm2022/ngwr-splash-cursor-testing.mjs +89 -0
- package/fesm2022/ngwr-splash-cursor-testing.mjs.map +1 -0
- package/fesm2022/ngwr-split-text-testing.mjs +224 -0
- package/fesm2022/ngwr-split-text-testing.mjs.map +1 -0
- package/fesm2022/ngwr-split-text.mjs +15 -1
- package/fesm2022/ngwr-split-text.mjs.map +1 -1
- package/fesm2022/ngwr-splitter-testing.mjs +245 -0
- package/fesm2022/ngwr-splitter-testing.mjs.map +1 -0
- package/fesm2022/ngwr-spotlight-card-testing.mjs +368 -0
- package/fesm2022/ngwr-spotlight-card-testing.mjs.map +1 -0
- package/fesm2022/ngwr-star-border-testing.mjs +172 -0
- package/fesm2022/ngwr-star-border-testing.mjs.map +1 -0
- package/fesm2022/ngwr-table.mjs +13 -3
- package/fesm2022/ngwr-table.mjs.map +1 -1
- package/fesm2022/ngwr-tabs.mjs +15 -1
- package/fesm2022/ngwr-tabs.mjs.map +1 -1
- package/fesm2022/ngwr-tilt-card-testing.mjs +190 -0
- package/fesm2022/ngwr-tilt-card-testing.mjs.map +1 -0
- package/fesm2022/ngwr-tilt-card.mjs +19 -1
- package/fesm2022/ngwr-tilt-card.mjs.map +1 -1
- package/fesm2022/ngwr-toast.mjs +36 -7
- package/fesm2022/ngwr-toast.mjs.map +1 -1
- package/fesm2022/ngwr-tour-testing.mjs +173 -0
- package/fesm2022/ngwr-tour-testing.mjs.map +1 -0
- package/fesm2022/ngwr-tour.mjs +2 -2
- package/fesm2022/ngwr-tour.mjs.map +1 -1
- package/fesm2022/ngwr-tree.mjs +10 -3
- package/fesm2022/ngwr-tree.mjs.map +1 -1
- package/fesm2022/ngwr-typewriter-testing.mjs +228 -0
- package/fesm2022/ngwr-typewriter-testing.mjs.map +1 -0
- package/fesm2022/ngwr-version.mjs +1 -1
- package/fesm2022/ngwr-version.mjs.map +1 -1
- package/fesm2022/ngwr-waves-testing.mjs +128 -0
- package/fesm2022/ngwr-waves-testing.mjs.map +1 -0
- package/fesm2022/ngwr-window-testing.mjs +361 -0
- package/fesm2022/ngwr-window-testing.mjs.map +1 -0
- package/fesm2022/ngwr-window.mjs +84 -10
- package/fesm2022/ngwr-window.mjs.map +1 -1
- package/i18n/en.json +16 -2
- package/i18n/ru.json +16 -2
- package/llms-full.txt +147 -0
- package/llms.txt +27 -8
- package/mcp/providers.js +29 -0
- package/mcp/tools.js +1 -29
- package/package.json +145 -1
- package/skills/ngwr/SKILL.md +115 -0
- package/skills/ngwr/references/catalog.md +212 -0
- package/skills/ngwr/references/setup.md +73 -0
- package/types/ngwr-action-sheet-testing.d.ts +260 -0
- package/types/ngwr-bar-chart-testing.d.ts +77 -0
- package/types/ngwr-blur-text-testing.d.ts +180 -0
- package/types/ngwr-blur-text.d.ts +12 -0
- package/types/ngwr-border-glow-testing.d.ts +219 -0
- package/types/ngwr-calendar-heatmap-testing.d.ts +79 -0
- package/types/ngwr-calendar-testing.d.ts +277 -0
- package/types/ngwr-calendar.d.ts +10 -0
- package/types/ngwr-checkbox.d.ts +13 -0
- package/types/ngwr-circular-text-testing.d.ts +196 -0
- package/types/ngwr-color-picker-testing.d.ts +291 -0
- package/types/ngwr-color-picker.d.ts +13 -1
- package/types/ngwr-context-menu.d.ts +15 -1
- package/types/ngwr-date-picker-testing.d.ts +6 -66
- package/types/ngwr-decrypt-text-testing.d.ts +222 -0
- package/types/ngwr-donut-chart-testing.d.ts +79 -0
- package/types/ngwr-drawer.d.ts +12 -0
- package/types/ngwr-dropdown.d.ts +9 -0
- package/types/ngwr-event-calendar-testing.d.ts +301 -0
- package/types/ngwr-event-calendar.d.ts +9 -6
- package/types/ngwr-falling-text-testing.d.ts +186 -0
- package/types/ngwr-fuzzy-text-testing.d.ts +130 -0
- package/types/ngwr-gauge-testing.d.ts +71 -0
- package/types/ngwr-glitch-text-testing.d.ts +172 -0
- package/types/ngwr-gradient-text-testing.d.ts +173 -0
- package/types/ngwr-image-cropper-testing.d.ts +134 -0
- package/types/ngwr-image-cropper.d.ts +1 -0
- package/types/ngwr-input-number.d.ts +33 -1
- package/types/ngwr-knob-testing.d.ts +150 -0
- package/types/ngwr-lightbox-testing.d.ts +154 -0
- package/types/ngwr-lightbox.d.ts +6 -0
- package/types/ngwr-line-chart-testing.d.ts +93 -0
- package/types/ngwr-marquee-testing.d.ts +280 -0
- package/types/ngwr-meter-group-testing.d.ts +67 -0
- package/types/ngwr-overlay.d.ts +19 -1
- package/types/ngwr-rotating-text-testing.d.ts +191 -0
- package/types/ngwr-select.d.ts +32 -0
- package/types/ngwr-shiny-text-testing.d.ts +165 -0
- package/types/ngwr-slider.d.ts +15 -0
- package/types/ngwr-sparkline-testing.d.ts +69 -0
- package/types/ngwr-speed-dial-testing.d.ts +195 -0
- package/types/ngwr-splash-cursor-testing.d.ts +95 -0
- package/types/ngwr-split-text-testing.d.ts +209 -0
- package/types/ngwr-split-text.d.ts +12 -0
- package/types/ngwr-splitter-testing.d.ts +174 -0
- package/types/ngwr-spotlight-card-testing.d.ts +303 -0
- package/types/ngwr-star-border-testing.d.ts +135 -0
- package/types/ngwr-table.d.ts +10 -1
- package/types/ngwr-tilt-card-testing.d.ts +158 -0
- package/types/ngwr-tilt-card.d.ts +2 -0
- package/types/ngwr-toast.d.ts +14 -5
- package/types/ngwr-tour-testing.d.ts +143 -0
- package/types/ngwr-tree.d.ts +7 -0
- package/types/ngwr-typewriter-testing.d.ts +201 -0
- package/types/ngwr-version.d.ts +1 -1
- package/types/ngwr-waves-testing.d.ts +121 -0
- package/types/ngwr-window-testing.d.ts +263 -0
- package/types/ngwr-window.d.ts +27 -10
- package/window/styles/_index.scss +44 -16
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
import { ComponentHarness, HarnessPredicate, TestKey } from '@angular/cdk/testing';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @license
|
|
5
|
+
*
|
|
6
|
+
* Use of this source code is governed by an MIT-style license that can be
|
|
7
|
+
* found in the LICENSE file at https://github.com/thekhegay/ngwr/blob/main/LICENSE
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* Test harness for one action inside a {@link WrSpeedDialHarness}.
|
|
11
|
+
*
|
|
12
|
+
* The label is read from `aria-label` rather than from the button's text, and that
|
|
13
|
+
* is the whole point of the control: an action renders an ICON, or — with no icon —
|
|
14
|
+
* the first glyph of its label. Its visible text is therefore `"S"` for "Share", or
|
|
15
|
+
* nothing at all. {@link getInitial} answers what is drawn; {@link getLabel} answers
|
|
16
|
+
* what it is.
|
|
17
|
+
*
|
|
18
|
+
* @see https://ngwr.dev/guides/testing
|
|
19
|
+
*/
|
|
20
|
+
class WrSpeedDialActionHarness extends ComponentHarness {
|
|
21
|
+
/** The action button. `role="menuitem"` sits on this element too. */
|
|
22
|
+
static hostSelector = '.wr-speed-dial__action';
|
|
23
|
+
/** Build a predicate that narrows the query — pass to `getHarness` / `getAllHarnesses`. */
|
|
24
|
+
static with(options = {}) {
|
|
25
|
+
return new HarnessPredicate(WrSpeedDialActionHarness, options)
|
|
26
|
+
.addOption('label', options.label, (harness, label) => HarnessPredicate.stringMatches(harness.getLabel(), label))
|
|
27
|
+
.addOption('disabled', options.disabled, async (harness, disabled) => (await harness.isDisabled()) === disabled);
|
|
28
|
+
}
|
|
29
|
+
/** The action's accessible name — what a screen reader reads, and what `title` shows. */
|
|
30
|
+
async getLabel() {
|
|
31
|
+
return (await this.host()).getAttribute('aria-label');
|
|
32
|
+
}
|
|
33
|
+
/** The role the action announces — `menuitem`. */
|
|
34
|
+
async getRole() {
|
|
35
|
+
return (await this.host()).getAttribute('role');
|
|
36
|
+
}
|
|
37
|
+
/** Whether the action draws an icon rather than a letter. */
|
|
38
|
+
async hasIcon() {
|
|
39
|
+
return (await this.locatorForOptional('wr-icon')()) !== null;
|
|
40
|
+
}
|
|
41
|
+
/** The icon's registered name, from `wr-icon`'s reflected `data-icon`, or `null`. */
|
|
42
|
+
async getIconName() {
|
|
43
|
+
const icon = await this.locatorForOptional('wr-icon')();
|
|
44
|
+
return icon ? icon.getAttribute('data-icon') : null;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* The single glyph drawn in place of a missing icon, or `null` when there is one.
|
|
48
|
+
*
|
|
49
|
+
* A GLYPH, not a character: the component splits the label by code point, so an
|
|
50
|
+
* emoji label draws the whole emoji rather than the broken half of a surrogate pair.
|
|
51
|
+
*/
|
|
52
|
+
async getInitial() {
|
|
53
|
+
if (await this.hasIcon())
|
|
54
|
+
return null;
|
|
55
|
+
return (await this.host()).text();
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Whether the action refuses interaction.
|
|
59
|
+
*
|
|
60
|
+
* Every action follows the DIAL's `disabled` — there is no per-action flag — so
|
|
61
|
+
* this is really "is the dial disabled", asked at the action. Worth having anyway:
|
|
62
|
+
* it is what the DOM and the CDK consult before delivering a click.
|
|
63
|
+
*/
|
|
64
|
+
async isDisabled() {
|
|
65
|
+
return (await this.host()).getProperty('disabled');
|
|
66
|
+
}
|
|
67
|
+
/** Pick this action — emitting `pick` and closing the dial. */
|
|
68
|
+
async click() {
|
|
69
|
+
if (await this.isDisabled()) {
|
|
70
|
+
throw new Error(`WrSpeedDialActionHarness.click(): "${await this.getLabel()}" is disabled. The DOM swallows the click ` +
|
|
71
|
+
'and the component checks `disabled` again before emitting, so nothing would happen.');
|
|
72
|
+
}
|
|
73
|
+
await (await this.host()).click();
|
|
74
|
+
}
|
|
75
|
+
/** Move keyboard focus to the action. */
|
|
76
|
+
async focus() {
|
|
77
|
+
return (await this.host()).focus();
|
|
78
|
+
}
|
|
79
|
+
/** Whether the action currently holds focus. */
|
|
80
|
+
async isFocused() {
|
|
81
|
+
return (await this.host()).isFocused();
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* @license
|
|
87
|
+
*
|
|
88
|
+
* Use of this source code is governed by an MIT-style license that can be
|
|
89
|
+
* found in the LICENSE file at https://github.com/thekhegay/ngwr/blob/main/LICENSE
|
|
90
|
+
*/
|
|
91
|
+
const DIRECTIONS = ['up', 'down', 'left', 'right'];
|
|
92
|
+
/**
|
|
93
|
+
* Test harness for `<wr-speed-dial>` — the floating trigger and the actions that fan
|
|
94
|
+
* out of it.
|
|
95
|
+
*
|
|
96
|
+
* **The actions never leave the DOM.** They are hidden with `visibility`, which is
|
|
97
|
+
* what actually takes them out of the tab order and the accessibility tree —
|
|
98
|
+
* `opacity: 0` alone would leave them focusable — and CSS is exactly what a unit
|
|
99
|
+
* test does not have. So a closed dial's buttons are queryable, clickable and
|
|
100
|
+
* readable from a spec while being unreachable in a browser. {@link getActions} and
|
|
101
|
+
* {@link pick} therefore REFUSE while the dial is closed rather than answering with
|
|
102
|
+
* elements nobody can see; open it first, which is what a user has to do too.
|
|
103
|
+
*
|
|
104
|
+
* @example
|
|
105
|
+
* ```ts
|
|
106
|
+
* const dial = await loader.getHarness(WrSpeedDialHarness);
|
|
107
|
+
*
|
|
108
|
+
* await dial.open();
|
|
109
|
+
* expect(await dial.getActionLabels()).toEqual(['Share', 'Copy link']);
|
|
110
|
+
*
|
|
111
|
+
* await dial.pick({ label: 'Share' });
|
|
112
|
+
* expect(await dial.isOpen()).toBe(false);
|
|
113
|
+
* ```
|
|
114
|
+
*
|
|
115
|
+
* @see https://ngwr.dev/guides/testing
|
|
116
|
+
*/
|
|
117
|
+
class WrSpeedDialHarness extends ComponentHarness {
|
|
118
|
+
static hostSelector = 'wr-speed-dial';
|
|
119
|
+
/** Build a predicate that narrows the query — pass to `getHarness` / `getAllHarnesses`. */
|
|
120
|
+
static with(options = {}) {
|
|
121
|
+
return new HarnessPredicate(WrSpeedDialHarness, options)
|
|
122
|
+
.addOption('triggerLabel', options.triggerLabel, (harness, label) => HarnessPredicate.stringMatches(harness.getTriggerLabel(), label))
|
|
123
|
+
.addOption('direction', options.direction, async (harness, direction) => (await harness.getDirection()) === direction)
|
|
124
|
+
.addOption('open', options.open, async (harness, open) => (await harness.isOpen()) === open)
|
|
125
|
+
.addOption('disabled', options.disabled, async (harness, disabled) => (await harness.isDisabled()) === disabled);
|
|
126
|
+
}
|
|
127
|
+
trigger = this.locatorFor('.wr-speed-dial__trigger');
|
|
128
|
+
menu = this.locatorFor('.wr-speed-dial__actions');
|
|
129
|
+
/** Whether the dial is fanned out, from the trigger's `aria-expanded`. */
|
|
130
|
+
async isOpen() {
|
|
131
|
+
return (await (await this.trigger()).getAttribute('aria-expanded')) === 'true';
|
|
132
|
+
}
|
|
133
|
+
/** The direction the actions travel, from the host modifier. */
|
|
134
|
+
async getDirection() {
|
|
135
|
+
const host = await this.host();
|
|
136
|
+
for (const direction of DIRECTIONS) {
|
|
137
|
+
if (await host.hasClass(`wr-speed-dial--${direction}`))
|
|
138
|
+
return direction;
|
|
139
|
+
}
|
|
140
|
+
throw new Error('WrSpeedDialHarness.getDirection(): this host carries no `wr-speed-dial--<direction>` class, which ' +
|
|
141
|
+
'every dial sets — the element matched is probably not an ngwr speed dial.');
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* The trigger's accessible name.
|
|
145
|
+
*
|
|
146
|
+
* The trigger is icon-only, so this is its ONLY name — there is no text to fall
|
|
147
|
+
* back on, and a dial that loses it is announced as an unnamed button.
|
|
148
|
+
*/
|
|
149
|
+
async getTriggerLabel() {
|
|
150
|
+
return (await this.trigger()).getAttribute('aria-label');
|
|
151
|
+
}
|
|
152
|
+
/** The trigger icon's registered name, from `wr-icon`'s reflected `data-icon`. */
|
|
153
|
+
async getTriggerIcon() {
|
|
154
|
+
const icon = await this.locatorForOptional('.wr-speed-dial__trigger wr-icon')();
|
|
155
|
+
return icon ? icon.getAttribute('data-icon') : null;
|
|
156
|
+
}
|
|
157
|
+
/** Whether the dial refuses interaction, from the trigger's own `disabled` property. */
|
|
158
|
+
async isDisabled() {
|
|
159
|
+
return (await this.trigger()).getProperty('disabled');
|
|
160
|
+
}
|
|
161
|
+
/** Whether the host is padded for the device's safe area (`safeArea`). */
|
|
162
|
+
async hasSafeArea() {
|
|
163
|
+
return (await this.host()).hasClass('wr-speed-dial--safe-area');
|
|
164
|
+
}
|
|
165
|
+
/** The role the action list announces — `menu`. */
|
|
166
|
+
async getMenuRole() {
|
|
167
|
+
return (await this.menu()).getAttribute('role');
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Whether the trigger's `aria-controls` names THIS dial's menu, and nothing else on
|
|
171
|
+
* the page.
|
|
172
|
+
*
|
|
173
|
+
* Both halves matter. The id is resolved inside the host first, so a trigger
|
|
174
|
+
* pointing at a SIBLING dial's menu — which resolves perfectly well from the
|
|
175
|
+
* document — fails here. Then it is counted across the document, because two menus
|
|
176
|
+
* answering to one id hand every reference to whichever comes first.
|
|
177
|
+
*/
|
|
178
|
+
async isMenuBound() {
|
|
179
|
+
const id = await (await this.trigger()).getAttribute('aria-controls');
|
|
180
|
+
if (!id)
|
|
181
|
+
return false;
|
|
182
|
+
const own = await this.locatorForOptional(`.wr-speed-dial__actions#${id}`)();
|
|
183
|
+
if (!own)
|
|
184
|
+
return false;
|
|
185
|
+
const everywhere = await this.documentRootLocatorFactory().locatorForAll(`#${id}`)();
|
|
186
|
+
return everywhere.length === 1;
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* The actions, in DOM order.
|
|
190
|
+
*
|
|
191
|
+
* Throws while the dial is closed. The buttons are still in the DOM then — they
|
|
192
|
+
* have to be, for the fan-out to animate — and jsdom applies no `visibility`, so
|
|
193
|
+
* they would come back looking perfectly reachable when a real user cannot tab to
|
|
194
|
+
* one. A spec that read them closed would pass on a dial that never opens.
|
|
195
|
+
*/
|
|
196
|
+
async getActions(filters = {}) {
|
|
197
|
+
await this.requireOpen('getActions');
|
|
198
|
+
return this.locatorForAll(WrSpeedDialActionHarness.with(filters))();
|
|
199
|
+
}
|
|
200
|
+
/** The accessible names of the actions, in DOM order. Throws while closed. */
|
|
201
|
+
async getActionLabels() {
|
|
202
|
+
const actions = await this.getActions();
|
|
203
|
+
return Promise.all(actions.map(action => action.getLabel()));
|
|
204
|
+
}
|
|
205
|
+
/** How many actions the dial holds — readable while closed, since it is not a reach. */
|
|
206
|
+
async getActionCount() {
|
|
207
|
+
return (await this.locatorForAll('.wr-speed-dial__action')()).length;
|
|
208
|
+
}
|
|
209
|
+
/** Open the dial. An already-open one is left alone; a disabled one throws. */
|
|
210
|
+
async open() {
|
|
211
|
+
if (await this.isOpen())
|
|
212
|
+
return;
|
|
213
|
+
await this.toggle();
|
|
214
|
+
if (!(await this.isOpen())) {
|
|
215
|
+
throw new Error('WrSpeedDialHarness.open(): the dial did not open. A disabled trigger refuses the click twice over — ' +
|
|
216
|
+
'the DOM swallows it, and the component checks `disabled` again.');
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
/** Close the dial. An already-closed one is left alone. */
|
|
220
|
+
async close() {
|
|
221
|
+
if (!(await this.isOpen()))
|
|
222
|
+
return;
|
|
223
|
+
await this.toggle();
|
|
224
|
+
}
|
|
225
|
+
/** Click the trigger once, whichever way that takes it. */
|
|
226
|
+
async toggle() {
|
|
227
|
+
return (await this.trigger()).click();
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Pick the first action matching the filters — emitting `pick` and closing the dial.
|
|
231
|
+
*
|
|
232
|
+
* Opens first if it has to, the way `WrDropdownHarness.clickItem` does: a spec that
|
|
233
|
+
* says "pick Share" means the whole gesture. Throws when nothing matches, naming
|
|
234
|
+
* what the dial does offer.
|
|
235
|
+
*/
|
|
236
|
+
async pick(filters) {
|
|
237
|
+
await this.open();
|
|
238
|
+
const [action] = await this.getActions(filters);
|
|
239
|
+
if (!action) {
|
|
240
|
+
const offered = await this.getActionLabels();
|
|
241
|
+
throw new Error(`WrSpeedDialHarness.pick(): no action matched ${JSON.stringify(filters)}. ` +
|
|
242
|
+
`The dial offers: ${offered.join(', ')}.`);
|
|
243
|
+
}
|
|
244
|
+
await action.click();
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Press Escape — closing the dial and putting focus back on the trigger.
|
|
248
|
+
*
|
|
249
|
+
* Sent to the host, which is where the component listens. The focus return is the
|
|
250
|
+
* half worth asserting: `role="menu"` promises a way out, and the actions are
|
|
251
|
+
* ordinary tab stops, so without it a keyboard user has to walk through every
|
|
252
|
+
* action to leave.
|
|
253
|
+
*/
|
|
254
|
+
async sendEscape() {
|
|
255
|
+
await (await this.host()).sendKeys(TestKey.ESCAPE);
|
|
256
|
+
}
|
|
257
|
+
/** Move keyboard focus to the trigger. */
|
|
258
|
+
async focusTrigger() {
|
|
259
|
+
return (await this.trigger()).focus();
|
|
260
|
+
}
|
|
261
|
+
/** Whether the trigger currently holds focus. */
|
|
262
|
+
async isTriggerFocused() {
|
|
263
|
+
return (await this.trigger()).isFocused();
|
|
264
|
+
}
|
|
265
|
+
async requireOpen(method) {
|
|
266
|
+
if (await this.isOpen())
|
|
267
|
+
return;
|
|
268
|
+
throw new Error(`WrSpeedDialHarness.${method}(): the dial is closed. Its actions stay in the DOM while collapsed — the ` +
|
|
269
|
+
'fan-out animates them — and only `visibility` hides them, which a unit test does not apply. Reading ' +
|
|
270
|
+
'them now would assert buttons nobody can reach. Call open() first, or getActionCount().');
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Generated bundle index. Do not edit.
|
|
276
|
+
*/
|
|
277
|
+
|
|
278
|
+
export { WrSpeedDialActionHarness, WrSpeedDialHarness };
|
|
279
|
+
//# sourceMappingURL=ngwr-speed-dial-testing.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ngwr-speed-dial-testing.mjs","sources":["../../../projects/lib/speed-dial/testing/wr-speed-dial-action-harness.ts","../../../projects/lib/speed-dial/testing/wr-speed-dial-harness.ts","../../../projects/lib/speed-dial/testing/ngwr-speed-dial-testing.ts"],"sourcesContent":["/**\n * @license\n *\n * Use of this source code is governed by an MIT-style license that can be\n * found in the LICENSE file at https://github.com/thekhegay/ngwr/blob/main/LICENSE\n */\n\nimport { ComponentHarness, HarnessPredicate } from '@angular/cdk/testing';\n\nimport type { WrSpeedDialActionHarnessFilters } from './interfaces';\n\n/**\n * Test harness for one action inside a {@link WrSpeedDialHarness}.\n *\n * The label is read from `aria-label` rather than from the button's text, and that\n * is the whole point of the control: an action renders an ICON, or — with no icon —\n * the first glyph of its label. Its visible text is therefore `\"S\"` for \"Share\", or\n * nothing at all. {@link getInitial} answers what is drawn; {@link getLabel} answers\n * what it is.\n *\n * @see https://ngwr.dev/guides/testing\n */\nexport class WrSpeedDialActionHarness extends ComponentHarness {\n /** The action button. `role=\"menuitem\"` sits on this element too. */\n static hostSelector = '.wr-speed-dial__action';\n\n /** Build a predicate that narrows the query — pass to `getHarness` / `getAllHarnesses`. */\n static with(options: WrSpeedDialActionHarnessFilters = {}): HarnessPredicate<WrSpeedDialActionHarness> {\n return new HarnessPredicate(WrSpeedDialActionHarness, options)\n .addOption('label', options.label, (harness, label) => HarnessPredicate.stringMatches(harness.getLabel(), label))\n .addOption('disabled', options.disabled, async (harness, disabled) => (await harness.isDisabled()) === disabled);\n }\n\n /** The action's accessible name — what a screen reader reads, and what `title` shows. */\n async getLabel(): Promise<string | null> {\n return (await this.host()).getAttribute('aria-label');\n }\n\n /** The role the action announces — `menuitem`. */\n async getRole(): Promise<string | null> {\n return (await this.host()).getAttribute('role');\n }\n\n /** Whether the action draws an icon rather than a letter. */\n async hasIcon(): Promise<boolean> {\n return (await this.locatorForOptional('wr-icon')()) !== null;\n }\n\n /** The icon's registered name, from `wr-icon`'s reflected `data-icon`, or `null`. */\n async getIconName(): Promise<string | null> {\n const icon = await this.locatorForOptional('wr-icon')();\n return icon ? icon.getAttribute('data-icon') : null;\n }\n\n /**\n * The single glyph drawn in place of a missing icon, or `null` when there is one.\n *\n * A GLYPH, not a character: the component splits the label by code point, so an\n * emoji label draws the whole emoji rather than the broken half of a surrogate pair.\n */\n async getInitial(): Promise<string | null> {\n if (await this.hasIcon()) return null;\n return (await this.host()).text();\n }\n\n /**\n * Whether the action refuses interaction.\n *\n * Every action follows the DIAL's `disabled` — there is no per-action flag — so\n * this is really \"is the dial disabled\", asked at the action. Worth having anyway:\n * it is what the DOM and the CDK consult before delivering a click.\n */\n async isDisabled(): Promise<boolean> {\n return (await this.host()).getProperty<boolean>('disabled');\n }\n\n /** Pick this action — emitting `pick` and closing the dial. */\n async click(): Promise<void> {\n if (await this.isDisabled()) {\n throw new Error(\n `WrSpeedDialActionHarness.click(): \"${await this.getLabel()}\" is disabled. The DOM swallows the click ` +\n 'and the component checks `disabled` again before emitting, so nothing would happen.'\n );\n }\n await (await this.host()).click();\n }\n\n /** Move keyboard focus to the action. */\n async focus(): Promise<void> {\n return (await this.host()).focus();\n }\n\n /** Whether the action currently holds focus. */\n async isFocused(): Promise<boolean> {\n return (await this.host()).isFocused();\n }\n}\n","/**\n * @license\n *\n * Use of this source code is governed by an MIT-style license that can be\n * found in the LICENSE file at https://github.com/thekhegay/ngwr/blob/main/LICENSE\n */\n\nimport { ComponentHarness, HarnessPredicate, TestKey } from '@angular/cdk/testing';\n\nimport type { WrSpeedDialDirection } from 'ngwr/speed-dial';\n\nimport type { WrSpeedDialActionHarnessFilters, WrSpeedDialHarnessFilters } from './interfaces';\nimport { WrSpeedDialActionHarness } from './wr-speed-dial-action-harness';\n\nconst DIRECTIONS: readonly WrSpeedDialDirection[] = ['up', 'down', 'left', 'right'];\n\n/**\n * Test harness for `<wr-speed-dial>` — the floating trigger and the actions that fan\n * out of it.\n *\n * **The actions never leave the DOM.** They are hidden with `visibility`, which is\n * what actually takes them out of the tab order and the accessibility tree —\n * `opacity: 0` alone would leave them focusable — and CSS is exactly what a unit\n * test does not have. So a closed dial's buttons are queryable, clickable and\n * readable from a spec while being unreachable in a browser. {@link getActions} and\n * {@link pick} therefore REFUSE while the dial is closed rather than answering with\n * elements nobody can see; open it first, which is what a user has to do too.\n *\n * @example\n * ```ts\n * const dial = await loader.getHarness(WrSpeedDialHarness);\n *\n * await dial.open();\n * expect(await dial.getActionLabels()).toEqual(['Share', 'Copy link']);\n *\n * await dial.pick({ label: 'Share' });\n * expect(await dial.isOpen()).toBe(false);\n * ```\n *\n * @see https://ngwr.dev/guides/testing\n */\nexport class WrSpeedDialHarness extends ComponentHarness {\n static hostSelector = 'wr-speed-dial';\n\n /** Build a predicate that narrows the query — pass to `getHarness` / `getAllHarnesses`. */\n static with(options: WrSpeedDialHarnessFilters = {}): HarnessPredicate<WrSpeedDialHarness> {\n return new HarnessPredicate(WrSpeedDialHarness, options)\n .addOption('triggerLabel', options.triggerLabel, (harness, label) =>\n HarnessPredicate.stringMatches(harness.getTriggerLabel(), label)\n )\n .addOption(\n 'direction',\n options.direction,\n async (harness, direction) => (await harness.getDirection()) === direction\n )\n .addOption('open', options.open, async (harness, open) => (await harness.isOpen()) === open)\n .addOption('disabled', options.disabled, async (harness, disabled) => (await harness.isDisabled()) === disabled);\n }\n\n private readonly trigger = this.locatorFor('.wr-speed-dial__trigger');\n private readonly menu = this.locatorFor('.wr-speed-dial__actions');\n\n /** Whether the dial is fanned out, from the trigger's `aria-expanded`. */\n async isOpen(): Promise<boolean> {\n return (await (await this.trigger()).getAttribute('aria-expanded')) === 'true';\n }\n\n /** The direction the actions travel, from the host modifier. */\n async getDirection(): Promise<WrSpeedDialDirection> {\n const host = await this.host();\n for (const direction of DIRECTIONS) {\n if (await host.hasClass(`wr-speed-dial--${direction}`)) return direction;\n }\n throw new Error(\n 'WrSpeedDialHarness.getDirection(): this host carries no `wr-speed-dial--<direction>` class, which ' +\n 'every dial sets — the element matched is probably not an ngwr speed dial.'\n );\n }\n\n /**\n * The trigger's accessible name.\n *\n * The trigger is icon-only, so this is its ONLY name — there is no text to fall\n * back on, and a dial that loses it is announced as an unnamed button.\n */\n async getTriggerLabel(): Promise<string | null> {\n return (await this.trigger()).getAttribute('aria-label');\n }\n\n /** The trigger icon's registered name, from `wr-icon`'s reflected `data-icon`. */\n async getTriggerIcon(): Promise<string | null> {\n const icon = await this.locatorForOptional('.wr-speed-dial__trigger wr-icon')();\n return icon ? icon.getAttribute('data-icon') : null;\n }\n\n /** Whether the dial refuses interaction, from the trigger's own `disabled` property. */\n async isDisabled(): Promise<boolean> {\n return (await this.trigger()).getProperty<boolean>('disabled');\n }\n\n /** Whether the host is padded for the device's safe area (`safeArea`). */\n async hasSafeArea(): Promise<boolean> {\n return (await this.host()).hasClass('wr-speed-dial--safe-area');\n }\n\n /** The role the action list announces — `menu`. */\n async getMenuRole(): Promise<string | null> {\n return (await this.menu()).getAttribute('role');\n }\n\n /**\n * Whether the trigger's `aria-controls` names THIS dial's menu, and nothing else on\n * the page.\n *\n * Both halves matter. The id is resolved inside the host first, so a trigger\n * pointing at a SIBLING dial's menu — which resolves perfectly well from the\n * document — fails here. Then it is counted across the document, because two menus\n * answering to one id hand every reference to whichever comes first.\n */\n async isMenuBound(): Promise<boolean> {\n const id = await (await this.trigger()).getAttribute('aria-controls');\n if (!id) return false;\n\n const own = await this.locatorForOptional(`.wr-speed-dial__actions#${id}`)();\n if (!own) return false;\n\n const everywhere = await this.documentRootLocatorFactory().locatorForAll(`#${id}`)();\n return everywhere.length === 1;\n }\n\n /**\n * The actions, in DOM order.\n *\n * Throws while the dial is closed. The buttons are still in the DOM then — they\n * have to be, for the fan-out to animate — and jsdom applies no `visibility`, so\n * they would come back looking perfectly reachable when a real user cannot tab to\n * one. A spec that read them closed would pass on a dial that never opens.\n */\n async getActions(filters: WrSpeedDialActionHarnessFilters = {}): Promise<WrSpeedDialActionHarness[]> {\n await this.requireOpen('getActions');\n return this.locatorForAll(WrSpeedDialActionHarness.with(filters))();\n }\n\n /** The accessible names of the actions, in DOM order. Throws while closed. */\n async getActionLabels(): Promise<(string | null)[]> {\n const actions = await this.getActions();\n return Promise.all(actions.map(action => action.getLabel()));\n }\n\n /** How many actions the dial holds — readable while closed, since it is not a reach. */\n async getActionCount(): Promise<number> {\n return (await this.locatorForAll('.wr-speed-dial__action')()).length;\n }\n\n /** Open the dial. An already-open one is left alone; a disabled one throws. */\n async open(): Promise<void> {\n if (await this.isOpen()) return;\n await this.toggle();\n\n if (!(await this.isOpen())) {\n throw new Error(\n 'WrSpeedDialHarness.open(): the dial did not open. A disabled trigger refuses the click twice over — ' +\n 'the DOM swallows it, and the component checks `disabled` again.'\n );\n }\n }\n\n /** Close the dial. An already-closed one is left alone. */\n async close(): Promise<void> {\n if (!(await this.isOpen())) return;\n await this.toggle();\n }\n\n /** Click the trigger once, whichever way that takes it. */\n async toggle(): Promise<void> {\n return (await this.trigger()).click();\n }\n\n /**\n * Pick the first action matching the filters — emitting `pick` and closing the dial.\n *\n * Opens first if it has to, the way `WrDropdownHarness.clickItem` does: a spec that\n * says \"pick Share\" means the whole gesture. Throws when nothing matches, naming\n * what the dial does offer.\n */\n async pick(filters: WrSpeedDialActionHarnessFilters): Promise<void> {\n await this.open();\n\n const [action] = await this.getActions(filters);\n if (!action) {\n const offered = await this.getActionLabels();\n throw new Error(\n `WrSpeedDialHarness.pick(): no action matched ${JSON.stringify(filters)}. ` +\n `The dial offers: ${offered.join(', ')}.`\n );\n }\n await action.click();\n }\n\n /**\n * Press Escape — closing the dial and putting focus back on the trigger.\n *\n * Sent to the host, which is where the component listens. The focus return is the\n * half worth asserting: `role=\"menu\"` promises a way out, and the actions are\n * ordinary tab stops, so without it a keyboard user has to walk through every\n * action to leave.\n */\n async sendEscape(): Promise<void> {\n await (await this.host()).sendKeys(TestKey.ESCAPE);\n }\n\n /** Move keyboard focus to the trigger. */\n async focusTrigger(): Promise<void> {\n return (await this.trigger()).focus();\n }\n\n /** Whether the trigger currently holds focus. */\n async isTriggerFocused(): Promise<boolean> {\n return (await this.trigger()).isFocused();\n }\n\n private async requireOpen(method: string): Promise<void> {\n if (await this.isOpen()) return;\n throw new Error(\n `WrSpeedDialHarness.${method}(): the dial is closed. Its actions stay in the DOM while collapsed — the ` +\n 'fan-out animates them — and only `visibility` hides them, which a unit test does not apply. Reading ' +\n 'them now would assert buttons nobody can reach. Call open() first, or getActionCount().'\n );\n }\n}\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public-api';\n"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { ComponentHarness, HarnessPredicate } from '@angular/cdk/testing';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @license
|
|
5
|
+
*
|
|
6
|
+
* Use of this source code is governed by an MIT-style license that can be
|
|
7
|
+
* found in the LICENSE file at https://github.com/thekhegay/ngwr/blob/main/LICENSE
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* Test harness for `<wr-splash-cursor>` — a WebGL fluid that splashes dye under the
|
|
11
|
+
* pointer.
|
|
12
|
+
*
|
|
13
|
+
* **This is a deliberately small surface, and the reason is the component.** The
|
|
14
|
+
* simulation is GL programs and framebuffers driven by `window` pointer listeners; the
|
|
15
|
+
* layout is `position: fixed`, `inset: 0`, `z-index: 50` and a `100dvh` canvas, all of
|
|
16
|
+
* it in the stylesheet. A unit test gets no WebGL context — the simulation is never
|
|
17
|
+
* constructed — and loads no stylesheet, so asking `getCssValue('position')` would
|
|
18
|
+
* answer `static` about a component whose whole layout contract is `fixed`. Three
|
|
19
|
+
* questions survive that, and they are the three below.
|
|
20
|
+
*
|
|
21
|
+
* There is no `isSimulating()`, `getSplatCount()` or `splat()`: the simulation returns
|
|
22
|
+
* `null` the moment the context is refused, so every one of those would report on
|
|
23
|
+
* something that was never built. No `moveMouse()` either — the listeners live on
|
|
24
|
+
* `window` and are only attached once a context exists, so a dispatched move reaches
|
|
25
|
+
* nothing. And no `isReducedMotion()` or `hasWebGL()`: the component renders
|
|
26
|
+
* byte-identical DOM whether it is running, standing down for reduced motion, or on a
|
|
27
|
+
* machine with no WebGL at all, so any answer would be the same for a working component
|
|
28
|
+
* and a broken one.
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* ```ts
|
|
32
|
+
* const splash = await loader.getHarness(WrSplashCursorHarness);
|
|
33
|
+
*
|
|
34
|
+
* expect(await splash.isFullscreen()).toBe(true);
|
|
35
|
+
* expect(await splash.isDecorative()).toBe(true);
|
|
36
|
+
* ```
|
|
37
|
+
*
|
|
38
|
+
* @see https://ngwr.dev/guides/testing
|
|
39
|
+
*/
|
|
40
|
+
class WrSplashCursorHarness extends ComponentHarness {
|
|
41
|
+
static hostSelector = 'wr-splash-cursor';
|
|
42
|
+
/** Build a predicate that narrows the query — pass to `getHarness` / `getAllHarnesses`. */
|
|
43
|
+
static with(options = {}) {
|
|
44
|
+
return new HarnessPredicate(WrSplashCursorHarness, options).addOption('fullscreen', options.fullscreen, async (harness, fullscreen) => (await harness.isFullscreen()) === fullscreen);
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Whether the effect covers the whole viewport, rather than filling the nearest
|
|
48
|
+
* positioned ancestor.
|
|
49
|
+
*
|
|
50
|
+
* The component's one behavioural switch: fullscreen is a fixed, click-through
|
|
51
|
+
* overlay above the page, contained is an absolutely positioned box whose walls the
|
|
52
|
+
* dye collides with. Note what is being read — there is no `--fullscreen` class, only
|
|
53
|
+
* a `--contained` one written from a NEGATED binding, so this method answers the
|
|
54
|
+
* absence of a class. That inversion is exactly the kind that gets flipped in a
|
|
55
|
+
* refactor and looks fine on a page where the effect is full-viewport anyway, which
|
|
56
|
+
* is the whole reason the method exists.
|
|
57
|
+
*/
|
|
58
|
+
async isFullscreen() {
|
|
59
|
+
return !(await (await this.host()).hasClass('wr-splash-cursor--contained'));
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Whether the effect is hidden from assistive tech.
|
|
63
|
+
*
|
|
64
|
+
* It has no text and nothing to say, and at full size it sits in front of the entire
|
|
65
|
+
* page — so losing this puts a nameless graphic over everything a screen reader is
|
|
66
|
+
* trying to get through.
|
|
67
|
+
*/
|
|
68
|
+
async isDecorative() {
|
|
69
|
+
return (await (await this.locatorFor('.wr-splash-cursor__canvas')()).getAttribute('aria-hidden')) === 'true';
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Whether the drawing surface is in the markup.
|
|
73
|
+
*
|
|
74
|
+
* It is there in all three states where nothing is ever drawn on it — prerendered on
|
|
75
|
+
* the server, WebGL refused, or reduced motion — because the documented contract is
|
|
76
|
+
* that the component renders nothing VISIBLE and stops, never that it renders no
|
|
77
|
+
* element and never that it throws on a null context.
|
|
78
|
+
*/
|
|
79
|
+
async hasCanvas() {
|
|
80
|
+
return (await this.locatorForOptional('.wr-splash-cursor__canvas')()) !== null;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Generated bundle index. Do not edit.
|
|
86
|
+
*/
|
|
87
|
+
|
|
88
|
+
export { WrSplashCursorHarness };
|
|
89
|
+
//# sourceMappingURL=ngwr-splash-cursor-testing.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ngwr-splash-cursor-testing.mjs","sources":["../../../projects/lib/splash-cursor/testing/wr-splash-cursor-harness.ts","../../../projects/lib/splash-cursor/testing/ngwr-splash-cursor-testing.ts"],"sourcesContent":["/**\n * @license\n *\n * Use of this source code is governed by an MIT-style license that can be\n * found in the LICENSE file at https://github.com/thekhegay/ngwr/blob/main/LICENSE\n */\n\nimport { ComponentHarness, HarnessPredicate } from '@angular/cdk/testing';\n\nimport type { WrSplashCursorHarnessFilters } from './interfaces';\n\n/**\n * Test harness for `<wr-splash-cursor>` — a WebGL fluid that splashes dye under the\n * pointer.\n *\n * **This is a deliberately small surface, and the reason is the component.** The\n * simulation is GL programs and framebuffers driven by `window` pointer listeners; the\n * layout is `position: fixed`, `inset: 0`, `z-index: 50` and a `100dvh` canvas, all of\n * it in the stylesheet. A unit test gets no WebGL context — the simulation is never\n * constructed — and loads no stylesheet, so asking `getCssValue('position')` would\n * answer `static` about a component whose whole layout contract is `fixed`. Three\n * questions survive that, and they are the three below.\n *\n * There is no `isSimulating()`, `getSplatCount()` or `splat()`: the simulation returns\n * `null` the moment the context is refused, so every one of those would report on\n * something that was never built. No `moveMouse()` either — the listeners live on\n * `window` and are only attached once a context exists, so a dispatched move reaches\n * nothing. And no `isReducedMotion()` or `hasWebGL()`: the component renders\n * byte-identical DOM whether it is running, standing down for reduced motion, or on a\n * machine with no WebGL at all, so any answer would be the same for a working component\n * and a broken one.\n *\n * @example\n * ```ts\n * const splash = await loader.getHarness(WrSplashCursorHarness);\n *\n * expect(await splash.isFullscreen()).toBe(true);\n * expect(await splash.isDecorative()).toBe(true);\n * ```\n *\n * @see https://ngwr.dev/guides/testing\n */\nexport class WrSplashCursorHarness extends ComponentHarness {\n static hostSelector = 'wr-splash-cursor';\n\n /** Build a predicate that narrows the query — pass to `getHarness` / `getAllHarnesses`. */\n static with(options: WrSplashCursorHarnessFilters = {}): HarnessPredicate<WrSplashCursorHarness> {\n return new HarnessPredicate(WrSplashCursorHarness, options).addOption(\n 'fullscreen',\n options.fullscreen,\n async (harness, fullscreen) => (await harness.isFullscreen()) === fullscreen\n );\n }\n\n /**\n * Whether the effect covers the whole viewport, rather than filling the nearest\n * positioned ancestor.\n *\n * The component's one behavioural switch: fullscreen is a fixed, click-through\n * overlay above the page, contained is an absolutely positioned box whose walls the\n * dye collides with. Note what is being read — there is no `--fullscreen` class, only\n * a `--contained` one written from a NEGATED binding, so this method answers the\n * absence of a class. That inversion is exactly the kind that gets flipped in a\n * refactor and looks fine on a page where the effect is full-viewport anyway, which\n * is the whole reason the method exists.\n */\n async isFullscreen(): Promise<boolean> {\n return !(await (await this.host()).hasClass('wr-splash-cursor--contained'));\n }\n\n /**\n * Whether the effect is hidden from assistive tech.\n *\n * It has no text and nothing to say, and at full size it sits in front of the entire\n * page — so losing this puts a nameless graphic over everything a screen reader is\n * trying to get through.\n */\n async isDecorative(): Promise<boolean> {\n return (await (await this.locatorFor('.wr-splash-cursor__canvas')()).getAttribute('aria-hidden')) === 'true';\n }\n\n /**\n * Whether the drawing surface is in the markup.\n *\n * It is there in all three states where nothing is ever drawn on it — prerendered on\n * the server, WebGL refused, or reduced motion — because the documented contract is\n * that the component renders nothing VISIBLE and stops, never that it renders no\n * element and never that it throws on a null context.\n */\n async hasCanvas(): Promise<boolean> {\n return (await this.locatorForOptional('.wr-splash-cursor__canvas')()) !== null;\n }\n}\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public-api';\n"],"names":[],"mappings":";;AAAA;;;;;AAKG;AAMH;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8BG;AACG,MAAO,qBAAsB,SAAQ,gBAAgB,CAAA;AACzD,IAAA,OAAO,YAAY,GAAG,kBAAkB;;AAGxC,IAAA,OAAO,IAAI,CAAC,OAAA,GAAwC,EAAE,EAAA;AACpD,QAAA,OAAO,IAAI,gBAAgB,CAAC,qBAAqB,EAAE,OAAO,CAAC,CAAC,SAAS,CACnE,YAAY,EACZ,OAAO,CAAC,UAAU,EAClB,OAAO,OAAO,EAAE,UAAU,KAAK,CAAC,MAAM,OAAO,CAAC,YAAY,EAAE,MAAM,UAAU,CAC7E;IACH;AAEA;;;;;;;;;;;AAWG;AACH,IAAA,MAAM,YAAY,GAAA;AAChB,QAAA,OAAO,EAAE,MAAM,CAAC,MAAM,IAAI,CAAC,IAAI,EAAE,EAAE,QAAQ,CAAC,6BAA6B,CAAC,CAAC;IAC7E;AAEA;;;;;;AAMG;AACH,IAAA,MAAM,YAAY,GAAA;QAChB,OAAO,CAAC,MAAM,CAAC,MAAM,IAAI,CAAC,UAAU,CAAC,2BAA2B,CAAC,EAAE,EAAE,YAAY,CAAC,aAAa,CAAC,MAAM,MAAM;IAC9G;AAEA;;;;;;;AAOG;AACH,IAAA,MAAM,SAAS,GAAA;AACb,QAAA,OAAO,CAAC,MAAM,IAAI,CAAC,kBAAkB,CAAC,2BAA2B,CAAC,EAAE,MAAM,IAAI;IAChF;;;AC3FF;;AAEG;;;;"}
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import { ComponentHarness, HarnessPredicate } from '@angular/cdk/testing';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @license
|
|
5
|
+
*
|
|
6
|
+
* Use of this source code is governed by an MIT-style license that can be
|
|
7
|
+
* found in the LICENSE file at https://github.com/thekhegay/ngwr/blob/main/LICENSE
|
|
8
|
+
*/
|
|
9
|
+
/** The animated pieces — one per character or per word, whitespace excluded. */
|
|
10
|
+
const PIECE = '.wr-split-text__piece';
|
|
11
|
+
/** The whitespace runs between them, rendered but never staggered. */
|
|
12
|
+
const SPACE = '.wr-split-text__space';
|
|
13
|
+
/** The readable copy of the string, carried once and hidden with CSS. */
|
|
14
|
+
const SR_ONLY = '.wr-split-text__sr-only';
|
|
15
|
+
/**
|
|
16
|
+
* An inline `opacity` declaration — written on every piece the tween touches.
|
|
17
|
+
*
|
|
18
|
+
* `motionToStyle` always produces an opacity, whether or not `from` declared one, and
|
|
19
|
+
* the same property is re-committed when the tween finishes. Matched off the `style`
|
|
20
|
+
* ATTRIBUTE, never through `getCssValue`: a computed style answers `1` for a piece
|
|
21
|
+
* nothing was ever written to, which is the exact case this has to distinguish.
|
|
22
|
+
*/
|
|
23
|
+
const INLINE_OPACITY = /(?:^|;)\s*opacity\s*:/;
|
|
24
|
+
/** The host's own alignment declaration, as the host binding wrote it. */
|
|
25
|
+
const INLINE_TEXT_ALIGN = /(?:^|;)\s*text-align\s*:\s*([^;]+)/;
|
|
26
|
+
/** Exactly what the element holds — untrimmed, unlike `TestElement.text()`. */
|
|
27
|
+
async function rawText(element) {
|
|
28
|
+
return (await element.getProperty('textContent')) ?? '';
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Test harness for `<wr-split-text>`.
|
|
32
|
+
*
|
|
33
|
+
* **The tween is unreadable from a test, and the absent methods are absent for that
|
|
34
|
+
* reason.** Each piece is animated from a `from` motion state to a `to` one through the
|
|
35
|
+
* Web Animations API, started by an `IntersectionObserver`. A unit test has no layout,
|
|
36
|
+
* no compositor and no WAAPI, so there is no `getOpacity()` and no `getTransform()`:
|
|
37
|
+
* both would report the frozen staged value in jsdom and a value the compositor is not
|
|
38
|
+
* painting in a browser. There is no `getFromState()` / `getToState()` either — those
|
|
39
|
+
* objects only reach the DOM as staged inline styles, on the one path that throws here,
|
|
40
|
+
* so reporting them would mean re-deriving an input from a string the component may
|
|
41
|
+
* never have written. No `isAnimating()`, no `whenAnimationComplete()`: nothing in the
|
|
42
|
+
* document says a tween is running, and completion is the `animationComplete` OUTPUT,
|
|
43
|
+
* which the host binds. No `enterViewport()` / `trigger()`, because an observer is not
|
|
44
|
+
* reachable from a `TestElement` — and observation here is additionally gated behind
|
|
45
|
+
* `document.fonts.ready`, which jsdom does not have, so a `waitForFonts()` would resolve
|
|
46
|
+
* instantly and prove nothing about the gating it claimed to test. Nothing measures
|
|
47
|
+
* either: `getLineCount()` / `didItWrap()` would need layout (and `lines` is not a split
|
|
48
|
+
* this port supports), and the `overflow: hidden` mask with its descender compensation
|
|
49
|
+
* is a stylesheet, which a test runs none of.
|
|
50
|
+
*
|
|
51
|
+
* **What is real is the split, the accessible layer and the alignment.** The string is
|
|
52
|
+
* carried once in a visually-hidden span and every drawn piece is `aria-hidden`, so
|
|
53
|
+
* {@link getAccessibleText} is what is heard while {@link getPieces} is what is drawn —
|
|
54
|
+
* two questions, and a spec asserting only the second would pass on a component that
|
|
55
|
+
* spells the text out letter by letter. {@link getRenderedText} closes the loop by
|
|
56
|
+
* putting the pieces and the spaces back together: the split has to be lossless, and
|
|
57
|
+
* that comparison is what catches a dropped space or a halved surrogate pair. Whether
|
|
58
|
+
* the hidden copy is genuinely off-screen is CSS, so nothing here claims to know.
|
|
59
|
+
*
|
|
60
|
+
* **{@link getTextAlign} is the one input this component reflects**, written straight
|
|
61
|
+
* onto the host as an inline style — an honest read with no layout involved, unlike
|
|
62
|
+
* anything else about how the text is arranged. And {@link hasStagedMotion} is the one
|
|
63
|
+
* fact about the tween worth pinning: that the pieces were taken over, not that anything
|
|
64
|
+
* is moving.
|
|
65
|
+
*
|
|
66
|
+
* Driving the reveal is the spec's job — jsdom supplies neither half of it, and the font
|
|
67
|
+
* gate defers observation by a microtask:
|
|
68
|
+
*
|
|
69
|
+
* ```ts
|
|
70
|
+
* // Before TestBed.createComponent — keep the callbacks so the spec can fire them.
|
|
71
|
+
* vi.stubGlobal('IntersectionObserver', StubObserver);
|
|
72
|
+
* // Otherwise `el.animate(...)` throws mid-stagger and leaves the DOM half-staged.
|
|
73
|
+
* Reflect.set(Element.prototype, 'animate', () => ({ cancel: () => undefined, onfinish: null }));
|
|
74
|
+
* // …and always settle before tearing down: observation is queued off a promise, and
|
|
75
|
+
* // landing after destroy() registers a teardown on a dead DestroyRef (NG0911).
|
|
76
|
+
* await fixture.whenStable();
|
|
77
|
+
* ```
|
|
78
|
+
*
|
|
79
|
+
* @example
|
|
80
|
+
* ```ts
|
|
81
|
+
* const hero = await loader.getHarness(WrSplitTextHarness.with({ textAlign: 'center' }));
|
|
82
|
+
*
|
|
83
|
+
* expect(await hero.getPieces()).toEqual(['H', 'e', 'l', 'l', 'o']);
|
|
84
|
+
* expect(await hero.getRenderedText()).toBe(await hero.getAccessibleText());
|
|
85
|
+
* expect(await hero.isTextHidden()).toBe(true);
|
|
86
|
+
* ```
|
|
87
|
+
*
|
|
88
|
+
* @see https://ngwr.dev/guides/testing
|
|
89
|
+
*/
|
|
90
|
+
class WrSplitTextHarness extends ComponentHarness {
|
|
91
|
+
static hostSelector = 'wr-split-text';
|
|
92
|
+
/** Build a predicate that narrows the query — pass to `getHarness` / `getAllHarnesses`. */
|
|
93
|
+
static with(options = {}) {
|
|
94
|
+
return new HarnessPredicate(WrSplitTextHarness, options)
|
|
95
|
+
.addOption('text', options.text, (harness, text) => HarnessPredicate.stringMatches(harness.getAccessibleText(), text))
|
|
96
|
+
.addOption('pieceCount', options.pieceCount, async (harness, count) => (await harness.getPieceCount()) === count)
|
|
97
|
+
.addOption('textAlign', options.textAlign, async (harness, align) => (await harness.getTextAlign()) === align);
|
|
98
|
+
}
|
|
99
|
+
pieces = this.locatorForAll(PIECE);
|
|
100
|
+
spaces = this.locatorForAll(SPACE);
|
|
101
|
+
/**
|
|
102
|
+
* The whole string, as the one readable copy holds it.
|
|
103
|
+
*
|
|
104
|
+
* All a screen reader gets, since every drawn piece is out of the tree — lose this
|
|
105
|
+
* span and the component becomes a nameless pile of hidden spans, with no other
|
|
106
|
+
* symptom anywhere in the DOM.
|
|
107
|
+
*
|
|
108
|
+
* Untrimmed, deliberately: leading and trailing whitespace is content the split has to
|
|
109
|
+
* carry, and {@link getRenderedText} is meant to compare equal to this. A trimmed read
|
|
110
|
+
* on one side of that comparison would forgive the case it exists to catch.
|
|
111
|
+
*/
|
|
112
|
+
async getAccessibleText() {
|
|
113
|
+
return rawText(await this.locatorFor(SR_ONLY)());
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* The animated pieces, in document order — one per character, or one per word.
|
|
117
|
+
*
|
|
118
|
+
* The split the stagger runs over: `['H', 'i']` at the default `splitType="chars"`
|
|
119
|
+
* against `['Hi', 'there']` for `"words"`, and `['a', '🚀']` for a surrogate pair the
|
|
120
|
+
* code-point split kept whole instead of halving into two broken glyphs. Whitespace is
|
|
121
|
+
* not here — it is drawn separately and never staggered, so see {@link getSpaceCount}.
|
|
122
|
+
*/
|
|
123
|
+
async getPieces() {
|
|
124
|
+
return Promise.all((await this.pieces()).map(rawText));
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* How many pieces the stagger runs over.
|
|
128
|
+
*
|
|
129
|
+
* `delay` × this is the real length of the effect, and the only number about the
|
|
130
|
+
* animation present in the document.
|
|
131
|
+
*/
|
|
132
|
+
async getPieceCount() {
|
|
133
|
+
return (await this.pieces()).length;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* How many whitespace runs sit between the pieces.
|
|
137
|
+
*
|
|
138
|
+
* Its own method because whitespace is deliberately excluded from the animated set:
|
|
139
|
+
* reclassifying spaces as pieces would leave {@link getRenderedText} identical, change
|
|
140
|
+
* the stagger count and the rhythm of the reveal, and show up nowhere else.
|
|
141
|
+
*/
|
|
142
|
+
async getSpaceCount() {
|
|
143
|
+
return (await this.spaces()).length;
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Everything the component drew, pieces and spaces together, in document order.
|
|
147
|
+
*
|
|
148
|
+
* The assertion this exists for is `getRenderedText() === getAccessibleText()`: the
|
|
149
|
+
* split must be lossless. A swallowed space, a re-split that dropped a character or a
|
|
150
|
+
* surrogate pair cut in half all leave the accessible copy intact and are invisible
|
|
151
|
+
* from anywhere else.
|
|
152
|
+
*/
|
|
153
|
+
async getRenderedText() {
|
|
154
|
+
const parts = await this.locatorForAll(`${PIECE}, ${SPACE}`)();
|
|
155
|
+
return (await Promise.all(parts.map(rawText))).join('');
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Whether every drawn piece is out of the accessibility tree.
|
|
159
|
+
*
|
|
160
|
+
* The defect this component exists to avoid is a screen reader reading
|
|
161
|
+
* `H. e. l. l. o.`, and one piece missing `aria-hidden` brings it back — so this is
|
|
162
|
+
* all-or-nothing, across both element kinds: a readable space is heard as a pause in
|
|
163
|
+
* the middle of a word.
|
|
164
|
+
*
|
|
165
|
+
* Throws when the split produced nothing at all. An empty run of elements satisfies
|
|
166
|
+
* "every one of them is hidden" without anything being hidden, which is also what a
|
|
167
|
+
* component that stopped rendering its pieces would report.
|
|
168
|
+
*/
|
|
169
|
+
async isTextHidden() {
|
|
170
|
+
const parts = await this.locatorForAll(`${PIECE}, ${SPACE}`)();
|
|
171
|
+
if (parts.length === 0) {
|
|
172
|
+
throw new Error('WrSplitTextHarness.isTextHidden(): this <wr-split-text> drew no pieces, so there is nothing to be ' +
|
|
173
|
+
'hidden from a screen reader and "true" would be a vacuous answer — the same one a component that ' +
|
|
174
|
+
'stopped splitting its text would give. Check `text` is set, or ask getPieceCount().');
|
|
175
|
+
}
|
|
176
|
+
const hidden = await Promise.all(parts.map(part => part.getAttribute('aria-hidden')));
|
|
177
|
+
return hidden.every(value => value === 'true');
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* The alignment the host was given, as the component wrote it.
|
|
181
|
+
*
|
|
182
|
+
* Off the inline `style` attribute rather than through `getCssValue`, and the
|
|
183
|
+
* difference is not pedantry: computed style answers for an element nobody wrote to —
|
|
184
|
+
* with the inherited or user-agent value — so a host that stopped reflecting the input
|
|
185
|
+
* altogether would keep reporting a plausible `left` or `start`. The attribute is the
|
|
186
|
+
* fact being asserted.
|
|
187
|
+
*
|
|
188
|
+
* `null` means the host carries no alignment at all, which the component never does:
|
|
189
|
+
* the binding is unconditional and the input defaults to `'center'`.
|
|
190
|
+
*/
|
|
191
|
+
async getTextAlign() {
|
|
192
|
+
const match = INLINE_TEXT_ALIGN.exec((await (await this.host()).getAttribute('style')) ?? '');
|
|
193
|
+
return match ? match[1].trim() : null;
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Whether the reveal has taken the pieces over — NOT whether anything is animating.
|
|
197
|
+
*
|
|
198
|
+
* Read off the inline `style` attribute the component writes itself, so it stays
|
|
199
|
+
* `true` once the reveal has finished: the tween commits its end state back to the
|
|
200
|
+
* same inline properties on `onfinish`. Anyone reaching for `isAnimating()` wants a
|
|
201
|
+
* frame, and there are no frames here.
|
|
202
|
+
*
|
|
203
|
+
* `false` is the answer worth asserting. It means no piece was written to, which is
|
|
204
|
+
* both the pre-trigger state and the reduced-motion promise — the component returns
|
|
205
|
+
* before staging anything, so a reader who asked for less motion cannot be left with
|
|
206
|
+
* text parked at `from.opacity: 0` by a tween that never runs.
|
|
207
|
+
*
|
|
208
|
+
* ANY piece counts, not all of them, and that is the conservative reading rather than
|
|
209
|
+
* a loose one: a stagger that stages the first piece and then throws leaves exactly
|
|
210
|
+
* one glyph invisible, which is a real failure — and one jsdom reproduces every time
|
|
211
|
+
* `Element.prototype.animate` is missing.
|
|
212
|
+
*/
|
|
213
|
+
async hasStagedMotion() {
|
|
214
|
+
const styles = await Promise.all((await this.pieces()).map(piece => piece.getAttribute('style')));
|
|
215
|
+
return styles.some(style => INLINE_OPACITY.test(style ?? ''));
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Generated bundle index. Do not edit.
|
|
221
|
+
*/
|
|
222
|
+
|
|
223
|
+
export { WrSplitTextHarness };
|
|
224
|
+
//# sourceMappingURL=ngwr-split-text-testing.mjs.map
|