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 @@
|
|
|
1
|
+
{"version":3,"file":"ngwr-falling-text-testing.mjs","sources":["../../../projects/lib/falling-text/testing/wr-falling-text-harness.ts","../../../projects/lib/falling-text/testing/ngwr-falling-text-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 { WrFallingTextHarnessFilters } from './interfaces';\n\n/** Read one declaration out of an inline `style` attribute, or `null` when it is not there. */\nfunction inlineStyle(style: string | null, property: string): string | null {\n for (const declaration of (style ?? '').split(';')) {\n const colon = declaration.indexOf(':');\n if (colon === -1) continue;\n if (declaration.slice(0, colon).trim() !== property) continue;\n return declaration.slice(colon + 1).trim();\n }\n return null;\n}\n\n/**\n * Test harness for `<wr-falling-text>`.\n *\n * **The assertion that matters here is that the animation did not eat the text.** The\n * component cuts the sentence into one span per word so a physics loop can throw them\n * around, and {@link getText} is the check that a screen reader still gets prose out of\n * the other end. It normalises whitespace on the way, because the separators are real\n * ` ` characters rather than plain spaces — so `textContent` is never byte-identical\n * to the `[text]` input, and a spec comparing the two raw would fail on a component that\n * works perfectly.\n *\n * The rest of the readable surface is the split itself: {@link getWords} pins the rule\n * (split on spaces, empty tokens dropped) and {@link getHighlightedWords} pins the\n * prefix match, which is genuine logic — `grav` highlights `gravity`, and a whole-word\n * implementation would satisfy every naive test but not this one.\n *\n * **Nothing positional is offered, and that is not a jsdom concession.** The words'\n * `transform` values are one frame of a loop that never stops, seeded by `Math.random()`,\n * so they are not reproducible even in a real browser; there is no rest state to call\n * settled; the drag path needs `setPointerCapture` and AABB hit-testing over boxes that\n * a unit test measures as 0×0 at the origin, where every word contains every point. Nor\n * is `[gravity]` or `[trigger]` readable — neither reaches the DOM, and answering them\n * would mean reaching into the component instance. There is no `isReducedMotion()`\n * either: under reduced motion the component simply returns before touching anything, so\n * the only honest proxy is {@link hasReleased}, which reports what it did rather than\n * what it was told.\n *\n * **{@link hasReleased} and {@link release} have a precondition, and without it they are\n * worthless.** The simulator measures the host and each word before it starts, and bails\n * on a zero-sized box — which is every box in a test with no layout. So the spec has to\n * give them one, and it has to do it BEFORE the trigger fires: the component marks itself\n * started on the way past that guard, and the trigger listener is `{ once: true }`, so a\n * failed first attempt is permanent.\n *\n * ```ts\n * // In your spec, on a fixture mounted with trigger=\"click\", before calling release():\n * const box = { width: 300, height: 120, left: 0, top: 0, right: 300, bottom: 120 } as DOMRect;\n * for (const el of fixture.nativeElement.querySelectorAll('wr-falling-text, .wr-falling-text__word')) {\n * el.getBoundingClientRect = () => box;\n * }\n * ```\n *\n * Assert `hasReleased()` is `true` in that control case before you assert it is `false`\n * anywhere else, or a spec proves only that the guard fired.\n *\n * @example\n * ```ts\n * const loader = TestbedHarnessEnvironment.loader(fixture);\n * const text = await loader.getHarness(WrFallingTextHarness);\n *\n * expect(await text.getText()).toBe('gravity pulls every word down');\n * expect(await text.getHighlightedWords()).toEqual(['gravity']);\n * ```\n *\n * @see https://ngwr.dev/guides/testing\n */\nexport class WrFallingTextHarness extends ComponentHarness {\n static hostSelector = 'wr-falling-text';\n\n /** Build a predicate that narrows the query — pass to `getHarness` / `getAllHarnesses`. */\n static with(options: WrFallingTextHarnessFilters = {}): HarnessPredicate<WrFallingTextHarness> {\n return new HarnessPredicate(WrFallingTextHarness, options).addOption('text', options.text, (harness, text) =>\n HarnessPredicate.stringMatches(harness.getText(), text)\n );\n }\n\n private readonly wordEls = this.locatorForAll('.wr-falling-text__word');\n\n /**\n * The whole sentence, as a screen reader reads it: whitespace collapsed and trimmed.\n *\n * The one assertion worth making about a component that dismantles a paragraph. The\n * words are separated by `.wr-falling-text__space` spans holding U+00A0, so this\n * collapses them back to ordinary spaces — losing those spans would be invisible to\n * every other check here while turning the announcement into one run-on word.\n *\n * DOM order never changes once the physics starts. The words are absolutely positioned\n * and scattered on screen, but they are still read in the order they were typeset, so\n * this answers the same before and after {@link release}.\n */\n async getText(): Promise<string> {\n return (await (await this.host()).text()).replace(/\\s+/g, ' ').trim();\n }\n\n /**\n * Each word, in DOM order.\n *\n * Pins the split rule: on a literal space, with empty tokens dropped. Each of these is\n * also one body in the simulator, so the length is the body count — which is why there\n * is no separate `getWordCount()`, a method that would invite asserting how many\n * without asserting which.\n */\n async getWords(): Promise<string[]> {\n return Promise.all((await this.wordEls()).map(word => word.text()));\n }\n\n /**\n * The words carrying the highlight, in DOM order.\n *\n * `[highlightWords]` is matched as a PREFIX and case-sensitively, so `['grav']` picks\n * up `gravity` and `['Grav']` picks up nothing. The highlight is colour alone with no\n * non-visual counterpart, which is acceptable only because it adds no meaning the\n * sentence does not already carry.\n */\n async getHighlightedWords(): Promise<string[]> {\n return Promise.all((await this.locatorForAll('.wr-falling-text__word--hl')()).map(word => word.text()));\n }\n\n /**\n * Whether one word is highlighted — for asserting a keyword without depending on the\n * order or the completeness of the whole list.\n *\n * Takes the word as it is rendered, not the keyword that matched it: ask for `gravity`,\n * not for `grav`. Throws for a word the sentence does not contain, since `false` there\n * would read as \"present and not highlighted\" and quietly survive a typo. A word that\n * appears twice is answered once, which is safe — matching is on the text alone, so\n * both copies always agree.\n */\n async isHighlighted(word: string): Promise<boolean> {\n const words = await this.wordEls();\n const texts = await Promise.all(words.map(el => el.text()));\n const index = texts.indexOf(word);\n\n if (index === -1) {\n throw new Error(\n `WrFallingTextHarness.isHighlighted(): there is no word \"${word}\" in \"${await this.getText()}\". The text ` +\n 'is split on spaces, so ask for a single word rather than a phrase, and for the word as rendered ' +\n 'rather than the prefix that highlights it.'\n );\n }\n return words[index].hasClass('wr-falling-text__word--hl');\n }\n\n /**\n * The font size as the consumer wrote it — `'2rem'`, not `'32px'`.\n *\n * Read off the inline `style` attribute rather than through `getCssValue()`. The two\n * agree here and would part company in a real browser, which resolves `font-size` to\n * pixels; `[fontSize]` takes a CSS length and handing back a different unit than the\n * one that went in is the sort of green unit test that hides a broken assertion.\n *\n * Throws when nothing is written, which takes an empty `[fontSize]` — the component\n * writes this on every render otherwise, default included.\n */\n async getFontSize(): Promise<string> {\n const words = await this.locatorFor('.wr-falling-text__words')();\n const size = inlineStyle(await words.getAttribute('style'), 'font-size');\n\n if (size === null) {\n throw new Error(\n 'WrFallingTextHarness.getFontSize(): the words carry no inline font-size. The component writes one from ' +\n '[fontSize] on every render — an empty string is the only binding that removes it, and it leaves the ' +\n 'text at whatever the page inherits.'\n );\n }\n return size;\n }\n\n /**\n * Whether the simulator has taken the words over.\n *\n * Reads the `position: absolute` the component writes onto every word before its first\n * frame — a value it wrote itself, not a measurement, and the only difference in the\n * DOM between \"the physics is running\" and \"the words are still in normal flow\". That\n * makes it the way to observe the trigger contract and the reduced-motion escape, since\n * neither leaves anything else behind.\n *\n * **It is `false` by default in a test, for a reason that has nothing to do with the\n * component.** Without layout every box measures 0×0 and the simulator refuses to start\n * — see this class's docs for the stub, and assert a `true` before you trust a `false`.\n *\n * Throws for a component rendering no words at all, where the question does not apply.\n */\n async hasReleased(): Promise<boolean> {\n const words = await this.wordEls();\n\n if (words.length === 0) {\n throw new Error(\n 'WrFallingTextHarness.hasReleased(): there are no words. Empty [text] renders nothing, and the simulator ' +\n 'has nothing to take over — check getWords() instead.'\n );\n }\n return inlineStyle(await words[0].getAttribute('style'), 'position') === 'absolute';\n }\n\n /**\n * Fire the events a `click` or `hover` trigger is waiting for.\n *\n * Both, because the harness cannot tell which one it is: `[trigger]` never reaches the\n * DOM. The component consumes at most one — its listener is registered `{ once: true }`\n * and the simulator refuses to start twice — so this is safe to call whatever the\n * trigger is, and safe to call again.\n *\n * It does nothing for `auto`, which released on mount, and nothing for `scroll`, which\n * waits on an `IntersectionObserver`: jsdom has none, so a spec has to stub the global\n * and fire the callback itself. Same precondition as {@link hasReleased} — stub the\n * boxes BEFORE calling this, because a start that bails on a zero-sized host still\n * burns the trigger.\n *\n * Both are dispatched as bare events rather than through the harness's `click()` and\n * `hover()` helpers, which would bring a whole pointer sequence with them. Released\n * words listen for `pointerdown` so they can be picked up and thrown, and that handler\n * calls `setPointerCapture` — which a unit environment does not implement, so the\n * realistic version of this method would throw the moment the simulator was running.\n */\n async release(): Promise<void> {\n const host = await this.host();\n await host.dispatchEvent('click');\n await host.dispatchEvent('mouseenter');\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,134 @@
|
|
|
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-fuzzy-text>` — a headline drawn into a canvas, one row of pixels
|
|
11
|
+
* at a time, with a random offset per row.
|
|
12
|
+
*
|
|
13
|
+
* **The drawing is out of reach and the accessible half is not, which is the whole shape
|
|
14
|
+
* of this harness.** There is no 2D context in a unit test — `getContext('2d')` returns
|
|
15
|
+
* `null`, and the component's `init()` gives up on the line after — so nothing downstream
|
|
16
|
+
* of that ever happens: no font load, no animation frame, no canvas listeners, no sizing.
|
|
17
|
+
* What survives is the markup, and the markup is the part that regressed: everything this
|
|
18
|
+
* component rendered used to be a canvas, so a screen reader got an empty element where
|
|
19
|
+
* the page showed a headline. {@link getText} and {@link isDecorative} are that fix,
|
|
20
|
+
* asserted as the PAIR it has to be — a readable copy plus a hidden pixel copy. Reading
|
|
21
|
+
* one without the other passes just as happily on a component that announces the headline
|
|
22
|
+
* twice.
|
|
23
|
+
*
|
|
24
|
+
* **The canvas is selected by tag, not by a class.** It carries no BEM name, unlike the
|
|
25
|
+
* two sibling animation canvases — this entry point ships no stylesheet at all, which is
|
|
26
|
+
* also why {@link isTextVisuallyHidden} is a real question rather than a formality.
|
|
27
|
+
*
|
|
28
|
+
* Nothing about the animation is offered, and none of it is a gap that could be closed by
|
|
29
|
+
* trying harder. There is no `getIntensity()` / `isFuzzing()` / `isGlitching()`: those are
|
|
30
|
+
* closure variables inside `init()` that only ever manifest as pixels, and in a test the
|
|
31
|
+
* function returns before they exist. No `hover()` / `click()` / `getHoverIntensity()`:
|
|
32
|
+
* the pointer listeners are attached only after a real context AND a resolved
|
|
33
|
+
* `document.fonts` — neither of which a unit test has — so a dispatched event would land
|
|
34
|
+
* on nothing and the assertion would pass vacuously; the hit test also measures the canvas
|
|
35
|
+
* box, which is all zeros. No `getCanvasSize()`, which would be asserting jsdom's untouched
|
|
36
|
+
* 300×150 default rather than the component's layout maths. No `getColor()` /
|
|
37
|
+
* `getFontSize()` / `getFontFamily()` / `getGradient()`: every one of those becomes a
|
|
38
|
+
* canvas font string or a fill style and never reaches the DOM. No `getRenderedText()` or
|
|
39
|
+
* `readPixel()` — there are no pixels, and faking a context would change what every spec
|
|
40
|
+
* here is testing. And no `isReducedMotion()`: the preference is honoured by taking a
|
|
41
|
+
* different DRAWING path, with identical markup, classes and attributes either way, so the
|
|
42
|
+
* method would answer the same for a component that honoured it and one that ignored it.
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* ```ts
|
|
46
|
+
* const headline = await loader.getHarness(WrFuzzyTextHarness.with({ text: '404' }));
|
|
47
|
+
*
|
|
48
|
+
* expect(await headline.getText()).toBe('404');
|
|
49
|
+
* expect(await headline.isDecorative()).toBe(true);
|
|
50
|
+
* ```
|
|
51
|
+
*
|
|
52
|
+
* @see https://ngwr.dev/guides/testing
|
|
53
|
+
*/
|
|
54
|
+
class WrFuzzyTextHarness extends ComponentHarness {
|
|
55
|
+
static hostSelector = 'wr-fuzzy-text';
|
|
56
|
+
/** Build a predicate that narrows the query — pass to `getHarness` / `getAllHarnesses`. */
|
|
57
|
+
static with(options = {}) {
|
|
58
|
+
return new HarnessPredicate(WrFuzzyTextHarness, options).addOption('text', options.text, (harness, text) => HarnessPredicate.stringMatches(harness.getText(), text));
|
|
59
|
+
}
|
|
60
|
+
srOnly = this.locatorFor('.wr-fuzzy-text__sr-only');
|
|
61
|
+
canvas = this.locatorForOptional('canvas');
|
|
62
|
+
/**
|
|
63
|
+
* The headline as TEXT, trimmed.
|
|
64
|
+
*
|
|
65
|
+
* From the screen-reader span, which is the only place the string exists as characters —
|
|
66
|
+
* the visible copy is glyphs painted into a canvas. This is the single assertion that
|
|
67
|
+
* separates the component from an empty box for anyone not looking at the screen, and it
|
|
68
|
+
* is the one that has already regressed once.
|
|
69
|
+
*
|
|
70
|
+
* There is no letter-by-letter split to worry about here, unlike the other text
|
|
71
|
+
* animations in the library: the span holds one text node, so what a reader announces is
|
|
72
|
+
* what {@link WrFuzzyTextHarness.getText} returns.
|
|
73
|
+
*/
|
|
74
|
+
async getText() {
|
|
75
|
+
return (await this.srOnly()).text();
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Whether the pixel copy stays out of the accessibility tree.
|
|
79
|
+
*
|
|
80
|
+
* The other half of {@link getText}. The canvas has no text and no name, so without its
|
|
81
|
+
* `aria-hidden` a reader meets an anonymous element next to the headline it already
|
|
82
|
+
* read — or, if the span ever goes away again, only the anonymous element. Asserting the
|
|
83
|
+
* two together is what pins the arrangement; asserting either alone does not.
|
|
84
|
+
*/
|
|
85
|
+
async isDecorative() {
|
|
86
|
+
const canvas = await this.canvas();
|
|
87
|
+
if (!canvas) {
|
|
88
|
+
throw new Error('WrFuzzyTextHarness.isDecorative(): there is no <canvas> in this headline at all, so there is nothing to ' +
|
|
89
|
+
'keep out of the accessibility tree. That is a different failure from an announced canvas — ask ' +
|
|
90
|
+
'hasCanvas() to tell the two apart.');
|
|
91
|
+
}
|
|
92
|
+
return (await canvas.getAttribute('aria-hidden')) === 'true';
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Whether the drawing surface is in the markup.
|
|
96
|
+
*
|
|
97
|
+
* True even where it can never be painted — under SSR, in a prerender, in a browser that
|
|
98
|
+
* refuses a 2D context, and in every unit test — because the element is rendered
|
|
99
|
+
* unconditionally and only the drawing depends on a context. That is precisely the state
|
|
100
|
+
* the readable copy exists to cover, so this is the method that says "the fallback is
|
|
101
|
+
* carrying the page right now".
|
|
102
|
+
*/
|
|
103
|
+
async hasCanvas() {
|
|
104
|
+
return (await this.canvas()) !== null;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Whether the readable copy hides itself without a stylesheet.
|
|
108
|
+
*
|
|
109
|
+
* This entry point ships no CSS. The span is therefore hidden with inline declarations
|
|
110
|
+
* rather than an `.sr-only` class, and that is load-bearing rather than a style choice:
|
|
111
|
+
* move the block into a class and every consumer who imported no CSS sees the headline
|
|
112
|
+
* twice — once as pixels, once as a stray line of markup underneath.
|
|
113
|
+
*
|
|
114
|
+
* Three of the declarations are checked, not one, because they fail apart: `position`
|
|
115
|
+
* takes the span out of flow, the 1px box collapses it, and `clip-path` is what keeps
|
|
116
|
+
* the character still reachable by a reader instead of `display: none`-ing it away.
|
|
117
|
+
*/
|
|
118
|
+
async isTextVisuallyHidden() {
|
|
119
|
+
const span = await this.srOnly();
|
|
120
|
+
const [position, width, clipPath] = await Promise.all([
|
|
121
|
+
span.getCssValue('position'),
|
|
122
|
+
span.getCssValue('width'),
|
|
123
|
+
span.getCssValue('clip-path'),
|
|
124
|
+
]);
|
|
125
|
+
return position === 'absolute' && width === '1px' && clipPath === 'inset(50%)';
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Generated bundle index. Do not edit.
|
|
131
|
+
*/
|
|
132
|
+
|
|
133
|
+
export { WrFuzzyTextHarness };
|
|
134
|
+
//# sourceMappingURL=ngwr-fuzzy-text-testing.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ngwr-fuzzy-text-testing.mjs","sources":["../../../projects/lib/fuzzy-text/testing/wr-fuzzy-text-harness.ts","../../../projects/lib/fuzzy-text/testing/ngwr-fuzzy-text-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 { WrFuzzyTextHarnessFilters } from './interfaces';\n\n/**\n * Test harness for `<wr-fuzzy-text>` — a headline drawn into a canvas, one row of pixels\n * at a time, with a random offset per row.\n *\n * **The drawing is out of reach and the accessible half is not, which is the whole shape\n * of this harness.** There is no 2D context in a unit test — `getContext('2d')` returns\n * `null`, and the component's `init()` gives up on the line after — so nothing downstream\n * of that ever happens: no font load, no animation frame, no canvas listeners, no sizing.\n * What survives is the markup, and the markup is the part that regressed: everything this\n * component rendered used to be a canvas, so a screen reader got an empty element where\n * the page showed a headline. {@link getText} and {@link isDecorative} are that fix,\n * asserted as the PAIR it has to be — a readable copy plus a hidden pixel copy. Reading\n * one without the other passes just as happily on a component that announces the headline\n * twice.\n *\n * **The canvas is selected by tag, not by a class.** It carries no BEM name, unlike the\n * two sibling animation canvases — this entry point ships no stylesheet at all, which is\n * also why {@link isTextVisuallyHidden} is a real question rather than a formality.\n *\n * Nothing about the animation is offered, and none of it is a gap that could be closed by\n * trying harder. There is no `getIntensity()` / `isFuzzing()` / `isGlitching()`: those are\n * closure variables inside `init()` that only ever manifest as pixels, and in a test the\n * function returns before they exist. No `hover()` / `click()` / `getHoverIntensity()`:\n * the pointer listeners are attached only after a real context AND a resolved\n * `document.fonts` — neither of which a unit test has — so a dispatched event would land\n * on nothing and the assertion would pass vacuously; the hit test also measures the canvas\n * box, which is all zeros. No `getCanvasSize()`, which would be asserting jsdom's untouched\n * 300×150 default rather than the component's layout maths. No `getColor()` /\n * `getFontSize()` / `getFontFamily()` / `getGradient()`: every one of those becomes a\n * canvas font string or a fill style and never reaches the DOM. No `getRenderedText()` or\n * `readPixel()` — there are no pixels, and faking a context would change what every spec\n * here is testing. And no `isReducedMotion()`: the preference is honoured by taking a\n * different DRAWING path, with identical markup, classes and attributes either way, so the\n * method would answer the same for a component that honoured it and one that ignored it.\n *\n * @example\n * ```ts\n * const headline = await loader.getHarness(WrFuzzyTextHarness.with({ text: '404' }));\n *\n * expect(await headline.getText()).toBe('404');\n * expect(await headline.isDecorative()).toBe(true);\n * ```\n *\n * @see https://ngwr.dev/guides/testing\n */\nexport class WrFuzzyTextHarness extends ComponentHarness {\n static hostSelector = 'wr-fuzzy-text';\n\n /** Build a predicate that narrows the query — pass to `getHarness` / `getAllHarnesses`. */\n static with(options: WrFuzzyTextHarnessFilters = {}): HarnessPredicate<WrFuzzyTextHarness> {\n return new HarnessPredicate(WrFuzzyTextHarness, options).addOption('text', options.text, (harness, text) =>\n HarnessPredicate.stringMatches(harness.getText(), text)\n );\n }\n\n private readonly srOnly = this.locatorFor('.wr-fuzzy-text__sr-only');\n private readonly canvas = this.locatorForOptional('canvas');\n\n /**\n * The headline as TEXT, trimmed.\n *\n * From the screen-reader span, which is the only place the string exists as characters —\n * the visible copy is glyphs painted into a canvas. This is the single assertion that\n * separates the component from an empty box for anyone not looking at the screen, and it\n * is the one that has already regressed once.\n *\n * There is no letter-by-letter split to worry about here, unlike the other text\n * animations in the library: the span holds one text node, so what a reader announces is\n * what {@link WrFuzzyTextHarness.getText} returns.\n */\n async getText(): Promise<string> {\n return (await this.srOnly()).text();\n }\n\n /**\n * Whether the pixel copy stays out of the accessibility tree.\n *\n * The other half of {@link getText}. The canvas has no text and no name, so without its\n * `aria-hidden` a reader meets an anonymous element next to the headline it already\n * read — or, if the span ever goes away again, only the anonymous element. Asserting the\n * two together is what pins the arrangement; asserting either alone does not.\n */\n async isDecorative(): Promise<boolean> {\n const canvas = await this.canvas();\n if (!canvas) {\n throw new Error(\n 'WrFuzzyTextHarness.isDecorative(): there is no <canvas> in this headline at all, so there is nothing to ' +\n 'keep out of the accessibility tree. That is a different failure from an announced canvas — ask ' +\n 'hasCanvas() to tell the two apart.'\n );\n }\n return (await canvas.getAttribute('aria-hidden')) === 'true';\n }\n\n /**\n * Whether the drawing surface is in the markup.\n *\n * True even where it can never be painted — under SSR, in a prerender, in a browser that\n * refuses a 2D context, and in every unit test — because the element is rendered\n * unconditionally and only the drawing depends on a context. That is precisely the state\n * the readable copy exists to cover, so this is the method that says \"the fallback is\n * carrying the page right now\".\n */\n async hasCanvas(): Promise<boolean> {\n return (await this.canvas()) !== null;\n }\n\n /**\n * Whether the readable copy hides itself without a stylesheet.\n *\n * This entry point ships no CSS. The span is therefore hidden with inline declarations\n * rather than an `.sr-only` class, and that is load-bearing rather than a style choice:\n * move the block into a class and every consumer who imported no CSS sees the headline\n * twice — once as pixels, once as a stray line of markup underneath.\n *\n * Three of the declarations are checked, not one, because they fail apart: `position`\n * takes the span out of flow, the 1px box collapses it, and `clip-path` is what keeps\n * the character still reachable by a reader instead of `display: none`-ing it away.\n */\n async isTextVisuallyHidden(): Promise<boolean> {\n const span = await this.srOnly();\n const [position, width, clipPath] = await Promise.all([\n span.getCssValue('position'),\n span.getCssValue('width'),\n span.getCssValue('clip-path'),\n ]);\n return position === 'absolute' && width === '1px' && clipPath === 'inset(50%)';\n }\n}\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public-api';\n"],"names":[],"mappings":";;AAAA;;;;;AAKG;AAMH;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4CG;AACG,MAAO,kBAAmB,SAAQ,gBAAgB,CAAA;AACtD,IAAA,OAAO,YAAY,GAAG,eAAe;;AAGrC,IAAA,OAAO,IAAI,CAAC,OAAA,GAAqC,EAAE,EAAA;AACjD,QAAA,OAAO,IAAI,gBAAgB,CAAC,kBAAkB,EAAE,OAAO,CAAC,CAAC,SAAS,CAAC,MAAM,EAAE,OAAO,CAAC,IAAI,EAAE,CAAC,OAAO,EAAE,IAAI,KACrG,gBAAgB,CAAC,aAAa,CAAC,OAAO,CAAC,OAAO,EAAE,EAAE,IAAI,CAAC,CACxD;IACH;AAEiB,IAAA,MAAM,GAAG,IAAI,CAAC,UAAU,CAAC,yBAAyB,CAAC;AACnD,IAAA,MAAM,GAAG,IAAI,CAAC,kBAAkB,CAAC,QAAQ,CAAC;AAE3D;;;;;;;;;;;AAWG;AACH,IAAA,MAAM,OAAO,GAAA;QACX,OAAO,CAAC,MAAM,IAAI,CAAC,MAAM,EAAE,EAAE,IAAI,EAAE;IACrC;AAEA;;;;;;;AAOG;AACH,IAAA,MAAM,YAAY,GAAA;AAChB,QAAA,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,MAAM,EAAE;QAClC,IAAI,CAAC,MAAM,EAAE;YACX,MAAM,IAAI,KAAK,CACb,0GAA0G;gBACxG,iGAAiG;AACjG,gBAAA,oCAAoC,CACvC;QACH;QACA,OAAO,CAAC,MAAM,MAAM,CAAC,YAAY,CAAC,aAAa,CAAC,MAAM,MAAM;IAC9D;AAEA;;;;;;;;AAQG;AACH,IAAA,MAAM,SAAS,GAAA;QACb,OAAO,CAAC,MAAM,IAAI,CAAC,MAAM,EAAE,MAAM,IAAI;IACvC;AAEA;;;;;;;;;;;AAWG;AACH,IAAA,MAAM,oBAAoB,GAAA;AACxB,QAAA,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,MAAM,EAAE;AAChC,QAAA,MAAM,CAAC,QAAQ,EAAE,KAAK,EAAE,QAAQ,CAAC,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC;AACpD,YAAA,IAAI,CAAC,WAAW,CAAC,UAAU,CAAC;AAC5B,YAAA,IAAI,CAAC,WAAW,CAAC,OAAO,CAAC;AACzB,YAAA,IAAI,CAAC,WAAW,CAAC,WAAW,CAAC;AAC9B,SAAA,CAAC;QACF,OAAO,QAAQ,KAAK,UAAU,IAAI,KAAK,KAAK,KAAK,IAAI,QAAQ,KAAK,YAAY;IAChF;;;AC1IF;;AAEG;;;;"}
|
|
@@ -0,0 +1,86 @@
|
|
|
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-gauge>`.
|
|
11
|
+
*
|
|
12
|
+
* **The dial is `aria-hidden` and the printed number is optional, so the ARIA is the
|
|
13
|
+
* whole readable surface.** The component carries `role="meter"` with the value
|
|
14
|
+
* trio and an `aria-valuetext` that includes the suffix — which is what makes a
|
|
15
|
+
* gauge with `showValue` off readable at all. {@link getValue} is what it announces;
|
|
16
|
+
* {@link getDisplayValue} is what is drawn, and it is `null` when nothing is.
|
|
17
|
+
*
|
|
18
|
+
* The arc is an SVG path, and its `d` is a rendering detail rather than an
|
|
19
|
+
* assertion — it moves with the size, the stroke width and the sweep.
|
|
20
|
+
*
|
|
21
|
+
* @see https://ngwr.dev/guides/testing
|
|
22
|
+
*/
|
|
23
|
+
class WrGaugeHarness extends ComponentHarness {
|
|
24
|
+
static hostSelector = 'wr-gauge';
|
|
25
|
+
/** Build a predicate that narrows the query — pass to `getHarness` / `getAllHarnesses`. */
|
|
26
|
+
static with(options = {}) {
|
|
27
|
+
return new HarnessPredicate(WrGaugeHarness, options)
|
|
28
|
+
.addOption('ariaLabel', options.ariaLabel, (harness, label) => HarnessPredicate.stringMatches(harness.getAccessibleName(), label))
|
|
29
|
+
.addOption('value', options.value, async (harness, value) => (await harness.getValue()) === value);
|
|
30
|
+
}
|
|
31
|
+
surface = this.locatorFor('.wr-gauge__surface');
|
|
32
|
+
/** The role the dial announces — `meter`, which is what a gauge is. */
|
|
33
|
+
async getRole() {
|
|
34
|
+
return (await this.surface()).getAttribute('role');
|
|
35
|
+
}
|
|
36
|
+
/** The dial's accessible name. */
|
|
37
|
+
async getAccessibleName() {
|
|
38
|
+
return (await this.surface()).getAttribute('aria-label');
|
|
39
|
+
}
|
|
40
|
+
/** The value it announces, from `aria-valuenow`. */
|
|
41
|
+
async getValue() {
|
|
42
|
+
return Number(await (await this.surface()).getAttribute('aria-valuenow'));
|
|
43
|
+
}
|
|
44
|
+
/** The bottom of the range, from `aria-valuemin`. */
|
|
45
|
+
async getMin() {
|
|
46
|
+
return Number(await (await this.surface()).getAttribute('aria-valuemin'));
|
|
47
|
+
}
|
|
48
|
+
/** The top of the range, from `aria-valuemax`. */
|
|
49
|
+
async getMax() {
|
|
50
|
+
return Number(await (await this.surface()).getAttribute('aria-valuemax'));
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* The spoken form of the value — the number with its suffix.
|
|
54
|
+
*
|
|
55
|
+
* The reason a gauge with no printed value is still readable, and worth asserting
|
|
56
|
+
* separately from {@link getValue}: a percentage announced as a bare `72` says
|
|
57
|
+
* something different from `72%`.
|
|
58
|
+
*/
|
|
59
|
+
async getValueText() {
|
|
60
|
+
return (await this.surface()).getAttribute('aria-valuetext');
|
|
61
|
+
}
|
|
62
|
+
/** The number printed in the middle, suffix included, or `null` when `showValue` is off. */
|
|
63
|
+
async getDisplayValue() {
|
|
64
|
+
const text = await this.locatorForOptional('.wr-gauge__text')();
|
|
65
|
+
return text ? text.text() : null;
|
|
66
|
+
}
|
|
67
|
+
/** The suffix alone, or `null` when there is none — or nothing is printed. */
|
|
68
|
+
async getSuffix() {
|
|
69
|
+
const suffix = await this.locatorForOptional('.wr-gauge__suffix')();
|
|
70
|
+
if (!suffix)
|
|
71
|
+
return null;
|
|
72
|
+
const text = await suffix.text();
|
|
73
|
+
return text === '' ? null : text;
|
|
74
|
+
}
|
|
75
|
+
/** The dial's width in pixels, as the component writes it inline. */
|
|
76
|
+
async getSize() {
|
|
77
|
+
return Number.parseFloat(await (await this.surface()).getCssValue('width'));
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Generated bundle index. Do not edit.
|
|
83
|
+
*/
|
|
84
|
+
|
|
85
|
+
export { WrGaugeHarness };
|
|
86
|
+
//# sourceMappingURL=ngwr-gauge-testing.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ngwr-gauge-testing.mjs","sources":["../../../projects/lib/gauge/testing/wr-gauge-harness.ts","../../../projects/lib/gauge/testing/ngwr-gauge-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 { WrGaugeHarnessFilters } from './interfaces';\n\n/**\n * Test harness for `<wr-gauge>`.\n *\n * **The dial is `aria-hidden` and the printed number is optional, so the ARIA is the\n * whole readable surface.** The component carries `role=\"meter\"` with the value\n * trio and an `aria-valuetext` that includes the suffix — which is what makes a\n * gauge with `showValue` off readable at all. {@link getValue} is what it announces;\n * {@link getDisplayValue} is what is drawn, and it is `null` when nothing is.\n *\n * The arc is an SVG path, and its `d` is a rendering detail rather than an\n * assertion — it moves with the size, the stroke width and the sweep.\n *\n * @see https://ngwr.dev/guides/testing\n */\nexport class WrGaugeHarness extends ComponentHarness {\n static hostSelector = 'wr-gauge';\n\n /** Build a predicate that narrows the query — pass to `getHarness` / `getAllHarnesses`. */\n static with(options: WrGaugeHarnessFilters = {}): HarnessPredicate<WrGaugeHarness> {\n return new HarnessPredicate(WrGaugeHarness, options)\n .addOption('ariaLabel', options.ariaLabel, (harness, label) =>\n HarnessPredicate.stringMatches(harness.getAccessibleName(), label)\n )\n .addOption('value', options.value, async (harness, value) => (await harness.getValue()) === value);\n }\n\n private readonly surface = this.locatorFor('.wr-gauge__surface');\n\n /** The role the dial announces — `meter`, which is what a gauge is. */\n async getRole(): Promise<string | null> {\n return (await this.surface()).getAttribute('role');\n }\n\n /** The dial's accessible name. */\n async getAccessibleName(): Promise<string | null> {\n return (await this.surface()).getAttribute('aria-label');\n }\n\n /** The value it announces, from `aria-valuenow`. */\n async getValue(): Promise<number> {\n return Number(await (await this.surface()).getAttribute('aria-valuenow'));\n }\n\n /** The bottom of the range, from `aria-valuemin`. */\n async getMin(): Promise<number> {\n return Number(await (await this.surface()).getAttribute('aria-valuemin'));\n }\n\n /** The top of the range, from `aria-valuemax`. */\n async getMax(): Promise<number> {\n return Number(await (await this.surface()).getAttribute('aria-valuemax'));\n }\n\n /**\n * The spoken form of the value — the number with its suffix.\n *\n * The reason a gauge with no printed value is still readable, and worth asserting\n * separately from {@link getValue}: a percentage announced as a bare `72` says\n * something different from `72%`.\n */\n async getValueText(): Promise<string | null> {\n return (await this.surface()).getAttribute('aria-valuetext');\n }\n\n /** The number printed in the middle, suffix included, or `null` when `showValue` is off. */\n async getDisplayValue(): Promise<string | null> {\n const text = await this.locatorForOptional('.wr-gauge__text')();\n return text ? text.text() : null;\n }\n\n /** The suffix alone, or `null` when there is none — or nothing is printed. */\n async getSuffix(): Promise<string | null> {\n const suffix = await this.locatorForOptional('.wr-gauge__suffix')();\n if (!suffix) return null;\n const text = await suffix.text();\n return text === '' ? null : text;\n }\n\n /** The dial's width in pixels, as the component writes it inline. */\n async getSize(): Promise<number> {\n return Number.parseFloat(await (await this.surface()).getCssValue('width'));\n }\n}\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public-api';\n"],"names":[],"mappings":";;AAAA;;;;;AAKG;AAMH;;;;;;;;;;;;;AAaG;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;aAChD,SAAS,CAAC,WAAW,EAAE,OAAO,CAAC,SAAS,EAAE,CAAC,OAAO,EAAE,KAAK,KACxD,gBAAgB,CAAC,aAAa,CAAC,OAAO,CAAC,iBAAiB,EAAE,EAAE,KAAK,CAAC;aAEnE,SAAS,CAAC,OAAO,EAAE,OAAO,CAAC,KAAK,EAAE,OAAO,OAAO,EAAE,KAAK,KAAK,CAAC,MAAM,OAAO,CAAC,QAAQ,EAAE,MAAM,KAAK,CAAC;IACtG;AAEiB,IAAA,OAAO,GAAG,IAAI,CAAC,UAAU,CAAC,oBAAoB,CAAC;;AAGhE,IAAA,MAAM,OAAO,GAAA;AACX,QAAA,OAAO,CAAC,MAAM,IAAI,CAAC,OAAO,EAAE,EAAE,YAAY,CAAC,MAAM,CAAC;IACpD;;AAGA,IAAA,MAAM,iBAAiB,GAAA;AACrB,QAAA,OAAO,CAAC,MAAM,IAAI,CAAC,OAAO,EAAE,EAAE,YAAY,CAAC,YAAY,CAAC;IAC1D;;AAGA,IAAA,MAAM,QAAQ,GAAA;AACZ,QAAA,OAAO,MAAM,CAAC,MAAM,CAAC,MAAM,IAAI,CAAC,OAAO,EAAE,EAAE,YAAY,CAAC,eAAe,CAAC,CAAC;IAC3E;;AAGA,IAAA,MAAM,MAAM,GAAA;AACV,QAAA,OAAO,MAAM,CAAC,MAAM,CAAC,MAAM,IAAI,CAAC,OAAO,EAAE,EAAE,YAAY,CAAC,eAAe,CAAC,CAAC;IAC3E;;AAGA,IAAA,MAAM,MAAM,GAAA;AACV,QAAA,OAAO,MAAM,CAAC,MAAM,CAAC,MAAM,IAAI,CAAC,OAAO,EAAE,EAAE,YAAY,CAAC,eAAe,CAAC,CAAC;IAC3E;AAEA;;;;;;AAMG;AACH,IAAA,MAAM,YAAY,GAAA;AAChB,QAAA,OAAO,CAAC,MAAM,IAAI,CAAC,OAAO,EAAE,EAAE,YAAY,CAAC,gBAAgB,CAAC;IAC9D;;AAGA,IAAA,MAAM,eAAe,GAAA;QACnB,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,kBAAkB,CAAC,iBAAiB,CAAC,EAAE;AAC/D,QAAA,OAAO,IAAI,GAAG,IAAI,CAAC,IAAI,EAAE,GAAG,IAAI;IAClC;;AAGA,IAAA,MAAM,SAAS,GAAA;QACb,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,kBAAkB,CAAC,mBAAmB,CAAC,EAAE;AACnE,QAAA,IAAI,CAAC,MAAM;AAAE,YAAA,OAAO,IAAI;AACxB,QAAA,MAAM,IAAI,GAAG,MAAM,MAAM,CAAC,IAAI,EAAE;QAChC,OAAO,IAAI,KAAK,EAAE,GAAG,IAAI,GAAG,IAAI;IAClC;;AAGA,IAAA,MAAM,OAAO,GAAA;AACX,QAAA,OAAO,MAAM,CAAC,UAAU,CAAC,MAAM,CAAC,MAAM,IAAI,CAAC,OAAO,EAAE,EAAE,WAAW,CAAC,OAAO,CAAC,CAAC;IAC7E;;;AC5FF;;AAEG;;;;"}
|
|
@@ -0,0 +1,181 @@
|
|
|
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-glitch-text>`.
|
|
11
|
+
*
|
|
12
|
+
* **The one thing worth testing here is the pairing.** The two torn clones are
|
|
13
|
+
* `::before` / `::after` pseudo-elements whose glyphs come from `content:
|
|
14
|
+
* attr(data-text)`, so the string has to be in the host's text node AND in the host's
|
|
15
|
+
* `data-text` attribute. Drop either binding and the visible text stays flawless while
|
|
16
|
+
* the effect disappears — no error, no missing element, nothing else in the DOM
|
|
17
|
+
* changes. {@link isCloneTextInSync} is that assertion in one call.
|
|
18
|
+
*
|
|
19
|
+
* **Everything else the harness reads is an inline custom property**, because that is
|
|
20
|
+
* everything the component writes: the two clone durations, the two colour-split
|
|
21
|
+
* shadows and the optional slice background. They are worth pinning for the unit
|
|
22
|
+
* transforms — a duration that loses its `s`, or two clones that end up on the same
|
|
23
|
+
* multiple and tear in lockstep, is a regression that changes no class, no attribute
|
|
24
|
+
* and no text.
|
|
25
|
+
*
|
|
26
|
+
* **What the harness deliberately does not offer**, and why. There is no
|
|
27
|
+
* `isGlitching()`: the tear is a `@keyframes` animation on a pseudo-element, applied by
|
|
28
|
+
* a stylesheet a unit test never loads, so the only available answer is a class name
|
|
29
|
+
* repackaged as a state. There is no `hover()` / `isGlitchingOnHover()`: the gate is a
|
|
30
|
+
* `:hover` SELECTOR, and the DOM after a dispatched mouse event is byte-identical to
|
|
31
|
+
* the DOM before it — a spec built on that would pass against a component whose hover
|
|
32
|
+
* rules had been deleted. There is no `getCloneOffset()` / `getClipPath()`: the ±10px
|
|
33
|
+
* offsets and the tear bands live in the stylesheet and are pure layout besides. There
|
|
34
|
+
* is no `respectsReducedMotion()`: the handling is a `@media` block with no TypeScript
|
|
35
|
+
* counterpart, so the DOM is identical in both states and there is nothing to read.
|
|
36
|
+
* There is no resolver for the shadow colours — they hold `var(--wr-color-info)` /
|
|
37
|
+
* `var(--wr-color-danger)`, and resolving those needs the theme stylesheet. And there
|
|
38
|
+
* is no `getSpeed()`: the input is never written to the DOM, only its two products are,
|
|
39
|
+
* and dividing one back would invent a precision the component never published (at
|
|
40
|
+
* `[speed]="0.3"` the after-duration is literally `0.8999999999999999s`).
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* ```ts
|
|
44
|
+
* const glitch = await loader.getHarness(WrGlitchTextHarness);
|
|
45
|
+
*
|
|
46
|
+
* expect(await glitch.isCloneTextInSync()).toBe(true);
|
|
47
|
+
* expect((await glitch.getDurations()).after).toBeCloseTo(3);
|
|
48
|
+
* ```
|
|
49
|
+
*
|
|
50
|
+
* @see https://ngwr.dev/guides/testing
|
|
51
|
+
*/
|
|
52
|
+
class WrGlitchTextHarness extends ComponentHarness {
|
|
53
|
+
static hostSelector = 'wr-glitch-text';
|
|
54
|
+
/** Build a predicate that narrows the query — pass to `getHarness` / `getAllHarnesses`. */
|
|
55
|
+
static with(options = {}) {
|
|
56
|
+
return new HarnessPredicate(WrGlitchTextHarness, options)
|
|
57
|
+
.addOption('text', options.text, (harness, text) => HarnessPredicate.stringMatches(harness.getText(), text))
|
|
58
|
+
.addOption('hoverOnly', options.hoverOnly, async (harness, hoverOnly) => (await harness.isHoverOnly()) === hoverOnly);
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* The rendered text, trimmed.
|
|
62
|
+
*
|
|
63
|
+
* The half a sighted user reads, and the half every other check would notice
|
|
64
|
+
* breaking. The component interpolates the input straight onto the host, so there is
|
|
65
|
+
* no inner element to query.
|
|
66
|
+
*/
|
|
67
|
+
async getText() {
|
|
68
|
+
return (await this.host()).text();
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* The string the two clone layers draw from — the host's `data-text`.
|
|
72
|
+
*
|
|
73
|
+
* The half nothing else notices. `content: attr(data-text)` is how both pseudo-clones
|
|
74
|
+
* get their glyphs, so this attribute IS the effect. `null` means the mirror is gone
|
|
75
|
+
* and the component is rendering plain text with extra classes.
|
|
76
|
+
*/
|
|
77
|
+
async getCloneText() {
|
|
78
|
+
return (await this.host()).getAttribute('data-text');
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Whether the clone attribute still mirrors the rendered text.
|
|
82
|
+
*
|
|
83
|
+
* The component's defining silent failure, as a single assertion — two separate reads
|
|
84
|
+
* are two things a spec can forget to write together. Compared raw on both sides,
|
|
85
|
+
* with no trimming: the template is the bare interpolation, so the text node and the
|
|
86
|
+
* attribute are byte-identical or the mirror is broken. An empty `text` is legal and
|
|
87
|
+
* counts as in sync — `data-text=""` is a mirror of nothing, not a missing binding,
|
|
88
|
+
* which is why {@link getCloneText} reports the absent case as `null` instead.
|
|
89
|
+
*/
|
|
90
|
+
async isCloneTextInSync() {
|
|
91
|
+
const host = await this.host();
|
|
92
|
+
return (await host.getProperty('textContent')) === (await host.getAttribute('data-text'));
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Whether the effect idles until the pointer arrives (`enableOnHover`).
|
|
96
|
+
*
|
|
97
|
+
* The `wr-glitch-text--hover-only` class is the only DOM trace of that input, the gate
|
|
98
|
+
* itself being a `:hover` rule. It defaults to `true`, so this is also how a spec
|
|
99
|
+
* catches the day someone "fixes" the default to match the component's own JSDoc,
|
|
100
|
+
* which describes the opposite.
|
|
101
|
+
*/
|
|
102
|
+
async isHoverOnly() {
|
|
103
|
+
return (await this.host()).hasClass('wr-glitch-text--hover-only');
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* How long each clone takes to complete a cycle, in seconds.
|
|
107
|
+
*
|
|
108
|
+
* The two must NOT be equal: they run at 2× and 3× the `speed` multiplier so the tears
|
|
109
|
+
* fall out of phase, and clones tearing in lockstep read as one shifted copy rather
|
|
110
|
+
* than as a glitch. That regression changes no class, no attribute and no text, and no
|
|
111
|
+
* visual gate in this repo can see it either.
|
|
112
|
+
*
|
|
113
|
+
* These are raw JavaScript floats — `[speed]="0.3"` writes `0.8999999999999999s` —
|
|
114
|
+
* so compare with `toBeCloseTo`, never against a formatted string.
|
|
115
|
+
*/
|
|
116
|
+
async getDurations() {
|
|
117
|
+
return {
|
|
118
|
+
before: await this.seconds('--wr-glitch-text-before-duration'),
|
|
119
|
+
after: await this.seconds('--wr-glitch-text-after-duration'),
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Whether the red / cyan colour split is switched on (`enableShadows`).
|
|
124
|
+
*
|
|
125
|
+
* The component writes the literal `none` rather than removing the properties when the
|
|
126
|
+
* split is off, so "disabled" and "never set" stay distinguishable — an absent property
|
|
127
|
+
* would mean a broken binding, and this reports that as no split either.
|
|
128
|
+
*/
|
|
129
|
+
async hasColourSplit() {
|
|
130
|
+
const { before, after } = await this.getColourSplit();
|
|
131
|
+
return before !== 'none' && after !== 'none' && before !== '' && after !== '';
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* The two clone `text-shadow`s, exactly as written — `var()` tokens and all.
|
|
135
|
+
*
|
|
136
|
+
* Returned unresolved on purpose: resolving `var(--wr-color-info)` needs the theme
|
|
137
|
+
* stylesheet, and a unit test that answered with a colour would be inventing one.
|
|
138
|
+
* What the raw pair does prove is the thing a copy-paste breaks — the offsets must
|
|
139
|
+
* have OPPOSITE signs and the two tints must be DIFFERENT intents. Give both clones
|
|
140
|
+
* the same colour, or the same sign, and the chromatic aberration is gone while every
|
|
141
|
+
* other reading stays green.
|
|
142
|
+
*/
|
|
143
|
+
async getColourSplit() {
|
|
144
|
+
const host = await this.host();
|
|
145
|
+
return {
|
|
146
|
+
before: (await host.getCssValue('--wr-glitch-text-before-shadow')).trim(),
|
|
147
|
+
after: (await host.getCssValue('--wr-glitch-text-after-shadow')).trim(),
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* The masking colour behind the clone slices, or `null` when none was given.
|
|
152
|
+
*
|
|
153
|
+
* The binding emits `null` for an empty input, so the property is ABSENT rather than
|
|
154
|
+
* set to `transparent`, and the stylesheet's own `var(--wr-glitch-text-bg, transparent)`
|
|
155
|
+
* fallback does the work. A component that started writing `transparent` inline would
|
|
156
|
+
* look identical and break nothing until a consumer relied on that fallback — which is
|
|
157
|
+
* the whole reason this reports the absent case as `null` instead of as a colour.
|
|
158
|
+
*/
|
|
159
|
+
async getSliceBackground() {
|
|
160
|
+
const raw = (await (await this.host()).getCssValue('--wr-glitch-text-bg')).trim();
|
|
161
|
+
return raw === '' ? null : raw;
|
|
162
|
+
}
|
|
163
|
+
/** One of the host's inline duration properties, as a number of seconds. */
|
|
164
|
+
async seconds(property) {
|
|
165
|
+
const raw = (await (await this.host()).getCssValue(property)).trim();
|
|
166
|
+
const value = Number.parseFloat(raw);
|
|
167
|
+
if (Number.isNaN(value)) {
|
|
168
|
+
throw new Error(`WrGlitchTextHarness.getDurations(): \`${property}\` computed to "${raw}", which is not a number of ` +
|
|
169
|
+
'seconds. The component writes it inline on the host as a multiple of `[speed]`; an empty value means ' +
|
|
170
|
+
'that binding is gone, and there is no honest duration to report in its place.');
|
|
171
|
+
}
|
|
172
|
+
return value;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Generated bundle index. Do not edit.
|
|
178
|
+
*/
|
|
179
|
+
|
|
180
|
+
export { WrGlitchTextHarness };
|
|
181
|
+
//# sourceMappingURL=ngwr-glitch-text-testing.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ngwr-glitch-text-testing.mjs","sources":["../../../projects/lib/glitch-text/testing/wr-glitch-text-harness.ts","../../../projects/lib/glitch-text/testing/ngwr-glitch-text-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 { WrGlitchTextColourSplit, WrGlitchTextDurations, WrGlitchTextHarnessFilters } from './interfaces';\n\n/**\n * Test harness for `<wr-glitch-text>`.\n *\n * **The one thing worth testing here is the pairing.** The two torn clones are\n * `::before` / `::after` pseudo-elements whose glyphs come from `content:\n * attr(data-text)`, so the string has to be in the host's text node AND in the host's\n * `data-text` attribute. Drop either binding and the visible text stays flawless while\n * the effect disappears — no error, no missing element, nothing else in the DOM\n * changes. {@link isCloneTextInSync} is that assertion in one call.\n *\n * **Everything else the harness reads is an inline custom property**, because that is\n * everything the component writes: the two clone durations, the two colour-split\n * shadows and the optional slice background. They are worth pinning for the unit\n * transforms — a duration that loses its `s`, or two clones that end up on the same\n * multiple and tear in lockstep, is a regression that changes no class, no attribute\n * and no text.\n *\n * **What the harness deliberately does not offer**, and why. There is no\n * `isGlitching()`: the tear is a `@keyframes` animation on a pseudo-element, applied by\n * a stylesheet a unit test never loads, so the only available answer is a class name\n * repackaged as a state. There is no `hover()` / `isGlitchingOnHover()`: the gate is a\n * `:hover` SELECTOR, and the DOM after a dispatched mouse event is byte-identical to\n * the DOM before it — a spec built on that would pass against a component whose hover\n * rules had been deleted. There is no `getCloneOffset()` / `getClipPath()`: the ±10px\n * offsets and the tear bands live in the stylesheet and are pure layout besides. There\n * is no `respectsReducedMotion()`: the handling is a `@media` block with no TypeScript\n * counterpart, so the DOM is identical in both states and there is nothing to read.\n * There is no resolver for the shadow colours — they hold `var(--wr-color-info)` /\n * `var(--wr-color-danger)`, and resolving those needs the theme stylesheet. And there\n * is no `getSpeed()`: the input is never written to the DOM, only its two products are,\n * and dividing one back would invent a precision the component never published (at\n * `[speed]=\"0.3\"` the after-duration is literally `0.8999999999999999s`).\n *\n * @example\n * ```ts\n * const glitch = await loader.getHarness(WrGlitchTextHarness);\n *\n * expect(await glitch.isCloneTextInSync()).toBe(true);\n * expect((await glitch.getDurations()).after).toBeCloseTo(3);\n * ```\n *\n * @see https://ngwr.dev/guides/testing\n */\nexport class WrGlitchTextHarness extends ComponentHarness {\n static hostSelector = 'wr-glitch-text';\n\n /** Build a predicate that narrows the query — pass to `getHarness` / `getAllHarnesses`. */\n static with(options: WrGlitchTextHarnessFilters = {}): HarnessPredicate<WrGlitchTextHarness> {\n return new HarnessPredicate(WrGlitchTextHarness, options)\n .addOption('text', options.text, (harness, text) => HarnessPredicate.stringMatches(harness.getText(), text))\n .addOption(\n 'hoverOnly',\n options.hoverOnly,\n async (harness, hoverOnly) => (await harness.isHoverOnly()) === hoverOnly\n );\n }\n\n /**\n * The rendered text, trimmed.\n *\n * The half a sighted user reads, and the half every other check would notice\n * breaking. The component interpolates the input straight onto the host, so there is\n * no inner element to query.\n */\n async getText(): Promise<string> {\n return (await this.host()).text();\n }\n\n /**\n * The string the two clone layers draw from — the host's `data-text`.\n *\n * The half nothing else notices. `content: attr(data-text)` is how both pseudo-clones\n * get their glyphs, so this attribute IS the effect. `null` means the mirror is gone\n * and the component is rendering plain text with extra classes.\n */\n async getCloneText(): Promise<string | null> {\n return (await this.host()).getAttribute('data-text');\n }\n\n /**\n * Whether the clone attribute still mirrors the rendered text.\n *\n * The component's defining silent failure, as a single assertion — two separate reads\n * are two things a spec can forget to write together. Compared raw on both sides,\n * with no trimming: the template is the bare interpolation, so the text node and the\n * attribute are byte-identical or the mirror is broken. An empty `text` is legal and\n * counts as in sync — `data-text=\"\"` is a mirror of nothing, not a missing binding,\n * which is why {@link getCloneText} reports the absent case as `null` instead.\n */\n async isCloneTextInSync(): Promise<boolean> {\n const host = await this.host();\n return (await host.getProperty<string>('textContent')) === (await host.getAttribute('data-text'));\n }\n\n /**\n * Whether the effect idles until the pointer arrives (`enableOnHover`).\n *\n * The `wr-glitch-text--hover-only` class is the only DOM trace of that input, the gate\n * itself being a `:hover` rule. It defaults to `true`, so this is also how a spec\n * catches the day someone \"fixes\" the default to match the component's own JSDoc,\n * which describes the opposite.\n */\n async isHoverOnly(): Promise<boolean> {\n return (await this.host()).hasClass('wr-glitch-text--hover-only');\n }\n\n /**\n * How long each clone takes to complete a cycle, in seconds.\n *\n * The two must NOT be equal: they run at 2× and 3× the `speed` multiplier so the tears\n * fall out of phase, and clones tearing in lockstep read as one shifted copy rather\n * than as a glitch. That regression changes no class, no attribute and no text, and no\n * visual gate in this repo can see it either.\n *\n * These are raw JavaScript floats — `[speed]=\"0.3\"` writes `0.8999999999999999s` —\n * so compare with `toBeCloseTo`, never against a formatted string.\n */\n async getDurations(): Promise<WrGlitchTextDurations> {\n return {\n before: await this.seconds('--wr-glitch-text-before-duration'),\n after: await this.seconds('--wr-glitch-text-after-duration'),\n };\n }\n\n /**\n * Whether the red / cyan colour split is switched on (`enableShadows`).\n *\n * The component writes the literal `none` rather than removing the properties when the\n * split is off, so \"disabled\" and \"never set\" stay distinguishable — an absent property\n * would mean a broken binding, and this reports that as no split either.\n */\n async hasColourSplit(): Promise<boolean> {\n const { before, after } = await this.getColourSplit();\n return before !== 'none' && after !== 'none' && before !== '' && after !== '';\n }\n\n /**\n * The two clone `text-shadow`s, exactly as written — `var()` tokens and all.\n *\n * Returned unresolved on purpose: resolving `var(--wr-color-info)` needs the theme\n * stylesheet, and a unit test that answered with a colour would be inventing one.\n * What the raw pair does prove is the thing a copy-paste breaks — the offsets must\n * have OPPOSITE signs and the two tints must be DIFFERENT intents. Give both clones\n * the same colour, or the same sign, and the chromatic aberration is gone while every\n * other reading stays green.\n */\n async getColourSplit(): Promise<WrGlitchTextColourSplit> {\n const host = await this.host();\n return {\n before: (await host.getCssValue('--wr-glitch-text-before-shadow')).trim(),\n after: (await host.getCssValue('--wr-glitch-text-after-shadow')).trim(),\n };\n }\n\n /**\n * The masking colour behind the clone slices, or `null` when none was given.\n *\n * The binding emits `null` for an empty input, so the property is ABSENT rather than\n * set to `transparent`, and the stylesheet's own `var(--wr-glitch-text-bg, transparent)`\n * fallback does the work. A component that started writing `transparent` inline would\n * look identical and break nothing until a consumer relied on that fallback — which is\n * the whole reason this reports the absent case as `null` instead of as a colour.\n */\n async getSliceBackground(): Promise<string | null> {\n const raw = (await (await this.host()).getCssValue('--wr-glitch-text-bg')).trim();\n return raw === '' ? null : raw;\n }\n\n /** One of the host's inline duration properties, as a number of seconds. */\n private async seconds(property: string): Promise<number> {\n const raw = (await (await this.host()).getCssValue(property)).trim();\n const value = Number.parseFloat(raw);\n\n if (Number.isNaN(value)) {\n throw new Error(\n `WrGlitchTextHarness.getDurations(): \\`${property}\\` computed to \"${raw}\", which is not a number of ` +\n 'seconds. The component writes it inline on the host as a multiple of `[speed]`; an empty value means ' +\n 'that binding is gone, and there is no honest duration to report in its place.'\n );\n }\n return value;\n }\n}\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public-api';\n"],"names":[],"mappings":";;AAAA;;;;;AAKG;AAMH;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA0CG;AACG,MAAO,mBAAoB,SAAQ,gBAAgB,CAAA;AACvD,IAAA,OAAO,YAAY,GAAG,gBAAgB;;AAGtC,IAAA,OAAO,IAAI,CAAC,OAAA,GAAsC,EAAE,EAAA;AAClD,QAAA,OAAO,IAAI,gBAAgB,CAAC,mBAAmB,EAAE,OAAO;aACrD,SAAS,CAAC,MAAM,EAAE,OAAO,CAAC,IAAI,EAAE,CAAC,OAAO,EAAE,IAAI,KAAK,gBAAgB,CAAC,aAAa,CAAC,OAAO,CAAC,OAAO,EAAE,EAAE,IAAI,CAAC;aAC1G,SAAS,CACR,WAAW,EACX,OAAO,CAAC,SAAS,EACjB,OAAO,OAAO,EAAE,SAAS,KAAK,CAAC,MAAM,OAAO,CAAC,WAAW,EAAE,MAAM,SAAS,CAC1E;IACL;AAEA;;;;;;AAMG;AACH,IAAA,MAAM,OAAO,GAAA;QACX,OAAO,CAAC,MAAM,IAAI,CAAC,IAAI,EAAE,EAAE,IAAI,EAAE;IACnC;AAEA;;;;;;AAMG;AACH,IAAA,MAAM,YAAY,GAAA;AAChB,QAAA,OAAO,CAAC,MAAM,IAAI,CAAC,IAAI,EAAE,EAAE,YAAY,CAAC,WAAW,CAAC;IACtD;AAEA;;;;;;;;;AASG;AACH,IAAA,MAAM,iBAAiB,GAAA;AACrB,QAAA,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,IAAI,EAAE;AAC9B,QAAA,OAAO,CAAC,MAAM,IAAI,CAAC,WAAW,CAAS,aAAa,CAAC,OAAO,MAAM,IAAI,CAAC,YAAY,CAAC,WAAW,CAAC,CAAC;IACnG;AAEA;;;;;;;AAOG;AACH,IAAA,MAAM,WAAW,GAAA;AACf,QAAA,OAAO,CAAC,MAAM,IAAI,CAAC,IAAI,EAAE,EAAE,QAAQ,CAAC,4BAA4B,CAAC;IACnE;AAEA;;;;;;;;;;AAUG;AACH,IAAA,MAAM,YAAY,GAAA;QAChB,OAAO;AACL,YAAA,MAAM,EAAE,MAAM,IAAI,CAAC,OAAO,CAAC,kCAAkC,CAAC;AAC9D,YAAA,KAAK,EAAE,MAAM,IAAI,CAAC,OAAO,CAAC,iCAAiC,CAAC;SAC7D;IACH;AAEA;;;;;;AAMG;AACH,IAAA,MAAM,cAAc,GAAA;QAClB,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,GAAG,MAAM,IAAI,CAAC,cAAc,EAAE;AACrD,QAAA,OAAO,MAAM,KAAK,MAAM,IAAI,KAAK,KAAK,MAAM,IAAI,MAAM,KAAK,EAAE,IAAI,KAAK,KAAK,EAAE;IAC/E;AAEA;;;;;;;;;AASG;AACH,IAAA,MAAM,cAAc,GAAA;AAClB,QAAA,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,IAAI,EAAE;QAC9B,OAAO;AACL,YAAA,MAAM,EAAE,CAAC,MAAM,IAAI,CAAC,WAAW,CAAC,gCAAgC,CAAC,EAAE,IAAI,EAAE;AACzE,YAAA,KAAK,EAAE,CAAC,MAAM,IAAI,CAAC,WAAW,CAAC,+BAA+B,CAAC,EAAE,IAAI,EAAE;SACxE;IACH;AAEA;;;;;;;;AAQG;AACH,IAAA,MAAM,kBAAkB,GAAA;QACtB,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC,MAAM,IAAI,CAAC,IAAI,EAAE,EAAE,WAAW,CAAC,qBAAqB,CAAC,EAAE,IAAI,EAAE;QACjF,OAAO,GAAG,KAAK,EAAE,GAAG,IAAI,GAAG,GAAG;IAChC;;IAGQ,MAAM,OAAO,CAAC,QAAgB,EAAA;QACpC,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC,MAAM,IAAI,CAAC,IAAI,EAAE,EAAE,WAAW,CAAC,QAAQ,CAAC,EAAE,IAAI,EAAE;QACpE,MAAM,KAAK,GAAG,MAAM,CAAC,UAAU,CAAC,GAAG,CAAC;AAEpC,QAAA,IAAI,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,EAAE;AACvB,YAAA,MAAM,IAAI,KAAK,CACb,yCAAyC,QAAQ,CAAA,gBAAA,EAAmB,GAAG,CAAA,4BAAA,CAA8B;gBACnG,uGAAuG;AACvG,gBAAA,+EAA+E,CAClF;QACH;AACA,QAAA,OAAO,KAAK;IACd;;;AChMF;;AAEG;;;;"}
|