srcdev-nuxt-components 9.6.19 → 9.6.20

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 (135) hide show
  1. package/.claude/rules/consumer/ids-from-useid.md +3 -2
  2. package/.claude/skills/component-compliance-checklist.md +5 -0
  3. package/.claude/skills/components/auto-grid.md +3 -1
  4. package/.claude/skills/components/capture-qr-code.md +9 -5
  5. package/.claude/skills/components/carousel-flip.md +41 -30
  6. package/.claude/skills/components/column-flow-grid.md +2 -0
  7. package/.claude/skills/components/dashboard-quad-grid.md +7 -2
  8. package/.claude/skills/components/dashboard-stats-grid.md +4 -0
  9. package/.claude/skills/components/decode-qr-code.md +7 -2
  10. package/.claude/skills/components/deep-expanding-menu-classic.md +31 -30
  11. package/.claude/skills/components/deep-expanding-menu.md +22 -21
  12. package/.claude/skills/components/grid-stack.md +2 -0
  13. package/.claude/skills/components/layout-grid-by-cols.md +2 -0
  14. package/.claude/skills/components/masonry-grid.md +9 -1
  15. package/.claude/skills/components/page-row.md +3 -0
  16. package/.claude/skills/components/samaritan-prompt.md +24 -8
  17. package/.claude/skills/components/site-header.md +16 -2
  18. package/.claude/skills/index.md +12 -12
  19. package/.claude/skills/storybook-add-story.md +25 -0
  20. package/.claude/skills/testing-add-unit-test.md +26 -0
  21. package/.vscode/srcdev-component-capture-decode-qr-code.code-snippets +11 -2
  22. package/.vscode/srcdev-component-carousel-flip.code-snippets +22 -4
  23. package/.vscode/srcdev-component-deep-expanding-menu-classic.code-snippets +32 -2
  24. package/.vscode/srcdev-component-deep-expanding-menu.code-snippets +31 -1
  25. package/.vscode/srcdev-component-samaritan-prompt.code-snippets +5 -4
  26. package/.vscode/srcdev-component-site-header.code-snippets +13 -1
  27. package/app/components/01.atoms/content-wrappers/layout-grid/dashboard-quad-grid/CONSUMER-STYLING.md +12 -2
  28. package/app/components/01.atoms/content-wrappers/layout-grid/dashboard-quad-grid/DashboardQuadGrid.vue +19 -17
  29. package/app/components/01.atoms/content-wrappers/layout-grid/dashboard-quad-grid/stories/DashboardQuadGrid.stories.ts +42 -0
  30. package/app/components/01.atoms/content-wrappers/layout-grid/dashboard-quad-grid/tests/DashboardQuadGrid.spec.ts +31 -44
  31. package/app/components/01.atoms/content-wrappers/layout-grid/dashboard-stats-grid/CONSUMER-STYLING.md +13 -1
  32. package/app/components/01.atoms/content-wrappers/layout-grid/dashboard-stats-grid/DashboardStatsGrid.vue +40 -31
  33. package/app/components/01.atoms/content-wrappers/layout-grid/dashboard-stats-grid/stories/DashboardStatsGrid.stories.ts +47 -0
  34. package/app/components/01.atoms/content-wrappers/layout-grid/dashboard-stats-grid/tests/DashboardStatsGrid.spec.ts +51 -53
  35. package/app/components/01.atoms/content-wrappers/layout-grid/layout-grid-by-cols/CONSUMER-STYLING.md +3 -0
  36. package/app/components/01.atoms/content-wrappers/layout-grid/layout-grid-by-cols/LayoutGridByCols.vue +4 -0
  37. package/app/components/01.atoms/content-wrappers/layout-grid/layout-grid-by-cols/stories/LayoutGridByCols.stories.ts +47 -2
  38. package/app/components/01.atoms/content-wrappers/layout-grid/layout-grid-by-cols/tests/LayoutGridByCols.spec.ts +36 -23
  39. package/app/components/01.atoms/grids/auto-grid/AutoGrid.vue +7 -3
  40. package/app/components/01.atoms/grids/auto-grid/CONSUMER-STYLING.md +1 -1
  41. package/app/components/01.atoms/grids/auto-grid/stories/AutoGrid.stories.ts +46 -1
  42. package/app/components/01.atoms/grids/auto-grid/tests/AutoGrid.spec.ts +40 -27
  43. package/app/components/01.atoms/grids/auto-grid/tests/__snapshots__/AutoGrid.spec.ts.snap +1 -1
  44. package/app/components/01.atoms/grids/column-flow-grid/CONSUMER-STYLING.md +9 -0
  45. package/app/components/01.atoms/grids/column-flow-grid/ColumnFlowGrid.vue +3 -2
  46. package/app/components/01.atoms/grids/column-flow-grid/stories/ColumnFlowGrid.stories.ts +46 -0
  47. package/app/components/01.atoms/grids/column-flow-grid/tests/ColumnFlowGrid.spec.ts +24 -11
  48. package/app/components/01.atoms/grids/grid-stack/CONSUMER-STYLING.md +3 -0
  49. package/app/components/01.atoms/grids/grid-stack/GridStack.vue +2 -0
  50. package/app/components/01.atoms/grids/grid-stack/stories/GridStack.stories.ts +41 -0
  51. package/app/components/01.atoms/grids/grid-stack/tests/GridStack.spec.ts +26 -13
  52. package/app/components/01.atoms/grids/masonry-grid/CONSUMER-STYLING.md +15 -0
  53. package/app/components/01.atoms/grids/masonry-grid/MasonryGrid.vue +51 -31
  54. package/app/components/01.atoms/grids/masonry-grid/stories/MasonryGrid.stories.ts +48 -0
  55. package/app/components/01.atoms/grids/masonry-grid/tests/MasonryGrid.spec.ts +84 -29
  56. package/app/components/01.atoms/page-row/CONSUMER-STYLING.md +6 -1
  57. package/app/components/01.atoms/page-row/PageRow.vue +16 -15
  58. package/app/components/01.atoms/page-row/stories/PageRow.stories.ts +23 -0
  59. package/app/components/01.atoms/page-row/tests/PageRow.spec.ts +6 -5
  60. package/app/components/02.molecules/navigation/deep-expanding-menu/CONSUMER-STYLING.md +63 -44
  61. package/app/components/02.molecules/navigation/deep-expanding-menu/DeepExpandingMenu.vue +74 -37
  62. package/app/components/02.molecules/navigation/deep-expanding-menu/stories/DeepExpandingMenu.stories.ts +139 -19
  63. package/app/components/02.molecules/navigation/deep-expanding-menu/tests/DeepExpandingMenu.spec.ts +90 -27
  64. package/app/components/02.molecules/navigation/deep-expanding-menu/tests/__snapshots__/DeepExpandingMenu.spec.ts.snap +8 -8
  65. package/app/components/02.molecules/navigation/deep-expanding-menu-classic/CONSUMER-STYLING.md +58 -26
  66. package/app/components/02.molecules/navigation/deep-expanding-menu-classic/DeepExpandingMenuClassic.vue +113 -57
  67. package/app/components/02.molecules/navigation/deep-expanding-menu-classic/stories/DeepExpandingMenuClassic.stories.ts +139 -19
  68. package/app/components/02.molecules/navigation/deep-expanding-menu-classic/tests/DeepExpandingMenuClassic.spec.ts +140 -22
  69. package/app/components/02.molecules/navigation/deep-expanding-menu-classic/tests/__snapshots__/DeepExpandingMenuClassic.spec.ts.snap +9 -9
  70. package/app/components/02.molecules/qr-code/CONSUMER-STYLING.md +22 -7
  71. package/app/components/02.molecules/qr-code/CaptureQrCode.vue +63 -71
  72. package/app/components/02.molecules/qr-code/DecodeQrCode.vue +47 -10
  73. package/app/components/02.molecules/qr-code/stories/CaptureQrCode.stories.ts +114 -26
  74. package/app/components/02.molecules/qr-code/stories/DecodeQrCode.stories.ts +115 -24
  75. package/app/components/02.molecules/qr-code/tests/CaptureQrCode.spec.ts +124 -51
  76. package/app/components/02.molecules/qr-code/tests/DecodeQrCode.spec.ts +74 -25
  77. package/app/components/02.molecules/samaritan-prompt/CONSUMER-STYLING.md +22 -10
  78. package/app/components/02.molecules/samaritan-prompt/SamaritanPrompt.vue +30 -12
  79. package/app/components/02.molecules/samaritan-prompt/SamaritanPromptMixed.vue +34 -10
  80. package/app/components/02.molecules/samaritan-prompt/stories/SamaritanPrompt.stories.ts +55 -4
  81. package/app/components/02.molecules/samaritan-prompt/stories/SamaritanPromptMixed.stories.ts +67 -7
  82. package/app/components/02.molecules/samaritan-prompt/tests/SamaritanPrompt.spec.ts +113 -29
  83. package/app/components/02.molecules/samaritan-prompt/tests/SamaritanPromptMixed.spec.ts +92 -45
  84. package/app/components/03.organisms/image-galleries/carousel-flip/CONSUMER-STYLING.md +82 -29
  85. package/app/components/03.organisms/image-galleries/carousel-flip/CarouselFlip.vue +176 -113
  86. package/app/components/03.organisms/image-galleries/carousel-flip/stories/CarouselFlip.stories.ts +153 -23
  87. package/app/components/03.organisms/image-galleries/carousel-flip/tests/CarouselFlip.spec.ts +119 -63
  88. package/app/components/03.organisms/services/service-detail/tests/ServiceDetail.spec.ts +8 -8
  89. package/app/components/03.organisms/services/service-detail/tests/__snapshots__/ServiceDetail.spec.ts.snap +4 -4
  90. package/app/components/03.organisms/site-header/CONSUMER-STYLING.md +41 -23
  91. package/app/components/03.organisms/site-header/SiteHeader.vue +16 -21
  92. package/app/components/03.organisms/site-header/stories/SiteHeader.stories.ts +138 -0
  93. package/app/components/03.organisms/site-header/tests/SiteHeader.spec.ts +53 -26
  94. package/app/components/03.organisms/site-header/tests/__snapshots__/SiteHeader.spec.ts.snap +2 -2
  95. package/app/types/components/qr-code.d.ts +12 -0
  96. package/app/utils/graphemes.ts +8 -0
  97. package/package.json +1 -1
  98. package/app/components/01.atoms/animations/entry/.nuxt/app.config.mjs +0 -21
  99. package/app/components/01.atoms/animations/entry/.nuxt/components.d.ts +0 -66
  100. package/app/components/01.atoms/animations/entry/.nuxt/imports.d.ts +0 -30
  101. package/app/components/01.atoms/animations/entry/.nuxt/manifest/meta/test.json +0 -1
  102. package/app/components/01.atoms/animations/entry/.nuxt/types/app.config.d.ts +0 -35
  103. package/app/components/01.atoms/animations/entry/.nuxt/types/build.d.ts +0 -23
  104. package/app/components/01.atoms/animations/entry/.nuxt/types/builder-env.d.ts +0 -1
  105. package/app/components/01.atoms/animations/entry/.nuxt/types/components.d.ts +0 -71
  106. package/app/components/01.atoms/animations/entry/.nuxt/types/imports.d.ts +0 -386
  107. package/app/components/01.atoms/animations/entry/.nuxt/types/middleware.d.ts +0 -11
  108. package/app/components/01.atoms/animations/entry/.nuxt/types/modules.d.ts +0 -79
  109. package/app/components/01.atoms/animations/entry/.nuxt/types/nitro-layouts.d.ts +0 -17
  110. package/app/components/01.atoms/animations/entry/.nuxt/types/nitro-nuxt.d.ts +0 -64
  111. package/app/components/01.atoms/animations/entry/.nuxt/types/plugins.d.ts +0 -29
  112. package/app/components/01.atoms/animations/entry/.nuxt/types/runtime-config.d.ts +0 -32
  113. package/app/components/01.atoms/animations/entry/.nuxt/types/shared-imports.d.ts +0 -10
  114. package/app/components/01.atoms/animations/entry/.nuxt/types/vue-shim.d.ts +0 -0
  115. package/app/components/01.atoms/animations/entry/.nuxtrc +0 -1
  116. package/app/components/01.atoms/animations/entry/node_modules/.cache/nuxt/chrome-workspace.json +0 -1
  117. package/app/components/01.atoms/animations/entry/node_modules/.cache/vite/client/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +0 -1
  118. package/app/components/03.organisms/responsive-header/tests/.nuxt/app.config.mjs +0 -21
  119. package/app/components/03.organisms/responsive-header/tests/.nuxt/components.d.ts +0 -66
  120. package/app/components/03.organisms/responsive-header/tests/.nuxt/imports.d.ts +0 -30
  121. package/app/components/03.organisms/responsive-header/tests/.nuxt/manifest/meta/test.json +0 -1
  122. package/app/components/03.organisms/responsive-header/tests/.nuxt/types/app.config.d.ts +0 -35
  123. package/app/components/03.organisms/responsive-header/tests/.nuxt/types/build.d.ts +0 -23
  124. package/app/components/03.organisms/responsive-header/tests/.nuxt/types/builder-env.d.ts +0 -1
  125. package/app/components/03.organisms/responsive-header/tests/.nuxt/types/components.d.ts +0 -71
  126. package/app/components/03.organisms/responsive-header/tests/.nuxt/types/imports.d.ts +0 -386
  127. package/app/components/03.organisms/responsive-header/tests/.nuxt/types/middleware.d.ts +0 -11
  128. package/app/components/03.organisms/responsive-header/tests/.nuxt/types/modules.d.ts +0 -79
  129. package/app/components/03.organisms/responsive-header/tests/.nuxt/types/nitro-layouts.d.ts +0 -17
  130. package/app/components/03.organisms/responsive-header/tests/.nuxt/types/nitro-nuxt.d.ts +0 -64
  131. package/app/components/03.organisms/responsive-header/tests/.nuxt/types/plugins.d.ts +0 -29
  132. package/app/components/03.organisms/responsive-header/tests/.nuxt/types/runtime-config.d.ts +0 -32
  133. package/app/components/03.organisms/responsive-header/tests/.nuxt/types/shared-imports.d.ts +0 -10
  134. package/app/components/03.organisms/responsive-header/tests/.nuxt/types/vue-shim.d.ts +0 -0
  135. package/app/components/03.organisms/responsive-header/tests/node_modules/.cache/nuxt/chrome-workspace.json +0 -1
