@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
@@ -1,199 +1,257 @@
1
- import { Component, signal } from '@angular/core';
2
- import { render, waitForUpdate } from '@blotch/angular-lynx-testing-library';
1
+ import { Component, signal, viewChild } from '@angular/core';
2
+ import {
3
+ fireEvent,
4
+ render,
5
+ waitForUpdate,
6
+ } from '@blotch/angular-lynx-testing-library';
3
7
  import { LYNX_ELEMENTS } from '@blotch/angular-lynx';
4
- import { describe, expect, it } from 'vitest';
5
- import { setInputSignal } from '../../../test-utils/set-input-signal';
8
+ import {
9
+ type MockInstance,
10
+ afterEach,
11
+ beforeEach,
12
+ describe,
13
+ expect,
14
+ it,
15
+ vi,
16
+ } from 'vitest';
17
+ import {
18
+ type LynxMainThread,
19
+ lynxMainThread,
20
+ } from '../../../test-utils/lynx-main-thread';
6
21
  import { UiTabs, UiTabsContent, UiTabsList, UiTabsTrigger } from './tabs';
7
22
 
8
23
  @Component({
9
- standalone: true,
10
24
  imports: [UiTabs, UiTabsList, UiTabsTrigger, UiTabsContent, LYNX_ELEMENTS],
11
25
  template: `
12
- <ui-tabs [(value)]="value">
13
- <ui-tabs-list>
14
- <ui-tabs-trigger value="a">A</ui-tabs-trigger>
15
- <ui-tabs-trigger value="b">B</ui-tabs-trigger>
26
+ <ui-tabs [(value)]="value" (changed)="changes.push($event)" class="w-80">
27
+ <ui-tabs-list class="mb-2">
28
+ <ui-tabs-trigger value="account" class="px-4">Account</ui-tabs-trigger>
29
+ <ui-tabs-trigger value="billing">Billing</ui-tabs-trigger>
30
+ <ui-tabs-trigger value="contact">Contact</ui-tabs-trigger>
16
31
  </ui-tabs-list>
17
- <ui-tabs-content value="a"><text>Panel A</text></ui-tabs-content>
18
- <ui-tabs-content value="b"><text>Panel B</text></ui-tabs-content>
32
+ <ui-tabs-content value="account" class="gap-2">
33
+ <text>Account panel</text>
34
+ </ui-tabs-content>
35
+ <ui-tabs-content value="billing">
36
+ <text>Billing panel</text>
37
+ </ui-tabs-content>
38
+ <ui-tabs-content value="contact">
39
+ <text>Contact panel</text>
40
+ </ui-tabs-content>
19
41
  </ui-tabs>
20
42
  `,
21
43
  })
22
44
  class TabsHost {
23
- value = 'a';
45
+ readonly value = signal('account');
46
+ readonly changes: string[] = [];
47
+ readonly tabs = viewChild.required(UiTabs);
24
48
  }
25
49
 
