diff --git a/package-lock.json b/package-lock.json index 1324c85..b5ae432 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "careerops", - "version": "1.1.0", + "version": "1.2.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "careerops", - "version": "1.1.0", + "version": "1.2.1", "license": "Apache-2.0", "devDependencies": { "playwright": "^1.62.0" diff --git a/package.json b/package.json index 9848440..b780097 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "description": "CareerOps public repo — dashboard SPA + agent skill", "scripts": { "test": "node scripts/privacy-scan.mjs && node scripts/smoke-web.mjs && node scripts/test-nav-sections.mjs && node scripts/test-board-layout.mjs && node scripts/test-find-match.mjs && node scripts/test-career-os.mjs && node scripts/test-sample-board-pack.mjs && node scripts/test-schema-hardening.mjs && node scripts/test-promote-rpc.mjs && node scripts/test-credential-vault.mjs && node scripts/test-plugins.mjs && node scripts/test-chains.mjs && npm test --prefix packages/careerops", - "test:web": "node scripts/smoke-web.mjs && node scripts/test-nav-sections.mjs && node scripts/test-board-layout.mjs", + "test:web": "node scripts/smoke-web.mjs && node scripts/test-nav-sections.mjs && node scripts/test-board-layout.mjs && node scripts/test-modal-a11y.mjs", "test:nav": "node scripts/test-nav-sections.mjs", "test:board-layout": "node scripts/test-board-layout.mjs", "test:career-os": "node scripts/test-career-os.mjs", @@ -23,7 +23,8 @@ "deploy:functions": "bash scripts/deploy-functions.sh", "deploy:web": "bash scripts/deploy-web.sh", "pack:careerops": "npm pack --prefix packages/careerops", - "test:sample-pack": "node scripts/test-sample-board-pack.mjs" + "test:sample-pack": "node scripts/test-sample-board-pack.mjs", + "test:modal-a11y": "node scripts/test-modal-a11y.mjs" }, "devDependencies": { "playwright": "^1.62.0" diff --git a/scripts/smoke-web.mjs b/scripts/smoke-web.mjs index 347018f..839495a 100644 --- a/scripts/smoke-web.mjs +++ b/scripts/smoke-web.mjs @@ -112,6 +112,16 @@ const requiredStrings = [ { name: 'JD triage modal', re: /id="jdtriage"/ }, { name: 'Triage a JD control', re: /Triage a JD/ }, { name: 'JD triage Add to board', re: /id="jt_add"/ }, + { name: 'modal focus trap helper', re: /export const createFocusTrap/ }, + { name: 'shared modal trap/close helpers', re: /function trapModal\(|function closeModal\(/ }, + { name: 'settings dialog name', re: /id="settings"[^>]*aria-labelledby="settings_heading"/ }, + { name: 'addrole dialog name', re: /id="addrole"[^>]*aria-labelledby="ar_heading"/ }, + { name: 'jdtriage dialog name', re: /id="jdtriage"[^>]*aria-labelledby="jt_heading"/ }, + { name: 'ligoogle dialog name', re: /id="ligoogle"[^>]*aria-labelledby="li_heading"/ }, + { name: 'rolepanel dialog name', re: /id="rolepanel"[^>]*aria-labelledby="rp_title"/ }, + { name: 'modals use aria-modal', re: /id="settings"[^>]*aria-modal="true"/ }, + { name: 'modal page inert helper', re: /function syncPageInert\(/ }, + { name: 'modal backdrop click close', re: /e\.target === el\) closeModal\(/ }, { name: 'no legacy Resume tool button', re: />📄 Resume tool +modal a11y fixture + +
+ + +
+ + +` + +const server = createServer((req, res) => { + try { + if (req.url === '/' || req.url === '/fixture') { + res.writeHead(200, { 'content-type': 'text/html' }) + res.end(fixture) + return + } + const file = join(root, decodeURIComponent(req.url.split('?')[0])) + const body = readFileSync(file) + res.writeHead(200, { 'content-type': types[extname(file)] || 'application/octet-stream' }) + res.end(body) + } catch { + res.writeHead(404); res.end('missing') + } +}) + +await new Promise((r) => server.listen(0, '127.0.0.1', r)) +const { port } = server.address() +const browser = await chromium.launch({ headless: true }) +const page = await browser.newPage() +const fails = [] +const ok = (name, cond) => { console.log(cond ? `ok ${name}` : `FAIL ${name}`); if (!cond) fails.push(name) } + +await page.goto(`http://127.0.0.1:${port}/fixture`) +await page.click('#open') +ok('opens with focus on first field', await page.evaluate(() => document.activeElement?.id === 's_titles')) +ok('app is inert while open', await page.evaluate(() => document.getElementById('app').inert === true)) +ok('settings not inert', await page.evaluate(() => document.getElementById('settings').inert === false)) + +// Tab cycles inside modal (titles -> close -> titles) +await page.keyboard.press('Tab') +ok('Tab moves to close', await page.evaluate(() => document.activeElement?.id === 'settingsclose')) +await page.keyboard.press('Tab') +ok('Tab wraps to titles', await page.evaluate(() => document.activeElement?.id === 's_titles')) +await page.keyboard.press('Shift+Tab') +ok('Shift+Tab wraps to close', await page.evaluate(() => document.activeElement?.id === 'settingsclose')) + +// Cannot tab to behind control while open +const behindFocused = await page.evaluate(() => { + const behind = document.getElementById('behind') + behind.focus() + return document.activeElement === behind +}) +ok('inert blocks focusing page behind', behindFocused === false) + +await page.keyboard.press('Escape') +ok('Escape closes modal', await page.evaluate(() => document.getElementById('settings').classList.contains('hidden'))) +ok('Escape restores opener focus', await page.evaluate(() => document.activeElement?.id === 'open')) +ok('inert cleared after Escape', await page.evaluate(() => document.getElementById('app').inert === false)) + +await page.click('#open') +// Backdrop click: click the modal root, not the box +await page.locator('#settings').click({ position: { x: 2, y: 2 } }) +ok('backdrop click closes', await page.evaluate(() => document.getElementById('settings').classList.contains('hidden'))) +ok('backdrop restores opener focus', await page.evaluate(() => document.activeElement?.id === 'open')) + +await page.click('#open') +await page.click('#settingsclose') +ok('close button restores opener', await page.evaluate(() => document.activeElement?.id === 'open')) + +await browser.close() +server.close() +if (fails.length) { + console.error(`\ntest-modal-a11y: ${fails.length} failure(s)`) + process.exit(1) +} +console.log('\ntest-modal-a11y passed') diff --git a/web/index.html b/web/index.html index 75785fa..2c5838d 100644 --- a/web/index.html +++ b/web/index.html @@ -1149,8 +1149,8 @@

Career advisor

-