@happyvertical/smrt-ui 0.51.39 → 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 +25 -3
- package/README.md +558 -8
- package/dist/actions/__tests__/swipe-dismiss.test.js +69 -0
- package/dist/actions/swipe-dismiss.d.ts +42 -0
- package/dist/actions/swipe-dismiss.d.ts.map +1 -0
- package/dist/actions/swipe-dismiss.js +69 -0
- package/dist/components/calendar/Calendar.svelte +5 -0
- package/dist/components/calendar/Calendar.svelte.d.ts.map +1 -1
- package/dist/components/calendar/CalendarView.svelte +1250 -0
- package/dist/components/calendar/CalendarView.svelte.d.ts +66 -0
- package/dist/components/calendar/CalendarView.svelte.d.ts.map +1 -0
- package/dist/components/calendar/DayView.svelte +5 -0
- package/dist/components/calendar/DayView.svelte.d.ts.map +1 -1
- package/dist/components/calendar/__tests__/CalendarView.test.js +226 -0
- package/dist/components/calendar/__tests__/calendar-model.test.js +138 -0
- package/dist/components/calendar/calendar-model.d.ts +178 -0
- package/dist/components/calendar/calendar-model.d.ts.map +1 -0
- package/dist/components/calendar/calendar-model.js +362 -0
- package/dist/components/calendar/index.d.ts +12 -1
- package/dist/components/calendar/index.d.ts.map +1 -1
- package/dist/components/calendar/index.js +11 -1
- package/dist/components/data/CollectionList.svelte +23 -5
- package/dist/components/data/CollectionList.svelte.d.ts +8 -2
- package/dist/components/data/CollectionList.svelte.d.ts.map +1 -1
- package/dist/components/data/DataTable.svelte +178 -55
- package/dist/components/data/DataTable.svelte.d.ts.map +1 -1
- package/dist/components/data/DataTableController.d.ts +4 -0
- package/dist/components/data/DataTableController.d.ts.map +1 -1
- package/dist/components/data/DataTableController.js +11 -4
- 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/ListSortSelect.svelte +121 -0
- package/dist/components/data/ListSortSelect.svelte.d.ts +22 -0
- package/dist/components/data/ListSortSelect.svelte.d.ts.map +1 -0
- package/dist/components/data/SortableHeader.svelte +169 -0
- package/dist/components/data/SortableHeader.svelte.d.ts +30 -0
- package/dist/components/data/SortableHeader.svelte.d.ts.map +1 -0
- package/dist/components/data/__tests__/CollectionList.test.js +37 -0
- package/dist/components/data/__tests__/DataTable.test.js +76 -0
- package/dist/components/data/__tests__/DataTableController.test.js +35 -0
- package/dist/components/data/__tests__/DataTableResponsive.test.js +80 -0
- package/dist/components/data/__tests__/SortableHeader.test.js +101 -0
- package/dist/components/data/__tests__/link-step-surface.test.js +172 -0
- package/dist/components/data/__tests__/list-sort-state.svelte.test.js +71 -0
- package/dist/components/data/__tests__/list-sort.test.js +91 -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/__tests__/surface-navigation.test.js +121 -0
- package/dist/components/data/data-surface.d.ts +1 -0
- package/dist/components/data/data-surface.d.ts.map +1 -1
- package/dist/components/data/data-surface.js +1 -0
- package/dist/components/data/index.d.ts +6 -0
- package/dist/components/data/index.d.ts.map +1 -1
- package/dist/components/data/index.js +6 -0
- package/dist/components/data/link-surface.d.ts +79 -0
- package/dist/components/data/link-surface.d.ts.map +1 -0
- package/dist/components/data/link-surface.js +155 -0
- package/dist/components/data/list-sort-state.svelte.d.ts +39 -0
- package/dist/components/data/list-sort-state.svelte.d.ts.map +1 -0
- package/dist/components/data/list-sort-state.svelte.js +47 -0
- package/dist/components/data/list-sort.d.ts +94 -0
- package/dist/components/data/list-sort.d.ts.map +1 -0
- package/dist/components/data/list-sort.js +182 -0
- package/dist/components/data/step-surface.d.ts +73 -0
- package/dist/components/data/step-surface.d.ts.map +1 -0
- package/dist/components/data/step-surface.js +177 -0
- package/dist/components/data/surface-navigation.d.ts +49 -0
- package/dist/components/data/surface-navigation.d.ts.map +1 -0
- package/dist/components/data/surface-navigation.js +100 -0
- package/dist/components/data/types.d.ts +27 -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/PhoneSheet.svelte +224 -0
- package/dist/components/feedback/PhoneSheet.svelte.d.ts +35 -0
- package/dist/components/feedback/PhoneSheet.svelte.d.ts.map +1 -0
- 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/WorkingStrip.svelte +426 -0
- package/dist/components/feedback/WorkingStrip.svelte.d.ts +37 -0
- package/dist/components/feedback/WorkingStrip.svelte.d.ts.map +1 -0
- package/dist/components/feedback/__tests__/ConfirmDialog.test.js +38 -20
- package/dist/components/feedback/__tests__/PhoneSheet.test.js +101 -0
- package/dist/components/feedback/__tests__/WorkingStrip.test.js +172 -0
- 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 +5 -1
- package/dist/components/feedback/index.d.ts.map +1 -1
- package/dist/components/feedback/index.js +5 -0
- 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/feedback/working-status.d.ts +34 -0
- package/dist/components/feedback/working-status.d.ts.map +1 -0
- package/dist/components/feedback/working-status.js +5 -0
- 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 +34 -3
- 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 +108 -12
- package/dist/components/forms/Combobox.svelte.d.ts +6 -0
- package/dist/components/forms/Combobox.svelte.d.ts.map +1 -1
- package/dist/components/forms/DictationButton.svelte +128 -0
- package/dist/components/forms/DictationButton.svelte.d.ts +19 -0
- package/dist/components/forms/DictationButton.svelte.d.ts.map +1 -0
- package/dist/components/forms/DictationStatus.svelte +164 -0
- package/dist/components/forms/DictationStatus.svelte.d.ts +11 -0
- package/dist/components/forms/DictationStatus.svelte.d.ts.map +1 -0
- package/dist/components/forms/Fieldset.svelte +5 -1
- package/dist/components/forms/Fieldset.svelte.d.ts +2 -0
- package/dist/components/forms/Fieldset.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/FormActionBar.svelte +84 -0
- package/dist/components/forms/FormActionBar.svelte.d.ts +23 -0
- package/dist/components/forms/FormActionBar.svelte.d.ts.map +1 -0
- package/dist/components/forms/Input.svelte +17 -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 +16 -4
- 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/SearchInput.svelte +207 -0
- package/dist/components/forms/SearchInput.svelte.d.ts +22 -0
- package/dist/components/forms/SearchInput.svelte.d.ts.map +1 -0
- 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 +17 -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/StagedControlReview.svelte +21 -12
- package/dist/components/forms/StagedControlReview.svelte.d.ts.map +1 -1
- package/dist/components/forms/Switch.svelte +18 -5
- 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/TagsInput.svelte +8 -1
- package/dist/components/forms/Textarea.svelte +16 -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 +354 -0
- package/dist/components/forms/__tests__/ControlHitArea.test.js +73 -0
- package/dist/components/forms/__tests__/DictationButton.test.js +176 -0
- package/dist/components/forms/__tests__/Fieldset.test.js +37 -0
- package/dist/components/forms/__tests__/Form.test.js +46 -0
- package/dist/components/forms/__tests__/FormActionBar.test.js +22 -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__/SearchInput.test.js +92 -0
- package/dist/components/forms/__tests__/SignaturePad.test.js +338 -0
- package/dist/components/forms/__tests__/StagedControlReview.test.js +37 -3
- 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__/combobox-labels.fixture.svelte +19 -0
- package/dist/components/forms/__tests__/combobox-labels.fixture.svelte.d.ts +11 -0
- package/dist/components/forms/__tests__/combobox-labels.fixture.svelte.d.ts.map +1 -0
- package/dist/components/forms/__tests__/composite-registration.fixture.svelte +29 -0
- package/dist/components/forms/__tests__/composite-registration.fixture.svelte.d.ts +8 -0
- package/dist/components/forms/__tests__/composite-registration.fixture.svelte.d.ts.map +1 -0
- package/dist/components/forms/__tests__/control-interaction.test.js +19 -1
- package/dist/components/forms/__tests__/control-proposals.test.js +63 -0
- package/dist/components/forms/__tests__/dictation-recording.test.js +582 -0
- package/dist/components/forms/__tests__/dictation.test.js +486 -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__/long-press.test.js +175 -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__/place-picker.fixture.svelte +33 -0
- package/dist/components/forms/__tests__/place-picker.fixture.svelte.d.ts +12 -0
- package/dist/components/forms/__tests__/place-picker.fixture.svelte.d.ts.map +1 -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/audio-capture.d.ts +69 -0
- package/dist/components/forms/audio-capture.d.ts.map +1 -0
- package/dist/components/forms/audio-capture.js +193 -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/control-interaction.d.ts +6 -0
- package/dist/components/forms/control-interaction.d.ts.map +1 -1
- package/dist/components/forms/control-interaction.js +12 -1
- package/dist/components/forms/control-proposals.d.ts +55 -0
- package/dist/components/forms/control-proposals.d.ts.map +1 -0
- package/dist/components/forms/control-proposals.js +188 -0
- package/dist/components/forms/dictation-transcribe.d.ts +36 -0
- package/dist/components/forms/dictation-transcribe.d.ts.map +1 -0
- package/dist/components/forms/dictation-transcribe.js +65 -0
- package/dist/components/forms/dictation.svelte.d.ts +200 -0
- package/dist/components/forms/dictation.svelte.d.ts.map +1 -0
- package/dist/components/forms/dictation.svelte.js +686 -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 +21 -2
- package/dist/components/forms/index.d.ts.map +1 -1
- package/dist/components/forms/index.js +22 -1
- package/dist/components/forms/insert-text.d.ts +12 -0
- package/dist/components/forms/insert-text.d.ts.map +1 -0
- package/dist/components/forms/insert-text.js +38 -0
- package/dist/components/forms/long-press.d.ts +88 -0
- package/dist/components/forms/long-press.d.ts.map +1 -0
- package/dist/components/forms/long-press.js +213 -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/ready-beep.d.ts +24 -0
- package/dist/components/forms/ready-beep.d.ts.map +1 -0
- package/dist/components/forms/ready-beep.js +82 -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/layout/PageHeader.svelte +151 -17
- package/dist/components/layout/PageHeader.svelte.d.ts +47 -7
- package/dist/components/layout/PageHeader.svelte.d.ts.map +1 -1
- package/dist/components/layout/__tests__/PageHeader.test.js +87 -1
- package/dist/components/layout/index.d.ts +1 -0
- package/dist/components/layout/index.d.ts.map +1 -1
- package/dist/components/layout/index.js +1 -0
- package/dist/components/layout/page-header-context.d.ts +25 -0
- package/dist/components/layout/page-header-context.d.ts.map +1 -0
- package/dist/components/layout/page-header-context.js +16 -0
- 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/nav/Tabs.svelte +241 -0
- package/dist/components/nav/Tabs.svelte.d.ts +15 -0
- package/dist/components/nav/Tabs.svelte.d.ts.map +1 -1
- package/dist/components/nav/__tests__/LinkTabs.test.js +117 -0
- package/dist/components/nav/index.d.ts +1 -0
- package/dist/components/nav/index.d.ts.map +1 -1
- package/dist/components/nav/index.js +1 -0
- package/dist/components/nav/tabs-overflow.d.ts +12 -0
- package/dist/components/nav/tabs-overflow.d.ts.map +1 -0
- package/dist/components/nav/tabs-overflow.js +17 -0
- package/dist/components/nav/types.d.ts +8 -0
- package/dist/components/nav/types.d.ts.map +1 -1
- package/dist/components/ui/Button.svelte +16 -2
- package/dist/components/ui/Button.svelte.d.ts +2 -0
- package/dist/components/ui/Button.svelte.d.ts.map +1 -1
- package/dist/components/ui/Card.svelte +14 -5
- package/dist/components/ui/Dropdown.svelte +55 -1
- package/dist/components/ui/Dropdown.svelte.d.ts +8 -0
- package/dist/components/ui/Dropdown.svelte.d.ts.map +1 -1
- package/dist/components/ui/IconToggle.svelte +139 -0
- package/dist/components/ui/IconToggle.svelte.d.ts +32 -0
- package/dist/components/ui/IconToggle.svelte.d.ts.map +1 -0
- package/dist/components/ui/IconToggleGroup.svelte +56 -0
- package/dist/components/ui/IconToggleGroup.svelte.d.ts +21 -0
- package/dist/components/ui/IconToggleGroup.svelte.d.ts.map +1 -0
- package/dist/components/ui/Pagination.svelte +1 -1
- package/dist/components/ui/Popover.svelte +17 -1
- package/dist/components/ui/Popover.svelte.d.ts +8 -0
- package/dist/components/ui/Popover.svelte.d.ts.map +1 -1
- package/dist/components/ui/__tests__/IconToggle.test.js +59 -0
- package/dist/components/ui/__tests__/IconToggleGroup.test.js +30 -0
- package/dist/components/ui/__tests__/Popover.test.js +44 -0
- package/dist/components/ui/__tests__/dropdown-popover-icon.test.js +125 -0
- package/dist/components/ui/__tests__/phone-touch-targets.test.js +39 -0
- package/dist/components/ui/index.d.ts +2 -0
- package/dist/components/ui/index.d.ts.map +1 -1
- package/dist/components/ui/index.js +2 -0
- package/dist/i18n/strings.ui.d.ts +65 -0
- package/dist/i18n/strings.ui.d.ts.map +1 -1
- package/dist/i18n/strings.ui.js +72 -0
- package/dist/styles/tokens.css +2 -0
- package/dist/svelte/__tests__/playground.test.js +3 -1
- package/dist/svelte/playground/CalendarPreview.svelte +140 -0
- package/dist/svelte/playground/CalendarPreview.svelte.d.ts +4 -0
- package/dist/svelte/playground/CalendarPreview.svelte.d.ts.map +1 -0
- 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 +20 -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/dist/types-generic.d.ts +2 -0
- package/dist/types-generic.d.ts.map +1 -1
- package/package.json +9 -3
|
@@ -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
|
+
});
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `createFormRetry()` around a form holding a named `CameraCapture` or
|
|
3
|
+
* `SignaturePad` (#3290, #3291), through the structural fake of SvelteKit's
|
|
4
|
+
* `use:enhance` from the form-retry tests.
|
|
5
|
+
*
|
|
6
|
+
* A successful submit of an unchanged form calls `update({ reset: true })`,
|
|
7
|
+
* which resets the form and so empties every native file input. The capture
|
|
8
|
+
* component must empty with it: otherwise it keeps showing the attachment
|
|
9
|
+
* while the next submit posts an empty file entry, and the person believes a
|
|
10
|
+
* photo or signature went with a record that has none.
|
|
11
|
+
*
|
|
12
|
+
* The reverse matters as much: a capture taken or replaced while the submit is
|
|
13
|
+
* in flight is an edit the server has never seen, so the success must NOT
|
|
14
|
+
* reset it. Under the `formdata`-event strategy the file lives in no element,
|
|
15
|
+
* only in the entry list, so `useBrowserFormData()` makes `new FormData(form)`
|
|
16
|
+
* fire `formdata` as browsers do.
|
|
17
|
+
*/
|
|
18
|
+
import { fireEvent, render, screen, waitFor } from '@testing-library/svelte';
|
|
19
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
20
|
+
import { fakeEnhance, fakeRunOnceServer, } from '../form-retry/__tests__/fake-kit.js';
|
|
21
|
+
import { createFormRetry } from '../form-retry/controller.js';
|
|
22
|
+
import { resetFormRetryMemory } from '../form-retry/submission-key.js';
|
|
23
|
+
import CaptureFormFixture from './capture-form.fixture.svelte';
|
|
24
|
+
import { fakeStream, installMediaDevices, removeMediaDevices, stubCanvas, stubMediaPlayback, useBrowserFormData, useFileFieldStrategy, } from './capture-test-env.js';
|
|
25
|
+
const FIELD = { camera: 'photo', signature: 'signature' };
|
|
26
|
+
const FILE = {
|
|
27
|
+
camera: 'file:photo.jpg',
|
|
28
|
+
signature: 'file:signature.png',
|
|
29
|
+
};
|
|
30
|
+
beforeEach(() => {
|
|
31
|
+
resetFormRetryMemory();
|
|
32
|
+
sessionStorage.clear();
|
|
33
|
+
stubCanvas();
|
|
34
|
+
stubMediaPlayback();
|
|
35
|
+
useBrowserFormData();
|
|
36
|
+
installMediaDevices({
|
|
37
|
+
getUserMedia: vi.fn().mockResolvedValue(fakeStream()),
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
afterEach(() => {
|
|
41
|
+
removeMediaDevices();
|
|
42
|
+
vi.restoreAllMocks();
|
|
43
|
+
vi.unstubAllGlobals();
|
|
44
|
+
});
|
|
45
|
+
function root(container) {
|
|
46
|
+
return container.querySelector('[data-state]');
|
|
47
|
+
}
|
|
48
|
+
async function expectState(container, state) {
|
|
49
|
+
await waitFor(() => expect(root(container)).toHaveAttribute('data-state', state));
|
|
50
|
+
}
|
|
51
|
+
async function commit(container, kind) {
|
|
52
|
+
if (kind === 'camera') {
|
|
53
|
+
await expectState(container, 'streaming');
|
|
54
|
+
await fireEvent.click(screen.getByRole('button', { name: 'Take photo' }));
|
|
55
|
+
await expectState(container, 'reviewing');
|
|
56
|
+
await fireEvent.click(screen.getByRole('button', { name: 'Use photo' }));
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
const canvas = container.querySelector('canvas');
|
|
60
|
+
await fireEvent.pointerDown(canvas, { pointerType: 'mouse', pointerId: 1 });
|
|
61
|
+
await fireEvent.pointerMove(canvas, {
|
|
62
|
+
pointerType: 'mouse',
|
|
63
|
+
pointerId: 1,
|
|
64
|
+
clientX: 40,
|
|
65
|
+
clientY: 20,
|
|
66
|
+
});
|
|
67
|
+
await fireEvent.pointerUp(canvas, { pointerType: 'mouse', pointerId: 1 });
|
|
68
|
+
await fireEvent.click(screen.getByRole('button', { name: 'Use signature' }));
|
|
69
|
+
}
|
|
70
|
+
await expectState(container, 'committed');
|
|
71
|
+
}
|
|
72
|
+
/** Discard the committed capture as the person would, then commit another. */
|
|
73
|
+
async function replace(container, kind) {
|
|
74
|
+
// A different photo or signature: different bytes, so a different file.
|
|
75
|
+
vi.mocked(HTMLCanvasElement.prototype.toBlob).mockImplementation((callback, type) => callback(new Blob(['other pixels'], { type })));
|
|
76
|
+
await fireEvent.click(screen.getByRole('button', {
|
|
77
|
+
name: kind === 'camera' ? 'Retake' : 'Clear',
|
|
78
|
+
}));
|
|
79
|
+
await commit(container, kind);
|
|
80
|
+
}
|
|
81
|
+
function setup(kind, strategy) {
|
|
82
|
+
useFileFieldStrategy(strategy);
|
|
83
|
+
const onClear = vi.fn();
|
|
84
|
+
const view = render(CaptureFormFixture, {
|
|
85
|
+
props: { kind, name: FIELD[kind], onClear },
|
|
86
|
+
});
|
|
87
|
+
const form = view.container.querySelector('form');
|
|
88
|
+
const server = fakeRunOnceServer();
|
|
89
|
+
const retry = createFormRetry({ form: `capture-${kind}`, storage: null });
|
|
90
|
+
retry.attach(form);
|
|
91
|
+
const kit = fakeEnhance(form, retry.enhance(), server);
|
|
92
|
+
return { ...view, form, server, retry, kit, onClear };
|
|
93
|
+
}
|
|
94
|
+
const CASES = [
|
|
95
|
+
['camera', 'data-transfer'],
|
|
96
|
+
['camera', 'formdata-event'],
|
|
97
|
+
['signature', 'data-transfer'],
|
|
98
|
+
['signature', 'formdata-event'],
|
|
99
|
+
];
|
|
100
|
+
describe('createFormRetry with a capture field', () => {
|
|
101
|
+
it.each(CASES)('a successful submit clears the %s (%s), so the next submit cannot post a shown-but-empty attachment', async (kind, strategy) => {
|
|
102
|
+
const name = FIELD[kind];
|
|
103
|
+
const { container, server, retry, kit, onClear } = setup(kind, strategy);
|
|
104
|
+
await waitFor(() => expect(root(container)).toHaveAttribute('data-smrt-file-field', strategy));
|
|
105
|
+
await commit(container, kind);
|
|
106
|
+
await kit.submit();
|
|
107
|
+
expect(retry.state.status).toBe('success');
|
|
108
|
+
expect(kit.updates).toEqual([{ reset: true }]);
|
|
109
|
+
expect(server.rows).toHaveLength(1);
|
|
110
|
+
expect(server.rows[0].fields[name]).toEqual([FILE[kind]]);
|
|
111
|
+
// The reset reached the component: nothing is shown, onClear fired.
|
|
112
|
+
await expectState(container, kind === 'camera' ? 'streaming' : 'empty');
|
|
113
|
+
expect(onClear).toHaveBeenCalledTimes(1);
|
|
114
|
+
expect(screen.queryByRole('img')).not.toBeInTheDocument();
|
|
115
|
+
// The next submit posts what is shown: no file, and not the old one.
|
|
116
|
+
await kit.submit();
|
|
117
|
+
expect(server.rows).toHaveLength(2);
|
|
118
|
+
expect(server.rows[1].fields[name]).toEqual(['file:']);
|
|
119
|
+
// A new capture posts as its own record.
|
|
120
|
+
await commit(container, kind);
|
|
121
|
+
await kit.submit();
|
|
122
|
+
expect(server.rows).toHaveLength(3);
|
|
123
|
+
expect(server.rows[2].fields[name]).toEqual([FILE[kind]]);
|
|
124
|
+
});
|
|
125
|
+
it.each(CASES)('keeps a %s (%s) captured while the submit was in flight', async (kind, strategy) => {
|
|
126
|
+
const name = FIELD[kind];
|
|
127
|
+
const { container, server, retry, kit, onClear } = setup(kind, strategy);
|
|
128
|
+
await waitFor(() => expect(root(container)).toHaveAttribute('data-smrt-file-field', strategy));
|
|
129
|
+
const held = server.hold();
|
|
130
|
+
const first = kit.submit();
|
|
131
|
+
await vi.waitFor(() => expect(retry.state.status).toBe('submitting'));
|
|
132
|
+
// The next entry's capture is taken while the first is unresolved.
|
|
133
|
+
await commit(container, kind);
|
|
134
|
+
held.release();
|
|
135
|
+
await first;
|
|
136
|
+
expect(kit.updates).toEqual([{ reset: false }]);
|
|
137
|
+
expect(root(container)).toHaveAttribute('data-state', 'committed');
|
|
138
|
+
expect(onClear).not.toHaveBeenCalled();
|
|
139
|
+
await kit.submit();
|
|
140
|
+
expect(server.rows.at(-1)?.fields[name]).toEqual([FILE[kind]]);
|
|
141
|
+
});
|
|
142
|
+
it.each(CASES)('keeps a %s (%s) replaced while the submit was in flight', async (kind, strategy) => {
|
|
143
|
+
const name = FIELD[kind];
|
|
144
|
+
const { container, server, retry, kit, onClear } = setup(kind, strategy);
|
|
145
|
+
await waitFor(() => expect(root(container)).toHaveAttribute('data-smrt-file-field', strategy));
|
|
146
|
+
await commit(container, kind);
|
|
147
|
+
const held = server.hold();
|
|
148
|
+
const first = kit.submit();
|
|
149
|
+
await vi.waitFor(() => expect(retry.state.status).toBe('submitting'));
|
|
150
|
+
await replace(container, kind);
|
|
151
|
+
expect(onClear).toHaveBeenCalledTimes(1);
|
|
152
|
+
held.release();
|
|
153
|
+
await first;
|
|
154
|
+
// The first capture was recorded; the newer one is an unsent edit, so the
|
|
155
|
+
// success must leave it on screen and in the field.
|
|
156
|
+
expect(server.rows[0].fields[name]).toEqual([FILE[kind]]);
|
|
157
|
+
expect(kit.updates).toEqual([{ reset: false }]);
|
|
158
|
+
expect(root(container)).toHaveAttribute('data-state', 'committed');
|
|
159
|
+
expect(onClear).toHaveBeenCalledTimes(1);
|
|
160
|
+
await kit.submit();
|
|
161
|
+
const sent = server.requests.at(-1)?.get(name);
|
|
162
|
+
expect(sent.name).toBe(FILE[kind].slice('file:'.length));
|
|
163
|
+
expect(sent.size).toBe('other pixels'.length);
|
|
164
|
+
expect(server.rows).toHaveLength(2);
|
|
165
|
+
});
|
|
166
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import CameraCapture from '../CameraCapture.svelte';
|
|
3
|
+
import SignaturePad from '../SignaturePad.svelte';
|
|
4
|
+
|
|
5
|
+
let {
|
|
6
|
+
kind,
|
|
7
|
+
inFieldset = false,
|
|
8
|
+
...props
|
|
9
|
+
}: {
|
|
10
|
+
kind: 'camera' | 'signature';
|
|
11
|
+
/** Wrap the component in a `<fieldset>` the test can disable. */
|
|
12
|
+
inFieldset?: boolean;
|
|
13
|
+
[key: string]: unknown;
|
|
14
|
+
} = $props();
|
|
15
|
+
</script>
|
|
16
|
+
|
|
17
|
+
{#snippet capture()}
|
|
18
|
+
{#if kind === 'camera'}
|
|
19
|
+
<CameraCapture {...props} />
|
|
20
|
+
{:else}
|
|
21
|
+
<SignaturePad {...props} />
|
|
22
|
+
{/if}
|
|
23
|
+
{/snippet}
|
|
24
|
+
|
|
25
|
+
<!-- A plain native form: no submit handler, no hidden inputs wired by the caller. -->
|
|
26
|
+
<form method="POST" enctype="multipart/form-data" action="/submit">
|
|
27
|
+
<input type="hidden" name="note" value="hazard" />
|
|
28
|
+
{#if inFieldset}
|
|
29
|
+
<fieldset>
|
|
30
|
+
<legend>Capture</legend>
|
|
31
|
+
{@render capture()}
|
|
32
|
+
</fieldset>
|
|
33
|
+
{:else}
|
|
34
|
+
{@render capture()}
|
|
35
|
+
{/if}
|
|
36
|
+
<button type="submit">Submit</button>
|
|
37
|
+
<button type="reset">Reset form</button>
|
|
38
|
+
</form>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
type $$ComponentProps = {
|
|
2
|
+
kind: 'camera' | 'signature';
|
|
3
|
+
/** Wrap the component in a `<fieldset>` the test can disable. */
|
|
4
|
+
inFieldset?: boolean;
|
|
5
|
+
[key: string]: unknown;
|
|
6
|
+
};
|
|
7
|
+
declare const CaptureForm: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
8
|
+
type CaptureForm = ReturnType<typeof CaptureForm>;
|
|
9
|
+
export default CaptureForm;
|
|
10
|
+
//# sourceMappingURL=capture-form.fixture.svelte.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"capture-form.fixture.svelte.d.ts","sourceRoot":"","sources":["../../../../src/components/forms/__tests__/capture-form.fixture.svelte.ts"],"names":[],"mappings":"AAMC,KAAK,gBAAgB,GAAI;IACxB,IAAI,EAAE,QAAQ,GAAG,WAAW,CAAC;IAC7B,iEAAiE;IACjE,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB,CAAC;AAqCF,QAAA,MAAM,WAAW,sDAAwC,CAAC;AAC1D,KAAK,WAAW,GAAG,UAAU,CAAC,OAAO,WAAW,CAAC,CAAC;AAClD,eAAe,WAAW,CAAC"}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* jsdom stand-ins for the browser APIs `CameraCapture` and `SignaturePad` use
|
|
3
|
+
* (smrt#3290). jsdom has no `DataTransfer`, no canvas, no media playback and
|
|
4
|
+
* never fires the `formdata` event, so each is supplied here, as close to the
|
|
5
|
+
* browser contract as the tests need.
|
|
6
|
+
*/
|
|
7
|
+
import { type Mock } from 'vitest';
|
|
8
|
+
/**
|
|
9
|
+
* A real jsdom `FileList` holding `files`. jsdom's `input.files` setter only
|
|
10
|
+
* accepts genuine `FileList` instances and exposes no constructor, so the list
|
|
11
|
+
* is taken from a detached file input and filled through its implementation
|
|
12
|
+
* object. This keeps `new FormData(form)` reading the files exactly as a
|
|
13
|
+
* browser would after `input.files = dataTransfer.files`.
|
|
14
|
+
*/
|
|
15
|
+
export declare function createFileList(files: File[]): FileList;
|
|
16
|
+
/** Browser-shaped `DataTransfer` for jsdom: `items.add()` plus a real `files`. */
|
|
17
|
+
export declare class TestDataTransfer {
|
|
18
|
+
#private;
|
|
19
|
+
readonly items: {
|
|
20
|
+
add: (file: File) => null;
|
|
21
|
+
};
|
|
22
|
+
get files(): FileList;
|
|
23
|
+
}
|
|
24
|
+
/** Marker constructor: its presence is what feature detection reads. */
|
|
25
|
+
export declare class TestFormDataEvent {
|
|
26
|
+
}
|
|
27
|
+
/** Select the posting strategy the component will detect on mount. */
|
|
28
|
+
export declare function useFileFieldStrategy(strategy: 'data-transfer' | 'formdata-event' | 'none'): void;
|
|
29
|
+
/**
|
|
30
|
+
* The entry list a native submit of `form` would send: what `new FormData`
|
|
31
|
+
* builds, plus the browser's `formdata` event (which jsdom never fires) so the
|
|
32
|
+
* fallback strategy's listener can contribute.
|
|
33
|
+
*/
|
|
34
|
+
export declare function submittedEntries(form: HTMLFormElement): FormData;
|
|
35
|
+
/**
|
|
36
|
+
* Make `new FormData(form)` fire `formdata` on the form, as browsers do while
|
|
37
|
+
* constructing the entry list (jsdom does not), so code that snapshots a form
|
|
38
|
+
* — SvelteKit's `enhance`, form-retry's draft comparison — sees files the
|
|
39
|
+
* `formdata`-event strategy appends. Do not combine with
|
|
40
|
+
* {@link submittedEntries}, which dispatches the event itself.
|
|
41
|
+
*/
|
|
42
|
+
export declare function useBrowserFormData(): void;
|
|
43
|
+
/** Canvas 2D, `toBlob` and `toDataURL` stubs. Returns the fake context. */
|
|
44
|
+
/** The canvas 2D surface the capture components touch. */
|
|
45
|
+
export interface FakeCanvasContext {
|
|
46
|
+
drawImage: Mock;
|
|
47
|
+
fillRect: Mock;
|
|
48
|
+
beginPath: Mock;
|
|
49
|
+
moveTo: Mock;
|
|
50
|
+
lineTo: Mock;
|
|
51
|
+
stroke: Mock;
|
|
52
|
+
fillStyle: string;
|
|
53
|
+
strokeStyle: string;
|
|
54
|
+
lineWidth: number;
|
|
55
|
+
lineCap: string;
|
|
56
|
+
lineJoin: string;
|
|
57
|
+
}
|
|
58
|
+
export declare function stubCanvas(): FakeCanvasContext;
|
|
59
|
+
/** `play()` is unimplemented in jsdom; resolve it like an allowed autoplay. */
|
|
60
|
+
export declare function stubMediaPlayback(): void;
|
|
61
|
+
export interface FakeTrack {
|
|
62
|
+
stop: Mock;
|
|
63
|
+
}
|
|
64
|
+
export declare function fakeStream(tracks?: FakeTrack[]): MediaStream;
|
|
65
|
+
/** Install `navigator.mediaDevices` (or remove it with `undefined`). */
|
|
66
|
+
export declare function installMediaDevices(mediaDevices: {
|
|
67
|
+
getUserMedia: (...args: unknown[]) => unknown;
|
|
68
|
+
} | undefined): void;
|
|
69
|
+
export declare function removeMediaDevices(): void;
|
|
70
|
+
//# sourceMappingURL=capture-test-env.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"capture-test-env.d.ts","sourceRoot":"","sources":["../../../../src/components/forms/__tests__/capture-test-env.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EAAE,KAAK,IAAI,EAAM,MAAM,QAAQ,CAAC;AAYvC;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,IAAI,EAAE,GAAG,QAAQ,CAWtD;AAED,kFAAkF;AAClF,qBAAa,gBAAgB;;IAE3B,QAAQ,CAAC,KAAK;oBACA,IAAI;MAIhB;IACF,IAAI,KAAK,IAAI,QAAQ,CAEpB;CACF;AAED,wEAAwE;AACxE,qBAAa,iBAAiB;CAAG;AAEjC,sEAAsE;AACtE,wBAAgB,oBAAoB,CAClC,QAAQ,EAAE,eAAe,GAAG,gBAAgB,GAAG,MAAM,GACpD,IAAI,CASN;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,eAAe,GAAG,QAAQ,CAMhE;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,IAAI,IAAI,CAYzC;AAED,2EAA2E;AAC3E,0DAA0D;AAC1D,MAAM,WAAW,iBAAiB;IAChC,SAAS,EAAE,IAAI,CAAC;IAChB,QAAQ,EAAE,IAAI,CAAC;IACf,SAAS,EAAE,IAAI,CAAC;IAChB,MAAM,EAAE,IAAI,CAAC;IACb,MAAM,EAAE,IAAI,CAAC;IACb,MAAM,EAAE,IAAI,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,wBAAgB,UAAU,IAAI,iBAAiB,CA0B9C;AAED,+EAA+E;AAC/E,wBAAgB,iBAAiB,IAAI,IAAI,CAExC;AAED,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,IAAI,CAAC;CACZ;AAED,wBAAgB,UAAU,CAAC,MAAM,GAAE,SAAS,EAAwB,GACjB,WAAW,CAC7D;AAED,wEAAwE;AACxE,wBAAgB,mBAAmB,CACjC,YAAY,EAAE;IAAE,YAAY,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,OAAO,CAAA;CAAE,GAAG,SAAS,GAC1E,IAAI,CAKN;AAED,wBAAgB,kBAAkB,IAAI,IAAI,CAEzC"}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* jsdom stand-ins for the browser APIs `CameraCapture` and `SignaturePad` use
|
|
3
|
+
* (smrt#3290). jsdom has no `DataTransfer`, no canvas, no media playback and
|
|
4
|
+
* never fires the `formdata` event, so each is supplied here, as close to the
|
|
5
|
+
* browser contract as the tests need.
|
|
6
|
+
*/
|
|
7
|
+
import { vi } from 'vitest';
|
|
8
|
+
function implOf(wrapper, guard) {
|
|
9
|
+
const key = Object.getOwnPropertySymbols(wrapper).find((symbol) => guard(wrapper[symbol]));
|
|
10
|
+
if (!key)
|
|
11
|
+
throw new Error('jsdom wrapper has no implementation symbol');
|
|
12
|
+
return wrapper[key];
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* A real jsdom `FileList` holding `files`. jsdom's `input.files` setter only
|
|
16
|
+
* accepts genuine `FileList` instances and exposes no constructor, so the list
|
|
17
|
+
* is taken from a detached file input and filled through its implementation
|
|
18
|
+
* object. This keeps `new FormData(form)` reading the files exactly as a
|
|
19
|
+
* browser would after `input.files = dataTransfer.files`.
|
|
20
|
+
*/
|
|
21
|
+
export function createFileList(files) {
|
|
22
|
+
const input = document.createElement('input');
|
|
23
|
+
input.type = 'file';
|
|
24
|
+
const list = input.files;
|
|
25
|
+
const impl = implOf(list, Array.isArray);
|
|
26
|
+
for (const file of files) {
|
|
27
|
+
impl.push(implOf(file, (value) => typeof value === 'object' && value !== null));
|
|
28
|
+
}
|
|
29
|
+
return list;
|
|
30
|
+
}
|
|
31
|
+
/** Browser-shaped `DataTransfer` for jsdom: `items.add()` plus a real `files`. */
|
|
32
|
+
export class TestDataTransfer {
|
|
33
|
+
#files = [];
|
|
34
|
+
items = {
|
|
35
|
+
add: (file) => {
|
|
36
|
+
this.#files.push(file);
|
|
37
|
+
return null;
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
get files() {
|
|
41
|
+
return createFileList(this.#files);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/** Marker constructor: its presence is what feature detection reads. */
|
|
45
|
+
export class TestFormDataEvent {
|
|
46
|
+
}
|
|
47
|
+
/** Select the posting strategy the component will detect on mount. */
|
|
48
|
+
export function useFileFieldStrategy(strategy) {
|
|
49
|
+
vi.stubGlobal('DataTransfer', strategy === 'data-transfer' ? TestDataTransfer : undefined);
|
|
50
|
+
vi.stubGlobal('FormDataEvent', strategy === 'formdata-event' ? TestFormDataEvent : undefined);
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* The entry list a native submit of `form` would send: what `new FormData`
|
|
54
|
+
* builds, plus the browser's `formdata` event (which jsdom never fires) so the
|
|
55
|
+
* fallback strategy's listener can contribute.
|
|
56
|
+
*/
|
|
57
|
+
export function submittedEntries(form) {
|
|
58
|
+
const formData = new FormData(form);
|
|
59
|
+
const event = new Event('formdata');
|
|
60
|
+
Object.defineProperty(event, 'formData', { value: formData });
|
|
61
|
+
form.dispatchEvent(event);
|
|
62
|
+
return formData;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Make `new FormData(form)` fire `formdata` on the form, as browsers do while
|
|
66
|
+
* constructing the entry list (jsdom does not), so code that snapshots a form
|
|
67
|
+
* — SvelteKit's `enhance`, form-retry's draft comparison — sees files the
|
|
68
|
+
* `formdata`-event strategy appends. Do not combine with
|
|
69
|
+
* {@link submittedEntries}, which dispatches the event itself.
|
|
70
|
+
*/
|
|
71
|
+
export function useBrowserFormData() {
|
|
72
|
+
const NativeFormData = FormData;
|
|
73
|
+
class BrowserFormData extends NativeFormData {
|
|
74
|
+
constructor(form, submitter) {
|
|
75
|
+
super(form, submitter);
|
|
76
|
+
if (!form)
|
|
77
|
+
return;
|
|
78
|
+
const event = new Event('formdata');
|
|
79
|
+
Object.defineProperty(event, 'formData', { value: this });
|
|
80
|
+
form.dispatchEvent(event);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
vi.stubGlobal('FormData', BrowserFormData);
|
|
84
|
+
}
|
|
85
|
+
export function stubCanvas() {
|
|
86
|
+
const context = {
|
|
87
|
+
drawImage: vi.fn(),
|
|
88
|
+
fillRect: vi.fn(),
|
|
89
|
+
beginPath: vi.fn(),
|
|
90
|
+
moveTo: vi.fn(),
|
|
91
|
+
lineTo: vi.fn(),
|
|
92
|
+
stroke: vi.fn(),
|
|
93
|
+
fillStyle: '',
|
|
94
|
+
strokeStyle: '',
|
|
95
|
+
lineWidth: 1,
|
|
96
|
+
lineCap: 'butt',
|
|
97
|
+
lineJoin: 'miter',
|
|
98
|
+
};
|
|
99
|
+
vi.spyOn(HTMLCanvasElement.prototype, 'getContext').mockImplementation(() => context);
|
|
100
|
+
vi.spyOn(HTMLCanvasElement.prototype, 'toDataURL').mockImplementation((type = 'image/png') => `data:${type};base64,AAAA`);
|
|
101
|
+
vi.spyOn(HTMLCanvasElement.prototype, 'toBlob').mockImplementation((callback, type = 'image/png') => {
|
|
102
|
+
callback(new Blob(['pixels'], { type }));
|
|
103
|
+
});
|
|
104
|
+
return context;
|
|
105
|
+
}
|
|
106
|
+
/** `play()` is unimplemented in jsdom; resolve it like an allowed autoplay. */
|
|
107
|
+
export function stubMediaPlayback() {
|
|
108
|
+
vi.spyOn(HTMLMediaElement.prototype, 'play').mockResolvedValue(undefined);
|
|
109
|
+
}
|
|
110
|
+
export function fakeStream(tracks = [{ stop: vi.fn() }]) {
|
|
111
|
+
return { getTracks: () => tracks };
|
|
112
|
+
}
|
|
113
|
+
/** Install `navigator.mediaDevices` (or remove it with `undefined`). */
|
|
114
|
+
export function installMediaDevices(mediaDevices) {
|
|
115
|
+
Object.defineProperty(window.navigator, 'mediaDevices', {
|
|
116
|
+
configurable: true,
|
|
117
|
+
value: mediaDevices,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
export function removeMediaDevices() {
|
|
121
|
+
delete window.navigator.mediaDevices;
|
|
122
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import ThemeProvider from '../../../themes/ThemeProvider.svelte';
|
|
3
|
+
import CameraCapture from '../CameraCapture.svelte';
|
|
4
|
+
import SignaturePad from '../SignaturePad.svelte';
|
|
5
|
+
|
|
6
|
+
let {
|
|
7
|
+
density = 'comfortable',
|
|
8
|
+
target = '48px',
|
|
9
|
+
}: { density?: 'comfortable' | 'touch'; target?: string } = $props();
|
|
10
|
+
</script>
|
|
11
|
+
<!-- Capture actions are touch targets at every density: comfortable is the default here on purpose. -->
|
|
12
|
+
<ThemeProvider {density} overrides={{ '--smrt-touch-target-min': target }} persist={false} inlineVariables>
|
|
13
|
+
<CameraCapture name="photo" fileInputFallback interaction={false} />
|
|
14
|
+
<SignaturePad name="signature" interaction={false} />
|
|
15
|
+
</ThemeProvider>
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
type $$ComponentProps = {
|
|
2
|
+
density?: 'comfortable' | 'touch';
|
|
3
|
+
target?: string;
|
|
4
|
+
};
|
|
5
|
+
declare const CaptureTouch: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
6
|
+
type CaptureTouch = ReturnType<typeof CaptureTouch>;
|
|
7
|
+
export default CaptureTouch;
|
|
8
|
+
//# sourceMappingURL=capture-touch.fixture.svelte.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"capture-touch.fixture.svelte.d.ts","sourceRoot":"","sources":["../../../../src/components/forms/__tests__/capture-touch.fixture.svelte.ts"],"names":[],"mappings":"AAOC,KAAK,gBAAgB,GAAI;IAAE,OAAO,CAAC,EAAE,aAAa,GAAG,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAmBjF,QAAA,MAAM,YAAY,sDAAwC,CAAC;AAC3D,KAAK,YAAY,GAAG,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC;AACpD,eAAe,YAAY,CAAC"}
|