Files
immich-folio/.claude/skills/run-immich-folio/driver.mjs

90 lines
4.0 KiB
JavaScript

/**
* Immich Folio smoke driver.
*
* Usage (run from repo root):
* node .claude/skills/run-immich-folio/driver.mjs [BASE_URL]
*
* BASE_URL defaults to http://localhost:3001.
* Screenshots land in /tmp/shots/.
*
* Exits 0 on success, 1 on any failure.
*/
import { chromium } from '@playwright/test';
import { mkdirSync } from 'fs';
const BASE = process.argv[2] ?? process.env.BASE_URL ?? 'http://localhost:3001';
const SHOTS = '/tmp/shots';
mkdirSync(SHOTS, { recursive: true });
let browser;
try {
browser = await chromium.launch({ args: ['--no-sandbox'] });
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
const page = await ctx.newPage();
const consoleErrors = [];
page.on('console', m => { if (m.type() === 'error') consoleErrors.push(m.text()); });
// ── 1. Homepage ──────────────────────────────────────────────
console.log('[1/4] Homepage...');
await page.goto(`${BASE}/`);
await page.waitForLoadState('networkidle');
const title = await page.title();
if (!title) throw new Error('Homepage has no title');
console.log(` title: ${title}`);
await page.screenshot({ path: `${SHOTS}/01-homepage.png` });
console.log(` → ${SHOTS}/01-homepage.png`);
// ── 2. First nav subpage ─────────────────────────────────────
// Grab the first subpage link from the header nav (skip "Home" and "About")
const firstSubpageHref = await page.$eval(
'.header__nav a:not([href="/"]):not([href="/about"]):not([href="/map"])',
el => el.getAttribute('href'),
);
if (!firstSubpageHref) throw new Error('No subpage nav link found');
console.log(`[2/4] Subpage: ${firstSubpageHref}`);
await page.goto(`${BASE}${firstSubpageHref}`);
await page.waitForLoadState('networkidle');
await page.screenshot({ path: `${SHOTS}/02-subpage.png` });
console.log(` → ${SHOTS}/02-subpage.png`);
// ── 3. Album detail ──────────────────────────────────────────
// Album cards on a subpage grid use class "subpage-grid__item".
// Navigate by href (not click) — click + networkidle can race with RSC streaming.
const albumCard = page.locator('a.subpage-grid__item').first();
const albumHref = await albumCard.getAttribute('href').catch(() => null);
if (albumHref) {
console.log(`[3/4] Album: ${albumHref}`);
await page.goto(`${BASE}${albumHref}`);
await page.waitForLoadState('networkidle');
await page.screenshot({ path: `${SHOTS}/03-album-detail.png` });
console.log(` → ${SHOTS}/03-album-detail.png`);
} else {
// Subpage had only one album — we're already on the album detail
console.log('[3/4] Already on album detail (single-album subpage)');
await page.screenshot({ path: `${SHOTS}/03-album-detail.png` });
console.log(` → ${SHOTS}/03-album-detail.png`);
}
// ── 4. Health endpoint ───────────────────────────────────────
console.log('[4/4] API health...');
const health = await page.evaluate(async (base) => {
const r = await fetch(`${base}/api/health`);
return r.json();
}, BASE);
console.log(` ${JSON.stringify(health)}`);
if (health.status !== 'ok') throw new Error(`Health check failed: ${JSON.stringify(health)}`);
// ── Summary ───────────────────────────────────────────────────
const appErrors = consoleErrors.filter(e => !e.includes('eval() is not supported'));
if (appErrors.length > 0) {
console.warn(`\n⚠ Console errors (${appErrors.length}):`);
appErrors.forEach(e => console.warn(' ', e));
}
console.log(`\n✓ All checks passed. Screenshots in ${SHOTS}/`);
} finally {
await browser?.close();
}