srcdev-nuxt-components 9.6.12 → 9.6.14

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 (111) hide show
  1. package/.claude/rules/consumer/ids-from-useid.md +2 -1
  2. package/.claude/rules/consumer/story-register-components.md +1 -1
  3. package/.claude/rules/consumer/vfor-keys-from-data.md +14 -0
  4. package/.claude/skills/component-compliance-checklist.md +24 -1
  5. package/.claude/skills/component-naming.md +1 -0
  6. package/.claude/skills/components/banner-video.md +24 -30
  7. package/.claude/skills/components/content-docs.md +18 -5
  8. package/.claude/skills/components/display-dialog.md +16 -6
  9. package/.claude/skills/components/display-qr-code.md +22 -4
  10. package/.claude/skills/components/{display-tooltip-defined.md → display-tooltip-rich.md} +40 -42
  11. package/.claude/skills/components/display-tooltip.md +17 -6
  12. package/.claude/skills/components/eyebrow-text.md +9 -5
  13. package/.claude/skills/components/header-block.md +13 -5
  14. package/.claude/skills/components/hero-text.md +13 -9
  15. package/.claude/skills/components/link-text.md +8 -5
  16. package/.claude/skills/components/pop-over.md +17 -3
  17. package/.claude/skills/components/skip-links.md +16 -0
  18. package/.claude/skills/composable-tooltips-guide.md +4 -4
  19. package/.claude/skills/index.md +6 -6
  20. package/.claude/skills/storybook-add-story.md +5 -2
  21. package/.claude/skills/testing-add-playwright.md +12 -7
  22. package/.claude/skills/testing-add-unit-test.md +8 -0
  23. package/.vscode/srcdev-component-banner-video.code-snippets +38 -25
  24. package/.vscode/srcdev-component-content-docs.code-snippets +69 -60
  25. package/.vscode/srcdev-component-display-dialog.code-snippets +7 -5
  26. package/.vscode/srcdev-component-display-qr-code.code-snippets +12 -1
  27. package/.vscode/srcdev-component-display-tooltip-rich.code-snippets +50 -0
  28. package/.vscode/srcdev-component-display-tooltip.code-snippets +31 -0
  29. package/.vscode/srcdev-component-hero-text.code-snippets +21 -5
  30. package/.vscode/srcdev-component-link-text.code-snippets +3 -3
  31. package/.vscode/srcdev-component-pop-over.code-snippets +28 -0
  32. package/.vscode/srcdev-component-skip-links.code-snippets +1 -1
  33. package/app/components/01.atoms/banner-video/BannerVideo.vue +51 -19
  34. package/app/components/01.atoms/banner-video/CONSUMER-STYLING.md +59 -40
  35. package/app/components/01.atoms/banner-video/stories/BannerVideo.stories.ts +88 -1
  36. package/app/components/01.atoms/banner-video/tests/BannerVideo.spec.ts +127 -78
  37. package/app/components/01.atoms/banner-video/tests/__snapshots__/BannerVideo.spec.ts.snap +12 -4
  38. package/app/components/01.atoms/content-wrappers/docs-pages/CONSUMER-STYLING.md +42 -14
  39. package/app/components/01.atoms/content-wrappers/docs-pages/ContentDocs.vue +86 -112
  40. package/app/components/01.atoms/content-wrappers/docs-pages/stories/ContentDocs.stories.ts +134 -8
  41. package/app/components/01.atoms/content-wrappers/docs-pages/tests/ContentDocs.spec.ts +131 -65
  42. package/app/components/01.atoms/display-dialog/CONSUMER-STYLING.md +28 -3
  43. package/app/components/01.atoms/display-dialog/DisplayDialog.vue +39 -31
  44. package/app/components/01.atoms/display-dialog/stories/DisplayDialog.stories.ts +92 -1
  45. package/app/components/01.atoms/display-dialog/tests/DisplayDialog.spec.ts +87 -41
  46. package/app/components/01.atoms/display-dialog/tests/__snapshots__/DisplayDialog.spec.ts.snap +5 -1
  47. package/app/components/01.atoms/display-tooltip/CONSUMER-STYLING.md +70 -24
  48. package/app/components/01.atoms/display-tooltip/DisplayTooltip.vue +24 -12
  49. package/app/components/01.atoms/display-tooltip/stories/DisplayTooltip.stories.ts +89 -2
  50. package/app/components/01.atoms/display-tooltip/tests/DisplayTooltip.spec.ts +65 -18
  51. package/app/components/01.atoms/display-tooltip/tests/__snapshots__/DisplayTooltip.spec.ts.snap +3 -3
  52. package/app/components/01.atoms/navigation/skip-links/CONSUMER-STYLING.md +65 -19
  53. package/app/components/01.atoms/navigation/skip-links/SkipLinks.vue +20 -14
  54. package/app/components/01.atoms/navigation/skip-links/stories/SkipLinks.stories.ts +89 -20
  55. package/app/components/01.atoms/navigation/skip-links/tests/SkipLinks.spec.ts +70 -10
  56. package/app/components/01.atoms/pop-over/CONSUMER-STYLING.md +42 -9
  57. package/app/components/01.atoms/pop-over/PopOver.vue +28 -12
  58. package/app/components/01.atoms/pop-over/stories/PopOver.stories.ts +114 -2
  59. package/app/components/01.atoms/pop-over/tests/PopOver.spec.ts +35 -0
  60. package/app/components/01.atoms/pop-over/tests/__snapshots__/PopOver.spec.ts.snap +5 -1
  61. package/app/components/01.atoms/qr-code/CONSUMER-STYLING.md +16 -3
  62. package/app/components/01.atoms/qr-code/DisplayQrCode.vue +31 -0
  63. package/app/components/01.atoms/qr-code/stories/DisplayQrCode.stories.ts +190 -157
  64. package/app/components/01.atoms/qr-code/tests/DisplayQrCode.spec.ts +58 -0
  65. package/app/components/01.atoms/text-blocks/eyebrow-text/CONSUMER-STYLING.md +9 -1
  66. package/app/components/01.atoms/text-blocks/eyebrow-text/EyebrowText.vue +5 -4
  67. package/app/components/01.atoms/text-blocks/eyebrow-text/playwright/eyebrow-text.playwright.ts +1 -1
  68. package/app/components/01.atoms/text-blocks/eyebrow-text/stories/EyebrowText.stories.ts +104 -17
  69. package/app/components/01.atoms/text-blocks/eyebrow-text/tests/EyebrowText.spec.ts +68 -118
  70. package/app/components/01.atoms/text-blocks/eyebrow-text/tests/__snapshots__/EyebrowText.spec.ts.snap +8 -8
  71. package/app/components/01.atoms/text-blocks/header-block/CONSUMER-STYLING.md +49 -24
  72. package/app/components/01.atoms/text-blocks/header-block/HeaderBlock.vue +9 -1
  73. package/app/components/01.atoms/text-blocks/header-block/stories/HeaderBlock.stories.ts +96 -26
  74. package/app/components/01.atoms/text-blocks/header-block/tests/HeaderBlock.spec.ts +64 -48
  75. package/app/components/01.atoms/text-blocks/header-block/tests/__snapshots__/HeaderBlock.spec.ts.snap +2 -2
  76. package/app/components/01.atoms/text-blocks/hero-text/CONSUMER-STYLING.md +22 -6
  77. package/app/components/01.atoms/text-blocks/hero-text/HeroText.vue +32 -23
  78. package/app/components/01.atoms/text-blocks/hero-text/playwright/hero-text.playwright.ts +1 -1
  79. package/app/components/01.atoms/text-blocks/hero-text/stories/HeroText.stories.ts +176 -23
  80. package/app/components/01.atoms/text-blocks/hero-text/tests/HeroText.spec.ts +129 -123
  81. package/app/components/01.atoms/text-blocks/hero-text/tests/__snapshots__/HeroText.spec.ts.snap +2 -2
  82. package/app/components/01.atoms/text-blocks/link-text/CONSUMER-STYLING.md +17 -0
  83. package/app/components/01.atoms/text-blocks/link-text/LinkText.vue +9 -3
  84. package/app/components/01.atoms/text-blocks/link-text/stories/LinkText.stories.ts +96 -99
  85. package/app/components/01.atoms/text-blocks/link-text/tests/LinkText.spec.ts +84 -93
  86. package/app/components/02.molecules/display-tooltip-rich/CONSUMER-STYLING.md +110 -0
  87. package/app/components/02.molecules/display-tooltip-rich/DisplayTooltipRich.vue +148 -0
  88. package/app/components/02.molecules/display-tooltip-rich/stories/DisplayTooltipRich.stories.ts +254 -0
  89. package/app/components/02.molecules/display-tooltip-rich/tests/DisplayTooltipRich.spec.ts +156 -0
  90. package/app/components/02.molecules/display-tooltip-rich/tests/__snapshots__/DisplayTooltipRich.spec.ts.snap +22 -0
  91. package/app/components/02.molecules/navigation/deep-expanding-menu/DeepExpandingMenu.vue +1 -0
  92. package/app/components/02.molecules/navigation/deep-expanding-menu/tests/__snapshots__/DeepExpandingMenu.spec.ts.snap +1 -1
  93. package/app/components/02.molecules/price-list/tests/__snapshots__/PriceList.spec.ts.snap +8 -8
  94. package/app/components/03.organisms/image-galleries/carousel-flip/CarouselFlip.vue +1 -0
  95. package/app/components/03.organisms/services/service-detail/tests/__snapshots__/ServiceDetail.spec.ts.snap +15 -15
  96. package/app/components/03.organisms/services/service-summary/tests/__snapshots__/ServiceSummary.spec.ts.snap +3 -3
  97. package/app/components/03.organisms/services/services-card/tests/ServicesCard.spec.ts +3 -3
  98. package/app/components/03.organisms/services/services-card/tests/__snapshots__/ServicesCard.spec.ts.snap +3 -3
  99. package/app/components/03.organisms/services/services-grids/tests/ServicesCardGrid.spec.ts +1 -1
  100. package/app/components/03.organisms/services/services-grids/tests/__snapshots__/ServiceSummaryGrid.spec.ts.snap +9 -9
  101. package/app/components/03.organisms/services/services-grids/tests/__snapshots__/ServicesCardGrid.spec.ts.snap +9 -9
  102. package/app/composables/tests/useTooltips.spec.ts +3 -3
  103. package/app/types/components/index.ts +1 -1
  104. package/package.json +1 -1
  105. package/.vscode/srcdev-component-display-tooltip-defined.code-snippets +0 -29
  106. package/app/components/02.molecules/display-tooltip-defined/CONSUMER-STYLING.md +0 -53
  107. package/app/components/02.molecules/display-tooltip-defined/DisplayTooltipDefined.vue +0 -117
  108. package/app/components/02.molecules/display-tooltip-defined/stories/DisplayTooltipDefined.stories.ts +0 -129
  109. package/app/components/02.molecules/display-tooltip-defined/tests/DisplayTooltipDefined.spec.ts +0 -85
  110. package/app/components/02.molecules/display-tooltip-defined/tests/__snapshots__/DisplayTooltipDefined.spec.ts.snap +0 -17
  111. /package/app/types/components/{display-tooltip-defined.d.ts → display-tooltip-rich.d.ts} +0 -0
