Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,9 @@ jobs:

- name: Run unit tests
run: npm test

- name: Install Playwright browsers
run: npx playwright install chromium --with-deps

- name: Integration smoke
run: PUPPETEER_SKIP_DOWNLOAD=true npx vitest run --config vitest.smoke.config.mjs tests/smoke/integration.test.mjs
41 changes: 38 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 12 additions & 0 deletions tests/fixture/about.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>About</title>
</head>
<body>
<h1>About</h1>
<p>This is the about page for the integration fixture.</p>
<a href="/index.html">Back to home</a>
</body>
</html>
11 changes: 11 additions & 0 deletions tests/fixture/error.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Error Fixture</title>
</head>
<body>
<h1>Error page</h1>
<script>window.addEventListener('load', function() { throw new Error('test-pageerror'); });</script>
</body>
</html>
25 changes: 25 additions & 0 deletions tests/fixture/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Fixture Index</title>
</head>
<body>
<nav>
<a href="/about.html">About</a>
<a href="/error.html">Error page</a>
</nav>

<button aria-label="Primary action">Do something</button>
<button aria-label="Secondary action">Do something else</button>

<label for="search-input">Search</label>
<input id="search-input" type="text" name="q" />

<form action="#" method="post">
<label for="email-input">Email</label>
<input id="email-input" type="email" name="email" />
<button type="submit">Submit</button>
</form>
</body>
</html>
48 changes: 48 additions & 0 deletions tests/smoke/fixture-server.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import http from 'http'
import fs from 'fs'
import path from 'path'
import { fileURLToPath } from 'url'

const __dirname = path.dirname(fileURLToPath(import.meta.url))
const FIXTURE_DIR = path.join(__dirname, '..', 'fixture')

export async function startFixtureServer(port = 0) {
const server = http.createServer((req, res) => {
// Strip query string and normalise to a file path
const urlPath = req.url.split('?')[0]
const relPath = urlPath === '/' ? 'index.html' : urlPath.replace(/^\//, '')
const filePath = path.join(FIXTURE_DIR, relPath)

// Prevent directory traversal outside FIXTURE_DIR
if (!filePath.startsWith(FIXTURE_DIR)) {
res.writeHead(403, { 'Content-Type': 'text/plain' })
res.end('Forbidden')
return
}

fs.readFile(filePath, (err, data) => {
if (err) {
res.writeHead(404, { 'Content-Type': 'text/html; charset=utf-8' })
res.end('<html><body><h1>404 Not Found</h1></body></html>')
return
}
const contentType = filePath.endsWith('.html')
? 'text/html; charset=utf-8'
: 'application/octet-stream'
res.writeHead(200, { 'Content-Type': contentType })
res.end(data)
})
})

return new Promise((resolve, reject) => {
server.listen(port, '127.0.0.1', () => {
const { port: assignedPort } = server.address()
resolve({ server, url: `http://127.0.0.1:${assignedPort}` })
})
server.on('error', reject)
})
}

export function stopFixtureServer(server) {
return new Promise(resolve => server.close(resolve))
}
83 changes: 83 additions & 0 deletions tests/smoke/integration.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { describe, it, expect, beforeAll, afterAll } from 'vitest'
import { chromium } from 'playwright'
import { startFixtureServer, stopFixtureServer } from './fixture-server.mjs'
import { pruneLayout } from '../../src/perception/a11yTree.js'

let fixtureServer, fixtureUrl

beforeAll(async () => {
const result = await startFixtureServer()
fixtureServer = result.server
fixtureUrl = result.url
}, 30000)

afterAll(async () => {
if (fixtureServer) await stopFixtureServer(fixtureServer)
})

// Playwright 1.44+ removed page.accessibility; use CDP to get the AX tree and
// pipe through pruneLayout from a11yTree.js to exercise the same snapshot path.
async function snapshotViaPlaywright(browser, page) {
const context = page.context()
const cdp = await context.newCDPSession(page)
const { nodes } = await cdp.send('Accessibility.getFullAXTree')
await cdp.detach()

// Build a minimal role/name tree compatible with pruneLayout's input shape
const nodeMap = new Map(nodes.map(n => [n.nodeId, n]))
function build(node) {
if (!node) return null
return {
role: node.role?.value ?? 'generic',
name: node.name?.value ?? '',
children: (node.childIds ?? []).map(id => build(nodeMap.get(id))).filter(Boolean),
}
}
const root = nodes[0]
return pruneLayout(build(root))
}

describe('integration smoke', () => {
it('crawls clean fixture without fatal error', async () => {
const browser = await chromium.launch({ headless: true })
const pageErrors = []

try {
const context = await browser.newContext()
const page = await context.newPage()
page.on('pageerror', e => pageErrors.push(e.message))

await page.goto(fixtureUrl, { waitUntil: 'domcontentloaded' })

const snapshot = await snapshotViaPlaywright(browser, page)

expect(snapshot).not.toBeNull()
expect(typeof snapshot).toBe('object')
expect(snapshot).toHaveProperty('role')

await page.goto(fixtureUrl + '/about.html', { waitUntil: 'domcontentloaded' })
expect(pageErrors.filter(m => m.includes('500'))).toHaveLength(0)
} finally {
await browser.close()
}
}, 20000)

it('detects JS error on error.html via PAGEERROR signal', async () => {
const browser = await chromium.launch({ headless: true })
const pageErrors = []

try {
const context = await browser.newContext()
const page = await context.newPage()
page.on('pageerror', e => pageErrors.push(e.message))

await page.goto(fixtureUrl + '/error.html', { waitUntil: 'domcontentloaded' })
await page.waitForTimeout(500)

expect(pageErrors.length).toBeGreaterThan(0)
expect(pageErrors[0]).toContain('test-pageerror')
} finally {
await browser.close()
}
}, 20000)
})
10 changes: 10 additions & 0 deletions vitest.smoke.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { defineConfig } from 'vitest/config';

export default defineConfig({
test: {
include: ['tests/smoke/**/*.test.mjs'],
environment: 'node',
testTimeout: 30000,
reporters: ['default'],
},
});
Loading