@happyvertical/smrt-ui 0.53.1 → 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,702 +0,0 @@
1
- import { render, screen, waitFor, within } from '@testing-library/svelte';
2
- import userEvent from '@testing-library/user-event';
3
- import { describe, expect, it, vi } from 'vitest';
4
- import { expectNoA11yViolations } from '../../../test-support/a11y';
5
- import { createControlInteractionRegistry, executeLocalControlBatch, executeLocalControlCommand, } from '../control-interaction.js';
6
- import StagedControlReview from '../StagedControlReview.svelte';
7
- import CompositeUserEditFixture from './composite-user-edit.fixture.svelte';
8
- import SelectFixture from './select-interaction.fixture.svelte';
9
- import Fixture from './staged-review.fixture.svelte';
10
- import FieldsetFixture from './staged-review-fieldset.fixture.svelte';
11
- import OuterFieldsetFixture from './staged-review-outer-fieldset.fixture.svelte';
12
- const identity = { formId: 'profile', controlId: 'display-name' };
13
- const createReviewRegistry = () => createControlInteractionRegistry({ isLocalGesture: () => true });
14
- function dispatchLocalGesture(execute) {
15
- const target = new EventTarget();
16
- let pending;
17
- target.addEventListener('click', (event) => {
18
- pending = execute(event);
19
- }, { once: true });
20
- target.dispatchEvent(new Event('click'));
21
- if (!pending)
22
- throw new Error('local gesture handler did not run');
23
- return pending;
24
- }
25
- describe('StagedControlReview', () => {
26
- it('keeps a click-driven composite edit made during a failed async apply', async () => {
27
- const registry = createReviewRegistry();
28
- const onchange = vi.fn();
29
- const modeState = {};
30
- render(CompositeUserEditFixture, {
31
- props: { registry, onchange, modeState },
32
- });
33
- const identity = registry
34
- .list('profile')
35
- .find((snapshot) => snapshot.identity.controlId === 'mode')?.identity;
36
- if (!identity)
37
- throw new Error('mode was not registered');
38
- const value = () => modeState.get?.();
39
- const setValue = (next) => modeState.set?.(String(next));
40
- let releaseSetter;
41
- let setterStarted;
42
- const setterBlocked = new Promise((resolve) => {
43
- releaseSetter = resolve;
44
- });
45
- const setterStartedPromise = new Promise((resolve) => {
46
- setterStarted = resolve;
47
- });
48
- registry.register({
49
- identity,
50
- metadata: { kind: 'segmented-control' },
51
- getValue: value,
52
- setValue: async (next) => {
53
- setValue(next);
54
- setterStarted?.();
55
- await setterBlocked;
56
- throw new Error('setter_failed');
57
- },
58
- restoreValue: setValue,
59
- });
60
- await registry.execute({ action: 'stage', identity, value: 'review' }, { source: 'agent' });
61
- const applying = dispatchLocalGesture((event) => executeLocalControlCommand(registry, { action: 'apply', identity, revision: 1 }, event));
62
- await setterStartedPromise;
63
- await userEvent.click(screen.getByRole('radio', { name: 'Published' }));
64
- await waitFor(() => expect(onchange).toHaveBeenCalled());
65
- releaseSetter?.();
66
- expect(await applying).toMatchObject({
67
- ok: false,
68
- reason: 'setter_failed',
69
- });
70
- expect(value()).toBe('published');
71
- });
72
- it('shows an adjacent indicator and applies an edited proposal only after a human click', async () => {
73
- const registry = createReviewRegistry();
74
- const { container } = render(Fixture, { props: { registry } });
75
- await registry.execute({ action: 'stage', identity, value: 'Grace' }, { source: 'agent', actorId: 'assistant' });
76
- const field = screen.getByRole('textbox', { name: 'Display name' });
77
- expect(field).toHaveValue('Ada');
78
- await waitFor(() => expect(field).toHaveAttribute('data-smrt-staged', 'true'));
79
- expect(screen.getByRole('region', { name: 'Review proposed changes' })).toBeInTheDocument();
80
- expect(screen.getByText(/Proposed by assistant/)).toBeInTheDocument();
81
- // Plain field labels only: registry addressing never shows (#forms-qa).
82
- expect(screen.queryByText(/profile\/display-name/)).not.toBeInTheDocument();
83
- const proposal = screen.getByRole('textbox', {
84
- name: 'Edit proposed value for Display name',
85
- });
86
- await userEvent.clear(proposal);
87
- await userEvent.type(proposal, 'Grace Hopper');
88
- expect(field).toHaveValue('Ada');
89
- await userEvent.click(screen.getByRole('button', { name: 'Apply Display name' }));
90
- expect(field).toHaveValue('Grace Hopper');
91
- await waitFor(() => expect(screen.queryByRole('region', { name: 'Review proposed changes' })).not.toBeInTheDocument());
92
- expect(screen.getByText('Applied proposed change.', {
93
- selector: 'p[role="status"]',
94
- })).toHaveTextContent('Applied proposed change.');
95
- expect(field).toHaveFocus();
96
- await expectNoA11yViolations(container);
97
- });
98
- it('shows plain field labels, never registry ids, for unlabeled controls', async () => {
99
- const registry = createReviewRegistry();
100
- let town = '';
101
- registry.register({
102
- identity: {
103
- formId: 'setup-network',
104
- controlId: 'setup-network/town_name',
105
- subject: { type: 'site', id: 'site-1', label: 'Lacombe News' },
106
- },
107
- metadata: { kind: 'text' },
108
- getValue: () => town,
109
- setValue: (next) => {
110
- town = String(next);
111
- },
112
- });
113
- render(StagedControlReview, {
114
- props: { registry, formId: 'setup-network' },
115
- });
116
- await registry.execute({
117
- action: 'stage',
118
- identity: {
119
- formId: 'setup-network',
120
- controlId: 'setup-network/town_name',
121
- subject: { type: 'site', id: 'site-1' },
122
- },
123
- value: 'Lacombe',
124
- }, { source: 'agent' });
125
- await waitFor(() => expect(screen.getByText('Town name')).toBeVisible());
126
- expect(screen.getByText('Lacombe News')).toBeVisible();
127
- expect(screen.queryByText(/setup-network/)).not.toBeInTheDocument();
128
- expect(screen.queryByText(/site-1/)).not.toBeInTheDocument();
129
- });
130
- it('keeps Enter in the proposal editor from submitting the form', async () => {
131
- const registry = createReviewRegistry();
132
- render(Fixture, { props: { registry } });
133
- await registry.execute({ action: 'stage', identity, value: 'Grace' }, { source: 'agent' });
134
- const proposal = await screen.findByRole('textbox', {
135
- name: 'Edit proposed value for Display name',
136
- });
137
- const submitted = vi.fn((event) => event.preventDefault());
138
- screen
139
- .getByRole('form', { name: 'Profile form' })
140
- .addEventListener('submit', submitted);
141
- await userEvent.type(proposal, '{Enter}');
142
- expect(screen.getByRole('textbox', { name: 'Display name' })).toHaveValue('Ada');
143
- expect(registry.get(identity)?.state.staged?.value).toBe('Grace');
144
- expect(submitted).not.toHaveBeenCalled();
145
- });
146
- it('disables apply and restores it when an ancestor fieldset changes state in a base Form', async () => {
147
- const registry = createReviewRegistry();
148
- const { container } = render(FieldsetFixture, { props: { registry } });
149
- await registry.execute({ action: 'stage', identity, value: 'Grace' }, { source: 'agent' });
150
- const fieldset = container.querySelector('fieldset');
151
- if (!(fieldset instanceof HTMLFieldSetElement)) {
152
- throw new Error('Expected form fieldset');
153
- }
154
- fieldset.disabled = true;
155
- await waitFor(() => expect(screen.getByRole('button', { name: 'Apply Display name' })).toBeDisabled());
156
- expect(screen.getByRole('button', { name: 'Discard Display name' })).not.toBeDisabled();
157
- fieldset.disabled = false;
158
- await waitFor(() => expect(screen.getByRole('button', { name: 'Apply Display name' })).not.toBeDisabled());
159
- });
160
- it('refreshes staged state when a fieldset wrapping the form changes state', async () => {
161
- const registry = createReviewRegistry();
162
- const { container } = render(OuterFieldsetFixture, {
163
- props: { registry },
164
- });
165
- await registry.execute({ action: 'stage', identity, value: 'Grace' }, { source: 'agent' });
166
- const fieldset = container.querySelector('fieldset');
167
- if (!(fieldset instanceof HTMLFieldSetElement)) {
168
- throw new Error('Expected outer form fieldset');
169
- }
170
- fieldset.disabled = true;
171
- await waitFor(() => expect(registry.get(identity)?.state.disabled).toBe(true));
172
- expect(screen.getByRole('button', { name: 'Apply Display name' })).toBeDisabled();
173
- fieldset.disabled = false;
174
- await waitFor(() => expect(registry.get(identity)?.state.disabled).toBe(false));
175
- expect(screen.getByRole('button', { name: 'Apply Display name' })).not.toBeDisabled();
176
- });
177
- it('resets an edited draft when a replacement proposal has a new revision', async () => {
178
- const registry = createReviewRegistry();
179
- render(Fixture, { props: { registry } });
180
- await registry.execute({ action: 'stage', identity, value: 'Grace' }, { source: 'agent' });
181
- const proposal = await screen.findByRole('textbox', {
182
- name: 'Edit proposed value for Display name',
183
- });
184
- await userEvent.clear(proposal);
185
- await userEvent.type(proposal, 'Katherine');
186
- await registry.execute({ action: 'stage', identity, value: 'Hopper' }, { source: 'agent' });
187
- expect(proposal).toHaveValue('Hopper');
188
- await userEvent.click(screen.getByRole('button', { name: 'Apply Display name' }));
189
- expect(screen.getByRole('textbox', { name: 'Display name' })).toHaveValue('Hopper');
190
- });
191
- it('moves focus to the next proposal after applying one', async () => {
192
- const registry = createReviewRegistry();
193
- render(Fixture, { props: { registry } });
194
- let secondValue = 'Ada';
195
- const secondIdentity = { formId: 'profile', controlId: 'family-name' };
196
- registry.register({
197
- identity: secondIdentity,
198
- metadata: { kind: 'text', label: 'Family name' },
199
- getValue: () => secondValue,
200
- setValue: (next) => {
201
- secondValue = String(next);
202
- },
203
- });
204
- await registry.execute({ action: 'stage', identity, value: 'Grace' }, { source: 'agent' });
205
- await registry.execute({ action: 'stage', identity: secondIdentity, value: 'Hopper' }, { source: 'agent' });
206
- const applyButtons = await screen.findAllByRole('button', {
207
- name: /^Apply (?!valid changes$)/,
208
- });
209
- await userEvent.click(applyButtons[0]);
210
- await waitFor(() => expect(screen.getByRole('button', { name: 'Apply Family name' })).toHaveFocus());
211
- });
212
- it('names every proposal action and reports batch failures on their affected fields', async () => {
213
- const familyIdentity = { formId: 'profile', controlId: 'family-name' };
214
- const preferencesIdentity = {
215
- formId: 'profile',
216
- controlId: 'preferences',
217
- };
218
- const registry = createControlInteractionRegistry({
219
- isLocalGesture: () => true,
220
- policy: (command) => command.action === 'apply' &&
221
- command.identity.controlId === familyIdentity.controlId
222
- ? { allowed: false, reason: 'opaque_policy_code' }
223
- : { allowed: true },
224
- });
225
- let familyName = 'Lovelace';
226
- let preferences = { layout: 'comfortable' };
227
- render(Fixture, { props: { registry } });
228
- registry.register({
229
- identity: familyIdentity,
230
- metadata: { kind: 'text', label: 'Family name' },
231
- getValue: () => familyName,
232
- setValue: (next) => {
233
- familyName = String(next);
234
- },
235
- });
236
- registry.register({
237
- identity: preferencesIdentity,
238
- metadata: { kind: 'custom', label: 'Preferences' },
239
- getValue: () => preferences,
240
- setValue: (next) => {
241
- preferences = next;
242
- },
243
- });
244
- await registry.execute({ action: 'stage', identity, value: 'Grace' }, { source: 'agent' });
245
- await registry.execute({ action: 'stage', identity: familyIdentity, value: 'Hopper' }, { source: 'agent' });
246
- await registry.execute({
247
- action: 'stage',
248
- identity: preferencesIdentity,
249
- value: { layout: 'compact' },
250
- }, { source: 'agent' });
251
- expect(screen.getByRole('button', { name: 'Apply Display name' })).toBeInTheDocument();
252
- expect(screen.getByRole('button', { name: 'Discard Display name' })).toBeInTheDocument();
253
- expect(screen.getByRole('button', { name: 'Apply Family name' })).toBeInTheDocument();
254
- expect(screen.getByRole('button', { name: 'Discard Family name' })).toBeInTheDocument();
255
- expect(screen.getByRole('button', { name: 'Apply Preferences' })).toBeInTheDocument();
256
- const preferencesEditor = screen.getByRole('textbox', {
257
- name: 'Edit proposed value for Preferences',
258
- });
259
- await userEvent.clear(preferencesEditor);
260
- await userEvent.type(preferencesEditor, '"');
261
- await userEvent.click(screen.getByRole('button', { name: 'Apply valid changes' }));
262
- await waitFor(() => expect(screen.getByText('Processed 1 of 3 proposed changes.')).toBeInTheDocument());
263
- const familyItem = screen
264
- .getByText('Family name', {
265
- selector: 'strong',
266
- })
267
- .closest('li');
268
- const preferencesItem = screen
269
- .getByText('Preferences', {
270
- selector: 'strong',
271
- })
272
- .closest('li');
273
- if (!familyItem || !preferencesItem) {
274
- throw new Error('Expected remaining proposal items');
275
- }
276
- expect(within(familyItem).getByRole('alert')).toHaveTextContent('This proposal is not valid.');
277
- expect(within(preferencesItem).getByRole('alert')).toHaveTextContent('This proposal is not valid.');
278
- expect(screen.queryByText('opaque_policy_code')).not.toBeInTheDocument();
279
- expect(screen.queryByText('Unexpected end of JSON input')).not.toBeInTheDocument();
280
- expect(familyName).toBe('Lovelace');
281
- expect(preferences).toEqual({ layout: 'comfortable' });
282
- });
283
- it('returns focus to the affected control after applying the final batch', async () => {
284
- const registry = createReviewRegistry();
285
- render(Fixture, { props: { registry } });
286
- await registry.execute({ action: 'stage', identity, value: 'Grace' }, { source: 'agent' });
287
- await userEvent.click(await screen.findByRole('button', { name: 'Apply valid changes' }));
288
- await waitFor(() => expect(screen.getByRole('textbox', { name: 'Display name' })).toHaveFocus());
289
- });
290
- it('returns focus through the registry for a composite control wrapper', async () => {
291
- const registry = createReviewRegistry();
292
- render(Fixture, { props: { registry } });
293
- await registry.execute({
294
- action: 'stage',
295
- identity: { formId: 'profile', controlId: 'volume' },
296
- value: 50,
297
- }, { source: 'agent' });
298
- await userEvent.click(await screen.findByRole('button', { name: 'Apply valid changes' }));
299
- await waitFor(() => expect(screen.getByRole('slider', { name: 'Volume' })).toHaveFocus());
300
- });
301
- it('keeps a nonnumeric optional number proposal invalid and unapplied', async () => {
302
- const registry = createReviewRegistry();
303
- render(Fixture, { props: { registry } });
304
- const numberIdentity = { formId: 'profile', controlId: 'score' };
305
- expect(await registry.execute({ action: 'stage', identity: numberIdentity, value: 'abc' }, { source: 'agent' })).toMatchObject({ ok: true });
306
- expect(registry.get(numberIdentity)?.state.staged).toMatchObject({
307
- valid: false,
308
- validationMessage: 'invalid_number',
309
- });
310
- expect(screen.getByRole('spinbutton', { name: 'Score' })).toHaveValue(1);
311
- });
312
- it.each([
313
- {
314
- controlId: 'birthday',
315
- proposal: '2025-02-30',
316
- currentValue: '',
317
- },
318
- {
319
- controlId: 'meeting-time',
320
- proposal: '25:00',
321
- currentValue: '',
322
- },
323
- {
324
- controlId: 'intensity',
325
- proposal: 5,
326
- currentValue: 50,
327
- },
328
- ])('keeps native $controlId proposals invalid when the browser would canonicalize them', async ({ controlId, proposal, currentValue, }) => {
329
- const registry = createReviewRegistry();
330
- render(Fixture, { props: { registry } });
331
- const nativeIdentity = { formId: 'profile', controlId };
332
- expect(await registry.execute({ action: 'stage', identity: nativeIdentity, value: proposal }, { source: 'agent' })).toMatchObject({ ok: true });
333
- expect(registry.get(nativeIdentity)?.state.staged).toMatchObject({
334
- value: proposal,
335
- valid: false,
336
- validationMessage: 'invalid_value',
337
- });
338
- expect(registry.get(nativeIdentity)?.state.value).toBe(currentValue);
339
- expect(await dispatchLocalGesture((event) => executeLocalControlCommand(registry, { action: 'apply', identity: nativeIdentity }, event))).toMatchObject({ ok: false, reason: 'invalid_value' });
340
- expect(registry.get(nativeIdentity)?.state.value).toBe(currentValue);
341
- });
342
- it('does not coerce an emptied numeric draft to zero', async () => {
343
- const registry = createReviewRegistry();
344
- render(Fixture, { props: { registry } });
345
- const numberIdentity = { formId: 'profile', controlId: 'score' };
346
- await registry.execute({ action: 'stage', identity: numberIdentity, value: 5 }, { source: 'agent' });
347
- const proposal = await screen.findByRole('textbox', {
348
- name: 'Edit proposed value for Score',
349
- });
350
- await userEvent.clear(proposal);
351
- await userEvent.click(screen.getByRole('button', { name: 'Apply Score' }));
352
- expect(screen.getByRole('spinbutton', { name: 'Score' })).toHaveValue(1);
353
- expect(registry.get(numberIdentity)?.state.staged?.value).toBe(5);
354
- });
355
- it.each([
356
- { staged: true, applied: false },
357
- { staged: false, applied: true },
358
- ])('edits a $staged boolean proposal with a native checkbox and applies $applied', async ({ staged, applied, }) => {
359
- let value = staged;
360
- const booleanIdentity = { formId: 'profile', controlId: 'enabled' };
361
- const setValue = vi.fn((next) => {
362
- value = next;
363
- });
364
- const registry = createReviewRegistry();
365
- registry.register({
366
- identity: booleanIdentity,
367
- metadata: { kind: 'checkbox', label: 'Enabled' },
368
- getValue: () => value,
369
- setValue,
370
- });
371
- render(Fixture, { props: { registry } });
372
- await registry.execute({ action: 'stage', identity: booleanIdentity, value: staged }, { source: 'agent' });
373
- const editor = await screen.findByRole('checkbox', {
374
- name: 'Edit proposed value for Enabled',
375
- });
376
- expect(editor).toHaveProperty('checked', staged);
377
- expect(screen.queryByRole('textbox', {
378
- name: 'Edit proposed value for Enabled',
379
- })).not.toBeInTheDocument();
380
- const submitted = vi.fn((event) => event.preventDefault());
381
- screen
382
- .getByRole('form', { name: 'Profile form' })
383
- .addEventListener('submit', submitted);
384
- const enter = new KeyboardEvent('keydown', {
385
- key: 'Enter',
386
- bubbles: true,
387
- cancelable: true,
388
- });
389
- expect(editor.dispatchEvent(enter)).toBe(false);
390
- expect(enter.defaultPrevented).toBe(true);
391
- expect(submitted).not.toHaveBeenCalled();
392
- expect(registry.get(booleanIdentity)?.state.staged?.value).toBe(staged);
393
- await userEvent.click(editor);
394
- expect(editor).toHaveProperty('checked', applied);
395
- await userEvent.click(screen.getByRole('button', { name: 'Apply Enabled' }));
396
- expect(setValue).toHaveBeenLastCalledWith(applied);
397
- expect(value).toBe(applied);
398
- expect(typeof value).toBe('boolean');
399
- expect(registry.get(booleanIdentity)?.state.staged).toBeUndefined();
400
- });
401
- it('announces stale apply failures with localized review text', async () => {
402
- let registry;
403
- registry = createControlInteractionRegistry({
404
- isLocalGesture: () => true,
405
- policy: (command) => {
406
- if (command.action === 'apply') {
407
- registry.recordUserEdit?.(command.identity);
408
- }
409
- return { allowed: true };
410
- },
411
- });
412
- render(Fixture, { props: { registry } });
413
- await registry.execute({ action: 'stage', identity, value: 'Grace' }, { source: 'agent' });
414
- await userEvent.click(screen.getByRole('button', { name: 'Apply Display name' }));
415
- const status = screen.getByText('The field changed after this proposal was staged.', { selector: 'p[role="status"]' });
416
- expect(status).not.toHaveTextContent('staged_value_stale');
417
- });
418
- it('announces gesture failures with localized invalid text', async () => {
419
- const registry = createControlInteractionRegistry({
420
- isLocalGesture: () => false,
421
- });
422
- render(Fixture, { props: { registry } });
423
- await registry.execute({ action: 'stage', identity, value: 'Grace' }, { source: 'agent' });
424
- await userEvent.click(screen.getByRole('button', { name: 'Apply Display name' }));
425
- const status = screen.getByText('This proposal is not valid.', {
426
- selector: 'p[role="status"]',
427
- });
428
- expect(status).not.toHaveTextContent('local_gesture_required');
429
- });
430
- it('announces the pre-batch proposal total', async () => {
431
- const registry = createReviewRegistry();
432
- render(Fixture, { props: { registry } });
433
- await registry.execute({ action: 'stage', identity, value: 'Grace' }, { source: 'agent' });
434
- await registry.execute({
435
- action: 'stage',
436
- identity: { formId: 'profile', controlId: 'score' },
437
- value: 'abc',
438
- }, { source: 'agent' });
439
- await userEvent.click(screen.getByRole('button', { name: 'Apply valid changes' }));
440
- expect(screen.getByText('Processed 1 of 2 proposed changes.')).toBeInTheDocument();
441
- });
442
- it('marks competing user edits stale and lets the human discard them', async () => {
443
- const registry = createReviewRegistry();
444
- render(Fixture, { props: { registry } });
445
- await registry.execute({ action: 'stage', identity, value: 'Grace' }, { source: 'agent' });
446
- const field = screen.getByRole('textbox', { name: 'Display name' });
447
- await userEvent.clear(field);
448
- await userEvent.type(field, 'Katherine');
449
- await waitFor(() => expect(screen.getByText('The field changed after this proposal was staged.')).toBeInTheDocument());
450
- expect(screen.getByRole('button', { name: 'Apply Display name' })).toBeDisabled();
451
- await userEvent.click(screen.getByRole('button', { name: 'Discard Display name' }));
452
- expect(field).toHaveValue('Katherine');
453
- await waitFor(() => expect(screen.queryByRole('region', { name: 'Review proposed changes' })).not.toBeInTheDocument());
454
- expect(screen.getByText('Discarded proposed change.', {
455
- selector: 'p[role="status"]',
456
- })).toHaveTextContent('Discarded proposed change.');
457
- });
458
- it('keeps trusted edit tracking when the consumer supplies an input handler', async () => {
459
- const registry = createReviewRegistry();
460
- const formOnInput = vi.fn();
461
- render(Fixture, { props: { registry, formOnInput } });
462
- await registry.execute({ action: 'stage', identity, value: 'Grace' }, { source: 'agent' });
463
- const field = screen.getByRole('textbox', { name: 'Display name' });
464
- await userEvent.clear(field);
465
- await userEvent.type(field, 'Katherine');
466
- expect(formOnInput).toHaveBeenCalled();
467
- await waitFor(() => expect(registry.get(identity)?.state.staged?.stale).toBe(true));
468
- });
469
- it('preserves a staged proposal across same-identity metadata updates', async () => {
470
- const registry = createReviewRegistry();
471
- const view = render(Fixture, {
472
- props: { registry, displayNameLabel: 'Display name' },
473
- });
474
- await registry.execute({ action: 'stage', identity, value: 'Grace' }, { source: 'agent' });
475
- await view.rerender({
476
- registry,
477
- displayNameLabel: 'Preferred name',
478
- });
479
- await waitFor(() => expect(registry.get(identity)?.state.staged?.value).toBe('Grace'));
480
- expect(screen.getByRole('textbox', {
481
- name: 'Edit proposed value for Preferred name',
482
- })).toHaveValue('Grace');
483
- });
484
- it('preserves undo history across same-identity metadata updates', async () => {
485
- const registry = createReviewRegistry();
486
- const view = render(Fixture, {
487
- props: { registry, displayNameLabel: 'Display name' },
488
- });
489
- await registry.execute({ action: 'stage', identity, value: 'Grace' }, { source: 'agent' });
490
- const revision = registry.get(identity)?.state.staged?.revision;
491
- expect(await dispatchLocalGesture((event) => executeLocalControlBatch(registry, [{ action: 'apply', identity, revision }], event))).toMatchObject({ ok: true });
492
- expect(screen.getByRole('textbox', { name: 'Display name' })).toHaveValue('Grace');
493
- await view.rerender({
494
- registry,
495
- displayNameLabel: 'Preferred name',
496
- });
497
- expect(screen.getByRole('textbox', { name: 'Preferred name' })).toHaveValue('Grace');
498
- await waitFor(() => expect(registry.get(identity)?.metadata.label).toBe('Preferred name'));
499
- const undo = await dispatchLocalGesture((event) => executeLocalControlBatch(registry, [{ action: 'undo', identity }], event));
500
- expect(undo.results[0]).toMatchObject({ ok: true });
501
- expect(screen.getByRole('textbox', { name: 'Preferred name' })).toHaveValue('Ada');
502
- });
503
- it('moves registration when the form registry and identity change', async () => {
504
- const firstRegistry = createReviewRegistry();
505
- const secondRegistry = createReviewRegistry();
506
- const view = render(Fixture, {
507
- props: { registry: firstRegistry, formId: 'profile' },
508
- });
509
- expect(firstRegistry.get(identity)).toBeDefined();
510
- await view.rerender({ registry: secondRegistry, formId: 'account' });
511
- await waitFor(() => expect(firstRegistry.get(identity)).toBeUndefined());
512
- expect(secondRegistry.get({ formId: 'account', controlId: 'display-name' })).toBeDefined();
513
- });
514
- it('moves registration when a subject identity mutates in place', async () => {
515
- const registry = createReviewRegistry();
516
- const subject = { type: 'record', id: 'one' };
517
- render(Fixture, { props: { registry, subject } });
518
- const firstIdentity = { ...identity, subject: { ...subject } };
519
- expect(registry.get(firstIdentity)).toBeDefined();
520
- await userEvent.click(screen.getByRole('button', { name: 'Mutate subject' }));
521
- await waitFor(() => expect(registry.get(firstIdentity)).toBeUndefined());
522
- expect(registry.get({
523
- ...identity,
524
- subject: { type: 'record', id: 'two' },
525
- })).toBeDefined();
526
- });
527
- it('rejects disabled and undeclared base-select proposals on apply', async () => {
528
- const registry = createReviewRegistry();
529
- render(SelectFixture, { props: { registry } });
530
- const selectIdentity = { formId: 'account', controlId: 'role' };
531
- expect(registry
532
- .get(selectIdentity)
533
- ?.metadata.options?.find((option) => option.value === 'owner')).toMatchObject({ disabled: true });
534
- for (const candidate of ['admin', 'owner']) {
535
- await registry.execute({ action: 'stage', identity: selectIdentity, value: candidate }, { source: 'agent' });
536
- expect(registry.get(selectIdentity)?.state.staged).toMatchObject({
537
- value: candidate,
538
- valid: false,
539
- });
540
- const batch = await dispatchLocalGesture((event) => executeLocalControlBatch(registry, [
541
- {
542
- action: 'apply',
543
- identity: selectIdentity,
544
- revision: registry.get(selectIdentity)?.state.staged?.revision,
545
- },
546
- ], event));
547
- expect(batch.results[0]).toMatchObject({
548
- ok: false,
549
- reason: 'staged_value_invalid',
550
- });
551
- expect(screen.getByRole('combobox', { name: 'Role' })).toHaveValue('user');
552
- }
553
- });
554
- it('rejects an enabled empty option when the base select is required', async () => {
555
- const registry = createReviewRegistry();
556
- render(SelectFixture, { props: { registry, required: true } });
557
- const selectIdentity = { formId: 'account', controlId: 'role' };
558
- await registry.execute({ action: 'stage', identity: selectIdentity, value: '' }, { source: 'agent' });
559
- expect(registry.get(selectIdentity)?.state.staged).toMatchObject({
560
- value: '',
561
- valid: false,
562
- });
563
- expect(screen.getByRole('combobox', { name: 'Role' })).toHaveValue('user');
564
- });
565
- it('lets the human correct an invalid proposal before applying it', async () => {
566
- const registry = createReviewRegistry();
567
- render(Fixture, { props: { registry } });
568
- await registry.execute({ action: 'stage', identity, value: '' }, { source: 'agent' });
569
- await waitFor(() => expect(screen.getByRole('alert')).toHaveTextContent('This proposal is not valid.'));
570
- const field = screen.getByRole('textbox', { name: 'Display name' });
571
- expect(field).toHaveValue('Ada');
572
- const proposal = screen.getByRole('textbox', {
573
- name: 'Edit proposed value for Display name',
574
- });
575
- await userEvent.type(proposal, 'Grace');
576
- expect(screen.getByRole('button', { name: 'Apply Display name' })).toBeEnabled();
577
- await userEvent.click(screen.getByRole('button', { name: 'Apply Display name' }));
578
- await waitFor(() => expect(field).toHaveValue('Grace'));
579
- expect(registry.get(identity)?.state.staged).toBeUndefined();
580
- });
581
- it('keeps invalid entries out of valid-only batch actions', async () => {
582
- const registry = createReviewRegistry();
583
- render(Fixture, { props: { registry } });
584
- await registry.execute({ action: 'stage', identity, value: '' }, { source: 'agent' });
585
- await userEvent.click(screen.getByRole('button', { name: 'Discard valid changes' }));
586
- expect(registry.get(identity)?.state.staged).toBeDefined();
587
- });
588
- it('does not coerce an unedited null proposal into a string', async () => {
589
- const registry = createReviewRegistry();
590
- render(Fixture, { props: { registry } });
591
- await registry.execute({ action: 'stage', identity, value: null }, { source: 'agent' });
592
- expect(screen.getByRole('textbox', {
593
- name: 'Edit proposed value for Display name',
594
- })).toHaveValue('null');
595
- await userEvent.click(screen.getByRole('button', { name: 'Apply Display name' }));
596
- expect(screen.getByRole('textbox', { name: 'Display name' })).toHaveValue('Ada');
597
- expect(registry.get(identity)?.state.staged?.value).toBeNull();
598
- });
599
- it('gives same-label subject-qualified proposals unique action names', async () => {
600
- const registry = createReviewRegistry();
601
- let secondValue = 'Ada';
602
- registry.register({
603
- identity: {
604
- ...identity,
605
- subject: { type: 'record', id: 'two' },
606
- },
607
- metadata: { kind: 'text', label: 'Display name' },
608
- getValue: () => secondValue,
609
- setValue: (next) => {
610
- secondValue = String(next);
611
- },
612
- });
613
- render(Fixture, {
614
- props: { registry, subject: { type: 'record', id: 'one' } },
615
- });
616
- const renderedFirst = screen.getByRole('textbox', {
617
- name: 'Display name',
618
- });
619
- await registry.execute({
620
- action: 'stage',
621
- identity: { ...identity, subject: { type: 'record', id: 'two' } },
622
- value: 'Katherine',
623
- }, { source: 'agent' });
624
- await waitFor(() => expect(renderedFirst).not.toHaveAttribute('data-smrt-staged'));
625
- await registry.execute({
626
- action: 'stage',
627
- identity: { ...identity, subject: { type: 'record', id: 'one' } },
628
- value: 'Grace',
629
- }, { source: 'agent' });
630
- await waitFor(() => expect(renderedFirst).toHaveAttribute('data-smrt-staged', 'true'));
631
- expect(screen.queryByText(/profile\/display-name/)).not.toBeInTheDocument();
632
- expect(screen.queryByText(/record:one/)).not.toBeInTheDocument();
633
- for (const subjectId of ['one', 'two']) {
634
- const actionName = `Display name record:${subjectId} [identity:["profile","display-name","record","${subjectId}"]]`;
635
- expect(screen.getByRole('textbox', {
636
- name: `Edit proposed value for ${actionName}`,
637
- })).toBeInTheDocument();
638
- expect(screen.getByRole('button', { name: `Apply ${actionName}` })).toBeInTheDocument();
639
- expect(screen.getByRole('button', { name: `Discard ${actionName}` })).toBeInTheDocument();
640
- }
641
- });
642
- it('keeps tagged action identities distinct when human and fallback subjects collide', async () => {
643
- const registry = createReviewRegistry();
644
- let secondValue = 'Ada';
645
- registry.register({
646
- identity: {
647
- ...identity,
648
- subject: { type: 'record', id: 'b' },
649
- },
650
- metadata: { kind: 'text', label: 'Display name' },
651
- getValue: () => secondValue,
652
- setValue: (next) => {
653
- secondValue = String(next);
654
- },
655
- });
656
- render(Fixture, {
657
- props: {
658
- registry,
659
- subject: { type: 'record', id: 'a', label: 'record:b' },
660
- },
661
- });
662
- await registry.execute({
663
- action: 'stage',
664
- identity: { ...identity, subject: { type: 'record', id: 'b' } },
665
- value: 'Katherine',
666
- }, { source: 'agent' });
667
- await registry.execute({
668
- action: 'stage',
669
- identity: {
670
- ...identity,
671
- subject: { type: 'record', id: 'a', label: 'record:b' },
672
- },
673
- value: 'Grace',
674
- }, { source: 'agent' });
675
- const first = 'Display name record:b [identity:["profile","display-name","record","a"]]';
676
- const second = 'Display name record:b [identity:["profile","display-name","record","b"]]';
677
- expect(first).not.toBe(second);
678
- for (const actionName of [first, second]) {
679
- expect(screen.getByRole('textbox', {
680
- name: `Edit proposed value for ${actionName}`,
681
- })).toBeInTheDocument();
682
- expect(screen.getByRole('button', { name: `Apply ${actionName}` })).toBeInTheDocument();
683
- expect(screen.getByRole('button', { name: `Discard ${actionName}` })).toBeInTheDocument();
684
- }
685
- });
686
- it('rejects secret staging and discards proposals on form reset and unmount', async () => {
687
- const registry = createReviewRegistry();
688
- const rendered = render(Fixture, { props: { registry } });
689
- const secret = await registry.execute({
690
- action: 'stage',
691
- identity: { formId: 'profile', controlId: 'api-token' },
692
- value: 'replacement',
693
- }, { source: 'agent' });
694
- expect(secret).toMatchObject({ ok: false, reason: 'sensitive_control' });
695
- await registry.execute({ action: 'stage', identity, value: 'Grace' }, { source: 'agent' });
696
- await userEvent.click(screen.getByRole('button', { name: 'Reset' }));
697
- await waitFor(() => expect(registry.get(identity)?.state.staged).toBeUndefined());
698
- await registry.execute({ action: 'stage', identity, value: 'Grace' }, { source: 'agent' });
699
- rendered.unmount();
700
- expect(registry.get(identity)).toBeUndefined();
701
- });
702
- });