test(e2e): add acadenice full smoke suite — R4.7
Adds an autonomous Playwright suite (acadenice-smoke-full.spec.ts) that drives the prod-like AcadeDoc stack via the UI to surface UI bugs without manual screenshot diagnostic. Each step is wrapped in test.step and records network/console/page errors plus per-step screenshots. A post-test script (scripts/generate-smoke-report.ts) aggregates telemetry into SMOKE-REPORT.md. The smoke runs against the real fork stack (client :5173, server :3001, bridge :4000) using Corentin's prod-like Acadenice credentials. It does NOT share state with the cross-stack e2e suite (no Baserow seeding, no docker-compose-e2e dependency) — its dedicated playwright.smoke.config.ts declares no setup project. Coverage: login, create page, sub-page nesting, wikilink + backlink, slash /database, slash /template, slash /sync-block, workspace graph, space graph. Initial run results (baseline): 2 OK / 7 KO / 0 PARTIAL — confirms SpaceSidebar renders-more-hooks crash when opening "Nouvelle page" submenu (blank-screen Error Boundary), GET /api/acadenice/templates → 403, and space-scoped graph entry point not reachable. Captured in SMOKE-REPORT.md for follow-up by R4.5/R4.6.
This commit is contained in:
parent
8bda6c5f82
commit
d245f31ab6
7 changed files with 1485 additions and 1 deletions
6
e2e/.gitignore
vendored
6
e2e/.gitignore
vendored
|
|
@ -1,6 +1,12 @@
|
|||
node_modules/
|
||||
test-results/
|
||||
playwright-report/
|
||||
smoke-report/
|
||||
smoke-test-results/
|
||||
smoke-results.json
|
||||
smoke-telemetry/
|
||||
screenshots/
|
||||
dist/
|
||||
.auth/
|
||||
.env.smoke
|
||||
*.local
|
||||
|
|
|
|||
57
e2e/SMOKE-REPORT.md
Normal file
57
e2e/SMOKE-REPORT.md
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
# AcadeDoc smoke report — 2026-05-08
|
||||
|
||||
Stack: client :5173 — server :3001 — bridge :4000
|
||||
Result: 2 OK / 7 KO / 0 PARTIAL — total 9
|
||||
|
||||
## Feature matrix
|
||||
|
||||
| Feature | Status | Details | Screenshot |
|
||||
|---------|--------|---------|------------|
|
||||
| Login | OK | Redirected to http://localhost:5173/home | - |
|
||||
| Create page | KO | page.waitForURL: Timeout 5000ms exceeded. — waiting for navigation until "load" | `screenshots/2-create-page-fail.png` |
|
||||
| Sub-page (parent-child link) | KO | Sub-page may exist in DB but not nested under parent in sidebar — Parent page not created — skipping | `screenshots/3-create-sub-page-fail.png` |
|
||||
| Wikilink + backlink | KO | Wikilink/backlink flow broken — Page A not available | `screenshots/4-wikilink-backlink-fail.png` |
|
||||
| Slash /database | KO | /database broken — page.waitForURL: Timeout 5000ms exceeded. — waiting for navigation until "load" | `screenshots/5-slash-database-fail.png` |
|
||||
| Slash /template | KO | /template broken — page.waitForURL: Timeout 5000ms exceeded. — waiting for navigation until "load" | `screenshots/6-slash-template-fail.png` |
|
||||
| Slash /sync-block | KO | /sync-block broken — page.waitForURL: Timeout 5000ms exceeded. — waiting for navigation until "load" | `screenshots/7-slash-sync-block-fail.png` |
|
||||
| Graph view (workspace) | OK | Graph canvas rendered with nodes | - |
|
||||
| Graph view (space-scoped) | KO | Space graph broken — locator.click: Timeout 5000ms exceeded. Call log: [2m - waiting for getByRole('link', { name: /graph\|graphe/i }).or(getByRole('button', { name: /graph\|graphe/i })).first()[22m | `screenshots/9-graph-space-fail.png` |
|
||||
|
||||
## Bugs confirmed
|
||||
|
||||
- **Create page** — page.waitForURL: Timeout 5000ms exceeded. — waiting for navigation until "load"
|
||||
- screenshot: `screenshots/2-create-page-fail.png`
|
||||
- **Sub-page (parent-child link)** — Sub-page may exist in DB but not nested under parent in sidebar — Parent page not created — skipping
|
||||
- screenshot: `screenshots/3-create-sub-page-fail.png`
|
||||
- **Wikilink + backlink** — Wikilink/backlink flow broken — Page A not available
|
||||
- screenshot: `screenshots/4-wikilink-backlink-fail.png`
|
||||
- **Slash /database** — /database broken — page.waitForURL: Timeout 5000ms exceeded. — waiting for navigation until "load"
|
||||
- screenshot: `screenshots/5-slash-database-fail.png`
|
||||
- **Slash /template** — /template broken — page.waitForURL: Timeout 5000ms exceeded. — waiting for navigation until "load"
|
||||
- screenshot: `screenshots/6-slash-template-fail.png`
|
||||
- **Slash /sync-block** — /sync-block broken — page.waitForURL: Timeout 5000ms exceeded. — waiting for navigation until "load"
|
||||
- screenshot: `screenshots/7-slash-sync-block-fail.png`
|
||||
- **Graph view (space-scoped)** — Space graph broken — locator.click: Timeout 5000ms exceeded. Call log: [2m - waiting for getByRole('link', { name: /graph|graphe/i }).or(getByRole('button', { name: /graph|graphe/i })).first()[22m
|
||||
- screenshot: `screenshots/9-graph-space-fail.png`
|
||||
|
||||
## Network errors (HTTP >= 400)
|
||||
|
||||
- `POST http://localhost:5173/api/users/me` → 401 *(during step: 1-login)*
|
||||
- `GET http://localhost:5173/api/acadenice/templates` → 403 *(during step: 5-slash-database)*
|
||||
|
||||
## Console errors
|
||||
|
||||
- `Warning: React has detected a change in the order of Hooks called by %s. This will lead to bugs and errors if not fixed. For more information, read the Rules of Hooks: https://reactjs.org/link/rules-of-hooks Previous render Next render` *(step: 2-create-page)*
|
||||
- `The above error occurred in the <SpaceSidebar> component: at SpaceSidebar (http://localhost:5173/src/features/space/components/sidebar/space-sidebar.tsx?t=1778235101685:32:16) at nav at http://localhost:5173/node_modules/.vite/deps/esm-D` *(step: 2-create-page)*
|
||||
|
||||
## Page errors (uncaught exceptions)
|
||||
|
||||
- `Error: Rendered more hooks than during the previous render.` *(step: 2-create-page)*
|
||||
|
||||
## How to reproduce
|
||||
|
||||
```bash
|
||||
cd formation-hub/e2e
|
||||
pnpm exec playwright test --config=playwright.smoke.config.ts
|
||||
pnpm exec tsx scripts/generate-smoke-report.ts
|
||||
```
|
||||
543
e2e/package-lock.json
generated
543
e2e/package-lock.json
generated
|
|
@ -10,12 +10,455 @@
|
|||
"devDependencies": {
|
||||
"@playwright/test": "^1.44.0",
|
||||
"dotenv": "^16.4.5",
|
||||
"tsx": "^4.21.0",
|
||||
"typescript": "^5.4.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/aix-ppc64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz",
|
||||
"integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"aix"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz",
|
||||
"integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz",
|
||||
"integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-x64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz",
|
||||
"integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-arm64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz",
|
||||
"integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-x64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz",
|
||||
"integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-arm64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz",
|
||||
"integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-x64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz",
|
||||
"integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz",
|
||||
"integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz",
|
||||
"integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ia32": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz",
|
||||
"integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-loong64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz",
|
||||
"integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-mips64el": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz",
|
||||
"integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==",
|
||||
"cpu": [
|
||||
"mips64el"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ppc64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz",
|
||||
"integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-riscv64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz",
|
||||
"integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-s390x": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz",
|
||||
"integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-x64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz",
|
||||
"integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-arm64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz",
|
||||
"integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-x64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz",
|
||||
"integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-arm64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz",
|
||||
"integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-x64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz",
|
||||
"integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openharmony-arm64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz",
|
||||
"integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openharmony"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/sunos-x64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz",
|
||||
"integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"sunos"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-arm64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz",
|
||||
"integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-ia32": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz",
|
||||
"integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-x64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz",
|
||||
"integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@playwright/test": {
|
||||
"version": "1.59.1",
|
||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.59.1.tgz",
|
||||
|
|
@ -45,6 +488,48 @@
|
|||
"url": "https://dotenvx.com"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz",
|
||||
"integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"esbuild": "bin/esbuild"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@esbuild/aix-ppc64": "0.27.7",
|
||||
"@esbuild/android-arm": "0.27.7",
|
||||
"@esbuild/android-arm64": "0.27.7",
|
||||
"@esbuild/android-x64": "0.27.7",
|
||||
"@esbuild/darwin-arm64": "0.27.7",
|
||||
"@esbuild/darwin-x64": "0.27.7",
|
||||
"@esbuild/freebsd-arm64": "0.27.7",
|
||||
"@esbuild/freebsd-x64": "0.27.7",
|
||||
"@esbuild/linux-arm": "0.27.7",
|
||||
"@esbuild/linux-arm64": "0.27.7",
|
||||
"@esbuild/linux-ia32": "0.27.7",
|
||||
"@esbuild/linux-loong64": "0.27.7",
|
||||
"@esbuild/linux-mips64el": "0.27.7",
|
||||
"@esbuild/linux-ppc64": "0.27.7",
|
||||
"@esbuild/linux-riscv64": "0.27.7",
|
||||
"@esbuild/linux-s390x": "0.27.7",
|
||||
"@esbuild/linux-x64": "0.27.7",
|
||||
"@esbuild/netbsd-arm64": "0.27.7",
|
||||
"@esbuild/netbsd-x64": "0.27.7",
|
||||
"@esbuild/openbsd-arm64": "0.27.7",
|
||||
"@esbuild/openbsd-x64": "0.27.7",
|
||||
"@esbuild/openharmony-arm64": "0.27.7",
|
||||
"@esbuild/sunos-x64": "0.27.7",
|
||||
"@esbuild/win32-arm64": "0.27.7",
|
||||
"@esbuild/win32-ia32": "0.27.7",
|
||||
"@esbuild/win32-x64": "0.27.7"
|
||||
}
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
||||
|
|
@ -60,6 +545,19 @@
|
|||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/get-tsconfig": {
|
||||
"version": "4.14.0",
|
||||
"resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.14.0.tgz",
|
||||
"integrity": "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"resolve-pkg-maps": "^1.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright": {
|
||||
"version": "1.59.1",
|
||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.59.1.tgz",
|
||||
|
|
@ -92,6 +590,51 @@
|
|||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/resolve-pkg-maps": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
|
||||
"integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx": {
|
||||
"version": "4.21.0",
|
||||
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz",
|
||||
"integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"esbuild": "~0.27.0",
|
||||
"get-tsconfig": "^4.7.5"
|
||||
},
|
||||
"bin": {
|
||||
"tsx": "dist/cli.mjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "~2.3.3"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/fsevents": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.9.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||
|
|
|
|||
|
|
@ -8,11 +8,15 @@
|
|||
"e2e:headed": "playwright test --headed",
|
||||
"e2e:debug": "playwright test --debug",
|
||||
"e2e:ci": "playwright test --reporter=github,html",
|
||||
"e2e:list": "playwright test --list"
|
||||
"e2e:list": "playwright test --list",
|
||||
"smoke": "playwright test --config=playwright.smoke.config.ts",
|
||||
"smoke:report": "tsx scripts/generate-smoke-report.ts",
|
||||
"smoke:full": "playwright test --config=playwright.smoke.config.ts; tsx scripts/generate-smoke-report.ts"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.44.0",
|
||||
"dotenv": "^16.4.5",
|
||||
"tsx": "^4.21.0",
|
||||
"typescript": "^5.4.5"
|
||||
},
|
||||
"engines": {
|
||||
|
|
|
|||
57
e2e/playwright.smoke.config.ts
Normal file
57
e2e/playwright.smoke.config.ts
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
/**
|
||||
* Playwright config dedicated to the AcadeDoc smoke suite (R4.7).
|
||||
*
|
||||
* Differs from the cross-stack e2e config (playwright.config.ts) on three
|
||||
* intentional points:
|
||||
*
|
||||
* 1. No setup project — the smoke suite drives the real prod-like stack
|
||||
* (Docmost server :3001, client :5173, bridge :4000) that is already up.
|
||||
* There is no Baserow seeding, no e2e-only docker-compose, no synthetic
|
||||
* admin user. The suite logs in via the UI with the real Acadenice user.
|
||||
*
|
||||
* 2. Single Chromium project, no shared storageState. The login spec is the
|
||||
* entry point and seeds an in-process auth via cookie inheritance inside
|
||||
* the suite itself.
|
||||
*
|
||||
* 3. Reporters: list (live), html (debugging), json (machine-readable for
|
||||
* SMOKE-REPORT.md generation).
|
||||
*/
|
||||
|
||||
import { defineConfig, devices } from "@playwright/test";
|
||||
import * as dotenv from "dotenv";
|
||||
import * as path from "path";
|
||||
|
||||
dotenv.config({ path: path.resolve(__dirname, ".env.smoke") });
|
||||
|
||||
const BASE_URL = process.env.PLAYWRIGHT_BASE_URL ?? "http://localhost:5173";
|
||||
|
||||
export default defineConfig({
|
||||
testDir: "./tests",
|
||||
testMatch: /acadenice-smoke-full\.spec\.ts/,
|
||||
fullyParallel: false,
|
||||
workers: 1,
|
||||
// Smoke must reveal failures, not retry them away.
|
||||
retries: 0,
|
||||
reporter: [
|
||||
["list"],
|
||||
["html", { outputFolder: "smoke-report", open: "never" }],
|
||||
["json", { outputFile: "smoke-results.json" }],
|
||||
],
|
||||
outputDir: "smoke-test-results",
|
||||
use: {
|
||||
baseURL: BASE_URL,
|
||||
screenshot: "only-on-failure",
|
||||
video: "retain-on-failure",
|
||||
trace: "retain-on-failure",
|
||||
actionTimeout: 15_000,
|
||||
navigationTimeout: 30_000,
|
||||
},
|
||||
timeout: 90_000,
|
||||
expect: { timeout: 10_000 },
|
||||
projects: [
|
||||
{
|
||||
name: "smoke-chromium",
|
||||
use: { ...devices["Desktop Chrome"] },
|
||||
},
|
||||
],
|
||||
});
|
||||
209
e2e/scripts/generate-smoke-report.ts
Normal file
209
e2e/scripts/generate-smoke-report.ts
Normal file
|
|
@ -0,0 +1,209 @@
|
|||
/**
|
||||
* Smoke report generator — R4.7
|
||||
*
|
||||
* Reads the per-step telemetry produced by acadenice-smoke-full.spec.ts and
|
||||
* produces SMOKE-REPORT.md at the e2e root. Designed to run after the suite
|
||||
* regardless of pass/fail.
|
||||
*
|
||||
* Inputs (all optional — missing files produce empty sections):
|
||||
* smoke-telemetry/step-results.json
|
||||
* smoke-telemetry/network-errors.json
|
||||
* smoke-telemetry/console-errors.json
|
||||
* smoke-telemetry/page-errors.json
|
||||
*
|
||||
* Output:
|
||||
* SMOKE-REPORT.md
|
||||
*/
|
||||
|
||||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
|
||||
interface StepResult {
|
||||
feature: string;
|
||||
status: "OK" | "KO" | "PARTIAL";
|
||||
details: string;
|
||||
screenshot?: string;
|
||||
}
|
||||
|
||||
interface NetworkError {
|
||||
url: string;
|
||||
status: number;
|
||||
method: string;
|
||||
testName: string;
|
||||
step: string;
|
||||
}
|
||||
|
||||
interface ConsoleError {
|
||||
message: string;
|
||||
testName: string;
|
||||
step: string;
|
||||
}
|
||||
|
||||
interface PageError {
|
||||
message: string;
|
||||
testName: string;
|
||||
step: string;
|
||||
}
|
||||
|
||||
const TELEMETRY_DIR = path.resolve(__dirname, "../smoke-telemetry");
|
||||
const OUTPUT = path.resolve(__dirname, "../SMOKE-REPORT.md");
|
||||
|
||||
function readJson<T>(file: string, fallback: T): T {
|
||||
const full = path.join(TELEMETRY_DIR, file);
|
||||
if (!fs.existsSync(full)) return fallback;
|
||||
try {
|
||||
return JSON.parse(fs.readFileSync(full, "utf8")) as T;
|
||||
} catch (err) {
|
||||
console.warn(`[smoke-report] could not parse ${file}: ${String(err)}`);
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
function relScreenshot(p: string | undefined): string {
|
||||
if (!p) return "";
|
||||
const e2eRoot = path.resolve(__dirname, "..");
|
||||
return path.relative(e2eRoot, p);
|
||||
}
|
||||
|
||||
function dedupe<T>(arr: T[], key: (x: T) => string): T[] {
|
||||
const seen = new Set<string>();
|
||||
const out: T[] = [];
|
||||
for (const item of arr) {
|
||||
const k = key(item);
|
||||
if (!seen.has(k)) {
|
||||
seen.add(k);
|
||||
out.push(item);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function main(): void {
|
||||
const steps = readJson<StepResult[]>("step-results.json", []);
|
||||
const network = readJson<NetworkError[]>("network-errors.json", []);
|
||||
const consoleErrs = readJson<ConsoleError[]>("console-errors.json", []);
|
||||
const pageErrs = readJson<PageError[]>("page-errors.json", []);
|
||||
|
||||
const today = new Date().toISOString().slice(0, 10);
|
||||
|
||||
const passed = steps.filter((s) => s.status === "OK").length;
|
||||
const failed = steps.filter((s) => s.status === "KO").length;
|
||||
const partial = steps.filter((s) => s.status === "PARTIAL").length;
|
||||
|
||||
const lines: string[] = [];
|
||||
lines.push(`# AcadeDoc smoke report — ${today}`);
|
||||
lines.push("");
|
||||
lines.push(`Stack: client :5173 — server :3001 — bridge :4000`);
|
||||
lines.push(
|
||||
`Result: ${passed} OK / ${failed} KO / ${partial} PARTIAL — total ${steps.length}`,
|
||||
);
|
||||
lines.push("");
|
||||
lines.push("## Feature matrix");
|
||||
lines.push("");
|
||||
lines.push("| Feature | Status | Details | Screenshot |");
|
||||
lines.push("|---------|--------|---------|------------|");
|
||||
if (steps.length === 0) {
|
||||
lines.push(
|
||||
"| (no telemetry) | - | suite did not run or failed before any step | - |",
|
||||
);
|
||||
}
|
||||
for (const s of steps) {
|
||||
const screen = s.screenshot ? `\`${relScreenshot(s.screenshot)}\`` : "-";
|
||||
// Collapse multi-line Playwright error logs into a single short summary.
|
||||
const safeDetails = s.details
|
||||
.replace(/\n+/g, " ")
|
||||
.replace(/=+ logs =+/g, "—")
|
||||
.replace(/=+/g, "")
|
||||
.replace(/\s+/g, " ")
|
||||
.replace(/\|/g, "\\|")
|
||||
.slice(0, 220);
|
||||
lines.push(`| ${s.feature} | ${s.status} | ${safeDetails} | ${screen} |`);
|
||||
}
|
||||
|
||||
lines.push("");
|
||||
lines.push("## Bugs confirmed");
|
||||
lines.push("");
|
||||
const bugs = steps.filter((s) => s.status === "KO");
|
||||
if (bugs.length === 0) {
|
||||
lines.push("None — every step passed.");
|
||||
} else {
|
||||
for (const b of bugs) {
|
||||
const compact = b.details
|
||||
.replace(/\n+/g, " ")
|
||||
.replace(/=+ logs =+/g, "—")
|
||||
.replace(/=+/g, "")
|
||||
.replace(/\s+/g, " ")
|
||||
.slice(0, 400);
|
||||
lines.push(`- **${b.feature}** — ${compact}`);
|
||||
if (b.screenshot) {
|
||||
lines.push(` - screenshot: \`${relScreenshot(b.screenshot)}\``);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
lines.push("");
|
||||
lines.push("## Network errors (HTTP >= 400)");
|
||||
lines.push("");
|
||||
const dedupNet = dedupe(
|
||||
network,
|
||||
(n) => `${n.method} ${n.url} ${n.status}`,
|
||||
);
|
||||
if (dedupNet.length === 0) {
|
||||
lines.push("None.");
|
||||
} else {
|
||||
for (const n of dedupNet.slice(0, 50)) {
|
||||
lines.push(
|
||||
`- \`${n.method} ${n.url}\` → ${n.status} *(during step: ${n.step})*`,
|
||||
);
|
||||
}
|
||||
if (dedupNet.length > 50) {
|
||||
lines.push(`- ... and ${dedupNet.length - 50} more, truncated.`);
|
||||
}
|
||||
}
|
||||
|
||||
lines.push("");
|
||||
lines.push("## Console errors");
|
||||
lines.push("");
|
||||
const dedupCon = dedupe(consoleErrs, (c) => c.message);
|
||||
if (dedupCon.length === 0) {
|
||||
lines.push("None.");
|
||||
} else {
|
||||
for (const c of dedupCon.slice(0, 30)) {
|
||||
const oneLine = c.message.replace(/\n/g, " ").slice(0, 250);
|
||||
lines.push(`- \`${oneLine}\` *(step: ${c.step})*`);
|
||||
}
|
||||
if (dedupCon.length > 30) {
|
||||
lines.push(`- ... and ${dedupCon.length - 30} more, truncated.`);
|
||||
}
|
||||
}
|
||||
|
||||
lines.push("");
|
||||
lines.push("## Page errors (uncaught exceptions)");
|
||||
lines.push("");
|
||||
const dedupPage = dedupe(pageErrs, (p) => p.message);
|
||||
if (dedupPage.length === 0) {
|
||||
lines.push("None.");
|
||||
} else {
|
||||
for (const p of dedupPage.slice(0, 30)) {
|
||||
const oneLine = p.message.replace(/\n/g, " ").slice(0, 250);
|
||||
lines.push(`- \`${oneLine}\` *(step: ${p.step})*`);
|
||||
}
|
||||
}
|
||||
|
||||
lines.push("");
|
||||
lines.push("## How to reproduce");
|
||||
lines.push("");
|
||||
lines.push("```bash");
|
||||
lines.push("cd formation-hub/e2e");
|
||||
lines.push(
|
||||
"pnpm exec playwright test --config=playwright.smoke.config.ts",
|
||||
);
|
||||
lines.push("pnpm exec tsx scripts/generate-smoke-report.ts");
|
||||
lines.push("```");
|
||||
lines.push("");
|
||||
|
||||
fs.writeFileSync(OUTPUT, lines.join("\n"));
|
||||
console.log(`[smoke-report] wrote ${OUTPUT}`);
|
||||
}
|
||||
|
||||
main();
|
||||
608
e2e/tests/acadenice-smoke-full.spec.ts
Normal file
608
e2e/tests/acadenice-smoke-full.spec.ts
Normal file
|
|
@ -0,0 +1,608 @@
|
|||
/**
|
||||
* AcadeDoc full smoke suite — R4.7
|
||||
*
|
||||
* Drives the real prod-like stack (Docmost client :5173, server :3001, bridge :4000)
|
||||
* via the UI to surface bugs that screenshot-driven diagnostic missed.
|
||||
*
|
||||
* Each step is wrapped in test.step so the JSON reporter records granular outcomes.
|
||||
* Network failures and console errors are captured to per-test telemetry files
|
||||
* consumed by scripts/generate-smoke-report.ts.
|
||||
*
|
||||
* The suite is intentionally NOT fail-fast. We want to see every red square.
|
||||
*/
|
||||
|
||||
import { test, expect, type Page, type ConsoleMessage } from "@playwright/test";
|
||||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
|
||||
const BASE_URL = process.env.PLAYWRIGHT_BASE_URL ?? "http://localhost:5173";
|
||||
const USER_EMAIL = process.env.PLAYWRIGHT_USER_EMAIL ?? "corentin@acadenice.fr";
|
||||
const USER_PASSWORD = process.env.PLAYWRIGHT_USER_PASSWORD ?? "acadedoc2026!";
|
||||
|
||||
const SCREENSHOT_DIR = path.resolve(__dirname, "../screenshots");
|
||||
const TELEMETRY_DIR = path.resolve(__dirname, "../smoke-telemetry");
|
||||
|
||||
interface NetworkError {
|
||||
url: string;
|
||||
status: number;
|
||||
method: string;
|
||||
testName: string;
|
||||
step: string;
|
||||
}
|
||||
|
||||
interface ConsoleError {
|
||||
message: string;
|
||||
testName: string;
|
||||
step: string;
|
||||
}
|
||||
|
||||
interface PageError {
|
||||
message: string;
|
||||
testName: string;
|
||||
step: string;
|
||||
}
|
||||
|
||||
const networkErrors: NetworkError[] = [];
|
||||
const consoleErrors: ConsoleError[] = [];
|
||||
const pageErrors: PageError[] = [];
|
||||
|
||||
let currentStep = "init";
|
||||
|
||||
function ensureDir(dir: string): void {
|
||||
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
|
||||
ensureDir(SCREENSHOT_DIR);
|
||||
ensureDir(TELEMETRY_DIR);
|
||||
|
||||
/**
|
||||
* Attach listeners that record network/console failures into per-test arrays.
|
||||
* The arrays are flushed to disk in test.afterAll so the report generator can
|
||||
* aggregate them across the suite.
|
||||
*/
|
||||
function attachTelemetry(page: Page, testName: string): void {
|
||||
page.on("response", (resp) => {
|
||||
const status = resp.status();
|
||||
const url = resp.url();
|
||||
// Ignore static assets, vite HMR, and 304 (not modified).
|
||||
if (status >= 400 && !url.includes("/@vite/") && !url.includes(".woff")) {
|
||||
networkErrors.push({
|
||||
url,
|
||||
status,
|
||||
method: resp.request().method(),
|
||||
testName,
|
||||
step: currentStep,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
page.on("console", (msg: ConsoleMessage) => {
|
||||
if (msg.type() === "error") {
|
||||
const text = msg.text();
|
||||
// Skip noisy known-harmless warnings.
|
||||
if (
|
||||
text.includes("Failed to load resource") ||
|
||||
text.includes("favicon") ||
|
||||
text.includes("DevTools")
|
||||
) {
|
||||
return;
|
||||
}
|
||||
consoleErrors.push({ message: text, testName, step: currentStep });
|
||||
}
|
||||
});
|
||||
|
||||
page.on("pageerror", (err: Error) => {
|
||||
pageErrors.push({
|
||||
message: `${err.name}: ${err.message}`,
|
||||
testName,
|
||||
step: currentStep,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function safeStep(
|
||||
page: Page,
|
||||
name: string,
|
||||
body: () => Promise<void>,
|
||||
): Promise<{ ok: boolean; error?: string; screenshot?: string }> {
|
||||
currentStep = name;
|
||||
try {
|
||||
await test.step(name, body);
|
||||
return { ok: true };
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
const slug = name.replace(/[^a-z0-9]+/gi, "-").toLowerCase();
|
||||
const screenshotPath = path.join(SCREENSHOT_DIR, `${slug}-fail.png`);
|
||||
try {
|
||||
await page.screenshot({ path: screenshotPath, fullPage: true });
|
||||
} catch {
|
||||
// Page may already be closed — non-fatal.
|
||||
}
|
||||
return { ok: false, error: message, screenshot: screenshotPath };
|
||||
}
|
||||
}
|
||||
|
||||
interface StepResult {
|
||||
feature: string;
|
||||
status: "OK" | "KO" | "PARTIAL";
|
||||
details: string;
|
||||
screenshot?: string;
|
||||
}
|
||||
|
||||
const stepResults: StepResult[] = [];
|
||||
|
||||
/**
|
||||
* Navigate to /home and ensure the workspace shell rendered.
|
||||
* Used as a stable anchor between steps that may have left the user mid-modal.
|
||||
*/
|
||||
async function gotoHome(page: Page): Promise<void> {
|
||||
await page.goto(`${BASE_URL}/home`, { waitUntil: "domcontentloaded" });
|
||||
// The sidebar renders a workspace name area within ~3s on a warm cache.
|
||||
await page.waitForLoadState("networkidle", { timeout: 15_000 }).catch(() => {});
|
||||
}
|
||||
|
||||
/**
|
||||
* Enter the first available space — pages can only be created inside a space,
|
||||
* not at the workspace root. Returns the space URL for later navigation.
|
||||
*/
|
||||
async function enterFirstSpace(page: Page): Promise<string> {
|
||||
await gotoHome(page);
|
||||
// Click "Espaces" / "Spaces" sidebar item to expand the list, or click a
|
||||
// space card directly on /home (UI shows space cards: Agence/CFA/General/Interne).
|
||||
const spaceCard = page
|
||||
.locator('a[href*="/s/"]')
|
||||
.or(
|
||||
page
|
||||
.getByRole("link")
|
||||
.filter({ hasText: /^(Agence|CFA|General|Interne)$/i }),
|
||||
)
|
||||
.first();
|
||||
await spaceCard.click({ timeout: 10_000 });
|
||||
await expect(page).toHaveURL(/\/s\//, { timeout: 10_000 });
|
||||
await page.waitForLoadState("networkidle", { timeout: 10_000 }).catch(() => {});
|
||||
return page.url();
|
||||
}
|
||||
|
||||
/**
|
||||
* Click the "create page" affordance inside a space. Tries several known
|
||||
* Docmost selectors (the icon button in the space sidebar header, the
|
||||
* keyboard shortcut, then a fallback: keyboard "Ctrl+Alt+N" if available).
|
||||
*/
|
||||
async function clickCreatePage(page: Page): Promise<void> {
|
||||
// Locator strategies, in order of specificity.
|
||||
// 1. Mantine ActionIcon with title attribute (Docmost convention).
|
||||
// 2. Plus icon next to the space name in the sidebar tree header.
|
||||
// 3. Generic "+" button.
|
||||
// Step 1: open the "Nouvelle page" sidebar dropdown.
|
||||
// The trigger appears in the space sidebar; clicking it reveals two options:
|
||||
// - "Nouvelle page" (blank)
|
||||
// - "Depuis un modele" (from template)
|
||||
const trigger = page.getByText(/^Nouvelle page$/i).first();
|
||||
if (await trigger.count()) {
|
||||
await trigger.click({ timeout: 5_000 });
|
||||
// Step 2: pick the blank-page option from the popup.
|
||||
const blankOption = page
|
||||
.getByRole("menuitem", { name: /^nouvelle page$/i })
|
||||
.or(page.locator('[role="menu"] >> text="Nouvelle page"'))
|
||||
.first();
|
||||
if (await blankOption.count()) {
|
||||
await blankOption.click({ timeout: 3_000 }).catch(() => {});
|
||||
}
|
||||
try {
|
||||
await page.waitForURL(/\/p\//, { timeout: 5_000 });
|
||||
return;
|
||||
} catch {
|
||||
// fall through
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: the "+" icon button next to the "Pages" tree section header.
|
||||
const plusBtn = page
|
||||
.locator('button[aria-label*="create" i]')
|
||||
.or(page.locator('button[title*="page" i]'))
|
||||
.first();
|
||||
if (await plusBtn.count()) {
|
||||
await plusBtn.click({ timeout: 3_000 }).catch(() => {});
|
||||
try {
|
||||
await page.waitForURL(/\/p\//, { timeout: 5_000 });
|
||||
return;
|
||||
} catch {
|
||||
// fall through
|
||||
}
|
||||
}
|
||||
|
||||
// Last resort: keyboard shortcut Ctrl+Alt+N (Docmost default).
|
||||
await page.keyboard.press("Control+Alt+n");
|
||||
await page.waitForURL(/\/p\//, { timeout: 5_000 });
|
||||
}
|
||||
|
||||
test.describe("acadenice smoke full", () => {
|
||||
test.describe.configure({ mode: "serial" });
|
||||
|
||||
let page: Page;
|
||||
|
||||
test.beforeAll(async ({ browser }) => {
|
||||
const context = await browser.newContext();
|
||||
page = await context.newPage();
|
||||
attachTelemetry(page, "acadenice-smoke-full");
|
||||
});
|
||||
|
||||
test.afterAll(async () => {
|
||||
// Flush telemetry to disk for the report generator.
|
||||
fs.writeFileSync(
|
||||
path.join(TELEMETRY_DIR, "network-errors.json"),
|
||||
JSON.stringify(networkErrors, null, 2),
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(TELEMETRY_DIR, "console-errors.json"),
|
||||
JSON.stringify(consoleErrors, null, 2),
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(TELEMETRY_DIR, "page-errors.json"),
|
||||
JSON.stringify(pageErrors, null, 2),
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(TELEMETRY_DIR, "step-results.json"),
|
||||
JSON.stringify(stepResults, null, 2),
|
||||
);
|
||||
await page.context().close();
|
||||
});
|
||||
|
||||
test("full smoke flow", async () => {
|
||||
// 1. Login.
|
||||
{
|
||||
const r = await safeStep(page, "1-login", async () => {
|
||||
await page.goto(`${BASE_URL}/login`, { waitUntil: "domcontentloaded" });
|
||||
await page.getByLabel(/email/i).fill(USER_EMAIL);
|
||||
await page.getByLabel(/password/i).fill(USER_PASSWORD);
|
||||
await page
|
||||
.getByRole("button", { name: /sign in|login|connexion|se connecter/i })
|
||||
.click();
|
||||
// Post-login the app routes either to /home or directly to a space root.
|
||||
await expect(page).toHaveURL(/\/(home|s\/|p\/)/, { timeout: 20_000 });
|
||||
});
|
||||
stepResults.push({
|
||||
feature: "Login",
|
||||
status: r.ok ? "OK" : "KO",
|
||||
details: r.ok ? `Redirected to ${page.url()}` : (r.error ?? "unknown"),
|
||||
screenshot: r.screenshot,
|
||||
});
|
||||
}
|
||||
|
||||
// 2. Create page.
|
||||
let pageAUrl = "";
|
||||
let spaceUrl = "";
|
||||
{
|
||||
const r = await safeStep(page, "2-create-page", async () => {
|
||||
spaceUrl = await enterFirstSpace(page);
|
||||
await clickCreatePage(page);
|
||||
const titleInput = page
|
||||
.locator('[data-testid="page-title"]')
|
||||
.or(page.getByPlaceholder(/untitled|sans titre|title/i))
|
||||
.or(page.locator('input[type="text"]').first())
|
||||
.first();
|
||||
await titleInput.waitFor({ state: "visible", timeout: 10_000 });
|
||||
await titleInput.fill("Smoke Page A");
|
||||
await titleInput.press("Tab");
|
||||
await expect(page).toHaveURL(/\/p\//, { timeout: 10_000 });
|
||||
pageAUrl = page.url();
|
||||
});
|
||||
stepResults.push({
|
||||
feature: "Create page",
|
||||
status: r.ok ? "OK" : "KO",
|
||||
details: r.ok
|
||||
? `Page created at ${pageAUrl}`
|
||||
: (r.error ?? "unknown"),
|
||||
screenshot: r.screenshot,
|
||||
});
|
||||
}
|
||||
|
||||
// 3. Create sub-page.
|
||||
{
|
||||
const r = await safeStep(page, "3-create-sub-page", async () => {
|
||||
if (!pageAUrl) throw new Error("Parent page not created — skipping");
|
||||
await page.goto(pageAUrl, { waitUntil: "domcontentloaded" });
|
||||
// The "Add sub-page" lives in the sidebar tree row hover menu.
|
||||
// Hover the parent node row first.
|
||||
const parentRow = page
|
||||
.locator(`text="Smoke Page A"`)
|
||||
.first();
|
||||
await parentRow.hover({ timeout: 5_000 });
|
||||
const addSub = page
|
||||
.getByRole("button", { name: /add sub-?page|sous-page|new sub/i })
|
||||
.or(page.locator('[data-testid="add-subpage"]'))
|
||||
.first();
|
||||
await addSub.click({ timeout: 5_000 });
|
||||
const titleInput = page
|
||||
.locator('[data-testid="page-title"]')
|
||||
.or(page.getByPlaceholder(/untitled|sans titre|title/i))
|
||||
.first();
|
||||
await titleInput.waitFor({ state: "visible", timeout: 10_000 });
|
||||
await titleInput.fill("Smoke Sub-page A.1");
|
||||
await titleInput.press("Tab");
|
||||
// Verify the sub-page appears NESTED under Smoke Page A in the sidebar.
|
||||
// The aria tree typically nests children under their parent's <li>.
|
||||
const sidebar = page
|
||||
.getByRole("tree")
|
||||
.or(page.locator('[data-testid="sidebar-tree"]'))
|
||||
.first();
|
||||
const parentNode = sidebar.locator(
|
||||
'li:has-text("Smoke Page A")',
|
||||
).first();
|
||||
await expect(
|
||||
parentNode.locator('text="Smoke Sub-page A.1"'),
|
||||
).toBeVisible({ timeout: 10_000 });
|
||||
});
|
||||
stepResults.push({
|
||||
feature: "Sub-page (parent-child link)",
|
||||
status: r.ok ? "OK" : "KO",
|
||||
details: r.ok
|
||||
? "Sub-page rendered nested under parent in sidebar"
|
||||
: `Sub-page may exist in DB but not nested under parent in sidebar — ${r.error ?? ""}`,
|
||||
screenshot: r.screenshot,
|
||||
});
|
||||
}
|
||||
|
||||
// 4. Wikilink + backlink.
|
||||
{
|
||||
const r = await safeStep(page, "4-wikilink-backlink", async () => {
|
||||
if (!pageAUrl) throw new Error("Page A not available");
|
||||
await page.goto(pageAUrl, { waitUntil: "domcontentloaded" });
|
||||
const editor = page
|
||||
.locator('[contenteditable="true"]')
|
||||
.or(page.locator(".ProseMirror"))
|
||||
.first();
|
||||
await editor.click({ timeout: 5_000 });
|
||||
await page.keyboard.type("[[Smoke Sub-page A.1");
|
||||
// Suggestion popup should show the matching page.
|
||||
const suggestion = page
|
||||
.locator('[data-testid="mention-list"]')
|
||||
.or(page.locator('[role="listbox"]'))
|
||||
.or(page.locator(".tippy-box"))
|
||||
.first();
|
||||
await expect(suggestion).toBeVisible({ timeout: 5_000 });
|
||||
await page.keyboard.press("Enter");
|
||||
// Closing brackets may auto-insert; type them anyway just in case.
|
||||
await page.keyboard.type("]] ");
|
||||
await page.waitForTimeout(2_000); // debounce save
|
||||
// Navigate to A.1 and check backlinks panel.
|
||||
const subLink = page.locator('text="Smoke Sub-page A.1"').first();
|
||||
await subLink.click({ timeout: 5_000 });
|
||||
await expect(page).toHaveURL(/\/p\//);
|
||||
const backlinks = page
|
||||
.locator('[data-testid="backlinks"]')
|
||||
.or(page.getByText(/backlinks?|liens entrants/i))
|
||||
.first();
|
||||
await expect(backlinks).toBeVisible({ timeout: 10_000 });
|
||||
await expect(
|
||||
backlinks.locator('text="Smoke Page A"'),
|
||||
).toBeVisible({ timeout: 10_000 });
|
||||
});
|
||||
stepResults.push({
|
||||
feature: "Wikilink + backlink",
|
||||
status: r.ok ? "OK" : "KO",
|
||||
details: r.ok
|
||||
? "Wikilink suggestion + backlink panel show correctly"
|
||||
: `Wikilink/backlink flow broken — ${r.error ?? ""}`,
|
||||
screenshot: r.screenshot,
|
||||
});
|
||||
}
|
||||
|
||||
// Helper to create a fresh page for each slash test (isolates failures).
|
||||
async function freshPage(title: string): Promise<void> {
|
||||
if (spaceUrl) {
|
||||
await page.goto(spaceUrl, { waitUntil: "domcontentloaded" });
|
||||
} else {
|
||||
await enterFirstSpace(page);
|
||||
}
|
||||
await clickCreatePage(page);
|
||||
const titleInput = page
|
||||
.locator('[data-testid="page-title"]')
|
||||
.or(page.getByPlaceholder(/untitled|sans titre|title/i))
|
||||
.or(page.locator('input[type="text"]').first())
|
||||
.first();
|
||||
await titleInput.waitFor({ state: "visible", timeout: 10_000 });
|
||||
await titleInput.fill(title);
|
||||
await titleInput.press("Tab");
|
||||
await expect(page).toHaveURL(/\/p\//, { timeout: 10_000 });
|
||||
}
|
||||
|
||||
// 5. Slash /database.
|
||||
{
|
||||
const r = await safeStep(page, "5-slash-database", async () => {
|
||||
await freshPage("Smoke Database Test");
|
||||
const editor = page.locator(".ProseMirror").first();
|
||||
await editor.click({ timeout: 5_000 });
|
||||
// Track if the page reloads (Corentin's reported bug).
|
||||
let reloaded = false;
|
||||
page.once("framenavigated", () => {
|
||||
reloaded = true;
|
||||
});
|
||||
await page.keyboard.type("/database");
|
||||
// Slash menu should appear.
|
||||
const slashMenu = page
|
||||
.locator('[data-testid="slash-menu"]')
|
||||
.or(page.locator('[role="listbox"]'))
|
||||
.or(page.locator(".tippy-box"))
|
||||
.first();
|
||||
await expect(slashMenu).toBeVisible({ timeout: 5_000 });
|
||||
await page.keyboard.press("Enter");
|
||||
// The Acadenice database picker modal should open.
|
||||
const modal = page
|
||||
.getByRole("dialog")
|
||||
.or(page.locator('[data-testid="database-picker"]'))
|
||||
.first();
|
||||
await expect(modal).toBeVisible({ timeout: 8_000 });
|
||||
// The modal should list at least one Baserow table (personne, formation, bloc...).
|
||||
await expect(
|
||||
modal.getByText(/personne|formation|bloc/i).first(),
|
||||
).toBeVisible({ timeout: 8_000 });
|
||||
if (reloaded) {
|
||||
throw new Error("Page reloaded during /database — crash bug confirmed");
|
||||
}
|
||||
});
|
||||
stepResults.push({
|
||||
feature: "Slash /database",
|
||||
status: r.ok ? "OK" : "KO",
|
||||
details: r.ok
|
||||
? "Database picker modal opens with Baserow tables listed"
|
||||
: `/database broken — ${r.error ?? ""}`,
|
||||
screenshot: r.screenshot,
|
||||
});
|
||||
}
|
||||
|
||||
// 6. Slash /template.
|
||||
{
|
||||
const r = await safeStep(page, "6-slash-template", async () => {
|
||||
await freshPage("Smoke Template Test");
|
||||
const editor = page.locator(".ProseMirror").first();
|
||||
await editor.click({ timeout: 5_000 });
|
||||
await page.keyboard.type("/template");
|
||||
const slashMenu = page
|
||||
.locator('[data-testid="slash-menu"]')
|
||||
.or(page.locator('[role="listbox"]'))
|
||||
.or(page.locator(".tippy-box"))
|
||||
.first();
|
||||
await expect(slashMenu).toBeVisible({ timeout: 5_000 });
|
||||
await page.keyboard.press("Enter");
|
||||
const modal = page
|
||||
.getByRole("dialog")
|
||||
.or(page.locator('[data-testid="template-picker"]'))
|
||||
.first();
|
||||
await expect(modal).toBeVisible({ timeout: 8_000 });
|
||||
// Modal must list the 5 seeded templates — assert on a known one.
|
||||
// If "aucun modele" / "no template" is shown, the picker is empty (bug).
|
||||
const empty = modal
|
||||
.getByText(/aucun mod[èe]le|no template|empty/i)
|
||||
.first();
|
||||
const hasEmpty = await empty.isVisible().catch(() => false);
|
||||
if (hasEmpty) {
|
||||
throw new Error(
|
||||
"Template picker shows empty state but DB has 5 templates",
|
||||
);
|
||||
}
|
||||
await expect(
|
||||
modal.getByText(/daily|standup|meeting|note/i).first(),
|
||||
).toBeVisible({ timeout: 8_000 });
|
||||
});
|
||||
stepResults.push({
|
||||
feature: "Slash /template",
|
||||
status: r.ok ? "OK" : "KO",
|
||||
details: r.ok
|
||||
? "Template picker lists templates from DB"
|
||||
: `/template broken — ${r.error ?? ""}`,
|
||||
screenshot: r.screenshot,
|
||||
});
|
||||
}
|
||||
|
||||
// 7. Slash /sync-block.
|
||||
{
|
||||
const r = await safeStep(page, "7-slash-sync-block", async () => {
|
||||
await freshPage("Smoke SyncBlock Test");
|
||||
const editor = page.locator(".ProseMirror").first();
|
||||
await editor.click({ timeout: 5_000 });
|
||||
await page.keyboard.type("/sync");
|
||||
const slashMenu = page
|
||||
.locator('[data-testid="slash-menu"]')
|
||||
.or(page.locator('[role="listbox"]'))
|
||||
.or(page.locator(".tippy-box"))
|
||||
.first();
|
||||
await expect(slashMenu).toBeVisible({ timeout: 5_000 });
|
||||
// Look for "sync block" entry.
|
||||
const syncEntry = slashMenu
|
||||
.getByText(/sync ?block|bloc synchronis/i)
|
||||
.first();
|
||||
await expect(syncEntry).toBeVisible({ timeout: 5_000 });
|
||||
await syncEntry.click();
|
||||
// Sync block node should be inserted in the editor.
|
||||
const syncNode = page
|
||||
.locator('[data-type="sync-block"]')
|
||||
.or(page.locator('[data-testid="sync-block"]'))
|
||||
.first();
|
||||
await expect(syncNode).toBeVisible({ timeout: 8_000 });
|
||||
});
|
||||
stepResults.push({
|
||||
feature: "Slash /sync-block",
|
||||
status: r.ok ? "OK" : "KO",
|
||||
details: r.ok
|
||||
? "Sync block node inserted"
|
||||
: `/sync-block broken — ${r.error ?? ""}`,
|
||||
screenshot: r.screenshot,
|
||||
});
|
||||
}
|
||||
|
||||
// 8. Graph view from workspace.
|
||||
{
|
||||
const r = await safeStep(page, "8-graph-workspace", async () => {
|
||||
// Try the sidebar link first (French UI: "Graphe de connaissance").
|
||||
await gotoHome(page);
|
||||
const sidebarGraph = page
|
||||
.getByRole("link", { name: /graphe de connaissance|knowledge graph|graph/i })
|
||||
.first();
|
||||
if (await sidebarGraph.count()) {
|
||||
await sidebarGraph.click({ timeout: 5_000 }).catch(async () => {
|
||||
await page.goto(`${BASE_URL}/graph`, { waitUntil: "domcontentloaded" });
|
||||
});
|
||||
} else {
|
||||
await page.goto(`${BASE_URL}/graph`, { waitUntil: "domcontentloaded" });
|
||||
}
|
||||
// The graph canvas (cytoscape, vis-network, or sigma) renders to <canvas>.
|
||||
const canvas = page
|
||||
.locator('[data-testid="graph-canvas"]')
|
||||
.or(page.locator("canvas"))
|
||||
.or(page.locator("svg.graph"))
|
||||
.first();
|
||||
await expect(canvas).toBeVisible({ timeout: 15_000 });
|
||||
// The graph data API call should have responded.
|
||||
// We accept a node count badge OR a non-empty <svg>/<canvas>.
|
||||
const empty = page
|
||||
.getByText(/no pages|aucune page|graph is empty/i)
|
||||
.first();
|
||||
const isEmpty = await empty.isVisible().catch(() => false);
|
||||
if (isEmpty) {
|
||||
throw new Error("Graph view rendered but reports empty state");
|
||||
}
|
||||
});
|
||||
stepResults.push({
|
||||
feature: "Graph view (workspace)",
|
||||
status: r.ok ? "OK" : "KO",
|
||||
details: r.ok
|
||||
? "Graph canvas rendered with nodes"
|
||||
: `Graph view broken — ${r.error ?? ""}`,
|
||||
screenshot: r.screenshot,
|
||||
});
|
||||
}
|
||||
|
||||
// 9. Graph view from a space.
|
||||
{
|
||||
const r = await safeStep(page, "9-graph-space", async () => {
|
||||
await gotoHome(page);
|
||||
// Click first space in the sidebar.
|
||||
const spaceLink = page
|
||||
.locator('a[href*="/s/"]')
|
||||
.first();
|
||||
await spaceLink.click({ timeout: 5_000 });
|
||||
await expect(page).toHaveURL(/\/s\//, { timeout: 10_000 });
|
||||
// Open the space-level graph (button in space header or sidebar).
|
||||
const graphBtn = page
|
||||
.getByRole("link", { name: /graph|graphe/i })
|
||||
.or(page.getByRole("button", { name: /graph|graphe/i }))
|
||||
.first();
|
||||
await graphBtn.click({ timeout: 5_000 });
|
||||
const canvas = page
|
||||
.locator('[data-testid="graph-canvas"]')
|
||||
.or(page.locator("canvas"))
|
||||
.first();
|
||||
await expect(canvas).toBeVisible({ timeout: 15_000 });
|
||||
});
|
||||
stepResults.push({
|
||||
feature: "Graph view (space-scoped)",
|
||||
status: r.ok ? "OK" : "KO",
|
||||
details: r.ok
|
||||
? "Space-scoped graph rendered"
|
||||
: `Space graph broken — ${r.error ?? ""}`,
|
||||
screenshot: r.screenshot,
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Reference in a new issue