Add workbench screenshot capture smoke
This commit is contained in:
@@ -13,6 +13,7 @@ build/
|
|||||||
*.tsbuildinfo
|
*.tsbuildinfo
|
||||||
|
|
||||||
# Large local data
|
# Large local data
|
||||||
|
/artifacts/
|
||||||
/datasets/raw/*
|
/datasets/raw/*
|
||||||
/datasets/processed/*
|
/datasets/processed/*
|
||||||
/datasets/cache/*
|
/datasets/cache/*
|
||||||
|
|||||||
@@ -652,3 +652,11 @@ Added:
|
|||||||
- Added the smoke syntax check to the main readiness gate and regression coverage for the non-mutating script contract.
|
- 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`.
|
- 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.
|
- 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.
|
||||||
|
|||||||
@@ -58,6 +58,29 @@ def test_demo_cleanup_dry_run_script_is_dry_run_only() -> None:
|
|||||||
assert "project_report_html" in content
|
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:
|
def test_readiness_gate_compiles_yolo_preflight_script() -> None:
|
||||||
script = Path(__file__).resolve().parents[2] / "scripts" / "run_readiness_check.sh"
|
script = Path(__file__).resolve().parents[2] / "scripts" / "run_readiness_check.sh"
|
||||||
content = script.read_text(encoding="utf-8")
|
content = script.read_text(encoding="utf-8")
|
||||||
|
|||||||
@@ -2323,3 +2323,29 @@ Limitations:
|
|||||||
|
|
||||||
Next recommended pass:
|
Next recommended pass:
|
||||||
- Add browser screenshot artifact automation for visual regression handoff.
|
- 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.
|
||||||
|
|||||||
+3
-2
@@ -59,7 +59,7 @@ This file now starts with the current implementation status. Older preparation/b
|
|||||||
- [x] Demo workflow orchestration hook decomposition.
|
- [x] Demo workflow orchestration hook decomposition.
|
||||||
- [x] Final `App.tsx` import/encoding cleanup and size audit.
|
- [x] Final `App.tsx` import/encoding cleanup and size audit.
|
||||||
- [x] Optional final bootstrap-effect extraction.
|
- [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
|
## 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 export history filtering controls for long-running demo environments.
|
||||||
- [x] Add a safe export retention/cleanup command for 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.
|
- [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.
|
||||||
|
|||||||
@@ -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`
|
proxy used by the browser. The frontend also exposes stable `data-testid`
|
||||||
anchors for Codex/browser click checks on those controls.
|
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:
|
Verify the deterministic QA/QC golden benchmark:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
Executable
+170
@@ -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}"
|
||||||
@@ -56,4 +56,5 @@ bash -n scripts/verify_workbench_interactions.sh
|
|||||||
bash -n scripts/verify_gis_runtime.sh
|
bash -n scripts/verify_gis_runtime.sh
|
||||||
bash -n scripts/verify_golden_qa_benchmark.sh
|
bash -n scripts/verify_golden_qa_benchmark.sh
|
||||||
bash -n scripts/verify_demo_cleanup_dry_run.sh
|
bash -n scripts/verify_demo_cleanup_dry_run.sh
|
||||||
|
bash -n scripts/capture_workbench_screenshots.sh
|
||||||
echo "== Run readiness check passed =="
|
echo "== Run readiness check passed =="
|
||||||
|
|||||||
Reference in New Issue
Block a user