@happyvertical/smrt-ui 0.52.0 → 0.53.0
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/AGENTS.md +11 -1
- package/README.md +300 -2
- package/dist/components/data/DataTable.svelte +60 -15
- package/dist/components/data/DataTable.svelte.d.ts.map +1 -1
- package/dist/components/data/DataTableResponsive.d.ts +4 -0
- package/dist/components/data/DataTableResponsive.d.ts.map +1 -0
- package/dist/components/data/DataTableResponsive.js +22 -0
- package/dist/components/data/__tests__/DataTableResponsive.test.js +80 -0
- package/dist/components/data/__tests__/responsive-table.fixture.svelte +29 -0
- package/dist/components/data/__tests__/responsive-table.fixture.svelte.d.ts +7 -0
- package/dist/components/data/__tests__/responsive-table.fixture.svelte.d.ts.map +1 -0
- package/dist/components/data/types.d.ts +8 -0
- package/dist/components/data/types.d.ts.map +1 -1
- package/dist/components/display/DateDisplay.svelte +76 -14
- package/dist/components/display/DateDisplay.svelte.d.ts +2 -0
- package/dist/components/display/DateDisplay.svelte.d.ts.map +1 -1
- package/dist/components/display/Icon.svelte +3 -16
- package/dist/components/display/Icon.svelte.d.ts +0 -9
- package/dist/components/display/Icon.svelte.d.ts.map +1 -1
- package/dist/components/display/StatusBadge.svelte +29 -2
- package/dist/components/display/StatusBadge.svelte.d.ts +4 -2
- package/dist/components/display/StatusBadge.svelte.d.ts.map +1 -1
- package/dist/components/display/__tests__/DateDisplay.test.js +82 -1
- package/dist/components/display/__tests__/Icon.test.js +132 -2
- package/dist/components/display/__tests__/StatusBadge.test.js +78 -0
- package/dist/components/display/icons.svelte.d.ts +11 -0
- package/dist/components/display/icons.svelte.d.ts.map +1 -0
- package/dist/components/display/icons.svelte.js +60 -0
- package/dist/components/display/index.d.ts +3 -1
- package/dist/components/display/index.d.ts.map +1 -1
- package/dist/components/display/index.js +1 -0
- package/dist/components/display/types.d.ts +2 -0
- package/dist/components/display/types.d.ts.map +1 -1
- package/dist/components/feedback/ConfirmDialog.svelte +59 -48
- package/dist/components/feedback/ConfirmDialog.svelte.d.ts +14 -2
- package/dist/components/feedback/ConfirmDialog.svelte.d.ts.map +1 -1
- package/dist/components/feedback/ToastViewport.svelte +156 -8
- package/dist/components/feedback/ToastViewport.svelte.d.ts +5 -1
- package/dist/components/feedback/ToastViewport.svelte.d.ts.map +1 -1
- package/dist/components/feedback/__tests__/ConfirmDialog.test.js +38 -20
- package/dist/components/feedback/__tests__/confirm-message.fixture.svelte +9 -0
- package/dist/components/feedback/__tests__/confirm-message.fixture.svelte.d.ts +19 -0
- package/dist/components/feedback/__tests__/confirm-message.fixture.svelte.d.ts.map +1 -0
- package/dist/components/feedback/__tests__/toast.test.js +142 -1
- package/dist/components/feedback/index.d.ts +1 -1
- package/dist/components/feedback/index.d.ts.map +1 -1
- package/dist/components/feedback/toast.d.ts +13 -1
- package/dist/components/feedback/toast.d.ts.map +1 -1
- package/dist/components/feedback/toast.js +51 -10
- package/dist/components/forms/CameraCapture.svelte +619 -0
- package/dist/components/forms/CameraCapture.svelte.d.ts +42 -0
- package/dist/components/forms/CameraCapture.svelte.d.ts.map +1 -0
- package/dist/components/forms/Checkbox.svelte +8 -1
- package/dist/components/forms/Checkbox.svelte.d.ts +2 -0
- package/dist/components/forms/Checkbox.svelte.d.ts.map +1 -1
- package/dist/components/forms/Combobox.svelte +40 -0
- package/dist/components/forms/Combobox.svelte.d.ts.map +1 -1
- package/dist/components/forms/Form.svelte +5 -0
- package/dist/components/forms/Form.svelte.d.ts +1 -0
- package/dist/components/forms/Form.svelte.d.ts.map +1 -1
- package/dist/components/forms/Input.svelte +8 -1
- package/dist/components/forms/Input.svelte.d.ts +2 -0
- package/dist/components/forms/Input.svelte.d.ts.map +1 -1
- package/dist/components/forms/Listbox.svelte +39 -1
- package/dist/components/forms/Listbox.svelte.d.ts.map +1 -1
- package/dist/components/forms/MultiSelect.svelte +41 -1
- package/dist/components/forms/MultiSelect.svelte.d.ts.map +1 -1
- package/dist/components/forms/Radio.svelte +1 -0
- package/dist/components/forms/RadioGroup.svelte +7 -1
- package/dist/components/forms/RadioGroup.svelte.d.ts +2 -0
- package/dist/components/forms/RadioGroup.svelte.d.ts.map +1 -1
- package/dist/components/forms/SegmentedControl.svelte +152 -11
- package/dist/components/forms/SegmentedControl.svelte.d.ts +4 -0
- package/dist/components/forms/SegmentedControl.svelte.d.ts.map +1 -1
- package/dist/components/forms/Select.svelte +8 -1
- package/dist/components/forms/Select.svelte.d.ts +2 -0
- package/dist/components/forms/Select.svelte.d.ts.map +1 -1
- package/dist/components/forms/SignaturePad.svelte +436 -0
- package/dist/components/forms/SignaturePad.svelte.d.ts +32 -0
- package/dist/components/forms/SignaturePad.svelte.d.ts.map +1 -0
- package/dist/components/forms/Switch.svelte +8 -1
- package/dist/components/forms/Switch.svelte.d.ts +2 -0
- package/dist/components/forms/Switch.svelte.d.ts.map +1 -1
- package/dist/components/forms/Textarea.svelte +8 -1
- package/dist/components/forms/Textarea.svelte.d.ts +2 -0
- package/dist/components/forms/Textarea.svelte.d.ts.map +1 -1
- package/dist/components/forms/__tests__/CameraCapture.test.js +504 -0
- package/dist/components/forms/__tests__/Combobox.test.js +279 -12
- package/dist/components/forms/__tests__/Form.test.js +46 -0
- package/dist/components/forms/__tests__/Listbox.test.js +303 -0
- package/dist/components/forms/__tests__/MultiSelect.test.js +281 -0
- package/dist/components/forms/__tests__/SignaturePad.test.js +338 -0
- package/dist/components/forms/__tests__/camera-capture-session.test.js +207 -0
- package/dist/components/forms/__tests__/capture-form-retry.test.js +166 -0
- package/dist/components/forms/__tests__/capture-form.fixture.svelte +38 -0
- package/dist/components/forms/__tests__/capture-form.fixture.svelte.d.ts +10 -0
- package/dist/components/forms/__tests__/capture-form.fixture.svelte.d.ts.map +1 -0
- package/dist/components/forms/__tests__/capture-test-env.d.ts +70 -0
- package/dist/components/forms/__tests__/capture-test-env.d.ts.map +1 -0
- package/dist/components/forms/__tests__/capture-test-env.js +122 -0
- package/dist/components/forms/__tests__/capture-touch.fixture.svelte +15 -0
- package/dist/components/forms/__tests__/capture-touch.fixture.svelte.d.ts +8 -0
- package/dist/components/forms/__tests__/capture-touch.fixture.svelte.d.ts.map +1 -0
- package/dist/components/forms/__tests__/combobox-form.fixture.svelte +29 -0
- package/dist/components/forms/__tests__/combobox-form.fixture.svelte.d.ts +14 -0
- package/dist/components/forms/__tests__/combobox-form.fixture.svelte.d.ts.map +1 -0
- package/dist/components/forms/__tests__/form-attachment.fixture.svelte +28 -0
- package/dist/components/forms/__tests__/form-attachment.fixture.svelte.d.ts +11 -0
- package/dist/components/forms/__tests__/form-attachment.fixture.svelte.d.ts.map +1 -0
- package/dist/components/forms/__tests__/form-retry-form.fixture.svelte +31 -0
- package/dist/components/forms/__tests__/form-retry-form.fixture.svelte.d.ts +13 -0
- package/dist/components/forms/__tests__/form-retry-form.fixture.svelte.d.ts.map +1 -0
- package/dist/components/forms/__tests__/form-retry-form.test.js +107 -0
- package/dist/components/forms/__tests__/hidden-controls-form-retry.test.js +69 -0
- package/dist/components/forms/__tests__/hidden-controls-form.fixture.svelte +29 -0
- package/dist/components/forms/__tests__/hidden-controls-form.fixture.svelte.d.ts +9 -0
- package/dist/components/forms/__tests__/hidden-controls-form.fixture.svelte.d.ts.map +1 -0
- package/dist/components/forms/__tests__/listbox-form.fixture.svelte +27 -0
- package/dist/components/forms/__tests__/listbox-form.fixture.svelte.d.ts +13 -0
- package/dist/components/forms/__tests__/listbox-form.fixture.svelte.d.ts.map +1 -0
- package/dist/components/forms/__tests__/multiselect-form.fixture.svelte +27 -0
- package/dist/components/forms/__tests__/multiselect-form.fixture.svelte.d.ts +13 -0
- package/dist/components/forms/__tests__/multiselect-form.fixture.svelte.d.ts.map +1 -0
- package/dist/components/forms/__tests__/native-file-field.test.js +177 -0
- package/dist/components/forms/__tests__/segmented-posting.fixture.svelte +28 -0
- package/dist/components/forms/__tests__/segmented-posting.fixture.svelte.d.ts +12 -0
- package/dist/components/forms/__tests__/segmented-posting.fixture.svelte.d.ts.map +1 -0
- package/dist/components/forms/__tests__/segmented-posting.test.js +92 -0
- package/dist/components/forms/__tests__/signature-pad-logic.test.js +54 -0
- package/dist/components/forms/__tests__/touch-density.fixture.svelte +34 -0
- package/dist/components/forms/__tests__/touch-density.fixture.svelte.d.ts +8 -0
- package/dist/components/forms/__tests__/touch-density.fixture.svelte.d.ts.map +1 -0
- package/dist/components/forms/__tests__/touch-density.test.js +66 -0
- package/dist/components/forms/camera-capture-session.d.ts +81 -0
- package/dist/components/forms/camera-capture-session.d.ts.map +1 -0
- package/dist/components/forms/camera-capture-session.js +130 -0
- package/dist/components/forms/form-retry/__tests__/boundaries.test.js +23 -0
- package/dist/components/forms/form-retry/__tests__/content-digest.test.js +70 -0
- package/dist/components/forms/form-retry/__tests__/controller.test.js +802 -0
- package/dist/components/forms/form-retry/__tests__/fake-kit.d.ts +82 -0
- package/dist/components/forms/form-retry/__tests__/fake-kit.d.ts.map +1 -0
- package/dist/components/forms/form-retry/__tests__/fake-kit.js +180 -0
- package/dist/components/forms/form-retry/__tests__/form-draft.test.js +169 -0
- package/dist/components/forms/form-retry/__tests__/ssr.test.js +37 -0
- package/dist/components/forms/form-retry/__tests__/storage.d.ts +26 -0
- package/dist/components/forms/form-retry/__tests__/storage.d.ts.map +1 -0
- package/dist/components/forms/form-retry/__tests__/storage.js +63 -0
- package/dist/components/forms/form-retry/__tests__/submission-key.test.js +236 -0
- package/dist/components/forms/form-retry/__tests__/submitted-draft.test.js +334 -0
- package/dist/components/forms/form-retry/content-digest.d.ts +54 -0
- package/dist/components/forms/form-retry/content-digest.d.ts.map +1 -0
- package/dist/components/forms/form-retry/content-digest.js +169 -0
- package/dist/components/forms/form-retry/controller.d.ts +214 -0
- package/dist/components/forms/form-retry/controller.d.ts.map +1 -0
- package/dist/components/forms/form-retry/controller.js +404 -0
- package/dist/components/forms/form-retry/form-draft.d.ts +68 -0
- package/dist/components/forms/form-retry/form-draft.d.ts.map +1 -0
- package/dist/components/forms/form-retry/form-draft.js +161 -0
- package/dist/components/forms/form-retry/index.d.ts +19 -0
- package/dist/components/forms/form-retry/index.d.ts.map +1 -0
- package/dist/components/forms/form-retry/index.js +16 -0
- package/dist/components/forms/form-retry/submission-key.d.ts +244 -0
- package/dist/components/forms/form-retry/submission-key.d.ts.map +1 -0
- package/dist/components/forms/form-retry/submission-key.js +342 -0
- package/dist/components/forms/form-retry/submitted-draft.d.ts +112 -0
- package/dist/components/forms/form-retry/submitted-draft.d.ts.map +1 -0
- package/dist/components/forms/form-retry/submitted-draft.js +270 -0
- package/dist/components/forms/form-retry/types.d.ts +84 -0
- package/dist/components/forms/form-retry/types.d.ts.map +1 -0
- package/dist/components/forms/form-retry/types.js +13 -0
- package/dist/components/forms/index.d.ts +7 -1
- package/dist/components/forms/index.d.ts.map +1 -1
- package/dist/components/forms/index.js +8 -0
- package/dist/components/forms/native-file-field.d.ts +94 -0
- package/dist/components/forms/native-file-field.d.ts.map +1 -0
- package/dist/components/forms/native-file-field.js +147 -0
- package/dist/components/forms/signature-pad-logic.d.ts +50 -0
- package/dist/components/forms/signature-pad-logic.d.ts.map +1 -0
- package/dist/components/forms/signature-pad-logic.js +48 -0
- package/dist/components/forms/types.d.ts +41 -0
- package/dist/components/forms/types.d.ts.map +1 -1
- package/dist/components/nav/FilterChips.svelte +8 -1
- package/dist/components/nav/FilterChips.svelte.d.ts +2 -0
- package/dist/components/nav/FilterChips.svelte.d.ts.map +1 -1
- package/dist/components/ui/Button.svelte +9 -2
- package/dist/components/ui/Button.svelte.d.ts +2 -0
- package/dist/components/ui/Button.svelte.d.ts.map +1 -1
- package/dist/i18n/strings.ui.d.ts +24 -0
- package/dist/i18n/strings.ui.d.ts.map +1 -1
- package/dist/i18n/strings.ui.js +26 -0
- package/dist/styles/tokens.css +2 -0
- package/dist/svelte/__tests__/playground.test.js +2 -1
- package/dist/svelte/playground/CapturePreview.svelte +56 -0
- package/dist/svelte/playground/CapturePreview.svelte.d.ts +4 -0
- package/dist/svelte/playground/CapturePreview.svelte.d.ts.map +1 -0
- package/dist/svelte/playground.d.ts.map +1 -1
- package/dist/svelte/playground.js +10 -0
- package/dist/themes/ThemeProvider.svelte +9 -0
- package/dist/themes/ThemeProvider.svelte.d.ts +2 -0
- package/dist/themes/ThemeProvider.svelte.d.ts.map +1 -1
- package/dist/themes/css-generator.d.ts.map +1 -1
- package/dist/themes/css-generator.js +3 -0
- package/dist/themes/styles/glass.css +4 -0
- package/dist/themes/styles/happyvertical.css +4 -0
- package/dist/themes/styles/material.css +4 -0
- package/dist/themes/styles/smrt.css +4 -0
- package/dist/themes/styles/studio.css +4 -0
- package/package.json +8 -2
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SignaturePad rendered states, pointer gating, PNG output, and native form
|
|
3
|
+
* posting (smrt#3290).
|
|
4
|
+
*/
|
|
5
|
+
import { fireEvent, render, screen, waitFor } from '@testing-library/svelte';
|
|
6
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
7
|
+
import { expectNoA11yViolations } from '../../../test-support/a11y.js';
|
|
8
|
+
import SignaturePad from '../SignaturePad.svelte';
|
|
9
|
+
import { SIGNATURE_INK_COLOR, SIGNATURE_PAPER_COLOR, } from '../signature-pad-logic.js';
|
|
10
|
+
import CaptureFormFixture from './capture-form.fixture.svelte';
|
|
11
|
+
import { stubCanvas, submittedEntries, useFileFieldStrategy, } from './capture-test-env.js';
|
|
12
|
+
let context;
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
context = stubCanvas();
|
|
15
|
+
useFileFieldStrategy('data-transfer');
|
|
16
|
+
});
|
|
17
|
+
afterEach(() => {
|
|
18
|
+
vi.restoreAllMocks();
|
|
19
|
+
vi.unstubAllGlobals();
|
|
20
|
+
});
|
|
21
|
+
function canvas(container) {
|
|
22
|
+
return container.querySelector('canvas');
|
|
23
|
+
}
|
|
24
|
+
function root(container) {
|
|
25
|
+
return container.querySelector('.signature-pad');
|
|
26
|
+
}
|
|
27
|
+
async function stroke(target, pointerType = 'mouse', pointerId = 1) {
|
|
28
|
+
await fireEvent.pointerDown(target, {
|
|
29
|
+
pointerType,
|
|
30
|
+
pointerId,
|
|
31
|
+
clientX: 10,
|
|
32
|
+
clientY: 10,
|
|
33
|
+
});
|
|
34
|
+
await fireEvent.pointerMove(target, {
|
|
35
|
+
pointerType,
|
|
36
|
+
pointerId,
|
|
37
|
+
clientX: 60,
|
|
38
|
+
clientY: 40,
|
|
39
|
+
});
|
|
40
|
+
await fireEvent.pointerUp(target, { pointerType, pointerId });
|
|
41
|
+
}
|
|
42
|
+
const useButton = () => screen.getByRole('button', { name: 'Use signature' });
|
|
43
|
+
describe('SignaturePad states', () => {
|
|
44
|
+
it('starts empty on white paper with Use signature disabled', async () => {
|
|
45
|
+
const { container } = render(SignaturePad);
|
|
46
|
+
expect(root(container)).toHaveAttribute('data-state', 'empty');
|
|
47
|
+
expect(useButton()).toBeDisabled();
|
|
48
|
+
expect(screen.getByRole('button', { name: 'Clear' })).toBeEnabled();
|
|
49
|
+
expect(canvas(container)).toHaveAttribute('aria-label', 'Signature area, not yet signed');
|
|
50
|
+
expect(screen.getByText('Sign with a stylus, your finger, or the mouse.')).toBeInTheDocument();
|
|
51
|
+
// Paper is painted into the bitmap and shown behind it regardless of theme.
|
|
52
|
+
expect(context.fillStyle).toBe(SIGNATURE_PAPER_COLOR);
|
|
53
|
+
expect(context.fillRect).toHaveBeenCalledWith(0, 0, 600, 240);
|
|
54
|
+
expect(canvas(container).style.backgroundColor).toBe('rgb(255, 255, 255)');
|
|
55
|
+
await expectNoA11yViolations(container);
|
|
56
|
+
});
|
|
57
|
+
it('enables Use signature once an accepted stroke exists, inked dark', async () => {
|
|
58
|
+
const { container } = render(SignaturePad);
|
|
59
|
+
await stroke(canvas(container));
|
|
60
|
+
expect(root(container)).toHaveAttribute('data-state', 'signed');
|
|
61
|
+
expect(canvas(container)).toHaveAttribute('aria-label', 'Signature area, signed');
|
|
62
|
+
expect(useButton()).toBeEnabled();
|
|
63
|
+
expect(context.stroke).toHaveBeenCalled();
|
|
64
|
+
expect(context.strokeStyle).toBe(SIGNATURE_INK_COLOR);
|
|
65
|
+
await expectNoA11yViolations(container);
|
|
66
|
+
});
|
|
67
|
+
it('ignores finger and mouse under stylusOnly, and accepts the pen', async () => {
|
|
68
|
+
const { container } = render(SignaturePad, { props: { stylusOnly: true } });
|
|
69
|
+
expect(screen.getByText('Sign with the stylus.')).toBeInTheDocument();
|
|
70
|
+
await stroke(canvas(container), 'touch');
|
|
71
|
+
await stroke(canvas(container), 'mouse', 2);
|
|
72
|
+
expect(root(container)).toHaveAttribute('data-state', 'empty');
|
|
73
|
+
expect(useButton()).toBeDisabled();
|
|
74
|
+
expect(context.stroke).not.toHaveBeenCalled();
|
|
75
|
+
await stroke(canvas(container), 'pen', 3);
|
|
76
|
+
expect(useButton()).toBeEnabled();
|
|
77
|
+
});
|
|
78
|
+
it('ignores a second pointer that did not start the stroke', async () => {
|
|
79
|
+
const { container } = render(SignaturePad);
|
|
80
|
+
await fireEvent.pointerMove(canvas(container), {
|
|
81
|
+
pointerType: 'mouse',
|
|
82
|
+
pointerId: 9,
|
|
83
|
+
});
|
|
84
|
+
expect(root(container)).toHaveAttribute('data-state', 'empty');
|
|
85
|
+
});
|
|
86
|
+
it('is inert when disabled', async () => {
|
|
87
|
+
const { container } = render(SignaturePad, { props: { disabled: true } });
|
|
88
|
+
await stroke(canvas(container));
|
|
89
|
+
expect(root(container)).toHaveAttribute('data-state', 'empty');
|
|
90
|
+
expect(useButton()).toBeDisabled();
|
|
91
|
+
expect(screen.getByRole('button', { name: 'Clear' })).toBeDisabled();
|
|
92
|
+
});
|
|
93
|
+
it('returns a PNG through onCapture and locks the pad until cleared', async () => {
|
|
94
|
+
const onCapture = vi.fn();
|
|
95
|
+
const onClear = vi.fn();
|
|
96
|
+
const { container } = render(SignaturePad, {
|
|
97
|
+
props: { onCapture, onClear },
|
|
98
|
+
});
|
|
99
|
+
await stroke(canvas(container));
|
|
100
|
+
await fireEvent.click(useButton());
|
|
101
|
+
await waitFor(() => expect(root(container)).toHaveAttribute('data-state', 'committed'));
|
|
102
|
+
const [{ blob, dataUrl }] = onCapture.mock.calls[0];
|
|
103
|
+
expect(blob.type).toBe('image/png');
|
|
104
|
+
expect(dataUrl).toMatch(/^data:image\/png/);
|
|
105
|
+
expect(screen.getByRole('status')).toHaveTextContent('Signature attached.');
|
|
106
|
+
expect(useButton()).toBeDisabled();
|
|
107
|
+
context.stroke.mockClear();
|
|
108
|
+
await stroke(canvas(container));
|
|
109
|
+
expect(context.stroke).not.toHaveBeenCalled();
|
|
110
|
+
await fireEvent.click(screen.getByRole('button', { name: 'Clear' }));
|
|
111
|
+
expect(onClear).toHaveBeenCalledTimes(1);
|
|
112
|
+
expect(root(container)).toHaveAttribute('data-state', 'empty');
|
|
113
|
+
});
|
|
114
|
+
it('locks the pad while the PNG encodes so it commits once and matches the canvas', async () => {
|
|
115
|
+
let finish;
|
|
116
|
+
vi.mocked(HTMLCanvasElement.prototype.toBlob).mockImplementation((callback, type) => {
|
|
117
|
+
finish = () => callback(new Blob(['pixels'], { type }));
|
|
118
|
+
});
|
|
119
|
+
const onCapture = vi.fn();
|
|
120
|
+
const { container } = render(SignaturePad, { props: { onCapture } });
|
|
121
|
+
await stroke(canvas(container));
|
|
122
|
+
await fireEvent.click(useButton());
|
|
123
|
+
expect(useButton()).toBeDisabled();
|
|
124
|
+
await fireEvent.click(useButton());
|
|
125
|
+
context.stroke.mockClear();
|
|
126
|
+
await stroke(canvas(container), 'mouse', 2);
|
|
127
|
+
expect(context.stroke).not.toHaveBeenCalled();
|
|
128
|
+
finish();
|
|
129
|
+
await waitFor(() => expect(root(container)).toHaveAttribute('data-state', 'committed'));
|
|
130
|
+
expect(onCapture).toHaveBeenCalledTimes(1);
|
|
131
|
+
});
|
|
132
|
+
it('discards an encoding superseded by Clear', async () => {
|
|
133
|
+
let finish;
|
|
134
|
+
vi.mocked(HTMLCanvasElement.prototype.toBlob).mockImplementation((callback, type) => {
|
|
135
|
+
finish = () => callback(new Blob(['pixels'], { type }));
|
|
136
|
+
});
|
|
137
|
+
const onCapture = vi.fn();
|
|
138
|
+
const { container } = render(SignaturePad, { props: { onCapture } });
|
|
139
|
+
await stroke(canvas(container));
|
|
140
|
+
await fireEvent.click(useButton());
|
|
141
|
+
await fireEvent.click(screen.getByRole('button', { name: 'Clear' }));
|
|
142
|
+
finish();
|
|
143
|
+
await Promise.resolve();
|
|
144
|
+
expect(onCapture).not.toHaveBeenCalled();
|
|
145
|
+
expect(root(container)).toHaveAttribute('data-state', 'empty');
|
|
146
|
+
});
|
|
147
|
+
it('keeps a committed signature across prop updates and resets only on a size change', async () => {
|
|
148
|
+
const onClear = vi.fn();
|
|
149
|
+
const props = { name: 'signature', onClear };
|
|
150
|
+
const { container, rerender } = render(SignaturePad, { props });
|
|
151
|
+
await stroke(canvas(container));
|
|
152
|
+
await fireEvent.click(useButton());
|
|
153
|
+
await waitFor(() => expect(root(container)).toHaveAttribute('data-state', 'committed'));
|
|
154
|
+
// The harness replaces the whole props object, as a parent re-spreading a
|
|
155
|
+
// fresh object does; width and height are unchanged.
|
|
156
|
+
await rerender({ ...props, disabled: true });
|
|
157
|
+
await rerender({ ...props, disabled: false });
|
|
158
|
+
expect(root(container)).toHaveAttribute('data-state', 'committed');
|
|
159
|
+
expect(onClear).not.toHaveBeenCalled();
|
|
160
|
+
await rerender({ ...props, width: 300 });
|
|
161
|
+
expect(root(container)).toHaveAttribute('data-state', 'empty');
|
|
162
|
+
expect(onClear).toHaveBeenCalledTimes(1);
|
|
163
|
+
});
|
|
164
|
+
it('accepts label overrides', () => {
|
|
165
|
+
render(SignaturePad, {
|
|
166
|
+
props: { labels: { useSignature: 'Signer', clear: 'Effacer' } },
|
|
167
|
+
});
|
|
168
|
+
expect(screen.getByRole('button', { name: 'Signer' })).toBeInTheDocument();
|
|
169
|
+
expect(screen.getByRole('button', { name: 'Effacer' })).toBeInTheDocument();
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
describe('SignaturePad native form post', () => {
|
|
173
|
+
it('posts the committed signature as signature.png and removes it on Clear', async () => {
|
|
174
|
+
const { container } = render(CaptureFormFixture, {
|
|
175
|
+
props: { kind: 'signature', name: 'signature' },
|
|
176
|
+
});
|
|
177
|
+
const form = container.querySelector('form');
|
|
178
|
+
expect(root(container)).toHaveAttribute('data-smrt-file-field', 'data-transfer');
|
|
179
|
+
expect(submittedEntries(form).get('signature').size).toBe(0);
|
|
180
|
+
await stroke(canvas(container));
|
|
181
|
+
await fireEvent.click(useButton());
|
|
182
|
+
await waitFor(() => expect(root(container)).toHaveAttribute('data-state', 'committed'));
|
|
183
|
+
const posted = submittedEntries(form).getAll('signature');
|
|
184
|
+
expect(posted).toHaveLength(1);
|
|
185
|
+
expect(posted[0].name).toBe('signature.png');
|
|
186
|
+
expect(posted[0].type).toBe('image/png');
|
|
187
|
+
expect(submittedEntries(form).get('note')).toBe('hazard');
|
|
188
|
+
await fireEvent.click(screen.getByRole('button', { name: 'Clear' }));
|
|
189
|
+
expect(submittedEntries(form).get('signature').size).toBe(0);
|
|
190
|
+
});
|
|
191
|
+
it('falls back to the formdata event when DataTransfer is unavailable', async () => {
|
|
192
|
+
useFileFieldStrategy('formdata-event');
|
|
193
|
+
const { container } = render(CaptureFormFixture, {
|
|
194
|
+
props: { kind: 'signature', name: 'sig', fileName: 'flha-signature.png' },
|
|
195
|
+
});
|
|
196
|
+
await waitFor(() => expect(root(container)).toHaveAttribute('data-smrt-file-field', 'formdata-event'));
|
|
197
|
+
await stroke(canvas(container));
|
|
198
|
+
await fireEvent.click(useButton());
|
|
199
|
+
await waitFor(() => expect(root(container)).toHaveAttribute('data-state', 'committed'));
|
|
200
|
+
const posted = submittedEntries(container.querySelector('form')).getAll('sig');
|
|
201
|
+
expect(posted).toHaveLength(1);
|
|
202
|
+
expect(posted[0].name).toBe('flha-signature.png');
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
describe('SignaturePad in a disabled fieldset', () => {
|
|
206
|
+
it.each([
|
|
207
|
+
'data-transfer',
|
|
208
|
+
'formdata-event',
|
|
209
|
+
])('posts nothing, committed or empty, like a disabled native control (%s)', async (strategy) => {
|
|
210
|
+
useFileFieldStrategy(strategy);
|
|
211
|
+
const { container } = render(CaptureFormFixture, {
|
|
212
|
+
props: { kind: 'signature', name: 'signature', inFieldset: true },
|
|
213
|
+
});
|
|
214
|
+
await waitFor(() => expect(root(container)).toHaveAttribute('data-smrt-file-field', strategy));
|
|
215
|
+
const form = container.querySelector('form');
|
|
216
|
+
const fieldset = form.querySelector('fieldset');
|
|
217
|
+
fieldset.disabled = true;
|
|
218
|
+
expect(submittedEntries(form).has('signature')).toBe(false);
|
|
219
|
+
fieldset.disabled = false;
|
|
220
|
+
await stroke(canvas(container));
|
|
221
|
+
await fireEvent.click(useButton());
|
|
222
|
+
await waitFor(() => expect(root(container)).toHaveAttribute('data-state', 'committed'));
|
|
223
|
+
expect(submittedEntries(form).getAll('signature')).toHaveLength(1);
|
|
224
|
+
fieldset.disabled = true;
|
|
225
|
+
expect(submittedEntries(form).has('signature')).toBe(false);
|
|
226
|
+
expect(submittedEntries(form).get('note')).toBe('hazard');
|
|
227
|
+
fieldset.disabled = false;
|
|
228
|
+
expect(submittedEntries(form).getAll('signature')).toHaveLength(1);
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
/**
|
|
232
|
+
* A reset of the owning form (smrt#3290): SvelteKit `enhance`'s `update()`
|
|
233
|
+
* after a success, `createFormRetry()`'s conditional reset, a reset button and
|
|
234
|
+
* `form.reset()` all empty a native file input, so the pad must empty with it.
|
|
235
|
+
* jsdom's reset does not empty file inputs (browsers do), so these assertions
|
|
236
|
+
* prove the component empties its own field.
|
|
237
|
+
*/
|
|
238
|
+
describe('SignaturePad form reset', () => {
|
|
239
|
+
function form(container) {
|
|
240
|
+
return container.querySelector('form');
|
|
241
|
+
}
|
|
242
|
+
async function commit(container) {
|
|
243
|
+
await stroke(canvas(container));
|
|
244
|
+
await fireEvent.click(useButton());
|
|
245
|
+
await waitFor(() => expect(root(container)).toHaveAttribute('data-state', 'committed'));
|
|
246
|
+
}
|
|
247
|
+
function postedSignatures(container) {
|
|
248
|
+
return submittedEntries(form(container)).getAll('signature');
|
|
249
|
+
}
|
|
250
|
+
it.each([
|
|
251
|
+
'data-transfer',
|
|
252
|
+
'formdata-event',
|
|
253
|
+
])('discards a committed signature like Clear (%s)', async (strategy) => {
|
|
254
|
+
useFileFieldStrategy(strategy);
|
|
255
|
+
const onClear = vi.fn();
|
|
256
|
+
const { container } = render(CaptureFormFixture, {
|
|
257
|
+
props: { kind: 'signature', name: 'signature', onClear },
|
|
258
|
+
});
|
|
259
|
+
await waitFor(() => expect(root(container)).toHaveAttribute('data-smrt-file-field', strategy));
|
|
260
|
+
await commit(container);
|
|
261
|
+
expect(postedSignatures(container)[0].name).toBe('signature.png');
|
|
262
|
+
context.fillRect.mockClear();
|
|
263
|
+
form(container).reset();
|
|
264
|
+
await waitFor(() => expect(root(container)).toHaveAttribute('data-state', 'empty'));
|
|
265
|
+
expect(onClear).toHaveBeenCalledTimes(1);
|
|
266
|
+
expect(context.fillRect).toHaveBeenCalled();
|
|
267
|
+
expect(context.fillStyle).toBe(SIGNATURE_PAPER_COLOR);
|
|
268
|
+
expect(screen.getByRole('status')).toHaveTextContent('');
|
|
269
|
+
const posted = postedSignatures(container);
|
|
270
|
+
expect(posted).toHaveLength(1);
|
|
271
|
+
expect(posted[0].size).toBe(0);
|
|
272
|
+
expect(posted[0].name).toBe('');
|
|
273
|
+
// Unlocked: the next entry can be signed and committed.
|
|
274
|
+
await commit(container);
|
|
275
|
+
expect(postedSignatures(container)[0].name).toBe('signature.png');
|
|
276
|
+
});
|
|
277
|
+
it('a reset button discards ink not yet committed, without onClear', async () => {
|
|
278
|
+
const onClear = vi.fn();
|
|
279
|
+
const { container } = render(CaptureFormFixture, {
|
|
280
|
+
props: { kind: 'signature', name: 'signature', onClear },
|
|
281
|
+
});
|
|
282
|
+
await stroke(canvas(container));
|
|
283
|
+
expect(useButton()).toBeEnabled();
|
|
284
|
+
await fireEvent.click(screen.getByRole('button', { name: 'Reset form' }));
|
|
285
|
+
expect(root(container)).toHaveAttribute('data-state', 'empty');
|
|
286
|
+
expect(useButton()).toBeDisabled();
|
|
287
|
+
expect(onClear).not.toHaveBeenCalled();
|
|
288
|
+
});
|
|
289
|
+
it('clears a committed signature even while disabled', async () => {
|
|
290
|
+
const onClear = vi.fn();
|
|
291
|
+
const { container, rerender } = render(CaptureFormFixture, {
|
|
292
|
+
props: { kind: 'signature', name: 'signature', onClear },
|
|
293
|
+
});
|
|
294
|
+
await commit(container);
|
|
295
|
+
await rerender({
|
|
296
|
+
kind: 'signature',
|
|
297
|
+
name: 'signature',
|
|
298
|
+
onClear,
|
|
299
|
+
disabled: true,
|
|
300
|
+
});
|
|
301
|
+
form(container).reset();
|
|
302
|
+
await waitFor(() => expect(root(container)).toHaveAttribute('data-state', 'empty'));
|
|
303
|
+
expect(onClear).toHaveBeenCalledTimes(1);
|
|
304
|
+
expect(postedSignatures(container)[0].size).toBe(0);
|
|
305
|
+
});
|
|
306
|
+
it('supersedes a signature still encoding', async () => {
|
|
307
|
+
let finish;
|
|
308
|
+
vi.mocked(HTMLCanvasElement.prototype.toBlob).mockImplementation((callback, type) => {
|
|
309
|
+
finish = () => callback(new Blob(['pixels'], { type }));
|
|
310
|
+
});
|
|
311
|
+
const onCapture = vi.fn();
|
|
312
|
+
const { container } = render(CaptureFormFixture, {
|
|
313
|
+
props: { kind: 'signature', name: 'signature', onCapture },
|
|
314
|
+
});
|
|
315
|
+
await stroke(canvas(container));
|
|
316
|
+
await fireEvent.click(useButton());
|
|
317
|
+
form(container).reset();
|
|
318
|
+
finish();
|
|
319
|
+
await Promise.resolve();
|
|
320
|
+
expect(onCapture).not.toHaveBeenCalled();
|
|
321
|
+
expect(root(container)).toHaveAttribute('data-state', 'empty');
|
|
322
|
+
expect(postedSignatures(container)[0].size).toBe(0);
|
|
323
|
+
});
|
|
324
|
+
it('ignores the reset of another form', async () => {
|
|
325
|
+
const onClear = vi.fn();
|
|
326
|
+
const { container } = render(CaptureFormFixture, {
|
|
327
|
+
props: { kind: 'signature', name: 'signature', onClear },
|
|
328
|
+
});
|
|
329
|
+
await commit(container);
|
|
330
|
+
const other = document.createElement('form');
|
|
331
|
+
document.body.append(other);
|
|
332
|
+
other.reset();
|
|
333
|
+
expect(root(container)).toHaveAttribute('data-state', 'committed');
|
|
334
|
+
expect(onClear).not.toHaveBeenCalled();
|
|
335
|
+
expect(postedSignatures(container)[0].name).toBe('signature.png');
|
|
336
|
+
other.remove();
|
|
337
|
+
});
|
|
338
|
+
});
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Camera session lifecycle against a fake `MediaDevices` (smrt#3290).
|
|
3
|
+
* Ported from teamworks-os `camera-capture-session.test.ts` and
|
|
4
|
+
* `camera-capture-logic.test.ts`.
|
|
5
|
+
*/
|
|
6
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
7
|
+
import { classifyGetUserMediaError, createCameraSession, isCameraApiSupported, } from '../camera-capture-session.js';
|
|
8
|
+
function fakeStream(tracks) {
|
|
9
|
+
return { getTracks: () => tracks };
|
|
10
|
+
}
|
|
11
|
+
function deferred() {
|
|
12
|
+
let resolve;
|
|
13
|
+
let reject;
|
|
14
|
+
const promise = new Promise((res, rej) => {
|
|
15
|
+
resolve = res;
|
|
16
|
+
reject = rej;
|
|
17
|
+
});
|
|
18
|
+
return { promise, resolve, reject };
|
|
19
|
+
}
|
|
20
|
+
describe('isCameraApiSupported', () => {
|
|
21
|
+
it('is false without navigator, mediaDevices, or getUserMedia', () => {
|
|
22
|
+
expect(isCameraApiSupported(undefined)).toBe(false);
|
|
23
|
+
expect(isCameraApiSupported({ mediaDevices: undefined })).toBe(false);
|
|
24
|
+
expect(isCameraApiSupported({ mediaDevices: {} })).toBe(false);
|
|
25
|
+
expect(isCameraApiSupported({ mediaDevices: { getUserMedia: 'nope' } })).toBe(false);
|
|
26
|
+
});
|
|
27
|
+
it('is true when getUserMedia is a function', () => {
|
|
28
|
+
expect(isCameraApiSupported({
|
|
29
|
+
mediaDevices: { getUserMedia: async () => ({}) },
|
|
30
|
+
})).toBe(true);
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
describe('classifyGetUserMediaError', () => {
|
|
34
|
+
it.each([
|
|
35
|
+
'NotAllowedError',
|
|
36
|
+
'PermissionDeniedError',
|
|
37
|
+
'SecurityError',
|
|
38
|
+
])('maps %s to permission-denied', (name) => {
|
|
39
|
+
expect(classifyGetUserMediaError(new DOMException('x', name))).toBe('permission-denied');
|
|
40
|
+
});
|
|
41
|
+
it.each([
|
|
42
|
+
'NotFoundError',
|
|
43
|
+
'DevicesNotFoundError',
|
|
44
|
+
'OverconstrainedError',
|
|
45
|
+
])('maps %s to no-camera', (name) => {
|
|
46
|
+
expect(classifyGetUserMediaError(new DOMException('x', name))).toBe('no-camera');
|
|
47
|
+
});
|
|
48
|
+
it('maps unknown names and non-errors to error', () => {
|
|
49
|
+
expect(classifyGetUserMediaError(new DOMException('x', 'AbortError'))).toBe('error');
|
|
50
|
+
expect(classifyGetUserMediaError(new Error('boom'))).toBe('error');
|
|
51
|
+
expect(classifyGetUserMediaError('not an error')).toBe('error');
|
|
52
|
+
expect(classifyGetUserMediaError(null)).toBe('error');
|
|
53
|
+
expect(classifyGetUserMediaError({ name: 42 })).toBe('error');
|
|
54
|
+
});
|
|
55
|
+
it('reads name from a plain object, as browser DOMExceptions are not Errors', () => {
|
|
56
|
+
expect(classifyGetUserMediaError({ name: 'NotAllowedError' })).toBe('permission-denied');
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
describe('createCameraSession', () => {
|
|
60
|
+
it('reports unsupported without touching any API', async () => {
|
|
61
|
+
const session = createCameraSession({ mediaDevices: undefined });
|
|
62
|
+
await expect(session.start('environment')).resolves.toMatchObject({
|
|
63
|
+
ok: false,
|
|
64
|
+
kind: 'unsupported',
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
it('classifies rejections and keeps the diagnostic message', async () => {
|
|
68
|
+
const denied = createCameraSession({
|
|
69
|
+
mediaDevices: {
|
|
70
|
+
getUserMedia: vi
|
|
71
|
+
.fn()
|
|
72
|
+
.mockRejectedValue(new DOMException('Permission denied', 'NotAllowedError')),
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
await expect(denied.start('environment')).resolves.toEqual({
|
|
76
|
+
ok: false,
|
|
77
|
+
kind: 'permission-denied',
|
|
78
|
+
message: 'Permission denied',
|
|
79
|
+
});
|
|
80
|
+
const missing = createCameraSession({
|
|
81
|
+
mediaDevices: {
|
|
82
|
+
getUserMedia: vi
|
|
83
|
+
.fn()
|
|
84
|
+
.mockRejectedValue(new DOMException('No device', 'NotFoundError')),
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
await expect(missing.start('environment')).resolves.toEqual({
|
|
88
|
+
ok: false,
|
|
89
|
+
kind: 'no-camera',
|
|
90
|
+
message: 'No device',
|
|
91
|
+
});
|
|
92
|
+
const odd = createCameraSession({
|
|
93
|
+
mediaDevices: { getUserMedia: vi.fn().mockRejectedValue('weird') },
|
|
94
|
+
});
|
|
95
|
+
await expect(odd.start('environment')).resolves.toMatchObject({
|
|
96
|
+
ok: false,
|
|
97
|
+
kind: 'error',
|
|
98
|
+
message: expect.any(String),
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
it('requests video only with the given facing mode and exposes the stream', async () => {
|
|
102
|
+
const stream = fakeStream([{ stop: vi.fn() }]);
|
|
103
|
+
const getUserMedia = vi.fn().mockResolvedValue(stream);
|
|
104
|
+
const session = createCameraSession({ mediaDevices: { getUserMedia } });
|
|
105
|
+
await expect(session.start('user')).resolves.toEqual({ ok: true, stream });
|
|
106
|
+
expect(session.stream).toBe(stream);
|
|
107
|
+
expect(getUserMedia).toHaveBeenCalledWith({
|
|
108
|
+
video: { facingMode: 'user' },
|
|
109
|
+
audio: false,
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
it('stops every track once and forgets the stream; stop is idempotent', async () => {
|
|
113
|
+
const a = { stop: vi.fn() };
|
|
114
|
+
const b = { stop: vi.fn() };
|
|
115
|
+
const session = createCameraSession({
|
|
116
|
+
mediaDevices: {
|
|
117
|
+
getUserMedia: vi.fn().mockResolvedValue(fakeStream([a, b])),
|
|
118
|
+
},
|
|
119
|
+
});
|
|
120
|
+
await session.start('environment');
|
|
121
|
+
session.stop();
|
|
122
|
+
session.stop();
|
|
123
|
+
expect(a.stop).toHaveBeenCalledTimes(1);
|
|
124
|
+
expect(b.stop).toHaveBeenCalledTimes(1);
|
|
125
|
+
expect(session.stream).toBeNull();
|
|
126
|
+
});
|
|
127
|
+
it('stop before any start is a safe no-op', () => {
|
|
128
|
+
const session = createCameraSession({ mediaDevices: undefined });
|
|
129
|
+
expect(() => session.stop()).not.toThrow();
|
|
130
|
+
expect(session.stream).toBeNull();
|
|
131
|
+
});
|
|
132
|
+
it('discards a stream granted after stop() and releases its tracks', async () => {
|
|
133
|
+
const pending = deferred();
|
|
134
|
+
const track = { stop: vi.fn() };
|
|
135
|
+
const session = createCameraSession({
|
|
136
|
+
mediaDevices: { getUserMedia: vi.fn().mockReturnValue(pending.promise) },
|
|
137
|
+
});
|
|
138
|
+
const result = session.start('environment');
|
|
139
|
+
session.stop();
|
|
140
|
+
pending.resolve(fakeStream([track]));
|
|
141
|
+
await expect(result).resolves.toMatchObject({
|
|
142
|
+
ok: false,
|
|
143
|
+
kind: 'cancelled',
|
|
144
|
+
});
|
|
145
|
+
expect(track.stop).toHaveBeenCalledTimes(1);
|
|
146
|
+
expect(session.stream).toBeNull();
|
|
147
|
+
});
|
|
148
|
+
it('discards a rejection that settles after stop()', async () => {
|
|
149
|
+
const pending = deferred();
|
|
150
|
+
const session = createCameraSession({
|
|
151
|
+
mediaDevices: { getUserMedia: vi.fn().mockReturnValue(pending.promise) },
|
|
152
|
+
});
|
|
153
|
+
const result = session.start('environment');
|
|
154
|
+
session.stop();
|
|
155
|
+
pending.reject(new DOMException('denied', 'NotAllowedError'));
|
|
156
|
+
await expect(result).resolves.toMatchObject({
|
|
157
|
+
ok: false,
|
|
158
|
+
kind: 'cancelled',
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
it('a newer start supersedes a pending one, so overlapping requests never leak', async () => {
|
|
162
|
+
const first = deferred();
|
|
163
|
+
const firstTrack = { stop: vi.fn() };
|
|
164
|
+
const secondStream = fakeStream([{ stop: vi.fn() }]);
|
|
165
|
+
const getUserMedia = vi
|
|
166
|
+
.fn()
|
|
167
|
+
.mockReturnValueOnce(first.promise)
|
|
168
|
+
.mockResolvedValueOnce(secondStream);
|
|
169
|
+
const session = createCameraSession({ mediaDevices: { getUserMedia } });
|
|
170
|
+
const stale = session.start('environment');
|
|
171
|
+
const fresh = session.start('user');
|
|
172
|
+
await expect(fresh).resolves.toEqual({ ok: true, stream: secondStream });
|
|
173
|
+
first.resolve(fakeStream([firstTrack]));
|
|
174
|
+
await expect(stale).resolves.toMatchObject({
|
|
175
|
+
ok: false,
|
|
176
|
+
kind: 'cancelled',
|
|
177
|
+
});
|
|
178
|
+
expect(firstTrack.stop).toHaveBeenCalledTimes(1);
|
|
179
|
+
expect(session.stream).toBe(secondStream);
|
|
180
|
+
});
|
|
181
|
+
it('starting again releases the held stream first', async () => {
|
|
182
|
+
const oldTrack = { stop: vi.fn() };
|
|
183
|
+
const getUserMedia = vi
|
|
184
|
+
.fn()
|
|
185
|
+
.mockResolvedValueOnce(fakeStream([oldTrack]))
|
|
186
|
+
.mockResolvedValueOnce(fakeStream([{ stop: vi.fn() }]));
|
|
187
|
+
const session = createCameraSession({ mediaDevices: { getUserMedia } });
|
|
188
|
+
await session.start('environment');
|
|
189
|
+
await session.start('environment');
|
|
190
|
+
expect(oldTrack.stop).toHaveBeenCalledTimes(1);
|
|
191
|
+
});
|
|
192
|
+
it('a fresh start after stop is not treated as stale', async () => {
|
|
193
|
+
const session = createCameraSession({
|
|
194
|
+
mediaDevices: {
|
|
195
|
+
getUserMedia: vi
|
|
196
|
+
.fn()
|
|
197
|
+
.mockResolvedValue(fakeStream([{ stop: vi.fn() }])),
|
|
198
|
+
},
|
|
199
|
+
});
|
|
200
|
+
await session.start('environment');
|
|
201
|
+
session.stop();
|
|
202
|
+
await expect(session.start('environment')).resolves.toMatchObject({
|
|
203
|
+
ok: true,
|
|
204
|
+
});
|
|
205
|
+
expect(session.stream).not.toBeNull();
|
|
206
|
+
});
|
|
207
|
+
});
|