@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,342 +1,463 @@
1
- import { ElementRef } from '@angular/core';
2
- import { render, waitForUpdate } from '@blotch/angular-lynx-testing-library';
3
- import { afterEach, describe, expect, it, vi } from 'vitest';
4
- import { setInputSignal } from '../../../test-utils/set-input-signal';
5
- import { UiToastItem, UiToaster } from './toast';
6
- import { toast, toasts, type ToastData } from './toast-state';
1
+ import {
2
+ fireEvent,
3
+ render,
4
+ waitForUpdate,
5
+ } from '@blotch/angular-lynx-testing-library';
6
+ import {
7
+ type MockInstance,
8
+ afterEach,
9
+ beforeEach,
10
+ describe,
11
+ expect,
12
+ it,
13
+ vi,
14
+ } from 'vitest';
15
+ import {
16
+ type LynxMainThread,
17
+ lynxMainThread,
18
+ } from '../../../test-utils/lynx-main-thread';
19
+ import { DURATION } from '../../utils/animate';
20
+ import { UiToaster } from './toast';
21
+ import { type ToastOptions, dismissToast, toast, toasts } from './toast-state';
22
+
23
+ const sleep = (ms: number) =>
24
+ new Promise<void>((resolve) => setTimeout(resolve, ms));
25
+
26
+ /**
27
+ * Waits out a toast's exit animation, after which the toaster drops it from
28
+ * the queue, and renders that.
29
+ */
30
+ const waitForExit = async () => {
31
+ await sleep(DURATION.fast + 40);
32
+ await waitForUpdate();
33
+ };
34
+
35
+ const renderToaster = async () => {
36
+ const result = await render(UiToaster);
37
+ /**
38
+ * A toast's card: its host element, around the view holding its texts.
39
+ */
40
+ const card = (title: string) =>
41
+ result.getByText(title).parentElement!.parentElement!;
42
+ return {
43
+ ...result,
44
+ card,
45
+ /** One property of a toast card's inline style. */
46
+ styleOf: (title: string, property: string) =>
47
+ (card(title) as HTMLElement).style.getPropertyValue(property),
48
+ /**
49
+ * Raises a toast and lets it mount, then start its entrance, which waits
50
+ * a macrotask so the card is committed to Lynx's tree first.
51
+ */
52
+ raise: async (options: ToastOptions) => {
53
+ const id = toast(options);
54
+ await waitForUpdate();
55
+ await waitForUpdate();
56
+ return id;
57
+ },
58
+ };
59
+ };
60
+
61
+ /** `__ElementAnimate`'s first argument for starting an animation. */
62
+ const START = 0;
63
+
64
+ /** Where the front toast rests. */
65
+ const FRONT = 'translateY(0px) scale(1)';
66
+ /** Where the toast behind the front one rests: lifted, and a little smaller. */
67
+ const SECOND = 'translateY(-14px) scale(0.95)';
7
68
 
