feat(editor): a DHCP server's two pages as a segmented control

"Grundeinstellungen" and "Statische Adresszuweisung" (Java's words)
become the dashboard's segmented control: one frame, equal shares, the
chosen one in the accent. Measured: "Grundeinstellungen" is 138 px at
14 px and a half of the 340 px sheet holds 139 (157 bold). So the words
wrap at their spaces inside a segment, the chosen one is not bold (its
width would jump), and where a segment's longest word with its padding no
longer fits its share — a phone, another system font — the segments stack,
still one control. Decided by measuring the words in the segments' font,
not by a breakpoint that would hold for one font only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-10 18:52:27 +00:00
co-authored by Claude Opus 5.5
parent 16d9cc2095
commit 9fae9e59b2
5 changed files with 210 additions and 3 deletions
+67
View File
@@ -504,9 +504,76 @@ test("opens a home router's firewall from its general tab", async ({ page }) =>
await expect(sheet(page).getByLabel('Firewall aktivieren')).not.toBeChecked();
});
/** The DHCP page's segmented control: its frame, its two segments' boxes, and how they stand. */
async function dhcpSegments(page: Page) {
await page.locator('[data-palette="pc"]').click();
await sheet(page).getByRole('button', { name: 'DHCP-Server einrichten' }).click();
const control = sheet(page).getByRole('tablist');
await expect(control).toHaveClass('sheet-segments');
const boxes = await Promise.all([0, 1].map(async (index) => (await control.getByRole('tab').nth(index).boundingBox())!));
return { control, boxes };
}
test("shows a DHCP server's two pages as a segmented control: side by side in the sheet, the long word wrapped, nothing cut off", async ({ page }) => {
await openEditor(page, server.base, 'new');
const { control, boxes } = await dhcpSegments(page);
await expect(control).not.toHaveAttribute('data-stacked', 'true');
const [basic, assignments] = boxes as [{ x: number; y: number; width: number; height: number }, { x: number; y: number; width: number; height: number }];
// One row, equal shares, equal heights — „Statische Adresszuweisung" on two lines inside its 44 px.
expect(Math.round(basic.y)).toBe(Math.round(assignments.y));
expect(Math.abs(basic.width - assignments.width)).toBeLessThanOrEqual(1);
expect(basic.height).toBe(assignments.height);
expect(basic.height).toBeGreaterThanOrEqual(44);
// No word runs over its segment's edge.
for (const tab of await control.getByRole('tab').all()) {
expect(await tab.evaluate((element) => element.scrollWidth <= element.clientWidth)).toBe(true);
}
// The chosen one in the accent, the other quiet; a choice moves it.
const tabs = control.getByRole('tab');
await expect(tabs.nth(0)).toHaveAttribute('aria-selected', 'true');
await tabs.nth(1).click();
await expect(tabs.nth(1)).toHaveAttribute('aria-selected', 'true');
await expect(tabs.nth(0)).toHaveAttribute('aria-selected', 'false');
// Not bold when chosen: the width does not jump.
expect(Math.round((await tabs.nth(1).boundingBox())!.width)).toBe(Math.round(assignments.width));
});
test("stacks the DHCP server's segments exactly where a segment's longest word, with its padding, no longer fits its share of the row", async ({ page }) => {
await openEditor(page, server.base, 'new');
const { control } = await dhcpSegments(page);
// What one segment needs: its longest word in the segments' font, and its padding.
const need = await control.evaluate((list) => {
const segments = [...list.querySelectorAll<HTMLElement>('[role="tab"]')];
const style = getComputedStyle(segments[0]!);
const measure = document.createElement('canvas').getContext('2d')!;
measure.font = style.font;
const words = segments.flatMap((segment) => (segment.textContent ?? '').split(/\s+/));
return Math.max(...words.map((word) => measure.measureText(word).width)) + parseFloat(style.paddingLeft) + parseFloat(style.paddingRight);
});
// Two shares and the line between them, inside the frame; a little under and a little over.
const inside = async (width: number) => control.evaluate((list, value) => { list.style.boxSizing = 'content-box'; list.style.width = `${value}px`; }, width);
await inside(2 * need + 1 - 0.6);
await expect(control).toHaveAttribute('data-stacked', 'true');
await inside(2 * need + 1 + 0.6);
await expect(control).not.toHaveAttribute('data-stacked', 'true');
});
test.describe('on a phone', () => {
test.use({ viewport: { width: 390, height: 844 }, hasTouch: true });
test("stacks the DHCP server's segments where a word no longer fits its share — still one control", async ({ page }) => {
await openEditor(page, server.base, 'new');
const { control, boxes } = await dhcpSegments(page);
await expect(control).toHaveAttribute('data-stacked', 'true');
await expect(control).toHaveAttribute('aria-orientation', 'vertical');
const [basic, assignments] = boxes as [{ x: number; y: number; width: number; height: number }, { x: number; y: number; width: number; height: number }];
expect(Math.round(basic.x)).toBe(Math.round(assignments.x));
expect(assignments.y).toBeGreaterThanOrEqual(basic.y + basic.height - 1);
for (const tab of await control.getByRole('tab').all()) {
expect(await tab.evaluate((element) => element.scrollWidth <= element.clientWidth)).toBe(true);
}
});
test('opens below the surface with a tap, the device still in sight', async ({ page }) => {
await openEditor(page, server.base, 'zwei_rechnernetze_komplett.fls');
const id = await deviceNamed(page, 'Notebook 1.10');