From 4f74b9d165feedba4ad6d255473e6848fdda5a52 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sun, 25 Oct 2026 19:53:36 +0700 Subject: [PATCH 1/8] test(web): add headless Chromium bilingual viewport and keyboard navigation smoke --- scripts/test-public-browser-qa.py | 163 ++++++++++++++++++++++++++++++ 1 file changed, 163 insertions(+) create mode 100644 scripts/test-public-browser-qa.py diff --git a/scripts/test-public-browser-qa.py b/scripts/test-public-browser-qa.py new file mode 100644 index 000000000..aee56de86 --- /dev/null +++ b/scripts/test-public-browser-qa.py @@ -0,0 +1,163 @@ +#!/usr/bin/env python3 +"""Real Chromium smoke checks of the fully-rendered ARSAS product website. + +The run is explicitly lab/headless evidence, NOT real mobile hardware, WCAG +certification, NVDA sign-off, or CrUX field Core Web Vitals. +""" +from __future__ import annotations + +import argparse +import functools +import json +import threading +from datetime import datetime, timezone +from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer +from pathlib import Path +from urllib.parse import urljoin + +from playwright.sync_api import sync_playwright + +PAIRS = ( + ("index.html", "id.html"), + ("download.html", "unduh.html"), + ("quick-start.html", "panduan-mulai-arsas.html"), + ("demo.html", "demo-arsas.html"), + ("faq.html", "faq-arsas.html"), + ("compatibility.html", "bukti-kompatibilitas.html"), +) +WIDTHS = (360, 390, 768, 1280, 1920) + + +class QuietHandler(SimpleHTTPRequestHandler): + def log_message(self, *args: object) -> None: + pass + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--site", type=Path, default=Path("_site")) + parser.add_argument("--output", type=Path, default=Path("_browser-qa")) + args = parser.parse_args() + site = args.site.resolve() + if not site.is_dir(): + parser.error(f"Site directory missing: {site}") + output = args.output.resolve() + output.mkdir(parents=True, exist_ok=True) + server = ThreadingHTTPServer( + ("127.0.0.1", 0), + functools.partial(QuietHandler, directory=str(site)), + ) + server.daemon_threads = True + thread = threading.Thread(target=server.serve_forever, daemon=True) + thread.start() + root = f"http://127.0.0.1:{server.server_port}/" + failures: list[str] = [] + rows: list[dict[str, object]] = [] + checked = 0 + try: + with sync_playwright() as playwright: + browser = playwright.chromium.launch(headless=True) + try: + for width in WIDTHS: + pages = PAIRS if width in (390, 1280) else PAIRS[:2] + for english, indonesian in pages: + for lang, path in (("en", english), ("id", indonesian)): + checked += 1 + context = browser.new_context( + viewport={"width": width, "height": 850}, + device_scale_factor=1, + reduced_motion="reduce", + ) + page = context.new_page() + errors: list[str] = [] + page.on("pageerror", lambda exc, error_list=errors: error_list.append(str(exc))) + try: + response = page.goto(urljoin(root, path), wait_until="load", timeout=30000) + if response is None or response.status != 200: + raise AssertionError(f"HTTP {response.status if response else 'none'}") + actual_lang = page.locator("html").get_attribute("lang") + if actual_lang != lang: + errors.append(f"html lang={actual_lang}, expected {lang}") + if page.locator("main#main").count() != 1: + errors.append("missing main#main") + h1 = page.locator("main h1").count() + if h1 != 1: + errors.append(f"expected one main h1, got {h1}") + if page.locator('a.skip-link[href="#main"]').count() != 1: + errors.append("broken skip-to-content link") + overflow = page.evaluate( + "() => ({page: document.documentElement.scrollWidth, viewport: innerWidth})" + ) + if overflow["page"] > overflow["viewport"] + 2: + errors.append(f"horizontal overflow {overflow['page']}>{overflow['viewport']}") + if width < 1081: + toggle = page.locator("button[data-menu-toggle]") + if not toggle.is_visible(): + errors.append("mobile navigation toggle hidden") + else: + expected_open = "Buka navigasi" if lang == "id" else "Open navigation" + expected_close = "Tutup navigasi" if lang == "id" else "Close navigation" + if toggle.get_attribute("aria-label") != expected_open: + errors.append("incorrect mobile menu accessible open name") + toggle.click() + if toggle.get_attribute("aria-expanded") != "true": + errors.append("mobile menu did not open") + if toggle.get_attribute("aria-label") != expected_close: + errors.append("incorrect mobile menu accessible close name") + page.keyboard.press("Escape") + if toggle.get_attribute("aria-expanded") != "false": + errors.append("Escape did not close menu") + toggle.click() + page.locator("nav[data-nav-links] a[data-nav-page='capabilities'] span[data-lang='" + lang + "']").click() + if page.url.split("?")[0].split("/")[-1] != "features.html": + errors.append("nested localized navigation click failed") + if page.locator("button[data-menu-toggle]").get_attribute("aria-expanded") != "false": + errors.append("nested link did not close menu") + else: + if not page.locator("nav[data-nav-links]").is_visible(): + errors.append("desktop primary navigation hidden") + if width in (390, 1280) and path in ("index.html", "id.html"): + screenshot_path = output / f"homepage-{lang}-{width}px.png" + page.goto(urljoin(root, path), wait_until="load") + page.screenshot(path=str(screenshot_path), full_page=True, animations="disabled") + except Exception as exc: + errors.append(f"{type(exc).__name__}: {exc}") + finally: + rows.append({"page": path, "width": width, "lang": lang, "errors": errors}) + failures.extend(f"{path} at {width}px: {msg}" for msg in errors) + page.close() + context.close() + finally: + browser.close() + finally: + server.shutdown() + server.server_close() + report = { + "scope": "headless Chromium lab browser smoke; not physical-mobile, NVDA, or field CWV", + "timestamp_utc": datetime.now(timezone.utc).isoformat(), + "checks": checked, + "failures": len(failures), + "cases": rows, + } + (output / "browser-qa.json").write_text(json.dumps(report, indent=2) + "\n", encoding="utf-8") + summary = [ + "# ARSAS public-site browser QA", + "", + f"- Browser: Playwright Chromium (headless)", + f"- Widths: {', '.join(str(x) for x in WIDTHS)} px", + f"- Page/viewport tests: **{checked}**", + f"- Findings: **{len(failures)}**", + "- Scope: viewport overflow, live JS, EN/ID header, headings/skip link, mobile menu Escape and nested-label clicks", + "- Limitations: not NVDA, WCAG certification, physical device, Lighthouse or field Core Web Vitals", + "", + ] + summary += ["## Findings", "", *[f"- {finding}" for finding in failures]] if failures else [ + "No test failures observed in this headless Chromium run." + ] + (output / "browser-qa.md").write_text("\n".join(summary) + "\n", encoding="utf-8") + print("\n".join(summary)) + return 1 if failures else 0 + + +if __name__ == "__main__": + raise SystemExit(main()) From 1a0736ff459d768836bd3c2ebe6e268e577f0dc5 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sun, 25 Oct 2026 19:53:59 +0700 Subject: [PATCH 2/8] ci(site): run real headless Chromium EN-ID viewport and keyboard QA --- .../workflows/public-website-browser-qa.yml | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 .github/workflows/public-website-browser-qa.yml diff --git a/.github/workflows/public-website-browser-qa.yml b/.github/workflows/public-website-browser-qa.yml new file mode 100644 index 000000000..0d31cbe7a --- /dev/null +++ b/.github/workflows/public-website-browser-qa.yml @@ -0,0 +1,66 @@ +name: Public website browser QA + +on: + pull_request: + branches: [main] + paths: + - "landing/**" + - "scripts/test-public-browser-qa.py" + - ".github/workflows/public-website-browser-qa.yml" + push: + branches: [main] + paths: + - "landing/**" + - "scripts/test-public-browser-qa.py" + - ".github/workflows/public-website-browser-qa.yml" + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: public-browser-qa-${{ github.ref }} + cancel-in-progress: true + +jobs: + chromium: + name: Headless Chromium EN-ID visual and keyboard smoke + runs-on: ubuntu-latest + timeout-minutes: 20 + steps: + - uses: actions/checkout@v4 + with: + show-progress: false + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + - name: Install Playwright with isolated Chromium + run: | + python -m pip install --disable-pip-version-check "playwright==1.56.0" + python -m playwright install --with-deps chromium + - name: Generate exact public v1.6.42 website + run: | + set -euo pipefail + cp landing/latest.json /tmp/arsas-published.json + python scripts/build-product-site.py --output _site --release-evidence /tmp/arsas-published.json + python scripts/generate-privacy-pages.py --output _site --release-evidence /tmp/arsas-published.json --measurement-id "" + python scripts/build-responsive-media.py _site + python scripts/inject-site-measurement.py _site --measurement-id "" + - name: Smoke EN/ID pages on Chromium 360-1920px + run: | + set -euo pipefail + python scripts/test-public-browser-qa.py --site _site --output _browser-qa + - name: Preserve browser evidence + if: always() + uses: actions/upload-artifact@v4 + with: + name: arsas-public-site-browser-qa + path: _browser-qa/ + if-no-files-found: warn + retention-days: 30 + - name: Summarize browser findings + if: always() + run: | + if test -f _browser-qa/browser-qa.md; then + cat _browser-qa/browser-qa.md >> "$GITHUB_STEP_SUMMARY" + fi From 0b2cdbd00bd9335038ee88556397f30ab2bffbe8 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sun, 25 Oct 2026 19:54:47 +0700 Subject: [PATCH 3/8] ci(site): provision Pillow for responsive image variants in browser smoke --- .github/workflows/public-website-browser-qa.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/public-website-browser-qa.yml b/.github/workflows/public-website-browser-qa.yml index 0d31cbe7a..57e6fa9ef 100644 --- a/.github/workflows/public-website-browser-qa.yml +++ b/.github/workflows/public-website-browser-qa.yml @@ -36,7 +36,7 @@ jobs: python-version: "3.12" - name: Install Playwright with isolated Chromium run: | - python -m pip install --disable-pip-version-check "playwright==1.56.0" + python -m pip install --disable-pip-version-check "playwright==1.56.0" "pillow>=10,<13" python -m playwright install --with-deps chromium - name: Generate exact public v1.6.42 website run: | From 9b7781c60d77a3c706c232b11c7212b5ecc63bcf Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sun, 25 Oct 2026 19:57:35 +0700 Subject: [PATCH 4/8] ci(web): consolidate Chromium QA in existing measurement workflow without CI-sprawl increase --- .github/workflows/site-measurement.yml | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/.github/workflows/site-measurement.yml b/.github/workflows/site-measurement.yml index 8c51228b6..fbf459080 100644 --- a/.github/workflows/site-measurement.yml +++ b/.github/workflows/site-measurement.yml @@ -22,6 +22,7 @@ on: - "scripts/inject-site-measurement.py" - "scripts/validate-site-measurement.py" - "scripts/check-site-health.py" + - "scripts/test-public-browser-qa.py" - "scripts/check-measurement-readiness.py" - "scripts/build-site-measurement-report.py" - "scripts/validate-measurement-report.py" @@ -38,6 +39,7 @@ on: - "scripts/inject-site-measurement.py" - "scripts/validate-site-measurement.py" - "scripts/check-site-health.py" + - "scripts/test-public-browser-qa.py" - "scripts/check-measurement-readiness.py" - "scripts/build-site-measurement-report.py" - "scripts/validate-measurement-report.py" @@ -69,8 +71,10 @@ jobs: with: python-version: "3.12" - - name: Install offline report dependency - run: python -m pip install --disable-pip-version-check --quiet requests + - name: Install measurement and browser QA dependencies + run: | + python -m pip install --disable-pip-version-check --quiet requests "playwright==1.56.0" + python -m playwright install --with-deps chromium - name: Prepare deterministic stable release evidence shell: bash @@ -110,6 +114,9 @@ jobs: continue-on-error: true run: python scripts/check-site-health.py --site _site --output _measurement + - name: Exercise real Chromium bilingual and mobile navigation QA + run: python scripts/test-public-browser-qa.py --site _site --output _measurement/browser-qa + - name: Exercise disabled readiness state env: GA4_MEASUREMENT_ID: "" From 4f34d05ef2ebca8603108baade89b095a331c9ae Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sun, 25 Oct 2026 19:57:40 +0700 Subject: [PATCH 5/8] ci(web): remove redundant workflow; preserve exact CI topology budget --- .../workflows/public-website-browser-qa.yml | 66 ------------------- 1 file changed, 66 deletions(-) delete mode 100644 .github/workflows/public-website-browser-qa.yml diff --git a/.github/workflows/public-website-browser-qa.yml b/.github/workflows/public-website-browser-qa.yml deleted file mode 100644 index 57e6fa9ef..000000000 --- a/.github/workflows/public-website-browser-qa.yml +++ /dev/null @@ -1,66 +0,0 @@ -name: Public website browser QA - -on: - pull_request: - branches: [main] - paths: - - "landing/**" - - "scripts/test-public-browser-qa.py" - - ".github/workflows/public-website-browser-qa.yml" - push: - branches: [main] - paths: - - "landing/**" - - "scripts/test-public-browser-qa.py" - - ".github/workflows/public-website-browser-qa.yml" - workflow_dispatch: - -permissions: - contents: read - -concurrency: - group: public-browser-qa-${{ github.ref }} - cancel-in-progress: true - -jobs: - chromium: - name: Headless Chromium EN-ID visual and keyboard smoke - runs-on: ubuntu-latest - timeout-minutes: 20 - steps: - - uses: actions/checkout@v4 - with: - show-progress: false - - uses: actions/setup-python@v5 - with: - python-version: "3.12" - - name: Install Playwright with isolated Chromium - run: | - python -m pip install --disable-pip-version-check "playwright==1.56.0" "pillow>=10,<13" - python -m playwright install --with-deps chromium - - name: Generate exact public v1.6.42 website - run: | - set -euo pipefail - cp landing/latest.json /tmp/arsas-published.json - python scripts/build-product-site.py --output _site --release-evidence /tmp/arsas-published.json - python scripts/generate-privacy-pages.py --output _site --release-evidence /tmp/arsas-published.json --measurement-id "" - python scripts/build-responsive-media.py _site - python scripts/inject-site-measurement.py _site --measurement-id "" - - name: Smoke EN/ID pages on Chromium 360-1920px - run: | - set -euo pipefail - python scripts/test-public-browser-qa.py --site _site --output _browser-qa - - name: Preserve browser evidence - if: always() - uses: actions/upload-artifact@v4 - with: - name: arsas-public-site-browser-qa - path: _browser-qa/ - if-no-files-found: warn - retention-days: 30 - - name: Summarize browser findings - if: always() - run: | - if test -f _browser-qa/browser-qa.md; then - cat _browser-qa/browser-qa.md >> "$GITHUB_STEP_SUMMARY" - fi From 3e4ae8a471a6443f5ff973f4963f183d440836ab Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sun, 25 Oct 2026 19:59:02 +0700 Subject: [PATCH 6/8] test(web): expose exact overflowing elements and screenshots for small Download pages --- scripts/test-public-browser-qa.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/scripts/test-public-browser-qa.py b/scripts/test-public-browser-qa.py index aee56de86..4c8b54698 100644 --- a/scripts/test-public-browser-qa.py +++ b/scripts/test-public-browser-qa.py @@ -89,7 +89,21 @@ def main() -> int: "() => ({page: document.documentElement.scrollWidth, viewport: innerWidth})" ) if overflow["page"] > overflow["viewport"] + 2: - errors.append(f"horizontal overflow {overflow['page']}>{overflow['viewport']}") + offenders = page.evaluate("""() => [...document.querySelectorAll('body *')] + .filter(el => el.getBoundingClientRect().right > innerWidth + 2) + .slice(0, 8).map(el => ({ + tag: el.tagName.toLowerCase(), + cls: (el.getAttribute('class') || '').slice(0, 70), + right: Math.round(el.getBoundingClientRect().right), + width: Math.round(el.getBoundingClientRect().width), + label: (el.textContent || '').trim().slice(0, 55) + }))""") + errors.append(f"horizontal overflow {overflow['page']}>{overflow['viewport']}; offenders={offenders}") + if path in ("download.html", "unduh.html") and width in (360, 390): + page.screenshot( + path=str(output / f"overflow-{path}-{width}px.png"), + full_page=True, animations="disabled" + ) if width < 1081: toggle = page.locator("button[data-menu-toggle]") if not toggle.is_visible(): From 53e6871a0570ad412dab440303047651fb8d098c Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sun, 25 Oct 2026 20:01:24 +0700 Subject: [PATCH 7/8] fix(site): prevent release-evidence cards from forcing mobile overflow --- landing/adoption.css | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/landing/adoption.css b/landing/adoption.css index 6b29f24db..4a0329ab3 100644 --- a/landing/adoption.css +++ b/landing/adoption.css @@ -206,6 +206,8 @@ gap: 1rem; } .supply-chain-note > * { + min-width: 0; + overflow-wrap: anywhere; padding: 1rem; border: 1px solid var(--line); border-radius: var(--radius-sm); @@ -217,7 +219,7 @@ .quick-start-grid, .evidence-grid, .faq-grid, - .supply-chain-note { grid-template-columns: 1fr; } + .supply-chain-note { grid-template-columns: minmax(0, 1fr); } .demo-shell { grid-template-columns: 1fr; } .demo-controls { flex-direction: row; } .demo-step-button { width: auto; } From 0a22452388c0f029da574e2fbb62a09b06c0ea5d Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sun, 25 Oct 2026 20:01:27 +0700 Subject: [PATCH 8/8] fix(site): wrap long release source hashes on small Download pages --- landing/download.css | 2 ++ 1 file changed, 2 insertions(+) diff --git a/landing/download.css b/landing/download.css index f19557c58..773921d2b 100644 --- a/landing/download.css +++ b/landing/download.css @@ -1 +1,3 @@ .download-hero{padding-bottom:2.5rem}.release-status{display:inline-flex;align-items:center;gap:.62rem;margin-top:1.35rem;padding:.62rem .78rem;border:1px solid rgba(56,189,248,.2);border-radius:13px;color:var(--muted);background:rgba(56,189,248,.055);font-size:.88rem}.download-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:1rem}.download-card{display:flex;flex-direction:column;min-height:410px;padding:1.5rem;border:1px solid var(--line);border-radius:var(--radius);background:linear-gradient(180deg,rgba(255,255,255,.065),rgba(255,255,255,.032));box-shadow:0 18px 55px rgba(0,0,0,.18)}.download-card.recommended{border-color:rgba(56,189,248,.28);background:radial-gradient(circle at 10% 0%,rgba(56,189,248,.13),transparent 18rem),linear-gradient(180deg,rgba(59,130,246,.09),rgba(255,255,255,.03))}.download-card h2{margin-top:.65rem;font-size:clamp(1.75rem,3vw,2.45rem)}.download-card p{color:var(--muted)}.download-meta{margin-top:auto;padding:.85rem 0;color:var(--subtle);font-size:.86rem;overflow-wrap:anywhere}.download-action{width:100%;margin-top:auto}.download-action[aria-disabled="true"]{opacity:.62;pointer-events:none}.download-card .kicker{align-self:flex-start}.release-facts{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:.8rem}.release-fact{padding:1rem 1.05rem;border:1px solid var(--line);border-radius:15px;background:rgba(255,255,255,.035)}.release-fact span{display:block;color:var(--subtle);font-size:.78rem;text-transform:uppercase;letter-spacing:.06em}.release-fact strong{display:block;margin-top:.35rem;font-size:1.05rem}.package-meta{display:grid;gap:.48rem;margin:1.1rem 0 1.25rem;padding:1rem 0;border-top:1px solid var(--line);border-bottom:1px solid var(--line)}.package-meta div{display:grid;grid-template-columns:5.5rem minmax(0,1fr);gap:.75rem}.package-meta dt{color:var(--subtle);font-size:.82rem}.package-meta dd{margin:0;color:var(--text);font-size:.85rem;overflow-wrap:anywhere}.package-comparison{overflow:hidden;border:1px solid var(--line);border-radius:var(--radius);background:rgba(255,255,255,.025)}.comparison-row{display:grid;grid-template-columns:minmax(8rem,.8fr) repeat(2,minmax(0,1.3fr));gap:1rem;padding:.92rem 1.05rem;border-top:1px solid var(--line)}.comparison-row:first-child{border-top:0}.comparison-row>span:first-child{color:var(--subtle);font-weight:680}.comparison-head{background:rgba(56,189,248,.075)}.checksum-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:1rem}.checksum-card{padding:1.25rem;border:1px solid var(--line);border-radius:var(--radius-sm);background:rgba(255,255,255,.035)}.checksum-value{display:block;margin:.85rem 0;padding:.85rem;border:1px solid rgba(125,211,252,.16);background:rgba(3,10,20,.58);font-size:.82rem;line-height:1.55;overflow-wrap:anywhere;user-select:all}.copy-button{cursor:pointer}.verification-command{display:grid;grid-template-columns:auto minmax(0,1fr);align-items:center;gap:.9rem;margin-top:1rem;padding:1rem 1.05rem;border:1px solid var(--line);border-radius:14px;background:rgba(255,255,255,.025)}.verification-command span{color:var(--subtle);font-size:.83rem}.verification-command code{overflow-wrap:anywhere}.release-list{margin:.8rem 0 0;padding-left:1.15rem;color:var(--muted)}.release-list li+li{margin-top:.55rem}.warning-panel{border-color:rgba(251,191,36,.25)!important;background:linear-gradient(145deg,rgba(245,158,11,.07),rgba(255,255,255,.025))!important}.trust-badge{display:inline-flex;margin-top:1rem;padding:.48rem .68rem;border-radius:999px;font-size:.78rem;font-weight:750;letter-spacing:.025em}.trust-badge-warning{border:1px solid rgba(251,191,36,.3);color:#fde68a;background:rgba(245,158,11,.1)}.small-note{margin-top:1rem!important;color:var(--subtle)!important;font-size:.82rem}.text-link{display:inline-flex;margin-top:.7rem;color:#bae6fd;font-weight:650}.text-link:hover{text-decoration:underline}@media(max-width:900px){.release-facts{grid-template-columns:repeat(2,minmax(0,1fr))}}@media(max-width:760px){.download-grid,.checksum-grid{grid-template-columns:1fr}.download-card{min-height:auto}.comparison-row{grid-template-columns:1fr;gap:.35rem;padding:1rem}.comparison-head{display:none}.comparison-row>span:first-child{margin-bottom:.25rem}.comparison-row>span:nth-child(2):before{content:"Installer: ";font-weight:700;color:var(--text)}.comparison-row>span:nth-child(3):before{content:"Portable: ";font-weight:700;color:var(--text)}.verification-command{grid-template-columns:1fr}}@media(max-width:520px){.release-facts{grid-template-columns:1fr}.package-meta div{grid-template-columns:1fr;gap:.2rem}} +/* Prevent full source commit hashes from expanding mobile release-trust cards. */ +body[data-page="download"] main code { overflow-wrap: anywhere; word-break: break-word; }