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.
- package/.claude/settings.json +2 -1
- package/.claude/settings.local.json +2 -2
- package/.claude/skills/components/capture-qr-code.md +84 -0
- package/.claude/skills/components/decode-qr-code.md +84 -0
- package/.claude/skills/components/display-qr-code.md +64 -0
- package/.claude/skills/index.md +4 -2
- package/app/components/01.atoms/qr-code/DisplayQrCode.vue +50 -0
- package/app/components/01.atoms/qr-code/stories/DisplayQrCode.stories.ts +206 -0
- package/app/components/01.atoms/qr-code/tests/DisplayQrCode.spec.ts +139 -0
- package/app/components/{qr-code → 02.molecules/qr-code}/CaptureQrCode.vue +18 -59
- package/app/components/{qr-code → 02.molecules/qr-code}/DecodeQrCode.vue +11 -35
- package/app/components/02.molecules/qr-code/stories/QrCode.stories.ts +101 -0
- package/app/components/02.molecules/qr-code/tests/CaptureQrCode.spec.ts +212 -0
- package/app/components/02.molecules/qr-code/tests/DecodeQrCode.spec.ts +145 -0
- package/app/layouts/default.vue +0 -1
- package/app/pages/ui/qr-code/[componentName].vue +3 -3
- package/package.json +1 -1
- package/.claude/skills/components/scroll-parallax-section.md +0 -148
- package/app/components/01.atoms/scroll-parallax-section/ScrollParallaxSection.vue +0 -108
- package/app/components/01.atoms/scroll-parallax-section/stories/ScrollParallaxSection.stories.ts +0 -151
- package/app/components/01.atoms/scroll-parallax-section/tests/ScrollParallaxSection.spec.ts +0 -91
- package/app/components/qr-code/DisplayQrCode.vue +0 -53
- package/app/components/qr-code/stories/QrCode.stories.ts +0 -933
- 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="
|
|
10
|
-
<
|
|
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
|
|
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
|
-
<
|
|
22
|
-
|
|
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
|
-
|
|
33
|
-
styleClassPassthrough
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
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
|
-
|
|
121
|
-
allVideoElements.forEach((video) => {
|
|
91
|
+
document.querySelectorAll("video").forEach((video) => {
|
|
122
92
|
if (video.srcObject) {
|
|
123
93
|
const stream = video.srcObject as MediaStream
|
|
124
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
30
|
-
styleClassPassthrough
|
|
31
|
-
|
|
32
|
-
|
|
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
|
+
});
|