ui-package.test.ts 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643
  1. /**
  2. * `@colbymchenry/codegraph-ui` — the package's own test (task CG-61).
  3. *
  4. * A minimal Svelte host mounts the three headline components from the package
  5. * entry against a MOCK adapter and asserts what lands in the document. That is
  6. * the whole promise of the package in one file: CodeGraph Pro renders these
  7. * same components over its own in-process engine reads, so if a screen can be
  8. * drawn from an object literal here, it can be drawn from a graph there.
  9. *
  10. * The import is `ui/src/index.ts` — the package entry itself, not the
  11. * components one by one — so a name dropped from the public surface fails here
  12. * rather than in the Pro app.
  13. *
  14. * Everything below is deliberately about the SEAM, not about the screens:
  15. * layout, geometry and the rails have their own suites (`ui-symbol-model`,
  16. * `ui-flow-model`, `ui-map-model`). What is being proved here is that no
  17. * component reaches past the adapter for anything.
  18. */
  19. import { readFileSync } from 'node:fs';
  20. import { join } from 'node:path';
  21. import { flushSync, mount, unmount } from 'svelte';
  22. import { afterEach, beforeAll, beforeEach, describe, expect, it } from 'vitest';
  23. import {
  24. ArchitectureMap,
  25. CodegraphUi,
  26. FlowStrip,
  27. SearchPalette,
  28. SymbolView,
  29. TrailBar,
  30. createHttpAdapter,
  31. fileHref,
  32. flowHref,
  33. getGraphAdapter,
  34. hashNavigation,
  35. live,
  36. mapHref,
  37. setGraphAdapter,
  38. setNavigationDriver,
  39. symbolHref,
  40. trail,
  41. type GraphAdapter,
  42. type NavigationDriver,
  43. type WireFlowPayload,
  44. type WireMapPayload,
  45. type WireNodeRef,
  46. type WireSource,
  47. type WireStats,
  48. type WireSymbolPayload,
  49. } from '../ui/src/index';
  50. /* ---------------------------------------------------------------- fixtures */
  51. const ROOT = join(import.meta.dirname, '..');
  52. function nodeRef(overrides: Partial<WireNodeRef> = {}): WireNodeRef {
  53. return {
  54. id: 'function:parseToken@src/auth/token.ts:12',
  55. kind: 'function',
  56. name: 'parseToken',
  57. qualifiedName: 'parseToken',
  58. file: 'src/auth/token.ts',
  59. line: 12,
  60. endLine: 18,
  61. language: 'typescript',
  62. test: false,
  63. ...overrides,
  64. };
  65. }
  66. const CALLER = nodeRef({
  67. id: 'function:handleCallback@src/auth/callback.ts:40',
  68. name: 'handleCallback',
  69. qualifiedName: 'handleCallback',
  70. file: 'src/auth/callback.ts',
  71. line: 40,
  72. endLine: 60,
  73. });
  74. const CALLEE = nodeRef({
  75. id: 'function:decodeJwt@src/auth/jwt.ts:3',
  76. name: 'decodeJwt',
  77. qualifiedName: 'decodeJwt',
  78. file: 'src/auth/jwt.ts',
  79. line: 3,
  80. endLine: 9,
  81. });
  82. const SYMBOL: WireSymbolPayload = {
  83. node: {
  84. ...nodeRef(),
  85. startColumn: 0,
  86. endColumn: 1,
  87. lines: 7,
  88. exported: true,
  89. },
  90. ancestors: [nodeRef({ id: 'file:src/auth/token.ts', kind: 'file', name: 'token.ts' })],
  91. members: { total: 0, shown: 0, truncated: false, items: [] },
  92. incoming: {
  93. total: 1,
  94. shown: 1,
  95. truncated: false,
  96. items: [
  97. {
  98. node: CALLER,
  99. edgeKinds: ['calls'],
  100. edges: [{ kind: 'calls', line: 44, col: 6, confidence: 1 }],
  101. edgeCount: 1,
  102. lines: [44],
  103. confidence: 1,
  104. uncertain: false,
  105. synthesized: false,
  106. },
  107. ],
  108. },
  109. outgoing: {
  110. total: 1,
  111. shown: 1,
  112. truncated: false,
  113. items: [
  114. {
  115. node: CALLEE,
  116. edgeKinds: ['calls'],
  117. edges: [{ kind: 'calls', line: 14, col: 10, confidence: 1 }],
  118. edgeCount: 1,
  119. lines: [14],
  120. confidence: 1,
  121. uncertain: false,
  122. synthesized: false,
  123. },
  124. ],
  125. },
  126. typesUsed: [],
  127. counts: { callers: 1, callees: 1, typesUsed: 0, fanIn: 1, fanOut: 1, members: 0, hub: false },
  128. tests: { reached: false, hops: null, fileCount: 0, files: [], exhaustive: true, hopsSearched: 3 },
  129. outsideIndex: { total: 0, byKind: {}, samples: [] },
  130. blast: {
  131. direct: 1,
  132. withinHops: 2,
  133. hops: 3,
  134. files: 2,
  135. testFiles: 0,
  136. routes: 0,
  137. topFiles: [{ file: 'src/auth/callback.ts', symbols: 1, test: false }],
  138. },
  139. drift: false,
  140. };
  141. const SOURCE_LINES = [
  142. 'export function parseToken(raw: string): Token {',
  143. ' // Normalize expiry before anything else reads it.',
  144. ' const claims = decodeJwt(raw);',
  145. ' return { ...claims, expiresAt: claims.exp * 1000 };',
  146. '}',
  147. ];
  148. const SOURCE: WireSource = {
  149. file: 'src/auth/token.ts',
  150. language: 'typescript',
  151. drift: false,
  152. showing: 'indexed',
  153. contentHash: 'abc123',
  154. indexedAt: 1_700_000_000_000,
  155. generated: false,
  156. totalLines: 40,
  157. from: 12,
  158. to: 18,
  159. lines: SOURCE_LINES,
  160. };
  161. const FLOW: WireFlowPayload = {
  162. query: { kind: 'directed', from: 'handleCallback', to: 'decodeJwt', symbols: [] },
  163. flows: [
  164. {
  165. id: 'flow-1',
  166. label: 'handleCallback → decodeJwt',
  167. partial: false,
  168. boundary: null,
  169. hops: [
  170. {
  171. node: CALLER,
  172. edge: null,
  173. callRef: { line: 44, col: 6, name: 'parseToken', targetId: SYMBOL.node.id, backwards: false },
  174. source: {
  175. file: 'src/auth/callback.ts',
  176. language: 'typescript',
  177. from: 44,
  178. to: 46,
  179. lines: [' const token = parseToken(raw);'],
  180. drift: false,
  181. },
  182. },
  183. {
  184. node: nodeRef(),
  185. edge: {
  186. kind: 'calls',
  187. line: 44,
  188. label: 'calls',
  189. upward: false,
  190. uncertain: false,
  191. synthesized: false,
  192. },
  193. callRef: null,
  194. source: {
  195. file: 'src/auth/token.ts',
  196. language: 'typescript',
  197. from: 12,
  198. to: 14,
  199. lines: SOURCE_LINES.slice(0, 3),
  200. drift: false,
  201. },
  202. },
  203. ],
  204. },
  205. ],
  206. ambiguous: [],
  207. unresolved: [],
  208. reason: null,
  209. index: { lastIndexedAt: 1_700_000_000_000, edges: 4, files: 3 },
  210. timing: { elapsedMs: 2 },
  211. };
  212. const MAP: WireMapPayload = {
  213. root: 'src',
  214. depth: 1,
  215. roots: [{ root: 'src', label: 'src', files: 3 }],
  216. modules: [
  217. {
  218. id: 'src/auth',
  219. label: 'auth',
  220. files: 2,
  221. symbols: 6,
  222. languages: [{ language: 'typescript', files: 2 }],
  223. test: false,
  224. facade: false,
  225. fileList: { total: 2, shown: 2, truncated: false, items: ['src/auth/token.ts', 'src/auth/callback.ts'] },
  226. },
  227. {
  228. id: 'src/http',
  229. label: 'http',
  230. files: 1,
  231. symbols: 3,
  232. languages: [{ language: 'typescript', files: 1 }],
  233. test: false,
  234. facade: false,
  235. fileList: { total: 1, shown: 1, truncated: false, items: ['src/http/server.ts'] },
  236. },
  237. ],
  238. links: [
  239. {
  240. source: 'src/http',
  241. target: 'src/auth',
  242. count: 9,
  243. declared: 7,
  244. byKind: [{ kind: 'calls', count: 9 }],
  245. topPairs: [{ from: 'src/http/server.ts', to: 'src/auth/token.ts', count: 9, declared: 7 }],
  246. },
  247. ],
  248. cycles: { total: 0, shown: 0, truncated: false, items: [] },
  249. excluded: { uncertainEdges: 0, confidenceBelow: 0.6 },
  250. index: { lastIndexedAt: 1_700_000_000_000, edges: 9, files: 3 },
  251. timing: { elapsedMs: 1, cached: false },
  252. };
  253. const STATS: WireStats = {
  254. project: { root: '/tmp/demo', name: 'demo' },
  255. index: {
  256. state: 'ready',
  257. lastIndexedAt: 1_700_000_000_000,
  258. stale: false,
  259. version: '1.0.0',
  260. extractionVersion: 1,
  261. backend: 'node-sqlite',
  262. journalMode: 'wal',
  263. pendingReferences: 0,
  264. generatedFiles: 0,
  265. watching: false,
  266. watcherDegraded: false,
  267. },
  268. graph: {
  269. nodes: 9,
  270. edges: 9,
  271. files: 3,
  272. nodesByKind: { function: 9 },
  273. edgesByKind: { calls: 9 },
  274. filesByLanguage: { typescript: 3 },
  275. dbSizeBytes: 1024,
  276. walSizeBytes: 0,
  277. },
  278. frameworks: [],
  279. thresholds: { hub: 40, uncertainBelow: 0.6 },
  280. blastScale: { maxDirect: 20, maxWithinHops: 60, hops: 3, sampled: 24, estimated: true },
  281. };
  282. /* ------------------------------------------------------------ mock adapter */
  283. /** Every method the components can reach, and a record of which ones they did. */
  284. function mockAdapter(): { adapter: GraphAdapter; calls: string[] } {
  285. const calls: string[] = [];
  286. const seen = <T>(name: string, value: T): Promise<T> => {
  287. calls.push(name);
  288. return Promise.resolve(value);
  289. };
  290. const adapter: GraphAdapter = {
  291. stats: () => seen('stats', STATS),
  292. search: () =>
  293. seen('search', {
  294. query: '',
  295. text: '',
  296. filters: { kinds: [], languages: [], paths: [], names: [] },
  297. results: { total: 0, shown: 0, truncated: false, items: [] },
  298. groups: [],
  299. }),
  300. node: (id) => {
  301. calls.push(`node:${id}`);
  302. return Promise.resolve(SYMBOL);
  303. },
  304. nodes: () => seen('nodes', { items: [], missing: [] }),
  305. source: (request) => {
  306. calls.push(`source:${request.file}`);
  307. return Promise.resolve(SOURCE);
  308. },
  309. file: () =>
  310. seen('file', {
  311. file: {
  312. path: 'src/auth/token.ts',
  313. language: 'typescript',
  314. size: 900,
  315. modifiedAt: 0,
  316. indexedAt: 0,
  317. contentHash: 'abc123',
  318. nodeCount: 3,
  319. generated: false,
  320. test: false,
  321. errors: [],
  322. id: 'file:src/auth/token.ts',
  323. },
  324. topLevel: { calls: 0 },
  325. drift: false,
  326. outline: { total: 0, shown: 0, truncated: false, items: [] },
  327. imports: { total: 0, shown: 0, truncated: false, items: [] },
  328. importedBy: { total: 0, shown: 0, truncated: false, items: [] },
  329. unresolvedImports: [],
  330. dependencies: [],
  331. dependents: [],
  332. }),
  333. fileCode: () =>
  334. seen('fileCode', {
  335. file: {
  336. path: 'src/auth/token.ts',
  337. language: 'typescript',
  338. size: 900,
  339. indexedAt: 0,
  340. contentHash: 'abc123',
  341. generated: false,
  342. test: false,
  343. errors: [],
  344. id: 'file:src/auth/token.ts',
  345. totalLines: 40,
  346. },
  347. drift: false,
  348. outline: { total: 0, shown: 0, truncated: false, items: [] },
  349. calls: { total: 0, shown: 0, truncated: false, items: [] },
  350. outside: { total: 0, shown: 0, truncated: false, items: [] },
  351. intraFileCalls: 0,
  352. timing: { elapsedMs: 1 },
  353. }),
  354. flow: () => seen('flow', FLOW),
  355. map: () => seen('map', MAP),
  356. routes: () =>
  357. seen('routes', {
  358. routed: false,
  359. routeCount: 0,
  360. shown: 0,
  361. truncated: false,
  362. topHandlerFile: null,
  363. topHandlerFileCount: 0,
  364. entries: [],
  365. }),
  366. entryPoints: () =>
  367. seen('entryPoints', {
  368. frameworks: [],
  369. routes: { routed: false, routeCount: 0, items: { total: 0, shown: 0, truncated: false, items: [] } },
  370. files: { total: 0, shown: 0, truncated: false, items: [] },
  371. tests: { total: 0, shown: 0, truncated: false, items: [] },
  372. hubs: { total: 0, shown: 0, truncated: false, items: [] },
  373. index: { lastIndexedAt: null, files: 3 },
  374. timing: { elapsedMs: 1, cached: false },
  375. }),
  376. // Deliberately no `events`: a host without a live channel is the normal
  377. // case, and nothing may poll in its absence.
  378. };
  379. return { adapter, calls };
  380. }
  381. /* ----------------------------------------------------------------- harness */
  382. let host: HTMLDivElement;
  383. let mounted: Record<string, unknown> | null = null;
  384. /** jsdom has none of the observers a canvas library expects. */
  385. beforeAll(() => {
  386. class NoopObserver {
  387. observe(): void {}
  388. unobserve(): void {}
  389. disconnect(): void {}
  390. }
  391. const globals = globalThis as Record<string, unknown>;
  392. globals.ResizeObserver ??= NoopObserver;
  393. globals.IntersectionObserver ??= NoopObserver;
  394. globals.MutationObserver ??= NoopObserver;
  395. globals.requestAnimationFrame ??= (fn: FrameRequestCallback) =>
  396. setTimeout(() => fn(0), 0) as unknown as number;
  397. globals.cancelAnimationFrame ??= (handle: number) => clearTimeout(handle);
  398. // jsdom's own `matchMedia` is a stub that is not callable here, and Svelte's
  399. // `MediaQuery` (which `@xyflow/svelte`'s store constructs eagerly) calls it
  400. // the moment a canvas mounts. Replace it outright rather than guarding.
  401. const media = (query: string) => ({
  402. media: query,
  403. matches: false,
  404. onchange: null,
  405. addEventListener() {},
  406. removeEventListener() {},
  407. addListener() {},
  408. removeListener() {},
  409. dispatchEvent: () => false,
  410. });
  411. Object.defineProperty(window, 'matchMedia', { configurable: true, writable: true, value: media });
  412. globals.matchMedia = media;
  413. if (!Element.prototype.scrollIntoView) Element.prototype.scrollIntoView = () => {};
  414. });
  415. beforeEach(() => {
  416. host = document.createElement('div');
  417. document.body.appendChild(host);
  418. trail.clear();
  419. });
  420. afterEach(() => {
  421. if (mounted) {
  422. void unmount(mounted);
  423. mounted = null;
  424. }
  425. host.remove();
  426. setGraphAdapter(null);
  427. setNavigationDriver(null);
  428. });
  429. /**
  430. * Mount a component and let its data effects settle.
  431. *
  432. * Every screen fetches inside an `$effect`, so a render is not finished until
  433. * the promise the adapter returned has resolved and the follow-up render has
  434. * flushed. Two macrotask turns cover the deepest chain any of them has (the
  435. * Symbol view: node, then its source).
  436. */
  437. async function render(
  438. // eslint-disable-next-line @typescript-eslint/no-explicit-any
  439. component: any,
  440. props: Record<string, unknown>
  441. ): Promise<void> {
  442. mounted = mount(component, { target: host, props }) as Record<string, unknown>;
  443. for (let turn = 0; turn < 4; turn += 1) {
  444. await new Promise((resolve) => setTimeout(resolve, 0));
  445. flushSync();
  446. }
  447. }
  448. describe('@colbymchenry/codegraph-ui — a host renders the package', () => {
  449. it('SymbolView draws callers, source and the callee rail from a mock adapter', async () => {
  450. const { adapter, calls } = mockAdapter();
  451. setGraphAdapter(adapter);
  452. await render(SymbolView, { id: SYMBOL.node.id, line: null });
  453. // It asked the adapter, by id, and it asked for the symbol's own slice.
  454. expect(calls).toContain(`node:${SYMBOL.node.id}`);
  455. expect(calls).toContain('source:src/auth/token.ts');
  456. const text = host.textContent ?? '';
  457. expect(text).toContain('parseToken');
  458. // The caller rail (left) and the callee rail (right) are both drawn.
  459. expect(text).toContain('handleCallback');
  460. expect(text).toContain('decodeJwt');
  461. // The verbatim source, not a summary of it.
  462. expect(text).toContain('expiresAt');
  463. // The honesty badge: nothing in the fixture's graph tests this symbol.
  464. expect(text.toLowerCase()).toContain('test');
  465. });
  466. it('FlowStrip draws one card per hop from a mock adapter', async () => {
  467. const { adapter, calls } = mockAdapter();
  468. setGraphAdapter(adapter);
  469. await render(FlowStrip, {
  470. from: 'handleCallback',
  471. to: 'decodeJwt',
  472. symbols: null,
  473. trailParam: null,
  474. });
  475. expect(calls).toContain('flow');
  476. const text = host.textContent ?? '';
  477. expect(text).toContain('handleCallback');
  478. expect(text).toContain('parseToken');
  479. });
  480. it('ArchitectureMap draws modules and their dependency from a mock adapter', async () => {
  481. const { adapter, calls } = mockAdapter();
  482. setGraphAdapter(adapter);
  483. await render(ArchitectureMap, { root: 'src', depth: 1, tests: false });
  484. expect(calls).toContain('map');
  485. const text = host.textContent ?? '';
  486. expect(text).toContain('auth');
  487. expect(text).toContain('http');
  488. });
  489. it('TrailBar and SearchPalette mount and read through the same adapter', async () => {
  490. const { adapter } = mockAdapter();
  491. setGraphAdapter(adapter);
  492. trail.push({ id: SYMBOL.node.id, name: 'parseToken', kind: 'function', dir: 'start' });
  493. await render(TrailBar, {});
  494. expect(host.textContent ?? '').toContain('parseToken');
  495. void unmount(mounted as Record<string, unknown>);
  496. mounted = null;
  497. host.innerHTML = '';
  498. await render(SearchPalette, {});
  499. expect(host.querySelector('input[role="combobox"]')).not.toBeNull();
  500. });
  501. it('CodegraphUi installs the adapter before its children ask for data', async () => {
  502. const { adapter, calls } = mockAdapter();
  503. // NOT installed by hand — the provider is the only thing that installs it.
  504. expect(getGraphAdapter()).not.toBe(adapter);
  505. mounted = mount(CodegraphUi, { target: host, props: { adapter } }) as Record<string, unknown>;
  506. flushSync();
  507. expect(getGraphAdapter()).toBe(adapter);
  508. expect(calls).toEqual([]);
  509. });
  510. });
  511. describe('@colbymchenry/codegraph-ui — the seams', () => {
  512. it('a host navigation driver replaces every href the components build', () => {
  513. const seen: string[] = [];
  514. const driver: NavigationDriver = {
  515. symbolHref: (id) => `/review/42/symbol/${encodeURIComponent(id)}`,
  516. fileHref: (path) => `/review/42/file/${path}`,
  517. mapHref: () => '/review/42/map',
  518. flowHref: () => '/review/42/flow',
  519. entryHref: () => '/review/42',
  520. navigate: (href) => seen.push(href),
  521. back: () => seen.push('back'),
  522. };
  523. setNavigationDriver(driver);
  524. expect(symbolHref('function:x')).toBe('/review/42/symbol/function%3Ax');
  525. expect(fileHref('src/a.ts')).toBe('/review/42/file/src/a.ts');
  526. expect(mapHref()).toBe('/review/42/map');
  527. expect(flowHref()).toBe('/review/42/flow');
  528. setNavigationDriver(null);
  529. // Back to the viewer's own address space, unchanged.
  530. expect(symbolHref('function:x')).toBe(hashNavigation.symbolHref('function:x'));
  531. expect(symbolHref('function:x')).toBe('#/s/function%3Ax');
  532. });
  533. it('the default adapter is the loopback JSON API and asks for `api/...`', async () => {
  534. const asked: string[] = [];
  535. const adapter = createHttpAdapter({
  536. fetch: async (input) => {
  537. asked.push(String(input));
  538. return new Response(JSON.stringify(STATS), {
  539. status: 200,
  540. headers: { 'content-type': 'application/json' },
  541. });
  542. },
  543. });
  544. await adapter.stats();
  545. await adapter.node('function:parse@a.ts:1');
  546. await adapter.source({ file: 'src/a.ts', from: 1, to: 4 });
  547. await adapter.nodes(['a', 'b']);
  548. expect(asked[0]).toBe('api/stats');
  549. // Ids are encoded per slash-separated segment, so ':' survives and '/' is
  550. // still a path separator.
  551. expect(asked[1]).toBe('api/node/function%3Aparse%40a.ts%3A1');
  552. expect(asked[2]).toBe('api/source?file=src%2Fa.ts&from=1&to=4');
  553. // Repeated `id` params, never a comma-joined list.
  554. expect(asked[3]).toBe('api/nodes?id=a&id=b');
  555. });
  556. it('an adapter with no live channel never connects and never polls', () => {
  557. const { adapter } = mockAdapter();
  558. setGraphAdapter(adapter);
  559. expect(adapter.events).toBeUndefined();
  560. // `live.start()` is a no-op in a jsdom test that never called it; what is
  561. // asserted here is the counters a host can still drive by hand.
  562. const before = live.indexTick;
  563. live.signal('index', { index: { lastIndexedAt: 1, files: 3 } });
  564. expect(live.indexTick).toBe(before + 1);
  565. });
  566. });
  567. describe('@colbymchenry/codegraph-ui — the published shape', () => {
  568. const manifest = JSON.parse(
  569. readFileSync(join(ROOT, 'ui', 'package.json'), 'utf8')
  570. ) as Record<string, any>;
  571. it('is versioned with the engine', () => {
  572. const engine = JSON.parse(readFileSync(join(ROOT, 'package.json'), 'utf8')) as {
  573. version: string;
  574. };
  575. expect(manifest.version).toBe(engine.version);
  576. });
  577. it('is named, scoped and not publishable by accident', () => {
  578. expect(manifest.name).toBe('@colbymchenry/codegraph-ui');
  579. // The package is PREPARED, not published (CG-61). `private` is the guard:
  580. // npm refuses to publish it until the maintainer deliberately removes this.
  581. expect(manifest.private).toBe(true);
  582. });
  583. it('exports the entry, the theme and nothing else', () => {
  584. expect(Object.keys(manifest.exports).sort()).toEqual(['.', './package.json', './theme.css']);
  585. expect(manifest.exports['.'].svelte).toBe('./dist/index.js');
  586. expect(manifest.exports['.'].types).toBe('./dist/index.d.ts');
  587. });
  588. it('takes svelte as a peer, so a host never gets a second copy', () => {
  589. expect(manifest.peerDependencies.svelte).toBeDefined();
  590. expect(manifest.dependencies?.svelte).toBeUndefined();
  591. // The canvas library is a real dependency: the Map and the Flow strip are
  592. // unusable without it and a host must not have to know its version.
  593. expect(manifest.dependencies['@xyflow/svelte']).toBeDefined();
  594. });
  595. });