@blotch/dolan 22.2.3 → 22.2.5

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 (227) 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 +38 -3
  4. package/dist/commands/add.test.js.map +1 -1
  5. package/dist/commands/doctor.js +258 -21
  6. package/dist/commands/doctor.js.map +1 -1
  7. package/dist/commands/doctor.test.js +425 -11
  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 +105 -2
  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 +7 -0
  17. package/dist/commands/update.js +194 -50
  18. package/dist/commands/update.js.map +1 -1
  19. package/dist/commands/update.test.js +570 -3
  20. package/dist/commands/update.test.js.map +1 -1
  21. package/dist/lockfile.d.ts +17 -0
  22. package/dist/lockfile.js +56 -1
  23. package/dist/lockfile.js.map +1 -1
  24. package/dist/lockfile.test.d.ts +1 -0
  25. package/dist/lockfile.test.js +65 -0
  26. package/dist/lockfile.test.js.map +1 -0
  27. package/dist/registry.d.ts +6 -0
  28. package/dist/registry.js +119 -11
  29. package/dist/registry.js.map +1 -1
  30. package/dist/registry.test.js +61 -6
  31. package/dist/registry.test.js.map +1 -1
  32. package/dist/test-utils.d.ts +27 -0
  33. package/dist/test-utils.js +45 -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/boolean-inputs.test.ts +76 -0
  51. package/dist/ui/components/accordion/accordion.test.ts +158 -144
  52. package/dist/ui/components/accordion/accordion.ts +19 -14
  53. package/dist/ui/components/action-sheet/action-sheet.test.ts +637 -183
  54. package/dist/ui/components/action-sheet/action-sheet.ts +339 -55
  55. package/dist/ui/components/action-sheet/index.ts +1 -0
  56. package/dist/ui/components/alert/alert.test.ts +67 -19
  57. package/dist/ui/components/alert/alert.ts +14 -16
  58. package/dist/ui/components/alert-dialog/alert-dialog.test.ts +740 -129
  59. package/dist/ui/components/alert-dialog/alert-dialog.ts +672 -58
  60. package/dist/ui/components/alert-dialog/index.ts +5 -0
  61. package/dist/ui/components/area-chart/area-chart.test.ts +81 -1
  62. package/dist/ui/components/area-chart/area-chart.ts +3 -2
  63. package/dist/ui/components/avatar/avatar.test.ts +89 -45
  64. package/dist/ui/components/badge/badge.test.ts +57 -23
  65. package/dist/ui/components/badge/badge.ts +14 -15
  66. package/dist/ui/components/bar-chart/bar-chart.test.ts +82 -1
  67. package/dist/ui/components/bottom-sheet/bottom-sheet.test.ts +1314 -273
  68. package/dist/ui/components/bottom-sheet/bottom-sheet.ts +1046 -119
  69. package/dist/ui/components/bottom-sheet/index.ts +7 -1
  70. package/dist/ui/components/button/button.test.ts +646 -2
  71. package/dist/ui/components/button/button.ts +443 -64
  72. package/dist/ui/components/button/index.ts +2 -0
  73. package/dist/ui/components/button-group/button-group.test.ts +528 -2
  74. package/dist/ui/components/button-group/button-group.ts +428 -70
  75. package/dist/ui/components/button-group/index.ts +3 -0
  76. package/dist/ui/components/candlestick-chart/candlestick-chart.test.ts +82 -1
  77. package/dist/ui/components/card/card.test.ts +97 -61
  78. package/dist/ui/components/card/card.ts +2 -1
  79. package/dist/ui/components/chart/chart-presets.test.ts +197 -1
  80. package/dist/ui/components/chart/chart-presets.ts +14 -7
  81. package/dist/ui/components/chart/chart-theme.ts +12 -0
  82. package/dist/ui/components/chart/chart.test.ts +1031 -0
  83. package/dist/ui/components/chart/chart.ts +66 -46
  84. package/dist/ui/components/checkbox/checkbox.test.ts +124 -42
  85. package/dist/ui/components/checkbox/checkbox.ts +25 -24
  86. package/dist/ui/components/collapsible/collapsible.test.ts +158 -81
  87. package/dist/ui/components/collapsible/collapsible.ts +21 -15
  88. package/dist/ui/components/context-menu/context-menu.test.ts +224 -0
  89. package/dist/ui/components/context-menu/context-menu.ts +110 -0
  90. package/dist/ui/components/context-menu/index.ts +1 -0
  91. package/dist/ui/components/dialog/dialog.test.ts +210 -152
  92. package/dist/ui/components/dialog/dialog.ts +35 -29
  93. package/dist/ui/components/dock/dock-scroll.test.ts +130 -0
  94. package/dist/ui/components/dock/dock-scroll.ts +142 -0
  95. package/dist/ui/components/dock/dock-search.test.ts +594 -0
  96. package/dist/ui/components/dock/dock.test.ts +752 -0
  97. package/dist/ui/components/dock/dock.ts +1438 -0
  98. package/dist/ui/components/dock/index.ts +2 -0
  99. package/dist/ui/components/dynamic-color/dynamic-color.test.ts +144 -0
  100. package/dist/ui/components/dynamic-color/dynamic-color.ts +146 -0
  101. package/dist/ui/components/dynamic-color/index.ts +5 -0
  102. package/dist/ui/components/empty-state/empty-state.test.ts +28 -39
  103. package/dist/ui/components/glass/concentric-radius.test.ts +13 -0
  104. package/dist/ui/components/glass/concentric-radius.ts +17 -0
  105. package/dist/ui/components/glass/glass-design.test.ts +78 -0
  106. package/dist/ui/components/glass/glass-design.ts +119 -0
  107. package/dist/ui/components/glass/glass.test.ts +376 -0
  108. package/dist/ui/components/glass/glass.ts +430 -0
  109. package/dist/ui/components/glass/index.ts +19 -0
  110. package/dist/ui/components/icon/icon-theme.test.ts +64 -0
  111. package/dist/ui/components/icon/icon-theme.ts +70 -16
  112. package/dist/ui/components/icon/icon.test.ts +52 -41
  113. package/dist/ui/components/input/input.test.ts +178 -101
  114. package/dist/ui/components/input/input.ts +17 -13
  115. package/dist/ui/components/label/label.test.ts +11 -24
  116. package/dist/ui/components/label/label.ts +8 -2
  117. package/dist/ui/components/line-chart/line-chart.test.ts +76 -1
  118. package/dist/ui/components/line-chart/line-chart.ts +3 -2
  119. package/dist/ui/components/list/list.test.ts +80 -49
  120. package/dist/ui/components/list/list.ts +5 -4
  121. package/dist/ui/components/menu/index.ts +25 -0
  122. package/dist/ui/components/menu/menu-placement.test.ts +153 -0
  123. package/dist/ui/components/menu/menu-placement.ts +159 -0
  124. package/dist/ui/components/menu/menu-popup.test.ts +636 -0
  125. package/dist/ui/components/menu/menu-popup.ts +565 -0
  126. package/dist/ui/components/menu/menu.test.ts +523 -0
  127. package/dist/ui/components/menu/menu.ts +455 -0
  128. package/dist/ui/components/nav-drawer/nav-drawer.test.ts +425 -260
  129. package/dist/ui/components/nav-drawer/nav-drawer.ts +75 -58
  130. package/dist/ui/components/navigation-bar/index.ts +1 -0
  131. package/dist/ui/components/navigation-bar/navigation-bar.test.ts +425 -0
  132. package/dist/ui/components/navigation-bar/navigation-bar.ts +411 -0
  133. package/dist/ui/components/pagination/pagination.test.ts +136 -86
  134. package/dist/ui/components/popover/index.ts +9 -0
  135. package/dist/ui/components/popover/popover-panel.test.ts +735 -0
  136. package/dist/ui/components/popover/popover-panel.ts +603 -0
  137. package/dist/ui/components/popover/popover.test.ts +286 -0
  138. package/dist/ui/components/popover/popover.ts +207 -0
  139. package/dist/ui/components/progress/progress.test.ts +102 -46
  140. package/dist/ui/components/progress/progress.ts +54 -55
  141. package/dist/ui/components/radio-group/radio-group.test.ts +128 -83
  142. package/dist/ui/components/radio-group/radio-group.ts +26 -25
  143. package/dist/ui/components/scatter-chart/scatter-chart.test.ts +74 -1
  144. package/dist/ui/components/scroll-area/scroll-area.test.ts +72 -36
  145. package/dist/ui/components/scroll-area/scroll-area.ts +4 -3
  146. package/dist/ui/components/search-field/index.ts +15 -0
  147. package/dist/ui/components/search-field/keyboard.test.ts +147 -0
  148. package/dist/ui/components/search-field/keyboard.ts +123 -0
  149. package/dist/ui/components/search-field/search-field.test.ts +549 -0
  150. package/dist/ui/components/search-field/search-field.ts +628 -0
  151. package/dist/ui/components/segmented-control/index.ts +8 -0
  152. package/dist/ui/components/segmented-control/segmented-control.test.ts +861 -0
  153. package/dist/ui/components/segmented-control/segmented-control.ts +884 -0
  154. package/dist/ui/components/select/select.test.ts +138 -78
  155. package/dist/ui/components/select/select.ts +19 -12
  156. package/dist/ui/components/separator/separator.test.ts +18 -17
  157. package/dist/ui/components/sheet/sheet.test.ts +151 -11
  158. package/dist/ui/components/sheet/sheet.ts +68 -5
  159. package/dist/ui/components/skeleton/skeleton.test.ts +24 -7
  160. package/dist/ui/components/skeleton/skeleton.ts +10 -16
  161. package/dist/ui/components/slider/index.ts +1 -0
  162. package/dist/ui/components/slider/slider.test.ts +695 -0
  163. package/dist/ui/components/slider/slider.ts +901 -0
  164. package/dist/ui/components/spinner/spinner.test.ts +64 -46
  165. package/dist/ui/components/spinner/spinner.ts +16 -19
  166. package/dist/ui/components/switch/index.ts +1 -1
  167. package/dist/ui/components/switch/switch.test.ts +534 -48
  168. package/dist/ui/components/switch/switch.ts +613 -48
  169. package/dist/ui/components/tab-bar/index.ts +1 -0
  170. package/dist/ui/components/tab-bar/tab-bar.test.ts +280 -0
  171. package/dist/ui/components/tab-bar/tab-bar.ts +230 -0
  172. package/dist/ui/components/tabs/index.ts +7 -1
  173. package/dist/ui/components/tabs/tabs.test.ts +484 -150
  174. package/dist/ui/components/tabs/tabs.ts +344 -149
  175. package/dist/ui/components/textarea/textarea.test.ts +206 -100
  176. package/dist/ui/components/textarea/textarea.ts +16 -13
  177. package/dist/ui/components/toast/toast.test.ts +434 -350
  178. package/dist/ui/components/toast/toast.ts +42 -51
  179. package/dist/ui/components/toggle/index.ts +1 -0
  180. package/dist/ui/components/toggle/toggle.test.ts +559 -146
  181. package/dist/ui/components/toggle/toggle.ts +241 -26
  182. package/dist/ui/components/toolbar/bar-scroll.test.ts +310 -0
  183. package/dist/ui/components/toolbar/bar-scroll.ts +372 -0
  184. package/dist/ui/components/toolbar/index.ts +12 -0
  185. package/dist/ui/components/toolbar/toolbar.test.ts +438 -0
  186. package/dist/ui/components/toolbar/toolbar.ts +535 -0
  187. package/dist/ui/theme/chart-colors.test.ts +8 -3
  188. package/dist/ui/theme/dark.css +38 -2
  189. package/dist/ui/theme/default.css +55 -7
  190. package/dist/ui/theme/icon-colors.test.ts +8 -3
  191. package/dist/ui/theme/rgb-channels.test.ts +84 -0
  192. package/dist/ui/theme/tailwind-build.test.ts +114 -0
  193. package/dist/ui/theme/tailwind-css.test.ts +83 -0
  194. package/dist/ui/theme/tailwind-plugin.test.ts +42 -2
  195. package/dist/ui/theme/tailwind-plugin.ts +93 -29
  196. package/dist/ui/theme/tailwind.css +113 -0
  197. package/dist/ui/utils/animate.test.ts +13 -1
  198. package/dist/utils/analyze.d.ts +19 -0
  199. package/dist/utils/analyze.js +16 -0
  200. package/dist/utils/analyze.js.map +1 -1
  201. package/dist/utils/analyze.test.js +14 -1
  202. package/dist/utils/analyze.test.js.map +1 -1
  203. package/dist/utils/installed.d.ts +25 -1
  204. package/dist/utils/installed.js +33 -0
  205. package/dist/utils/installed.js.map +1 -1
  206. package/dist/utils/installed.test.js +55 -1
  207. package/dist/utils/installed.test.js.map +1 -1
  208. package/dist/utils/resolve-paths.d.ts +4 -4
  209. package/dist/utils/resolve-paths.js +5 -7
  210. package/dist/utils/resolve-paths.js.map +1 -1
  211. package/dist/utils/resolve-paths.test.js +9 -5
  212. package/dist/utils/resolve-paths.test.js.map +1 -1
  213. package/dist/utils/tailwind.d.ts +45 -0
  214. package/dist/utils/tailwind.js +90 -0
  215. package/dist/utils/tailwind.js.map +1 -0
  216. package/dist/utils/tailwind.test.d.ts +1 -0
  217. package/dist/utils/tailwind.test.js +134 -0
  218. package/dist/utils/tailwind.test.js.map +1 -0
  219. package/package.json +9 -1
  220. package/dist/ui/components/area-chart/area-chart-render.test.ts +0 -63
  221. package/dist/ui/components/bar-chart/bar-chart-render.test.ts +0 -58
  222. package/dist/ui/components/button/button-render.test.ts +0 -132
  223. package/dist/ui/components/button-group/button-group-render.test.ts +0 -146
  224. package/dist/ui/components/candlestick-chart/candlestick-chart-render.test.ts +0 -59
  225. package/dist/ui/components/chart/chart-render.test.ts +0 -730
  226. package/dist/ui/components/line-chart/line-chart-render.test.ts +0 -83
  227. package/dist/ui/components/scatter-chart/scatter-chart-render.test.ts +0 -64
