fix(m4): tape line is a fixed fixture, not draggable (#99)
This commit is contained in:
@@ -31,6 +31,12 @@ Fixtures: `tests/fixtures/set1-48x2/` (L=48, thickness=2) and `set2-31x2p5/` (L=
|
||||
| `04-axis.png` | Tape calibration page |
|
||||
| `05-tape-calibration.png` | Length and thickness entered on the tape page |
|
||||
| `06-photos-ready.png` | Photos ready lineup (clean + overlay + siblings) |
|
||||
| `tape-fixed-line.png` | #99 fixed tape line: no tick-drag copy, ticks follow the crop slider |
|
||||
| `tape-fixed-line-crop50.png` | #99 fixed tape line at 50% crop fill (line ends pulled in) |
|
||||
|
||||
`tape-fixed-line.spec.ts` asserts the #99 contract: the line is a stage fixture,
|
||||
its ends are a pure function of `crop_fill`, a drag starting on a tick behaves
|
||||
exactly like a pan elsewhere, and arrow keys cannot move the ends.
|
||||
|
||||
Large regenerating `*.png` and Playwright `test-results/` / `playwright-report/` are gitignored.
|
||||
Committed small evidence JPEGs live in `evidence/` (regenerate with a green `npm run test:e2e`, then re-encode if needed).
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
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 the ticks.
|
||||
await expect(page.getByText('Drag the ticks')).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 before = await canvas.screenshot();
|
||||
|
||||
// Move the crop-fill slider to a different value.
|
||||
const fill = page.getByLabel('Crop fill percent');
|
||||
await fill.fill('50');
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
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');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user