@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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@happyvertical/smrt-ui",
3
- "version": "0.53.1",
3
+ "version": "0.53.3",
4
4
  "smrtJsdoc": "strict",
5
5
  "description": "Domain-agnostic Svelte 5 UI runtime for SMRT: primitives, i18n client, theme system, and module UI registry",
6
6
  "type": "module",
@@ -134,7 +134,7 @@
134
134
  "access": "public"
135
135
  },
136
136
  "dependencies": {
137
- "@happyvertical/smrt-types": "0.53.1",
137
+ "@happyvertical/smrt-types": "0.53.3",
138
138
  "esm-env": "^1.2.2"
139
139
  },
140
140
  "peerDependencies": {
@@ -160,7 +160,7 @@
160
160
  "vitest": "4.1.10"
161
161
  },
162
162
  "scripts": {
163
- "build": "svelte-package -i src --tsconfig tsconfig.svelte.json",
163
+ "build": "svelte-package -i src --tsconfig tsconfig.svelte.json && node ../../scripts/prune-svelte-package-artifacts.js dist",
164
164
  "dev": "svelte-package -i src --watch",
165
165
  "check": "pnpm exec svelte-check --tsconfig ./tsconfig.svelte.json",
166
166
  "bench:data-table": "vitest bench src/components/data/__benchmarks__/DataTable.bench.ts",
@@ -1,82 +0,0 @@
1
- /**
2
- * Unit test for the ripple action (coverage uplift, S6 gate).
3
- */
4
- import { afterEach, describe, expect, it, vi } from 'vitest';
5
- import { ripple } from '../ripple';
6
- afterEach(() => {
7
- vi.restoreAllMocks();
8
- // Drop any matchMedia stub a test installed.
9
- Reflect.deleteProperty(window, 'matchMedia');
10
- });
11
- describe('ripple action', () => {
12
- it('appends a ripple element on pointer down', () => {
13
- const node = document.createElement('button');
14
- document.body.appendChild(node);
15
- const action = ripple(node);
16
- const before = node.querySelectorAll('span').length;
17
- node.dispatchEvent(new MouseEvent('mousedown', { clientX: 5, clientY: 5, bubbles: true }));
18
- expect(node.querySelectorAll('span').length).toBeGreaterThan(before);
19
- // destroy detaches listeners without throwing
20
- action?.destroy?.();
21
- node.remove();
22
- });
23
- it('supports touch start', () => {
24
- const node = document.createElement('button');
25
- document.body.appendChild(node);
26
- const action = ripple(node);
27
- const touch = { clientX: 3, clientY: 3 };
28
- node.dispatchEvent(new TouchEvent('touchstart', { touches: [touch], bubbles: true }));
29
- expect(node.querySelectorAll('span').length).toBeGreaterThan(0);
30
- action?.destroy?.();
31
- node.remove();
32
- });
33
- it('fills the ripple with the canonical --smrt-color-primary token (#1586)', () => {
34
- // Regression: the canonical theme system only emits `--smrt-color-*`; the
35
- // old `--md-sys-color-primary` namespace always fell back to currentColor.
36
- const node = document.createElement('button');
37
- document.body.appendChild(node);
38
- const action = ripple(node);
39
- node.dispatchEvent(new MouseEvent('mousedown', { clientX: 5, clientY: 5, bubbles: true }));
40
- const span = node.querySelector('span');
41
- expect(span.style.backgroundColor).toContain('--smrt-color-primary');
42
- expect(span.style.backgroundColor).not.toContain('--md-sys');
43
- action?.destroy?.();
44
- node.remove();
45
- });
46
- it('destroy() removes pending ripple spans and clears window listeners (#1586)', () => {
47
- const node = document.createElement('button');
48
- document.body.appendChild(node);
49
- const action = ripple(node);
50
- // Press without releasing: the ripple span stays appended, and a one-shot
51
- // window mouseup listener is still registered.
52
- node.dispatchEvent(new MouseEvent('mousedown', { clientX: 5, clientY: 5, bubbles: true }));
53
- expect(node.querySelectorAll('span.smrt-ripple').length).toBe(1);
54
- const removeSpy = vi.spyOn(window, 'removeEventListener');
55
- action?.destroy?.();
56
- // The leaked span is gone immediately (no waiting for a later global mouseup).
57
- expect(node.querySelectorAll('span.smrt-ripple').length).toBe(0);
58
- // And the pending window pointer-up listeners were detached.
59
- expect(removeSpy).toHaveBeenCalledWith('mouseup', expect.any(Function));
60
- expect(removeSpy).toHaveBeenCalledWith('touchend', expect.any(Function));
61
- node.remove();
62
- });
63
- it('skips the ripple when prefers-reduced-motion is set (#1586)', () => {
64
- window.matchMedia = vi.fn().mockImplementation((query) => ({
65
- matches: query.includes('prefers-reduced-motion'),
66
- media: query,
67
- onchange: null,
68
- addEventListener: vi.fn(),
69
- removeEventListener: vi.fn(),
70
- addListener: vi.fn(),
71
- removeListener: vi.fn(),
72
- dispatchEvent: vi.fn(),
73
- }));
74
- const node = document.createElement('button');
75
- document.body.appendChild(node);
76
- const action = ripple(node);
77
- node.dispatchEvent(new MouseEvent('mousedown', { clientX: 5, clientY: 5, bubbles: true }));
78
- expect(node.querySelectorAll('span.smrt-ripple').length).toBe(0);
79
- action?.destroy?.();
80
- node.remove();
81
- });
82
- });
@@ -1,69 +0,0 @@
1
- import { describe, expect, it, vi } from 'vitest';
2
- import { swipeDismiss, swipeDismisses } from '../swipe-dismiss.js';
3
- describe('swipeDismisses', () => {
4
- it('dismisses a drawer on a clear swipe left only', () => {
5
- expect(swipeDismisses(-80, 10, 'left')).toBe(true);
6
- expect(swipeDismisses(-40, 0, 'left')).toBe(false);
7
- expect(swipeDismisses(-80, 70, 'left')).toBe(false);
8
- expect(swipeDismisses(80, 0, 'left')).toBe(false);
9
- });
10
- it('dismisses a right-hand drawer on a clear swipe right only', () => {
11
- expect(swipeDismisses(80, 5, 'right')).toBe(true);
12
- expect(swipeDismisses(-80, 5, 'right')).toBe(false);
13
- });
14
- it('dismisses a sheet on a clear swipe down only', () => {
15
- expect(swipeDismisses(5, 90, 'down')).toBe(true);
16
- expect(swipeDismisses(0, -90, 'down')).toBe(false);
17
- expect(swipeDismisses(80, 90, 'down')).toBe(false);
18
- });
19
- it('honours a custom threshold', () => {
20
- expect(swipeDismisses(0, 40, 'down', 32)).toBe(true);
21
- expect(swipeDismisses(0, 40, 'down')).toBe(false);
22
- });
23
- });
24
- function touch(type, x, y) {
25
- const point = { clientX: x, clientY: y };
26
- const event = new Event(type);
27
- Object.defineProperty(event, 'touches', {
28
- value: type === 'touchend' ? [] : [point],
29
- });
30
- Object.defineProperty(event, 'changedTouches', { value: [point] });
31
- return event;
32
- }
33
- describe('swipeDismiss action', () => {
34
- it('calls onDismiss after a swipe in its direction', () => {
35
- const node = document.createElement('div');
36
- const onDismiss = vi.fn();
37
- const action = swipeDismiss(node, { direction: 'down', onDismiss });
38
- node.dispatchEvent(touch('touchstart', 10, 10));
39
- node.dispatchEvent(touch('touchend', 12, 100));
40
- expect(onDismiss).toHaveBeenCalledTimes(1);
41
- action.destroy();
42
- });
43
- it('ignores swipes while disabled and after update re-enables them', () => {
44
- const node = document.createElement('div');
45
- const onDismiss = vi.fn();
46
- const action = swipeDismiss(node, {
47
- direction: 'left',
48
- onDismiss,
49
- enabled: false,
50
- });
51
- node.dispatchEvent(touch('touchstart', 100, 10));
52
- node.dispatchEvent(touch('touchend', 10, 10));
53
- expect(onDismiss).not.toHaveBeenCalled();
54
- action.update({ direction: 'left', onDismiss, enabled: true });
55
- node.dispatchEvent(touch('touchstart', 100, 10));
56
- node.dispatchEvent(touch('touchend', 10, 10));
57
- expect(onDismiss).toHaveBeenCalledTimes(1);
58
- action.destroy();
59
- });
60
- it('forgets a cancelled touch', () => {
61
- const node = document.createElement('div');
62
- const onDismiss = vi.fn();
63
- swipeDismiss(node, { direction: 'down', onDismiss });
64
- node.dispatchEvent(touch('touchstart', 10, 10));
65
- node.dispatchEvent(new Event('touchcancel'));
66
- node.dispatchEvent(touch('touchend', 10, 200));
67
- expect(onDismiss).not.toHaveBeenCalled();
68
- });
69
- });
@@ -1,226 +0,0 @@
1
- import { act, render, screen, 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 CalendarView from '../CalendarView.svelte';
6
- const tz = 'America/Edmonton';
7
- const now = new Date('2026-09-29T18:00:00Z');
8
- const items = [
9
- { id: 'fair', title: 'Fall Fair', start: '2026-09-05', end: '2026-09-07' },
10
- {
11
- id: 'council',
12
- title: 'Council meeting',
13
- start: new Date('2026-09-15T01:00:00Z'),
14
- label: 'Council',
15
- href: '/meetings/sept',
16
- },
17
- { id: 'a', title: 'Game A', start: new Date('2026-09-20T18:00:00Z') },
18
- { id: 'b', title: 'Game B', start: new Date('2026-09-20T19:00:00Z') },
19
- { id: 'c', title: 'Game C', start: new Date('2026-09-20T20:00:00Z') },
20
- { id: 'd', title: 'Game D', start: new Date('2026-09-20T21:00:00Z') },
21
- ];
22
- function renderMonth(props = {}) {
23
- return render(CalendarView, {
24
- props: {
25
- items,
26
- timeZone: tz,
27
- locale: 'en-US',
28
- weekStartsOn: 0,
29
- mode: 'month',
30
- now,
31
- year: 2026,
32
- month: 9,
33
- ...props,
34
- },
35
- });
36
- }
37
- describe('CalendarView (month)', () => {
38
- it('renders a localized grid in the given zone', () => {
39
- renderMonth();
40
- expect(screen.getByRole('heading', { name: 'September 2026' })).toBeTruthy();
41
- expect(screen.getByRole('grid')).toBeTruthy();
42
- expect(screen.getAllByRole('columnheader')).toHaveLength(7);
43
- // 19:00 MDT on the 14th, not the 15th (UTC)
44
- expect(screen.getByRole('button', {
45
- name: 'Monday, September 14, 2026, 1 item',
46
- })).toBeTruthy();
47
- expect(screen
48
- .getByRole('button', { name: 'Tuesday, September 29, 2026' })
49
- .getAttribute('aria-current')).toBe('date');
50
- });
51
- it('renders month names in the requested locale', () => {
52
- renderMonth({ locale: 'fr-FR' });
53
- expect(screen.getByRole('heading').textContent?.trim()).toBe('septembre 2026');
54
- });
55
- it('renders all-day spans as bands and overflow as +N more', async () => {
56
- const onSelectDate = vi.fn();
57
- const { container } = renderMonth({ onSelectDate });
58
- // Sat Sep 5 - Mon Sep 7 wraps: one segment per week row.
59
- const bands = [...container.querySelectorAll('.cv-band')];
60
- expect(bands.map((b) => b.textContent?.trim())).toEqual([
61
- 'Fall Fair',
62
- 'Fall Fair',
63
- ]);
64
- expect(bands[0].style.gridColumn).toBe('7 / span 1');
65
- expect(bands[0].classList.contains('cv-band--after')).toBe(true);
66
- expect(bands[1].style.gridColumn).toBe('1 / span 2');
67
- expect(bands[1].classList.contains('cv-band--before')).toBe(true);
68
- const more = screen.getByText('+2 more');
69
- await userEvent.click(more);
70
- expect(onSelectDate).toHaveBeenCalledWith('2026-09-20');
71
- const panel = screen.getByRole('region', {
72
- name: 'Sunday, September 20, 2026',
73
- });
74
- expect(within(panel).getAllByRole('button', { name: /Game/ })).toHaveLength(4);
75
- await userEvent.click(within(panel).getByRole('button', {
76
- name: 'Close Sunday, September 20, 2026',
77
- }));
78
- expect(screen.queryByRole('region')).toBeNull();
79
- });
80
- it('navigates with prev/next/today and reports the month', async () => {
81
- const onNavigate = vi.fn();
82
- render(CalendarView, {
83
- props: {
84
- items,
85
- timeZone: tz,
86
- locale: 'en-US',
87
- mode: 'month',
88
- now,
89
- onNavigate,
90
- },
91
- });
92
- await userEvent.click(screen.getByRole('button', { name: 'Next month' }));
93
- expect(onNavigate).toHaveBeenLastCalledWith({ year: 2026, month: 10 });
94
- expect(screen.getByRole('heading').textContent?.trim()).toBe('October 2026');
95
- await userEvent.click(screen.getByRole('button', { name: 'Today' }));
96
- expect(onNavigate).toHaveBeenLastCalledWith({ year: 2026, month: 9 });
97
- });
98
- it('moves focus with arrow keys and crosses months', async () => {
99
- const onNavigate = vi.fn();
100
- renderMonth({ onNavigate, year: undefined, month: undefined });
101
- const today = screen.getByRole('button', {
102
- name: 'Tuesday, September 29, 2026',
103
- });
104
- expect(today.tabIndex).toBe(0);
105
- today.focus();
106
- await userEvent.keyboard('{ArrowRight}');
107
- expect(document.activeElement.dataset.cvDay).toBe('2026-09-30');
108
- await userEvent.keyboard('{ArrowRight}');
109
- expect(onNavigate).toHaveBeenLastCalledWith({ year: 2026, month: 10 });
110
- expect(document.activeElement.dataset.cvDay).toBe('2026-10-01');
111
- await userEvent.keyboard('{ArrowUp}');
112
- expect(document.activeElement.dataset.cvDay).toBe('2026-09-24');
113
- await userEvent.keyboard('{Home}');
114
- expect(document.activeElement.dataset.cvDay).toBe('2026-09-20');
115
- });
116
- it('turns days into links with dayHref', () => {
117
- renderMonth({ dayHref: (d) => `/events/${d}` });
118
- const link = screen.getByRole('link', {
119
- name: 'Monday, September 14, 2026, 1 item',
120
- });
121
- expect(link.getAttribute('href')).toBe('/events/2026-09-14');
122
- });
123
- });
124
- describe('CalendarView accessibility', () => {
125
- it('is axe-clean in month mode with an open day', async () => {
126
- const { container } = renderMonth({ selectedDate: '2026-09-20' });
127
- await expectNoA11yViolations(container);
128
- });
129
- it('is axe-clean in agenda mode', async () => {
130
- const { container } = renderMonth({ mode: 'agenda' });
131
- await expectNoA11yViolations(container);
132
- });
133
- });
134
- describe('CalendarView (agenda)', () => {
135
- it('lists days with items and a day strip', async () => {
136
- const onItemSelect = vi.fn();
137
- render(CalendarView, {
138
- props: {
139
- items,
140
- timeZone: tz,
141
- locale: 'en-US',
142
- mode: 'agenda',
143
- now,
144
- year: 2026,
145
- month: 9,
146
- onItemSelect,
147
- },
148
- });
149
- const strip = screen.getByRole('toolbar', {
150
- name: 'Days in September 2026',
151
- });
152
- expect(within(strip).getAllByRole('button')).toHaveLength(30);
153
- const headings = screen
154
- .getAllByRole('heading', { level: 3 })
155
- .map((h) => h.textContent?.trim());
156
- expect(headings).toEqual([
157
- 'Saturday, September 5, 2026',
158
- 'Sunday, September 6, 2026',
159
- 'Monday, September 7, 2026',
160
- 'Monday, September 14, 2026',
161
- 'Sunday, September 20, 2026',
162
- ]);
163
- const link = screen.getByRole('link', { name: /Council meeting/ });
164
- expect(link.getAttribute('href')).toBe('/meetings/sept');
165
- expect(link.textContent).toContain('7:00 PM');
166
- expect(link.textContent).toContain('Council');
167
- expect(screen.getAllByText('Until Sep 7').length).toBeGreaterThan(0);
168
- await userEvent.click(within(strip).getByRole('button', {
169
- name: 'Thursday, September 10, 2026',
170
- }));
171
- expect(screen.getByRole('heading', { name: 'Thursday, September 10, 2026' })).toBeTruthy();
172
- expect(screen.getAllByText('Nothing scheduled').length).toBe(1);
173
- });
174
- it('switches to the agenda on phones in auto mode', async () => {
175
- const matchMedia = vi.fn().mockImplementation((query) => ({
176
- matches: query === '(max-width: 48rem)',
177
- media: query,
178
- addEventListener: vi.fn(),
179
- removeEventListener: vi.fn(),
180
- }));
181
- vi.stubGlobal('matchMedia', matchMedia);
182
- try {
183
- const { container } = render(CalendarView, {
184
- props: { items, timeZone: tz, locale: 'en-US', now },
185
- });
186
- await Promise.resolve();
187
- expect(container.querySelector('[data-view]')?.getAttribute('data-view')).toBe('agenda');
188
- expect(screen.queryByRole('grid')).toBeNull();
189
- }
190
- finally {
191
- vi.unstubAllGlobals();
192
- }
193
- });
194
- });
195
- describe('CalendarView robustness', () => {
196
- it('falls back instead of throwing on an invalid time zone', () => {
197
- const warn = vi.spyOn(console, 'warn').mockImplementation(() => { });
198
- try {
199
- expect(() => renderMonth({ timeZone: 'Mars/Olympus_Mons' })).not.toThrow();
200
- expect(screen.getByRole('heading', { name: 'September 2026' })).toBeTruthy();
201
- expect(warn).toHaveBeenCalledWith(expect.stringContaining('Mars/Olympus_Mons'));
202
- }
203
- finally {
204
- warn.mockRestore();
205
- }
206
- });
207
- it('moves the today highlight after midnight without a now prop', async () => {
208
- vi.useFakeTimers({ toFake: ['Date', 'setInterval', 'clearInterval'] });
209
- try {
210
- // 23:59 on Sep 29 in Edmonton (05:59Z on the 30th).
211
- vi.setSystemTime(new Date('2026-09-30T05:59:00Z'));
212
- renderMonth({ now: undefined });
213
- const day = (name) => screen.getByRole('button', { name });
214
- await act(() => { });
215
- expect(day('Tuesday, September 29, 2026').getAttribute('aria-current')).toBe('date');
216
- await act(() => {
217
- vi.advanceTimersByTime(2 * 60_000);
218
- });
219
- expect(day('Wednesday, September 30, 2026').getAttribute('aria-current')).toBe('date');
220
- expect(day('Tuesday, September 29, 2026').getAttribute('aria-current')).toBeNull();
221
- }
222
- finally {
223
- vi.useRealTimers();
224
- }
225
- });
226
- });
@@ -1,138 +0,0 @@
1
- import { describe, expect, it } from 'vitest';
2
- import { addMonthsToKey, dateKeyInZone, defaultWeekStart, layoutMonth, monthWeeks, shiftMonth, toEntries, toneFor, } from '../calendar-model.js';
3
- describe('calendar model', () => {
4
- it('computes days in the given zone, not the host zone', () => {
5
- const instant = new Date('2026-09-29T03:30:00Z');
6
- expect(dateKeyInZone(instant, 'America/Edmonton')).toBe('2026-09-28');
7
- expect(dateKeyInZone(instant, 'Pacific/Auckland')).toBe('2026-09-29');
8
- });
9
- it('builds full weeks for any first weekday', () => {
10
- const sunday = monthWeeks({ year: 2026, month: 9 }, 0);
11
- expect(sunday[0][0]).toBe('2026-08-30');
12
- expect(sunday.at(-1)?.[6]).toBe('2026-10-03');
13
- const monday = monthWeeks({ year: 2026, month: 9 }, 1);
14
- expect(monday[0][0]).toBe('2026-08-31');
15
- expect(monday).toHaveLength(5);
16
- });
17
- it('shifts months and clamps days', () => {
18
- expect(shiftMonth({ year: 2026, month: 12 }, 1)).toEqual({
19
- year: 2027,
20
- month: 1,
21
- });
22
- expect(shiftMonth({ year: 2026, month: 1 }, -1)).toEqual({
23
- year: 2025,
24
- month: 12,
25
- });
26
- expect(addMonthsToKey('2026-01-31', 1)).toBe('2026-02-28');
27
- });
28
- it('reads the locale week start when the runtime knows it', () => {
29
- const start = defaultWeekStart('en-GB');
30
- expect([0, 1]).toContain(start);
31
- expect(defaultWeekStart('not a locale!')).toBe(0);
32
- });
33
- it('normalizes all-day dates, exclusive midnights, and timed spans', () => {
34
- const tz = 'America/Edmonton';
35
- const entries = toEntries([
36
- { id: 'a', title: 'Fair', start: '2026-09-05', end: '2026-09-07' },
37
- {
38
- id: 'b',
39
- title: 'Exclusive',
40
- allDay: true,
41
- start: new Date('2026-09-10T06:00:00Z'),
42
- end: new Date('2026-09-12T06:00:00Z'),
43
- },
44
- {
45
- id: 'c',
46
- title: 'Late meeting',
47
- start: new Date('2026-09-15T01:00:00Z'),
48
- end: new Date('2026-09-15T03:00:00Z'),
49
- },
50
- {
51
- id: 'd',
52
- title: 'Overnight',
53
- start: new Date('2026-09-20T04:00:00Z'),
54
- end: new Date('2026-09-20T16:00:00Z'),
55
- },
56
- { id: 'bad', title: 'Bad', start: 'not a date' },
57
- ], tz);
58
- const byId = Object.fromEntries(entries.map((e) => [e.item.id, e]));
59
- expect(byId.a).toMatchObject({
60
- startKey: '2026-09-05',
61
- endKey: '2026-09-07',
62
- allDay: true,
63
- band: true,
64
- });
65
- expect(byId.b).toMatchObject({
66
- startKey: '2026-09-10',
67
- endKey: '2026-09-11',
68
- });
69
- // 19:00-21:00 MDT on the 14th
70
- expect(byId.c).toMatchObject({
71
- startKey: '2026-09-14',
72
- endKey: '2026-09-14',
73
- band: false,
74
- });
75
- // 22:00 on the 19th to 10:00 on the 20th spans two days
76
- expect(byId.d).toMatchObject({
77
- startKey: '2026-09-19',
78
- endKey: '2026-09-20',
79
- band: true,
80
- });
81
- expect(byId.bad).toBeUndefined();
82
- });
83
- it('lays out bands in lanes and counts overflow as "+N more"', () => {
84
- const entries = toEntries([
85
- { id: 'span', title: 'Span', start: '2026-09-01', end: '2026-09-09' },
86
- { id: 'one', title: 'One', start: '2026-09-02' },
87
- {
88
- id: 't1',
89
- title: 'T1',
90
- start: new Date('2026-09-02T15:00:00Z'),
91
- },
92
- {
93
- id: 't2',
94
- title: 'T2',
95
- start: new Date('2026-09-02T16:00:00Z'),
96
- },
97
- ], 'UTC');
98
- const weeks = layoutMonth({ year: 2026, month: 9 }, entries, {
99
- weekStartsOn: 0,
100
- maxPerDay: 3,
101
- });
102
- const first = weeks[0];
103
- // Span continues into the next week; "One" takes a second lane.
104
- const span = first.bands.find((b) => b.entry.item.id === 'span');
105
- expect(span).toMatchObject({ column: 2, span: 5, lane: 0 });
106
- expect(span?.continuesAfter).toBe(true);
107
- expect(first.lanes).toBe(2);
108
- const sep2 = first.days.find((d) => d.key === '2026-09-02');
109
- // One row left for timed items; two don't fit, so show none + "+2 more".
110
- expect(sep2?.visible).toHaveLength(0);
111
- expect(sep2?.hiddenCount).toBe(2);
112
- expect(sep2?.all.map((e) => e.item.id)).toEqual([
113
- 'span',
114
- 'one',
115
- 't1',
116
- 't2',
117
- ]);
118
- const second = weeks[1];
119
- const cont = second.bands.find((b) => b.entry.item.id === 'span');
120
- expect(cont).toMatchObject({ column: 0, span: 4, continuesBefore: true });
121
- });
122
- it('derives a stable tone from the group', () => {
123
- const a = toneFor({
124
- id: '1',
125
- title: 'x',
126
- start: '2026-01-01',
127
- group: 'council',
128
- });
129
- const b = toneFor({
130
- id: '2',
131
- title: 'y',
132
- start: '2026-01-01',
133
- group: 'council',
134
- });
135
- expect(a).toBe(b);
136
- expect(toneFor({ id: '3', title: 'z', start: '2026-01-01', tone: 'error' })).toBe('error');
137
- });
138
- });
@@ -1,118 +0,0 @@
1
- /**
2
- * Golden tests for the L3 chat primitives (`./chat` subpath, #1422):
3
- * MessageBubble, ReactionPicker, TypingIndicator.
4
- */
5
- import { render, screen } from '@testing-library/svelte';
6
- import userEvent from '@testing-library/user-event';
7
- import { createRawSnippet } from 'svelte';
8
- import { describe, expect, it, vi } from 'vitest';
9
- import { expectNoA11yViolations } from '../../../test-support/a11y';
10
- import MessageBubble from '../MessageBubble.svelte';
11
- import ReactionPicker from '../ReactionPicker.svelte';
12
- import TypingIndicator from '../TypingIndicator.svelte';
13
- const body = (text) => createRawSnippet(() => ({ render: () => `<p>${text}</p>` }));
14
- describe('MessageBubble', () => {
15
- it('labels the group by author + role and renders the body', () => {
16
- render(MessageBubble, {
17
- props: { role: 'agent', author: 'Ada', children: body('Hello there') },
18
- });
19
- const group = screen.getByRole('group', { name: 'Ada (agent)' });
20
- expect(group).toHaveTextContent('Hello there');
21
- });
22
- it('renders a machine-readable timestamp', () => {
23
- const { container } = render(MessageBubble, {
24
- props: {
25
- author: 'Ada',
26
- timestamp: new Date('2026-01-01T10:00:00.000Z'),
27
- children: body('hi'),
28
- },
29
- });
30
- expect(container.querySelector('time')).toHaveAttribute('datetime', '2026-01-01T10:00:00.000Z');
31
- });
32
- it('renders a bare bubble (no header/group) from plain content', () => {
33
- render(MessageBubble, {
34
- props: { content: 'Hello there', variant: 'default', own: false },
35
- });
36
- expect(screen.getByText('Hello there')).toBeInTheDocument();
37
- // A bare bubble adds no labelled landmark — its host row owns the label.
38
- expect(screen.queryByRole('group')).toBeNull();
39
- });
40
- it('renders a legacy card header even without an author', () => {
41
- // The legacy card form (role, no styling axes) keeps its header + labelled
42
- // group, falling back to the role label.
43
- render(MessageBubble, { props: { role: 'agent', children: body('hi') } });
44
- expect(screen.getByRole('group', { name: 'Assistant (agent)' })).toBeInTheDocument();
45
- });
46
- it('is axe-clean as a labelled card', async () => {
47
- const { container } = render(MessageBubble, {
48
- props: { role: 'user', author: 'You', children: body('hi') },
49
- });
50
- await expectNoA11yViolations(container);
51
- });
52
- it('is axe-clean as an own bare bubble', async () => {
53
- const { container } = render(MessageBubble, {
54
- props: { content: 'Hi', variant: 'default', own: true },
55
- });
56
- await expectNoA11yViolations(container);
57
- });
58
- });
59
- describe('ReactionPicker', () => {
60
- it('is a labelled group of named emoji buttons', () => {
61
- render(ReactionPicker);
62
- expect(screen.getByRole('group', { name: 'Add reaction' })).toBeInTheDocument();
63
- expect(screen.getByRole('button', { name: 'Thumbs up' })).toBeInTheDocument();
64
- });
65
- it('reports the picked emoji', async () => {
66
- const onpick = vi.fn();
67
- render(ReactionPicker, { props: { onpick } });
68
- await userEvent.click(screen.getByRole('button', { name: 'Heart' }));
69
- expect(onpick).toHaveBeenCalledWith('❤️');
70
- });
71
- it('renders nothing when closed', () => {
72
- render(ReactionPicker, { props: { isOpen: false } });
73
- expect(screen.queryByRole('group')).toBeNull();
74
- });
75
- it('honors caller-supplied group + per-emoji labels', () => {
76
- render(ReactionPicker, {
77
- props: {
78
- emojis: ['🚀'],
79
- label: 'Emoji reactions',
80
- emojiLabel: (emoji) => `React with ${emoji}`,
81
- },
82
- });
83
- expect(screen.getByRole('group', { name: 'Emoji reactions' })).toBeInTheDocument();
84
- expect(screen.getByRole('button', { name: 'React with 🚀' })).toBeInTheDocument();
85
- });
86
- it('is axe-clean', async () => {
87
- const { container } = render(ReactionPicker);
88
- await expectNoA11yViolations(container);
89
- });
90
- });
91
- describe('TypingIndicator', () => {
92
- it('announces who is typing via a status region', () => {
93
- render(TypingIndicator, { props: { name: 'Assistant' } });
94
- expect(screen.getByRole('status')).toHaveTextContent('Assistant is typing');
95
- });
96
- it('names a single typist from a list', () => {
97
- render(TypingIndicator, { props: { names: ['Ada'] } });
98
- expect(screen.getByText('Ada is typing')).toBeInTheDocument();
99
- });
100
- it('names two typists from a list', () => {
101
- render(TypingIndicator, { props: { names: ['Ada', 'Bob'] } });
102
- expect(screen.getByText('Ada and Bob are typing')).toBeInTheDocument();
103
- });
104
- it('aggregates three or more typists', () => {
105
- render(TypingIndicator, { props: { names: ['Ada', 'Bob', 'Cy'] } });
106
- expect(screen.getByText('Ada and 2 others are typing')).toBeInTheDocument();
107
- });
108
- it('renders nothing when nobody is typing', () => {
109
- const { container } = render(TypingIndicator, { props: { names: [] } });
110
- expect(container.querySelector('.typing')).toBeNull();
111
- });
112
- it('is axe-clean', async () => {
113
- const { container } = render(TypingIndicator, {
114
- props: { name: 'Assistant' },
115
- });
116
- await expectNoA11yViolations(container);
117
- });
118
- });