| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746 |
- /**
- * `@colbymchenry/codegraph-ui` — the package's own test (task CG-61).
- *
- * A minimal Svelte host mounts the three headline components from the package
- * entry against a MOCK adapter and asserts what lands in the document. That is
- * the whole promise of the package in one file: CodeGraph Pro renders these
- * same components over its own in-process engine reads, so if a screen can be
- * drawn from an object literal here, it can be drawn from a graph there.
- *
- * The import is `ui/src/index.ts` — the package entry itself, not the
- * components one by one — so a name dropped from the public surface fails here
- * rather than in the Pro app.
- *
- * Everything below is deliberately about the SEAM, not about the screens:
- * layout, geometry and the rails have their own suites (`ui-symbol-model`,
- * `ui-flow-model`, `ui-map-model`). What is being proved here is that no
- * component reaches past the adapter for anything.
- */
- import { readFileSync } from 'node:fs';
- import { join } from 'node:path';
- import { flushSync, mount, unmount } from 'svelte';
- import { afterEach, beforeAll, beforeEach, describe, expect, it } from 'vitest';
- import {
- ArchitectureMap,
- CodegraphUi,
- FlowStrip,
- SearchPalette,
- SymbolView,
- SavedTrails,
- TrailBar,
- TypeHierarchy,
- createHttpAdapter,
- fileHref,
- flowHref,
- getGraphAdapter,
- hashNavigation,
- live,
- mapHref,
- setGraphAdapter,
- setNavigationDriver,
- symbolHref,
- trail,
- type GraphAdapter,
- type NavigationDriver,
- type WireFlowPayload,
- type WireMapPayload,
- type WireNodeRef,
- type WireSource,
- type WireStats,
- type WireHierarchy,
- type WireSymbolPayload,
- } from '../ui/src/index';
- /* ---------------------------------------------------------------- fixtures */
- const ROOT = join(import.meta.dirname, '..');
- function nodeRef(overrides: Partial<WireNodeRef> = {}): WireNodeRef {
- return {
- id: 'function:parseToken@src/auth/token.ts:12',
- kind: 'function',
- name: 'parseToken',
- qualifiedName: 'parseToken',
- file: 'src/auth/token.ts',
- line: 12,
- endLine: 18,
- language: 'typescript',
- test: false,
- ...overrides,
- };
- }
- const CALLER = nodeRef({
- id: 'function:handleCallback@src/auth/callback.ts:40',
- name: 'handleCallback',
- qualifiedName: 'handleCallback',
- file: 'src/auth/callback.ts',
- line: 40,
- endLine: 60,
- });
- const CALLEE = nodeRef({
- id: 'function:decodeJwt@src/auth/jwt.ts:3',
- name: 'decodeJwt',
- qualifiedName: 'decodeJwt',
- file: 'src/auth/jwt.ts',
- line: 3,
- endLine: 9,
- });
- const SYMBOL: WireSymbolPayload = {
- node: {
- ...nodeRef(),
- startColumn: 0,
- endColumn: 1,
- lines: 7,
- exported: true,
- },
- ancestors: [nodeRef({ id: 'file:src/auth/token.ts', kind: 'file', name: 'token.ts' })],
- members: { total: 0, shown: 0, truncated: false, items: [] },
- incoming: {
- total: 1,
- shown: 1,
- truncated: false,
- items: [
- {
- node: CALLER,
- edgeKinds: ['calls'],
- edges: [{ kind: 'calls', line: 44, col: 6, confidence: 1 }],
- edgeCount: 1,
- lines: [44],
- confidence: 1,
- uncertain: false,
- synthesized: false,
- },
- ],
- },
- outgoing: {
- total: 1,
- shown: 1,
- truncated: false,
- items: [
- {
- node: CALLEE,
- edgeKinds: ['calls'],
- edges: [{ kind: 'calls', line: 14, col: 10, confidence: 1 }],
- edgeCount: 1,
- lines: [14],
- confidence: 1,
- uncertain: false,
- synthesized: false,
- },
- ],
- },
- typesUsed: [],
- hierarchy: null,
- counts: { callers: 1, callees: 1, typesUsed: 0, fanIn: 1, fanOut: 1, members: 0, hub: false },
- tests: { reached: false, hops: null, fileCount: 0, files: [], exhaustive: true, hopsSearched: 3 },
- outsideIndex: { total: 0, byKind: {}, samples: [] },
- blast: {
- direct: 1,
- withinHops: 2,
- hops: 3,
- files: 2,
- testFiles: 0,
- routes: 0,
- topFiles: [{ file: 'src/auth/callback.ts', symbols: 1, test: false }],
- },
- drift: false,
- };
- const SOURCE_LINES = [
- 'export function parseToken(raw: string): Token {',
- ' // Normalize expiry before anything else reads it.',
- ' const claims = decodeJwt(raw);',
- ' return { ...claims, expiresAt: claims.exp * 1000 };',
- '}',
- ];
- const SOURCE: WireSource = {
- file: 'src/auth/token.ts',
- language: 'typescript',
- drift: false,
- showing: 'indexed',
- contentHash: 'abc123',
- indexedAt: 1_700_000_000_000,
- generated: false,
- totalLines: 40,
- from: 12,
- to: 18,
- lines: SOURCE_LINES,
- };
- const FLOW: WireFlowPayload = {
- query: { kind: 'directed', from: 'handleCallback', to: 'decodeJwt', symbols: [] },
- flows: [
- {
- id: 'flow-1',
- label: 'handleCallback → decodeJwt',
- partial: false,
- boundary: null,
- hops: [
- {
- node: CALLER,
- edge: null,
- callRef: { line: 44, col: 6, name: 'parseToken', targetId: SYMBOL.node.id, backwards: false },
- source: {
- file: 'src/auth/callback.ts',
- language: 'typescript',
- from: 44,
- to: 46,
- lines: [' const token = parseToken(raw);'],
- drift: false,
- },
- },
- {
- node: nodeRef(),
- edge: {
- kind: 'calls',
- line: 44,
- label: 'calls',
- upward: false,
- uncertain: false,
- synthesized: false,
- },
- callRef: null,
- source: {
- file: 'src/auth/token.ts',
- language: 'typescript',
- from: 12,
- to: 14,
- lines: SOURCE_LINES.slice(0, 3),
- drift: false,
- },
- },
- ],
- },
- ],
- ambiguous: [],
- unresolved: [],
- reason: null,
- index: { lastIndexedAt: 1_700_000_000_000, edges: 4, files: 3 },
- timing: { elapsedMs: 2 },
- };
- const MAP: WireMapPayload = {
- root: 'src',
- depth: 1,
- roots: [{ root: 'src', label: 'src', files: 3 }],
- modules: [
- {
- id: 'src/auth',
- label: 'auth',
- files: 2,
- symbols: 6,
- languages: [{ language: 'typescript', files: 2 }],
- test: false,
- facade: false,
- fileList: { total: 2, shown: 2, truncated: false, items: ['src/auth/token.ts', 'src/auth/callback.ts'] },
- },
- {
- id: 'src/http',
- label: 'http',
- files: 1,
- symbols: 3,
- languages: [{ language: 'typescript', files: 1 }],
- test: false,
- facade: false,
- fileList: { total: 1, shown: 1, truncated: false, items: ['src/http/server.ts'] },
- },
- ],
- links: [
- {
- source: 'src/http',
- target: 'src/auth',
- count: 9,
- declared: 7,
- byKind: [{ kind: 'calls', count: 9 }],
- topPairs: [{ from: 'src/http/server.ts', to: 'src/auth/token.ts', count: 9, declared: 7 }],
- },
- ],
- cycles: { total: 0, shown: 0, truncated: false, items: [] },
- excluded: { uncertainEdges: 0, confidenceBelow: 0.6 },
- index: { lastIndexedAt: 1_700_000_000_000, edges: 9, files: 3 },
- timing: { elapsedMs: 1, cached: false },
- };
- const STATS: WireStats = {
- project: { root: '/tmp/demo', name: 'demo' },
- index: {
- state: 'ready',
- lastIndexedAt: 1_700_000_000_000,
- stale: false,
- version: '1.0.0',
- extractionVersion: 1,
- backend: 'node-sqlite',
- journalMode: 'wal',
- pendingReferences: 0,
- generatedFiles: 0,
- watching: false,
- watcherDegraded: false,
- },
- graph: {
- nodes: 9,
- edges: 9,
- files: 3,
- nodesByKind: { function: 9 },
- edgesByKind: { calls: 9 },
- filesByLanguage: { typescript: 3 },
- dbSizeBytes: 1024,
- walSizeBytes: 0,
- },
- frameworks: [],
- thresholds: { hub: 40, uncertainBelow: 0.6 },
- blastScale: { maxDirect: 20, maxWithinHops: 60, hops: 3, sampled: 24, estimated: true },
- };
- /* ------------------------------------------------------------ mock adapter */
- /** Every method the components can reach, and a record of which ones they did. */
- function mockAdapter(): { adapter: GraphAdapter; calls: string[] } {
- const calls: string[] = [];
- const seen = <T>(name: string, value: T): Promise<T> => {
- calls.push(name);
- return Promise.resolve(value);
- };
- const adapter: GraphAdapter = {
- stats: () => seen('stats', STATS),
- search: () =>
- seen('search', {
- query: '',
- text: '',
- filters: { kinds: [], languages: [], paths: [], names: [] },
- results: { total: 0, shown: 0, truncated: false, items: [] },
- groups: [],
- }),
- node: (id) => {
- calls.push(`node:${id}`);
- return Promise.resolve(SYMBOL);
- },
- nodes: () => seen('nodes', { items: [], missing: [] }),
- source: (request) => {
- calls.push(`source:${request.file}`);
- return Promise.resolve(SOURCE);
- },
- file: () =>
- seen('file', {
- file: {
- path: 'src/auth/token.ts',
- language: 'typescript',
- size: 900,
- modifiedAt: 0,
- indexedAt: 0,
- contentHash: 'abc123',
- nodeCount: 3,
- generated: false,
- test: false,
- errors: [],
- id: 'file:src/auth/token.ts',
- },
- topLevel: { calls: 0 },
- drift: false,
- outline: { total: 0, shown: 0, truncated: false, items: [] },
- imports: { total: 0, shown: 0, truncated: false, items: [] },
- importedBy: { total: 0, shown: 0, truncated: false, items: [] },
- unresolvedImports: [],
- dependencies: [],
- dependents: [],
- }),
- fileCode: () =>
- seen('fileCode', {
- file: {
- path: 'src/auth/token.ts',
- language: 'typescript',
- size: 900,
- indexedAt: 0,
- contentHash: 'abc123',
- generated: false,
- test: false,
- errors: [],
- id: 'file:src/auth/token.ts',
- totalLines: 40,
- },
- drift: false,
- outline: { total: 0, shown: 0, truncated: false, items: [] },
- calls: { total: 0, shown: 0, truncated: false, items: [] },
- outside: { total: 0, shown: 0, truncated: false, items: [] },
- intraFileCalls: 0,
- timing: { elapsedMs: 1 },
- }),
- flow: () => seen('flow', FLOW),
- map: () => seen('map', MAP),
- routes: () =>
- seen('routes', {
- routed: false,
- routeCount: 0,
- shown: 0,
- truncated: false,
- topHandlerFile: null,
- topHandlerFileCount: 0,
- entries: [],
- }),
- entryPoints: () =>
- seen('entryPoints', {
- frameworks: [],
- routes: { routed: false, routeCount: 0, items: { total: 0, shown: 0, truncated: false, items: [] } },
- files: { total: 0, shown: 0, truncated: false, items: [] },
- tests: { total: 0, shown: 0, truncated: false, items: [] },
- hubs: { total: 0, shown: 0, truncated: false, items: [] },
- index: { lastIndexedAt: null, files: 3 },
- timing: { elapsedMs: 1, cached: false },
- }),
- deadCode: () =>
- seen('deadCode', {
- rows: { total: 0, shown: 0, truncated: false, items: [] },
- groups: [],
- candidates: 0,
- excluded: [],
- excludedTotal: 0,
- kinds: ['function'],
- includeExported: false,
- includeTests: false,
- includeGenerated: false,
- bounded: false,
- corroborated: true,
- timing: { elapsedMs: 1 },
- }),
- trails: () =>
- seen('trails', {
- trails: [],
- // A host with nowhere to keep trails still ANSWERS the question — it
- // says it is read-only rather than omitting the method, so the screens
- // show the section explained instead of showing a Save that does
- // nothing.
- readOnly: true,
- readOnlyReason: 'This host does not store trails.',
- directory: '.codegraph/ui/trails',
- skipped: 0,
- bounded: false,
- }),
- // Deliberately no `events`, `saveTrail` or `deleteTrail`: a host without a
- // live channel and without anywhere to write is the normal case, and
- // nothing may poll or offer to save in their absence.
- };
- return { adapter, calls };
- }
- /* ----------------------------------------------------------------- harness */
- let host: HTMLDivElement;
- let mounted: Record<string, unknown> | null = null;
- /** jsdom has none of the observers a canvas library expects. */
- beforeAll(() => {
- class NoopObserver {
- observe(): void {}
- unobserve(): void {}
- disconnect(): void {}
- }
- const globals = globalThis as Record<string, unknown>;
- globals.ResizeObserver ??= NoopObserver;
- globals.IntersectionObserver ??= NoopObserver;
- globals.MutationObserver ??= NoopObserver;
- globals.requestAnimationFrame ??= (fn: FrameRequestCallback) =>
- setTimeout(() => fn(0), 0) as unknown as number;
- globals.cancelAnimationFrame ??= (handle: number) => clearTimeout(handle);
- // jsdom's own `matchMedia` is a stub that is not callable here, and Svelte's
- // `MediaQuery` (which `@xyflow/svelte`'s store constructs eagerly) calls it
- // the moment a canvas mounts. Replace it outright rather than guarding.
- const media = (query: string) => ({
- media: query,
- matches: false,
- onchange: null,
- addEventListener() {},
- removeEventListener() {},
- addListener() {},
- removeListener() {},
- dispatchEvent: () => false,
- });
- Object.defineProperty(window, 'matchMedia', { configurable: true, writable: true, value: media });
- globals.matchMedia = media;
- if (!Element.prototype.scrollIntoView) Element.prototype.scrollIntoView = () => {};
- });
- beforeEach(() => {
- host = document.createElement('div');
- document.body.appendChild(host);
- trail.clear();
- });
- afterEach(() => {
- if (mounted) {
- void unmount(mounted);
- mounted = null;
- }
- host.remove();
- setGraphAdapter(null);
- setNavigationDriver(null);
- });
- /**
- * Mount a component and let its data effects settle.
- *
- * Every screen fetches inside an `$effect`, so a render is not finished until
- * the promise the adapter returned has resolved and the follow-up render has
- * flushed. Two macrotask turns cover the deepest chain any of them has (the
- * Symbol view: node, then its source).
- */
- async function render(
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- component: any,
- props: Record<string, unknown>
- ): Promise<void> {
- mounted = mount(component, { target: host, props }) as Record<string, unknown>;
- for (let turn = 0; turn < 4; turn += 1) {
- await new Promise((resolve) => setTimeout(resolve, 0));
- flushSync();
- }
- }
- describe('@colbymchenry/codegraph-ui — a host renders the package', () => {
- it('SymbolView draws callers, source and the callee rail from a mock adapter', async () => {
- const { adapter, calls } = mockAdapter();
- setGraphAdapter(adapter);
- await render(SymbolView, { id: SYMBOL.node.id, line: null });
- // It asked the adapter, by id, and it asked for the symbol's own slice.
- expect(calls).toContain(`node:${SYMBOL.node.id}`);
- expect(calls).toContain('source:src/auth/token.ts');
- const text = host.textContent ?? '';
- expect(text).toContain('parseToken');
- // The caller rail (left) and the callee rail (right) are both drawn.
- expect(text).toContain('handleCallback');
- expect(text).toContain('decodeJwt');
- // The verbatim source, not a summary of it.
- expect(text).toContain('expiresAt');
- // The honesty badge: nothing in the fixture's graph tests this symbol.
- expect(text.toLowerCase()).toContain('test');
- });
- it('TypeHierarchy draws the fan, its wiring and its fold from a payload alone', async () => {
- const implementers = Array.from({ length: 14 }, (_, i) => ({
- id: `impl-${i}`,
- kind: 'class' as const,
- name: `Target${i}`,
- qualifiedName: `Target${i}`,
- file: `src/targets/target-${i}.ts`,
- line: 1,
- endLine: 9,
- language: 'typescript' as const,
- test: false,
- depth: 1,
- parentId: SYMBOL.node.id,
- relation: 'implements' as const,
- // The first one arrived through a resolver rather than a parse, which is
- // the case the block has to draw differently.
- synthesized: i === 0,
- ...(i === 0 ? { via: 'go-implements', registeredAt: 'src/clock.go:11' } : {}),
- hiddenSubtypes: 0,
- }));
- const hierarchy: WireHierarchy = {
- ancestors: { total: 0, shown: 0, truncated: false, items: [] },
- descendants: {
- total: implementers.length,
- shown: implementers.length,
- truncated: false,
- items: implementers,
- },
- direct: implementers.length,
- implementers: implementers.length,
- bounded: false,
- polymorphic: true,
- };
- await render(TypeHierarchy, { hierarchy, focus: SYMBOL.node, onopen: () => {} });
- const text = host.textContent ?? '';
- // The claim a reader cannot get by counting rows.
- expect(text).toContain('14 implementations');
- // The wiring site of the synthesized edge.
- expect(text).toContain('go-implements');
- // Twelve rows, then the fold — never a silent truncation.
- expect(text).toContain('+2 more implementations');
- expect(text).toContain('Target0');
- expect(text).not.toContain('Target13');
- // It draws no network of its own: this component was handed a payload.
- expect(host.querySelectorAll('path').length).toBe(12);
- });
- it('FlowStrip draws one card per hop from a mock adapter', async () => {
- const { adapter, calls } = mockAdapter();
- setGraphAdapter(adapter);
- await render(FlowStrip, {
- from: 'handleCallback',
- to: 'decodeJwt',
- symbols: null,
- trailParam: null,
- });
- expect(calls).toContain('flow');
- const text = host.textContent ?? '';
- expect(text).toContain('handleCallback');
- expect(text).toContain('parseToken');
- });
- it('ArchitectureMap draws modules and their dependency from a mock adapter', async () => {
- const { adapter, calls } = mockAdapter();
- setGraphAdapter(adapter);
- await render(ArchitectureMap, { root: 'src', depth: 1, tests: false });
- expect(calls).toContain('map');
- const text = host.textContent ?? '';
- expect(text).toContain('auth');
- expect(text).toContain('http');
- });
- it('TrailBar and SearchPalette mount and read through the same adapter', async () => {
- const { adapter } = mockAdapter();
- setGraphAdapter(adapter);
- trail.push({ id: SYMBOL.node.id, name: 'parseToken', kind: 'function', dir: 'start' });
- await render(TrailBar, {});
- expect(host.textContent ?? '').toContain('parseToken');
- void unmount(mounted as Record<string, unknown>);
- mounted = null;
- host.innerHTML = '';
- await render(SearchPalette, {});
- expect(host.querySelector('input[role="combobox"]')).not.toBeNull();
- });
- it('offers no Save when the adapter cannot write, and says why in the list', async () => {
- const { adapter } = mockAdapter();
- setGraphAdapter(adapter);
- trail.push({ id: SYMBOL.node.id, name: 'parseToken', kind: 'function', dir: 'start' });
- await render(TrailBar, {});
- // The one screen affordance that must never appear against a read-only
- // host: an adapter with no `saveTrail` has no button, not a button that
- // fails.
- expect(host.textContent ?? '').not.toContain('Save trail');
- void unmount(mounted as Record<string, unknown>);
- mounted = null;
- host.innerHTML = '';
- await render(SavedTrails, { hideWhenEmpty: false });
- const text = host.textContent ?? '';
- expect(text).toContain('Saved trails');
- expect(text).toContain('This host does not store trails.');
- });
- it('CodegraphUi installs the adapter before its children ask for data', async () => {
- const { adapter, calls } = mockAdapter();
- // NOT installed by hand — the provider is the only thing that installs it.
- expect(getGraphAdapter()).not.toBe(adapter);
- mounted = mount(CodegraphUi, { target: host, props: { adapter } }) as Record<string, unknown>;
- flushSync();
- expect(getGraphAdapter()).toBe(adapter);
- expect(calls).toEqual([]);
- });
- });
- describe('@colbymchenry/codegraph-ui — the seams', () => {
- it('a host navigation driver replaces every href the components build', () => {
- const seen: string[] = [];
- const driver: NavigationDriver = {
- symbolHref: (id) => `/review/42/symbol/${encodeURIComponent(id)}`,
- fileHref: (path) => `/review/42/file/${path}`,
- mapHref: () => '/review/42/map',
- flowHref: () => '/review/42/flow',
- entryHref: () => '/review/42',
- navigate: (href) => seen.push(href),
- back: () => seen.push('back'),
- };
- setNavigationDriver(driver);
- expect(symbolHref('function:x')).toBe('/review/42/symbol/function%3Ax');
- expect(fileHref('src/a.ts')).toBe('/review/42/file/src/a.ts');
- expect(mapHref()).toBe('/review/42/map');
- expect(flowHref()).toBe('/review/42/flow');
- setNavigationDriver(null);
- // Back to the viewer's own address space, unchanged.
- expect(symbolHref('function:x')).toBe(hashNavigation.symbolHref('function:x'));
- expect(symbolHref('function:x')).toBe('#/s/function%3Ax');
- });
- it('the default adapter is the loopback JSON API and asks for `api/...`', async () => {
- const asked: string[] = [];
- const adapter = createHttpAdapter({
- fetch: async (input) => {
- asked.push(String(input));
- return new Response(JSON.stringify(STATS), {
- status: 200,
- headers: { 'content-type': 'application/json' },
- });
- },
- });
- await adapter.stats();
- await adapter.node('function:parse@a.ts:1');
- await adapter.source({ file: 'src/a.ts', from: 1, to: 4 });
- await adapter.nodes(['a', 'b']);
- expect(asked[0]).toBe('api/stats');
- // Ids are encoded per slash-separated segment, so ':' survives and '/' is
- // still a path separator.
- expect(asked[1]).toBe('api/node/function%3Aparse%40a.ts%3A1');
- expect(asked[2]).toBe('api/source?file=src%2Fa.ts&from=1&to=4');
- // Repeated `id` params, never a comma-joined list.
- expect(asked[3]).toBe('api/nodes?id=a&id=b');
- });
- it('an adapter with no live channel never connects and never polls', () => {
- const { adapter } = mockAdapter();
- setGraphAdapter(adapter);
- expect(adapter.events).toBeUndefined();
- // `live.start()` is a no-op in a jsdom test that never called it; what is
- // asserted here is the counters a host can still drive by hand.
- const before = live.indexTick;
- live.signal('index', { index: { lastIndexedAt: 1, files: 3 } });
- expect(live.indexTick).toBe(before + 1);
- });
- });
- describe('@colbymchenry/codegraph-ui — the published shape', () => {
- const manifest = JSON.parse(
- readFileSync(join(ROOT, 'ui', 'package.json'), 'utf8')
- ) as Record<string, any>;
- it('is versioned with the engine', () => {
- const engine = JSON.parse(readFileSync(join(ROOT, 'package.json'), 'utf8')) as {
- version: string;
- };
- expect(manifest.version).toBe(engine.version);
- });
- it('is named, scoped and not publishable by accident', () => {
- expect(manifest.name).toBe('@colbymchenry/codegraph-ui');
- // The package is PREPARED, not published (CG-61). `private` is the guard:
- // npm refuses to publish it until the maintainer deliberately removes this.
- expect(manifest.private).toBe(true);
- });
- it('exports the entry, the theme and nothing else', () => {
- expect(Object.keys(manifest.exports).sort()).toEqual(['.', './package.json', './theme.css']);
- expect(manifest.exports['.'].svelte).toBe('./dist/index.js');
- expect(manifest.exports['.'].types).toBe('./dist/index.d.ts');
- });
- it('takes svelte as a peer, so a host never gets a second copy', () => {
- expect(manifest.peerDependencies.svelte).toBeDefined();
- expect(manifest.dependencies?.svelte).toBeUndefined();
- // The canvas library is a real dependency: the Map and the Flow strip are
- // unusable without it and a host must not have to know its version.
- expect(manifest.dependencies['@xyflow/svelte']).toBeDefined();
- });
- });
|