better
This commit is contained in:
parent
b9485e4097
commit
0c0f08b350
45 changed files with 1565 additions and 1579 deletions
|
|
@ -1,6 +1,5 @@
|
|||
import { expect, test } from '@playwright/test';
|
||||
import type { Locator, Page } from '@playwright/test';
|
||||
import LZString from 'lz-string';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import { decodeState, encodeState } from '../src/lib/url-state';
|
||||
import { PROJECT_STORAGE_KEY } from '../src/lib/use-project';
|
||||
|
|
@ -9,6 +8,7 @@ import { planFlooring } from '../src/lib/flooring';
|
|||
import { planSkirting } from '../src/lib/skirting';
|
||||
import { flooringCuts, skirtingCuts, uncutFlooring } from '../src/lib/cut-list';
|
||||
import { cutMm } from '../src/lib/format';
|
||||
import type { UrlState } from '../src/lib/types';
|
||||
|
||||
const errors = new WeakMap<Page, string[]>();
|
||||
const panel = (page: Page, section: string) =>
|
||||
|
|
@ -22,7 +22,7 @@ const skirtCodes = (page: Page) =>
|
|||
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;
|
||||
return decodeState(url.search).state;
|
||||
};
|
||||
const savedQuery = (page: Page) =>
|
||||
page.evaluate((key) => localStorage.getItem(key), PROJECT_STORAGE_KEY);
|
||||
|
|
@ -60,6 +60,7 @@ function shortFireplaceFixture() {
|
|||
length: 1200,
|
||||
width: 190,
|
||||
pattern: 'random',
|
||||
optimisation: 'cuts',
|
||||
minOffset: 100,
|
||||
maxOffset: 1000,
|
||||
minLength: 10,
|
||||
|
|
@ -265,7 +266,7 @@ test.beforeEach(async ({ page }) => {
|
|||
page.on('pageerror', (error) => errors.get(page)!.push(error.message));
|
||||
await page.goto('/?planner');
|
||||
await expect(page.getByRole('heading', { name: 'Room measurements' })).toBeVisible();
|
||||
await expect(page).toHaveURL(/\?version=1&/);
|
||||
await expect(page).toHaveURL(/\?version=2&/);
|
||||
});
|
||||
|
||||
test.afterEach(async ({ page }) => {
|
||||
|
|
@ -345,10 +346,14 @@ test('source-board groups contain every numbered cut once and leave full boards
|
|||
}
|
||||
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(
|
||||
cutFloor.length,
|
||||
);
|
||||
await page
|
||||
.getByRole('group', { name: 'Plan view' })
|
||||
.getByRole('button', { name: 'Skirting', exact: true })
|
||||
.click();
|
||||
await expect(preview(page).locator('.skirt-piece')).toHaveCount(skirt.pieces.length);
|
||||
await expect(preview(page).getByRole('button', { name: /^Select skirting cut / })).toHaveCount(
|
||||
cutSkirt.length,
|
||||
);
|
||||
|
|
@ -539,7 +544,7 @@ test('internal skirting corners retain coping blanks and expose the installed mi
|
|||
expect(
|
||||
skirt.pieces.every(
|
||||
(piece) =>
|
||||
Math.abs(piece.length - 2400) < 0.001 && Math.abs(piece.visibleLength! - 2364) < 0.001,
|
||||
Math.abs(piece.length - 2400) < 0.001 && Math.abs(piece.visibleLength - 2364) < 0.001,
|
||||
),
|
||||
).toBe(true);
|
||||
const receiver = skirt.pieces.find((piece) => piece.wallIndex === 0)!;
|
||||
|
|
@ -580,6 +585,7 @@ test('internal skirting corners retain coping blanks and expose the installed mi
|
|||
expect(piece.leftOverlap).toBeCloseTo(18, 3);
|
||||
await page.keyboard.press('Escape');
|
||||
const floorCount = await floorEntries(page).count();
|
||||
await page.getByText('Advanced skirting settings', { exact: true }).click();
|
||||
await page.getByLabel('Minimum piece', { exact: true }).fill('2365');
|
||||
await expect(skirtEntries(page)).toHaveCount(0);
|
||||
await expect(floorEntries(page)).toHaveCount(floorCount);
|
||||
|
|
@ -666,6 +672,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([
|
||||
'collapsedSections',
|
||||
'material',
|
||||
'roomId',
|
||||
'search',
|
||||
'selectedPiece',
|
||||
|
|
@ -713,6 +720,7 @@ test('localStorage restores all project and view parameters when reopening witho
|
|||
selectedPiece: 'F1',
|
||||
search: 'Study',
|
||||
collapsedSections: ['cuts', 'materials'],
|
||||
material: 'flooring',
|
||||
};
|
||||
const query = encodeState(fixture);
|
||||
await page.goto(`/${query}`);
|
||||
|
|
@ -760,7 +768,7 @@ for (const saved of ['damaged data', '?flooring.length=broken']) {
|
|||
[PROJECT_STORAGE_KEY, saved],
|
||||
);
|
||||
await page.goto('/?planner');
|
||||
await expect(page).toHaveURL(/\?version=1&/);
|
||||
await expect(page).toHaveURL(/\?version=2&/);
|
||||
await expect(page.locator('.url-error')).toHaveCount(0);
|
||||
expect(state(page)).toEqual(defaultState());
|
||||
expect(await savedQuery(page)).toBe(encodeState(defaultState()));
|
||||
|
|
@ -869,6 +877,8 @@ test('shared URLs reopen the selected preview and copying the link preserves the
|
|||
});
|
||||
|
||||
test('saw kerf help works with keyboard and touch or mouse', async ({ page, isMobile }) => {
|
||||
await page.getByText('Advanced flooring settings', { exact: true }).click();
|
||||
await page.getByText('Advanced skirting settings', { exact: true }).click();
|
||||
for (const name of ['What is saw kerf?', 'What is skirting saw kerf?']) {
|
||||
const button = page.getByRole('button', { name, exact: true });
|
||||
await button.focus();
|
||||
|
|
@ -904,6 +914,10 @@ test('invalid URLs recover when measurements are edited', async ({ page }) => {
|
|||
});
|
||||
|
||||
test('clicking the skirting code itself opens its cutting dialog', async ({ page }) => {
|
||||
await page
|
||||
.getByRole('group', { name: 'Plan view' })
|
||||
.getByRole('button', { name: 'Skirting', exact: true })
|
||||
.click();
|
||||
for (const target of ['rect', 'text']) {
|
||||
const code = preview(page).locator('.skirting-code').first();
|
||||
const id = await code.evaluate((element) =>
|
||||
|
|
@ -920,18 +934,14 @@ test('clicking the skirting code itself opens its cutting dialog', async ({ page
|
|||
}
|
||||
});
|
||||
|
||||
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);
|
||||
expect(await savedQuery(page)).toBe(encodeState(legacy));
|
||||
test('readable query fields and browser history preserve the plan', async ({ page }) => {
|
||||
const initial = defaultState();
|
||||
initial.project.rooms[0].name = 'Current lounge';
|
||||
initial.project.flooring.pattern = 'random';
|
||||
await page.goto(`/${encodeState(initial)}`);
|
||||
await expect(page).toHaveURL(/\?version=2&/);
|
||||
expect(state(page)).toEqual(initial);
|
||||
await expect.poll(() => savedQuery(page)).toBe(encodeState(initial));
|
||||
const original = page.url();
|
||||
await page.getByRole('button', { name: 'Add room', exact: true }).click();
|
||||
await page.getByLabel('Room name', { exact: true }).fill('Study');
|
||||
|
|
@ -945,7 +955,7 @@ test('readable query fields, legacy links and browser history preserve the plan'
|
|||
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');
|
||||
await expect(page.getByLabel('Room name', { exact: true })).toHaveValue('Current lounge');
|
||||
expect(state(page).project.rooms).toHaveLength(1);
|
||||
expect(await savedQuery(page)).toBe(new URL(original).search);
|
||||
await page.goForward();
|
||||
|
|
@ -961,35 +971,6 @@ test('readable query fields, legacy links and browser history preserve the plan'
|
|||
expect(state(page).project.rooms).toHaveLength(2);
|
||||
});
|
||||
|
||||
test('legacy skirting sizes migrate to one editable length and reset changed allocations', async ({
|
||||
page,
|
||||
}) => {
|
||||
const initial = defaultState();
|
||||
const legacy = {
|
||||
...initial,
|
||||
project: {
|
||||
...initial.project,
|
||||
completedBoards: ['skirting:S1'],
|
||||
skirting: { ...initial.project.skirting, stockLength: undefined, stockLengths: [3600, 2400] },
|
||||
},
|
||||
};
|
||||
await page.goto(`/#plan=${LZString.compressToEncodedURIComponent(JSON.stringify(legacy))}`);
|
||||
await expect(page.getByLabel('Skirting board length', { exact: true })).toHaveValue('3600');
|
||||
expect(state(page).project.skirting.stockLength).toBe(3600);
|
||||
expect(state(page).project.completedBoards).toEqual([]);
|
||||
const params = new URL(page.url()).searchParams;
|
||||
expect(params.get('skirting.length')).toBe('3600');
|
||||
expect(params.has('skirting.stock')).toBe(false);
|
||||
await page.getByLabel('Skirting board length', { exact: true }).fill('3000');
|
||||
await page.reload();
|
||||
await expect(page.getByLabel('Skirting board length', { exact: true })).toHaveValue('3000');
|
||||
await page.goto('/?skirting.stock=2400&skirting.stock=2400&done=skirting:S1');
|
||||
await expect(page.getByLabel('Skirting board length', { exact: true })).toHaveValue('2400');
|
||||
expect(state(page).project.completedBoards).toEqual([]);
|
||||
expect(new URL(page.url()).searchParams.get('skirting.length')).toBe('2400');
|
||||
expect(new URL(page.url()).searchParams.has('skirting.stock')).toBe(false);
|
||||
});
|
||||
|
||||
test('printing includes every room and every cut once, grouped by its source board', async ({
|
||||
page,
|
||||
}) => {
|
||||
|
|
@ -1173,6 +1154,7 @@ test('inserting and reordering walls keeps stable wall IDs and attached doors',
|
|||
if (!isMobile) {
|
||||
const source = page.locator(`.wall-row[data-wall-id="${doorWall.id}"] .wall-drag-handle`);
|
||||
const target = page.locator(`.wall-row[data-wall-id="${originalIds[0]}"]`);
|
||||
await page.locator('.wall-table').evaluate((table) => table.scrollIntoView({ block: 'start' }));
|
||||
await source.dragTo(target);
|
||||
expect(state(page).project.rooms[0].walls[0]).toEqual(doorWall);
|
||||
}
|
||||
|
|
@ -1201,9 +1183,8 @@ test('the supplied angled room keeps W12 lines and labels inside the drawing', a
|
|||
new URL('../tests/fixtures/angled-room.json', import.meta.url),
|
||||
'utf8',
|
||||
);
|
||||
const fixture = decodeState(`#plan=${LZString.compressToEncodedURIComponent(angledRoom)}`);
|
||||
expect(fixture.error).toBeNull();
|
||||
await page.goto(`/${encodeState(fixture.state)}`);
|
||||
const fixture = JSON.parse(angledRoom) as UrlState;
|
||||
await page.goto(`/${encodeState(fixture)}`);
|
||||
await expect(preview(page).locator('.wall-drawing')).toHaveCount(12);
|
||||
for (const width of isMobile ? [320, 390] : [1280]) {
|
||||
await page.setViewportSize({ width, height: 844 });
|
||||
|
|
@ -1317,11 +1298,10 @@ test('the supplied F8 room aligns a row seam with its fireplace corner and keeps
|
|||
new URL('../tests/fixtures/angled-room-f8.json', import.meta.url),
|
||||
'utf8',
|
||||
);
|
||||
const fixture = decodeState(`#plan=${LZString.compressToEncodedURIComponent(json)}`);
|
||||
expect(fixture.error).toBeNull();
|
||||
fixture.state.view.selectedPiece = null;
|
||||
fixture.state.view.search = '';
|
||||
await page.goto(`/${encodeState(fixture.state)}`);
|
||||
const fixture = JSON.parse(json) as UrlState;
|
||||
fixture.view.selectedPiece = null;
|
||||
fixture.view.search = '';
|
||||
await page.goto(`/${encodeState(fixture)}`);
|
||||
const { floor, skirt } = plans(page);
|
||||
const corner = { x: 7607.513, y: 2160.34 };
|
||||
const atCorner = floor.pieces.filter((piece) =>
|
||||
|
|
@ -1355,7 +1335,7 @@ test('the supplied F8 room aligns a row seam with its fireplace corner and keeps
|
|||
dimensions.some(({ actual }) => Math.abs(actual * 2 - Math.round(actual * 2)) > 0.001),
|
||||
).toBe(true);
|
||||
expectHalfMillimetreCuts(dimensions);
|
||||
expect(state(page).project.rooms[0].walls).toEqual(fixture.state.project.rooms[0].walls);
|
||||
expect(state(page).project.rooms[0].walls).toEqual(fixture.project.rooms[0].walls);
|
||||
await page.keyboard.press('Escape');
|
||||
const skirtCut = skirtingCuts(skirt).find(
|
||||
(piece) =>
|
||||
|
|
@ -1399,6 +1379,7 @@ test('completed source boards and locate actions follow shared offcuts across ro
|
|||
const sourcePieces = floor.pieces.filter((piece) => piece.stockId === shared.id);
|
||||
const key = `floor:${shared.id}`;
|
||||
const group = page.locator(`.stock-cut-group[data-board-key="${key}"]`);
|
||||
await expect(group).toBeVisible();
|
||||
const groupCodes = await group
|
||||
.locator('.board-cut')
|
||||
.evaluateAll((cuts) => cuts.map((cut) => cut.getAttribute('data-cut-id')).join(' + '));
|
||||
|
|
@ -1464,6 +1445,10 @@ test('completed source boards and locate actions follow shared offcuts across ro
|
|||
.getByRole('checkbox')
|
||||
.check();
|
||||
expect(state(page).project.completedBoards).toEqual([key, skirtKey]);
|
||||
await page
|
||||
.getByRole('group', { name: 'Plan view' })
|
||||
.getByRole('button', { name: 'Skirting', exact: true })
|
||||
.click();
|
||||
expect(await preview(page).locator('.skirt-piece[data-done="true"]').count()).toBeGreaterThan(0);
|
||||
await page.getByLabel('Board width', { exact: true }).fill('201');
|
||||
expect(state(page).project.completedBoards).toEqual([]);
|
||||
|
|
@ -1508,6 +1493,7 @@ test('cuts from one board share the available width and wrap on a narrow phone',
|
|||
.filter({ has: page.locator('.board-cut:nth-child(2)') })
|
||||
.first();
|
||||
const pieces = group.locator('.board-cut');
|
||||
await expect(group).toBeVisible();
|
||||
expect(await pieces.count()).toBeGreaterThan(1);
|
||||
const first = (await pieces.nth(0).boundingBox())!;
|
||||
const second = (await pieces.nth(1).boundingBox())!;
|
||||
|
|
@ -1558,6 +1544,7 @@ for (const width of [320, 390]) {
|
|||
}) => {
|
||||
await page.setViewportSize({ width, height: 844 });
|
||||
await openWalls(page);
|
||||
await page.getByText('Advanced flooring settings', { exact: true }).click();
|
||||
await page.getByRole('button', { name: 'What is saw kerf?', exact: true }).click();
|
||||
await noHorizontalOverflow(page);
|
||||
await expect(page.getByRole('tooltip')).toBeVisible();
|
||||
|
|
|
|||
Loading…
Reference in a new issue