142 lines
5.8 KiB
TypeScript
142 lines
5.8 KiB
TypeScript
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');
|
|
});
|
|
});
|