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
148 changes: 148 additions & 0 deletions .github/workflows/visual-regression.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
name: visual-regression

on:
pull_request:
branches: [main]
paths:
- "themes/**"
- "tests/visual/**"
- "scripts/visual-*"
- "scripts/visual-diff.js"
- ".github/workflows/visual-regression.yml"
- "package.json"
- "pnpm-lock.yaml"
push:
branches: [main]
paths:
- "themes/**"
- "tests/visual/**"
workflow_dispatch:
inputs:
bootstrap:
description: "Capture baselines and commit them to this branch (use after merging a UI change). Requires write access."
required: false
type: boolean
default: false

permissions:
contents: read
pull-requests: write
checks: write

concurrency:
group: visual-regression-${{ github.ref }}
cancel-in-progress: true

jobs:
visual-regression:
name: Capture + diff
runs-on: ubuntu-latest
# Soft-fail for two weeks while baselines stabilize. Flip to `false` on or after 2026-06-02.
continue-on-error: true
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
# Use a PAT or the default token; defaults are fine for read-only check runs.
# For the bootstrap path we need a push-capable token.
token: ${{ secrets.GITHUB_TOKEN }}

- name: Set up Node 20
uses: actions/setup-node@v4
with:
node-version: "20"

- name: Set up pnpm
uses: pnpm/action-setup@v4
with:
version: "9.15.0"
run_install: false

- name: Restore pnpm store
uses: actions/cache@v4
with:
path: ~/.local/share/pnpm/store
key: pnpm-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}
restore-keys: |
pnpm-${{ runner.os }}-

- name: Install Node deps
run: pnpm install --frozen-lockfile

- name: Install Playwright Chromium
run: pnpm exec playwright install --with-deps chromium

- name: Prepare .env for docker compose
run: cp .env.example .env

- name: Boot WordPress stack
run: |
docker compose up -d wordpress db
for i in $(seq 1 90); do
if curl --silent --fail --max-time 2 http://localhost:8080/ >/dev/null 2>&1; then
echo "WordPress responding."
break
fi
sleep 2
done

- name: Install WooCommerce (no sample data)
env:
WC_INSTALL_SAMPLE_DATA: "false"
run: docker compose --profile woocommerce up --exit-code-from woocommerce-installer woocommerce-installer

- name: Seed deterministic content
run: bash tests/visual/seed.sh

- name: Capture screenshots
run: pnpm visual:capture -- --out tests/visual/actual

- name: Detect missing baselines
id: baselines
run: |
shopt -s nullglob
baseline_count=$(ls tests/visual/baselines/*.png 2>/dev/null | wc -l)
actual_count=$(ls tests/visual/actual/*.png 2>/dev/null | wc -l)
echo "baseline_count=$baseline_count" >> "$GITHUB_OUTPUT"
echo "actual_count=$actual_count" >> "$GITHUB_OUTPUT"
if [ "$baseline_count" -eq 0 ]; then
echo "::warning title=No baselines committed::tests/visual/baselines/ is empty. Run the 'visual-regression' workflow with bootstrap=true to populate it, or run scripts/visual-update-baselines.sh locally."
fi

- name: Diff vs baselines
if: steps.baselines.outputs.baseline_count != '0' && github.event.inputs.bootstrap != 'true'
run: pnpm visual:diff

- name: Upload artifacts (actual, diffs, report)
if: always()
uses: actions/upload-artifact@v4
with:
name: visual-regression-${{ github.run_id }}
path: |
tests/visual/actual/
tests/visual/diffs/
tests/visual/report.json
if-no-files-found: ignore
retention-days: 14

# --- Bootstrap path: commit fresh baselines back to the branch ---
- name: Promote actuals to baselines (bootstrap)
if: github.event.inputs.bootstrap == 'true'
run: |
rm -f tests/visual/baselines/*.png
cp tests/visual/actual/*.png tests/visual/baselines/
ls -la tests/visual/baselines/

- name: Commit baselines (bootstrap)
if: github.event.inputs.bootstrap == 'true'
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add tests/visual/baselines/
if git diff --cached --quiet; then
echo "No baseline changes to commit."
else
git commit -m "test(visual): bootstrap baselines [skip ci]"
git push origin HEAD:${{ github.ref_name }}
fi
7 changes: 6 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,12 @@ pnpm-debug.log*
.pnpm-debug.log
package-lock.json
yarn.lock
pnpm-lock.yaml
# pnpm-lock.yaml is tracked — required for reproducible CI installs.

# Visual regression scratch (baselines/ is tracked; actual/, diffs/, report are not)
tests/visual/actual/
tests/visual/diffs/
tests/visual/report.json
composer.lock

# Environment & Config Files
Expand Down
3 changes: 3 additions & 0 deletions .npmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
engine-strict=true
auto-install-peers=true
save-exact=false
18 changes: 18 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,24 @@ Before submitting your changes:
- Ensure there are no PHP errors or warnings
- Run any relevant linting or PHPCS checks

### Visual regression

PRs that touch `themes/**` automatically run the visual regression suite,
which screenshots the seeded WP site at four breakpoints and diffs against
committed baselines in `tests/visual/baselines/`. If you intentionally
changed the UI, regenerate baselines locally and commit them alongside the
code change:

```bash
bash scripts/visual-update-baselines.sh
git add tests/visual/baselines
git commit -m "test(visual): update baselines for <feature>"
```

The script captures inside the same Playwright Docker image used by CI to
keep baselines portable. See [tests/visual/README.md](tests/visual/README.md)
for the full workflow and debugging tips.

## Pull Requests

To help your PR get reviewed and merged quickly:
Expand Down
37 changes: 37 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
{
"name": "flavian",
"version": "0.0.0",
"private": true,
"description": "Claude Code-integrated WordPress development template. Source of truth for the repository version is git tags + .release-please-manifest.json, not this file.",
"license": "MIT",
"homepage": "https://github.com/PMDevSolutions/Flavian",
"repository": {
"type": "git",
"url": "git+https://github.com/PMDevSolutions/Flavian.git"
},
"bugs": {
"url": "https://github.com/PMDevSolutions/Flavian/issues"
},
"engines": {
"node": ">=20.0.0"
},
"packageManager": "pnpm@9.15.0",
"type": "module",
"scripts": {
"visual:capture": "node tests/visual/capture.mjs",
"visual:diff": "node scripts/visual-diff.js --batch tests/visual/actual tests/visual/baselines --output-dir tests/visual/diffs --threshold 0.005 --json > tests/visual/report.json && node tests/visual/print-report.mjs tests/visual/report.json",
"visual:update": "bash scripts/visual-update-baselines.sh",
"visual:seed": "bash tests/visual/seed.sh",
"lint:commits": "commitlint --from=$(git merge-base origin/main HEAD) --to=HEAD",
"lint:commits:last": "commitlint --from=HEAD~1 --to=HEAD",
"test:visual": "pnpm visual:diff",
"playwright:install": "playwright install --with-deps chromium"
},
"devDependencies": {
"@commitlint/cli": "^19.5.0",
"@commitlint/config-conventional": "^19.5.0",
"pixelmatch": "^7.1.0",
"playwright": "1.60.0",
"pngjs": "^7.0.0"
}
}
Loading
Loading