@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,7 +1,7 @@
1
1
  import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
2
  import { readFileSync, writeFileSync, existsSync, readdirSync } from 'node:fs';
3
3
  import { join } from 'node:path';
4
- import { createFixture, DEFAULT_CONFIG } from '../test-utils';
4
+ import { createFixture, DEFAULT_CONFIG, shippedTheme, } from '../test-utils';
5
5
  import { hashContent } from '../lockfile';
6
6
  const spinnerMock = { start: vi.fn(), stop: vi.fn() };
7
7
  vi.mock('@clack/prompts', () => ({
@@ -20,14 +20,29 @@ vi.mock('@clack/prompts', () => ({
20
20
  cancel: vi.fn(),
21
21
  isCancel: vi.fn().mockReturnValue(false),
22
22
  }));
23
+ // Vitest's stdin isn't a terminal, which reads as "nobody can answer". Most
24
+ // tests are about the questions, so they run as if in one.
25
+ let interactive = true;
26
+ vi.mock('../utils/interactive.js', () => ({
27
+ isInteractive: () => interactive,
28
+ }));
23
29
  let fixture;
24
30
  vi.mock('../utils/resolve-paths.js', () => ({
25
31
  getUiSourceDir: () => fixture.uiDir,
26
32
  getComponentSourceDir: (name) => join(fixture.uiDir, 'components', name),
27
- getComponentFiles: (name) => readdirSync(join(fixture.uiDir, 'components', name)).filter((f) => f.endsWith('.ts')),
33
+ getComponentFiles: (name, { tests }) => readdirSync(join(fixture.uiDir, 'components', name)).filter((f) => f.endsWith('.ts') && (tests || !f.endsWith('.test.ts'))),
28
34
  rewriteImports: (content) => content,
29
35
  }));
36
+ // The install step runs the project's package manager; record calls instead.
37
+ const installComponentPackages = vi.fn();
38
+ vi.mock('./install-packages.js', () => ({
39
+ installComponentPackages: (...args) => installComponentPackages(...args),
40
+ }));
30
41
  beforeEach(() => {
42
+ interactive = true;
43
+ // Each test sees only its own calls. Without this, an assertion could pass
44
+ // on a message an earlier test logged.
45
+ vi.clearAllMocks();
31
46
  vi.spyOn(process, 'exit').mockImplementation((code) => {
32
47
  throw new Error(`process.exit(${code})`);
33
48
  });
@@ -91,28 +106,28 @@ describe('updateCommand --selective', () => {
91
106
  it('handles mixed decisions across multiple files', async () => {
92
107
  const cardOriginal = 'export const Card = { v: 1 };';
93
108
  const cardUpstream = 'export const Card = { v: 2 };';
94
- const spinnerOriginal = 'export const Spinner = { v: 1 };';
95
- const spinnerUpstream = 'export const Spinner = { v: 2 };';
109
+ const skeletonOriginal = 'export const Skeleton = { v: 1 };';
110
+ const skeletonUpstream = 'export const Skeleton = { v: 2 };';
96
111
  const cardHash = hashContent(cardOriginal);
97
- const spinnerHash = hashContent(spinnerOriginal);
112
+ const skeletonHash = hashContent(skeletonOriginal);
98
113
  fixture = createFixture({
99
114
  config: DEFAULT_CONFIG,
100
115
  lockfile: {
101
116
  version: 1,
102
117
  components: {
103
118
  card: { 'card.ts': { hash: cardHash } },
104
- spinner: { 'spinner.ts': { hash: spinnerHash } },
119
+ skeleton: { 'skeleton.ts': { hash: skeletonHash } },
105
120
  },
106
121
  utils: {},
107
122
  theme: {},
108
123
  },
109
124
  components: {
110
125
  card: { 'card.ts': cardOriginal },
111
- spinner: { 'spinner.ts': spinnerOriginal },
126
+ skeleton: { 'skeleton.ts': skeletonOriginal },
112
127
  },
113
128
  uiSource: {
114
129
  card: { 'card.ts': cardUpstream },
115
- spinner: { 'spinner.ts': spinnerUpstream },
130
+ skeleton: { 'skeleton.ts': skeletonUpstream },
116
131
  },
117
132
  });
118
133
  vi.spyOn(process, 'cwd').mockReturnValue(fixture.dir);
@@ -124,11 +139,11 @@ describe('updateCommand --selective', () => {
124
139
  await updateCommand({ selective: true });
125
140
  // Card should be updated (first select → upstream)
126
141
  expect(readInstalledFile(fixture.dir, 'card', 'card.ts')).toBe(cardUpstream);
127
- // Spinner should be skipped (second select → keep)
128
- expect(readInstalledFile(fixture.dir, 'spinner', 'spinner.ts')).toBe(spinnerOriginal);
142
+ // Skeleton should be skipped (second select → keep)
143
+ expect(readInstalledFile(fixture.dir, 'skeleton', 'skeleton.ts')).toBe(skeletonOriginal);
129
144
  const lockfile = readLockfile(fixture.dir);
130
145
  expect(lockfile.components.card['card.ts'].hash).toBe(hashContent(cardUpstream));
131
- expect(lockfile.components.spinner['spinner.ts'].hash).toBe(spinnerHash);
146
+ expect(lockfile.components.skeleton['skeleton.ts'].hash).toBe(skeletonHash);
132
147
  });
133
148
  it('does not create new-upstream file when user chooses skip', async () => {
134
149
  const upstream = 'export const Card = {};';
@@ -136,7 +151,7 @@ describe('updateCommand --selective', () => {
136
151
  config: DEFAULT_CONFIG,
137
152
  lockfile: {
138
153
  version: 1,
139
- components: {},
154
+ components: { card: {} },
140
155
  utils: {},
141
156
  theme: {},
142
157
  },
@@ -169,7 +184,6 @@ describe('updateCommand --selective', () => {
169
184
  });
170
185
  vi.spyOn(process, 'cwd').mockReturnValue(fixture.dir);
171
186
  const p = await import('@clack/prompts');
172
- p.select.mockClear();
173
187
  const { updateCommand } = await import('./update');
174
188
  await updateCommand({ force: true, selective: true });
175
189
  expect(readInstalledFile(fixture.dir, 'card', 'card.ts')).toBe(upstream);
@@ -219,7 +233,6 @@ describe('updateCommand --selective', () => {
219
233
  vi.spyOn(process, 'cwd').mockReturnValue(fixture.dir);
220
234
  const p = await import('@clack/prompts');
221
235
  p.confirm.mockResolvedValue(true);
222
- p.select.mockClear();
223
236
  const { updateCommand } = await import('./update');
224
237
  await updateCommand({});
225
238
  expect(p.confirm).toHaveBeenCalled();
@@ -306,8 +319,6 @@ describe('updateCommand --force', () => {
306
319
  });
307
320
  vi.spyOn(process, 'cwd').mockReturnValue(fixture.dir);
308
321
  const p = await import('@clack/prompts');
309
- p.select.mockClear();
310
- p.confirm.mockClear();
311
322
  const { updateCommand } = await import('./update');
312
323
  await updateCommand({ force: true });
313
324
  // Local edit discarded, file reset to upstream.
@@ -337,7 +348,6 @@ describe('updateCommand --force', () => {
337
348
  });
338
349
  vi.spyOn(process, 'cwd').mockReturnValue(fixture.dir);
339
350
  const p = await import('@clack/prompts');
340
- p.select.mockClear();
341
351
  const { updateCommand } = await import('./update');
342
352
  await updateCommand({ force: true });
343
353
  expect(readInstalledFile(fixture.dir, 'card', 'card.ts')).toBe(upstream);
@@ -345,14 +355,14 @@ describe('updateCommand --force', () => {
345
355
  const lockfile = readLockfile(fixture.dir);
346
356
  expect(lockfile.components.card['card.ts'].hash).toBe(hashContent(upstream));
347
357
  });
348
- it('overwrites a drifted file with no lockfile entry (unknown base → conflict)', async () => {
358
+ it('overwrites a drifted file with no lockfile hash (unknown base → conflict)', async () => {
349
359
  // With no stored hash the base is unknown, so any local difference is
350
360
  // treated as a conflict. --force must still overwrite it.
351
361
  const userVersion = 'export const Card = { v: 1, local: true };';
352
362
  const upstream = 'export const Card = { v: 2 };';
353
363
  fixture = createFixture({
354
364
  config: DEFAULT_CONFIG,
355
- lockfile: { version: 1, components: {}, utils: {}, theme: {} },
365
+ lockfile: { version: 1, components: { card: {} }, utils: {}, theme: {} },
356
366
  components: { card: { 'card.ts': userVersion } },
357
367
  uiSource: { card: { 'card.ts': upstream } },
358
368
  });
@@ -477,25 +487,25 @@ describe('updateCommand --force', () => {
477
487
  version: 1,
478
488
  components: {
479
489
  card: { 'card.ts': { hash: hashContent('card-base') } },
480
- spinner: { 'spinner.ts': { hash: hashContent('spinner-base') } },
490
+ skeleton: { 'skeleton.ts': { hash: hashContent('skeleton-base') } },
481
491
  },
482
492
  utils: {},
483
493
  theme: {},
484
494
  },
485
495
  components: {
486
496
  card: { 'card.ts': 'card-local' },
487
- spinner: { 'spinner.ts': 'spinner-local' },
497
+ skeleton: { 'skeleton.ts': 'skeleton-local' },
488
498
  },
489
499
  uiSource: {
490
500
  card: { 'card.ts': 'card-upstream' },
491
- spinner: { 'spinner.ts': 'spinner-upstream' },
501
+ skeleton: { 'skeleton.ts': 'skeleton-upstream' },
492
502
  },
493
503
  });
494
504
  vi.spyOn(process, 'cwd').mockReturnValue(fixture.dir);
495
505
  const { updateCommand } = await import('./update');
496
506
  await updateCommand({ force: true });
497
507
  expect(readInstalledFile(fixture.dir, 'card', 'card.ts')).toBe('card-upstream');
498
- expect(readInstalledFile(fixture.dir, 'spinner', 'spinner.ts')).toBe('spinner-upstream');
508
+ expect(readInstalledFile(fixture.dir, 'skeleton', 'skeleton.ts')).toBe('skeleton-upstream');
499
509
  });
500
510
  it('does not delete untracked local files sitting alongside components', async () => {
501
511
  // Force overwrites tracked files but must not touch files the registry
@@ -606,6 +616,26 @@ describe('updateCommand --force', () => {
606
616
  const lockfile = readLockfile(fixture.dir);
607
617
  expect(lockfile.theme['default.css'].hash).toBe(hashContent(upstream));
608
618
  });
619
+ it("adds the Tailwind plugin's test once the project has the testing library", async () => {
620
+ const pluginTest = "import { blotchPlugin } from './tailwind-plugin';";
621
+ fixture = createFixture({
622
+ config: DEFAULT_CONFIG,
623
+ lockfile: { version: 1, components: {}, utils: {}, theme: {} },
624
+ themeFiles: { 'tailwind-plugin.test.ts': pluginTest },
625
+ });
626
+ vi.spyOn(process, 'cwd').mockReturnValue(fixture.dir);
627
+ const { updateCommand } = await import('./update');
628
+ await updateCommand({ force: true });
629
+ expect(existsSync(join(fixture.dir, DEFAULT_CONFIG.aliases.theme, 'tailwind-plugin.test.ts'))).toBe(false);
630
+ writeFileSync(join(fixture.dir, 'package.json'), JSON.stringify({
631
+ devDependencies: { '@blotch/angular-lynx-testing-library': '^22.2.3' },
632
+ }));
633
+ await updateCommand({ force: true });
634
+ expect(readTheme(fixture, 'tailwind-plugin.test.ts')).toBe(pluginTest);
635
+ expect(readLockfile(fixture.dir).theme['tailwind-plugin.test.ts']).toEqual({
636
+ hash: hashContent(pluginTest),
637
+ });
638
+ });
609
639
  it('never prompts even when conflicts, user-modifications and new files coexist', async () => {
610
640
  fixture = createFixture({
611
641
  config: DEFAULT_CONFIG,
@@ -635,8 +665,6 @@ describe('updateCommand --force', () => {
635
665
  seedTheme(fixture, 'default.css', 'theme-local'); // theme conflict
636
666
  vi.spyOn(process, 'cwd').mockReturnValue(fixture.dir);
637
667
  const p = await import('@clack/prompts');
638
- p.select.mockClear();
639
- p.confirm.mockClear();
640
668
  const { updateCommand } = await import('./update');
641
669
  await updateCommand({ force: true });
642
670
  expect(p.select).not.toHaveBeenCalled();
@@ -781,8 +809,6 @@ describe('updateCommand — config / no-op paths', () => {
781
809
  });
782
810
  vi.spyOn(process, 'cwd').mockReturnValue(fixture.dir);
783
811
  const p = await import('@clack/prompts');
784
- p.confirm.mockClear();
785
- p.select.mockClear();
786
812
  const { updateCommand } = await import('./update');
787
813
  await updateCommand({});
788
814
  // Nothing prompts — a lone user-modified file with no force/selective is
@@ -874,7 +900,6 @@ describe('updateCommand — config / no-op paths', () => {
874
900
  vi.spyOn(process, 'cwd').mockReturnValue(fixture.dir);
875
901
  const p = await import('@clack/prompts');
876
902
  p.confirm.mockResolvedValueOnce(true);
877
- p.select.mockClear();
878
903
  const { updateCommand } = await import('./update');
879
904
  await updateCommand({});
880
905
  expect(readInstalledFile(fixture.dir, 'card', 'card.ts')).toBe(cardUpstream);
@@ -888,15 +913,15 @@ describe('updateCommand — config / no-op paths', () => {
888
913
  // The already up-to-date theme file's hash is still recorded.
889
914
  expect(lockfile.theme['default.css'].hash).toBe(hashContent(themeContent));
890
915
  });
891
- it('advances the lockfile for an up-to-date component and theme file with no prior lockfile entries', async () => {
892
- // Neither the component nor the theme file has ANY lockfile record at
893
- // all (not just a missing hash) — buildLockfile() must create fresh
894
- // entries for both from scratch on the "nothing to do" fast path.
916
+ it('advances the lockfile for an up-to-date component with no file hashes and a theme file with no entry', async () => {
917
+ // The component is tracked without file hashes, and the theme file has
918
+ // no lockfile record at all — buildLockfile() must create fresh entries
919
+ // for both on the "nothing to do" fast path.
895
920
  const cardContent = 'export const Card = {};';
896
921
  const themeContent = ':root { --primary: rgba(1, 1, 1, 1); }';
897
922
  fixture = createFixture({
898
923
  config: DEFAULT_CONFIG,
899
- lockfile: { version: 1, components: {}, utils: {}, theme: {} },
924
+ lockfile: { version: 1, components: { card: {} }, utils: {}, theme: {} },
900
925
  components: { card: { 'card.ts': cardContent } },
901
926
  uiSource: { card: { 'card.ts': cardContent } },
902
927
  themeFiles: { 'default.css': themeContent },
@@ -1230,4 +1255,533 @@ describe('updateCommand — selective review of shared theme files', () => {
1230
1255
  expect(lockfile.theme['default.css'].hash).toBe(hashContent(upstream));
1231
1256
  });
1232
1257
  });
1258
+ describe('updateCommand dependencies', () => {
1259
+ it("adds a dependency a component's new version needs, and its packages", async () => {
1260
+ // line-chart used to stand alone; upstream it now builds on chart, which
1261
+ // the project doesn't have yet.
1262
+ const lineChart = "import { UiChart } from '../chart/chart';";
1263
+ fixture = createFixture({
1264
+ config: DEFAULT_CONFIG,
1265
+ lockfile: {
1266
+ version: 1,
1267
+ components: {
1268
+ 'line-chart': { 'line-chart.ts': { hash: hashContent('old') } },
1269
+ },
1270
+ utils: {},
1271
+ theme: {},
1272
+ },
1273
+ components: { 'line-chart': { 'line-chart.ts': 'old' } },
1274
+ uiSource: {
1275
+ 'line-chart': { 'line-chart.ts': lineChart },
1276
+ chart: { 'chart.ts': 'export const UiChart = {};' },
1277
+ },
1278
+ });
1279
+ vi.spyOn(process, 'cwd').mockReturnValue(fixture.dir);
1280
+ const { updateCommand } = await import('./update');
1281
+ await updateCommand({ force: true });
1282
+ expect(readInstalledFile(fixture.dir, 'chart', 'chart.ts')).toBe('export const UiChart = {};');
1283
+ expect(readInstalledFile(fixture.dir, 'line-chart', 'line-chart.ts')).toBe(lineChart);
1284
+ expect(installComponentPackages).toHaveBeenCalledWith(fixture.dir, [
1285
+ 'chart',
1286
+ 'line-chart',
1287
+ ]);
1288
+ });
1289
+ it('leaves out a new dependency the user skips in selective mode', async () => {
1290
+ const lineChart = "import { UiChart } from '../chart/chart';";
1291
+ fixture = createFixture({
1292
+ config: DEFAULT_CONFIG,
1293
+ lockfile: {
1294
+ version: 1,
1295
+ components: {
1296
+ 'line-chart': { 'line-chart.ts': { hash: hashContent(lineChart) } },
1297
+ },
1298
+ utils: {},
1299
+ theme: {},
1300
+ },
1301
+ components: { 'line-chart': { 'line-chart.ts': lineChart } },
1302
+ uiSource: {
1303
+ 'line-chart': { 'line-chart.ts': lineChart },
1304
+ chart: { 'chart.ts': 'export const UiChart = {};' },
1305
+ },
1306
+ });
1307
+ vi.spyOn(process, 'cwd').mockReturnValue(fixture.dir);
1308
+ const p = await import('@clack/prompts');
1309
+ p.select.mockResolvedValueOnce('keep');
1310
+ const { updateCommand } = await import('./update');
1311
+ await updateCommand({ selective: true });
1312
+ expect(existsSync(join(fixture.dir, DEFAULT_CONFIG.aliases.components, 'chart/chart.ts'))).toBe(false);
1313
+ expect(readLockfile(fixture.dir).components['chart']).toEqual({});
1314
+ });
1315
+ });
1316
+ describe('updateCommand — ejected components', () => {
1317
+ /**
1318
+ * `dolan eject card` deleted card's lockfile entry and kept its files,
1319
+ * which the app has since changed.
1320
+ */
1321
+ const ejectedCard = (lockfileComponents = {}) => createFixture({
1322
+ config: DEFAULT_CONFIG,
1323
+ lockfile: {
1324
+ version: 1,
1325
+ components: lockfileComponents,
1326
+ utils: {},
1327
+ theme: {},
1328
+ },
1329
+ components: {
1330
+ card: { 'card.ts': 'export const Card = { mine: true };' },
1331
+ },
1332
+ uiSource: { card: { 'card.ts': 'export const Card = { v: 2 };' } },
1333
+ });
1334
+ it("doesn't overwrite or track them again, even with --force", async () => {
1335
+ fixture = ejectedCard();
1336
+ vi.spyOn(process, 'cwd').mockReturnValue(fixture.dir);
1337
+ const { updateCommand } = await import('./update');
1338
+ await updateCommand({ force: true });
1339
+ expect(readInstalledFile(fixture.dir, 'card', 'card.ts')).toBe('export const Card = { mine: true };');
1340
+ expect(readLockfile(fixture.dir).components).not.toHaveProperty('card');
1341
+ });
1342
+ it("doesn't report them as a conflict", async () => {
1343
+ fixture = ejectedCard();
1344
+ vi.spyOn(process, 'cwd').mockReturnValue(fixture.dir);
1345
+ const p = await import('@clack/prompts');
1346
+ const { updateCommand } = await import('./update');
1347
+ await updateCommand({});
1348
+ const messages = p.log.message.mock.calls
1349
+ .map((c) => c[0])
1350
+ .join('\n');
1351
+ expect(messages).not.toContain('card');
1352
+ expect(p.select).not.toHaveBeenCalled();
1353
+ });
1354
+ it('leaves an ejected dependency of a tracked component alone', async () => {
1355
+ // select is tracked and depends on bottom-sheet, which was ejected.
1356
+ const select = 'export const Select = {};';
1357
+ fixture = createFixture({
1358
+ config: DEFAULT_CONFIG,
1359
+ lockfile: {
1360
+ version: 1,
1361
+ components: { select: { 'select.ts': { hash: hashContent(select) } } },
1362
+ utils: {},
1363
+ theme: {},
1364
+ },
1365
+ components: {
1366
+ select: { 'select.ts': select },
1367
+ 'bottom-sheet': {
1368
+ 'bottom-sheet.ts': 'export const BottomSheet = { mine: true };',
1369
+ },
1370
+ },
1371
+ uiSource: {
1372
+ select: { 'select.ts': select },
1373
+ 'bottom-sheet': { 'bottom-sheet.ts': 'export const BottomSheet = {};' },
1374
+ },
1375
+ });
1376
+ vi.spyOn(process, 'cwd').mockReturnValue(fixture.dir);
1377
+ const { updateCommand } = await import('./update');
1378
+ await updateCommand({ force: true });
1379
+ expect(readInstalledFile(fixture.dir, 'bottom-sheet', 'bottom-sheet.ts')).toBe('export const BottomSheet = { mine: true };');
1380
+ expect(readLockfile(fixture.dir).components).not.toHaveProperty('bottom-sheet');
1381
+ });
1382
+ });
1383
+ // CI, agents and scripts: a question there would wait for input that never
1384
+ // comes.
1385
+ describe('updateCommand without a terminal', () => {
1386
+ const base = 'export const Card = { v: 1 };';
1387
+ const userVersion = 'export const Card = { v: 1, local: true };';
1388
+ const upstream = 'export const Card = { v: 2 };';
1389
+ const themeBase = ':root { --primary: rgba(1, 1, 1, 1); }';
1390
+ const themeMine = ':root { --primary: rgba(9, 9, 9, 1); }';
1391
+ const themeUpstream = ':root { --primary: rgba(2, 2, 2, 1); }';
1392
+ /**
1393
+ * `card` and `default.css` conflict (both sides changed), and `skeleton` is
1394
+ * a plain auto-update.
1395
+ */
1396
+ const setup = () => {
1397
+ fixture = createFixture({
1398
+ config: DEFAULT_CONFIG,
1399
+ lockfile: {
1400
+ version: 1,
1401
+ components: {
1402
+ card: { 'card.ts': { hash: hashContent(base) } },
1403
+ skeleton: { 'skeleton.ts': { hash: hashContent(base) } },
1404
+ },
1405
+ utils: {},
1406
+ theme: { 'default.css': { hash: hashContent(themeBase) } },
1407
+ },
1408
+ components: {
1409
+ card: { 'card.ts': userVersion },
1410
+ skeleton: { 'skeleton.ts': base },
1411
+ },
1412
+ uiSource: {
1413
+ card: { 'card.ts': upstream },
1414
+ skeleton: { 'skeleton.ts': upstream },
1415
+ },
1416
+ themeFiles: { 'default.css': themeUpstream },
1417
+ });
1418
+ writeFileSync(join(fixture.dir, DEFAULT_CONFIG.aliases.theme, 'default.css'), themeMine);
1419
+ vi.spyOn(process, 'cwd').mockReturnValue(fixture.dir);
1420
+ };
1421
+ const readTheme = () => readFileSync(join(fixture.dir, DEFAULT_CONFIG.aliases.theme, 'default.css'), 'utf-8');
1422
+ beforeEach(() => {
1423
+ interactive = false;
1424
+ });
1425
+ it('applies auto-updates without asking', async () => {
1426
+ fixture = createFixture({
1427
+ config: DEFAULT_CONFIG,
1428
+ lockfile: {
1429
+ version: 1,
1430
+ components: { card: { 'card.ts': { hash: hashContent(base) } } },
1431
+ utils: {},
1432
+ theme: {},
1433
+ },
1434
+ components: { card: { 'card.ts': base } },
1435
+ uiSource: { card: { 'card.ts': upstream } },
1436
+ });
1437
+ vi.spyOn(process, 'cwd').mockReturnValue(fixture.dir);
1438
+ const p = await import('@clack/prompts');
1439
+ const { updateCommand } = await import('./update');
1440
+ await updateCommand({});
1441
+ expect(p.confirm).not.toHaveBeenCalled();
1442
+ expect(readInstalledFile(fixture.dir, 'card', 'card.ts')).toBe(upstream);
1443
+ expect(p.outro).toHaveBeenCalledWith('Update complete.');
1444
+ });
1445
+ it('leaves conflicts as they are for a terminal to ask about, and exits 1', async () => {
1446
+ setup();
1447
+ const p = await import('@clack/prompts');
1448
+ const { updateCommand } = await import('./update');
1449
+ await expect(updateCommand({})).rejects.toThrow('process.exit(1)');
1450
+ expect(p.select).not.toHaveBeenCalled();
1451
+ // The count is bold, and CI turns colors on, so match the plain text.
1452
+ const [warning] = p.log.warn.mock.calls[0];
1453
+ // oxlint-disable-next-line no-control-regex -- matching ANSI color codes
1454
+ expect(warning.replace(/\u001b\[\d+m/g, '')).toMatch(/2 conflict\(s\) left.*dolan update --force/);
1455
+ // The auto-update still applies.
1456
+ expect(readInstalledFile(fixture.dir, 'skeleton', 'skeleton.ts')).toBe(upstream);
1457
+ // The conflicts keep both their files and their lockfile base. Keeping
1458
+ // the user's version would advance the base and lose the upstream change.
1459
+ expect(readInstalledFile(fixture.dir, 'card', 'card.ts')).toBe(userVersion);
1460
+ expect(readTheme()).toBe(themeMine);
1461
+ const lockfile = readLockfile(fixture.dir);
1462
+ expect(lockfile.components.card['card.ts'].hash).toBe(hashContent(base));
1463
+ expect(lockfile.theme['default.css'].hash).toBe(hashContent(themeBase));
1464
+ // So an update in a terminal still asks about both.
1465
+ interactive = true;
1466
+ vi.clearAllMocks();
1467
+ // A reset drops answers an earlier test queued and never used.
1468
+ p.select
1469
+ .mockReset()
1470
+ .mockResolvedValue('upstream');
1471
+ await updateCommand({});
1472
+ expect(p.select).toHaveBeenCalledTimes(2);
1473
+ expect(readInstalledFile(fixture.dir, 'card', 'card.ts')).toBe(upstream);
1474
+ expect(readTheme()).toBe(themeUpstream);
1475
+ });
1476
+ it("takes upstream's version of conflicts with --force, which wins over --selective", async () => {
1477
+ setup();
1478
+ const p = await import('@clack/prompts');
1479
+ const { updateCommand } = await import('./update');
1480
+ await updateCommand({ force: true, selective: true });
1481
+ expect(p.log.error).not.toHaveBeenCalled();
1482
+ expect(readInstalledFile(fixture.dir, 'card', 'card.ts')).toBe(upstream);
1483
+ expect(readTheme()).toBe(themeUpstream);
1484
+ expect(p.outro).toHaveBeenCalledWith('Update complete.');
1485
+ });
1486
+ it('fails with --selective, which asks about each file', async () => {
1487
+ setup();
1488
+ const p = await import('@clack/prompts');
1489
+ const { updateCommand } = await import('./update');
1490
+ await expect(updateCommand({ selective: true })).rejects.toThrow('process.exit(1)');
1491
+ expect(p.log.error).toHaveBeenCalledWith(expect.stringContaining('--selective'));
1492
+ expect(readInstalledFile(fixture.dir, 'skeleton', 'skeleton.ts')).toBe(base);
1493
+ });
1494
+ });
1495
+ // 22.2.2's `init` copied the theme but recorded no hashes for it, so the first
1496
+ // `update` after a release that changed the theme called every theme file a
1497
+ // conflict, though nobody had touched them.
1498
+ describe('updateCommand — theme files an older init copied', () => {
1499
+ const themePath = (file) => join(fixture.dir, DEFAULT_CONFIG.aliases.theme, file);
1500
+ const upstream = {
1501
+ 'default.css': ':root { --primary: rgba(2, 2, 2, 1); }',
1502
+ 'dark.css': 'page.dark { --primary: rgba(3, 3, 3, 1); }',
1503
+ };
1504
+ /**
1505
+ * Writes `files` where init put the theme, with `"theme": {}` locked.
1506
+ */
1507
+ const setup = (files) => {
1508
+ fixture = createFixture({
1509
+ config: DEFAULT_CONFIG,
1510
+ lockfile: { version: 1, components: {}, theme: {} },
1511
+ themeFiles: upstream,
1512
+ });
1513
+ for (const [file, content] of Object.entries(files)) {
1514
+ writeFileSync(themePath(file), content);
1515
+ }
1516
+ vi.spyOn(process, 'cwd').mockReturnValue(fixture.dir);
1517
+ };
1518
+ it('updates an untouched copy as an auto-update, not a conflict', async () => {
1519
+ setup({
1520
+ 'default.css': shippedTheme('default.css'),
1521
+ 'dark.css': shippedTheme('dark.css'),
1522
+ });
1523
+ const p = await import('@clack/prompts');
1524
+ const { updateCommand } = await import('./update');
1525
+ await updateCommand({});
1526
+ expect(p.select).not.toHaveBeenCalled();
1527
+ expect(p.confirm).toHaveBeenCalledWith(expect.objectContaining({ message: 'Apply 2 auto-update(s)?' }));
1528
+ expect(readFileSync(themePath('default.css'), 'utf-8')).toBe(upstream['default.css']);
1529
+ expect(readFileSync(themePath('dark.css'), 'utf-8')).toBe(upstream['dark.css']);
1530
+ expect(readLockfile(fixture.dir).theme).toEqual({
1531
+ 'default.css': { hash: hashContent(upstream['default.css']) },
1532
+ 'dark.css': { hash: hashContent(upstream['dark.css']) },
1533
+ });
1534
+ });
1535
+ it('applies them without a terminal, rather than leaving conflicts and exiting 1', async () => {
1536
+ interactive = false;
1537
+ setup({ 'default.css': shippedTheme('default.css') });
1538
+ const p = await import('@clack/prompts');
1539
+ const { updateCommand } = await import('./update');
1540
+ await updateCommand({});
1541
+ expect(readFileSync(themePath('default.css'), 'utf-8')).toBe(upstream['default.css']);
1542
+ expect(p.outro).toHaveBeenCalledWith('Update complete.');
1543
+ });
1544
+ it('still asks about a copy that was edited, since its base is unknown', async () => {
1545
+ const edited = `${shippedTheme('default.css')}\npage { --mine: red; }\n`;
1546
+ setup({ 'default.css': edited });
1547
+ const p = await import('@clack/prompts');
1548
+ p.select.mockResolvedValueOnce('keep');
1549
+ const { updateCommand } = await import('./update');
1550
+ await updateCommand({});
1551
+ expect(p.select).toHaveBeenCalledWith(expect.objectContaining({
1552
+ message: expect.stringContaining('how to resolve?'),
1553
+ }));
1554
+ expect(readFileSync(themePath('default.css'), 'utf-8')).toBe(edited);
1555
+ });
1556
+ });
1557
+ // 22.2.3 stopped copying `*-render.test.ts`, but `update` only looked at the
1558
+ // files upstream ships now and kept every old lockfile entry, so the stale
1559
+ // spec stayed, importing a helper dolan doesn't copy, and broke `ng test`.
1560
+ describe('updateCommand — files upstream no longer ships', () => {
1561
+ const card = 'export const Card = {};';
1562
+ const stale = "import { setInputSignal } from '../../../test-utils/x';";
1563
+ const edited = `${stale}\n// mine\n`;
1564
+ const STALE = 'card-render.test.ts';
1565
+ const stalePath = () => join(fixture.dir, DEFAULT_CONFIG.aliases.components, 'card', STALE);
1566
+ /**
1567
+ * card is up to date, and it has STALE (as add copied it, unless `onDisk`
1568
+ * says otherwise), which upstream no longer ships. `upstreamCard` changes
1569
+ * card.ts upstream too.
1570
+ */
1571
+ const setup = ({ onDisk = stale, upstreamCard = card, } = {}) => {
1572
+ fixture = createFixture({
1573
+ config: DEFAULT_CONFIG,
1574
+ lockfile: {
1575
+ version: 1,
1576
+ components: {
1577
+ card: {
1578
+ 'card.ts': { hash: hashContent(card) },
1579
+ [STALE]: { hash: hashContent(stale) },
1580
+ },
1581
+ },
1582
+ theme: {},
1583
+ },
1584
+ components: {
1585
+ card: {
1586
+ 'card.ts': card,
1587
+ ...(onDisk === null ? {} : { [STALE]: onDisk }),
1588
+ },
1589
+ },
1590
+ uiSource: { card: { 'card.ts': upstreamCard } },
1591
+ });
1592
+ vi.spyOn(process, 'cwd').mockReturnValue(fixture.dir);
1593
+ };
1594
+ const lockedCard = () => readLockfile(fixture.dir).components.card;
1595
+ /**
1596
+ * The log messages, colors stripped.
1597
+ */
1598
+ const messages = async () => {
1599
+ const p = await import('@clack/prompts');
1600
+ const text = p.log.message.mock.calls
1601
+ .map(([m]) => m)
1602
+ .join('\n');
1603
+ // oxlint-disable-next-line no-control-regex -- matching ANSI color codes
1604
+ return text.replace(/\u001b\[\d+m/g, '');
1605
+ };
1606
+ /**
1607
+ * The options of the select prompt's first call.
1608
+ */
1609
+ const selectOptions = async () => {
1610
+ const p = await import('@clack/prompts');
1611
+ const [{ options }] = p.select.mock
1612
+ .calls[0];
1613
+ return options;
1614
+ };
1615
+ beforeEach(async () => {
1616
+ // A reset drops answers an earlier test queued and never used.
1617
+ const p = await import('@clack/prompts');
1618
+ p.select
1619
+ .mockReset()
1620
+ .mockResolvedValue('upstream');
1621
+ });
1622
+ it('deletes one nobody edited, with its lockfile entry, after the usual question', async () => {
1623
+ setup();
1624
+ const p = await import('@clack/prompts');
1625
+ const { updateCommand } = await import('./update');
1626
+ await updateCommand({});
1627
+ expect(p.confirm).toHaveBeenCalledWith(expect.objectContaining({ message: 'Apply 1 auto-update(s)?' }));
1628
+ expect(p.select).not.toHaveBeenCalled();
1629
+ expect(await messages()).toContain(`card/${STALE} — upstream no longer ships it → delete`);
1630
+ expect(existsSync(stalePath())).toBe(false);
1631
+ expect(lockedCard()).toEqual({ 'card.ts': { hash: hashContent(card) } });
1632
+ expect(p.log.success).toHaveBeenCalledWith(expect.stringContaining('deleted: upstream no longer ships them'));
1633
+ });
1634
+ it('deletes one nobody edited without a terminal too', async () => {
1635
+ interactive = false;
1636
+ setup();
1637
+ const p = await import('@clack/prompts');
1638
+ const { updateCommand } = await import('./update');
1639
+ await updateCommand({});
1640
+ expect(p.confirm).not.toHaveBeenCalled();
1641
+ expect(existsSync(stalePath())).toBe(false);
1642
+ expect(lockedCard()).not.toHaveProperty(STALE);
1643
+ expect(p.outro).toHaveBeenCalledWith('Update complete.');
1644
+ });
1645
+ it('asks before deleting one you edited, and deletes it when told to', async () => {
1646
+ setup({ onDisk: edited });
1647
+ const p = await import('@clack/prompts');
1648
+ const { updateCommand } = await import('./update');
1649
+ await updateCommand({});
1650
+ expect(await messages()).toContain(`card/${STALE} — upstream no longer ships it, and you modified it`);
1651
+ expect(p.select).toHaveBeenCalledTimes(1);
1652
+ expect(p.select).toHaveBeenCalledWith(expect.objectContaining({
1653
+ message: expect.stringContaining('you modified it, and upstream no longer ships it'),
1654
+ }));
1655
+ // A deletion is all or nothing, so there are no hunks to review.
1656
+ expect(await selectOptions()).toEqual([
1657
+ expect.objectContaining({ value: 'diff' }),
1658
+ {
1659
+ value: 'keep',
1660
+ label: 'Keep my version',
1661
+ hint: 'keep it as my own file',
1662
+ },
1663
+ { value: 'upstream', label: 'Delete it', hint: 'discard my changes' },
1664
+ ]);
1665
+ expect(existsSync(stalePath())).toBe(false);
1666
+ expect(lockedCard()).not.toHaveProperty(STALE);
1667
+ });
1668
+ it('shows what deleting it would take away', async () => {
1669
+ setup({ onDisk: edited });
1670
+ const p = await import('@clack/prompts');
1671
+ p.select
1672
+ .mockResolvedValueOnce('diff')
1673
+ .mockResolvedValueOnce('upstream');
1674
+ const { updateCommand } = await import('./update');
1675
+ await updateCommand({});
1676
+ expect(await messages()).toContain(`- // mine`);
1677
+ expect(existsSync(stalePath())).toBe(false);
1678
+ });
1679
+ it('keeps one you edited when told to, as your own file from then on', async () => {
1680
+ setup({ onDisk: edited });
1681
+ const p = await import('@clack/prompts');
1682
+ p.select.mockResolvedValueOnce('keep');
1683
+ const { updateCommand } = await import('./update');
1684
+ await updateCommand({});
1685
+ expect(readFileSync(stalePath(), 'utf-8')).toBe(edited);
1686
+ // Upstream has nothing left to merge into it, so dolan stops tracking it,
1687
+ // and the next update doesn't ask again.
1688
+ expect(lockedCard()).toEqual({ 'card.ts': { hash: hashContent(card) } });
1689
+ expect(p.log.info).toHaveBeenCalledWith(expect.stringContaining('file(s) kept as-is'));
1690
+ vi.clearAllMocks();
1691
+ await updateCommand({});
1692
+ expect(p.select).not.toHaveBeenCalled();
1693
+ expect(readFileSync(stalePath(), 'utf-8')).toBe(edited);
1694
+ });
1695
+ it('leaves one you edited for a terminal to ask about without one, and exits 1', async () => {
1696
+ interactive = false;
1697
+ setup({ onDisk: edited });
1698
+ const p = await import('@clack/prompts');
1699
+ const { updateCommand } = await import('./update');
1700
+ await expect(updateCommand({})).rejects.toThrow('process.exit(1)');
1701
+ expect(p.select).not.toHaveBeenCalled();
1702
+ expect(p.log.warn).toHaveBeenCalledWith(expect.stringContaining('conflict(s) left as they are'));
1703
+ expect(readFileSync(stalePath(), 'utf-8')).toBe(edited);
1704
+ expect(lockedCard()[STALE]).toEqual({ hash: hashContent(stale) });
1705
+ });
1706
+ it('deletes one you edited with --force', async () => {
1707
+ interactive = false;
1708
+ setup({ onDisk: edited });
1709
+ const { updateCommand } = await import('./update');
1710
+ await updateCommand({ force: true });
1711
+ expect(existsSync(stalePath())).toBe(false);
1712
+ expect(lockedCard()).not.toHaveProperty(STALE);
1713
+ });
1714
+ describe('with --selective', () => {
1715
+ it('asks about the deletion, and "Skip for now" keeps the file and its entry', async () => {
1716
+ setup();
1717
+ const p = await import('@clack/prompts');
1718
+ p.select.mockResolvedValueOnce('keep');
1719
+ const { updateCommand } = await import('./update');
1720
+ await updateCommand({ selective: true });
1721
+ expect(p.select).toHaveBeenCalledWith(expect.objectContaining({
1722
+ message: expect.stringMatching(/upstream no longer ships it$/),
1723
+ }));
1724
+ expect(await selectOptions()).toEqual([
1725
+ expect.objectContaining({ value: 'diff' }),
1726
+ expect.objectContaining({ value: 'keep', label: 'Skip for now' }),
1727
+ { value: 'upstream', label: 'Delete it', hint: 'remove the file' },
1728
+ ]);
1729
+ expect(readFileSync(stalePath(), 'utf-8')).toBe(stale);
1730
+ expect(lockedCard()[STALE]).toEqual({ hash: hashContent(stale) });
1731
+ });
1732
+ it('deletes it when accepted', async () => {
1733
+ setup();
1734
+ const { updateCommand } = await import('./update');
1735
+ await updateCommand({ selective: true });
1736
+ expect(existsSync(stalePath())).toBe(false);
1737
+ expect(lockedCard()).not.toHaveProperty(STALE);
1738
+ });
1739
+ });
1740
+ describe('already gone from disk', () => {
1741
+ it('drops its entry when nothing else changed', async () => {
1742
+ setup({ onDisk: null });
1743
+ const p = await import('@clack/prompts');
1744
+ const { updateCommand } = await import('./update');
1745
+ await updateCommand({});
1746
+ expect(p.confirm).not.toHaveBeenCalled();
1747
+ expect(await messages()).not.toContain(STALE);
1748
+ expect(p.outro).toHaveBeenCalledWith('Everything is up to date.');
1749
+ expect(lockedCard()).toEqual({ 'card.ts': { hash: hashContent(card) } });
1750
+ });
1751
+ it('drops its entry alongside an update', async () => {
1752
+ const upstreamCard = 'export const Card = { v: 2 };';
1753
+ setup({ onDisk: null, upstreamCard });
1754
+ const { updateCommand } = await import('./update');
1755
+ await updateCommand({});
1756
+ expect(lockedCard()).toEqual({
1757
+ 'card.ts': { hash: hashContent(upstreamCard) },
1758
+ });
1759
+ });
1760
+ });
1761
+ it("leaves the tests a project without the testing library doesn't get", async () => {
1762
+ // Copied before dolan checked for the library. Upstream still ships them,
1763
+ // so they aren't deleted: `doctor` says what to do about them instead.
1764
+ const test = "import '@blotch/angular-lynx-testing-library';";
1765
+ fixture = createFixture({
1766
+ config: DEFAULT_CONFIG,
1767
+ lockfile: {
1768
+ version: 1,
1769
+ components: {
1770
+ card: {
1771
+ 'card.ts': { hash: hashContent(card) },
1772
+ 'card.test.ts': { hash: hashContent(test) },
1773
+ },
1774
+ },
1775
+ theme: {},
1776
+ },
1777
+ components: { card: { 'card.ts': card, 'card.test.ts': test } },
1778
+ uiSource: { card: { 'card.ts': card, 'card.test.ts': test } },
1779
+ });
1780
+ vi.spyOn(process, 'cwd').mockReturnValue(fixture.dir);
1781
+ const { updateCommand } = await import('./update');
1782
+ await updateCommand({});
1783
+ expect(readInstalledFile(fixture.dir, 'card', 'card.test.ts')).toBe(test);
1784
+ expect(lockedCard()['card.test.ts']).toEqual({ hash: hashContent(test) });
1785
+ });
1786
+ });
1233
1787
  //# sourceMappingURL=update.test.js.map