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
49 changes: 42 additions & 7 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -123,22 +123,57 @@ jobs:
path: shots/
if-no-files-found: ignore
retention-days: 14
- name: Visual diff vs main (informational)
if: github.event_name == 'pull_request' && !cancelled()
continue-on-error: true
timeout-minutes: 12

visual:
needs: prep
if: github.event_name == 'pull_request' && needs.prep.outputs.site == 'true'
runs-on: ubuntu-24.04
timeout-minutes: 15
continue-on-error: true
env: { SHOT_WIDTHS: '360,1280' }
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with: { node-version: 24, cache: npm }
- name: Cache Playwright browsers
uses: actions/cache@v6
with:
path: ~/.cache/ms-playwright
key: playwright-${{ runner.os }}-chromium-${{ hashFiles('package-lock.json') }}
- name: Install tools
run: |
npm ci
npx playwright install --with-deps chromium
python3 -m venv ~/v && ~/v/bin/pip install -q pillow
- name: Resolve main SHA
id: m
run: echo "sha=$(git ls-remote origin refs/heads/main | cut -f1)" >> "$GITHUB_OUTPUT"
- name: Cache main screenshots
id: bc
uses: actions/cache@v6
with:
path: shots_base
key: shots-base-${{ steps.m.outputs.sha }}-360-1280-${{ hashFiles('tests/shots_all.js') }}
- name: Screenshot main
if: steps.bc.outputs.cache-hit != 'true'
run: |
git clone -q --depth 1 --branch main "https://github.com/${{ github.repository }}.git" _base
if [ -d site/downloads ]; then cp -r site/downloads _base/site/downloads; fi
(cd _base && python3 build/build.py --publish)
python3 -m http.server 8766 --directory _base/site >/dev/null 2>&1 &
curl -fsS --retry 10 --retry-connrefused --retry-delay 1 -o /dev/null http://localhost:8766/
node tests/shots_all.js http://localhost:8766/ shots_base
- name: Screenshot PR
run: |
python3 build/build.py --publish
python3 -m http.server 8765 --directory site >/dev/null 2>&1 &
curl -fsS --retry 10 --retry-connrefused --retry-delay 1 -o /dev/null http://localhost:8765/
node tests/shots_all.js http://localhost:8765/ shots_pr
python3 -m venv ~/v && ~/v/bin/pip install -q pillow
- name: Diff
run: |
~/v/bin/python tests/visual_diff.py shots_base shots_pr shots_diff 0.02 || true
{ echo '### Visual diff vs main'; echo '```'; tail -40 shots_diff/report.txt 2>/dev/null || echo 'no report'; echo '```'; } >> "$GITHUB_STEP_SUMMARY"
- uses: actions/upload-artifact@v7
if: github.event_name == 'pull_request' && !cancelled()
if: always()
with:
name: visual-diff
path: |
Expand Down
14 changes: 11 additions & 3 deletions tests/shots_all.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,29 @@
const { chromium } = require("playwright");
const fs = require("fs"), path = require("path");
const base = process.argv[2], out = process.argv[3];
const WIDTHS = (process.env.SHOT_WIDTHS || "360,768,1280").split(",").map(Number);
const THEMES = (process.env.SHOT_THEMES || "dark,light").split(",");
const CONC = +process.env.SHOT_CONC || 4;
const SITE = path.join(__dirname, "..", "site");
const walk = d => fs.readdirSync(d, { withFileTypes: true }).flatMap(e =>
e.isDirectory() ? (e.name === "downloads" ? [] : walk(path.join(d, e.name))) : [path.join(d, e.name)]);
const pages = walk(SITE).filter(f => f.endsWith(".html") && !f.endsWith("404.html")).map(f => path.relative(SITE, f)).sort();
(async () => {
fs.mkdirSync(out, { recursive: true });
const b = await chromium.launch();
for (const theme of ["dark", "light"]) for (const w of [360, 768, 1280]) {
for (const theme of THEMES) for (const w of WIDTHS) {
const c = await b.newContext({ viewport: { width: w, height: 900 }, reducedMotion: "reduce" });
await c.addInitScript(t => { try { localStorage.setItem("ha-theme", t); } catch (e) {} }, theme);
for (const pg of pages) {
let i = 0;
const worker = async () => {
while (i < pages.length) {
const pg = pages[i++];
const p = await c.newPage();
try {
await p.goto(base + pg, { waitUntil: "networkidle" });
await p.addStyleTag({ content: "*,*::before,*::after{animation:none!important;transition:none!important;caret-color:transparent!important}" });
await p.evaluate(() => document.fonts.ready);
await p.waitForTimeout(400);
await p.waitForTimeout(250);
const name = pg.replace(/[\/.]/g, "_") + "-" + w + "-" + theme;
await p.screenshot({ path: out + "/" + name + ".png", fullPage: true });
if (w === 360 && pg === "index.html") {
Expand All @@ -29,6 +35,8 @@ if (m) { try { await m.click({ timeout: 500 }); await p.waitForTimeout(300); awa
} catch (e) { console.log("skip " + pg + ": " + e.message); }
await p.close();
}
};
await Promise.all(Array.from({ length: CONC }, worker));
await c.close();
}
await b.close();
Expand Down
Loading