struct graph
This commit is contained in:
@@ -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();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user