@blotch/dolan 22.2.3 → 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 (185) hide show
  1. package/dist/commands/add.js +9 -1
  2. package/dist/commands/add.js.map +1 -1
  3. package/dist/commands/add.test.js +36 -3
  4. package/dist/commands/add.test.js.map +1 -1
  5. package/dist/commands/doctor.js +187 -7
  6. package/dist/commands/doctor.js.map +1 -1
  7. package/dist/commands/doctor.test.js +280 -7
  8. package/dist/commands/doctor.test.js.map +1 -1
  9. package/dist/commands/init.d.ts +2 -2
  10. package/dist/commands/init.js +38 -18
  11. package/dist/commands/init.js.map +1 -1
  12. package/dist/commands/init.test.js +107 -1
  13. package/dist/commands/init.test.js.map +1 -1
  14. package/dist/commands/theme.js +1 -1
  15. package/dist/commands/theme.js.map +1 -1
  16. package/dist/commands/update.d.ts +5 -0
  17. package/dist/commands/update.js +126 -45
  18. package/dist/commands/update.js.map +1 -1
  19. package/dist/commands/update.test.js +314 -2
  20. package/dist/commands/update.test.js.map +1 -1
  21. package/dist/lockfile.d.ts +7 -0
  22. package/dist/lockfile.js +36 -0
  23. package/dist/lockfile.js.map +1 -1
  24. package/dist/lockfile.test.d.ts +1 -0
  25. package/dist/lockfile.test.js +25 -0
  26. package/dist/lockfile.test.js.map +1 -0
  27. package/dist/registry.d.ts +6 -0
  28. package/dist/registry.js +48 -2
  29. package/dist/registry.js.map +1 -1
  30. package/dist/registry.test.js +45 -3
  31. package/dist/registry.test.js.map +1 -1
  32. package/dist/test-utils.d.ts +5 -0
  33. package/dist/test-utils.js +6 -1
  34. package/dist/test-utils.js.map +1 -1
  35. package/dist/testing/fire-lynx-event.d.ts +14 -0
  36. package/dist/testing/fire-lynx-event.js +19 -0
  37. package/dist/testing/fire-lynx-event.js.map +1 -0
  38. package/dist/testing/fire-lynx-event.test.d.ts +1 -0
  39. package/dist/testing/fire-lynx-event.test.js +44 -0
  40. package/dist/testing/fire-lynx-event.test.js.map +1 -0
  41. package/dist/testing/lynx-main-thread.d.ts +22 -0
  42. package/dist/testing/lynx-main-thread.js +11 -0
  43. package/dist/testing/lynx-main-thread.js.map +1 -0
  44. package/dist/testing/lynx-main-thread.test.d.ts +1 -0
  45. package/dist/testing/lynx-main-thread.test.js +24 -0
  46. package/dist/testing/lynx-main-thread.test.js.map +1 -0
  47. package/dist/testing/set-input-signal.d.ts +5 -4
  48. package/dist/testing/set-input-signal.js +5 -4
  49. package/dist/testing/set-input-signal.js.map +1 -1
  50. package/dist/ui/components/accordion/accordion.test.ts +158 -144
  51. package/dist/ui/components/accordion/accordion.ts +19 -14
  52. package/dist/ui/components/action-sheet/action-sheet.test.ts +272 -187
  53. package/dist/ui/components/action-sheet/action-sheet.ts +40 -34
  54. package/dist/ui/components/alert/alert.test.ts +67 -19
  55. package/dist/ui/components/alert/alert.ts +14 -16
  56. package/dist/ui/components/alert-dialog/alert-dialog.test.ts +206 -139
  57. package/dist/ui/components/alert-dialog/alert-dialog.ts +32 -26
  58. package/dist/ui/components/area-chart/area-chart.test.ts +81 -1
  59. package/dist/ui/components/avatar/avatar.test.ts +89 -45
  60. package/dist/ui/components/badge/badge.test.ts +57 -23
  61. package/dist/ui/components/badge/badge.ts +12 -14
  62. package/dist/ui/components/bar-chart/bar-chart.test.ts +82 -1
  63. package/dist/ui/components/bottom-sheet/bottom-sheet.test.ts +373 -297
  64. package/dist/ui/components/bottom-sheet/bottom-sheet.ts +34 -33
  65. package/dist/ui/components/button/button.test.ts +611 -2
  66. package/dist/ui/components/button/button.ts +431 -62
  67. package/dist/ui/components/button/index.ts +2 -0
  68. package/dist/ui/components/button-group/button-group.test.ts +484 -2
  69. package/dist/ui/components/button-group/button-group.ts +380 -68
  70. package/dist/ui/components/button-group/index.ts +3 -0
  71. package/dist/ui/components/candlestick-chart/candlestick-chart.test.ts +82 -1
  72. package/dist/ui/components/card/card.test.ts +82 -62
  73. package/dist/ui/components/chart/chart-presets.test.ts +197 -1
  74. package/dist/ui/components/chart/chart-presets.ts +8 -1
  75. package/dist/ui/components/chart/chart-theme.ts +12 -0
  76. package/dist/ui/components/chart/chart.test.ts +1017 -0
  77. package/dist/ui/components/chart/chart.ts +56 -43
  78. package/dist/ui/components/checkbox/checkbox.test.ts +124 -42
  79. package/dist/ui/components/checkbox/checkbox.ts +23 -23
  80. package/dist/ui/components/collapsible/collapsible.test.ts +158 -81
  81. package/dist/ui/components/collapsible/collapsible.ts +19 -14
  82. package/dist/ui/components/context-menu/context-menu.test.ts +224 -0
  83. package/dist/ui/components/context-menu/context-menu.ts +110 -0
  84. package/dist/ui/components/context-menu/index.ts +1 -0
  85. package/dist/ui/components/dialog/dialog.test.ts +210 -152
  86. package/dist/ui/components/dialog/dialog.ts +29 -23
  87. package/dist/ui/components/dock/dock-scroll.test.ts +130 -0
  88. package/dist/ui/components/dock/dock-scroll.ts +133 -0
  89. package/dist/ui/components/dock/dock.test.ts +749 -0
  90. package/dist/ui/components/dock/dock.ts +960 -0
  91. package/dist/ui/components/dock/index.ts +2 -0
  92. package/dist/ui/components/empty-state/empty-state.test.ts +28 -39
  93. package/dist/ui/components/glass/concentric-radius.test.ts +13 -0
  94. package/dist/ui/components/glass/concentric-radius.ts +17 -0
  95. package/dist/ui/components/glass/glass-design.test.ts +78 -0
  96. package/dist/ui/components/glass/glass-design.ts +119 -0
  97. package/dist/ui/components/glass/glass.test.ts +338 -0
  98. package/dist/ui/components/glass/glass.ts +360 -0
  99. package/dist/ui/components/glass/index.ts +18 -0
  100. package/dist/ui/components/icon/icon-theme.test.ts +36 -0
  101. package/dist/ui/components/icon/icon-theme.ts +36 -13
  102. package/dist/ui/components/icon/icon.test.ts +52 -41
  103. package/dist/ui/components/input/input.test.ts +178 -101
  104. package/dist/ui/components/input/input.ts +15 -12
  105. package/dist/ui/components/label/label.test.ts +11 -24
  106. package/dist/ui/components/line-chart/line-chart.test.ts +76 -1
  107. package/dist/ui/components/list/list.test.ts +80 -49
  108. package/dist/ui/components/menu/index.ts +25 -0
  109. package/dist/ui/components/menu/menu-placement.test.ts +153 -0
  110. package/dist/ui/components/menu/menu-placement.ts +159 -0
  111. package/dist/ui/components/menu/menu-popup.test.ts +632 -0
  112. package/dist/ui/components/menu/menu-popup.ts +544 -0
  113. package/dist/ui/components/menu/menu.test.ts +429 -0
  114. package/dist/ui/components/menu/menu.ts +415 -0
  115. package/dist/ui/components/nav-drawer/nav-drawer.test.ts +425 -260
  116. package/dist/ui/components/nav-drawer/nav-drawer.ts +72 -56
  117. package/dist/ui/components/pagination/pagination.test.ts +136 -86
  118. package/dist/ui/components/progress/progress.test.ts +102 -46
  119. package/dist/ui/components/progress/progress.ts +52 -54
  120. package/dist/ui/components/radio-group/radio-group.test.ts +128 -83
  121. package/dist/ui/components/radio-group/radio-group.ts +24 -24
  122. package/dist/ui/components/scatter-chart/scatter-chart.test.ts +74 -1
  123. package/dist/ui/components/scroll-area/scroll-area.test.ts +72 -36
  124. package/dist/ui/components/select/select.test.ts +138 -78
  125. package/dist/ui/components/select/select.ts +17 -11
  126. package/dist/ui/components/separator/separator.test.ts +18 -17
  127. package/dist/ui/components/sheet/sheet.test.ts +64 -11
  128. package/dist/ui/components/sheet/sheet.ts +9 -1
  129. package/dist/ui/components/skeleton/skeleton.test.ts +24 -7
  130. package/dist/ui/components/skeleton/skeleton.ts +10 -16
  131. package/dist/ui/components/spinner/spinner.test.ts +64 -46
  132. package/dist/ui/components/spinner/spinner.ts +16 -19
  133. package/dist/ui/components/switch/switch.test.ts +116 -46
  134. package/dist/ui/components/switch/switch.ts +8 -13
  135. package/dist/ui/components/tab-bar/index.ts +1 -0
  136. package/dist/ui/components/tab-bar/tab-bar.test.ts +280 -0
  137. package/dist/ui/components/tab-bar/tab-bar.ts +230 -0
  138. package/dist/ui/components/tabs/tabs.test.ts +215 -157
  139. package/dist/ui/components/tabs/tabs.ts +110 -118
  140. package/dist/ui/components/textarea/textarea.test.ts +206 -100
  141. package/dist/ui/components/textarea/textarea.ts +14 -12
  142. package/dist/ui/components/toast/toast.test.ts +434 -350
  143. package/dist/ui/components/toast/toast.ts +42 -51
  144. package/dist/ui/components/toggle/toggle.test.ts +247 -140
  145. package/dist/ui/theme/chart-colors.test.ts +8 -3
  146. package/dist/ui/theme/dark.css +38 -2
  147. package/dist/ui/theme/default.css +55 -7
  148. package/dist/ui/theme/icon-colors.test.ts +8 -3
  149. package/dist/ui/theme/rgb-channels.test.ts +84 -0
  150. package/dist/ui/theme/tailwind-build.test.ts +114 -0
  151. package/dist/ui/theme/tailwind-css.test.ts +83 -0
  152. package/dist/ui/theme/tailwind-plugin.test.ts +42 -2
  153. package/dist/ui/theme/tailwind-plugin.ts +93 -29
  154. package/dist/ui/theme/tailwind.css +113 -0
  155. package/dist/ui/utils/animate.test.ts +13 -1
  156. package/dist/utils/analyze.d.ts +14 -0
  157. package/dist/utils/analyze.js +15 -0
  158. package/dist/utils/analyze.js.map +1 -1
  159. package/dist/utils/analyze.test.js +14 -1
  160. package/dist/utils/analyze.test.js.map +1 -1
  161. package/dist/utils/installed.d.ts +12 -0
  162. package/dist/utils/installed.js +16 -0
  163. package/dist/utils/installed.js.map +1 -1
  164. package/dist/utils/installed.test.js +17 -1
  165. package/dist/utils/installed.test.js.map +1 -1
  166. package/dist/utils/resolve-paths.d.ts +4 -4
  167. package/dist/utils/resolve-paths.js +5 -7
  168. package/dist/utils/resolve-paths.js.map +1 -1
  169. package/dist/utils/resolve-paths.test.js +9 -5
  170. package/dist/utils/resolve-paths.test.js.map +1 -1
  171. package/dist/utils/tailwind.d.ts +25 -0
  172. package/dist/utils/tailwind.js +51 -0
  173. package/dist/utils/tailwind.js.map +1 -0
  174. package/dist/utils/tailwind.test.d.ts +1 -0
  175. package/dist/utils/tailwind.test.js +71 -0
  176. package/dist/utils/tailwind.test.js.map +1 -0
  177. package/package.json +9 -1
  178. package/dist/ui/components/area-chart/area-chart-render.test.ts +0 -63
  179. package/dist/ui/components/bar-chart/bar-chart-render.test.ts +0 -58
  180. package/dist/ui/components/button/button-render.test.ts +0 -132
  181. package/dist/ui/components/button-group/button-group-render.test.ts +0 -146
  182. package/dist/ui/components/candlestick-chart/candlestick-chart-render.test.ts +0 -59
  183. package/dist/ui/components/chart/chart-render.test.ts +0 -730
  184. package/dist/ui/components/line-chart/line-chart-render.test.ts +0 -83
  185. package/dist/ui/components/scatter-chart/scatter-chart-render.test.ts +0 -64
