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
25 changes: 24 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,30 @@ jobs:
with:
name: route-width-screenshots
path: test-results/route-width
if-no-files-found: error
if-no-files-found: warn

pwa:
runs-on: ubuntu-latest
timeout-minutes: 30

steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Use Node.js 20.x
uses: actions/setup-node@v6
with:
node-version: 20.x
cache: "npm"

- name: Install dependencies
run: npm ci

- name: Build exported app
run: npm run build

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

- name: Run PWA Smoke
run: npm run test:e2e:pwa
6 changes: 6 additions & 0 deletions docs/accessibility/audit-23-25-26-manual-closure-runbook.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,9 @@ Routes:
- `/en/all-tools`
- `/en/data-code-formats`
- `/en/json-formatter`
- `/en/qr-code-generator`
- `/en/base64-encode-decode`
- `/en/csv-json-converter`
- `/en/jwt-decoder`
- `/en/regex-tester`
- `/en/pipeline-builder`
Expand All @@ -100,11 +102,15 @@ Software keyboard:
- On `/en/json-formatter`, focus the input, type invalid JSON, fix it, and run format.
- Confirm the keyboard does not hide the active input or primary action in a way that prevents completion.
- On `/en/base64-encode-decode`, enter a long URL-like value and copy output.
- On `/en/qr-code-generator`, edit QR content, download PNG, and confirm copy feedback with the keyboard open.
- On `/en/csv-json-converter`, convert a short CSV payload, copy the result, and confirm the Convert action remains reachable.
- On `/en/regex-tester`, edit pattern and sample text with the keyboard open.

Pipeline Builder:

- Open `/en/pipeline-builder`.
- Use the compact Steps, Input/output, and Inspector section jumps in portrait and landscape.
- Select a step, edit its label in Inspector, and return to the input without searching through the full page.
- Confirm the mobile diagnostics are readable without forcing page-level horizontal scrolling.
- Run the default sample or a local sample.
- Confirm run status and output are visible.
Expand Down
25 changes: 23 additions & 2 deletions docs/accessibility/audit-25-26-mobile-a11y-matrix.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ Mobile layout routes:
- `/en/all-tools`
- `/en/data-code-formats`
- `/en/json-formatter`
- `/en/qr-code-generator`
- `/en/base64-encode-decode`
- `/en/csv-json-converter`
- `/en/jwt-decoder`
- `/en/regex-tester`
- `/en/pipeline-builder`
- `/en/trust-center`
- `/en/install-app`
Expand All @@ -31,6 +36,20 @@ Automated checks:
- Page-level horizontal overflow stays within the 2px tolerance.
- Runtime, console, and same-origin critical resource errors fail the smoke.

Core mobile workflows run at both `390x844` and `430x932`:

- QR content update, copy feedback, and PNG download.
- CSV-to-JSON conversion, copy feedback, and JSON download.
- Pipeline sample loading, section jumps, Inspector editing, execution, output, and copy feedback.
- JSON, Base64, JWT, Markdown, Regex, Cron, Image Resizer, JSON Diff, and Text Diff core interactions.

Viewport-pressure checks run for QR, CSV, and Pipeline Builder:

- A focused input remains visible in a reduced-height `390x420` keyboard proxy.
- The primary action remains reachable at `390x420`.
- Portrait-to-landscape transition to `844x390` does not introduce page-level horizontal overflow.
- The primary action remains reachable after the landscape transition.

Accessibility routes:

- `/en`
Expand All @@ -46,13 +65,14 @@ Automated checks:
- Color contrast is covered by token-level guards, because runtime CSS variables are not reliable in every axe environment.
- Skip link, header language menu, mobile navigation focus restoration, command palette, copy feedback, and Pipeline Builder workflows are covered by Playwright smoke journeys.

## Validation Run
## Validation Commands

The June 27, 2026 audit batch passed:
Run the full candidate commit through:

- `npm test -- --run tests/guards/mobile-a11y-audit-issues-262-263.test.ts tests/guards/playwright-smoke-matrix-guard.test.ts tests/guards/a11y-mobile-baseline.test.ts tests/guards/bf-037-a11y-qa-guard.test.ts tests/guards/bf-036-bf-038-accessibility-visual-qa-guard.test.ts`
- `npm test -- --run tests/component/representative-a11y-axe.test.tsx tests/component/phase3-pipeline-builder-page.test.tsx`
- `npm run test:e2e:smoke`
- `npm run test:e2e:mobile`
- `npm run test:e2e:pwa`
- `npm run validate`
- `npm run lint`
Expand All @@ -69,5 +89,6 @@ The following checks still require real device or assistive-technology verificat
- Lighthouse accessibility score verification on representative pages in a stable Chrome/Lighthouse environment.
- Screen-reader output in NVDA, VoiceOver, or TalkBack for Pipeline Builder and external-request confirmation flows.
- Software keyboard overlap on physical mobile devices for multiline tool inputs.
- Real orientation-change behavior and browser chrome/visual viewport interaction on physical devices.

