@happyvertical/smrt-ui 0.52.0 → 0.53.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +11 -1
- package/README.md +300 -2
- package/dist/components/data/DataTable.svelte +60 -15
- package/dist/components/data/DataTable.svelte.d.ts.map +1 -1
- package/dist/components/data/DataTableResponsive.d.ts +4 -0
- package/dist/components/data/DataTableResponsive.d.ts.map +1 -0
- package/dist/components/data/DataTableResponsive.js +22 -0
- package/dist/components/data/__tests__/DataTableResponsive.test.js +80 -0
- package/dist/components/data/__tests__/responsive-table.fixture.svelte +29 -0
- package/dist/components/data/__tests__/responsive-table.fixture.svelte.d.ts +7 -0
- package/dist/components/data/__tests__/responsive-table.fixture.svelte.d.ts.map +1 -0
- package/dist/components/data/types.d.ts +8 -0
- package/dist/components/data/types.d.ts.map +1 -1
- package/dist/components/display/DateDisplay.svelte +76 -14
- package/dist/components/display/DateDisplay.svelte.d.ts +2 -0
- package/dist/components/display/DateDisplay.svelte.d.ts.map +1 -1
- package/dist/components/display/Icon.svelte +3 -16
- package/dist/components/display/Icon.svelte.d.ts +0 -9
- package/dist/components/display/Icon.svelte.d.ts.map +1 -1
- package/dist/components/display/StatusBadge.svelte +29 -2
- package/dist/components/display/StatusBadge.svelte.d.ts +4 -2
- package/dist/components/display/StatusBadge.svelte.d.ts.map +1 -1
- package/dist/components/display/__tests__/DateDisplay.test.js +82 -1
- package/dist/components/display/__tests__/Icon.test.js +132 -2
- package/dist/components/display/__tests__/StatusBadge.test.js +78 -0
- package/dist/components/display/icons.svelte.d.ts +11 -0
- package/dist/components/display/icons.svelte.d.ts.map +1 -0
- package/dist/components/display/icons.svelte.js +60 -0
- package/dist/components/display/index.d.ts +3 -1
- package/dist/components/display/index.d.ts.map +1 -1
- package/dist/components/display/index.js +1 -0
- package/dist/components/display/types.d.ts +2 -0
- package/dist/components/display/types.d.ts.map +1 -1
- package/dist/components/feedback/ConfirmDialog.svelte +59 -48
- package/dist/components/feedback/ConfirmDialog.svelte.d.ts +14 -2
- package/dist/components/feedback/ConfirmDialog.svelte.d.ts.map +1 -1
- package/dist/components/feedback/ToastViewport.svelte +156 -8
- package/dist/components/feedback/ToastViewport.svelte.d.ts +5 -1
- package/dist/components/feedback/ToastViewport.svelte.d.ts.map +1 -1
- package/dist/components/feedback/__tests__/ConfirmDialog.test.js +38 -20
- package/dist/components/feedback/__tests__/confirm-message.fixture.svelte +9 -0
- package/dist/components/feedback/__tests__/confirm-message.fixture.svelte.d.ts +19 -0
- package/dist/components/feedback/__tests__/confirm-message.fixture.svelte.d.ts.map +1 -0
- package/dist/components/feedback/__tests__/toast.test.js +142 -1
- package/dist/components/feedback/index.d.ts +1 -1
- package/dist/components/feedback/index.d.ts.map +1 -1
- package/dist/components/feedback/toast.d.ts +13 -1
- package/dist/components/feedback/toast.d.ts.map +1 -1
- package/dist/components/feedback/toast.js +51 -10
- package/dist/components/forms/CameraCapture.svelte +619 -0
- package/dist/components/forms/CameraCapture.svelte.d.ts +42 -0
- package/dist/components/forms/CameraCapture.svelte.d.ts.map +1 -0
- package/dist/components/forms/Checkbox.svelte +8 -1
- package/dist/components/forms/Checkbox.svelte.d.ts +2 -0
- package/dist/components/forms/Checkbox.svelte.d.ts.map +1 -1
- package/dist/components/forms/Combobox.svelte +40 -0
- package/dist/components/forms/Combobox.svelte.d.ts.map +1 -1
- package/dist/components/forms/Form.svelte +5 -0
- package/dist/components/forms/Form.svelte.d.ts +1 -0
- package/dist/components/forms/Form.svelte.d.ts.map +1 -1
- package/dist/components/forms/Input.svelte +8 -1
- package/dist/components/forms/Input.svelte.d.ts +2 -0
- package/dist/components/forms/Input.svelte.d.ts.map +1 -1
- package/dist/components/forms/Listbox.svelte +39 -1
- package/dist/components/forms/Listbox.svelte.d.ts.map +1 -1
- package/dist/components/forms/MultiSelect.svelte +41 -1
- package/dist/components/forms/MultiSelect.svelte.d.ts.map +1 -1
- package/dist/components/forms/Radio.svelte +1 -0
- package/dist/components/forms/RadioGroup.svelte +7 -1
- package/dist/components/forms/RadioGroup.svelte.d.ts +2 -0
- package/dist/components/forms/RadioGroup.svelte.d.ts.map +1 -1
- package/dist/components/forms/SegmentedControl.svelte +152 -11
- package/dist/components/forms/SegmentedControl.svelte.d.ts +4 -0
- package/dist/components/forms/SegmentedControl.svelte.d.ts.map +1 -1
- package/dist/components/forms/Select.svelte +8 -1
- package/dist/components/forms/Select.svelte.d.ts +2 -0
- package/dist/components/forms/Select.svelte.d.ts.map +1 -1
- package/dist/components/forms/SignaturePad.svelte +436 -0
- package/dist/components/forms/SignaturePad.svelte.d.ts +32 -0
- package/dist/components/forms/SignaturePad.svelte.d.ts.map +1 -0
- package/dist/components/forms/Switch.svelte +8 -1
- package/dist/components/forms/Switch.svelte.d.ts +2 -0
- package/dist/components/forms/Switch.svelte.d.ts.map +1 -1
- package/dist/components/forms/Textarea.svelte +8 -1
- package/dist/components/forms/Textarea.svelte.d.ts +2 -0
- package/dist/components/forms/Textarea.svelte.d.ts.map +1 -1
- package/dist/components/forms/__tests__/CameraCapture.test.js +504 -0
- package/dist/components/forms/__tests__/Combobox.test.js +279 -12
- package/dist/components/forms/__tests__/Form.test.js +46 -0
- package/dist/components/forms/__tests__/Listbox.test.js +303 -0
- package/dist/components/forms/__tests__/MultiSelect.test.js +281 -0
- package/dist/components/forms/__tests__/SignaturePad.test.js +338 -0
- package/dist/components/forms/__tests__/camera-capture-session.test.js +207 -0
- package/dist/components/forms/__tests__/capture-form-retry.test.js +166 -0
- package/dist/components/forms/__tests__/capture-form.fixture.svelte +38 -0
- package/dist/components/forms/__tests__/capture-form.fixture.svelte.d.ts +10 -0
- package/dist/components/forms/__tests__/capture-form.fixture.svelte.d.ts.map +1 -0
- package/dist/components/forms/__tests__/capture-test-env.d.ts +70 -0
- package/dist/components/forms/__tests__/capture-test-env.d.ts.map +1 -0
- package/dist/components/forms/__tests__/capture-test-env.js +122 -0
- package/dist/components/forms/__tests__/capture-touch.fixture.svelte +15 -0
- package/dist/components/forms/__tests__/capture-touch.fixture.svelte.d.ts +8 -0
- package/dist/components/forms/__tests__/capture-touch.fixture.svelte.d.ts.map +1 -0
- package/dist/components/forms/__tests__/combobox-form.fixture.svelte +29 -0
- package/dist/components/forms/__tests__/combobox-form.fixture.svelte.d.ts +14 -0
- package/dist/components/forms/__tests__/combobox-form.fixture.svelte.d.ts.map +1 -0
- package/dist/components/forms/__tests__/form-attachment.fixture.svelte +28 -0
- package/dist/components/forms/__tests__/form-attachment.fixture.svelte.d.ts +11 -0
- package/dist/components/forms/__tests__/form-attachment.fixture.svelte.d.ts.map +1 -0
- package/dist/components/forms/__tests__/form-retry-form.fixture.svelte +31 -0
- package/dist/components/forms/__tests__/form-retry-form.fixture.svelte.d.ts +13 -0
- package/dist/components/forms/__tests__/form-retry-form.fixture.svelte.d.ts.map +1 -0
- package/dist/components/forms/__tests__/form-retry-form.test.js +107 -0
- package/dist/components/forms/__tests__/hidden-controls-form-retry.test.js +69 -0
- package/dist/components/forms/__tests__/hidden-controls-form.fixture.svelte +29 -0
- package/dist/components/forms/__tests__/hidden-controls-form.fixture.svelte.d.ts +9 -0
- package/dist/components/forms/__tests__/hidden-controls-form.fixture.svelte.d.ts.map +1 -0
- package/dist/components/forms/__tests__/listbox-form.fixture.svelte +27 -0
- package/dist/components/forms/__tests__/listbox-form.fixture.svelte.d.ts +13 -0
- package/dist/components/forms/__tests__/listbox-form.fixture.svelte.d.ts.map +1 -0
- package/dist/components/forms/__tests__/multiselect-form.fixture.svelte +27 -0
- package/dist/components/forms/__tests__/multiselect-form.fixture.svelte.d.ts +13 -0
- package/dist/components/forms/__tests__/multiselect-form.fixture.svelte.d.ts.map +1 -0
- package/dist/components/forms/__tests__/native-file-field.test.js +177 -0
- package/dist/components/forms/__tests__/segmented-posting.fixture.svelte +28 -0
- package/dist/components/forms/__tests__/segmented-posting.fixture.svelte.d.ts +12 -0
- package/dist/components/forms/__tests__/segmented-posting.fixture.svelte.d.ts.map +1 -0
- package/dist/components/forms/__tests__/segmented-posting.test.js +92 -0
- package/dist/components/forms/__tests__/signature-pad-logic.test.js +54 -0
- package/dist/components/forms/__tests__/touch-density.fixture.svelte +34 -0
- package/dist/components/forms/__tests__/touch-density.fixture.svelte.d.ts +8 -0
- package/dist/components/forms/__tests__/touch-density.fixture.svelte.d.ts.map +1 -0
- package/dist/components/forms/__tests__/touch-density.test.js +66 -0
- package/dist/components/forms/camera-capture-session.d.ts +81 -0
- package/dist/components/forms/camera-capture-session.d.ts.map +1 -0
- package/dist/components/forms/camera-capture-session.js +130 -0
- package/dist/components/forms/form-retry/__tests__/boundaries.test.js +23 -0
- package/dist/components/forms/form-retry/__tests__/content-digest.test.js +70 -0
- package/dist/components/forms/form-retry/__tests__/controller.test.js +802 -0
- package/dist/components/forms/form-retry/__tests__/fake-kit.d.ts +82 -0
- package/dist/components/forms/form-retry/__tests__/fake-kit.d.ts.map +1 -0
- package/dist/components/forms/form-retry/__tests__/fake-kit.js +180 -0
- package/dist/components/forms/form-retry/__tests__/form-draft.test.js +169 -0
- package/dist/components/forms/form-retry/__tests__/ssr.test.js +37 -0
- package/dist/components/forms/form-retry/__tests__/storage.d.ts +26 -0
- package/dist/components/forms/form-retry/__tests__/storage.d.ts.map +1 -0
- package/dist/components/forms/form-retry/__tests__/storage.js +63 -0
- package/dist/components/forms/form-retry/__tests__/submission-key.test.js +236 -0
- package/dist/components/forms/form-retry/__tests__/submitted-draft.test.js +334 -0
- package/dist/components/forms/form-retry/content-digest.d.ts +54 -0
- package/dist/components/forms/form-retry/content-digest.d.ts.map +1 -0
- package/dist/components/forms/form-retry/content-digest.js +169 -0
- package/dist/components/forms/form-retry/controller.d.ts +214 -0
- package/dist/components/forms/form-retry/controller.d.ts.map +1 -0
- package/dist/components/forms/form-retry/controller.js +404 -0
- package/dist/components/forms/form-retry/form-draft.d.ts +68 -0
- package/dist/components/forms/form-retry/form-draft.d.ts.map +1 -0
- package/dist/components/forms/form-retry/form-draft.js +161 -0
- package/dist/components/forms/form-retry/index.d.ts +19 -0
- package/dist/components/forms/form-retry/index.d.ts.map +1 -0
- package/dist/components/forms/form-retry/index.js +16 -0
- package/dist/components/forms/form-retry/submission-key.d.ts +244 -0
- package/dist/components/forms/form-retry/submission-key.d.ts.map +1 -0
- package/dist/components/forms/form-retry/submission-key.js +342 -0
- package/dist/components/forms/form-retry/submitted-draft.d.ts +112 -0
- package/dist/components/forms/form-retry/submitted-draft.d.ts.map +1 -0
- package/dist/components/forms/form-retry/submitted-draft.js +270 -0
- package/dist/components/forms/form-retry/types.d.ts +84 -0
- package/dist/components/forms/form-retry/types.d.ts.map +1 -0
- package/dist/components/forms/form-retry/types.js +13 -0
- package/dist/components/forms/index.d.ts +7 -1
- package/dist/components/forms/index.d.ts.map +1 -1
- package/dist/components/forms/index.js +8 -0
- package/dist/components/forms/native-file-field.d.ts +94 -0
- package/dist/components/forms/native-file-field.d.ts.map +1 -0
- package/dist/components/forms/native-file-field.js +147 -0
- package/dist/components/forms/signature-pad-logic.d.ts +50 -0
- package/dist/components/forms/signature-pad-logic.d.ts.map +1 -0
- package/dist/components/forms/signature-pad-logic.js +48 -0
- package/dist/components/forms/types.d.ts +41 -0
- package/dist/components/forms/types.d.ts.map +1 -1
- package/dist/components/nav/FilterChips.svelte +8 -1
- package/dist/components/nav/FilterChips.svelte.d.ts +2 -0
- package/dist/components/nav/FilterChips.svelte.d.ts.map +1 -1
- package/dist/components/ui/Button.svelte +9 -2
- package/dist/components/ui/Button.svelte.d.ts +2 -0
- package/dist/components/ui/Button.svelte.d.ts.map +1 -1
- package/dist/i18n/strings.ui.d.ts +24 -0
- package/dist/i18n/strings.ui.d.ts.map +1 -1
- package/dist/i18n/strings.ui.js +26 -0
- package/dist/styles/tokens.css +2 -0
- package/dist/svelte/__tests__/playground.test.js +2 -1
- package/dist/svelte/playground/CapturePreview.svelte +56 -0
- package/dist/svelte/playground/CapturePreview.svelte.d.ts +4 -0
- package/dist/svelte/playground/CapturePreview.svelte.d.ts.map +1 -0
- package/dist/svelte/playground.d.ts.map +1 -1
- package/dist/svelte/playground.js +10 -0
- package/dist/themes/ThemeProvider.svelte +9 -0
- package/dist/themes/ThemeProvider.svelte.d.ts +2 -0
- package/dist/themes/ThemeProvider.svelte.d.ts.map +1 -1
- package/dist/themes/css-generator.d.ts.map +1 -1
- package/dist/themes/css-generator.js +3 -0
- package/dist/themes/styles/glass.css +4 -0
- package/dist/themes/styles/happyvertical.css +4 -0
- package/dist/themes/styles/material.css +4 -0
- package/dist/themes/styles/smrt.css +4 -0
- package/dist/themes/styles/studio.css +4 -0
- package/package.json +8 -2
|
@@ -1,8 +1,98 @@
|
|
|
1
|
-
import { render, screen } from '@testing-library/svelte';
|
|
1
|
+
import { fireEvent, render, screen } from '@testing-library/svelte';
|
|
2
2
|
import userEvent from '@testing-library/user-event';
|
|
3
3
|
import { tick } from 'svelte';
|
|
4
|
-
import { describe, expect, it } from 'vitest';
|
|
5
|
-
import
|
|
4
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
5
|
+
import Combobox from '../Combobox.svelte';
|
|
6
|
+
import { createControlInteractionRegistry, executeLocalControlCommand, } from '../control-interaction.js';
|
|
7
|
+
import FormFixture from './combobox-form.fixture.svelte';
|
|
8
|
+
import LabelsFixture from './combobox-labels.fixture.svelte';
|
|
9
|
+
function data() {
|
|
10
|
+
return new FormData(screen.getByRole('form', { name: 'Order' }));
|
|
11
|
+
}
|
|
12
|
+
async function selectPlate() {
|
|
13
|
+
await userEvent.click(screen.getByRole('combobox', { name: 'SKU' }));
|
|
14
|
+
await userEvent.click(screen.getByRole('option', { name: 'Plate 1/4 A36' }));
|
|
15
|
+
}
|
|
16
|
+
describe('Combobox native form value', () => {
|
|
17
|
+
it('posts the selected value once, retaining it while searching', async () => {
|
|
18
|
+
render(FormFixture);
|
|
19
|
+
await selectPlate();
|
|
20
|
+
expect(data().getAll('sku')).toEqual(['42']);
|
|
21
|
+
const input = screen.getByRole('combobox', { name: 'SKU' });
|
|
22
|
+
expect(input).not.toHaveAttribute('name');
|
|
23
|
+
await userEvent.clear(input);
|
|
24
|
+
await userEvent.type(input, 'unselected search');
|
|
25
|
+
expect(data().getAll('sku')).toEqual(['42']);
|
|
26
|
+
});
|
|
27
|
+
it('posts keyboard-selected and custom values', async () => {
|
|
28
|
+
render(FormFixture, { props: { allowCustom: true } });
|
|
29
|
+
const input = screen.getByRole('combobox', { name: 'SKU' });
|
|
30
|
+
await userEvent.type(input, 'Plate');
|
|
31
|
+
await userEvent.keyboard('{Enter}');
|
|
32
|
+
expect(data().getAll('sku')).toEqual(['42']);
|
|
33
|
+
await userEvent.clear(input);
|
|
34
|
+
await userEvent.type(input, 'custom');
|
|
35
|
+
expect(data().getAll('sku')).toEqual(['custom']);
|
|
36
|
+
});
|
|
37
|
+
it('posts an empty named value and omits missing or empty names', async () => {
|
|
38
|
+
const { rerender } = render(FormFixture);
|
|
39
|
+
expect(data().getAll('sku')).toEqual(['']);
|
|
40
|
+
await rerender({ name: '' });
|
|
41
|
+
expect([...data()]).toEqual([]);
|
|
42
|
+
await rerender({ unnamed: true });
|
|
43
|
+
expect([...data()]).toEqual([]);
|
|
44
|
+
});
|
|
45
|
+
it.each([
|
|
46
|
+
{ disabled: true },
|
|
47
|
+
{ fieldsetDisabled: true },
|
|
48
|
+
])('omits disabled values: %j', (props) => {
|
|
49
|
+
render(FormFixture, { props: { ...props, value: '42' } });
|
|
50
|
+
expect([...data()]).toEqual([]);
|
|
51
|
+
});
|
|
52
|
+
it('does not select disabled options', async () => {
|
|
53
|
+
render(FormFixture);
|
|
54
|
+
await userEvent.click(screen.getByRole('combobox', { name: 'SKU' }));
|
|
55
|
+
await userEvent.click(screen.getByRole('option', { name: 'Disabled plate' }));
|
|
56
|
+
expect(data().getAll('sku')).toEqual(['']);
|
|
57
|
+
});
|
|
58
|
+
it.each([
|
|
59
|
+
'',
|
|
60
|
+
'42',
|
|
61
|
+
])('restores initial selection %j on native reset', async (value) => {
|
|
62
|
+
render(FormFixture, { props: { value, allowCustom: true } });
|
|
63
|
+
const input = screen.getByRole('combobox', { name: 'SKU' });
|
|
64
|
+
await userEvent.clear(input);
|
|
65
|
+
await userEvent.type(input, 'changed');
|
|
66
|
+
screen.getByRole('form', { name: 'Order' }).reset();
|
|
67
|
+
await settleReset();
|
|
68
|
+
expect(data().getAll('sku')).toEqual([value]);
|
|
69
|
+
expect(input).toHaveValue(value ? 'Plate 1/4 A36' : '');
|
|
70
|
+
});
|
|
71
|
+
it('preserves selection when reset is canceled', async () => {
|
|
72
|
+
render(FormFixture);
|
|
73
|
+
await selectPlate();
|
|
74
|
+
const form = screen.getByRole('form', { name: 'Order' });
|
|
75
|
+
form.addEventListener('reset', (event) => event.preventDefault());
|
|
76
|
+
await fireEvent.reset(form);
|
|
77
|
+
await settleReset();
|
|
78
|
+
expect(data().getAll('sku')).toEqual(['42']);
|
|
79
|
+
});
|
|
80
|
+
it('posts registry-applied values while preserving identity and staged isolation', async () => {
|
|
81
|
+
const registry = createControlInteractionRegistry({
|
|
82
|
+
isLocalGesture: () => true,
|
|
83
|
+
});
|
|
84
|
+
render(FormFixture, { props: { registry } });
|
|
85
|
+
await registry.execute({
|
|
86
|
+
action: 'stage',
|
|
87
|
+
identity: { formId: 'order', controlId: 'sku' },
|
|
88
|
+
value: '42',
|
|
89
|
+
}, { source: 'agent' });
|
|
90
|
+
expect(data().getAll('sku')).toEqual(['']);
|
|
91
|
+
await userEvent.click(screen.getByRole('button', { name: 'Apply valid changes' }));
|
|
92
|
+
expect(data().getAll('sku')).toEqual(['42']);
|
|
93
|
+
expect(registry.get({ formId: 'order', controlId: 'sku' })?.state.value).toBe('42');
|
|
94
|
+
});
|
|
95
|
+
});
|
|
6
96
|
const towns = [
|
|
7
97
|
{ value: '0b9c-uuid-lacombe', label: 'Lacombe' },
|
|
8
98
|
{ value: '77aa-uuid-blackfalds', label: 'Blackfalds' },
|
|
@@ -10,13 +100,13 @@ const towns = [
|
|
|
10
100
|
];
|
|
11
101
|
describe('Combobox labels and reopening', () => {
|
|
12
102
|
it('shows the option label on first render, never the raw id', () => {
|
|
13
|
-
render(
|
|
103
|
+
render(LabelsFixture, {
|
|
14
104
|
props: { options: towns, value: '77aa-uuid-blackfalds' },
|
|
15
105
|
});
|
|
16
106
|
expect(screen.getByRole('combobox', { name: 'Town' })).toHaveValue('Blackfalds');
|
|
17
107
|
});
|
|
18
108
|
it('shows valueLabel (or nothing) until async options arrive, then the label', async () => {
|
|
19
|
-
const view = render(
|
|
109
|
+
const view = render(LabelsFixture, {
|
|
20
110
|
props: { options: [], value: '12cd-uuid-ponoka', valueLabel: 'Ponoka' },
|
|
21
111
|
});
|
|
22
112
|
const input = screen.getByRole('combobox', { name: 'Town' });
|
|
@@ -26,15 +116,15 @@ describe('Combobox labels and reopening', () => {
|
|
|
26
116
|
value: '12cd-uuid-ponoka',
|
|
27
117
|
valueLabel: undefined,
|
|
28
118
|
});
|
|
29
|
-
await
|
|
119
|
+
await settleReset();
|
|
30
120
|
expect(input).toHaveValue('');
|
|
31
121
|
expect(input).not.toHaveValue('12cd-uuid-ponoka');
|
|
32
122
|
await view.rerender({ options: towns, value: '12cd-uuid-ponoka' });
|
|
33
|
-
await
|
|
123
|
+
await settleReset();
|
|
34
124
|
expect(input).toHaveValue('Ponoka');
|
|
35
125
|
});
|
|
36
126
|
it('reopening after a choice lists every option, not only the current one', async () => {
|
|
37
|
-
render(
|
|
127
|
+
render(LabelsFixture, { props: { options: towns, value: '' } });
|
|
38
128
|
const input = screen.getByRole('combobox', { name: 'Town' });
|
|
39
129
|
await userEvent.type(input, 'lacom');
|
|
40
130
|
expect(screen.getAllByRole('option')).toHaveLength(1);
|
|
@@ -47,7 +137,7 @@ describe('Combobox labels and reopening', () => {
|
|
|
47
137
|
expect(screen.getByRole('option', { name: 'Lacombe' })).toHaveAttribute('aria-selected', 'true');
|
|
48
138
|
});
|
|
49
139
|
it('puts the chosen label back when the person leaves without choosing', async () => {
|
|
50
|
-
render(
|
|
140
|
+
render(LabelsFixture, {
|
|
51
141
|
props: { options: towns, value: '77aa-uuid-blackfalds' },
|
|
52
142
|
});
|
|
53
143
|
const input = screen.getByRole('combobox', { name: 'Town' });
|
|
@@ -58,7 +148,7 @@ describe('Combobox labels and reopening', () => {
|
|
|
58
148
|
expect(screen.getByTestId('value')).toHaveTextContent('77aa-uuid-blackfalds');
|
|
59
149
|
});
|
|
60
150
|
it('posts the committed id under its name, not the visible label', async () => {
|
|
61
|
-
const { container } = render(
|
|
151
|
+
const { container } = render(LabelsFixture, {
|
|
62
152
|
props: { options: towns, value: '77aa-uuid-blackfalds' },
|
|
63
153
|
});
|
|
64
154
|
const input = screen.getByRole('combobox', { name: 'Town' });
|
|
@@ -71,7 +161,7 @@ describe('Combobox labels and reopening', () => {
|
|
|
71
161
|
expect(container.querySelector('input[name="town"]')).toHaveValue('12cd-uuid-ponoka');
|
|
72
162
|
});
|
|
73
163
|
it('a required combobox is valid while its value is committed, even before its option loads', async () => {
|
|
74
|
-
const view = render(
|
|
164
|
+
const view = render(LabelsFixture, {
|
|
75
165
|
props: { options: [], value: '12cd-uuid-ponoka', required: true },
|
|
76
166
|
});
|
|
77
167
|
const input = screen.getByRole('combobox', {
|
|
@@ -81,7 +171,184 @@ describe('Combobox labels and reopening', () => {
|
|
|
81
171
|
expect(input.checkValidity()).toBe(true);
|
|
82
172
|
expect(input).toHaveAttribute('aria-required', 'true');
|
|
83
173
|
await view.rerender({ options: towns, value: '', required: true });
|
|
84
|
-
await
|
|
174
|
+
await settleReset();
|
|
85
175
|
expect(input.checkValidity()).toBe(false);
|
|
86
176
|
});
|
|
87
177
|
});
|
|
178
|
+
async function settleReset() {
|
|
179
|
+
await new Promise((resolve) => setTimeout(resolve));
|
|
180
|
+
await tick();
|
|
181
|
+
}
|
|
182
|
+
describe('Combobox deferred native reset', () => {
|
|
183
|
+
it('preserves a newer programmatic value before reset completion', async () => {
|
|
184
|
+
const target = document.createElement('form');
|
|
185
|
+
document.body.append(target);
|
|
186
|
+
const view = render(Combobox, {
|
|
187
|
+
target,
|
|
188
|
+
props: {
|
|
189
|
+
options: [
|
|
190
|
+
{ value: 42, label: 'Initial' },
|
|
191
|
+
{ value: 'other', label: 'Other' },
|
|
192
|
+
{ value: 'changed', label: 'Changed' },
|
|
193
|
+
],
|
|
194
|
+
label: 'Choice',
|
|
195
|
+
name: 'choice',
|
|
196
|
+
value: '42',
|
|
197
|
+
},
|
|
198
|
+
});
|
|
199
|
+
await view.rerender({ value: 'changed' });
|
|
200
|
+
target.reset();
|
|
201
|
+
await view.rerender({ value: 'other' });
|
|
202
|
+
await settleReset();
|
|
203
|
+
expect(new FormData(target).getAll('choice')).toEqual(['other']);
|
|
204
|
+
await view.unmount();
|
|
205
|
+
target.remove();
|
|
206
|
+
});
|
|
207
|
+
it('cancels pending completion when unmounted', async () => {
|
|
208
|
+
const target = document.createElement('form');
|
|
209
|
+
document.body.append(target);
|
|
210
|
+
const onChange = vi.fn();
|
|
211
|
+
const view = render(Combobox, {
|
|
212
|
+
target,
|
|
213
|
+
props: {
|
|
214
|
+
options: [
|
|
215
|
+
{ value: 42, label: 'Initial' },
|
|
216
|
+
{ value: 'other', label: 'Other' },
|
|
217
|
+
{ value: 'changed', label: 'Changed' },
|
|
218
|
+
],
|
|
219
|
+
label: 'Choice',
|
|
220
|
+
name: 'choice',
|
|
221
|
+
value: '42',
|
|
222
|
+
onvaluechange: onChange,
|
|
223
|
+
},
|
|
224
|
+
});
|
|
225
|
+
target.reset();
|
|
226
|
+
await view.unmount();
|
|
227
|
+
await settleReset();
|
|
228
|
+
expect(onChange).not.toHaveBeenCalled();
|
|
229
|
+
target.remove();
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
describe('Combobox reset listener ordering', () => {
|
|
233
|
+
it('honors cancellation from a later listener microtask', async () => {
|
|
234
|
+
const target = document.createElement('form');
|
|
235
|
+
document.body.append(target);
|
|
236
|
+
const onChange = vi.fn();
|
|
237
|
+
const view = render(Combobox, {
|
|
238
|
+
target,
|
|
239
|
+
props: {
|
|
240
|
+
options: [
|
|
241
|
+
{ value: 42, label: 'Initial' },
|
|
242
|
+
{ value: 'changed', label: 'Changed' },
|
|
243
|
+
],
|
|
244
|
+
label: 'Choice',
|
|
245
|
+
name: 'choice',
|
|
246
|
+
value: '42',
|
|
247
|
+
onvaluechange: onChange,
|
|
248
|
+
},
|
|
249
|
+
});
|
|
250
|
+
await view.rerender({ value: 'changed' });
|
|
251
|
+
target.addEventListener('reset', (event) => {
|
|
252
|
+
queueMicrotask(() => event.preventDefault());
|
|
253
|
+
});
|
|
254
|
+
target.reset();
|
|
255
|
+
await settleReset();
|
|
256
|
+
expect(new FormData(target).getAll('choice')).toEqual(['changed']);
|
|
257
|
+
expect(onChange).not.toHaveBeenCalled();
|
|
258
|
+
await view.unmount();
|
|
259
|
+
target.remove();
|
|
260
|
+
});
|
|
261
|
+
});
|
|
262
|
+
function dispatchLocalCommand(registry, command) {
|
|
263
|
+
const target = new EventTarget();
|
|
264
|
+
let pending;
|
|
265
|
+
target.addEventListener('click', (event) => {
|
|
266
|
+
pending = executeLocalControlCommand(registry, command, event);
|
|
267
|
+
}, { once: true });
|
|
268
|
+
target.dispatchEvent(new Event('click'));
|
|
269
|
+
if (!pending)
|
|
270
|
+
throw new Error('local command handler did not run');
|
|
271
|
+
return pending;
|
|
272
|
+
}
|
|
273
|
+
describe('Combobox reset registry currency', () => {
|
|
274
|
+
it.each([
|
|
275
|
+
false,
|
|
276
|
+
true,
|
|
277
|
+
])('keeps staged proposal currency correct when cancellation is %s', async (cancel) => {
|
|
278
|
+
const registry = createControlInteractionRegistry({
|
|
279
|
+
isLocalGesture: () => true,
|
|
280
|
+
});
|
|
281
|
+
render(FormFixture, {
|
|
282
|
+
props: { ...{ value: '42', allowCustom: true }, registry },
|
|
283
|
+
});
|
|
284
|
+
const identity = { formId: 'order', controlId: 'sku' };
|
|
285
|
+
const target = screen.getByRole('form', {
|
|
286
|
+
name: 'Order',
|
|
287
|
+
});
|
|
288
|
+
await userEvent.clear(screen.getByRole('combobox', { name: 'SKU' }));
|
|
289
|
+
await userEvent.type(screen.getByRole('combobox', { name: 'SKU' }), 'custom');
|
|
290
|
+
expect((await registry.execute({ action: 'stage', identity, value: '42' }, { source: 'agent' })).ok).toBe(true);
|
|
291
|
+
if (cancel)
|
|
292
|
+
target.addEventListener('reset', (event) => event.preventDefault());
|
|
293
|
+
target.reset();
|
|
294
|
+
await settleReset();
|
|
295
|
+
expect(registry.get(identity)?.state.staged?.stale).toBe(!cancel);
|
|
296
|
+
const applied = await dispatchLocalCommand(registry, {
|
|
297
|
+
action: 'apply',
|
|
298
|
+
identity,
|
|
299
|
+
});
|
|
300
|
+
expect(applied.ok).toBe(cancel);
|
|
301
|
+
if (!cancel) {
|
|
302
|
+
expect(applied.reason).toBe('staged_value_stale');
|
|
303
|
+
expect(new FormData(target).getAll('sku')).toEqual(['42']);
|
|
304
|
+
}
|
|
305
|
+
});
|
|
306
|
+
it.each([
|
|
307
|
+
false,
|
|
308
|
+
true,
|
|
309
|
+
])('preserves reset over an async local clear when cancellation is %s', async (cancel) => {
|
|
310
|
+
let releasePolicy;
|
|
311
|
+
let policyStarted;
|
|
312
|
+
const blocked = new Promise((resolve) => {
|
|
313
|
+
releasePolicy = resolve;
|
|
314
|
+
});
|
|
315
|
+
const started = new Promise((resolve) => {
|
|
316
|
+
policyStarted = resolve;
|
|
317
|
+
});
|
|
318
|
+
const registry = createControlInteractionRegistry({
|
|
319
|
+
isLocalGesture: () => true,
|
|
320
|
+
policy: async (command) => {
|
|
321
|
+
if (command.action === 'clear') {
|
|
322
|
+
policyStarted();
|
|
323
|
+
await blocked;
|
|
324
|
+
}
|
|
325
|
+
return { allowed: true };
|
|
326
|
+
},
|
|
327
|
+
});
|
|
328
|
+
render(FormFixture, {
|
|
329
|
+
props: { ...{ value: '42', allowCustom: true }, registry },
|
|
330
|
+
});
|
|
331
|
+
const identity = { formId: 'order', controlId: 'sku' };
|
|
332
|
+
const target = screen.getByRole('form', {
|
|
333
|
+
name: 'Order',
|
|
334
|
+
});
|
|
335
|
+
await userEvent.clear(screen.getByRole('combobox', { name: 'SKU' }));
|
|
336
|
+
await userEvent.type(screen.getByRole('combobox', { name: 'SKU' }), 'custom');
|
|
337
|
+
const clearing = dispatchLocalCommand(registry, {
|
|
338
|
+
action: 'clear',
|
|
339
|
+
identity,
|
|
340
|
+
});
|
|
341
|
+
await started;
|
|
342
|
+
if (cancel)
|
|
343
|
+
target.addEventListener('reset', (event) => event.preventDefault());
|
|
344
|
+
target.reset();
|
|
345
|
+
await settleReset();
|
|
346
|
+
releasePolicy();
|
|
347
|
+
const result = await clearing;
|
|
348
|
+
await tick();
|
|
349
|
+
expect(result.ok).toBe(cancel);
|
|
350
|
+
if (!cancel)
|
|
351
|
+
expect(result.reason).toBe('staged_value_stale');
|
|
352
|
+
expect(new FormData(target).getAll('sku')).toEqual(cancel ? [''] : ['42']);
|
|
353
|
+
});
|
|
354
|
+
});
|
|
@@ -12,6 +12,7 @@ import { describe, expect, it, vi } from 'vitest';
|
|
|
12
12
|
import { expectNoA11yViolations } from '../../../test-support/a11y';
|
|
13
13
|
import { createControlInteractionRegistry, executeLocalControlCommand, } from '../control-interaction.js';
|
|
14
14
|
import Form from '../Form.svelte';
|
|
15
|
+
import FormAttachmentFixture from './form-attachment.fixture.svelte';
|
|
15
16
|
const children = createRawSnippet(() => ({
|
|
16
17
|
render: () => '<button type="submit">Save</button>',
|
|
17
18
|
}));
|
|
@@ -41,6 +42,51 @@ function dispatchLocalGesture(execute) {
|
|
|
41
42
|
return pending;
|
|
42
43
|
}
|
|
43
44
|
describe('Form', () => {
|
|
45
|
+
it('exposes its native element while mounted and clears it on unmount', async () => {
|
|
46
|
+
const { component, container, unmount } = render(Form, {
|
|
47
|
+
props: { children },
|
|
48
|
+
});
|
|
49
|
+
expect(component.getFormElement()).toBe(container.querySelector('form'));
|
|
50
|
+
await unmount();
|
|
51
|
+
expect(component.getFormElement()).toBeNull();
|
|
52
|
+
});
|
|
53
|
+
it('forwards an action attachment and preserves form posting and control registration', async () => {
|
|
54
|
+
const registry = createControlInteractionRegistry();
|
|
55
|
+
const attached = vi.fn();
|
|
56
|
+
const destroyed = vi.fn();
|
|
57
|
+
const submitted = vi.fn();
|
|
58
|
+
const onsubmit = vi.fn();
|
|
59
|
+
const enhance = (form) => {
|
|
60
|
+
attached(form);
|
|
61
|
+
const submit = (event) => {
|
|
62
|
+
submitted(event.defaultPrevented, new FormData(form));
|
|
63
|
+
event.preventDefault();
|
|
64
|
+
};
|
|
65
|
+
form.addEventListener('submit', submit);
|
|
66
|
+
return {
|
|
67
|
+
destroy() {
|
|
68
|
+
form.removeEventListener('submit', submit);
|
|
69
|
+
destroyed();
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
};
|
|
73
|
+
const { container, unmount } = render(FormAttachmentFixture, {
|
|
74
|
+
props: { enhance, registry, onsubmit },
|
|
75
|
+
});
|
|
76
|
+
const form = container.querySelector('form');
|
|
77
|
+
expect(attached).toHaveBeenCalledExactlyOnceWith(form);
|
|
78
|
+
expect(form).toHaveAttribute('method', 'POST');
|
|
79
|
+
expect(form).toHaveAttribute('action', '/profile');
|
|
80
|
+
expect(registry.list()).toHaveLength(1);
|
|
81
|
+
await userEvent.click(screen.getByRole('button', { name: 'Save' }));
|
|
82
|
+
expect(submitted).toHaveBeenCalledOnce();
|
|
83
|
+
expect(submitted.mock.calls[0][0]).toBe(false);
|
|
84
|
+
expect(submitted.mock.calls[0][1].get('displayName')).toBe('Dana');
|
|
85
|
+
expect(onsubmit).toHaveBeenCalledOnce();
|
|
86
|
+
await unmount();
|
|
87
|
+
expect(destroyed).toHaveBeenCalledOnce();
|
|
88
|
+
expect(registry.list()).toHaveLength(0);
|
|
89
|
+
});
|
|
44
90
|
it('renders a <form> with forwarded attributes and its children', () => {
|
|
45
91
|
const { container } = render(Form, {
|
|
46
92
|
props: { name: 'profile', class: 'profile-form', children },
|