@blotch/dolan 22.2.2 → 22.2.4

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 (259) hide show
  1. package/dist/commands/add.d.ts +6 -2
  2. package/dist/commands/add.js +41 -11
  3. package/dist/commands/add.js.map +1 -1
  4. package/dist/commands/add.test.js +162 -9
  5. package/dist/commands/add.test.js.map +1 -1
  6. package/dist/commands/diff.d.ts +2 -1
  7. package/dist/commands/diff.js +38 -25
  8. package/dist/commands/diff.js.map +1 -1
  9. package/dist/commands/diff.test.js +72 -11
  10. package/dist/commands/diff.test.js.map +1 -1
  11. package/dist/commands/doctor.js +228 -34
  12. package/dist/commands/doctor.js.map +1 -1
  13. package/dist/commands/doctor.test.js +367 -10
  14. package/dist/commands/doctor.test.js.map +1 -1
  15. package/dist/commands/eject.js +7 -0
  16. package/dist/commands/eject.js.map +1 -1
  17. package/dist/commands/eject.test.js +39 -0
  18. package/dist/commands/eject.test.js.map +1 -1
  19. package/dist/commands/info.js +3 -2
  20. package/dist/commands/info.js.map +1 -1
  21. package/dist/commands/init.d.ts +37 -1
  22. package/dist/commands/init.js +169 -40
  23. package/dist/commands/init.js.map +1 -1
  24. package/dist/commands/init.test.js +392 -27
  25. package/dist/commands/init.test.js.map +1 -1
  26. package/dist/commands/install-packages.d.ts +6 -0
  27. package/dist/commands/install-packages.js +20 -0
  28. package/dist/commands/install-packages.js.map +1 -0
  29. package/dist/commands/install-packages.test.d.ts +1 -0
  30. package/dist/commands/install-packages.test.js +55 -0
  31. package/dist/commands/install-packages.test.js.map +1 -0
  32. package/dist/commands/list.js +21 -14
  33. package/dist/commands/list.js.map +1 -1
  34. package/dist/commands/list.test.js +62 -5
  35. package/dist/commands/list.test.js.map +1 -1
  36. package/dist/commands/outdated.js +11 -10
  37. package/dist/commands/outdated.js.map +1 -1
  38. package/dist/commands/outdated.test.js +20 -5
  39. package/dist/commands/outdated.test.js.map +1 -1
  40. package/dist/commands/remove.d.ts +4 -0
  41. package/dist/commands/remove.js +17 -4
  42. package/dist/commands/remove.js.map +1 -1
  43. package/dist/commands/remove.test.js +53 -2
  44. package/dist/commands/remove.test.js.map +1 -1
  45. package/dist/commands/theme.js +1 -1
  46. package/dist/commands/theme.js.map +1 -1
  47. package/dist/commands/update.d.ts +11 -0
  48. package/dist/commands/update.js +174 -58
  49. package/dist/commands/update.js.map +1 -1
  50. package/dist/commands/update.test.js +587 -33
  51. package/dist/commands/update.test.js.map +1 -1
  52. package/dist/index.js +7 -0
  53. package/dist/index.js.map +1 -1
  54. package/dist/lockfile.d.ts +7 -0
  55. package/dist/lockfile.js +36 -0
  56. package/dist/lockfile.js.map +1 -1
  57. package/dist/lockfile.test.d.ts +1 -0
  58. package/dist/lockfile.test.js +25 -0
  59. package/dist/lockfile.test.js.map +1 -0
  60. package/dist/registry.d.ts +14 -0
  61. package/dist/registry.js +78 -24
  62. package/dist/registry.js.map +1 -1
  63. package/dist/registry.test.js +119 -38
  64. package/dist/registry.test.js.map +1 -1
  65. package/dist/test-utils.d.ts +5 -0
  66. package/dist/test-utils.js +6 -1
  67. package/dist/test-utils.js.map +1 -1
  68. package/dist/testing/fire-lynx-event.d.ts +14 -0
  69. package/dist/testing/fire-lynx-event.js +19 -0
  70. package/dist/testing/fire-lynx-event.js.map +1 -0
  71. package/dist/testing/fire-lynx-event.test.d.ts +1 -0
  72. package/dist/testing/fire-lynx-event.test.js +44 -0
  73. package/dist/testing/fire-lynx-event.test.js.map +1 -0
  74. package/dist/testing/lynx-main-thread.d.ts +22 -0
  75. package/dist/testing/lynx-main-thread.js +11 -0
  76. package/dist/testing/lynx-main-thread.js.map +1 -0
  77. package/dist/testing/lynx-main-thread.test.d.ts +1 -0
  78. package/dist/testing/lynx-main-thread.test.js +24 -0
  79. package/dist/testing/lynx-main-thread.test.js.map +1 -0
  80. package/dist/testing/set-input-signal.d.ts +14 -0
  81. package/dist/testing/set-input-signal.js +21 -0
  82. package/dist/testing/set-input-signal.js.map +1 -0
  83. package/dist/testing/set-input-signal.test.d.ts +1 -0
  84. package/dist/testing/set-input-signal.test.js +20 -0
  85. package/dist/testing/set-input-signal.test.js.map +1 -0
  86. package/dist/ui/components/accordion/accordion.test.ts +158 -144
  87. package/dist/ui/components/accordion/accordion.ts +19 -14
  88. package/dist/ui/components/action-sheet/action-sheet.test.ts +283 -151
  89. package/dist/ui/components/action-sheet/action-sheet.ts +77 -43
  90. package/dist/ui/components/alert/alert.test.ts +67 -19
  91. package/dist/ui/components/alert/alert.ts +14 -16
  92. package/dist/ui/components/alert-dialog/alert-dialog.test.ts +208 -94
  93. package/dist/ui/components/alert-dialog/alert-dialog.ts +75 -38
  94. package/dist/ui/components/area-chart/area-chart.test.ts +134 -119
  95. package/dist/ui/components/area-chart/area-chart.ts +100 -315
  96. package/dist/ui/components/area-chart/index.ts +2 -3
  97. package/dist/ui/components/avatar/avatar.test.ts +89 -45
  98. package/dist/ui/components/badge/badge.test.ts +57 -23
  99. package/dist/ui/components/badge/badge.ts +12 -14
  100. package/dist/ui/components/bar-chart/bar-chart.test.ts +172 -45
  101. package/dist/ui/components/bar-chart/bar-chart.ts +140 -294
  102. package/dist/ui/components/bar-chart/index.ts +3 -3
  103. package/dist/ui/components/bottom-sheet/bottom-sheet.test.ts +386 -128
  104. package/dist/ui/components/bottom-sheet/bottom-sheet.ts +170 -53
  105. package/dist/ui/components/button/button.test.ts +611 -2
  106. package/dist/ui/components/button/button.ts +434 -49
  107. package/dist/ui/components/button/index.ts +2 -0
  108. package/dist/ui/components/button-group/button-group.test.ts +484 -2
  109. package/dist/ui/components/button-group/button-group.ts +380 -68
  110. package/dist/ui/components/button-group/index.ts +3 -0
  111. package/dist/ui/components/candlestick-chart/candlestick-chart.test.ts +183 -58
  112. package/dist/ui/components/candlestick-chart/candlestick-chart.ts +150 -323
  113. package/dist/ui/components/candlestick-chart/index.ts +3 -4
  114. package/dist/ui/components/card/card.test.ts +82 -62
  115. package/dist/ui/components/chart/chart-focus.test.ts +132 -0
  116. package/dist/ui/components/chart/chart-focus.ts +115 -0
  117. package/dist/ui/components/chart/chart-presets.test.ts +335 -0
  118. package/dist/ui/components/chart/chart-presets.ts +278 -0
  119. package/dist/ui/components/chart/chart-scene.test.ts +589 -0
  120. package/dist/ui/components/chart/chart-scene.ts +483 -0
  121. package/dist/ui/components/chart/chart-theme.test.ts +85 -0
  122. package/dist/ui/components/chart/chart-theme.ts +210 -0
  123. package/dist/ui/components/chart/chart-zoom.test.ts +159 -0
  124. package/dist/ui/components/chart/chart-zoom.ts +162 -0
  125. package/dist/ui/components/chart/chart.test.ts +1017 -0
  126. package/dist/ui/components/chart/chart.ts +934 -0
  127. package/dist/ui/components/chart/index.ts +24 -0
  128. package/dist/ui/components/checkbox/checkbox.test.ts +124 -42
  129. package/dist/ui/components/checkbox/checkbox.ts +23 -23
  130. package/dist/ui/components/collapsible/collapsible.test.ts +158 -81
  131. package/dist/ui/components/collapsible/collapsible.ts +19 -14
  132. package/dist/ui/components/context-menu/context-menu.test.ts +224 -0
  133. package/dist/ui/components/context-menu/context-menu.ts +110 -0
  134. package/dist/ui/components/context-menu/index.ts +1 -0
  135. package/dist/ui/components/dialog/dialog.test.ts +213 -108
  136. package/dist/ui/components/dialog/dialog.ts +72 -35
  137. package/dist/ui/components/dock/dock-scroll.test.ts +130 -0
  138. package/dist/ui/components/dock/dock-scroll.ts +133 -0
  139. package/dist/ui/components/dock/dock.test.ts +749 -0
  140. package/dist/ui/components/dock/dock.ts +960 -0
  141. package/dist/ui/components/dock/index.ts +2 -0
  142. package/dist/ui/components/empty-state/empty-state.test.ts +28 -39
  143. package/dist/ui/components/glass/concentric-radius.test.ts +13 -0
  144. package/dist/ui/components/glass/concentric-radius.ts +17 -0
  145. package/dist/ui/components/glass/glass-design.test.ts +78 -0
  146. package/dist/ui/components/glass/glass-design.ts +119 -0
  147. package/dist/ui/components/glass/glass.test.ts +338 -0
  148. package/dist/ui/components/glass/glass.ts +360 -0
  149. package/dist/ui/components/glass/index.ts +18 -0
  150. package/dist/ui/components/icon/icon-theme.test.ts +169 -0
  151. package/dist/ui/components/icon/icon-theme.ts +227 -0
  152. package/dist/ui/components/icon/icon.test.ts +85 -25
  153. package/dist/ui/components/icon/icon.ts +38 -21
  154. package/dist/ui/components/input/input.test.ts +178 -101
  155. package/dist/ui/components/input/input.ts +15 -12
  156. package/dist/ui/components/label/label.test.ts +11 -24
  157. package/dist/ui/components/line-chart/index.ts +2 -3
  158. package/dist/ui/components/line-chart/line-chart.test.ts +149 -56
  159. package/dist/ui/components/line-chart/line-chart.ts +83 -241
  160. package/dist/ui/components/list/list.test.ts +119 -21
  161. package/dist/ui/components/list/list.ts +21 -15
  162. package/dist/ui/components/menu/index.ts +25 -0
  163. package/dist/ui/components/menu/menu-placement.test.ts +153 -0
  164. package/dist/ui/components/menu/menu-placement.ts +159 -0
  165. package/dist/ui/components/menu/menu-popup.test.ts +632 -0
  166. package/dist/ui/components/menu/menu-popup.ts +544 -0
  167. package/dist/ui/components/menu/menu.test.ts +429 -0
  168. package/dist/ui/components/menu/menu.ts +415 -0
  169. package/dist/ui/components/nav-drawer/nav-drawer.test.ts +435 -191
  170. package/dist/ui/components/nav-drawer/nav-drawer.ts +127 -56
  171. package/dist/ui/components/pagination/pagination.test.ts +177 -65
  172. package/dist/ui/components/pagination/pagination.ts +55 -8
  173. package/dist/ui/components/progress/progress.test.ts +102 -46
  174. package/dist/ui/components/progress/progress.ts +52 -54
  175. package/dist/ui/components/radio-group/radio-group.test.ts +128 -83
  176. package/dist/ui/components/radio-group/radio-group.ts +24 -24
  177. package/dist/ui/components/scatter-chart/index.ts +2 -3
  178. package/dist/ui/components/scatter-chart/scatter-chart.test.ts +131 -26
  179. package/dist/ui/components/scatter-chart/scatter-chart.ts +104 -250
  180. package/dist/ui/components/scroll-area/scroll-area.test.ts +95 -24
  181. package/dist/ui/components/scroll-area/scroll-area.ts +8 -8
  182. package/dist/ui/components/select/select.test.ts +138 -78
  183. package/dist/ui/components/select/select.ts +17 -11
  184. package/dist/ui/components/separator/separator.test.ts +18 -17
  185. package/dist/ui/components/sheet/sheet.test.ts +64 -11
  186. package/dist/ui/components/sheet/sheet.ts +9 -1
  187. package/dist/ui/components/skeleton/skeleton.test.ts +24 -7
  188. package/dist/ui/components/skeleton/skeleton.ts +10 -16
  189. package/dist/ui/components/spinner/spinner.test.ts +67 -22
  190. package/dist/ui/components/spinner/spinner.ts +48 -41
  191. package/dist/ui/components/switch/switch.test.ts +116 -46
  192. package/dist/ui/components/switch/switch.ts +8 -13
  193. package/dist/ui/components/tab-bar/index.ts +1 -0
  194. package/dist/ui/components/tab-bar/tab-bar.test.ts +280 -0
  195. package/dist/ui/components/tab-bar/tab-bar.ts +230 -0
  196. package/dist/ui/components/tabs/tabs.test.ts +215 -157
  197. package/dist/ui/components/tabs/tabs.ts +110 -118
  198. package/dist/ui/components/textarea/textarea.test.ts +206 -100
  199. package/dist/ui/components/textarea/textarea.ts +14 -12
  200. package/dist/ui/components/toast/toast.test.ts +436 -315
  201. package/dist/ui/components/toast/toast.ts +62 -55
  202. package/dist/ui/components/toggle/toggle.test.ts +247 -140
  203. package/dist/ui/theme/chart-colors.test.ts +35 -0
  204. package/dist/ui/theme/dark.css +46 -2
  205. package/dist/ui/theme/default.css +63 -7
  206. package/dist/ui/theme/icon-colors.test.ts +35 -0
  207. package/dist/ui/theme/rgb-channels.test.ts +84 -0
  208. package/dist/ui/theme/tailwind-build.test.ts +114 -0
  209. package/dist/ui/theme/tailwind-css.test.ts +83 -0
  210. package/dist/ui/theme/tailwind-plugin.test.ts +42 -2
  211. package/dist/ui/theme/tailwind-plugin.ts +98 -25
  212. package/dist/ui/theme/tailwind.css +113 -0
  213. package/dist/ui/utils/animate.test.ts +13 -1
  214. package/dist/utils/analyze.d.ts +14 -0
  215. package/dist/utils/analyze.js +15 -0
  216. package/dist/utils/analyze.js.map +1 -1
  217. package/dist/utils/analyze.test.js +14 -1
  218. package/dist/utils/analyze.test.js.map +1 -1
  219. package/dist/utils/installed.d.ts +29 -0
  220. package/dist/utils/installed.js +43 -0
  221. package/dist/utils/installed.js.map +1 -0
  222. package/dist/utils/installed.test.d.ts +1 -0
  223. package/dist/utils/installed.test.js +57 -0
  224. package/dist/utils/installed.test.js.map +1 -0
  225. package/dist/utils/interactive.d.ts +9 -0
  226. package/dist/utils/interactive.js +10 -0
  227. package/dist/utils/interactive.js.map +1 -0
  228. package/dist/utils/interactive.test.d.ts +1 -0
  229. package/dist/utils/interactive.test.js +24 -0
  230. package/dist/utils/interactive.test.js.map +1 -0
  231. package/dist/utils/packages.d.ts +36 -0
  232. package/dist/utils/packages.js +70 -0
  233. package/dist/utils/packages.js.map +1 -0
  234. package/dist/utils/packages.test.d.ts +1 -0
  235. package/dist/utils/packages.test.js +89 -0
  236. package/dist/utils/packages.test.js.map +1 -0
  237. package/dist/utils/resolve-paths.d.ts +17 -1
  238. package/dist/utils/resolve-paths.js +21 -3
  239. package/dist/utils/resolve-paths.js.map +1 -1
  240. package/dist/utils/resolve-paths.test.js +38 -2
  241. package/dist/utils/resolve-paths.test.js.map +1 -1
  242. package/dist/utils/tailwind.d.ts +25 -0
  243. package/dist/utils/tailwind.js +51 -0
  244. package/dist/utils/tailwind.js.map +1 -0
  245. package/dist/utils/tailwind.test.d.ts +1 -0
  246. package/dist/utils/tailwind.test.js +71 -0
  247. package/dist/utils/tailwind.test.js.map +1 -0
  248. package/package.json +13 -1
  249. package/dist/ui/components/area-chart/area-chart-render.test.ts +0 -121
  250. package/dist/ui/components/bar-chart/bar-chart-render.test.ts +0 -174
  251. package/dist/ui/components/button/button-render.test.ts +0 -113
  252. package/dist/ui/components/button-group/button-group-render.test.ts +0 -146
  253. package/dist/ui/components/candlestick-chart/candlestick-chart-render.test.ts +0 -174
  254. package/dist/ui/components/cartesian-chart/cartesian-chart-render.test.ts +0 -188
  255. package/dist/ui/components/cartesian-chart/cartesian-chart.test.ts +0 -525
  256. package/dist/ui/components/cartesian-chart/cartesian-chart.ts +0 -1140
  257. package/dist/ui/components/cartesian-chart/index.ts +0 -18
  258. package/dist/ui/components/line-chart/line-chart-render.test.ts +0 -121
  259. package/dist/ui/components/scatter-chart/scatter-chart-render.test.ts +0 -157
