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:
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user