@happyvertical/smrt-ui 0.53.1 → 0.53.3
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,802 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `createFormRetry()` end to end (#3291): a plain `<form method="POST">`
|
|
3
|
-
* wired through a structural fake of SvelteKit's `use:enhance`, posting to a
|
|
4
|
-
* fake action whose write is guarded by a `runOnce()`-style claim over
|
|
5
|
-
* (submission key, content digest).
|
|
6
|
-
*
|
|
7
|
-
* The "Done when" cases of the issue: a double submit, a lost response, a
|
|
8
|
-
* reload, a private window, and "typed the next entry before the first
|
|
9
|
-
* resolved".
|
|
10
|
-
*/
|
|
11
|
-
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
12
|
-
import { createFormRetry } from '../controller.js';
|
|
13
|
-
import { resetFormRetryMemory } from '../submission-key.js';
|
|
14
|
-
import { fakeEnhance, fakeRunOnceServer } from './fake-kit.js';
|
|
15
|
-
import { memoryStorage, refusingStorage, stickyStorage } from './storage.js';
|
|
16
|
-
const FORM_HTML = `
|
|
17
|
-
<input name="title" value="" />
|
|
18
|
-
<input name="qty" value="" />
|
|
19
|
-
<input type="file" name="photo" />
|
|
20
|
-
<button type="submit">Send</button>
|
|
21
|
-
`;
|
|
22
|
-
function mountForm(html = FORM_HTML) {
|
|
23
|
-
document.body.innerHTML = `<form method="POST">${html}</form>`;
|
|
24
|
-
const form = document.body.querySelector('form');
|
|
25
|
-
if (!form)
|
|
26
|
-
throw new Error('no form');
|
|
27
|
-
return form;
|
|
28
|
-
}
|
|
29
|
-
function fill(form, values) {
|
|
30
|
-
for (const [name, value] of Object.entries(values)) {
|
|
31
|
-
const control = form.querySelector(`[name="${name}"]`);
|
|
32
|
-
if (!control)
|
|
33
|
-
throw new Error(`no ${name}`);
|
|
34
|
-
control.value = value;
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
const fieldValue = (form, name) => form.querySelector(`[name="${name}"]`)?.value;
|
|
38
|
-
function setup(options = {}, html) {
|
|
39
|
-
const form = mountForm(html);
|
|
40
|
-
const server = fakeRunOnceServer();
|
|
41
|
-
const retry = createFormRetry({ form: 'report', ...options });
|
|
42
|
-
const detach = retry.attach(form);
|
|
43
|
-
const kit = fakeEnhance(form, retry.enhance(), server);
|
|
44
|
-
return { form, server, retry, kit, detach };
|
|
45
|
-
}
|
|
46
|
-
beforeEach(() => {
|
|
47
|
-
resetFormRetryMemory();
|
|
48
|
-
sessionStorage.clear();
|
|
49
|
-
});
|
|
50
|
-
describe('the SvelteKit contract', () => {
|
|
51
|
-
it('is assignable to kit’s SubmitFunction without importing kit', () => {
|
|
52
|
-
const retry = createFormRetry({ form: 'report', storage: null });
|
|
53
|
-
const asKit = retry.enhance();
|
|
54
|
-
const inner = () => { };
|
|
55
|
-
const composed = retry.enhance(inner);
|
|
56
|
-
expect(typeof asKit).toBe('function');
|
|
57
|
-
expect(typeof composed).toBe('function');
|
|
58
|
-
});
|
|
59
|
-
});
|
|
60
|
-
describe('a double submit', () => {
|
|
61
|
-
it('sends once: the second activation is refused while the first is in flight', async () => {
|
|
62
|
-
const { form, server, retry, kit } = setup({ storage: memoryStorage() });
|
|
63
|
-
fill(form, { title: 'Hose', qty: '1' });
|
|
64
|
-
const held = server.hold();
|
|
65
|
-
const first = kit.submit();
|
|
66
|
-
await vi.waitFor(() => expect(retry.state.status).toBe('submitting'));
|
|
67
|
-
await kit.submit();
|
|
68
|
-
expect(kit.cancelled).toBe(1);
|
|
69
|
-
expect(retry.state.status).toBe('busy');
|
|
70
|
-
held.release();
|
|
71
|
-
await first;
|
|
72
|
-
expect(server.requests).toHaveLength(1);
|
|
73
|
-
expect(server.rows).toHaveLength(1);
|
|
74
|
-
expect(retry.state.status).toBe('success');
|
|
75
|
-
});
|
|
76
|
-
it('puts the key in the body even when the hidden field is stale', async () => {
|
|
77
|
-
const { form, server, retry, kit } = setup({ storage: memoryStorage() });
|
|
78
|
-
const hidden = form.querySelector('[name=submissionKey]');
|
|
79
|
-
expect(hidden?.type).toBe('hidden');
|
|
80
|
-
expect(hidden?.value).toBe(retry.token);
|
|
81
|
-
if (hidden)
|
|
82
|
-
hidden.value = 'tampered';
|
|
83
|
-
const token = retry.token;
|
|
84
|
-
fill(form, { title: 'Hose' });
|
|
85
|
-
await kit.submit();
|
|
86
|
-
expect(server.requests[0].get('submissionKey')).toBe(token);
|
|
87
|
-
});
|
|
88
|
-
});
|
|
89
|
-
describe('a lost response', () => {
|
|
90
|
-
it('keeps the form, the key and the mount; the unchanged retry records ONE row', async () => {
|
|
91
|
-
const { form, server, retry, kit } = setup({ storage: memoryStorage() });
|
|
92
|
-
fill(form, { title: 'Hose', qty: '1' });
|
|
93
|
-
const token = retry.token;
|
|
94
|
-
server.next('lost-response');
|
|
95
|
-
await kit.submit();
|
|
96
|
-
expect(server.rows).toHaveLength(1);
|
|
97
|
-
expect(retry.state.status).toBe('transport-error');
|
|
98
|
-
// update() was not called: in kit it would apply the error and unmount.
|
|
99
|
-
expect(kit.updates).toHaveLength(0);
|
|
100
|
-
expect(kit.unmounted).toBe(0);
|
|
101
|
-
expect(fieldValue(form, 'title')).toBe('Hose');
|
|
102
|
-
expect(retry.token).toBe(token);
|
|
103
|
-
await kit.submit();
|
|
104
|
-
expect(server.requests).toHaveLength(2);
|
|
105
|
-
expect(server.rows).toHaveLength(1);
|
|
106
|
-
expect(retry.state.status).toBe('success');
|
|
107
|
-
// Confirmed and unchanged: reset, and the next fill gets a new key.
|
|
108
|
-
expect(fieldValue(form, 'title')).toBe('');
|
|
109
|
-
expect(retry.token).not.toBe(token);
|
|
110
|
-
expect(fieldValue(form, 'submissionKey')).toBe(retry.token);
|
|
111
|
-
});
|
|
112
|
-
it('a retry with DIFFERENT content is a new submission, never the old claim (#3136)', async () => {
|
|
113
|
-
const { form, server, kit } = setup({ storage: memoryStorage() });
|
|
114
|
-
fill(form, { title: 'Hose', qty: '1' });
|
|
115
|
-
server.next('lost-response');
|
|
116
|
-
await kit.submit();
|
|
117
|
-
fill(form, { qty: '2' });
|
|
118
|
-
await kit.submit();
|
|
119
|
-
expect(server.rows.map((row) => row.fields.qty)).toEqual([['1'], ['2']]);
|
|
120
|
-
});
|
|
121
|
-
it('an unreachable server writes nothing; the retry writes once', async () => {
|
|
122
|
-
const { form, server, retry, kit } = setup({ storage: memoryStorage() });
|
|
123
|
-
fill(form, { title: 'Hose' });
|
|
124
|
-
server.next('unreachable');
|
|
125
|
-
await kit.submit();
|
|
126
|
-
expect(server.rows).toHaveLength(0);
|
|
127
|
-
expect(retry.state.status).toBe('transport-error');
|
|
128
|
-
await kit.submit();
|
|
129
|
-
expect(server.rows).toHaveLength(1);
|
|
130
|
-
});
|
|
131
|
-
it('an aborted request settles like a transport error instead of wedging the form', async () => {
|
|
132
|
-
const { form, server, retry, kit } = setup({ storage: memoryStorage() });
|
|
133
|
-
fill(form, { title: 'Hose' });
|
|
134
|
-
const token = retry.token;
|
|
135
|
-
const held = server.hold();
|
|
136
|
-
const pending = kit.submit();
|
|
137
|
-
await vi.waitFor(() => expect(kit.controllers).toHaveLength(1));
|
|
138
|
-
kit.controllers[0].abort();
|
|
139
|
-
held.release();
|
|
140
|
-
await pending;
|
|
141
|
-
expect(retry.state).toMatchObject({
|
|
142
|
-
status: 'transport-error',
|
|
143
|
-
inFlight: null,
|
|
144
|
-
});
|
|
145
|
-
expect(retry.token).toBe(token);
|
|
146
|
-
await kit.submit();
|
|
147
|
-
expect(server.rows).toHaveLength(1);
|
|
148
|
-
});
|
|
149
|
-
});
|
|
150
|
-
describe('a validation failure', () => {
|
|
151
|
-
it('keeps the key and the typed values; the corrected submit carries the same key', async () => {
|
|
152
|
-
const storage = memoryStorage();
|
|
153
|
-
const { form, server, retry, kit } = setup({
|
|
154
|
-
storage,
|
|
155
|
-
restore: { owner: 'person-1' },
|
|
156
|
-
});
|
|
157
|
-
fill(form, { title: 'Hose' });
|
|
158
|
-
const token = retry.token;
|
|
159
|
-
server.next('fail');
|
|
160
|
-
await kit.submit();
|
|
161
|
-
expect(retry.state.status).toBe('failure');
|
|
162
|
-
expect(fieldValue(form, 'title')).toBe('Hose');
|
|
163
|
-
expect(retry.token).toBe(token);
|
|
164
|
-
// Nothing was recorded, so nothing is kept for a reload.
|
|
165
|
-
expect(storage.store.has('smrt:form-retry:report:draft')).toBe(false);
|
|
166
|
-
await kit.submit();
|
|
167
|
-
expect(server.requests[1].get('submissionKey')).toBe(token);
|
|
168
|
-
});
|
|
169
|
-
});
|
|
170
|
-
describe('typed the next entry before the first resolved', () => {
|
|
171
|
-
it('keeps the new entry, refuses to send it early, and records it as its own submission', async () => {
|
|
172
|
-
const { form, server, retry, kit } = setup({ storage: memoryStorage() });
|
|
173
|
-
fill(form, { title: 'First', qty: '1' });
|
|
174
|
-
const token = retry.token;
|
|
175
|
-
const held = server.hold();
|
|
176
|
-
const first = kit.submit();
|
|
177
|
-
await vi.waitFor(() => expect(retry.state.status).toBe('submitting'));
|
|
178
|
-
// The person moves on while the first is still in flight.
|
|
179
|
-
fill(form, { title: 'Second', qty: '2' });
|
|
180
|
-
await kit.submit();
|
|
181
|
-
expect(retry.state.status).toBe('busy');
|
|
182
|
-
held.release();
|
|
183
|
-
await first;
|
|
184
|
-
expect(retry.state.status).toBe('success');
|
|
185
|
-
// Not reset: the typed second entry is still there.
|
|
186
|
-
expect(kit.updates).toEqual([{ reset: false }]);
|
|
187
|
-
expect(fieldValue(form, 'title')).toBe('Second');
|
|
188
|
-
// The form still shows content, so the key is kept (see settleSubmit).
|
|
189
|
-
expect(retry.token).toBe(token);
|
|
190
|
-
await kit.submit();
|
|
191
|
-
expect(server.rows.map((row) => row.fields.title)).toEqual([
|
|
192
|
-
['First'],
|
|
193
|
-
['Second'],
|
|
194
|
-
]);
|
|
195
|
-
expect(fieldValue(form, 'title')).toBe('');
|
|
196
|
-
expect(retry.token).not.toBe(token);
|
|
197
|
-
});
|
|
198
|
-
});
|
|
199
|
-
describe('a reload', () => {
|
|
200
|
-
it('keeps the key across a reload even without restore', async () => {
|
|
201
|
-
const storage = memoryStorage();
|
|
202
|
-
const before = createFormRetry({ form: 'report', storage });
|
|
203
|
-
resetFormRetryMemory();
|
|
204
|
-
const after = createFormRetry({ form: 'report', storage });
|
|
205
|
-
expect(after.token).toBe(before.token);
|
|
206
|
-
expect(after.state.persistent).toBe(true);
|
|
207
|
-
});
|
|
208
|
-
it('restores what was submitted so the retry is byte-identical — ONE row', async () => {
|
|
209
|
-
const storage = memoryStorage();
|
|
210
|
-
let captured = 'capture-1';
|
|
211
|
-
const restoredValues = [];
|
|
212
|
-
const options = {
|
|
213
|
-
storage,
|
|
214
|
-
restore: {
|
|
215
|
-
owner: 'tenant:person-1',
|
|
216
|
-
values: {
|
|
217
|
-
capture: () => ({ captured }),
|
|
218
|
-
restore: (value) => restoredValues.push(value),
|
|
219
|
-
},
|
|
220
|
-
},
|
|
221
|
-
};
|
|
222
|
-
const first = setup(options);
|
|
223
|
-
fill(first.form, { title: 'Hose', qty: '3' });
|
|
224
|
-
first.server.next('lost-response');
|
|
225
|
-
await first.kit.submit();
|
|
226
|
-
expect(first.server.rows).toHaveLength(1);
|
|
227
|
-
const token = first.retry.token;
|
|
228
|
-
// Full reload: a fresh document, fresh module memory, same tab storage.
|
|
229
|
-
first.detach();
|
|
230
|
-
resetFormRetryMemory();
|
|
231
|
-
captured = 'never-read';
|
|
232
|
-
const form = mountForm();
|
|
233
|
-
const retry = createFormRetry({ form: 'report', ...options });
|
|
234
|
-
expect(retry.token).toBe(token);
|
|
235
|
-
expect(retry.state.restored).toBe(true);
|
|
236
|
-
expect(restoredValues).toEqual([{ captured: 'capture-1' }]);
|
|
237
|
-
expect(retry.restored?.fields).toEqual([
|
|
238
|
-
['title', 'Hose'],
|
|
239
|
-
['qty', '3'],
|
|
240
|
-
]);
|
|
241
|
-
retry.attach(form);
|
|
242
|
-
expect(fieldValue(form, 'title')).toBe('Hose');
|
|
243
|
-
expect(fieldValue(form, 'qty')).toBe('3');
|
|
244
|
-
const kit = fakeEnhance(form, retry.enhance(), first.server);
|
|
245
|
-
await kit.submit();
|
|
246
|
-
expect(first.server.rows).toHaveLength(1);
|
|
247
|
-
expect(retry.state).toMatchObject({ status: 'success', restored: false });
|
|
248
|
-
expect(storage.store.has('smrt:form-retry:report:draft')).toBe(false);
|
|
249
|
-
});
|
|
250
|
-
it('never restores for a different person on the same tab', async () => {
|
|
251
|
-
const storage = memoryStorage();
|
|
252
|
-
const first = setup({ storage, restore: { owner: 'person-1' } });
|
|
253
|
-
fill(first.form, { title: 'Private' });
|
|
254
|
-
first.server.next('lost-response');
|
|
255
|
-
await first.kit.submit();
|
|
256
|
-
resetFormRetryMemory();
|
|
257
|
-
const retry = createFormRetry({
|
|
258
|
-
form: 'report',
|
|
259
|
-
storage,
|
|
260
|
-
restore: { owner: 'person-2' },
|
|
261
|
-
});
|
|
262
|
-
expect(retry.restored).toBeNull();
|
|
263
|
-
expect(storage.store.has('smrt:form-retry:report:draft')).toBe(false);
|
|
264
|
-
});
|
|
265
|
-
it('never stores a draft without an owner, and clears a stale one', async () => {
|
|
266
|
-
const storage = memoryStorage();
|
|
267
|
-
storage.store.set('smrt:form-retry:report:draft', '{"stale":true}');
|
|
268
|
-
const { form, server, kit } = setup({ storage, restore: { owner: '' } });
|
|
269
|
-
fill(form, { title: 'Private' });
|
|
270
|
-
server.next('lost-response');
|
|
271
|
-
await kit.submit();
|
|
272
|
-
expect(storage.store.has('smrt:form-retry:report:draft')).toBe(false);
|
|
273
|
-
});
|
|
274
|
-
it('never restores a draft older than maxAgeMs', async () => {
|
|
275
|
-
const storage = memoryStorage();
|
|
276
|
-
let now = new Date('2026-10-01T08:00:00Z');
|
|
277
|
-
const options = {
|
|
278
|
-
storage,
|
|
279
|
-
now: () => now,
|
|
280
|
-
restore: { owner: 'person-1', maxAgeMs: 60_000 },
|
|
281
|
-
};
|
|
282
|
-
const first = setup(options);
|
|
283
|
-
fill(first.form, { title: 'Old' });
|
|
284
|
-
first.server.next('lost-response');
|
|
285
|
-
await first.kit.submit();
|
|
286
|
-
resetFormRetryMemory();
|
|
287
|
-
now = new Date('2026-10-01T08:02:00Z');
|
|
288
|
-
expect(createFormRetry({ form: 'report', ...options }).restored).toBeNull();
|
|
289
|
-
});
|
|
290
|
-
it('reports a file that must be chosen again instead of submitting without it', async () => {
|
|
291
|
-
const storage = memoryStorage();
|
|
292
|
-
const options = {
|
|
293
|
-
storage,
|
|
294
|
-
restore: { owner: 'person-1' },
|
|
295
|
-
};
|
|
296
|
-
const first = setup(options);
|
|
297
|
-
fill(first.form, { title: 'With photo' });
|
|
298
|
-
first.server.next('lost-response');
|
|
299
|
-
const photo = () => new File(['abc'], 'a.jpg', { type: 'image/jpeg' });
|
|
300
|
-
await first.kit.submit((data) => data.set('photo', photo()));
|
|
301
|
-
expect(first.server.rows).toHaveLength(1);
|
|
302
|
-
resetFormRetryMemory();
|
|
303
|
-
const form = mountForm();
|
|
304
|
-
const retry = createFormRetry({ form: 'report', ...options });
|
|
305
|
-
retry.attach(form);
|
|
306
|
-
expect(retry.state.filesToReselect).toEqual(['photo']);
|
|
307
|
-
expect(retry.restored?.files).toEqual([
|
|
308
|
-
{ field: 'photo', name: 'a.jpg', type: 'image/jpeg', size: 3 },
|
|
309
|
-
]);
|
|
310
|
-
const kit = fakeEnhance(form, retry.enhance(), first.server);
|
|
311
|
-
await kit.submit();
|
|
312
|
-
expect(kit.cancelled).toBe(1);
|
|
313
|
-
expect(retry.state).toMatchObject({
|
|
314
|
-
status: 'files-required',
|
|
315
|
-
filesToReselect: ['photo'],
|
|
316
|
-
inFlight: null,
|
|
317
|
-
});
|
|
318
|
-
expect(first.server.requests).toHaveLength(1);
|
|
319
|
-
// The same file chosen again: same name, type and size, so the same digest
|
|
320
|
-
// and the same claim.
|
|
321
|
-
await kit.submit((data) => data.set('photo', photo()));
|
|
322
|
-
expect(first.server.rows).toHaveLength(1);
|
|
323
|
-
expect(retry.state).toMatchObject({
|
|
324
|
-
status: 'success',
|
|
325
|
-
filesToReselect: [],
|
|
326
|
-
});
|
|
327
|
-
});
|
|
328
|
-
it('never writes a password or an excluded field to storage', async () => {
|
|
329
|
-
const storage = memoryStorage();
|
|
330
|
-
const { form, server, kit } = setup({ storage, restore: { owner: 'person-1', exclude: ['card'] } }, `<input name="title" /><input type="password" name="pin" /><input name="card" />`);
|
|
331
|
-
fill(form, { title: 'T', pin: '1234', card: '4111' });
|
|
332
|
-
server.next('lost-response');
|
|
333
|
-
await kit.submit();
|
|
334
|
-
const raw = storage.store.get('smrt:form-retry:report:draft') ?? '';
|
|
335
|
-
expect(raw).toContain('"title"');
|
|
336
|
-
expect(raw).not.toContain('1234');
|
|
337
|
-
expect(raw).not.toContain('4111');
|
|
338
|
-
expect(raw).not.toContain('submissionKey');
|
|
339
|
-
});
|
|
340
|
-
it('a values hook that rejects the stored value discards the whole draft', async () => {
|
|
341
|
-
const storage = memoryStorage();
|
|
342
|
-
const restore = {
|
|
343
|
-
owner: 'person-1',
|
|
344
|
-
values: {
|
|
345
|
-
capture: () => ({ keys: ['k-1'] }),
|
|
346
|
-
restore: () => {
|
|
347
|
-
throw new Error('not a valid capture');
|
|
348
|
-
},
|
|
349
|
-
},
|
|
350
|
-
};
|
|
351
|
-
const first = setup({ storage, restore });
|
|
352
|
-
fill(first.form, { title: 'Hose' });
|
|
353
|
-
first.server.next('lost-response');
|
|
354
|
-
await first.kit.submit();
|
|
355
|
-
resetFormRetryMemory();
|
|
356
|
-
const retry = createFormRetry({ form: 'report', storage, restore });
|
|
357
|
-
expect(retry.restored).toBeNull();
|
|
358
|
-
expect(retry.state.restored).toBe(false);
|
|
359
|
-
expect(storage.store.has('smrt:form-retry:report:draft')).toBe(false);
|
|
360
|
-
});
|
|
361
|
-
it('discard() abandons the restored fill: draft dropped, key rotated', async () => {
|
|
362
|
-
const storage = memoryStorage();
|
|
363
|
-
const first = setup({ storage, restore: { owner: 'person-1' } });
|
|
364
|
-
fill(first.form, { title: 'Hose' });
|
|
365
|
-
first.server.next('lost-response');
|
|
366
|
-
await first.kit.submit();
|
|
367
|
-
resetFormRetryMemory();
|
|
368
|
-
const retry = createFormRetry({
|
|
369
|
-
form: 'report',
|
|
370
|
-
storage,
|
|
371
|
-
restore: { owner: 'person-1' },
|
|
372
|
-
});
|
|
373
|
-
const token = retry.token;
|
|
374
|
-
retry.discard();
|
|
375
|
-
expect(retry.token).not.toBe(token);
|
|
376
|
-
expect(retry.restored).toBeNull();
|
|
377
|
-
expect(retry.state).toMatchObject({ restored: false, status: 'idle' });
|
|
378
|
-
expect(storage.store.has('smrt:form-retry:report:draft')).toBe(false);
|
|
379
|
-
});
|
|
380
|
-
});
|
|
381
|
-
describe('a private window (storage throws)', () => {
|
|
382
|
-
it('still sends one claim per fill, and the key survives a remount', async () => {
|
|
383
|
-
const storage = refusingStorage();
|
|
384
|
-
const { form, server, retry, kit, detach } = setup({
|
|
385
|
-
storage,
|
|
386
|
-
restore: { owner: 'person-1' },
|
|
387
|
-
});
|
|
388
|
-
expect(retry.state.persistent).toBe(false);
|
|
389
|
-
fill(form, { title: 'Hose' });
|
|
390
|
-
const token = retry.token;
|
|
391
|
-
server.next('lost-response');
|
|
392
|
-
await kit.submit();
|
|
393
|
-
// `enhance`'s error path remounts the component: same page, same memory.
|
|
394
|
-
detach();
|
|
395
|
-
const remounted = createFormRetry({ form: 'report', storage });
|
|
396
|
-
expect(remounted.token).toBe(token);
|
|
397
|
-
const remountedKit = fakeEnhance(form, remounted.enhance(), server);
|
|
398
|
-
await remountedKit.submit();
|
|
399
|
-
expect(server.rows).toHaveLength(1);
|
|
400
|
-
expect(remounted.state.status).toBe('success');
|
|
401
|
-
});
|
|
402
|
-
it('works when the sessionStorage property itself throws', async () => {
|
|
403
|
-
const original = Object.getOwnPropertyDescriptor(globalThis, 'sessionStorage');
|
|
404
|
-
Object.defineProperty(globalThis, 'sessionStorage', {
|
|
405
|
-
configurable: true,
|
|
406
|
-
get() {
|
|
407
|
-
throw new DOMException('The operation is insecure.', 'SecurityError');
|
|
408
|
-
},
|
|
409
|
-
});
|
|
410
|
-
try {
|
|
411
|
-
const { form, server, retry, kit } = setup();
|
|
412
|
-
fill(form, { title: 'Hose' });
|
|
413
|
-
server.next('lost-response');
|
|
414
|
-
await kit.submit();
|
|
415
|
-
await kit.submit();
|
|
416
|
-
expect(server.rows).toHaveLength(1);
|
|
417
|
-
expect(retry.state.status).toBe('success');
|
|
418
|
-
}
|
|
419
|
-
finally {
|
|
420
|
-
if (original)
|
|
421
|
-
Object.defineProperty(globalThis, 'sessionStorage', original);
|
|
422
|
-
}
|
|
423
|
-
});
|
|
424
|
-
});
|
|
425
|
-
describe('redirects', () => {
|
|
426
|
-
it('keep the key and the draft by default — a redirect cannot vouch for a write', async () => {
|
|
427
|
-
const storage = memoryStorage();
|
|
428
|
-
const { form, server, retry, kit } = setup({
|
|
429
|
-
storage,
|
|
430
|
-
restore: { owner: 'p' },
|
|
431
|
-
});
|
|
432
|
-
fill(form, { title: 'Hose' });
|
|
433
|
-
const token = retry.token;
|
|
434
|
-
server.next('redirect');
|
|
435
|
-
await kit.submit();
|
|
436
|
-
expect(retry.state.status).toBe('redirect');
|
|
437
|
-
expect(retry.token).toBe(token);
|
|
438
|
-
expect(storage.store.has('smrt:form-retry:report:draft')).toBe(true);
|
|
439
|
-
expect(kit.applied).toEqual(['redirect']);
|
|
440
|
-
});
|
|
441
|
-
it('rotate with redirectConfirmsWrite', async () => {
|
|
442
|
-
const storage = memoryStorage();
|
|
443
|
-
const { form, server, retry, kit } = setup({
|
|
444
|
-
storage,
|
|
445
|
-
redirectConfirmsWrite: true,
|
|
446
|
-
restore: { owner: 'p' },
|
|
447
|
-
});
|
|
448
|
-
fill(form, { title: 'Hose' });
|
|
449
|
-
const token = retry.token;
|
|
450
|
-
server.next('redirect');
|
|
451
|
-
await kit.submit();
|
|
452
|
-
expect(retry.state.status).toBe('redirect');
|
|
453
|
-
expect(retry.token).not.toBe(token);
|
|
454
|
-
expect(storage.store.has('smrt:form-retry:report:draft')).toBe(false);
|
|
455
|
-
});
|
|
456
|
-
});
|
|
457
|
-
describe('composing with the page’s own submit function', () => {
|
|
458
|
-
it('runs it after the gate with the key in formData, and hands it the safe reset', async () => {
|
|
459
|
-
const form = mountForm();
|
|
460
|
-
const server = fakeRunOnceServer();
|
|
461
|
-
const retry = createFormRetry({ form: 'report', storage: memoryStorage() });
|
|
462
|
-
retry.attach(form);
|
|
463
|
-
const seen = [];
|
|
464
|
-
const resets = [];
|
|
465
|
-
const kit = fakeEnhance(form, retry.enhance(({ formData }) => {
|
|
466
|
-
seen.push(formData.get('submissionKey'));
|
|
467
|
-
return async ({ update }) => {
|
|
468
|
-
await update();
|
|
469
|
-
resets.push(kit.updates.at(-1)?.reset);
|
|
470
|
-
};
|
|
471
|
-
}), server);
|
|
472
|
-
fill(form, { title: 'Hose' });
|
|
473
|
-
await kit.submit();
|
|
474
|
-
expect(seen).toEqual([server.requests[0].get('submissionKey')]);
|
|
475
|
-
expect(resets).toEqual([true]);
|
|
476
|
-
});
|
|
477
|
-
it('releases the slot when the page cancels or throws', async () => {
|
|
478
|
-
const form = mountForm();
|
|
479
|
-
const server = fakeRunOnceServer();
|
|
480
|
-
const retry = createFormRetry({ form: 'report', storage: memoryStorage() });
|
|
481
|
-
let mode = 'cancel';
|
|
482
|
-
const kit = fakeEnhance(form, retry.enhance(({ cancel }) => {
|
|
483
|
-
if (mode === 'cancel')
|
|
484
|
-
cancel();
|
|
485
|
-
if (mode === 'throw')
|
|
486
|
-
throw new Error('page bug');
|
|
487
|
-
}), server);
|
|
488
|
-
await kit.submit();
|
|
489
|
-
expect(retry.state).toMatchObject({ status: 'idle', inFlight: null });
|
|
490
|
-
mode = 'throw';
|
|
491
|
-
await expect(kit.submit()).rejects.toThrow('page bug');
|
|
492
|
-
expect(retry.state).toMatchObject({ status: 'idle', inFlight: null });
|
|
493
|
-
mode = 'send';
|
|
494
|
-
await kit.submit();
|
|
495
|
-
expect(server.requests).toHaveLength(1);
|
|
496
|
-
});
|
|
497
|
-
});
|
|
498
|
-
describe('the store contract and the attachment', () => {
|
|
499
|
-
it('notifies subscribers now and on every change, until unsubscribed', async () => {
|
|
500
|
-
const { form, retry, kit } = setup({ storage: memoryStorage() });
|
|
501
|
-
const statuses = [];
|
|
502
|
-
const unsubscribe = retry.subscribe((state) => statuses.push(state.status));
|
|
503
|
-
fill(form, { title: 'Hose' });
|
|
504
|
-
await kit.submit();
|
|
505
|
-
unsubscribe();
|
|
506
|
-
await kit.submit();
|
|
507
|
-
expect(statuses).toEqual(['idle', 'submitting', 'success']);
|
|
508
|
-
});
|
|
509
|
-
it('keeps one hidden key field in sync, and stops when detached', async () => {
|
|
510
|
-
const { form, retry, kit, detach } = setup({ storage: memoryStorage() });
|
|
511
|
-
const fields = () => Array.from(form.querySelectorAll('[name=submissionKey]'));
|
|
512
|
-
expect(fields().map((field) => field.value)).toEqual([retry.token]);
|
|
513
|
-
fill(form, { title: 'Hose' });
|
|
514
|
-
await kit.submit();
|
|
515
|
-
expect(fields().map((field) => field.value)).toEqual([retry.token]);
|
|
516
|
-
detach();
|
|
517
|
-
const stale = retry.token;
|
|
518
|
-
fill(form, { title: 'Again' });
|
|
519
|
-
await kit.submit();
|
|
520
|
-
expect(fields()[0].value).toBe(stale);
|
|
521
|
-
});
|
|
522
|
-
it('reuses a hidden field the page rendered, and works as a use: action', () => {
|
|
523
|
-
const form = mountForm('<input type="hidden" name="key" value="server-rendered" /><input name="title" />');
|
|
524
|
-
const retry = createFormRetry({
|
|
525
|
-
form: 'report',
|
|
526
|
-
fieldName: 'key',
|
|
527
|
-
storage: memoryStorage(),
|
|
528
|
-
});
|
|
529
|
-
const handle = retry.action(form);
|
|
530
|
-
const fields = form.querySelectorAll('[name=key]');
|
|
531
|
-
expect(fields).toHaveLength(1);
|
|
532
|
-
expect(fields[0].value).toBe(retry.token);
|
|
533
|
-
handle.destroy();
|
|
534
|
-
});
|
|
535
|
-
});
|
|
536
|
-
describe('the page’s submit hook aborts the request itself', () => {
|
|
537
|
-
it('settles like a transport error instead of leaving the slot in flight', async () => {
|
|
538
|
-
const form = mountForm();
|
|
539
|
-
const server = fakeRunOnceServer();
|
|
540
|
-
const retry = createFormRetry({ form: 'report', storage: memoryStorage() });
|
|
541
|
-
let abort = true;
|
|
542
|
-
const kit = fakeEnhance(form, retry.enhance(({ controller }) => {
|
|
543
|
-
// Abandon the request without cancelling the submission: kit still
|
|
544
|
-
// sends, the fetch rejects with an AbortError, and no result arrives.
|
|
545
|
-
if (abort)
|
|
546
|
-
controller.abort();
|
|
547
|
-
}), server);
|
|
548
|
-
fill(form, { title: 'Hose' });
|
|
549
|
-
const token = retry.token;
|
|
550
|
-
await kit.submit();
|
|
551
|
-
expect(server.requests).toHaveLength(0);
|
|
552
|
-
expect(retry.state).toMatchObject({
|
|
553
|
-
status: 'transport-error',
|
|
554
|
-
inFlight: null,
|
|
555
|
-
});
|
|
556
|
-
expect(retry.token).toBe(token);
|
|
557
|
-
abort = false;
|
|
558
|
-
await kit.submit();
|
|
559
|
-
expect(server.rows).toHaveLength(1);
|
|
560
|
-
});
|
|
561
|
-
});
|
|
562
|
-
describe('an edit while the page’s async submit hook is pending', () => {
|
|
563
|
-
it('is not mistaken for the submitted content: the success keeps it', async () => {
|
|
564
|
-
const form = mountForm();
|
|
565
|
-
const server = fakeRunOnceServer();
|
|
566
|
-
const retry = createFormRetry({ form: 'report', storage: memoryStorage() });
|
|
567
|
-
retry.attach(form);
|
|
568
|
-
let release;
|
|
569
|
-
const gate = new Promise((resolve) => {
|
|
570
|
-
release = resolve;
|
|
571
|
-
});
|
|
572
|
-
let entered = false;
|
|
573
|
-
const kit = fakeEnhance(form, retry.enhance(async () => {
|
|
574
|
-
entered = true;
|
|
575
|
-
await gate;
|
|
576
|
-
}), server);
|
|
577
|
-
fill(form, { title: 'First' });
|
|
578
|
-
const token = retry.token;
|
|
579
|
-
const pending = kit.submit();
|
|
580
|
-
await vi.waitFor(() => expect(entered).toBe(true));
|
|
581
|
-
// kit built the body before awaiting the hook: this edit is unsent.
|
|
582
|
-
fill(form, { title: 'Second' });
|
|
583
|
-
release();
|
|
584
|
-
await pending;
|
|
585
|
-
expect(server.rows.map((row) => row.fields.title)).toEqual([['First']]);
|
|
586
|
-
expect(retry.state.status).toBe('success');
|
|
587
|
-
expect(fieldValue(form, 'title')).toBe('Second');
|
|
588
|
-
expect(retry.token).toBe(token);
|
|
589
|
-
});
|
|
590
|
-
});
|
|
591
|
-
describe('a custom result callback decides the reset', () => {
|
|
592
|
-
function customSetup(handle) {
|
|
593
|
-
const form = mountForm();
|
|
594
|
-
const server = fakeRunOnceServer();
|
|
595
|
-
const retry = createFormRetry({ form: 'report', storage: memoryStorage() });
|
|
596
|
-
retry.attach(form);
|
|
597
|
-
const kit = fakeEnhance(form, retry.enhance(() => async (input) => handle(input)), server);
|
|
598
|
-
return { form, server, retry, kit };
|
|
599
|
-
}
|
|
600
|
-
it('update({ reset: false }) keeps the values AND their key, so resending them replays', async () => {
|
|
601
|
-
const { form, server, retry, kit } = customSetup(({ update }) => update({ reset: false }));
|
|
602
|
-
fill(form, { title: 'Hose', qty: '1' });
|
|
603
|
-
const token = retry.token;
|
|
604
|
-
await kit.submit();
|
|
605
|
-
expect(retry.state.status).toBe('success');
|
|
606
|
-
expect(fieldValue(form, 'title')).toBe('Hose');
|
|
607
|
-
expect(retry.token).toBe(token);
|
|
608
|
-
await kit.submit();
|
|
609
|
-
expect(server.requests).toHaveLength(2);
|
|
610
|
-
expect(server.rows).toHaveLength(1);
|
|
611
|
-
});
|
|
612
|
-
it('a callback that never calls update keeps the values AND their key', async () => {
|
|
613
|
-
const { form, server, retry, kit } = customSetup(() => { });
|
|
614
|
-
fill(form, { title: 'Hose' });
|
|
615
|
-
const token = retry.token;
|
|
616
|
-
await kit.submit();
|
|
617
|
-
expect(fieldValue(form, 'title')).toBe('Hose');
|
|
618
|
-
expect(retry.token).toBe(token);
|
|
619
|
-
await kit.submit();
|
|
620
|
-
expect(server.rows).toHaveLength(1);
|
|
621
|
-
});
|
|
622
|
-
it('a reset the callback performs (through update or by hand) retires the key', async () => {
|
|
623
|
-
for (const handle of [
|
|
624
|
-
({ update }) => update(),
|
|
625
|
-
({ formElement }) => formElement.reset(),
|
|
626
|
-
]) {
|
|
627
|
-
const { form, retry, kit } = customSetup(handle);
|
|
628
|
-
fill(form, { title: 'Hose' });
|
|
629
|
-
const token = retry.token;
|
|
630
|
-
await kit.submit();
|
|
631
|
-
expect(fieldValue(form, 'title')).toBe('');
|
|
632
|
-
expect(retry.token).not.toBe(token);
|
|
633
|
-
expect(fieldValue(form, 'submissionKey')).toBe(retry.token);
|
|
634
|
-
resetFormRetryMemory();
|
|
635
|
-
}
|
|
636
|
-
});
|
|
637
|
-
it('never retires the key out from under a submit started during the callback', async () => {
|
|
638
|
-
let release;
|
|
639
|
-
const gate = new Promise((resolve) => {
|
|
640
|
-
release = resolve;
|
|
641
|
-
});
|
|
642
|
-
let reset = false;
|
|
643
|
-
const { form, server, retry, kit } = customSetup(async ({ update }) => {
|
|
644
|
-
await update();
|
|
645
|
-
reset = true;
|
|
646
|
-
await gate;
|
|
647
|
-
});
|
|
648
|
-
fill(form, { title: 'First' });
|
|
649
|
-
const token = retry.token;
|
|
650
|
-
const first = kit.submit();
|
|
651
|
-
await vi.waitFor(() => expect(reset).toBe(true));
|
|
652
|
-
// The form is already reset; the next entry is sent before the callback
|
|
653
|
-
// finishes, and its response is lost.
|
|
654
|
-
fill(form, { title: 'Second' });
|
|
655
|
-
server.next('lost-response');
|
|
656
|
-
const held = server.hold();
|
|
657
|
-
const second = kit.submit();
|
|
658
|
-
await vi.waitFor(() => expect(retry.state.inFlight).toBe(token));
|
|
659
|
-
release();
|
|
660
|
-
await first;
|
|
661
|
-
held.release();
|
|
662
|
-
await second;
|
|
663
|
-
expect(retry.state.status).toBe('transport-error');
|
|
664
|
-
// The unchanged resend must carry the key the lost request carried.
|
|
665
|
-
await kit.submit();
|
|
666
|
-
expect(server.rows.map((row) => row.fields.title)).toEqual([
|
|
667
|
-
['First'],
|
|
668
|
-
['Second'],
|
|
669
|
-
]);
|
|
670
|
-
});
|
|
671
|
-
});
|
|
672
|
-
describe('discard() while a submit is in flight', () => {
|
|
673
|
-
it('is refused: the key and the draft stay with the unresolved submit', async () => {
|
|
674
|
-
const storage = memoryStorage();
|
|
675
|
-
const { form, server, retry, kit } = setup({
|
|
676
|
-
storage,
|
|
677
|
-
restore: { owner: 'person-1' },
|
|
678
|
-
});
|
|
679
|
-
fill(form, { title: 'Hose' });
|
|
680
|
-
const token = retry.token;
|
|
681
|
-
server.next('lost-response');
|
|
682
|
-
const held = server.hold();
|
|
683
|
-
const pending = kit.submit();
|
|
684
|
-
await vi.waitFor(() => expect(retry.state.status).toBe('submitting'));
|
|
685
|
-
const refused = retry.discard();
|
|
686
|
-
held.release();
|
|
687
|
-
await pending;
|
|
688
|
-
expect(retry.state.status).toBe('transport-error');
|
|
689
|
-
expect(retry.token).toBe(token);
|
|
690
|
-
expect(storage.store.has('smrt:form-retry:report:draft')).toBe(true);
|
|
691
|
-
// The unchanged resend is the same claim: ONE row.
|
|
692
|
-
await kit.submit();
|
|
693
|
-
expect(server.rows).toHaveLength(1);
|
|
694
|
-
expect(server.requests[1].get('submissionKey')).toBe(token);
|
|
695
|
-
expect(refused).toBe(false);
|
|
696
|
-
// Once settled, discard works again.
|
|
697
|
-
expect(storage.store.has('smrt:form-retry:report:draft')).toBe(false);
|
|
698
|
-
fill(form, { title: 'Other' });
|
|
699
|
-
const next = retry.token;
|
|
700
|
-
expect(retry.discard()).toBe(true);
|
|
701
|
-
expect(retry.token).not.toBe(next);
|
|
702
|
-
});
|
|
703
|
-
});
|
|
704
|
-
describe('restore with a store that refuses to remove the draft', () => {
|
|
705
|
-
const restore = {
|
|
706
|
-
owner: 'person-1',
|
|
707
|
-
values: {
|
|
708
|
-
capture: () => current,
|
|
709
|
-
restore: () => { },
|
|
710
|
-
},
|
|
711
|
-
};
|
|
712
|
-
let current;
|
|
713
|
-
beforeEach(() => {
|
|
714
|
-
current = { photoKey: 'k-1' };
|
|
715
|
-
});
|
|
716
|
-
function cyclic() {
|
|
717
|
-
const value = {};
|
|
718
|
-
value.self = value;
|
|
719
|
-
return value;
|
|
720
|
-
}
|
|
721
|
-
it('an unserializable later attempt is never followed by the earlier draft after a reload', async () => {
|
|
722
|
-
const storage = stickyStorage();
|
|
723
|
-
const first = setup({ storage, restore });
|
|
724
|
-
fill(first.form, { title: 'First attempt' });
|
|
725
|
-
first.server.next('lost-response');
|
|
726
|
-
await first.kit.submit();
|
|
727
|
-
fill(first.form, { title: 'Second attempt' });
|
|
728
|
-
current = cyclic();
|
|
729
|
-
first.server.next('lost-response');
|
|
730
|
-
await first.kit.submit();
|
|
731
|
-
resetFormRetryMemory(); // a reload: storage survives, memory does not
|
|
732
|
-
const reloaded = createFormRetry({ form: 'report', storage, restore });
|
|
733
|
-
expect(reloaded.restored).toBeNull();
|
|
734
|
-
expect(reloaded.state.restored).toBe(false);
|
|
735
|
-
});
|
|
736
|
-
it('a validation failure is never followed by its draft after a reload', async () => {
|
|
737
|
-
const storage = stickyStorage();
|
|
738
|
-
const first = setup({ storage, restore });
|
|
739
|
-
fill(first.form, { title: 'Hose' });
|
|
740
|
-
first.server.next('fail');
|
|
741
|
-
await first.kit.submit();
|
|
742
|
-
resetFormRetryMemory();
|
|
743
|
-
const reloaded = createFormRetry({ form: 'report', storage, restore });
|
|
744
|
-
expect(reloaded.restored).toBeNull();
|
|
745
|
-
});
|
|
746
|
-
it('with writes refused too, the stale draft is never restored in this page', async () => {
|
|
747
|
-
const storage = stickyStorage();
|
|
748
|
-
const first = setup({ storage, restore });
|
|
749
|
-
fill(first.form, { title: 'First attempt' });
|
|
750
|
-
first.server.next('lost-response');
|
|
751
|
-
await first.kit.submit();
|
|
752
|
-
storage.refuseWrites = true;
|
|
753
|
-
fill(first.form, { title: 'Second attempt' });
|
|
754
|
-
current = cyclic();
|
|
755
|
-
first.server.next('lost-response');
|
|
756
|
-
await first.kit.submit();
|
|
757
|
-
// A remount in the same page (memory kept): nothing to restore.
|
|
758
|
-
const remounted = createFormRetry({ form: 'report', storage, restore });
|
|
759
|
-
expect(remounted.restored).toBeNull();
|
|
760
|
-
});
|
|
761
|
-
});
|
|
762
|
-
describe('page-managed hidden fields', () => {
|
|
763
|
-
const NONCE_FORM = `${FORM_HTML}<input type="hidden" name="csrf" value="n-1" />`;
|
|
764
|
-
it('a hidden field the page rewrites after submit keeps the form (safe direction)', async () => {
|
|
765
|
-
const { form, retry, kit } = setup({ storage: memoryStorage() }, NONCE_FORM);
|
|
766
|
-
fill(form, { title: 'Hose' });
|
|
767
|
-
const token = retry.token;
|
|
768
|
-
const held = kit.submit();
|
|
769
|
-
fill(form, { csrf: 'n-2' });
|
|
770
|
-
await held;
|
|
771
|
-
expect(kit.updates).toEqual([{ reset: false }]);
|
|
772
|
-
expect(fieldValue(form, 'title')).toBe('Hose');
|
|
773
|
-
expect(retry.token).toBe(token);
|
|
774
|
-
});
|
|
775
|
-
it('ignoreFields leaves such a field out of the comparison', async () => {
|
|
776
|
-
const { form, retry, kit } = setup({ storage: memoryStorage(), ignoreFields: ['csrf'] }, NONCE_FORM);
|
|
777
|
-
fill(form, { title: 'Hose' });
|
|
778
|
-
const token = retry.token;
|
|
779
|
-
const held = kit.submit();
|
|
780
|
-
fill(form, { csrf: 'n-2' });
|
|
781
|
-
await held;
|
|
782
|
-
expect(kit.updates).toEqual([{ reset: true }]);
|
|
783
|
-
expect(fieldValue(form, 'title')).toBe('');
|
|
784
|
-
expect(retry.token).not.toBe(token);
|
|
785
|
-
});
|
|
786
|
-
it('a custom key field name rotating never looks like an edit', async () => {
|
|
787
|
-
const form = mountForm();
|
|
788
|
-
const retry = createFormRetry({
|
|
789
|
-
form: 'report',
|
|
790
|
-
storage: memoryStorage(),
|
|
791
|
-
fieldName: 'retryKey',
|
|
792
|
-
});
|
|
793
|
-
retry.attach(form);
|
|
794
|
-
const kit = fakeEnhance(form, retry.enhance(), fakeRunOnceServer('retryKey'));
|
|
795
|
-
fill(form, { title: 'Hose' });
|
|
796
|
-
const token = retry.token;
|
|
797
|
-
await kit.submit();
|
|
798
|
-
expect(kit.updates).toEqual([{ reset: true }]);
|
|
799
|
-
expect(retry.token).not.toBe(token);
|
|
800
|
-
expect(fieldValue(form, 'retryKey')).toBe(retry.token);
|
|
801
|
-
});
|
|
802
|
-
});
|