feat(screenshots): add main orchestrator and fix capture selectors

- Use @playwright/test instead of playwright (matches devDependencies)
- Use card-specific locators to avoid substring content matches
- Close history panel before editor to avoid overlay blocking
- Add Origin header for multipart upload CSRF compliance

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Frederic Cabassut
2026-03-20 22:32:16 +01:00
committed by Fred Cabassut
co-authored by Claude Opus 4.6
parent 82a9b4fa64
commit 92c2cf7952
3 changed files with 126 additions and 9 deletions
+18 -8
View File
@@ -1,4 +1,4 @@
import type { Page } from 'playwright';
import type { Page, Locator } from '@playwright/test';
import { join } from 'path';
import { BASE_URL, OUTPUT_DIR } from './constants';
@@ -12,6 +12,11 @@ async function waitForApp(page: Page): Promise<void> {
await page.waitForTimeout(300);
}
/** Click a note card by its title (targets the h3 inside note-card to avoid content substring matches) */
function noteCard(page: Page, title: string): Locator {
return page.locator('[data-testid="note-card"]', { has: page.locator('h3', { hasText: title }) });
}
export async function captureDesktop(page: Page): Promise<void> {
await page.emulateMedia({ reducedMotion: 'reduce' });
console.log('Capturing desktop screenshots...');
@@ -22,7 +27,7 @@ export async function captureDesktop(page: Page): Promise<void> {
await screenshot(page, 'screenshot-grid.png');
// D2: Rich text editor — open "Self-Hosting Stack"
await page.getByText('Self-Hosting Stack').first().click();
await noteCard(page, 'Self-Hosting Stack').click();
await page.waitForSelector('[data-testid="note-editor"]');
await waitForApp(page);
await screenshot(page, 'screenshot-editor.png');
@@ -30,7 +35,7 @@ export async function captureDesktop(page: Page): Promise<void> {
await waitForApp(page);
// D3: Checklist — open "Bretzel Ingredients"
await page.getByText('Bretzel Ingredients').first().click();
await noteCard(page, 'Bretzel Ingredients').click();
await page.waitForSelector('[data-testid="note-editor"]');
await waitForApp(page);
await screenshot(page, 'screenshot-checklist.png');
@@ -38,7 +43,7 @@ export async function captureDesktop(page: Page): Promise<void> {
await waitForApp(page);
// D4: Sharing dialog — open "Weekend Plans", click share
await page.getByText('Weekend Plans').first().click();
await noteCard(page, 'Weekend Plans').click();
await page.waitForSelector('[data-testid="note-editor"]');
await page.getByTestId('share-toggle').click();
await page.waitForSelector('[data-testid="share-dialog"]');
@@ -49,21 +54,26 @@ export async function captureDesktop(page: Page): Promise<void> {
await waitForApp(page);
// D5: Version history — open "Self-Hosting Stack", click history
await page.getByText('Self-Hosting Stack').first().click();
await noteCard(page, 'Self-Hosting Stack').click();
await page.waitForSelector('[data-testid="note-editor"]');
await page.getByTestId('history-toggle').click();
await page.waitForSelector('[data-testid="history-panel"]');
// Wait for version items to load from API
await page.waitForSelector('[data-testid="version-item"]', { timeout: 10_000 });
const versionItems = page.getByTestId('version-item');
if ((await versionItems.count()) > 1) {
await versionItems.nth(1).click();
}
await waitForApp(page);
await screenshot(page, 'screenshot-history.png');
// Close history panel first (overlay blocks close-editor-btn)
await page.getByTestId('close-history-btn').click();
await waitForApp(page);
await page.getByTestId('close-editor-btn').click();
await waitForApp(page);
// D6: Attachments — open "Bretzel Ingredients" (has featured bretzel image)
await page.getByText('Bretzel Ingredients').first().click();
await noteCard(page, 'Bretzel Ingredients').click();
await page.waitForSelector('[data-testid="note-editor"]');
await page.getByTestId('image-toggle').click();
await page.waitForSelector('[data-testid="image-upload"]');
@@ -88,7 +98,7 @@ export async function captureMobile(page: Page): Promise<void> {
await screenshot(page, 'screenshot-mobile-grid.png');
// M2: Mobile editor — open "Self-Hosting Stack"
await page.getByText('Self-Hosting Stack').first().click();
await noteCard(page, 'Self-Hosting Stack').click();
await page.waitForSelector('[data-testid="note-editor"]');
await waitForApp(page);
await screenshot(page, 'screenshot-mobile-editor.png');
@@ -96,7 +106,7 @@ export async function captureMobile(page: Page): Promise<void> {
await waitForApp(page);
// M3: Mobile checklist — open "Bretzel Ingredients"
await page.getByText('Bretzel Ingredients').first().click();
await noteCard(page, 'Bretzel Ingredients').click();
await page.waitForSelector('[data-testid="note-editor"]');
await waitForApp(page);
await screenshot(page, 'screenshot-mobile-checklist.png');
+2 -1
View File
@@ -1,4 +1,4 @@
import type { Page } from 'playwright';
import type { Page } from '@playwright/test';
import { generateBretzelPng, generateSaltShakerPng } from './bretzel-pixel';
import { BASE_URL, ADMIN, COLLABORATOR } from './constants';
@@ -24,6 +24,7 @@ async function uploadAttachment(
featured: boolean
): Promise<void> {
const res = await page.request.post(`${BASE_URL}/api/notes/${noteId}/attachments`, {
headers: { Origin: BASE_URL },
multipart: {
file: {
name: filename,