@happyvertical/smrt-ui 0.53.1 → 0.53.2

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 (205) hide show
  1. package/package.json +3 -3
  2. package/dist/actions/__tests__/ripple.test.js +0 -82
  3. package/dist/actions/__tests__/swipe-dismiss.test.js +0 -69
  4. package/dist/components/calendar/__tests__/CalendarView.test.js +0 -226
  5. package/dist/components/calendar/__tests__/calendar-model.test.js +0 -138
  6. package/dist/components/chat/__tests__/chat-primitives.test.js +0 -118
  7. package/dist/components/data/__tests__/CollectionList.test.js +0 -37
  8. package/dist/components/data/__tests__/DataTable.test.js +0 -807
  9. package/dist/components/data/__tests__/DataTableConformance.test.js +0 -212
  10. package/dist/components/data/__tests__/DataTableController.test.js +0 -371
  11. package/dist/components/data/__tests__/DataTableIdentity.test.js +0 -29
  12. package/dist/components/data/__tests__/DataTableLayout.test.js +0 -195
  13. package/dist/components/data/__tests__/DataTablePerformance.test.js +0 -21
  14. package/dist/components/data/__tests__/DataTableResponsive.test.js +0 -80
  15. package/dist/components/data/__tests__/DataTableVirtualization.test.js +0 -80
  16. package/dist/components/data/__tests__/DataTableVirtualizationComponent.test.js +0 -255
  17. package/dist/components/data/__tests__/SortableHeader.test.js +0 -101
  18. package/dist/components/data/__tests__/data-surface.test.js +0 -746
  19. package/dist/components/data/__tests__/data-table-helpers.test.js +0 -36
  20. package/dist/components/data/__tests__/data-table-surface.test.js +0 -49
  21. package/dist/components/data/__tests__/link-step-surface.test.js +0 -172
  22. package/dist/components/data/__tests__/list-sort-state.svelte.test.js +0 -71
  23. package/dist/components/data/__tests__/list-sort.test.js +0 -91
  24. package/dist/components/data/__tests__/mounted-data-surface.test.js +0 -794
  25. package/dist/components/data/__tests__/responsive-table.fixture.svelte +0 -29
  26. package/dist/components/data/__tests__/responsive-table.fixture.svelte.d.ts +0 -7
  27. package/dist/components/data/__tests__/responsive-table.fixture.svelte.d.ts.map +0 -1
  28. package/dist/components/data/__tests__/surface-navigation.test.js +0 -121
  29. package/dist/components/display/__tests__/ConfidenceBadge.test.js +0 -109
  30. package/dist/components/display/__tests__/CurrencyDisplay.test.js +0 -304
  31. package/dist/components/display/__tests__/CurrencyDisplayI18nHarness.svelte +0 -20
  32. package/dist/components/display/__tests__/CurrencyDisplayI18nHarness.svelte.d.ts +0 -19
  33. package/dist/components/display/__tests__/CurrencyDisplayI18nHarness.svelte.d.ts.map +0 -1
  34. package/dist/components/display/__tests__/CurrencyDisplaySsrHarness.svelte +0 -7
  35. package/dist/components/display/__tests__/CurrencyDisplaySsrHarness.svelte.d.ts +0 -19
  36. package/dist/components/display/__tests__/CurrencyDisplaySsrHarness.svelte.d.ts.map +0 -1
  37. package/dist/components/display/__tests__/DateDisplay.test.js +0 -195
  38. package/dist/components/display/__tests__/Icon.test.js +0 -223
  39. package/dist/components/display/__tests__/StatusBadge.test.js +0 -191
  40. package/dist/components/feedback/__tests__/ConfirmDialog.test.js +0 -181
  41. package/dist/components/feedback/__tests__/LoadingOverlay.test.js +0 -113
  42. package/dist/components/feedback/__tests__/Modal.test.js +0 -72
  43. package/dist/components/feedback/__tests__/PhoneSheet.test.js +0 -101
  44. package/dist/components/feedback/__tests__/ProgressBar.test.js +0 -89
  45. package/dist/components/feedback/__tests__/WorkingStrip.test.js +0 -172
  46. package/dist/components/feedback/__tests__/confirm-message.fixture.svelte +0 -9
  47. package/dist/components/feedback/__tests__/confirm-message.fixture.svelte.d.ts +0 -19
  48. package/dist/components/feedback/__tests__/confirm-message.fixture.svelte.d.ts.map +0 -1
  49. package/dist/components/feedback/__tests__/toast.test.js +0 -190
  50. package/dist/components/forms/__tests__/CameraCapture.test.js +0 -504
  51. package/dist/components/forms/__tests__/Combobox.test.js +0 -354
  52. package/dist/components/forms/__tests__/ControlHitArea.test.js +0 -73
  53. package/dist/components/forms/__tests__/DictationButton.test.js +0 -176
  54. package/dist/components/forms/__tests__/Fieldset.test.js +0 -37
  55. package/dist/components/forms/__tests__/Form.test.js +0 -201
  56. package/dist/components/forms/__tests__/FormActionBar.test.js +0 -22
  57. package/dist/components/forms/__tests__/FormGroup.test.js +0 -48
  58. package/dist/components/forms/__tests__/Input.test.js +0 -49
  59. package/dist/components/forms/__tests__/Listbox.test.js +0 -303
  60. package/dist/components/forms/__tests__/MultiSelect.test.js +0 -281
  61. package/dist/components/forms/__tests__/SearchInput.test.js +0 -92
  62. package/dist/components/forms/__tests__/Select.test.js +0 -37
  63. package/dist/components/forms/__tests__/SignaturePad.test.js +0 -338
  64. package/dist/components/forms/__tests__/StagedControlReview.test.js +0 -702
  65. package/dist/components/forms/__tests__/Textarea.test.js +0 -39
  66. package/dist/components/forms/__tests__/Toggle.test.js +0 -87
  67. package/dist/components/forms/__tests__/camera-capture-session.test.js +0 -207
  68. package/dist/components/forms/__tests__/capture-form-retry.test.js +0 -166
  69. package/dist/components/forms/__tests__/capture-form.fixture.svelte +0 -38
  70. package/dist/components/forms/__tests__/capture-form.fixture.svelte.d.ts +0 -10
  71. package/dist/components/forms/__tests__/capture-form.fixture.svelte.d.ts.map +0 -1
  72. package/dist/components/forms/__tests__/capture-test-env.d.ts +0 -70
  73. package/dist/components/forms/__tests__/capture-test-env.d.ts.map +0 -1
  74. package/dist/components/forms/__tests__/capture-test-env.js +0 -122
  75. package/dist/components/forms/__tests__/capture-touch.fixture.svelte +0 -15
  76. package/dist/components/forms/__tests__/capture-touch.fixture.svelte.d.ts +0 -8
  77. package/dist/components/forms/__tests__/capture-touch.fixture.svelte.d.ts.map +0 -1
  78. package/dist/components/forms/__tests__/combobox-form.fixture.svelte +0 -29
  79. package/dist/components/forms/__tests__/combobox-form.fixture.svelte.d.ts +0 -14
  80. package/dist/components/forms/__tests__/combobox-form.fixture.svelte.d.ts.map +0 -1
  81. package/dist/components/forms/__tests__/combobox-labels.fixture.svelte +0 -19
  82. package/dist/components/forms/__tests__/combobox-labels.fixture.svelte.d.ts +0 -11
  83. package/dist/components/forms/__tests__/combobox-labels.fixture.svelte.d.ts.map +0 -1
  84. package/dist/components/forms/__tests__/composite-controls.fixture.svelte +0 -47
  85. package/dist/components/forms/__tests__/composite-controls.fixture.svelte.d.ts +0 -12
  86. package/dist/components/forms/__tests__/composite-controls.fixture.svelte.d.ts.map +0 -1
  87. package/dist/components/forms/__tests__/composite-controls.test.js +0 -128
  88. package/dist/components/forms/__tests__/composite-registration.fixture.svelte +0 -29
  89. package/dist/components/forms/__tests__/composite-registration.fixture.svelte.d.ts +0 -8
  90. package/dist/components/forms/__tests__/composite-registration.fixture.svelte.d.ts.map +0 -1
  91. package/dist/components/forms/__tests__/composite-user-edit.fixture.svelte +0 -41
  92. package/dist/components/forms/__tests__/composite-user-edit.fixture.svelte.d.ts +0 -13
  93. package/dist/components/forms/__tests__/composite-user-edit.fixture.svelte.d.ts.map +0 -1
  94. package/dist/components/forms/__tests__/control-interaction.test.js +0 -3421
  95. package/dist/components/forms/__tests__/control-proposals.test.js +0 -63
  96. package/dist/components/forms/__tests__/core-controls.fixture.svelte +0 -50
  97. package/dist/components/forms/__tests__/core-controls.fixture.svelte.d.ts +0 -9
  98. package/dist/components/forms/__tests__/core-controls.fixture.svelte.d.ts.map +0 -1
  99. package/dist/components/forms/__tests__/core-controls.test.js +0 -311
  100. package/dist/components/forms/__tests__/decimal-sliders.fixture.svelte +0 -25
  101. package/dist/components/forms/__tests__/decimal-sliders.fixture.svelte.d.ts +0 -8
  102. package/dist/components/forms/__tests__/decimal-sliders.fixture.svelte.d.ts.map +0 -1
  103. package/dist/components/forms/__tests__/dictation-recording.test.js +0 -582
  104. package/dist/components/forms/__tests__/dictation.test.js +0 -486
  105. package/dist/components/forms/__tests__/form-attachment.fixture.svelte +0 -28
  106. package/dist/components/forms/__tests__/form-attachment.fixture.svelte.d.ts +0 -11
  107. package/dist/components/forms/__tests__/form-attachment.fixture.svelte.d.ts.map +0 -1
  108. package/dist/components/forms/__tests__/form-group-input.fixture.svelte +0 -16
  109. package/dist/components/forms/__tests__/form-group-input.fixture.svelte.d.ts +0 -9
  110. package/dist/components/forms/__tests__/form-group-input.fixture.svelte.d.ts.map +0 -1
  111. package/dist/components/forms/__tests__/form-retry-form.fixture.svelte +0 -31
  112. package/dist/components/forms/__tests__/form-retry-form.fixture.svelte.d.ts +0 -13
  113. package/dist/components/forms/__tests__/form-retry-form.fixture.svelte.d.ts.map +0 -1
  114. package/dist/components/forms/__tests__/form-retry-form.test.js +0 -107
  115. package/dist/components/forms/__tests__/hidden-controls-form-retry.test.js +0 -69
  116. package/dist/components/forms/__tests__/hidden-controls-form.fixture.svelte +0 -29
  117. package/dist/components/forms/__tests__/hidden-controls-form.fixture.svelte.d.ts +0 -9
  118. package/dist/components/forms/__tests__/hidden-controls-form.fixture.svelte.d.ts.map +0 -1
  119. package/dist/components/forms/__tests__/listbox-form.fixture.svelte +0 -27
  120. package/dist/components/forms/__tests__/listbox-form.fixture.svelte.d.ts +0 -13
  121. package/dist/components/forms/__tests__/listbox-form.fixture.svelte.d.ts.map +0 -1
  122. package/dist/components/forms/__tests__/long-press.test.js +0 -175
  123. package/dist/components/forms/__tests__/multiselect-form.fixture.svelte +0 -27
  124. package/dist/components/forms/__tests__/multiselect-form.fixture.svelte.d.ts +0 -13
  125. package/dist/components/forms/__tests__/multiselect-form.fixture.svelte.d.ts.map +0 -1
  126. package/dist/components/forms/__tests__/native-file-field.test.js +0 -177
  127. package/dist/components/forms/__tests__/place-picker.fixture.svelte +0 -33
  128. package/dist/components/forms/__tests__/place-picker.fixture.svelte.d.ts +0 -12
  129. package/dist/components/forms/__tests__/place-picker.fixture.svelte.d.ts.map +0 -1
  130. package/dist/components/forms/__tests__/segmented-posting.fixture.svelte +0 -28
  131. package/dist/components/forms/__tests__/segmented-posting.fixture.svelte.d.ts +0 -12
  132. package/dist/components/forms/__tests__/segmented-posting.fixture.svelte.d.ts.map +0 -1
  133. package/dist/components/forms/__tests__/segmented-posting.test.js +0 -92
  134. package/dist/components/forms/__tests__/select-interaction.fixture.svelte +0 -25
  135. package/dist/components/forms/__tests__/select-interaction.fixture.svelte.d.ts +0 -9
  136. package/dist/components/forms/__tests__/select-interaction.fixture.svelte.d.ts.map +0 -1
  137. package/dist/components/forms/__tests__/signature-pad-logic.test.js +0 -54
  138. package/dist/components/forms/__tests__/staged-review-fieldset.fixture.svelte +0 -21
  139. package/dist/components/forms/__tests__/staged-review-fieldset.fixture.svelte.d.ts +0 -8
  140. package/dist/components/forms/__tests__/staged-review-fieldset.fixture.svelte.d.ts.map +0 -1
  141. package/dist/components/forms/__tests__/staged-review-outer-fieldset.fixture.svelte +0 -23
  142. package/dist/components/forms/__tests__/staged-review-outer-fieldset.fixture.svelte.d.ts +0 -8
  143. package/dist/components/forms/__tests__/staged-review-outer-fieldset.fixture.svelte.d.ts.map +0 -1
  144. package/dist/components/forms/__tests__/staged-review.fixture.svelte +0 -56
  145. package/dist/components/forms/__tests__/staged-review.fixture.svelte.d.ts +0 -12
  146. package/dist/components/forms/__tests__/staged-review.fixture.svelte.d.ts.map +0 -1
  147. package/dist/components/forms/__tests__/touch-density.fixture.svelte +0 -34
  148. package/dist/components/forms/__tests__/touch-density.fixture.svelte.d.ts +0 -8
  149. package/dist/components/forms/__tests__/touch-density.fixture.svelte.d.ts.map +0 -1
  150. package/dist/components/forms/__tests__/touch-density.test.js +0 -66
  151. package/dist/components/forms/form-retry/__tests__/boundaries.test.js +0 -23
  152. package/dist/components/forms/form-retry/__tests__/content-digest.test.js +0 -70
  153. package/dist/components/forms/form-retry/__tests__/controller.test.js +0 -802
  154. package/dist/components/forms/form-retry/__tests__/fake-kit.d.ts +0 -82
  155. package/dist/components/forms/form-retry/__tests__/fake-kit.d.ts.map +0 -1
  156. package/dist/components/forms/form-retry/__tests__/fake-kit.js +0 -180
  157. package/dist/components/forms/form-retry/__tests__/form-draft.test.js +0 -169
  158. package/dist/components/forms/form-retry/__tests__/ssr.test.js +0 -37
  159. package/dist/components/forms/form-retry/__tests__/storage.d.ts +0 -26
  160. package/dist/components/forms/form-retry/__tests__/storage.d.ts.map +0 -1
  161. package/dist/components/forms/form-retry/__tests__/storage.js +0 -63
  162. package/dist/components/forms/form-retry/__tests__/submission-key.test.js +0 -236
  163. package/dist/components/forms/form-retry/__tests__/submitted-draft.test.js +0 -334
  164. package/dist/components/layout/__tests__/Container.test.js +0 -62
  165. package/dist/components/layout/__tests__/EmptyState.test.js +0 -83
  166. package/dist/components/layout/__tests__/Footer.test.js +0 -50
  167. package/dist/components/layout/__tests__/Grid.test.js +0 -121
  168. package/dist/components/layout/__tests__/Header.test.js +0 -48
  169. package/dist/components/layout/__tests__/Masthead.test.js +0 -93
  170. package/dist/components/layout/__tests__/PageHeader.test.js +0 -166
  171. package/dist/components/layout/__tests__/SummaryCard.test.js +0 -104
  172. package/dist/components/nav/__tests__/FilterChips.test.js +0 -94
  173. package/dist/components/nav/__tests__/LinkTabs.test.js +0 -117
  174. package/dist/components/nav/__tests__/Tabs.test.js +0 -128
  175. package/dist/components/roles/__tests__/RoleSelector.test.js +0 -134
  176. package/dist/components/ui/__tests__/Badge.test.js +0 -76
  177. package/dist/components/ui/__tests__/Button.test.js +0 -166
  178. package/dist/components/ui/__tests__/Card.test.js +0 -103
  179. package/dist/components/ui/__tests__/IconToggle.test.js +0 -59
  180. package/dist/components/ui/__tests__/IconToggleGroup.test.js +0 -30
  181. package/dist/components/ui/__tests__/Pagination.test.js +0 -113
  182. package/dist/components/ui/__tests__/Popover.test.js +0 -44
  183. package/dist/components/ui/__tests__/dropdown-popover-icon.test.js +0 -125
  184. package/dist/components/ui/__tests__/gap-primitives-interactive.test.js +0 -112
  185. package/dist/components/ui/__tests__/gap-primitives.test.js +0 -84
  186. package/dist/components/ui/__tests__/interaction-primitives.fixture.svelte +0 -13
  187. package/dist/components/ui/__tests__/interaction-primitives.fixture.svelte.d.ts +0 -19
  188. package/dist/components/ui/__tests__/interaction-primitives.fixture.svelte.d.ts.map +0 -1
  189. package/dist/components/ui/__tests__/interaction-primitives.test.js +0 -31
  190. package/dist/components/ui/__tests__/phone-touch-targets.test.js +0 -39
  191. package/dist/i18n/__tests__/i18n.test.js +0 -74
  192. package/dist/i18n/__tests__/render-parity.spec.js +0 -37
  193. package/dist/svelte/__tests__/playground.test.js +0 -20
  194. package/dist/themes/__tests__/control-boundary-contrast.test.js +0 -112
  195. package/dist/themes/__tests__/create-theme.test.js +0 -79
  196. package/dist/themes/__tests__/css-generator.test.js +0 -129
  197. package/dist/themes/__tests__/happyvertical-theme.test.js +0 -231
  198. package/dist/themes/__tests__/registry.test.js +0 -43
  199. package/dist/themes/__tests__/styles-lockstep.test.js +0 -32
  200. package/dist/themes/__tests__/theme-provider.test.js +0 -147
  201. package/dist/themes/__tests__/theme-script.test.js +0 -130
  202. package/dist/themes/__tests__/token-aliases.test.js +0 -178
  203. package/dist/utils/forms/__tests__/formatters.test.js +0 -27
  204. package/dist/utils/theme/__tests__/color.test.js +0 -89
  205. package/dist/utils/theme/__tests__/typography.test.js +0 -11