@@ -1,29 +1,39 @@
1
- import { describe, it, expect } from "vitest";
1
+ import { describe, it, expect, vi, afterEach } from "vitest";
2
2
  import { mountSuspended } from "@nuxt/test-utils/runtime";
3
3
  import BannerVideo from "../BannerVideo.vue";
4
4
 
5
+ type Wrapper = Awaited<ReturnType<typeof mountSuspended>>;
6
+
5
7
  const defaultProps = {
6
8
  src: "/images/banners/video/lake-banner.mp4",
7
9
  poster: "/images/banners/video/lake-banner.jpg",
8
10
  };
9
11
 
10
12
  describe("BannerVideo", () => {
13
+ let wrapper: Wrapper | undefined;
14
+
15
+ afterEach(() => {
16
+ wrapper?.unmount();
17
+ wrapper = undefined;
18
+ vi.restoreAllMocks();
19
+ });
20
+
11
21
  // ─── Mount ───────────────────────────────────────────────────────────────
12
22
 
13
23
  it("mounts without error", async () => {
14
- const wrapper = await mountSuspended(BannerVideo, { props: defaultProps });
24
+ wrapper = await mountSuspended(BannerVideo, { props: defaultProps });
15
25
  expect(wrapper.vm).toBeTruthy();
16
26
  });
17
27
 
18
28
  // ─── Snapshots ───────────────────────────────────────────────────────────
19
29
 
20
30
  it("renders correct HTML structure (default props)", async () => {
21
- const wrapper = await mountSuspended(BannerVideo, { props: defaultProps });
31
+ wrapper = await mountSuspended(BannerVideo, { props: defaultProps });
22
32
  expect(wrapper.html()).toMatchSnapshot();
23
33
  });
24
34
 
25
35
  it("renders correct HTML structure (all props set)", async () => {
26
- const wrapper = await mountSuspended(BannerVideo, {
36
+ wrapper = await mountSuspended(BannerVideo, {
27
37
  props: {
28
38
  ...defaultProps,
29
39
  tag: "header",
@@ -41,79 +51,80 @@ describe("BannerVideo", () => {
41
51
  // ─── Root element ────────────────────────────────────────────────────────
42
52
 
43
53
  it("renders a <div> as the root element by default", async () => {
44
- const wrapper = await mountSuspended(BannerVideo, { props: defaultProps });
54
+ wrapper = await mountSuspended(BannerVideo, { props: defaultProps });
45
55
  expect(wrapper.element.tagName).toBe("DIV");
46
56
  });
47
57
 
48
58
  it("renders the specified tag as root", async () => {
49
59
  for (const tag of ["div", "header", "main", "article"] as const) {
50
- const wrapper = await mountSuspended(BannerVideo, {
60
+ wrapper = await mountSuspended(BannerVideo, {
51
61
  props: { ...defaultProps, tag },
52
62
  });
53
63
  expect(wrapper.element.tagName).toBe(tag.toUpperCase());
64
+ wrapper.unmount();
54
65
  }
55
66
  });
56
67
 
57
68
  it("always has the banner-video class", async () => {
58
- const wrapper = await mountSuspended(BannerVideo, { props: defaultProps });
69
+ wrapper = await mountSuspended(BannerVideo, { props: defaultProps });
59
70
  expect(wrapper.classes()).toContain("banner-video");
60
71
  });
61
72
 
62
73
  // ─── Video element ───────────────────────────────────────────────────────
63
74
 
64
75
  it("renders a video element", async () => {
65
- const wrapper = await mountSuspended(BannerVideo, { props: defaultProps });
76
+ wrapper = await mountSuspended(BannerVideo, { props: defaultProps });
66
77
  expect(wrapper.find("video").exists()).toBe(true);
67
78
  });
68
79
 
69
- it("video has the video class", async () => {
70
- const wrapper = await mountSuspended(BannerVideo, { props: defaultProps });
71
- expect(wrapper.find("video").classes()).toContain("video");
80
+ it("video has the banner-video-media class", async () => {
81
+ wrapper = await mountSuspended(BannerVideo, { props: defaultProps });
82
+ expect(wrapper.find("video").classes()).toContain("banner-video-media");
72
83
  });
73
84
 
74
85
  it("video has autoplay attribute", async () => {
75
- const wrapper = await mountSuspended(BannerVideo, { props: defaultProps });
86
+ wrapper = await mountSuspended(BannerVideo, { props: defaultProps });
76
87
  expect(wrapper.find("video").attributes()).toHaveProperty("autoplay");
77
88
  });
78
89
 
79
90
  it("video has muted attribute", async () => {
80
- const wrapper = await mountSuspended(BannerVideo, { props: defaultProps });
91
+ wrapper = await mountSuspended(BannerVideo, { props: defaultProps });
81
92
  expect(wrapper.find("video").attributes()).toHaveProperty("muted");
82
93
  });
83
94
 
84
95
  it("video has loop attribute", async () => {
85
- const wrapper = await mountSuspended(BannerVideo, { props: defaultProps });
96
+ wrapper = await mountSuspended(BannerVideo, { props: defaultProps });
86
97
  expect(wrapper.find("video").attributes()).toHaveProperty("loop");
87
98
  });
88
99
 
89
100
  it("video has playsinline attribute", async () => {
90
- const wrapper = await mountSuspended(BannerVideo, { props: defaultProps });
101
+ wrapper = await mountSuspended(BannerVideo, { props: defaultProps });
91
102
  expect(wrapper.find("video").attributes()).toHaveProperty("playsinline");
92
103
  });
93
104
 
94
105
  it("video has the poster attribute set to the poster prop", async () => {
95
- const wrapper = await mountSuspended(BannerVideo, { props: defaultProps });
106
+ wrapper = await mountSuspended(BannerVideo, { props: defaultProps });
96
107
  expect(wrapper.find("video").attributes("poster")).toBe(defaultProps.poster);
97
108
  });
98
109
 
99
110
  it("video element has preload=auto", async () => {
100
- const wrapper = await mountSuspended(BannerVideo, { props: defaultProps });
111
+ wrapper = await mountSuspended(BannerVideo, { props: defaultProps });
101
112
  expect(wrapper.find("video").attributes("preload")).toBe("auto");
102
113
  });
103
114
 
104
115
  it("source element has the correct src", async () => {
105
- const wrapper = await mountSuspended(BannerVideo, { props: defaultProps });
116
+ wrapper = await mountSuspended(BannerVideo, { props: defaultProps });
106
117
  expect(wrapper.find("source").attributes("src")).toBe(defaultProps.src);
107
118
  });
108
119
 
109
120
  it("source element has type video/mp4", async () => {
110
- const wrapper = await mountSuspended(BannerVideo, { props: defaultProps });
121
+ wrapper = await mountSuspended(BannerVideo, { props: defaultProps });
111
122
  expect(wrapper.find("source").attributes("type")).toBe("video/mp4");
112
123
  });
113
124
 
114
125
  it("calls play() when loadeddata fires", async () => {
115
126
  const play = vi.fn().mockResolvedValue(undefined);
116
- const wrapper = await mountSuspended(BannerVideo, { props: defaultProps });
127
+ wrapper = await mountSuspended(BannerVideo, { props: defaultProps });
117
128
  (wrapper.find("video").element as HTMLVideoElement).play = play;
118
129
  await wrapper.find("video").trigger("loadeddata");
119
130
  expect(play).toHaveBeenCalled();
@@ -121,7 +132,7 @@ describe("BannerVideo", () => {
121
132
 
122
133
  it("calls play() when canplay fires", async () => {
123
134
  const play = vi.fn().mockResolvedValue(undefined);
124
- const wrapper = await mountSuspended(BannerVideo, { props: defaultProps });
135
+ wrapper = await mountSuspended(BannerVideo, { props: defaultProps });
125
136
  (wrapper.find("video").element as HTMLVideoElement).play = play;
126
137
  await wrapper.find("video").trigger("canplay");
127
138
  expect(play).toHaveBeenCalled();
@@ -130,90 +141,111 @@ describe("BannerVideo", () => {
130
141
  // ─── Pause/play toggle (WCAG 2.2.2) ─────────────────────────────────────
131
142
 
132
143
  it("renders a pause/play toggle button", async () => {
133
- const wrapper = await mountSuspended(BannerVideo, { props: defaultProps });
134
- expect(wrapper.find(".banner-video__toggle").exists()).toBe(true);
144
+ wrapper = await mountSuspended(BannerVideo, { props: defaultProps });
145
+ expect(wrapper.find(".banner-video-toggle").exists()).toBe(true);
135
146
  });
136
147
 
137
- it("toggle defaults to a 'Pause background video' label once playback starts", async () => {
138
- const wrapper = await mountSuspended(BannerVideo, { props: defaultProps });
148
+ it("toggle label reads 'Pause background video' once playback starts", async () => {
149
+ wrapper = await mountSuspended(BannerVideo, { props: defaultProps });
139
150
  await wrapper.find("video").trigger("play");
140
- expect(wrapper.find(".banner-video__toggle").attributes("aria-label")).toBe("Pause background video");
151
+ expect(wrapper.find(".banner-video-toggle").find(".input-button-text").text()).toBe("Pause background video");
141
152
  });
142
153
 
143
154
  it("toggle reads 'Play background video' before playback has started", async () => {
144
- const wrapper = await mountSuspended(BannerVideo, { props: defaultProps });
145
- expect(wrapper.find(".banner-video__toggle").attributes("aria-label")).toBe("Play background video");
155
+ wrapper = await mountSuspended(BannerVideo, { props: defaultProps });
156
+ expect(wrapper.find(".banner-video-toggle").find(".input-button-text").text()).toBe("Play background video");
146
157
  });
147
158
 
148
159
  it("clicking the toggle calls pause() while playing", async () => {
149
160
  const pause = vi.fn();
150
- const wrapper = await mountSuspended(BannerVideo, { props: defaultProps });
161
+ wrapper = await mountSuspended(BannerVideo, { props: defaultProps });
151
162
  const videoElement = wrapper.find("video").element as HTMLVideoElement;
152
163
  videoElement.pause = pause;
153
164
  Object.defineProperty(videoElement, "paused", { value: false, configurable: true });
154
- await wrapper.find(".banner-video__toggle").trigger("click");
165
+ await wrapper.find(".banner-video-toggle").trigger("click");
155
166
  expect(pause).toHaveBeenCalled();
156
167
  });
157
168
 
158
169
  it("clicking the toggle calls play() while paused", async () => {
159
170
  const play = vi.fn().mockResolvedValue(undefined);
160
- const wrapper = await mountSuspended(BannerVideo, { props: defaultProps });
171
+ wrapper = await mountSuspended(BannerVideo, { props: defaultProps });
161
172
  const videoElement = wrapper.find("video").element as HTMLVideoElement;
162
173
  videoElement.play = play;
163
174
  Object.defineProperty(videoElement, "paused", { value: true, configurable: true });
164
- await wrapper.find(".banner-video__toggle").trigger("click");
175
+ await wrapper.find(".banner-video-toggle").trigger("click");
165
176
  expect(play).toHaveBeenCalled();
166
177
  });
167
178
 
168
- it("updates aria-label to 'Play background video' after the video pauses", async () => {
169
- const wrapper = await mountSuspended(BannerVideo, { props: defaultProps });
179
+ it("updates the toggle label to 'Play background video' after the video pauses", async () => {
180
+ wrapper = await mountSuspended(BannerVideo, { props: defaultProps });
170
181
  await wrapper.find("video").trigger("play");
171
182
  await wrapper.find("video").trigger("pause");
172
- expect(wrapper.find(".banner-video__toggle").attributes("aria-label")).toBe("Play background video");
183
+ expect(wrapper.find(".banner-video-toggle").find(".input-button-text").text()).toBe("Play background video");
184
+ });
185
+
186
+ it("renders the toggle as an icon-only InputButton with a visually hidden label", async () => {
187
+ wrapper = await mountSuspended(BannerVideo, { props: defaultProps });
188
+ const toggle = wrapper.find(".banner-video-toggle");
189
+ expect(toggle.classes()).toContain("input-button");
190
+ expect(toggle.attributes("type")).toBe("button");
191
+ expect(toggle.attributes("data-variant")).toBe("inline");
192
+ expect(toggle.attributes()).toHaveProperty("data-icon-only");
193
+ expect(toggle.find(".input-button-text").classes()).toContain("sr-only");
194
+ expect(toggle.attributes("aria-label")).toBeUndefined();
195
+ });
196
+
197
+ it("uses custom playLabel and pauseLabel", async () => {
198
+ wrapper = await mountSuspended(BannerVideo, {
199
+ props: { ...defaultProps, playLabel: "Video abspielen", pauseLabel: "Video anhalten" },
200
+ });
201
+ const label = () => wrapper!.find(".banner-video-toggle .input-button-text").text();
202
+ expect(label()).toBe("Video abspielen");
203
+ await wrapper.find("video").trigger("play");
204
+ expect(label()).toBe("Video anhalten");
173
205
  });
174
206
 
175
207
  // ─── Fallback image ──────────────────────────────────────────────────────
176
208
 
177
209
  it("renders a NuxtImg fallback element", async () => {
178
- const wrapper = await mountSuspended(BannerVideo, { props: defaultProps });
210
+ wrapper = await mountSuspended(BannerVideo, { props: defaultProps });
179
211
  expect(wrapper.find("img[data-nuxt-img]").exists()).toBe(true);
180
212
  });
181
213
 
182
- it("fallback image has the fallback class", async () => {
183
- const wrapper = await mountSuspended(BannerVideo, { props: defaultProps });
184
- expect(wrapper.find("img[data-nuxt-img]").classes()).toContain("fallback");
214
+ it("fallback image has the banner-video-fallback class", async () => {
215
+ wrapper = await mountSuspended(BannerVideo, { props: defaultProps });
216
+ expect(wrapper.find("img[data-nuxt-img]").classes()).toContain("banner-video-fallback");
185
217
  });
186
218
 
187
219
  it("fallback image src contains the poster filename", async () => {
188
- const wrapper = await mountSuspended(BannerVideo, { props: defaultProps });
220
+ wrapper = await mountSuspended(BannerVideo, { props: defaultProps });
189
221
  const src = wrapper.find("img[data-nuxt-img]").attributes("src") ?? "";
190
222
  expect(src).toContain("lake-banner.jpg");
191
223
  });
192
224
 
193
225
  it("fallback image alt defaults to empty string", async () => {
194
- const wrapper = await mountSuspended(BannerVideo, { props: defaultProps });
226
+ wrapper = await mountSuspended(BannerVideo, { props: defaultProps });
195
227
  expect(wrapper.find("img[data-nuxt-img]").attributes("alt")).toBe("");
196
228
  });
197
229
 
198
230
  it("fallback image alt reflects the alt prop", async () => {
199
- const wrapper = await mountSuspended(BannerVideo, {
231
+ wrapper = await mountSuspended(BannerVideo, {
200
232
  props: { ...defaultProps, alt: "A serene lake" },
201
233
  });
202
234
  expect(wrapper.find("img[data-nuxt-img]").attributes("alt")).toBe("A serene lake");
203
235
  });
204
236
 
205
237
  it("fallback image width defaults to 1920", async () => {
206
- const wrapper = await mountSuspended(BannerVideo, { props: defaultProps });
238
+ wrapper = await mountSuspended(BannerVideo, { props: defaultProps });
207
239
  expect(wrapper.find("img[data-nuxt-img]").attributes("width")).toBe("1920");
208
240
  });
209
241
 
210
242
  it("fallback image height defaults to 1080", async () => {
211
- const wrapper = await mountSuspended(BannerVideo, { props: defaultProps });
243
+ wrapper = await mountSuspended(BannerVideo, { props: defaultProps });
212
244
  expect(wrapper.find("img[data-nuxt-img]").attributes("height")).toBe("1080");
213
245
  });
214
246
 
215
247
  it("fallback image uses custom imgWidth and imgHeight when provided", async () => {
216
- const wrapper = await mountSuspended(BannerVideo, {
248
+ wrapper = await mountSuspended(BannerVideo, {
217
249
  props: { ...defaultProps, imgWidth: 1280, imgHeight: 720 },
218
250
  });
219
251
  const img = wrapper.find("img[data-nuxt-img]");
@@ -222,24 +254,24 @@ describe("BannerVideo", () => {
222
254
  });
223
255
 
224
256
  it("fallback image has loading=eager", async () => {
225
- const wrapper = await mountSuspended(BannerVideo, { props: defaultProps });
257
+ wrapper = await mountSuspended(BannerVideo, { props: defaultProps });
226
258
  expect(wrapper.find("img[data-nuxt-img]").attributes("loading")).toBe("eager");
227
259
  });
228
260
 
229
261
  it("fallback image has decoding=async", async () => {
230
- const wrapper = await mountSuspended(BannerVideo, { props: defaultProps });
262
+ wrapper = await mountSuspended(BannerVideo, { props: defaultProps });
231
263
  expect(wrapper.find("img[data-nuxt-img]").attributes("decoding")).toBe("async");
232
264
  });
233
265
 
234
266
  // ─── depth / data-depth ──────────────────────────────────────────────────
235
267
 
236
268
  it("sets data-depth to md by default", async () => {
237
- const wrapper = await mountSuspended(BannerVideo, { props: defaultProps });
269
+ wrapper = await mountSuspended(BannerVideo, { props: defaultProps });
238
270
  expect(wrapper.attributes("data-depth")).toBe("md");
239
271
  });
240
272
 
241
273
  it.each(["xs", "sm", "md", "lg", "xl"] as const)("sets data-depth to %s when depth prop is %s", async (depth) => {
242
- const wrapper = await mountSuspended(BannerVideo, {
274
+ wrapper = await mountSuspended(BannerVideo, {
243
275
  props: { ...defaultProps, depth },
244
276
  });
245
277
  expect(wrapper.attributes("data-depth")).toBe(depth);
@@ -248,27 +280,42 @@ describe("BannerVideo", () => {
248
280
  // ─── CSS custom properties ───────────────────────────────────────────────
249
281
 
250
282
  it("sets --_aspect-ratio to 21/9 by default", async () => {
251
- const wrapper = await mountSuspended(BannerVideo, { props: defaultProps });
283
+ wrapper = await mountSuspended(BannerVideo, { props: defaultProps });
252
284
  const style = (wrapper.element as HTMLElement).style;
253
285
  expect(style.getPropertyValue("--_aspect-ratio")).toBe("21/9");
254
286
  });
255
287
 
256
288
  it("reflects aspectRatio prop in --_aspect-ratio", async () => {
257
- const wrapper = await mountSuspended(BannerVideo, {
289
+ wrapper = await mountSuspended(BannerVideo, {
258
290
  props: { ...defaultProps, aspectRatio: "16/9" },
259
291
  });
260
292
  const style = (wrapper.element as HTMLElement).style;
261
293
  expect(style.getPropertyValue("--_aspect-ratio")).toBe("16/9");
262
294
  });
263
295
 
296
+ it.each(["4/3", "1", "1.5", " 16 / 9 ", "2.35/1"])("accepts a valid aspectRatio %j", async (aspectRatio) => {
297
+ wrapper = await mountSuspended(BannerVideo, { props: { ...defaultProps, aspectRatio } });
298
+ const style = (wrapper.element as HTMLElement).style;
299
+ expect(style.getPropertyValue("--_aspect-ratio")).toBe(aspectRatio.trim());
300
+ });
301
+
302
+ it.each(["", "abc", "0", "0/9", "16/0", "-16/9", "16/9/4", "16:9"])(
303
+ "falls back to 21/9 for an invalid aspectRatio %j",
304
+ async (aspectRatio) => {
305
+ wrapper = await mountSuspended(BannerVideo, { props: { ...defaultProps, aspectRatio } });
306
+ const style = (wrapper.element as HTMLElement).style;
307
+ expect(style.getPropertyValue("--_aspect-ratio")).toBe("21/9");
308
+ }
309
+ );
310
+
264
311
  it("sets object-fit to cover on the video element by default", async () => {
265
- const wrapper = await mountSuspended(BannerVideo, { props: defaultProps });
312
+ wrapper = await mountSuspended(BannerVideo, { props: defaultProps });
266
313
  const style = (wrapper.find("video").element as HTMLElement).style;
267
314
  expect(style.objectFit).toBe("cover");
268
315
  });
269
316
 
270
317
  it("reflects objectFit prop on the video element", async () => {
271
- const wrapper = await mountSuspended(BannerVideo, {
318
+ wrapper = await mountSuspended(BannerVideo, {
272
319
  props: { ...defaultProps, objectFit: "contain" },
273
320
  });
274
321
  const style = (wrapper.find("video").element as HTMLElement).style;
@@ -276,13 +323,13 @@ describe("BannerVideo", () => {
276
323
  });
277
324
 
278
325
  it("sets object-fit to cover on the fallback image by default", async () => {
279
- const wrapper = await mountSuspended(BannerVideo, { props: defaultProps });
326
+ wrapper = await mountSuspended(BannerVideo, { props: defaultProps });
280
327
  const style = (wrapper.find("img[data-nuxt-img]").element as HTMLElement).style;
281
328
  expect(style.objectFit).toBe("cover");
282
329
  });
283
330
 
284
331
  it("reflects objectFit prop on the fallback image", async () => {
285
- const wrapper = await mountSuspended(BannerVideo, {
332
+ wrapper = await mountSuspended(BannerVideo, {
286
333
  props: { ...defaultProps, objectFit: "contain" },
287
334
  });
288
335
  const style = (wrapper.find("img[data-nuxt-img]").element as HTMLElement).style;
@@ -290,13 +337,13 @@ describe("BannerVideo", () => {
290
337
  });
291
338
 
292
339
  it("video element does not have object-position style", async () => {
293
- const wrapper = await mountSuspended(BannerVideo, { props: defaultProps });
340
+ wrapper = await mountSuspended(BannerVideo, { props: defaultProps });
294
341
  const style = (wrapper.find("video").element as HTMLElement).style;
295
342
  expect(style.objectPosition).toBe("");
296
343
  });
297
344
 
298
345
  it("sets --_align-self to center by default", async () => {
299
- const wrapper = await mountSuspended(BannerVideo, { props: defaultProps });
346
+ wrapper = await mountSuspended(BannerVideo, { props: defaultProps });
300
347
  const style = (wrapper.element as HTMLElement).style;
301
348
  expect(style.getPropertyValue("--_align-self")).toBe("center");
302
349
  });
@@ -306,15 +353,16 @@ describe("BannerVideo", () => {
306
353
  ["start", "start"],
307
354
  ["end", "end"],
308
355
  ] as const) {
309
- const wrapper = await mountSuspended(BannerVideo, {
356
+ wrapper = await mountSuspended(BannerVideo, {
310
357
  props: { ...defaultProps, verticalPosition: prop },
311
358
  });
312
359
  expect((wrapper.element as HTMLElement).style.getPropertyValue("--_align-self")).toBe(expected);
360
+ wrapper.unmount();
313
361
  }
314
362
  });
315
363
 
316
364
  it("sets --_justify-self to center by default", async () => {
317
- const wrapper = await mountSuspended(BannerVideo, { props: defaultProps });
365
+ wrapper = await mountSuspended(BannerVideo, { props: defaultProps });
318
366
  const style = (wrapper.element as HTMLElement).style;
319
367
  expect(style.getPropertyValue("--_justify-self")).toBe("center");
320
368
  });
@@ -324,21 +372,22 @@ describe("BannerVideo", () => {
324
372
  ["start", "start"],
325
373
  ["end", "end"],
326
374
  ] as const) {
327
- const wrapper = await mountSuspended(BannerVideo, {
375
+ wrapper = await mountSuspended(BannerVideo, {
328
376
  props: { ...defaultProps, horizontalPosition: prop },
329
377
  });
330
378
  expect((wrapper.element as HTMLElement).style.getPropertyValue("--_justify-self")).toBe(expected);
379
+ wrapper.unmount();
331
380
  }
332
381
  });
333
382
 
334
383
  it("fallback image object-position defaults to center center", async () => {
335
- const wrapper = await mountSuspended(BannerVideo, { props: defaultProps });
384
+ wrapper = await mountSuspended(BannerVideo, { props: defaultProps });
336
385
  const style = (wrapper.find("img[data-nuxt-img]").element as HTMLElement).style;
337
386
  expect(style.objectPosition).toBe("center center");
338
387
  });
339
388
 
340
389
  it("fallback image object-position reflects verticalPosition", async () => {
341
- const wrapper = await mountSuspended(BannerVideo, {
390
+ wrapper = await mountSuspended(BannerVideo, {
342
391
  props: { ...defaultProps, verticalPosition: "start" },
343
392
  });
344
393
  const style = (wrapper.find("img[data-nuxt-img]").element as HTMLElement).style;
@@ -346,7 +395,7 @@ describe("BannerVideo", () => {
346
395
  });
347
396
 
348
397
  it("fallback image object-position reflects horizontalPosition", async () => {
349
- const wrapper = await mountSuspended(BannerVideo, {
398
+ wrapper = await mountSuspended(BannerVideo, {
350
399
  props: { ...defaultProps, horizontalPosition: "end" },
351
400
  });
352
401
  const style = (wrapper.find("img[data-nuxt-img]").element as HTMLElement).style;
@@ -354,7 +403,7 @@ describe("BannerVideo", () => {
354
403
  });
355
404
 
356
405
  it("fallback image object-position reflects both verticalPosition and horizontalPosition", async () => {
357
- const wrapper = await mountSuspended(BannerVideo, {
406
+ wrapper = await mountSuspended(BannerVideo, {
358
407
  props: { ...defaultProps, verticalPosition: "end", horizontalPosition: "start" },
359
408
  });
360
409
  const style = (wrapper.find("img[data-nuxt-img]").element as HTMLElement).style;
@@ -364,14 +413,14 @@ describe("BannerVideo", () => {
364
413
  // ─── styleClassPassthrough ───────────────────────────────────────────────
365
414
 
366
415
  it("applies a single styleClassPassthrough string to the root", async () => {
367
- const wrapper = await mountSuspended(BannerVideo, {
416
+ wrapper = await mountSuspended(BannerVideo, {
368
417
  props: { ...defaultProps, styleClassPassthrough: "full-bleed" },
369
418
  });
370
419
  expect(wrapper.classes()).toContain("full-bleed");
371
420
  });
372
421
 
373
422
  it("applies multiple styleClassPassthrough classes from an array", async () => {
374
- const wrapper = await mountSuspended(BannerVideo, {
423
+ wrapper = await mountSuspended(BannerVideo, {
375
424
  props: { ...defaultProps, styleClassPassthrough: ["full-bleed", "mbe-0"] },
376
425
  });
377
426
  expect(wrapper.classes()).toContain("full-bleed");
@@ -379,14 +428,14 @@ describe("BannerVideo", () => {
379
428
  });
380
429
 
381
430
  it("does not apply styleClassPassthrough to the video element", async () => {
382
- const wrapper = await mountSuspended(BannerVideo, {
431
+ wrapper = await mountSuspended(BannerVideo, {
383
432
  props: { ...defaultProps, styleClassPassthrough: "full-bleed" },
384
433
  });
385
434
  expect(wrapper.find("video").classes()).not.toContain("full-bleed");
386
435
  });
387
436
 
388
437
  it("updates classes when styleClassPassthrough prop changes", async () => {
389
- const wrapper = await mountSuspended(BannerVideo, {
438
+ wrapper = await mountSuspended(BannerVideo, {
390
439
  props: { ...defaultProps, styleClassPassthrough: ["original"] },
391
440
  });
392
441
  expect(wrapper.classes()).toContain("original");
@@ -398,33 +447,33 @@ describe("BannerVideo", () => {
398
447
  // ─── Toggle icon ─────────────────────────────────────────────────────────
399
448
 
400
449
  it("renders the default play icon before playback starts", async () => {
401
- const wrapper = await mountSuspended(BannerVideo, { props: defaultProps });
402
- expect(wrapper.find(".banner-video__toggle").html()).toContain("mdi:play");
450
+ wrapper = await mountSuspended(BannerVideo, { props: defaultProps });
451
+ expect(wrapper.find(".banner-video-toggle").html()).toContain("mdi:play");
403
452
  });
404
453
 
405
454
  it("renders the default pause icon once playback starts", async () => {
406
- const wrapper = await mountSuspended(BannerVideo, { props: defaultProps });
455
+ wrapper = await mountSuspended(BannerVideo, { props: defaultProps });
407
456
  await wrapper.find("video").trigger("play");
408
- expect(wrapper.find(".banner-video__toggle").html()).toContain("mdi:pause");
457
+ expect(wrapper.find(".banner-video-toggle").html()).toContain("mdi:pause");
409
458
  });
410
459
 
411
460
  it("renders a custom playIcon", async () => {
412
- const wrapper = await mountSuspended(BannerVideo, {
461
+ wrapper = await mountSuspended(BannerVideo, {
413
462
  props: { ...defaultProps, playIcon: "mdi:play-circle" },
414
463
  });
415
- expect(wrapper.find(".banner-video__toggle").html()).toContain("mdi:play-circle");
464
+ expect(wrapper.find(".banner-video-toggle").html()).toContain("mdi:play-circle");
416
465
  });
417
466
 
418
467
  it("renders a custom pauseIcon", async () => {
419
- const wrapper = await mountSuspended(BannerVideo, {
468
+ wrapper = await mountSuspended(BannerVideo, {
420
469
  props: { ...defaultProps, pauseIcon: "mdi:pause-circle" },
421
470
  });
422
471
  await wrapper.find("video").trigger("play");
423
- expect(wrapper.find(".banner-video__toggle").html()).toContain("mdi:pause-circle");
472
+ expect(wrapper.find(".banner-video-toggle").html()).toContain("mdi:pause-circle");
424
473
  });
425
474
 
426
475
  it("replaces the toggle icon via the toggle-icon slot", async () => {
427
- const wrapper = await mountSuspended(BannerVideo, {
476
+ wrapper = await mountSuspended(BannerVideo, {
428
477
  props: defaultProps,
429
478
  slots: {
430
479
  "toggle-icon": '<span class="test-custom-icon">custom</span>',
@@ -434,7 +483,7 @@ describe("BannerVideo", () => {
434
483
  });
435
484
 
436
485
  it("passes isPlaying to the toggle-icon slot", async () => {
437
- const wrapper = await mountSuspended(BannerVideo, {
486
+ wrapper = await mountSuspended(BannerVideo, {
438
487
  props: defaultProps,
439
488
  slots: {
440
489
  "toggle-icon": `<span class="test-is-playing">{{ params.isPlaying }}</span>`,
@@ -1,13 +1,21 @@
1
1
  // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
2
 
3
3
  exports[`BannerVideo > renders correct HTML structure (all props set) 1`] = `
4
- "<header class="banner-video full-bleed" data-depth="xl" style="--_aspect-ratio: 16/9; --_align-self: center; --_justify-self: center;"><video class="video" autoplay="" muted="" loop="" playsinline="" preload="auto" fetchpriority="high" poster="/images/banners/video/lake-banner.jpg" style="object-fit: cover;">
4
+ "<header class="banner-video full-bleed" data-depth="xl" style="--_aspect-ratio: 16/9; --_align-self: center; --_justify-self: center;"><video class="banner-video-media" autoplay="" muted="" loop="" playsinline="" preload="auto" fetchpriority="high" poster="/images/banners/video/lake-banner.jpg" style="object-fit: cover;">
5
5
  <source src="/images/banners/video/lake-banner.mp4" type="video/mp4">
6
- </video><img width="1280" height="720" data-nuxt-img="" srcset="/_ipx/s_1280x720/images/banners/video/lake-banner.jpg 1x, /_ipx/s_2560x1440/images/banners/video/lake-banner.jpg 2x" class="fallback" alt="Lake banner" loading="eager" fetchpriority="high" decoding="async" style="object-fit: cover; object-position: center center;" src="/_ipx/s_1280x720/images/banners/video/lake-banner.jpg"><button type="button" class="banner-video__toggle" aria-label="Play background video"><span class="iconify i-mdi:play" aria-hidden="true"></span></button></header>"
6
+ </video><img width="1280" height="720" data-nuxt-img="" srcset="/_ipx/s_1280x720/images/banners/video/lake-banner.jpg 1x, /_ipx/s_2560x1440/images/banners/video/lake-banner.jpg 2x" class="banner-video-fallback" alt="Lake banner" loading="eager" fetchpriority="high" decoding="async" style="object-fit: cover; object-position: center center;" src="/_ipx/s_1280x720/images/banners/video/lake-banner.jpg"><button type="button" data-testid="input-button" data-theme="default" data-variant="inline" data-icon-only="" class="input-button banner-video-toggle">
7
+ <!--v-if-->
8
+ <!--v-if--><span class="input-button-text sr-only">Play background video</span>
9
+ <!--v-if--><span class="input-button-icon" data-slot="icon-only"><span class="iconify i-mdi:play" aria-hidden="true"></span></span>
10
+ </button></header>"
7
11
  `;
8
12
 
9
13
  exports[`BannerVideo > renders correct HTML structure (default props) 1`] = `
10
- "<div class="banner-video" data-depth="md" style="--_aspect-ratio: 21/9; --_align-self: center; --_justify-self: center;"><video class="video" autoplay="" muted="" loop="" playsinline="" preload="auto" fetchpriority="high" poster="/images/banners/video/lake-banner.jpg" style="object-fit: cover;">
14
+ "<div class="banner-video" data-depth="md" style="--_aspect-ratio: 21/9; --_align-self: center; --_justify-self: center;"><video class="banner-video-media" autoplay="" muted="" loop="" playsinline="" preload="auto" fetchpriority="high" poster="/images/banners/video/lake-banner.jpg" style="object-fit: cover;">
11
15
  <source src="/images/banners/video/lake-banner.mp4" type="video/mp4">
12
- </video><img width="1920" height="1080" data-nuxt-img="" srcset="/_ipx/s_1920x1080/images/banners/video/lake-banner.jpg 1x, /_ipx/s_3840x2160/images/banners/video/lake-banner.jpg 2x" class="fallback" alt="" loading="eager" fetchpriority="high" decoding="async" style="object-fit: cover; object-position: center center;" src="/_ipx/s_1920x1080/images/banners/video/lake-banner.jpg"><button type="button" class="banner-video__toggle" aria-label="Play background video"><span class="iconify i-mdi:play" aria-hidden="true"></span></button></div>"
16
+ </video><img width="1920" height="1080" data-nuxt-img="" srcset="/_ipx/s_1920x1080/images/banners/video/lake-banner.jpg 1x, /_ipx/s_3840x2160/images/banners/video/lake-banner.jpg 2x" class="banner-video-fallback" alt="" loading="eager" fetchpriority="high" decoding="async" style="object-fit: cover; object-position: center center;" src="/_ipx/s_1920x1080/images/banners/video/lake-banner.jpg"><button type="button" data-testid="input-button" data-theme="default" data-variant="inline" data-icon-only="" class="input-button banner-video-toggle">
17
+ <!--v-if-->
18
+ <!--v-if--><span class="input-button-text sr-only">Play background video</span>
19
+ <!--v-if--><span class="input-button-icon" data-slot="icon-only"><span class="iconify i-mdi:play" aria-hidden="true"></span></span>
20
+ </button></div>"
13
21
  `;