@@ -1,69 +1,113 @@
1
- import { Component } from '@angular/core';
1
+ import { Component, signal } from '@angular/core';
2
2
  import { render, waitForUpdate } from '@blotch/angular-lynx-testing-library';
3
- import { LYNX_ELEMENTS } from '@blotch/angular-lynx';
4
3
  import { describe, expect, it } from 'vitest';
5
- import { setInputSignal } from '../../../test-utils/set-input-signal';
4
+ import { fireLynxEvent } from '../../../test-utils/fire-lynx-event';
6
5
  import { UiAvatar } from './avatar';
7
6
 
7
+ type AvatarSize = 'default' | 'sm' | 'lg' | 'xl' | '2xl';
8
+
8
9
  @Component({
9
- standalone: true,
10
- imports: [UiAvatar, LYNX_ELEMENTS],
11
- template: `<ui-avatar [src]="src" [fallback]="fallback" />`,
10
+ imports: [UiAvatar],
11
+ template: `
12
+ <ui-avatar [src]="src()" fallback="AB" [size]="size()" class="border" />
13
+ `,
12
14
  })
13
15
  class AvatarHost {
14
- src = '';
15
- fallback = 'AB';
16
+ readonly src = signal('');
17
+ readonly size = signal<AvatarSize>('default');
16
18
  }
