added playwright tests
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -22,3 +22,6 @@ dist-ssr
|
|||||||
*.njsproj
|
*.njsproj
|
||||||
*.sln
|
*.sln
|
||||||
*.sw?
|
*.sw?
|
||||||
|
|
||||||
|
playwright-report
|
||||||
|
test-results
|
||||||
136
e2e/app.spec.ts
Normal file
136
e2e/app.spec.ts
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
|
||||||
|
// Helper: wait for the SVG graph to fully render (viz.js is async)
|
||||||
|
async function waitForGraph(page: import('@playwright/test').Page) {
|
||||||
|
await page.waitForSelector('svg g.node', { timeout: 15000 });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper: find a graph node by its label text
|
||||||
|
function nodeByLabel(page: import('@playwright/test').Page, label: string) {
|
||||||
|
return page.locator('g.node').filter({ hasText: label });
|
||||||
|
}
|
||||||
|
|
||||||
|
test.describe('ConceptSketch', () => {
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
|
await page.goto('/');
|
||||||
|
await waitForGraph(page);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('renders the initial graph with Start and End nodes', async ({ page }) => {
|
||||||
|
await expect(page.locator('svg g.graph')).toBeVisible();
|
||||||
|
await expect(nodeByLabel(page, 'Start')).toBeVisible();
|
||||||
|
await expect(nodeByLabel(page, 'End')).toBeVisible();
|
||||||
|
// There is exactly one edge connecting them
|
||||||
|
await expect(page.locator('g.edge')).toHaveCount(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('toggles the sidebar open and closed', async ({ page }) => {
|
||||||
|
const sider = page.locator('.ant-layout-sider');
|
||||||
|
const toggleBtn = page.locator('header button').first();
|
||||||
|
|
||||||
|
// Initially collapsed (collapsedWidth=0 → ant-layout-sider-collapsed)
|
||||||
|
await expect(sider).toHaveClass(/ant-layout-sider-collapsed/);
|
||||||
|
|
||||||
|
// Expand
|
||||||
|
await toggleBtn.click();
|
||||||
|
await expect(sider).not.toHaveClass(/ant-layout-sider-collapsed/);
|
||||||
|
|
||||||
|
// Collapse again
|
||||||
|
await toggleBtn.click();
|
||||||
|
await expect(sider).toHaveClass(/ant-layout-sider-collapsed/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('creates a child node when clicking a node', async ({ page }) => {
|
||||||
|
const initialNodeCount = await page.locator('g.node').count();
|
||||||
|
const initialEdgeCount = await page.locator('g.edge').count();
|
||||||
|
|
||||||
|
await nodeByLabel(page, 'Start').click();
|
||||||
|
|
||||||
|
// One more node and one more edge appear after re-render
|
||||||
|
await expect(page.locator('g.node')).toHaveCount(initialNodeCount + 1, { timeout: 5000 });
|
||||||
|
await expect(page.locator('g.edge')).toHaveCount(initialEdgeCount + 1, { timeout: 5000 });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('removes a node via the context menu', async ({ page }) => {
|
||||||
|
const initialNodeCount = await page.locator('g.node').count();
|
||||||
|
|
||||||
|
await nodeByLabel(page, 'End').click({ button: 'right' });
|
||||||
|
|
||||||
|
await expect(page.locator('.ant-dropdown:visible')).toBeVisible({ timeout: 3000 });
|
||||||
|
await page.locator('.ant-dropdown-menu-item').filter({ hasText: 'Remove' }).click();
|
||||||
|
|
||||||
|
await expect(page.locator('g.node')).toHaveCount(initialNodeCount - 1, { timeout: 5000 });
|
||||||
|
await expect(nodeByLabel(page, 'End')).not.toBeAttached();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('renames a node via the context menu', async ({ page }) => {
|
||||||
|
await nodeByLabel(page, 'Start').click({ button: 'right' });
|
||||||
|
|
||||||
|
await expect(page.locator('.ant-dropdown:visible')).toBeVisible({ timeout: 3000 });
|
||||||
|
await page.locator('.ant-dropdown-menu-item').filter({ hasText: 'Rename' }).click();
|
||||||
|
|
||||||
|
const modal = page.locator('.ant-modal');
|
||||||
|
await expect(modal).toBeVisible({ timeout: 3000 });
|
||||||
|
|
||||||
|
const input = modal.locator('.ant-input');
|
||||||
|
await input.clear();
|
||||||
|
await input.fill('Concept');
|
||||||
|
|
||||||
|
await modal.locator('.ant-btn-primary').click();
|
||||||
|
|
||||||
|
await expect(modal).not.toBeVisible();
|
||||||
|
await expect(nodeByLabel(page, 'Concept')).toBeVisible({ timeout: 5000 });
|
||||||
|
await expect(nodeByLabel(page, 'Start')).not.toBeAttached();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('navigates into a subgraph via the context menu', async ({ page }) => {
|
||||||
|
await nodeByLabel(page, 'Start').click({ button: 'right' });
|
||||||
|
|
||||||
|
await expect(page.locator('.ant-dropdown:visible')).toBeVisible({ timeout: 3000 });
|
||||||
|
await page.locator('.ant-dropdown-menu-item').filter({ hasText: 'Subgraph' }).click();
|
||||||
|
|
||||||
|
// Breadcrumb shows "Start" as the new path segment
|
||||||
|
await expect(page.locator('.ant-breadcrumb')).toContainText('Start', { timeout: 3000 });
|
||||||
|
|
||||||
|
// A fresh subgraph is rendered with its own default Start/End nodes
|
||||||
|
await waitForGraph(page);
|
||||||
|
await expect(nodeByLabel(page, 'Start')).toBeVisible();
|
||||||
|
await expect(nodeByLabel(page, 'End')).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('navigating back via breadcrumb restores the parent graph', async ({ page }) => {
|
||||||
|
// Go into Start's subgraph
|
||||||
|
await nodeByLabel(page, 'Start').click({ button: 'right' });
|
||||||
|
await expect(page.locator('.ant-dropdown:visible')).toBeVisible({ timeout: 3000 });
|
||||||
|
await page.locator('.ant-dropdown-menu-item').filter({ hasText: 'Subgraph' }).click();
|
||||||
|
await expect(page.locator('.ant-breadcrumb')).toContainText('Start', { timeout: 3000 });
|
||||||
|
await waitForGraph(page);
|
||||||
|
|
||||||
|
// Click the first breadcrumb item (Main) to go back
|
||||||
|
await page.locator('.ant-breadcrumb-link').first().click();
|
||||||
|
|
||||||
|
// Breadcrumb should only show the root segment again
|
||||||
|
await expect(page.locator('.ant-breadcrumb-link')).toHaveCount(1, { timeout: 3000 });
|
||||||
|
await waitForGraph(page);
|
||||||
|
await expect(nodeByLabel(page, 'Start')).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('links nodes using Ctrl+click selection', async ({ page }) => {
|
||||||
|
// Create a third node to link to
|
||||||
|
await nodeByLabel(page, 'Start').click();
|
||||||
|
await expect(page.locator('g.node')).toHaveCount(3, { timeout: 5000 });
|
||||||
|
|
||||||
|
const edgeCountBefore = await page.locator('g.edge').count();
|
||||||
|
|
||||||
|
// Ctrl+click End to select it as a parent
|
||||||
|
await page.keyboard.down('Control');
|
||||||
|
await nodeByLabel(page, 'End').click();
|
||||||
|
await page.keyboard.up('Control');
|
||||||
|
|
||||||
|
// Click Start (without Ctrl) — links selected End as a parent of Start
|
||||||
|
await nodeByLabel(page, 'Start').click();
|
||||||
|
|
||||||
|
// One new edge End→Start should have been created
|
||||||
|
await expect(page.locator('g.edge')).toHaveCount(edgeCountBefore + 1, { timeout: 5000 });
|
||||||
|
});
|
||||||
|
});
|
||||||
64
package-lock.json
generated
64
package-lock.json
generated
@@ -18,6 +18,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.36.0",
|
"@eslint/js": "^9.36.0",
|
||||||
|
"@playwright/test": "^1.58.2",
|
||||||
"@types/d3": "^7.4.3",
|
"@types/d3": "^7.4.3",
|
||||||
"@types/lodash": "^4.17.20",
|
"@types/lodash": "^4.17.20",
|
||||||
"@types/node": "^24.6.0",
|
"@types/node": "^24.6.0",
|
||||||
@@ -1174,6 +1175,22 @@
|
|||||||
"node": ">= 8"
|
"node": ">= 8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@playwright/test": {
|
||||||
|
"version": "1.58.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.58.2.tgz",
|
||||||
|
"integrity": "sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"playwright": "1.58.2"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"playwright": "cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@rc-component/async-validator": {
|
"node_modules/@rc-component/async-validator": {
|
||||||
"version": "5.0.4",
|
"version": "5.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/@rc-component/async-validator/-/async-validator-5.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/@rc-component/async-validator/-/async-validator-5.0.4.tgz",
|
||||||
@@ -3967,6 +3984,53 @@
|
|||||||
"url": "https://github.com/sponsors/jonschlinkert"
|
"url": "https://github.com/sponsors/jonschlinkert"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/playwright": {
|
||||||
|
"version": "1.58.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.2.tgz",
|
||||||
|
"integrity": "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"playwright-core": "1.58.2"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"playwright": "cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"fsevents": "2.3.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/playwright-core": {
|
||||||
|
"version": "1.58.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.2.tgz",
|
||||||
|
"integrity": "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"bin": {
|
||||||
|
"playwright-core": "cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/playwright/node_modules/fsevents": {
|
||||||
|
"version": "2.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
||||||
|
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||||
|
"dev": true,
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/postcss": {
|
"node_modules/postcss": {
|
||||||
"version": "8.5.6",
|
"version": "8.5.6",
|
||||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
|
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
|
||||||
|
|||||||
@@ -7,7 +7,9 @@
|
|||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "tsc -b && vite build",
|
"build": "tsc -b && vite build",
|
||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
"preview": "vite preview"
|
"preview": "vite preview",
|
||||||
|
"test:e2e": "playwright test",
|
||||||
|
"test:e2e:ui": "playwright test --ui"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"antd": "^5.28.0",
|
"antd": "^5.28.0",
|
||||||
@@ -20,6 +22,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.36.0",
|
"@eslint/js": "^9.36.0",
|
||||||
|
"@playwright/test": "^1.58.2",
|
||||||
"@types/d3": "^7.4.3",
|
"@types/d3": "^7.4.3",
|
||||||
"@types/lodash": "^4.17.20",
|
"@types/lodash": "^4.17.20",
|
||||||
"@types/node": "^24.6.0",
|
"@types/node": "^24.6.0",
|
||||||
|
|||||||
24
playwright.config.ts
Normal file
24
playwright.config.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { defineConfig, devices } from '@playwright/test';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
testDir: './e2e',
|
||||||
|
fullyParallel: true,
|
||||||
|
forbidOnly: !!process.env.CI,
|
||||||
|
retries: process.env.CI ? 1 : 0,
|
||||||
|
reporter: 'html',
|
||||||
|
use: {
|
||||||
|
baseURL: 'http://localhost:5173',
|
||||||
|
trace: 'on-first-retry',
|
||||||
|
},
|
||||||
|
projects: [
|
||||||
|
{
|
||||||
|
name: 'chromium',
|
||||||
|
use: { ...devices['Desktop Chrome'] },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
webServer: {
|
||||||
|
command: 'npm run dev',
|
||||||
|
url: 'http://localhost:5173',
|
||||||
|
reuseExistingServer: !process.env.CI,
|
||||||
|
},
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user