@@ -1,8 +1,21 @@
1
- import { describe, it, expect } from "vitest";
1
+ import { describe, it, expect, afterEach, vi } from "vitest";
2
2
  import { mountSuspended } from "@nuxt/test-utils/runtime";
3
3
  import { nextTick } from "vue";
4
4
  import SamaritanPrompt from "../SamaritanPrompt.vue";
5
5
 
6
+ const mounted: Awaited<ReturnType<typeof mountSuspended>>[] = [];
7
+
8
+ const mountPrompt = async (options?: Parameters<typeof mountSuspended<typeof SamaritanPrompt>>[1]) => {
9
+ const wrapper = await mountSuspended(SamaritanPrompt, options);
10
+ mounted.push(wrapper);
11
+ return wrapper;
12
+ };
13
+
14
+ afterEach(() => {
15
+ mounted.splice(0).forEach((wrapper) => wrapper.unmount());
16
+ vi.restoreAllMocks();
17
+ });
18
+
6
19
  const messages: string[] = ["Hello, world.", "Surveillance active.", "Threat detected."];
7
20
  const [firstMessage, secondMessage] = messages as [string, string, string];
8
21
 
@@ -10,35 +23,35 @@ const [firstMessage, secondMessage] = messages as [string, string, string];
10
23
 