17
19
 
20
+ const renderAvatar = async (src = '') => {
21
+ const result = await render(AvatarHost);
22
+ const host = result.componentRef.instance as AvatarHost;
23
+ host.src.set(src);
24
+ await waitForUpdate();
25
+ return {
26
+ ...result,
27
+ host,
28
+ avatar: () => result.container.querySelector('.overflow-hidden')!,
29
+ image: () => result.container.querySelector('image'),
30
+ /** The pulsing placeholder over the image while it loads. */
31
+ skeleton: () => result.container.querySelector('.inset-0'),
32
+ initials: () => result.queryByText('AB'),
33
+ };
34
+ };
35
+
18
36
  describe('UiAvatar', () => {
19
- it('renders the fallback branch when src is empty', async () => {
20
- const { container } = await render(AvatarHost);
21
- expect(container).toBeTruthy();
37
+ it('shows the initials in a muted circle without an image', async () => {
38
+ const { avatar, image, initials } = await renderAvatar();
39
+ expect(image()).toBeNull();
40
+ expect(initials()?.parentElement?.classList).toContain('bg-muted');
41
+ expect(avatar().classList).toContain('border');
22
42
  });
23
43
 
24
- it('renders without a src (default fallback path)', async () => {
25
- const { container } = await render(UiAvatar);
26
- expect(container).toBeTruthy();
44
+ it('covers the image with a placeholder while it loads', async () => {
45
+ const { image, skeleton, initials } = await renderAvatar(
46
+ 'https://example.com/avatar.png',
47
+ );
48
+ expect(image()?.getAttribute('src')).toBe('https://example.com/avatar.png');
49
+ expect(skeleton()).not.toBeNull();
50
+ expect(initials()).toBeNull();
27
51
  });
28
52
 
29
- it('renders the image branch when src is programmatically set', async () => {
30
- // JIT template bindings are inert against input(); drive src directly so
31
- // the @if(src()) branch fires and the image + skeleton render.
32
- const { componentRef, container } = await render(UiAvatar);
33
- setInputSignal(
34
- (componentRef.instance as UiAvatar).src,
53
+ it('reveals the image once it loads', async () => {
54
+ const { image, skeleton, initials } = await renderAvatar(
35
55
  'https://example.com/avatar.png',
36
56
  );
57
+ fireLynxEvent(image()!, 'load');
37
58
  await waitForUpdate();
38
- expect(container.querySelector('image')).toBeTruthy();
59
+ expect(skeleton()).toBeNull();
60
+ expect(image()).not.toBeNull();
61
+
62
+ // Lynx can dispatch `load` again as the placeholder's animation stops:
63
+ // the avatar stays as it is.
64
+ fireLynxEvent(image()!, 'load');
65
+ await waitForUpdate();
66
+ expect(skeleton()).toBeNull();
67
+ expect(image()).not.toBeNull();
68
+ expect(initials()).toBeNull();
39
69
  });
40
70
 
41
- it('keeps the image load / error handlers idempotent', async () => {
42
- // Directly invoke the protected handlers via the instance to cover the
43
- // re-entrancy latches — Lynx's on-device behavior re-dispatches the load
44
- // event when the skeleton animation tears down.
45
- const { componentRef } = await render(UiAvatar);
46
- const inst = componentRef.instance as unknown as {
47
- onImageLoad: () => void;
48
- onImageError: () => void;
49
- loaded: () => boolean;
50
- errored: () => boolean;
51
- };
52
- inst.onImageLoad();
53
- inst.onImageLoad(); // second call is a no-op (loaded latch)
54
- expect(inst.loaded()).toBe(true);
71
+ it('falls back to the initials when the image fails to load', async () => {
72
+ const { image, skeleton, initials } = await renderAvatar(
73
+ 'https://example.com/missing.png',
74
+ );
75
+ // Lynx can dispatch the event again before the avatar updates, as the
76
+ // placeholder's animation stops: the second one changes nothing.
77
+ fireLynxEvent(image()!, 'error', { detail: { errMsg: 'Not found' } });
78
+ fireLynxEvent(image()!, 'error', { detail: { errMsg: 'Not found' } });
79
+ await waitForUpdate();
80
+ expect(image()).toBeNull();
81
+ expect(skeleton()).toBeNull();
82
+ expect(initials()).not.toBeNull();
83
+ });
55
84
 
56
- inst.onImageError();
57
- inst.onImageError(); // second call is a no-op (errored latch)
58
- expect(inst.errored()).toBe(true);
85
+ it.each<[AvatarSize, string, string]>([
86
+ ['sm', 'h-8', 'text-sm'],
87
+ ['default', 'h-10', 'text-sm'],
88
+ ['lg', 'h-14', 'text-lg'],
89
+ ['xl', 'h-20', 'text-2xl'],
90
+ ['2xl', 'h-28', 'text-3xl'],
91
+ ])('sizes a %s avatar and its initials', async (size, height, text) => {
92
+ const { host, avatar, initials } = await renderAvatar();
93
+ host.size.set(size);
94
+ await waitForUpdate();
95
+ expect(avatar().classList).toContain(height);
96
+ expect(initials()?.classList).toContain(text);
59
97
  });
60
98
 
61
- it('applies size-specific classes for each supported size', async () => {
62
- const { componentRef, container } = await render(UiAvatar);
63
- for (const size of ['sm', 'default', 'lg', 'xl', '2xl'] as const) {
64
- setInputSignal((componentRef.instance as UiAvatar).size, size);
65
- await waitForUpdate();
66
- expect(container).toBeTruthy();
67
- }
99
+ it.each<[AvatarSize, string]>([
100
+ ['sm', 'h-8'],
101
+ ['default', 'h-10'],
102
+ ['lg', 'h-14'],
103
+ ['xl', 'h-20'],
104
+ ['2xl', 'h-28'],
105
+ ])('sizes the image of a %s avatar', async (size, height) => {
106
+ const { host, image } = await renderAvatar(
107
+ 'https://example.com/avatar.png',
108
+ );
109
+ host.size.set(size);
110
+ await waitForUpdate();
111
+ expect(image()?.classList).toContain(height);
68
112
  });
69
113
  });
@@ -1,25 +1,30 @@
1
- import { render } from '@blotch/angular-lynx-testing-library';
2
- import { Component } from '@angular/core';
3
- import { describe, expect, it } from 'vitest';
4
- import { LYNX_ELEMENTS } from '@blotch/angular-lynx';
5
- import { UiBadge, badgeVariants } from './badge';
1
+ import { Component, signal } from '@angular/core';
2
+ import { render, waitForUpdate } from '@blotch/angular-lynx-testing-library';
3
+ import { describe, expect, it, vi } from 'vitest';
4
+ import { lynxMainThread } from '../../../test-utils/lynx-main-thread';
5
+ import { type BadgeVariant, UiBadge, badgeVariants } from './badge';
6
6
 
7
- /**
8
- * Host wrappers project content and bind inputs the way real app templates do.
9
- * Required-input components would need this; UiBadge has none but the pattern
10
- * keeps content projection (<ng-content>) exercised.
11
- */
12
7
  @Component({
13
- standalone: true,
14
- imports: [UiBadge, LYNX_ELEMENTS],
15
- template: `<ui-badge [variant]="variant" [animated]="false"
16
- >Status</ui-badge
17
- >`,
8
+ imports: [UiBadge],
9
+ template: `
10
+ <ui-badge [variant]="variant()" [animated]="animated()" class="ml-1">
11
+ Status
12
+ </ui-badge>
13
+ `,
18
14
  })
19
15
  class BadgeHost {
20
- variant: 'default' | 'secondary' | 'destructive' | 'outline' = 'default';
16
+ readonly variant = signal<BadgeVariant>('default');
17
+ readonly animated = signal(true);
21
18
  }
22
19
 
20
+ /**
21
+ * The keyframes of the animations started on `element`.
22
+ */
23
+ const keyframesOn = (calls: [Element, unknown[]][], element: Element) =>
24
+ calls
25
+ .filter(([target, [operation]]) => target === element && operation === 0)
26
+ .map(([, args]) => args[2]);
27
+
23
28
  describe('badgeVariants', () => {
24
29
  it('returns default variant classes', () => {
25
30
  expect(badgeVariants()).toContain('bg-primary');
@@ -35,15 +40,44 @@ describe('badgeVariants', () => {
35
40
  });
36
41
 
37
42
  describe('UiBadge', () => {
38
- it('renders projected content', async () => {
43
+ it('renders its label in the variant colors', async () => {
44
+ const { componentRef, getByText } = await render(BadgeHost);
45
+ const label = getByText('Status');
46
+ expect(label.classList).toContain('text-primary-foreground');
47
+ expect(label.parentElement?.classList).toContain('bg-primary');
48
+ expect(label.parentElement?.classList).toContain('ml-1');
49
+
50
+ (componentRef.instance as BadgeHost).variant.set('outline');
51
+ await waitForUpdate();
52
+ expect(label.classList).toContain('text-foreground');
53
+ expect(label.parentElement?.classList).toContain('border-border');
54
+ });
55
+
56
+ it('pops in on first render', async () => {
57
+ const animate = vi.spyOn(lynxMainThread(), '__ElementAnimate');
39
58
  const { getByText } = await render(BadgeHost);
40
- expect(getByText('Status')).toBeTruthy();
59
+ const badge = getByText('Status').parentElement!;
60
+ expect(keyframesOn(animate.mock.calls, badge)).toEqual([
61
+ [
62
+ { transform: 'scale(0.6)', opacity: 0 },
63
+ { transform: 'scale(1)', opacity: 1 },
64
+ ],
65
+ ]);
66
+ animate.mockRestore();
41
67
  });
42
68
 
43
- it('runs the pop-in animation when animated is true', async () => {
44
- const { getByText } = await render(UiBadge);
45
- // With no host binding, animated defaults to true → effect runs popIn on the
46
- // container. Just assert it renders without throwing.
47
- expect(getByText).toBeTruthy();
69
+ it("doesn't animate when animated is off", async () => {
70
+ @Component({
71
+ imports: [UiBadge],
72
+ template: `<ui-badge [animated]="false">Status</ui-badge>`,
73
+ })
74
+ class StillBadgeHost {}
75
+
76
+ const animate = vi.spyOn(lynxMainThread(), '__ElementAnimate');
77
+ const { getByText } = await render(StillBadgeHost);
78
+ expect(
79
+ keyframesOn(animate.mock.calls, getByText('Status').parentElement!),
80
+ ).toEqual([]);
81
+ animate.mockRestore();
48
82
  });
49
83
  });
@@ -71,21 +71,19 @@ export class UiBadge {
71
71
  readonly animated = input(true);
72
72
  readonly userClass = input<string>('', { alias: 'class' });
73
73
 
74
- readonly containerRef = viewChild<ElementRef>('container');
74
+ readonly containerRef = viewChild.required<ElementRef>('container');
75
75
 
76
- constructor() {
77
- effect(() => {
78
- const el = this.containerRef()?.nativeElement;
79
- // popIn calls el.animate() which is inert in jsdom (the offscreen web
80
- // renderer path — see animate.ts's transitionTransform comment). The
81
- // effect's rendering-adjacent work is not exercisable from a JIT test,
82
- // so ignore the animation branch for coverage.
83
- /* v8 ignore next 3 */
84
- if (el && this.animated()) {
85
- popIn(el, { duration: 200 });
86
- }
87
- });
88
- }
76
+ /**
77
+ * Pops the badge in on first render, unless `animated` is off. A field
78
+ * rather than a constructor: Angular's test build keeps a function for a
79
+ * constructor that nothing calls, which an app's coverage report would show
80
+ * as uncovered.
81
+ */
82
+ protected readonly entrance = effect(() => {
83
+ if (this.animated()) {
84
+ popIn(this.containerRef().nativeElement, { duration: 200 });
85
+ }
86
+ });
89
87
 
90
88
  protected readonly containerClass = computed(() =>
91
89
  cn(badgeVariants({ variant: this.variant() }), this.userClass()),
@@ -1,3 +1,9 @@
1
+ import { Component, signal } from '@angular/core';
2
+ import {
3
+ fireEvent,
4
+ render,
5
+ waitForUpdate,
6
+ } from '@blotch/angular-lynx-testing-library';
1
7
  import { createChartRuntime } from '@tanstack/charts/runtime';
2
8
  import type {
3
9
  ChartDefinition,
@@ -5,7 +11,7 @@ import type {
5
11
  SceneRect,
6
12
  } from '@tanstack/charts/types';
7
13
  import { describe, expect, it } from 'vitest';
8
- import { barChartDefinition } from './bar-chart';
14
+ import { type BarDatum, UiBarChart, barChartDefinition } from './bar-chart';
9
15
 
10
16
  /**
11
17
  * Every bar in a definition's scene.
@@ -95,3 +101,78 @@ describe('barChartDefinition', () => {
95
101
  expect(found.map((bar) => bar.style?.fill)).toEqual(['red', 'blue', 'red']);
96
102
  });
97
103
  });
104
+
105
+ @Component({
106
+ imports: [UiBarChart],
107
+ template: `
108
+ <ui-bar-chart
109
+ [data]="data"
110
+ [width]="300"
111
+ [color]="color()"
112
+ [radius]="4"
113
+ (barTap)="tapped.push($event)"
114
+ />
115
+ `,
116
+ })
117
+ class BarChartHost {
118
+ readonly data: readonly BarDatum[] = [
119
+ { x: 'Mon', y: 5 },
120
+ { x: 'Tue', y: 8 },
121
+ ];
122
+ readonly color = signal('red');
123
+ /** Every `barTap`, in order. */
124
+ readonly tapped: BarDatum[] = [];
125
+ }
126
+
127
+ /**
128
+ * The SVG the chart's marks layer draws.
129
+ */
130
+ const marksSvg = (container: Element) =>
131
+ container.querySelectorAll('svg')[1]!.getAttribute('content')!;
132
+
133
+ /**
134
+ * The middle of each bar the chart drew.
135
+ */
136
+ const barsDrawn = (container: Element) =>
137
+ [
138
+ ...marksSvg(container).matchAll(
139
+ /<rect x="([\d.-]+)" y="([\d.-]+)" width="([\d.-]+)" height="([\d.-]+)"[^>]*fill=/g,
140
+ ),
141
+ ].map(([, x, y, width, height]) => ({
142
+ x: Number(x) + Number(width) / 2,
143
+ y: Number(y) + Number(height) / 2,
144
+ }));
145
+
146
+ /**
147
+ * Touches a spot of the chart and lifts the finger right there: a tap.
148
+ */
149
+ const tap = async (container: Element, at: { x: number; y: number }) => {
150
+ const root = container.querySelector('[accessibility-label]')!;
151
+ fireEvent.touchstart(root, { touches: [at] });
152
+ fireEvent.touchend(root, { touches: [at] });
153
+ await waitForUpdate();
154
+ };
155
+
156
+ describe('UiBarChart', () => {
157
+ it('draws a bar per datum with its inputs', async () => {
158
+ const { componentRef, container, getByText } = await render(BarChartHost);
159
+ expect(barsDrawn(container)).toHaveLength(2);
160
+ expect(marksSvg(container)).toContain('fill="red"');
161
+ expect(marksSvg(container)).toContain('rx="4"');
162
+ expect(getByText('Mon')).toBeTruthy();
163
+
164
+ (componentRef.instance as BarChartHost).color.set('blue');
165
+ await waitForUpdate();
166
+ expect(marksSvg(container)).toContain('fill="blue"');
167
+ });
168
+
169
+ it("emits a tapped bar's datum, and nothing for a tap off the bars", async () => {
170
+ const { componentRef, container } = await render(BarChartHost);
171
+ const host = componentRef.instance as BarChartHost;
172
+ await tap(container, barsDrawn(container)[1]!);
173
+ expect(host.tapped).toEqual([host.data[1]]);
174
+
175
+ await tap(container, { x: -100, y: -100 });
176
+ expect(host.tapped).toEqual([host.data[1]]);
177
+ });
178
+ });