@happyvertical/smrt-ui 0.53.1 → 0.53.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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,746 +0,0 @@
1
- import { describe, expect, it, vi } from 'vitest';
2
- import { createDataSurfaceRegistry, DATA_SURFACE_IDENTIFIER_MAX_LENGTH, DATA_SURFACE_MAX_REPLAY_ENTRIES, DATA_SURFACE_MAX_REQUEST_BYTES, normalizeDataSurfaceActionRequest, normalizeDataSurfaceDescriptor, normalizeDataSurfaceQueryRequest, normalizeDataSurfaceVisibleCommand, } from '../data-surface.js';
3
- const identity = {
4
- surfaceId: 'content-library',
5
- kind: 'table',
6
- subject: { type: 'site', id: 'docs' },
7
- };
8
- function descriptor(overrides = {}) {
9
- return {
10
- version: 1,
11
- identity,
12
- schemaVersion: 1,
13
- label: 'Content library',
14
- rowKey: 'id',
15
- columns: [
16
- { id: 'id', label: 'ID', capabilities: ['read', 'project'] },
17
- {
18
- id: 'title',
19
- label: 'Title',
20
- capabilities: ['read', 'search', 'filter', 'sort', 'project'],
21
- },
22
- {
23
- id: 'internal',
24
- label: 'Internal',
25
- sensitivity: 'sensitive',
26
- capabilities: [],
27
- },
28
- ],
29
- query: {
30
- modes: ['rows', 'count', 'facets'],
31
- projectableColumnIds: ['id', 'title'],
32
- },
33
- controls: [{ id: 'set-search', label: 'Search' }],
34
- actions: [
35
- {
36
- id: 'archive',
37
- label: 'Archive',
38
- selectionScopes: ['explicit-ids', 'all-matching'],
39
- requiresConfirmation: true,
40
- },
41
- ],
42
- limits: { maxQueryRows: 50, maxQueryBytes: 10_000, maxSelectionSize: 3 },
43
- ...overrides,
44
- };
45
- }
46
- function command(overrides = {}) {
47
- return {
48
- version: 1,
49
- commandId: 'search-1',
50
- identity,
51
- expectedRevision: 3,
52
- controlId: 'set-search',
53
- payload: { search: 'Grace' },
54
- ...overrides,
55
- };
56
- }
57
- function registerFixture(options = {}) {
58
- let revision = 3;
59
- let state = { search: 'Ada', internal: 'not-for-discovery' };
60
- const execute = vi.fn((next) => {
61
- state = {
62
- ...state,
63
- search: next.payload &&
64
- typeof next.payload === 'object' &&
65
- !Array.isArray(next.payload)
66
- ? String(next.payload.search ?? '')
67
- : '',
68
- };
69
- if (options.advance !== false)
70
- revision += 1;
71
- });
72
- const registry = createDataSurfaceRegistry();
73
- const unregister = registry.register({
74
- descriptor: descriptor(),
75
- getSnapshot: () => ({ revision, state }),
76
- execute,
77
- ...(options.redact
78
- ? {
79
- redact: (snapshot) => ({
80
- ...snapshot,
81
- state: { search: snapshot.state.search ?? '' },
82
- }),
83
- }
84
- : {}),
85
- });
86
- return { registry, unregister, execute };
87
- }
88
- describe('data surface registry', () => {
89
- it('identifies unknown action columns in descriptor validation errors', () => {
90
- expect(() => normalizeDataSurfaceDescriptor(descriptor({
91
- actions: [
92
- {
93
- id: 'archive',
94
- label: 'Archive',
95
- selectionScopes: ['explicit-ids'],
96
- columnIds: ['missing'],
97
- },
98
- ],
99
- }))).toThrow('Unknown DataSurface action column id: missing');
100
- });
101
- it('aligns bounded query result shapes with each query kind', () => {
102
- const results = [
103
- {
104
- version: 1,
105
- requestId: 'rows-1',
106
- identity,
107
- revision: 3,
108
- kind: 'rows',
109
- rowKey: 'id',
110
- rows: [{ id: 'a' }],
111
- hasMore: false,
112
- truncated: false,
113
- },
114
- {
115
- version: 1,
116
- requestId: 'count-1',
117
- identity,
118
- revision: 3,
119
- kind: 'count',
120
- count: 1,
121
- },
122
- {
123
- version: 1,
124
- requestId: 'facets-1',
125
- identity,
126
- revision: 3,
127
- kind: 'facets',
128
- columnId: 'title',
129
- facets: [{ value: 'Guide', count: 1 }],
130
- truncated: false,
131
- },
132
- ];
133
- expect(results.map((result) => result.kind)).toEqual([
134
- 'rows',
135
- 'count',
136
- 'facets',
137
- ]);
138
- });
139
- it('discovers defensive serializable descriptors and snapshots', () => {
140
- const { registry } = registerFixture();
141
- const [listed] = registry.list();
142
- expect(listed).toMatchObject({
143
- identity,
144
- rowKey: 'id',
145
- query: { projectableColumnIds: ['id', 'title'] },
146
- });
147
- listed.label = 'mutated by a consumer';
148
- expect(registry.list()[0]?.label).toBe('Content library');
149
- const snapshot = registry.inspect(identity);
150
- expect(snapshot).toMatchObject({
151
- revision: 3,
152
- state: { search: 'Ada', internal: 'not-for-discovery' },
153
- selection: null,
154
- });
155
- expect(JSON.parse(JSON.stringify(snapshot))).toEqual(snapshot);
156
- });
157
- it('rejects duplicate mounted identities rather than replacing them', () => {
158
- const { registry } = registerFixture();
159
- expect(() => registry.register({
160
- descriptor: descriptor(),
161
- getSnapshot: () => ({ revision: 0, state: {} }),
162
- })).toThrow(JSON.stringify(['table', 'content-library', ['site', 'docs']]));
163
- });
164
- it('keeps identities with delimiter-like components distinct', () => {
165
- const firstIdentity = {
166
- kind: 'table',
167
- surfaceId: 'x',
168
- subject: { type: 'y', id: 'z\u0000w' },
169
- };
170
- const secondIdentity = {
171
- kind: 'table',
172
- surfaceId: 'x\u0000y',
173
- subject: { type: 'z', id: 'w' },
174
- };
175
- const registry = createDataSurfaceRegistry();
176
- registry.register({
177
- descriptor: descriptor({ identity: firstIdentity }),
178
- getSnapshot: () => ({ revision: 0, state: {} }),
179
- });
180
- registry.register({
181
- descriptor: descriptor({ identity: secondIdentity }),
182
- getSnapshot: () => ({ revision: 0, state: {} }),
183
- });
184
- expect(registry.list()).toHaveLength(2);
185
- registry.unregister(secondIdentity);
186
- expect(registry.inspect(firstIdentity)?.revision).toBe(0);
187
- expect(registry.inspect(secondIdentity)).toBeUndefined();
188
- });
189
- it('applies the host-owned redaction boundary before inspect results escape', () => {
190
- const { registry } = registerFixture({ redact: true });
191
- expect(registry.inspect(identity)?.state).toEqual({ search: 'Ada' });
192
- });
193
- it('rejects authority and SQL keys from default and redacted snapshots', () => {
194
- const registry = createDataSurfaceRegistry();
195
- expect(() => registry.register({
196
- descriptor: descriptor(),
197
- getSnapshot: () => ({ revision: 0, state: { tenantId: 'other' } }),
198
- })).toThrow('authority or SQL');
199
- expect(() => registry.register({
200
- descriptor: descriptor({
201
- identity: { ...identity, surfaceId: 'tenant-context' },
202
- }),
203
- getSnapshot: () => ({
204
- revision: 0,
205
- state: { tenant: { id: 'other' } },
206
- }),
207
- })).toThrow('authority or SQL');
208
- expect(() => registry.register({
209
- descriptor: descriptor({
210
- identity: { ...identity, surfaceId: 'authorization-header' },
211
- }),
212
- getSnapshot: () => ({
213
- revision: 0,
214
- state: { authorizationHeader: 'Bearer credential' },
215
- }),
216
- })).toThrow('authority or SQL');
217
- expect(() => registry.register({
218
- descriptor: descriptor({
219
- identity: { ...identity, surfaceId: 'authorization-token' },
220
- }),
221
- getSnapshot: () => ({
222
- revision: 0,
223
- state: { authToken: 'Bearer credential' },
224
- }),
225
- })).toThrow('authority or SQL');
226
- for (const [surfaceId, state] of [
227
- ['bare-token', { token: 'credential' }],
228
- ['bare-bearer', { bearer: 'credential' }],
229
- ]) {
230
- expect(() => registry.register({
231
- descriptor: descriptor({
232
- identity: { ...identity, surfaceId },
233
- }),
234
- getSnapshot: () => ({ revision: 0, state }),
235
- })).toThrow('authority or SQL');
236
- }
237
- expect(() => registry.register({
238
- descriptor: descriptor(),
239
- getSnapshot: () => ({ revision: 0, state: {} }),
240
- redact: (snapshot) => ({ ...snapshot, state: { sql: 'select 1' } }),
241
- })).toThrow('authority or SQL');
242
- });
243
- it('rejects unsupported and stale commands before invoking the surface', async () => {
244
- const { registry, execute } = registerFixture();
245
- await expect(registry.execute(command({ controlId: 'delete-everything' }))).resolves.toMatchObject({ ok: false, reason: 'unsupported' });
246
- await expect(registry.execute(command({ commandId: 'stale', expectedRevision: 2 }))).resolves.toMatchObject({
247
- ok: false,
248
- reason: 'stale_revision',
249
- revision: 3,
250
- });
251
- expect(execute).not.toHaveBeenCalled();
252
- });
253
- it('rejects commands that unmount and replace their surface', async () => {
254
- let unregister = () => undefined;
255
- const registry = createDataSurfaceRegistry();
256
- unregister = registry.register({
257
- descriptor: descriptor(),
258
- getSnapshot: () => ({ revision: 3, state: { search: 'Ada' } }),
259
- execute: () => {
260
- unregister();
261
- registry.register({
262
- descriptor: descriptor(),
263
- getSnapshot: () => ({ revision: 9, state: { search: 'Grace' } }),
264
- });
265
- },
266
- });
267
- await expect(registry.execute(command())).resolves.toMatchObject({
268
- ok: false,
269
- reason: 'not_found',
270
- });
271
- expect(registry.inspect(identity)).toMatchObject({
272
- revision: 9,
273
- state: { search: 'Grace' },
274
- });
275
- });
276
- it('replays a command idempotently and rejects conflicting reuse of its id', async () => {
277
- const { registry, execute } = registerFixture();
278
- const initial = command();
279
- const first = await registry.execute(initial);
280
- const replay = await registry.execute(initial);
281
- const conflict = await registry.execute(command({ payload: { search: 'Ada' } }));
282
- expect(first).toMatchObject({ ok: true, revision: 4 });
283
- expect(replay).toEqual(first);
284
- expect(conflict).toMatchObject({
285
- ok: false,
286
- reason: 'idempotency_conflict',
287
- });
288
- expect(execute).toHaveBeenCalledTimes(1);
289
- });
290
- it('bounds the LRU replay cache for long-lived mounted surfaces', async () => {
291
- let revision = 0;
292
- const execute = vi.fn(() => {
293
- revision += 1;
294
- });
295
- const registry = createDataSurfaceRegistry();
296
- registry.register({
297
- descriptor: descriptor(),
298
- getSnapshot: () => ({ revision, state: {} }),
299
- execute,
300
- });
301
- for (let index = 0; index < DATA_SURFACE_MAX_REPLAY_ENTRIES; index += 1) {
302
- await expect(registry.execute(command({
303
- commandId: `command-${index}`,
304
- expectedRevision: index,
305
- }))).resolves.toMatchObject({ ok: true, revision: index + 1 });
306
- }
307
- await expect(registry.execute(command({ commandId: 'command-0', expectedRevision: 0 }))).resolves.toMatchObject({ ok: true, revision: 1 });
308
- await expect(registry.execute(command({
309
- commandId: `command-${DATA_SURFACE_MAX_REPLAY_ENTRIES}`,
310
- expectedRevision: DATA_SURFACE_MAX_REPLAY_ENTRIES,
311
- }))).resolves.toMatchObject({
312
- ok: true,
313
- revision: DATA_SURFACE_MAX_REPLAY_ENTRIES + 1,
314
- });
315
- await expect(registry.execute(command({ commandId: 'command-1', expectedRevision: 1 }))).resolves.toMatchObject({
316
- ok: false,
317
- reason: 'stale_revision',
318
- revision: DATA_SURFACE_MAX_REPLAY_ENTRIES + 1,
319
- });
320
- await expect(registry.execute(command({ commandId: 'command-0', expectedRevision: 0 }))).resolves.toMatchObject({ ok: true, revision: 1 });
321
- expect(execute).toHaveBeenCalledTimes(DATA_SURFACE_MAX_REPLAY_ENTRIES + 1);
322
- });
323
- it('serializes concurrent commands so only one can execute at a revision', async () => {
324
- let revision = 3;
325
- let releaseFirst;
326
- const firstExecution = new Promise((resolve) => {
327
- releaseFirst = resolve;
328
- });
329
- const execute = vi.fn(async () => {
330
- await firstExecution;
331
- revision += 1;
332
- });
333
- const registry = createDataSurfaceRegistry();
334
- registry.register({
335
- descriptor: descriptor(),
336
- getSnapshot: () => ({ revision, state: {} }),
337
- execute,
338
- });
339
- const first = registry.execute(command({ commandId: 'first' }));
340
- const second = registry.execute(command({ commandId: 'second' }));
341
- await vi.waitFor(() => expect(execute).toHaveBeenCalledTimes(1));
342
- releaseFirst();
343
- await expect(first).resolves.toMatchObject({ ok: true, revision: 4 });
344
- await expect(second).resolves.toMatchObject({
345
- ok: false,
346
- reason: 'stale_revision',
347
- revision: 4,
348
- });
349
- expect(execute).toHaveBeenCalledTimes(1);
350
- });
351
- it('rejects in-flight and queued commands after their surface unregisters', async () => {
352
- let revision = 3;
353
- let releaseFirst;
354
- const firstExecution = new Promise((resolve) => {
355
- releaseFirst = resolve;
356
- });
357
- const execute = vi.fn(async () => {
358
- await firstExecution;
359
- revision += 1;
360
- });
361
- const registry = createDataSurfaceRegistry();
362
- const unregister = registry.register({
363
- descriptor: descriptor(),
364
- getSnapshot: () => ({ revision, state: {} }),
365
- execute,
366
- });
367
- const first = registry.execute(command({ commandId: 'first' }));
368
- const queued = registry.execute(command({ commandId: 'queued' }));
369
- await vi.waitFor(() => expect(execute).toHaveBeenCalledTimes(1));
370
- unregister();
371
- releaseFirst();
372
- await expect(first).resolves.toMatchObject({
373
- ok: false,
374
- commandId: 'first',
375
- reason: 'not_found',
376
- });
377
- await expect(queued).resolves.toMatchObject({
378
- ok: false,
379
- commandId: 'queued',
380
- reason: 'not_found',
381
- });
382
- expect(execute).toHaveBeenCalledTimes(1);
383
- });
384
- it('returns execution_failed when handler teardown also breaks snapshots', async () => {
385
- let tornDown = false;
386
- const registry = createDataSurfaceRegistry();
387
- registry.register({
388
- descriptor: descriptor(),
389
- getSnapshot: () => {
390
- if (tornDown)
391
- throw new Error('renderer is gone');
392
- return { revision: 3, state: {} };
393
- },
394
- execute: () => {
395
- tornDown = true;
396
- throw new Error('handler failed');
397
- },
398
- });
399
- await expect(registry.execute(command())).resolves.toMatchObject({
400
- ok: false,
401
- commandId: 'search-1',
402
- reason: 'execution_failed',
403
- });
404
- });
405
- it('quarantines a changed snapshot when its handler throws before revision advances', async () => {
406
- let state = { search: 'Ada' };
407
- const execute = vi.fn(() => {
408
- state = { search: 'Grace' };
409
- throw new Error('handler failed');
410
- });
411
- const registry = createDataSurfaceRegistry();
412
- registry.register({
413
- descriptor: descriptor(),
414
- getSnapshot: () => ({ revision: 3, state }),
415
- execute,
416
- });
417
- await expect(registry.execute(command())).resolves.toMatchObject({
418
- ok: false,
419
- reason: 'non_monotonic_revision',
420
- });
421
- await expect(registry.execute(command({ commandId: 'blocked-after-failure' }))).resolves.toMatchObject({
422
- ok: false,
423
- reason: 'non_monotonic_revision',
424
- });
425
- expect(execute).toHaveBeenCalledTimes(1);
426
- });
427
- it('quarantines a changed snapshot until its renderer advances revision', async () => {
428
- let revision = 3;
429
- let state = { search: 'Ada' };
430
- let advance = false;
431
- const execute = vi.fn(() => {
432
- state = { search: 'Grace' };
433
- if (advance)
434
- revision += 1;
435
- });
436
- const registry = createDataSurfaceRegistry();
437
- registry.register({
438
- descriptor: descriptor(),
439
- getSnapshot: () => ({ revision, state }),
440
- execute,
441
- });
442
- await expect(registry.execute(command())).resolves.toMatchObject({
443
- ok: false,
444
- reason: 'non_monotonic_revision',
445
- });
446
- await expect(registry.execute(command({ commandId: 'blocked-at-revision-3' }))).resolves.toMatchObject({
447
- ok: false,
448
- reason: 'non_monotonic_revision',
449
- });
450
- expect(execute).toHaveBeenCalledTimes(1);
451
- revision = 4;
452
- advance = true;
453
- await expect(registry.execute(command({
454
- commandId: 'after-refresh',
455
- expectedRevision: 4,
456
- }))).resolves.toMatchObject({ ok: true, revision: 5 });
457
- expect(execute).toHaveBeenCalledTimes(2);
458
- });
459
- it('quarantines host-driven snapshot changes until revision advances', async () => {
460
- let revision = 3;
461
- const state = { search: 'Ada' };
462
- const execute = vi.fn(() => {
463
- state.search = 'Lin';
464
- revision += 1;
465
- });
466
- const registry = createDataSurfaceRegistry();
467
- registry.register({
468
- descriptor: descriptor(),
469
- getSnapshot: () => ({ revision, state }),
470
- execute,
471
- });
472
- state.search = 'Grace';
473
- expect(registry.inspect(identity)?.state).toEqual({ search: 'Grace' });
474
- await expect(registry.execute(command())).resolves.toMatchObject({
475
- ok: false,
476
- reason: 'non_monotonic_revision',
477
- });
478
- expect(execute).not.toHaveBeenCalled();
479
- revision = 4;
480
- await expect(registry.execute(command({ commandId: 'after-host-refresh', expectedRevision: 4 }))).resolves.toMatchObject({ ok: true, revision: 5 });
481
- expect(execute).toHaveBeenCalledTimes(1);
482
- });
483
- it('quarantines redacted snapshot changes until revision advances', async () => {
484
- let audience = 'owner';
485
- const execute = vi.fn();
486
- const registry = createDataSurfaceRegistry();
487
- registry.register({
488
- descriptor: descriptor(),
489
- getSnapshot: () => ({ revision: 3, state: { search: 'Ada' } }),
490
- execute,
491
- redact: (snapshot) => ({
492
- ...snapshot,
493
- state: { search: audience },
494
- }),
495
- });
496
- audience = 'viewer';
497
- expect(registry.inspect(identity)?.state).toEqual({ search: 'viewer' });
498
- await expect(registry.execute(command())).resolves.toMatchObject({
499
- ok: false,
500
- reason: 'non_monotonic_revision',
501
- });
502
- expect(execute).not.toHaveBeenCalled();
503
- });
504
- it('emits monotonic sequence events and removes unregistered surfaces', async () => {
505
- const { registry, unregister } = registerFixture();
506
- const events = [];
507
- const unsubscribe = registry.subscribe((event) => events.push(event));
508
- await registry.execute(command());
509
- unregister();
510
- unsubscribe();
511
- expect(events.map(({ type, sequence, revision }) => ({
512
- type,
513
- sequence,
514
- revision,
515
- }))).toEqual([
516
- { type: 'command', sequence: 2, revision: 4 },
517
- { type: 'unregistered', sequence: 3, revision: 4 },
518
- ]);
519
- expect(registry.inspect(identity)).toBeUndefined();
520
- expect(registry.list()).toEqual([]);
521
- });
522
- it('isolates subscriber failures from registry operations', async () => {
523
- const { registry, execute } = registerFixture();
524
- registry.subscribe(() => {
525
- throw new Error('transport failed');
526
- });
527
- await expect(registry.execute(command())).resolves.toMatchObject({
528
- ok: true,
529
- revision: 4,
530
- });
531
- expect(execute).toHaveBeenCalledTimes(1);
532
- const secondIdentity = { ...identity, surfaceId: 'secondary-surface' };
533
- expect(() => registry.register({
534
- descriptor: descriptor({ identity: secondIdentity }),
535
- getSnapshot: () => ({ revision: 0, state: {} }),
536
- })).not.toThrow();
537
- expect(() => registry.unregister(secondIdentity)).not.toThrow();
538
- });
539
- it('validates bounded query and preview/apply action envelopes without executing them', () => {
540
- const { registry } = registerFixture();
541
- expect(registry.validateQuery({
542
- version: 1,
543
- requestId: 'rows-1',
544
- identity,
545
- kind: 'rows',
546
- limit: 25,
547
- projection: ['title', 'id'],
548
- })).toEqual({ ok: true });
549
- expect(registry.validateQuery({
550
- version: 1,
551
- requestId: 'rows-too-large',
552
- identity,
553
- kind: 'rows',
554
- limit: 51,
555
- })).toEqual({ ok: false, reason: 'limit_exceeded' });
556
- expect(registry.validateQuery({
557
- version: 1,
558
- requestId: 'rows-hidden',
559
- identity,
560
- kind: 'rows',
561
- limit: 1,
562
- projection: ['internal'],
563
- })).toEqual({ ok: false, reason: 'projection_not_allowed' });
564
- expect(registry.validateAction({
565
- version: 1,
566
- requestId: 'archive-preview',
567
- identity,
568
- actionId: 'archive',
569
- phase: 'preview',
570
- selection: { scope: 'explicit-ids', rowIds: ['b', 'a', 'a'] },
571
- })).toEqual({ ok: true });
572
- expect(registry.validateAction({
573
- version: 1,
574
- requestId: 'archive-apply',
575
- identity,
576
- actionId: 'archive',
577
- phase: 'apply',
578
- selection: { scope: 'explicit-ids', rowIds: ['a'] },
579
- })).toEqual({ ok: false, reason: 'confirmation_required' });
580
- });
581
- it('enforces the descriptor query-byte limit using the normalized UTF-8 envelope', () => {
582
- const request = {
583
- version: 1,
584
- requestId: 'unicode-cursor',
585
- identity,
586
- kind: 'rows',
587
- limit: 1,
588
- cursor: 'é',
589
- };
590
- const maxQueryBytes = new TextEncoder().encode(JSON.stringify(normalizeDataSurfaceQueryRequest(request))).byteLength;
591
- const registry = createDataSurfaceRegistry();
592
- registry.register({
593
- descriptor: descriptor({
594
- limits: { maxQueryRows: 50, maxQueryBytes, maxSelectionSize: 3 },
595
- }),
596
- getSnapshot: () => ({ revision: 0, state: {} }),
597
- });
598
- expect(registry.validateQuery(request)).toEqual({ ok: true });
599
- expect(registry.validateQuery({ ...request, cursor: 'éé' })).toEqual({
600
- ok: false,
601
- reason: 'limit_exceeded',
602
- });
603
- });
604
- it('rejects SQL and authority values at the browser-contract boundary', () => {
605
- expect(() => normalizeDataSurfaceVisibleCommand(command({ payload: { sql: 'select * from content' } }))).toThrow('authority or SQL');
606
- expect(() => normalizeDataSurfaceVisibleCommand(command({ payload: { tenantId: 'other-tenant' } }))).toThrow('authority or SQL');
607
- });
608
- it('enforces the shared identifier bound at registry boundaries', () => {
609
- const tooLong = 'x'.repeat(DATA_SURFACE_IDENTIFIER_MAX_LENGTH + 1);
610
- const identifiers = [
611
- ['command id', command({ commandId: tooLong })],
612
- ['control id', command({ controlId: tooLong })],
613
- [
614
- 'surface id',
615
- command({ identity: { ...identity, surfaceId: tooLong } }),
616
- ],
617
- [
618
- 'subject type',
619
- command({
620
- identity: {
621
- ...identity,
622
- subject: { type: tooLong, id: 'docs' },
623
- },
624
- }),
625
- ],
626
- [
627
- 'subject id',
628
- command({
629
- identity: {
630
- ...identity,
631
- subject: { type: 'site', id: tooLong },
632
- },
633
- }),
634
- ],
635
- ];
636
- for (const [label, value] of identifiers) {
637
- expect(() => normalizeDataSurfaceVisibleCommand(value), label).toThrow();
638
- }
639
- expect(() => normalizeDataSurfaceActionRequest({
640
- version: 1,
641
- requestId: tooLong,
642
- identity,
643
- actionId: 'archive',
644
- phase: 'preview',
645
- selection: { scope: 'current-page' },
646
- })).toThrow();
647
- expect(() => normalizeDataSurfaceActionRequest({
648
- version: 1,
649
- requestId: 'row-id',
650
- identity,
651
- actionId: 'archive',
652
- phase: 'preview',
653
- selection: { scope: 'explicit-ids', rowIds: [tooLong] },
654
- })).toThrow();
655
- expect(() => createDataSurfaceRegistry().register({
656
- descriptor: descriptor({
657
- identity: { ...identity, surfaceId: tooLong },
658
- }),
659
- getSnapshot: () => ({ revision: 0, state: {} }),
660
- })).toThrow();
661
- });
662
- it('rejects parsed prototype keys at every browser-contract boundary', () => {
663
- const prototypePayload = JSON.parse('{"__proto__":{"tenantId":"other"}}');
664
- expect(() => normalizeDataSurfaceVisibleCommand(command({ payload: prototypePayload }))).toThrow('prototype key');
665
- expect(() => normalizeDataSurfaceActionRequest({
666
- version: 1,
667
- requestId: 'prototype-action',
668
- identity,
669
- actionId: 'archive',
670
- phase: 'preview',
671
- selection: { scope: 'current-page' },
672
- payload: prototypePayload,
673
- })).toThrow('prototype key');
674
- const registry = createDataSurfaceRegistry();
675
- expect(() => registry.register({
676
- descriptor: descriptor(),
677
- getSnapshot: () => ({
678
- revision: 0,
679
- state: JSON.parse('{"__proto__":{"tenantId":"other"}}'),
680
- }),
681
- })).toThrow('prototype key');
682
- });
683
- it('bounds command and action envelopes before dispatch', () => {
684
- const oversized = 'x'.repeat(DATA_SURFACE_MAX_REQUEST_BYTES);
685
- expect(() => normalizeDataSurfaceVisibleCommand(command({ payload: { value: oversized } }))).toThrow('UTF-8 bytes');
686
- expect(() => normalizeDataSurfaceActionRequest({
687
- version: 1,
688
- requestId: 'oversized-action',
689
- identity,
690
- actionId: 'archive',
691
- phase: 'preview',
692
- selection: { scope: 'current-page' },
693
- payload: { value: oversized },
694
- })).toThrow('UTF-8 bytes');
695
- expect(() => normalizeDataSurfaceActionRequest({
696
- version: 1,
697
- requestId: 'many-row-ids',
698
- identity,
699
- actionId: 'archive',
700
- phase: 'preview',
701
- selection: {
702
- scope: 'explicit-ids',
703
- rowIds: Array.from({ length: 1_001 }, () => 'duplicate'),
704
- },
705
- })).toThrow('more than 1000 row ids');
706
- });
707
- it('bounds query content during structural validation', () => {
708
- const stringify = vi.spyOn(JSON, 'stringify');
709
- try {
710
- expect(() => normalizeDataSurfaceQueryRequest({
711
- version: 1,
712
- requestId: 'oversized-projection',
713
- identity,
714
- kind: 'rows',
715
- limit: 1,
716
- projection: ['x'.repeat(DATA_SURFACE_MAX_REQUEST_BYTES)],
717
- })).toThrow('DataSurface query request cannot exceed');
718
- expect(stringify).not.toHaveBeenCalled();
719
- }
720
- finally {
721
- stringify.mockRestore();
722
- }
723
- });
724
- it('accepts a query whose canonical envelope is exactly at the byte limit', () => {
725
- const request = {
726
- version: 1,
727
- requestId: 'exact-limit',
728
- identity,
729
- kind: 'rows',
730
- limit: 1,
731
- projection: Array.from({ length: 394 }, (_, index) => `${String(index).padStart(3, '0')}${'x'.repeat(247)}`),
732
- };
733
- const withEmpty = { ...request, projection: [...request.projection, ''] };
734
- const overhead = new TextEncoder().encode(JSON.stringify(withEmpty)).byteLength -
735
- new TextEncoder().encode(JSON.stringify(request)).byteLength;
736
- const finalLength = DATA_SURFACE_MAX_REQUEST_BYTES -
737
- new TextEncoder().encode(JSON.stringify(request)).byteLength -
738
- overhead;
739
- const exactLimit = {
740
- ...request,
741
- projection: [...request.projection, 'x'.repeat(finalLength)],
742
- };
743
- expect(new TextEncoder().encode(JSON.stringify(exactLimit)).byteLength).toBe(DATA_SURFACE_MAX_REQUEST_BYTES);
744
- expect(normalizeDataSurfaceQueryRequest(exactLimit)).toEqual(exactLimit);
745
- });
746
- });