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,336 @@
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 one entry of a `<wr-marquee>`.
11
+ *
12
+ * **Reach these through `WrMarqueeHarness.getItems()`, which scopes them to the first
13
+ * copy.** The strip duplicates its whole sequence to make the loop seamless, so asking
14
+ * the document loader for all of these returns one per entry per copy — at least twice
15
+ * everything, and a count assertion that quietly multiplies. The parent harness is the
16
+ * only place that knows which copy is the original.
17
+ *
18
+ * An entry is built from an image or from a projected template, and it may or may not be
19
+ * a link. That is the reason to ask a harness rather than the DOM: the same question —
20
+ * what does this announce, is it decoration — is answered from a different attribute in
21
+ * each of those shapes, and {@link getAccessibleName} and {@link isDecorative} pick the
22
+ * right one.
23
+ *
24
+ * @see https://ngwr.dev/guides/testing
25
+ */
26
+ class WrMarqueeItemHarness extends ComponentHarness {
27
+ static hostSelector = '.wr-marquee__item';
28
+ /** Build a predicate that narrows the query — pass to `getHarness` / `getAllHarnesses`. */
29
+ static with(options = {}) {
30
+ return new HarnessPredicate(WrMarqueeItemHarness, options)
31
+ .addOption('name', options.name, (harness, name) => HarnessPredicate.stringMatches(harness.getAccessibleName(), name))
32
+ .addOption('link', options.link, async (harness, link) => (await harness.isLink()) === link);
33
+ }
34
+ link = this.locatorForOptional('a.wr-marquee__link');
35
+ image = this.locatorForOptional('img');
36
+ node = this.locatorForOptional('.wr-marquee__node');
37
+ /**
38
+ * What this entry announces, without the caller needing to know how it was built.
39
+ *
40
+ * A linked entry answers with the link's own name, which is the end of a four-step
41
+ * ladder — the item's `ariaLabel`, then its `title`, then an image's `alt`, then the
42
+ * catalog's `marquee.link`. An unlinked image answers with its `alt`, where `''` means
43
+ * the logo is decoration and announces nothing. Anything else answers with its text.
44
+ */
45
+ async getAccessibleName() {
46
+ const link = await this.link();
47
+ if (link)
48
+ return link.getAttribute('aria-label');
49
+ const image = await this.image();
50
+ if (image)
51
+ return image.getAttribute('alt');
52
+ return (await this.host()).text();
53
+ }
54
+ /** Whether this entry became a link — `[items]` carries `href` per entry, optionally. */
55
+ async isLink() {
56
+ return (await this.link()) !== null;
57
+ }
58
+ /** The `href` exactly as the entry declared it, unresolved, or `null` when it is not a link. */
59
+ async getHref() {
60
+ const link = await this.link();
61
+ return link ? link.getAttribute('href') : null;
62
+ }
63
+ /**
64
+ * Whether the link opens in a new tab AND says so safely: `target="_blank"` with both
65
+ * `noreferrer` and `noopener` in `rel`.
66
+ *
67
+ * One question rather than two, because half of it is a reverse-tabnabbing hole that
68
+ * nothing visual would ever show. The library hard-codes the safe pair, so this is a
69
+ * regression check on the template rather than on the caller's data.
70
+ *
71
+ * Throws for an entry that is not a link, where a `false` would read as "it opens in
72
+ * this tab" — an answer about safety that is not true of anything.
73
+ */
74
+ async opensInNewTab() {
75
+ const link = await this.link();
76
+ if (!link) {
77
+ throw new Error(`WrMarqueeItemHarness.opensInNewTab(): "${await this.getAccessibleName()}" is not a link — its entry in ` +
78
+ '[items] carries no `href`, so there is no target or rel to check. Ask isLink() first.');
79
+ }
80
+ const rel = (await link.getAttribute('rel')) ?? '';
81
+ return (await link.getAttribute('target')) === '_blank' && rel.includes('noreferrer') && rel.includes('noopener');
82
+ }
83
+ /**
84
+ * Whether this entry's content is kept out of the accessible tree, leaving whatever
85
+ * wraps it to do the announcing.
86
+ *
87
+ * The marker depends on how the entry was built, which is the whole reason to ask here:
88
+ * a projected template inside a named link carries `aria-hidden`, so the link's own name
89
+ * is not read twice, while an image says the same thing with `alt=""`. It describes the
90
+ * CONTENT — a decorative logo inside a link is still announced, by the link, and that
91
+ * name is {@link getAccessibleName}.
92
+ *
93
+ * Throws for an entry that rendered neither, which should not happen — every entry of
94
+ * `[items]` is one or the other.
95
+ */
96
+ async isDecorative() {
97
+ const node = await this.node();
98
+ if (node)
99
+ return (await node.getAttribute('aria-hidden')) === 'true';
100
+ const image = await this.image();
101
+ if (image)
102
+ return (await image.getAttribute('alt')) === '';
103
+ throw new Error('WrMarqueeItemHarness.isDecorative(): this entry rendered neither a projected node nor an image, so there ' +
104
+ 'is nothing carrying a decorative marker. Every entry of [items] is one or the other.');
105
+ }
106
+ }
107
+
108
+ /**
109
+ * @license
110
+ *
111
+ * Use of this source code is governed by an MIT-style license that can be
112
+ * found in the LICENSE file at https://github.com/thekhegay/ngwr/blob/main/LICENSE
113
+ */
114
+ /** Copy 0 is the original sequence; every other `.wr-marquee__list` is a clone of it. */
115
+ const FIRST_COPY = '.wr-marquee__list[data-copy="0"]';
116
+ /** Read one declaration out of an inline `style` attribute, or `null` when it is not there. */
117
+ function inlineStyle(style, property) {
118
+ for (const declaration of (style ?? '').split(';')) {
119
+ const colon = declaration.indexOf(':');
120
+ if (colon === -1)
121
+ continue;
122
+ if (declaration.slice(0, colon).trim() !== property)
123
+ continue;
124
+ return declaration.slice(colon + 1).trim();
125
+ }
126
+ return null;
127
+ }
128
+ /**
129
+ * Test harness for `<wr-marquee>`.
130
+ *
131
+ * **Almost everything worth asserting about a marquee is structural, not animated.** The
132
+ * strip duplicates its sequence so the loop has no seam, and only the first copy is left
133
+ * in the accessible tree — {@link getAnnouncedCopyCount} is the assertion that catches
134
+ * the regression that matters, because a duplicate leaking in makes a screen reader read
135
+ * the whole logo strip twice and changes nothing on screen. The other half is the naming
136
+ * ladder on linked entries, which is real logic with a real bug in its history: the
137
+ * fallback used to be the hard-coded English word "link".
138
+ *
139
+ * **Every list query is scoped to the first copy**, and that is the reason to use
140
+ * {@link getItems} rather than a raw `.wr-marquee__item` query, which returns at least
141
+ * twice as many elements as there are entries.
142
+ *
143
+ * Nothing about the motion is offered. The track's `translate3d` is written only when the
144
+ * measured sequence is wider than zero, so it never appears in a test at all, and in a
145
+ * browser it is one frame of a loop that is already stale by the time an assertion reads
146
+ * it. `[speed]` and `[direction]` reach the DOM in no form. Neither does the hover state:
147
+ * pausing flips a private signal with no class, attribute or property behind it, so a
148
+ * `hover()` here would fire events that provably change nothing readable. And the
149
+ * reduced-motion freeze is a CSS `!important` rule with no counterpart in TypeScript,
150
+ * which means the DOM is identical either way — see `getGap` for what a harness can
151
+ * honestly read off a stylesheet-driven component, and why it reads it where it does.
152
+ *
153
+ * @example
154
+ * ```ts
155
+ * const loader = TestbedHarnessEnvironment.loader(fixture);
156
+ * const strip = await loader.getHarness(WrMarqueeHarness);
157
+ *
158
+ * expect(await strip.getAnnouncedCopyCount()).toBe(1);
159
+ * expect(await strip.getItemCount()).toBe(logos.length);
160
+ * ```
161
+ *
162
+ * @see https://ngwr.dev/guides/testing
163
+ */
164
+ class WrMarqueeHarness extends ComponentHarness {
165
+ static hostSelector = 'wr-marquee';
166
+ /** Build a predicate that narrows the query — pass to `getHarness` / `getAllHarnesses`. */
167
+ static with(options = {}) {
168
+ return new HarnessPredicate(WrMarqueeHarness, options).addOption('ariaLabel', options.ariaLabel, (harness, label) => HarnessPredicate.stringMatches(harness.getAccessibleName(), label));
169
+ }
170
+ copies = this.locatorForAll('.wr-marquee__list');
171
+ /**
172
+ * The name the region announces.
173
+ *
174
+ * Pins the i18n ladder end to end: `'Marquee'` with no catalog loaded, the consumer's
175
+ * `[ariaLabel]` when one is given, the localized string under a catalog. A landmark
176
+ * with no name is an axe failure, so this is what keeps the `region` legitimate rather
177
+ * than a decoration a screen-reader user is invited to jump to and finds unnamed.
178
+ */
179
+ async getAccessibleName() {
180
+ return (await this.host()).getAttribute('aria-label');
181
+ }
182
+ /**
183
+ * The landmark role the strip claims.
184
+ *
185
+ * Static in the template and one attribute away from being dropped in a refactor, which
186
+ * is exactly why a consumer asserting their page's landmark structure should be able to
187
+ * see it from here.
188
+ */
189
+ async getRole() {
190
+ return (await this.host()).getAttribute('role');
191
+ }
192
+ /**
193
+ * How many copies of the sequence are rendered.
194
+ *
195
+ * The seamless loop is built by repeating the whole list until it covers the container,
196
+ * so the count is decided by measurement — which means it stays at the floor of 2 in a
197
+ * test with no layout, whatever the container would really be. Assert `>= 2`; the
198
+ * growth needs a browser.
199
+ */
200
+ async getCopyCount() {
201
+ return (await this.copies()).length;
202
+ }
203
+ /**
204
+ * How many copies are left in the accessible tree. Must be exactly 1.
205
+ *
206
+ * The one assertion here that catches a silent regression: every copy after the first
207
+ * carries `aria-hidden="true"`, and losing that makes a screen reader read the whole
208
+ * strip once per copy while the page looks unchanged.
209
+ *
210
+ * Note the first copy carries the literal `aria-hidden="false"` rather than no
211
+ * attribute at all, so this compares against the string `'true'` — testing the
212
+ * attribute for presence would count nothing at all as announced.
213
+ */
214
+ async getAnnouncedCopyCount() {
215
+ const copies = await this.copies();
216
+ const hidden = await Promise.all(copies.map(copy => copy.getAttribute('aria-hidden')));
217
+ return hidden.filter(value => value !== 'true').length;
218
+ }
219
+ /** The entries of the first copy, in order — one per entry of `[items]`. */
220
+ async getItems() {
221
+ return this.locatorForAll(WrMarqueeItemHarness.with({ ancestor: FIRST_COPY }))();
222
+ }
223
+ /**
224
+ * How many entries the strip drew, counting the first copy only.
225
+ *
226
+ * One per entry of `[items]` — the mapping a consumer's own data pipeline is really
227
+ * being tested for.
228
+ */
229
+ async getItemCount() {
230
+ return (await this.locatorForAll(`${FIRST_COPY} .wr-marquee__item`)()).length;
231
+ }
232
+ /**
233
+ * The `alt` of each image in the first copy, in order.
234
+ *
235
+ * `null` in this list is the thing to look for: it means the attribute is MISSING, which
236
+ * is a serious axe violation and announces the file name. An empty string is the
237
+ * opposite — a deliberately decorative logo — and the template's `alt || ''` is what
238
+ * keeps the two apart. A method typed `string[]` would have had to flatten them.
239
+ */
240
+ async getImageAlts() {
241
+ const images = await this.locatorForAll(`${FIRST_COPY} img`)();
242
+ return Promise.all(images.map(image => image.getAttribute('alt')));
243
+ }
244
+ /**
245
+ * Every linked entry of the first copy: where it goes and what it announces.
246
+ *
247
+ * The name is the interesting half. It walks a ladder ending in the catalog's
248
+ * `marquee.link`, so an unlabelled logo announces a localized word rather than nothing
249
+ * — real component logic rather than an echo of the input.
250
+ */
251
+ async getLinks() {
252
+ const links = await this.locatorForAll(`${FIRST_COPY} a.wr-marquee__link`)();
253
+ return Promise.all(links.map(async (link) => ({ href: await link.getAttribute('href'), name: await link.getAttribute('aria-label') })));
254
+ }
255
+ /**
256
+ * Whether the edge fades are on.
257
+ *
258
+ * The fades themselves are `::before` / `::after` gradients, which no unit environment
259
+ * can see — so the modifier class is the observable, and it is public API a consumer
260
+ * may be styling against in any case.
261
+ */
262
+ async hasFade() {
263
+ return (await this.host()).hasClass('wr-marquee--fade');
264
+ }
265
+ /**
266
+ * The fade colour as the component passed it through — a CSS value such as
267
+ * `'var(--wr-color-success)'`, not a resolved colour — or `null` when none was given.
268
+ *
269
+ * Worth asserting next to {@link hasFade}: a colour with no fade modifier renders
270
+ * nothing at all, which is a consumer mistake this pairing makes visible.
271
+ */
272
+ async getFadeColor() {
273
+ return this.hostStyle('--wr-marquee-fade-color');
274
+ }
275
+ /**
276
+ * Whether entries scale up under the pointer.
277
+ *
278
+ * A `:hover` transform, so again the modifier is the only honest read — and it earns
279
+ * its place by doing more than the name suggests: it also changes the strip's vertical
280
+ * padding, so a lost class shifts the layout as well as the hover.
281
+ */
282
+ async hasScaleOnHover() {
283
+ return (await this.host()).hasClass('wr-marquee--scale-hover');
284
+ }
285
+ /** The gap between entries in pixels — the seam width the stylesheet reads. */
286
+ async getGap() {
287
+ return this.pixels('--wr-marquee-gap', 'getGap');
288
+ }
289
+ /**
290
+ * The entry height in pixels.
291
+ *
292
+ * It drives the image height AND the entry font size, so a binding that stops arriving
293
+ * collapses the whole strip rather than merely resizing it.
294
+ */
295
+ async getItemHeight() {
296
+ return this.pixels('--wr-marquee-height', 'getItemHeight');
297
+ }
298
+ /**
299
+ * Read a custom property off the host's inline `style`, rather than through
300
+ * `getCssValue()`.
301
+ *
302
+ * Not a jsdom workaround — the opposite. The stylesheet declares its own
303
+ * `--wr-marquee-gap: 32px` and `--wr-marquee-height: 28px` on `.wr-marquee`, so a
304
+ * computed read in a real browser answers with the stylesheet's default when the host
305
+ * binding is the very thing that broke. The inline value is what the component wrote,
306
+ * and nothing else can forge it.
307
+ */
308
+ async hostStyle(property) {
309
+ return inlineStyle(await (await this.host()).getAttribute('style'), property);
310
+ }
311
+ /**
312
+ * Parse a `<n>px` custom property, refusing anything that lost its unit.
313
+ *
314
+ * The refusal is the point. `[gap]` and `[itemHeight]` are numbers that the host binding
315
+ * turns into lengths, and the stylesheet uses them raw — a bare `32` is an invalid
316
+ * declaration, so the property falls back to the stylesheet default and the strip looks
317
+ * merely a little different. A `parseFloat` would report 32 either way and hide it.
318
+ */
319
+ async pixels(property, method) {
320
+ const raw = await this.hostStyle(property);
321
+ const match = /^(-?\d*\.?\d+)px$/.exec(raw ?? '');
322
+ if (!match) {
323
+ throw new Error(`WrMarqueeHarness.${method}(): \`${property}\` reads "${raw}" rather than a px length. The stylesheet ` +
324
+ 'uses this value as a raw CSS length, so anything without the unit is an invalid declaration and the ' +
325
+ 'strip silently falls back to the default in the stylesheet.');
326
+ }
327
+ return Number(match[1]);
328
+ }
329
+ }
330
+
331
+ /**
332
+ * Generated bundle index. Do not edit.
333
+ */
334
+
335
+ export { WrMarqueeHarness, WrMarqueeItemHarness };
336
+ //# sourceMappingURL=ngwr-marquee-testing.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ngwr-marquee-testing.mjs","sources":["../../../projects/lib/marquee/testing/wr-marquee-item-harness.ts","../../../projects/lib/marquee/testing/wr-marquee-harness.ts","../../../projects/lib/marquee/testing/ngwr-marquee-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 { WrMarqueeItemHarnessFilters } from './interfaces';\n\n/**\n * Test harness for one entry of a `<wr-marquee>`.\n *\n * **Reach these through `WrMarqueeHarness.getItems()`, which scopes them to the first\n * copy.** The strip duplicates its whole sequence to make the loop seamless, so asking\n * the document loader for all of these returns one per entry per copy — at least twice\n * everything, and a count assertion that quietly multiplies. The parent harness is the\n * only place that knows which copy is the original.\n *\n * An entry is built from an image or from a projected template, and it may or may not be\n * a link. That is the reason to ask a harness rather than the DOM: the same question —\n * what does this announce, is it decoration — is answered from a different attribute in\n * each of those shapes, and {@link getAccessibleName} and {@link isDecorative} pick the\n * right one.\n *\n * @see https://ngwr.dev/guides/testing\n */\nexport class WrMarqueeItemHarness extends ComponentHarness {\n static hostSelector = '.wr-marquee__item';\n\n /** Build a predicate that narrows the query — pass to `getHarness` / `getAllHarnesses`. */\n static with(options: WrMarqueeItemHarnessFilters = {}): HarnessPredicate<WrMarqueeItemHarness> {\n return new HarnessPredicate(WrMarqueeItemHarness, options)\n .addOption('name', options.name, (harness, name) =>\n HarnessPredicate.stringMatches(harness.getAccessibleName(), name)\n )\n .addOption('link', options.link, async (harness, link) => (await harness.isLink()) === link);\n }\n\n private readonly link = this.locatorForOptional('a.wr-marquee__link');\n private readonly image = this.locatorForOptional('img');\n private readonly node = this.locatorForOptional('.wr-marquee__node');\n\n /**\n * What this entry announces, without the caller needing to know how it was built.\n *\n * A linked entry answers with the link's own name, which is the end of a four-step\n * ladder — the item's `ariaLabel`, then its `title`, then an image's `alt`, then the\n * catalog's `marquee.link`. An unlinked image answers with its `alt`, where `''` means\n * the logo is decoration and announces nothing. Anything else answers with its text.\n */\n async getAccessibleName(): Promise<string | null> {\n const link = await this.link();\n if (link) return link.getAttribute('aria-label');\n\n const image = await this.image();\n if (image) return image.getAttribute('alt');\n\n return (await this.host()).text();\n }\n\n /** Whether this entry became a link — `[items]` carries `href` per entry, optionally. */\n async isLink(): Promise<boolean> {\n return (await this.link()) !== null;\n }\n\n /** The `href` exactly as the entry declared it, unresolved, or `null` when it is not a link. */\n async getHref(): Promise<string | null> {\n const link = await this.link();\n return link ? link.getAttribute('href') : null;\n }\n\n /**\n * Whether the link opens in a new tab AND says so safely: `target=\"_blank\"` with both\n * `noreferrer` and `noopener` in `rel`.\n *\n * One question rather than two, because half of it is a reverse-tabnabbing hole that\n * nothing visual would ever show. The library hard-codes the safe pair, so this is a\n * regression check on the template rather than on the caller's data.\n *\n * Throws for an entry that is not a link, where a `false` would read as \"it opens in\n * this tab\" — an answer about safety that is not true of anything.\n */\n async opensInNewTab(): Promise<boolean> {\n const link = await this.link();\n if (!link) {\n throw new Error(\n `WrMarqueeItemHarness.opensInNewTab(): \"${await this.getAccessibleName()}\" is not a link — its entry in ` +\n '[items] carries no `href`, so there is no target or rel to check. Ask isLink() first.'\n );\n }\n\n const rel = (await link.getAttribute('rel')) ?? '';\n return (await link.getAttribute('target')) === '_blank' && rel.includes('noreferrer') && rel.includes('noopener');\n }\n\n /**\n * Whether this entry's content is kept out of the accessible tree, leaving whatever\n * wraps it to do the announcing.\n *\n * The marker depends on how the entry was built, which is the whole reason to ask here:\n * a projected template inside a named link carries `aria-hidden`, so the link's own name\n * is not read twice, while an image says the same thing with `alt=\"\"`. It describes the\n * CONTENT — a decorative logo inside a link is still announced, by the link, and that\n * name is {@link getAccessibleName}.\n *\n * Throws for an entry that rendered neither, which should not happen — every entry of\n * `[items]` is one or the other.\n */\n async isDecorative(): Promise<boolean> {\n const node = await this.node();\n if (node) return (await node.getAttribute('aria-hidden')) === 'true';\n\n const image = await this.image();\n if (image) return (await image.getAttribute('alt')) === '';\n\n throw new Error(\n 'WrMarqueeItemHarness.isDecorative(): this entry rendered neither a projected node nor an image, so there ' +\n 'is nothing carrying a decorative marker. Every entry of [items] is one or the other.'\n );\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 { WrMarqueeHarnessFilters, WrMarqueeLink } from './interfaces';\nimport { WrMarqueeItemHarness } from './wr-marquee-item-harness';\n\n/** Copy 0 is the original sequence; every other `.wr-marquee__list` is a clone of it. */\nconst FIRST_COPY = '.wr-marquee__list[data-copy=\"0\"]';\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-marquee>`.\n *\n * **Almost everything worth asserting about a marquee is structural, not animated.** The\n * strip duplicates its sequence so the loop has no seam, and only the first copy is left\n * in the accessible tree — {@link getAnnouncedCopyCount} is the assertion that catches\n * the regression that matters, because a duplicate leaking in makes a screen reader read\n * the whole logo strip twice and changes nothing on screen. The other half is the naming\n * ladder on linked entries, which is real logic with a real bug in its history: the\n * fallback used to be the hard-coded English word \"link\".\n *\n * **Every list query is scoped to the first copy**, and that is the reason to use\n * {@link getItems} rather than a raw `.wr-marquee__item` query, which returns at least\n * twice as many elements as there are entries.\n *\n * Nothing about the motion is offered. The track's `translate3d` is written only when the\n * measured sequence is wider than zero, so it never appears in a test at all, and in a\n * browser it is one frame of a loop that is already stale by the time an assertion reads\n * it. `[speed]` and `[direction]` reach the DOM in no form. Neither does the hover state:\n * pausing flips a private signal with no class, attribute or property behind it, so a\n * `hover()` here would fire events that provably change nothing readable. And the\n * reduced-motion freeze is a CSS `!important` rule with no counterpart in TypeScript,\n * which means the DOM is identical either way — see `getGap` for what a harness can\n * honestly read off a stylesheet-driven component, and why it reads it where it does.\n *\n * @example\n * ```ts\n * const loader = TestbedHarnessEnvironment.loader(fixture);\n * const strip = await loader.getHarness(WrMarqueeHarness);\n *\n * expect(await strip.getAnnouncedCopyCount()).toBe(1);\n * expect(await strip.getItemCount()).toBe(logos.length);\n * ```\n *\n * @see https://ngwr.dev/guides/testing\n */\nexport class WrMarqueeHarness extends ComponentHarness {\n static hostSelector = 'wr-marquee';\n\n /** Build a predicate that narrows the query — pass to `getHarness` / `getAllHarnesses`. */\n static with(options: WrMarqueeHarnessFilters = {}): HarnessPredicate<WrMarqueeHarness> {\n return new HarnessPredicate(WrMarqueeHarness, options).addOption('ariaLabel', options.ariaLabel, (harness, label) =>\n HarnessPredicate.stringMatches(harness.getAccessibleName(), label)\n );\n }\n\n private readonly copies = this.locatorForAll('.wr-marquee__list');\n\n /**\n * The name the region announces.\n *\n * Pins the i18n ladder end to end: `'Marquee'` with no catalog loaded, the consumer's\n * `[ariaLabel]` when one is given, the localized string under a catalog. A landmark\n * with no name is an axe failure, so this is what keeps the `region` legitimate rather\n * than a decoration a screen-reader user is invited to jump to and finds unnamed.\n */\n async getAccessibleName(): Promise<string | null> {\n return (await this.host()).getAttribute('aria-label');\n }\n\n /**\n * The landmark role the strip claims.\n *\n * Static in the template and one attribute away from being dropped in a refactor, which\n * is exactly why a consumer asserting their page's landmark structure should be able to\n * see it from here.\n */\n async getRole(): Promise<string | null> {\n return (await this.host()).getAttribute('role');\n }\n\n /**\n * How many copies of the sequence are rendered.\n *\n * The seamless loop is built by repeating the whole list until it covers the container,\n * so the count is decided by measurement — which means it stays at the floor of 2 in a\n * test with no layout, whatever the container would really be. Assert `>= 2`; the\n * growth needs a browser.\n */\n async getCopyCount(): Promise<number> {\n return (await this.copies()).length;\n }\n\n /**\n * How many copies are left in the accessible tree. Must be exactly 1.\n *\n * The one assertion here that catches a silent regression: every copy after the first\n * carries `aria-hidden=\"true\"`, and losing that makes a screen reader read the whole\n * strip once per copy while the page looks unchanged.\n *\n * Note the first copy carries the literal `aria-hidden=\"false\"` rather than no\n * attribute at all, so this compares against the string `'true'` — testing the\n * attribute for presence would count nothing at all as announced.\n */\n async getAnnouncedCopyCount(): Promise<number> {\n const copies = await this.copies();\n const hidden = await Promise.all(copies.map(copy => copy.getAttribute('aria-hidden')));\n return hidden.filter(value => value !== 'true').length;\n }\n\n /** The entries of the first copy, in order — one per entry of `[items]`. */\n async getItems(): Promise<WrMarqueeItemHarness[]> {\n return this.locatorForAll(WrMarqueeItemHarness.with({ ancestor: FIRST_COPY }))();\n }\n\n /**\n * How many entries the strip drew, counting the first copy only.\n *\n * One per entry of `[items]` — the mapping a consumer's own data pipeline is really\n * being tested for.\n */\n async getItemCount(): Promise<number> {\n return (await this.locatorForAll(`${FIRST_COPY} .wr-marquee__item`)()).length;\n }\n\n /**\n * The `alt` of each image in the first copy, in order.\n *\n * `null` in this list is the thing to look for: it means the attribute is MISSING, which\n * is a serious axe violation and announces the file name. An empty string is the\n * opposite — a deliberately decorative logo — and the template's `alt || ''` is what\n * keeps the two apart. A method typed `string[]` would have had to flatten them.\n */\n async getImageAlts(): Promise<(string | null)[]> {\n const images = await this.locatorForAll(`${FIRST_COPY} img`)();\n return Promise.all(images.map(image => image.getAttribute('alt')));\n }\n\n /**\n * Every linked entry of the first copy: where it goes and what it announces.\n *\n * The name is the interesting half. It walks a ladder ending in the catalog's\n * `marquee.link`, so an unlabelled logo announces a localized word rather than nothing\n * — real component logic rather than an echo of the input.\n */\n async getLinks(): Promise<WrMarqueeLink[]> {\n const links = await this.locatorForAll(`${FIRST_COPY} a.wr-marquee__link`)();\n return Promise.all(\n links.map(async link => ({ href: await link.getAttribute('href'), name: await link.getAttribute('aria-label') }))\n );\n }\n\n /**\n * Whether the edge fades are on.\n *\n * The fades themselves are `::before` / `::after` gradients, which no unit environment\n * can see — so the modifier class is the observable, and it is public API a consumer\n * may be styling against in any case.\n */\n async hasFade(): Promise<boolean> {\n return (await this.host()).hasClass('wr-marquee--fade');\n }\n\n /**\n * The fade colour as the component passed it through — a CSS value such as\n * `'var(--wr-color-success)'`, not a resolved colour — or `null` when none was given.\n *\n * Worth asserting next to {@link hasFade}: a colour with no fade modifier renders\n * nothing at all, which is a consumer mistake this pairing makes visible.\n */\n async getFadeColor(): Promise<string | null> {\n return this.hostStyle('--wr-marquee-fade-color');\n }\n\n /**\n * Whether entries scale up under the pointer.\n *\n * A `:hover` transform, so again the modifier is the only honest read — and it earns\n * its place by doing more than the name suggests: it also changes the strip's vertical\n * padding, so a lost class shifts the layout as well as the hover.\n */\n async hasScaleOnHover(): Promise<boolean> {\n return (await this.host()).hasClass('wr-marquee--scale-hover');\n }\n\n /** The gap between entries in pixels — the seam width the stylesheet reads. */\n async getGap(): Promise<number> {\n return this.pixels('--wr-marquee-gap', 'getGap');\n }\n\n /**\n * The entry height in pixels.\n *\n * It drives the image height AND the entry font size, so a binding that stops arriving\n * collapses the whole strip rather than merely resizing it.\n */\n async getItemHeight(): Promise<number> {\n return this.pixels('--wr-marquee-height', 'getItemHeight');\n }\n\n /**\n * Read a custom property off the host's inline `style`, rather than through\n * `getCssValue()`.\n *\n * Not a jsdom workaround — the opposite. The stylesheet declares its own\n * `--wr-marquee-gap: 32px` and `--wr-marquee-height: 28px` on `.wr-marquee`, so a\n * computed read in a real browser answers with the stylesheet's default when the host\n * binding is the very thing that broke. The inline value is what the component wrote,\n * and nothing else can forge it.\n */\n private async hostStyle(property: string): Promise<string | null> {\n return inlineStyle(await (await this.host()).getAttribute('style'), property);\n }\n\n /**\n * Parse a `<n>px` custom property, refusing anything that lost its unit.\n *\n * The refusal is the point. `[gap]` and `[itemHeight]` are numbers that the host binding\n * turns into lengths, and the stylesheet uses them raw — a bare `32` is an invalid\n * declaration, so the property falls back to the stylesheet default and the strip looks\n * merely a little different. A `parseFloat` would report 32 either way and hide it.\n */\n private async pixels(property: string, method: string): Promise<number> {\n const raw = await this.hostStyle(property);\n const match = /^(-?\\d*\\.?\\d+)px$/.exec(raw ?? '');\n\n if (!match) {\n throw new Error(\n `WrMarqueeHarness.${method}(): \\`${property}\\` reads \"${raw}\" rather than a px length. The stylesheet ` +\n 'uses this value as a raw CSS length, so anything without the unit is an invalid declaration and the ' +\n 'strip silently falls back to the default in the stylesheet.'\n );\n }\n return Number(match[1]);\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,80 @@
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-meter-group>` — one bar divided into labelled bands.
11
+ *
12
+ * **The bar is a single `progressbar`, and its value is the TOTAL.** The bands
13
+ * themselves announce nothing: they carry a `title` for a mouse and their share as an
14
+ * inline width, and the legend is where their labels and values are readable. So a
15
+ * spec that wants "how much of each" reads {@link getLegend} or {@link getSlices},
16
+ * and one that wants "how full" reads {@link getValue}.
17
+ *
18
+ * @see https://ngwr.dev/guides/testing
19
+ */
20
+ class WrMeterGroupHarness extends ComponentHarness {
21
+ static hostSelector = 'wr-meter-group';
22
+ /** Build a predicate that narrows the query — pass to `getHarness` / `getAllHarnesses`. */
23
+ static with(options = {}) {
24
+ return new HarnessPredicate(WrMeterGroupHarness, options)
25
+ .addOption('ariaLabel', options.ariaLabel, (harness, label) => HarnessPredicate.stringMatches(harness.getAccessibleName(), label))
26
+ .addOption('sliceLabel', options.sliceLabel, async (harness, label) => {
27
+ for (const printed of await harness.getLegendLabels()) {
28
+ if (await HarnessPredicate.stringMatches(printed, label))
29
+ return true;
30
+ }
31
+ return false;
32
+ });
33
+ }
34
+ bar = this.locatorFor('.wr-meter-group__bar');
35
+ /** The role the bar announces — `progressbar`, for the total. */
36
+ async getRole() {
37
+ return (await this.bar()).getAttribute('role');
38
+ }
39
+ /** The bar's accessible name. */
40
+ async getAccessibleName() {
41
+ return (await this.bar()).getAttribute('aria-label');
42
+ }
43
+ /** The total the bands add up to, from `aria-valuenow`. */
44
+ async getValue() {
45
+ return Number(await (await this.bar()).getAttribute('aria-valuenow'));
46
+ }
47
+ /** The full capacity, from `aria-valuemax` — given, or the sum of the bands. */
48
+ async getMax() {
49
+ return Number(await (await this.bar()).getAttribute('aria-valuemax'));
50
+ }
51
+ /** The bands, in order, with the share of the bar each takes. */
52
+ async getSlices() {
53
+ const slices = await this.locatorForAll('.wr-meter-group__slice')();
54
+ return Promise.all(slices.map(async (slice) => ({
55
+ label: await slice.getAttribute('title'),
56
+ percent: Number.parseFloat(await slice.getCssValue('width')),
57
+ })));
58
+ }
59
+ /** Whether the legend is rendered (`showLegend`, and at least one band). */
60
+ async hasLegend() {
61
+ return (await this.locatorForOptional('.wr-meter-group__legend')()) !== null;
62
+ }
63
+ /** The legend's labels, in order. */
64
+ async getLegendLabels() {
65
+ const labels = await this.locatorForAll('.wr-meter-group__legend-label')();
66
+ return Promise.all(labels.map(label => label.text()));
67
+ }
68
+ /** The legend's values, in order — empty when `showValues` is off. */
69
+ async getLegendValues() {
70
+ const values = await this.locatorForAll('.wr-meter-group__legend-value')();
71
+ return Promise.all(values.map(value => value.text()));
72
+ }
73
+ }
74
+
75
+ /**
76
+ * Generated bundle index. Do not edit.
77
+ */
78
+
79
+ export { WrMeterGroupHarness };
80
+ //# sourceMappingURL=ngwr-meter-group-testing.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ngwr-meter-group-testing.mjs","sources":["../../../projects/lib/meter-group/testing/wr-meter-group-harness.ts","../../../projects/lib/meter-group/testing/ngwr-meter-group-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 { WrMeterGroupHarnessFilters, WrMeterGroupSlice } from './interfaces';\n\n/**\n * Test harness for `<wr-meter-group>` — one bar divided into labelled bands.\n *\n * **The bar is a single `progressbar`, and its value is the TOTAL.** The bands\n * themselves announce nothing: they carry a `title` for a mouse and their share as an\n * inline width, and the legend is where their labels and values are readable. So a\n * spec that wants \"how much of each\" reads {@link getLegend} or {@link getSlices},\n * and one that wants \"how full\" reads {@link getValue}.\n *\n * @see https://ngwr.dev/guides/testing\n */\nexport class WrMeterGroupHarness extends ComponentHarness {\n static hostSelector = 'wr-meter-group';\n\n /** Build a predicate that narrows the query — pass to `getHarness` / `getAllHarnesses`. */\n static with(options: WrMeterGroupHarnessFilters = {}): HarnessPredicate<WrMeterGroupHarness> {\n return new HarnessPredicate(WrMeterGroupHarness, options)\n .addOption('ariaLabel', options.ariaLabel, (harness, label) =>\n HarnessPredicate.stringMatches(harness.getAccessibleName(), label)\n )\n .addOption('sliceLabel', options.sliceLabel, async (harness, label) => {\n for (const printed of await harness.getLegendLabels()) {\n if (await HarnessPredicate.stringMatches(printed, label)) return true;\n }\n return false;\n });\n }\n\n private readonly bar = this.locatorFor('.wr-meter-group__bar');\n\n /** The role the bar announces — `progressbar`, for the total. */\n async getRole(): Promise<string | null> {\n return (await this.bar()).getAttribute('role');\n }\n\n /** The bar's accessible name. */\n async getAccessibleName(): Promise<string | null> {\n return (await this.bar()).getAttribute('aria-label');\n }\n\n /** The total the bands add up to, from `aria-valuenow`. */\n async getValue(): Promise<number> {\n return Number(await (await this.bar()).getAttribute('aria-valuenow'));\n }\n\n /** The full capacity, from `aria-valuemax` — given, or the sum of the bands. */\n async getMax(): Promise<number> {\n return Number(await (await this.bar()).getAttribute('aria-valuemax'));\n }\n\n /** The bands, in order, with the share of the bar each takes. */\n async getSlices(): Promise<WrMeterGroupSlice[]> {\n const slices = await this.locatorForAll('.wr-meter-group__slice')();\n\n return Promise.all(\n slices.map(async slice => ({\n label: await slice.getAttribute('title'),\n percent: Number.parseFloat(await slice.getCssValue('width')),\n }))\n );\n }\n\n /** Whether the legend is rendered (`showLegend`, and at least one band). */\n async hasLegend(): Promise<boolean> {\n return (await this.locatorForOptional('.wr-meter-group__legend')()) !== null;\n }\n\n /** The legend's labels, in order. */\n async getLegendLabels(): Promise<string[]> {\n const labels = await this.locatorForAll('.wr-meter-group__legend-label')();\n return Promise.all(labels.map(label => label.text()));\n }\n\n /** The legend's values, in order — empty when `showValues` is off. */\n async getLegendValues(): Promise<string[]> {\n const values = await this.locatorForAll('.wr-meter-group__legend-value')();\n return Promise.all(values.map(value => value.text()));\n }\n}\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public-api';\n"],"names":[],"mappings":";;AAAA;;;;;AAKG;AAMH;;;;;;;;;;AAUG;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,WAAW,EAAE,OAAO,CAAC,SAAS,EAAE,CAAC,OAAO,EAAE,KAAK,KACxD,gBAAgB,CAAC,aAAa,CAAC,OAAO,CAAC,iBAAiB,EAAE,EAAE,KAAK,CAAC;AAEnE,aAAA,SAAS,CAAC,YAAY,EAAE,OAAO,CAAC,UAAU,EAAE,OAAO,OAAO,EAAE,KAAK,KAAI;YACpE,KAAK,MAAM,OAAO,IAAI,MAAM,OAAO,CAAC,eAAe,EAAE,EAAE;gBACrD,IAAI,MAAM,gBAAgB,CAAC,aAAa,CAAC,OAAO,EAAE,KAAK,CAAC;AAAE,oBAAA,OAAO,IAAI;YACvE;AACA,YAAA,OAAO,KAAK;AACd,QAAA,CAAC,CAAC;IACN;AAEiB,IAAA,GAAG,GAAG,IAAI,CAAC,UAAU,CAAC,sBAAsB,CAAC;;AAG9D,IAAA,MAAM,OAAO,GAAA;AACX,QAAA,OAAO,CAAC,MAAM,IAAI,CAAC,GAAG,EAAE,EAAE,YAAY,CAAC,MAAM,CAAC;IAChD;;AAGA,IAAA,MAAM,iBAAiB,GAAA;AACrB,QAAA,OAAO,CAAC,MAAM,IAAI,CAAC,GAAG,EAAE,EAAE,YAAY,CAAC,YAAY,CAAC;IACtD;;AAGA,IAAA,MAAM,QAAQ,GAAA;AACZ,QAAA,OAAO,MAAM,CAAC,MAAM,CAAC,MAAM,IAAI,CAAC,GAAG,EAAE,EAAE,YAAY,CAAC,eAAe,CAAC,CAAC;IACvE;;AAGA,IAAA,MAAM,MAAM,GAAA;AACV,QAAA,OAAO,MAAM,CAAC,MAAM,CAAC,MAAM,IAAI,CAAC,GAAG,EAAE,EAAE,YAAY,CAAC,eAAe,CAAC,CAAC;IACvE;;AAGA,IAAA,MAAM,SAAS,GAAA;QACb,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,aAAa,CAAC,wBAAwB,CAAC,EAAE;AAEnE,QAAA,OAAO,OAAO,CAAC,GAAG,CAChB,MAAM,CAAC,GAAG,CAAC,OAAM,KAAK,MAAK;AACzB,YAAA,KAAK,EAAE,MAAM,KAAK,CAAC,YAAY,CAAC,OAAO,CAAC;AACxC,YAAA,OAAO,EAAE,MAAM,CAAC,UAAU,CAAC,MAAM,KAAK,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC;SAC7D,CAAC,CAAC,CACJ;IACH;;AAGA,IAAA,MAAM,SAAS,GAAA;AACb,QAAA,OAAO,CAAC,MAAM,IAAI,CAAC,kBAAkB,CAAC,yBAAyB,CAAC,EAAE,MAAM,IAAI;IAC9E;;AAGA,IAAA,MAAM,eAAe,GAAA;QACnB,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,aAAa,CAAC,+BAA+B,CAAC,EAAE;AAC1E,QAAA,OAAO,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,KAAK,IAAI,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC;IACvD;;AAGA,IAAA,MAAM,eAAe,GAAA;QACnB,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,aAAa,CAAC,+BAA+B,CAAC,EAAE;AAC1E,QAAA,OAAO,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,KAAK,IAAI,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC;IACvD;;;ACxFF;;AAEG;;;;"}
@@ -138,6 +138,7 @@ function containsAcrossShadow(parent, child) {
138
138
  *
139
139
  * Registration is per subscription: the document listeners go up with the first
140
140
  * watcher and come down with the last, so an app with no open overlay has none.
141
+ * That sentence was aspirational until v11 — see {@link outsidePointerEvents}.
141
142
  */
142
143
  class WrOutsideClick {
143
144
  doc = inject(DOCUMENT);
@@ -148,7 +149,24 @@ class WrOutsideClick {
148
149
  pressOrigin = null;
149
150
  /**
150
151
  * Emits every `click` / `auxclick` / `contextmenu` that lands outside the
151
- * given overlay's pane, for as long as the returned observable is subscribed.
152
+ * given overlay's pane, for as long as the overlay is attached.
153
+ *
154
+ * **It completes when the overlay detaches, and that is load-bearing.** Every
155
+ * one of the ten call sites subscribes inside its own `openOverlay()` and
156
+ * pipes `takeUntilDestroyed(destroyRef)` — a DIRECTIVE lifetime, not an
157
+ * overlay one — and none of them unsubscribes on close, because the natural
158
+ * reading of that idiom is "this ends with the component". Without the
159
+ * completion below the teardown never ran: one watcher accumulated per OPEN
160
+ * for the life of the page, each pinning a disposed `OverlayRef` (and through
161
+ * it a detached pane, its portal outlet and their injectors), the four
162
+ * capture-phase document listeners could never come down because
163
+ * `watchers.length` never reached zero, and every click on the page walked a
164
+ * list that only grew. A `mode="tooltip"` popover made it unbounded — one
165
+ * permanent watcher per hover.
166
+ *
167
+ * Completing here rather than fixing ten call sites is deliberate: the next
168
+ * component to open an overlay would have made the same reasonable mistake.
169
+ * It is also what CDK's own `OverlayRef.outsidePointerEvents()` does.
152
170
  */
153
171
  outsidePointerEvents(overlayRef) {
154
172
  return new Observable(subscriber => {
@@ -158,7 +176,16 @@ class WrOutsideClick {
158
176
  this.watchers.push(watcher);
159
177
  if (this.watchers.length === 1)
160
178
  this.listen();
179
+ // BOTH handlers, and the second one is not defensive padding. `dispose()`
180
+ // emits a detachment and then completes the subject, so a live overlay
181
+ // arrives through `next` — but a ref that was already disposed before
182
+ // anyone subscribed has nothing left to emit and arrives through
183
+ // `complete` alone. Listening for `next` only left exactly the watcher
184
+ // this whole change exists to remove.
185
+ const ended = () => subscriber.complete();
186
+ const detached = overlayRef.detachments().subscribe({ next: ended, complete: ended });
161
187
  return () => {
188
+ detached.unsubscribe();
162
189
  const index = this.watchers.indexOf(watcher);
163
190
  if (index > -1)
164
191
  this.watchers.splice(index, 1);