diff --git a/.gitignore b/.gitignore index 42425ac6..120a046d 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,7 @@ build/ *.tsbuildinfo # Large local data +/artifacts/ /datasets/raw/* /datasets/processed/* /datasets/cache/* diff --git a/CHANGELOG.md b/CHANGELOG.md index 539e07c4..cc337eca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -652,3 +652,11 @@ Added: - Added the smoke syntax check to the main readiness gate and regression coverage for the non-mutating script contract. - Updated maintenance documentation in `scripts/README.md`, `docs/STORAGE_ARCHITECTURE.md` and `backend/README.md`. - No API contracts, migrations, product capabilities, live provider fetching or AI/model dependency changes were introduced. + +## Sprint 59 workbench screenshot artifacts (2026-06-18) + +- Added `scripts/capture_workbench_screenshots.sh` for optional visual regression handoff screenshots. +- The capture script seeds the explicit offline demo workflow, opens each main workspace and writes viewport PNG screenshots plus a `manifest.json` under ignored local artifacts. +- Desktop screenshots are always captured; mobile screenshots are captured by default and can be disabled with `CAPTURE_MOBILE=0`. +- Added readiness syntax coverage and regression checks for the non-mutating screenshot capture contract. +- No API contracts, migrations, product capabilities, live provider fetching or AI/model dependency changes were introduced. diff --git a/backend/tests/test_readiness_gate.py b/backend/tests/test_readiness_gate.py index 2fb9d9f6..7b5fa8e1 100644 --- a/backend/tests/test_readiness_gate.py +++ b/backend/tests/test_readiness_gate.py @@ -58,6 +58,29 @@ def test_demo_cleanup_dry_run_script_is_dry_run_only() -> None: assert "project_report_html" in content +def test_readiness_gate_checks_workbench_screenshot_capture_syntax() -> None: + script = Path(__file__).resolve().parents[2] / "scripts" / "run_readiness_check.sh" + content = script.read_text(encoding="utf-8") + + assert "bash -n scripts/capture_workbench_screenshots.sh" in content + + +def test_workbench_screenshot_capture_is_artifact_based_and_non_mutating() -> None: + script = Path(__file__).resolve().parents[2] / "scripts" / "capture_workbench_screenshots.sh" + content = script.read_text(encoding="utf-8") + + assert "artifacts/screenshots" in content + assert "/api/v1/demo/workflow" in content + assert "['overview', 'Overview']" in content + assert "['system', 'System']" in content + assert "workspace-nav-${workspaceKey}" in content + assert "manifest.json" in content + assert "page.screenshot" in content + assert "fullPage: false" in content + assert "Playwright is required" in content + assert "--apply" not in content + + def test_readiness_gate_compiles_yolo_preflight_script() -> None: script = Path(__file__).resolve().parents[2] / "scripts" / "run_readiness_check.sh" content = script.read_text(encoding="utf-8") diff --git a/docs/CODEX_EXECUTION_LOG.md b/docs/CODEX_EXECUTION_LOG.md index 4cdf282e..20dd4cf3 100644 --- a/docs/CODEX_EXECUTION_LOG.md +++ b/docs/CODEX_EXECUTION_LOG.md @@ -2323,3 +2323,29 @@ Limitations: Next recommended pass: - Add browser screenshot artifact automation for visual regression handoff. + +## Sprint 59 workbench screenshot artifacts (2026-06-18) + +Changed: +- Added `scripts/capture_workbench_screenshots.sh` for optional visual regression handoff screenshots. +- The script seeds the explicit offline demo workflow, opens Overview, Data, Map, QA/QC, AI Labs, Exports and System, then writes viewport screenshots plus `manifest.json`. +- Desktop capture is always enabled; mobile capture is enabled by default and can be disabled with `CAPTURE_MOBILE=0`. +- Added `/artifacts/` to `.gitignore` so screenshot outputs stay local. +- Added readiness syntax coverage and regression checks for the screenshot capture contract. +- Updated `scripts/README.md`, `docs/TODO.md` and `CHANGELOG.md`. + +Tested: +- `bash -n scripts/capture_workbench_screenshots.sh` +- `cd backend && python -m pytest tests/test_readiness_gate.py -q` (`13 passed`) +- `bash scripts/run_readiness_check.sh` (`221 passed`) +- Local script execution without Playwright fails cleanly with setup instructions instead of producing partial artifacts. +- Browser-plugin visual capture against `http://192.168.10.150:1202` created viewport screenshots for all seven workspaces with no console errors or warnings. + +Open: +- Commit, deploy Tower and keep the visual artifact script available for future Playwright-enabled runners. + +Limitations: +- Playwright/Chromium is intentionally optional and not added to the frontend dependency set. The readiness gate checks script syntax only. + +Next recommended pass: +- Run a backend error-envelope audit for expected user-error paths. diff --git a/docs/TODO.md b/docs/TODO.md index bb65560d..0978aa46 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -59,7 +59,7 @@ This file now starts with the current implementation status. Older preparation/b - [x] Demo workflow orchestration hook decomposition. - [x] Final `App.tsx` import/encoding cleanup and size audit. - [x] Optional final bootstrap-effect extraction. -- [ ] Decide next V1 stabilization focus: browser screenshot artifact automation, backend error-envelope audit, or golden dataset expansion. +- [ ] Decide next V1 stabilization focus: backend error-envelope audit or golden dataset expansion. ## Sprint 8 status @@ -328,4 +328,5 @@ This file now starts with the current implementation status. Older preparation/b - [x] Add export history filtering controls for long-running demo environments. - [x] Add a safe export retention/cleanup command for demo environments. - [x] Add a live dry-run maintenance smoke for demo export cleanup. -- [ ] Add browser screenshot artifact automation for visual regression handoff. +- [x] Add browser screenshot artifact automation for visual regression handoff. +- [ ] Add backend error-envelope audit for expected user-error paths. diff --git a/scripts/README.md b/scripts/README.md index 0c3260bc..80c7b02a 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -61,6 +61,21 @@ dataset selection, QA refresh and export refresh through the same frontend proxy used by the browser. The frontend also exposes stable `data-testid` anchors for Codex/browser click checks on those controls. +Capture visual regression handoff screenshots for the workbench: + +```bash +bash scripts/capture_workbench_screenshots.sh http://192.168.10.150:1202 +CAPTURE_MOBILE=0 bash scripts/capture_workbench_screenshots.sh http://192.168.10.150:1202 /tmp/geointel-screens +``` + +The capture script seeds the explicit offline demo workflow, opens each main +workspace, captures viewport desktop screenshots and, by default, viewport +mobile screenshots. +It writes PNG files plus `manifest.json` under `artifacts/screenshots/...` or a +caller-provided output directory. Playwright/Chromium must be available in the +runner environment; GeoIntel does not add Playwright as a frontend dependency +by default. The main readiness gate checks script syntax only. + Verify the deterministic QA/QC golden benchmark: ```bash diff --git a/scripts/capture_workbench_screenshots.sh b/scripts/capture_workbench_screenshots.sh new file mode 100755 index 00000000..01552ed1 --- /dev/null +++ b/scripts/capture_workbench_screenshots.sh @@ -0,0 +1,170 @@ +#!/usr/bin/env bash +set -euo pipefail + +BASE_URL="${1:-${GE_INTEL_BASE_URL:-http://localhost:1202}}" +OUTPUT_ROOT="${2:-${SCREENSHOT_OUTPUT_DIR:-artifacts/screenshots}}" +TIMESTAMP="$(date -u +"%Y%m%dT%H%M%SZ")" +OUTPUT_DIR="${OUTPUT_ROOT%/}/workbench-${TIMESTAMP}" +CAPTURE_MOBILE="${CAPTURE_MOBILE:-1}" +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "${ROOT}" + +NODE_BIN="" +if command -v node >/dev/null 2>&1; then + NODE_BIN="$(command -v node)" +elif command -v node.exe >/dev/null 2>&1; then + NODE_BIN="$(command -v node.exe)" +fi + +if [ -z "${NODE_BIN}" ]; then + echo "Node.js is required for screenshot capture." >&2 + exit 1 +fi + +if ! "${NODE_BIN}" --input-type=module -e "await import('playwright')" >/dev/null 2>&1; then + cat >&2 <<'EOF' +Playwright is required for screenshot capture but is not available to Node. + +Install or expose Playwright in the runner environment, then retry: + npm install --no-save playwright + npx playwright install chromium + +GeoIntel does not add Playwright as a frontend dependency by default; this +script is an optional visual regression handoff tool. +EOF + exit 2 +fi + +mkdir -p "${OUTPUT_DIR}" +tmp_js="$(mktemp "${ROOT}/.capture_workbench_screenshots.XXXXXX.mjs")" +trap 'rm -f "${tmp_js}"' EXIT + +cat >"${tmp_js}" <<'JS' +import { chromium, request } from 'playwright' +import { mkdir, writeFile } from 'node:fs/promises' +import path from 'node:path' + +const [baseUrl, outputDir, captureMobileRaw] = process.argv.slice(2) +const captureMobile = captureMobileRaw === '1' +const normalizedBaseUrl = baseUrl.replace(/\/$/, '') + +const workspaces = [ + ['overview', 'Overview'], + ['data', 'Data'], + ['map', 'Map'], + ['analysis', 'QA/QC'], + ['ai', 'AI Labs'], + ['exports', 'Exports'], + ['system', 'System'], +] + +const viewports = [ + { name: 'desktop', width: 1366, height: 900 }, +] + +if (captureMobile) { + viewports.push({ name: 'mobile', width: 390, height: 844 }) +} + +function sanitize(value) { + return value.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '') +} + +async function ensureDemoWorkflow() { + const api = await request.newContext({ baseURL: normalizedBaseUrl }) + const response = await api.post('/api/v1/demo/workflow') + if (!response.ok()) { + throw new Error(`Demo workflow seed failed with HTTP ${response.status()}`) + } + const payload = await response.json() + if (!payload?.data?.project_id) { + throw new Error('Demo workflow response did not include data.project_id') + } + await api.dispose() + return payload.data +} + +async function capture() { + await mkdir(outputDir, { recursive: true }) + const demo = await ensureDemoWorkflow() + const browser = await chromium.launch() + const screenshots = [] + const consoleMessages = [] + + try { + for (const viewport of viewports) { + const page = await browser.newPage({ viewport }) + page.on('console', (message) => { + if (['error', 'warning'].includes(message.type())) { + consoleMessages.push({ + viewport: viewport.name, + type: message.type(), + text: message.text(), + }) + } + }) + page.on('pageerror', (error) => { + consoleMessages.push({ + viewport: viewport.name, + type: 'pageerror', + text: error.message, + }) + }) + + await page.goto(normalizedBaseUrl, { waitUntil: 'networkidle' }) + await page.locator('#root').waitFor({ state: 'visible', timeout: 15000 }) + await page.getByText('GeoIntel', { exact: false }).first().waitFor({ timeout: 15000 }) + + const bodyText = await page.locator('body').innerText({ timeout: 15000 }) + if (/vite|webpack|runtime error|failed to compile/i.test(bodyText)) { + throw new Error(`Framework error overlay detected in ${viewport.name} viewport`) + } + + for (const [workspaceKey, label] of workspaces) { + await page.locator(`[data-testid="workspace-nav-${workspaceKey}"]`).click() + await page.waitForTimeout(300) + await page.locator('.workspace-heading').waitFor({ state: 'visible', timeout: 10000 }) + const screenshotName = `${viewport.name}-${sanitize(workspaceKey)}.png` + const screenshotPath = path.join(outputDir, screenshotName) + await page.screenshot({ path: screenshotPath, fullPage: false }) + screenshots.push({ + viewport: viewport.name, + workspace: workspaceKey, + label, + path: screenshotPath, + }) + } + + await page.close() + } + } finally { + await browser.close() + } + + const relevantConsoleMessages = consoleMessages.filter((message) => message.type() !== 'warning') + const manifest = { + captured_at: new Date().toISOString(), + base_url: normalizedBaseUrl, + demo_project_id: demo.project_id, + output_dir: outputDir, + capture_mobile: captureMobile, + screenshots, + console_messages: consoleMessages, + } + await writeFile(path.join(outputDir, 'manifest.json'), JSON.stringify(manifest, null, 2), 'utf8') + + if (relevantConsoleMessages.length > 0) { + throw new Error(`Console/page errors detected. See ${path.join(outputDir, 'manifest.json')}`) + } + + console.log(`Workbench screenshots captured: ${screenshots.length}`) + console.log(`Output: ${outputDir}`) +} + +capture().catch((error) => { + console.error(error.message) + process.exit(1) +}) +JS + +"${NODE_BIN}" "${tmp_js}" "${BASE_URL}" "${OUTPUT_DIR}" "${CAPTURE_MOBILE}" diff --git a/scripts/run_readiness_check.sh b/scripts/run_readiness_check.sh index 66358d73..9d4bdd71 100755 --- a/scripts/run_readiness_check.sh +++ b/scripts/run_readiness_check.sh @@ -56,4 +56,5 @@ bash -n scripts/verify_workbench_interactions.sh bash -n scripts/verify_gis_runtime.sh bash -n scripts/verify_golden_qa_benchmark.sh bash -n scripts/verify_demo_cleanup_dry_run.sh +bash -n scripts/capture_workbench_screenshots.sh echo "== Run readiness check passed =="