diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5188dcf..c7fe9a2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,11 +18,9 @@ jobs: - uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} - - name: Syntax check - run: | - node --check server.js - node --check public/app.js - - name: Privacy audit - run: npm run audit:privacy - - name: Smoke test - run: npm test + - name: Install dependencies + run: npm ci + - name: Install Chromium + run: npx playwright install --with-deps chromium + - name: Full check + run: npm run check diff --git a/docs/development.md b/docs/development.md index 5d0ab22..44aa1ef 100644 --- a/docs/development.md +++ b/docs/development.md @@ -27,6 +27,13 @@ node --check server.js node --check public/app.js npm run audit:privacy npm test +npm run test:browser +``` + +The browser smoke check uses Playwright with Chromium. After a fresh install, run: + +```bash +npx playwright install chromium ``` ## Live Check diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..1c62e14 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,66 @@ +{ + "name": "claw-space", + "version": "0.1.6", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "claw-space", + "version": "0.1.6", + "license": "MIT", + "devDependencies": { + "playwright": "^1.55.1" + }, + "engines": { + "node": ">=20" + } + }, + "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/playwright": { + "version": "1.60.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.60.0.tgz", + "integrity": "sha512-hheHdokM8cdqCb0lcE3s+zT4t4W+vvjpGxsZlDnikarzx8tSzMebh3UiFtgqwFwnTnjYQcsyMF8ei2mCO/tpeA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.60.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.60.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.60.0.tgz", + "integrity": "sha512-9bW6zvX/m0lEbgTKJ6YppOKx8H3VOPBMOCFh2irXFOT4BbHgrx5hPjwJYLT40Lu+4qtD36qKc/Hn56StUW57IA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + } + } +} diff --git a/package.json b/package.json index 7960f5b..81ad93a 100644 --- a/package.json +++ b/package.json @@ -21,11 +21,15 @@ ], "scripts": { "audit:privacy": "node scripts/privacy-audit.js", - "check": "node --check server.js && node --check public/app.js && npm run audit:privacy && npm test", + "check": "node --check server.js && node --check public/app.js && npm run audit:privacy && npm test && npm run test:browser", "start": "node server.js", "test": "node tests/smoke.js", + "test:browser": "node tests/browser-smoke.js", "test:live": "node tests/live.js" }, + "devDependencies": { + "playwright": "^1.55.1" + }, "engines": { "node": ">=20" } diff --git a/tests/browser-smoke.js b/tests/browser-smoke.js new file mode 100644 index 0000000..5ecfb19 --- /dev/null +++ b/tests/browser-smoke.js @@ -0,0 +1,72 @@ +'use strict'; + +const assert = require('assert'); + +process.env.OPENCLAW_WEB_UI_MOCK = '1'; + +const { chromium } = require('playwright'); +const { createApp } = require('../server'); + +async function expectDashboard(page, baseUrl, width, height) { + await page.setViewportSize({ width, height }); + await page.goto(baseUrl, { waitUntil: 'load' }); + + const localAccess = page.locator('#securityStatusCard'); + await localAccess.waitFor({ state: 'visible', timeout: 10000 }); + await page.locator('#tab-dashboard').waitFor({ state: 'visible', timeout: 10000 }); + + const heading = await localAccess.getByText('Local Access', { exact: true }).count(); + assert.equal(heading, 1, `missing Local Access panel at ${width}x${height}`); + + const overflow = await page.evaluate(() => { + const root = document.documentElement; + const dashboard = document.querySelector('#tab-dashboard'); + const surface = dashboard ? dashboard.scrollWidth - dashboard.clientWidth : 0; + return { + pageOverflow: Math.max(0, root.scrollWidth - root.clientWidth), + surfaceOverflow: Math.max(0, surface) + }; + }); + + assert.equal( + overflow.pageOverflow, + 0, + `horizontal page overflow at ${width}x${height}: ${overflow.pageOverflow}px` + ); + assert.equal( + overflow.surfaceOverflow, + 0, + `dashboard overflow at ${width}x${height}: ${overflow.surfaceOverflow}px` + ); +} + +async function main() { + const app = createApp(); + await new Promise((resolve, reject) => { + app.once('error', reject); + app.listen(0, '127.0.0.1', resolve); + }); + + try { + const address = app.address(); + const port = typeof address === 'object' && address ? address.port : 0; + const baseUrl = `http://127.0.0.1:${port}`; + const browser = await chromium.launch({ headless: true }); + try { + const page = await browser.newPage(); + await expectDashboard(page, baseUrl, 1280, 900); + await expectDashboard(page, baseUrl, 390, 844); + } finally { + await browser.close(); + } + } finally { + await new Promise((resolve) => app.close(resolve)); + } + + console.log('browser-smoke-ok'); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +});