floor-planner/e2e/planner.spec.ts
2026-09-11 16:09:28 +01:00

580 lines
27 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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);
});
}