@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.
Files changed (205) hide show
  1. package/package.json +3 -3
  2. package/dist/actions/__tests__/ripple.test.js +0 -82
  3. package/dist/actions/__tests__/swipe-dismiss.test.js +0 -69
  4. package/dist/components/calendar/__tests__/CalendarView.test.js +0 -226
  5. package/dist/components/calendar/__tests__/calendar-model.test.js +0 -138
  6. package/dist/components/chat/__tests__/chat-primitives.test.js +0 -118
  7. package/dist/components/data/__tests__/CollectionList.test.js +0 -37
  8. package/dist/components/data/__tests__/DataTable.test.js +0 -807
  9. package/dist/components/data/__tests__/DataTableConformance.test.js +0 -212
  10. package/dist/components/data/__tests__/DataTableController.test.js +0 -371
  11. package/dist/components/data/__tests__/DataTableIdentity.test.js +0 -29
  12. package/dist/components/data/__tests__/DataTableLayout.test.js +0 -195
  13. package/dist/components/data/__tests__/DataTablePerformance.test.js +0 -21
  14. package/dist/components/data/__tests__/DataTableResponsive.test.js +0 -80
  15. package/dist/components/data/__tests__/DataTableVirtualization.test.js +0 -80
  16. package/dist/components/data/__tests__/DataTableVirtualizationComponent.test.js +0 -255
  17. package/dist/components/data/__tests__/SortableHeader.test.js +0 -101
  18. package/dist/components/data/__tests__/data-surface.test.js +0 -746
  19. package/dist/components/data/__tests__/data-table-helpers.test.js +0 -36
  20. package/dist/components/data/__tests__/data-table-surface.test.js +0 -49
  21. package/dist/components/data/__tests__/link-step-surface.test.js +0 -172
  22. package/dist/components/data/__tests__/list-sort-state.svelte.test.js +0 -71
  23. package/dist/components/data/__tests__/list-sort.test.js +0 -91
  24. package/dist/components/data/__tests__/mounted-data-surface.test.js +0 -794
  25. package/dist/components/data/__tests__/responsive-table.fixture.svelte +0 -29
  26. package/dist/components/data/__tests__/responsive-table.fixture.svelte.d.ts +0 -7
  27. package/dist/components/data/__tests__/responsive-table.fixture.svelte.d.ts.map +0 -1
  28. package/dist/components/data/__tests__/surface-navigation.test.js +0 -121
  29. package/dist/components/display/__tests__/ConfidenceBadge.test.js +0 -109
  30. package/dist/components/display/__tests__/CurrencyDisplay.test.js +0 -304
  31. package/dist/components/display/__tests__/CurrencyDisplayI18nHarness.svelte +0 -20
  32. package/dist/components/display/__tests__/CurrencyDisplayI18nHarness.svelte.d.ts +0 -19
  33. package/dist/components/display/__tests__/CurrencyDisplayI18nHarness.svelte.d.ts.map +0 -1
  34. package/dist/components/display/__tests__/CurrencyDisplaySsrHarness.svelte +0 -7
  35. package/dist/components/display/__tests__/CurrencyDisplaySsrHarness.svelte.d.ts +0 -19
  36. package/dist/components/display/__tests__/CurrencyDisplaySsrHarness.svelte.d.ts.map +0 -1
  37. package/dist/components/display/__tests__/DateDisplay.test.js +0 -195
  38. package/dist/components/display/__tests__/Icon.test.js +0 -223
  39. package/dist/components/display/__tests__/StatusBadge.test.js +0 -191
  40. package/dist/components/feedback/__tests__/ConfirmDialog.test.js +0 -181
  41. package/dist/components/feedback/__tests__/LoadingOverlay.test.js +0 -113
  42. package/dist/components/feedback/__tests__/Modal.test.js +0 -72
  43. package/dist/components/feedback/__tests__/PhoneSheet.test.js +0 -101
  44. package/dist/components/feedback/__tests__/ProgressBar.test.js +0 -89
  45. package/dist/components/feedback/__tests__/WorkingStrip.test.js +0 -172
  46. package/dist/components/feedback/__tests__/confirm-message.fixture.svelte +0 -9
  47. package/dist/components/feedback/__tests__/confirm-message.fixture.svelte.d.ts +0 -19
  48. package/dist/components/feedback/__tests__/confirm-message.fixture.svelte.d.ts.map +0 -1
  49. package/dist/components/feedback/__tests__/toast.test.js +0 -190
  50. package/dist/components/forms/__tests__/CameraCapture.test.js +0 -504
  51. package/dist/components/forms/__tests__/Combobox.test.js +0 -354
  52. package/dist/components/forms/__tests__/ControlHitArea.test.js +0 -73
  53. package/dist/components/forms/__tests__/DictationButton.test.js +0 -176
  54. package/dist/components/forms/__tests__/Fieldset.test.js +0 -37
  55. package/dist/components/forms/__tests__/Form.test.js +0 -201
  56. package/dist/components/forms/__tests__/FormActionBar.test.js +0 -22
  57. package/dist/components/forms/__tests__/FormGroup.test.js +0 -48
  58. package/dist/components/forms/__tests__/Input.test.js +0 -49
  59. package/dist/components/forms/__tests__/Listbox.test.js +0 -303
  60. package/dist/components/forms/__tests__/MultiSelect.test.js +0 -281
  61. package/dist/components/forms/__tests__/SearchInput.test.js +0 -92
  62. package/dist/components/forms/__tests__/Select.test.js +0 -37
  63. package/dist/components/forms/__tests__/SignaturePad.test.js +0 -338
  64. package/dist/components/forms/__tests__/StagedControlReview.test.js +0 -702
  65. package/dist/components/forms/__tests__/Textarea.test.js +0 -39
  66. package/dist/components/forms/__tests__/Toggle.test.js +0 -87
  67. package/dist/components/forms/__tests__/camera-capture-session.test.js +0 -207
  68. package/dist/components/forms/__tests__/capture-form-retry.test.js +0 -166
  69. package/dist/components/forms/__tests__/capture-form.fixture.svelte +0 -38
  70. package/dist/components/forms/__tests__/capture-form.fixture.svelte.d.ts +0 -10
  71. package/dist/components/forms/__tests__/capture-form.fixture.svelte.d.ts.map +0 -1
  72. package/dist/components/forms/__tests__/capture-test-env.d.ts +0 -70
  73. package/dist/components/forms/__tests__/capture-test-env.d.ts.map +0 -1
  74. package/dist/components/forms/__tests__/capture-test-env.js +0 -122
  75. package/dist/components/forms/__tests__/capture-touch.fixture.svelte +0 -15
  76. package/dist/components/forms/__tests__/capture-touch.fixture.svelte.d.ts +0 -8
  77. package/dist/components/forms/__tests__/capture-touch.fixture.svelte.d.ts.map +0 -1
  78. package/dist/components/forms/__tests__/combobox-form.fixture.svelte +0 -29
  79. package/dist/components/forms/__tests__/combobox-form.fixture.svelte.d.ts +0 -14
  80. package/dist/components/forms/__tests__/combobox-form.fixture.svelte.d.ts.map +0 -1
  81. package/dist/components/forms/__tests__/combobox-labels.fixture.svelte +0 -19
  82. package/dist/components/forms/__tests__/combobox-labels.fixture.svelte.d.ts +0 -11
  83. package/dist/components/forms/__tests__/combobox-labels.fixture.svelte.d.ts.map +0 -1
  84. package/dist/components/forms/__tests__/composite-controls.fixture.svelte +0 -47
  85. package/dist/components/forms/__tests__/composite-controls.fixture.svelte.d.ts +0 -12
  86. package/dist/components/forms/__tests__/composite-controls.fixture.svelte.d.ts.map +0 -1
  87. package/dist/components/forms/__tests__/composite-controls.test.js +0 -128
  88. package/dist/components/forms/__tests__/composite-registration.fixture.svelte +0 -29
  89. package/dist/components/forms/__tests__/composite-registration.fixture.svelte.d.ts +0 -8
  90. package/dist/components/forms/__tests__/composite-registration.fixture.svelte.d.ts.map +0 -1
  91. package/dist/components/forms/__tests__/composite-user-edit.fixture.svelte +0 -41
  92. package/dist/components/forms/__tests__/composite-user-edit.fixture.svelte.d.ts +0 -13
  93. package/dist/components/forms/__tests__/composite-user-edit.fixture.svelte.d.ts.map +0 -1
  94. package/dist/components/forms/__tests__/control-interaction.test.js +0 -3421
  95. package/dist/components/forms/__tests__/control-proposals.test.js +0 -63
  96. package/dist/components/forms/__tests__/core-controls.fixture.svelte +0 -50
  97. package/dist/components/forms/__tests__/core-controls.fixture.svelte.d.ts +0 -9
  98. package/dist/components/forms/__tests__/core-controls.fixture.svelte.d.ts.map +0 -1
  99. package/dist/components/forms/__tests__/core-controls.test.js +0 -311
  100. package/dist/components/forms/__tests__/decimal-sliders.fixture.svelte +0 -25
  101. package/dist/components/forms/__tests__/decimal-sliders.fixture.svelte.d.ts +0 -8
  102. package/dist/components/forms/__tests__/decimal-sliders.fixture.svelte.d.ts.map +0 -1
  103. package/dist/components/forms/__tests__/dictation-recording.test.js +0 -582
  104. package/dist/components/forms/__tests__/dictation.test.js +0 -486
  105. package/dist/components/forms/__tests__/form-attachment.fixture.svelte +0 -28
  106. package/dist/components/forms/__tests__/form-attachment.fixture.svelte.d.ts +0 -11
  107. package/dist/components/forms/__tests__/form-attachment.fixture.svelte.d.ts.map +0 -1
  108. package/dist/components/forms/__tests__/form-group-input.fixture.svelte +0 -16
  109. package/dist/components/forms/__tests__/form-group-input.fixture.svelte.d.ts +0 -9
  110. package/dist/components/forms/__tests__/form-group-input.fixture.svelte.d.ts.map +0 -1
  111. package/dist/components/forms/__tests__/form-retry-form.fixture.svelte +0 -31
  112. package/dist/components/forms/__tests__/form-retry-form.fixture.svelte.d.ts +0 -13
  113. package/dist/components/forms/__tests__/form-retry-form.fixture.svelte.d.ts.map +0 -1
  114. package/dist/components/forms/__tests__/form-retry-form.test.js +0 -107
  115. package/dist/components/forms/__tests__/hidden-controls-form-retry.test.js +0 -69
  116. package/dist/components/forms/__tests__/hidden-controls-form.fixture.svelte +0 -29
  117. package/dist/components/forms/__tests__/hidden-controls-form.fixture.svelte.d.ts +0 -9
  118. package/dist/components/forms/__tests__/hidden-controls-form.fixture.svelte.d.ts.map +0 -1
  119. package/dist/components/forms/__tests__/listbox-form.fixture.svelte +0 -27
  120. package/dist/components/forms/__tests__/listbox-form.fixture.svelte.d.ts +0 -13
  121. package/dist/components/forms/__tests__/listbox-form.fixture.svelte.d.ts.map +0 -1
  122. package/dist/components/forms/__tests__/long-press.test.js +0 -175
  123. package/dist/components/forms/__tests__/multiselect-form.fixture.svelte +0 -27
  124. package/dist/components/forms/__tests__/multiselect-form.fixture.svelte.d.ts +0 -13
  125. package/dist/components/forms/__tests__/multiselect-form.fixture.svelte.d.ts.map +0 -1
  126. package/dist/components/forms/__tests__/native-file-field.test.js +0 -177
  127. package/dist/components/forms/__tests__/place-picker.fixture.svelte +0 -33
  128. package/dist/components/forms/__tests__/place-picker.fixture.svelte.d.ts +0 -12
  129. package/dist/components/forms/__tests__/place-picker.fixture.svelte.d.ts.map +0 -1
  130. package/dist/components/forms/__tests__/segmented-posting.fixture.svelte +0 -28
  131. package/dist/components/forms/__tests__/segmented-posting.fixture.svelte.d.ts +0 -12
  132. package/dist/components/forms/__tests__/segmented-posting.fixture.svelte.d.ts.map +0 -1
  133. package/dist/components/forms/__tests__/segmented-posting.test.js +0 -92
  134. package/dist/components/forms/__tests__/select-interaction.fixture.svelte +0 -25
  135. package/dist/components/forms/__tests__/select-interaction.fixture.svelte.d.ts +0 -9
  136. package/dist/components/forms/__tests__/select-interaction.fixture.svelte.d.ts.map +0 -1
  137. package/dist/components/forms/__tests__/signature-pad-logic.test.js +0 -54
  138. package/dist/components/forms/__tests__/staged-review-fieldset.fixture.svelte +0 -21
  139. package/dist/components/forms/__tests__/staged-review-fieldset.fixture.svelte.d.ts +0 -8
  140. package/dist/components/forms/__tests__/staged-review-fieldset.fixture.svelte.d.ts.map +0 -1
  141. package/dist/components/forms/__tests__/staged-review-outer-fieldset.fixture.svelte +0 -23
  142. package/dist/components/forms/__tests__/staged-review-outer-fieldset.fixture.svelte.d.ts +0 -8
  143. package/dist/components/forms/__tests__/staged-review-outer-fieldset.fixture.svelte.d.ts.map +0 -1
  144. package/dist/components/forms/__tests__/staged-review.fixture.svelte +0 -56
  145. package/dist/components/forms/__tests__/staged-review.fixture.svelte.d.ts +0 -12
  146. package/dist/components/forms/__tests__/staged-review.fixture.svelte.d.ts.map +0 -1
  147. package/dist/components/forms/__tests__/touch-density.fixture.svelte +0 -34
  148. package/dist/components/forms/__tests__/touch-density.fixture.svelte.d.ts +0 -8
  149. package/dist/components/forms/__tests__/touch-density.fixture.svelte.d.ts.map +0 -1
  150. package/dist/components/forms/__tests__/touch-density.test.js +0 -66
  151. package/dist/components/forms/form-retry/__tests__/boundaries.test.js +0 -23
  152. package/dist/components/forms/form-retry/__tests__/content-digest.test.js +0 -70
  153. package/dist/components/forms/form-retry/__tests__/controller.test.js +0 -802
  154. package/dist/components/forms/form-retry/__tests__/fake-kit.d.ts +0 -82
  155. package/dist/components/forms/form-retry/__tests__/fake-kit.d.ts.map +0 -1
  156. package/dist/components/forms/form-retry/__tests__/fake-kit.js +0 -180
  157. package/dist/components/forms/form-retry/__tests__/form-draft.test.js +0 -169
  158. package/dist/components/forms/form-retry/__tests__/ssr.test.js +0 -37
  159. package/dist/components/forms/form-retry/__tests__/storage.d.ts +0 -26
  160. package/dist/components/forms/form-retry/__tests__/storage.d.ts.map +0 -1
  161. package/dist/components/forms/form-retry/__tests__/storage.js +0 -63
  162. package/dist/components/forms/form-retry/__tests__/submission-key.test.js +0 -236
  163. package/dist/components/forms/form-retry/__tests__/submitted-draft.test.js +0 -334
  164. package/dist/components/layout/__tests__/Container.test.js +0 -62
  165. package/dist/components/layout/__tests__/EmptyState.test.js +0 -83
  166. package/dist/components/layout/__tests__/Footer.test.js +0 -50
  167. package/dist/components/layout/__tests__/Grid.test.js +0 -121
  168. package/dist/components/layout/__tests__/Header.test.js +0 -48
  169. package/dist/components/layout/__tests__/Masthead.test.js +0 -93
  170. package/dist/components/layout/__tests__/PageHeader.test.js +0 -166
  171. package/dist/components/layout/__tests__/SummaryCard.test.js +0 -104
  172. package/dist/components/nav/__tests__/FilterChips.test.js +0 -94
  173. package/dist/components/nav/__tests__/LinkTabs.test.js +0 -117
  174. package/dist/components/nav/__tests__/Tabs.test.js +0 -128
  175. package/dist/components/roles/__tests__/RoleSelector.test.js +0 -134
  176. package/dist/components/ui/__tests__/Badge.test.js +0 -76
  177. package/dist/components/ui/__tests__/Button.test.js +0 -166
  178. package/dist/components/ui/__tests__/Card.test.js +0 -103
  179. package/dist/components/ui/__tests__/IconToggle.test.js +0 -59
  180. package/dist/components/ui/__tests__/IconToggleGroup.test.js +0 -30
  181. package/dist/components/ui/__tests__/Pagination.test.js +0 -113
  182. package/dist/components/ui/__tests__/Popover.test.js +0 -44
  183. package/dist/components/ui/__tests__/dropdown-popover-icon.test.js +0 -125
  184. package/dist/components/ui/__tests__/gap-primitives-interactive.test.js +0 -112
  185. package/dist/components/ui/__tests__/gap-primitives.test.js +0 -84
  186. package/dist/components/ui/__tests__/interaction-primitives.fixture.svelte +0 -13
  187. package/dist/components/ui/__tests__/interaction-primitives.fixture.svelte.d.ts +0 -19
  188. package/dist/components/ui/__tests__/interaction-primitives.fixture.svelte.d.ts.map +0 -1
  189. package/dist/components/ui/__tests__/interaction-primitives.test.js +0 -31
  190. package/dist/components/ui/__tests__/phone-touch-targets.test.js +0 -39
  191. package/dist/i18n/__tests__/i18n.test.js +0 -74
  192. package/dist/i18n/__tests__/render-parity.spec.js +0 -37
  193. package/dist/svelte/__tests__/playground.test.js +0 -20
  194. package/dist/themes/__tests__/control-boundary-contrast.test.js +0 -112
  195. package/dist/themes/__tests__/create-theme.test.js +0 -79
  196. package/dist/themes/__tests__/css-generator.test.js +0 -129
  197. package/dist/themes/__tests__/happyvertical-theme.test.js +0 -231
  198. package/dist/themes/__tests__/registry.test.js +0 -43
  199. package/dist/themes/__tests__/styles-lockstep.test.js +0 -32
  200. package/dist/themes/__tests__/theme-provider.test.js +0 -147
  201. package/dist/themes/__tests__/theme-script.test.js +0 -130
  202. package/dist/themes/__tests__/token-aliases.test.js +0 -178
  203. package/dist/utils/forms/__tests__/formatters.test.js +0 -27
  204. package/dist/utils/theme/__tests__/color.test.js +0 -89
  205. package/dist/utils/theme/__tests__/typography.test.js +0 -11
@@ -1,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
- });