Files
codegraph/__tests__/ui-repeated-keys.test.ts
Colby Mchenry 81e08b76ed fix(viewer): pages that went blank — repeated ids, and Steps on a function full of checks (#2264)
* fix(viewer): a repeated id no longer blanks Entry points or a component's Symbol view

The viewer keys its lists by id and a repeat stops the page drawing. The
routes list was a row per (route, edge), so an inline handler came back once
per call in its body; it is now one row per route — its bound handler, or
the route itself as an inline handler. A Vue/Svelte/Astro component's
members listed its script's symbols twice, plus the component and its file;
each member is now listed once.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(viewer): Steps reads a function full of checks in one pass

The in-order reading kept every way forward through an `if` with no
`else` twice — under its condition and without — so eighty checks that
draw nothing (jsoup's `parse`) made 2^80 ways and the page died on a
stack overflow. Ways waiting at the same place are merged, keeping the
conditions they all share.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 20:23:31 +00:00

98 lines
3.6 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* The viewer draws its lists keyed by id, and a repeated id stops the page
* from drawing at all — Entry points on hono, the Symbol view of every Vue /
* Svelte / Astro component. Two payloads repeated themselves:
*
* - the routes list was a row per (route, edge), so an inline handler came
* back once per call in its body (`GET /stream/text` as `streamText`,
* `writeln` and `sleep`). A route is one row: its bound handler, or the
* route itself as an inline handler;
* - a component's members listed its script's symbols twice, and the
* component and its file among them.
*/
import { describe, it, expect, afterAll, beforeAll } from 'vitest';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { CodeGraph } from '../src';
import { buildRoutes } from '../src/ui-server/api/routes';
import { buildNode } from '../src/ui-server/api/node';
let root = '';
let cg: CodeGraph;
beforeAll(async () => {
root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-ui-repeated-'));
const files: Record<string, string> = {
'package.json': JSON.stringify({ name: 'app', private: true, dependencies: { express: '^4.0.0', astro: '^4.0.0' } }),
'server/app.js': `const express = require('express');
const app = express();
function loadUser(id) { return { id }; }
function audit(id) { return id; }
function render(user) { return user; }
function listUsers(req, res) { res.json([]); }
function showUser(req, res) { res.json(loadUser(req.params.id)); }
app.get('/users/:id/card', async (req, res) => {
const user = loadUser(req.params.id);
audit(user.id);
res.send(render(user));
});
app.get('/users', listUsers);
app.get('/users/:id', showUser);
`,
'src/components/Card.astro': `---
import Badge from './Badge.astro';
import { format } from '../lib/format';
const title = format('card');
---
<div><Badge />{title}</div>
`,
'src/components/Badge.astro': `<span>badge</span>
`,
'src/lib/format.ts': `export function format(s: string): string {
return s.trim();
}
`,
};
for (const [rel, content] of Object.entries(files)) {
fs.mkdirSync(path.dirname(path.join(root, rel)), { recursive: true });
fs.writeFileSync(path.join(root, rel), content);
}
cg = await CodeGraph.init(root, { index: true });
});
afterAll(() => {
cg?.close();
if (root) fs.rmSync(root, { recursive: true, force: true });
});
describe('the routes list', () => {
it('has one row per route', () => {
const { entries } = buildRoutes(cg, new URLSearchParams());
const ids = entries.map((e) => e.routeId);
expect(new Set(ids).size).toBe(ids.length);
});
it('names an inline handler as one, and a bound handler by its name', () => {
const { entries } = buildRoutes(cg, new URLSearchParams());
const card = entries.find((e) => e.url === 'GET /users/:id/card')!;
expect(card).toMatchObject({ handler: 'inline handler', inline: true, handlerId: card.routeId });
expect(entries.find((e) => e.url === 'GET /users/:id')).toMatchObject({ handler: 'showUser', inline: false });
});
});
describe('a component’s members', () => {
it('lists each symbol once, and never the component or a file', async () => {
const component = cg.getNodesByKind('component').find((n) => n.name === 'Card')!;
const payload = (await buildNode(cg, root, component.id)) as { members: { items: Array<{ id: string; kind: string }> } };
const ids = payload.members.items.map((m) => m.id);
expect(ids.length).toBeGreaterThan(0);
expect(new Set(ids).size).toBe(ids.length);
expect(ids).not.toContain(component.id);
expect(payload.members.items.map((m) => m.kind)).not.toContain('file');
});
});