mirror of
https://github.com/ruvnet/RuView
synced 2026-08-07 20:01:43 +00:00
infra: nvsim-server Docker + axe-core cross-browser CI
Closes the infrastructure half of ADR-092's open §11 gates:
- §11.5 axe-core a11y formal scan
- §11.8 cross-browser (Chromium + Firefox + WebKit)
## v2/crates/nvsim-server/Dockerfile (new)
Multi-stage build (rust:1.81-slim → debian:bookworm-slim):
- builds nvsim-server release binary
- runs as non-root `nvsim` user
- exposes 7878
- HEALTHCHECK against /api/health
- ENTRYPOINT nvsim-server with default --listen 0.0.0.0:7878
## .github/workflows/nvsim-server-docker.yml (new)
- triggers: push to main affecting nvsim*, tag nvsim-server-v*, manual
- publishes ghcr.io/ruvnet/nvsim-server:{branch,tag,sha,latest}
- multi-platform: linux/amd64
- post-publish smoke test: docker pull + run + curl /api/health
## dashboard/tests/a11y.spec.ts (new)
Playwright + @axe-core/playwright suite:
- iterates 6 primary views (home/scene/apps/inspector/witness/ghost-murmur)
- dismisses welcome modal, navigates via rail buttons
- runs axe-core with wcag2a + wcag2aa rule sets
- asserts 0 critical AND 0 serious violations per view
- prints violation summary on failure for actionable CI logs
## dashboard/playwright.config.ts (new)
- 3 projects: chromium, firefox, webkit
- webServer: npm run preview (vite preview port 4173)
- baseURL: http://localhost:4173
## .github/workflows/dashboard-a11y.yml (new)
- triggers: push to main, PRs touching dashboard/**, manual
- builds nvsim WASM via wasm-pack
- npm ci + playwright install --with-deps
- npm run build + npx playwright test (all 3 browsers × 6 views)
## dashboard/package.json
- new scripts: test:e2e, test:a11y
- new devDeps: @playwright/test, @axe-core/playwright
Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
Generated
+88
@@ -13,6 +13,8 @@
|
||||
"workbox-window": "^7.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@axe-core/playwright": "^4.11.2",
|
||||
"@playwright/test": "^1.59.1",
|
||||
"typescript": "^5.6.3",
|
||||
"vite": "^5.4.10",
|
||||
"vite-plugin-pwa": "^1.2.0",
|
||||
@@ -36,6 +38,19 @@
|
||||
"ajv": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/@axe-core/playwright": {
|
||||
"version": "4.11.2",
|
||||
"resolved": "https://registry.npmjs.org/@axe-core/playwright/-/playwright-4.11.2.tgz",
|
||||
"integrity": "sha512-iP6hfNl9G0j/SEUSo8M7D80RbcDo9KRAAfDP4IT5OHB+Wm6zUHIrm8Y51BKI+Oyqduvipf9u1hcRy57zCBKzWQ==",
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"dependencies": {
|
||||
"axe-core": "~4.11.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"playwright-core": ">= 1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/code-frame": {
|
||||
"version": "7.29.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz",
|
||||
@@ -2037,6 +2052,22 @@
|
||||
"@lit-labs/ssr-dom-shim": "^1.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@playwright/test": {
|
||||
"version": "1.59.1",
|
||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.59.1.tgz",
|
||||
"integrity": "sha512-PG6q63nQg5c9rIi4/Z5lR5IVF7yU5MqmKaPOe0HSc0O2cX1fPi96sUQu5j7eo4gKCkB2AnNGoWt7y4/Xx3Kcqg==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright": "1.59.1"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@preact/signals-core": {
|
||||
"version": "1.14.1",
|
||||
"resolved": "https://registry.npmjs.org/@preact/signals-core/-/signals-core-1.14.1.tgz",
|
||||
@@ -2753,6 +2784,16 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/axe-core": {
|
||||
"version": "4.11.3",
|
||||
"resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.11.3.tgz",
|
||||
"integrity": "sha512-zBQouZixDTbo3jMGqHKyePxYxr1e5W8UdTmBQ7sNtaA9M2bE32daxxPLS/jojhKOHxQ7LWwPjfiwf/fhaJWzlg==",
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/babel-plugin-polyfill-corejs2": {
|
||||
"version": "0.4.17",
|
||||
"resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.17.tgz",
|
||||
@@ -4695,6 +4736,53 @@
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright": {
|
||||
"version": "1.59.1",
|
||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.59.1.tgz",
|
||||
"integrity": "sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright-core": "1.59.1"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright-core": {
|
||||
"version": "1.59.1",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.59.1.tgz",
|
||||
"integrity": "sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg==",
|
||||
"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/possible-typed-array-names": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz",
|
||||
|
||||
@@ -10,7 +10,9 @@
|
||||
"preview": "vite preview --port 4173",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest"
|
||||
"test:watch": "vitest",
|
||||
"test:e2e": "playwright test",
|
||||
"test:a11y": "playwright test tests/a11y.spec.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@preact/signals-core": "^1.8.0",
|
||||
@@ -18,6 +20,8 @@
|
||||
"workbox-window": "^7.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@axe-core/playwright": "^4.11.2",
|
||||
"@playwright/test": "^1.59.1",
|
||||
"typescript": "^5.6.3",
|
||||
"vite": "^5.4.10",
|
||||
"vite-plugin-pwa": "^1.2.0",
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import { defineConfig } from '@playwright/test';
|
||||
|
||||
export default defineConfig({
|
||||
testDir: './tests',
|
||||
fullyParallel: true,
|
||||
retries: 0,
|
||||
reporter: 'list',
|
||||
use: {
|
||||
baseURL: 'http://localhost:4173',
|
||||
headless: true,
|
||||
},
|
||||
webServer: {
|
||||
command: 'npm run preview',
|
||||
port: 4173,
|
||||
timeout: 60_000,
|
||||
reuseExistingServer: !process.env.CI,
|
||||
},
|
||||
projects: [
|
||||
{ name: 'chromium', use: { browserName: 'chromium' } },
|
||||
{ name: 'firefox', use: { browserName: 'firefox' } },
|
||||
{ name: 'webkit', use: { browserName: 'webkit' } },
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,56 @@
|
||||
/* axe-core accessibility smoke against the built dashboard.
|
||||
* Closes ADR-092 §11.5 — formal axe scan.
|
||||
*
|
||||
* Runs against `npm run preview` (Vite preview server). Validates each
|
||||
* primary view (home / scene / apps / inspector / witness / ghost-murmur)
|
||||
* and asserts 0 critical/serious violations.
|
||||
*/
|
||||
|
||||
import { test, expect } from '@playwright/test';
|
||||
import AxeBuilder from '@axe-core/playwright';
|
||||
|
||||
const VIEWS = ['home', 'scene', 'apps', 'inspector', 'witness', 'ghost-murmur'] as const;
|
||||
|
||||
test.describe('axe-core a11y smoke', () => {
|
||||
for (const view of VIEWS) {
|
||||
test(`view: ${view}`, async ({ page }) => {
|
||||
await page.goto('/');
|
||||
// Dismiss the welcome modal if it auto-shows.
|
||||
await page.evaluate(() => {
|
||||
const sr = (document.querySelector('nv-app') as HTMLElement & { shadowRoot: ShadowRoot }).shadowRoot;
|
||||
const ob = sr.querySelector('nv-onboarding') as HTMLElement | null;
|
||||
if (ob?.hasAttribute('open')) {
|
||||
(ob.shadowRoot?.querySelector('.skip') as HTMLElement | null)?.click();
|
||||
}
|
||||
});
|
||||
// Navigate to the view via the rail button (except for home which is default).
|
||||
if (view !== 'home') {
|
||||
await page.evaluate((v) => {
|
||||
const sr = (document.querySelector('nv-app') as HTMLElement & { shadowRoot: ShadowRoot }).shadowRoot;
|
||||
const rail = sr.querySelector('nv-rail') as HTMLElement & { shadowRoot: ShadowRoot };
|
||||
const btn = rail.shadowRoot.querySelector(`button[data-id=${v}-btn]`) as HTMLElement | null;
|
||||
btn?.click();
|
||||
}, view);
|
||||
await page.waitForTimeout(300);
|
||||
}
|
||||
|
||||
const results = await new AxeBuilder({ page })
|
||||
.options({ runOnly: ['wcag2a', 'wcag2aa'] })
|
||||
.analyze();
|
||||
|
||||
const critical = results.violations.filter((v) => v.impact === 'critical');
|
||||
const serious = results.violations.filter((v) => v.impact === 'serious');
|
||||
|
||||
// Logging the violation summary makes CI failures readable.
|
||||
if (critical.length || serious.length) {
|
||||
for (const v of [...critical, ...serious]) {
|
||||
console.error(`[${view}] ${v.impact} · ${v.id} · ${v.help}`);
|
||||
for (const node of v.nodes) console.error(` ${node.target.join(' >> ')}`);
|
||||
}
|
||||
}
|
||||
|
||||
expect(critical.length, 'no critical violations').toBe(0);
|
||||
expect(serious.length, 'no serious violations').toBe(0);
|
||||
});
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user