Use `docs/accessibility/audit-23-25-26-manual-closure-runbook.md` for the required closure procedure and issue comment template.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,10 @@
"test": "vitest run",
"test:coverage": "vitest run --coverage",
"test:e2e:smoke": "node scripts/e2e/run-playwright-smoke.js",
"test:e2e:mobile": "node scripts/e2e/run-playwright-smoke.js --mobile-only",
"test:e2e:first-load": "node scripts/e2e/run-playwright-smoke.js --first-load-only --first-load-artifacts",
"test:e2e:route-width": "node scripts/e2e/run-route-width-matrix.js",
"test:e2e:pwa": "node scripts/e2e/run-playwright-smoke.js --pwa",
"test:e2e:pwa": "node scripts/e2e/run-playwright-smoke.js --pwa-only",
"check:i18n": "node scripts/gates/check-i18n.js",
"check:i18n-qa": "node scripts/gates/check-i18n-qa.js",
"check:i18n:ratchet": "node scripts/gates/check-i18n-untranslated-ratchet.js --mode=fail",
Expand Down
Binary file modified public/pwa-screenshots/home-en-1280x720.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified public/pwa-screenshots/install-android.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified public/pwa-screenshots/install-chrome-desktop.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified public/pwa-screenshots/install-edge.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified public/pwa-screenshots/install-firefox.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified public/pwa-screenshots/install-ios-safari.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/pwa-screenshots/install-safari-desktop.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified public/pwa-screenshots/json-formatter-en-1280x720.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
30 changes: 30 additions & 0 deletions public/runtime/theme-manifest-bootstrap.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,34 @@
(function () {
var installPromptBridgeReadySlot = "__byteflowPwaInstallPromptBridgeReady";
var installPromptChangeEvent = "byteflow:pwa-install-prompt-change";
var installPromptInstalledKey = "byteflow:pwa-install:installed";
var installPromptSlot = "__byteflowPwaInstallPrompt";

function notifyInstallPromptChange() {
try {
window.dispatchEvent(new CustomEvent(installPromptChangeEvent));
} catch {}
}

if (!window[installPromptBridgeReadySlot]) {
window[installPromptBridgeReadySlot] = true;
window.addEventListener("beforeinstallprompt", function (event) {
event.preventDefault();
try {
localStorage.removeItem(installPromptInstalledKey);
} catch {}
window[installPromptSlot] = event;
notifyInstallPromptChange();
});
window.addEventListener("appinstalled", function () {
try {
localStorage.setItem(installPromptInstalledKey, "1");
} catch {}
window[installPromptSlot] = null;
notifyInstallPromptChange();
});
}

try {
var locales = ["en","zh-CN","zh-TW","ja","ko","de","fr"];
var p = window.location.pathname || "/";
Expand Down
79 changes: 79 additions & 0 deletions scripts/e2e/playwright-smoke-args.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
const EXCLUSIVE_MODE_FLAGS = new Map([
["--pwa-only", "pwaOnly"],
["--first-load-only", "firstLoadOnly"],
["--input-intents-only", "inputIntentsOnly"],
["--mobile-only", "mobileOnly"],
]);

export function parsePlaywrightSmokeArgs(argv, { defaultPort = 4173 } = {}) {
const args = {
port: defaultPort,
baseUrl: "",
skipServer: false,
includePwa: false,
pwaOnly: false,
firstLoadOnly: false,
writeFirstLoadArtifacts: false,
inputIntentsOnly: false,
mobileOnly: false,
};
const activeExclusiveFlags = new Set();
const unknownArgs = new Set();

for (const arg of argv) {
const exclusiveMode = EXCLUSIVE_MODE_FLAGS.get(arg);
if (exclusiveMode) {
args[exclusiveMode] = true;
activeExclusiveFlags.add(arg);
continue;
}

if (arg === "--skip-server") {
args.skipServer = true;
continue;
}

if (arg === "--pwa") {
args.includePwa = true;
continue;
}

if (arg === "--first-load-artifacts") {
args.writeFirstLoadArtifacts = true;
continue;
}

if (arg.startsWith("--port=")) {
const parsed = Number(arg.slice("--port=".length));
if (Number.isFinite(parsed) && parsed > 0) {
args.port = parsed;
}
continue;
}

if (arg.startsWith("--base-url=")) {
args.baseUrl = arg.slice("--base-url=".length).trim();
continue;
}

unknownArgs.add(arg);
}

const validationErrors = [];
if (unknownArgs.size > 0) {
validationErrors.push(`Unknown argument(s): ${[...unknownArgs].join(", ")}`);
}
if (activeExclusiveFlags.size > 1) {
validationErrors.push(`Conflicting --*-only flags: ${[...activeExclusiveFlags].join(", ")}`);
}
if (validationErrors.length > 0) {
throw new Error(`[playwright-smoke] Invalid arguments:\n- ${validationErrors.join("\n- ")}`);
}

if (!args.baseUrl) {
args.baseUrl = `http://127.0.0.1:${args.port}`;
}

args.baseUrl = args.baseUrl.replace(/\/+$/, "");
return args;
}
19 changes: 19 additions & 0 deletions scripts/e2e/promise-timeout.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
export async function raceWithTimeout(
promise,
timeoutMs,
timeoutMessage,
timerApi = globalThis,
) {
let timeoutId
const timeoutPromise = new Promise((_, reject) => {
timeoutId = timerApi.setTimeout(() => {
reject(new Error(timeoutMessage))
}, timeoutMs)
})

try {
return await Promise.race([promise, timeoutPromise])
} finally {
if (timeoutId !== undefined) timerApi.clearTimeout(timeoutId)
}
}
Loading