@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,112 +0,0 @@
1
- /**
2
- * Golden tests for the L3 gap primitives — batch 2 (Tooltip, Dropdown, Tree).
3
- * Render + keyboard/pointer interaction + a11y, per the L4 harness (#1422).
4
- */
5
- import { render, screen, waitFor } 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 Dropdown from '../Dropdown.svelte';
11
- import Tooltip from '../Tooltip.svelte';
12
- import Tree from '../Tree.svelte';
13
- const triggerSnippet = (text) => createRawSnippet(() => ({ render: () => `<button>${text}</button>` }));
14
- describe('Tooltip', () => {
15
- it('wires the trigger to the bubble via aria-describedby', () => {
16
- render(Tooltip, {
17
- props: { text: 'More info', children: triggerSnippet('Help') },
18
- });
19
- const tip = screen.getByRole('tooltip');
20
- expect(tip).toHaveTextContent('More info');
21
- // The trigger wrapper (direct parent of the snippet content) is described
22
- // by the bubble.
23
- const trigger = screen.getByRole('button', { name: 'Help' }).parentElement;
24
- expect(trigger).toHaveAttribute('aria-describedby', tip.id);
25
- });
26
- it('reveals on hover', async () => {
27
- render(Tooltip, {
28
- props: { text: 'More info', delay: 0, children: triggerSnippet('Help') },
29
- });
30
- await userEvent.hover(screen.getByRole('button', { name: 'Help' }));
31
- await waitFor(() => expect(screen.getByRole('tooltip')).toHaveClass('open'));
32
- });
33
- it('is axe-clean', async () => {
34
- const { container } = render(Tooltip, {
35
- props: { text: 'More info', children: triggerSnippet('Help') },
36
- });
37
- await expectNoA11yViolations(container);
38
- });
39
- });
40
- const MENU = [
41
- { id: 'edit', label: 'Edit' },
42
- { id: 'dup', label: 'Duplicate' },
43
- { id: 'del', label: 'Delete' },
44
- ];
45
- describe('Dropdown', () => {
46
- it('trigger advertises a collapsed menu', () => {
47
- render(Dropdown, { props: { label: 'Actions', items: MENU } });
48
- const trigger = screen.getByRole('button', { name: 'Actions' });
49
- expect(trigger).toHaveAttribute('aria-haspopup', 'menu');
50
- expect(trigger).toHaveAttribute('aria-expanded', 'false');
51
- });
52
- it('opens a menu and reports selection', async () => {
53
- const onselect = vi.fn();
54
- render(Dropdown, { props: { label: 'Actions', items: MENU, onselect } });
55
- await userEvent.click(screen.getByRole('button', { name: 'Actions' }));
56
- expect(screen.getByRole('menu')).toBeInTheDocument();
57
- expect(screen.getAllByRole('menuitem')).toHaveLength(3);
58
- await userEvent.click(screen.getByRole('menuitem', { name: 'Delete' }));
59
- expect(onselect).toHaveBeenCalledWith('del');
60
- expect(screen.queryByRole('menu')).not.toBeInTheDocument();
61
- });
62
- it('closes on Escape', async () => {
63
- render(Dropdown, { props: { label: 'Actions', items: MENU } });
64
- await userEvent.click(screen.getByRole('button', { name: 'Actions' }));
65
- await userEvent.keyboard('{Escape}');
66
- expect(screen.queryByRole('menu')).not.toBeInTheDocument();
67
- });
68
- it('is axe-clean when open', async () => {
69
- const { container } = render(Dropdown, {
70
- props: { label: 'Actions', items: MENU },
71
- });
72
- await userEvent.click(screen.getByRole('button', { name: 'Actions' }));
73
- await expectNoA11yViolations(container);
74
- });
75
- });
76
- const NODES = [
77
- {
78
- id: 'src',
79
- label: 'src',
80
- children: [
81
- { id: 'a', label: 'a.ts' },
82
- { id: 'b', label: 'b.ts' },
83
- ],
84
- },
85
- { id: 'readme', label: 'README.md' },
86
- ];
87
- describe('Tree', () => {
88
- it('is a labelled tree with levelled items', () => {
89
- render(Tree, { props: { nodes: NODES, 'aria-label': 'Files' } });
90
- expect(screen.getByRole('tree', { name: 'Files' })).toBeInTheDocument();
91
- const folder = screen.getByRole('treeitem', { name: /src/ });
92
- expect(folder).toHaveAttribute('aria-level', '1');
93
- expect(folder).toHaveAttribute('aria-expanded', 'false');
94
- });
95
- it('expands a node to reveal children', async () => {
96
- render(Tree, { props: { nodes: NODES } });
97
- await userEvent.click(screen.getByRole('treeitem', { name: /src/ }));
98
- expect(screen.getByRole('treeitem', { name: 'a.ts' })).toHaveAttribute('aria-level', '2');
99
- });
100
- it('reports leaf selection', async () => {
101
- const onselect = vi.fn();
102
- render(Tree, { props: { nodes: NODES, onselect } });
103
- await userEvent.click(screen.getByRole('treeitem', { name: 'README.md' }));
104
- expect(onselect).toHaveBeenCalledWith('readme');
105
- });
106
- it('is axe-clean', async () => {
107
- const { container } = render(Tree, {
108
- props: { nodes: NODES, expanded: ['src'], 'aria-label': 'Files' },
109
- });
110
- await expectNoA11yViolations(container);
111
- });
112
- });
@@ -1,84 +0,0 @@
1
- /**
2
- * Golden tests for the L3 gap primitives — batch 1 (Avatar, Chip, Skeleton).
3
- * Render + interaction + a11y, per the L4 harness pattern (#1422 / #1423).
4
- */
5
- import { fireEvent, render, screen } from '@testing-library/svelte';
6
- import userEvent from '@testing-library/user-event';
7
- import { describe, expect, it, vi } from 'vitest';
8
- import { expectNoA11yViolations } from '../../../test-support/a11y';
9
- import Avatar from '../Avatar.svelte';
10
- import Chip from '../Chip.svelte';
11
- import Skeleton from '../Skeleton.svelte';
12
- describe('Avatar', () => {
13
- it('shows initials + accessible name when no src', () => {
14
- render(Avatar, { props: { name: 'Ada Lovelace' } });
15
- const img = screen.getByRole('img', { name: 'Ada Lovelace' });
16
- expect(img).toHaveTextContent('AL');
17
- });
18
- it('renders an <img> with alt when src is set', () => {
19
- render(Avatar, { props: { name: 'Ada', src: 'https://x/a.png' } });
20
- expect(screen.getByRole('img', { name: 'Ada' }).tagName).toBe('IMG');
21
- });
22
- it('announces presence status', () => {
23
- render(Avatar, { props: { name: 'Ada', status: 'online' } });
24
- expect(screen.getByText('Online')).toBeInTheDocument();
25
- });
26
- it('falls back to initials when the image fails to load', async () => {
27
- render(Avatar, {
28
- props: { name: 'Ada Lovelace', src: 'https://x/broken.png' },
29
- });
30
- const img = screen.getByRole('img', { name: 'Ada Lovelace' });
31
- expect(img.tagName).toBe('IMG');
32
- await fireEvent.error(img);
33
- expect(screen.getByRole('img', { name: 'Ada Lovelace' })).toHaveTextContent('AL');
34
- });
35
- it('is axe-clean', async () => {
36
- const { container } = render(Avatar, {
37
- props: { name: 'Ada Lovelace', status: 'away' },
38
- });
39
- await expectNoA11yViolations(container);
40
- });
41
- });
42
- describe('Chip', () => {
43
- it('renders its label', () => {
44
- render(Chip, { props: { label: 'TypeScript' } });
45
- expect(screen.getByText('TypeScript')).toBeInTheDocument();
46
- });
47
- it('selectable: toggle button exposes aria-pressed and fires onselect', async () => {
48
- const onselect = vi.fn();
49
- render(Chip, {
50
- props: { label: 'Tag', selectable: true, selected: true, onselect },
51
- });
52
- const btn = screen.getByRole('button', { name: 'Tag', pressed: true });
53
- await userEvent.click(btn);
54
- expect(onselect).toHaveBeenCalledTimes(1);
55
- });
56
- it('closeable: labelled remove button fires onclose', async () => {
57
- const onclose = vi.fn();
58
- render(Chip, { props: { label: 'Tag', closeable: true, onclose } });
59
- await userEvent.click(screen.getByRole('button', { name: 'Remove Tag' }));
60
- expect(onclose).toHaveBeenCalledTimes(1);
61
- });
62
- it('is axe-clean (selectable + closeable)', async () => {
63
- const { container } = render(Chip, {
64
- props: { label: 'Tag', selectable: true, closeable: true },
65
- });
66
- await expectNoA11yViolations(container);
67
- });
68
- });
69
- describe('Skeleton', () => {
70
- it('is a labelled status region', () => {
71
- render(Skeleton, { props: { label: 'Loading profile' } });
72
- expect(screen.getByRole('status', { name: 'Loading profile' })).toBeInTheDocument();
73
- });
74
- it('renders one placeholder per text line', () => {
75
- const { container } = render(Skeleton, {
76
- props: { variant: 'text', lines: 3 },
77
- });
78
- expect(container.querySelectorAll('.skeleton')).toHaveLength(3);
79
- });
80
- it('is axe-clean', async () => {
81
- const { container } = render(Skeleton, { props: { variant: 'circle' } });
82
- await expectNoA11yViolations(container);
83
- });
84
- });
@@ -1,13 +0,0 @@
1
- <script lang="ts">
2
- import Accordion from '../Accordion.svelte';
3
- import AccordionItem from '../AccordionItem.svelte';
4
- import Disclosure from '../Disclosure.svelte';
5
- import Popover from '../Popover.svelte';
6
- </script>
7
-
8
- <Popover label="Filters"><p>Filter controls</p></Popover>
9
- <Disclosure title="Details"><p>Disclosure details</p></Disclosure>
10
- <Accordion>
11
- <AccordionItem value="one" title="First section"><p>First content</p></AccordionItem>
12
- <AccordionItem value="two" title="Second section"><p>Second content</p></AccordionItem>
13
- </Accordion>
@@ -1,19 +0,0 @@
1
- interface $$__sveltets_2_IsomorphicComponent<Props extends Record<string, any> = any, Events extends Record<string, any> = any, Slots extends Record<string, any> = any, Exports = {}, Bindings = string> {
2
- new (options: import('svelte').ComponentConstructorOptions<Props>): import('svelte').SvelteComponent<Props, Events, Slots> & {
3
- $$bindings?: Bindings;
4
- } & Exports;
5
- (internal: unknown, props: {
6
- $$events?: Events;
7
- $$slots?: Slots;
8
- }): Exports & {
9
- $set?: any;
10
- $on?: any;
11
- };
12
- z_$$bindings?: Bindings;
13
- }
14
- declare const InteractionPrimitives: $$__sveltets_2_IsomorphicComponent<Record<string, never>, {
15
- [evt: string]: CustomEvent<any>;
16
- }, {}, {}, string>;
17
- type InteractionPrimitives = InstanceType<typeof InteractionPrimitives>;
18
- export default InteractionPrimitives;
19
- //# sourceMappingURL=interaction-primitives.fixture.svelte.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"interaction-primitives.fixture.svelte.d.ts","sourceRoot":"","sources":["../../../../src/components/ui/__tests__/interaction-primitives.fixture.svelte.ts"],"names":[],"mappings":"AAwBA,UAAU,kCAAkC,CAAC,KAAK,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,GAAG,EAAE,MAAM,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,GAAG,EAAE,KAAK,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,GAAG,EAAE,OAAO,GAAG,EAAE,EAAE,QAAQ,GAAG,MAAM;IACpM,KAAK,OAAO,EAAE,OAAO,QAAQ,EAAE,2BAA2B,CAAC,KAAK,CAAC,GAAG,OAAO,QAAQ,EAAE,eAAe,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,CAAC,GAAG;QAAE,UAAU,CAAC,EAAE,QAAQ,CAAA;KAAE,GAAG,OAAO,CAAC;IACjK,CAAC,QAAQ,EAAE,OAAO,EAAE,KAAK,EAAE;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,KAAK,CAAA;KAAC,GAAG,OAAO,GAAG;QAAE,IAAI,CAAC,EAAE,GAAG,CAAC;QAAC,GAAG,CAAC,EAAE,GAAG,CAAA;KAAE,CAAC;IACtG,YAAY,CAAC,EAAE,QAAQ,CAAC;CAC3B;AAKD,QAAA,MAAM,qBAAqB;;kBAA+E,CAAC;AACzF,KAAK,qBAAqB,GAAG,YAAY,CAAC,OAAO,qBAAqB,CAAC,CAAC;AAC1E,eAAe,qBAAqB,CAAC"}
@@ -1,31 +0,0 @@
1
- import { render, screen } from '@testing-library/svelte';
2
- import userEvent from '@testing-library/user-event';
3
- import { describe, expect, it } from 'vitest';
4
- import { expectNoA11yViolations } from '../../../test-support/a11y';
5
- import Fixture from './interaction-primitives.fixture.svelte';
6
- describe('interaction primitives', () => {
7
- it('opens and dismisses a popover', async () => {
8
- render(Fixture);
9
- const trigger = screen.getByRole('button', { name: 'Filters' });
10
- await userEvent.click(trigger);
11
- expect(screen.getByRole('dialog', { name: 'Filters' })).toBeInTheDocument();
12
- await userEvent.keyboard('{Escape}');
13
- expect(screen.queryByRole('dialog', { name: 'Filters' })).not.toBeInTheDocument();
14
- expect(trigger).toHaveFocus();
15
- });
16
- it('supports disclosure and single-value accordion semantics', async () => {
17
- render(Fixture);
18
- await userEvent.click(screen.getByText('Details'));
19
- expect(screen.getByText('Disclosure details')).toBeVisible();
20
- await userEvent.click(screen.getByRole('button', { name: 'First section' }));
21
- expect(screen.getByRole('region', { name: 'First section' })).toHaveTextContent('First content');
22
- await userEvent.click(screen.getByRole('button', { name: 'Second section' }));
23
- expect(screen.queryByText('First content')).not.toBeInTheDocument();
24
- });
25
- it('is axe-clean with interactive content open', async () => {
26
- const { container } = render(Fixture);
27
- await userEvent.click(screen.getByRole('button', { name: 'Filters' }));
28
- await userEvent.click(screen.getByRole('button', { name: 'First section' }));
29
- await expectNoA11yViolations(container);
30
- });
31
- });
@@ -1,39 +0,0 @@
1
- /**
2
- * Touch targets: on phones (48rem and under) Button of every size and the
3
- * Switch row are at least 44px tall, and TagsInput's remove button is a 44px
4
- * target on phones and touch screens. The sizing lives in each component's
5
- * stylesheet, so an app never needs its own `min-height: 44px` override.
6
- */
7
- import { readFileSync } from 'node:fs';
8
- import { join } from 'node:path';
9
- import { describe, expect, it } from 'vitest';
10
- const root = process.cwd().endsWith('packages/smrt-ui')
11
- ? process.cwd()
12
- : join(process.cwd(), 'packages/smrt-ui');
13
- const read = (path) => readFileSync(join(root, 'src/components', path), 'utf8');
14
- /** The body of the first `@media (…max-width: 48rem…)` block in `source`. */
15
- function phoneBlock(source) {
16
- const start = source.indexOf('@media (max-width: 48rem)');
17
- expect(start).toBeGreaterThan(-1);
18
- return source.slice(start, source.indexOf('\n }', start) + 4);
19
- }
20
- describe('phone touch targets', () => {
21
- it('Button is at least 44px tall on phones, in every size', () => {
22
- const phone = phoneBlock(read('ui/Button.svelte'));
23
- expect(phone).toMatch(/\.button\s*\{[^}]*min-block-size:\s*2\.75rem/);
24
- });
25
- it('Switch is at least 44px tall on phones', () => {
26
- expect(read('forms/Switch.svelte')).toMatch(/@media \(max-width: 48rem\)\s*\{\s*\.switch\s*\{\s*min-block-size:\s*2\.75rem/);
27
- });
28
- it('TagsInput remove buttons are 44px on phones and touch screens', () => {
29
- const source = read('forms/TagsInput.svelte');
30
- const start = source.indexOf('@media (max-width: 48rem), (pointer: coarse)');
31
- expect(start).toBeGreaterThan(-1);
32
- const block = source.slice(start, source.indexOf('\n }', start));
33
- expect(block).toMatch(/\.tag button\s*\{[^}]*min-inline-size:\s*2\.75rem/);
34
- expect(block).toMatch(/\.tag button\s*\{[^}]*min-block-size:\s*2\.75rem/);
35
- });
36
- it('Dropdown menu rows are at least 44px tall', () => {
37
- expect(read('ui/Dropdown.svelte')).toMatch(/\.dropdown__item\s*\{[^}]*min-block-size:\s*2\.75rem/);
38
- });
39
- });
@@ -1,74 +0,0 @@
1
- /**
2
- * Unit coverage for the client i18n core (S13 #1418): the message-default
3
- * registry, the reactive store's resolution order, and `{var}` interpolation.
4
- */
5
- import { afterEach, describe, expect, it } from 'vitest';
6
- import { clearRegisteredMessages, createI18nContext, defineMessages, getRegisteredDefault, } from '../index.js';
7
- afterEach(() => {
8
- clearRegisteredMessages();
9
- });
10
- describe('defineMessages', () => {
11
- it('registers English defaults and returns a typed key map', () => {
12
- const M = defineMessages({
13
- 'ui.demo.hello': 'Hello',
14
- 'ui.demo.bye': 'Goodbye',
15
- });
16
- expect(M['ui.demo.hello']).toBe('ui.demo.hello');
17
- expect(getRegisteredDefault('ui.demo.hello')).toBe('Hello');
18
- expect(getRegisteredDefault('ui.demo.bye')).toBe('Goodbye');
19
- });
20
- it('is idempotent for an identical default', () => {
21
- defineMessages({ 'ui.demo.hello': 'Hello' });
22
- expect(() => defineMessages({ 'ui.demo.hello': 'Hello' })).not.toThrow();
23
- });
24
- it('throws when a key is redefined with a different default', () => {
25
- defineMessages({ 'ui.demo.hello': 'Hello' });
26
- expect(() => defineMessages({ 'ui.demo.hello': 'Hi' })).toThrow(/already registered/);
27
- });
28
- });
29
- describe('I18nStore.t resolution order', () => {
30
- it('falls back to the registered default when no snapshot has the key', () => {
31
- defineMessages({ 'ui.demo.empty': 'No data available' });
32
- const store = createI18nContext();
33
- expect(store.t('ui.demo.empty')).toBe('No data available');
34
- expect(store.locale).toBe('en');
35
- });
36
- it('prefers the snapshot template over the registered default', () => {
37
- defineMessages({ 'ui.demo.empty': 'No data available' });
38
- const store = createI18nContext({
39
- locale: 'fr',
40
- messages: { 'ui.demo.empty': 'Aucune donnée' },
41
- });
42
- expect(store.t('ui.demo.empty')).toBe('Aucune donnée');
43
- expect(store.locale).toBe('fr');
44
- });
45
- it('falls back to the key itself for an unknown message (never blank)', () => {
46
- const store = createI18nContext();
47
- expect(store.t('ui.demo.unknown')).toBe('ui.demo.unknown');
48
- });
49
- it('reflects a snapshot reassignment (locale switch)', () => {
50
- defineMessages({ 'ui.demo.empty': 'No data available' });
51
- const store = createI18nContext();
52
- expect(store.t('ui.demo.empty')).toBe('No data available');
53
- store.snapshot = {
54
- locale: 'es',
55
- messages: { 'ui.demo.empty': 'Sin datos' },
56
- };
57
- expect(store.locale).toBe('es');
58
- expect(store.t('ui.demo.empty')).toBe('Sin datos');
59
- });
60
- });
61
- describe('interpolation', () => {
62
- it('substitutes {var} placeholders from the snapshot template', () => {
63
- const store = createI18nContext({
64
- locale: 'en',
65
- messages: { 'ui.demo.range': 'Showing {count} of {total}' },
66
- });
67
- expect(store.t('ui.demo.range', { count: 3, total: 9 })).toBe('Showing 3 of 9');
68
- });
69
- it('collapses missing variables to empty strings', () => {
70
- defineMessages({ 'ui.demo.greet': 'Hi {name}{title}' });
71
- const store = createI18nContext();
72
- expect(store.t('ui.demo.greet', { name: 'Ada' })).toBe('Hi Ada');
73
- });
74
- });
@@ -1,37 +0,0 @@
1
- /**
2
- * Pins the client's inlined `renderTemplate` (src/i18n/render.ts) against the
3
- * canonical `@happyvertical/smrt-languages` implementation so the two
4
- * interpolation contracts never drift (S13 #1418). The client keeps its own
5
- * copy to avoid bundling the heavy languages package; this test is the guard.
6
- *
7
- * Importing the languages root triggers its `__smrt-register__` manifest
8
- * side-effect, which throws under vitest's source-alias (dev-server URL, not a
9
- * file) — unrelated to the pure `renderTemplate`. Record-not-throw via
10
- * SMRT_STRICT_REGISTRY=false set before a dynamic import.
11
- */
12
- import { beforeAll, describe, expect, it, vi } from 'vitest';
13
- import { renderTemplate as clientRender } from '../render.js';
14
- let languagesRender;
15
- beforeAll(async () => {
16
- vi.stubEnv('SMRT_STRICT_REGISTRY', 'false');
17
- ({ renderTemplate: languagesRender } = await import('@happyvertical/smrt-languages'));
18
- });
19
- const NOW = new Date('2026-06-17T12:00:00.000Z');
20
- const cases = [
21
- ['No interpolation here', undefined],
22
- ['No interpolation here', {}],
23
- ['Showing {count} of {total}', { count: 3, total: 9 }],
24
- ['Hi {name}{title}', { name: 'Ada' }], // missing → empty
25
- ['Null is {x} here', { x: null }],
26
- ['Date: {when}', { when: NOW }],
27
- ['List: {items}', { items: [1, 2, 3] }],
28
- ['Obj: {meta}', { meta: { a: 1 } }],
29
- ['Trim { spaced } key', { spaced: 'ok' }],
30
- ['Number {n}', { n: 0 }],
31
- ['Bool {b}', { b: false }],
32
- ];
33
- describe('renderTemplate parity with @happyvertical/smrt-languages', () => {
34
- it.each(cases)('renders %j identically', (template, vars) => {
35
- expect(clientRender(template, vars)).toBe(languagesRender(template, vars));
36
- });
37
- });
@@ -1,20 +0,0 @@
1
- import { describe, expect, it } from 'vitest';
2
- import playground from '../playground.js';
3
- describe('smrt-ui playground', () => {
4
- it('publishes themed base-control, data-table, and capture previews', () => {
5
- expect(playground.packageName).toBe('@happyvertical/smrt-ui');
6
- expect(playground.entries.map((entry) => entry.id)).toEqual([
7
- 'base-controls',
8
- 'interactive-controls',
9
- 'feedback-overlays',
10
- 'collections',
11
- 'data-table',
12
- 'calendar',
13
- 'capture',
14
- ]);
15
- for (const entry of playground.entries) {
16
- expect(entry.loadComponent).toEqual(expect.any(Function));
17
- expect(entry.modes).toHaveProperty('mock');
18
- }
19
- });
20
- });
@@ -1,112 +0,0 @@
1
- /**
2
- * Control-boundary contrast floor, every preset (#2322).
3
- *
4
- * WCAG 2.2 SC 1.4.11 requires the visual boundary that identifies a UI
5
- * component to clear 3:1 against its adjacent background. The form primitives
6
- * used to draw their resting border with `outline-variant` — the divider role,
7
- * which measures 1.2–1.7:1 in every preset — so an input's edge was effectively
8
- * invisible. They now consume `outline`, and this test pins the other half of
9
- * that contract: `outline` must actually be a boundary-strength value in every
10
- * preset and scheme, on every surface a control can sit on.
11
- *
12
- * `outlineVariant` is deliberately NOT held to this floor. It is the quiet
13
- * hairline for dividers, panel edges, and readout rows; raising it would erase
14
- * the distinction the two tokens exist to draw.
15
- */
16
- import { describe, expect, it } from 'vitest';
17
- import { themes } from '../registry.js';
18
- /** Every surface role a control can be placed on. */
19
- const SURFACES = [
20
- 'surface',
21
- 'surfaceVariant',
22
- 'surfaceContainer',
23
- 'surfaceContainerLow',
24
- 'surfaceContainerHigh',
25
- 'surfaceContainerHighest',
26
- 'surfaceContainerLowest',
27
- 'surfaceDim',
28
- 'surfaceBright',
29
- 'background',
30
- ];
31
- /**
32
- * Parse `#rrggbb` or `rgba(r, g, b, a)` into a straight-alpha colour.
33
- *
34
- * The glass preset expresses its surfaces and outlines as `rgba(...)`, so a
35
- * hex-only parser silently skips exactly the preset whose translucency makes
36
- * contrast hardest to reason about — which is how glass sat at 1.42:1 unnoticed.
37
- */
38
- function parseColor(value) {
39
- const hex = value.trim().match(/^#([0-9a-f]{6})$/i);
40
- if (hex) {
41
- const n = Number.parseInt(hex[1], 16);
42
- return { rgb: [(n >> 16) & 255, (n >> 8) & 255, n & 255], alpha: 1 };
43
- }
44
- const rgba = value
45
- .trim()
46
- .match(/^rgba?\(\s*([\d.]+)[\s,]+([\d.]+)[\s,]+([\d.]+)(?:[\s,/]+([\d.]+))?\s*\)$/i);
47
- if (rgba) {
48
- return {
49
- rgb: [Number(rgba[1]), Number(rgba[2]), Number(rgba[3])],
50
- alpha: rgba[4] === undefined ? 1 : Number(rgba[4]),
51
- };
52
- }
53
- return null;
54
- }
55
- /** Composite `value` over an opaque backdrop, resolving any alpha. */
56
- function flatten(value, backdrop) {
57
- const parsed = parseColor(value);
58
- if (!parsed)
59
- return null;
60
- const { rgb, alpha } = parsed;
61
- return rgb.map((c, i) => Math.round(c * alpha + backdrop[i] * (1 - alpha)));
62
- }
63
- function relativeLuminance([r, g, b]) {
64
- const [rr, gg, bb] = [r, g, b].map((n) => {
65
- const s = n / 255;
66
- return s <= 0.03928 ? s / 12.92 : ((s + 0.055) / 1.055) ** 2.4;
67
- });
68
- return 0.2126 * rr + 0.7152 * gg + 0.0722 * bb;
69
- }
70
- function contrast(a, b) {
71
- const [hi, lo] = [relativeLuminance(a), relativeLuminance(b)].sort((x, y) => y - x);
72
- return (hi + 0.05) / (lo + 0.05);
73
- }
74
- describe('control boundaries clear the 3:1 non-text floor (#2322)', () => {
75
- for (const theme of Object.values(themes)) {
76
- for (const scheme of ['light', 'dark']) {
77
- const palette = theme[scheme];
78
- it(`${theme.id} ${scheme}: outline is boundary-strength on every surface`, () => {
79
- // Translucent surfaces resolve against the scheme's own background,
80
- // which is the only backdrop the theme can promise.
81
- const page = flatten(palette.background, [255, 255, 255]);
82
- expect(page, `${theme.id} ${scheme} background must be parseable`).not.toBeNull();
83
- const failures = [];
84
- for (const surfaceName of SURFACES) {
85
- // Assert each parse BEFORE it is used as the next one's backdrop.
86
- // Folding these together would let an unparseable surface reach
87
- // `flatten` as a null backdrop, and the test would die on a
88
- // TypeError instead of naming the token that could not be read.
89
- const surface = flatten(palette[surfaceName], page);
90
- expect(surface, `${theme.id} ${scheme} ${surfaceName} must be parseable`).not.toBeNull();
91
- const outline = flatten(palette.outline, surface);
92
- expect(outline, `${theme.id} ${scheme} outline must be parseable`).not.toBeNull();
93
- const ratio = contrast(outline, surface);
94
- if (ratio < 3)
95
- failures.push(`${surfaceName} ${ratio.toFixed(2)}:1`);
96
- }
97
- expect(failures, `${theme.id} ${scheme} outline below 3:1`).toEqual([]);
98
- });
99
- }
100
- }
101
- it('keeps outline and outlineVariant as distinct roles', () => {
102
- // If a preset ever "fixes" its bezel by setting it equal to the boundary,
103
- // dividers become as loud as control edges and the two-token system
104
- // collapses. The hairline is allowed to be quiet — it just may not BE the
105
- // boundary token.
106
- for (const theme of Object.values(themes)) {
107
- for (const scheme of ['light', 'dark']) {
108
- expect(theme[scheme].outlineVariant, `${theme.id} ${scheme} outlineVariant must stay distinct from outline`).not.toBe(theme[scheme].outline);
109
- }
110
- }
111
- });
112
- });
@@ -1,79 +0,0 @@
1
- /**
2
- * Regression tests for createTheme color handling (#1586, epic #1354).
3
- *
4
- * The dark-color derivation and primary-container derivation used to assume the
5
- * primary was 6-digit hex. For `rgb()`, 3-digit hex, or a named color they
6
- * emitted invalid CSS — `rgb(0,122,255)20`, `#f6320`, `#NaNNaNNaN`. Inputs are
7
- * now normalized to RGB first (or rejected with a clear error), so the generated
8
- * palette is always valid CSS.
9
- */
10
- import { describe, expect, it } from 'vitest';
11
- import { createTheme } from '../create-theme';
12
- const HEX6 = /^#[0-9a-fA-F]{6}$/;
13
- const HEX_ALPHA = /^#[0-9a-fA-F]{8}$/;
14
- /** Every color value in a palette must be syntactically valid CSS. */
15
- function expectAllColorsValid(colors) {
16
- for (const [key, value] of Object.entries(colors)) {
17
- expect(typeof value).toBe('string');
18
- // The regression: derivations used to emit '#NaNNaNNaN' / 'rgb(...)20'.
19
- expect(value, `${key}=${value}`).not.toContain('NaN');
20
- // Hex values must be well-formed: exactly 3, 4, 6, or 8 digits (all valid
21
- // CSS). A 5- or 7-digit hex (e.g. the '#f6320' the bug produced) must fail.
22
- // rgb()/rgba() and other formats pass through unchanged.
23
- if (value.startsWith('#')) {
24
- expect(value, `${key}=${value}`).toMatch(/^#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/);
25
- }
26
- }
27
- }
28
- describe('createTheme color handling (#1586)', () => {
29
- it('derives a valid primary container from a 6-digit hex primary', () => {
30
- const theme = createTheme({
31
- id: 'brand6',
32
- name: 'Brand 6',
33
- light: { primary: '#0b57d0', background: '#ffffff' },
34
- });
35
- // 8-digit hex alpha form: primary + "20".
36
- expect(theme.light.primaryContainer).toMatch(HEX_ALPHA);
37
- expect(theme.light.primaryContainer.toLowerCase()).toBe('#0b57d020');
38
- expectAllColorsValid(theme.light);
39
- expectAllColorsValid(theme.dark);
40
- });
41
- it('normalizes an rgb() primary instead of emitting invalid CSS', () => {
42
- const theme = createTheme({
43
- id: 'brandRgb',
44
- name: 'Brand RGB',
45
- light: { primary: 'rgb(0, 122, 255)', background: '#ffffff' },
46
- });
47
- // Was 'rgb(0, 122, 255)20' (invalid); now a normalized 8-digit hex.
48
- expect(theme.light.primaryContainer).not.toContain('rgb(');
49
- expect(theme.light.primaryContainer).toMatch(HEX_ALPHA);
50
- expect(theme.light.primaryContainer.toLowerCase()).toBe('#007aff20');
51
- // Auto-generated dark colors used to be '#NaNNaNNaN' for an rgb() input;
52
- // every value is now valid CSS (hex values are well-formed, rgb() values
53
- // pass through unchanged).
54
- expectAllColorsValid(theme.light);
55
- expectAllColorsValid(theme.dark);
56
- // The brightness-derived dark colors normalize to proper hex.
57
- expect(theme.dark.onPrimary).toMatch(HEX6);
58
- expect(theme.dark.secondary).toMatch(HEX6);
59
- });
60
- it('expands a 3-digit hex primary correctly', () => {
61
- const theme = createTheme({
62
- id: 'brand3',
63
- name: 'Brand 3',
64
- light: { primary: '#07f', background: '#fff' },
65
- });
66
- // '#07f' -> '#0077ff', container '#0077ff20'.
67
- expect(theme.light.primaryContainer.toLowerCase()).toBe('#0077ff20');
68
- expectAllColorsValid(theme.light);
69
- expectAllColorsValid(theme.dark);
70
- expect(theme.dark.onPrimary).toMatch(HEX6);
71
- });
72
- it('throws a clear error for an unsupported named color', () => {
73
- expect(() => createTheme({
74
- id: 'brandNamed',
75
- name: 'Brand Named',
76
- light: { primary: 'tomato', background: '#ffffff' },
77
- })).toThrow(/unsupported color "tomato"/);
78
- });
79
- });