SPB Git

Repository Public

shots.spec.ts 11.0 KB 170 lines typescript

1// Design review screenshots (not a test suite): one real account per project, real uploads from the e2e fixtures, then2// a picture of every main screen. Run against the development stack:3//   pnpm exec playwright test -c tests/shots/playwright.config.ts --output /tmp/spd-shots-out4// Pictures land in $SPD_SHOTS_DIR (default /tmp/sp-drive-shots)/<project>/NN-name.png.5import { mkdirSync, readFileSync } from 'node:fs';6import { dirname, join } from 'node:path';7import { fileURLToPath } from 'node:url';8import { expect, type Page, test } from '@playwright/test';9import { apiSignUp } from '../e2e/helpers.ts';1011test.use({ locale: 'fr-CA' });12const FIX = join(dirname(fileURLToPath(import.meta.url)), '../e2e/fixtures');13const OUT = process.env.SPD_SHOTS_DIR ?? '/tmp/sp-drive-shots';14const fx = (name: string, as = name) => ({ name: as, mimeType: 'application/octet-stream', buffer: readFileSync(join(FIX, name)) });15const row = (page: Page, name: string) => page.locator(`[role=row][data-id]:has([data-name]:text-is("${name}"))`);16const call = <T = any>(page: Page, method: string, path: string, body?: unknown): Promise<T> =>17  page.evaluate(async ([m, p, b]) => {18    const r = await fetch(`/api/v1${p}`, { method: m as string, headers: { 'Content-Type': 'application/json', 'X-SPD-CSRF': '1' }, body: m === 'GET' ? undefined : JSON.stringify(b ?? {}) });19    if (!r.ok) throw new Error(`${m} ${p}: ${r.status} ${await r.text()}`);20    return r.json();21  }, [method, path, body] as const);2223test('screens', async ({ page, context }, info) => {24  test.setTimeout(Number(process.env.SPD_SHOTS_TIMEOUT ?? 300_000));25  const dark = info.project.name.includes('dark');26  const phone = info.project.name.includes('phone');27  const dir = join(OUT, info.project.name);28  mkdirSync(dir, { recursive: true });29  let n = 0;30  const shot = async (name: string, full = false) => { await page.waitForTimeout(450); await page.screenshot({ path: join(dir, `${String(++n).padStart(2, '0')}-${name}.png`), fullPage: full }); };31  await context.addInitScript((d) => { try { localStorage.setItem('spd.theme', d ? 'dark' : 'light'); } catch { /* opaque origin */ } }, dark);3233  await page.goto('/login');34  await expect(page.getByRole('button', { name: /passkey/i }).first()).toBeVisible();35  await shot('login');3637  await apiSignUp(context, page);38  await page.goto('/');39  await expect(page.getByRole('heading', { name: 'Ce dossier est vide' })).toBeVisible();40  await shot('empty');4142  for (const name of ['Projets', 'Photos de voyage', 'Factures 2026', 'Contrats']) await call(page, 'POST', '/folders', { parentId: 'root', name });43  await page.getByTestId('upload-input').setInputFiles([44    fx('sample.jpg', 'Coucher de soleil.jpg'), fx('sample.png', 'Maquette accueil.png'), fx('sample.pdf', 'Rapport annuel 2026.pdf'), fx('sample.docx', 'Lettre de mission.docx'),45    fx('sample.xlsx', 'Budget prévisionnel.xlsx'), fx('sample.mp4', 'Visite du chantier.mp4'), fx('sample.mp3', 'Mémo vocal.mp3'), fx('sample.zip', 'Archives 2025.zip'),46    fx('sample.txt', 'Notes de réunion.txt'), fx('sample.csv', 'Ventes par région.csv'),47  ]);48  await expect(page.getByTestId('upload-panel')).toBeVisible();49  await expect(page.locator('[data-upload-state=done]')).toHaveCount(10, { timeout: 90_000 });50  await shot('uploads-done');51  await page.getByRole('button', { name: 'Tout effacer' }).first().click().catch(() => {});52  const files = (await call(page, 'GET', '/folders/root')).files as { id: string; name: string }[];53  const byName = (s: string) => files.find((f) => f.name === s)!;54  await call(page, 'PATCH', `/files/${byName('Rapport annuel 2026.pdf').id}`, { starred: true });55  await call(page, 'PATCH', `/files/${byName('Coucher de soleil.jpg').id}`, { starred: true });56  // thumbnails come from the worker a moment later57  await expect.poll(async () => page.locator('[data-testid=thumb][data-state=image]').count(), { timeout: 60_000 }).toBeGreaterThan(1).catch(() => {});58  await page.reload();59  await expect(row(page, 'Projets')).toBeVisible();60  await page.waitForTimeout(1500);6162  const toggle = page.getByTestId('view-toggle');63  if ((await toggle.getAttribute('data-mode')) !== 'list') await toggle.click();64  await shot('explorer-list');65  const bottom = () => page.locator('[data-testid=listing]').evaluate((el) => { const s = el.parentElement!; s.scrollTop = s.scrollHeight; });66  const top = () => page.locator('[data-testid=listing]').evaluate((el) => { el.parentElement!.scrollTop = 0; });67  await bottom(); await shot('explorer-list-bottom'); await top();68  await toggle.click();69  await page.waitForTimeout(1200);70  await shot('explorer-grid');71  await bottom(); await shot('explorer-grid-bottom'); await top();72  await page.waitForTimeout(300);7374  // actions menu and details75  await row(page, 'Rapport annuel 2026.pdf').locator('[data-more]').click();76  await expect(page.getByRole('menu')).toBeVisible();77  await shot('menu');78  await page.getByRole('menuitem', { name: 'Détails' }).click();79  await expect(page.getByTestId('details')).toBeVisible();80  await shot('details');81  if (phone) await page.getByRole('dialog').getByRole('button', { name: 'Fermer' }).click(); else await page.getByRole('complementary', { name: 'Détails' }).getByRole('button', { name: 'Fermer' }).click();82  await page.waitForTimeout(400);8384  if (await page.getByTestId('selection-bar').count()) await page.getByTestId('selection-bar').getByRole('button').first().click();85  // "New" menu86  await page.getByRole('button', { name: 'Nouveau', exact: true }).click();87  await shot('new-menu');88  await page.keyboard.press('Escape');8990  // an upload in progress: hold the blocks of a larger file91  await page.evaluate(() => {92    const orig = window.fetch.bind(window);93    window.fetch = async (u, o) => { if (o?.method === 'PATCH' && String(u).includes('/uploads/')) await new Promise((r) => setTimeout(r, 1400)); return orig(u, o); };94  });95  const big = Buffer.alloc(28 * 1024 * 1024, 7);96  await page.getByTestId('upload-input').setInputFiles([{ name: 'Présentation client.key', mimeType: 'application/octet-stream', buffer: big }, { name: 'Plans architecte.dwg', mimeType: 'application/octet-stream', buffer: big.subarray(0, 15 * 1024 * 1024) }]);97  await expect(page.locator('[data-upload-state=uploading]').first()).toBeVisible({ timeout: 30_000 });98  await page.waitForTimeout(3200);99  await shot('upload-progress');100  await expect(page.locator('[data-upload-state=uploading],[data-upload-state=queued],[data-upload-state=finishing]')).toHaveCount(0, { timeout: 120_000 });101  await page.getByRole('button', { name: 'Tout effacer' }).first().click().catch(() => {});102103  // selection bar104  if (!phone) { await row(page, 'Coucher de soleil.jpg').click(); await row(page, 'Budget prévisionnel.xlsx').click({ modifiers: ['ControlOrMeta'] }); await shot('selection'); await page.keyboard.press('Escape'); }105106  // viewer: image then PDF107  const open = async (name: string) => { if (phone) await row(page, name).locator('[data-name]').tap(); else await row(page, name).dblclick(); };108  await open('Coucher de soleil.jpg');109  await expect(page.getByRole('dialog')).toBeVisible();110  await page.waitForTimeout(1500);111  await shot('viewer-image');112  await page.keyboard.press('Escape');113  await open('Rapport annuel 2026.pdf');114  await expect(page.getByRole('dialog')).toBeVisible();115  await page.waitForTimeout(3000);116  await shot('viewer-pdf');117  await page.keyboard.press('Escape');118119  // share dialog120  await row(page, 'Rapport annuel 2026.pdf').locator('[data-more]').click();121  await page.getByRole('menuitem', { name: 'Partager' }).click();122  await expect(page.getByRole('dialog')).toBeVisible();123  await page.waitForTimeout(600);124  await shot('share-dialog');125  await page.keyboard.press('Escape');126  await page.keyboard.press('Escape');127128  // public pages need links129  const photos = ((await call(page, 'GET', '/folders/root')).folders as { id: string; name: string }[]).find((f) => f.name === 'Photos de voyage')!;130  for (const f of ['sample.jpg', 'sample.png', 'sample.pdf']) void f;131  const sFile = await call(page, 'POST', '/shares', { fileId: byName('Rapport annuel 2026.pdf').id });132  const projets = ((await call(page, 'GET', '/folders/root')).folders as { id: string; name: string }[]).find((f) => f.name === 'Projets')!;133  await call(page, 'POST', '/move', { fileIds: [byName('Coucher de soleil.jpg').id, byName('Maquette accueil.png').id, byName('Lettre de mission.docx').id, byName('Ventes par région.csv').id], folderIds: [], destinationId: projets.id, onConflict: 'fail' });134  const sFolder = await call(page, 'POST', '/shares', { folderId: projets.id });135  const req = await call(page, 'POST', '/requests', { folderId: photos.id, title: 'Documents pour votre dossier', brandName: 'Cabinet Tremblay', message: 'Merci de déposer vos pièces justificatives : avis de cotisation, relevés et pièce d’identité.' });136137  for (const [href, name] of [['/recent', 'recent'], ['/starred', 'starred'], ['/shares', 'shares'], ['/requests', 'requests'], ['/trash', 'trash-empty'], ['/settings', 'settings'], ['/search?q=rapport', 'search']] as const) {138    await page.goto(href);139    await expect(page.getByRole('heading', { level: 1 }).first()).toBeVisible();140    await page.waitForTimeout(700);141    await shot(name, name === 'settings');142  }143  if (phone) { await page.goto('/'); await page.getByRole('button', { name: 'Plus', exact: true }).click(); await shot('more-sheet'); await page.keyboard.press('Escape'); }144  else { await page.goto('/'); await page.keyboard.press('ControlOrMeta+k'); await page.waitForTimeout(300); await page.keyboard.type('rap'); await shot('palette'); await page.keyboard.press('Escape'); }145146  // anonymous pages, in a context without the session147  const anon = await context.browser()!.newContext({ ...info.project.use, baseURL: info.project.use.baseURL, locale: 'fr-CA', extraHTTPHeaders: { 'X-Forwarded-For': `10.9.${Math.floor(Math.random() * 250)}.${Math.floor(Math.random() * 250)}` } });148  const pub = await anon.newPage();149  const pshot = async (name: string) => { await pub.waitForTimeout(900); await pub.screenshot({ path: join(dir, `${String(++n).padStart(2, '0')}-${name}.png`), fullPage: true }); };150  await pub.goto(new URL(sFile.url).pathname);151  await expect(pub.getByRole('heading', { level: 1 })).toBeVisible();152  await pub.waitForTimeout(2500);153  await pshot('public-share-file');154  await pub.goto(new URL(sFolder.url).pathname);155  await expect(pub.getByRole('heading', { level: 1 })).toBeVisible();156  await pub.waitForTimeout(1500);157  await pshot('public-share-folder');158  await pub.goto(new URL(req.url).pathname);159  await expect(pub.getByRole('heading', { level: 1 })).toBeVisible();160  await pshot('public-portal');161  await pub.locator('input[type=file]').first().setInputFiles([fx('sample.pdf', 'Avis de cotisation.pdf'), fx('sample.jpg', 'Pièce identité.jpg')]).catch(() => {});162  await pshot('public-portal-files');163  await pub.goto('/s/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa');164  await pub.waitForTimeout(800);165  await pshot('public-unavailable');166  await pub.goto('/invite/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa');167  await pshot('invite-invalid');168  await anon.close();169});170
Files main
Menu

Theme