@@ -1,7 +1,8 @@
1
1
  import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
- import { readFileSync, writeFileSync, existsSync, readdirSync } from 'node:fs';
2
+ import { readFileSync, writeFileSync, existsSync, readdirSync, rmSync, } from 'node:fs';
3
3
  import { join } from 'node:path';
4
- import { createFixture, DEFAULT_CONFIG } from '../test-utils';
4
+ import { stripVTControlCharacters } from 'node:util';
5
+ import { createFixture, DEFAULT_CONFIG, onTailwind, shippedTheme, } from '../test-utils';
5
6
  import { hashContent } from '../lockfile';
6
7
  const spinnerMock = { start: vi.fn(), stop: vi.fn() };
7
8
  vi.mock('@clack/prompts', () => ({
@@ -30,7 +31,7 @@ let fixture;
30
31
  vi.mock('../utils/resolve-paths.js', () => ({
31
32
  getUiSourceDir: () => fixture.uiDir,
32
33
  getComponentSourceDir: (name) => join(fixture.uiDir, 'components', name),
33
- getComponentFiles: (name) => readdirSync(join(fixture.uiDir, 'components', name)).filter((f) => f.endsWith('.ts')),
34
+ getComponentFiles: (name, { tests }) => readdirSync(join(fixture.uiDir, 'components', name)).filter((f) => f.endsWith('.ts') && (tests || !f.endsWith('.test.ts'))),
34
35
  rewriteImports: (content) => content,
35
36
  }));
36
37
  // The install step runs the project's package manager; record calls instead.
@@ -616,6 +617,26 @@ describe('updateCommand --force', () => {
616
617
  const lockfile = readLockfile(fixture.dir);
617
618
  expect(lockfile.theme['default.css'].hash).toBe(hashContent(upstream));
618
619
  });
620
+ it("adds the Tailwind plugin's test once the project has the testing library", async () => {
621
+ const pluginTest = "import { blotchPlugin } from './tailwind-plugin';";
622
+ fixture = createFixture({
623
+ config: DEFAULT_CONFIG,
624
+ lockfile: { version: 1, components: {}, utils: {}, theme: {} },
625
+ themeFiles: { 'tailwind-plugin.test.ts': pluginTest },
626
+ });
627
+ vi.spyOn(process, 'cwd').mockReturnValue(fixture.dir);
628
+ const { updateCommand } = await import('./update');
629
+ await updateCommand({ force: true });
630
+ expect(existsSync(join(fixture.dir, DEFAULT_CONFIG.aliases.theme, 'tailwind-plugin.test.ts'))).toBe(false);
631
+ writeFileSync(join(fixture.dir, 'package.json'), JSON.stringify({
632
+ devDependencies: { '@blotch/angular-lynx-testing-library': '^22.2.3' },
633
+ }));
634
+ await updateCommand({ force: true });
635
+ expect(readTheme(fixture, 'tailwind-plugin.test.ts')).toBe(pluginTest);
636
+ expect(readLockfile(fixture.dir).theme['tailwind-plugin.test.ts']).toEqual({
637
+ hash: hashContent(pluginTest),
638
+ });
639
+ });
619
640
  it('never prompts even when conflicts, user-modifications and new files coexist', async () => {
620
641
  fixture = createFixture({
621
642
  config: DEFAULT_CONFIG,
@@ -1254,6 +1275,7 @@ describe('updateCommand dependencies', () => {
1254
1275
  uiSource: {
1255
1276
  'line-chart': { 'line-chart.ts': lineChart },
1256
1277
  chart: { 'chart.ts': 'export const UiChart = {};' },
1278
+ icon: { 'icon.ts': 'export const UiIcon = {};' },
1257
1279
  },
1258
1280
  });
1259
1281
  vi.spyOn(process, 'cwd').mockReturnValue(fixture.dir);
@@ -1263,6 +1285,7 @@ describe('updateCommand dependencies', () => {
1263
1285
  expect(readInstalledFile(fixture.dir, 'line-chart', 'line-chart.ts')).toBe(lineChart);
1264
1286
  expect(installComponentPackages).toHaveBeenCalledWith(fixture.dir, [
1265
1287
  'chart',
1288
+ 'icon',
1266
1289
  'line-chart',
1267
1290
  ]);
1268
1291
  });
@@ -1282,6 +1305,7 @@ describe('updateCommand dependencies', () => {
1282
1305
  uiSource: {
1283
1306
  'line-chart': { 'line-chart.ts': lineChart },
1284
1307
  chart: { 'chart.ts': 'export const UiChart = {};' },
1308
+ icon: { 'icon.ts': 'export const UiIcon = {};' },
1285
1309
  },
1286
1310
  });
1287
1311
  vi.spyOn(process, 'cwd').mockReturnValue(fixture.dir);
@@ -1351,6 +1375,9 @@ describe('updateCommand — ejected components', () => {
1351
1375
  uiSource: {
1352
1376
  select: { 'select.ts': select },
1353
1377
  'bottom-sheet': { 'bottom-sheet.ts': 'export const BottomSheet = {};' },
1378
+ // What bottom-sheet's glass variant draws with.
1379
+ glass: { 'glass.ts': 'export const Glass = {};' },
1380
+ icon: { 'icon.ts': 'export const Icon = {};' },
1354
1381
  },
1355
1382
  });
1356
1383
  vi.spyOn(process, 'cwd').mockReturnValue(fixture.dir);
@@ -1472,4 +1499,544 @@ describe('updateCommand without a terminal', () => {
1472
1499
  expect(readInstalledFile(fixture.dir, 'skeleton', 'skeleton.ts')).toBe(base);
1473
1500
  });
1474
1501
  });
1502
+ // 22.2.2's `init` copied the theme but recorded no hashes for it, so the first
1503
+ // `update` after a release that changed the theme called every theme file a
1504
+ // conflict, though nobody had touched them.
1505
+ describe('updateCommand — theme files an older init copied', () => {
1506
+ const themePath = (file) => join(fixture.dir, DEFAULT_CONFIG.aliases.theme, file);
1507
+ const upstream = {
1508
+ 'default.css': ':root { --primary: rgba(2, 2, 2, 1); }',
1509
+ 'dark.css': 'page.dark { --primary: rgba(3, 3, 3, 1); }',
1510
+ };
1511
+ /**
1512
+ * Writes `files` where init put the theme, with `"theme": {}` locked.
1513
+ */
1514
+ const setup = (files) => {
1515
+ fixture = createFixture({
1516
+ config: DEFAULT_CONFIG,
1517
+ lockfile: { version: 1, components: {}, theme: {} },
1518
+ themeFiles: upstream,
1519
+ });
1520
+ for (const [file, content] of Object.entries(files)) {
1521
+ writeFileSync(themePath(file), content);
1522
+ }
1523
+ vi.spyOn(process, 'cwd').mockReturnValue(fixture.dir);
1524
+ };
1525
+ it('updates an untouched copy as an auto-update, not a conflict', async () => {
1526
+ setup({
1527
+ 'default.css': shippedTheme('default.css'),
1528
+ 'dark.css': shippedTheme('dark.css'),
1529
+ });
1530
+ const p = await import('@clack/prompts');
1531
+ const { updateCommand } = await import('./update');
1532
+ await updateCommand({});
1533
+ expect(p.select).not.toHaveBeenCalled();
1534
+ expect(p.confirm).toHaveBeenCalledWith(expect.objectContaining({ message: 'Apply 2 auto-update(s)?' }));
1535
+ expect(readFileSync(themePath('default.css'), 'utf-8')).toBe(upstream['default.css']);
1536
+ expect(readFileSync(themePath('dark.css'), 'utf-8')).toBe(upstream['dark.css']);
1537
+ expect(readLockfile(fixture.dir).theme).toEqual({
1538
+ 'default.css': { hash: hashContent(upstream['default.css']) },
1539
+ 'dark.css': { hash: hashContent(upstream['dark.css']) },
1540
+ });
1541
+ });
1542
+ it('applies them without a terminal, rather than leaving conflicts and exiting 1', async () => {
1543
+ interactive = false;
1544
+ setup({ 'default.css': shippedTheme('default.css') });
1545
+ const p = await import('@clack/prompts');
1546
+ const { updateCommand } = await import('./update');
1547
+ await updateCommand({});
1548
+ expect(readFileSync(themePath('default.css'), 'utf-8')).toBe(upstream['default.css']);
1549
+ expect(p.outro).toHaveBeenCalledWith('Update complete.');
1550
+ });
1551
+ it('still asks about a copy that was edited, since its base is unknown', async () => {
1552
+ const edited = `${shippedTheme('default.css')}\npage { --mine: red; }\n`;
1553
+ setup({ 'default.css': edited });
1554
+ const p = await import('@clack/prompts');
1555
+ p.select.mockResolvedValueOnce('keep');
1556
+ const { updateCommand } = await import('./update');
1557
+ await updateCommand({});
1558
+ expect(p.select).toHaveBeenCalledWith(expect.objectContaining({
1559
+ message: expect.stringContaining('how to resolve?'),
1560
+ }));
1561
+ expect(readFileSync(themePath('default.css'), 'utf-8')).toBe(edited);
1562
+ });
1563
+ });
1564
+ // 22.2.3 stopped copying `*-render.test.ts`, but `update` only looked at the
1565
+ // files upstream ships now and kept every old lockfile entry, so the stale
1566
+ // spec stayed, importing a helper dolan doesn't copy, and broke `ng test`.
1567
+ describe('updateCommand — files upstream no longer ships', () => {
1568
+ const card = 'export const Card = {};';
1569
+ const stale = "import { setInputSignal } from '../../../test-utils/x';";
1570
+ const edited = `${stale}\n// mine\n`;
1571
+ const STALE = 'card-render.test.ts';
1572
+ const stalePath = () => join(fixture.dir, DEFAULT_CONFIG.aliases.components, 'card', STALE);
1573
+ /**
1574
+ * card is up to date, and it has STALE (as add copied it, unless `onDisk`
1575
+ * says otherwise), which upstream no longer ships. `upstreamCard` changes
1576
+ * card.ts upstream too.
1577
+ */
1578
+ const setup = ({ onDisk = stale, upstreamCard = card, } = {}) => {
1579
+ fixture = createFixture({
1580
+ config: DEFAULT_CONFIG,
1581
+ lockfile: {
1582
+ version: 1,
1583
+ components: {
1584
+ card: {
1585
+ 'card.ts': { hash: hashContent(card) },
1586
+ [STALE]: { hash: hashContent(stale) },
1587
+ },
1588
+ },
1589
+ theme: {},
1590
+ },
1591
+ components: {
1592
+ card: {
1593
+ 'card.ts': card,
1594
+ ...(onDisk === null ? {} : { [STALE]: onDisk }),
1595
+ },
1596
+ },
1597
+ uiSource: { card: { 'card.ts': upstreamCard } },
1598
+ });
1599
+ vi.spyOn(process, 'cwd').mockReturnValue(fixture.dir);
1600
+ };
1601
+ const lockedCard = () => readLockfile(fixture.dir).components.card;
1602
+ /**
1603
+ * The log messages, colors stripped.
1604
+ */
1605
+ const messages = async () => {
1606
+ const p = await import('@clack/prompts');
1607
+ const text = p.log.message.mock.calls
1608
+ .map(([m]) => m)
1609
+ .join('\n');
1610
+ // oxlint-disable-next-line no-control-regex -- matching ANSI color codes
1611
+ return text.replace(/\u001b\[\d+m/g, '');
1612
+ };
1613
+ /**
1614
+ * The options of the select prompt's first call.
1615
+ */
1616
+ const selectOptions = async () => {
1617
+ const p = await import('@clack/prompts');
1618
+ const [{ options }] = p.select.mock
1619
+ .calls[0];
1620
+ return options;
1621
+ };
1622
+ beforeEach(async () => {
1623
+ // A reset drops answers an earlier test queued and never used.
1624
+ const p = await import('@clack/prompts');
1625
+ p.select
1626
+ .mockReset()
1627
+ .mockResolvedValue('upstream');
1628
+ });
1629
+ it('deletes one nobody edited, with its lockfile entry, after the usual question', async () => {
1630
+ setup();
1631
+ const p = await import('@clack/prompts');
1632
+ const { updateCommand } = await import('./update');
1633
+ await updateCommand({});
1634
+ expect(p.confirm).toHaveBeenCalledWith(expect.objectContaining({ message: 'Apply 1 auto-update(s)?' }));
1635
+ expect(p.select).not.toHaveBeenCalled();
1636
+ expect(await messages()).toContain(`card/${STALE} — upstream no longer ships it → delete`);
1637
+ expect(existsSync(stalePath())).toBe(false);
1638
+ expect(lockedCard()).toEqual({ 'card.ts': { hash: hashContent(card) } });
1639
+ expect(p.log.success).toHaveBeenCalledWith(expect.stringContaining('deleted: upstream no longer ships them'));
1640
+ });
1641
+ it('deletes one nobody edited without a terminal too', async () => {
1642
+ interactive = false;
1643
+ setup();
1644
+ const p = await import('@clack/prompts');
1645
+ const { updateCommand } = await import('./update');
1646
+ await updateCommand({});
1647
+ expect(p.confirm).not.toHaveBeenCalled();
1648
+ expect(existsSync(stalePath())).toBe(false);
1649
+ expect(lockedCard()).not.toHaveProperty(STALE);
1650
+ expect(p.outro).toHaveBeenCalledWith('Update complete.');
1651
+ });
1652
+ it('asks before deleting one you edited, and deletes it when told to', async () => {
1653
+ setup({ onDisk: edited });
1654
+ const p = await import('@clack/prompts');
1655
+ const { updateCommand } = await import('./update');
1656
+ await updateCommand({});
1657
+ expect(await messages()).toContain(`card/${STALE} — upstream no longer ships it, and you modified it`);
1658
+ expect(p.select).toHaveBeenCalledTimes(1);
1659
+ expect(p.select).toHaveBeenCalledWith(expect.objectContaining({
1660
+ message: expect.stringContaining('you modified it, and upstream no longer ships it'),
1661
+ }));
1662
+ // A deletion is all or nothing, so there are no hunks to review.
1663
+ expect(await selectOptions()).toEqual([
1664
+ expect.objectContaining({ value: 'diff' }),
1665
+ {
1666
+ value: 'keep',
1667
+ label: 'Keep my version',
1668
+ hint: 'keep it as my own file',
1669
+ },
1670
+ { value: 'upstream', label: 'Delete it', hint: 'discard my changes' },
1671
+ ]);
1672
+ expect(existsSync(stalePath())).toBe(false);
1673
+ expect(lockedCard()).not.toHaveProperty(STALE);
1674
+ });
1675
+ it('shows what deleting it would take away', async () => {
1676
+ setup({ onDisk: edited });
1677
+ const p = await import('@clack/prompts');
1678
+ p.select
1679
+ .mockResolvedValueOnce('diff')
1680
+ .mockResolvedValueOnce('upstream');
1681
+ const { updateCommand } = await import('./update');
1682
+ await updateCommand({});
1683
+ expect(await messages()).toContain(`- // mine`);
1684
+ expect(existsSync(stalePath())).toBe(false);
1685
+ });
1686
+ it('keeps one you edited when told to, as your own file from then on', async () => {
1687
+ setup({ onDisk: edited });
1688
+ const p = await import('@clack/prompts');
1689
+ p.select.mockResolvedValueOnce('keep');
1690
+ const { updateCommand } = await import('./update');
1691
+ await updateCommand({});
1692
+ expect(readFileSync(stalePath(), 'utf-8')).toBe(edited);
1693
+ // Upstream has nothing left to merge into it, so dolan stops tracking it,
1694
+ // and the next update doesn't ask again.
1695
+ expect(lockedCard()).toEqual({ 'card.ts': { hash: hashContent(card) } });
1696
+ expect(p.log.info).toHaveBeenCalledWith(expect.stringContaining('file(s) kept as-is'));
1697
+ vi.clearAllMocks();
1698
+ await updateCommand({});
1699
+ expect(p.select).not.toHaveBeenCalled();
1700
+ expect(readFileSync(stalePath(), 'utf-8')).toBe(edited);
1701
+ });
1702
+ it('leaves one you edited for a terminal to ask about without one, and exits 1', async () => {
1703
+ interactive = false;
1704
+ setup({ onDisk: edited });
1705
+ const p = await import('@clack/prompts');
1706
+ const { updateCommand } = await import('./update');
1707
+ await expect(updateCommand({})).rejects.toThrow('process.exit(1)');
1708
+ expect(p.select).not.toHaveBeenCalled();
1709
+ expect(p.log.warn).toHaveBeenCalledWith(expect.stringContaining('conflict(s) left as they are'));
1710
+ expect(readFileSync(stalePath(), 'utf-8')).toBe(edited);
1711
+ expect(lockedCard()[STALE]).toEqual({ hash: hashContent(stale) });
1712
+ });
1713
+ it('deletes one you edited with --force', async () => {
1714
+ interactive = false;
1715
+ setup({ onDisk: edited });
1716
+ const { updateCommand } = await import('./update');
1717
+ await updateCommand({ force: true });
1718
+ expect(existsSync(stalePath())).toBe(false);
1719
+ expect(lockedCard()).not.toHaveProperty(STALE);
1720
+ });
1721
+ describe('with --selective', () => {
1722
+ it('asks about the deletion, and "Skip for now" keeps the file and its entry', async () => {
1723
+ setup();
1724
+ const p = await import('@clack/prompts');
1725
+ p.select.mockResolvedValueOnce('keep');
1726
+ const { updateCommand } = await import('./update');
1727
+ await updateCommand({ selective: true });
1728
+ expect(p.select).toHaveBeenCalledWith(expect.objectContaining({
1729
+ message: expect.stringMatching(/upstream no longer ships it$/),
1730
+ }));
1731
+ expect(await selectOptions()).toEqual([
1732
+ expect.objectContaining({ value: 'diff' }),
1733
+ expect.objectContaining({ value: 'keep', label: 'Skip for now' }),
1734
+ { value: 'upstream', label: 'Delete it', hint: 'remove the file' },
1735
+ ]);
1736
+ expect(readFileSync(stalePath(), 'utf-8')).toBe(stale);
1737
+ expect(lockedCard()[STALE]).toEqual({ hash: hashContent(stale) });
1738
+ });
1739
+ it('deletes it when accepted', async () => {
1740
+ setup();
1741
+ const { updateCommand } = await import('./update');
1742
+ await updateCommand({ selective: true });
1743
+ expect(existsSync(stalePath())).toBe(false);
1744
+ expect(lockedCard()).not.toHaveProperty(STALE);
1745
+ });
1746
+ });
1747
+ describe('already gone from disk', () => {
1748
+ it('drops its entry when nothing else changed', async () => {
1749
+ setup({ onDisk: null });
1750
+ const p = await import('@clack/prompts');
1751
+ const { updateCommand } = await import('./update');
1752
+ await updateCommand({});
1753
+ expect(p.confirm).not.toHaveBeenCalled();
1754
+ expect(await messages()).not.toContain(STALE);
1755
+ expect(p.outro).toHaveBeenCalledWith('Everything is up to date.');
1756
+ expect(lockedCard()).toEqual({ 'card.ts': { hash: hashContent(card) } });
1757
+ });
1758
+ it('drops its entry alongside an update', async () => {
1759
+ const upstreamCard = 'export const Card = { v: 2 };';
1760
+ setup({ onDisk: null, upstreamCard });
1761
+ const { updateCommand } = await import('./update');
1762
+ await updateCommand({});
1763
+ expect(lockedCard()).toEqual({
1764
+ 'card.ts': { hash: hashContent(upstreamCard) },
1765
+ });
1766
+ });
1767
+ });
1768
+ it("leaves the tests a project without the testing library doesn't get", async () => {
1769
+ // Copied before dolan checked for the library. Upstream still ships them,
1770
+ // so they aren't deleted: `doctor` says what to do about them instead.
1771
+ const test = "import '@blotch/angular-lynx-testing-library';";
1772
+ fixture = createFixture({
1773
+ config: DEFAULT_CONFIG,
1774
+ lockfile: {
1775
+ version: 1,
1776
+ components: {
1777
+ card: {
1778
+ 'card.ts': { hash: hashContent(card) },
1779
+ 'card.test.ts': { hash: hashContent(test) },
1780
+ },
1781
+ },
1782
+ theme: {},
1783
+ },
1784
+ components: { card: { 'card.ts': card, 'card.test.ts': test } },
1785
+ uiSource: { card: { 'card.ts': card, 'card.test.ts': test } },
1786
+ });
1787
+ vi.spyOn(process, 'cwd').mockReturnValue(fixture.dir);
1788
+ const { updateCommand } = await import('./update');
1789
+ await updateCommand({});
1790
+ expect(readInstalledFile(fixture.dir, 'card', 'card.test.ts')).toBe(test);
1791
+ expect(lockedCard()['card.test.ts']).toEqual({ hash: hashContent(test) });
1792
+ });
1793
+ });
1794
+ // An app that moved from Tailwind 3 to Tailwind 4 kept the plugin and its
1795
+ // test: `update` only looked at the theme files the app takes now, so
1796
+ // nothing offered to delete them, though nothing imports the plugin and the
1797
+ // app's coverage counted it.
1798
+ describe('updateCommand — theme files for the Tailwind version the app left', () => {
1799
+ const theme = {
1800
+ 'default.css': ':root { --primary: rgba(1, 1, 1, 1); }',
1801
+ 'dark.css': 'page.dark { --primary: rgba(2, 2, 2, 1); }',
1802
+ 'tailwind.css': '@theme inline { --color-primary: var(--primary); }',
1803
+ 'tailwind-plugin.ts': 'export const blotchPlugin = {};',
1804
+ 'tailwind-plugin.test.ts': "import { blotchPlugin } from './tailwind-plugin';",
1805
+ };
1806
+ const PLUGIN = 'tailwind-plugin.ts';
1807
+ const PLUGIN_TEST = 'tailwind-plugin.test.ts';
1808
+ const edited = `${theme[PLUGIN]}\n// mine\n`;
1809
+ const themePath = (file) => join(fixture.dir, DEFAULT_CONFIG.aliases.theme, file);
1810
+ const recorded = (files) => Object.fromEntries(files.map((file) => [file, { hash: hashContent(theme[file]) }]));
1811
+ /**
1812
+ * An app `init` set up on Tailwind 3 with the testing library, now on
1813
+ * `tailwind` (4 unless it says otherwise). Dolan's theme ships every file.
1814
+ * `locked` are the theme files the lockfile records and the folder has as
1815
+ * copied, and `onDisk` overrides what the folder has: a string replaces a
1816
+ * file, and null deletes it.
1817
+ */
1818
+ const setup = ({ tailwind = 4, locked = ['default.css', 'dark.css', PLUGIN, PLUGIN_TEST], onDisk = {}, } = {}) => {
1819
+ fixture = createFixture({
1820
+ config: DEFAULT_CONFIG,
1821
+ lockfile: { version: 1, components: {}, theme: recorded(locked) },
1822
+ themeFiles: theme,
1823
+ });
1824
+ onTailwind(fixture.dir, tailwind, { testing: true });
1825
+ for (const file of locked)
1826
+ writeFileSync(themePath(file), theme[file]);
1827
+ for (const [file, content] of Object.entries(onDisk)) {
1828
+ if (content === null)
1829
+ rmSync(themePath(file));
1830
+ else
1831
+ writeFileSync(themePath(file), content);
1832
+ }
1833
+ vi.spyOn(process, 'cwd').mockReturnValue(fixture.dir);
1834
+ };
1835
+ const lockedTheme = () => readLockfile(fixture.dir).theme;
1836
+ /**
1837
+ * What a log function was called with, colors stripped.
1838
+ */
1839
+ const logged = (log) => log.mock.calls
1840
+ .map(([message]) => stripVTControlCharacters(message))
1841
+ .join('\n');
1842
+ beforeEach(async () => {
1843
+ // A reset drops answers an earlier test queued and never used.
1844
+ const p = await import('@clack/prompts');
1845
+ p.select
1846
+ .mockReset()
1847
+ .mockResolvedValue('upstream');
1848
+ });
1849
+ it('deletes the plugin and its test nobody edited, with their lockfile entries, after the usual question', async () => {
1850
+ setup();
1851
+ const p = await import('@clack/prompts');
1852
+ const { updateCommand } = await import('./update');
1853
+ await updateCommand({});
1854
+ expect(p.confirm).toHaveBeenCalledWith(expect.objectContaining({ message: 'Apply 3 auto-update(s)?' }));
1855
+ expect(p.select).not.toHaveBeenCalled();
1856
+ expect(logged(p.log.message)).toContain([
1857
+ 'Shared files:',
1858
+ ' ✓ theme/default.css — up to date',
1859
+ ' ✓ theme/dark.css — up to date',
1860
+ ' + theme/tailwind.css — new file from upstream',
1861
+ ' − theme/tailwind-plugin.ts — the app uses Tailwind 4 → delete',
1862
+ ' − theme/tailwind-plugin.test.ts — the app uses Tailwind 4 → delete',
1863
+ ].join('\n'));
1864
+ expect(existsSync(themePath(PLUGIN))).toBe(false);
1865
+ expect(existsSync(themePath(PLUGIN_TEST))).toBe(false);
1866
+ expect(readFileSync(themePath('tailwind.css'), 'utf-8')).toBe(theme['tailwind.css']);
1867
+ expect(lockedTheme()).toEqual(recorded(['default.css', 'dark.css', 'tailwind.css']));
1868
+ expect(logged(p.log.success)).toContain('2 theme file(s) deleted: the app uses Tailwind 4.');
1869
+ });
1870
+ it('deletes them without a terminal too', async () => {
1871
+ interactive = false;
1872
+ setup();
1873
+ const p = await import('@clack/prompts');
1874
+ const { updateCommand } = await import('./update');
1875
+ await updateCommand({});
1876
+ expect(p.confirm).not.toHaveBeenCalled();
1877
+ expect(existsSync(themePath(PLUGIN))).toBe(false);
1878
+ expect(lockedTheme()).not.toHaveProperty(PLUGIN);
1879
+ expect(p.outro).toHaveBeenCalledWith('Update complete.');
1880
+ });
1881
+ it('asks before deleting a plugin you edited, and deletes it when told to', async () => {
1882
+ setup({ onDisk: { [PLUGIN]: edited } });
1883
+ const p = await import('@clack/prompts');
1884
+ const { updateCommand } = await import('./update');
1885
+ await updateCommand({});
1886
+ expect(logged(p.log.message)).toContain(' ⚠ theme/tailwind-plugin.ts — the app uses Tailwind 4, and you modified it');
1887
+ expect(p.select).toHaveBeenCalledTimes(1);
1888
+ const [{ message, options }] = p.select.mock
1889
+ .calls[0];
1890
+ expect(stripVTControlCharacters(message)).toBe('shared/theme/tailwind-plugin.ts — you modified it, and the app uses Tailwind 4');
1891
+ // A deletion is all or nothing, so there are no hunks to review.
1892
+ expect(options.map(({ value }) => value)).toEqual([
1893
+ 'diff',
1894
+ 'keep',
1895
+ 'upstream',
1896
+ ]);
1897
+ expect(existsSync(themePath(PLUGIN))).toBe(false);
1898
+ expect(existsSync(themePath(PLUGIN_TEST))).toBe(false);
1899
+ expect(lockedTheme()).not.toHaveProperty(PLUGIN);
1900
+ });
1901
+ it('keeps a plugin you edited when told to, as your own file from then on', async () => {
1902
+ setup({ onDisk: { [PLUGIN]: edited } });
1903
+ const p = await import('@clack/prompts');
1904
+ p.select.mockResolvedValueOnce('keep');
1905
+ const { updateCommand } = await import('./update');
1906
+ await updateCommand({});
1907
+ expect(readFileSync(themePath(PLUGIN), 'utf-8')).toBe(edited);
1908
+ expect(lockedTheme()).not.toHaveProperty(PLUGIN);
1909
+ expect(logged(p.log.info)).toContain('1 file(s) kept as-is.');
1910
+ vi.clearAllMocks();
1911
+ await updateCommand({});
1912
+ expect(p.select).not.toHaveBeenCalled();
1913
+ expect(readFileSync(themePath(PLUGIN), 'utf-8')).toBe(edited);
1914
+ });
1915
+ it('leaves a plugin you edited for a terminal to ask about without one, and exits 1', async () => {
1916
+ interactive = false;
1917
+ setup({ onDisk: { [PLUGIN]: edited } });
1918
+ const p = await import('@clack/prompts');
1919
+ const { updateCommand } = await import('./update');
1920
+ await expect(updateCommand({})).rejects.toThrow('process.exit(1)');
1921
+ expect(p.select).not.toHaveBeenCalled();
1922
+ expect(logged(p.log.warn)).toContain('1 conflict(s) left as they are');
1923
+ expect(readFileSync(themePath(PLUGIN), 'utf-8')).toBe(edited);
1924
+ expect(lockedTheme()[PLUGIN]).toEqual({
1925
+ hash: hashContent(theme[PLUGIN]),
1926
+ });
1927
+ // The test nobody edited goes anyway, as auto-updates do.
1928
+ expect(existsSync(themePath(PLUGIN_TEST))).toBe(false);
1929
+ });
1930
+ it('deletes a plugin you edited with --force', async () => {
1931
+ interactive = false;
1932
+ setup({ onDisk: { [PLUGIN]: edited } });
1933
+ const { updateCommand } = await import('./update');
1934
+ await updateCommand({ force: true });
1935
+ expect(existsSync(themePath(PLUGIN))).toBe(false);
1936
+ expect(lockedTheme()).not.toHaveProperty(PLUGIN);
1937
+ });
1938
+ describe('with --selective', () => {
1939
+ it('asks about each deletion, and "Skip for now" keeps the file and its entry', async () => {
1940
+ setup();
1941
+ const p = await import('@clack/prompts');
1942
+ p.select.mockResolvedValue('keep');
1943
+ const { updateCommand } = await import('./update');
1944
+ await updateCommand({ selective: true });
1945
+ const messages = p.select.mock.calls.map(([{ message }]) => stripVTControlCharacters(message));
1946
+ expect(messages).toEqual([
1947
+ 'shared/theme/tailwind.css — new file from upstream',
1948
+ 'shared/theme/tailwind-plugin.ts — the app uses Tailwind 4',
1949
+ 'shared/theme/tailwind-plugin.test.ts — the app uses Tailwind 4',
1950
+ ]);
1951
+ expect(readFileSync(themePath(PLUGIN), 'utf-8')).toBe(theme[PLUGIN]);
1952
+ expect(lockedTheme()).toEqual(recorded(['default.css', 'dark.css', PLUGIN, PLUGIN_TEST]));
1953
+ expect(logged(p.log.info)).toContain('3 file(s) skipped');
1954
+ });
1955
+ it('deletes them when accepted', async () => {
1956
+ setup();
1957
+ const { updateCommand } = await import('./update');
1958
+ await updateCommand({ selective: true });
1959
+ expect(existsSync(themePath(PLUGIN))).toBe(false);
1960
+ expect(existsSync(themePath(PLUGIN_TEST))).toBe(false);
1961
+ expect(lockedTheme()).toEqual(recorded(['default.css', 'dark.css', 'tailwind.css']));
1962
+ });
1963
+ });
1964
+ describe('already deleted by hand', () => {
1965
+ it('drops their entries when nothing else changed', async () => {
1966
+ setup({
1967
+ locked: [
1968
+ 'default.css',
1969
+ 'dark.css',
1970
+ 'tailwind.css',
1971
+ PLUGIN,
1972
+ PLUGIN_TEST,
1973
+ ],
1974
+ onDisk: { [PLUGIN]: null, [PLUGIN_TEST]: null },
1975
+ });
1976
+ const p = await import('@clack/prompts');
1977
+ const { updateCommand } = await import('./update');
1978
+ await updateCommand({});
1979
+ expect(p.confirm).not.toHaveBeenCalled();
1980
+ expect(logged(p.log.message)).not.toContain(PLUGIN);
1981
+ expect(p.outro).toHaveBeenCalledWith('Everything is up to date.');
1982
+ expect(lockedTheme()).toEqual(recorded(['default.css', 'dark.css', 'tailwind.css']));
1983
+ });
1984
+ it('drops their entries alongside an update', async () => {
1985
+ setup({ onDisk: { [PLUGIN]: null, [PLUGIN_TEST]: null } });
1986
+ const p = await import('@clack/prompts');
1987
+ const { updateCommand } = await import('./update');
1988
+ await updateCommand({});
1989
+ expect(p.confirm).toHaveBeenCalledWith(expect.objectContaining({ message: 'Apply 1 auto-update(s)?' }));
1990
+ expect(logged(p.log.success)).not.toContain('deleted');
1991
+ expect(lockedTheme()).toEqual(recorded(['default.css', 'dark.css', 'tailwind.css']));
1992
+ });
1993
+ });
1994
+ it('deletes tailwind.css after a move back to Tailwind 3, and brings the plugin back', async () => {
1995
+ setup({ tailwind: 3, locked: ['default.css', 'dark.css', 'tailwind.css'] });
1996
+ const p = await import('@clack/prompts');
1997
+ const { updateCommand } = await import('./update');
1998
+ await updateCommand({});
1999
+ expect(logged(p.log.message)).toContain(' − theme/tailwind.css — the app uses Tailwind 3 → delete');
2000
+ expect(existsSync(themePath('tailwind.css'))).toBe(false);
2001
+ expect(readFileSync(themePath(PLUGIN), 'utf-8')).toBe(theme[PLUGIN]);
2002
+ expect(lockedTheme()).toEqual(recorded(['default.css', 'dark.css', PLUGIN, PLUGIN_TEST]));
2003
+ expect(logged(p.log.success)).toContain('1 theme file(s) deleted: the app uses Tailwind 3.');
2004
+ });
2005
+ it("keeps the plugin's test that a project without the testing library doesn't get", async () => {
2006
+ // Upstream still ships it to a Tailwind 3 app, as it does a component's
2007
+ // tests, so it isn't deleted for want of the library.
2008
+ setup({ tailwind: 3 });
2009
+ onTailwind(fixture.dir, 3);
2010
+ const { updateCommand } = await import('./update');
2011
+ await updateCommand({});
2012
+ expect(readFileSync(themePath(PLUGIN_TEST), 'utf-8')).toBe(theme[PLUGIN_TEST]);
2013
+ expect(lockedTheme()[PLUGIN_TEST]).toEqual({
2014
+ hash: hashContent(theme[PLUGIN_TEST]),
2015
+ });
2016
+ });
2017
+ // Tailwind 3 apps were mostly set up by a release whose `init` recorded no
2018
+ // theme hashes, so their plugin isn't in the lockfile.
2019
+ describe('copied by an older init', () => {
2020
+ it('deletes an untouched copy', async () => {
2021
+ setup({
2022
+ locked: [],
2023
+ onDisk: { [PLUGIN]: shippedTheme('tailwind-plugin.ts') },
2024
+ });
2025
+ const { updateCommand } = await import('./update');
2026
+ await updateCommand({});
2027
+ expect(existsSync(themePath(PLUGIN))).toBe(false);
2028
+ expect(lockedTheme()).toEqual(recorded(['default.css', 'dark.css', 'tailwind.css']));
2029
+ });
2030
+ it('leaves an edited copy alone, since nothing says it was dolan’s', async () => {
2031
+ const mine = `${shippedTheme('tailwind-plugin.ts')}\n// mine\n`;
2032
+ setup({ locked: [], onDisk: { [PLUGIN]: mine } });
2033
+ const p = await import('@clack/prompts');
2034
+ const { updateCommand } = await import('./update');
2035
+ await updateCommand({});
2036
+ expect(p.select).not.toHaveBeenCalled();
2037
+ expect(logged(p.log.message)).not.toContain(PLUGIN);
2038
+ expect(readFileSync(themePath(PLUGIN), 'utf-8')).toBe(mine);
2039
+ });
2040
+ });
2041
+ });
1475
2042
  //# sourceMappingURL=update.test.js.map