.
This commit is contained in:
parent
d8f2407ddf
commit
9689bfcc18
23 changed files with 1815 additions and 955 deletions
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue