srcdev-nuxt-components 9.1.36 → 9.1.37

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 (24) hide show
  1. package/.claude/settings.json +2 -1
  2. package/.claude/settings.local.json +2 -2
  3. package/.claude/skills/components/capture-qr-code.md +84 -0
  4. package/.claude/skills/components/decode-qr-code.md +84 -0
  5. package/.claude/skills/components/display-qr-code.md +64 -0
  6. package/.claude/skills/index.md +4 -2
  7. package/app/components/01.atoms/qr-code/DisplayQrCode.vue +50 -0
  8. package/app/components/01.atoms/qr-code/stories/DisplayQrCode.stories.ts +206 -0
  9. package/app/components/01.atoms/qr-code/tests/DisplayQrCode.spec.ts +139 -0
  10. package/app/components/{qr-code → 02.molecules/qr-code}/CaptureQrCode.vue +18 -59
  11. package/app/components/{qr-code → 02.molecules/qr-code}/DecodeQrCode.vue +11 -35
  12. package/app/components/02.molecules/qr-code/stories/QrCode.stories.ts +101 -0
  13. package/app/components/02.molecules/qr-code/tests/CaptureQrCode.spec.ts +212 -0
  14. package/app/components/02.molecules/qr-code/tests/DecodeQrCode.spec.ts +145 -0
  15. package/app/layouts/default.vue +0 -1
  16. package/app/pages/ui/qr-code/[componentName].vue +3 -3
  17. package/package.json +1 -1
  18. package/.claude/skills/components/scroll-parallax-section.md +0 -148
  19. package/app/components/01.atoms/scroll-parallax-section/ScrollParallaxSection.vue +0 -108
  20. package/app/components/01.atoms/scroll-parallax-section/stories/ScrollParallaxSection.stories.ts +0 -151
  21. package/app/components/01.atoms/scroll-parallax-section/tests/ScrollParallaxSection.spec.ts +0 -91
  22. package/app/components/qr-code/DisplayQrCode.vue +0 -53
  23. package/app/components/qr-code/stories/QrCode.stories.ts +0 -933
  24. package/app/pages/ui/scroll-parallax-section.vue +0 -65
@@ -1,27 +1,21 @@
1
1
  <template>
2
2
  <div class="capture-qr-stream" :class="[elementClasses]">
3
- <h2>Capture QR Code</h2>
4
3
  <div v-if="!state.error">
5
4
  <QrcodeStream v-if="state.cameraOn" ref="qrcodeStreamRef" @error="onError" @detect="onDetect" />
6
5
  <div v-else class="camera-stopped">
7
6
  <p>Camera stopped</p>
8
7
  </div>
9
- <div class="pt-4">
10
- <h5>Scanned QRCodes:</h5>
11
- <ul v-if="result" class="list-disc pl-4">
8
+ <div v-if="result?.length" class="scanned-results">
9
+ <ul>
12
10
  <li v-for="(r, i) in result" :key="i">
13
- <span class="text-wrap wrap-anywhere">
14
- {{ r }}
15
- </span>
11
+ <span>{{ r }}</span>
16
12
  </li>
17
13
  </ul>
18
14
  </div>
19
15
  </div>
20
- <div v-else>
21
- <h3>
22
- {{ state.errorMsg }}
23
- </h3>
24
- <button @click="resetCamera">reset</button>
16
+ <div v-else class="camera-error">
17
+ <p>{{ state.errorMsg }}</p>
18
+ <button @click="resetCamera">Reset camera</button>
25
19
  </div>
26
20
  </div>
27
21
  </template>
@@ -29,12 +23,13 @@
29
23
  <script setup lang="ts">
30
24
  import type { DetectedBarcode } from "nuxt-qrcode"
31
25
 
32
- const props = defineProps({
33
- styleClassPassthrough: {
34
- type: [String, Array] as PropType<string | string[]>,
35
- default: () => [],
36
- },
37
- })
26
+ interface Props {
27
+ styleClassPassthrough?: string | string[];
28
+ }
29
+
30
+ const props = withDefaults(defineProps<Props>(), {
31
+ styleClassPassthrough: () => [],
32
+ });
38
33
 
39
34
  const qrcodeStreamRef = ref()
40
35
  const result = ref<string[]>()
@@ -44,9 +39,7 @@ const state = reactive({
44
39
  cameraOn: true,
45
40
  })
46
41
 
