This commit is contained in:
Andras Schmelczer 2026-09-11 16:09:28 +01:00
commit d8f2407ddf
44 changed files with 10377 additions and 0 deletions

580
e2e/planner.spec.ts Normal file
View file

@ -0,0 +1,580 @@
import { expect, test } from '@playwright/test';
import type { Page } from '@playwright/test';
import { readFile } from 'node:fs/promises';
import { decodeState } from '../src/lib/url-state';
import { planFlooring } from '../src/lib/flooring';
import { planSkirting } from '../src/lib/skirting';
import { flooringCuts, skirtingCuts, uncutFlooring } from '../src/lib/cut-list';
import { mm } from '../src/lib/format';
const errors = new WeakMap<Page, string[]>();
const preview = (page: Page) => page.getByRole('region', { name: 'Floor plan preview' });
const floorRows = (page: Page) =>
page.getByRole('table', { name: 'Flooring cuts and offcut placements' }).locator('tbody tr');
const skirtRows = (page: Page) =>
page.getByRole('table', { name: 'Skirting pieces requiring cuts' }).locator('tbody tr');
const floorCodes = (page: Page) =>
floorRows(page).getByRole('button', { name: /^Preview flooring cut / });
const skirtCodes = (page: Page) =>
skirtRows(page).getByRole('button', { name: /^Preview skirting cut / });
const state = (page: Page) => decodeState(new URL(page.url()).hash).state;
const plans = (page: Page) => {
const { project } = state(page);
return {
project,
floor: planFlooring(project.rooms, project.flooring, project.tolerance),
skirt: planSkirting(project.rooms, project.skirting, project.tolerance),
};
};
async function openWalls(page: Page) {
if (
!(await page
.locator('.wall-details')
.evaluate((element) => (element as HTMLDetailsElement).open))
) {
await page.locator('.wall-details summary').click();
}
}
async function noHorizontalOverflow(page: Page) {
expect(
await page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth + 1),
).toBe(true);
}
async function noDrawingScroll(page: Page) {
const scrollContainers = await preview(page).evaluate((root) =>
[root, ...root.querySelectorAll('*')]
.filter((element): element is HTMLElement => element instanceof HTMLElement)
.filter((element) => {
const style = getComputedStyle(element);
return (
(/auto|scroll/.test(style.overflowX) && element.scrollWidth > element.clientWidth + 1) ||
(/auto|scroll/.test(style.overflowY) && element.scrollHeight > element.clientHeight + 1)
);
})
.map((element) => element.className),
);
expect(
scrollContainers,
'The room drawing must use the page scroll, without its own scrollbars',
).toEqual([]);
}
async function noWallLabelOverlap(page: Page) {
const labels = await preview(page)
.locator('.wall-label text')
.evaluateAll((elements) =>
elements.map((element) => {
const rect = element.getBoundingClientRect();
return {
text: element.textContent,
x: rect.x,
y: rect.y,
right: rect.right,
bottom: rect.bottom,
};
}),
);
expect(labels).toHaveLength(8);
const overlaps: string[] = [];
labels.forEach((label, i) =>
labels.slice(i + 1).forEach((other) => {
if (
Math.min(label.right, other.right) - Math.max(label.x, other.x) > 0.5 &&
Math.min(label.bottom, other.bottom) - Math.max(label.y, other.y) > 0.5
) {
overlaps.push(`${label.text} overlaps ${other.text}`);
}
}),
);
expect(overlaps, 'Fireplace wall dimensions should remain independently readable').toEqual([]);
}
test.beforeEach(async ({ page }) => {
errors.set(page, []);
page.on('pageerror', (error) => errors.get(page)!.push(error.message));
await page.goto('/');
await expect(page.getByRole('heading', { name: 'Room measurements' })).toBeVisible();
});
test.afterEach(async ({ page }) => {
expect(errors.get(page), 'The application should not throw browser errors').toEqual([]);
});
test('measurements, materials, drawing and both cut lists share one page', async ({ page }) => {
await expect(page.getByLabel('Room width', { exact: true })).toHaveValue('4800');
await expect(page.getByLabel('Room length', { exact: true })).toHaveValue('3600');
for (const label of ['Board length', 'Board width', 'Stock length 1', 'Skirting thickness']) {
await expect(page.getByLabel(label, { exact: true })).toBeVisible();
}
await expect(page.getByRole('heading', { name: 'Materials', exact: true })).toBeVisible();
await expect(page.getByRole('heading', { name: /^Flooring cuts/ })).toBeVisible();
await expect(page.getByRole('heading', { name: /^Skirting cuts/ })).toBeVisible();
await expect(preview(page).getByText('Outline closed', { exact: true })).toBeVisible();
await expect(page.locator('.layer-switch, .step-nav')).toHaveCount(0);
await expect(page.locator('.app-header').getByText('mm', { exact: true })).toHaveCount(0);
await expect(page.getByRole('button', { name: /^Zoom (in|out)$/ })).toHaveCount(0);
await noDrawingScroll(page);
await expect(
page.getByRole('button', { name: /Choose materials|Get my cut plan|Measure room/ }),
).toHaveCount(0);
await expect(
page.getByText(
/LESS WASTE\. MORE CONFIDENCE|A better floor starts|Thoughtful plans|Saved in your URL|bounded heuristic|inspect narrow tips/i,
),
).toHaveCount(0);
expect(
await page.locator('#cut-plan').evaluate((element) => element.getBoundingClientRect().top),
).toBeGreaterThan(
await page.locator('#materials').evaluate((element) => element.getBoundingClientRect().bottom),
);
});
test('drawing includes full boards, while numbered cut thumbnails stay complete and sequential', async ({
page,
}) => {
const { project, floor, skirt } = plans(page);
const cutFloor = flooringCuts(floor);
const cutSkirt = skirtingCuts(skirt);
const wholeBoards = uncutFlooring(floor, project.flooring).wholeBoards;
expect(wholeBoards).toBeGreaterThan(0);
await expect(floorCodes(page)).toHaveCount(cutFloor.length);
await expect(skirtCodes(page)).toHaveCount(cutSkirt.length);
const shownCodes = await floorCodes(page).evaluateAll((buttons) =>
buttons.map((button) =>
button.getAttribute('aria-label')!.replace('Preview flooring cut ', ''),
),
);
expect(shownCodes.sort()).toEqual(cutFloor.map((piece) => piece.id).sort());
expect(cutFloor.map((piece) => piece.id)).toEqual(cutFloor.map((_, i) => `F${i + 1}`));
expect(cutSkirt.map((piece) => piece.id)).toEqual(cutSkirt.map((_, i) => `A${i + 1}`));
expect(await floorRows(page).count(), 'Repeated dimensions share one cutting batch').toBeLessThan(
cutFloor.length,
);
const batches = await floorRows(page).evaluateAll((rows) =>
rows.map((row) => ({
codes: [...row.querySelectorAll('button[aria-label^="Preview flooring cut "]')].map(
(button) => button.getAttribute('aria-label')!.replace('Preview flooring cut ', ''),
),
size: row.querySelector('.batch-size')?.textContent?.trim(),
quantity: row.querySelector('.batch-qty')?.textContent?.trim(),
})),
);
for (const batch of batches) {
expect(batch.quantity).toBe(String(batch.codes.length));
for (const code of batch.codes) {
const piece = cutFloor.find((piece) => piece.id === code)!;
expect(batch.size).toBe(`${mm(piece.length)} × ${mm(piece.width)}`);
}
}
await expect(preview(page).locator('.floor-piece')).toHaveCount(floor.pieces.length);
await expect(preview(page).locator('.skirt-piece')).toHaveCount(skirt.pieces.length);
await expect(preview(page).getByRole('button', { name: /^Select flooring cut / })).toHaveCount(
cutFloor.length,
);
await expect(preview(page).getByRole('button', { name: /^Select skirting cut / })).toHaveCount(
cutSkirt.length,
);
await expect(page.locator('.flooring-cuts .cut-summary')).toContainText(
`${wholeBoards} full boards laid uncut`,
);
const drawnCodes = await preview(page).locator('svg text').allTextContents();
for (const piece of floor.pieces.filter((piece) => !piece.cut && !piece.reused)) {
expect(shownCodes).not.toContain(piece.id);
expect(drawnCodes).not.toContain(piece.id);
}
const skirtCutIds = new Set(cutSkirt.map((piece) => piece.id));
for (const piece of skirt.pieces.filter((piece) => !skirtCutIds.has(piece.id))) {
expect(drawnCodes).not.toContain(piece.id);
await expect(
preview(page).locator(`.skirt-piece[data-piece-id="${piece.id}"]`),
).toHaveAttribute('aria-hidden', 'true');
}
const repeatedCode = batches.find((batch) => batch.codes.length > 1)!.codes.at(-1)!;
const repeatedPiece = cutFloor.find((piece) => piece.id === repeatedCode)!;
const thumbnail = page.getByRole('button', {
name: `Preview flooring cut ${repeatedCode}`,
exact: true,
});
await expect(thumbnail.locator('svg')).toBeVisible();
await thumbnail.click();
const dialog = page.getByRole('dialog', { name: `Flooring cut ${repeatedCode}`, exact: true });
await expect(dialog).toBeVisible();
await expect(dialog.locator('.cut-origin')).toContainText(`Row ${repeatedPiece.row}`);
await expect(dialog.locator('.cut-origin')).toContainText(repeatedPiece.stockId);
await expect(dialog.getByRole('img', { name: `Flooring shape ${repeatedCode}` })).toBeVisible();
await page.keyboard.press('Escape');
await expect(page.getByRole('dialog')).toHaveCount(0);
await expect(thumbnail).toBeFocused();
await expect(page.getByRole('button', { name: 'Export CSV' })).toBeEnabled();
});
test('inconsistent final measurements block cuts and small residuals show the expected length', async ({
page,
}) => {
await openWalls(page);
await page.getByLabel('Wall 4 length', { exact: true }).fill('3700');
await expect(page.getByText(/The outline is open by 100\.0 mm/).first()).toBeVisible();
await expect(floorRows(page)).toHaveCount(0);
await expect(skirtRows(page)).toHaveCount(0);
await expect(page.getByRole('button', { name: 'Export CSV' })).toBeDisabled();
await page.getByLabel('Wall 4 length', { exact: true }).fill('3605');
await expect(page.getByText(/non-straight wall/)).toBeVisible();
await expect(page.getByText('Final wall: 3,600 mm expected · 5 mm difference')).toBeVisible();
await expect(page.getByRole('button', { name: 'Export CSV' })).toBeEnabled();
expect(await floorRows(page).count()).toBeGreaterThan(0);
});
test('room warnings stay with their room even when two room names match', async ({ page }) => {
await openWalls(page);
await page.getByLabel('Wall 4 length', { exact: true }).fill('3605');
await expect(page.getByText(/non-straight wall/).first()).toBeVisible();
await page.getByRole('button', { name: 'Add room', exact: true }).click();
await page.getByLabel('Room name', { exact: true }).fill('Living room');
expect(state(page).project.rooms.map((room) => room.name)).toEqual([
'Living room',
'Living room',
]);
await expect(page.getByText(/non-straight wall/)).toHaveCount(0);
await page.locator('.room-tab').first().click();
await expect(page.getByText(/non-straight wall/).first()).toBeVisible();
await openWalls(page);
await page.getByLabel('Wall 4 length', { exact: true }).fill('3700');
await expect(page.getByText(/outline is open by 100\.0 mm/).first()).toBeVisible();
await page.locator('.room-tab').nth(1).click();
await expect(page.getByText(/outline is open by 100\.0 mm/)).toHaveCount(0);
await page.locator('.room-tab').first().click();
await expect(page.getByText(/outline is open by 100\.0 mm/).first()).toBeVisible();
});
test('custom walls close using a calculated wall that can be measured again', async ({ page }) => {
await page.getByRole('button', { name: 'Custom', exact: true }).click();
await page.getByRole('button', { name: 'Add a wall', exact: true }).click();
await page.getByLabel('Wall 2 length', { exact: true }).fill('3000');
await page.getByRole('button', { name: 'Add a wall', exact: true }).click();
await page.getByLabel('Wall 3 length', { exact: true }).fill('4000');
await expect(page.getByText('3,000 mm to close')).toBeVisible();
await page.getByRole('button', { name: 'Add closing wall' }).click();
await expect(preview(page).getByText('Outline closed', { exact: true })).toBeVisible();
await openWalls(page);
await expect(page.getByLabel('Wall 4 length', { exact: true })).toHaveValue('3000');
await page.getByLabel('Wall 4 length', { exact: true }).fill('3050');
await expect(page.getByText(/3,000 mm expected; length differs by 50 mm/)).toBeVisible();
await expect(page.getByRole('button', { name: 'Export CSV' })).toBeDisabled();
});
test('fireplace thumbnails open true-scale mitres and shaped floor cuts in dialogs', async ({
page,
}) => {
await page.getByRole('button', { name: 'With fireplace' }).click();
await expect(page.getByLabel('Wall 8 length', { exact: true })).toHaveValue('3600');
await expect(preview(page).getByRole('img', { name: /floor plan with/ })).toHaveAttribute(
'aria-label',
/8 walls/,
);
await noWallLabelOverlap(page);
const { floor, skirt } = plans(page);
const mitred = skirtingCuts(skirt).find(
(piece) => Math.abs(piece.leftMitre) === 45 || Math.abs(piece.rightMitre) === 45,
)!;
expect(mitred).toBeDefined();
const thumbnail = page.getByRole('button', {
name: `Preview skirting cut ${mitred.id}`,
exact: true,
});
await expect(thumbnail.locator('svg').first()).toBeVisible();
await thumbnail.click();
const dialog = page.getByRole('dialog', { name: `Skirting cut ${mitred.id}`, exact: true });
await expect(dialog).toBeVisible();
await expect(dialog).toContainText(/long point/i);
const end = Math.abs(mitred.leftMitre) === 45 ? 'left' : 'right';
const projected = await dialog
.locator(`.skirting-cut-edge[data-end="${end}"]`)
.evaluate((element) => {
const line = element as SVGLineElement;
const matrix = line.getScreenCTM()!;
const start = new DOMPoint(line.x1.baseVal.value, line.y1.baseVal.value).matrixTransform(
matrix,
);
const finish = new DOMPoint(line.x2.baseVal.value, line.y2.baseVal.value).matrixTransform(
matrix,
);
return { dx: Math.abs(finish.x - start.x), dy: Math.abs(finish.y - start.y) };
});
expect(projected.dx).toBeGreaterThan(5);
expect(projected.dy).toBeGreaterThan(5);
expect(
projected.dx,
'A 45° mitre must have equal horizontal and vertical screen distances',
).toBeCloseTo(projected.dy, 1);
await page.getByRole('button', { name: 'Close cut preview', exact: true }).click();
await expect(thumbnail).toBeFocused();
const shaped = flooringCuts(floor).find((piece) => piece.shaped)!;
expect(shaped).toBeDefined();
await page
.getByRole('button', { name: `Preview flooring cut ${shaped.id}`, exact: true })
.click();
await expect(
page.getByRole('dialog', { name: `Flooring cut ${shaped.id}`, exact: true }),
).toBeVisible();
await expect(page.getByRole('img', { name: `Flooring shape ${shaped.id}` })).toBeVisible();
await page.keyboard.press('Escape');
await expect(page.getByRole('dialog')).toHaveCount(0);
await expect(page.locator('.selected-cut')).toHaveCount(0);
});
test('door positions and material lengths update skirting immediately', async ({ page }) => {
const before = await page.locator('.skirting-cuts').innerText();
await page.getByRole('button', { name: 'Add a doorway' }).click();
await expect(page.locator('.door-card')).toHaveCount(2);
await page.locator('.door-card').last().getByRole('combobox').selectOption({ label: 'W1' });
const movedDoor = page.locator('.door-card').first();
await movedDoor.getByLabel('Door offset', { exact: true }).fill('1200');
await movedDoor.getByLabel('Door width', { exact: true }).fill('900');
expect(state(page).project.rooms[0].walls[0].doors[0]).toMatchObject({
offset: 1200,
width: 900,
});
await expect(page.getByRole('button', { name: 'Export CSV' })).toBeEnabled();
expect(await page.locator('.skirting-cuts').innerText()).not.toBe(before);
await page.getByLabel('Stock length 1', { exact: true }).fill('3600');
await page.getByLabel('Skirting thickness', { exact: true }).fill('22');
const { skirt } = plans(page);
await expect(skirtCodes(page)).toHaveCount(skirtingCuts(skirt).length);
await expect(page.locator('.skirting-cuts .cut-summary')).toContainText(
`Buy ${skirt.stockCount} lengths`,
);
await expect(page.getByRole('button', { name: 'Export CSV' })).toBeEnabled();
await movedDoor.getByLabel('Door width', { exact: true }).fill('6000');
await expect(page.getByText(/must fit within the wall/).first()).toBeVisible();
await expect(skirtRows(page)).toHaveCount(0);
await expect(page.getByRole('button', { name: 'Export CSV' })).toBeDisabled();
});
test('invalid skirting settings keep flooring results usable', async ({ page }) => {
const floorCount = await floorCodes(page).count();
await page.getByLabel('Stock length 1', { exact: true }).fill('100');
await expect(skirtRows(page)).toHaveCount(0);
await expect(floorCodes(page)).toHaveCount(floorCount);
await expect(page.locator('.issue.error').first()).toBeVisible();
await expect(page.getByRole('button', { name: 'Export CSV' })).toBeDisabled();
await page.getByLabel('Stock length 1', { exact: true }).fill('2400');
expect(await skirtRows(page).count()).toBeGreaterThan(0);
await expect(page.getByRole('button', { name: 'Export CSV' })).toBeEnabled();
});
test('pattern, direction and spacers persist without steps, layers or stored results', async ({
page,
}) => {
await page.getByLabel('Room length', { exact: true }).fill('3650');
await page.getByRole('button', { name: 'Random', exact: true }).click();
await page.getByRole('button', { name: 'Across first wall', exact: true }).click();
await page.getByLabel('Wall spacer', { exact: true }).fill('12');
expect(state(page).project.flooring).toMatchObject({ pattern: 'random', gap: 12 });
expect(state(page).project.rooms[0].direction).toBe(90);
expect(Object.keys(state(page).view).sort()).toEqual(['roomId', 'selectedPiece']);
expect(Object.keys(state(page)).sort()).toEqual(['project', 'view']);
const table = await page.locator('.flooring-cuts tbody').innerText();
await page.reload();
await expect(page.getByLabel('Wall spacer', { exact: true })).toHaveValue('12');
await expect(page.getByRole('button', { name: 'Random', exact: true })).toHaveAttribute(
'aria-pressed',
'true',
);
await expect(
page.getByRole('button', { name: 'Across first wall', exact: true }),
).toHaveAttribute('aria-pressed', 'true');
await expect(page.locator('.flooring-cuts tbody')).toHaveText(table, { useInnerText: true });
await noHorizontalOverflow(page);
expect(await page.evaluate(() => Object.keys(localStorage))).toEqual([]);
});
test('a narrow remainder is shared between the first and last flooring rows', async ({ page }) => {
// 3650 minus two 10 mm spacers leaves 19 full rows and a 20 mm sliver.
await page.getByLabel('Room length', { exact: true }).fill('3650');
const { floor } = plans(page);
for (const row of [1, 20]) {
const piece = flooringCuts(floor).find((piece) => piece.row === row);
expect(piece).toBeDefined();
const group = floorRows(page).filter({
has: page.getByRole('button', { name: `Preview flooring cut ${piece!.id}`, exact: true }),
});
await expect(group.locator('[data-label="Cut size · mm"]')).toContainText('× 105');
}
});
test('multiple rooms retain independent measurements with combined cut lists', async ({ page }) => {
await page.getByRole('button', { name: 'Add room', exact: true }).click();
await expect(page.getByLabel('Room name', { exact: true })).toHaveValue('Room 2');
await page.getByLabel('Room name', { exact: true }).fill('Study');
await page.getByLabel('Room width', { exact: true }).fill('3200');
expect(state(page).project.rooms).toHaveLength(2);
expect(state(page).project.rooms[0].walls[0].length).toBe(4800);
expect(state(page).project.rooms[1].walls[0].length).toBe(3200);
expect(await floorRows(page).filter({ hasText: 'Living room' }).count()).toBeGreaterThan(0);
expect(await skirtRows(page).filter({ hasText: 'Study' }).count()).toBeGreaterThan(0);
await page.getByRole('button', { name: /^Living room.*m²/ }).click();
await expect(page.getByLabel('Room width', { exact: true })).toHaveValue('4800');
await page.getByRole('button', { name: /^Study.*m²/ }).click();
await expect(page.getByLabel('Room width', { exact: true })).toHaveValue('3200');
await page.getByRole('button', { name: 'Remove this room' }).click();
expect(state(page).project.rooms).toHaveLength(1);
await page.reload();
await expect(page.getByLabel('Room name', { exact: true })).toHaveValue('Living room');
});
test('shared URLs reopen the selected preview and CSV contains every cut once', async ({
page,
context,
}) => {
await page.getByLabel('Room name', { exact: true }).fill('Café 北');
const { floor, skirt } = plans(page);
const pieceCode = flooringCuts(floor)[0].id;
await page
.getByRole('button', { name: `Preview flooring cut ${pieceCode}`, exact: true })
.click();
await expect(
page.getByRole('dialog', { name: `Flooring cut ${pieceCode}`, exact: true }),
).toBeVisible();
expect(state(page).view.selectedPiece).toBe(pieceCode);
const shareUrl = page.url();
const originalTable = await page.locator('.flooring-cuts tbody').innerText();
const shared = await context.newPage();
await shared.goto(shareUrl);
await expect(
shared.getByRole('dialog', { name: `Flooring cut ${pieceCode}`, exact: true }),
).toBeVisible();
await expect(shared.locator('.flooring-cuts tbody')).toHaveText(originalTable, {
useInnerText: true,
});
await shared.reload();
await expect(
shared.getByRole('dialog', { name: `Flooring cut ${pieceCode}`, exact: true }),
).toBeVisible();
await shared.close();
await page.keyboard.press('Escape');
expect(state(page).view.selectedPiece).toBeNull();
const downloadPromise = page.waitForEvent('download');
await page.getByRole('button', { name: 'Export CSV' }).click();
const download = await downloadPromise;
expect(download.suggestedFilename()).toBe('flooring-cut-plan.csv');
const csv = await readFile((await download.path())!, 'utf8');
expect(csv).toContain('"Material","Room","Piece","Stock"');
expect(csv).toContain(`"Flooring","Café 北","${pieceCode}"`);
expect(csv.split('\r\n').filter((line) => line.startsWith('"Flooring",'))).toHaveLength(
flooringCuts(floor).length,
);
expect(csv.split('\r\n').filter((line) => line.startsWith('"Skirting",'))).toHaveLength(
skirtingCuts(skirt).length,
);
for (const piece of floor.pieces.filter((piece) => !piece.cut && !piece.reused))
expect(csv).not.toContain(`"${piece.id}"`);
expect(csv).not.toContain('bounded heuristic');
await context.grantPermissions(['clipboard-read', 'clipboard-write']);
const closedUrl = page.url();
await page.getByRole('button', { name: 'Copy link', exact: true }).click();
await expect(page.getByText('Link copied', { exact: true })).toBeVisible();
expect(await page.evaluate(() => navigator.clipboard.readText())).toBe(closedUrl);
await page.reload();
await expect(page.locator('.flooring-cuts tbody')).toHaveText(originalTable, {
useInnerText: true,
});
});
test('saw kerf help works with keyboard and touch or mouse', async ({ page, isMobile }) => {
for (const name of ['What is saw kerf?', 'What is skirting saw kerf?']) {
const button = page.getByRole('button', { name, exact: true });
await button.focus();
await button.press('Space');
await expect(button).toHaveAttribute('aria-expanded', 'true');
await expect(page.getByRole('tooltip')).toContainText(
'Width of material removed by the saw blade.',
);
await expect(page.getByRole('tooltip')).toContainText('600 mm + 397 mm');
await button.press('Escape');
await expect(page.getByRole('tooltip')).toHaveCount(0);
if (isMobile) await button.tap();
else await button.click();
await expect(page.getByRole('tooltip')).toBeVisible();
await noHorizontalOverflow(page);
await page.getByLabel('Board length', { exact: true }).focus();
await expect(page.getByRole('tooltip')).toHaveCount(0);
}
});
test('invalid URLs recover when measurements are edited', async ({ page }) => {
await page.goto('/#plan=broken');
await expect(page.getByText(/Could not load this plan/)).toBeVisible();
await page.getByLabel('Room name', { exact: true }).fill('Recovered plan');
await expect(page.getByText(/Could not load this plan/)).toHaveCount(0);
expect(decodeState(new URL(page.url()).hash).error).toBeNull();
await page.reload();
await expect(page.getByLabel('Room name', { exact: true })).toHaveValue('Recovered plan');
});
for (const width of [320, 390]) {
test(`phone layout works at ${width} px with readable inputs and touch targets`, async ({
page,
}) => {
await page.setViewportSize({ width, height: 844 });
await openWalls(page);
await page.getByRole('button', { name: 'What is saw kerf?', exact: true }).click();
await noHorizontalOverflow(page);
await expect(page.getByRole('tooltip')).toBeVisible();
const smallControls = await page
.locator('button, input, select, summary')
.evaluateAll((elements) =>
elements.flatMap((element) => {
const rect = element.getBoundingClientRect();
return rect.width > 0 && rect.height > 0 && (rect.width < 39.5 || rect.height < 39.5)
? [
{
label: element.getAttribute('aria-label') || element.textContent?.trim(),
width: rect.width,
height: rect.height,
},
]
: [];
}),
);
expect(smallControls, 'Every visible form control should have a usable touch target').toEqual(
[],
);
const smallInputs = await page
.locator('input, select')
.evaluateAll((elements) =>
elements
.filter(
(element) =>
element.getBoundingClientRect().height > 0 &&
parseFloat(getComputedStyle(element).fontSize) < 16,
)
.map((element) => element.getAttribute('aria-label') || element.id),
);
expect(smallInputs, '16 px input text avoids phone browser zoom').toEqual([]);
expect(
await floorRows(page)
.first()
.evaluate((element) => getComputedStyle(element).display),
).toMatch(/block|grid/);
await page.getByRole('button', { name: 'With fireplace' }).click();
await noWallLabelOverlap(page);
await noDrawingScroll(page);
await skirtCodes(page).first().click();
const dialog = page.getByRole('dialog');
await expect(dialog).toBeVisible();
const box = await dialog.boundingBox();
expect(box!.x).toBeGreaterThanOrEqual(0);
expect(box!.y).toBeGreaterThanOrEqual(0);
expect(box!.x + box!.width).toBeLessThanOrEqual(width);
expect(box!.y + box!.height).toBeLessThanOrEqual(844);
await noHorizontalOverflow(page);
await page.mouse.click(1, 1);
await expect(page.getByRole('dialog')).toHaveCount(0);
expect(state(page).view.selectedPiece).toBeNull();
await page.getByText('Source lengths for skirting cuts', { exact: true }).click();
await noHorizontalOverflow(page);
});
}