50
+ const renderTabs = async () => {
51
+ const result = await render(TabsHost);
52
+ /**
53
+ * A trigger's own view, which holds the tap listener, around its label.
54
+ */
55
+ const trigger = (label: string) => result.getByText(label).parentElement!;
56
+ /**
57
+ * A panel's own view, which wraps its projected text.
58
+ */
59
+ const panel = (text: string) => result.getByText(text).parentElement!;
60
+ return {
61
+ ...result,
62
+ host: result.componentRef.instance as TabsHost,
63
+ trigger,
64
+ panel,
65
+ /** The display of a panel's host element, which shows or hides it. */
66
+ displayOf: (text: string) =>
67
+ (panel(text).parentElement as HTMLElement).style.display,
68
+ tap: async (label: string) => {
69
+ fireEvent.tap(trigger(label));
70
+ await waitForUpdate();
71
+ },
72
+ };
73
+ };
74
+
75
+ /** `__ElementAnimate`'s first argument for starting an animation. */
76
+ const START = 0;
77
+ /** `__ElementAnimate`'s first argument for canceling an animation. */
78
+ const CANCEL = 3;
79
+
26
80
  describe('UiTabs', () => {
27
- it('renders the tab triggers and the active panel', async () => {
28
- const { getByText } = await render(TabsHost);
29
- expect(getByText('A')).toBeTruthy();
30
- expect(getByText('B')).toBeTruthy();
31
- expect(getByText('Panel A')).toBeTruthy();
81
+ it("merges each part's class with its own", async () => {
82
+ const { container, trigger, panel } = await renderTabs();
83
+ // Each host element carries the given class too, so look for the view
84
+ // that has both its own and the given class.
85
+ expect(container.querySelector('.flex-col.w-80')).not.toBeNull();
86
+ expect(container.querySelector('.rounded-lg.mb-2')).not.toBeNull();
87
+ expect(trigger('Account').classList).toContain('rounded-sm');
88
+ expect(trigger('Account').classList).toContain('px-4');
89
+ expect(panel('Account panel').classList).toContain('mt-2');
90
+ expect(panel('Account panel').classList).toContain('gap-2');
32
91
  });
33
92
 
34
- it('select() sets the model value and emits `changed`', async () => {
35
- const { componentRef } = await render(UiTabs);
36
- const inst = componentRef.instance as UiTabs;
37
- const emitted: string[] = [];
38
- inst.changed.subscribe((v) => emitted.push(v));
39
- inst.select('x');
40
- expect(inst.value()).toBe('x');
41
- expect(emitted).toEqual(['x']);
42
- });
93
+ it("shows only the selected tab's panel, and highlights its trigger", async () => {
94
+ const { getByText, trigger, displayOf } = await renderTabs();
95
+ expect(displayOf('Account panel')).toBe('flex');
96
+ expect(displayOf('Billing panel')).toBe('none');
97
+ expect(displayOf('Contact panel')).toBe('none');
43
98
 
44
- it('registerTab() records tab order without duplicates', async () => {
45
- const { componentRef } = await render(UiTabs);
46
- const inst = componentRef.instance as UiTabs;
47
- inst.registerTab('a');
48
- inst.registerTab('b');
49
- inst.registerTab('a'); // duplicate, should be ignored
50
- // Seed the effect with a known previousValue so subsequent transitions
51
- // exercise the direction-set branch (not the initial-run early return).
52
- inst.value.set('a');
53
- await waitForUpdate();
54
- // a=0, b=1 → nextIndex > prevIndex → direction=1 (forward branch)
55
- inst.value.set('b');
56
- await waitForUpdate();
57
- await waitForUpdate();
58
- expect(inst.direction()).toBe(1);
59
- // Reverse → direction=-1 (backward branch)
60
- inst.value.set('a');
61
- await waitForUpdate();
62
- await waitForUpdate();
63
- expect(inst.direction()).toBe(-1);
99
+ expect(trigger('Account').classList).toContain('bg-background');
100
+ expect(getByText('Account').classList).toContain('text-foreground');
101
+ expect(trigger('Billing').classList).toContain('bg-transparent');
102
+ expect(getByText('Billing').classList).toContain('text-muted-foreground');
64
103
  });
65
104
 
66
- it('direction effect ignores unknown values', async () => {
67
- // Covers the branch where prevIndex or nextIndex is -1.
68
- const { componentRef } = await render(UiTabs);
69
- const inst = componentRef.instance as UiTabs;
70
- inst.value.set('unknown');
71
- inst.value.set('also-unknown');
72
- await waitForUpdate();
73
- await waitForUpdate();
74
- expect(inst.direction()).toBe(1); // unchanged from default
105
+ it('selects a tab when its trigger is tapped', async () => {
106
+ const { host, tap, trigger, displayOf } = await renderTabs();
107
+ await tap('Billing');
108
+ expect(host.value()).toBe('billing');
109
+ expect(host.changes).toEqual(['billing']);
110
+ expect(displayOf('Account panel')).toBe('none');
111
+ expect(displayOf('Billing panel')).toBe('flex');
112
+ expect(trigger('Billing').classList).toContain('bg-background');
113
+ expect(trigger('Account').classList).toContain('bg-transparent');
75
114
  });
76
115
 
77
- it('direction effect leaves direction alone when transitioning from unknown to known', async () => {
78
- const { componentRef } = await render(UiTabs);
79
- const inst = componentRef.instance as UiTabs;
80
- inst.registerTab('a');
81
- inst.registerTab('b');
82
- inst.value.set('unknown');
116
+ it('shows the tab its value is set to, without reporting a change', async () => {
117
+ const { host, displayOf } = await renderTabs();
118
+ host.value.set('contact');
83
119
  await waitForUpdate();
84
- inst.value.set('a');
85
- await waitForUpdate();
86
- expect(inst.direction()).toBe(1);
120
+ expect(displayOf('Contact panel')).toBe('flex');
121
+ expect(displayOf('Account panel')).toBe('none');
122
+ // `changed` reports what the user selects, not what the app sets.
123
+ expect(host.changes).toEqual([]);
87
124
  });
88
125
 
89
- it('direction effect leaves direction alone when transitioning from known to unknown', async () => {
90
- const { componentRef } = await render(UiTabs);
91
- const inst = componentRef.instance as UiTabs;
92
- inst.registerTab('a');
93
- inst.registerTab('b');
94
- inst.value.set('a');
126
+ it('keeps its slide direction for a value with no trigger', async () => {
127
+ const { host, tap } = await renderTabs();
128
+ await tap('Contact');
129
+ await tap('Billing');
130
+ expect(host.tabs().direction()).toBe(-1);
131
+
132
+ // Neither moving to nor moving from a value without a trigger has a
133
+ // place in the tab order to slide from.
134
+ host.value.set('archived');
95
135
  await waitForUpdate();
96
- inst.value.set('unknown');
136
+ expect(host.tabs().direction()).toBe(-1);
137
+ host.value.set('contact');
97
138
  await waitForUpdate();
98
- expect(inst.direction()).toBe(1);
139
+ expect(host.tabs().direction()).toBe(-1);
99
140
  });
100
- });
101
141
 
102
- describe('UiTabsList', () => {
103
- it('renders projected content with the list container class', async () => {
104
- const { container } = await render(UiTabsList);
105
- const view = container.querySelector('view');
106
- expect(view?.getAttribute('class')).toContain('rounded-lg');
107
- });
108
- });
142
+ describe('animations', () => {
143
+ // Watched per test: the testing library resets the main thread after each.
144
+ let animate: MockInstance<LynxMainThread['__ElementAnimate']>;
145
+ beforeEach(() => {
146
+ animate = vi.spyOn(lynxMainThread(), '__ElementAnimate');
147
+ });
148
+ afterEach(() => animate.mockRestore());
109
149
 
110
- describe('UiTabsTrigger', () => {
111
- const renderTrigger = async (activeValue = 'a') => {
112
- const tabsResult = await render(UiTabs);
113
- const tabs = tabsResult.componentRef.instance as UiTabs;
114
- tabs.value.set(activeValue);
150
+ /**
151
+ * The keyframes of the animations started on an element.
152
+ */
153
+ const keyframesOn = (element: Element) =>
154
+ animate.mock.calls
155
+ .filter(
156
+ ([target, [operation]]) => target === element && operation === START,
157
+ )
158
+ .map(([, [, , keyframes]]) => keyframes);
115
159
 
116
- const triggerResult = await render(UiTabsTrigger, {
117
- providers: [{ provide: UiTabs, useValue: tabs }],
118
- });
119
- const trigger = triggerResult.componentRef.instance as UiTabsTrigger;
120
- setInputSignal(trigger.triggerValue, 'a');
121
- const view = triggerResult.container.querySelector('view') as Element;
122
- (trigger as unknown as { triggerRef: unknown }).triggerRef = signal({
123
- nativeElement: view,
124
- });
125
- await waitForUpdate();
126
- await waitForUpdate();
127
- return { tabs, trigger, triggerResult };
128
- };
160
+ /**
161
+ * The keys of the animations started on an element.
162
+ */
163
+ const keysOn = (element: Element) =>
164
+ animate.mock.calls
165
+ .filter(
166
+ ([target, [operation]]) => target === element && operation === START,
167
+ )
168
+ .map(([, [, key]]) => key);
129
169
 
130
- it('select() delegates to the parent tabs', async () => {
131
- const { tabs, trigger } = await renderTrigger('a');
132
- trigger.select();
133
- expect(tabs.value()).toBe('a');
134
- });
170
+ const reveal = [
171
+ { transform: 'translateY(6px)', opacity: 0 },
172
+ { transform: 'translateY(0)', opacity: 1 },
173
+ ];
174
+ const slideFrom = (x: number) => [
175
+ { transform: `translateX(${x}px)`, opacity: 0 },
176
+ { transform: 'translateX(0px)', opacity: 1 },
177
+ ];
178
+ const activate = [
179
+ { transform: 'scaleX(0.95)', opacity: 0.7 },
180
+ { transform: 'scaleX(1)', opacity: 1 },
181
+ ];
182
+ const deactivate = [
183
+ { transform: 'scaleX(1)', opacity: 1 },
184
+ { transform: 'scaleX(0.97)', opacity: 0.7 },
185
+ ];
135
186
 
136
- it('effect runs the activation animation when active flips true', async () => {
137
- // Active path — `#previousActive` starts undefined so the first flip
138
- // exercises the initialization guard, and toggling activeValue drives the
139
- // effect through both animate() branches.
140
- const { tabs } = await renderTrigger('b');
141
- // Switch tabs so trigger becomes active — activation branch.
142
- tabs.value.set('a');
143
- await waitForUpdate();
144
- await waitForUpdate();
145
- // Deactivation branch.
146
- tabs.value.set('b');
147
- await waitForUpdate();
148
- await waitForUpdate();
149
- expect(tabs.value()).toBe('b');
150
- });
151
- });
187
+ it('reveals a panel the first time it shows, then slides it in from the side of its tab', async () => {
188
+ const { panel, tap } = await renderTabs();
189
+ // The panel waits a macrotask so its display has applied before it
190
+ // animates; this update lets that run.
191
+ await waitForUpdate();
192
+ expect(keyframesOn(panel('Account panel'))).toEqual([reveal]);
152
193
 
153
- describe('UiTabsContent', () => {
154
- it('runs the reveal + directional slide animations when active flips', async () => {
155
- const tabsResult = await render(UiTabs);
156
- const tabs = tabsResult.componentRef.instance as UiTabs;
157
- tabs.registerTab('a');
158
- tabs.registerTab('b');
194
+ await tap('Contact');
195
+ await waitForUpdate();
196
+ expect(keyframesOn(panel('Contact panel'))).toEqual([reveal]);
159
197
 
160
- const contentResult = await render(UiTabsContent, {
161
- providers: [{ provide: UiTabs, useValue: tabs }],
162
- });
163
- const content = contentResult.componentRef.instance as UiTabsContent;
164
- setInputSignal(content.contentValue, 'a');
165
- const view = contentResult.container.querySelector('view') as Element;
166
- (content as unknown as { contentRef: unknown }).contentRef = signal({
167
- nativeElement: view,
198
+ // Account is left of Contact, so its panel slides in from the left.
199
+ await tap('Account');
200
+ await waitForUpdate();
201
+ expect(keyframesOn(panel('Account panel'))).toEqual([
202
+ reveal,
203
+ slideFrom(-24),
204
+ ]);
205
+ // Showing it again stopped its earlier reveal first.
206
+ const [revealKey] = keysOn(panel('Account panel'));
207
+ expect(animate).toHaveBeenCalledWith(panel('Account panel'), [
208
+ CANCEL,
209
+ revealKey,
210
+ ]);
211
+
212
+ // Contact is right of Account, so its panel slides in from the right.
213
+ await tap('Contact');
214
+ await waitForUpdate();
215
+ expect(keyframesOn(panel('Contact panel'))).toEqual([
216
+ reveal,
217
+ slideFrom(24),
218
+ ]);
168
219
  });
169
220
 
170
- // Activate — first render path (revealIn).
171
- tabs.value.set('a');
172
- await waitForUpdate();
173
- // The effect schedules setTimeout(_, 0); wait one more tick for it.
174
- await waitForUpdate();
221
+ it('scales a trigger up when selected and down when deselected', async () => {
222
+ const { trigger, tap } = await renderTabs();
223
+ // Nothing changed selection yet, so no trigger animates.
224
+ expect(keyframesOn(trigger('Account'))).toEqual([]);
175
225
 
176
- // Switch off and back on — subsequent path (directionalSlideIn) with cancel.
177
- tabs.value.set('b');
178
- await waitForUpdate();
179
- tabs.value.set('a');
180
- await waitForUpdate();
181
- await waitForUpdate();
226
+ await tap('Billing');
227
+ expect(keyframesOn(trigger('Billing'))).toEqual([activate]);
228
+ expect(keyframesOn(trigger('Account'))).toEqual([deactivate]);
182
229
 
183
- expect(tabs.value()).toBe('a');
184
- });
230
+ await tap('Account');
231
+ expect(keyframesOn(trigger('Account'))).toEqual([deactivate, activate]);
232
+ // Each animation stops the trigger's previous one.
233
+ const [deactivateKey] = keysOn(trigger('Account'));
234
+ expect(animate).toHaveBeenCalledWith(trigger('Account'), [
235
+ CANCEL,
236
+ deactivateKey,
237
+ ]);
238
+ });
239
+
240
+ it("doesn't animate a selection that changed back within one frame", async () => {
241
+ const { host, trigger, tap } = await renderTabs();
242
+ await tap('Billing');
243
+ await waitForUpdate();
244
+ animate.mockClear();
185
245
 
186
- it('setTimeout body bails out when contentRef is undefined', async () => {
187
- const tabsResult = await render(UiTabs);
188
- const tabs = tabsResult.componentRef.instance as UiTabs;
189
- const contentResult = await render(UiTabsContent, {
190
- providers: [{ provide: UiTabs, useValue: tabs }],
246
+ // Two taps before the next update: Billing is selected again.
247
+ fireEvent.tap(trigger('Contact'));
248
+ fireEvent.tap(trigger('Billing'));
249
+ await waitForUpdate();
250
+ await waitForUpdate();
251
+ expect(host.changes).toEqual(['billing', 'contact', 'billing']);
252
+ expect(animate).not.toHaveBeenCalled();
253
+ // The direction still points the way Billing was reached, from Account.
254
+ expect(host.tabs().direction()).toBe(1);
191
255
  });
192
- const content = contentResult.componentRef.instance as UiTabsContent;
193
- setInputSignal(content.contentValue, 'a');
194
- tabs.value.set('a');
195
- await waitForUpdate();
196
- await waitForUpdate();
197
- expect(tabs.value()).toBe('a');
198
256
  });
199
257
  });