@happyvertical/smrt-ui 0.53.0 → 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.
- package/package.json +3 -3
- package/dist/actions/__tests__/ripple.test.js +0 -82
- package/dist/actions/__tests__/swipe-dismiss.test.js +0 -69
- package/dist/components/calendar/__tests__/CalendarView.test.js +0 -226
- package/dist/components/calendar/__tests__/calendar-model.test.js +0 -138
- package/dist/components/chat/__tests__/chat-primitives.test.js +0 -118
- package/dist/components/data/__tests__/CollectionList.test.js +0 -37
- package/dist/components/data/__tests__/DataTable.test.js +0 -807
- package/dist/components/data/__tests__/DataTableConformance.test.js +0 -212
- package/dist/components/data/__tests__/DataTableController.test.js +0 -371
- package/dist/components/data/__tests__/DataTableIdentity.test.js +0 -29
- package/dist/components/data/__tests__/DataTableLayout.test.js +0 -195
- package/dist/components/data/__tests__/DataTablePerformance.test.js +0 -21
- package/dist/components/data/__tests__/DataTableResponsive.test.js +0 -80
- package/dist/components/data/__tests__/DataTableVirtualization.test.js +0 -80
- package/dist/components/data/__tests__/DataTableVirtualizationComponent.test.js +0 -255
- package/dist/components/data/__tests__/SortableHeader.test.js +0 -101
- package/dist/components/data/__tests__/data-surface.test.js +0 -746
- package/dist/components/data/__tests__/data-table-helpers.test.js +0 -36
- package/dist/components/data/__tests__/data-table-surface.test.js +0 -49
- package/dist/components/data/__tests__/link-step-surface.test.js +0 -172
- package/dist/components/data/__tests__/list-sort-state.svelte.test.js +0 -71
- package/dist/components/data/__tests__/list-sort.test.js +0 -91
- package/dist/components/data/__tests__/mounted-data-surface.test.js +0 -794
- package/dist/components/data/__tests__/responsive-table.fixture.svelte +0 -29
- package/dist/components/data/__tests__/responsive-table.fixture.svelte.d.ts +0 -7
- package/dist/components/data/__tests__/responsive-table.fixture.svelte.d.ts.map +0 -1
- package/dist/components/data/__tests__/surface-navigation.test.js +0 -121
- package/dist/components/display/__tests__/ConfidenceBadge.test.js +0 -109
- package/dist/components/display/__tests__/CurrencyDisplay.test.js +0 -304
- package/dist/components/display/__tests__/CurrencyDisplayI18nHarness.svelte +0 -20
- package/dist/components/display/__tests__/CurrencyDisplayI18nHarness.svelte.d.ts +0 -19
- package/dist/components/display/__tests__/CurrencyDisplayI18nHarness.svelte.d.ts.map +0 -1
- package/dist/components/display/__tests__/CurrencyDisplaySsrHarness.svelte +0 -7
- package/dist/components/display/__tests__/CurrencyDisplaySsrHarness.svelte.d.ts +0 -19
- package/dist/components/display/__tests__/CurrencyDisplaySsrHarness.svelte.d.ts.map +0 -1
- package/dist/components/display/__tests__/DateDisplay.test.js +0 -195
- package/dist/components/display/__tests__/Icon.test.js +0 -223
- package/dist/components/display/__tests__/StatusBadge.test.js +0 -191
- package/dist/components/feedback/__tests__/ConfirmDialog.test.js +0 -181
- package/dist/components/feedback/__tests__/LoadingOverlay.test.js +0 -113
- package/dist/components/feedback/__tests__/Modal.test.js +0 -72
- package/dist/components/feedback/__tests__/PhoneSheet.test.js +0 -101
- package/dist/components/feedback/__tests__/ProgressBar.test.js +0 -89
- package/dist/components/feedback/__tests__/WorkingStrip.test.js +0 -172
- package/dist/components/feedback/__tests__/confirm-message.fixture.svelte +0 -9
- package/dist/components/feedback/__tests__/confirm-message.fixture.svelte.d.ts +0 -19
- package/dist/components/feedback/__tests__/confirm-message.fixture.svelte.d.ts.map +0 -1
- package/dist/components/feedback/__tests__/toast.test.js +0 -190
- package/dist/components/forms/__tests__/CameraCapture.test.js +0 -504
- package/dist/components/forms/__tests__/Combobox.test.js +0 -354
- package/dist/components/forms/__tests__/ControlHitArea.test.js +0 -73
- package/dist/components/forms/__tests__/DictationButton.test.js +0 -176
- package/dist/components/forms/__tests__/Fieldset.test.js +0 -37
- package/dist/components/forms/__tests__/Form.test.js +0 -201
- package/dist/components/forms/__tests__/FormActionBar.test.js +0 -22
- package/dist/components/forms/__tests__/FormGroup.test.js +0 -48
- package/dist/components/forms/__tests__/Input.test.js +0 -49
- package/dist/components/forms/__tests__/Listbox.test.js +0 -303
- package/dist/components/forms/__tests__/MultiSelect.test.js +0 -281
- package/dist/components/forms/__tests__/SearchInput.test.js +0 -92
- package/dist/components/forms/__tests__/Select.test.js +0 -37
- package/dist/components/forms/__tests__/SignaturePad.test.js +0 -338
- package/dist/components/forms/__tests__/StagedControlReview.test.js +0 -702
- package/dist/components/forms/__tests__/Textarea.test.js +0 -39
- package/dist/components/forms/__tests__/Toggle.test.js +0 -87
- package/dist/components/forms/__tests__/camera-capture-session.test.js +0 -207
- package/dist/components/forms/__tests__/capture-form-retry.test.js +0 -166
- package/dist/components/forms/__tests__/capture-form.fixture.svelte +0 -38
- package/dist/components/forms/__tests__/capture-form.fixture.svelte.d.ts +0 -10
- package/dist/components/forms/__tests__/capture-form.fixture.svelte.d.ts.map +0 -1
- package/dist/components/forms/__tests__/capture-test-env.d.ts +0 -70
- package/dist/components/forms/__tests__/capture-test-env.d.ts.map +0 -1
- package/dist/components/forms/__tests__/capture-test-env.js +0 -122
- package/dist/components/forms/__tests__/capture-touch.fixture.svelte +0 -15
- package/dist/components/forms/__tests__/capture-touch.fixture.svelte.d.ts +0 -8
- package/dist/components/forms/__tests__/capture-touch.fixture.svelte.d.ts.map +0 -1
- package/dist/components/forms/__tests__/combobox-form.fixture.svelte +0 -29
- package/dist/components/forms/__tests__/combobox-form.fixture.svelte.d.ts +0 -14
- package/dist/components/forms/__tests__/combobox-form.fixture.svelte.d.ts.map +0 -1
- package/dist/components/forms/__tests__/combobox-labels.fixture.svelte +0 -19
- package/dist/components/forms/__tests__/combobox-labels.fixture.svelte.d.ts +0 -11
- package/dist/components/forms/__tests__/combobox-labels.fixture.svelte.d.ts.map +0 -1
- package/dist/components/forms/__tests__/composite-controls.fixture.svelte +0 -47
- package/dist/components/forms/__tests__/composite-controls.fixture.svelte.d.ts +0 -12
- package/dist/components/forms/__tests__/composite-controls.fixture.svelte.d.ts.map +0 -1
- package/dist/components/forms/__tests__/composite-controls.test.js +0 -128
- package/dist/components/forms/__tests__/composite-registration.fixture.svelte +0 -29
- package/dist/components/forms/__tests__/composite-registration.fixture.svelte.d.ts +0 -8
- package/dist/components/forms/__tests__/composite-registration.fixture.svelte.d.ts.map +0 -1
- package/dist/components/forms/__tests__/composite-user-edit.fixture.svelte +0 -41
- package/dist/components/forms/__tests__/composite-user-edit.fixture.svelte.d.ts +0 -13
- package/dist/components/forms/__tests__/composite-user-edit.fixture.svelte.d.ts.map +0 -1
- package/dist/components/forms/__tests__/control-interaction.test.js +0 -3421
- package/dist/components/forms/__tests__/control-proposals.test.js +0 -63
- package/dist/components/forms/__tests__/core-controls.fixture.svelte +0 -50
- package/dist/components/forms/__tests__/core-controls.fixture.svelte.d.ts +0 -9
- package/dist/components/forms/__tests__/core-controls.fixture.svelte.d.ts.map +0 -1
- package/dist/components/forms/__tests__/core-controls.test.js +0 -311
- package/dist/components/forms/__tests__/decimal-sliders.fixture.svelte +0 -25
- package/dist/components/forms/__tests__/decimal-sliders.fixture.svelte.d.ts +0 -8
- package/dist/components/forms/__tests__/decimal-sliders.fixture.svelte.d.ts.map +0 -1
- package/dist/components/forms/__tests__/dictation-recording.test.js +0 -582
- package/dist/components/forms/__tests__/dictation.test.js +0 -486
- package/dist/components/forms/__tests__/form-attachment.fixture.svelte +0 -28
- package/dist/components/forms/__tests__/form-attachment.fixture.svelte.d.ts +0 -11
- package/dist/components/forms/__tests__/form-attachment.fixture.svelte.d.ts.map +0 -1
- package/dist/components/forms/__tests__/form-group-input.fixture.svelte +0 -16
- package/dist/components/forms/__tests__/form-group-input.fixture.svelte.d.ts +0 -9
- package/dist/components/forms/__tests__/form-group-input.fixture.svelte.d.ts.map +0 -1
- package/dist/components/forms/__tests__/form-retry-form.fixture.svelte +0 -31
- package/dist/components/forms/__tests__/form-retry-form.fixture.svelte.d.ts +0 -13
- package/dist/components/forms/__tests__/form-retry-form.fixture.svelte.d.ts.map +0 -1
- package/dist/components/forms/__tests__/form-retry-form.test.js +0 -107
- package/dist/components/forms/__tests__/hidden-controls-form-retry.test.js +0 -69
- package/dist/components/forms/__tests__/hidden-controls-form.fixture.svelte +0 -29
- package/dist/components/forms/__tests__/hidden-controls-form.fixture.svelte.d.ts +0 -9
- package/dist/components/forms/__tests__/hidden-controls-form.fixture.svelte.d.ts.map +0 -1
- package/dist/components/forms/__tests__/listbox-form.fixture.svelte +0 -27
- package/dist/components/forms/__tests__/listbox-form.fixture.svelte.d.ts +0 -13
- package/dist/components/forms/__tests__/listbox-form.fixture.svelte.d.ts.map +0 -1
- package/dist/components/forms/__tests__/long-press.test.js +0 -175
- package/dist/components/forms/__tests__/multiselect-form.fixture.svelte +0 -27
- package/dist/components/forms/__tests__/multiselect-form.fixture.svelte.d.ts +0 -13
- package/dist/components/forms/__tests__/multiselect-form.fixture.svelte.d.ts.map +0 -1
- package/dist/components/forms/__tests__/native-file-field.test.js +0 -177
- package/dist/components/forms/__tests__/place-picker.fixture.svelte +0 -33
- package/dist/components/forms/__tests__/place-picker.fixture.svelte.d.ts +0 -12
- package/dist/components/forms/__tests__/place-picker.fixture.svelte.d.ts.map +0 -1
- package/dist/components/forms/__tests__/segmented-posting.fixture.svelte +0 -28
- package/dist/components/forms/__tests__/segmented-posting.fixture.svelte.d.ts +0 -12
- package/dist/components/forms/__tests__/segmented-posting.fixture.svelte.d.ts.map +0 -1
- package/dist/components/forms/__tests__/segmented-posting.test.js +0 -92
- package/dist/components/forms/__tests__/select-interaction.fixture.svelte +0 -25
- package/dist/components/forms/__tests__/select-interaction.fixture.svelte.d.ts +0 -9
- package/dist/components/forms/__tests__/select-interaction.fixture.svelte.d.ts.map +0 -1
- package/dist/components/forms/__tests__/signature-pad-logic.test.js +0 -54
- package/dist/components/forms/__tests__/staged-review-fieldset.fixture.svelte +0 -21
- package/dist/components/forms/__tests__/staged-review-fieldset.fixture.svelte.d.ts +0 -8
- package/dist/components/forms/__tests__/staged-review-fieldset.fixture.svelte.d.ts.map +0 -1
- package/dist/components/forms/__tests__/staged-review-outer-fieldset.fixture.svelte +0 -23
- package/dist/components/forms/__tests__/staged-review-outer-fieldset.fixture.svelte.d.ts +0 -8
- package/dist/components/forms/__tests__/staged-review-outer-fieldset.fixture.svelte.d.ts.map +0 -1
- package/dist/components/forms/__tests__/staged-review.fixture.svelte +0 -56
- package/dist/components/forms/__tests__/staged-review.fixture.svelte.d.ts +0 -12
- package/dist/components/forms/__tests__/staged-review.fixture.svelte.d.ts.map +0 -1
- package/dist/components/forms/__tests__/touch-density.fixture.svelte +0 -34
- package/dist/components/forms/__tests__/touch-density.fixture.svelte.d.ts +0 -8
- package/dist/components/forms/__tests__/touch-density.fixture.svelte.d.ts.map +0 -1
- package/dist/components/forms/__tests__/touch-density.test.js +0 -66
- package/dist/components/forms/form-retry/__tests__/boundaries.test.js +0 -23
- package/dist/components/forms/form-retry/__tests__/content-digest.test.js +0 -70
- package/dist/components/forms/form-retry/__tests__/controller.test.js +0 -802
- package/dist/components/forms/form-retry/__tests__/fake-kit.d.ts +0 -82
- package/dist/components/forms/form-retry/__tests__/fake-kit.d.ts.map +0 -1
- package/dist/components/forms/form-retry/__tests__/fake-kit.js +0 -180
- package/dist/components/forms/form-retry/__tests__/form-draft.test.js +0 -169
- package/dist/components/forms/form-retry/__tests__/ssr.test.js +0 -37
- package/dist/components/forms/form-retry/__tests__/storage.d.ts +0 -26
- package/dist/components/forms/form-retry/__tests__/storage.d.ts.map +0 -1
- package/dist/components/forms/form-retry/__tests__/storage.js +0 -63
- package/dist/components/forms/form-retry/__tests__/submission-key.test.js +0 -236
- package/dist/components/forms/form-retry/__tests__/submitted-draft.test.js +0 -334
- package/dist/components/layout/__tests__/Container.test.js +0 -62
- package/dist/components/layout/__tests__/EmptyState.test.js +0 -83
- package/dist/components/layout/__tests__/Footer.test.js +0 -50
- package/dist/components/layout/__tests__/Grid.test.js +0 -121
- package/dist/components/layout/__tests__/Header.test.js +0 -48
- package/dist/components/layout/__tests__/Masthead.test.js +0 -93
- package/dist/components/layout/__tests__/PageHeader.test.js +0 -166
- package/dist/components/layout/__tests__/SummaryCard.test.js +0 -104
- package/dist/components/nav/__tests__/FilterChips.test.js +0 -94
- package/dist/components/nav/__tests__/LinkTabs.test.js +0 -117
- package/dist/components/nav/__tests__/Tabs.test.js +0 -128
- package/dist/components/roles/__tests__/RoleSelector.test.js +0 -134
- package/dist/components/ui/__tests__/Badge.test.js +0 -76
- package/dist/components/ui/__tests__/Button.test.js +0 -166
- package/dist/components/ui/__tests__/Card.test.js +0 -103
- package/dist/components/ui/__tests__/IconToggle.test.js +0 -59
- package/dist/components/ui/__tests__/IconToggleGroup.test.js +0 -30
- package/dist/components/ui/__tests__/Pagination.test.js +0 -113
- package/dist/components/ui/__tests__/Popover.test.js +0 -44
- package/dist/components/ui/__tests__/dropdown-popover-icon.test.js +0 -125
- package/dist/components/ui/__tests__/gap-primitives-interactive.test.js +0 -112
- package/dist/components/ui/__tests__/gap-primitives.test.js +0 -84
- package/dist/components/ui/__tests__/interaction-primitives.fixture.svelte +0 -13
- package/dist/components/ui/__tests__/interaction-primitives.fixture.svelte.d.ts +0 -19
- package/dist/components/ui/__tests__/interaction-primitives.fixture.svelte.d.ts.map +0 -1
- package/dist/components/ui/__tests__/interaction-primitives.test.js +0 -31
- package/dist/components/ui/__tests__/phone-touch-targets.test.js +0 -39
- package/dist/i18n/__tests__/i18n.test.js +0 -74
- package/dist/i18n/__tests__/render-parity.spec.js +0 -37
- package/dist/svelte/__tests__/playground.test.js +0 -20
- package/dist/themes/__tests__/control-boundary-contrast.test.js +0 -112
- package/dist/themes/__tests__/create-theme.test.js +0 -79
- package/dist/themes/__tests__/css-generator.test.js +0 -129
- package/dist/themes/__tests__/happyvertical-theme.test.js +0 -231
- package/dist/themes/__tests__/registry.test.js +0 -43
- package/dist/themes/__tests__/styles-lockstep.test.js +0 -32
- package/dist/themes/__tests__/theme-provider.test.js +0 -147
- package/dist/themes/__tests__/theme-script.test.js +0 -130
- package/dist/themes/__tests__/token-aliases.test.js +0 -178
- package/dist/utils/forms/__tests__/formatters.test.js +0 -27
- package/dist/utils/theme/__tests__/color.test.js +0 -89
- package/dist/utils/theme/__tests__/typography.test.js +0 -11
|
@@ -1,236 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The submission key and its slot (#3291): one key per filled-in form,
|
|
3
|
-
* rotated at most once, never rotated by a late result, and never lost to a
|
|
4
|
-
* storage error.
|
|
5
|
-
*/
|
|
6
|
-
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
7
|
-
import { beginSubmit, clearSubmissionKey, isSubmissionKeyPersistent, mintSubmissionKey, readSubmissionKey, resetFormRetryMemory, rotateSubmissionKey, settleSubmit, submitDisposition, } from '../submission-key.js';
|
|
8
|
-
import { fullStorage, memoryStorage, refusingStorage, stickyStorage, } from './storage.js';
|
|
9
|
-
const UUID_V4 = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
|
|
10
|
-
beforeEach(() => {
|
|
11
|
-
resetFormRetryMemory();
|
|
12
|
-
});
|
|
13
|
-
afterEach(() => {
|
|
14
|
-
vi.unstubAllGlobals();
|
|
15
|
-
});
|
|
16
|
-
describe('submitDisposition', () => {
|
|
17
|
-
const LIVE = 'aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa';
|
|
18
|
-
const STALE = 'bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb';
|
|
19
|
-
it('rotates on a confirmed write', () => {
|
|
20
|
-
expect(submitDisposition(LIVE, LIVE, 'success')).toBe('rotate-and-apply');
|
|
21
|
-
});
|
|
22
|
-
it('keeps the key on a validation failure, which wrote nothing', () => {
|
|
23
|
-
expect(submitDisposition(LIVE, LIVE, 'failure')).toBe('apply');
|
|
24
|
-
});
|
|
25
|
-
it('keeps the key AND the form on a transport error', () => {
|
|
26
|
-
expect(submitDisposition(LIVE, LIVE, 'error')).toBe('transport-error');
|
|
27
|
-
});
|
|
28
|
-
it('ignores every result whose key is no longer the live one', () => {
|
|
29
|
-
for (const outcome of [
|
|
30
|
-
'success',
|
|
31
|
-
'failure',
|
|
32
|
-
'redirect',
|
|
33
|
-
'error',
|
|
34
|
-
]) {
|
|
35
|
-
expect(submitDisposition(STALE, LIVE, outcome)).toBe('ignore');
|
|
36
|
-
expect(submitDisposition(LIVE, null, outcome)).toBe('ignore');
|
|
37
|
-
}
|
|
38
|
-
});
|
|
39
|
-
it('treats a redirect as unconfirmed unless the caller says otherwise', () => {
|
|
40
|
-
expect(submitDisposition(LIVE, LIVE, 'redirect')).toBe('apply');
|
|
41
|
-
expect(submitDisposition(LIVE, LIVE, 'redirect', {
|
|
42
|
-
redirectConfirmsWrite: true,
|
|
43
|
-
})).toBe('rotate-and-apply');
|
|
44
|
-
});
|
|
45
|
-
});
|
|
46
|
-
describe('the submission slot', () => {
|
|
47
|
-
const A = 'aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa';
|
|
48
|
-
const B = 'bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb';
|
|
49
|
-
const C = 'cccccccc-cccc-4ccc-8ccc-cccccccccccc';
|
|
50
|
-
const idle = (token = A) => ({ token, inFlight: null });
|
|
51
|
-
it('refuses a SECOND submit while one is unresolved', () => {
|
|
52
|
-
const first = beginSubmit(idle());
|
|
53
|
-
expect(first).toMatchObject({ start: true, sent: A });
|
|
54
|
-
const second = beginSubmit(first.state);
|
|
55
|
-
expect(second.start).toBe(false);
|
|
56
|
-
expect(second.state).toEqual(first.state);
|
|
57
|
-
});
|
|
58
|
-
it('accepts the next submit once the first has settled', () => {
|
|
59
|
-
const first = beginSubmit(idle());
|
|
60
|
-
const settled = settleSubmit(first.state, first.sent, 'success', {
|
|
61
|
-
formCleared: true,
|
|
62
|
-
mint: () => B,
|
|
63
|
-
});
|
|
64
|
-
expect(settled).toEqual({
|
|
65
|
-
state: { token: B, inFlight: null },
|
|
66
|
-
disposition: 'rotate-and-apply',
|
|
67
|
-
});
|
|
68
|
-
expect(beginSubmit(settled.state)).toMatchObject({ start: true, sent: B });
|
|
69
|
-
});
|
|
70
|
-
it('keeps the key when the submit wrote nothing', () => {
|
|
71
|
-
for (const outcome of ['failure', 'error']) {
|
|
72
|
-
const first = beginSubmit(idle());
|
|
73
|
-
const settled = settleSubmit(first.state, first.sent, outcome, {
|
|
74
|
-
formCleared: true,
|
|
75
|
-
mint: () => B,
|
|
76
|
-
});
|
|
77
|
-
expect(settled.state, outcome).toEqual({ token: A, inFlight: null });
|
|
78
|
-
}
|
|
79
|
-
});
|
|
80
|
-
it('ignores a duplicate answer to a submit already settled', () => {
|
|
81
|
-
const first = beginSubmit(idle());
|
|
82
|
-
const settled = settleSubmit(first.state, first.sent, 'success', {
|
|
83
|
-
formCleared: true,
|
|
84
|
-
mint: () => B,
|
|
85
|
-
});
|
|
86
|
-
const late = settleSubmit(settled.state, first.sent, 'success', {
|
|
87
|
-
formCleared: true,
|
|
88
|
-
mint: () => C,
|
|
89
|
-
});
|
|
90
|
-
expect(late).toEqual({ state: settled.state, disposition: 'ignore' });
|
|
91
|
-
});
|
|
92
|
-
it('rotates at most once however many answers arrive', () => {
|
|
93
|
-
const begun = beginSubmit(idle());
|
|
94
|
-
let state = begun.state;
|
|
95
|
-
const mint = vi.fn(() => B);
|
|
96
|
-
for (let i = 0; i < 3; i += 1) {
|
|
97
|
-
state = settleSubmit(state, begun.sent, 'success', {
|
|
98
|
-
formCleared: true,
|
|
99
|
-
mint,
|
|
100
|
-
}).state;
|
|
101
|
-
}
|
|
102
|
-
expect(mint).toHaveBeenCalledTimes(1);
|
|
103
|
-
});
|
|
104
|
-
it('KEEPS the key when a confirmed write left content on the form', () => {
|
|
105
|
-
const first = beginSubmit(idle());
|
|
106
|
-
const settled = settleSubmit(first.state, first.sent, 'success', {
|
|
107
|
-
formCleared: false,
|
|
108
|
-
mint: () => B,
|
|
109
|
-
});
|
|
110
|
-
expect(settled).toEqual({
|
|
111
|
-
state: { token: A, inFlight: null },
|
|
112
|
-
disposition: 'rotate-and-apply',
|
|
113
|
-
});
|
|
114
|
-
});
|
|
115
|
-
});
|
|
116
|
-
describe('the key store', () => {
|
|
117
|
-
it('returns one stable UUID per form until it is rotated', () => {
|
|
118
|
-
const storage = memoryStorage();
|
|
119
|
-
const first = readSubmissionKey({ form: 'po-line', storage });
|
|
120
|
-
expect(first).toMatch(UUID_V4);
|
|
121
|
-
expect(readSubmissionKey({ form: 'po-line', storage })).toBe(first);
|
|
122
|
-
const rotated = rotateSubmissionKey({ form: 'po-line', storage });
|
|
123
|
-
expect(rotated).not.toBe(first);
|
|
124
|
-
expect(readSubmissionKey({ form: 'po-line', storage })).toBe(rotated);
|
|
125
|
-
});
|
|
126
|
-
it('keeps forms and scopes apart, under derived names', () => {
|
|
127
|
-
const storage = memoryStorage();
|
|
128
|
-
const a = readSubmissionKey({ form: 'receipt', scope: 'po/a', storage });
|
|
129
|
-
const b = readSubmissionKey({ form: 'receipt', scope: 'po-b', storage });
|
|
130
|
-
const other = readSubmissionKey({ form: 'request', storage });
|
|
131
|
-
expect(new Set([a, b, other]).size).toBe(3);
|
|
132
|
-
expect([...storage.store.keys()].sort()).toEqual([
|
|
133
|
-
'smrt:form-retry:receipt:po%2Fa:key',
|
|
134
|
-
'smrt:form-retry:receipt:po-b:key',
|
|
135
|
-
'smrt:form-retry:request:key',
|
|
136
|
-
]);
|
|
137
|
-
clearSubmissionKey({ form: 'receipt', scope: 'po-b', storage });
|
|
138
|
-
expect(readSubmissionKey({ form: 'receipt', scope: 'po/a', storage })).toBe(a);
|
|
139
|
-
});
|
|
140
|
-
it('honours an explicit storage name, for migrating existing keys', () => {
|
|
141
|
-
const storage = memoryStorage();
|
|
142
|
-
storage.store.set('legacy:flha:submission-key', 'carried');
|
|
143
|
-
expect(readSubmissionKey({
|
|
144
|
-
form: 'flha',
|
|
145
|
-
storage,
|
|
146
|
-
storageKey: 'legacy:flha:submission-key',
|
|
147
|
-
})).toBe('carried');
|
|
148
|
-
});
|
|
149
|
-
it('uses this tab sessionStorage by default, never localStorage', () => {
|
|
150
|
-
const session = memoryStorage();
|
|
151
|
-
const local = memoryStorage();
|
|
152
|
-
vi.stubGlobal('sessionStorage', session);
|
|
153
|
-
vi.stubGlobal('localStorage', local);
|
|
154
|
-
const key = readSubmissionKey({ form: 'po' });
|
|
155
|
-
expect(session.store.get('smrt:form-retry:po:key')).toBe(key);
|
|
156
|
-
expect(local.store.size).toBe(0);
|
|
157
|
-
expect(isSubmissionKeyPersistent({ form: 'po' })).toBe(true);
|
|
158
|
-
});
|
|
159
|
-
it('private window: falls back to memory, and the key still survives a remount', () => {
|
|
160
|
-
const storage = refusingStorage();
|
|
161
|
-
const first = readSubmissionKey({ form: 'po', storage });
|
|
162
|
-
// One page, two mounts: the second read must find the same key or a retry
|
|
163
|
-
// after `enhance`'s error path would carry a key the server never saw.
|
|
164
|
-
expect(readSubmissionKey({ form: 'po', storage })).toBe(first);
|
|
165
|
-
expect(isSubmissionKeyPersistent({ form: 'po', storage })).toBe(false);
|
|
166
|
-
expect(() => clearSubmissionKey({ form: 'po', storage })).not.toThrow();
|
|
167
|
-
expect(readSubmissionKey({ form: 'po', storage })).not.toBe(first);
|
|
168
|
-
});
|
|
169
|
-
it('private window: survives the sessionStorage property itself throwing', () => {
|
|
170
|
-
const original = Object.getOwnPropertyDescriptor(globalThis, 'sessionStorage');
|
|
171
|
-
Object.defineProperty(globalThis, 'sessionStorage', {
|
|
172
|
-
configurable: true,
|
|
173
|
-
get() {
|
|
174
|
-
throw new DOMException('The operation is insecure.', 'SecurityError');
|
|
175
|
-
},
|
|
176
|
-
});
|
|
177
|
-
try {
|
|
178
|
-
const first = readSubmissionKey({ form: 'po' });
|
|
179
|
-
expect(first).toMatch(UUID_V4);
|
|
180
|
-
expect(readSubmissionKey({ form: 'po' })).toBe(first);
|
|
181
|
-
}
|
|
182
|
-
finally {
|
|
183
|
-
if (original)
|
|
184
|
-
Object.defineProperty(globalThis, 'sessionStorage', original);
|
|
185
|
-
}
|
|
186
|
-
});
|
|
187
|
-
it('a refused write (full quota) still reads back the same key', () => {
|
|
188
|
-
const storage = fullStorage();
|
|
189
|
-
const first = readSubmissionKey({ form: 'po', storage });
|
|
190
|
-
expect(readSubmissionKey({ form: 'po', storage })).toBe(first);
|
|
191
|
-
});
|
|
192
|
-
it('rotates even when the store refuses to remove the old key', () => {
|
|
193
|
-
const storage = stickyStorage();
|
|
194
|
-
const first = readSubmissionKey({ form: 'po', storage });
|
|
195
|
-
const rotated = rotateSubmissionKey({ form: 'po', storage });
|
|
196
|
-
expect(rotated).not.toBe(first);
|
|
197
|
-
expect(readSubmissionKey({ form: 'po', storage })).toBe(rotated);
|
|
198
|
-
expect(storage.store.get('smrt:form-retry:po:key')).toBe(rotated);
|
|
199
|
-
expect(isSubmissionKeyPersistent({ form: 'po', storage })).toBe(true);
|
|
200
|
-
});
|
|
201
|
-
it('rotates when removal AND the replacement write are refused over an existing key', () => {
|
|
202
|
-
const storage = stickyStorage(true);
|
|
203
|
-
storage.store.set('smrt:form-retry:po:key', 'stale-key');
|
|
204
|
-
expect(readSubmissionKey({ form: 'po', storage })).toBe('stale-key');
|
|
205
|
-
const rotated = rotateSubmissionKey({ form: 'po', storage });
|
|
206
|
-
expect(rotated).not.toBe('stale-key');
|
|
207
|
-
// The fresh key lives in memory and takes precedence over stale storage.
|
|
208
|
-
expect(readSubmissionKey({ form: 'po', storage })).toBe(rotated);
|
|
209
|
-
expect(isSubmissionKeyPersistent({ form: 'po', storage })).toBe(false);
|
|
210
|
-
});
|
|
211
|
-
it.each([
|
|
212
|
-
['writes still work', false],
|
|
213
|
-
['writes are refused too', true],
|
|
214
|
-
])('clearing a key the store will not remove mints a fresh one (%s)', (_label, refuseWrites) => {
|
|
215
|
-
const storage = stickyStorage();
|
|
216
|
-
const first = readSubmissionKey({ form: 'po', storage });
|
|
217
|
-
storage.refuseWrites = refuseWrites;
|
|
218
|
-
clearSubmissionKey({ form: 'po', storage });
|
|
219
|
-
const next = readSubmissionKey({ form: 'po', storage });
|
|
220
|
-
expect(next).not.toBe(first);
|
|
221
|
-
expect(next).toMatch(UUID_V4);
|
|
222
|
-
expect(readSubmissionKey({ form: 'po', storage })).toBe(next);
|
|
223
|
-
expect(isSubmissionKeyPersistent({ form: 'po', storage })).toBe(!refuseWrites);
|
|
224
|
-
});
|
|
225
|
-
it('`storage: null` keeps everything in memory', () => {
|
|
226
|
-
const first = readSubmissionKey({ form: 'po', storage: null });
|
|
227
|
-
expect(readSubmissionKey({ form: 'po', storage: null })).toBe(first);
|
|
228
|
-
expect(isSubmissionKeyPersistent({ form: 'po', storage: null })).toBe(false);
|
|
229
|
-
});
|
|
230
|
-
it('mints distinct v4 UUIDs', () => {
|
|
231
|
-
const keys = new Set(Array.from({ length: 50 }, mintSubmissionKey));
|
|
232
|
-
expect(keys.size).toBe(50);
|
|
233
|
-
for (const key of keys)
|
|
234
|
-
expect(key).toMatch(UUID_V4);
|
|
235
|
-
});
|
|
236
|
-
});
|
|
@@ -1,334 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The restore-after-reload draft (#3291): restored only under the key it was
|
|
3
|
-
* submitted with, only for the owner who submitted it, only while fresh, only
|
|
4
|
-
* when well-formed — and never at the cost of a thrown storage error.
|
|
5
|
-
*/
|
|
6
|
-
import { describe, expect, it } from 'vitest';
|
|
7
|
-
import { applyDraftToForm, clearSubmittedDraft, describeFormData, readSubmittedDraft, saveSubmittedDraft, } from '../submitted-draft.js';
|
|
8
|
-
import { memoryStorage, refusingStorage, stickyStorage } from './storage.js';
|
|
9
|
-
const TOKEN = 'aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa';
|
|
10
|
-
const OWNER = 'tenant-1:person-1';
|
|
11
|
-
const NAME = 'smrt:form-retry:report:draft';
|
|
12
|
-
const ALWAYS = () => true;
|
|
13
|
-
const SAVED_AT = new Date('2026-10-01T12:00:00Z');
|
|
14
|
-
function save(storage, owner = OWNER) {
|
|
15
|
-
saveSubmittedDraft(storage, NAME, {
|
|
16
|
-
token: TOKEN,
|
|
17
|
-
owner,
|
|
18
|
-
savedAt: SAVED_AT,
|
|
19
|
-
fields: [
|
|
20
|
-
['title', 'Trip over a hose'],
|
|
21
|
-
['qty', '1'],
|
|
22
|
-
['qty', '2'],
|
|
23
|
-
],
|
|
24
|
-
files: [{ field: 'photo', name: 'a.jpg', type: 'image/jpeg', size: 3 }],
|
|
25
|
-
values: { captureKey: 'k-1' },
|
|
26
|
-
});
|
|
27
|
-
}
|
|
28
|
-
describe('saveSubmittedDraft / readSubmittedDraft', () => {
|
|
29
|
-
it('restores exactly what was submitted, under the same key and owner', () => {
|
|
30
|
-
const storage = memoryStorage();
|
|
31
|
-
save(storage);
|
|
32
|
-
expect(readSubmittedDraft(storage, NAME, {
|
|
33
|
-
token: TOKEN,
|
|
34
|
-
owner: OWNER,
|
|
35
|
-
fresh: ALWAYS,
|
|
36
|
-
})).toEqual({
|
|
37
|
-
token: TOKEN,
|
|
38
|
-
savedAt: SAVED_AT,
|
|
39
|
-
fields: [
|
|
40
|
-
['title', 'Trip over a hose'],
|
|
41
|
-
['qty', '1'],
|
|
42
|
-
['qty', '2'],
|
|
43
|
-
],
|
|
44
|
-
files: [{ field: 'photo', name: 'a.jpg', type: 'image/jpeg', size: 3 }],
|
|
45
|
-
values: { captureKey: 'k-1' },
|
|
46
|
-
});
|
|
47
|
-
});
|
|
48
|
-
it('does not restore under a new key, and drops the stale draft', () => {
|
|
49
|
-
const storage = memoryStorage();
|
|
50
|
-
save(storage);
|
|
51
|
-
expect(readSubmittedDraft(storage, NAME, {
|
|
52
|
-
token: 'bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb',
|
|
53
|
-
owner: OWNER,
|
|
54
|
-
fresh: ALWAYS,
|
|
55
|
-
})).toBeNull();
|
|
56
|
-
expect(storage.store.size).toBe(0);
|
|
57
|
-
});
|
|
58
|
-
it('never restores one person’s draft for another on shared hardware', () => {
|
|
59
|
-
const storage = memoryStorage();
|
|
60
|
-
save(storage);
|
|
61
|
-
expect(readSubmittedDraft(storage, NAME, {
|
|
62
|
-
token: TOKEN,
|
|
63
|
-
owner: 'tenant-1:person-2',
|
|
64
|
-
fresh: ALWAYS,
|
|
65
|
-
})).toBeNull();
|
|
66
|
-
expect(storage.store.size).toBe(0);
|
|
67
|
-
});
|
|
68
|
-
it('never restores for an empty owner', () => {
|
|
69
|
-
const storage = memoryStorage();
|
|
70
|
-
save(storage, '');
|
|
71
|
-
expect(readSubmittedDraft(storage, NAME, {
|
|
72
|
-
token: TOKEN,
|
|
73
|
-
owner: '',
|
|
74
|
-
fresh: ALWAYS,
|
|
75
|
-
})).toBeNull();
|
|
76
|
-
});
|
|
77
|
-
it('drops an expired draft', () => {
|
|
78
|
-
const storage = memoryStorage();
|
|
79
|
-
save(storage);
|
|
80
|
-
const seen = [];
|
|
81
|
-
expect(readSubmittedDraft(storage, NAME, {
|
|
82
|
-
token: TOKEN,
|
|
83
|
-
owner: OWNER,
|
|
84
|
-
fresh: (savedAt) => {
|
|
85
|
-
seen.push(savedAt);
|
|
86
|
-
return false;
|
|
87
|
-
},
|
|
88
|
-
})).toBeNull();
|
|
89
|
-
expect(seen).toEqual([SAVED_AT]);
|
|
90
|
-
expect(storage.store.size).toBe(0);
|
|
91
|
-
});
|
|
92
|
-
it('drops a draft that is malformed or not JSON', () => {
|
|
93
|
-
const storage = memoryStorage();
|
|
94
|
-
const bad = [
|
|
95
|
-
'{not json',
|
|
96
|
-
JSON.stringify({
|
|
97
|
-
v: 2,
|
|
98
|
-
token: TOKEN,
|
|
99
|
-
owner: OWNER,
|
|
100
|
-
savedAt: 1,
|
|
101
|
-
fields: [],
|
|
102
|
-
files: [],
|
|
103
|
-
}),
|
|
104
|
-
JSON.stringify({
|
|
105
|
-
v: 1,
|
|
106
|
-
token: TOKEN,
|
|
107
|
-
owner: OWNER,
|
|
108
|
-
fields: [],
|
|
109
|
-
files: [],
|
|
110
|
-
}),
|
|
111
|
-
JSON.stringify({
|
|
112
|
-
v: 1,
|
|
113
|
-
token: TOKEN,
|
|
114
|
-
owner: OWNER,
|
|
115
|
-
savedAt: 1,
|
|
116
|
-
fields: [['a', 1]],
|
|
117
|
-
files: [],
|
|
118
|
-
}),
|
|
119
|
-
JSON.stringify({
|
|
120
|
-
v: 1,
|
|
121
|
-
token: TOKEN,
|
|
122
|
-
owner: OWNER,
|
|
123
|
-
savedAt: 1,
|
|
124
|
-
fields: [],
|
|
125
|
-
files: [{ field: 'p' }],
|
|
126
|
-
}),
|
|
127
|
-
];
|
|
128
|
-
for (const raw of bad) {
|
|
129
|
-
storage.store.set(NAME, raw);
|
|
130
|
-
expect(readSubmittedDraft(storage, NAME, {
|
|
131
|
-
token: TOKEN,
|
|
132
|
-
owner: OWNER,
|
|
133
|
-
fresh: ALWAYS,
|
|
134
|
-
}), raw).toBeNull();
|
|
135
|
-
expect(storage.store.size, raw).toBe(0);
|
|
136
|
-
}
|
|
137
|
-
});
|
|
138
|
-
it('clears', () => {
|
|
139
|
-
const storage = memoryStorage();
|
|
140
|
-
save(storage);
|
|
141
|
-
clearSubmittedDraft(storage, NAME);
|
|
142
|
-
expect(storage.store.size).toBe(0);
|
|
143
|
-
});
|
|
144
|
-
it('survives a browser that refuses storage (private window)', () => {
|
|
145
|
-
const storage = refusingStorage();
|
|
146
|
-
expect(() => saveSubmittedDraft(storage, NAME, {
|
|
147
|
-
token: TOKEN,
|
|
148
|
-
owner: OWNER,
|
|
149
|
-
savedAt: SAVED_AT,
|
|
150
|
-
fields: [],
|
|
151
|
-
files: [],
|
|
152
|
-
})).not.toThrow();
|
|
153
|
-
expect(() => clearSubmittedDraft(storage, NAME)).not.toThrow();
|
|
154
|
-
});
|
|
155
|
-
it('skips a values capture that cannot be serialized', () => {
|
|
156
|
-
const storage = memoryStorage();
|
|
157
|
-
const cyclic = {};
|
|
158
|
-
cyclic.self = cyclic;
|
|
159
|
-
saveSubmittedDraft(storage, NAME, {
|
|
160
|
-
token: TOKEN,
|
|
161
|
-
owner: OWNER,
|
|
162
|
-
savedAt: SAVED_AT,
|
|
163
|
-
fields: [],
|
|
164
|
-
files: [],
|
|
165
|
-
values: cyclic,
|
|
166
|
-
});
|
|
167
|
-
expect(storage.store.size).toBe(0);
|
|
168
|
-
});
|
|
169
|
-
});
|
|
170
|
-
describe('a later attempt whose values cannot be serialized', () => {
|
|
171
|
-
it('clears the earlier attempt’s draft instead of leaving it to be restored', () => {
|
|
172
|
-
const storage = memoryStorage();
|
|
173
|
-
save(storage);
|
|
174
|
-
expect(storage.store.has(NAME)).toBe(true);
|
|
175
|
-
const cyclic = {};
|
|
176
|
-
cyclic.self = cyclic;
|
|
177
|
-
saveSubmittedDraft(storage, NAME, {
|
|
178
|
-
token: TOKEN,
|
|
179
|
-
owner: OWNER,
|
|
180
|
-
savedAt: SAVED_AT,
|
|
181
|
-
fields: [['title', 'A different submission']],
|
|
182
|
-
files: [],
|
|
183
|
-
values: cyclic,
|
|
184
|
-
});
|
|
185
|
-
expect(readSubmittedDraft(storage, NAME, {
|
|
186
|
-
token: TOKEN,
|
|
187
|
-
owner: OWNER,
|
|
188
|
-
fresh: ALWAYS,
|
|
189
|
-
})).toBeNull();
|
|
190
|
-
expect(storage.store.has(NAME)).toBe(false);
|
|
191
|
-
});
|
|
192
|
-
});
|
|
193
|
-
describe('a store that refuses to remove the draft', () => {
|
|
194
|
-
const read = (storage) => readSubmittedDraft(storage, NAME, {
|
|
195
|
-
token: TOKEN,
|
|
196
|
-
owner: OWNER,
|
|
197
|
-
fresh: ALWAYS,
|
|
198
|
-
});
|
|
199
|
-
it.each([
|
|
200
|
-
['writes still work', false],
|
|
201
|
-
['writes are refused too', true],
|
|
202
|
-
])('clearing leaves nothing to restore (%s)', (_label, refuseWrites) => {
|
|
203
|
-
const storage = stickyStorage();
|
|
204
|
-
save(storage);
|
|
205
|
-
expect(read(storage)).not.toBeNull();
|
|
206
|
-
storage.refuseWrites = refuseWrites;
|
|
207
|
-
clearSubmittedDraft(storage, NAME);
|
|
208
|
-
expect(read(storage)).toBeNull();
|
|
209
|
-
});
|
|
210
|
-
it.each([
|
|
211
|
-
['writes still work', false],
|
|
212
|
-
['writes are refused too', true],
|
|
213
|
-
])('an unserializable later attempt leaves nothing to restore (%s)', (_label, refuseWrites) => {
|
|
214
|
-
const storage = stickyStorage();
|
|
215
|
-
save(storage);
|
|
216
|
-
storage.refuseWrites = refuseWrites;
|
|
217
|
-
const cyclic = {};
|
|
218
|
-
cyclic.self = cyclic;
|
|
219
|
-
saveSubmittedDraft(storage, NAME, {
|
|
220
|
-
token: TOKEN,
|
|
221
|
-
owner: OWNER,
|
|
222
|
-
savedAt: SAVED_AT,
|
|
223
|
-
fields: [['title', 'A different submission']],
|
|
224
|
-
files: [],
|
|
225
|
-
values: cyclic,
|
|
226
|
-
});
|
|
227
|
-
expect(read(storage)).toBeNull();
|
|
228
|
-
});
|
|
229
|
-
it('a draft rejected at read time (another owner) is not restored later', () => {
|
|
230
|
-
const storage = stickyStorage();
|
|
231
|
-
save(storage);
|
|
232
|
-
expect(readSubmittedDraft(storage, NAME, {
|
|
233
|
-
token: TOKEN,
|
|
234
|
-
owner: 'tenant-1:person-2',
|
|
235
|
-
fresh: ALWAYS,
|
|
236
|
-
})).toBeNull();
|
|
237
|
-
expect(read(storage)).toBeNull();
|
|
238
|
-
});
|
|
239
|
-
it('a later save replaces the invalidation', () => {
|
|
240
|
-
const storage = stickyStorage();
|
|
241
|
-
save(storage);
|
|
242
|
-
clearSubmittedDraft(storage, NAME);
|
|
243
|
-
save(storage);
|
|
244
|
-
expect(read(storage)?.fields[0]).toEqual(['title', 'Trip over a hose']);
|
|
245
|
-
});
|
|
246
|
-
});
|
|
247
|
-
describe('describeFormData', () => {
|
|
248
|
-
it('keeps text entries in order, describes chosen files, skips excluded names', () => {
|
|
249
|
-
const data = new FormData();
|
|
250
|
-
data.append('submissionKey', TOKEN);
|
|
251
|
-
data.append('title', 'x');
|
|
252
|
-
data.append('photo', new File(['abc'], 'a.jpg', { type: 'image/jpeg' }));
|
|
253
|
-
data.append('photo', new File([], '', { type: 'application/octet-stream' }));
|
|
254
|
-
data.append('title', 'y');
|
|
255
|
-
expect(describeFormData(data, new Set(['submissionKey']))).toEqual({
|
|
256
|
-
fields: [
|
|
257
|
-
['title', 'x'],
|
|
258
|
-
['title', 'y'],
|
|
259
|
-
],
|
|
260
|
-
files: [{ field: 'photo', name: 'a.jpg', type: 'image/jpeg', size: 3 }],
|
|
261
|
-
});
|
|
262
|
-
});
|
|
263
|
-
});
|
|
264
|
-
describe('applyDraftToForm', () => {
|
|
265
|
-
function formOf(html) {
|
|
266
|
-
document.body.innerHTML = `<form>${html}</form>`;
|
|
267
|
-
const form = document.body.querySelector('form');
|
|
268
|
-
if (!form)
|
|
269
|
-
throw new Error('no form');
|
|
270
|
-
return form;
|
|
271
|
-
}
|
|
272
|
-
it('puts values back by name and order, and notifies bindings', () => {
|
|
273
|
-
const form = formOf(`
|
|
274
|
-
<input type="hidden" name="jobId" value="server" />
|
|
275
|
-
<input name="title" />
|
|
276
|
-
<input name="qty" /><input name="qty" />
|
|
277
|
-
<textarea name="notes"></textarea>
|
|
278
|
-
<select name="unit"><option value="pcs">pcs</option><option value="ft">ft</option></select>
|
|
279
|
-
<select name="tags" multiple><option value="a" selected>a</option><option value="b">b</option></select>
|
|
280
|
-
<input type="checkbox" name="urgent" value="yes" />
|
|
281
|
-
<input type="checkbox" name="checked" value="on" checked />
|
|
282
|
-
<input type="radio" name="sev" value="minor" checked /><input type="radio" name="sev" value="major" />
|
|
283
|
-
<input type="password" name="pin" />
|
|
284
|
-
<input type="file" name="photo" />
|
|
285
|
-
<input name="secret" />
|
|
286
|
-
`);
|
|
287
|
-
const events = [];
|
|
288
|
-
form.addEventListener('input', (event) => {
|
|
289
|
-
events.push(event.target.name);
|
|
290
|
-
});
|
|
291
|
-
applyDraftToForm(form, {
|
|
292
|
-
fields: [
|
|
293
|
-
['jobId', 'restored'],
|
|
294
|
-
['title', 'Trip'],
|
|
295
|
-
['qty', '1'],
|
|
296
|
-
['qty', '2'],
|
|
297
|
-
['notes', 'by the hose'],
|
|
298
|
-
['unit', 'ft'],
|
|
299
|
-
['tags', 'b'],
|
|
300
|
-
['urgent', 'yes'],
|
|
301
|
-
['sev', 'major'],
|
|
302
|
-
['pin', '1234'],
|
|
303
|
-
['secret', 'leaked'],
|
|
304
|
-
],
|
|
305
|
-
}, new Set(['secret']));
|
|
306
|
-
const value = (selector) => form.querySelector(selector).value;
|
|
307
|
-
const checked = (selector) => form.querySelector(selector).checked;
|
|
308
|
-
expect(value('[name=jobId]')).toBe('server');
|
|
309
|
-
expect(value('[name=title]')).toBe('Trip');
|
|
310
|
-
expect(Array.from(form.querySelectorAll('[name=qty]'), (q) => q.value)).toEqual(['1', '2']);
|
|
311
|
-
expect(value('[name=notes]')).toBe('by the hose');
|
|
312
|
-
expect(value('[name=unit]')).toBe('ft');
|
|
313
|
-
expect(Array.from(form.querySelector('[name=tags]')
|
|
314
|
-
.selectedOptions, (o) => o.value)).toEqual(['b']);
|
|
315
|
-
expect(checked('[name=urgent]')).toBe(true);
|
|
316
|
-
// Submitted unchecked (absent from the draft): unchecked again.
|
|
317
|
-
expect(checked('[name=checked]')).toBe(false);
|
|
318
|
-
expect(checked('[value=major]')).toBe(true);
|
|
319
|
-
expect(value('[name=pin]')).toBe('');
|
|
320
|
-
expect(value('[name=secret]')).toBe('');
|
|
321
|
-
expect(events).toEqual(expect.arrayContaining([
|
|
322
|
-
'title',
|
|
323
|
-
'qty',
|
|
324
|
-
'notes',
|
|
325
|
-
'unit',
|
|
326
|
-
'tags',
|
|
327
|
-
'urgent',
|
|
328
|
-
'checked',
|
|
329
|
-
'sev',
|
|
330
|
-
]));
|
|
331
|
-
expect(events).not.toContain('jobId');
|
|
332
|
-
expect(events).not.toContain('pin');
|
|
333
|
-
});
|
|
334
|
-
});
|
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Component tests for the Container layout primitive (Sweep S11, #1416).
|
|
3
|
-
*
|
|
4
|
-
* Mirrors the Button golden pattern: render via Testing Library, assert the
|
|
5
|
-
* rendered DOM/class/attribute API, and prove axe-clean output.
|
|
6
|
-
*/
|
|
7
|
-
import { render, screen } from '@testing-library/svelte';
|
|
8
|
-
import { createRawSnippet } from 'svelte';
|
|
9
|
-
import { describe, expect, it } from 'vitest';
|
|
10
|
-
import { expectNoA11yViolations } from '../../../test-support/a11y';
|
|
11
|
-
import Container from '../Container.svelte';
|
|
12
|
-
/** Build a text Snippet for the `children` prop. */
|
|
13
|
-
function textSnippet(text) {
|
|
14
|
-
return createRawSnippet(() => ({ render: () => `<span>${text}</span>` }));
|
|
15
|
-
}
|
|
16
|
-
describe('Container', () => {
|
|
17
|
-
it('renders slotted children', () => {
|
|
18
|
-
render(Container, { props: { children: textSnippet('Inside') } });
|
|
19
|
-
expect(screen.getByText('Inside')).toBeInTheDocument();
|
|
20
|
-
});
|
|
21
|
-
it('applies the default lg max-width class', () => {
|
|
22
|
-
const { container } = render(Container, {
|
|
23
|
-
props: { children: textSnippet('x') },
|
|
24
|
-
});
|
|
25
|
-
const div = container.querySelector('div.container');
|
|
26
|
-
expect(div).not.toBeNull();
|
|
27
|
-
expect(div).toHaveClass('container', 'max-w-lg');
|
|
28
|
-
});
|
|
29
|
-
it.each([
|
|
30
|
-
'sm',
|
|
31
|
-
'md',
|
|
32
|
-
'lg',
|
|
33
|
-
'xl',
|
|
34
|
-
'full',
|
|
35
|
-
])('maps maxWidth=%s to the max-w-%s class', (maxWidth) => {
|
|
36
|
-
const { container } = render(Container, {
|
|
37
|
-
props: { maxWidth, children: textSnippet('x') },
|
|
38
|
-
});
|
|
39
|
-
expect(container.querySelector('div.container')).toHaveClass(`max-w-${maxWidth}`);
|
|
40
|
-
});
|
|
41
|
-
it('passes ...rest attributes through to the root element', () => {
|
|
42
|
-
const { container } = render(Container, {
|
|
43
|
-
props: {
|
|
44
|
-
children: textSnippet('x'),
|
|
45
|
-
id: 'main-region',
|
|
46
|
-
'data-testid': 'wrap',
|
|
47
|
-
role: 'region',
|
|
48
|
-
'aria-label': 'Main content',
|
|
49
|
-
},
|
|
50
|
-
});
|
|
51
|
-
const div = container.querySelector('div.container');
|
|
52
|
-
expect(div).toHaveAttribute('id', 'main-region');
|
|
53
|
-
expect(div).toHaveAttribute('data-testid', 'wrap');
|
|
54
|
-
expect(screen.getByRole('region', { name: 'Main content' })).toBe(div);
|
|
55
|
-
});
|
|
56
|
-
it('is axe-clean with content', async () => {
|
|
57
|
-
const { container } = render(Container, {
|
|
58
|
-
props: { children: textSnippet('Accessible content') },
|
|
59
|
-
});
|
|
60
|
-
await expectNoA11yViolations(container);
|
|
61
|
-
});
|
|
62
|
-
});
|