@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,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,51 +1,178 @@
1
+ import { Component, signal } from '@angular/core';
2
+ import {
3
+ fireEvent,
4
+ render,
5
+ waitForUpdate,
6
+ } from '@blotch/angular-lynx-testing-library';
7
+ import { createChartRuntime } from '@tanstack/charts/runtime';
8
+ import type {
9
+ ChartDefinition,
10
+ SceneNode,
11
+ SceneRect,
12
+ } from '@tanstack/charts/types';
1
13
  import { describe, expect, it } from 'vitest';
2
- import { computeBars } from './bar-chart';
3
-
4
- // Screen y grows downward, so a value ABOVE the baseline has a SMALLER pixel y
5
- // than baseY. Baseline pixel y = 100 in these cases (bottom of the plot).
6
- describe('computeBars', () => {
7
- it('draws a positive bar from the baseline up to the value, centered on cx', () => {
8
- // value projects to y=30 (above the baseline at y=100).
9
- const [bar] = computeBars([{ cx: 50, valueY: 30 }], 100, 12);
10
- expect(bar.top).toBe(30); // min(valueY, baseY) — the higher edge
11
- expect(bar.height).toBe(70); // |valueY - baseY|
12
- expect(bar.width).toBe(12);
13
- expect(bar.left).toBe(50 - 6); // cx - barWidth/2
14
- });
15
-
16
- it('draws a negative bar growing downward from the baseline', () => {
17
- // value projects BELOW the baseline, so valueY (140) > baseY (100).
18
- const [bar] = computeBars([{ cx: 20, valueY: 140 }], 100, 10);
19
- expect(bar.top).toBe(100); // min(valueY, baseY) = baseY here
20
- expect(bar.height).toBe(40); // grows down to the value
21
- });
22
-
23
- it('draws nothing (zero height) for a value exactly on the baseline', () => {
24
- // A bar's height is its datum: on the baseline there is nothing to draw, so
25
- // the default minBarHeight of 0 leaves it invisible rather than a sliver.
26
- const [bar] = computeBars([{ cx: 0, valueY: 100 }], 100, 12);
27
- expect(bar.height).toBe(0);
28
- });
29
-
30
- it('honors a custom minimum bar height', () => {
31
- const [bar] = computeBars([{ cx: 0, valueY: 100 }], 100, 12, 3);
32
- expect(bar.height).toBe(3);
33
- });
34
-
35
- it('centers every bar on its own cx', () => {
36
- const bars = computeBars(
37
- [
38
- { cx: 10, valueY: 40 },
39
- { cx: 60, valueY: 20 },
40
- ],
41
- 100,
42
- 8,
14
+ import { type BarDatum, UiBarChart, barChartDefinition } from './bar-chart';
15
+
16
+ /**
17
+ * Every bar in a definition's scene.
18
+ */
19
+ const bars = (definition: ChartDefinition<any, any, any>) => {
20
+ const scene = createChartRuntime().render(definition, {
21
+ width: 300,
22
+ height: 200,
23
+ });
24
+ const found: SceneRect[] = [];
25
+ const walk = (nodes: readonly SceneNode[]) => {
26
+ for (const node of nodes) {
27
+ if (node.kind === 'group') {
28
+ if (!node.focus) walk(node.children);
29
+ } else if (node.kind === 'rect') found.push(node);
30
+ }
31
+ };
32
+ walk(scene.nodes);
33
+ return { scene, found };
34
+ };
35
+
36
+ describe('barChartDefinition', () => {
37
+ const week = [
38
+ { x: 'Mon', y: 5 },
39
+ { x: 'Tue', y: 8 },
40
+ { x: 'Wed', y: 3 },
41
+ ];
42
+
43
+ it('puts category labels on a band scale', () => {
44
+ const { scene, found } = bars(
45
+ barChartDefinition({ data: week, radius: 4 }),
46
+ );
47
+ expect(found).toHaveLength(3);
48
+ expect(scene.scales['x']!.bandwidth).toBeGreaterThan(0);
49
+ expect(found[0]!.style?.fill).toBe('var(--primary)');
50
+ });
51
+
52
+ it('cycles per-bar colors on categories', () => {
53
+ const { found } = bars(
54
+ barChartDefinition({ data: week, colors: ['red', 'blue'] }),
55
+ );
56
+ expect(found.map((bar) => bar.style?.fill)).toEqual(['red', 'blue', 'red']);
57
+ });
58
+
59
+ it('sizes bars on a band scale by barWidthRatio, clamped to 0–1', () => {
60
+ const wide = bars(
61
+ barChartDefinition({
62
+ data: week,
63
+ barWidthRatio: 2,
64
+ showXGrid: true,
65
+ xAxisLabel: 'Day',
66
+ }),
67
+ );
68
+ const narrow = bars(barChartDefinition({ data: week, barWidthRatio: 0.2 }));
69
+ expect(narrow.found[0]!.width).toBeLessThan(wide.found[0]!.width);
70
+ });
71
+
72
+ const series = [
73
+ { x: 0, y: 4 },
74
+ { x: 1, y: -2 },
75
+ { x: 3, y: 6 },
76
+ ];
77
+
78
+ it('places numeric bars at their values, the ratio of the smallest gap wide', () => {
79
+ const { scene, found } = bars(
80
+ barChartDefinition({ data: series, barWidthRatio: 0.5 }),
81
+ );
82
+ const x = scene.scales['x']!;
83
+ expect(x.bandwidth).toBe(0);
84
+ // The x axis has half a gap of room either side for the edge bars.
85
+ expect(x.domain[0]).toBeLessThanOrEqual(-0.5);
86
+ const unit = Math.abs(x.map(1) - x.map(0));
87
+ // rect trims a hair off each edge, so allow a couple of pixels.
88
+ expect(Math.abs(found[0]!.width - unit * 0.5)).toBeLessThan(2);
89
+ // Negative values grow down from the baseline.
90
+ expect(Math.abs(found[1]!.y - scene.scales['y']!.map(0))).toBeLessThan(1);
91
+ });
92
+
93
+ it('colors numeric bars through the color scale', () => {
94
+ const { found } = bars(
95
+ barChartDefinition({
96
+ data: series,
97
+ colors: ['red', 'blue'],
98
+ tooltip: false,
99
+ }),
43
100
  );
44
- expect(bars[0].left).toBe(10 - 4);
45
- expect(bars[1].left).toBe(60 - 4);
101
+ expect(found.map((bar) => bar.style?.fill)).toEqual(['red', 'blue', 'red']);
46
102
  });
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]]);
47
174
 
48
- it('returns an empty array for no bars', () => {
49
- expect(computeBars([], 100, 12)).toEqual([]);
175
+ await tap(container, { x: -100, y: -100 });
176
+ expect(host.tapped).toEqual([host.data[1]]);
50
177
  });
51
178
  });