47
- // Reset camera state when component mounts
48
42
  onMounted(() => {
49
- // Reset to default state on mount
50
43
  state.cameraOn = true
51
44
  state.error = false
52
45
  state.errorMsg = ""
@@ -61,28 +54,13 @@ onMounted(() => {
61
54
 
62
55
  document.addEventListener("visibilitychange", handleVisibilityChange)
63
56
 
64
- // Cleanup listener on unmount
65
57
  onBeforeUnmount(() => {
66
58
  document.removeEventListener("visibilitychange", handleVisibilityChange)
67
59
  })
68
60
  })
69
61
 
70
62
  function onDetect(detectedCodes: DetectedBarcode[]) {
71
- result.value = detectedCodes.map((code) => {
72
- // toast.add({
73
- // title: 'Detected',
74
- // description: `Value: ${code.rawValue}`,
75
- // actions: [
76
- // {
77
- // label: 'Copy',
78
- // onClick: () => {
79
- // navigator.clipboard.writeText(code.rawValue)
80
- // },
81
- // },
82
- // ],
83
- // })
84
- return code.rawValue
85
- })
63
+ result.value = detectedCodes.map((code) => code.rawValue)
86
64
  }
87
65
 
88
66
  function onError(err: Error) {
@@ -95,19 +73,13 @@ function resetCamera() {
95
73
  state.cameraOn = true
96
74
  }
97
75
 
98
- // Function to stop all media streams
99
76
  function stopAllMediaStreams() {
100
- // Stop streams via the QrcodeStream component ref
101
77
  if (qrcodeStreamRef.value) {
102
78
  try {
103
- // Try to access the video element and stop its tracks
104
79
  const videoElement = qrcodeStreamRef.value.$el?.querySelector("video")
105
80
  if (videoElement && videoElement.srcObject) {
106
81
  const stream = videoElement.srcObject as MediaStream
107
- const tracks = stream.getTracks()
108
- tracks.forEach((track) => {
109
- track.stop()
110
- })
82
+ stream.getTracks().forEach((track) => track.stop())
111
83
  videoElement.srcObject = null
112
84
  }
113
85
  } catch (error) {
@@ -115,16 +87,11 @@ function stopAllMediaStreams() {
115
87
  }
116
88
  }
117
89
 
118
- // Global cleanup: Find all video elements and stop their streams
119
90
  try {
120
- const allVideoElements = document.querySelectorAll("video")
121
- allVideoElements.forEach((video) => {
91
+ document.querySelectorAll("video").forEach((video) => {
122
92
  if (video.srcObject) {
123
93
  const stream = video.srcObject as MediaStream
124
- const tracks = stream.getTracks()
125
- tracks.forEach((track) => {
126
- track.stop()
127
- })
94
+ stream.getTracks().forEach((track) => track.stop())
128
95
  video.srcObject = null
129
96
  }
130
97
  })
@@ -133,39 +100,31 @@ function stopAllMediaStreams() {
133
100
  }
134
101
  }
135
102
 
136
- // Watch for camera state changes
137
103
  watch(
138
104
  () => state.cameraOn,
139
105
  (newValue) => {
140
106
  if (!newValue) {
141
- // Wait a tick for the component to unmount, then clean up
142
- nextTick(() => {
143
- stopAllMediaStreams()
144
- })
107
+ nextTick(() => stopAllMediaStreams())
145
108
  }
146
109
  }
147
110
  )
148
111
 
149
- // Stop camera when component is unmounted (e.g., route change)
150
112
  onBeforeUnmount(() => {
151
113
  state.cameraOn = false
152
114
  stopAllMediaStreams()
153
115
  })
154
116
 
155
- // Also handle dynamic component switching (like in [componentName].vue)
156
117
  onDeactivated(() => {
157
118
  state.cameraOn = false
158
119
  stopAllMediaStreams()
159
120
  })
160
121
 
161
122
  onActivated(() => {
162
- // Reset state when component becomes active again
163
123
  state.cameraOn = true
164
124
  state.error = false
165
125
  state.errorMsg = ""
166
126
  })
167
127
 
168
- // Use Nuxt's navigation guard to stop camera before route changes
169
128
  onBeforeRouteLeave(() => {
170
129
  state.cameraOn = false
171
130
  stopAllMediaStreams()
@@ -1,22 +1,11 @@
1
1
  <template>
2
2
  <div class="decode-qr-code" :class="[elementClasses]">
3
- <h2>Upload QR Code</h2>
4
3
  <QrcodeCapture class="qr-code-capture" @detect="onDetect" />
5
-
6
- <h2>Drop QR Code</h2>
7
4
  <QrcodeDropZone class="qr-code-dropzone" @detect="onDetect" @dragover="onDropping" />
8
-
9
- <div v-if="isDropping">
10
- <h5>Scanned QRCodes (Dropped): {{ isDropping ? "Dropping..." : "" }}</h5>
11
- </div>
12
-
13
- <div class="pt-4">
14
- <h5>Scanned QRCodes:</h5>
15
- <ul v-if="result" class="list-disc pl-4">
5
+ <div v-if="result?.length" class="scanned-results">
6
+ <ul>
16
7
  <li v-for="(r, i) in result" :key="i">
17
- <span class="text-wrap wrap-anywhere">
18
- {{ r }}
19
- </span>
8
+ <span>{{ r }}</span>
20
9
  </li>
21
10
  </ul>
22
11
  </div>
@@ -26,12 +15,13 @@
26
15
  <script setup lang="ts">
27
16
  import type { DetectedBarcode } from "nuxt-qrcode"
28
17
 
29
- const props = defineProps({
30
- styleClassPassthrough: {
31
- type: [String, Array] as PropType<string | string[]>,
32
- default: () => [],
33
- },
34
- })
18
+ interface Props {
19
+ styleClassPassthrough?: string | string[];
20
+ }
21
+
22
+ const props = withDefaults(defineProps<Props>(), {
23
+ styleClassPassthrough: () => [],
24
+ });
35
25
 
36
26
  const result = ref<string[]>()
37
27
  const isDropping = ref(false)
@@ -41,21 +31,7 @@ function onDropping(dropping: boolean) {
41
31
  }
42
32
 
43
33
  function onDetect(detectedCodes: DetectedBarcode[]) {
44
- result.value = detectedCodes.map((code) => {
45
- // toast.add({
46
- // title: 'Detected',
47
- // description: `Value: ${code.rawValue}`,
48
- // actions: [
49
- // {
50
- // label: 'Copy',
51
- // onClick: () => {
52
- // navigator.clipboard.writeText(code.rawValue)
53
- // },
54
- // },
55
- // ],
56
- // })
57
- return code.rawValue
58
- })
34
+ result.value = detectedCodes.map((code) => code.rawValue)
59
35
  }
60
36
 
61
37
  const { elementClasses } = useStyleClassPassthrough(props.styleClassPassthrough)
@@ -0,0 +1,101 @@
1
+ import type { Meta, StoryFn } from "@nuxtjs/storybook";
2
+ import CaptureQrCodeComponent from "../CaptureQrCode.vue";
3
+ import DecodeQrCodeComponent from "../DecodeQrCode.vue";
4
+
5
+ // ===== CAPTURE QR CODE =====
6
+
7
+ const CaptureQrCodeMeta: Meta<typeof CaptureQrCodeComponent> = {
8
+ title: "Molecules/QR Code/CaptureQrCode",
9
+ component: CaptureQrCodeComponent,
10
+ argTypes: {
11
+ styleClassPassthrough: { table: { disable: true } },
12
+ },
13
+ args: {
14
+ styleClassPassthrough: [],
15
+ },
16
+ parameters: {
17
+ docs: {
18
+ description: {
19
+ component:
20
+ "Interactive QR code scanner using device camera. Requires camera permissions to function properly. Features automatic detection, error handling, and camera management.",
21
+ },
22
+ },
23
+ },
24
+ };
25
+
26
+ const CaptureTemplate: StoryFn<typeof CaptureQrCodeComponent> = (args) => ({
27
+ components: { CaptureQrCodeComponent },
28
+ setup() {
29
+ return { args };
30
+ },
31
+ template: `
32
+ <div style="padding: 40px; max-width: 600px; margin: 0 auto;">
33
+ <div style="background: #f8fafc; padding: 20px; border-radius: 8px; margin-bottom: 20px;">
34
+ <h3 style="margin: 0 0 10px 0; color: #374151;">Live Camera QR Scanner</h3>
35
+ <p style="margin: 0 0 10px 0; color: #6b7280; font-size: 14px;">
36
+ This component uses your device camera to scan QR codes in real-time.
37
+ Make sure to allow camera access when prompted.
38
+ </p>
39
+ <div style="background: #fff3cd; padding: 12px; border-radius: 4px; border: 1px solid #ffeaa7;">
40
+ <p style="margin: 0; color: #856404; font-size: 12px;">
41
+ <strong>Features:</strong> Automatic detection, multiple code scanning, camera management, error recovery
42
+ </p>
43
+ </div>
44
+ </div>
45
+ <CaptureQrCodeComponent :style-class-passthrough="args.styleClassPassthrough" />
46
+ </div>
47
+ `,
48
+ });
49
+
50
+ export const CaptureDefault = CaptureTemplate.bind({});
51
+ export const CaptureDefaultMeta = CaptureQrCodeMeta;
52
+
53
+ // ===== DECODE QR CODE =====
54
+
55
+ const DecodeQrCodeMeta: Meta<typeof DecodeQrCodeComponent> = {
56
+ title: "Molecules/QR Code/DecodeQrCode",
57
+ component: DecodeQrCodeComponent,
58
+ argTypes: {
59
+ styleClassPassthrough: { table: { disable: true } },
60
+ },
61
+ args: {
62
+ styleClassPassthrough: [],
63
+ },
64
+ parameters: {
65
+ docs: {
66
+ description: {
67
+ component:
68
+ "Upload or drag-and-drop QR code images to decode their content. Supports various image formats including PNG, JPEG, WEBP, and more.",
69
+ },
70
+ },
71
+ },
72
+ };
73
+
74
+ const DecodeTemplate: StoryFn<typeof DecodeQrCodeComponent> = (args) => ({
75
+ components: { DecodeQrCodeComponent },
76
+ setup() {
77
+ return { args };
78
+ },
79
+ template: `
80
+ <div style="padding: 40px; max-width: 600px; margin: 0 auto;">
81
+ <div style="background: #f8fafc; padding: 20px; border-radius: 8px; margin-bottom: 20px;">
82
+ <h3 style="margin: 0 0 10px 0; color: #374151;">QR Image Decoder</h3>
83
+ <p style="margin: 0 0 10px 0; color: #6b7280; font-size: 14px;">
84
+ Upload or drag QR code images to extract their content.
85
+ Supports PNG, JPEG, WEBP, and other common image formats.
86
+ </p>
87
+ <div style="background: #d1ecf1; padding: 12px; border-radius: 4px; border: 1px solid #bee5eb;">
88
+ <p style="margin: 0; color: #0c5460; font-size: 12px;">
89
+ <strong>Supported formats:</strong> PNG, JPEG, WEBP, BMP, GIF • <strong>Methods:</strong> File upload, drag & drop
90
+ </p>
91
+ </div>
92
+ </div>
93
+ <DecodeQrCodeComponent :style-class-passthrough="args.styleClassPassthrough" />
94
+ </div>
95
+ `,
96
+ });
97
+
98
+ export const DecodeDefault = DecodeTemplate.bind({});
99
+ export const DecodeDefaultMeta = DecodeQrCodeMeta;
100
+
101
+ export default CaptureQrCodeMeta;
@@ -0,0 +1,212 @@
1
+ import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
2
+ import { mountSuspended } from "@nuxt/test-utils/runtime";
3
+ import { nextTick } from "vue";
4
+ import CaptureQrCode from "../CaptureQrCode.vue";
5
+
6
+ interface CaptureVM {
7
+ state: { error: boolean; errorMsg: string; cameraOn: boolean };
8
+ result: string[] | undefined;
9
+ onDetect: (codes: { rawValue: string }[]) => void;
10
+ onError: (err: Error) => void;
11
+ resetCamera: () => void;
12
+ }
13
+
14
+ describe("CaptureQrCode", () => {
15
+ // QrcodeStream (from nuxt-qrcode) runs canvas-based detection internally.
16
+ // jsdom defines HTMLCanvasElement but returns null for getContext, which
17
+ // causes the library to throw. We provide a minimal non-null mock.
18
+ beforeEach(() => {
19
+ vi.spyOn(HTMLCanvasElement.prototype, "getContext").mockReturnValue({} as any);
20
+ });
21
+
22
+ afterEach(() => {
23
+ vi.restoreAllMocks();
24
+ });
25
+
26
+ // ─── Mount ───────────────────────────────────────────────────────────────
27
+
28
+ it("mounts without error", async () => {
29
+ const wrapper = await mountSuspended(CaptureQrCode);
30
+ expect(wrapper.vm).toBeTruthy();
31
+ });
32
+
33
+ // ─── Root element ─────────────────────────────────────────────────────────
34
+
35
+ it("renders as a div", async () => {
36
+ const wrapper = await mountSuspended(CaptureQrCode);
37
+ expect(wrapper.element.tagName).toBe("DIV");
38
+ });
39
+
40
+ it("always has the capture-qr-stream class", async () => {
41
+ const wrapper = await mountSuspended(CaptureQrCode);
42
+ expect(wrapper.classes()).toContain("capture-qr-stream");
43
+ });
44
+
45
+ // ─── Initial state ────────────────────────────────────────────────────────
46
+
47
+ it("has camera on by default", async () => {
48
+ const wrapper = await mountSuspended(CaptureQrCode);
49
+ const vm = wrapper.vm as unknown as CaptureVM;
50
+ expect(vm.state.cameraOn).toBe(true);
51
+ });
52
+
53
+ it("does not show camera-stopped on mount", async () => {
54
+ const wrapper = await mountSuspended(CaptureQrCode);
55
+ expect(wrapper.find(".camera-stopped").exists()).toBe(false);
56
+ });
57
+
58
+ it("does not show scanned results initially", async () => {
59
+ const wrapper = await mountSuspended(CaptureQrCode);
60
+ expect(wrapper.find(".scanned-results").exists()).toBe(false);
61
+ });
62
+
63
+ it("does not show the error state initially", async () => {
64
+ const wrapper = await mountSuspended(CaptureQrCode);
65
+ expect(wrapper.find(".camera-error").exists()).toBe(false);
66
+ });
67
+
68
+ // ─── Camera off ───────────────────────────────────────────────────────────
69
+
70
+ it("hides the stream and shows camera-stopped when cameraOn is false", async () => {
71
+ const wrapper = await mountSuspended(CaptureQrCode);
72
+ const vm = wrapper.vm as unknown as CaptureVM;
73
+ vm.state.cameraOn = false;
74
+ await nextTick();
75
+ expect(vm.state.cameraOn).toBe(false);
76
+ expect(wrapper.find(".camera-stopped").exists()).toBe(true);
77
+ });
78
+
79
+ // ─── Error state ──────────────────────────────────────────────────────────
80
+
81
+ it("shows camera-error when onError is called", async () => {
82
+ const wrapper = await mountSuspended(CaptureQrCode);
83
+ const vm = wrapper.vm as unknown as CaptureVM;
84
+ vm.onError(new Error("Permission denied"));
85
+ await nextTick();
86
+ expect(wrapper.find(".camera-error").exists()).toBe(true);
87
+ });
88
+
89
+ it("shows the error message in the error state", async () => {
90
+ const wrapper = await mountSuspended(CaptureQrCode);
91
+ const vm = wrapper.vm as unknown as CaptureVM;
92
+ vm.onError(new Error("Permission denied"));
93
+ await nextTick();
94
+ expect(wrapper.find(".camera-error p").text()).toContain("Permission denied");
95
+ });
96
+
97
+ it("sets state.error when onError is called, hiding the stream", async () => {
98
+ const wrapper = await mountSuspended(CaptureQrCode);
99
+ const vm = wrapper.vm as unknown as CaptureVM;
100
+ vm.onError(new Error("test"));
101
+ await nextTick();
102
+ expect(vm.state.error).toBe(true);
103
+ expect(wrapper.find(".camera-error").exists()).toBe(true);
104
+ });
105
+
106
+ it("shows a reset button in the error state", async () => {
107
+ const wrapper = await mountSuspended(CaptureQrCode);
108
+ const vm = wrapper.vm as unknown as CaptureVM;
109
+ vm.onError(new Error("test"));
110
+ await nextTick();
111
+ expect(wrapper.find(".camera-error button").exists()).toBe(true);
112
+ });
113
+
114
+ // ─── Reset camera ─────────────────────────────────────────────────────────
115
+
116
+ it("clears the error state when resetCamera is called", async () => {
117
+ const wrapper = await mountSuspended(CaptureQrCode);
118
+ const vm = wrapper.vm as unknown as CaptureVM;
119
+ vm.onError(new Error("test"));
120
+ await nextTick();
121
+ vm.resetCamera();
122
+ await nextTick();
123
+ expect(wrapper.find(".camera-error").exists()).toBe(false);
124
+ expect(vm.state.cameraOn).toBe(true);
125
+ });
126
+
127
+ it("clears the error state when the reset button is clicked", async () => {
128
+ const wrapper = await mountSuspended(CaptureQrCode);
129
+ const vm = wrapper.vm as unknown as CaptureVM;
130
+ vm.onError(new Error("test"));
131
+ await nextTick();
132
+ await wrapper.find(".camera-error button").trigger("click");
133
+ expect(wrapper.find(".camera-error").exists()).toBe(false);
134
+ });
135
+
136
+ // ─── Detect ───────────────────────────────────────────────────────────────
137
+
138
+ it("shows scanned results after onDetect is called", async () => {
139
+ const wrapper = await mountSuspended(CaptureQrCode);
140
+ const vm = wrapper.vm as unknown as CaptureVM;
141
+ vm.onDetect([{ rawValue: "https://example.com" }]);
142
+ await nextTick();
143
+ expect(wrapper.find(".scanned-results").exists()).toBe(true);
144
+ });
145
+
146
+ it("shows the detected value in the results list", async () => {
147
+ const wrapper = await mountSuspended(CaptureQrCode);
148
+ const vm = wrapper.vm as unknown as CaptureVM;
149
+ vm.onDetect([{ rawValue: "https://example.com" }]);
150
+ await nextTick();
151
+ expect(wrapper.find(".scanned-results").text()).toContain("https://example.com");
152
+ });
153
+
154
+ it("shows all detected values when multiple codes are scanned", async () => {
155
+ const wrapper = await mountSuspended(CaptureQrCode);
156
+ const vm = wrapper.vm as unknown as CaptureVM;
157
+ vm.onDetect([{ rawValue: "https://one.com" }, { rawValue: "https://two.com" }]);
158
+ await nextTick();
159
+ const items = wrapper.findAll(".scanned-results li");
160
+ expect(items).toHaveLength(2);
161
+ expect(items[0]!.text()).toBe("https://one.com");
162
+ expect(items[1]!.text()).toBe("https://two.com");
163
+ });
164
+
165
+ it("replaces results on subsequent scans", async () => {
166
+ const wrapper = await mountSuspended(CaptureQrCode);
167
+ const vm = wrapper.vm as unknown as CaptureVM;
168
+ vm.onDetect([{ rawValue: "first" }]);
169
+ await nextTick();
170
+ vm.onDetect([{ rawValue: "second" }]);
171
+ await nextTick();
172
+ const items = wrapper.findAll(".scanned-results li");
173
+ expect(items).toHaveLength(1);
174
+ expect(items[0]!.text()).toBe("second");
175
+ });
176
+
177
+ // ─── Visibility change ────────────────────────────────────────────────────
178
+
179
+ it("stops the camera when the page becomes hidden", async () => {
180
+ const wrapper = await mountSuspended(CaptureQrCode);
181
+ Object.defineProperty(document, "hidden", { get: () => true, configurable: true });
182
+ document.dispatchEvent(new Event("visibilitychange"));
183
+ await nextTick();
184
+ expect(wrapper.find("qrcodestream").exists()).toBe(false);
185
+ Object.defineProperty(document, "hidden", { get: () => false, configurable: true });
186
+ });
187
+
188
+ // ─── styleClassPassthrough ────────────────────────────────────────────────
189
+
190
+ it("applies a single styleClassPassthrough string", async () => {
191
+ const wrapper = await mountSuspended(CaptureQrCode, {
192
+ props: { styleClassPassthrough: "custom-class" },
193
+ });
194
+ expect(wrapper.classes()).toContain("custom-class");
195
+ });
196
+
197
+ it("applies multiple styleClassPassthrough classes from an array", async () => {
198
+ const wrapper = await mountSuspended(CaptureQrCode, {
199
+ props: { styleClassPassthrough: ["class-a", "class-b"] },
200
+ });
201
+ expect(wrapper.classes()).toContain("class-a");
202
+ expect(wrapper.classes()).toContain("class-b");
203
+ });
204
+
205
+ it("retains capture-qr-stream class alongside styleClassPassthrough", async () => {
206
+ const wrapper = await mountSuspended(CaptureQrCode, {
207
+ props: { styleClassPassthrough: "extra" },
208
+ });
209
+ expect(wrapper.classes()).toContain("capture-qr-stream");
210
+ expect(wrapper.classes()).toContain("extra");
211
+ });
212
+ });