srcdev-nuxt-components 9.6.11 → 9.6.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/rules/consumer/ids-from-useid.md +2 -1
- package/.claude/rules/consumer/vfor-keys-from-data.md +14 -0
- package/.claude/skills/component-compliance-checklist.md +24 -1
- package/.claude/skills/component-naming.md +1 -0
- package/.claude/skills/components/banner-video.md +24 -30
- package/.claude/skills/components/content-docs.md +18 -5
- package/.claude/skills/components/display-dialog.md +16 -6
- package/.claude/skills/components/{display-tooltip-defined.md → display-tooltip-rich.md} +40 -42
- package/.claude/skills/components/display-tooltip.md +17 -6
- package/.claude/skills/components/form-field.md +15 -0
- package/.claude/skills/components/form-fieldset.md +18 -2
- package/.claude/skills/components/marquee-scroller.md +51 -14
- package/.claude/skills/components/skip-links.md +16 -0
- package/.claude/skills/composable-tooltips-guide.md +4 -4
- package/.claude/skills/index.md +6 -6
- package/.claude/skills/testing-add-unit-test.md +8 -0
- package/.vscode/srcdev-component-banner-video.code-snippets +38 -25
- package/.vscode/srcdev-component-content-docs.code-snippets +69 -60
- package/.vscode/srcdev-component-display-dialog.code-snippets +7 -5
- package/.vscode/srcdev-component-display-tooltip-rich.code-snippets +50 -0
- package/.vscode/srcdev-component-display-tooltip.code-snippets +31 -0
- package/.vscode/srcdev-component-form-field.code-snippets +7 -6
- package/.vscode/srcdev-component-form-fieldset.code-snippets +12 -12
- package/.vscode/srcdev-component-marquee-scroller.code-snippets +31 -28
- package/.vscode/srcdev-component-skip-links.code-snippets +1 -1
- package/app/components/01.atoms/animations/marquee-scroller/CONSUMER-STYLING.md +77 -54
- package/app/components/01.atoms/animations/marquee-scroller/MarqueeScroller.vue +103 -91
- package/app/components/01.atoms/animations/marquee-scroller/stories/MarqueeScroller.stories.ts +132 -3
- package/app/components/01.atoms/animations/marquee-scroller/tests/MarqueeScroller.spec.ts +190 -102
- package/app/components/01.atoms/animations/marquee-scroller/tests/__snapshots__/MarqueeScroller.spec.ts.snap +17 -13
- package/app/components/01.atoms/banner-video/BannerVideo.vue +51 -19
- package/app/components/01.atoms/banner-video/CONSUMER-STYLING.md +59 -40
- package/app/components/01.atoms/banner-video/stories/BannerVideo.stories.ts +88 -1
- package/app/components/01.atoms/banner-video/tests/BannerVideo.spec.ts +127 -78
- package/app/components/01.atoms/banner-video/tests/__snapshots__/BannerVideo.spec.ts.snap +12 -4
- package/app/components/01.atoms/content-wrappers/docs-pages/CONSUMER-STYLING.md +42 -14
- package/app/components/01.atoms/content-wrappers/docs-pages/ContentDocs.vue +86 -112
- package/app/components/01.atoms/content-wrappers/docs-pages/stories/ContentDocs.stories.ts +134 -8
- package/app/components/01.atoms/content-wrappers/docs-pages/tests/ContentDocs.spec.ts +131 -65
- package/app/components/01.atoms/display-dialog/CONSUMER-STYLING.md +28 -3
- package/app/components/01.atoms/display-dialog/DisplayDialog.vue +39 -31
- package/app/components/01.atoms/display-dialog/stories/DisplayDialog.stories.ts +92 -1
- package/app/components/01.atoms/display-dialog/tests/DisplayDialog.spec.ts +87 -41
- package/app/components/01.atoms/display-dialog/tests/__snapshots__/DisplayDialog.spec.ts.snap +5 -1
- package/app/components/01.atoms/display-tooltip/CONSUMER-STYLING.md +70 -24
- package/app/components/01.atoms/display-tooltip/DisplayTooltip.vue +24 -12
- package/app/components/01.atoms/display-tooltip/stories/DisplayTooltip.stories.ts +89 -2
- package/app/components/01.atoms/display-tooltip/tests/DisplayTooltip.spec.ts +65 -18
- package/app/components/01.atoms/display-tooltip/tests/__snapshots__/DisplayTooltip.spec.ts.snap +3 -3
- package/app/components/01.atoms/navigation/skip-links/CONSUMER-STYLING.md +65 -19
- package/app/components/01.atoms/navigation/skip-links/SkipLinks.vue +20 -14
- package/app/components/01.atoms/navigation/skip-links/stories/SkipLinks.stories.ts +89 -20
- package/app/components/01.atoms/navigation/skip-links/tests/SkipLinks.spec.ts +70 -10
- package/app/components/02.molecules/display-tooltip-rich/CONSUMER-STYLING.md +110 -0
- package/app/components/02.molecules/display-tooltip-rich/DisplayTooltipRich.vue +148 -0
- package/app/components/02.molecules/display-tooltip-rich/stories/DisplayTooltipRich.stories.ts +254 -0
- package/app/components/02.molecules/display-tooltip-rich/tests/DisplayTooltipRich.spec.ts +156 -0
- package/app/components/02.molecules/display-tooltip-rich/tests/__snapshots__/DisplayTooltipRich.spec.ts.snap +22 -0
- package/app/components/02.molecules/navigation/deep-expanding-menu/DeepExpandingMenu.vue +1 -0
- package/app/components/02.molecules/navigation/deep-expanding-menu/tests/__snapshots__/DeepExpandingMenu.spec.ts.snap +1 -1
- package/app/components/03.organisms/image-galleries/carousel-flip/CarouselFlip.vue +1 -0
- package/app/components/05.forms/form-field/CONSUMER-STYLING.md +7 -0
- package/app/components/05.forms/form-field/stories/FormField.stories.ts +116 -2
- package/app/components/05.forms/form-fieldset/CONSUMER-STYLING.md +11 -1
- package/app/components/05.forms/form-fieldset/FormFieldset.vue +4 -2
- package/app/components/05.forms/form-fieldset/stories/FormFieldset.stories.ts +68 -4
- package/app/components/05.forms/form-fieldset/tests/FormFieldset.spec.ts +10 -0
- package/app/composables/tests/useTooltips.spec.ts +3 -3
- package/app/types/components/index.ts +1 -1
- package/package.json +1 -1
- package/.vscode/srcdev-component-display-tooltip-defined.code-snippets +0 -29
- package/app/components/02.molecules/display-tooltip-defined/CONSUMER-STYLING.md +0 -53
- package/app/components/02.molecules/display-tooltip-defined/DisplayTooltipDefined.vue +0 -117
- package/app/components/02.molecules/display-tooltip-defined/stories/DisplayTooltipDefined.stories.ts +0 -129
- package/app/components/02.molecules/display-tooltip-defined/tests/DisplayTooltipDefined.spec.ts +0 -85
- package/app/components/02.molecules/display-tooltip-defined/tests/__snapshots__/DisplayTooltipDefined.spec.ts.snap +0 -17
- /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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
134
|
-
expect(wrapper.find(".banner-
|
|
144
|
+
wrapper = await mountSuspended(BannerVideo, { props: defaultProps });
|
|
145
|
+
expect(wrapper.find(".banner-video-toggle").exists()).toBe(true);
|
|
135
146
|
});
|
|
136
147
|
|
|
137
|
-
it("toggle
|
|
138
|
-
|
|
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-
|
|
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
|
-
|
|
145
|
-
expect(wrapper.find(".banner-
|
|
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
|
-
|
|
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-
|
|
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
|
-
|
|
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-
|
|
175
|
+
await wrapper.find(".banner-video-toggle").trigger("click");
|
|
165
176
|
expect(play).toHaveBeenCalled();
|
|
166
177
|
});
|
|
167
178
|
|
|
168
|
-
it("updates
|
|
169
|
-
|
|
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-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
402
|
-
expect(wrapper.find(".banner-
|
|
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
|
-
|
|
455
|
+
wrapper = await mountSuspended(BannerVideo, { props: defaultProps });
|
|
407
456
|
await wrapper.find("video").trigger("play");
|
|
408
|
-
expect(wrapper.find(".banner-
|
|
457
|
+
expect(wrapper.find(".banner-video-toggle").html()).toContain("mdi:pause");
|
|
409
458
|
});
|
|
410
459
|
|
|
411
460
|
it("renders a custom playIcon", async () => {
|
|
412
|
-
|
|
461
|
+
wrapper = await mountSuspended(BannerVideo, {
|
|
413
462
|
props: { ...defaultProps, playIcon: "mdi:play-circle" },
|
|
414
463
|
});
|
|
415
|
-
expect(wrapper.find(".banner-
|
|
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
|
-
|
|
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-
|
|
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
|
-
|
|
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
|
-
|
|
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"
|
|
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"
|
|
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
|
`;
|