@@ -0,0 +1,429 @@
1
+ import { Component, signal } from '@angular/core';
2
+ import {
3
+ fireEvent,
4
+ render,
5
+ waitForUpdate,
6
+ within,
7
+ } from '@blotch/angular-lynx-testing-library';
8
+ import { LYNX_ELEMENTS, LynxAccessibilitySettings } from '@blotch/angular-lynx';
9
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
10
+ import { fireLynxEvent } from '../../../test-utils/fire-lynx-event';
11
+ import {
12
+ UiMenu,
13
+ UiMenuItem,
14
+ UiMenuSection,
15
+ UiMenuSeparator,
16
+ UiMenuTrigger,
17
+ } from './menu';
18
+
19
+ @Component({
20
+ imports: [
21
+ LYNX_ELEMENTS,
22
+ UiMenu,
23
+ UiMenuTrigger,
24
+ UiMenuItem,
25
+ UiMenuSection,
26
+ UiMenuSeparator,
27
+ ],
28
+ template: `
29
+ <ui-menu
30
+ [open]="open()"
31
+ (openChange)="open.set($event)"
32
+ (closed)="closed()"
33
+ >
34
+ @if (withTrigger()) {
35
+ <view
36
+ uiMenuTrigger
37
+ class="trigger"
38
+ [accessibility-element]="true"
39
+ accessibility-traits="button"
40
+ accessibility-label="Sort"
41
+ >
42
+ <text>Sort</text>
43
+ </view>
44
+ }
45
+ <ui-menu-section title="Sort by">
46
+ <ui-menu-item
47
+ label="Name"
48
+ [checked]="sort() === 'name'"
49
+ (pressed)="sort.set('name')"
50
+ />
51
+ <ui-menu-item
52
+ label="Date"
53
+ [checked]="sort() === 'date'"
54
+ (pressed)="sort.set('date')"
55
+ />
56
+ </ui-menu-section>
57
+ <ui-menu-separator />
58
+ <ui-menu-section>
59
+ <ui-menu-item label="Share" icon="upload" (pressed)="shared()" />
60
+ <ui-menu-item
61
+ label="Delete"
62
+ icon="trash"
63
+ variant="destructive"
64
+ (pressed)="deleted()"
65
+ />
66
+ <ui-menu-item
67
+ label="Archive"
68
+ accessibilityLabel="Archive, unavailable offline"
69
+ disabled
70
+ (pressed)="archived()"
71
+ />
72
+ </ui-menu-section>
73
+ </ui-menu>
74
+ `,
75
+ })
76
+ class MenuHost {
77
+ readonly open = signal(false);
78
+ readonly withTrigger = signal(true);
79
+ readonly sort = signal<'name' | 'date'>('date');
80
+ readonly closed = vi.fn();
81
+ readonly shared = vi.fn();
82
+ readonly deleted = vi.fn();
83
+ readonly archived = vi.fn();
84
+ }
85
+
86
+ /**
87
+ * The Lynx function that runs an element's UI methods, such as measuring it
88
+ * (`boundingClientRect`). The testing library defines it on the global
89
+ * object.
90
+ */
91
+ type InvokeUIMethod = {
92
+ __InvokeUIMethod: (
93
+ element: Element,
94
+ method: string,
95
+ params: Record<string, unknown>,
96
+ callback: (result: { code: number; data?: unknown }) => void,
97
+ ) => void;
98
+ };
99
+
100
+ /** Accessibility settings a test can change, as the host app would. */
101
+ const settings = {
102
+ reduceMotion: signal(false),
103
+ reduceTransparency: signal(false),
104
+ increaseContrast: signal(false),
105
+ };
106
+
107
+ const IOS_26 = { platform: 'iOS', osVersion: '26.0' };
108
+ const IOS_18 = { platform: 'iOS', osVersion: '18.5' };
109
+
110
+ /**
111
+ * Renders the menu on a platform (none for Material), measuring elements as
112
+ * `boundingClientRect` would on a phone: the safe area (the element that
113
+ * holds the menu) a phone's screen, the menu's content 120pt tall, and
114
+ * anything else, the trigger, a small box near the top.
115
+ */
116
+ const renderMenu = async (
117
+ platform: { platform: string; osVersion: string } | null = IOS_26,
118
+ ) => {
119
+ vi.stubGlobal('SystemInfo', platform ?? undefined);
120
+ const invoke = vi
121
+ .spyOn(globalThis as unknown as InvokeUIMethod, '__InvokeUIMethod')
122
+ .mockImplementation((element, method, _params, callback) => {
123
+ if (method !== 'boundingClientRect') {
124
+ callback({ code: 0 });
125
+ return;
126
+ }
127
+ const data = element.querySelector(':scope > blur-view[ui-glass]')
128
+ ? { left: 0, top: 0, width: 402, height: 800 }
129
+ : element.parentElement?.tagName.toLowerCase() === 'scroll-view'
130
+ ? { left: 0, top: 0, width: 250, height: 120 }
131
+ : { left: 16, top: 80, width: 80, height: 44 };
132
+ callback({ code: 0, data });
133
+ });
134
+ const result = await render(MenuHost, {
135
+ providers: [{ provide: LynxAccessibilitySettings, useValue: settings }],
136
+ });
137
+ const host = result.componentRef.instance as MenuHost;
138
+ const overlay = result.container.querySelector('overlay')!;
139
+ return {
140
+ ...result,
141
+ host,
142
+ invoke,
143
+ overlay,
144
+ menu: within(overlay as HTMLElement),
145
+ trigger: () => result.container.querySelector('.trigger') as HTMLElement,
146
+ panel: () => overlay.querySelector('blur-view[ui-glass]')!,
147
+ };
148
+ };
149
+
150
+ type Rendered = Awaited<ReturnType<typeof renderMenu>>;
151
+
152
+ /**
153
+ * Waits until the menu has opened.
154
+ */
155
+ const waitForOpen = (rendered: Rendered) =>
156
+ vi.waitFor(() =>
157
+ expect(rendered.panel().getAttribute('accessibility-exclusive-focus')).toBe(
158
+ 'true',
159
+ ),
160
+ );
161
+
162
+ /**
163
+ * Renders the menu and opens it: a screen reader, and the queries, reach
164
+ * its items only while it shows.
165
+ */
166
+ const renderOpenMenu = async (
167
+ platform: { platform: string; osVersion: string } | null = IOS_26,
168
+ ) => {
169
+ const rendered = await renderMenu(platform);
170
+ rendered.host.open.set(true);
171
+ await waitForOpen(rendered);
172
+ return rendered;
173
+ };
174
+
175
+ /**
176
+ * Whether `first` comes before `second` in the document.
177
+ */
178
+ const precedes = (first: Node, second: Node) =>
179
+ !!(first.compareDocumentPosition(second) & Node.DOCUMENT_POSITION_FOLLOWING);
180
+
181
+ /**
182
+ * Waits until the menu has closed and its animation has finished.
183
+ */
184
+ const waitForClose = async (rendered: Rendered) => {
185
+ await vi.waitFor(() => expect(rendered.host.closed).toHaveBeenCalled());
186
+ await waitForUpdate();
187
+ };
188
+
189
+ const style = (element: Element, name: string) =>
190
+ (element as HTMLElement).style.getPropertyValue(name);
191
+
192
+ /**
193
+ * An item's row: the element a screen reader reads as a button.
194
+ */
195
+ const row = (rendered: Rendered, name: string | RegExp) =>
196
+ rendered.menu.getByRole('button', { name });
197
+
198
+ // The renderer warns that the components' selectors aren't Lynx elements.
199
+ beforeEach(() => {
200
+ vi.spyOn(console, 'warn').mockImplementation(() => {});
201
+ });
202
+
203
+ afterEach(() => {
204
+ vi.restoreAllMocks();
205
+ vi.unstubAllGlobals();
206
+ });
207
+
208
+ describe('UiMenu', () => {
209
+ it('opens from its trigger and grows out of it, hiding it meanwhile', async () => {
210
+ const rendered = await renderMenu();
211
+ const { host, trigger, overlay, invoke } = rendered;
212
+ expect(overlay.getAttribute('visible')).toBe('false');
213
+
214
+ fireEvent.tap(trigger());
215
+ await waitForOpen(rendered);
216
+
217
+ expect(host.open()).toBe(true);
218
+ expect(overlay.getAttribute('visible')).toBe('true');
219
+ // Measured from the trigger.
220
+ expect(invoke).toHaveBeenCalledWith(
221
+ trigger(),
222
+ 'boundingClientRect',
223
+ { relativeTo: 'screen' },
224
+ expect.any(Function),
225
+ );
226
+ expect(style(trigger(), 'opacity')).toBe('0');
227
+
228
+ // A tap on the trigger again closes it, and the trigger comes back.
229
+ fireEvent.tap(trigger());
230
+ await waitForClose(rendered);
231
+ expect(host.open()).toBe(false);
232
+ expect(style(trigger(), 'opacity')).toBe('');
233
+ });
234
+
235
+ it('closes when an item is chosen, after telling it', async () => {
236
+ const rendered = await renderMenu();
237
+ rendered.host.open.set(true);
238
+ await waitForOpen(rendered);
239
+
240
+ fireEvent.tap(row(rendered, 'Share'));
241
+
242
+ expect(rendered.host.shared).toHaveBeenCalledTimes(1);
243
+ expect(rendered.host.open()).toBe(false);
244
+ await waitForClose(rendered);
245
+ expect(rendered.overlay.getAttribute('visible')).toBe('false');
246
+ });
247
+
248
+ it('closes on a tap outside it', async () => {
249
+ const rendered = await renderMenu();
250
+ rendered.host.open.set(true);
251
+ await waitForOpen(rendered);
252
+
253
+ fireEvent.tap(rendered.overlay.firstElementChild!);
254
+
255
+ expect(rendered.host.open()).toBe(false);
256
+ await waitForClose(rendered);
257
+ });
258
+
259
+ it('grows out of the menu itself without a marked trigger', async () => {
260
+ const rendered = await renderMenu();
261
+ rendered.host.withTrigger.set(false);
262
+ await waitForUpdate();
263
+
264
+ rendered.host.open.set(true);
265
+ await waitForOpen(rendered);
266
+
267
+ // The menu's own element holds the overlay's element.
268
+ expect(rendered.invoke).toHaveBeenCalledWith(
269
+ rendered.overlay.parentElement!.parentElement,
270
+ 'boundingClientRect',
271
+ { relativeTo: 'screen' },
272
+ expect.any(Function),
273
+ );
274
+ });
275
+
276
+ it('opens beside its trigger on Material, leaving it in view', async () => {
277
+ const rendered = await renderMenu(null);
278
+
279
+ fireEvent.tap(rendered.trigger());
280
+ await waitForOpen(rendered);
281
+
282
+ expect(style(rendered.trigger(), 'opacity')).toBe('');
283
+ // Below the trigger, a gap away.
284
+ expect(style(rendered.panel(), 'top')).toBe(`${80 + 44 + 8}px`);
285
+ });
286
+ });
287
+
288
+ describe('UiMenuItem', () => {
289
+ it('is a button named by its label, and says when it is destructive', async () => {
290
+ const rendered = await renderOpenMenu();
291
+
292
+ expect(row(rendered, 'Share')).toBeTruthy();
293
+ expect(row(rendered, 'Delete, destructive')).toBeTruthy();
294
+ expect(row(rendered, 'Archive, unavailable offline')).toBeTruthy();
295
+ expect(
296
+ within(row(rendered, 'Delete, destructive')).getByText('Delete')
297
+ .className,
298
+ ).toContain('text-destructive');
299
+ expect(
300
+ within(row(rendered, 'Share')).getByText('Share').className,
301
+ ).toContain('text-glass-foreground');
302
+ });
303
+
304
+ it('shows a checkmark, and is selected to a screen reader, when checked', async () => {
305
+ const rendered = await renderOpenMenu();
306
+ const date = row(rendered, 'Date');
307
+ const name = row(rendered, 'Name');
308
+
309
+ expect(date.getAttribute('accessibility-traits')).toBe('button,selected');
310
+ expect(name.getAttribute('accessibility-traits')).toBe('button');
311
+ // Both keep the checkmark's column, so the labels line up.
312
+ expect(date.querySelector('.h-5.w-5 svg')).not.toBeNull();
313
+ expect(name.querySelector('.h-5.w-5')).not.toBeNull();
314
+ expect(name.querySelector('.h-5.w-5 svg')).toBeNull();
315
+
316
+ fireEvent.tap(name);
317
+ await waitForUpdate();
318
+ expect(rendered.host.sort()).toBe('name');
319
+ expect(name.getAttribute('accessibility-traits')).toBe('button,selected');
320
+ expect(date.getAttribute('accessibility-traits')).toBe('button');
321
+ await waitForClose(rendered);
322
+ });
323
+
324
+ it('highlights while pressed, by touch or mouse', async () => {
325
+ const rendered = await renderOpenMenu();
326
+ const share = row(rendered, 'Share');
327
+ const highlight = share.firstElementChild!;
328
+ expect(style(highlight, 'opacity')).toBe('0');
329
+
330
+ for (const [down, up] of [
331
+ ['touchstart', 'touchend'],
332
+ ['touchstart', 'touchcancel'],
333
+ ['mousedown', 'mouseup'],
334
+ ['mousedown', 'mouseleave'],
335
+ ]) {
336
+ fireLynxEvent(share, down!);
337
+ await waitForUpdate();
338
+ expect(style(highlight, 'opacity')).toBe('0.1');
339
+ fireLynxEvent(share, up!);
340
+ await waitForUpdate();
341
+ expect(style(highlight, 'opacity')).toBe('0');
342
+ }
343
+ });
344
+
345
+ it("can't be pressed or chosen when disabled", async () => {
346
+ const rendered = await renderOpenMenu();
347
+ const archive = row(rendered, 'Archive, unavailable offline');
348
+
349
+ expect(archive.className).toContain('opacity-40');
350
+ fireEvent.touchstart(archive);
351
+ await waitForUpdate();
352
+ expect(style(archive.firstElementChild!, 'opacity')).toBe('0');
353
+ fireEvent.tap(archive);
354
+
355
+ expect(rendered.host.archived).not.toHaveBeenCalled();
356
+ expect(rendered.host.open()).toBe(true);
357
+ });
358
+
359
+ it('lays out as iOS 26 does: 44pt rows with the icon first', async () => {
360
+ const rendered = await renderOpenMenu();
361
+ const share = row(rendered, 'Share');
362
+ const label = within(share).getByText('Share');
363
+
364
+ expect(share.className).toContain('min-h-[44px]');
365
+ expect(label.className).toContain('text-[17px]');
366
+ expect(precedes(share.querySelector('svg')!, label)).toBe(true);
367
+ });
368
+
369
+ it('puts the icon after the label before iOS 26', async () => {
370
+ const rendered = await renderOpenMenu(IOS_18);
371
+ const share = row(rendered, 'Share');
372
+
373
+ expect(
374
+ precedes(within(share).getByText('Share'), share.querySelector('svg')!),
375
+ ).toBe(true);
376
+ });
377
+
378
+ it('lays out as Material 3 does: 48dp rows with a leading icon', async () => {
379
+ const rendered = await renderOpenMenu(null);
380
+ const share = row(rendered, 'Share');
381
+ const label = within(share).getByText('Share');
382
+
383
+ expect(share.className).toContain('min-h-[48px]');
384
+ expect(label.className).toContain('text-[16px]');
385
+ expect(precedes(share.querySelector('svg')!, label)).toBe(true);
386
+ });
387
+ });
388
+
389
+ describe('UiMenuSection and UiMenuSeparator', () => {
390
+ it('titles a section, as a heading', async () => {
391
+ const rendered = await renderOpenMenu();
392
+
393
+ const title = rendered.menu.getByRole('heading', { name: 'Sort by' });
394
+ expect(title.className).toContain('px-4');
395
+ // The untitled section has no heading.
396
+ expect(rendered.menu.getAllByRole('heading')).toHaveLength(1);
397
+ });
398
+
399
+ it('indents a Material section title as Material does', async () => {
400
+ const rendered = await renderOpenMenu(null);
401
+
402
+ expect(
403
+ rendered.menu.getByRole('heading', { name: 'Sort by' }).className,
404
+ ).toContain('px-3');
405
+ });
406
+
407
+ it('parts sections with a line, or a band before iOS 26', async () => {
408
+ const separatorLine = async (
409
+ platform: { platform: string; osVersion: string } | null,
410
+ ) => {
411
+ const rendered = await renderMenu(platform);
412
+ const line = rendered.overlay.querySelector(
413
+ '[class~="h-px"], [class~="h-2"]',
414
+ )!.className;
415
+ rendered.unmount();
416
+ return line;
417
+ };
418
+
419
+ expect((await separatorLine(IOS_26)).split(' ')).toEqual(
420
+ expect.arrayContaining(['h-px', 'bg-glass-foreground', 'opacity-20']),
421
+ );
422
+ expect((await separatorLine(IOS_18)).split(' ')).toEqual(
423
+ expect.arrayContaining(['h-2', 'bg-glass-foreground', 'opacity-10']),
424
+ );
425
+ expect((await separatorLine(null)).split(' ')).toEqual(
426
+ expect.arrayContaining(['h-px', 'bg-border']),
427
+ );
428
+ });
429
+ });