feat(editor): F4.5d the firewall in the device sheet

Measured first: SheetRun drives FILIUS' firewall dialog down to its table
editor (Swing's own end of an edit). Java's table writes a cell into the
rule before it checks it, so that the firewall holds "abc", a mask of
255.0.255.0 or port 0 while the table shows the old value, and reads a
port that is no number as all ports. Here nothing the check refuses
reaches the rule.

- doc: the three switches, the default action, rules (new as Java's,
  cells by Java's checks: masks of ones then zeros, the directly connected
  marker as a source only, an empty port for all ports; protocol, action,
  moved up or down, removed)
- editor: the firewall as a page of the router's and home router's sheet,
  with Java's explanations; the general tab named so (Java: "network
  interfaces")

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-03 12:27:10 +00:00
co-authored by Claude Opus 5.5
parent c8edb87210
commit 4e062bb347
15 changed files with 1399 additions and 12 deletions
+60
View File
@@ -348,6 +348,66 @@ test("keeps a home router's port forwarding on its own page, and shows its DHCP
await expect(sheet(page).getByRole('tab', { name: 'LAN: 192.168.0.10' })).toHaveAttribute('aria-selected', 'true');
});
test("sets up a router's firewall on its own page: the switches, the default, rules edited, moved and removed", async ({ page }) => {
await openEditor(page, server.base, 'new');
await page.locator('[data-palette="router"]').click();
await page.getByRole('dialog').getByRole('button', { name: '2', exact: true }).click();
const id = (await drawing(page)).nodes[0]!.id;
const firewall = async () => ((await sheetOf(page, id)) as RouterSheet).firewall;
await sheet(page).getByRole('button', { name: 'Firewall einrichten' }).click();
await expect(sheet(page).getByRole('heading')).toHaveText('Firewall einrichten');
await expect(sheet(page).getByRole('tab')).toHaveText(['Allgemein', 'Firewall-Regeln']);
await sheet(page).getByLabel('Firewall aktivieren').check();
await sheet(page).getByLabel('nur SYN-Pakete verwerfen').uncheck();
await expect.poll(firewall).toMatchObject({ isActive: true, filterSynSegmentsOnly: false });
await sheet(page).getByRole('tab', { name: 'Firewall-Regeln' }).click();
await sheet(page).locator('select[name="defaultPolicy"]').selectOption('accept');
await sheet(page).getByRole('button', { name: 'Neue Regel' }).click();
const rows = sheet(page).locator('table[data-rows="firewall"] tbody tr');
await expect(rows.first().locator('td')).toHaveText(['1', '', '', '', '', 'TCP', '', 'verwerfen']);
await expect(sheet(page).getByRole('button', { name: 'rauf' })).toBeDisabled();
await field(page, 'firewall-sourceIPAddress').fill('abc');
await field(page, 'firewall-sourceIPAddress').press('Enter');
await expect(sheet(page).getByRole('status')).toHaveText('Nicht übernommen – es gilt weiterhin (leer).');
await field(page, 'firewall-sourceIPAddress').fill('10.0.0.0');
await field(page, 'firewall-sourceIPAddress').press('Enter');
await field(page, 'firewall-sourceSubnetMask').fill('255.0.255.0');
await field(page, 'firewall-sourceSubnetMask').press('Enter');
await expect(field(page, 'firewall-sourceSubnetMask')).toHaveAttribute('aria-invalid', 'true');
await field(page, 'firewall-sourceSubnetMask').fill('255.0.0.0');
await field(page, 'firewall-sourceSubnetMask').press('Enter');
await field(page, 'firewall-port').fill('80');
await field(page, 'firewall-port').press('Enter');
await sheet(page).locator('select[name="firewall-protocol"]').selectOption('udp');
await sheet(page).locator('select[name="firewall-action"]').selectOption('accept');
await expect(rows.first().locator('td')).toHaveText(['1', '10.0.0.0', '255.0.0.0', '', '', 'UDP', '80', 'akzeptieren']);
await sheet(page).getByRole('button', { name: 'Neue Regel' }).click();
await field(page, 'firewall-destinationIPAddress').fill('192.168.0.1');
await field(page, 'firewall-destinationIPAddress').press('Enter');
await expect(sheet(page).getByRole('button', { name: 'runter' })).toBeDisabled();
await sheet(page).getByRole('button', { name: 'rauf' }).click();
await expect.poll(async () => (await firewall()).rules.map((rule) => rule.destinationIPAddress)).toEqual(['192.168.0.1', '']);
await expect(rows.first()).toHaveAttribute('aria-selected', 'true');
await sheet(page).getByRole('button', { name: 'Regel löschen' }).click();
await expect.poll(async () => (await firewall()).rules.map((rule) => rule.sourceIPAddress)).toEqual(['10.0.0.0']);
// Java keeps a row chosen: the one that took the removed one's place.
await expect(rows.first()).toHaveAttribute('aria-selected', 'true');
await expect(field(page, 'firewall-sourceIPAddress')).toHaveValue('10.0.0.0');
await expect(await firewall()).toMatchObject({ defaultPolicy: 'accept' });
await sheet(page).getByRole('button', { name: 'Zurück' }).click();
await expect(sheet(page).getByRole('heading')).toHaveText('Vermittlungsrechner');
});
test("opens a home router's firewall from its general tab", async ({ page }) => {
await openEditor(page, server.base, 'new');
await page.locator('[data-palette="gateway"]').click();
await sheet(page).getByRole('button', { name: 'Firewall einrichten' }).click();
await expect(sheet(page).getByRole('heading')).toHaveText('Firewall einrichten');
await expect(sheet(page).getByLabel('Firewall aktivieren')).not.toBeChecked();
});
test.describe('on a phone', () => {
test.use({ viewport: { width: 390, height: 844 }, hasTouch: true });