From cab06a6adcc2bbc72c537f5526775aebfe89f6df Mon Sep 17 00:00:00 2001 From: Harted Date: Sun, 30 Aug 2026 10:30:15 +0200 Subject: [PATCH 1/2] fix: point the feature tour at the server it builds The tour has been failing at scene 19 and taking the seventeen scenes after it with it, so half the manual's screenshots could not be refreshed. Act II builds registers through the add dialog to show the dialog, and left the simulator holding them, so Act III read a server that no longer matched the inverter the tour is named after. It reloads the config before the client connects now. Five scenes join it, all of them features with no picture: the strip over the toolbar with the grid filling underneath, which is what a scan looks like since the grid stopped being unmounted; a unit ID scan against a server holding two of the five IDs, so the results carry both answers; the poll rate and timeout; the word order table the BE/LE tooltip holds; and a column filter with the button that clears it. --- .../03-presentation/01-feature-tour.spec.ts | 106 ++++++++++++++++-- 1 file changed, 96 insertions(+), 10 deletions(-) diff --git a/e2e/specs/03-presentation/01-feature-tour.spec.ts b/e2e/specs/03-presentation/01-feature-tour.spec.ts index 55c2af5..216836d 100644 --- a/e2e/specs/03-presentation/01-feature-tour.spec.ts +++ b/e2e/specs/03-presentation/01-feature-tour.spec.ts @@ -24,6 +24,7 @@ import { disconnectClient, readRegisters, clearData, + openColumnMenu, selectRegisterType, selectUnitId, selectDataType, @@ -390,6 +391,14 @@ test.describe.serial('Act II — Building the Simulator', () => { test.describe.serial('Act III — Going Live', () => { test('scene 15 — client connects & reads raw data', async ({ mainPage }) => { + // Act II built registers through the add dialog to show the dialog, and + // left the simulator holding them. The client scenes are about reading the + // inverter this tour is named after, so put that back first: every value + // from here on is the one the documented config describes. + await navigateToServer(mainPage) + await loadServerConfig(mainPage, SERVER_CONFIG) + await beat(mainPage, 1000) + await navigateToClient(mainPage) await connectClient(mainPage, '127.0.0.1', '502', '0') await selectRegisterType(mainPage, 'Holding Registers') @@ -421,6 +430,31 @@ test.describe.serial('Act III — Going Live', () => { await beat(mainPage, 300) }) + test('scene 17b — poll rate and timeout', async ({ mainPage }) => { + await mainPage.getByTestId('time-settings-btn').click() + await beat(mainPage, 400) + + const popover = mainPage.getByTestId('time-settings-popover').locator('.MuiPaper-root').first() + await popover.screenshot({ path: resolve(SHOTS, 'client-time-settings.png') }) + + await mainPage.keyboard.press('Escape') + await beat(mainPage, 300) + }) + + test('scene 17c — the word order table', async ({ mainPage }) => { + // The BE/LE tooltip is the app explaining a Modbus problem to you: which + // half of a 32-bit value lands in the first register. It belongs in the + // manual as it appears on screen. + await mainPage.getByTestId('endian-be-btn').hover() + await beat(mainPage, 1400) + + const tooltip = mainPage.locator('.MuiTooltip-tooltip .MuiPaper-root').first() + await tooltip.screenshot({ path: resolve(SHOTS, 'client-endian-table.png') }) + + await mainPage.mouse.move(0, 0) + await beat(mainPage, 400) + }) + test('scene 18 — advanced mode', async ({ mainPage }) => { await mainPage.getByTestId('menu-btn').click() const advCheckbox = mainPage.getByTestId('advanced-mode-checkbox') @@ -471,6 +505,32 @@ test.describe.serial('Act III — Going Live', () => { await snap(mainPage, 'client-decoded-values') }) + test('scene 19b — filtering a column, and clearing it', async ({ mainPage }) => { + // The filter form is part of the grid, but the way back out of it is not: + // a filter left behind reads as missing data, so the toolbar grows a + // button while one is on. + await openColumnMenu(mainPage, 'hex') + await mainPage + .locator('.MuiDataGrid-menuList') + .getByRole('menuitem', { name: 'Filter' }) + .click() + await beat(mainPage, 400) + + const valueInput = mainPage.locator('.MuiDataGrid-filterFormValueInput input') + await valueInput.fill('00') + await beat(mainPage, 800) + + await snap(mainPage, 'client-column-filter') + + await mainPage.keyboard.press('Escape') + await beat(mainPage, 400) + await snap(mainPage, 'client-clear-filters') + + await mainPage.getByTestId('clear-filters-btn').click() + await expect(mainPage.getByTestId('clear-filters-btn')).not.toBeVisible() + await beat(mainPage, 300) + }) + test('scene 20 — read configuration mode', async ({ mainPage }) => { await enableReadConfiguration(mainPage) await mainPage.getByTestId('read-btn').click() @@ -782,18 +842,44 @@ test.describe.serial('Act IV — Interaction', () => { // Start scan await mainPage.getByTestId('scan-start-stop-btn').click() - await beat(mainPage, 1500) + await beat(mainPage, 2500) - // Screenshot dialog with progress bar - const scanDialog = paperOf(mainPage, 'scan-start-stop-btn') - await scanDialog.screenshot({ path: resolve(SHOTS, 'client-scanning.png') }) + // The whole window, not the dialog on its own: what a scan looks like is + // the strip over the toolbar, the count beside it, and the grid filling + // underneath while it walks the range. + await snap(mainPage, 'client-scanning') // Stop scan await mainPage.getByTestId('scan-start-stop-btn').click() await beat(mainPage, 500) - // Close scan dialog - await mainPage.keyboard.press('Escape') + await mainPage.getByTestId('scan-registers-close-btn').click() + await beat(mainPage, 300) + }) + + test('scene 29b — scanning for unit IDs', async ({ mainPage }) => { + await mainPage.getByTestId('menu-btn').click() + await beat(mainPage, 300) + await mainPage.getByTestId('scan-unitids-btn').click() + await beat(mainPage, 500) + + // Five IDs against a server that holds two of them, so the results carry + // both answers: the ones that replied with data, and the ones that + // answered by refusing. + await mainPage.getByTestId('scan-unitid-count-input').locator('input').fill('5') + await mainPage.getByTestId('scan-unitid-type-coils').click() + await beat(mainPage, 200) + + await mainPage.getByTestId('scan-unitid-start-stop-btn').click() + await expect(mainPage.getByTestId('scan-unitid-start-stop-btn')).toContainText( + 'Start Scanning', + { timeout: 60000 } + ) + await beat(mainPage, 600) + + await snap(mainPage, 'client-scan-unit-ids') + + await mainPage.getByTestId('scan-unitid-close-btn').click() await beat(mainPage, 300) }) }) @@ -805,7 +891,7 @@ test.describe.serial('Act IV — Interaction', () => { let serverPage: Page test.describe.serial('Act V — Side by Side', () => { - test('scene 29b — RTU over TCP in the cog menu', async ({ mainPage }) => { + test('scene 30 — RTU over TCP in the cog menu', async ({ mainPage }) => { // Only reachable while disconnected: the transport cannot change mid-session. // The warning colour is the whole point -- it is what turns the TCP button // warning too, and that is the only thing telling the two TCP transports @@ -834,7 +920,7 @@ test.describe.serial('Act V — Side by Side', () => { await snap(mainPage, 'client-rtu-over-tcp-off') }) - test('scene 30 — split view', async ({ mainPage, electronApp }) => { + test('scene 31 — split view', async ({ mainPage, electronApp }) => { await navigateToHome(mainPage) await beat(mainPage, 3500) @@ -848,7 +934,7 @@ test.describe.serial('Act V — Side by Side', () => { await snap(serverPage, 'split-view-server') }) - test('scene 31 — split view connected', async ({ mainPage }) => { + test('scene 32 — split view connected', async ({ mainPage }) => { await connectClient(mainPage, '127.0.0.1', '502', '0') await selectRegisterType(mainPage, 'Holding Registers') await loadClientConfig(mainPage, CLIENT_CONFIG) @@ -863,7 +949,7 @@ test.describe.serial('Act V — Side by Side', () => { await snap(mainPage, 'split-view-connected') }) - test('scene 32 — cleanup', async ({ electronApp, mainPage }) => { + test('scene 33 — cleanup', async ({ electronApp, mainPage }) => { await disconnectClient(mainPage) await electronApp.evaluate(({ BrowserWindow }) => { From 9aefcab7060d1ca51c1dcc90eacd13592bbf5b78 Mon Sep 17 00:00:00 2001 From: Harted Date: Sun, 30 Aug 2026 12:04:52 +0200 Subject: [PATCH 2/2] test: shoot the unit ID dialog, not the window around it The dialog fills the view anyway, and the strip of client behind it says nothing the manual needs. --- e2e/specs/03-presentation/01-feature-tour.spec.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/e2e/specs/03-presentation/01-feature-tour.spec.ts b/e2e/specs/03-presentation/01-feature-tour.spec.ts index 216836d..28b1c7b 100644 --- a/e2e/specs/03-presentation/01-feature-tour.spec.ts +++ b/e2e/specs/03-presentation/01-feature-tour.spec.ts @@ -877,7 +877,8 @@ test.describe.serial('Act IV — Interaction', () => { ) await beat(mainPage, 600) - await snap(mainPage, 'client-scan-unit-ids') + const dialog = paperOf(mainPage, 'scan-unitid-start-stop-btn') + await dialog.screenshot({ path: resolve(SHOTS, 'client-scan-unit-ids.png') }) await mainPage.getByTestId('scan-unitid-close-btn').click() await beat(mainPage, 300)