ui-map-model.test.ts 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522
  1. /**
  2. * The Map's layout, without a browser (CG-49).
  3. *
  4. * The properties under test are the ones that make the picture mean something.
  5. * A map is only worth reading if the vertical position of a box is a claim
  6. * about the code — so the tests here are mostly about *why* a module ends up
  7. * where it does:
  8. *
  9. * - the layering rests on `declared` weight, not raw counts, because bare name
  10. * matching invents cross-module links out of shared method names;
  11. * - a two-cycle keeps its heavier direction and the lighter one is reported,
  12. * never quietly dropped;
  13. * - the same payload always produces the same picture, because a diagram you
  14. * cannot recognise between two visits is not a map of anything.
  15. *
  16. * The endpoint that feeds it is tested against a real index in
  17. * `ui-map-api.test.ts`.
  18. */
  19. import { describe, it, expect } from 'vitest';
  20. import {
  21. buildMapLayout,
  22. isEdgeVisible,
  23. linkId,
  24. moduleMetaLabel,
  25. nodeWidth,
  26. portPoint,
  27. strokeWidthFor,
  28. LAYER_GAP,
  29. MIN_WEIGHT,
  30. MIN_WEIGHT_WITH_TESTS,
  31. NODE_HEIGHT,
  32. type MapLayout,
  33. } from '../ui/src/lib/map-model';
  34. import type { WireMapLink, WireMapModule } from '../ui/src/lib/api';
  35. /* ------------------------------------------------------------- fixtures -- */
  36. function mod(id: string, over: Partial<WireMapModule> = {}): WireMapModule {
  37. return {
  38. id,
  39. label: id.slice(id.lastIndexOf('/') + 1) || id,
  40. files: over.files ?? 3,
  41. symbols: over.symbols ?? 30,
  42. languages: over.languages ?? [{ language: 'typescript', files: over.files ?? 3 }],
  43. test: over.test ?? false,
  44. facade: over.facade ?? false,
  45. fileList: over.fileList ?? { total: 3, shown: 3, truncated: false, items: [] },
  46. dependents: over.dependents ?? { files: 0, modules: 0 },
  47. };
  48. }
  49. function link(
  50. source: string,
  51. target: string,
  52. count: number,
  53. declared = count
  54. ): WireMapLink {
  55. return {
  56. source,
  57. target,
  58. count,
  59. declared,
  60. byKind: [{ kind: 'calls', count }],
  61. topPairs: [],
  62. };
  63. }
  64. function layerOf(layout: MapLayout, id: string): number {
  65. const node = layout.nodes.find((n) => n.id === id);
  66. expect(node, `no node ${id}`).toBeTruthy();
  67. return node!.layer;
  68. }
  69. const OPTS = { includeTests: false };
  70. /* ---------------------------------------------------------------- specs -- */
  71. describe('nodeWidth', () => {
  72. it('fits the wider of the two lines and never goes under the floor', () => {
  73. expect(nodeWidth('ui')).toBe(110);
  74. // A long id outgrows the floor; a long meta line outgrows a short id.
  75. expect(nodeWidth('src/resolution/(root files)')).toBeGreaterThan(200);
  76. expect(nodeWidth('src/db', '1218 symbols · 54 files')).toBeGreaterThan(nodeWidth('src/db'));
  77. });
  78. });
  79. describe('moduleMetaLabel', () => {
  80. it('says the counts in singular when there is one of them', () => {
  81. expect(moduleMetaLabel(mod('src/x', { symbols: 1, files: 1 }))).toBe('1 symbol · 1 file');
  82. expect(moduleMetaLabel(mod('src/x', { symbols: 9, files: 2 }))).toBe('9 symbols · 2 files');
  83. });
  84. });
  85. describe('strokeWidthFor', () => {
  86. it('grows with the logarithm of the count and stops at 6', () => {
  87. expect(strokeWidthFor(1)).toBe(1);
  88. expect(strokeWidthFor(700)).toBeLessThanOrEqual(6);
  89. expect(strokeWidthFor(1_000_000)).toBe(6);
  90. expect(strokeWidthFor(64)).toBeGreaterThan(strokeWidthFor(8));
  91. // A count of zero must not produce -Infinity.
  92. expect(Number.isFinite(strokeWidthFor(0))).toBe(true);
  93. });
  94. });
  95. describe('layering', () => {
  96. const modules = [mod('src/bin'), mod('src/core'), mod('src/db')];
  97. it('puts a module one layer above everything it depends on', () => {
  98. const layout = buildMapLayout(
  99. { modules, links: [link('src/bin', 'src/core', 10), link('src/core', 'src/db', 10)] },
  100. OPTS
  101. );
  102. expect(layerOf(layout, 'src/db')).toBe(0);
  103. expect(layerOf(layout, 'src/core')).toBe(1);
  104. expect(layerOf(layout, 'src/bin')).toBe(2);
  105. // Layer 0 is the foundations, and it is drawn at the BOTTOM.
  106. const bin = layout.nodes.find((n) => n.id === 'src/bin')!;
  107. const db = layout.nodes.find((n) => n.id === 'src/db')!;
  108. expect(bin.y).toBeLessThan(db.y);
  109. expect(db.y - bin.y).toBe(2 * (NODE_HEIGHT + LAYER_GAP));
  110. });
  111. it('names only the top and bottom layers', () => {
  112. const layout = buildMapLayout(
  113. { modules, links: [link('src/bin', 'src/core', 10), link('src/core', 'src/db', 10)] },
  114. OPTS
  115. );
  116. expect(layout.layers.map((l) => l.label)).toEqual([
  117. 'foundations — depend on nothing below',
  118. null,
  119. 'entry points',
  120. ]);
  121. });
  122. it('ignores a link with nothing declared behind it', () => {
  123. // `src/db -> src/bin` is 40 name-only matches (`run`, `push`, `finish`) and
  124. // would otherwise lift the storage layer above the CLI. It is still drawn —
  125. // as a back-edge — but it must not decide the vertical order.
  126. const layout = buildMapLayout(
  127. {
  128. modules,
  129. links: [
  130. link('src/bin', 'src/core', 10, 10),
  131. link('src/core', 'src/db', 10, 10),
  132. link('src/db', 'src/bin', 40, 0),
  133. ],
  134. },
  135. OPTS
  136. );
  137. expect(layout.basis.kind).toBe('declared');
  138. expect(layerOf(layout, 'src/db')).toBe(0);
  139. expect(layerOf(layout, 'src/bin')).toBe(2);
  140. const noisy = layout.edges.find((e) => e.source === 'src/db' && e.target === 'src/bin')!;
  141. expect(noisy).toBeTruthy();
  142. expect(noisy.back).toBe(true);
  143. });
  144. it('falls back to raw counts, and says so, when almost nothing is declared', () => {
  145. const layout = buildMapLayout(
  146. {
  147. modules,
  148. links: [
  149. link('src/bin', 'src/core', 10, 0),
  150. link('src/core', 'src/db', 10, 0),
  151. link('src/db', 'src/core', 2, 1),
  152. ],
  153. },
  154. OPTS
  155. );
  156. expect(layout.basis.kind).toBe('all');
  157. expect(layout.basis.declaredLinks).toBe(1);
  158. expect(layout.basis.totalLinks).toBe(3);
  159. expect(layout.basis.declaredLinks / layout.basis.totalLinks).toBeLessThan(0.4);
  160. // With raw counts the chain is still a chain, and the light back-reference
  161. // becomes the mutual one.
  162. expect(layerOf(layout, 'src/db')).toBe(0);
  163. expect(layerOf(layout, 'src/bin')).toBe(2);
  164. expect(layout.mutual.map((m) => m.back.source)).toEqual(['src/db']);
  165. });
  166. it('survives a three-module loop instead of recursing forever', () => {
  167. const layout = buildMapLayout(
  168. {
  169. modules,
  170. links: [
  171. link('src/bin', 'src/core', 5),
  172. link('src/core', 'src/db', 5),
  173. link('src/db', 'src/bin', 5),
  174. ],
  175. },
  176. OPTS
  177. );
  178. expect(layout.nodes).toHaveLength(3);
  179. expect(layout.moduleCycles).toEqual([['src/bin', 'src/core', 'src/db']]);
  180. // Every module still got a finite layer.
  181. expect(layout.nodes.every((n) => Number.isInteger(n.layer))).toBe(true);
  182. });
  183. });
  184. describe('two-cycles', () => {
  185. const modules = [mod('src/a'), mod('src/b')];
  186. it('keeps the heavier direction and reports the lighter as mutual', () => {
  187. const layout = buildMapLayout(
  188. { modules, links: [link('src/a', 'src/b', 20), link('src/b', 'src/a', 3)] },
  189. OPTS
  190. );
  191. expect(layerOf(layout, 'src/a')).toBe(1);
  192. expect(layerOf(layout, 'src/b')).toBe(0);
  193. expect(layout.mutual).toHaveLength(1);
  194. expect(layout.mutual[0]!.forward.source).toBe('src/a');
  195. expect(layout.mutual[0]!.back.source).toBe('src/b');
  196. // Both directions are still on the canvas; the lighter one points up.
  197. expect(layout.edges).toHaveLength(2);
  198. expect(layout.edges.find((e) => e.source === 'src/b')!.back).toBe(true);
  199. expect(layout.edges.find((e) => e.source === 'src/a')!.back).toBe(false);
  200. });
  201. it('breaks an exact tie the same way every time', () => {
  202. const one = buildMapLayout(
  203. { modules, links: [link('src/a', 'src/b', 7), link('src/b', 'src/a', 7)] },
  204. OPTS
  205. );
  206. const two = buildMapLayout(
  207. { modules, links: [link('src/b', 'src/a', 7), link('src/a', 'src/b', 7)] },
  208. OPTS
  209. );
  210. expect(one.mutual[0]!.back.source).toBe('src/b');
  211. expect(two.mutual[0]!.back.source).toBe('src/b');
  212. expect(layerOf(one, 'src/a')).toBe(layerOf(two, 'src/a'));
  213. });
  214. });
  215. describe('tests and thresholds', () => {
  216. const modules = [mod('src/core'), mod('__tests__', { test: true })];
  217. const links = [link('__tests__', 'src/core', 30), link('src/core', '__tests__', 2)];
  218. it('leaves test modules out until they are asked for, and their links with them', () => {
  219. const off = buildMapLayout({ modules, links }, { includeTests: false });
  220. expect(off.nodes.map((n) => n.id)).toEqual(['src/core']);
  221. expect(off.edges).toHaveLength(0);
  222. expect(off.minWeight).toBe(MIN_WEIGHT);
  223. const on = buildMapLayout({ modules, links }, { includeTests: true });
  224. expect(on.nodes).toHaveLength(2);
  225. expect(on.edges).toHaveLength(2);
  226. // A test module touches everything, so the bar for a visible link is higher.
  227. expect(on.minWeight).toBe(MIN_WEIGHT_WITH_TESTS);
  228. });
  229. it('marks a link under the threshold thin rather than deleting it', () => {
  230. const layout = buildMapLayout(
  231. {
  232. modules: [mod('src/a'), mod('src/b'), mod('src/c')],
  233. links: [link('src/a', 'src/b', 12), link('src/a', 'src/c', 2)],
  234. },
  235. OPTS
  236. );
  237. const thin = layout.edges.find((e) => e.target === 'src/c')!;
  238. expect(thin.thin).toBe(true);
  239. expect(isEdgeVisible(thin, null)).toBe(false);
  240. // Selecting either end brings it back — that is the whole point of hiding
  241. // it rather than dropping it.
  242. expect(isEdgeVisible(thin, 'src/a')).toBe(true);
  243. expect(isEdgeVisible(thin, 'src/c')).toBe(true);
  244. expect(isEdgeVisible(thin, 'src/b')).toBe(false);
  245. const fat = layout.edges.find((e) => e.target === 'src/b')!;
  246. expect(isEdgeVisible(fat, null)).toBe(true);
  247. expect(isEdgeVisible(fat, 'src/c')).toBe(false);
  248. });
  249. });
  250. describe('ports', () => {
  251. it('gives every link its own port, ordered by where the other end sits', () => {
  252. const layout = buildMapLayout(
  253. {
  254. modules: [mod('src/top'), mod('src/left'), mod('src/mid'), mod('src/right')],
  255. links: [
  256. link('src/top', 'src/left', 9),
  257. link('src/top', 'src/mid', 9),
  258. link('src/top', 'src/right', 9),
  259. ],
  260. },
  261. OPTS
  262. );
  263. const top = layout.nodes.find((n) => n.id === 'src/top')!;
  264. expect(top.sourceHandles).toHaveLength(3);
  265. expect(new Set(top.sourceHandles).size).toBe(3);
  266. // The handle order must follow the targets' left-to-right order, or the
  267. // three edges cross each other inside the gap for no reason.
  268. const xOf = (id: string) => {
  269. const n = layout.nodes.find((m) => m.id === id)!;
  270. return n.x + n.width / 2;
  271. };
  272. const targets = top.sourceHandles.map(
  273. (id) => layout.edges.find((e) => e.id === id)!.target
  274. );
  275. const xs = targets.map(xOf);
  276. expect(xs).toEqual([...xs].sort((a, b) => a - b));
  277. // Each target's single incoming link is its only target handle.
  278. for (const id of ['src/left', 'src/mid', 'src/right']) {
  279. expect(layout.nodes.find((n) => n.id === id)!.targetHandles).toHaveLength(1);
  280. }
  281. });
  282. it('names an edge by its endpoints, so two runs key the same', () => {
  283. expect(linkId({ source: 'a', target: 'b' })).toBe(linkId({ source: 'a', target: 'b' }));
  284. expect(linkId({ source: 'a', target: 'b' })).not.toBe(linkId({ source: 'b', target: 'a' }));
  285. });
  286. });
  287. describe('determinism', () => {
  288. const modules = [
  289. mod('src/alpha'),
  290. mod('src/beta'),
  291. mod('src/gamma'),
  292. mod('src/delta'),
  293. mod('src/epsilon'),
  294. ];
  295. const links = [
  296. link('src/alpha', 'src/beta', 12),
  297. link('src/alpha', 'src/gamma', 8),
  298. link('src/beta', 'src/delta', 15),
  299. link('src/gamma', 'src/delta', 6),
  300. link('src/delta', 'src/epsilon', 20),
  301. link('src/beta', 'src/epsilon', 5),
  302. ];
  303. it('produces an identical layout from an identical payload', () => {
  304. const a = buildMapLayout({ modules, links }, OPTS);
  305. const b = buildMapLayout({ modules, links }, OPTS);
  306. expect(JSON.stringify(b)).toBe(JSON.stringify(a));
  307. });
  308. it('does not depend on the order the payload happened to arrive in', () => {
  309. const a = buildMapLayout({ modules, links }, OPTS);
  310. const b = buildMapLayout(
  311. { modules: [...modules].reverse(), links: [...links].reverse() },
  312. OPTS
  313. );
  314. const positions = (l: MapLayout) =>
  315. l.nodes
  316. .map((n) => `${n.id}@${n.layer}:${Math.round(n.x)},${Math.round(n.y)}`)
  317. .sort()
  318. .join('|');
  319. expect(positions(b)).toBe(positions(a));
  320. });
  321. it('places an unconnected module without stretching the canvas around it', () => {
  322. const withIsland = buildMapLayout(
  323. { modules: [...modules, mod('src/island')], links },
  324. OPTS
  325. );
  326. const island = withIsland.nodes.find((n) => n.id === 'src/island')!;
  327. expect(island).toBeTruthy();
  328. expect(island.layer).toBe(0);
  329. // Parked at the right-hand end of its layer, not interleaved through the
  330. // modules that actually connect.
  331. const sameLayer = withIsland.nodes.filter((n) => n.layer === 0);
  332. expect(Math.max(...sameLayer.map((n) => n.x))).toBe(island.x);
  333. // And the canvas is no wider than the boxes standing shoulder to shoulder.
  334. const widest = Math.max(
  335. ...[0, 1, 2, 3].map((layer) =>
  336. withIsland.nodes
  337. .filter((n) => n.layer === layer)
  338. .reduce((sum, n) => sum + n.width, 0)
  339. )
  340. );
  341. expect(withIsland.width).toBeLessThan(widest + 6 * 34 + 200);
  342. });
  343. });
  344. describe('empty and degenerate inputs', () => {
  345. it('answers an empty payload without throwing', () => {
  346. const layout = buildMapLayout({ modules: [], links: [] }, OPTS);
  347. expect(layout.nodes).toHaveLength(0);
  348. expect(layout.edges).toHaveLength(0);
  349. expect(layout.basis.kind).toBe('all');
  350. expect(Number.isFinite(layout.width)).toBe(true);
  351. expect(Number.isFinite(layout.height)).toBe(true);
  352. });
  353. it('drops a link whose other end was filtered out', () => {
  354. const layout = buildMapLayout(
  355. {
  356. modules: [mod('src/a'), mod('__tests__', { test: true })],
  357. links: [link('src/a', '__tests__', 9), link('src/a', 'src/ghost', 9)],
  358. },
  359. OPTS
  360. );
  361. expect(layout.edges).toHaveLength(0);
  362. });
  363. it('leaves a single layer unlabelled', () => {
  364. const layout = buildMapLayout({ modules: [mod('src/only')], links: [] }, OPTS);
  365. expect(layout.layers).toHaveLength(1);
  366. expect(layout.layers[0]!.label).toBeNull();
  367. });
  368. });
  369. describe('directional ports and room', () => {
  370. const modules = [mod('src/a'), mod('src/b')];
  371. it('draws the Map exactly as before: bottoms leave, tops arrive, every link runs down', () => {
  372. const layout = buildMapLayout(
  373. { modules: [mod('src/bin'), mod('src/core'), mod('src/db')], links: [link('src/bin', 'src/core', 10), link('src/core', 'src/db', 10)] },
  374. OPTS
  375. );
  376. for (const node of layout.nodes) {
  377. expect(node.ports.bottom.map((p) => p.id)).toEqual(node.sourceHandles);
  378. expect(node.ports.top.map((p) => p.id)).toEqual(node.targetHandles);
  379. expect(node.ports.bottom.every((p) => p.type === 'source')).toBe(true);
  380. expect(node.ports.top.every((p) => p.type === 'target')).toBe(true);
  381. }
  382. expect(layout.edges.every((e) => e.route === 'down')).toBe(true);
  383. });
  384. it('keeps a back-edge bottom-to-top under layered ports, and top-to-bottom under directional ones', () => {
  385. const payload = { modules, links: [link('src/a', 'src/b', 10), link('src/b', 'src/a', 2)] };
  386. const layered = buildMapLayout(payload, OPTS);
  387. const directional = buildMapLayout(payload, { ...OPTS, ports: 'directional' });
  388. for (const layout of [layered, directional]) {
  389. const back = layout.edges.find((e) => e.source === 'src/b')!;
  390. expect(back.route).toBe('up');
  391. expect(back.back).toBe(true);
  392. }
  393. const a = (l: MapLayout) => l.nodes.find((n) => n.id === 'src/a')!;
  394. const b = (l: MapLayout) => l.nodes.find((n) => n.id === 'src/b')!;
  395. const id = linkId({ source: 'src/b', target: 'src/a' });
  396. // The Map: leaves b's bottom, arrives at a's top — through both boxes, as it always has.
  397. expect(portPoint(b(layered), id, 'source').y).toBe(b(layered).y + NODE_HEIGHT);
  398. expect(portPoint(a(layered), id, 'target').y).toBe(a(layered).y);
  399. // Directional: leaves b's top, arrives at a's bottom — around them.
  400. expect(portPoint(b(directional), id, 'source').y).toBe(b(directional).y);
  401. expect(portPoint(a(directional), id, 'target').y).toBe(a(directional).y + NODE_HEIGHT);
  402. });
  403. it('joins two modules on one layer over the top, under directional ports', () => {
  404. const layout = buildMapLayout(
  405. { modules, links: [link('src/a', 'src/b', 10)] },
  406. { ...OPTS, ports: 'directional', layering: (ids) => new Map(ids.map((id) => [id, 0])) }
  407. );
  408. const edge = layout.edges[0]!;
  409. expect(edge.route).toBe('level');
  410. const a = layout.nodes.find((n) => n.id === 'src/a')!;
  411. const b = layout.nodes.find((n) => n.id === 'src/b')!;
  412. expect(portPoint(a, edge.id, 'source').y).toBe(a.y);
  413. expect(portPoint(b, edge.id, 'target').y).toBe(b.y);
  414. });
  415. it('widens a box to keep its ports apart, and only then', () => {
  416. const leaves = Array.from({ length: 20 }, (_, i) => mod(`src/leaf${i}`));
  417. const payload = { modules: [mod('src/hub'), ...leaves], links: leaves.map((l) => link('src/hub', l.id, 5)) };
  418. const plain = buildMapLayout(payload, OPTS).nodes.find((n) => n.id === 'src/hub')!;
  419. const pitched = buildMapLayout(payload, { ...OPTS, portPitch: 12 }).nodes.find((n) => n.id === 'src/hub')!;
  420. // Nothing depends on the hub, so its second line is the island's.
  421. expect(plain.width).toBe(nodeWidth('src/hub', moduleMetaLabel(mod('src/hub'), true)));
  422. expect(pitched.width).toBe(Math.max(plain.width, 21 * 12));
  423. expect(pitched.width).toBeGreaterThan(plain.width);
  424. });
  425. it('spaces layers by the gap a view asks for', () => {
  426. const payload = { modules, links: [link('src/a', 'src/b', 10)] };
  427. const wide = buildMapLayout(payload, { ...OPTS, layerGap: 116 });
  428. const a = wide.nodes.find((n) => n.id === 'src/a')!;
  429. const b = wide.nodes.find((n) => n.id === 'src/b')!;
  430. expect(b.y - a.y).toBe(NODE_HEIGHT + 116);
  431. expect(wide.height).toBe(buildMapLayout(payload, OPTS).height + (116 - LAYER_GAP));
  432. // Layer 0 is the bottom, so it has the larger y.
  433. expect(wide.layers[0]!.y - wide.layers[1]!.y).toBe(NODE_HEIGHT + 116);
  434. });
  435. });
  436. describe('how much leans on a box', () => {
  437. it('scales the bar against the heaviest module DRAWN', () => {
  438. const modules = [
  439. mod('src/types', { dependents: { files: 90, modules: 5 } }),
  440. mod('src/db', { dependents: { files: 45, modules: 3 } }),
  441. mod('src/cli', { dependents: { files: 0, modules: 0 } }),
  442. ];
  443. const links = [link('src/db', 'src/types', 20), link('src/cli', 'src/db', 20)];
  444. const layout = buildMapLayout({ modules, links }, OPTS);
  445. const weightOf = (id: string) => layout.nodes.find((n) => n.id === id)!.weight;
  446. expect(weightOf('src/types')).toBe(1);
  447. expect(weightOf('src/db')).toBeCloseTo(0.5, 5);
  448. // Nothing depends on the CLI, so it draws no bar at all rather than a
  449. // sliver a reader would have to squint at to call empty.
  450. expect(weightOf('src/cli')).toBe(0);
  451. });
  452. it('rescales when a heavier test module joins the picture', () => {
  453. const modules = [
  454. mod('src/types', { dependents: { files: 40, modules: 4 } }),
  455. mod('src/app', { dependents: { files: 10, modules: 1 } }),
  456. mod('__tests__', { test: true, dependents: { files: 80, modules: 6 } }),
  457. ];
  458. const links = [link('src/app', 'src/types', 20), link('__tests__', 'src/app', 20)];
  459. const spec = { modules, links };
  460. // Tests off: the app's own busiest box is the full bar.
  461. const off = buildMapLayout(spec, { includeTests: false });
  462. expect(off.nodes.find((n) => n.id === 'src/types')!.weight).toBe(1);
  463. // Tests on: the scale moves, rather than leaving a bar running past a
  464. // maximum the reader cannot see.
  465. const on = buildMapLayout(spec, { includeTests: true });
  466. expect(on.nodes.find((n) => n.id === 'src/types')!.weight).toBeCloseTo(0.5, 5);
  467. expect(on.nodes.find((n) => n.id === '__tests__')!.weight).toBe(1);
  468. });
  469. it('says the count on the box, and says nothing when nothing depends on it', () => {
  470. expect(moduleMetaLabel(mod('src/db', { dependents: { files: 45, modules: 3 } }))).toBe(
  471. '30 symbols · 3 files · 45 depend on it'
  472. );
  473. expect(moduleMetaLabel(mod('src/cli'))).toBe('30 symbols · 3 files');
  474. // An island's line is still the one sentence that matters about it.
  475. expect(moduleMetaLabel(mod('src/cli'), true)).toBe('nothing depends on this');
  476. });
  477. });