struct graph

This commit is contained in:
2026-09-07 15:52:46 -04:00
parent 30ed3351b5
commit 93521c53fb
15 changed files with 1061 additions and 30 deletions

View File

@@ -25,6 +25,7 @@ async function mockDesktop(page: Page) {
case 'remember_recent': return null;
case 'remove_recent': Object.assign(window, { recentProjects: [] }); return null;
case 'inspect_workspace': if ((window as unknown as { missingProject?: boolean }).missingProject) throw new Error('Source directory does not exist.'); return { root: '/sample', settings: state, warning: null, can_reset: true };
case 'close_workspace': return null;
case 'open_workspace': return { root: '/sample', settings: { ...state, build_directory: args.build, last_file: args.reset ? null : state.last_file } };
case 'list_directory': return args.path ? [{ name: 'concepts.hpp', path: 'include/concepts.hpp', directory: false }] : [{ name: 'include', path: 'include', directory: true }, { name: 'main.cpp', path: 'main.cpp', directory: false }];
case 'read_file': return { path: args.path, content };
@@ -38,6 +39,7 @@ async function mockDesktop(page: Page) {
case 'graph_request':
case 'analysis_request':
if (command === 'graph_request' || args.kind === 'conceptgraph') {
if ((window as unknown as {slowGraph?:boolean}).slowGraph) await new Promise(resolve => setTimeout(resolve, 900));
if ((window as unknown as { failGraph?: boolean }).failGraph) throw new Error('Select a concept or function name to view its graph.');
const location = (line: number, label: string) => ({ path: 'main.cpp', range: { start: { line, character: 8 }, end: { line, character: 14 } }, label });
const graph = { title: 'Number', nodes: [
@@ -45,7 +47,15 @@ async function mockDesktop(page: Page) {
{ id: 'requirement', label: 'requires(T value) { value + value; }', kind: 'requires', detail: 'requires(T value) { value + value; }', location: location(3, 'requires') },
{ id: 'concept', label: 'Base<T>', kind: 'concept', detail: 'Base<T>', location: location(2, 'Base') },
{ id: 'use', label: 'main', kind: 'usage', detail: 'used by main.cpp:8', location: location(7, 'main') },
], edges: [{ from: 'concept', to: 'requirement', label: '' }, { from: 'requirement', to: 'selected', label: '' }, { from: 'selected', to: 'use', label: 'used by' }], warnings: ['Active build only.'] }; return command === 'graph_request' ? graph : {graph};
], edges: [{ from: 'concept', to: 'requirement', label: '' }, { from: 'requirement', to: 'selected', label: '' }, { from: 'selected', to: 'use', label: 'used by' }], warnings: ['Active build only.'] };
if ((window as unknown as {functionGraph?:boolean}).functionGraph) { graph.title='target'; graph.nodes[0].label='target'; graph.nodes[1].kind='function'; graph.nodes[1].label='helper'; graph.nodes[2].kind='function'; graph.nodes[2].label='leaf'; graph.nodes[3].kind='function'; graph.nodes[3].label='caller'; }
if ((window as unknown as {typeGraph?:boolean}).typeGraph) {
graph.title='demo::Box'; graph.nodes[0].label='demo::Box';
graph.nodes[1].kind='template-parameter'; graph.nodes[1].label='template T';
graph.nodes[2].kind='type'; graph.nodes[2].label='demo::Base';
graph.nodes[3].kind='function'; graph.nodes[3].label='demo::Box::run';
}
return command === 'graph_request' ? graph : {graph};
}
return {
locations: (window as unknown as { multipleLocations?: boolean }).multipleLocations && ['callers', 'references'].includes(args.kind as string) ? [{ path: 'first.cpp', range: { start: { line: 3, character: 8 }, end: { line: 3, character: 14 } }, label: 'first' }, { path: 'second.cpp', range: { start: { line: 7, character: 4 }, end: { line: 7, character: 8 } }, label: 'second' }] : args.kind === 'folding' || args.kind === 'hover' ? [] : [{ path: args.kind === 'definition' ? 'lib.cpp' : 'main.cpp', range: { start: { line: 3, character: 8 }, end: { line: 3, character: 14 } }, label: args.kind === 'callers' ? 'main' : '' }],
@@ -437,3 +447,105 @@ test('graph options are separate from context and navigation supports history an
await page.locator('#graph-fit').click();
await page.screenshot({path:'test-results/graph-navigation.png'});
});
test('function graphs navigate callees above and callers below using nodes and keyboard', async ({page}) => {
await mockDesktop(page); await page.addInitScript(() => Object.assign(window,{functionGraph:true}));
await openProject(page); await page.locator('#nav-graph').click();
await expect(page.locator('#graph-title')).toHaveText('target');
const root = page.getByRole('button',{name:'selected: target',exact:true});
const caller = page.getByRole('button',{name:'function: caller',exact:true});
const helper = page.getByRole('button',{name:'function: helper',exact:true});
expect((await helper.boundingBox())!.y).toBeLessThan((await root.boundingBox())!.y);
expect((await caller.boundingBox())!.y).toBeGreaterThan((await root.boundingBox())!.y);
await root.focus(); await root.press('ArrowUp');
await expect(helper).toBeFocused();
await page.getByRole('button',{name:'Focus this symbol',exact:true}).click();
await expect(page.locator('#graph-back')).toBeEnabled();
});
test('graph building indicator covers success and failure and clears on return to welcome', async ({page}) => {
await mockDesktop(page); await page.addInitScript(() => Object.assign(window,{slowGraph:true}));
await openProject(page); await page.locator('#nav-graph').click();
await expect(page.locator('#graph-progress')).toBeVisible();
await expect(page.locator('#graph-canvas')).toHaveAttribute('aria-busy','true');
await expect(page.locator('#graph-progress')).toBeHidden();
await page.evaluate(() => Object.assign(window,{failGraph:true}));
await page.locator('#graph-refresh').click();
await expect(page.locator('#graph-progress')).toBeVisible();
await expect(page.locator('#graph-status')).toContainText('Select a concept or function');
await expect(page.locator('#graph-progress')).toBeHidden();
await page.evaluate(() => Object.assign(window,{failGraph:false}));
await page.locator('#graph-refresh').click();
await expect(page.locator('#graph-progress')).toBeVisible();
await page.locator('#file-menu > summary').click();
await page.getByRole('button',{name:'Return to Welcome',exact:true}).click();
await expect(page.locator('#landing')).toBeVisible();
await expect(page.locator('#graph-progress')).toBeHidden();
await expect(page.locator('#explanation-pane')).toBeHidden();
});
test('File menu opens fresh projects, recent projects, and returns to Welcome', async ({page}) => {
await mockDesktop(page);
await page.addInitScript(() => Object.assign(window,{recentProjects:[{root:'/sample',last_opened:1700000000}]}));
await openProject(page);
await page.locator('#file-menu > summary').click();
await page.getByRole('button',{name:'Open Project…',exact:true}).click();
await expect(page.locator('#source')).toHaveValue('');
await expect(page.locator('#build')).toHaveValue('');
await page.locator('#setup-cancel').click();
await expect(page.locator('#file-path')).toHaveText('main.cpp');
await page.locator('#file-menu > summary').click();
await page.locator('#file-recents > summary').click();
await expect(page.locator('#file-recent-list button')).toHaveCount(1);
await page.locator('#file-recent-list button').click();
await expect(page.locator('#setup')).toBeHidden();
await expect(page.locator('#file-menu')).not.toHaveAttribute('open','');
await page.locator('#file-menu > summary').click();
await page.getByRole('button',{name:'Return to Welcome',exact:true}).click();
await expect(page.locator('#landing')).toBeVisible();
await expect(page.locator('#project-name')).toHaveText('No project open');
await expect(page.locator('#toggle-sidebar')).toBeDisabled();
expect(await page.evaluate(() => (window as unknown as {calls:{command:string;args:unknown}[]}).calls.filter(c=>c.command==='close_workspace').at(-1)?.args)).toEqual({root:'/sample'});
await page.locator('.recent-project').click();
await expect(page.locator('#landing')).toBeHidden();
await expect(page.locator('#file-path')).toHaveText('main.cpp');
});
test('both sidebars resize with pointer and keyboard and keep widths across collapse', async ({page}) => {
await mockDesktop(page); await openProject(page); await page.locator('#nav-graph').click();
const left=page.getByRole('separator',{name:'Resize left sidebar'}), right=page.getByRole('separator',{name:'Resize right sidebar'});
const initialLeft=(await page.locator('main > aside').boundingBox())!.width;
const handle=(await left.boundingBox())!;
await page.mouse.move(handle.x+4,handle.y+150);await page.mouse.down();await page.mouse.move(handle.x+64,handle.y+150);await page.mouse.up();
await expect.poll(async()=>(await page.locator('main > aside').boundingBox())!.width).toBeGreaterThan(initialLeft+45);
const initialRight=(await page.locator('#explanation-pane').boundingBox())!.width;
const r=(await right.boundingBox())!;
await page.mouse.move(r.x+4,r.y+150);await page.mouse.down();await page.mouse.move(r.x-46,r.y+150);await page.mouse.up();
await expect.poll(async()=>(await page.locator('#explanation-pane').boundingBox())!.width).toBeGreaterThan(initialRight+35);
await right.focus();await right.press('ArrowLeft');
const width=(await page.locator('#explanation-pane').boundingBox())!.width;
await page.getByRole('button',{name:'Collapse right sidebar'}).click();await page.locator('#toggle-sidebar').click();
expect((await page.locator('#explanation-pane').boundingBox())!.width).toBeCloseTo(width,0);
await page.locator('#graph-expand').click();
expect((await page.locator('#explanation-pane').boundingBox())!.width).toBeGreaterThan(width+100);
await page.locator('#graph-expand').click();
expect((await page.locator('#explanation-pane').boundingBox())!.width).toBeCloseTo(width,0);
await page.setViewportSize({width:950,height:850});
await expect(page.locator('#explanation-pane')).toHaveCSS('position','fixed');
await right.focus();await right.press('ArrowRight');
expect((await page.locator('#explanation-pane').boundingBox())!.width).toBeLessThan(width);
});
test('type graph nodes can be refocused and member nodes remain navigable', async ({page}) => {
await mockDesktop(page);await page.addInitScript(()=>Object.assign(window,{typeGraph:true}));
await openProject(page);await page.locator('#nav-graph').click();
await expect(page.locator('#graph-title')).toHaveText('demo::Box');
await page.getByRole('button',{name:'type: demo::Base',exact:true}).click();
await page.getByRole('button',{name:'Focus this symbol',exact:true}).click();
await expect(page.locator('#graph-back')).toBeEnabled();
await page.getByRole('button',{name:'function: demo::Box::run',exact:true}).click();
await expect(page.getByRole('button',{name:'Open source',exact:true})).toBeVisible();
});