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,228 @@
|
|
|
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 one text node the state machine writes into — the whole of its observable output. */
|
|
10
|
+
const CONTENT = '.wr-typewriter__content';
|
|
11
|
+
/** The caret. Not hidden when it is unwanted — removed from the document entirely. */
|
|
12
|
+
const CURSOR = '.wr-typewriter__cursor';
|
|
13
|
+
/**
|
|
14
|
+
* A `color:` declaration in an inline `style` attribute.
|
|
15
|
+
*
|
|
16
|
+
* The run has to start at the beginning or just after a `;` so that `background-color`
|
|
17
|
+
* — or any other `-color` longhand a consumer's own style attribute might carry — cannot
|
|
18
|
+
* be mistaken for it.
|
|
19
|
+
*/
|
|
20
|
+
const INLINE_COLOR = /(?:^|;)\s*color\s*:\s*([^;]+)/;
|
|
21
|
+
/** Exactly what the element holds — untrimmed, unlike `TestElement.text()`. */
|
|
22
|
+
async function rawText(element) {
|
|
23
|
+
return (await element.getProperty('textContent')) ?? '';
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Test harness for `<wr-typewriter>`.
|
|
27
|
+
*
|
|
28
|
+
* **The typed string is the entire component, and it is a moving target.**
|
|
29
|
+
* {@link getText} reads the one text node the machine writes into, which mid-run holds
|
|
30
|
+
* a FRAGMENT — `'Hell'` on the way to `'Hello'` — and between sentences holds whatever
|
|
31
|
+
* is left of the previous one. That is not a limitation to work around; it is what the
|
|
32
|
+
* component renders, and pinning it tick by tick is what proves the machine types one
|
|
33
|
+
* character at a time, deletes one at a time, pauses, cycles, and stops when told not to
|
|
34
|
+
* loop. Install fake timers for `setTimeout` / `clearTimeout` only and advance the clock
|
|
35
|
+
* yourself before every read — awaiting a harness method flushes microtasks, never
|
|
36
|
+
* timers, so an unadvanced read answers with the previous frame.
|
|
37
|
+
*
|
|
38
|
+
* **What is typed is also what is announced, and no second copy exists.** There is
|
|
39
|
+
* deliberately no `getAccessibleText()` here: the component renders one plain text node
|
|
40
|
+
* and nothing else, so a screen reader landing mid-word hears the fragment. (Its sibling
|
|
41
|
+
* `wr-rotating-text` carries a visually-hidden copy of the whole phrase; this one does
|
|
42
|
+
* not, and inventing a method that implied otherwise would report an accessible layer
|
|
43
|
+
* that is not there.) The single a11y decision the component does make is the caret, and
|
|
44
|
+
* {@link isCursorDecorative} is how a spec holds it: without `aria-hidden` every phrase
|
|
45
|
+
* is announced as `Hello pipe`, and nothing else in the DOM would change.
|
|
46
|
+
*
|
|
47
|
+
* **The methods that are missing are missing on purpose.** There is no `isTyping()` or
|
|
48
|
+
* `getPhase()` — the phase is a private signal that never reaches the document, and its
|
|
49
|
+
* only proxy, the caret vanishing, exists solely when `hideCursorWhileTyping` is set, so
|
|
50
|
+
* the method would report "not typing" for the DEFAULT configuration while the machine
|
|
51
|
+
* types. No `isComplete()` / `isFinished()`: a machine stopped on its last sentence and
|
|
52
|
+
* one pausing between sentences render the identical string and the identical caret, so
|
|
53
|
+
* assert the `sentenceComplete` output instead. No `isCursorBlinking()` /
|
|
54
|
+
* `getCursorOpacity()`: the blink is a `@keyframes` rule from a stylesheet a unit test
|
|
55
|
+
* never loads, and a mid-animation opacity is a number that means nothing.
|
|
56
|
+
* No `isVisible()` / `waitForVisible()`: `startOnVisible` hangs off an
|
|
57
|
+
* `IntersectionObserver` jsdom does not implement, so any answer would be the spec's own
|
|
58
|
+
* stub echoing back. And no `getTypingSpeed()` / `getDeletingSpeed()` /
|
|
59
|
+
* `getPauseDuration()` / `getInitialDelay()`: none of them is ever written to the DOM, so
|
|
60
|
+
* reading them would mean reading the component instance rather than the rendered
|
|
61
|
+
* control. They are observable exactly once — as the cadence of {@link getText}.
|
|
62
|
+
*
|
|
63
|
+
* What DOES survive a test with no layout, besides the string: the caret's presence,
|
|
64
|
+
* glyph and `aria-hidden`, and the two values the host writes inline —
|
|
65
|
+
* {@link getColor} and {@link getCursorBlinkDuration}.
|
|
66
|
+
*
|
|
67
|
+
* ```ts
|
|
68
|
+
* // jsdom implements no IntersectionObserver, so `[startOnVisible]` throws on boot.
|
|
69
|
+
* vi.stubGlobal('IntersectionObserver', StubObserver);
|
|
70
|
+
* // Fake these two and no more: faking the microtask queue deadlocks `whenStable`,
|
|
71
|
+
* // which is where the machine boots and where every harness read stabilizes.
|
|
72
|
+
* vi.useFakeTimers({ toFake: ['setTimeout', 'clearTimeout'] });
|
|
73
|
+
* ```
|
|
74
|
+
*
|
|
75
|
+
* @example
|
|
76
|
+
* ```ts
|
|
77
|
+
* const typewriter = await loader.getHarness(WrTypewriterHarness);
|
|
78
|
+
*
|
|
79
|
+
* vi.advanceTimersByTime(0); // the first tick is scheduled at `initialDelay`
|
|
80
|
+
* expect(await typewriter.getText()).toBe('H');
|
|
81
|
+
*
|
|
82
|
+
* vi.advanceTimersByTime(50);
|
|
83
|
+
* expect(await typewriter.getText()).toBe('He');
|
|
84
|
+
* expect(await typewriter.isCursorDecorative()).toBe(true);
|
|
85
|
+
* ```
|
|
86
|
+
*
|
|
87
|
+
* @see https://ngwr.dev/guides/testing
|
|
88
|
+
*/
|
|
89
|
+
class WrTypewriterHarness extends ComponentHarness {
|
|
90
|
+
static hostSelector = 'wr-typewriter';
|
|
91
|
+
/** Build a predicate that narrows the query — pass to `getHarness` / `getAllHarnesses`. */
|
|
92
|
+
static with(options = {}) {
|
|
93
|
+
return new HarnessPredicate(WrTypewriterHarness, options)
|
|
94
|
+
.addOption('text', options.text, (harness, text) => HarnessPredicate.stringMatches(harness.getText(), text))
|
|
95
|
+
.addOption('hasCursor', options.hasCursor, async (harness, hasCursor) => (await harness.hasCursor()) === hasCursor);
|
|
96
|
+
}
|
|
97
|
+
content = this.locatorFor(CONTENT);
|
|
98
|
+
cursor = this.locatorForOptional(CURSOR);
|
|
99
|
+
/**
|
|
100
|
+
* What has been typed so far — a FRAGMENT while the machine is running.
|
|
101
|
+
*
|
|
102
|
+
* This is the whole state machine in one string, and every timing input is observable
|
|
103
|
+
* only through it: one character per `typingSpeed`, one fewer per `deletingSpeed`, the
|
|
104
|
+
* `pauseDuration` gap, the swap to the next entry in `[texts]`, and the stop that
|
|
105
|
+
* `[loop]="false"` puts on the last sentence. `reverseMode` shows up here too, and
|
|
106
|
+
* `[...await getText()].length` is what proves the reversal counted CODE POINTS: a
|
|
107
|
+
* `split('')` reversal turns one emoji into two lone surrogates, which is a pair of
|
|
108
|
+
* broken glyphs on screen and the same character count as a correct answer.
|
|
109
|
+
*
|
|
110
|
+
* Untrimmed, because the fragment is what it is: a phrase typed as far as `'Hello '`
|
|
111
|
+
* genuinely has a trailing space, and trimming would make that frame indistinguishable
|
|
112
|
+
* from the one before it.
|
|
113
|
+
*
|
|
114
|
+
* The empty string is a real state, not a failure — nothing has been typed until the
|
|
115
|
+
* first tick fires, and a deletion run passes back through it on its way to the next
|
|
116
|
+
* sentence.
|
|
117
|
+
*/
|
|
118
|
+
async getText() {
|
|
119
|
+
return rawText(await this.content());
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Whether a caret is in the document at all.
|
|
123
|
+
*
|
|
124
|
+
* The component removes the element rather than hiding it, which makes this two
|
|
125
|
+
* questions at once. Statically it answers `showCursor`. While the machine runs it
|
|
126
|
+
* also answers `hideCursorWhileTyping` — and the caret going away is the ONLY evidence
|
|
127
|
+
* anywhere in the DOM that the component considers itself mid-phase, since the phase
|
|
128
|
+
* signal is private. Do not read that as `isTyping()`, though: with
|
|
129
|
+
* `hideCursorWhileTyping` off, which is the default, the caret stays put from the first
|
|
130
|
+
* character to the last.
|
|
131
|
+
*/
|
|
132
|
+
async hasCursor() {
|
|
133
|
+
return (await this.cursor()) !== null;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* The caret glyph, or `null` when there is no caret.
|
|
137
|
+
*
|
|
138
|
+
* `null` rather than `''` on purpose: "no cursor element" and "a cursor element
|
|
139
|
+
* rendering nothing" are different failures, and an interpolated glyph that stopped
|
|
140
|
+
* arriving is exactly the second one. Untrimmed, since a caret can legitimately be a
|
|
141
|
+
* space-padded glyph and `TestElement.text()` would erase it.
|
|
142
|
+
*/
|
|
143
|
+
async getCursorCharacter() {
|
|
144
|
+
const cursor = await this.cursor();
|
|
145
|
+
return cursor ? rawText(cursor) : null;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Whether the caret is kept out of the accessibility tree.
|
|
149
|
+
*
|
|
150
|
+
* The component's one deliberate a11y decision, and a silent one to lose: drop the
|
|
151
|
+
* attribute and nothing on screen changes while every phrase starts being announced as
|
|
152
|
+
* `Hello pipe`.
|
|
153
|
+
*
|
|
154
|
+
* Throws when there is no caret. `false` would read as "the caret is announced", which
|
|
155
|
+
* is the opposite of what an absent caret means — and it is the answer a component that
|
|
156
|
+
* stopped rendering its cursor entirely would also give. Ask {@link hasCursor} first
|
|
157
|
+
* when the caret's presence is itself in question.
|
|
158
|
+
*/
|
|
159
|
+
async isCursorDecorative() {
|
|
160
|
+
const cursor = await this.cursor();
|
|
161
|
+
if (!cursor) {
|
|
162
|
+
throw new Error('WrTypewriterHarness.isCursorDecorative(): this <wr-typewriter> has no cursor element, so there is ' +
|
|
163
|
+
'nothing to be hidden from a screen reader and "false" would read as "the cursor is announced" — the ' +
|
|
164
|
+
'opposite of what an absent cursor means. `showCursor` is off, or `hideCursorWhileTyping` has taken ' +
|
|
165
|
+
'it away for this phase; ask hasCursor().');
|
|
166
|
+
}
|
|
167
|
+
return (await cursor.getAttribute('aria-hidden')) === 'true';
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* The caret's blink half-cycle in seconds, from the custom property the host writes.
|
|
171
|
+
*
|
|
172
|
+
* The blink itself is a CSS keyframe and unobservable, so this custom property is the
|
|
173
|
+
* only channel `[cursorBlinkDuration]` has into it — a binding that quietly stopped
|
|
174
|
+
* writing it would leave the blink stuck at the stylesheet's default with no other
|
|
175
|
+
* symptom anywhere.
|
|
176
|
+
*
|
|
177
|
+
* Throws rather than answering when the property is missing or unparseable, and says
|
|
178
|
+
* which it was. The stylesheet's own `0.5s` fallback is never applied in a test that
|
|
179
|
+
* loads no stylesheets, so an absent property reads as the empty string here — and
|
|
180
|
+
* `0.5` returned from a `parseFloat('')` would be indistinguishable from the component
|
|
181
|
+
* writing the default correctly.
|
|
182
|
+
*/
|
|
183
|
+
async getCursorBlinkDuration() {
|
|
184
|
+
const raw = (await (await this.host()).getCssValue('--wr-typewriter-cursor-blink')).trim();
|
|
185
|
+
const seconds = Number.parseFloat(raw);
|
|
186
|
+
if (!Number.isFinite(seconds)) {
|
|
187
|
+
throw new Error(`WrTypewriterHarness.getCursorBlinkDuration(): --wr-typewriter-cursor-blink reads ${JSON.stringify(raw)}, ` +
|
|
188
|
+
'which is not a number of seconds. The component writes this property inline on every render; the ' +
|
|
189
|
+
'stylesheet default behind it never applies in a test, so an empty read means the binding stopped ' +
|
|
190
|
+
'reaching the DOM rather than that the caret blinks at 0.5s.');
|
|
191
|
+
}
|
|
192
|
+
return seconds;
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* The colour the host paints the phrase in, exactly as it is written inline.
|
|
196
|
+
*
|
|
197
|
+
* `[textColors]` cycles per SENTENCE, so this is how a spec pins that the colour index
|
|
198
|
+
* follows the sentence index rather than drifting; and with no colours given the
|
|
199
|
+
* component writes the literal `inherit`, which is a value worth asserting rather than
|
|
200
|
+
* an absence.
|
|
201
|
+
*
|
|
202
|
+
* Read off the `style` ATTRIBUTE rather than through `getCssValue`, which would answer
|
|
203
|
+
* from a computed style. jsdom hands `inherit` straight back, so `getCssValue` would
|
|
204
|
+
* agree here and lie in a real browser, where the keyword resolves to whatever rgb
|
|
205
|
+
* triple the parent happens to carry.
|
|
206
|
+
*
|
|
207
|
+
* Throws when the host carries no `color` declaration: the component writes one on
|
|
208
|
+
* every render, so its absence means the binding is gone, and any string returned in
|
|
209
|
+
* its place would be a guess at what the page inherits.
|
|
210
|
+
*/
|
|
211
|
+
async getColor() {
|
|
212
|
+
const style = (await (await this.host()).getAttribute('style')) ?? '';
|
|
213
|
+
const declaration = INLINE_COLOR.exec(style);
|
|
214
|
+
if (!declaration) {
|
|
215
|
+
throw new Error(`WrTypewriterHarness.getColor(): the host carries no inline color — its style attribute is ` +
|
|
216
|
+
`${JSON.stringify(style)}. The component writes one on every render, "inherit" when [textColors] is ` +
|
|
217
|
+
'empty, so nothing there means the binding stopped reaching the DOM.');
|
|
218
|
+
}
|
|
219
|
+
return declaration[1].trim();
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Generated bundle index. Do not edit.
|
|
225
|
+
*/
|
|
226
|
+
|
|
227
|
+
export { WrTypewriterHarness };
|
|
228
|
+
//# sourceMappingURL=ngwr-typewriter-testing.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ngwr-typewriter-testing.mjs","sources":["../../../projects/lib/typewriter/testing/wr-typewriter-harness.ts","../../../projects/lib/typewriter/testing/ngwr-typewriter-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, type TestElement } from '@angular/cdk/testing';\n\nimport type { WrTypewriterHarnessFilters } from './interfaces';\n\n/** The one text node the state machine writes into — the whole of its observable output. */\nconst CONTENT = '.wr-typewriter__content';\n\n/** The caret. Not hidden when it is unwanted — removed from the document entirely. */\nconst CURSOR = '.wr-typewriter__cursor';\n\n/**\n * A `color:` declaration in an inline `style` attribute.\n *\n * The run has to start at the beginning or just after a `;` so that `background-color`\n * — or any other `-color` longhand a consumer's own style attribute might carry — cannot\n * be mistaken for it.\n */\nconst INLINE_COLOR = /(?:^|;)\\s*color\\s*:\\s*([^;]+)/;\n\n/** Exactly what the element holds — untrimmed, unlike `TestElement.text()`. */\nasync function rawText(element: TestElement): Promise<string> {\n return (await element.getProperty<string | null>('textContent')) ?? '';\n}\n\n/**\n * Test harness for `<wr-typewriter>`.\n *\n * **The typed string is the entire component, and it is a moving target.**\n * {@link getText} reads the one text node the machine writes into, which mid-run holds\n * a FRAGMENT — `'Hell'` on the way to `'Hello'` — and between sentences holds whatever\n * is left of the previous one. That is not a limitation to work around; it is what the\n * component renders, and pinning it tick by tick is what proves the machine types one\n * character at a time, deletes one at a time, pauses, cycles, and stops when told not to\n * loop. Install fake timers for `setTimeout` / `clearTimeout` only and advance the clock\n * yourself before every read — awaiting a harness method flushes microtasks, never\n * timers, so an unadvanced read answers with the previous frame.\n *\n * **What is typed is also what is announced, and no second copy exists.** There is\n * deliberately no `getAccessibleText()` here: the component renders one plain text node\n * and nothing else, so a screen reader landing mid-word hears the fragment. (Its sibling\n * `wr-rotating-text` carries a visually-hidden copy of the whole phrase; this one does\n * not, and inventing a method that implied otherwise would report an accessible layer\n * that is not there.) The single a11y decision the component does make is the caret, and\n * {@link isCursorDecorative} is how a spec holds it: without `aria-hidden` every phrase\n * is announced as `Hello pipe`, and nothing else in the DOM would change.\n *\n * **The methods that are missing are missing on purpose.** There is no `isTyping()` or\n * `getPhase()` — the phase is a private signal that never reaches the document, and its\n * only proxy, the caret vanishing, exists solely when `hideCursorWhileTyping` is set, so\n * the method would report \"not typing\" for the DEFAULT configuration while the machine\n * types. No `isComplete()` / `isFinished()`: a machine stopped on its last sentence and\n * one pausing between sentences render the identical string and the identical caret, so\n * assert the `sentenceComplete` output instead. No `isCursorBlinking()` /\n * `getCursorOpacity()`: the blink is a `@keyframes` rule from a stylesheet a unit test\n * never loads, and a mid-animation opacity is a number that means nothing.\n * No `isVisible()` / `waitForVisible()`: `startOnVisible` hangs off an\n * `IntersectionObserver` jsdom does not implement, so any answer would be the spec's own\n * stub echoing back. And no `getTypingSpeed()` / `getDeletingSpeed()` /\n * `getPauseDuration()` / `getInitialDelay()`: none of them is ever written to the DOM, so\n * reading them would mean reading the component instance rather than the rendered\n * control. They are observable exactly once — as the cadence of {@link getText}.\n *\n * What DOES survive a test with no layout, besides the string: the caret's presence,\n * glyph and `aria-hidden`, and the two values the host writes inline —\n * {@link getColor} and {@link getCursorBlinkDuration}.\n *\n * ```ts\n * // jsdom implements no IntersectionObserver, so `[startOnVisible]` throws on boot.\n * vi.stubGlobal('IntersectionObserver', StubObserver);\n * // Fake these two and no more: faking the microtask queue deadlocks `whenStable`,\n * // which is where the machine boots and where every harness read stabilizes.\n * vi.useFakeTimers({ toFake: ['setTimeout', 'clearTimeout'] });\n * ```\n *\n * @example\n * ```ts\n * const typewriter = await loader.getHarness(WrTypewriterHarness);\n *\n * vi.advanceTimersByTime(0); // the first tick is scheduled at `initialDelay`\n * expect(await typewriter.getText()).toBe('H');\n *\n * vi.advanceTimersByTime(50);\n * expect(await typewriter.getText()).toBe('He');\n * expect(await typewriter.isCursorDecorative()).toBe(true);\n * ```\n *\n * @see https://ngwr.dev/guides/testing\n */\nexport class WrTypewriterHarness extends ComponentHarness {\n static hostSelector = 'wr-typewriter';\n\n /** Build a predicate that narrows the query — pass to `getHarness` / `getAllHarnesses`. */\n static with(options: WrTypewriterHarnessFilters = {}): HarnessPredicate<WrTypewriterHarness> {\n return new HarnessPredicate(WrTypewriterHarness, options)\n .addOption('text', options.text, (harness, text) => HarnessPredicate.stringMatches(harness.getText(), text))\n .addOption(\n 'hasCursor',\n options.hasCursor,\n async (harness, hasCursor) => (await harness.hasCursor()) === hasCursor\n );\n }\n\n private readonly content = this.locatorFor(CONTENT);\n private readonly cursor = this.locatorForOptional(CURSOR);\n\n /**\n * What has been typed so far — a FRAGMENT while the machine is running.\n *\n * This is the whole state machine in one string, and every timing input is observable\n * only through it: one character per `typingSpeed`, one fewer per `deletingSpeed`, the\n * `pauseDuration` gap, the swap to the next entry in `[texts]`, and the stop that\n * `[loop]=\"false\"` puts on the last sentence. `reverseMode` shows up here too, and\n * `[...await getText()].length` is what proves the reversal counted CODE POINTS: a\n * `split('')` reversal turns one emoji into two lone surrogates, which is a pair of\n * broken glyphs on screen and the same character count as a correct answer.\n *\n * Untrimmed, because the fragment is what it is: a phrase typed as far as `'Hello '`\n * genuinely has a trailing space, and trimming would make that frame indistinguishable\n * from the one before it.\n *\n * The empty string is a real state, not a failure — nothing has been typed until the\n * first tick fires, and a deletion run passes back through it on its way to the next\n * sentence.\n */\n async getText(): Promise<string> {\n return rawText(await this.content());\n }\n\n /**\n * Whether a caret is in the document at all.\n *\n * The component removes the element rather than hiding it, which makes this two\n * questions at once. Statically it answers `showCursor`. While the machine runs it\n * also answers `hideCursorWhileTyping` — and the caret going away is the ONLY evidence\n * anywhere in the DOM that the component considers itself mid-phase, since the phase\n * signal is private. Do not read that as `isTyping()`, though: with\n * `hideCursorWhileTyping` off, which is the default, the caret stays put from the first\n * character to the last.\n */\n async hasCursor(): Promise<boolean> {\n return (await this.cursor()) !== null;\n }\n\n /**\n * The caret glyph, or `null` when there is no caret.\n *\n * `null` rather than `''` on purpose: \"no cursor element\" and \"a cursor element\n * rendering nothing\" are different failures, and an interpolated glyph that stopped\n * arriving is exactly the second one. Untrimmed, since a caret can legitimately be a\n * space-padded glyph and `TestElement.text()` would erase it.\n */\n async getCursorCharacter(): Promise<string | null> {\n const cursor = await this.cursor();\n return cursor ? rawText(cursor) : null;\n }\n\n /**\n * Whether the caret is kept out of the accessibility tree.\n *\n * The component's one deliberate a11y decision, and a silent one to lose: drop the\n * attribute and nothing on screen changes while every phrase starts being announced as\n * `Hello pipe`.\n *\n * Throws when there is no caret. `false` would read as \"the caret is announced\", which\n * is the opposite of what an absent caret means — and it is the answer a component that\n * stopped rendering its cursor entirely would also give. Ask {@link hasCursor} first\n * when the caret's presence is itself in question.\n */\n async isCursorDecorative(): Promise<boolean> {\n const cursor = await this.cursor();\n\n if (!cursor) {\n throw new Error(\n 'WrTypewriterHarness.isCursorDecorative(): this <wr-typewriter> has no cursor element, so there is ' +\n 'nothing to be hidden from a screen reader and \"false\" would read as \"the cursor is announced\" — the ' +\n 'opposite of what an absent cursor means. `showCursor` is off, or `hideCursorWhileTyping` has taken ' +\n 'it away for this phase; ask hasCursor().'\n );\n }\n\n return (await cursor.getAttribute('aria-hidden')) === 'true';\n }\n\n /**\n * The caret's blink half-cycle in seconds, from the custom property the host writes.\n *\n * The blink itself is a CSS keyframe and unobservable, so this custom property is the\n * only channel `[cursorBlinkDuration]` has into it — a binding that quietly stopped\n * writing it would leave the blink stuck at the stylesheet's default with no other\n * symptom anywhere.\n *\n * Throws rather than answering when the property is missing or unparseable, and says\n * which it was. The stylesheet's own `0.5s` fallback is never applied in a test that\n * loads no stylesheets, so an absent property reads as the empty string here — and\n * `0.5` returned from a `parseFloat('')` would be indistinguishable from the component\n * writing the default correctly.\n */\n async getCursorBlinkDuration(): Promise<number> {\n const raw = (await (await this.host()).getCssValue('--wr-typewriter-cursor-blink')).trim();\n const seconds = Number.parseFloat(raw);\n\n if (!Number.isFinite(seconds)) {\n throw new Error(\n `WrTypewriterHarness.getCursorBlinkDuration(): --wr-typewriter-cursor-blink reads ${JSON.stringify(raw)}, ` +\n 'which is not a number of seconds. The component writes this property inline on every render; the ' +\n 'stylesheet default behind it never applies in a test, so an empty read means the binding stopped ' +\n 'reaching the DOM rather than that the caret blinks at 0.5s.'\n );\n }\n\n return seconds;\n }\n\n /**\n * The colour the host paints the phrase in, exactly as it is written inline.\n *\n * `[textColors]` cycles per SENTENCE, so this is how a spec pins that the colour index\n * follows the sentence index rather than drifting; and with no colours given the\n * component writes the literal `inherit`, which is a value worth asserting rather than\n * an absence.\n *\n * Read off the `style` ATTRIBUTE rather than through `getCssValue`, which would answer\n * from a computed style. jsdom hands `inherit` straight back, so `getCssValue` would\n * agree here and lie in a real browser, where the keyword resolves to whatever rgb\n * triple the parent happens to carry.\n *\n * Throws when the host carries no `color` declaration: the component writes one on\n * every render, so its absence means the binding is gone, and any string returned in\n * its place would be a guess at what the page inherits.\n */\n async getColor(): Promise<string> {\n const style = (await (await this.host()).getAttribute('style')) ?? '';\n const declaration = INLINE_COLOR.exec(style);\n\n if (!declaration) {\n throw new Error(\n `WrTypewriterHarness.getColor(): the host carries no inline color — its style attribute is ` +\n `${JSON.stringify(style)}. The component writes one on every render, \"inherit\" when [textColors] is ` +\n 'empty, so nothing there means the binding stopped reaching the DOM.'\n );\n }\n\n return declaration[1].trim();\n }\n}\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public-api';\n"],"names":[],"mappings":"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"}
|
|
@@ -16,7 +16,7 @@ import { InjectionToken } from '@angular/core';
|
|
|
16
16
|
* Distinct from `Version` exported by `@angular/core`, which reports the
|
|
17
17
|
* loaded Angular runtime version (e.g. `'22.0.0'`), not ngwr's.
|
|
18
18
|
*/
|
|
19
|
-
const NGWR_VERSION = '11.
|
|
19
|
+
const NGWR_VERSION = '11.2.0';
|
|
20
20
|
|
|
21
21
|
/**
|
|
22
22
|
* @license
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ngwr-version.mjs","sources":["../../../projects/lib/version/version.ts","../../../projects/lib/version/version.token.ts","../../../projects/lib/version/ngwr-version.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\n/**\n * The ngwr library version string, kept in sync with\n * `projects/lib/package.json` by `scripts/release-prepare.ts`.\n *\n * Prefer this over reading `package.json` at runtime — that requires a JSON\n * import or `require`, neither of which tree-shakes well in Angular builds.\n *\n * Distinct from `Version` exported by `@angular/core`, which reports the\n * loaded Angular runtime version (e.g. `'22.0.0'`), not ngwr's.\n */\nexport const NGWR_VERSION = '11.
|
|
1
|
+
{"version":3,"file":"ngwr-version.mjs","sources":["../../../projects/lib/version/version.ts","../../../projects/lib/version/version.token.ts","../../../projects/lib/version/ngwr-version.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\n/**\n * The ngwr library version string, kept in sync with\n * `projects/lib/package.json` by `scripts/release-prepare.ts`.\n *\n * Prefer this over reading `package.json` at runtime — that requires a JSON\n * import or `require`, neither of which tree-shakes well in Angular builds.\n *\n * Distinct from `Version` exported by `@angular/core`, which reports the\n * loaded Angular runtime version (e.g. `'22.0.0'`), not ngwr's.\n */\nexport const NGWR_VERSION = '11.2.0';\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 { InjectionToken } from '@angular/core';\n\nimport { NGWR_VERSION } from './version';\n\n/**\n * Injection token holding the ngwr library version string.\n *\n * Default factory returns {@link NGWR_VERSION}. Override it in tests or in\n * apps that want to surface a custom build identifier.\n *\n * @example\n * ```ts\n * @Component({...})\n * export class FooterComponent {\n * readonly version = inject(NGWR_VERSION_TOKEN);\n * }\n * ```\n */\nexport const NGWR_VERSION_TOKEN = new InjectionToken<string>('NGWR_VERSION_TOKEN', {\n providedIn: 'root',\n factory: () => NGWR_VERSION,\n});\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public-api';\n"],"names":[],"mappings":";;AAAA;;;;;AAKG;AAEH;;;;;;;;;AASG;AACI,MAAM,YAAY,GAAG;;ACjB5B;;;;;AAKG;AAMH;;;;;;;;;;;;;AAaG;MACU,kBAAkB,GAAG,IAAI,cAAc,CAAS,oBAAoB,EAAE;AACjF,IAAA,UAAU,EAAE,MAAM;AAClB,IAAA,OAAO,EAAE,MAAM,YAAY;AAC5B,CAAA;;AC5BD;;AAEG;;;;"}
|
|
@@ -0,0 +1,128 @@
|
|
|
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
|
+
/** `--wr-waves-x-gap: 40px` on the host's own `style` attribute — the pitch, as published. */
|
|
10
|
+
const X_GAP = /(?:^|;)\s*--wr-waves-x-gap\s*:\s*([^;]+)/;
|
|
11
|
+
/**
|
|
12
|
+
* Test harness for `<wr-waves>` — a field of lines drifting on perlin noise.
|
|
13
|
+
*
|
|
14
|
+
* **The lines themselves are unreadable and always will be.** They are strokes on a 2D
|
|
15
|
+
* canvas, seeded from the host's measured box and redrawn every frame; a unit test has
|
|
16
|
+
* no box and no drawing context, so there is nothing to count and nothing to compare.
|
|
17
|
+
* What the component publishes to the DOM is small, deliberate and worth pinning: the
|
|
18
|
+
* handover flag {@link isPainted}, the pitch {@link getLineGapPx} that the CSS stand-in
|
|
19
|
+
* grid draws at before the canvas takes over, and the surface itself.
|
|
20
|
+
*
|
|
21
|
+
* Two style reads, taken two different ways, and the difference is the point.
|
|
22
|
+
* {@link getBackgroundColor} is computed, because nothing but the component declares
|
|
23
|
+
* that property and the resolved value is what gets painted. {@link getLineGapPx} is
|
|
24
|
+
* read from the inline declaration, because the stylesheet declares
|
|
25
|
+
* `--wr-waves-x-gap` too — as the stand-in's own 10px fallback — so a computed read
|
|
26
|
+
* would answer `10px` on a component that had stopped publishing anything, which is
|
|
27
|
+
* precisely the break the method exists to catch.
|
|
28
|
+
*
|
|
29
|
+
* There is no `getLineCount()`, `getPoints()` or `getCanvasSize()`: the grid is seeded
|
|
30
|
+
* from `getBoundingClientRect()`, which is 0×0 here, and the numbers that fall out of
|
|
31
|
+
* that describe nothing. No `getLineColor()` either — it is a canvas `strokeStyle`
|
|
32
|
+
* resolved from a themed custom property that only a stylesheet can supply. No
|
|
33
|
+
* `moveMouse()`: the pointer listeners are on `window` and are only installed once a
|
|
34
|
+
* context exists. And no `isReducedMotion()`, which is the sharpest of them — the
|
|
35
|
+
* reduced-motion path draws one static grid through the same code and sets the same
|
|
36
|
+
* flag, so the DOM is identical either way and any answer would be a guess.
|
|
37
|
+
*
|
|
38
|
+
* @example
|
|
39
|
+
* ```ts
|
|
40
|
+
* const waves = await loader.getHarness(WrWavesHarness);
|
|
41
|
+
*
|
|
42
|
+
* expect(await waves.hasCanvas()).toBe(true);
|
|
43
|
+
* expect(await waves.isDecorative()).toBe(true);
|
|
44
|
+
* expect(await waves.getLineGapPx()).toBe(10);
|
|
45
|
+
* ```
|
|
46
|
+
*
|
|
47
|
+
* @see https://ngwr.dev/guides/testing
|
|
48
|
+
*/
|
|
49
|
+
class WrWavesHarness extends ComponentHarness {
|
|
50
|
+
static hostSelector = 'wr-waves';
|
|
51
|
+
/** Build a predicate that narrows the query — pass to `getHarness` / `getAllHarnesses`. */
|
|
52
|
+
static with(options = {}) {
|
|
53
|
+
return new HarnessPredicate(WrWavesHarness, options).addOption('painted', options.painted, async (harness, painted) => (await harness.isPainted()) === painted);
|
|
54
|
+
}
|
|
55
|
+
canvas = this.locatorFor('.wr-waves__canvas');
|
|
56
|
+
/**
|
|
57
|
+
* Whether the canvas has drawn a frame and retired the CSS stand-in grid.
|
|
58
|
+
*
|
|
59
|
+
* **In a unit test this is always `false`, and asserting exactly that is the point.**
|
|
60
|
+
* jsdom hands back no 2D context, so the component must leave the stand-in up rather
|
|
61
|
+
* than declare itself painted over a blank box — the flag is set inside the draw for
|
|
62
|
+
* that reason, not at the end of the boot. A spec that waits for this to turn `true`
|
|
63
|
+
* waits forever.
|
|
64
|
+
*/
|
|
65
|
+
async isPainted() {
|
|
66
|
+
return (await this.host()).hasClass('wr-waves--painted');
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* The horizontal gap between lines, in pixels, as the component publishes it.
|
|
70
|
+
*
|
|
71
|
+
* `[xGap]` feeds the canvas maths, which is invisible here — but it is also published
|
|
72
|
+
* to the stand-in grid, and that is the half a spec can hold: a field whose stand-in
|
|
73
|
+
* draws at 10px and whose canvas then arrives at 40px changes pitch under the reader
|
|
74
|
+
* on hydration, a visible pop that nothing else can catch.
|
|
75
|
+
*/
|
|
76
|
+
async getLineGapPx() {
|
|
77
|
+
const style = (await (await this.host()).getAttribute('style')) ?? '';
|
|
78
|
+
const match = X_GAP.exec(style);
|
|
79
|
+
const gap = match ? Number.parseFloat(match[1]) : Number.NaN;
|
|
80
|
+
if (Number.isNaN(gap)) {
|
|
81
|
+
throw new Error(`WrWavesHarness.getLineGapPx(): the host publishes no \`--wr-waves-x-gap\` (style: "${style}"). The ` +
|
|
82
|
+
'component writes it from `[xGap]` on every render, so its absence means the stand-in grid has fallen ' +
|
|
83
|
+
"back to the stylesheet's own 10px and no longer follows the input.");
|
|
84
|
+
}
|
|
85
|
+
return gap;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* The fill behind the lines, resolved.
|
|
89
|
+
*
|
|
90
|
+
* Comes back as the browser reports it rather than as it was authored — the default
|
|
91
|
+
* `'transparent'` reads as `'rgba(0, 0, 0, 0)'` and `'#5227ff'` as
|
|
92
|
+
* `'rgb(82, 39, 255)'` — which is true in jsdom and in a real browser alike, so an
|
|
93
|
+
* assertion written against it travels. Worth pinning because the alternative
|
|
94
|
+
* implementation is to fill the canvas instead, and a canvas fill is invisible to
|
|
95
|
+
* everything until it has painted: the background has to be a real declaration on the
|
|
96
|
+
* host so the field is the right colour from the prerendered HTML onwards.
|
|
97
|
+
*/
|
|
98
|
+
async getBackgroundColor() {
|
|
99
|
+
return (await this.host()).getCssValue('background-color');
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Whether the field is hidden from assistive tech.
|
|
103
|
+
*
|
|
104
|
+
* A background canvas has nothing to announce, so losing this leaves a screen reader
|
|
105
|
+
* stopping on a nameless graphic in front of the section's actual content.
|
|
106
|
+
*/
|
|
107
|
+
async isDecorative() {
|
|
108
|
+
return (await (await this.canvas()).getAttribute('aria-hidden')) === 'true';
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Whether the drawing surface is in the markup.
|
|
112
|
+
*
|
|
113
|
+
* It is there in every state, including the ones where nothing is ever drawn on it —
|
|
114
|
+
* prerendered, no 2D context, reduced motion. That is the contract: the element goes
|
|
115
|
+
* up and the component stops, so the stand-in grid has something to sit behind and
|
|
116
|
+
* {@link isPainted} has something to answer about.
|
|
117
|
+
*/
|
|
118
|
+
async hasCanvas() {
|
|
119
|
+
return (await this.locatorForOptional('.wr-waves__canvas')()) !== null;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Generated bundle index. Do not edit.
|
|
125
|
+
*/
|
|
126
|
+
|
|
127
|
+
export { WrWavesHarness };
|
|
128
|
+
//# sourceMappingURL=ngwr-waves-testing.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ngwr-waves-testing.mjs","sources":["../../../projects/lib/waves/testing/wr-waves-harness.ts","../../../projects/lib/waves/testing/ngwr-waves-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 { WrWavesHarnessFilters } from './interfaces';\n\n/** `--wr-waves-x-gap: 40px` on the host's own `style` attribute — the pitch, as published. */\nconst X_GAP = /(?:^|;)\\s*--wr-waves-x-gap\\s*:\\s*([^;]+)/;\n\n/**\n * Test harness for `<wr-waves>` — a field of lines drifting on perlin noise.\n *\n * **The lines themselves are unreadable and always will be.** They are strokes on a 2D\n * canvas, seeded from the host's measured box and redrawn every frame; a unit test has\n * no box and no drawing context, so there is nothing to count and nothing to compare.\n * What the component publishes to the DOM is small, deliberate and worth pinning: the\n * handover flag {@link isPainted}, the pitch {@link getLineGapPx} that the CSS stand-in\n * grid draws at before the canvas takes over, and the surface itself.\n *\n * Two style reads, taken two different ways, and the difference is the point.\n * {@link getBackgroundColor} is computed, because nothing but the component declares\n * that property and the resolved value is what gets painted. {@link getLineGapPx} is\n * read from the inline declaration, because the stylesheet declares\n * `--wr-waves-x-gap` too — as the stand-in's own 10px fallback — so a computed read\n * would answer `10px` on a component that had stopped publishing anything, which is\n * precisely the break the method exists to catch.\n *\n * There is no `getLineCount()`, `getPoints()` or `getCanvasSize()`: the grid is seeded\n * from `getBoundingClientRect()`, which is 0×0 here, and the numbers that fall out of\n * that describe nothing. No `getLineColor()` either — it is a canvas `strokeStyle`\n * resolved from a themed custom property that only a stylesheet can supply. No\n * `moveMouse()`: the pointer listeners are on `window` and are only installed once a\n * context exists. And no `isReducedMotion()`, which is the sharpest of them — the\n * reduced-motion path draws one static grid through the same code and sets the same\n * flag, so the DOM is identical either way and any answer would be a guess.\n *\n * @example\n * ```ts\n * const waves = await loader.getHarness(WrWavesHarness);\n *\n * expect(await waves.hasCanvas()).toBe(true);\n * expect(await waves.isDecorative()).toBe(true);\n * expect(await waves.getLineGapPx()).toBe(10);\n * ```\n *\n * @see https://ngwr.dev/guides/testing\n */\nexport class WrWavesHarness extends ComponentHarness {\n static hostSelector = 'wr-waves';\n\n /** Build a predicate that narrows the query — pass to `getHarness` / `getAllHarnesses`. */\n static with(options: WrWavesHarnessFilters = {}): HarnessPredicate<WrWavesHarness> {\n return new HarnessPredicate(WrWavesHarness, options).addOption(\n 'painted',\n options.painted,\n async (harness, painted) => (await harness.isPainted()) === painted\n );\n }\n\n private readonly canvas = this.locatorFor('.wr-waves__canvas');\n\n /**\n * Whether the canvas has drawn a frame and retired the CSS stand-in grid.\n *\n * **In a unit test this is always `false`, and asserting exactly that is the point.**\n * jsdom hands back no 2D context, so the component must leave the stand-in up rather\n * than declare itself painted over a blank box — the flag is set inside the draw for\n * that reason, not at the end of the boot. A spec that waits for this to turn `true`\n * waits forever.\n */\n async isPainted(): Promise<boolean> {\n return (await this.host()).hasClass('wr-waves--painted');\n }\n\n /**\n * The horizontal gap between lines, in pixels, as the component publishes it.\n *\n * `[xGap]` feeds the canvas maths, which is invisible here — but it is also published\n * to the stand-in grid, and that is the half a spec can hold: a field whose stand-in\n * draws at 10px and whose canvas then arrives at 40px changes pitch under the reader\n * on hydration, a visible pop that nothing else can catch.\n */\n async getLineGapPx(): Promise<number> {\n const style = (await (await this.host()).getAttribute('style')) ?? '';\n const match = X_GAP.exec(style);\n const gap = match ? Number.parseFloat(match[1]) : Number.NaN;\n if (Number.isNaN(gap)) {\n throw new Error(\n `WrWavesHarness.getLineGapPx(): the host publishes no \\`--wr-waves-x-gap\\` (style: \"${style}\"). The ` +\n 'component writes it from `[xGap]` on every render, so its absence means the stand-in grid has fallen ' +\n \"back to the stylesheet's own 10px and no longer follows the input.\"\n );\n }\n return gap;\n }\n\n /**\n * The fill behind the lines, resolved.\n *\n * Comes back as the browser reports it rather than as it was authored — the default\n * `'transparent'` reads as `'rgba(0, 0, 0, 0)'` and `'#5227ff'` as\n * `'rgb(82, 39, 255)'` — which is true in jsdom and in a real browser alike, so an\n * assertion written against it travels. Worth pinning because the alternative\n * implementation is to fill the canvas instead, and a canvas fill is invisible to\n * everything until it has painted: the background has to be a real declaration on the\n * host so the field is the right colour from the prerendered HTML onwards.\n */\n async getBackgroundColor(): Promise<string> {\n return (await this.host()).getCssValue('background-color');\n }\n\n /**\n * Whether the field is hidden from assistive tech.\n *\n * A background canvas has nothing to announce, so losing this leaves a screen reader\n * stopping on a nameless graphic in front of the section's actual content.\n */\n async isDecorative(): Promise<boolean> {\n return (await (await this.canvas()).getAttribute('aria-hidden')) === 'true';\n }\n\n /**\n * Whether the drawing surface is in the markup.\n *\n * It is there in every state, including the ones where nothing is ever drawn on it —\n * prerendered, no 2D context, reduced motion. That is the contract: the element goes\n * up and the component stops, so the stand-in grid has something to sit behind and\n * {@link isPainted} has something to answer about.\n */\n async hasCanvas(): Promise<boolean> {\n return (await this.locatorForOptional('.wr-waves__canvas')()) !== null;\n }\n}\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public-api';\n"],"names":[],"mappings":";;AAAA;;;;;AAKG;AAMH;AACA,MAAM,KAAK,GAAG,0CAA0C;AAExD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAqCG;AACG,MAAO,cAAe,SAAQ,gBAAgB,CAAA;AAClD,IAAA,OAAO,YAAY,GAAG,UAAU;;AAGhC,IAAA,OAAO,IAAI,CAAC,OAAA,GAAiC,EAAE,EAAA;AAC7C,QAAA,OAAO,IAAI,gBAAgB,CAAC,cAAc,EAAE,OAAO,CAAC,CAAC,SAAS,CAC5D,SAAS,EACT,OAAO,CAAC,OAAO,EACf,OAAO,OAAO,EAAE,OAAO,KAAK,CAAC,MAAM,OAAO,CAAC,SAAS,EAAE,MAAM,OAAO,CACpE;IACH;AAEiB,IAAA,MAAM,GAAG,IAAI,CAAC,UAAU,CAAC,mBAAmB,CAAC;AAE9D;;;;;;;;AAQG;AACH,IAAA,MAAM,SAAS,GAAA;AACb,QAAA,OAAO,CAAC,MAAM,IAAI,CAAC,IAAI,EAAE,EAAE,QAAQ,CAAC,mBAAmB,CAAC;IAC1D;AAEA;;;;;;;AAOG;AACH,IAAA,MAAM,YAAY,GAAA;AAChB,QAAA,MAAM,KAAK,GAAG,CAAC,MAAM,CAAC,MAAM,IAAI,CAAC,IAAI,EAAE,EAAE,YAAY,CAAC,OAAO,CAAC,KAAK,EAAE;QACrE,MAAM,KAAK,GAAG,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC;QAC/B,MAAM,GAAG,GAAG,KAAK,GAAG,MAAM,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,GAAG,MAAM,CAAC,GAAG;AAC5D,QAAA,IAAI,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE;AACrB,YAAA,MAAM,IAAI,KAAK,CACb,CAAA,mFAAA,EAAsF,KAAK,CAAA,QAAA,CAAU;gBACnG,uGAAuG;AACvG,gBAAA,oEAAoE,CACvE;QACH;AACA,QAAA,OAAO,GAAG;IACZ;AAEA;;;;;;;;;;AAUG;AACH,IAAA,MAAM,kBAAkB,GAAA;AACtB,QAAA,OAAO,CAAC,MAAM,IAAI,CAAC,IAAI,EAAE,EAAE,WAAW,CAAC,kBAAkB,CAAC;IAC5D;AAEA;;;;;AAKG;AACH,IAAA,MAAM,YAAY,GAAA;AAChB,QAAA,OAAO,CAAC,MAAM,CAAC,MAAM,IAAI,CAAC,MAAM,EAAE,EAAE,YAAY,CAAC,aAAa,CAAC,MAAM,MAAM;IAC7E;AAEA;;;;;;;AAOG;AACH,IAAA,MAAM,SAAS,GAAA;AACb,QAAA,OAAO,CAAC,MAAM,IAAI,CAAC,kBAAkB,CAAC,mBAAmB,CAAC,EAAE,MAAM,IAAI;IACxE;;;ACxIF;;AAEG;;;;"}
|