feature: loading completed with teste

This commit is contained in:
2026-03-06 14:54:20 +01:00
parent 0af50e165a
commit 8bf3ab296d
2 changed files with 236 additions and 2 deletions

233
e2e/load.spec.ts Normal file
View File

@@ -0,0 +1,233 @@
import { test, expect, type Page } from '@playwright/test';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
// ── fixtures ──────────────────────────────────────────────────────────────────
const SIMPLE_GRAPH = {
id: 'main',
nodes: [
{ id: 'node-a', label: 'Alpha' },
{ id: 'node-b', label: 'Beta' },
],
edges: [{ id: 'edge-1', from: 'node-a', to: 'node-b' }],
};
const GRAPH_WITH_SUBGRAPH = {
id: 'main',
nodes: [
{
id: 'node-a',
label: 'Alpha',
subgraph: {
id: 'node-a',
nodes: [
{ id: 'sub-1', label: 'SubAlpha' },
{ id: 'sub-2', label: 'SubBeta' },
],
edges: [{ id: 'sub-edge-1', from: 'sub-1', to: 'sub-2' }],
},
},
{ id: 'node-b', label: 'Beta' },
],
edges: [{ id: 'edge-1', from: 'node-a', to: 'node-b' }],
};
function writeTempJson(data: object): string {
const name = `concept-sketch-test-${Date.now()}-${Math.random().toString(36).slice(2)}.json`;
const filePath = path.join(os.tmpdir(), name);
fs.writeFileSync(filePath, JSON.stringify(data));
return filePath;
}
// ── helpers ───────────────────────────────────────────────────────────────────
async function waitForGraph(page: Page) {
await page.waitForSelector('svg g.node', { timeout: 15000 });
}
function nodeByLabel(page: Page, label: string) {
return page.locator('g.node').filter({ hasText: label });
}
async function openSubgraph(page: Page, nodeLabel: string) {
await nodeByLabel(page, nodeLabel).click({ button: 'right' });
await expect(page.locator('.ant-dropdown:visible')).toBeVisible({ timeout: 3000 });
await page.waitForTimeout(300);
await page.locator('.ant-dropdown-menu-item').filter({ hasText: 'Subgraph' }).click();
await waitForGraph(page);
}
function loadButton(page: Page) {
return page.getByTitle('Load JSON');
}
// Triggers load and waits until a specific node label appears in the graph —
// that is the reliable signal that the full async load pipeline has finished.
async function loadFile(page: Page, filePath: string, waitForLabel: string) {
const [fileChooser] = await Promise.all([
page.waitForEvent('filechooser'),
loadButton(page).click(),
]);
await fileChooser.setFiles(filePath);
await expect(nodeByLabel(page, waitForLabel)).toBeVisible({ timeout: 10000 });
}
// ── tests ─────────────────────────────────────────────────────────────────────
test.describe('Graph loading', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/');
await waitForGraph(page);
});
// ── button presence ──────────────────────────────────────────────────────────
test('load button is visible in the header', async ({ page }) => {
await expect(loadButton(page)).toBeVisible();
});
test('clicking the load button opens a file chooser', async ({ page }) => {
const [fileChooser] = await Promise.all([
page.waitForEvent('filechooser'),
loadButton(page).click(),
]);
expect(fileChooser).toBeDefined();
});
// ── basic load ───────────────────────────────────────────────────────────────
test('loaded nodes and edges replace the default graph', async ({ page }) => {
const filePath = writeTempJson(SIMPLE_GRAPH);
await loadFile(page, filePath, 'Alpha');
await expect(nodeByLabel(page, 'Alpha')).toBeVisible();
await expect(nodeByLabel(page, 'Beta')).toBeVisible();
await expect(page.locator('g.node')).toHaveCount(2);
await expect(page.locator('g.edge')).toHaveCount(1);
});
test('loading removes nodes that existed before the load', async ({ page }) => {
await nodeByLabel(page, 'Start').click();
await expect(page.locator('g.node')).toHaveCount(3, { timeout: 5000 });
const filePath = writeTempJson(SIMPLE_GRAPH);
await loadFile(page, filePath, 'Alpha');
await expect(page.locator('g.node')).toHaveCount(2, { timeout: 5000 });
await expect(nodeByLabel(page, 'Start')).not.toBeAttached();
await expect(nodeByLabel(page, 'End')).not.toBeAttached();
});
test('a second load completely replaces the previously loaded graph', async ({ page }) => {
const firstFile = writeTempJson(SIMPLE_GRAPH);
await loadFile(page, firstFile, 'Alpha');
const secondFile = writeTempJson({
id: 'main',
nodes: [
{ id: 'n1', label: 'Gamma' },
{ id: 'n2', label: 'Delta' },
],
edges: [{ id: 'e1', from: 'n1', to: 'n2' }],
});
await loadFile(page, secondFile, 'Gamma');
await expect(nodeByLabel(page, 'Gamma')).toBeVisible();
await expect(nodeByLabel(page, 'Delta')).toBeVisible();
await expect(nodeByLabel(page, 'Alpha')).not.toBeAttached();
await expect(page.locator('g.node')).toHaveCount(2);
});
// ── navigation reset ──────────────────────────────────────────────────────────
test('loading resets the breadcrumb to "Main" only', async ({ page }) => {
await openSubgraph(page, 'Start');
await expect(page.locator('.ant-breadcrumb-link')).toHaveCount(2);
const filePath = writeTempJson(SIMPLE_GRAPH);
await loadFile(page, filePath, 'Alpha');
await expect(page.locator('.ant-breadcrumb-link')).toHaveCount(1, { timeout: 3000 });
await expect(page.locator('.ant-breadcrumb')).toContainText('Main');
await expect(page.locator('.ant-breadcrumb')).not.toContainText('Start');
});
test('loading while inside a subgraph shows the loaded main graph', async ({ page }) => {
await openSubgraph(page, 'Start');
const filePath = writeTempJson(SIMPLE_GRAPH);
await loadFile(page, filePath, 'Alpha');
await expect(nodeByLabel(page, 'Alpha')).toBeVisible();
await expect(nodeByLabel(page, 'Beta')).toBeVisible();
await expect(nodeByLabel(page, 'Start')).not.toBeAttached();
await expect(nodeByLabel(page, 'End')).not.toBeAttached();
});
// ── subgraph restoration ──────────────────────────────────────────────────────
test('subgraph from loaded JSON is accessible via the context menu', async ({ page }) => {
const filePath = writeTempJson(GRAPH_WITH_SUBGRAPH);
await loadFile(page, filePath, 'Alpha');
await openSubgraph(page, 'Alpha');
await expect(nodeByLabel(page, 'SubAlpha')).toBeVisible();
await expect(nodeByLabel(page, 'SubBeta')).toBeVisible();
await expect(page.locator('g.node')).toHaveCount(2);
await expect(page.locator('g.edge')).toHaveCount(1);
});
test('breadcrumb shows the loaded node label when navigated into its subgraph', async ({ page }) => {
const filePath = writeTempJson(GRAPH_WITH_SUBGRAPH);
await loadFile(page, filePath, 'Alpha');
await openSubgraph(page, 'Alpha');
await expect(page.locator('.ant-breadcrumb-link')).toHaveCount(2);
await expect(page.locator('.ant-breadcrumb')).toContainText('Alpha');
});
test('navigating back from a loaded subgraph restores the loaded main graph', async ({ page }) => {
const filePath = writeTempJson(GRAPH_WITH_SUBGRAPH);
await loadFile(page, filePath, 'Alpha');
await openSubgraph(page, 'Alpha');
await page.locator('.ant-breadcrumb-link').first().click();
await waitForGraph(page);
await expect(nodeByLabel(page, 'Alpha')).toBeVisible();
await expect(nodeByLabel(page, 'Beta')).toBeVisible();
await expect(page.locator('g.node')).toHaveCount(2);
});
test('node without a subgraph in the loaded JSON does not expose a pre-existing subgraph', async ({ page }) => {
// First load graph with subgraph, then load a graph without
const withSubgraph = writeTempJson(GRAPH_WITH_SUBGRAPH);
await loadFile(page, withSubgraph, 'Alpha');
await openSubgraph(page, 'Alpha');
await expect(nodeByLabel(page, 'SubAlpha')).toBeVisible();
// Go back and load a plain graph
await page.locator('.ant-breadcrumb-link').first().click();
await waitForGraph(page);
const plain = writeTempJson(SIMPLE_GRAPH);
await loadFile(page, plain, 'Alpha');
// Alpha no longer has a subgraph — entering it should create a fresh default graph
await openSubgraph(page, 'Alpha');
await expect(nodeByLabel(page, 'Start')).toBeVisible();
await expect(nodeByLabel(page, 'End')).toBeVisible();
await expect(nodeByLabel(page, 'SubAlpha')).not.toBeAttached();
});
// ── sidebar tree ──────────────────────────────────────────────────────────────
test('sidebar tree reflects subgraph structure from loaded JSON', async ({ page }) => {
const filePath = writeTempJson(GRAPH_WITH_SUBGRAPH);
await loadFile(page, filePath, 'Alpha');
// Expand sidebar
await page.locator('header button').first().click();
await expect(page.locator('.ant-layout-sider')).not.toHaveClass(/ant-layout-sider-collapsed/, { timeout: 3000 });
// Alpha has a subgraph so it appears in the tree
await expect(page.locator('.ant-tree-title').filter({ hasText: 'Alpha' })).toBeVisible({ timeout: 3000 });
});
});