8
69
  describe('UiToaster', () => {
9
- afterEach(() => {
10
- toasts.set([]);
11
- });
70
+ // The queue is module state, so each test starts it empty.
71
+ beforeEach(() => toasts.set([]));
12
72
 
13
- it('renders without any active toasts', async () => {
14
- const { container } = await render(UiToaster);
15
- expect(container).toBeTruthy();
73
+ // Watched per test: the testing library resets the main thread after each.
74
+ let animate: MockInstance<LynxMainThread['__ElementAnimate']>;
75
+ beforeEach(() => {
76
+ animate = vi.spyOn(lynxMainThread(), '__ElementAnimate');
16
77
  });
78
+ afterEach(() => animate.mockRestore());
17
79
 
18
- it('renders the stack container when a toast is added', async () => {
19
- toast({ title: 'Saved!' });
20
- const { container } = await render(UiToaster);
21
- await waitForUpdate();
22
- // Toast content isn't asserted here — JIT does not wire the required
23
- // `data` input on the projected <ui-toast-item> so the child template
24
- // silently fails. UiToastItem's lifecycle is exercised directly below.
25
- expect(container.querySelectorAll('view').length).toBeGreaterThan(0);
26
- });
80
+ /**
81
+ * The keyframes of the animations started on an element.
82
+ */
83
+ const keyframesOn = (element: Element) =>
84
+ animate.mock.calls
85
+ .filter(
86
+ ([target, [operation]]) => target === element && operation === START,
87
+ )
88
+ .map(([, [, , keyframes]]) => keyframes);
27
89
 
28
- it('onDismissed removes the toast from the queue', async () => {
29
- const id = toast({ title: 'ByeBye' });
30
- const { componentRef } = await render(UiToaster);
31
- const inst = componentRef.instance as unknown as {
32
- onDismissed: (id: string) => void;
33
- };
34
- inst.onDismissed(id);
35
- expect(toasts()).toHaveLength(0);
36
- });
37
- });
90
+ const entrance = [
91
+ { transform: 'translateY(100%) scale(0.95)', opacity: 0 },
92
+ { transform: FRONT, opacity: 1 },
93
+ ];
94
+ const exit = (fromY: number) => [
95
+ { transform: `translateY(${fromY}px) scale(1)`, opacity: 1 },
96
+ { transform: 'translateY(300px) scale(0.95)', opacity: 0 },
97
+ ];
98
+ const snapBack = (fromY: number) => [
99
+ { transform: `translateY(${fromY}px) scale(1)` },
100
+ { transform: FRONT },
101
+ ];
38
102
 
39
- describe('UiToastItem', () => {
40
- const baseData = (over: Partial<ToastData> = {}): ToastData => ({
41
- id: 'toast-x',
42
- title: 'Hello',
43
- description: 'World',
44
- variant: 'default',
45
- duration: 60_000,
46
- ...over,
47
- });
103
+ describe('showing toasts', () => {
104
+ it('shows a toast once one is raised', async () => {
105
+ const { queryByText, getByText, raise } = await renderToaster();
106
+ expect(queryByText('Saved')).toBeNull();
48
107
 
49
- it('renders default and destructive variants', async () => {
50
- const { componentRef, container } = await render(UiToastItem);
51
- const inst = componentRef.instance as UiToastItem;
52
- setInputSignal(inst.data, baseData({ variant: 'destructive' }));
53
- setInputSignal(inst.depth, 0);
54
- await waitForUpdate();
55
- // JIT + input.required races the initial CD: even with the signal set, the
56
- // template DOM may not be materialized in this synthetic bootstrap. The
57
- // variant computed is exercised elsewhere; asserting on the container ref
58
- // proves the render didn't throw.
59
- expect(container).toBeTruthy();
60
- });
108
+ await raise({ title: 'Saved', description: 'Your changes are live.' });
109
+ expect(getByText('Saved')).toBeTruthy();
110
+ expect(getByText('Your changes are live.')).toBeTruthy();
111
+ });
61
112
 
62
- it('exercises all computed presentation getters (default + destructive)', async () => {
63
- // The template computed getters (`cardClass`, `titleClass`, etc.) are only
64
- // read by the template when it materializes; JIT + required inputs races
65
- // that, so read them directly so their branches run under coverage.
66
- const { componentRef } = await render(UiToastItem);
67
- const inst = componentRef.instance as UiToastItem;
68
- const cast = inst as unknown as {
69
- hostStyle: () => string;
70
- cardClass: () => string;
71
- titleClass: () => string;
72
- descriptionClass: () => string;
73
- actionClass: () => string;
74
- actionTextClass: () => string;
75
- };
76
-
77
- setInputSignal(inst.data, baseData());
78
- setInputSignal(inst.depth, 0);
79
- await waitForUpdate();
80
- expect(cast.hostStyle()).toContain('translateY');
81
- expect(cast.cardClass()).toContain('bg-background');
82
- expect(cast.titleClass()).toContain('text-foreground');
83
- expect(cast.descriptionClass()).toContain('text-muted-foreground');
84
- expect(cast.actionClass()).toContain('border-border');
85
- expect(cast.actionTextClass()).toContain('text-foreground');
86
-
87
- setInputSignal(inst.data, baseData({ variant: 'destructive' }));
88
- setInputSignal(inst.depth, 2);
89
- await waitForUpdate();
90
- expect(cast.cardClass()).toContain('bg-destructive');
91
- expect(cast.titleClass()).toContain('text-destructive-foreground');
92
- expect(cast.descriptionClass()).toContain('text-destructive-foreground');
93
- expect(cast.actionClass()).toContain('border-destructive-foreground');
94
- expect(cast.actionTextClass()).toContain('text-destructive-foreground');
95
- expect(cast.hostStyle()).toContain('translateY');
96
- });
113
+ it("leaves out a title or description it doesn't have", async () => {
114
+ const { getByText, raise } = await renderToaster();
115
+ await raise({ title: 'Only a title' });
116
+ await raise({ title: '', description: 'Only a description' });
117
+ const textsBeside = (text: string) =>
118
+ getByText(text).parentElement!.querySelectorAll('text');
119
+ expect(textsBeside('Only a title')).toHaveLength(1);
120
+ expect(textsBeside('Only a description')).toHaveLength(1);
121
+ });
97
122
 
98
- it('renders an action button and invokes it via onAction', async () => {
99
- const cb = vi.fn();
100
- const { componentRef } = await render(UiToastItem);
101
- const inst = componentRef.instance as UiToastItem;
102
- setInputSignal(
103
- inst.data,
104
- baseData({ action: { label: 'Do it', onAction: cb } }),
105
- );
106
- setInputSignal(inst.depth, 0);
107
- await waitForUpdate();
108
- await new Promise((r) => setTimeout(r, 20));
109
- (inst as unknown as { onAction: () => void }).onAction();
110
- expect(cb).toHaveBeenCalledTimes(1);
111
- });
123
+ it('styles a toast by its variant', async () => {
124
+ const { card, getByText, raise } = await renderToaster();
125
+ const action = { label: 'Undo', onAction: () => {} };
126
+ await raise({ title: 'Saved', description: 'Done.', action });
127
+ expect(card('Saved').classList).toContain('bg-background');
128
+ expect(getByText('Saved').classList).toContain('text-foreground');
129
+ expect(getByText('Done.').classList).toContain('text-muted-foreground');
130
+ expect(getByText('Undo').classList).toContain('text-foreground');
131
+ expect(getByText('Undo').parentElement!.classList).toContain(
132
+ 'border-border',
133
+ );
112
134
 
113
- it('onAction handles missing action gracefully', async () => {
114
- const { componentRef } = await render(UiToastItem);
115
- const inst = componentRef.instance as UiToastItem;
116
- setInputSignal(inst.data, baseData());
117
- setInputSignal(inst.depth, 0);
118
- await waitForUpdate();
119
- expect(() =>
120
- (inst as unknown as { onAction: () => void }).onAction(),
121
- ).not.toThrow();
122
- });
135
+ await raise({
136
+ title: 'Failed',
137
+ description: 'Try again.',
138
+ variant: 'destructive',
139
+ action: { label: 'Retry', onAction: () => {} },
140
+ });
141
+ expect(card('Failed').classList).toContain('bg-destructive');
142
+ for (const text of ['Failed', 'Try again.', 'Retry']) {
143
+ expect(getByText(text).classList).toContain(
144
+ 'text-destructive-foreground',
145
+ );
146
+ }
147
+ expect(getByText('Retry').parentElement!.classList).toContain(
148
+ 'border-destructive-foreground',
149
+ );
150
+ });
123
151
 
124
- it('onTap dismisses only when the toast is front (depth 0)', async () => {
125
- const { componentRef } = await render(UiToastItem);
126
- const inst = componentRef.instance as UiToastItem;
127
- setInputSignal(inst.data, baseData());
128
- setInputSignal(inst.depth, 1); // behind — no-op
129
- await waitForUpdate();
130
- expect(() =>
131
- (inst as unknown as { onTap: () => void }).onTap(),
132
- ).not.toThrow();
133
-
134
- setInputSignal(inst.depth, 0);
135
- await waitForUpdate();
136
- (inst as unknown as { onTap: () => void }).onTap();
137
- });
152
+ it('stays hidden until it slides up into view', async () => {
153
+ const { card, styleOf } = await renderToaster();
154
+ toast({ title: 'Saved' });
155
+ // Mounted, but its entrance waits a macrotask: until then it's hidden,
156
+ // or it would flash at its resting place first.
157
+ await waitForUpdate();
158
+ expect(styleOf('Saved', 'opacity')).toBe('0');
159
+ expect(keyframesOn(card('Saved'))).toEqual([]);
138
160
 
139
- it('auto-dismisses after the duration timer fires', async () => {
140
- const dismissed = vi.fn();
141
- const { componentRef } = await render(UiToastItem);
142
- const inst = componentRef.instance as UiToastItem;
143
- inst.dismissed.subscribe(dismissed);
144
- setInputSignal(inst.data, baseData({ duration: 30 }));
145
- setInputSignal(inst.depth, 0);
146
- await waitForUpdate();
147
- // Manually call ngOnInit after data is set so the auto-dismiss timer
148
- // registers (JIT's initial CD ran ngOnInit before the required-input's
149
- // value was available, so the timer was skipped).
150
- inst.ngOnInit();
151
- // Wait long enough for both the auto-dismiss (30ms) and the dismissed
152
- // emit setTimeout(DURATION.fast + 20 = 170ms) to fire.
153
- await new Promise((r) => setTimeout(r, 400));
154
- expect(dismissed).toHaveBeenCalled();
155
- });
161
+ await waitForUpdate();
162
+ expect(styleOf('Saved', 'opacity')).toBe('1');
163
+ expect(keyframesOn(card('Saved'))).toEqual([entrance]);
164
+ });
156
165
 
157
- it('touch drag beyond threshold dismisses; below threshold snaps back', async () => {
158
- const { componentRef } = await render(UiToastItem);
159
- const inst = componentRef.instance as UiToastItem;
160
- setInputSignal(inst.data, baseData({ duration: 60_000 }));
161
- setInputSignal(inst.depth, 0);
162
- await waitForUpdate();
163
- // Call ngOnInit manually so the auto-dismiss timer is set — that's the
164
- // branch inside dismiss() that touchend beyond threshold exercises.
165
- inst.ngOnInit();
166
- await new Promise((r) => setTimeout(r, 10));
167
-
168
- const cast = inst as unknown as {
169
- onTouchStart: (e: { touches?: { clientY: number }[] }) => void;
170
- onTouchMove: (e: { touches?: { clientY: number }[] }) => void;
171
- onTouchEnd: () => void;
172
- };
173
- // Below threshold → snap back.
174
- cast.onTouchStart({ touches: [{ clientY: 100 }] });
175
- cast.onTouchMove({ touches: [{ clientY: 130 }] });
176
- cast.onTouchEnd();
177
- await waitForUpdate();
178
-
179
- // Beyond threshold → dismiss.
180
- cast.onTouchStart({ touches: [{ clientY: 100 }] });
181
- cast.onTouchMove({ touches: [{ clientY: 400 }] });
182
- cast.onTouchEnd();
183
- await new Promise((r) => setTimeout(r, 200));
184
- });
166
+ it('stacks the newest three, newest in front, and queues the rest', async () => {
167
+ const { queryByText, styleOf, raise } = await renderToaster();
168
+ for (const title of ['First', 'Second', 'Third', 'Fourth']) {
169
+ await raise({ title });
170
+ }
171
+ // The oldest waits its turn, off screen.
172
+ expect(queryByText('First')).toBeNull();
173
+ expect(toasts()).toHaveLength(4);
185
174
 
186
- it('touch handlers guard non-front toasts and missing touches', async () => {
187
- const { componentRef } = await render(UiToastItem);
188
- const inst = componentRef.instance as UiToastItem;
189
- setInputSignal(inst.data, baseData());
190
- setInputSignal(inst.depth, 1); // behind — early-return in onTouchStart
191
- await waitForUpdate();
192
-
193
- const cast = inst as unknown as {
194
- onTouchStart: (e: { touches?: { clientY: number }[] }) => void;
195
- onTouchMove: (e: { touches?: { clientY: number }[] }) => void;
196
- onTouchEnd: () => void;
197
- };
198
- cast.onTouchStart({ touches: [{ clientY: 100 }] });
199
- cast.onTouchMove({ touches: [{ clientY: 200 }] }); // not dragging → no-op
200
- cast.onTouchEnd(); // not dragging → no-op
201
-
202
- setInputSignal(inst.depth, 0);
203
- await waitForUpdate();
204
- // Empty touches array → no-op
205
- cast.onTouchStart({ touches: [] });
206
- cast.onTouchMove({ touches: [] });
207
- // No prior touchstart with real coords, so onTouchEnd is a no-op.
208
- cast.onTouchEnd();
209
- });
175
+ expect(styleOf('Fourth', 'z-index')).toBe('100');
176
+ expect(styleOf('Fourth', 'transform')).toBe(FRONT);
177
+ expect(styleOf('Third', 'z-index')).toBe('99');
178
+ expect(styleOf('Third', 'transform')).toBe(SECOND);
179
+ expect(styleOf('Second', 'z-index')).toBe('98');
180
+ expect(styleOf('Second', 'transform')).toBe(
181
+ 'translateY(-28px) scale(0.9)',
182
+ );
183
+ });
210
184
 
211
- it('onTouchMove ignores subsequent moves with missing touches once dragging', async () => {
212
- const { componentRef } = await render(UiToastItem);
213
- const inst = componentRef.instance as UiToastItem;
214
- setInputSignal(inst.data, baseData());
215
- setInputSignal(inst.depth, 0);
216
- await waitForUpdate();
217
- inst.ngOnInit();
218
- const cast = inst as unknown as {
219
- onTouchStart: (e: { touches?: { clientY: number }[] }) => void;
220
- onTouchMove: (e: { touches?: { clientY: number }[] }) => void;
221
- };
222
- // Start a real drag (sets #isDragging = true).
223
- cast.onTouchStart({ touches: [{ clientY: 50 }] });
224
- // A subsequent move without touches must fall through the `y == null` guard
225
- // in onTouchMove (line 257) — this is the "finger left the surface without
226
- // a touchend" native quirk we defend against.
227
- cast.onTouchMove({ touches: [] });
228
- cast.onTouchMove({}); // touches undefined
229
- });
185
+ it('moves the stack back for a new toast, and forward when the front one goes', async () => {
186
+ const { card, raise } = await renderToaster();
187
+ await raise({ title: 'Older' });
188
+ await raise({ title: 'Newer' });
189
+ expect(keyframesOn(card('Older'))).toEqual([
190
+ entrance,
191
+ [{ transform: FRONT }, { transform: SECOND }],
192
+ ]);
230
193
 
231
- it('onTouchEnd is a no-op when dismissal is already in progress', async () => {
232
- const { componentRef } = await render(UiToastItem);
233
- const inst = componentRef.instance as UiToastItem;
234
- setInputSignal(inst.data, baseData());
235
- setInputSignal(inst.depth, 0);
236
- await waitForUpdate();
237
- inst.ngOnInit();
238
- const cast = inst as unknown as {
239
- onTouchStart: (e: { touches?: { clientY: number }[] }) => void;
240
- onTouchMove: (e: { touches?: { clientY: number }[] }) => void;
241
- onTouchEnd: () => void;
242
- };
243
- // Start a drag.
244
- cast.onTouchStart({ touches: [{ clientY: 100 }] });
245
- cast.onTouchMove({ touches: [{ clientY: 200 }] });
246
- // Race the dismiss(): tapping or the timer already started tearing down.
247
- inst.dismiss();
248
- // touchend must observe #isDismissing and bail out (line 271).
249
- cast.onTouchEnd();
250
- });
194
+ fireEvent.tap(card('Newer'));
195
+ await waitForExit();
196
+ expect(keyframesOn(card('Older')).at(-1)).toEqual([
197
+ { transform: SECOND },
198
+ { transform: FRONT },
199
+ ]);
200
+ });
251
201
 
252
- it('snap-back reschedules the auto-dismiss timer that eventually fires', async () => {
253
- const dismissed = vi.fn();
254
- const { componentRef } = await render(UiToastItem);
255
- const inst = componentRef.instance as UiToastItem;
256
- inst.dismissed.subscribe(dismissed);
257
- // Short duration so the snap-back's rescheduled setTimeout callback
258
- // (line 419) actually fires before the test times out.
259
- setInputSignal(inst.data, baseData({ duration: 30 }));
260
- setInputSignal(inst.depth, 0);
261
- await waitForUpdate();
262
- inst.ngOnInit();
263
- const cast = inst as unknown as {
264
- onTouchStart: (e: { touches?: { clientY: number }[] }) => void;
265
- onTouchMove: (e: { touches?: { clientY: number }[] }) => void;
266
- onTouchEnd: () => void;
267
- };
268
- // Drag below threshold → snap back reschedules the auto-dismiss.
269
- cast.onTouchStart({ touches: [{ clientY: 100 }] });
270
- cast.onTouchMove({ touches: [{ clientY: 120 }] });
271
- cast.onTouchEnd();
272
- // Wait long enough for the rescheduled dismiss (30ms) + dismissed emit
273
- // (DURATION.fast + 20 ≈ 170ms) to fire.
274
- await new Promise((r) => setTimeout(r, 400));
275
- expect(dismissed).toHaveBeenCalled();
202
+ it('brings a queued toast into the back of the stack once there is room', async () => {
203
+ const { card, raise } = await renderToaster();
204
+ for (const title of ['First', 'Second', 'Third', 'Fourth']) {
205
+ await raise({ title });
206
+ }
207
+ fireEvent.tap(card('Fourth'));
208
+ await waitForExit();
209
+ // It mounts there, so it rises into the back slot rather than sliding
210
+ // up past the others.
211
+ await waitForUpdate();
212
+ expect(keyframesOn(card('First'))).toEqual([
213
+ [
214
+ { transform: 'translateY(-20px) scale(0.864)', opacity: 0 },
215
+ { transform: 'translateY(-28px) scale(0.9)', opacity: 1 },
216
+ ],
217
+ ]);
218
+ });
276
219
  });
277
220
 
278
- it('restack effect runs when depth changes on an already-mounted item', async () => {
279
- const { componentRef } = await render(UiToastItem);
280
- const inst = componentRef.instance as UiToastItem;
281
- setInputSignal(inst.data, baseData());
282
- setInputSignal(inst.depth, 0);
283
- await waitForUpdate();
284
- setInputSignal(inst.depth, 1); // triggers restack animation
285
- await waitForUpdate();
286
- setInputSignal(inst.depth, 2);
287
- await waitForUpdate();
288
- });
221
+ describe('dismissing', () => {
222
+ it('slides the front toast away when tapped', async () => {
223
+ const { card, queryByText, raise } = await renderToaster();
224
+ await raise({ title: 'Saved' });
225
+ fireEvent.tap(card('Saved'));
226
+ expect(keyframesOn(card('Saved')).at(-1)).toEqual(exit(0));
289
227
 
290
- it('hostStyle flips opacity to 1 after the entrance animation runs', async () => {
291
- const { componentRef } = await render(UiToastItem);
292
- const inst = componentRef.instance as UiToastItem;
293
- setInputSignal(inst.data, baseData());
294
- setInputSignal(inst.depth, 0);
295
- await waitForUpdate();
296
- const cast = inst as unknown as { hostStyle: () => string };
297
- // Before ngOnInit → #hasEntered = false → opacity: 0.
298
- expect(cast.hostStyle()).toContain('opacity: 0');
299
- inst.ngOnInit();
300
- // Wait past the setTimeout(0) that runs #animateIn, which sets #hasEntered.
301
- await new Promise((r) => setTimeout(r, 30));
302
- // After animateIn → #hasEntered = true → opacity: 1 (the ternary's truthy arm).
303
- expect(cast.hostStyle()).toContain('opacity: 1');
304
- });
228
+ await waitForExit();
229
+ expect(queryByText('Saved')).toBeNull();
230
+ expect(toasts()).toEqual([]);
231
+ });
305
232
 
306
- it('dismiss() is idempotent (re-entry guard)', async () => {
307
- const dismissed = vi.fn();
308
- const { componentRef } = await render(UiToastItem);
309
- const inst = componentRef.instance as UiToastItem;
310
- inst.dismissed.subscribe(dismissed);
311
- setInputSignal(inst.data, baseData({ duration: 60_000 }));
312
- setInputSignal(inst.depth, 0);
313
- await waitForUpdate();
314
- inst.dismiss();
315
- inst.dismiss(); // guarded
316
- await new Promise((r) => setTimeout(r, 200));
317
- expect(dismissed).toHaveBeenCalledTimes(1);
318
- });
233
+ it('slides away only once, however often it is tapped', async () => {
234
+ const { card, queryByText, raise } = await renderToaster();
235
+ await raise({ title: 'Saved' });
236
+ fireEvent.tap(card('Saved'));
237
+ fireEvent.tap(card('Saved'));
238
+ expect(keyframesOn(card('Saved'))).toEqual([entrance, exit(0)]);
239
+
240
+ await waitForExit();
241
+ expect(queryByText('Saved')).toBeNull();
242
+ });
243
+
244
+ it('ignores taps on a toast behind the front one', async () => {
245
+ const { card, getByText, raise } = await renderToaster();
246
+ await raise({ title: 'Older' });
247
+ await raise({ title: 'Newer' });
248
+ animate.mockClear();
249
+
250
+ fireEvent.tap(card('Older'));
251
+ await waitForExit();
252
+ expect(animate).not.toHaveBeenCalled();
253
+ expect(getByText('Older')).toBeTruthy();
254
+ });
255
+
256
+ it('runs its action, then goes', async () => {
257
+ const onAction = vi.fn();
258
+ const { getByText, queryByText, raise } = await renderToaster();
259
+ await raise({ title: 'Deleted', action: { label: 'Undo', onAction } });
260
+
261
+ // The action catches its tap, so the card doesn't see it as well.
262
+ fireEvent.tap(getByText('Undo').parentElement!, {
263
+ eventType: 'catchEvent',
264
+ });
265
+ expect(onAction).toHaveBeenCalledTimes(1);
319
266
 
320
- it('behind toast (depth > 0) uses the promoted-from-queue entrance animation', async () => {
321
- const { componentRef } = await render(UiToastItem);
322
- const inst = componentRef.instance as UiToastItem;
323
- setInputSignal(inst.data, baseData());
324
- setInputSignal(inst.depth, 2);
325
- await waitForUpdate();
326
- inst.ngOnInit();
327
- await new Promise((r) => setTimeout(r, 30));
267
+ await waitForExit();
268
+ expect(queryByText('Deleted')).toBeNull();
269
+ });
270
+
271
+ it('goes by itself once its duration is up', async () => {
272
+ const { queryByText, raise } = await renderToaster();
273
+ await raise({ title: 'Saved', duration: 30 });
274
+ expect(queryByText('Saved')).not.toBeNull();
275
+
276
+ await sleep(30);
277
+ await waitForExit();
278
+ expect(queryByText('Saved')).toBeNull();
279
+ });
280
+
281
+ it('fades a toast behind the front one away in place when it expires', async () => {
282
+ const { card, queryByText, raise } = await renderToaster();
283
+ // Raised together, so the older one is behind from the start.
284
+ toast({ title: 'Older', duration: 30 });
285
+ await raise({ title: 'Newer' });
286
+
287
+ await sleep(30);
288
+ await waitForUpdate();
289
+ expect(keyframesOn(card('Older')).at(-1)).toEqual([
290
+ { transform: SECOND, opacity: 1 },
291
+ { transform: 'translateY(-14px) scale(0.855)', opacity: 0 },
292
+ ]);
293
+
294
+ await waitForExit();
295
+ expect(queryByText('Older')).toBeNull();
296
+ expect(queryByText('Newer')).not.toBeNull();
297
+ });
298
+
299
+ it("lets a toast that's going finish, rather than moving back for a new one", async () => {
300
+ const { card, raise } = await renderToaster();
301
+ await raise({ title: 'Older' });
302
+ fireEvent.tap(card('Older'));
303
+ await raise({ title: 'Newer' });
304
+ expect(keyframesOn(card('Older'))).toEqual([entrance, exit(0)]);
305
+ });
306
+
307
+ describe('removed before its timers fire', () => {
308
+ // A destroyed output warns (NG0953) if something still emits on it.
309
+ let warn: MockInstance<typeof console.warn>;
310
+ beforeEach(() => {
311
+ warn = vi.spyOn(console, 'warn');
312
+ });
313
+ afterEach(() => warn.mockRestore());
314
+
315
+ it("doesn't expire once it's gone", async () => {
316
+ const { card, queryByText, raise } = await renderToaster();
317
+ const id = await raise({ title: 'Saved', duration: 100 });
318
+ const removed = card('Saved');
319
+ dismissToast(id);
320
+ await waitForUpdate();
321
+ expect(queryByText('Saved')).toBeNull();
322
+
323
+ await sleep(100);
324
+ await waitForExit();
325
+ expect(keyframesOn(removed)).toEqual([entrance]);
326
+ expect(warn).not.toHaveBeenCalledWith(
327
+ expect.stringContaining('NG0953'),
328
+ );
329
+ });
330
+
331
+ it("doesn't report finishing its exit once it's gone", async () => {
332
+ const { card, queryByText, raise } = await renderToaster();
333
+ const id = await raise({ title: 'Saved' });
334
+ fireEvent.tap(card('Saved'));
335
+ dismissToast(id);
336
+ await waitForUpdate();
337
+ expect(queryByText('Saved')).toBeNull();
338
+
339
+ await waitForExit();
340
+ expect(warn).not.toHaveBeenCalledWith(
341
+ expect.stringContaining('NG0953'),
342
+ );
343
+ });
344
+ });
328
345
  });
329
346
 
330
- it('behind toast (depth > 0) also dismisses via a per-depth exit animation', async () => {
331
- const { componentRef } = await render(UiToastItem);
332
- const inst = componentRef.instance as UiToastItem;
333
- setInputSignal(inst.data, baseData());
334
- setInputSignal(inst.depth, 2);
335
- await waitForUpdate();
336
- inst.ngOnInit();
337
- await new Promise((r) => setTimeout(r, 20));
338
- inst.dismiss();
339
- await new Promise((r) => setTimeout(r, 200));
347
+ describe('dragging', () => {
348
+ /**
349
+ * Fires a touch event the card catches, at a height on screen.
350
+ */
351
+ const touch = (
352
+ name: 'touchstart' | 'touchmove' | 'touchend' | 'touchcancel',
353
+ element: Element,
354
+ clientY?: number,
355
+ ) =>
356
+ fireEvent[name](element, {
357
+ eventType: 'catchEvent',
358
+ ...(clientY === undefined ? {} : { touches: [{ clientY }] }),
359
+ });
360
+
361
+ it('follows the finger down, and springs back if let go early', async () => {
362
+ const { card, styleOf, raise } = await renderToaster();
363
+ await raise({ title: 'Saved' });
364
+
365
+ touch('touchstart', card('Saved'), 100);
366
+ touch('touchmove', card('Saved'), 130);
367
+ await waitForUpdate();
368
+ expect(styleOf('Saved', 'transform')).toBe('translateY(30px) scale(1)');
369
+
370
+ // Docked at the bottom, it only moves down.
371
+ touch('touchmove', card('Saved'), 60);
372
+ await waitForUpdate();
373
+ expect(styleOf('Saved', 'transform')).toBe(FRONT);
374
+
375
+ touch('touchmove', card('Saved'), 150);
376
+ touch('touchend', card('Saved'));
377
+ await waitForUpdate();
378
+ expect(keyframesOn(card('Saved')).at(-1)).toEqual(snapBack(50));
379
+ expect(styleOf('Saved', 'transform')).toBe(FRONT);
380
+ });
381
+
382
+ it('springs back when the touch is canceled', async () => {
383
+ const { card, raise } = await renderToaster();
384
+ await raise({ title: 'Saved' });
385
+ touch('touchstart', card('Saved'), 100);
386
+ touch('touchmove', card('Saved'), 140);
387
+ touch('touchcancel', card('Saved'));
388
+ expect(keyframesOn(card('Saved')).at(-1)).toEqual(snapBack(40));
389
+ });
390
+
391
+ it('slides away from the finger when dragged far enough', async () => {
392
+ const { card, queryByText, raise } = await renderToaster();
393
+ await raise({ title: 'Saved' });
394
+ touch('touchstart', card('Saved'), 100);
395
+ touch('touchmove', card('Saved'), 220);
396
+ touch('touchend', card('Saved'));
397
+ expect(keyframesOn(card('Saved')).at(-1)).toEqual(exit(120));
398
+
399
+ await waitForExit();
400
+ expect(queryByText('Saved')).toBeNull();
401
+ });
402
+
403
+ it("doesn't expire while held, and gets its full time again once let go", async () => {
404
+ const { card, queryByText, raise } = await renderToaster();
405
+ await raise({ title: 'Saved', duration: 100 });
406
+ touch('touchstart', card('Saved'), 100);
407
+
408
+ await sleep(100);
409
+ await waitForExit();
410
+ expect(queryByText('Saved')).not.toBeNull();
411
+
412
+ touch('touchend', card('Saved'));
413
+ await sleep(100);
414
+ await waitForExit();
415
+ expect(queryByText('Saved')).toBeNull();
416
+ });
417
+
418
+ it('ignores touches on a toast behind the front one', async () => {
419
+ const { card, styleOf, raise } = await renderToaster();
420
+ await raise({ title: 'Older' });
421
+ await raise({ title: 'Newer' });
422
+ animate.mockClear();
423
+
424
+ touch('touchstart', card('Older'), 100);
425
+ touch('touchmove', card('Older'), 200);
426
+ touch('touchend', card('Older'));
427
+ await waitForUpdate();
428
+ expect(styleOf('Older', 'transform')).toBe(SECOND);
429
+ expect(animate).not.toHaveBeenCalled();
430
+ });
431
+
432
+ it('ignores touches without a position', async () => {
433
+ const { card, styleOf, raise } = await renderToaster();
434
+ await raise({ title: 'Saved' });
435
+
436
+ // Without a starting point, there's no drag to follow.
437
+ touch('touchstart', card('Saved'));
438
+ touch('touchmove', card('Saved'), 200);
439
+ await waitForUpdate();
440
+ expect(styleOf('Saved', 'transform')).toBe(FRONT);
441
+
442
+ // A move without a position leaves the toast where it was.
443
+ touch('touchstart', card('Saved'), 100);
444
+ touch('touchmove', card('Saved'), 140);
445
+ touch('touchmove', card('Saved'));
446
+ await waitForUpdate();
447
+ expect(styleOf('Saved', 'transform')).toBe('translateY(40px) scale(1)');
448
+ });
449
+
450
+ it("doesn't spring back when a tap dismissed it mid-drag", async () => {
451
+ const { card, queryByText, raise } = await renderToaster();
452
+ await raise({ title: 'Saved' });
453
+ touch('touchstart', card('Saved'), 100);
454
+ touch('touchmove', card('Saved'), 120);
455
+ fireEvent.tap(card('Saved'));
456
+ touch('touchend', card('Saved'));
457
+ expect(keyframesOn(card('Saved'))).toEqual([entrance, exit(20)]);
458
+
459
+ await waitForExit();
460
+ expect(queryByText('Saved')).toBeNull();
461
+ });
340
462
  });
341
463
  });
342
- void ElementRef;