From da324badce89ac1d83b4e112158505433c2b69ff Mon Sep 17 00:00:00 2001 From: Sanskar Date: Sat, 7 Nov 2026 20:17:04 +0530 Subject: [PATCH 1/3] feat(a11y): keyboard search navigation and real-browser clipboard denial tests --- SCARP-DOCS-ENGINE.md | 2 +- docs/guide/search-and-navigation.md | 10 +++- docs/hi/guide/search-and-navigation.md | 10 +++- scripts/scarp-site/browser-test.ts | 64 +++++++++++++++++++++++--- scripts/scarp-site/client.ts | 15 ++++++ 5 files changed, 90 insertions(+), 11 deletions(-) diff --git a/SCARP-DOCS-ENGINE.md b/SCARP-DOCS-ENGINE.md index be8262f..5de40d1 100644 --- a/SCARP-DOCS-ENGINE.md +++ b/SCARP-DOCS-ENGINE.md @@ -36,7 +36,7 @@ The engine's test build is generated under artifacts/scarp-engine-preview/scarp- 1. **142 pages**: preserve at least 71 English and 71 Hindi source routes; new paired release pages may increase the count, local links, home features, heading anchors, code samples, callouts and both embedded interactive widgets. 2. **Search and releases**: use a full local index with no external provider; verify correct pinned compiler versions and do not invent unshipped commands. -3. **Accessible UI**: test keyboard, focus return, language switching, appearance persistence, 320px reflow, mobile navigation and code/table scrolling. Run axe WCAG A/AA on representative states. Automated tests are not manual screen-reader certification; see Issue #65. +3. **Accessible UI**: test keyboard search-result cycling, release selection, clipboard success/permission denial, focus return, language switching, appearance persistence, 320px reflow, reduced motion, mobile navigation and code/table scrolling. Run axe WCAG A/AA on representative states. Automated tests are not manual screen-reader certification; see Issue #65. 4. **Deploy and offline**: change production build, archive generation, Pages upload, sitemap/live smoke and downloaded offline browser checks together. Confirm complete URL continuity. 5. **Remove VitePress**: only after actual production and offline verification, migrate pure release data out of the old theme, remove its Vue/VitePress files and dependencies, and update older conformance and toolchain tests without weakening them. 6. **Release policy**: no stable documentation version is created by this migration. Keep the existing prerelease release gates. diff --git a/docs/guide/search-and-navigation.md b/docs/guide/search-and-navigation.md index 39452d0..867142d 100644 --- a/docs/guide/search-and-navigation.md +++ b/docs/guide/search-and-navigation.md @@ -13,10 +13,16 @@ The keyboard shortcut shown in the search trigger is part of its accessible name ## Find the right version -A search result may come from a **historical** tutorial. Start with the [seven-stage learning path](/guide/learning-path), [current installation](/guide/installation-alpha6) or [published feature status](/reference/status) to determine the correct compiler version before running an example. The core language reference remains pinned to alpha.1, while newer guides identify their own prereleases. +A search result may come from a **historical** tutorial. Start with the [seven-stage learning path](/guide/learning-path), [latest published compiler downloads](/releases/v0.0.6-alpha.3) or [published feature status](/reference/status) to determine the correct compiler version before running an example. The core language reference remains pinned to alpha.1, while newer guides identify their own prereleases. For exact commands, compare the [alpha.6 CLI](/tooling/cli-alpha6), [AST inspection](/tooling/inspect-ast-alpha6) and [troubleshooting](/tooling/troubleshooting). Use the [Hindi search help](/hi/guide/search-and-navigation) to read these instructions in हिन्दी. ## When search shows no results -Check the spelling of Scarp keywords, use a shorter term, or switch locale using the language menu. Search is provided by Scarp Docs' generated static full-text index and needs JavaScript enabled to display results. The offline Chromium suite also checks the no-results message and that **Esc** returns focus to the search trigger when opened from that button. A failed local index load is not evidence that a compiler feature does not exist: use the [page navigation](/guide/learning-path) or the [compiler's published release notes](/releases/v0.0.6-alpha.1) instead. +Check the spelling of Scarp keywords, use a shorter term, or switch locale using the language menu. Search is provided by Scarp Docs' generated static full-text index and needs JavaScript enabled to display results. The offline Chromium suite also checks the no-results message and that **Esc** returns focus to the search trigger when opened from that button. A failed local index load is not evidence that a compiler feature does not exist: use the [page navigation](/guide/learning-path) or the [compiler's published release notes](/releases/v0.0.6-alpha.3) instead. + +## Keyboard and clipboard accessibility + +After results load, press **↓** to focus the first search link or **↑** to focus the last. Use Up/Down to move between links, **Enter** to open one, and **Esc** to close the dialog. Normal **Tab** navigation remains available. Closing a search opened from its trigger restores focus to that trigger. + +Use the [version-aware Command Finder](/tooling/command-finder) with the keyboard to select the installed compiler. Older versions explicitly indicate unavailable commands. If **Copy commands** fails or the browser denies clipboard permission, the command block remains keyboard-focusable and can be selected manually. The real Chromium suite tests keyboard navigation, permission denial, 320px layouts and reduced motion. **Automated testing does not replace a manual screen-reader review or constitute WCAG certification.** diff --git a/docs/hi/guide/search-and-navigation.md b/docs/hi/guide/search-and-navigation.md index d279481..a40bb9e 100644 --- a/docs/hi/guide/search-and-navigation.md +++ b/docs/hi/guide/search-and-navigation.md @@ -13,10 +13,16 @@ Search button के साथ दिखाई देने वाला **K** s ## सही compiler version चुनें -Search परिणाम में कोई **पुराना** tutorial भी हो सकता है। Code चलाने से पहले [सात-चरण वाला learning path](/hi/guide/learning-path), [नवीनतम installation](/hi/guide/installation-alpha6) और [प्रकाशित feature status](/hi/reference/status) देखें। मुख्य language reference alpha.1 पर आधारित है; नई सुविधाएँ अपने अलग प्रकाशित compiler releases से सत्यापित हैं। +Search परिणाम में कोई **पुराना** tutorial भी हो सकता है। Code चलाने से पहले [सात-चरण वाला learning path](/hi/guide/learning-path), [नवीनतम प्रकाशित compiler डाउनलोड](/hi/releases/v0.0.6-alpha.3) और [प्रकाशित feature status](/hi/reference/status) देखें। मुख्य language reference alpha.1 पर आधारित है; नई सुविधाएँ अपने अलग प्रकाशित compiler releases से सत्यापित हैं। सही CLI command पहचानने के लिए [alpha.6 CLI](/hi/tooling/cli-alpha6), [AST inspection](/hi/tooling/inspect-ast-alpha6) तथा [troubleshooting](/hi/tooling/troubleshooting) पढ़ें। [English search help](/guide/search-and-navigation) भी उपलब्ध है। ## कोई परिणाम न मिले तो -Keyword की spelling जाँचें, छोटा शब्द खोजें या भाषा बदलें। Local search को परिणाम दिखाने के लिए browser में JavaScript चाहिए, क्योंकि index static website से लोड होता है। Offline Chromium परीक्षण में कोई परिणाम न मिलने वाला संदेश और search button से खोलने के बाद **Esc** दबाने पर keyboard focus वापस आने की भी जाँच होती है। Search विफल होने से यह सिद्ध **नहीं** होता कि compiler में कोई सुविधा नहीं है। ऐसे में [learning path](/hi/guide/learning-path) या [प्रकाशित compiler release](/hi/releases/v0.0.6-alpha.1) से सीधे आगे बढ़ें। +Keyword की spelling जाँचें, छोटा शब्द खोजें या भाषा बदलें। Local search को परिणाम दिखाने के लिए browser में JavaScript चाहिए, क्योंकि index static website से लोड होता है। Offline Chromium परीक्षण में कोई परिणाम न मिलने वाला संदेश और search button से खोलने के बाद **Esc** दबाने पर keyboard focus वापस आने की भी जाँच होती है। Search विफल होने से यह सिद्ध **नहीं** होता कि compiler में कोई सुविधा नहीं है। ऐसे में [learning path](/hi/guide/learning-path) या [प्रकाशित compiler release](/hi/releases/v0.0.6-alpha.3) से सीधे आगे बढ़ें। + +## Keyboard और clipboard accessibility + +Search में परिणाम आने के बाद **↓** से पहला link, **↑** से आखिरी link चुन सकते हैं। परिणामों में ↑/↓ से आगे-पीछे जाएँ; **Enter** से link खोलें। **Tab** से भी सामान्य क्रम में चल सकते हैं और **Esc** से search बंद कर सकते हैं। Search button से खोलने पर बंद करने के बाद focus उसी button पर लौटता है। + +[Version-aware Command Finder](/hi/tooling/command-finder) में keyboard से compiler version चुन सकते हैं। पुराने version में अनुपलब्ध commands का स्पष्ट हिन्दी संदेश मिलता है। **कॉपी करें** काम न करे या browser permission मना कर दे, तो command text चयन के लिए उपलब्ध रहता है। स्वचालित Chromium परीक्षण keyboard, अनुमति-अस्वीकृति, 320px layout और reduced-motion की जाँच करते हैं; इन्हें **manual screen-reader या पूर्ण WCAG certification** न समझें। diff --git a/scripts/scarp-site/browser-test.ts b/scripts/scarp-site/browser-test.ts index 959626a..31f554c 100644 --- a/scripts/scarp-site/browser-test.ts +++ b/scripts/scarp-site/browser-test.ts @@ -93,11 +93,48 @@ async function testLocale( assert.match(await widget.locator('pre').innerText(), /scarp inspect symbols --json FILE\.srp/) assert.equal(await widget.locator('pre').getAttribute('tabindex'),'0') - await widget.locator('#scarp-release').selectOption('v0.0.5-alpha.1') - assert.equal(await widget.locator('pre').count(),0) + // Use the real keyboard to change release choices, not only selectOption. + const releaseChoice=widget.locator('#scarp-release') + await releaseChoice.focus() + await releaseChoice.press('Home') + assert.equal(await releaseChoice.inputValue(),'v0.0.5-alpha.1') + await widget.locator('pre').waitFor({state:'detached'}) assert.match(await widget.locator('.finder-result').innerText(),/v0\.0\.6-alpha\.1/) - await widget.locator('#scarp-release').selectOption(LATEST_RELEASE) + await releaseChoice.press('End') + assert.equal(await releaseChoice.inputValue(),LATEST_RELEASE) await widget.locator('pre').waitFor({state:'visible'}) + assert.equal(await widget.locator('.finder-result').getAttribute('aria-live'),'polite') + assertions++ + + // Deterministic in-page clipboard stubs: no OS permissions or external APIs. + await page.evaluate(()=>{ + const view=window as Window & { __scarpCopied?:string } + Object.defineProperty(navigator,'clipboard',{ + configurable:true, + value:{writeText:async (value:string)=>{view.__scarpCopied=value}} + }) + }) + const copy=widget.locator('.finder-result button') + await copy.focus() + await page.keyboard.press('Enter') + await page.waitForFunction(()=>(window as Window & { __scarpCopied?:string }) + .__scarpCopied?.includes('scarp inspect symbols')) + assert.match(await page.evaluate(()=>(window as Window & { __scarpCopied?:string }) + .__scarpCopied??''),/scarp inspect symbols --json FILE\.srp/) + await page.getByRole('button',{name:lang==='hi'?'कॉपी हो गया':'Copied'}).waitFor() + await page.evaluate(()=>{ + Object.defineProperty(navigator,'clipboard',{ + configurable:true,value:{writeText:async()=>{throw new DOMException('Denied','NotAllowedError')}} + }) + }) + await copy.click() + await page.getByRole('button',{ + name:lang==='hi' + ?'कॉपी नहीं हो पाया। Command text चुनकर स्वयं कॉपी करें।' + :'Copy failed. Select the command text manually.' + }).waitFor() + assert.equal(await widget.locator('pre').getAttribute('tabindex'),'0', + lang+': command text must remain selectable after clipboard denial') assertions++ const theme=page.locator('#theme-toggle') @@ -117,8 +154,18 @@ async function testLocale( const searchInput=page.locator('#search-query') assert.equal(await searchInput.evaluate(node=>document.activeElement===node),true) await searchInput.fill('funcra') - await page.locator('#search-results li a').first().waitFor({state:'visible',timeout:15000}) - assert.ok((await page.locator('#search-results li a').first().getAttribute('href'))?.startsWith(prefix)) + const results=page.locator('#search-results li a') + await results.first().waitFor({state:'visible',timeout:15000}) + assert.ok((await results.first().getAttribute('href'))?.startsWith(prefix)) + await searchInput.press('ArrowDown') + assert.equal(await results.first().evaluate(node=>document.activeElement===node),true, + lang+': ArrowDown must focus the first search result') + await page.keyboard.press('ArrowUp') + assert.equal(await results.last().evaluate(node=>document.activeElement===node),true, + lang+': ArrowUp must cycle through search results') + await page.keyboard.press('ArrowDown') + assert.equal(await results.first().evaluate(node=>document.activeElement===node),true) + await searchInput.focus() await searchInput.fill('zzqvkpwmntrxhfbjkldsrp9776') await page.getByRole('status').getByText( lang==='hi'?'कोई परिणाम नहीं मिला':'No results found',{exact:true}).waitFor() @@ -158,6 +205,11 @@ async function mobile( await page.locator('#search-query').waitFor({state:'visible'}) await page.keyboard.press('Escape') assert.deepEqual(outside,[]) + await page.setViewportSize({width:320,height:800}) + await page.emulateMedia({reducedMotion:'reduce'}) + assert.equal(await page.evaluate(()=>matchMedia('(prefers-reduced-motion: reduce)').matches),true) + const narrowOverflow=await page.evaluate(()=>document.documentElement.scrollWidth-window.innerWidth) + assert.ok(narrowOverflow<=2,lang+': 320px layout overflow '+narrowOverflow) const audit=await auditOfflineAccessibility(page,lang+'-engine-mobile', join(previewRoot,'audit-results')) assert.ok(audit.passes>0) @@ -175,7 +227,7 @@ try{ await mobile(browser,origin,locale) } console.log('PASS: '+assertions+ - ' Scarp-owned engine Chromium tests: translated routes, local search, theme, CLI, 375px navigation and axe WCAG A/AA scans') + ' Scarp-owned engine Chromium tests: keyboard search, clipboard success/denial, release selector, bilingual UI, 320px reflow, reduced motion and axe A/AA scans') }catch(error){ console.error('FAIL: Scarp-owned engine Chromium: '+String(error)) process.exitCode=1 diff --git a/scripts/scarp-site/client.ts b/scripts/scarp-site/client.ts index c56315f..b22ea57 100644 --- a/scripts/scarp-site/client.ts +++ b/scripts/scarp-site/client.ts @@ -161,6 +161,21 @@ function installSearch():void { else open.focus({preventScroll:true}) }) input.addEventListener('input',()=>{void refresh()}) + // Arrow navigation keeps search results reachable without a mouse. Ordinary + // Tab/Shift+Tab remain available, and Enter uses the native link behavior. + dialog.addEventListener('keydown', event=>{ + if(event.key!=='ArrowDown'&&event.key!=='ArrowUp')return + const target=event.target + if(target!==input&&!(target instanceof Node&&output.contains(target)))return + const links=[...output.querySelectorAll('a[href]')] + if(!links.length)return + const index=links.findIndex(link=>link===target) + const direction=event.key==='ArrowDown'?1:-1 + const next=index<0?(direction===1?0:links.length-1): + (index+direction+links.length)%links.length + event.preventDefault() + links[next]?.focus() + }) document.addEventListener('keydown',event=>{ // Search inputs may consume Escape to clear their own value. The explicit // handler guarantees the dialog always closes and restores trigger focus. From d18d7263b1a3f6fe843d222f33b72f39a8533530 Mon Sep 17 00:00:00 2001 From: Sanskar Date: Sat, 7 Nov 2026 20:18:46 +0530 Subject: [PATCH 2/3] test(a11y): fix browser clipboard stub serialization and release-aware Hindi links --- scripts/lib/hindi-search-policy.ts | 4 +++- scripts/scarp-site/browser-test.ts | 29 +++++++++++++---------------- scripts/test-hindi-search.ts | 5 ++++- 3 files changed, 20 insertions(+), 18 deletions(-) diff --git a/scripts/lib/hindi-search-policy.ts b/scripts/lib/hindi-search-policy.ts index 085a26f..1384be2 100644 --- a/scripts/lib/hindi-search-policy.ts +++ b/scripts/lib/hindi-search-policy.ts @@ -1,3 +1,5 @@ +import { LATEST_RELEASE } from './command-finder.ts' + /** Guard user-facing language consistency without importing the VitePress app. */ interface SearchTranslationBlock { [key: string]: string | Record | undefined @@ -89,7 +91,7 @@ export function validateHindiLocalSearch( for (const marker of [ 'Ctrl+K','⌘+K','Enter','Esc', ']('+localePrefix+'/guide/learning-path)', - ']('+localePrefix+'/guide/installation-alpha6)', + ']('+localePrefix+'/releases/'+LATEST_RELEASE+')', ']('+localePrefix+'/reference/status)', ']('+localePrefix+'/tooling/cli-alpha6)' ]) { diff --git a/scripts/scarp-site/browser-test.ts b/scripts/scarp-site/browser-test.ts index 31f554c..98a2a59 100644 --- a/scripts/scarp-site/browser-test.ts +++ b/scripts/scarp-site/browser-test.ts @@ -107,26 +107,23 @@ async function testLocale( assertions++ // Deterministic in-page clipboard stubs: no OS permissions or external APIs. - await page.evaluate(()=>{ - const view=window as Window & { __scarpCopied?:string } - Object.defineProperty(navigator,'clipboard',{ - configurable:true, - value:{writeText:async (value:string)=>{view.__scarpCopied=value}} - }) - }) + // Literal browser-side JS avoids injecting tsx/esbuild __name helpers + // that do not exist inside Playwright's isolated page runtime. + await page.evaluate(`Object.defineProperty(navigator,'clipboard',{ + configurable:true, + value:{writeText(value){window.__scarpCopied=value;return Promise.resolve()}} + })`) const copy=widget.locator('.finder-result button') await copy.focus() await page.keyboard.press('Enter') - await page.waitForFunction(()=>(window as Window & { __scarpCopied?:string }) - .__scarpCopied?.includes('scarp inspect symbols')) - assert.match(await page.evaluate(()=>(window as Window & { __scarpCopied?:string }) - .__scarpCopied??''),/scarp inspect symbols --json FILE\.srp/) + await page.waitForFunction("window.__scarpCopied?.includes('scarp inspect symbols')") + assert.match(await page.evaluate("window.__scarpCopied || ''"), + /scarp inspect symbols --json FILE\.srp/) await page.getByRole('button',{name:lang==='hi'?'कॉपी हो गया':'Copied'}).waitFor() - await page.evaluate(()=>{ - Object.defineProperty(navigator,'clipboard',{ - configurable:true,value:{writeText:async()=>{throw new DOMException('Denied','NotAllowedError')}} - }) - }) + await page.evaluate(`Object.defineProperty(navigator,'clipboard',{ + configurable:true, + value:{writeText(){return Promise.reject(new DOMException('Denied','NotAllowedError'))}} + })`) await copy.click() await page.getByRole('button',{ name:lang==='hi' diff --git a/scripts/test-hindi-search.ts b/scripts/test-hindi-search.ts index ee6538b..a41ccbe 100644 --- a/scripts/test-hindi-search.ts +++ b/scripts/test-hindi-search.ts @@ -3,6 +3,7 @@ import { readFileSync } from 'node:fs' import { resolve } from 'node:path' import { fileURLToPath } from 'node:url' import { localSearchOptions } from '../docs/.vitepress/local-search.ts' +import { LATEST_RELEASE } from './lib/command-finder.ts' import { validateHindiLocalSearch, type SearchLocalizationOptions, @@ -64,6 +65,8 @@ mutatePage('no local provider','config',"provider: 'local'","provider: 'algolia' mutatePage('no search options','config',"options: localSearchOptions","options: {}") mutatePage('missing Hindi sidebar','config',"link: '/hi/guide/search-and-navigation'","link: '/hi/guide/missing'") mutatePage('missing theme access label','config',"langMenuLabel: 'भाषा बदलें'","langMenuLabel: 'Language'") +mutatePage('missing current compiler release link','english', + '](/releases/'+LATEST_RELEASE+')','](/releases/v0.0.5-alpha.1)') mutatePage('missing Hindi shortcut','hindi','Ctrl+K','Alt+K') mutatePage('missing English shortcut','english','⌘+K','Ctrl+J') mutatePage('missing Hindi crosslink','hindi','](/guide/search-and-navigation)','](/hi/guide/search-and-navigation)') @@ -72,4 +75,4 @@ mutatePage('missing Hindi intro link','hindiIntro','](/hi/guide/search-and-navig mutatePage('missing README Hindi path','readme','docs/hi/guide/search-and-navigation.md','docs/hi/guide/not-found.md') assert.ok(validateHindiLocalSearch(localSearchOptions,{...pages,hindi:pages.hindi.replace(/[\u0900-\u097f]/gu,'')}).length, 'Hindi locale cannot silently become untranslated') -console.log('PASS: 17 negative regressions for Hindi search UI, locale navigation, language parity and keyboard access') +console.log('PASS: 18 negative regressions for Hindi search UI, locale navigation, current release, language parity and keyboard access') From f4cf71c37cc6fabd5f91bce720cf7d85d8782cfc Mon Sep 17 00:00:00 2001 From: Sanskar Date: Sat, 7 Nov 2026 20:19:54 +0530 Subject: [PATCH 3/3] test(i18n): remove all release-link occurrences in negative mutation --- scripts/test-hindi-search.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/scripts/test-hindi-search.ts b/scripts/test-hindi-search.ts index a41ccbe..4ba9490 100644 --- a/scripts/test-hindi-search.ts +++ b/scripts/test-hindi-search.ts @@ -65,8 +65,12 @@ mutatePage('no local provider','config',"provider: 'local'","provider: 'algolia' mutatePage('no search options','config',"options: localSearchOptions","options: {}") mutatePage('missing Hindi sidebar','config',"link: '/hi/guide/search-and-navigation'","link: '/hi/guide/missing'") mutatePage('missing theme access label','config',"langMenuLabel: 'भाषा बदलें'","langMenuLabel: 'Language'") -mutatePage('missing current compiler release link','english', - '](/releases/'+LATEST_RELEASE+')','](/releases/v0.0.5-alpha.1)') +// Both the download link and release-notes fallback mention the latest tag. +const withoutLatest=pages.english.replaceAll('](/releases/'+LATEST_RELEASE+')', + '](/releases/v0.0.5-alpha.1)') +assert.notEqual(withoutLatest,pages.english,'latest release link must exist') +assert.ok(validateHindiLocalSearch(localSearchOptions,{...pages,english:withoutLatest}).length>0, + 'removing all current release links must fail the localized-search policy') mutatePage('missing Hindi shortcut','hindi','Ctrl+K','Alt+K') mutatePage('missing English shortcut','english','⌘+K','Ctrl+J') mutatePage('missing Hindi crosslink','hindi','](/guide/search-and-navigation)','](/hi/guide/search-and-navigation)')