@@ -1,504 +0,0 @@
1
- /**
2
- * CameraCapture rendered states, stream release, and native form posting
3
- * (smrt#3290). `navigator.mediaDevices` is a fake; canvas, playback,
4
- * DataTransfer and the `formdata` event are jsdom stand-ins from
5
- * `capture-test-env.ts`.
6
- */
7
- import { fireEvent, render, screen, waitFor } from '@testing-library/svelte';
8
- import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
9
- import { expectNoA11yViolations } from '../../../test-support/a11y.js';
10
- import CameraCapture from '../CameraCapture.svelte';
11
- import CaptureFormFixture from './capture-form.fixture.svelte';
12
- import { createFileList, fakeStream, installMediaDevices, removeMediaDevices, stubCanvas, stubMediaPlayback, submittedEntries, useFileFieldStrategy, } from './capture-test-env.js';
13
- function grantCamera(track = { stop: vi.fn() }) {
14
- const getUserMedia = vi.fn().mockResolvedValue(fakeStream([track]));
15
- installMediaDevices({ getUserMedia });
16
- return { getUserMedia, track };
17
- }
18
- function rejectCamera(name) {
19
- const getUserMedia = vi.fn().mockRejectedValue(new DOMException('x', name));
20
- installMediaDevices({ getUserMedia });
21
- return getUserMedia;
22
- }
23
- function root(container) {
24
- return container.querySelector('.camera-capture');
25
- }
26
- async function expectState(container, state) {
27
- await waitFor(() => expect(root(container)).toHaveAttribute('data-state', state));
28
- }
29
- beforeEach(() => {
30
- stubCanvas();
31
- stubMediaPlayback();
32
- useFileFieldStrategy('data-transfer');
33
- });
34
- afterEach(() => {
35
- removeMediaDevices();
36
- vi.restoreAllMocks();
37
- vi.unstubAllGlobals();
38
- });
39
- describe('CameraCapture states', () => {
40
- it('shows the starting state while the permission prompt is pending', async () => {
41
- installMediaDevices({ getUserMedia: () => new Promise(() => { }) });
42
- const { container } = render(CameraCapture);
43
- await expectState(container, 'starting');
44
- expect(screen.getByRole('status')).toHaveTextContent('Starting camera…');
45
- expect(screen.queryByRole('button')).not.toBeInTheDocument();
46
- });
47
- it('streams a live preview with a Take photo action', async () => {
48
- const { getUserMedia } = grantCamera();
49
- const { container } = render(CameraCapture, {
50
- props: { facingMode: 'user' },
51
- });
52
- await expectState(container, 'streaming');
53
- expect(getUserMedia).toHaveBeenCalledWith({
54
- video: { facingMode: 'user' },
55
- audio: false,
56
- });
57
- expect(container.querySelector('video')).not.toHaveClass('hidden');
58
- expect(screen.getByRole('button', { name: 'Take photo' })).toBeEnabled();
59
- await expectNoA11yViolations(container);
60
- });
61
- it.each([
62
- ['NotAllowedError', 'permission-denied', /Camera access was denied/],
63
- ['NotFoundError', 'no-camera', /No camera was found/],
64
- ['AbortError', 'error', /Could not access the camera/],
65
- ])('renders %s as the %s state with a retry', async (name, state, copy) => {
66
- const getUserMedia = rejectCamera(name);
67
- const { container } = render(CameraCapture);
68
- await expectState(container, state);
69
- expect(screen.getByRole('status')).toHaveTextContent(copy);
70
- await expectNoA11yViolations(container);
71
- await fireEvent.click(screen.getByRole('button', { name: 'Try again' }));
72
- await waitFor(() => expect(getUserMedia).toHaveBeenCalledTimes(2));
73
- });
74
- it('renders the unsupported state without getUserMedia, with no file-input fallback by default', async () => {
75
- installMediaDevices(undefined);
76
- const { container } = render(CameraCapture, { props: { name: 'photo' } });
77
- await expectState(container, 'unsupported');
78
- expect(screen.getByRole('status')).toHaveTextContent(/cannot use the camera/);
79
- expect(container.querySelector('input[capture]')).toBeNull();
80
- expect(screen.queryByRole('button')).not.toBeInTheDocument();
81
- await expectNoA11yViolations(container);
82
- });
83
- it('renders the off state when disabled and never requests the camera', async () => {
84
- const { getUserMedia } = grantCamera();
85
- const { container } = render(CameraCapture, { props: { disabled: true } });
86
- await expectState(container, 'off');
87
- expect(screen.getByRole('status')).toHaveTextContent('The camera is off.');
88
- expect(getUserMedia).not.toHaveBeenCalled();
89
- });
90
- it('accepts label overrides', async () => {
91
- rejectCamera('NotAllowedError');
92
- render(CameraCapture, {
93
- props: {
94
- labels: { permissionDenied: 'Kamera verweigert', retry: 'Nochmal' },
95
- },
96
- });
97
- expect(await screen.findByText('Kamera verweigert')).toBeInTheDocument();
98
- expect(screen.getByRole('button', { name: 'Nochmal' })).toBeInTheDocument();
99
- });
100
- });
101
- describe('CameraCapture review flow', () => {
102
- it('captures, reviews, retakes, and reports only the committed photo', async () => {
103
- const { getUserMedia, track } = grantCamera();
104
- const onCapture = vi.fn();
105
- const { container } = render(CameraCapture, { props: { onCapture } });
106
- await expectState(container, 'streaming');
107
- await fireEvent.click(screen.getByRole('button', { name: 'Take photo' }));
108
- await expectState(container, 'reviewing');
109
- expect(track.stop).toHaveBeenCalledTimes(1);
110
- expect(screen.getByRole('img', { name: 'Captured photo, not yet used' })).toBeInTheDocument();
111
- await expectNoA11yViolations(container);
112
- await fireEvent.click(screen.getByRole('button', { name: 'Retake' }));
113
- await expectState(container, 'streaming');
114
- expect(getUserMedia).toHaveBeenCalledTimes(2);
115
- expect(onCapture).not.toHaveBeenCalled();
116
- await fireEvent.click(screen.getByRole('button', { name: 'Take photo' }));
117
- await expectState(container, 'reviewing');
118
- await fireEvent.click(screen.getByRole('button', { name: 'Use photo' }));
119
- await expectState(container, 'committed');
120
- expect(onCapture).toHaveBeenCalledTimes(1);
121
- const [{ blob, dataUrl }] = onCapture.mock.calls[0];
122
- expect(blob).toBeInstanceOf(Blob);
123
- expect(blob.type).toBe('image/jpeg');
124
- expect(dataUrl).toMatch(/^data:image\/jpeg/);
125
- expect(screen.getByRole('status')).toHaveTextContent('Photo attached.');
126
- expect(screen.getByRole('img', { name: 'Attached photo' })).toBeInTheDocument();
127
- });
128
- it('stops the stream on unmount', async () => {
129
- const { track } = grantCamera();
130
- const { container, unmount } = render(CameraCapture);
131
- await expectState(container, 'streaming');
132
- unmount();
133
- expect(track.stop).toHaveBeenCalledTimes(1);
134
- });
135
- it('stops the stream when disabled and restarts it when re-enabled', async () => {
136
- const { getUserMedia, track } = grantCamera();
137
- const { container, rerender } = render(CameraCapture);
138
- await expectState(container, 'streaming');
139
- await rerender({ disabled: true });
140
- expect(track.stop).toHaveBeenCalledTimes(1);
141
- await expectState(container, 'off');
142
- await rerender({ disabled: false });
143
- await expectState(container, 'streaming');
144
- expect(getUserMedia).toHaveBeenCalledTimes(2);
145
- });
146
- it('re-requests the camera when facingMode changes while live', async () => {
147
- const { getUserMedia, track } = grantCamera();
148
- const { container, rerender } = render(CameraCapture);
149
- await expectState(container, 'streaming');
150
- await rerender({ facingMode: 'user' });
151
- expect(track.stop).toHaveBeenCalledTimes(1);
152
- await waitFor(() => expect(getUserMedia).toHaveBeenLastCalledWith({
153
- video: { facingMode: 'user' },
154
- audio: false,
155
- }));
156
- await expectState(container, 'streaming');
157
- });
158
- it('keeps a photo under review across disable without reopening the camera', async () => {
159
- const { getUserMedia } = grantCamera();
160
- const { container, rerender } = render(CameraCapture);
161
- await expectState(container, 'streaming');
162
- await fireEvent.click(screen.getByRole('button', { name: 'Take photo' }));
163
- await expectState(container, 'reviewing');
164
- await rerender({ disabled: true });
165
- expect(root(container)).toHaveAttribute('data-state', 'reviewing');
166
- expect(screen.getByRole('button', { name: 'Use photo' })).toBeDisabled();
167
- await rerender({ disabled: false });
168
- expect(root(container)).toHaveAttribute('data-state', 'reviewing');
169
- expect(getUserMedia).toHaveBeenCalledTimes(1);
170
- });
171
- it('releases a stream granted after unmount (late permission)', async () => {
172
- let grant;
173
- installMediaDevices({
174
- getUserMedia: () => new Promise((resolve) => {
175
- grant = resolve;
176
- }),
177
- });
178
- const track = { stop: vi.fn() };
179
- const { unmount } = render(CameraCapture);
180
- await waitFor(() => expect(grant).toBeTypeOf('function'));
181
- unmount();
182
- grant(fakeStream([track]));
183
- await waitFor(() => expect(track.stop).toHaveBeenCalledTimes(1));
184
- });
185
- });
186
- describe('CameraCapture native form post', () => {
187
- async function commitPhoto(container) {
188
- await expectState(container, 'streaming');
189
- await fireEvent.click(screen.getByRole('button', { name: 'Take photo' }));
190
- await expectState(container, 'reviewing');
191
- await fireEvent.click(screen.getByRole('button', { name: 'Use photo' }));
192
- await expectState(container, 'committed');
193
- }
194
- it('posts the committed photo as a file through DataTransfer', async () => {
195
- grantCamera();
196
- const onClear = vi.fn();
197
- const { container } = render(CaptureFormFixture, {
198
- props: { kind: 'camera', name: 'photo', onClear },
199
- });
200
- const form = container.querySelector('form');
201
- expect(root(container)).toHaveAttribute('data-smrt-file-field', 'data-transfer');
202
- expect(form.method).toBe('post');
203
- expect(form.enctype).toBe('multipart/form-data');
204
- // Before commit the field posts what an empty native file input posts.
205
- await expectState(container, 'streaming');
206
- expect(submittedEntries(form).get('photo').size).toBe(0);
207
- await commitPhoto(container);
208
- const posted = submittedEntries(form).getAll('photo');
209
- expect(posted).toHaveLength(1);
210
- expect(posted[0].name).toBe('photo.jpg');
211
- expect(posted[0].type).toBe('image/jpeg');
212
- expect(posted[0].size).toBeGreaterThan(0);
213
- await fireEvent.click(screen.getByRole('button', { name: 'Retake' }));
214
- expect(onClear).toHaveBeenCalledTimes(1);
215
- expect(submittedEntries(form).get('photo').size).toBe(0);
216
- });
217
- it('honours fileName and imageType', async () => {
218
- grantCamera();
219
- const { container } = render(CaptureFormFixture, {
220
- props: {
221
- kind: 'camera',
222
- name: 'receipt',
223
- fileName: 'receipt.png',
224
- imageType: 'image/png',
225
- },
226
- });
227
- await commitPhoto(container);
228
- const posted = submittedEntries(container.querySelector('form')).get('receipt');
229
- expect(posted.name).toBe('receipt.png');
230
- expect(posted.type).toBe('image/png');
231
- });
232
- it('falls back to the formdata event when DataTransfer is unavailable', async () => {
233
- useFileFieldStrategy('formdata-event');
234
- grantCamera();
235
- const { container } = render(CaptureFormFixture, {
236
- props: { kind: 'camera', name: 'photo' },
237
- });
238
- const form = container.querySelector('form');
239
- await waitFor(() => expect(root(container)).toHaveAttribute('data-smrt-file-field', 'formdata-event'));
240
- const field = container.querySelector('[data-smrt-capture-field]');
241
- expect(field).not.toHaveAttribute('name');
242
- await commitPhoto(container);
243
- const posted = submittedEntries(form).getAll('photo');
244
- expect(posted).toHaveLength(1);
245
- expect(posted[0].name).toBe('photo.jpg');
246
- });
247
- it('posts nothing but still reports the photo when no strategy exists', async () => {
248
- useFileFieldStrategy('none');
249
- grantCamera();
250
- const onCapture = vi.fn();
251
- const { container } = render(CaptureFormFixture, {
252
- props: { kind: 'camera', name: 'photo', onCapture },
253
- });
254
- await waitFor(() => expect(root(container)).toHaveAttribute('data-smrt-file-field', 'none'));
255
- await commitPhoto(container);
256
- expect(onCapture).toHaveBeenCalledTimes(1);
257
- expect(submittedEntries(container.querySelector('form')).has('photo')).toBe(false);
258
- });
259
- it('opt-in file-input fallback posts the chosen photo natively', async () => {
260
- installMediaDevices(undefined);
261
- const onCapture = vi.fn();
262
- const { container } = render(CaptureFormFixture, {
263
- props: {
264
- kind: 'camera',
265
- name: 'photo',
266
- fileInputFallback: true,
267
- onCapture,
268
- },
269
- });
270
- await expectState(container, 'fallback');
271
- expect(root(container)).toHaveAttribute('data-smrt-file-field', 'file-input');
272
- const picker = screen.getByLabelText('Take or choose a photo');
273
- expect(picker).toHaveAttribute('capture', 'environment');
274
- expect(picker).toHaveAttribute('accept', 'image/*');
275
- expect(container.querySelector('[data-smrt-capture-field]')).toBeNull();
276
- await expectNoA11yViolations(container);
277
- const chosen = new File(['jpeg'], 'IMG_0001.jpg', { type: 'image/jpeg' });
278
- picker.files = createFileList([chosen]);
279
- await fireEvent.change(picker);
280
- await expectState(container, 'committed');
281
- expect(onCapture).toHaveBeenCalledWith({
282
- blob: chosen,
283
- dataUrl: expect.stringMatching(/^data:image\/jpeg/),
284
- });
285
- expect(submittedEntries(container.querySelector('form')).getAll('photo')).toEqual([chosen]);
286
- });
287
- });
288
- describe('CameraCapture disabled with the opt-in file-input fallback', () => {
289
- it('keeps a committed photo in the submission while blocking the picker', async () => {
290
- installMediaDevices(undefined);
291
- const props = { kind: 'camera', name: 'photo', fileInputFallback: true };
292
- const { container, rerender } = render(CaptureFormFixture, { props });
293
- await expectState(container, 'fallback');
294
- const form = container.querySelector('form');
295
- const picker = screen.getByLabelText('Take or choose a photo');
296
- const chosen = new File(['jpeg'], 'IMG_0001.jpg', { type: 'image/jpeg' });
297
- picker.files = createFileList([chosen]);
298
- await fireEvent.change(picker);
299
- await expectState(container, 'committed');
300
- await rerender({ ...props, disabled: true });
301
- await waitFor(() => expect(root(container)).toHaveClass('disabled'));
302
- // The photo still shown is the photo still posted.
303
- expect(submittedEntries(form).getAll('photo')).toEqual([chosen]);
304
- // ...but the picker cannot be opened or tabbed to.
305
- expect(picker).toHaveAttribute('aria-disabled', 'true');
306
- expect(picker).toHaveAttribute('tabindex', '-1');
307
- const click = new MouseEvent('click', { bubbles: true, cancelable: true });
308
- picker.dispatchEvent(click);
309
- expect(click.defaultPrevented).toBe(true);
310
- await rerender({ ...props, disabled: false });
311
- await waitFor(() => expect(picker).not.toHaveAttribute('aria-disabled'));
312
- const reopen = new MouseEvent('click', { bubbles: true, cancelable: true });
313
- picker.dispatchEvent(reopen);
314
- expect(reopen.defaultPrevented).toBe(false);
315
- expect(submittedEntries(form).getAll('photo')).toEqual([chosen]);
316
- });
317
- });
318
- describe('CameraCapture in a disabled fieldset', () => {
319
- it.each([
320
- 'data-transfer',
321
- 'formdata-event',
322
- ])('posts nothing, committed or empty, like a disabled native control (%s)', async (strategy) => {
323
- useFileFieldStrategy(strategy);
324
- grantCamera();
325
- const { container } = render(CaptureFormFixture, {
326
- props: { kind: 'camera', name: 'photo', inFieldset: true },
327
- });
328
- await waitFor(() => expect(root(container)).toHaveAttribute('data-smrt-file-field', strategy));
329
- const form = container.querySelector('form');
330
- const fieldset = form.querySelector('fieldset');
331
- await expectState(container, 'streaming');
332
- fieldset.disabled = true;
333
- expect(submittedEntries(form).has('photo')).toBe(false);
334
- fieldset.disabled = false;
335
- await fireEvent.click(screen.getByRole('button', { name: 'Take photo' }));
336
- await expectState(container, 'reviewing');
337
- await fireEvent.click(screen.getByRole('button', { name: 'Use photo' }));
338
- await expectState(container, 'committed');
339
- expect(submittedEntries(form).getAll('photo')).toHaveLength(1);
340
- fieldset.disabled = true;
341
- expect(submittedEntries(form).has('photo')).toBe(false);
342
- expect(submittedEntries(form).get('note')).toBe('hazard');
343
- fieldset.disabled = false;
344
- expect(submittedEntries(form).getAll('photo')).toHaveLength(1);
345
- });
346
- });
347
- /**
348
- * A reset of the owning form (smrt#3290): SvelteKit `enhance`'s `update()`
349
- * after a success, `createFormRetry()`'s conditional reset, a reset button and
350
- * `form.reset()` all empty a native file input, so the photo goes with it.
351
- * jsdom's reset does not empty file inputs (browsers do), so these assertions
352
- * prove the component empties its own field.
353
- */
354
- describe('CameraCapture form reset', () => {
355
- function form(container) {
356
- return container.querySelector('form');
357
- }
358
- async function commitPhoto(container) {
359
- await expectState(container, 'streaming');
360
- await fireEvent.click(screen.getByRole('button', { name: 'Take photo' }));
361
- await expectState(container, 'reviewing');
362
- await fireEvent.click(screen.getByRole('button', { name: 'Use photo' }));
363
- await expectState(container, 'committed');
364
- }
365
- function postedPhotos(container) {
366
- return submittedEntries(form(container)).getAll('photo');
367
- }
368
- it.each([
369
- 'data-transfer',
370
- 'formdata-event',
371
- ])('discards a committed photo and reopens the camera (%s)', async (strategy) => {
372
- useFileFieldStrategy(strategy);
373
- const { getUserMedia } = grantCamera();
374
- const onClear = vi.fn();
375
- const { container } = render(CaptureFormFixture, {
376
- props: { kind: 'camera', name: 'photo', onClear },
377
- });
378
- await waitFor(() => expect(root(container)).toHaveAttribute('data-smrt-file-field', strategy));
379
- await commitPhoto(container);
380
- expect(postedPhotos(container)[0].name).toBe('photo.jpg');
381
- form(container).reset();
382
- await expectState(container, 'streaming');
383
- expect(onClear).toHaveBeenCalledTimes(1);
384
- expect(getUserMedia).toHaveBeenCalledTimes(2);
385
- expect(screen.queryByRole('img')).not.toBeInTheDocument();
386
- const posted = postedPhotos(container);
387
- expect(posted).toHaveLength(1);
388
- expect(posted[0].size).toBe(0);
389
- expect(posted[0].name).toBe('');
390
- // The next entry takes and posts its own photo.
391
- await commitPhoto(container);
392
- expect(postedPhotos(container)[0].name).toBe('photo.jpg');
393
- });
394
- it('a reset button discards a photo under review, without onClear', async () => {
395
- const { getUserMedia } = grantCamera();
396
- const onClear = vi.fn();
397
- const onCapture = vi.fn();
398
- const { container } = render(CaptureFormFixture, {
399
- props: { kind: 'camera', name: 'photo', onClear, onCapture },
400
- });
401
- await expectState(container, 'streaming');
402
- await fireEvent.click(screen.getByRole('button', { name: 'Take photo' }));
403
- await expectState(container, 'reviewing');
404
- await fireEvent.click(screen.getByRole('button', { name: 'Reset form' }));
405
- await expectState(container, 'streaming');
406
- expect(screen.queryByRole('img')).not.toBeInTheDocument();
407
- expect(getUserMedia).toHaveBeenCalledTimes(2);
408
- expect(onClear).not.toHaveBeenCalled();
409
- expect(onCapture).not.toHaveBeenCalled();
410
- });
411
- it('clears a committed photo while disabled and keeps the camera off', async () => {
412
- const { getUserMedia } = grantCamera();
413
- const onClear = vi.fn();
414
- const props = { kind: 'camera', name: 'photo', onClear };
415
- const { container, rerender } = render(CaptureFormFixture, { props });
416
- await commitPhoto(container);
417
- await rerender({ ...props, disabled: true });
418
- form(container).reset();
419
- await expectState(container, 'off');
420
- expect(onClear).toHaveBeenCalledTimes(1);
421
- expect(getUserMedia).toHaveBeenCalledTimes(1);
422
- expect(postedPhotos(container)[0].size).toBe(0);
423
- await rerender({ ...props, disabled: false });
424
- await expectState(container, 'streaming');
425
- expect(getUserMedia).toHaveBeenCalledTimes(2);
426
- });
427
- it('supersedes a frame still encoding', async () => {
428
- let finish;
429
- vi.mocked(HTMLCanvasElement.prototype.toBlob).mockImplementation((callback, type) => {
430
- finish = () => callback(new Blob(['pixels'], { type }));
431
- });
432
- const { getUserMedia } = grantCamera();
433
- const { container } = render(CaptureFormFixture, {
434
- props: { kind: 'camera', name: 'photo' },
435
- });
436
- await expectState(container, 'streaming');
437
- await fireEvent.click(screen.getByRole('button', { name: 'Take photo' }));
438
- form(container).reset();
439
- finish();
440
- await waitFor(() => expect(getUserMedia).toHaveBeenCalledTimes(2));
441
- await expectState(container, 'streaming');
442
- expect(screen.queryByRole('img')).not.toBeInTheDocument();
443
- });
444
- it('changes nothing when no photo is held, and never re-prompts', async () => {
445
- const getUserMedia = rejectCamera('NotAllowedError');
446
- const { container } = render(CaptureFormFixture, {
447
- props: { kind: 'camera', name: 'photo' },
448
- });
449
- await expectState(container, 'permission-denied');
450
- form(container).reset();
451
- expect(root(container)).toHaveAttribute('data-state', 'permission-denied');
452
- expect(getUserMedia).toHaveBeenCalledTimes(1);
453
- });
454
- it('opt-in file-input fallback returns to the picker', async () => {
455
- installMediaDevices(undefined);
456
- const onClear = vi.fn();
457
- const { container } = render(CaptureFormFixture, {
458
- props: {
459
- kind: 'camera',
460
- name: 'photo',
461
- fileInputFallback: true,
462
- onClear,
463
- },
464
- });
465
- await expectState(container, 'fallback');
466
- const picker = screen.getByLabelText('Take or choose a photo');
467
- picker.files = createFileList([
468
- new File(['jpeg'], 'IMG_0001.jpg', { type: 'image/jpeg' }),
469
- ]);
470
- await fireEvent.change(picker);
471
- await expectState(container, 'committed');
472
- form(container).reset();
473
- await expectState(container, 'fallback');
474
- expect(onClear).toHaveBeenCalledTimes(1);
475
- expect(screen.queryByRole('img')).not.toBeInTheDocument();
476
- expect(picker.files).toHaveLength(0);
477
- const posted = postedPhotos(container);
478
- expect(posted).toHaveLength(1);
479
- expect(posted[0].size).toBe(0);
480
- });
481
- it('opt-in file-input fallback drops a pick still being read', async () => {
482
- installMediaDevices(undefined);
483
- const onCapture = vi.fn();
484
- const { container } = render(CaptureFormFixture, {
485
- props: {
486
- kind: 'camera',
487
- name: 'photo',
488
- fileInputFallback: true,
489
- onCapture,
490
- },
491
- });
492
- await expectState(container, 'fallback');
493
- const picker = screen.getByLabelText('Take or choose a photo');
494
- picker.files = createFileList([
495
- new File(['jpeg'], 'IMG_0002.jpg', { type: 'image/jpeg' }),
496
- ]);
497
- // Reset in the same task as the change, before the FileReader can land.
498
- picker.dispatchEvent(new Event('change', { bubbles: true }));
499
- form(container).reset();
500
- await new Promise((resolve) => setTimeout(resolve, 20));
501
- expect(root(container)).toHaveAttribute('data-state', 'fallback');
502
- expect(onCapture).not.toHaveBeenCalled();
503
- });
504
- });