This commit is contained in:
Andras Schmelczer 2026-09-11 16:58:56 +01:00
commit 9689bfcc18
23 changed files with 1815 additions and 955 deletions

View file

@ -1,7 +1,9 @@
import { expect, test } from '@playwright/test';
import type { Page } from '@playwright/test';
import type { Locator, Page } from '@playwright/test';
import LZString from 'lz-string';
import { readFile } from 'node:fs/promises';
import { decodeState } from '../src/lib/url-state';
import { decodeState, encodeState } from '../src/lib/url-state';
import { defaultState, rectangle } from '../src/lib/defaults';
import { planFlooring } from '../src/lib/flooring';
import { planSkirting } from '../src/lib/skirting';
import { flooringCuts, skirtingCuts, uncutFlooring } from '../src/lib/cut-list';
@ -9,15 +11,16 @@ 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 floorEntries = (page: Page) => page.locator('.flooring-cuts .board-cut');
const skirtEntries = (page: Page) => page.locator('.skirting-cuts .board-cut');
const floorCodes = (page: Page) =>
floorRows(page).getByRole('button', { name: /^Preview flooring cut / });
floorEntries(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;
skirtEntries(page).getByRole('button', { name: /^Preview skirting cut / });
const state = (page: Page) => {
const url = new URL(page.url());
return decodeState(url.search || url.hash).state;
};
const plans = (page: Page) => {
const { project } = state(page);
return {
@ -27,6 +30,49 @@ const plans = (page: Page) => {
};
};
async function stockGroups(groups: Locator) {
return groups.evaluateAll((elements) =>
elements.map((element) => ({
stockId: element.getAttribute('data-stock-id'),
codes: [...element.querySelectorAll('[data-cut-id]')]
.map((cut) => cut.getAttribute('data-cut-id'))
.sort(),
})),
);
}
function expectedGroups(stocks: { id: string; pieceIds: string[] }[], cuts: { id: string }[]) {
const visible = new Set(cuts.map((piece) => piece.id));
return stocks
.map((stock) => ({
stockId: stock.id,
codes: stock.pieceIds.filter((id) => visible.has(id)).sort(),
}))
.filter((group) => group.codes.length);
}
async function measuredEdges(preview: Locator) {
return preview.evaluate((element) => {
const polygon = element.querySelector('polygon')!;
const points = [...(polygon as SVGPolygonElement).points].map((point) => ({
x: point.x,
y: point.y,
}));
return [...element.querySelectorAll('.dimension-caption')].map((caption) => {
const edge = Number(caption.getAttribute('data-edge'));
const from = points[edge],
to = points[(edge + 1) % points.length];
return {
shown: Number(
(caption.querySelector('.dimension-label tspan') ??
caption.querySelector('.dimension-label'))!.textContent!.replaceAll(',', ''),
),
actual: Math.hypot(to.x - from.x, to.y - from.y),
};
});
});
}
async function openWalls(page: Page) {
if (
!(await page
@ -97,6 +143,7 @@ test.beforeEach(async ({ page }) => {
page.on('pageerror', (error) => errors.get(page)!.push(error.message));
await page.goto('/');
await expect(page.getByRole('heading', { name: 'Room measurements' })).toBeVisible();
await expect(page).toHaveURL(/\?version=1&/);
});
test.afterEach(async ({ page }) => {
@ -132,43 +179,37 @@ test('measurements, materials, drawing and both cut lists share one page', async
);
});
test('drawing includes full boards, while numbered cut thumbnails stay complete and sequential', async ({
test('source-board groups contain every numbered cut once and leave full boards unlabelled', async ({
page,
}) => {
const { project, floor, skirt } = plans(page);
const cutFloor = flooringCuts(floor);
const cutSkirt = skirtingCuts(skirt);
const cutFloor = flooringCuts(floor),
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 ', ''),
),
);
const floorGroups = await stockGroups(page.locator('.flooring-cuts .stock-cut-group'));
const skirtGroups = await stockGroups(page.locator('.skirting-cuts .stock-cut-group'));
expect(floorGroups).toEqual(expectedGroups(floor.stocks, cutFloor));
expect(skirtGroups).toEqual(expectedGroups(skirt.stocks, cutSkirt));
const shownCodes = floorGroups.flatMap((group) => group.codes);
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)}`);
}
for (const piece of cutFloor) {
await expect(
floorEntries(page)
.filter({
has: page.getByRole('button', {
name: `Preview flooring cut ${piece.id}`,
exact: true,
}),
})
.locator('.cut-size'),
).toHaveText(`${mm(piece.length)} × ${mm(piece.width)} mm`);
}
await expect(page.locator('.material-cuts table')).toHaveCount(0);
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(
@ -178,7 +219,7 @@ test('drawing includes full boards, while numbered cut thumbnails stay complete
cutSkirt.length,
);
await expect(page.locator('.flooring-cuts .cut-summary')).toContainText(
`${wholeBoards} full boards laid uncut`,
`${wholeBoards} laid uncut`,
);
const drawnCodes = await preview(page).locator('svg text').allTextContents();
for (const piece of floor.pieces.filter((piece) => !piece.cut && !piece.reused)) {
@ -192,19 +233,29 @@ test('drawing includes full boards, while numbered cut thumbnails stay complete
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 uncutColor = await preview(page)
.locator('.skirt-piece.uncut > polygon')
.first()
.getAttribute('fill');
const cutColor = await preview(page).locator('.skirt-cut > polygon').first().getAttribute('fill');
expect(uncutColor).not.toBe(cutColor);
const piece = cutFloor[0];
const thumbnail = page.getByRole('button', {
name: `Preview flooring cut ${repeatedCode}`,
name: `Preview flooring cut ${piece.id}`,
exact: true,
});
await expect(thumbnail.locator('svg')).toBeVisible();
await thumbnail.click();
const dialog = page.getByRole('dialog', { name: `Flooring cut ${repeatedCode}`, exact: true });
const dialog = page.getByRole('dialog', { name: `Flooring cut ${piece.id}`, 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 expect(dialog.locator('.cut-origin')).toHaveText(
`${project.rooms[0].name} · Row ${piece.row}`,
);
const shape = dialog.getByRole('img', { name: `Flooring shape ${piece.id}` });
await expect(shape).toBeVisible();
const dimensions = await measuredEdges(shape);
expect(dimensions).toHaveLength(2);
for (const dimension of dimensions) expect(dimension.shown).toBeCloseTo(dimension.actual, 3);
await page.keyboard.press('Escape');
await expect(page.getByRole('dialog')).toHaveCount(0);
await expect(thumbnail).toBeFocused();
@ -217,14 +268,14 @@ test('inconsistent final measurements block cuts and small residuals show the ex
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(floorEntries(page)).toHaveCount(0);
await expect(skirtEntries(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);
expect(await floorEntries(page).count()).toBeGreaterThan(0);
});
test('room warnings stay with their room even when two room names match', async ({ page }) => {
@ -288,7 +339,14 @@ test('fireplace thumbnails open true-scale mitres and shaped floor cuts in dialo
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);
await expect(dialog).toContainText(/longest points/i);
const lengths = await dialog.locator('.skirting-full-preview .dimension-label').allTextContents();
expect(lengths.map((text) => Number(text.replaceAll(',', '')))).toEqual(
expect.arrayContaining([
Number(mitred.length.toFixed(3)),
Number(mitred.backLength.toFixed(3)),
]),
);
const end = Math.abs(mitred.leftMitre) === 45 ? 'left' : 'right';
const projected = await dialog
.locator(`.skirting-cut-edge[data-end="${end}"]`)
@ -319,7 +377,16 @@ test('fireplace thumbnails open true-scale mitres and shaped floor cuts in dialo
await expect(
page.getByRole('dialog', { name: `Flooring cut ${shaped.id}`, exact: true }),
).toBeVisible();
await expect(page.getByRole('img', { name: `Flooring shape ${shaped.id}` })).toBeVisible();
const shapedPreview = page
.getByRole('dialog')
.getByRole('img', { name: `Flooring shape ${shaped.id}` });
await expect(shapedPreview).toBeVisible();
const dimensions = await measuredEdges(shapedPreview);
expect(dimensions).toHaveLength(shaped.template.length);
for (const dimension of dimensions) expect(dimension.shown).toBeCloseTo(dimension.actual, 3);
await expect(
page.getByRole('dialog').locator('.template-coordinates, .vertex-label, .cut-measures'),
).toHaveCount(0);
await page.keyboard.press('Escape');
await expect(page.getByRole('dialog')).toHaveCount(0);
await expect(page.locator('.selected-cut')).toHaveCount(0);
@ -331,12 +398,19 @@ test('door positions and material lengths update skirting immediately', async ({
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 expect(movedDoor.locator('.door-position')).toContainText(
'W1: measure from W4 corner toward W2 corner.',
);
await movedDoor.getByLabel(/^From W\d+ (corner|start)$/).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(movedDoor.getByRole('img', { name: /^Door on W1:/ })).toHaveAttribute(
'aria-label',
'Door on W1: 1,200 mm from W4 corner, then 900 mm opening toward W2 corner',
);
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');
@ -349,19 +423,19 @@ test('door positions and material lengths update skirting immediately', async ({
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(skirtEntries(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(skirtEntries(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);
expect(await skirtEntries(page).count()).toBeGreaterThan(0);
await expect(page.getByRole('button', { name: 'Export CSV' })).toBeEnabled();
});
@ -376,7 +450,7 @@ test('pattern, direction and spacers persist without steps, layers or stored res
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();
const table = await page.locator('.flooring-cuts').innerText();
await page.reload();
await expect(page.getByLabel('Wall spacer', { exact: true })).toHaveValue('12');
await expect(page.getByRole('button', { name: 'Random', exact: true })).toHaveAttribute(
@ -386,7 +460,7 @@ test('pattern, direction and spacers persist without steps, layers or stored res
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 expect(page.locator('.flooring-cuts')).toHaveText(table, { useInnerText: true });
await noHorizontalOverflow(page);
expect(await page.evaluate(() => Object.keys(localStorage))).toEqual([]);
});
@ -398,10 +472,10 @@ test('a narrow remainder is shared between the first and last flooring rows', as
for (const row of [1, 20]) {
const piece = flooringCuts(floor).find((piece) => piece.row === row);
expect(piece).toBeDefined();
const group = floorRows(page).filter({
const group = floorEntries(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');
await expect(group.locator('.cut-size')).toContainText('× 105');
}
});
@ -413,8 +487,8 @@ test('multiple rooms retain independent measurements with combined cut lists', a
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);
expect(await floorEntries(page).filter({ hasText: 'Living room' }).count()).toBeGreaterThan(0);
expect(await skirtEntries(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();
@ -440,13 +514,13 @@ test('shared URLs reopen the selected preview and CSV contains every cut once',
).toBeVisible();
expect(state(page).view.selectedPiece).toBe(pieceCode);
const shareUrl = page.url();
const originalTable = await page.locator('.flooring-cuts tbody').innerText();
const originalTable = await page.locator('.flooring-cuts').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, {
await expect(shared.locator('.flooring-cuts')).toHaveText(originalTable, {
useInnerText: true,
});
await shared.reload();
@ -478,7 +552,7 @@ test('shared URLs reopen the selected preview and CSV contains every cut once',
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, {
await expect(page.locator('.flooring-cuts')).toHaveText(originalTable, {
useInnerText: true,
});
});
@ -505,15 +579,141 @@ test('saw kerf help works with keyboard and touch or mouse', async ({ page, isMo
});
test('invalid URLs recover when measurements are edited', async ({ page }) => {
await page.goto('/#plan=broken');
await page.goto('/?flooring.length=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();
expect(decodeState(new URL(page.url()).search).error).toBeNull();
expect(new URL(page.url()).hash).toBe('');
await page.reload();
await expect(page.getByLabel('Room name', { exact: true })).toHaveValue('Recovered plan');
});
test('clicking the skirting code itself opens its cutting dialog', async ({ page }) => {
for (const target of ['rect', 'text']) {
const code = preview(page).locator('.skirting-code').first();
const id = await code.evaluate((element) =>
element.closest('[data-piece-id]')!.getAttribute('data-piece-id'),
);
await code.locator(target).click();
await expect(
page.getByRole('dialog', { name: `Skirting cut ${id}`, exact: true }),
).toBeVisible();
expect(state(page).view.selectedPiece).toBe(id);
await page.keyboard.press('Escape');
await expect(page.getByRole('dialog')).toHaveCount(0);
}
});
test('readable query fields, legacy links and browser history preserve the plan', async ({
page,
}) => {
const legacy = defaultState();
legacy.project.rooms[0].name = 'Legacy lounge';
legacy.project.flooring.pattern = 'random';
const hash = `#plan=${LZString.compressToEncodedURIComponent(JSON.stringify(legacy))}`;
await page.goto(`/${hash}`);
await expect(page).toHaveURL(/\?version=1&/);
expect(new URL(page.url()).hash).toBe('');
expect(state(page)).toEqual(legacy);
const original = page.url();
await page.getByRole('button', { name: 'Add room', exact: true }).click();
await page.getByLabel('Room name', { exact: true }).fill('Study');
await page.getByLabel('Room width', { exact: true }).fill('3333');
const twoRooms = page.url();
const params = new URL(twoRooms).searchParams;
expect(params.get('room.2.name')).toBe('Study');
expect(params.get('room.2.wall.1.length')).toBe('3333');
expect(params.get('flooring.pattern')).toBe('random');
expect(params.has('plan')).toBe(false);
expect([...params.keys()].some((key) => /boardCount|polygon|pieceIds/.test(key))).toBe(false);
await page.goBack();
await expect(page).toHaveURL(original);
await expect(page.getByLabel('Room name', { exact: true })).toHaveValue('Legacy lounge');
expect(state(page).project.rooms).toHaveLength(1);
await page.goForward();
await expect(page).toHaveURL(twoRooms);
await expect(page.getByLabel('Room name', { exact: true })).toHaveValue('Study');
await expect(page.getByLabel('Room width', { exact: true })).toHaveValue('3333');
params.set('flooring.width', '205');
await page.goto(`/?${params}`);
await expect(page.getByLabel('Board width', { exact: true })).toHaveValue('205');
await page.reload();
await expect(page.getByLabel('Board width', { exact: true })).toHaveValue('205');
expect(state(page).project.rooms).toHaveLength(2);
});
test('printing includes every room and every cut once, grouped by its source board', async ({
page,
}) => {
const fixture = defaultState();
fixture.project.flooring.width = 200;
fixture.project.rooms = [
rectangle('room-1', 'Living room', 1820, 1220),
rectangle('room-2', 'Study', 1220, 1820),
];
fixture.project.rooms.forEach((room) =>
room.walls.forEach((wall) => {
wall.doors = [];
}),
);
fixture.project.rooms[1].direction = 90;
fixture.view.roomId = 'room-2';
await page.goto(`/${encodeState(fixture)}`);
await expect(page.getByLabel('Room name', { exact: true })).toHaveValue('Study');
const { floor, skirt, project } = plans(page);
const cuts = flooringCuts(floor),
skirting = skirtingCuts(skirt);
const screenFloor = await stockGroups(page.locator('.flooring-cuts .stock-cut-group'));
const screenSkirting = await stockGroups(page.locator('.skirting-cuts .stock-cut-group'));
expect(screenFloor).toEqual(expectedGroups(floor.stocks, cuts));
expect(screenSkirting).toEqual(expectedGroups(skirt.stocks, skirting));
expect(
screenFloor.some(
(group) =>
new Set(group.codes.map((id) => cuts.find((piece) => piece.id === id)!.roomId)).size > 1,
),
).toBe(true);
await expect(page.locator('.print-plan')).toBeHidden();
await page.emulateMedia({ media: 'print' });
const printed = page.locator('.print-plan');
await expect(printed).toBeVisible();
await expect(page.getByRole('heading', { name: 'Room measurements' })).toBeHidden();
await expect(printed.locator('.print-room')).toHaveCount(project.rooms.length);
for (const room of project.rooms) {
const drawing = printed.locator(`.print-room[data-room-id="${room.id}"]`);
await expect(drawing.locator('h2').first()).toContainText(room.name);
await expect(drawing.getByRole('img', { name: /floor plan with/ })).toBeVisible();
await expect(drawing.locator('.floor-piece')).toHaveCount(
floor.pieces.filter((piece) => piece.roomId === room.id).length,
);
await expect(drawing.locator('.skirt-piece')).toHaveCount(
skirt.pieces.filter((piece) => piece.roomId === room.id).length,
);
await expect(drawing.getByRole('button')).toHaveCount(0);
}
const printedFloor = await stockGroups(printed.locator('.print-flooring .print-stock'));
const printedSkirting = await stockGroups(printed.locator('.print-skirting .print-stock'));
expect(printedFloor).toEqual(screenFloor);
expect(printedSkirting).toEqual(screenSkirting);
expect(printedFloor.flatMap((group) => group.codes).sort()).toEqual(
cuts.map((piece) => piece.id).sort(),
);
expect(printedSkirting.flatMap((group) => group.codes).sort()).toEqual(
skirting.map((piece) => piece.id).sort(),
);
for (const piece of [...cuts, ...skirting]) {
const cut = printed.locator(`.print-cut[data-cut-id="${piece.id}"]`);
await expect(cut).toBeVisible();
expect(await cut.locator('.dimension-label').count()).toBeGreaterThanOrEqual(2);
await expect(cut.locator('svg').first()).toBeVisible();
}
expect(await printed.innerText()).not.toMatch(/\b(?:FU|SU)\d+\b/);
await page.emulateMedia({ media: 'screen' });
await expect(printed).toBeHidden();
await expect(page.getByLabel('Room name', { exact: true })).toHaveValue('Study');
});
for (const width of [320, 390]) {
test(`phone layout works at ${width} px with readable inputs and touch targets`, async ({
page,
@ -555,10 +755,10 @@ for (const width of [320, 390]) {
);
expect(smallInputs, '16 px input text avoids phone browser zoom').toEqual([]);
expect(
await floorRows(page)
await floorEntries(page)
.first()
.evaluate((element) => getComputedStyle(element).display),
).toMatch(/block|grid/);
).toMatch(/block|grid|flex/);
await page.getByRole('button', { name: 'With fireplace' }).click();
await noWallLabelOverlap(page);
await noDrawingScroll(page);
@ -574,7 +774,9 @@ for (const width of [320, 390]) {
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 expect(page.getByText('Source lengths for skirting cuts', { exact: true })).toHaveCount(
0,
);
await noHorizontalOverflow(page);
});
}