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.
Files changed (205) hide show
  1. package/circular-text/styles/_index.scss +11 -0
  2. package/command-palette/styles/_index.scss +12 -0
  3. package/fesm2022/ngwr-action-sheet-testing.mjs +313 -0
  4. package/fesm2022/ngwr-action-sheet-testing.mjs.map +1 -0
  5. package/fesm2022/ngwr-bar-chart-testing.mjs +89 -0
  6. package/fesm2022/ngwr-bar-chart-testing.mjs.map +1 -0
  7. package/fesm2022/ngwr-blur-text-testing.mjs +203 -0
  8. package/fesm2022/ngwr-blur-text-testing.mjs.map +1 -0
  9. package/fesm2022/ngwr-blur-text.mjs +15 -1
  10. package/fesm2022/ngwr-blur-text.mjs.map +1 -1
  11. package/fesm2022/ngwr-border-glow-testing.mjs +325 -0
  12. package/fesm2022/ngwr-border-glow-testing.mjs.map +1 -0
  13. package/fesm2022/ngwr-calendar-heatmap-testing.mjs +95 -0
  14. package/fesm2022/ngwr-calendar-heatmap-testing.mjs.map +1 -0
  15. package/fesm2022/ngwr-calendar-testing.mjs +368 -0
  16. package/fesm2022/ngwr-calendar-testing.mjs.map +1 -0
  17. package/fesm2022/ngwr-calendar.mjs +43 -3
  18. package/fesm2022/ngwr-calendar.mjs.map +1 -1
  19. package/fesm2022/ngwr-cascader.mjs +12 -0
  20. package/fesm2022/ngwr-cascader.mjs.map +1 -1
  21. package/fesm2022/ngwr-checkbox.mjs +8 -0
  22. package/fesm2022/ngwr-checkbox.mjs.map +1 -1
  23. package/fesm2022/ngwr-circular-text-testing.mjs +230 -0
  24. package/fesm2022/ngwr-circular-text-testing.mjs.map +1 -0
  25. package/fesm2022/ngwr-circular-text.mjs +2 -2
  26. package/fesm2022/ngwr-circular-text.mjs.map +1 -1
  27. package/fesm2022/ngwr-color-picker-testing.mjs +432 -0
  28. package/fesm2022/ngwr-color-picker-testing.mjs.map +1 -0
  29. package/fesm2022/ngwr-color-picker.mjs +29 -3
  30. package/fesm2022/ngwr-color-picker.mjs.map +1 -1
  31. package/fesm2022/ngwr-context-menu.mjs +25 -5
  32. package/fesm2022/ngwr-context-menu.mjs.map +1 -1
  33. package/fesm2022/ngwr-date-picker-testing.mjs +10 -79
  34. package/fesm2022/ngwr-date-picker-testing.mjs.map +1 -1
  35. package/fesm2022/ngwr-decrypt-text-testing.mjs +261 -0
  36. package/fesm2022/ngwr-decrypt-text-testing.mjs.map +1 -0
  37. package/fesm2022/ngwr-decrypt-text.mjs +6 -1
  38. package/fesm2022/ngwr-decrypt-text.mjs.map +1 -1
  39. package/fesm2022/ngwr-donut-chart-testing.mjs +93 -0
  40. package/fesm2022/ngwr-donut-chart-testing.mjs.map +1 -0
  41. package/fesm2022/ngwr-drawer.mjs +36 -7
  42. package/fesm2022/ngwr-drawer.mjs.map +1 -1
  43. package/fesm2022/ngwr-dropdown.mjs +32 -2
  44. package/fesm2022/ngwr-dropdown.mjs.map +1 -1
  45. package/fesm2022/ngwr-event-calendar-testing.mjs +446 -0
  46. package/fesm2022/ngwr-event-calendar-testing.mjs.map +1 -0
  47. package/fesm2022/ngwr-event-calendar.mjs +43 -18
  48. package/fesm2022/ngwr-event-calendar.mjs.map +1 -1
  49. package/fesm2022/ngwr-falling-text-testing.mjs +218 -0
  50. package/fesm2022/ngwr-falling-text-testing.mjs.map +1 -0
  51. package/fesm2022/ngwr-fuzzy-text-testing.mjs +134 -0
  52. package/fesm2022/ngwr-fuzzy-text-testing.mjs.map +1 -0
  53. package/fesm2022/ngwr-gauge-testing.mjs +86 -0
  54. package/fesm2022/ngwr-gauge-testing.mjs.map +1 -0
  55. package/fesm2022/ngwr-glitch-text-testing.mjs +181 -0
  56. package/fesm2022/ngwr-glitch-text-testing.mjs.map +1 -0
  57. package/fesm2022/ngwr-gradient-text-testing.mjs +223 -0
  58. package/fesm2022/ngwr-gradient-text-testing.mjs.map +1 -0
  59. package/fesm2022/ngwr-i18n-en.mjs +14 -0
  60. package/fesm2022/ngwr-i18n-en.mjs.map +1 -1
  61. package/fesm2022/ngwr-i18n-ru.mjs +14 -0
  62. package/fesm2022/ngwr-i18n-ru.mjs.map +1 -1
  63. package/fesm2022/ngwr-image-cropper-testing.mjs +156 -0
  64. package/fesm2022/ngwr-image-cropper-testing.mjs.map +1 -0
  65. package/fesm2022/ngwr-image-cropper.mjs +10 -3
  66. package/fesm2022/ngwr-image-cropper.mjs.map +1 -1
  67. package/fesm2022/ngwr-input-number.mjs +73 -6
  68. package/fesm2022/ngwr-input-number.mjs.map +1 -1
  69. package/fesm2022/ngwr-knob-testing.mjs +220 -0
  70. package/fesm2022/ngwr-knob-testing.mjs.map +1 -0
  71. package/fesm2022/ngwr-lightbox-testing.mjs +223 -0
  72. package/fesm2022/ngwr-lightbox-testing.mjs.map +1 -0
  73. package/fesm2022/ngwr-lightbox.mjs +9 -2
  74. package/fesm2022/ngwr-lightbox.mjs.map +1 -1
  75. package/fesm2022/ngwr-line-chart-testing.mjs +119 -0
  76. package/fesm2022/ngwr-line-chart-testing.mjs.map +1 -0
  77. package/fesm2022/ngwr-marquee-testing.mjs +336 -0
  78. package/fesm2022/ngwr-marquee-testing.mjs.map +1 -0
  79. package/fesm2022/ngwr-meter-group-testing.mjs +80 -0
  80. package/fesm2022/ngwr-meter-group-testing.mjs.map +1 -0
  81. package/fesm2022/ngwr-overlay.mjs +28 -1
  82. package/fesm2022/ngwr-overlay.mjs.map +1 -1
  83. package/fesm2022/ngwr-popover.mjs +14 -0
  84. package/fesm2022/ngwr-popover.mjs.map +1 -1
  85. package/fesm2022/ngwr-rotating-text-testing.mjs +220 -0
  86. package/fesm2022/ngwr-rotating-text-testing.mjs.map +1 -0
  87. package/fesm2022/ngwr-rotating-text.mjs +2 -2
  88. package/fesm2022/ngwr-rotating-text.mjs.map +1 -1
  89. package/fesm2022/ngwr-select.mjs +70 -11
  90. package/fesm2022/ngwr-select.mjs.map +1 -1
  91. package/fesm2022/ngwr-shiny-text-testing.mjs +237 -0
  92. package/fesm2022/ngwr-shiny-text-testing.mjs.map +1 -0
  93. package/fesm2022/ngwr-slider.mjs +44 -6
  94. package/fesm2022/ngwr-slider.mjs.map +1 -1
  95. package/fesm2022/ngwr-sparkline-testing.mjs +76 -0
  96. package/fesm2022/ngwr-sparkline-testing.mjs.map +1 -0
  97. package/fesm2022/ngwr-speed-dial-testing.mjs +279 -0
  98. package/fesm2022/ngwr-speed-dial-testing.mjs.map +1 -0
  99. package/fesm2022/ngwr-splash-cursor-testing.mjs +89 -0
  100. package/fesm2022/ngwr-splash-cursor-testing.mjs.map +1 -0
  101. package/fesm2022/ngwr-split-text-testing.mjs +224 -0
  102. package/fesm2022/ngwr-split-text-testing.mjs.map +1 -0
  103. package/fesm2022/ngwr-split-text.mjs +15 -1
  104. package/fesm2022/ngwr-split-text.mjs.map +1 -1
  105. package/fesm2022/ngwr-splitter-testing.mjs +245 -0
  106. package/fesm2022/ngwr-splitter-testing.mjs.map +1 -0
  107. package/fesm2022/ngwr-spotlight-card-testing.mjs +368 -0
  108. package/fesm2022/ngwr-spotlight-card-testing.mjs.map +1 -0
  109. package/fesm2022/ngwr-star-border-testing.mjs +172 -0
  110. package/fesm2022/ngwr-star-border-testing.mjs.map +1 -0
  111. package/fesm2022/ngwr-table.mjs +13 -3
  112. package/fesm2022/ngwr-table.mjs.map +1 -1
  113. package/fesm2022/ngwr-tabs.mjs +15 -1
  114. package/fesm2022/ngwr-tabs.mjs.map +1 -1
  115. package/fesm2022/ngwr-tilt-card-testing.mjs +190 -0
  116. package/fesm2022/ngwr-tilt-card-testing.mjs.map +1 -0
  117. package/fesm2022/ngwr-tilt-card.mjs +19 -1
  118. package/fesm2022/ngwr-tilt-card.mjs.map +1 -1
  119. package/fesm2022/ngwr-toast.mjs +36 -7
  120. package/fesm2022/ngwr-toast.mjs.map +1 -1
  121. package/fesm2022/ngwr-tour-testing.mjs +173 -0
  122. package/fesm2022/ngwr-tour-testing.mjs.map +1 -0
  123. package/fesm2022/ngwr-tour.mjs +2 -2
  124. package/fesm2022/ngwr-tour.mjs.map +1 -1
  125. package/fesm2022/ngwr-tree.mjs +10 -3
  126. package/fesm2022/ngwr-tree.mjs.map +1 -1
  127. package/fesm2022/ngwr-typewriter-testing.mjs +228 -0
  128. package/fesm2022/ngwr-typewriter-testing.mjs.map +1 -0
  129. package/fesm2022/ngwr-version.mjs +1 -1
  130. package/fesm2022/ngwr-version.mjs.map +1 -1
  131. package/fesm2022/ngwr-waves-testing.mjs +128 -0
  132. package/fesm2022/ngwr-waves-testing.mjs.map +1 -0
  133. package/fesm2022/ngwr-window-testing.mjs +361 -0
  134. package/fesm2022/ngwr-window-testing.mjs.map +1 -0
  135. package/fesm2022/ngwr-window.mjs +84 -10
  136. package/fesm2022/ngwr-window.mjs.map +1 -1
  137. package/i18n/en.json +16 -2
  138. package/i18n/ru.json +16 -2
  139. package/llms-full.txt +147 -0
  140. package/llms.txt +27 -8
  141. package/mcp/providers.js +29 -0
  142. package/mcp/tools.js +1 -29
  143. package/package.json +145 -1
  144. package/skills/ngwr/SKILL.md +115 -0
  145. package/skills/ngwr/references/catalog.md +212 -0
  146. package/skills/ngwr/references/setup.md +73 -0
  147. package/types/ngwr-action-sheet-testing.d.ts +260 -0
  148. package/types/ngwr-bar-chart-testing.d.ts +77 -0
  149. package/types/ngwr-blur-text-testing.d.ts +180 -0
  150. package/types/ngwr-blur-text.d.ts +12 -0
  151. package/types/ngwr-border-glow-testing.d.ts +219 -0
  152. package/types/ngwr-calendar-heatmap-testing.d.ts +79 -0
  153. package/types/ngwr-calendar-testing.d.ts +277 -0
  154. package/types/ngwr-calendar.d.ts +10 -0
  155. package/types/ngwr-checkbox.d.ts +13 -0
  156. package/types/ngwr-circular-text-testing.d.ts +196 -0
  157. package/types/ngwr-color-picker-testing.d.ts +291 -0
  158. package/types/ngwr-color-picker.d.ts +13 -1
  159. package/types/ngwr-context-menu.d.ts +15 -1
  160. package/types/ngwr-date-picker-testing.d.ts +6 -66
  161. package/types/ngwr-decrypt-text-testing.d.ts +222 -0
  162. package/types/ngwr-donut-chart-testing.d.ts +79 -0
  163. package/types/ngwr-drawer.d.ts +12 -0
  164. package/types/ngwr-dropdown.d.ts +9 -0
  165. package/types/ngwr-event-calendar-testing.d.ts +301 -0
  166. package/types/ngwr-event-calendar.d.ts +9 -6
  167. package/types/ngwr-falling-text-testing.d.ts +186 -0
  168. package/types/ngwr-fuzzy-text-testing.d.ts +130 -0
  169. package/types/ngwr-gauge-testing.d.ts +71 -0
  170. package/types/ngwr-glitch-text-testing.d.ts +172 -0
  171. package/types/ngwr-gradient-text-testing.d.ts +173 -0
  172. package/types/ngwr-image-cropper-testing.d.ts +134 -0
  173. package/types/ngwr-image-cropper.d.ts +1 -0
  174. package/types/ngwr-input-number.d.ts +33 -1
  175. package/types/ngwr-knob-testing.d.ts +150 -0
  176. package/types/ngwr-lightbox-testing.d.ts +154 -0
  177. package/types/ngwr-lightbox.d.ts +6 -0
  178. package/types/ngwr-line-chart-testing.d.ts +93 -0
  179. package/types/ngwr-marquee-testing.d.ts +280 -0
  180. package/types/ngwr-meter-group-testing.d.ts +67 -0
  181. package/types/ngwr-overlay.d.ts +19 -1
  182. package/types/ngwr-rotating-text-testing.d.ts +191 -0
  183. package/types/ngwr-select.d.ts +32 -0
  184. package/types/ngwr-shiny-text-testing.d.ts +165 -0
  185. package/types/ngwr-slider.d.ts +15 -0
  186. package/types/ngwr-sparkline-testing.d.ts +69 -0
  187. package/types/ngwr-speed-dial-testing.d.ts +195 -0
  188. package/types/ngwr-splash-cursor-testing.d.ts +95 -0
  189. package/types/ngwr-split-text-testing.d.ts +209 -0
  190. package/types/ngwr-split-text.d.ts +12 -0
  191. package/types/ngwr-splitter-testing.d.ts +174 -0
  192. package/types/ngwr-spotlight-card-testing.d.ts +303 -0
  193. package/types/ngwr-star-border-testing.d.ts +135 -0
  194. package/types/ngwr-table.d.ts +10 -1
  195. package/types/ngwr-tilt-card-testing.d.ts +158 -0
  196. package/types/ngwr-tilt-card.d.ts +2 -0
  197. package/types/ngwr-toast.d.ts +14 -5
  198. package/types/ngwr-tour-testing.d.ts +143 -0
  199. package/types/ngwr-tree.d.ts +7 -0
  200. package/types/ngwr-typewriter-testing.d.ts +201 -0
  201. package/types/ngwr-version.d.ts +1 -1
  202. package/types/ngwr-waves-testing.d.ts +121 -0
  203. package/types/ngwr-window-testing.d.ts +263 -0
  204. package/types/ngwr-window.d.ts +27 -10
  205. package/window/styles/_index.scss +44 -16
