mirror of
https://github.com/webstudio-is/webstudio.git
synced 2026-11-10 06:24:35 +09:00
## Summary Unifies frontend testing around three intentional boundaries: - Vitest Node projects for pure and server tests - Vitest Browser Mode with Playwright/Chromium for DOM and component tests - Playwright Test for Builder end-to-end workflows The Builder no longer uses jsdom. Ordinary E2E workflows use an automatically managed isolated `page` fixture; explicit contexts remain only for simultaneous identities and suite-scoped setup. ## Technical choices - Split Builder tests into one Node project and independent browser projects so each Chromium/Vite process has a bounded workload. - Added shared Browser Mode configuration for the affected SDK React packages. - Preserve the Builder's production top-window bridge ownership; Browser Mode uses explicit test-only installation seams. - Centralized Playwright context options and kept action/navigation timeouts bounded. - Validate Playwright filters before Docker, migrations, browser installation, or Builder builds start. - Preserve the first failed E2E attempt's report, traces, screenshots, and videos when CI retries a shard. - Split static checks, typechecks, CLI tests, core package tests, browser package tests, and Builder projects into parallel CI jobs with five-minute hard ceilings. - Distribute Builder E2E tests across ten isolated shards with two workers; parallel suites create unique per-worker setup data. - Reuse color contrast DOM/canvas samples so the exhaustive theme sweep takes about four seconds instead of timing out at 30 seconds. - The removed cost is jsdom behavior drift, oversized serial check jobs, and unnecessary E2E startup for empty selections. Real browser startup and E2E environment setup remain. ## Todo - [x] Split Builder pure/server tests from browser/component tests. - [x] Migrate Builder DOM tests from jsdom to Vitest Browser Mode. - [x] Migrate affected SDK React DOM tests to the shared Browser Mode setup. - [x] Remove jsdom annotations, dependencies, and compatibility setup. - [x] Replace ordinary manual E2E contexts with the isolated `page` fixture. - [x] Keep explicit contexts only for concurrent identities and `beforeAll` setup. - [x] Preserve setup authentication isolation from workflow pages. - [x] Add bounded Playwright action and navigation timeouts. - [x] Preserve first-attempt E2E failure evidence across CI retry. - [x] Reject empty E2E selections before expensive environment setup. - [x] Parallelize repository checks into bounded CI shards. - [x] Isolate parallel E2E setup data across workers. - [x] Remove the color contrast sweep bottleneck. - [x] Update E2E contributor documentation. - [x] Review product documentation impact; no user-facing product docs are affected. - [x] Run the full repository tests and typechecks. - [x] Run formatting, lint, package-boundary, generated-file, and fixture checks. - [x] Complete final correctness and simplification review. ## Verification - `pnpm --filter @webstudio-is/builder test` - `pnpm -r typecheck` - `pnpm lint` - `pnpm check:package-boundaries` - `pnpm check:generated-api` - `pnpm check:generated-docs` - `pnpm fixtures` - Design-system: 222 tests pass locally. - Builder: all seven Vitest projects pass locally. - The formerly slow E2E shard passes locally in 1m55s. - Full database-backed Builder E2E execution passes in CI. - All CI jobs pass; the slowest completed in 4m58s. - No fixture inputs/outputs or generated files changed. Ref #6270
170 lines
6.5 KiB
YAML
170 lines
6.5 KiB
YAML
name: Visual regression
|
|
|
|
on:
|
|
push:
|
|
branches:
|
|
- main
|
|
pull_request:
|
|
types: [opened, synchronize, reopened, labeled]
|
|
pull_request_target:
|
|
types: [opened, synchronize, reopened, labeled]
|
|
|
|
concurrency:
|
|
# A synchronize action emits both pull_request and pull_request_target. Keep
|
|
# their runs separate so approval cleanup cannot cancel the visual check.
|
|
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.event.number || github.sha }}
|
|
cancel-in-progress: true
|
|
|
|
permissions:
|
|
contents: read
|
|
|
|
jobs:
|
|
remove-approval:
|
|
if: github.event_name == 'pull_request_target' && github.event.action == 'synchronize'
|
|
permissions:
|
|
contents: read
|
|
pull-requests: write
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- name: Remove visual change approval
|
|
uses: actions/github-script@v8
|
|
with:
|
|
script: |
|
|
const labels = context.payload.pull_request.labels.map(({ name }) => name);
|
|
if (labels.includes('visual-change-approved')) {
|
|
await github.rest.issues.removeLabel({
|
|
owner: context.repo.owner,
|
|
repo: context.repo.repo,
|
|
issue_number: context.issue.number,
|
|
name: 'visual-change-approved',
|
|
});
|
|
}
|
|
|
|
visual-regression:
|
|
# Same-repository PRs use the unprivileged event. Organization members'
|
|
# fork PRs are trusted; every other fork revision needs safe-to-deploy.
|
|
if: |
|
|
github.event_name == 'push' ||
|
|
(
|
|
github.event_name == 'pull_request' &&
|
|
github.event.pull_request.head.repo.full_name == github.repository &&
|
|
(
|
|
github.event.action != 'labeled' ||
|
|
github.event.label.name == 'visual-change-approved'
|
|
)
|
|
) ||
|
|
(
|
|
github.event_name == 'pull_request_target' &&
|
|
github.event.pull_request.head.repo.full_name != github.repository &&
|
|
(
|
|
(
|
|
contains(fromJSON('["OWNER", "MEMBER"]'), github.event.pull_request.author_association) &&
|
|
(
|
|
github.event.action != 'labeled' ||
|
|
github.event.label.name == 'visual-change-approved'
|
|
)
|
|
) ||
|
|
(
|
|
github.event.action == 'labeled' &&
|
|
(
|
|
github.event.label.name == 'safe-to-deploy' ||
|
|
(
|
|
github.event.label.name == 'visual-change-approved' &&
|
|
contains(github.event.pull_request.labels.*.name, 'safe-to-deploy')
|
|
)
|
|
)
|
|
)
|
|
)
|
|
)
|
|
timeout-minutes: 5
|
|
runs-on: ubuntu-latest
|
|
environment:
|
|
name: development
|
|
steps:
|
|
- uses: actions/checkout@v5
|
|
with:
|
|
fetch-depth: 0
|
|
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
|
allow-unsafe-pr-checkout: ${{ github.event_name == 'pull_request_target' }}
|
|
|
|
- uses: ./.github/actions/submodules-checkout
|
|
with:
|
|
submodules-ssh-key: ${{ secrets.PRIVATE_GITHUB_DEPLOY_TOKEN }}
|
|
|
|
- uses: ./.github/actions/ci-setup
|
|
|
|
- name: Cache Playwright Chromium
|
|
uses: actions/cache@v5
|
|
with:
|
|
path: |
|
|
~/.cache/ms-playwright/chromium-*
|
|
~/.cache/ms-playwright/chromium_headless_shell-*
|
|
~/.cache/ms-playwright/ffmpeg-*
|
|
key: playwright-chromium-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}
|
|
|
|
- name: Install pinned visual browser
|
|
run: |
|
|
pnpm --filter @webstudio-is/sdk-components-animation exec playwright install chromium
|
|
playwright_browser_path="$(pnpm --filter @webstudio-is/sdk-components-animation exec node -e 'console.log(require("playwright").chromium.executablePath())')"
|
|
test -x "$playwright_browser_path"
|
|
echo "WEBSTUDIO_BROWSER_PATH=$playwright_browser_path" >> "$GITHUB_ENV"
|
|
|
|
- name: Install visual comparison dependencies
|
|
run: sudo apt-get install --no-install-recommends -y tesseract-ocr
|
|
|
|
- name: Resolve visual baseline cache key
|
|
id: visual-cache
|
|
env:
|
|
BASE_REF: ${{ github.event.pull_request.base.sha || github.event.before || 'origin/main' }}
|
|
run: |
|
|
echo "base=$(git merge-base HEAD "$BASE_REF")" >> "$GITHUB_OUTPUT"
|
|
echo "head=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
|
|
browser_version="$("$WEBSTUDIO_BROWSER_PATH" --version)"
|
|
runtime_hash="$(pnpm tsx scripts/visual-regression/runtime-hash.ts "$browser_version")"
|
|
echo "runtime=$runtime_hash" >> "$GITHUB_OUTPUT"
|
|
|
|
- name: Restore visual baseline screenshots
|
|
uses: actions/cache/restore@v5
|
|
with:
|
|
path: .visual-regression-cache/${{ steps.visual-cache.outputs.base }}-*
|
|
key: visual-baseline-v1-${{ runner.os }}-${{ steps.visual-cache.outputs.base }}-${{ steps.visual-cache.outputs.runtime }}
|
|
|
|
- name: Compare portable stories
|
|
env:
|
|
BASE_REF: ${{ github.event.pull_request.base.sha || github.event.before || 'origin/main' }}
|
|
APPROVE_VISUAL_CHANGES: ${{ github.event_name == 'push' || (github.event.action == 'labeled' && github.event.label.name == 'visual-change-approved') }}
|
|
run: |
|
|
args=(--base "$BASE_REF")
|
|
if [[ "$APPROVE_VISUAL_CHANGES" == "true" ]]; then
|
|
args+=(--approve-visual-changes)
|
|
fi
|
|
pnpm visual-regression -- "${args[@]}"
|
|
|
|
- name: Validate visual comparison report
|
|
if: always()
|
|
run: |
|
|
node -e 'const report = require("./.visual-regression/report/report.json"); if (!Array.isArray(report.comparisons) || report.comparisons.length === 0) throw new Error("Visual comparison report has no story results.")'
|
|
|
|
- name: Upload visual comparison report
|
|
if: always()
|
|
uses: actions/upload-artifact@v7
|
|
with:
|
|
name: visual-regression-report
|
|
path: .visual-regression/report
|
|
if-no-files-found: error
|
|
retention-days: 14
|
|
|
|
- name: Cache visual screenshots for dependent revisions
|
|
if: always()
|
|
uses: actions/cache/save@v5
|
|
with:
|
|
path: .visual-regression-cache/${{ steps.visual-cache.outputs.head }}-*
|
|
key: visual-baseline-v1-${{ runner.os }}-${{ steps.visual-cache.outputs.head }}-${{ steps.visual-cache.outputs.runtime }}
|
|
|
|
- name: Cache generated baseline screenshots
|
|
if: always()
|
|
uses: actions/cache/save@v5
|
|
with:
|
|
path: .visual-regression-cache/${{ steps.visual-cache.outputs.base }}-*
|
|
key: visual-baseline-v1-${{ runner.os }}-${{ steps.visual-cache.outputs.base }}-${{ steps.visual-cache.outputs.runtime }}
|