landing
All checks were successful
Deploy to Pages / build (push) Successful in 26s

This commit is contained in:
Andras Schmelczer 2026-09-11 20:34:36 +01:00
commit b9485e4097
32 changed files with 4692 additions and 65 deletions

View file

@ -3,6 +3,7 @@ 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';
import { defaultState, fireplace, rectangle } from '../src/lib/defaults';
import { planFlooring } from '../src/lib/flooring';
import { planSkirting } from '../src/lib/skirting';
@ -23,6 +24,8 @@ const state = (page: Page) => {
const url = new URL(page.url());
return decodeState(url.search || url.hash).state;
};
const savedQuery = (page: Page) =>
page.evaluate((key) => localStorage.getItem(key), PROJECT_STORAGE_KEY);
const plans = (page: Page) => {
const { project } = state(page);
return {
@ -37,7 +40,8 @@ function sharedRoomFixture() {
fixture.project.flooring.width = 200;
fixture.project.rooms = [
rectangle('room-1', 'Living room', 1820, 1220),
rectangle('room-2', 'Study', 1220, 1820),
// Its end pieces complement the first room's offcuts after saw kerf.
rectangle('room-2', 'Study', 1220, 1814),
];
fixture.project.rooms.forEach((room) =>
room.walls.forEach((wall) => {
@ -259,7 +263,7 @@ async function noWallLabelOverlap(page: Page) {
test.beforeEach(async ({ page }) => {
errors.set(page, []);
page.on('pageerror', (error) => errors.get(page)!.push(error.message));
await page.goto('/');
await page.goto('/?planner');
await expect(page.getByRole('heading', { name: 'Room measurements' })).toBeVisible();
await expect(page).toHaveURL(/\?version=1&/);
});
@ -679,9 +683,115 @@ test('pattern, direction and spacers persist without steps, layers or stored res
).toHaveAttribute('aria-pressed', 'true');
await expect(page.locator('.flooring-cuts')).toHaveText(table, { useInnerText: true });
await noHorizontalOverflow(page);
expect(await page.evaluate(() => Object.keys(localStorage))).toEqual([]);
expect(await savedQuery(page)).toBe(new URL(page.url()).search);
});
test('localStorage restores all project and view parameters when reopening without a link', async ({
page,
context,
}) => {
const fixture = sharedRoomFixture();
fixture.project.name = 'Café 北 flooring';
fixture.project.tolerance = 7;
fixture.project.flooring = {
...fixture.project.flooring,
length: 1300,
width: 180,
gap: 12,
pattern: 'random',
minOffset: 250,
maxOffset: 600,
minLength: 180,
minWidth: 50,
kerf: 2.5,
};
fixture.project.skirting = { stockLength: 3600, thickness: 20, minLength: 250, kerf: 2 };
fixture.project.rooms[1].walls[0].doors = [{ id: 'study-door', offset: 300, width: 850 }];
fixture.project.completedBoards = ['floor:B1', 'skirting:S1'];
fixture.view = {
roomId: 'room-2',
selectedPiece: 'F1',
search: 'Study',
collapsedSections: ['cuts', 'materials'],
};
const query = encodeState(fixture);
await page.goto(`/${query}`);
await expect(page.getByRole('dialog', { name: 'Flooring cut F1', exact: true })).toBeVisible();
expect(await savedQuery(page)).toBe(query);
const reopened = await context.newPage();
await reopened.goto('/?planner');
await expect(reopened).toHaveURL(page.url());
expect(state(reopened)).toEqual(fixture);
await expect(
reopened.getByRole('dialog', { name: 'Flooring cut F1', exact: true }),
).toBeVisible();
await reopened.keyboard.press('Escape');
await expect(reopened.getByLabel('Room name', { exact: true })).toHaveValue('Study');
await expect(reopened.locator('#materials-content')).toBeHidden();
await expect(reopened.locator('#cuts-content')).toBeHidden();
await reopened.getByLabel('Room name', { exact: true }).fill('Saved study');
fixture.project.rooms[1].name = 'Saved study';
fixture.view.selectedPiece = null;
expect(await savedQuery(reopened)).toBe(encodeState(fixture));
await reopened.goto('/?planner');
await expect(reopened.getByLabel('Room name', { exact: true })).toHaveValue('Saved study');
expect(state(reopened)).toEqual(fixture);
await reopened.close();
});
test('explicit query parameters override the saved plan and default omitted fields', async ({
page,
}) => {
await page.getByLabel('Room name', { exact: true }).fill('Saved room');
await page.getByLabel('Wall spacer', { exact: true }).fill('12');
await page.goto('/?flooring.length=1400');
await expect(page.getByLabel('Board length', { exact: true })).toHaveValue('1400');
const expected = defaultState();
expected.project.flooring.length = 1400;
expect(state(page)).toEqual(expected);
expect(await savedQuery(page)).toBe(encodeState(expected));
});
for (const saved of ['damaged data', '?flooring.length=broken']) {
test(`invalid localStorage falls back to defaults: ${saved}`, async ({ page }) => {
await page.evaluate(
([key, value]) => localStorage.setItem(key, value),
[PROJECT_STORAGE_KEY, saved],
);
await page.goto('/?planner');
await expect(page).toHaveURL(/\?version=1&/);
await expect(page.locator('.url-error')).toHaveCount(0);
expect(state(page)).toEqual(defaultState());
expect(await savedQuery(page)).toBe(encodeState(defaultState()));
});
}
for (const failure of ['blocked', 'full']) {
test(`URL persistence still works when localStorage is ${failure}`, async ({ page }) => {
await page.addInitScript((failure) => {
if (failure === 'blocked') {
Object.defineProperty(window, 'localStorage', {
get() {
throw new DOMException('Storage blocked', 'SecurityError');
},
});
} else {
Storage.prototype.setItem = () => {
throw new DOMException('Storage full', 'QuotaExceededError');
};
}
}, failure);
await page.goto('/?planner');
await expect(page.getByLabel('Room name', { exact: true })).toHaveValue('Living room');
await page.getByLabel('Room name', { exact: true }).fill('URL still works');
expect(state(page).project.rooms[0].name).toBe('URL still works');
await page.reload();
await expect(page.getByLabel('Room name', { exact: true })).toHaveValue('URL still works');
await expect(page.locator('.url-error')).toHaveCount(0);
});
}
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');
@ -780,12 +890,15 @@ test('saw kerf help works with keyboard and touch or mouse', async ({ page, isMo
});
test('invalid URLs recover when measurements are edited', async ({ page }) => {
const saved = await savedQuery(page);
await page.goto('/?flooring.length=broken');
await expect(page.getByText(/Could not load this plan/)).toBeVisible();
expect(await savedQuery(page)).toBe(saved);
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()).search).error).toBeNull();
expect(new URL(page.url()).hash).toBe('');
expect(await savedQuery(page)).toBe(new URL(page.url()).search);
await page.reload();
await expect(page.getByLabel('Room name', { exact: true })).toHaveValue('Recovered plan');
});
@ -818,6 +931,7 @@ test('readable query fields, legacy links and browser history preserve the plan'
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));
const original = page.url();
await page.getByRole('button', { name: 'Add room', exact: true }).click();
await page.getByLabel('Room name', { exact: true }).fill('Study');
@ -833,10 +947,12 @@ test('readable query fields, legacy links and browser history preserve the plan'
await expect(page).toHaveURL(original);
await expect(page.getByLabel('Room name', { exact: true })).toHaveValue('Legacy lounge');
expect(state(page).project.rooms).toHaveLength(1);
expect(await savedQuery(page)).toBe(new URL(original).search);
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');
expect(await savedQuery(page)).toBe(new URL(twoRooms).search);
params.set('flooring.width', '205');
await page.goto(`/?${params}`);
await expect(page.getByLabel('Board width', { exact: true })).toHaveValue('205');