@@ -0,0 +1,368 @@
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
+ * Split an inline `style` attribute into its declarations.
11
+ *
12
+ * Read from the attribute rather than through `getCssValue()`, and the difference is not
13
+ * cosmetic. `getCssValue()` is `getComputedStyle()`, which in a browser resolves the
14
+ * stylesheet's own `--wr-spotlight-x: 50%` / `--wr-spotlight-color: rgba(…)` defaults —
15
+ * so "the component has written nothing here yet, and the theme decides" would come back
16
+ * as a concrete value and the assertion would invert. jsdom loads no stylesheet, so the
17
+ * computed read happens to agree today and would start lying the moment these harnesses
18
+ * ran anywhere else. The attribute holds what the component or the directive itself
19
+ * wrote, and nothing else.
20
+ *
21
+ * Shared by both harnesses in this entry point because both read the same three
22
+ * variables off their host — in different units. See each class for which.
23
+ */
24
+ function wrSpotlightInlineVars(style) {
25
+ const declarations = new Map();
26
+ for (const declaration of style.split(';')) {
27
+ const colon = declaration.indexOf(':');
28
+ if (colon === -1)
29
+ continue;
30
+ const name = declaration.slice(0, colon).trim();
31
+ if (name !== '')
32
+ declarations.set(name, declaration.slice(colon + 1).trim());
33
+ }
34
+ return declarations;
35
+ }
36
+
37
+ /**
38
+ * @license
39
+ *
40
+ * Use of this source code is governed by an MIT-style license that can be
41
+ * found in the LICENSE file at https://github.com/thekhegay/ngwr/blob/main/LICENSE
42
+ */
43
+ /** `120px` — the card writes its pointer coordinates as a pixel length, always. */
44
+ const PIXEL_LENGTH = /^(-?[\d.]+)px$/;
45
+ /** `80%` — the radius, whose `%` suffix is the half that makes the gradient legal. */
46
+ const PERCENTAGE = /^(-?[\d.]+)%$/;
47
+ /**
48
+ * Test harness for `<wr-spotlight-card>` — a card with a soft radial highlight that
49
+ * follows the cursor.
50
+ *
51
+ * **The component's entire output is three custom properties on its own host, and this
52
+ * harness reads them off the inline declaration.** There is no DOM for the highlight at
53
+ * all: it is a `::before` pseudo-element, painted by the stylesheet from those three
54
+ * values. So the paint is out of reach here and always will be — what is in reach is the
55
+ * arithmetic and the exact spelling that feeds it, which is where this component breaks.
56
+ *
57
+ * **This harness reads PIXELS. {@link WrSpotlightHarness}, the `[wrSpotlight]` directive
58
+ * shipped from the same entry point, reads PERCENTAGES.** Both write
59
+ * `--wr-spotlight-x` / `--wr-spotlight-y`, in different units, off different events —
60
+ * the card listens for `mousemove`, the directive for `pointermove`. That is deliberate
61
+ * and documented, and it is also the mistake waiting to happen: putting `wrSpotlight` on
62
+ * a `<wr-spotlight-card>` gives you two handlers overwriting one variable in units that
63
+ * do not agree. Reach for the harness that matches the thing under test.
64
+ *
65
+ * **{@link movePointerTo} needs the card to have a box, and in a unit test it has none.**
66
+ * The component subtracts its own `getBoundingClientRect()` from the client coordinate,
67
+ * and jsdom answers that with a 0×0 rect at the origin — so the subtraction is a no-op
68
+ * and the coordinate comes back unchanged. No throw here, unlike the directive's version:
69
+ * the answer is degenerate rather than wrong, and it is legitimately what a real card
70
+ * with no box would report. But a spec that asserts `x === clientX` against an unstubbed
71
+ * rect has proved nothing about the box-relative conversion, which is the only thing
72
+ * {@link getSpotlightPosition} is for. Give the host a box first:
73
+ *
74
+ * ```ts
75
+ * const card = fixture.nativeElement.querySelector('wr-spotlight-card');
76
+ * card.getBoundingClientRect = () => ({ left: 40, top: 20, width: 200, height: 100 }) as DOMRect;
77
+ * ```
78
+ *
79
+ * Several plausible methods are deliberately absent. There is no `isSpotlightVisible()`
80
+ * or `getSpotlightOpacity()`: the highlight sits at `opacity: 0` and is lifted to `0.6`
81
+ * only under `:hover` / `:focus-within`, both of which are stylesheet selectors on a
82
+ * pseudo-element — jsdom loads no stylesheet, matches no `:hover`, and cannot read a
83
+ * pseudo-element's style at any time, so any answer would be invented. No `getGradient()`
84
+ * for the same reason. No `hover()` / `mouseAway()` wrappers, which would be worse than
85
+ * missing: the CDK's versions dispatch `mouseenter` / `mouseover` / `mouseleave`, none of
86
+ * which this component listens to and none of which changes a `:hover` match in a test —
87
+ * a spec calling them would look like it exercised the reveal and would have exercised
88
+ * nothing. No `getSpotlightRect()` or `getCardSize()`, which are layout. And no
89
+ * `respectsReducedMotion()`: this is the one animation component in the library that
90
+ * deliberately does not implement the preference — a gradient tracks the cursor and no
91
+ * content moves — so the method would answer identically for a component that honoured it
92
+ * and one that ignored it.
93
+ *
94
+ * @example
95
+ * ```ts
96
+ * const card = await loader.getHarness(WrSpotlightCardHarness.with({ text: 'Hover me' }));
97
+ *
98
+ * expect(await card.getRadiusPercent()).toBe(80);
99
+ * expect(await card.getSpotlightColor()).toBeNull(); // the theme decides
100
+ * expect(await card.getSpotlightPosition()).toBeNull(); // nothing has pointed at it
101
+ * ```
102
+ *
103
+ * @see https://ngwr.dev/guides/testing
104
+ */
105
+ class WrSpotlightCardHarness extends ComponentHarness {
106
+ static hostSelector = 'wr-spotlight-card';
107
+ /** Build a predicate that narrows the query — pass to `getHarness` / `getAllHarnesses`. */
108
+ static with(options = {}) {
109
+ return new HarnessPredicate(WrSpotlightCardHarness, options).addOption('text', options.text, (harness, text) => HarnessPredicate.stringMatches(harness.getContentText(), text));
110
+ }
111
+ /**
112
+ * Where the gradient fades out, as a percentage of its own radius.
113
+ *
114
+ * Returns the number, but the assertion it really makes is about the `%` that was
115
+ * stripped to get it — this method refuses a bare `80`. The stylesheet interpolates the
116
+ * variable straight into `transparent var(--wr-spotlight-radius, 80%)`, where a unitless
117
+ * value is not a length, so the whole `radial-gradient()` declaration is dropped and the
118
+ * highlight disappears with nothing in the console to say why. Reading this as a plain
119
+ * number would sail past exactly that.
120
+ *
121
+ * Always present, whatever the input says: a `[radius]` that is not a number coerces
122
+ * back to the default 80 rather than writing nonsense into the sheet.
123
+ */
124
+ async getRadiusPercent() {
125
+ const raw = (await this.vars()).get('--wr-spotlight-radius') ?? null;
126
+ const match = raw === null ? null : PERCENTAGE.exec(raw);
127
+ if (!match) {
128
+ throw new Error(`WrSpotlightCardHarness.getRadiusPercent(): \`--wr-spotlight-radius\` reads "${raw ?? '(absent)'}" on the ` +
129
+ 'host, and the stylesheet needs a percentage there — it drops the whole gradient for a bare number. The ' +
130
+ 'card writes this one on every render whatever the input says, so a missing or unsuffixed value means ' +
131
+ 'the binding itself stopped reaching the element.');
132
+ }
133
+ return Number.parseFloat(match[1]);
134
+ }
135
+ /**
136
+ * The highlight colour the author asked for, or `null` when they left it to the theme.
137
+ *
138
+ * `null` IS the contract for an unset `[spotlightColor]`, not a gap in it. The
139
+ * stylesheet's own default is `rgba(var(--wr-color-on-surface-rgb), .15)`, and that
140
+ * token flips per theme — which is what makes one component read as a dark glow on a
141
+ * light surface and a light beam on a dark one. A card that started writing a concrete
142
+ * colour here would look right in whichever theme it was authored against and wrong in
143
+ * the other, and this is the only method that can tell.
144
+ */
145
+ async getSpotlightColor() {
146
+ return (await this.vars()).get('--wr-spotlight-color') ?? null;
147
+ }
148
+ /**
149
+ * Where the highlight last sat, in pixels from the card's own top-left corner. `null`
150
+ * until something has pointed at the card.
151
+ *
152
+ * Pixels, not percentages — see this class's docs for why the distinction matters
153
+ * inside this one entry point.
154
+ *
155
+ * `null` is a state of its own rather than a stand-in for `{ x: 0, y: 0 }`: before the
156
+ * first move the stylesheet's `50% / 50%` default applies, and a pointer at the card's
157
+ * own origin legitimately reports zero, so folding the two together would make an
158
+ * untouched card indistinguishable from one being pointed at in its corner.
159
+ *
160
+ * Note there is no reset when the cursor leaves, and that is not an oversight the
161
+ * harness papers over: the stylesheet fades the whole overlay to `opacity: 0` on leave,
162
+ * so nothing is parked visibly — but the coordinate stays written for the life of the
163
+ * element. `null` therefore means "no pointer has EVER arrived", not "none is here now".
164
+ */
165
+ async getSpotlightPosition() {
166
+ const vars = await this.vars();
167
+ const x = vars.get('--wr-spotlight-x');
168
+ const y = vars.get('--wr-spotlight-y');
169
+ if (x === undefined || y === undefined)
170
+ return null;
171
+ return { x: this.parsePixels(x, '--wr-spotlight-x'), y: this.parsePixels(y, '--wr-spotlight-y') };
172
+ }
173
+ /**
174
+ * Point at the card, in client coordinates.
175
+ *
176
+ * A `mousemove`, not a `pointermove` — the component listens for the former, and the
177
+ * directive in the same entry point for the latter, so a pointer event dispatched here
178
+ * is silently ignored. It goes to the host itself, so it needs no hit test and cannot
179
+ * miss in a test that has no layout; the coordinates only ever reach the component's
180
+ * own subtraction.
181
+ *
182
+ * Stub the host's `getBoundingClientRect` before calling this, or the subtraction is
183
+ * invisible — see this class's docs.
184
+ */
185
+ async movePointerTo(clientX, clientY) {
186
+ await (await this.host()).dispatchEvent('mousemove', { clientX, clientY });
187
+ }
188
+ /**
189
+ * The projected content's text, trimmed.
190
+ *
191
+ * Read from the host, which is the whole assertion: the template is a bare
192
+ * `<ng-content />` with no wrapper element, so content lands as a direct child and an
193
+ * interactive one keeps its own name and role. The component adds no ARIA and no node
194
+ * of its own — there is nothing else here for a harness to ask about.
195
+ */
196
+ async getContentText() {
197
+ return (await this.host()).text();
198
+ }
199
+ /** The host's inline declarations — the component's whole written output. */
200
+ async vars() {
201
+ return wrSpotlightInlineVars((await (await this.host()).getAttribute('style')) ?? '');
202
+ }
203
+ /** One written coordinate, as a number, insisting on the `px` the card promises. */
204
+ parsePixels(raw, name) {
205
+ const match = PIXEL_LENGTH.exec(raw);
206
+ if (!match) {
207
+ throw new Error(`WrSpotlightCardHarness.getSpotlightPosition(): \`${name}\` reads "${raw}", which is not the pixel length ` +
208
+ 'the card writes. A percentage here is the tell that `[wrSpotlight]` is on the same element — the two ' +
209
+ 'exports of this entry point share these variables and disagree about the unit.');
210
+ }
211
+ return Number.parseFloat(match[1]);
212
+ }
213
+ }
214
+
215
+ /**
216
+ * @license
217
+ *
218
+ * Use of this source code is governed by an MIT-style license that can be
219
+ * found in the LICENSE file at https://github.com/thekhegay/ngwr/blob/main/LICENSE
220
+ */
221
+ /**
222
+ * Test harness for `[wrSpotlight]` — the directive that makes a cursor-following
223
+ * gradient possible on an element the consumer owns.
224
+ *
225
+ * **This harness reads PERCENTAGES. {@link WrSpotlightCardHarness}, shipped from the same
226
+ * entry point, reads PIXELS.** Both write `--wr-spotlight-x` / `--wr-spotlight-y`; the
227
+ * directive writes them as a share of its own box off `pointermove`, the card as a pixel
228
+ * offset off `mousemove`. That is why {@link getSpotlightPosition} hands back raw strings
229
+ * and not numbers — collapsing `'37.5%'` to `37.5` would hide the one thing that decides
230
+ * whether the gradient lands where the cursor is, and would make the two halves of this
231
+ * entry point look interchangeable when they are not.
232
+ *
233
+ * **The directive ships no styles and adds no node.** There is no class to assert, no
234
+ * wrapper, no ARIA: whatever the element announced before the directive applies is
235
+ * exactly what it announces after, and the gradient is entirely the consumer's own CSS.
236
+ * So there is no `isSpotlightVisible()` — there is nothing here to be visible — and no
237
+ * `getResetX()` / `getResetY()`, which would echo the inputs back rather than report the
238
+ * coordinate the directive actually WROTE. The echo would pass while the written value
239
+ * was wrong, which is not hypothetical (see {@link getSpotlightPosition}). And no
240
+ * `respectsReducedMotion()`: neither export in this entry point implements the
241
+ * preference — it is the library's one documented exemption, since a gradient tracks the
242
+ * cursor and no content moves — so the method would answer the same for a directive that
243
+ * honoured it and one that ignored it.
244
+ *
245
+ * @example
246
+ * ```ts
247
+ * const spotlight = await loader.getHarness(WrSpotlightHarness.with({ hostClass: 'card' }));
248
+ *
249
+ * expect(await spotlight.getSpotlightPosition()).toEqual({ x: '50%', y: '50%' });
250
+ * await spotlight.movePointerTo(150, 25); // stub the host's rect first
251
+ * ```
252
+ *
253
+ * @see https://ngwr.dev/guides/testing
254
+ */
255
+ class WrSpotlightHarness extends ComponentHarness {
256
+ static hostSelector = '[wrSpotlight]';
257
+ /**
258
+ * Build a predicate that narrows the query — pass to `getHarness` / `getAllHarnesses`.
259
+ *
260
+ * Both options describe the HOST rather than the directive, because the directive
261
+ * leaves no mark of its own to match on.
262
+ */
263
+ static with(options = {}) {
264
+ return new HarnessPredicate(WrSpotlightHarness, options)
265
+ .addOption('text', options.text, async (harness, text) => HarnessPredicate.stringMatches((await harness.host()).text(), text))
266
+ .addOption('hostClass', options.hostClass, async (harness, hostClass) => (await harness.host()).hasClass(hostClass));
267
+ }
268
+ /**
269
+ * The coordinates the directive has written, as the raw strings it wrote them.
270
+ *
271
+ * Strings on purpose — see this class's docs. The values are percentages of the host's
272
+ * own box while a pointer is over it, and whatever `[resetX]` / `[resetY]` say
273
+ * otherwise, so a `px` appearing here means something else is writing these variables.
274
+ *
275
+ * **Present from construction, before any pointer arrives.** The directive seeds the
276
+ * reset coordinates in its constructor, so the first read is `50%` / `50%` on a fresh
277
+ * host — do not mistake that for a pointer having been tracked. Worth knowing that the
278
+ * constructor runs before signal inputs are bound, so a bound (or even a static)
279
+ * `resetX` is NOT what appears here initially: it is honoured from the first
280
+ * {@link leave} onward, and the highlight jumps at that point. Reading the written
281
+ * coordinate rather than the input is what makes that visible at all.
282
+ *
283
+ * Throws once the directive has been destroyed, rather than answering with the last
284
+ * value it happened to leave behind — {@link hasTrackedPointer} is the question to ask
285
+ * when the host may be gone.
286
+ */
287
+ async getSpotlightPosition() {
288
+ const vars = await this.vars();
289
+ const x = vars.get('--wr-spotlight-x');
290
+ const y = vars.get('--wr-spotlight-y');
291
+ if (x === undefined || y === undefined) {
292
+ throw new Error('WrSpotlightHarness.getSpotlightPosition(): the host carries no `--wr-spotlight-x` / `--wr-spotlight-y`. ' +
293
+ 'The directive writes both in its constructor and removes both on destroy, so this is a torn-down ' +
294
+ 'directive rather than one that has not started — ask hasTrackedPointer() when the host may be gone.');
295
+ }
296
+ return { x, y };
297
+ }
298
+ /**
299
+ * Point at the host, in client coordinates.
300
+ *
301
+ * **Refuses on a host with no box, which in a unit test is every host.** The directive
302
+ * divides the cursor offset by the element's own width and height, so a 0×0 rect writes
303
+ * literally `Infinity%` or — at the origin — `NaN%` into the style. jsdom accepts either
304
+ * string without complaint, so an unguarded harness would hand back something that looks
305
+ * like a coordinate and describes nothing. Give the host a box first:
306
+ *
307
+ * ```ts
308
+ * const el = fixture.nativeElement.querySelector('[wrSpotlight]');
309
+ * el.getBoundingClientRect = () => ({ left: 0, top: 0, width: 200, height: 100 }) as DOMRect;
310
+ * ```
311
+ *
312
+ * A `pointermove`, not a `mousemove` — the directive listens for the former, and the
313
+ * component in the same entry point for the latter. It is dispatched on the host itself,
314
+ * so it needs no hit test and cannot miss.
315
+ */
316
+ async movePointerTo(clientX, clientY) {
317
+ const host = await this.host();
318
+ const { width, height } = await host.getDimensions();
319
+ if (!width || !height) {
320
+ throw new Error(`WrSpotlightHarness.movePointerTo(${clientX}, ${clientY}): this host measures ${width}×${height}, so a ` +
321
+ 'pointer position over it means nothing — the directive divides the offset by the box, and the result ' +
322
+ 'goes into the style as `Infinity%` or `NaN%`. Stub `getBoundingClientRect` on the host first.');
323
+ }
324
+ await host.dispatchEvent('pointermove', { clientX, clientY, pointerId: 1, isPrimary: true });
325
+ }
326
+ /**
327
+ * Take the pointer away.
328
+ *
329
+ * Dispatches `pointerleave` — the directive's reset path, and the only place `[resetX]`
330
+ * / `[resetY]` are read after construction. Needs no box: the handler writes the reset
331
+ * strings straight through without measuring anything, which is why this one does not
332
+ * refuse the way {@link movePointerTo} does.
333
+ *
334
+ * Not the CDK's `mouseAway()`, though that dispatches `pointerleave` too. This says what
335
+ * it does, and it does not also fire the `mouseleave` / `mouseout` that the CDK helper
336
+ * sends alongside it — events this directive ignores, and that would make a spec look
337
+ * like it had covered more than it did.
338
+ */
339
+ async leave() {
340
+ await (await this.host()).dispatchEvent('pointerleave');
341
+ }
342
+ /**
343
+ * Whether the directive still owns the host's spotlight variables.
344
+ *
345
+ * **This is "the directive is alive", not "a pointer has moved over it".** It is `true`
346
+ * from the moment the directive constructs, because the constructor seeds the reset
347
+ * coordinates — there is no earlier state to catch. What it distinguishes is a live
348
+ * directive from a torn-down one: destroy removes both declarations from the element,
349
+ * and nothing else in the repo asserts that cleanup. A directive that stopped removing
350
+ * them would leave a stale gradient position on an element the consumer went on using,
351
+ * and the only trace would be these two declarations outliving it.
352
+ */
353
+ async hasTrackedPointer() {
354
+ const vars = await this.vars();
355
+ return vars.has('--wr-spotlight-x') && vars.has('--wr-spotlight-y');
356
+ }
357
+ /** The host's inline declarations — the directive's whole written output. */
358
+ async vars() {
359
+ return wrSpotlightInlineVars((await (await this.host()).getAttribute('style')) ?? '');
360
+ }
361
+ }
362
+
363
+ /**
364
+ * Generated bundle index. Do not edit.
365
+ */
366
+
367
+ export { WrSpotlightCardHarness, WrSpotlightHarness };
368
+ //# sourceMappingURL=ngwr-spotlight-card-testing.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ngwr-spotlight-card-testing.mjs","sources":["../../../projects/lib/spotlight-card/testing/wr-spotlight-harness-vars.ts","../../../projects/lib/spotlight-card/testing/wr-spotlight-card-harness.ts","../../../projects/lib/spotlight-card/testing/wr-spotlight-harness.ts","../../../projects/lib/spotlight-card/testing/ngwr-spotlight-card-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\n/**\n * Split an inline `style` attribute into its declarations.\n *\n * Read from the attribute rather than through `getCssValue()`, and the difference is not\n * cosmetic. `getCssValue()` is `getComputedStyle()`, which in a browser resolves the\n * stylesheet's own `--wr-spotlight-x: 50%` / `--wr-spotlight-color: rgba(…)` defaults —\n * so \"the component has written nothing here yet, and the theme decides\" would come back\n * as a concrete value and the assertion would invert. jsdom loads no stylesheet, so the\n * computed read happens to agree today and would start lying the moment these harnesses\n * ran anywhere else. The attribute holds what the component or the directive itself\n * wrote, and nothing else.\n *\n * Shared by both harnesses in this entry point because both read the same three\n * variables off their host — in different units. See each class for which.\n */\nexport function wrSpotlightInlineVars(style: string): Map<string, string> {\n const declarations = new Map<string, string>();\n for (const declaration of style.split(';')) {\n const colon = declaration.indexOf(':');\n if (colon === -1) continue;\n const name = declaration.slice(0, colon).trim();\n if (name !== '') declarations.set(name, declaration.slice(colon + 1).trim());\n }\n return declarations;\n}\n","/**\n * @license\n *\n * Use of this source code is governed by an MIT-style license that can be\n * found in the LICENSE file at https://github.com/thekhegay/ngwr/blob/main/LICENSE\n */\n\nimport { ComponentHarness, HarnessPredicate } from '@angular/cdk/testing';\n\nimport type { WrSpotlightCardHarnessFilters } from './interfaces';\nimport { wrSpotlightInlineVars } from './wr-spotlight-harness-vars';\n\n/** `120px` — the card writes its pointer coordinates as a pixel length, always. */\nconst PIXEL_LENGTH = /^(-?[\\d.]+)px$/;\n\n/** `80%` — the radius, whose `%` suffix is the half that makes the gradient legal. */\nconst PERCENTAGE = /^(-?[\\d.]+)%$/;\n\n/**\n * Test harness for `<wr-spotlight-card>` — a card with a soft radial highlight that\n * follows the cursor.\n *\n * **The component's entire output is three custom properties on its own host, and this\n * harness reads them off the inline declaration.** There is no DOM for the highlight at\n * all: it is a `::before` pseudo-element, painted by the stylesheet from those three\n * values. So the paint is out of reach here and always will be — what is in reach is the\n * arithmetic and the exact spelling that feeds it, which is where this component breaks.\n *\n * **This harness reads PIXELS. {@link WrSpotlightHarness}, the `[wrSpotlight]` directive\n * shipped from the same entry point, reads PERCENTAGES.** Both write\n * `--wr-spotlight-x` / `--wr-spotlight-y`, in different units, off different events —\n * the card listens for `mousemove`, the directive for `pointermove`. That is deliberate\n * and documented, and it is also the mistake waiting to happen: putting `wrSpotlight` on\n * a `<wr-spotlight-card>` gives you two handlers overwriting one variable in units that\n * do not agree. Reach for the harness that matches the thing under test.\n *\n * **{@link movePointerTo} needs the card to have a box, and in a unit test it has none.**\n * The component subtracts its own `getBoundingClientRect()` from the client coordinate,\n * and jsdom answers that with a 0×0 rect at the origin — so the subtraction is a no-op\n * and the coordinate comes back unchanged. No throw here, unlike the directive's version:\n * the answer is degenerate rather than wrong, and it is legitimately what a real card\n * with no box would report. But a spec that asserts `x === clientX` against an unstubbed\n * rect has proved nothing about the box-relative conversion, which is the only thing\n * {@link getSpotlightPosition} is for. Give the host a box first:\n *\n * ```ts\n * const card = fixture.nativeElement.querySelector('wr-spotlight-card');\n * card.getBoundingClientRect = () => ({ left: 40, top: 20, width: 200, height: 100 }) as DOMRect;\n * ```\n *\n * Several plausible methods are deliberately absent. There is no `isSpotlightVisible()`\n * or `getSpotlightOpacity()`: the highlight sits at `opacity: 0` and is lifted to `0.6`\n * only under `:hover` / `:focus-within`, both of which are stylesheet selectors on a\n * pseudo-element — jsdom loads no stylesheet, matches no `:hover`, and cannot read a\n * pseudo-element's style at any time, so any answer would be invented. No `getGradient()`\n * for the same reason. No `hover()` / `mouseAway()` wrappers, which would be worse than\n * missing: the CDK's versions dispatch `mouseenter` / `mouseover` / `mouseleave`, none of\n * which this component listens to and none of which changes a `:hover` match in a test —\n * a spec calling them would look like it exercised the reveal and would have exercised\n * nothing. No `getSpotlightRect()` or `getCardSize()`, which are layout. And no\n * `respectsReducedMotion()`: this is the one animation component in the library that\n * deliberately does not implement the preference — a gradient tracks the cursor and no\n * content moves — so the method would answer identically for a component that honoured it\n * and one that ignored it.\n *\n * @example\n * ```ts\n * const card = await loader.getHarness(WrSpotlightCardHarness.with({ text: 'Hover me' }));\n *\n * expect(await card.getRadiusPercent()).toBe(80);\n * expect(await card.getSpotlightColor()).toBeNull(); // the theme decides\n * expect(await card.getSpotlightPosition()).toBeNull(); // nothing has pointed at it\n * ```\n *\n * @see https://ngwr.dev/guides/testing\n */\nexport class WrSpotlightCardHarness extends ComponentHarness {\n static hostSelector = 'wr-spotlight-card';\n\n /** Build a predicate that narrows the query — pass to `getHarness` / `getAllHarnesses`. */\n static with(options: WrSpotlightCardHarnessFilters = {}): HarnessPredicate<WrSpotlightCardHarness> {\n return new HarnessPredicate(WrSpotlightCardHarness, options).addOption('text', options.text, (harness, text) =>\n HarnessPredicate.stringMatches(harness.getContentText(), text)\n );\n }\n\n /**\n * Where the gradient fades out, as a percentage of its own radius.\n *\n * Returns the number, but the assertion it really makes is about the `%` that was\n * stripped to get it — this method refuses a bare `80`. The stylesheet interpolates the\n * variable straight into `transparent var(--wr-spotlight-radius, 80%)`, where a unitless\n * value is not a length, so the whole `radial-gradient()` declaration is dropped and the\n * highlight disappears with nothing in the console to say why. Reading this as a plain\n * number would sail past exactly that.\n *\n * Always present, whatever the input says: a `[radius]` that is not a number coerces\n * back to the default 80 rather than writing nonsense into the sheet.\n */\n async getRadiusPercent(): Promise<number> {\n const raw = (await this.vars()).get('--wr-spotlight-radius') ?? null;\n const match = raw === null ? null : PERCENTAGE.exec(raw);\n if (!match) {\n throw new Error(\n `WrSpotlightCardHarness.getRadiusPercent(): \\`--wr-spotlight-radius\\` reads \"${raw ?? '(absent)'}\" on the ` +\n 'host, and the stylesheet needs a percentage there — it drops the whole gradient for a bare number. The ' +\n 'card writes this one on every render whatever the input says, so a missing or unsuffixed value means ' +\n 'the binding itself stopped reaching the element.'\n );\n }\n return Number.parseFloat(match[1]);\n }\n\n /**\n * The highlight colour the author asked for, or `null` when they left it to the theme.\n *\n * `null` IS the contract for an unset `[spotlightColor]`, not a gap in it. The\n * stylesheet's own default is `rgba(var(--wr-color-on-surface-rgb), .15)`, and that\n * token flips per theme — which is what makes one component read as a dark glow on a\n * light surface and a light beam on a dark one. A card that started writing a concrete\n * colour here would look right in whichever theme it was authored against and wrong in\n * the other, and this is the only method that can tell.\n */\n async getSpotlightColor(): Promise<string | null> {\n return (await this.vars()).get('--wr-spotlight-color') ?? null;\n }\n\n /**\n * Where the highlight last sat, in pixels from the card's own top-left corner. `null`\n * until something has pointed at the card.\n *\n * Pixels, not percentages — see this class's docs for why the distinction matters\n * inside this one entry point.\n *\n * `null` is a state of its own rather than a stand-in for `{ x: 0, y: 0 }`: before the\n * first move the stylesheet's `50% / 50%` default applies, and a pointer at the card's\n * own origin legitimately reports zero, so folding the two together would make an\n * untouched card indistinguishable from one being pointed at in its corner.\n *\n * Note there is no reset when the cursor leaves, and that is not an oversight the\n * harness papers over: the stylesheet fades the whole overlay to `opacity: 0` on leave,\n * so nothing is parked visibly — but the coordinate stays written for the life of the\n * element. `null` therefore means \"no pointer has EVER arrived\", not \"none is here now\".\n */\n async getSpotlightPosition(): Promise<{ x: number; y: number } | null> {\n const vars = await this.vars();\n const x = vars.get('--wr-spotlight-x');\n const y = vars.get('--wr-spotlight-y');\n if (x === undefined || y === undefined) return null;\n\n return { x: this.parsePixels(x, '--wr-spotlight-x'), y: this.parsePixels(y, '--wr-spotlight-y') };\n }\n\n /**\n * Point at the card, in client coordinates.\n *\n * A `mousemove`, not a `pointermove` — the component listens for the former, and the\n * directive in the same entry point for the latter, so a pointer event dispatched here\n * is silently ignored. It goes to the host itself, so it needs no hit test and cannot\n * miss in a test that has no layout; the coordinates only ever reach the component's\n * own subtraction.\n *\n * Stub the host's `getBoundingClientRect` before calling this, or the subtraction is\n * invisible — see this class's docs.\n */\n async movePointerTo(clientX: number, clientY: number): Promise<void> {\n await (await this.host()).dispatchEvent('mousemove', { clientX, clientY });\n }\n\n /**\n * The projected content's text, trimmed.\n *\n * Read from the host, which is the whole assertion: the template is a bare\n * `<ng-content />` with no wrapper element, so content lands as a direct child and an\n * interactive one keeps its own name and role. The component adds no ARIA and no node\n * of its own — there is nothing else here for a harness to ask about.\n */\n async getContentText(): Promise<string> {\n return (await this.host()).text();\n }\n\n /** The host's inline declarations — the component's whole written output. */\n private async vars(): Promise<Map<string, string>> {\n return wrSpotlightInlineVars((await (await this.host()).getAttribute('style')) ?? '');\n }\n\n /** One written coordinate, as a number, insisting on the `px` the card promises. */\n private parsePixels(raw: string, name: string): number {\n const match = PIXEL_LENGTH.exec(raw);\n if (!match) {\n throw new Error(\n `WrSpotlightCardHarness.getSpotlightPosition(): \\`${name}\\` reads \"${raw}\", which is not the pixel length ` +\n 'the card writes. A percentage here is the tell that `[wrSpotlight]` is on the same element — the two ' +\n 'exports of this entry point share these variables and disagree about the unit.'\n );\n }\n return Number.parseFloat(match[1]);\n }\n}\n","/**\n * @license\n *\n * Use of this source code is governed by an MIT-style license that can be\n * found in the LICENSE file at https://github.com/thekhegay/ngwr/blob/main/LICENSE\n */\n\nimport { ComponentHarness, HarnessPredicate } from '@angular/cdk/testing';\n\nimport type { WrSpotlightHarnessFilters } from './interfaces';\nimport { wrSpotlightInlineVars } from './wr-spotlight-harness-vars';\n\n/**\n * Test harness for `[wrSpotlight]` — the directive that makes a cursor-following\n * gradient possible on an element the consumer owns.\n *\n * **This harness reads PERCENTAGES. {@link WrSpotlightCardHarness}, shipped from the same\n * entry point, reads PIXELS.** Both write `--wr-spotlight-x` / `--wr-spotlight-y`; the\n * directive writes them as a share of its own box off `pointermove`, the card as a pixel\n * offset off `mousemove`. That is why {@link getSpotlightPosition} hands back raw strings\n * and not numbers — collapsing `'37.5%'` to `37.5` would hide the one thing that decides\n * whether the gradient lands where the cursor is, and would make the two halves of this\n * entry point look interchangeable when they are not.\n *\n * **The directive ships no styles and adds no node.** There is no class to assert, no\n * wrapper, no ARIA: whatever the element announced before the directive applies is\n * exactly what it announces after, and the gradient is entirely the consumer's own CSS.\n * So there is no `isSpotlightVisible()` — there is nothing here to be visible — and no\n * `getResetX()` / `getResetY()`, which would echo the inputs back rather than report the\n * coordinate the directive actually WROTE. The echo would pass while the written value\n * was wrong, which is not hypothetical (see {@link getSpotlightPosition}). And no\n * `respectsReducedMotion()`: neither export in this entry point implements the\n * preference — it is the library's one documented exemption, since a gradient tracks the\n * cursor and no content moves — so the method would answer the same for a directive that\n * honoured it and one that ignored it.\n *\n * @example\n * ```ts\n * const spotlight = await loader.getHarness(WrSpotlightHarness.with({ hostClass: 'card' }));\n *\n * expect(await spotlight.getSpotlightPosition()).toEqual({ x: '50%', y: '50%' });\n * await spotlight.movePointerTo(150, 25); // stub the host's rect first\n * ```\n *\n * @see https://ngwr.dev/guides/testing\n */\nexport class WrSpotlightHarness extends ComponentHarness {\n static hostSelector = '[wrSpotlight]';\n\n /**\n * Build a predicate that narrows the query — pass to `getHarness` / `getAllHarnesses`.\n *\n * Both options describe the HOST rather than the directive, because the directive\n * leaves no mark of its own to match on.\n */\n static with(options: WrSpotlightHarnessFilters = {}): HarnessPredicate<WrSpotlightHarness> {\n return new HarnessPredicate(WrSpotlightHarness, options)\n .addOption('text', options.text, async (harness, text) =>\n HarnessPredicate.stringMatches((await harness.host()).text(), text)\n )\n .addOption('hostClass', options.hostClass, async (harness, hostClass) =>\n (await harness.host()).hasClass(hostClass)\n );\n }\n\n /**\n * The coordinates the directive has written, as the raw strings it wrote them.\n *\n * Strings on purpose — see this class's docs. The values are percentages of the host's\n * own box while a pointer is over it, and whatever `[resetX]` / `[resetY]` say\n * otherwise, so a `px` appearing here means something else is writing these variables.\n *\n * **Present from construction, before any pointer arrives.** The directive seeds the\n * reset coordinates in its constructor, so the first read is `50%` / `50%` on a fresh\n * host — do not mistake that for a pointer having been tracked. Worth knowing that the\n * constructor runs before signal inputs are bound, so a bound (or even a static)\n * `resetX` is NOT what appears here initially: it is honoured from the first\n * {@link leave} onward, and the highlight jumps at that point. Reading the written\n * coordinate rather than the input is what makes that visible at all.\n *\n * Throws once the directive has been destroyed, rather than answering with the last\n * value it happened to leave behind — {@link hasTrackedPointer} is the question to ask\n * when the host may be gone.\n */\n async getSpotlightPosition(): Promise<{ x: string; y: string }> {\n const vars = await this.vars();\n const x = vars.get('--wr-spotlight-x');\n const y = vars.get('--wr-spotlight-y');\n if (x === undefined || y === undefined) {\n throw new Error(\n 'WrSpotlightHarness.getSpotlightPosition(): the host carries no `--wr-spotlight-x` / `--wr-spotlight-y`. ' +\n 'The directive writes both in its constructor and removes both on destroy, so this is a torn-down ' +\n 'directive rather than one that has not started — ask hasTrackedPointer() when the host may be gone.'\n );\n }\n return { x, y };\n }\n\n /**\n * Point at the host, in client coordinates.\n *\n * **Refuses on a host with no box, which in a unit test is every host.** The directive\n * divides the cursor offset by the element's own width and height, so a 0×0 rect writes\n * literally `Infinity%` or — at the origin — `NaN%` into the style. jsdom accepts either\n * string without complaint, so an unguarded harness would hand back something that looks\n * like a coordinate and describes nothing. Give the host a box first:\n *\n * ```ts\n * const el = fixture.nativeElement.querySelector('[wrSpotlight]');\n * el.getBoundingClientRect = () => ({ left: 0, top: 0, width: 200, height: 100 }) as DOMRect;\n * ```\n *\n * A `pointermove`, not a `mousemove` — the directive listens for the former, and the\n * component in the same entry point for the latter. It is dispatched on the host itself,\n * so it needs no hit test and cannot miss.\n */\n async movePointerTo(clientX: number, clientY: number): Promise<void> {\n const host = await this.host();\n const { width, height } = await host.getDimensions();\n if (!width || !height) {\n throw new Error(\n `WrSpotlightHarness.movePointerTo(${clientX}, ${clientY}): this host measures ${width}×${height}, so a ` +\n 'pointer position over it means nothing — the directive divides the offset by the box, and the result ' +\n 'goes into the style as `Infinity%` or `NaN%`. Stub `getBoundingClientRect` on the host first.'\n );\n }\n await host.dispatchEvent('pointermove', { clientX, clientY, pointerId: 1, isPrimary: true });\n }\n\n /**\n * Take the pointer away.\n *\n * Dispatches `pointerleave` — the directive's reset path, and the only place `[resetX]`\n * / `[resetY]` are read after construction. Needs no box: the handler writes the reset\n * strings straight through without measuring anything, which is why this one does not\n * refuse the way {@link movePointerTo} does.\n *\n * Not the CDK's `mouseAway()`, though that dispatches `pointerleave` too. This says what\n * it does, and it does not also fire the `mouseleave` / `mouseout` that the CDK helper\n * sends alongside it — events this directive ignores, and that would make a spec look\n * like it had covered more than it did.\n */\n async leave(): Promise<void> {\n await (await this.host()).dispatchEvent('pointerleave');\n }\n\n /**\n * Whether the directive still owns the host's spotlight variables.\n *\n * **This is \"the directive is alive\", not \"a pointer has moved over it\".** It is `true`\n * from the moment the directive constructs, because the constructor seeds the reset\n * coordinates — there is no earlier state to catch. What it distinguishes is a live\n * directive from a torn-down one: destroy removes both declarations from the element,\n * and nothing else in the repo asserts that cleanup. A directive that stopped removing\n * them would leave a stale gradient position on an element the consumer went on using,\n * and the only trace would be these two declarations outliving it.\n */\n async hasTrackedPointer(): Promise<boolean> {\n const vars = await this.vars();\n return vars.has('--wr-spotlight-x') && vars.has('--wr-spotlight-y');\n }\n\n /** The host's inline declarations — the directive's whole written output. */\n private async vars(): Promise<Map<string, string>> {\n return wrSpotlightInlineVars((await (await this.host()).getAttribute('style')) ?? '');\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,172 @@
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
+ /** Read one declaration out of an inline `style` attribute, or `null` when it is not there. */
10
+ function inlineStyle(style, property) {
11
+ for (const declaration of (style ?? '').split(';')) {
12
+ const colon = declaration.indexOf(':');
13
+ if (colon === -1)
14
+ continue;
15
+ if (declaration.slice(0, colon).trim() !== property)
16
+ continue;
17
+ return declaration.slice(colon + 1).trim();
18
+ }
19
+ return null;
20
+ }
21
+ /**
22
+ * Test harness for `<wr-star-border>` and for the `[wr-star-border]` attribute form.
23
+ *
24
+ * **There is no `getRayCount()`, and no `hasTopRay()`.** `rays="single"` does not remove
25
+ * the top ray: both spans are always in the DOM and the stylesheet hides one with
26
+ * `display: none`. A count would answer 2 in both modes — flatly contradicting the input
27
+ * that was set — so the variant is read as {@link isSingleRay}, off the modifier class
28
+ * the CSS itself keys on. That class is public API, and it is the honest observable.
29
+ *
30
+ * Nothing here reports on the motion either. The sweep is a `@keyframes` rule in a
31
+ * stylesheet a unit test never loads, so `animationName` is `''` and `getAnimations()` is
32
+ * empty whether the component works or not; `mode="hover"` is a bare `:hover` selector
33
+ * with no state behind it, which is why there is no `hover()` to pair with anything; and
34
+ * the reduced-motion escape is a media query with no mirror in TypeScript, so the DOM is
35
+ * identical for a user who asked for less motion and one who did not.
36
+ *
37
+ * **The inline values are read off the `style` attribute rather than through
38
+ * `getCssValue()`, and that is not a jsdom concession.** The stylesheet declares its own
39
+ * `--wr-star-border-color` and `--wr-star-border-speed` on `.wr-star-border`, so in a
40
+ * real browser a computed read answers with the stylesheet's default at exactly the
41
+ * moment the host binding is the thing that broke. What the component wrote is the only
42
+ * value nothing else can forge.
43
+ *
44
+ * On the attribute form the host's `[class]` binding MERGES with the classes the consumer
45
+ * already put there, so read the variants through {@link isHoverOnly} and
46
+ * {@link isSingleRay} rather than comparing a whole `className` — the latter passes only
47
+ * as long as the host happens to carry nothing else.
48
+ *
49
+ * @example
50
+ * ```ts
51
+ * const loader = TestbedHarnessEnvironment.loader(fixture);
52
+ * const border = await loader.getHarness(WrStarBorderHarness);
53
+ *
54
+ * expect(await border.getText()).toBe('Upgrade');
55
+ * expect(await border.getSpeedSeconds()).toBe(6);
56
+ * ```
57
+ *
58
+ * @see https://ngwr.dev/guides/testing
59
+ */
60
+ class WrStarBorderHarness extends ComponentHarness {
61
+ static hostSelector = 'wr-star-border, [wr-star-border]';
62
+ /** Build a predicate that narrows the query — pass to `getHarness` / `getAllHarnesses`. */
63
+ static with(options = {}) {
64
+ return new HarnessPredicate(WrStarBorderHarness, options).addOption('text', options.text, (harness, text) => HarnessPredicate.stringMatches(harness.getText(), text));
65
+ }
66
+ /**
67
+ * The projected content's text, trimmed.
68
+ *
69
+ * Read from the inner panel rather than from the host, which would pick up anything the
70
+ * rays ever came to hold. They are empty spans today, so the two agree — but the panel
71
+ * is the slot the content was put in, and it is the thing a reader actually sees.
72
+ */
73
+ async getText() {
74
+ return (await this.locatorFor('.wr-star-border__inner')()).text();
75
+ }
76
+ /** Whether the rays wait for the pointer (`mode="hover"`) instead of running always. */
77
+ async isHoverOnly() {
78
+ return (await this.host()).hasClass('wr-star-border--hover');
79
+ }
80
+ /**
81
+ * Whether only the bottom ray is drawn (`rays="single"`).
82
+ *
83
+ * From the modifier class, because the DOM cannot be asked: both ray spans are rendered
84
+ * in both modes and the top one is hidden in CSS. See this class's docs.
85
+ */
86
+ async isSingleRay() {
87
+ return (await this.host()).hasClass('wr-star-border--single');
88
+ }
89
+ /**
90
+ * The ray colour as the consumer passed it — a CSS value, not a resolved one — or
91
+ * `null` when none was given.
92
+ *
93
+ * `null` is the common case and a real answer rather than a missing one: with no
94
+ * `[color]` the theme decides, primary on a light surface and white on a dark one, and
95
+ * a component that started writing a colour of its own there would silently take that
96
+ * choice away.
97
+ */
98
+ async getRayColor() {
99
+ return this.hostStyle('--wr-star-border-color');
100
+ }
101
+ /**
102
+ * How long one ray sweep takes, in seconds.
103
+ *
104
+ * Refuses a value that lost its unit rather than parsing the number out of it. The
105
+ * component builds this string from a plain number input and the stylesheet drops it
106
+ * straight into `animation-duration`, where a bare `6` is invalid — the declaration is
107
+ * discarded, the stylesheet's own default takes over, and nothing about the page looks
108
+ * wrong. A `parseFloat` would report 6 either way.
109
+ */
110
+ async getSpeedSeconds() {
111
+ const raw = await this.hostStyle('--wr-star-border-speed');
112
+ const match = /^(-?\d*\.?\d+)s$/.exec(raw ?? '');
113
+ if (!match) {
114
+ throw new Error(`WrStarBorderHarness.getSpeedSeconds(): \`--wr-star-border-speed\` reads "${raw}" rather than a value in ` +
115
+ 'seconds. The stylesheet uses it as an animation-duration, so anything without the unit is an invalid ' +
116
+ 'declaration and the rays fall back to the default sweep.');
117
+ }
118
+ return Number(match[1]);
119
+ }
120
+ /**
121
+ * How far the rays bleed past the inner panel, in pixels.
122
+ *
123
+ * Read off the host's `padding`, which is where `[thickness]` lands: the component
124
+ * writes the SHORTHAND, `<n>px 0`. Worth knowing on the attribute form — an inline
125
+ * shorthand wins the cascade, so a `<button wr-star-border>` loses whatever padding its
126
+ * own classes gave it, horizontal included. The visible padding lives on the inner
127
+ * panel, so this is a bleed measurement rather than a spacing one.
128
+ *
129
+ * Throws when the host's padding is not that shape, which means either the binding
130
+ * stopped arriving or something else is writing the property inline.
131
+ */
132
+ async getThickness() {
133
+ const raw = await this.hostStyle('padding');
134
+ const match = /^(-?\d*\.?\d+)px\s+0(?:px)?$/.exec(raw ?? '');
135
+ if (!match) {
136
+ throw new Error(`WrStarBorderHarness.getThickness(): the host's inline padding reads "${raw}" rather than the ` +
137
+ '`<n>px 0` shorthand the component writes from [thickness]. Either the binding is gone, or something ' +
138
+ 'else is setting padding inline on the same element.');
139
+ }
140
+ return Number(match[1]);
141
+ }
142
+ /**
143
+ * Whether every ray is kept out of the accessible tree.
144
+ *
145
+ * The one question worth asking about both spans at once — not how many there are, but
146
+ * whether either has stopped being decoration. They carry no text and mean nothing, so
147
+ * a ray that reached a screen reader would be an empty announcement in the middle of a
148
+ * button. Answered for all of them together, so one ray losing the attribute is enough
149
+ * to report `false`.
150
+ */
151
+ async areRaysDecorative() {
152
+ const rays = await this.locatorForAll('.wr-star-border__ray')();
153
+ if (rays.length === 0) {
154
+ throw new Error('WrStarBorderHarness.areRaysDecorative(): there are no rays at all. Both spans are rendered in every ' +
155
+ 'mode — `rays="single"` hides one in CSS rather than dropping it — so none of them is a broken template ' +
156
+ 'rather than a variant.');
157
+ }
158
+ const hidden = await Promise.all(rays.map(ray => ray.getAttribute('aria-hidden')));
159
+ return hidden.every(value => value === 'true');
160
+ }
161
+ /** Read one declaration off the host's inline `style` — see this class's docs for why not `getCssValue()`. */
162
+ async hostStyle(property) {
163
+ return inlineStyle(await (await this.host()).getAttribute('style'), property);
164
+ }
165
+ }
166
+
167
+ /**
168
+ * Generated bundle index. Do not edit.
169
+ */
170
+
171
+ export { WrStarBorderHarness };
172
+ //# sourceMappingURL=ngwr-star-border-testing.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ngwr-star-border-testing.mjs","sources":["../../../projects/lib/star-border/testing/wr-star-border-harness.ts","../../../projects/lib/star-border/testing/ngwr-star-border-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 { WrStarBorderHarnessFilters } 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-star-border>` and for the `[wr-star-border]` attribute form.\n *\n * **There is no `getRayCount()`, and no `hasTopRay()`.** `rays=\"single\"` does not remove\n * the top ray: both spans are always in the DOM and the stylesheet hides one with\n * `display: none`. A count would answer 2 in both modes — flatly contradicting the input\n * that was set — so the variant is read as {@link isSingleRay}, off the modifier class\n * the CSS itself keys on. That class is public API, and it is the honest observable.\n *\n * Nothing here reports on the motion either. The sweep is a `@keyframes` rule in a\n * stylesheet a unit test never loads, so `animationName` is `''` and `getAnimations()` is\n * empty whether the component works or not; `mode=\"hover\"` is a bare `:hover` selector\n * with no state behind it, which is why there is no `hover()` to pair with anything; and\n * the reduced-motion escape is a media query with no mirror in TypeScript, so the DOM is\n * identical for a user who asked for less motion and one who did not.\n *\n * **The inline values are read off the `style` attribute rather than through\n * `getCssValue()`, and that is not a jsdom concession.** The stylesheet declares its own\n * `--wr-star-border-color` and `--wr-star-border-speed` on `.wr-star-border`, so in a\n * real browser a computed read answers with the stylesheet's default at exactly the\n * moment the host binding is the thing that broke. What the component wrote is the only\n * value nothing else can forge.\n *\n * On the attribute form the host's `[class]` binding MERGES with the classes the consumer\n * already put there, so read the variants through {@link isHoverOnly} and\n * {@link isSingleRay} rather than comparing a whole `className` — the latter passes only\n * as long as the host happens to carry nothing else.\n *\n * @example\n * ```ts\n * const loader = TestbedHarnessEnvironment.loader(fixture);\n * const border = await loader.getHarness(WrStarBorderHarness);\n *\n * expect(await border.getText()).toBe('Upgrade');\n * expect(await border.getSpeedSeconds()).toBe(6);\n * ```\n *\n * @see https://ngwr.dev/guides/testing\n */\nexport class WrStarBorderHarness extends ComponentHarness {\n static hostSelector = 'wr-star-border, [wr-star-border]';\n\n /** Build a predicate that narrows the query — pass to `getHarness` / `getAllHarnesses`. */\n static with(options: WrStarBorderHarnessFilters = {}): HarnessPredicate<WrStarBorderHarness> {\n return new HarnessPredicate(WrStarBorderHarness, options).addOption('text', options.text, (harness, text) =>\n HarnessPredicate.stringMatches(harness.getText(), text)\n );\n }\n\n /**\n * The projected content's text, trimmed.\n *\n * Read from the inner panel rather than from the host, which would pick up anything the\n * rays ever came to hold. They are empty spans today, so the two agree — but the panel\n * is the slot the content was put in, and it is the thing a reader actually sees.\n */\n async getText(): Promise<string> {\n return (await this.locatorFor('.wr-star-border__inner')()).text();\n }\n\n /** Whether the rays wait for the pointer (`mode=\"hover\"`) instead of running always. */\n async isHoverOnly(): Promise<boolean> {\n return (await this.host()).hasClass('wr-star-border--hover');\n }\n\n /**\n * Whether only the bottom ray is drawn (`rays=\"single\"`).\n *\n * From the modifier class, because the DOM cannot be asked: both ray spans are rendered\n * in both modes and the top one is hidden in CSS. See this class's docs.\n */\n async isSingleRay(): Promise<boolean> {\n return (await this.host()).hasClass('wr-star-border--single');\n }\n\n /**\n * The ray colour as the consumer passed it — a CSS value, not a resolved one — or\n * `null` when none was given.\n *\n * `null` is the common case and a real answer rather than a missing one: with no\n * `[color]` the theme decides, primary on a light surface and white on a dark one, and\n * a component that started writing a colour of its own there would silently take that\n * choice away.\n */\n async getRayColor(): Promise<string | null> {\n return this.hostStyle('--wr-star-border-color');\n }\n\n /**\n * How long one ray sweep takes, in seconds.\n *\n * Refuses a value that lost its unit rather than parsing the number out of it. The\n * component builds this string from a plain number input and the stylesheet drops it\n * straight into `animation-duration`, where a bare `6` is invalid — the declaration is\n * discarded, the stylesheet's own default takes over, and nothing about the page looks\n * wrong. A `parseFloat` would report 6 either way.\n */\n async getSpeedSeconds(): Promise<number> {\n const raw = await this.hostStyle('--wr-star-border-speed');\n const match = /^(-?\\d*\\.?\\d+)s$/.exec(raw ?? '');\n\n if (!match) {\n throw new Error(\n `WrStarBorderHarness.getSpeedSeconds(): \\`--wr-star-border-speed\\` reads \"${raw}\" rather than a value in ` +\n 'seconds. The stylesheet uses it as an animation-duration, so anything without the unit is an invalid ' +\n 'declaration and the rays fall back to the default sweep.'\n );\n }\n return Number(match[1]);\n }\n\n /**\n * How far the rays bleed past the inner panel, in pixels.\n *\n * Read off the host's `padding`, which is where `[thickness]` lands: the component\n * writes the SHORTHAND, `<n>px 0`. Worth knowing on the attribute form — an inline\n * shorthand wins the cascade, so a `<button wr-star-border>` loses whatever padding its\n * own classes gave it, horizontal included. The visible padding lives on the inner\n * panel, so this is a bleed measurement rather than a spacing one.\n *\n * Throws when the host's padding is not that shape, which means either the binding\n * stopped arriving or something else is writing the property inline.\n */\n async getThickness(): Promise<number> {\n const raw = await this.hostStyle('padding');\n const match = /^(-?\\d*\\.?\\d+)px\\s+0(?:px)?$/.exec(raw ?? '');\n\n if (!match) {\n throw new Error(\n `WrStarBorderHarness.getThickness(): the host's inline padding reads \"${raw}\" rather than the ` +\n '`<n>px 0` shorthand the component writes from [thickness]. Either the binding is gone, or something ' +\n 'else is setting padding inline on the same element.'\n );\n }\n return Number(match[1]);\n }\n\n /**\n * Whether every ray is kept out of the accessible tree.\n *\n * The one question worth asking about both spans at once — not how many there are, but\n * whether either has stopped being decoration. They carry no text and mean nothing, so\n * a ray that reached a screen reader would be an empty announcement in the middle of a\n * button. Answered for all of them together, so one ray losing the attribute is enough\n * to report `false`.\n */\n async areRaysDecorative(): Promise<boolean> {\n const rays = await this.locatorForAll('.wr-star-border__ray')();\n\n if (rays.length === 0) {\n throw new Error(\n 'WrStarBorderHarness.areRaysDecorative(): there are no rays at all. Both spans are rendered in every ' +\n 'mode — `rays=\"single\"` hides one in CSS rather than dropping it — so none of them is a broken template ' +\n 'rather than a variant.'\n );\n }\n\n const hidden = await Promise.all(rays.map(ray => ray.getAttribute('aria-hidden')));\n return hidden.every(value => value === 'true');\n }\n\n /** Read one declaration off the host's inline `style` — see this class's docs for why not `getCssValue()`. */\n private async hostStyle(property: string): Promise<string | null> {\n return inlineStyle(await (await this.host()).getAttribute('style'), property);\n }\n}\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public-api';\n"],"names":[],"mappings":"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"}