11
24
  describe("SamaritanPrompt — typewriter", () => {
12
25
  it("mounts without error", async () => {
13
- const wrapper = await mountSuspended(SamaritanPrompt, {
26
+ const wrapper = await mountPrompt({
14
27
  props: { messages },
15
28
  });
16
29
  expect(wrapper.vm).toBeTruthy();
17
30
  });
18
31
 
19
32
  it("renders the triangle cursor", async () => {
20
- const wrapper = await mountSuspended(SamaritanPrompt, {
33
+ const wrapper = await mountPrompt({
21
34
  props: { messages },
22
35
  });
23
36
  expect(wrapper.find(".samaritan-prompt__cursor").text()).toBe("▲");
24
37
  });
25
38
 
26
39
  it("renders the underline element", async () => {
27
- const wrapper = await mountSuspended(SamaritanPrompt, {
40
+ const wrapper = await mountPrompt({
28
41
  props: { messages },
29
42
  });
30
43
  expect(wrapper.find(".samaritan-prompt__underline").exists()).toBe(true);
31
44
  });
32
45
 
33
46
  it("starts with empty display text", async () => {
34
- const wrapper = await mountSuspended(SamaritanPrompt, {
47
+ const wrapper = await mountPrompt({
35
48
  props: { messages },
36
49
  });
37
50
  expect(wrapper.find(".samaritan-prompt__text").text()).toBe("");
38
51
  });
39
52
 
40
53
  it("types one character per typeSpeed tick", async () => {
41
- const wrapper = await mountSuspended(SamaritanPrompt, {
54
+ const wrapper = await mountPrompt({
42
55
  props: { messages, typeSpeed: 100 },
43
56
  });
44
57
 
@@ -52,7 +65,7 @@ describe("SamaritanPrompt — typewriter", () => {
52
65
  });
53
66
 
54
67
  it("fully types the first message", async () => {
55
- const wrapper = await mountSuspended(SamaritanPrompt, {
68
+ const wrapper = await mountPrompt({
56
69
  props: { messages, typeSpeed: 10 },
57
70
  });
58
71
 
@@ -62,7 +75,7 @@ describe("SamaritanPrompt — typewriter", () => {
62
75
  });
63
76
 
64
77
  it("deletes text after hold duration", async () => {
65
- const wrapper = await mountSuspended(SamaritanPrompt, {
78
+ const wrapper = await mountPrompt({
66
79
  props: { messages, typeSpeed: 10, holdDuration: 500, deleteSpeed: 10 },
67
80
  });
68
81
 
@@ -77,7 +90,7 @@ describe("SamaritanPrompt — typewriter", () => {
77
90
  });
78
91
 
79
92
  it("advances to the next message after deleting", async () => {
80
- const wrapper = await mountSuspended(SamaritanPrompt, {
93
+ const wrapper = await mountPrompt({
81
94
  props: { messages, typeSpeed: 10, holdDuration: 100, deleteSpeed: 10, pauseDuration: 100 },
82
95
  });
83
96
 
@@ -97,7 +110,7 @@ describe("SamaritanPrompt — typewriter", () => {
97
110
 
98
111
  it("cycles back to first message after last message", async () => {
99
112
  const singleMessage = "Only one.";
100
- const wrapper = await mountSuspended(SamaritanPrompt, {
113
+ const wrapper = await mountPrompt({
101
114
  props: { messages: [singleMessage], typeSpeed: 10, holdDuration: 100, deleteSpeed: 10, pauseDuration: 100 },
102
115
  });
103
116
 
@@ -109,21 +122,21 @@ describe("SamaritanPrompt — typewriter", () => {
109
122
  });
110
123
 
111
124
  it("applies styleClassPassthrough", async () => {
112
- const wrapper = await mountSuspended(SamaritanPrompt, {
125
+ const wrapper = await mountPrompt({
113
126
  props: { messages, styleClassPassthrough: "custom-class" },
114
127
  });
115
128
  expect(wrapper.find(".samaritan-prompt").classes()).toContain("custom-class");
116
129
  });
117
130
 
118
131
  it("cursor is visible before typing starts", async () => {
119
- const wrapper = await mountSuspended(SamaritanPrompt, {
132
+ const wrapper = await mountPrompt({
120
133
  props: { messages, typeSpeed: 100 },
121
134
  });
122
135
  expect(wrapper.find(".samaritan-prompt__cursor").attributes("style")).toContain("opacity: 1");
123
136
  });
124
137
 
125
138
  it("cursor hides when typewriter starts typing first character", async () => {
126
- const wrapper = await mountSuspended(SamaritanPrompt, {
139
+ const wrapper = await mountPrompt({
127
140
  props: { messages, typeSpeed: 100 },
128
141
  });
129
142
 
@@ -134,7 +147,7 @@ describe("SamaritanPrompt — typewriter", () => {
134
147
  });
135
148
 
136
149
  it("cursor returns when typewriter enters pause phase", async () => {
137
- const wrapper = await mountSuspended(SamaritanPrompt, {
150
+ const wrapper = await mountPrompt({
138
151
  props: { messages, typeSpeed: 10, holdDuration: 100, deleteSpeed: 10, pauseDuration: 100 },
139
152
  });
140
153
 
@@ -150,7 +163,7 @@ describe("SamaritanPrompt — typewriter", () => {
150
163
  });
151
164
 
152
165
  it("cursor is always visible when hideCursorInCycle is false", async () => {
153
- const wrapper = await mountSuspended(SamaritanPrompt, {
166
+ const wrapper = await mountPrompt({
154
167
  props: { messages, typeSpeed: 100, hideCursorInCycle: false },
155
168
  });
156
169
 
@@ -165,14 +178,14 @@ describe("SamaritanPrompt — typewriter", () => {
165
178
 
166
179
  describe("SamaritanPrompt — word-pulse", () => {
167
180
  it("mounts without error", async () => {
168
- const wrapper = await mountSuspended(SamaritanPrompt, {
181
+ const wrapper = await mountPrompt({
169
182
  props: { messages, effect: "word-pulse" },
170
183
  });
171
184
  expect(wrapper.vm).toBeTruthy();
172
185
  });
173
186
 
174
187
  it("starts with empty display text and content visible", async () => {
175
- const wrapper = await mountSuspended(SamaritanPrompt, {
188
+ const wrapper = await mountPrompt({
176
189
  props: { messages, effect: "word-pulse", pauseDuration: 500 },
177
190
  });
178
191
  await nextTick();
@@ -182,7 +195,7 @@ describe("SamaritanPrompt — word-pulse", () => {
182
195
  });
183
196
 
184
197
  it("makes content invisible when cycle starts after pause", async () => {
185
- const wrapper = await mountSuspended(SamaritanPrompt, {
198
+ const wrapper = await mountPrompt({
186
199
  props: { messages, effect: "word-pulse", pauseDuration: 500, fadeDuration: 400, wordDuration: 1200 },
187
200
  });
188
201
  await nextTick();
@@ -198,7 +211,7 @@ describe("SamaritanPrompt — word-pulse", () => {
198
211
  });
199
212
 
200
213
  it("makes content visible after expansion delay", async () => {
201
- const wrapper = await mountSuspended(SamaritanPrompt, {
214
+ const wrapper = await mountPrompt({
202
215
  props: { messages, effect: "word-pulse", pauseDuration: 500, fadeDuration: 400, wordDuration: 1200 },
203
216
  });
204
217
  await nextTick();
@@ -222,7 +235,7 @@ describe("SamaritanPrompt — word-pulse", () => {
222
235
  });
223
236
 
224
237
  it("fades content back out after word duration", async () => {
225
- const wrapper = await mountSuspended(SamaritanPrompt, {
238
+ const wrapper = await mountPrompt({
226
239
  props: { messages, effect: "word-pulse", pauseDuration: 500, fadeDuration: 400, wordDuration: 1200 },
227
240
  });
228
241
  await nextTick();
@@ -250,7 +263,7 @@ describe("SamaritanPrompt — word-pulse", () => {
250
263
  });
251
264
 
252
265
  it("advances to the next message after fade out", async () => {
253
- const wrapper = await mountSuspended(SamaritanPrompt, {
266
+ const wrapper = await mountPrompt({
254
267
  props: { messages, effect: "word-pulse", pauseDuration: 500, fadeDuration: 400, wordDuration: 1200 },
255
268
  });
256
269
  await nextTick();
@@ -284,7 +297,7 @@ describe("SamaritanPrompt — word-pulse", () => {
284
297
 
285
298
  it("collapses displayText and restores opacity at end of cycle", async () => {
286
299
  const singleMsg = "Only.";
287
- const wrapper = await mountSuspended(SamaritanPrompt, {
300
+ const wrapper = await mountPrompt({
288
301
  props: { messages: [singleMsg], effect: "word-pulse", pauseDuration: 300, fadeDuration: 200, wordDuration: 500 },
289
302
  });
290
303
  await nextTick();
@@ -318,7 +331,7 @@ describe("SamaritanPrompt — word-pulse", () => {
318
331
  });
319
332
 
320
333
  it("cursor hides when word-pulse cycle starts", async () => {
321
- const wrapper = await mountSuspended(SamaritanPrompt, {
334
+ const wrapper = await mountPrompt({
322
335
  props: { messages, effect: "word-pulse", pauseDuration: 500, fadeDuration: 400, wordDuration: 1200 },
323
336
  });
324
337
  await nextTick();
@@ -334,7 +347,7 @@ describe("SamaritanPrompt — word-pulse", () => {
334
347
 
335
348
  it("cursor returns after word-pulse cycle resets", async () => {
336
349
  const singleMsg = "Only.";
337
- const wrapper = await mountSuspended(SamaritanPrompt, {
350
+ const wrapper = await mountPrompt({
338
351
  props: { messages: [singleMsg], effect: "word-pulse", pauseDuration: 300, fadeDuration: 200, wordDuration: 500 },
339
352
  });
340
353
  await nextTick();
@@ -371,7 +384,7 @@ describe("SamaritanPrompt — word-pulse", () => {
371
384
 
372
385
  describe("SamaritanPrompt — accessibility and pause", () => {
373
386
  it("hides the animated content from assistive tech and exposes a polite live region", async () => {
374
- const wrapper = await mountSuspended(SamaritanPrompt, { props: { messages } });
387
+ const wrapper = await mountPrompt({ props: { messages } });
375
388
  expect(wrapper.find(".samaritan-prompt__content").attributes("aria-hidden")).toBe("true");
376
389
  const live = wrapper.find(".samaritan-prompt__sr-text");
377
390
  expect(live.attributes("aria-live")).toBe("polite");
@@ -380,7 +393,7 @@ describe("SamaritanPrompt — accessibility and pause", () => {
380
393
  });
381
394
 
382
395
  it("announces the full message once typed, and clears it when deleting starts", async () => {
383
- const wrapper = await mountSuspended(SamaritanPrompt, {
396
+ const wrapper = await mountPrompt({
384
397
  props: { messages, typeSpeed: 10, holdDuration: 500, deleteSpeed: 10 },
385
398
  });
386
399
 
@@ -394,7 +407,7 @@ describe("SamaritanPrompt — accessibility and pause", () => {
394
407
  });
395
408
 
396
409
  it("stops typing while hovered and resumes on leave", async () => {
397
- const wrapper = await mountSuspended(SamaritanPrompt, { props: { messages, typeSpeed: 100 } });
410
+ const wrapper = await mountPrompt({ props: { messages, typeSpeed: 100 } });
398
411
  const root = wrapper.find(".samaritan-prompt");
399
412
 
400
413
  vi.advanceTimersByTime(200);
@@ -415,7 +428,7 @@ describe("SamaritanPrompt — accessibility and pause", () => {
415
428
  });
416
429
 
417
430
  it("renders a custom cursor from the cursor slot", async () => {
418
- const wrapper = await mountSuspended(SamaritanPrompt, {
431
+ const wrapper = await mountPrompt({
419
432
  props: { messages },
420
433
  slots: { cursor: () => "_" },
421
434
  });
@@ -423,7 +436,7 @@ describe("SamaritanPrompt — accessibility and pause", () => {
423
436
  });
424
437
 
425
438
  it("updates passthrough classes when the prop changes", async () => {
426
- const wrapper = await mountSuspended(SamaritanPrompt, {
439
+ const wrapper = await mountPrompt({
427
440
  props: { messages, styleClassPassthrough: "first-class" },
428
441
  });
429
442
  await wrapper.setProps({ styleClassPassthrough: "second-class" });
@@ -432,3 +445,74 @@ describe("SamaritanPrompt — accessibility and pause", () => {
432
445
  expect(classes).not.toContain("first-class");
433
446
  });
434
447
  });
448
+
449
+ // ─── Worst-case data ──────────────────────────────────────────────────────────
450
+
451
+ const stubReducedMotion = () => {
452
+ const original = window.matchMedia;
453
+ window.matchMedia = ((query: string) => ({
454
+ matches: query.includes("prefers-reduced-motion: reduce"),
455
+ media: query,
456
+ onchange: null,
457
+ addEventListener: () => {},
458
+ removeEventListener: () => {},
459
+ addListener: () => {},
460
+ removeListener: () => {},
461
+ dispatchEvent: () => false,
462
+ })) as unknown as typeof window.matchMedia;
463
+ return () => {
464
+ window.matchMedia = original;
465
+ };
466
+ };
467
+
468
+ describe("SamaritanPrompt — worst-case data", () => {
469
+ const text = (wrapper: Awaited<ReturnType<typeof mountPrompt>>) => wrapper.find(".samaritan-prompt__text").text();
470
+
471
+ it("types an emoji with a skin-tone modifier as one character", async () => {
472
+ const wrapper = await mountPrompt({ props: { messages: ["👍🏽A"], typeSpeed: 100 } });
473
+ vi.advanceTimersByTime(100);
474
+ await nextTick();
475
+ expect(text(wrapper)).toBe("👍🏽");
476
+ });
477
+
478
+ it("moves past an empty message instead of stopping", async () => {
479
+ const wrapper = await mountPrompt({
480
+ props: { messages: ["", "B"], typeSpeed: 10, holdDuration: 10, deleteSpeed: 10, pauseDuration: 10 },
481
+ });
482
+ const seen = new Set<string>();
483
+ for (let i = 0; i < 12; i++) {
484
+ vi.advanceTimersByTime(10);
485
+ await nextTick();
486
+ seen.add(text(wrapper));
487
+ }
488
+ expect(seen.has("B")).toBe(true);
489
+ });
490
+
491
+ it("restarts when messages change by value, but not for an equal new array", async () => {
492
+ const wrapper = await mountPrompt({ props: { messages: ["AB"], typeSpeed: 100 } });
493
+ vi.advanceTimersByTime(100);
494
+ await nextTick();
495
+ expect(text(wrapper)).toBe("A");
496
+
497
+ await wrapper.setProps({ messages: ["AB"] });
498
+ expect(text(wrapper)).toBe("A");
499
+
500
+ await wrapper.setProps({ messages: ["XY"] });
501
+ expect(text(wrapper)).toBe("");
502
+ vi.advanceTimersByTime(100);
503
+ await nextTick();
504
+ expect(text(wrapper)).toBe("X");
505
+ });
506
+
507
+ it("shows the whole message at once under reduced motion", async () => {
508
+ const restore = stubReducedMotion();
509
+ try {
510
+ const wrapper = await mountPrompt({ props: { messages: ["HELLO"], typeSpeed: 100 } });
511
+ vi.advanceTimersByTime(100);
512
+ await nextTick();
513
+ expect(text(wrapper)).toBe("HELLO");
514
+ } finally {
515
+ restore();
516
+ }
517
+ });
518
+ });
@@ -1,9 +1,22 @@
1
- import { describe, it, expect, afterEach } from "vitest";
1
+ import { describe, it, expect, afterEach, vi } from "vitest";
2
2
  import { mountSuspended } from "@nuxt/test-utils/runtime";
3
3
  import { nextTick } from "vue";
4
4
  import SamaritanPromptMixed from "../SamaritanPromptMixed.vue";
5
5
  import type { SamaritanPromptMessageConfig as MessageConfig } from "~/types/components/samaritan-prompt";
6
6
 
7
+ const mounted: Awaited<ReturnType<typeof mountSuspended>>[] = [];
8
+
9
+ const mountMixed = async (options: Parameters<typeof mountSuspended<typeof SamaritanPromptMixed>>[1]) => {
10
+ const wrapper = await mountSuspended(SamaritanPromptMixed, options);
11
+ mounted.push(wrapper);
12
+ return wrapper;
13
+ };
14
+
15
+ afterEach(() => {
16
+ mounted.splice(0).forEach((wrapper) => wrapper.unmount());
17
+ vi.restoreAllMocks();
18
+ });
19
+
7
20
  // ─── Typewriter ───────────────────────────────────────────────────────────────
8
21
  //
9
22
  // runTypewriter sets the first char and hides the cursor synchronously on mount,
@@ -22,30 +35,26 @@ describe("SamaritanPromptMixed — typewriter", () => {
22
35
 
23
36
  let wrapper: Awaited<ReturnType<typeof mountSuspended>>;
24
37
 
25
- afterEach(() => {
26
- wrapper?.unmount();
27
- });
28
-
29
38
  it("mounts without error", async () => {
30
- wrapper = await mountSuspended(SamaritanPromptMixed, { props: { messageConfigs: [config] } });
39
+ wrapper = await mountMixed({ props: { messageConfigs: [config] } });
31
40
  expect(wrapper.vm).toBeTruthy();
32
41
  });
33
42
 
34
43
  it("renders cursor, underline, and stage", async () => {
35
- wrapper = await mountSuspended(SamaritanPromptMixed, { props: { messageConfigs: [config] } });
44
+ wrapper = await mountMixed({ props: { messageConfigs: [config] } });
36
45
  expect(wrapper.find(".samaritan-prompt__cursor").text()).toBe("▲");
37
46
  expect(wrapper.find(".samaritan-prompt__underline").exists()).toBe(true);
38
47
  expect(wrapper.find(".samaritan-prompt__stage").exists()).toBe(true);
39
48
  });
40
49
 
41
50
  it("types the first character immediately on mount", async () => {
42
- wrapper = await mountSuspended(SamaritanPromptMixed, { props: { messageConfigs: [config], introDelay: 0 } });
51
+ wrapper = await mountMixed({ props: { messageConfigs: [config], introDelay: 0 } });
43
52
  await nextTick();
44
53
  expect(wrapper.find(".samaritan-prompt__text").text()).toBe("H");
45
54
  });
46
55
 
47
56
  it("types one character per typeSpeed tick", async () => {
48
- wrapper = await mountSuspended(SamaritanPromptMixed, { props: { messageConfigs: [config], introDelay: 0 } });
57
+ wrapper = await mountMixed({ props: { messageConfigs: [config], introDelay: 0 } });
49
58
  await nextTick();
50
59
  expect(wrapper.find(".samaritan-prompt__text").text()).toBe("H");
51
60
 
@@ -56,13 +65,13 @@ describe("SamaritanPromptMixed — typewriter", () => {
56
65
  });
57
66
 
58
67
  it("hides cursor when typing starts", async () => {
59
- wrapper = await mountSuspended(SamaritanPromptMixed, { props: { messageConfigs: [config], introDelay: 0 } });
68
+ wrapper = await mountMixed({ props: { messageConfigs: [config], introDelay: 0 } });
60
69
  await nextTick();
61
70
  expect(wrapper.find(".samaritan-prompt__cursor").attributes("style")).toContain("opacity: 0");
62
71
  });
63
72
 
64
73
  it("cursor stays visible when hideCursorInCycle is false", async () => {
65
- wrapper = await mountSuspended(SamaritanPromptMixed, {
74
+ wrapper = await mountMixed({
66
75
  props: { messageConfigs: [{ ...config, hideCursorInCycle: false }] },
67
76
  });
68
77
  await nextTick();
@@ -78,7 +87,7 @@ describe("SamaritanPromptMixed — typewriter", () => {
78
87
  holdDuration: 200,
79
88
  pauseDuration: 100,
80
89
  };
81
- wrapper = await mountSuspended(SamaritanPromptMixed, { props: { messageConfigs: [short] } });
90
+ wrapper = await mountMixed({ props: { messageConfigs: [short] } });
82
91
  await nextTick(); // "H" — first char immediate
83
92
 
84
93
  vi.advanceTimersByTime(100); // fire i=1 timer → "HI" typed, await wait(100) for i=2
@@ -106,7 +115,7 @@ describe("SamaritanPromptMixed — typewriter", () => {
106
115
  holdDuration: 200,
107
116
  pauseDuration: 100,
108
117
  };
109
- wrapper = await mountSuspended(SamaritanPromptMixed, { props: { messageConfigs: [short] } });
118
+ wrapper = await mountMixed({ props: { messageConfigs: [short] } });
110
119
  await nextTick(); // "H" — cursor hidden
111
120
 
112
121
  vi.advanceTimersByTime(100); // exit type loop (single char)
@@ -125,7 +134,7 @@ describe("SamaritanPromptMixed — typewriter", () => {
125
134
  });
126
135
 
127
136
  it("applies styleClassPassthrough", async () => {
128
- wrapper = await mountSuspended(SamaritanPromptMixed, {
137
+ wrapper = await mountMixed({
129
138
  props: { messageConfigs: [config], styleClassPassthrough: "custom-class" },
130
139
  });
131
140
  expect(wrapper.find(".samaritan-prompt").classes()).toContain("custom-class");
@@ -150,24 +159,20 @@ describe("SamaritanPromptMixed — word-pulse", () => {
150
159
 
151
160
  let wrapper: Awaited<ReturnType<typeof mountSuspended>>;
152
161
 
153
- afterEach(() => {
154
- wrapper?.unmount();
155
- });
156
-
157
162
  it("mounts without error", async () => {
158
- wrapper = await mountSuspended(SamaritanPromptMixed, { props: { messageConfigs: [config] } });
163
+ wrapper = await mountMixed({ props: { messageConfigs: [config] } });
159
164
  expect(wrapper.vm).toBeTruthy();
160
165
  });
161
166
 
162
167
  it("starts with empty display text", async () => {
163
168
  // mountSuspended flushes pending promises, resolving the component's initial await nextTick()
164
169
  // which immediately triggers the fade-out. Text is still empty until fadeDuration elapses.
165
- wrapper = await mountSuspended(SamaritanPromptMixed, { props: { messageConfigs: [config] } });
170
+ wrapper = await mountMixed({ props: { messageConfigs: [config] } });
166
171
  expect(wrapper.find(".samaritan-prompt__text").text()).toBe("");
167
172
  });
168
173
 
169
174
  it("fades content out when cycle starts", async () => {
170
- wrapper = await mountSuspended(SamaritanPromptMixed, { props: { messageConfigs: [config], introDelay: 0 } });
175
+ wrapper = await mountMixed({ props: { messageConfigs: [config], introDelay: 0 } });
171
176
  await nextTick(); // component's await nextTick() resolves → sets opacity=0
172
177
  await nextTick(); // DOM update
173
178
 
@@ -175,7 +180,7 @@ describe("SamaritanPromptMixed — word-pulse", () => {
175
180
  });
176
181
 
177
182
  it("hides cursor when cycle starts", async () => {
178
- wrapper = await mountSuspended(SamaritanPromptMixed, { props: { messageConfigs: [config], introDelay: 0 } });
183
+ wrapper = await mountMixed({ props: { messageConfigs: [config], introDelay: 0 } });
179
184
  await nextTick();
180
185
  await nextTick();
181
186
 
@@ -183,7 +188,7 @@ describe("SamaritanPromptMixed — word-pulse", () => {
183
188
  });
184
189
 
185
190
  it("cursor stays visible when hideCursorInCycle is false", async () => {
186
- wrapper = await mountSuspended(SamaritanPromptMixed, {
191
+ wrapper = await mountMixed({
187
192
  props: { messageConfigs: [{ ...config, hideCursorInCycle: false }] },
188
193
  });
189
194
  await nextTick();
@@ -193,7 +198,7 @@ describe("SamaritanPromptMixed — word-pulse", () => {
193
198
  });
194
199
 
195
200
  it("shows text and restores opacity after fade duration and settle delay", async () => {
196
- wrapper = await mountSuspended(SamaritanPromptMixed, { props: { messageConfigs: [config], introDelay: 0 } });
201
+ wrapper = await mountMixed({ props: { messageConfigs: [config], introDelay: 0 } });
197
202
  await nextTick(); // component's nextTick resolves → opacity=0, await wait(fadeDuration)
198
203
  await nextTick(); // DOM update
199
204
 
@@ -212,7 +217,7 @@ describe("SamaritanPromptMixed — word-pulse", () => {
212
217
  });
213
218
 
214
219
  it("fades text back out after word duration", async () => {
215
- wrapper = await mountSuspended(SamaritanPromptMixed, { props: { messageConfigs: [config], introDelay: 0 } });
220
+ wrapper = await mountMixed({ props: { messageConfigs: [config], introDelay: 0 } });
216
221
  await nextTick();
217
222
  await nextTick();
218
223
 
@@ -237,12 +242,8 @@ describe("SamaritanPromptMixed — word-pulse", () => {
237
242
  // ─── Config resolution ────────────────────────────────────────────────────────
238
243
 
239
244
  describe("SamaritanPromptMixed — config resolution", () => {
240
- afterEach(() => {
241
- // wrappers are unmounted inline in each test
242
- });
243
-
244
245
  it("falls back to global effect prop when not specified per message", async () => {
245
- const wrapper = await mountSuspended(SamaritanPromptMixed, {
246
+ const wrapper = await mountMixed({
246
247
  props: {
247
248
  messageConfigs: [{ text: "TEST" }], // no effect
248
249
  introDelay: 0,
@@ -253,11 +254,10 @@ describe("SamaritanPromptMixed — config resolution", () => {
253
254
  await nextTick();
254
255
  // typewriter first char is immediate
255
256
  expect(wrapper.find(".samaritan-prompt__text").text()).toBe("T");
256
- wrapper.unmount();
257
257
  });
258
258
 
259
259
  it("falls back to global typeSpeed when not specified per message", async () => {
260
- const wrapper = await mountSuspended(SamaritanPromptMixed, {
260
+ const wrapper = await mountMixed({
261
261
  props: {
262
262
  messageConfigs: [{ text: "HI", effect: "typewriter" }],
263
263
  introDelay: 0,
@@ -276,11 +276,10 @@ describe("SamaritanPromptMixed — config resolution", () => {
276
276
  await nextTick();
277
277
  expect(wrapper.find(".samaritan-prompt__text").text()).toBe("HI");
278
278
 
279
- wrapper.unmount();
280
279
  });
281
280
 
282
281
  it("per-message holdDuration overrides global default", async () => {
283
- const wrapper = await mountSuspended(SamaritanPromptMixed, {
282
+ const wrapper = await mountMixed({
284
283
  props: {
285
284
  messageConfigs: [
286
285
  { text: "H", effect: "typewriter", typeSpeed: 100, holdDuration: 200, deleteSpeed: 50, pauseDuration: 100 },
@@ -303,7 +302,6 @@ describe("SamaritanPromptMixed — config resolution", () => {
303
302
  await nextTick();
304
303
 
305
304
  expect(wrapper.find(".samaritan-prompt__text").text()).toBe("");
306
- wrapper.unmount();
307
305
  });
308
306
  });
309
307
 
@@ -321,12 +319,8 @@ describe("SamaritanPromptMixed — accessibility and pause", () => {
321
319
 
322
320
  let wrapper: Awaited<ReturnType<typeof mountSuspended>>;
323
321
 
324
- afterEach(() => {
325
- wrapper?.unmount();
326
- });
327
-
328
322
  it("hides the animated content from assistive tech and exposes a polite live region", async () => {
329
- wrapper = await mountSuspended(SamaritanPromptMixed, { props: { messageConfigs: [config], introDelay: 0 } });
323
+ wrapper = await mountMixed({ props: { messageConfigs: [config], introDelay: 0 } });
330
324
  expect(wrapper.find(".samaritan-prompt__content").attributes("aria-hidden")).toBe("true");
331
325
  const live = wrapper.find(".samaritan-prompt__sr-text");
332
326
  expect(live.attributes("aria-live")).toBe("polite");
@@ -334,14 +328,14 @@ describe("SamaritanPromptMixed — accessibility and pause", () => {
334
328
  });
335
329
 
336
330
  it("announces the full message once it is typed", async () => {
337
- wrapper = await mountSuspended(SamaritanPromptMixed, { props: { messageConfigs: [config], introDelay: 0 } });
331
+ wrapper = await mountMixed({ props: { messageConfigs: [config], introDelay: 0 } });
338
332
  await vi.advanceTimersByTimeAsync(200);
339
333
  await nextTick();
340
334
  expect(wrapper.find(".samaritan-prompt__sr-text").text()).toBe("HI");
341
335
  });
342
336
 
343
337
  it("sets data-paused on hover and removes it on leave", async () => {
344
- wrapper = await mountSuspended(SamaritanPromptMixed, { props: { messageConfigs: [config] } });
338
+ wrapper = await mountMixed({ props: { messageConfigs: [config] } });
345
339
  const root = wrapper.find(".samaritan-prompt");
346
340
  await root.trigger("pointerenter");
347
341
  expect(root.attributes("data-paused")).toBeDefined();
@@ -350,7 +344,7 @@ describe("SamaritanPromptMixed — accessibility and pause", () => {
350
344
  });
351
345
 
352
346
  it("stops typing while hovered and resumes on leave", async () => {
353
- wrapper = await mountSuspended(SamaritanPromptMixed, {
347
+ wrapper = await mountMixed({
354
348
  props: { messageConfigs: [{ ...config, text: "HELLO" }], introDelay: 0 },
355
349
  });
356
350
  await vi.advanceTimersByTimeAsync(100);
@@ -369,7 +363,7 @@ describe("SamaritanPromptMixed — accessibility and pause", () => {
369
363
  });
370
364
 
371
365
  it("renders a custom cursor from the cursor slot", async () => {
372
- wrapper = await mountSuspended(SamaritanPromptMixed, {
366
+ wrapper = await mountMixed({
373
367
  props: { messageConfigs: [config] },
374
368
  slots: { cursor: () => "_" },
375
369
  });
@@ -377,8 +371,61 @@ describe("SamaritanPromptMixed — accessibility and pause", () => {
377
371
  });
378
372
 
379
373
  it("exposes the fade duration as an inline private token", async () => {
380
- wrapper = await mountSuspended(SamaritanPromptMixed, { props: { messageConfigs: [config], fadeDuration: 900 } });
374
+ wrapper = await mountMixed({ props: { messageConfigs: [config], fadeDuration: 900 } });
381
375
  const style = (wrapper.find(".samaritan-prompt").element as HTMLElement).style;
382
376
  expect(style.getPropertyValue("--_fade-duration")).toBe("900ms");
383
377
  });
384
378
  });
379
+
380
+ // ─── Worst-case data ──────────────────────────────────────────────────────────
381
+
382
+ describe("SamaritanPromptMixed — worst-case data", () => {
383
+ const text = (wrapper: Awaited<ReturnType<typeof mountMixed>>) => wrapper.find(".samaritan-prompt__text").text();
384
+
385
+ it("idles without spinning when messageConfigs is empty and introDelay is 0", async () => {
386
+ const wrapper = await mountMixed({ props: { messageConfigs: [], introDelay: 0 } });
387
+ vi.advanceTimersByTime(2000);
388
+ await nextTick();
389
+ expect(text(wrapper)).toBe("");
390
+ });
391
+
392
+ it("starts once messages arrive after an empty start", async () => {
393
+ const wrapper = await mountMixed({ props: { messageConfigs: [], introDelay: 0 } });
394
+ await wrapper.setProps({ messageConfigs: [{ text: "GO", effect: "typewriter", typeSpeed: 100 }] });
395
+ vi.advanceTimersByTime(500);
396
+ await Promise.resolve();
397
+ await nextTick();
398
+ expect(text(wrapper)).toBe("G");
399
+ });
400
+
401
+ it("types an emoji with a skin-tone modifier as one character", async () => {
402
+ const wrapper = await mountMixed({
403
+ props: { messageConfigs: [{ text: "👍🏽A", effect: "typewriter", typeSpeed: 100 }], introDelay: 0 },
404
+ });
405
+ await nextTick();
406
+ expect(text(wrapper)).toBe("👍🏽");
407
+ });
408
+
409
+ it("shows the whole message at once under reduced motion", async () => {
410
+ const original = window.matchMedia;
411
+ window.matchMedia = ((query: string) => ({
412
+ matches: query.includes("prefers-reduced-motion: reduce"),
413
+ media: query,
414
+ onchange: null,
415
+ addEventListener: () => {},
416
+ removeEventListener: () => {},
417
+ addListener: () => {},
418
+ removeListener: () => {},
419
+ dispatchEvent: () => false,
420
+ })) as unknown as typeof window.matchMedia;
421
+ try {
422
+ const wrapper = await mountMixed({
423
+ props: { messageConfigs: [{ text: "HELLO", effect: "typewriter", typeSpeed: 100 }], introDelay: 0 },
424
+ });
425
+ await nextTick();
426
+ expect(text(wrapper)).toBe("HELLO");
427
+ } finally {
428
+ window.matchMedia = original;
429
+ }
430
+ });
431
+ });