import { test, expect, type Page } from '@playwright/test'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import fs from 'node:fs'; const here = path.dirname(fileURLToPath(import.meta.url)); const FACE_A = path.resolve(here, '../fixtures/set1-48x2/face-a.jpeg'); const ARTIFACTS = path.resolve(here, 'artifacts'); const shot = async (page: Page, name: string) => { fs.mkdirSync(ARTIFACTS, { recursive: true }); await page.screenshot({ path: path.join(ARTIFACTS, name), fullPage: true }); }; const ensureFreshBatch = async (page: Page) => { await page.goto('/'); const resumeHeading = page.getByRole('heading', { name: /resume/i }); const batchHeading = page.getByRole('heading', { name: 'Batch photos' }); await expect(resumeHeading.or(batchHeading)).toBeVisible({ timeout: 15_000 }); if (await resumeHeading.isVisible().catch(() => false)) { await page.getByRole('button', { name: /start fresh/i }).click(); } await expect(batchHeading).toBeVisible(); }; const prepareMaskForApprove = async (page: Page) => { await expect(page.getByRole('heading', { name: 'Mask' })).toBeVisible(); await page.getByRole('button', { name: 'Black', exact: true }).click(); const approve = page.getByRole('button', { name: 'Approve mask' }); try { await expect(approve).toBeEnabled({ timeout: 45_000 }); return; } catch { /* sensitivity ladder */ } for (const target of [60, 70, 40, 30, 80]) { await page.locator('input[type="range"]').fill(String(target)); await page.waitForTimeout(2800); if (await approve.isEnabled()) return; } await expect(approve).toBeEnabled({ timeout: 15_000 }); }; const toLengthPage = async (page: Page) => { await ensureFreshBatch(page); await page.locator('input[type="file"][multiple]').setInputFiles([FACE_A]); await expect(page.getByAltText('Batch photo 1')).toBeVisible({ timeout: 30_000 }); await page.getByRole('button', { name: 'Continue' }).click(); await page.getByRole('button', { name: 'Use photo 1 as calibration' }).click(); await prepareMaskForApprove(page); await page.getByRole('button', { name: 'Approve mask' }).click(); await expect(page.getByRole('heading', { name: 'Length' })).toBeVisible(); }; test.describe('#99 fixed tape line', () => { test('the line is not editable: no tick-drag copy and no tick drag', async ({ page }) => { test.setTimeout(180_000); expect(fs.existsSync(FACE_A), 'fixture face-a').toBeTruthy(); await toLengthPage(page); // The hint must never tell the owner to drag or nudge the ticks. await expect(page.getByText('Drag the ticks')).toHaveCount(0); await expect(page.getByText('nudge the last tick')).toHaveCount(0); await expect(page.getByText('The line is fixed in the middle')).toBeVisible(); const stage = page.getByTestId('tape-line'); await expect(stage).toBeVisible(); const box = (await stage.boundingBox())!; const canvas = stage.locator('canvas').first(); // A pan delta produces the same result wherever you grab the stage, because // pan is measured as a delta from the grab point. So: drag from a tick // position, then repeat the same delta from a plain part of the stage. If // the tick drag were still live it would move the line too, and the two // results would differ. Identical output ⇒ the line is fixed. const centerX = box.x + box.width / 2; const tickY = box.y + box.height * 0.05; const plainY = box.y + box.height * 0.62; const delta = box.height * 0.18; const dragBy = async (from: number) => { await page.mouse.move(centerX, from); await page.mouse.down(); await page.mouse.move(centerX, from + delta, { steps: 10 }); await page.mouse.up(); await page.waitForTimeout(300); }; await dragBy(tickY); const fromTick = await canvas.screenshot(); // Reset the transform, then repeat the identical delta from a non-tick point. await toLengthPage(page); await dragBy(plainY); const fromPlain = await canvas.screenshot(); expect( Buffer.compare(fromTick, fromPlain), 'a tick drag must behave exactly like a pan elsewhere (line stays fixed)', ).toBe(0); // Length and thickness live on this same screen; Calculate stays usable. await page.getByLabel('Length L').fill('48'); await page.getByLabel('Thickness').fill('2'); await expect(page.getByRole('button', { name: 'Calculate' })).toBeEnabled(); await shot(page, 'tape-fixed-line.png'); }); test('the crop slider sets the line ends and nothing else does', async ({ page }) => { test.setTimeout(180_000); await toLengthPage(page); const canvas = page.getByTestId('tape-line').locator('canvas').first(); const fill = page.getByLabel('Crop fill percent'); const before = await canvas.screenshot(); // Move the crop-fill slider and confirm the control really took the value, // so a no-op fill() can never masquerade as a passing assertion. await fill.fill('50'); await expect(fill).toHaveValue('50'); await page.waitForTimeout(1200); const after = await canvas.screenshot(); expect(Buffer.compare(before, after), 'crop fill must move the line ends').not.toBe(0); // Arrow-key nudge on the line must not change the geometry either. // Blur the slider first: arrow keys on a focused range input legitimately // change that input, which would move the line ends for the wrong reason. await page.locator('h1').click(); await page.keyboard.press('ArrowUp'); await page.keyboard.press('ArrowUp'); await page.keyboard.press('ArrowDown'); await page.waitForTimeout(500); const afterKeys = await canvas.screenshot(); expect( Buffer.compare(after, afterKeys), 'arrow keys must not move the fixed line ends', ).toBe(0); await shot(page, 'tape-fixed-line-crop50.png'); }); });