From 88f52387af1fe6ebd6554acd49320a2e5927c0e8 Mon Sep 17 00:00:00 2001 From: "pulse-triage[bot]" <249995291+pulse-triage[bot]@users.noreply.github.com> Date: Thu, 1 Oct 2026 04:24:25 +0100 Subject: [PATCH] Restore touch activation on shared table rows Mark clickable table rows as native WebKit touch targets without moving their actions out of Solid's delegated event ordering. Preserve keyboard disclosure, embedded controls, dynamic action removal and explicit native handlers; cover the shared table and data grid, and record production PBS drawer browser proof. Change-source: pulse-maintainer --- .../subsystems/frontend-primitives.md | 17 +- .../browser-tests/table-row-touch.cjs | 245 ++++++++++++++++ .../browser-tests/table-row-touch.html | 12 + .../browser-tests/table-row-touch.tsx | 91 ++++++ frontend-modern/browser-verification.json | 267 +++++++++++------- .../src/components/shared/Table.tsx | 6 + .../shared/__tests__/PulseDataGrid.test.tsx | 17 ++ .../shared/__tests__/Table.test.tsx | 127 ++++++++- 8 files changed, 682 insertions(+), 100 deletions(-) create mode 100644 frontend-modern/browser-tests/table-row-touch.cjs create mode 100644 frontend-modern/browser-tests/table-row-touch.html create mode 100644 frontend-modern/browser-tests/table-row-touch.tsx diff --git a/docs/release-control/v6/internal/subsystems/frontend-primitives.md b/docs/release-control/v6/internal/subsystems/frontend-primitives.md index c3bac1b5c9..560a03e88d 100644 --- a/docs/release-control/v6/internal/subsystems/frontend-primitives.md +++ b/docs/release-control/v6/internal/subsystems/frontend-primitives.md @@ -401,9 +401,20 @@ priority. Expandable platform summary rows use `getPlatformResourceDetailRowInteractionProps` (or -`createPlatformResourceDetailState`, which owns the same state contract) for -whole-row pointer activation, Enter/Space keyboard activation, focus treatment, -`aria-expanded` / `aria-controls`, and exclusion of embedded links and controls. +`createPlatformResourceDetailState` for their expansion state) for whole-row +pointer activation and exclusion of embedded links and controls. The nested +native disclosure button owns keyboard activation, focus and +`aria-expanded` / `aria-controls`; static table rows are not duplicate controls. +The shared `TableRow` installs a non-activating native click target only while +an `onClick` action exists. WebKit touch must be able to synthesize that row's +click, but actions remain document-delegated so embedded controls can stop +propagation before the row action runs. Do not move the action into the native +compatibility listener or add touch-end activation that opens rows during +scrolling. Explicit caller-owned native listeners remain caller-owned. +`Table.test.tsx` covers native binding, removal, single activation, child +isolation and bound handlers; `PulseDataGrid.test.tsx` checks inheritance. +The production PBS table/drawer browser fixture exercises first-tap disclosure +and History in WebKit phone emulation, not installed collection or devices. `PlatformResourceDetailToggleButton` is the desktop disclosure affordance and is visually removed on phone layouts where the complete row is the touch target; provider tables must not add a second mobile chevron. When row activation diff --git a/frontend-modern/browser-tests/table-row-touch.cjs b/frontend-modern/browser-tests/table-row-touch.cjs new file mode 100644 index 0000000000..025a5cd043 --- /dev/null +++ b/frontend-modern/browser-tests/table-row-touch.cjs @@ -0,0 +1,245 @@ +// Real production table rows and PBS drawers; synthetic bounded HTTP data. +// Do not instrument rows with native listeners: doing so masks the WebKit bug. +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const path = require('node:path'); +const { createHash } = require('node:crypto'); +const { chromium, firefox, webkit } = require('playwright'); + +(async () => { + const root = '/workspace/frontend-modern'; + process.chdir(root); + const engine = process.argv.find((arg) => arg.startsWith('--engine='))?.split('=')[1] || 'webkit'; + const theme = process.argv.includes('--dark') ? 'dark' : 'light'; + const width = process.argv.includes('--desktop') ? 1365 : 390; + assert.ok(['chromium', 'firefox', 'webkit'].includes(engine)); + const out = path.join(root, 'node_modules', `table-row-touch-${engine}-${width}-${theme}`); + fs.mkdirSync(out, { recursive: true }); + fs.chmodSync(out, 0o755); + const { createServer } = await import(path.join(root, 'node_modules/vite/dist/node/index.js')); + const server = await createServer({ + root, + configFile: path.join(root, 'vite.config.ts'), + cacheDir: fs.mkdtempSync(path.join(out, 'vite-cache-')), + server: { host: '127.0.0.1', port: 5227, strictPort: true }, + }); + const sourceHashes = Object.fromEntries( + ['src/components/shared/Table.tsx', 'package-lock.json'].map((file) => [ + file, + createHash('sha256') + .update(fs.readFileSync(path.join(root, file))) + .digest('hex'), + ]), + ); + const playwrightVersion = require('playwright/package.json').version; + assert.equal( + playwrightVersion, + JSON.parse(fs.readFileSync('/workspace/tests/integration/package-lock.json')).packages[ + 'node_modules/@playwright/test' + ].version, + ); + let browser, page; + const errors = [], + requests = [], + observations = []; + const snapshot = async (name) => { + const file = path.join(out, name + '.png'); + await page.screenshot({ path: file }); + fs.chmodSync(file, 0o644); + }; + const activate = (locator) => (width === 390 ? locator.tap() : locator.click()); + const noOverflow = async () => + assert.ok( + await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth), + 'document overflow', + ); + try { + await server.listen(); + browser = await { chromium, firefox, webkit }[engine].launch( + engine === 'chromium' + ? { headless: true, channel: 'chromium', args: ['--no-sandbox'] } + : { headless: true }, + ); + page = await browser.newPage({ + viewport: { width, height: width === 390 ? 844 : 900 }, + locale: 'en-GB', + timezoneId: 'UTC', + ...(width === 390 ? { isMobile: true, hasTouch: true } : {}), + }); + page.setDefaultTimeout(20_000); + page.on('pageerror', (error) => errors.push(error.message)); + await page.route('**/*', async (route) => { + const url = new URL(route.request().url()); + if (url.origin !== 'http://127.0.0.1:5227') return route.abort(); + if (!url.pathname.startsWith('/api/')) return route.continue(); + requests.push({ path: url.pathname, search: url.search, method: route.request().method() }); + assert.equal(route.request().method(), 'GET'); + if (url.pathname === '/api/license/runtime-capabilities') + return route.fulfill({ + json: { + capabilities: [], + limits: [], + max_history_days: 7, + hosted_mode: false, + runtime: { build: 'community', label: 'Pulse Community runtime' }, + blocked_capabilities: [], + }, + }); + if (url.pathname === '/api/metrics-store/history') { + const id = url.searchParams.get('resourceId'); + assert.ok(['vm-one', 'vm-two', 'agent-three'].includes(id), `unexpected target ${id}`); + const end = Date.UTC(2026, 9, 1, 12); + return route.fulfill({ + json: { + resourceType: url.searchParams.get('resourceType'), + resourceId: id, + range: url.searchParams.get('range'), + start: end - 86400000, + end, + source: 'store', + metrics: { + cpu: [ + { timestamp: end - 600000, value: 20 }, + { timestamp: end - 300000, value: 30 }, + ], + memory: [{ timestamp: end - 300000, value: 40 }], + netin: [ + { timestamp: end - 600000, value: 1024 }, + { timestamp: end - 300000, value: 2048 }, + ], + }, + }, + }); + } + return route.fulfill({ json: { data: [], enabled: false } }); + }); + console.log(JSON.stringify({ stage: 'production-pbs', engine, theme, width, sourceHashes })); + await page.goto('http://127.0.0.1:5227/browser-tests/pbs-identity-boundary.html', { + waitUntil: 'domcontentloaded', + timeout: 120_000, + }); + await page.evaluate( + (dark) => document.documentElement.classList.toggle('dark', dark), + theme === 'dark', + ); + await activate(page.getByRole('button', { name: 'Corroborate links', exact: true })); + for (const suffix of ['one', 'two', 'three']) { + const cell = page.locator(`td[title="backup-connection-${suffix} · tank"]`); + const disclosure = cell.getByRole('button'); + assert.equal(await disclosure.getAttribute('aria-expanded'), 'false'); + // No focus, force, dispatchEvent, synthetic click or added event listeners. + await activate(cell); + await page.locator(`[data-inline-platform-resource-detail-for="pbs-${suffix}"]`).waitFor(); + assert.equal(await disclosure.getAttribute('aria-expanded'), 'true'); + const detail = page.locator(`[data-inline-platform-resource-detail-for="pbs-${suffix}"]`); + observations.push({ suffix, firstActivationOpened: true }); + await noOverflow(); + if (suffix === 'one') await snapshot('pbs-first-activation'); + await activate(detail.getByRole('tab', { name: 'History', exact: true })); + await detail.getByTestId('guest-history-plot').first().locator('path').first().waitFor(); + assert.equal( + await disclosure.getAttribute('aria-expanded'), + 'true', + 'tab must not retoggle row', + ); + if (suffix === 'three') await snapshot('pbs-third-history'); + // Close by the same ordinary cell; nested History input cannot consume it. + await activate(cell); + assert.equal(await disclosure.getAttribute('aria-expanded'), 'false'); + assert.equal(await detail.count(), 0); + } + const keyboardCell = page.locator('td[title="backup-connection-one · tank"]'); + const keyboardDisclosure = keyboardCell.getByRole('button'); + await keyboardDisclosure.focus(); + await page.keyboard.press('Enter'); + assert.equal(await keyboardDisclosure.getAttribute('aria-expanded'), 'true'); + await page.keyboard.press('Space'); + assert.equal(await keyboardDisclosure.getAttribute('aria-expanded'), 'false'); + assert.ok(requests.some((request) => request.search.includes('resourceId=vm-one'))); + assert.ok(requests.some((request) => request.search.includes('resourceId=vm-two'))); + assert.ok(requests.some((request) => request.search.includes('resourceId=agent-three'))); + + console.log(JSON.stringify({ stage: 'shared-controls', engine, theme, width })); + await page.goto('http://127.0.0.1:5227/browser-tests/table-row-touch.html', { + waitUntil: 'domcontentloaded', + timeout: 120_000, + }); + await page.evaluate( + (dark) => document.documentElement.classList.toggle('dark', dark), + theme === 'dark', + ); + const state = async () => + JSON.parse(await page.getByRole('status', { name: 'Action state' }).innerText()); + const name = page.getByText('Shared resource name', { exact: true }); + await name.waitFor(); + await activate(name); + assert.equal((await state()).rowActions, 1); + assert.equal((await state()).expanded, true); + await activate(page.getByRole('button', { name: 'Child action', exact: true })); + await activate(page.getByRole('link', { name: 'Native link' })); + assert.equal(new URL(page.url()).hash, '#native-link'); + await activate(page.getByRole('checkbox', { name: 'Child checkbox' })); + assert.equal(await page.getByRole('checkbox').isChecked(), true); + assert.equal((await state()).rowActions, 1, 'nested controls must not run row action'); + assert.equal((await state()).childActions, 2); + const toggle = page.getByTestId('control-row').locator('[data-row-action="true"]'); + await activate(toggle); + assert.equal((await state()).rowActions, 2, 'nested disclosure must toggle once'); + await toggle.focus(); + await page.keyboard.press('Enter'); + assert.equal((await state()).rowActions, 3); + await page.keyboard.press('Space'); + assert.equal((await state()).rowActions, 4); + await activate(page.getByRole('button', { name: 'Toggle row action' })); + await activate(name); + assert.equal((await state()).rowActions, 4); + await activate(page.getByRole('button', { name: 'Toggle row action' })); + await activate(name); + assert.equal((await state()).rowActions, 5); + await activate(page.getByText('Static row', { exact: true })); + assert.equal((await state()).rowActions, 5); + await activate(page.getByText('Grid resource', { exact: true })); + assert.equal((await state()).gridActions, 1); + await activate(page.getByRole('button', { name: 'Grid child action', exact: true })); + assert.equal((await state()).gridActions, 1); + assert.equal((await state()).childActions, 3); + await noOverflow(); + await snapshot('shared-controls'); + assert.deepEqual(errors, []); + const result = { + engine, + browserVersion: browser.version(), + playwrightVersion, + theme, + width, + sourceHashes, + observations, + requests, + controlState: await state(), + errors, + result: 'passed', + }; + fs.writeFileSync(path.join(out, 'result.json'), JSON.stringify(result, null, 2) + '\n', { + mode: 0o644, + }); + console.log(JSON.stringify(result)); + } catch (error) { + if (page) await snapshot('failure').catch(() => {}); + fs.writeFileSync( + path.join(out, 'failure.json'), + JSON.stringify( + { error: String(error), errors, requests, observations, sourceHashes }, + null, + 2, + ) + '\n', + { mode: 0o644 }, + ); + throw error; + } finally { + await browser?.close(); + await server.close(); + } +})().catch((error) => { + console.error(error); + process.exitCode = 1; +}); diff --git a/frontend-modern/browser-tests/table-row-touch.html b/frontend-modern/browser-tests/table-row-touch.html new file mode 100644 index 0000000000..9fd7276bf5 --- /dev/null +++ b/frontend-modern/browser-tests/table-row-touch.html @@ -0,0 +1,12 @@ + + + + + + Shared table touch verification + + +
+ + + diff --git a/frontend-modern/browser-tests/table-row-touch.tsx b/frontend-modern/browser-tests/table-row-touch.tsx new file mode 100644 index 0000000000..ab8e033077 --- /dev/null +++ b/frontend-modern/browser-tests/table-row-touch.tsx @@ -0,0 +1,91 @@ +// Real shared table/grid actions with synthetic data, not an installed journey. +import { createSignal } from 'solid-js'; +import { render } from 'solid-js/web'; +import { Table, TableBody, TableCell, TableRow } from '../src/components/shared/Table'; +import { SummaryRowActionButton } from '../src/components/shared/SummaryRowActionButton'; +import { PulseDataGrid } from '../src/components/shared/PulseDataGrid'; +import '../src/index.css'; + +function Fixture() { + const [expanded, setExpanded] = createSignal(false); + const [rowActions, setRowActions] = createSignal(0); + const [childActions, setChildActions] = createSignal(0); + const [enabled, setEnabled] = createSignal(true); + const [gridActions, setGridActions] = createSignal(0); + const toggle = () => { + setRowActions((count) => count + 1); + setExpanded((value) => !value); + }; + const child = (event: MouseEvent) => { + event.stopPropagation(); + setChildActions((count) => count + 1); + }; + return ( +
+

Shared table touch verification

+ + + {JSON.stringify({ + expanded: expanded(), + rowActions: rowActions(), + childActions: childActions(), + enabled: enabled(), + gridActions: gridActions(), + })} + + + + + + + Shared resource name + + + + + Native link + + event.stopPropagation()} + /> + + + + Static row + No action + + +
+ + data={[{ id: 'grid-one', name: 'Grid resource' }]} + keyExtractor={(row) => row.id} + onRowClick={() => setGridActions((count) => count + 1)} + columns={[ + { key: 'name', label: 'Name' }, + { + key: 'action', + label: 'Action', + render: () => ( + + ), + }, + ]} + /> +
+ ); +} +render(() => , document.getElementById('root')!); diff --git a/frontend-modern/browser-verification.json b/frontend-modern/browser-verification.json index 6fd5437339..51ed3966f6 100644 --- a/frontend-modern/browser-verification.json +++ b/frontend-modern/browser-verification.json @@ -1,124 +1,189 @@ { "version": 1, - "base_sha": "00f82e2afc3cd75035c997e9e245c6e0eeffffa9", - "verified_at": "2026-10-01T01:42:43Z", + "base_sha": "920aa2f27c6cb717cca7eb8e2cd3f1379d0fdf73", + "verified_at": "2026-10-01T03:23:21Z", "result": "passed", "changed_paths": [ - "frontend-modern/src/components/Workloads/GuestDrawerHistory.tsx", - "frontend-modern/src/components/Workloads/guestDrawerModel.ts" + "frontend-modern/src/components/shared/Table.tsx" ], "content_sha256": { - "frontend-modern/src/components/Workloads/GuestDrawerHistory.tsx": "a49f3c6496e5515284a94c52b818f78f72d2e3530ae398794ea7432f251f48ec", - "frontend-modern/src/components/Workloads/guestDrawerModel.ts": "5ea99d3d995ddb2fa777273ed3f224b7fbb9e7b10a015bed52d61a8e10dc0b99" + "frontend-modern/src/components/shared/Table.tsx": "4009a751a3fa435cb4067a98989d25cac4e9e6d08acd7b91701fc98fbfd5aaf1" }, "routes": [ - "/browser-tests/pbs-identity-boundary.html (production PBS table, full ResourceDetailDrawer, shared History/query and CSS with synthetic VM/Agent APIs; not installed collection or complete #1723 acceptance)" + "/browser-tests/pbs-identity-boundary.html (production PBS table, full ResourceDetailDrawer and History with synthetic bounded API/Agent/PVE data)", + "/browser-tests/table-row-touch.html (production Table, SummaryRowActionButton and PulseDataGrid with synthetic rows and delegated embedded actions)" ], "viewports": [ - { - "width": 1365, - "height": 900 - }, { "width": 390, "height": 844 + }, + { + "width": 1365, + "height": 900 } ], "states": [ - "light/dark short sample coverage stays at its true position in the fulfilled 24-hour window rather than stretching across each plot", - "all configured utilization/network/disk/thermal panels share dated endpoints, including lone and empty panels", - "three separately mapped synthetic PBS drawers read vm-one, vm-two and agent-three, not unrelated host identities", - "same-time observations occupy the same horizontal coordinate across groups with different sample coverage", - "failed same-source refresh retains samples and window with a sanitised warning; successful refresh advances the window", - "held 1-hour range replacement clears former endpoints, paths and inspection controls; matching data then uses the new window", - "empty window has labelled bounds without a synthetic trend; seven-day bounds remain dated; locked fourteen-day range makes no read" + "WebKit phone light/dark, Chromium phone light and Firefox desktop dark: all three synthetic PBS drawers open with the first ordinary cell activation and close with the next", + "History tabs stay open and read only vm-one, vm-two and agent-three with bounded GETs", + "delegated embedded buttons, native fragment links and checkbox clicks retain their own action without firing the row", + "shared disclosure touch and Enter/Space activate once; disabled/static rows do not act; restoring the row handler works", + "PulseDataGrid row activates once and its embedded button does not activate the row; no page errors or document horizontal overflow" ], "interactions": [ - "desktop native keyboard disclosure and History tab; Chromium phone row touch; WebKit phone native keyboard disclosure (whole-row touch opening not established)", - "focus native observation input, use Home/ArrowRight/End and compare dated values and same-time markers between utilization and network", - "Chromium/WebKit phone-emulated touch taps on the at-least-44px native History input; inspection performs no extra API reads", - "fail a real query refresh; retry with advancing bounds; hold and fulfil a 1-hour replacement read; refresh empty, choose seven days and a licence-locked range" + "phone-emulated locator.tap on ordinary PBS cells without focus, force, synthetic click dispatch or added event instrumentation", + "tap History, inspect real plotted paths, close each drawer from its same name cell; focus native disclosure and use Enter/Space", + "tap embedded button, fragment link and checkbox; tap and keyboard-activate native disclosure; withdraw and restore row action; activate data-grid row and its child button" ], - "command": "pulse-worker-browser frontend-modern/browser-tests/history-window.cjs --engine=chromium --theme=light; --engine=chromium --theme=dark; both themes with --engine=chromium --phone; both themes with --engine=firefox; both themes with --engine=webkit --phone. Each is a separate bounded command from the assigned workspace root. Vite 6.4.3 / Playwright 1.56.1; integration-lock parity checked.", + "command": "pulse-worker-browser frontend-modern/browser-tests/table-row-touch.cjs --engine=webkit; --engine=webkit --dark; --engine=chromium; --engine=firefox --dark --desktop. Four separate bounded executions from the assigned workspace root. Playwright 1.56.1 / integration-lock parity checked; Vite 6.4.3.", "browser_versions": { "chromium": "141.0.7390.37", "firefox": "142.0.1", "webkit": "26.0" }, "artifacts": [ - "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-chromium-1365-dark/one-24h-dark.png", - "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-chromium-1365-dark/one-hour-dark.png", - "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-chromium-1365-dark/progress.json", - "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-chromium-1365-dark/result.json", - "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-chromium-1365-dark/retained-window-dark.png", - "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-chromium-1365-dark/three-24h-dark.png", - "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-chromium-1365-dark/two-24h-dark.png", - "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-chromium-1365-light/one-24h-light.png", - "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-chromium-1365-light/one-hour-light.png", - "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-chromium-1365-light/progress.json", - "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-chromium-1365-light/result.json", - "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-chromium-1365-light/retained-window-light.png", - "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-chromium-1365-light/three-24h-light.png", - "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-chromium-1365-light/two-24h-light.png", - "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-chromium-390-dark/one-24h-dark.png", - "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-chromium-390-dark/one-hour-dark.png", - "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-chromium-390-dark/progress.json", - "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-chromium-390-dark/result.json", - "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-chromium-390-dark/retained-window-dark.png", - "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-chromium-390-dark/three-24h-dark.png", - "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-chromium-390-dark/two-24h-dark.png", - "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-chromium-390-light/one-24h-light.png", - "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-chromium-390-light/one-hour-light.png", - "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-chromium-390-light/progress.json", - "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-chromium-390-light/result.json", - "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-chromium-390-light/retained-window-light.png", - "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-chromium-390-light/three-24h-light.png", - "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-chromium-390-light/two-24h-light.png", - "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-firefox-1365-dark/one-24h-dark.png", - "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-firefox-1365-dark/one-hour-dark.png", - "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-firefox-1365-dark/progress.json", - "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-firefox-1365-dark/result.json", - "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-firefox-1365-dark/retained-window-dark.png", - "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-firefox-1365-dark/three-24h-dark.png", - "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-firefox-1365-dark/two-24h-dark.png", - "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-firefox-1365-light/one-24h-light.png", - "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-firefox-1365-light/one-hour-light.png", - "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-firefox-1365-light/progress.json", - "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-firefox-1365-light/result.json", - "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-firefox-1365-light/retained-window-light.png", - "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-firefox-1365-light/three-24h-light.png", - "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-firefox-1365-light/two-24h-light.png", - "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-webkit-390-dark/one-24h-dark.png", - "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-webkit-390-dark/one-hour-dark.png", - "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-webkit-390-dark/progress.json", - "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-webkit-390-dark/result.json", - "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-webkit-390-dark/retained-window-dark.png", - "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-webkit-390-dark/three-24h-dark.png", - "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-webkit-390-dark/two-24h-dark.png", - "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-webkit-390-light/one-24h-light.png", - "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-webkit-390-light/one-hour-light.png", - "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-webkit-390-light/progress.json", - "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-webkit-390-light/result.json", - "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-webkit-390-light/retained-window-light.png", - "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-webkit-390-light/three-24h-light.png", - "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-webkit-390-light/two-24h-light.png", - "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/receipts/browser-chromium-desktop-dark-final.log", - "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/receipts/browser-chromium-desktop-light-final.log", - "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/receipts/browser-chromium-phone-dark-final.log", - "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/receipts/browser-chromium-phone-light-final.log", - "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/receipts/browser-firefox-desktop-dark-final.log", - "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/receipts/browser-firefox-desktop-light-final.log", - "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/receipts/browser-webkit-phone-dark-native.log", - "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/receipts/browser-webkit-phone-light-native.log" + "/var/lib/pulse-maintainer/worker-outputs/web-product-0i3s168w/browser-final/chromium-390-light/result.json", + "/var/lib/pulse-maintainer/worker-outputs/web-product-0i3s168w/browser-final/chromium-390-light/pbs-first-activation.png", + "/var/lib/pulse-maintainer/worker-outputs/web-product-0i3s168w/browser-final/chromium-390-light/pbs-third-history.png", + "/var/lib/pulse-maintainer/worker-outputs/web-product-0i3s168w/browser-final/chromium-390-light/shared-controls.png", + "/var/lib/pulse-maintainer/worker-outputs/web-product-0i3s168w/browser-final/firefox-1365-dark/result.json", + "/var/lib/pulse-maintainer/worker-outputs/web-product-0i3s168w/browser-final/firefox-1365-dark/pbs-first-activation.png", + "/var/lib/pulse-maintainer/worker-outputs/web-product-0i3s168w/browser-final/firefox-1365-dark/pbs-third-history.png", + "/var/lib/pulse-maintainer/worker-outputs/web-product-0i3s168w/browser-final/firefox-1365-dark/shared-controls.png", + "/var/lib/pulse-maintainer/worker-outputs/web-product-0i3s168w/browser-final/webkit-390-dark/result.json", + "/var/lib/pulse-maintainer/worker-outputs/web-product-0i3s168w/browser-final/webkit-390-dark/pbs-first-activation.png", + "/var/lib/pulse-maintainer/worker-outputs/web-product-0i3s168w/browser-final/webkit-390-dark/pbs-third-history.png", + "/var/lib/pulse-maintainer/worker-outputs/web-product-0i3s168w/browser-final/webkit-390-dark/shared-controls.png", + "/var/lib/pulse-maintainer/worker-outputs/web-product-0i3s168w/browser-final/webkit-390-light/result.json", + "/var/lib/pulse-maintainer/worker-outputs/web-product-0i3s168w/browser-final/webkit-390-light/pbs-first-activation.png", + "/var/lib/pulse-maintainer/worker-outputs/web-product-0i3s168w/browser-final/webkit-390-light/pbs-third-history.png", + "/var/lib/pulse-maintainer/worker-outputs/web-product-0i3s168w/browser-final/webkit-390-light/shared-controls.png" ], - "notes": "All 40 final screenshots inspected. Eight engine/viewport/theme cases pass with no page errors or horizontal overflow and exact final runtime hashes. The ten-case exact-base source-fbz6bjig input fails all ten assertions; an inspected full PBS drawer baseline measures five minutes occupying 265/318 SVG plot units under 24 hours, versus about 1.10 units after repair. Initial navigation timeout, unknown browser-execution capability-unavailable exit, synthetic edge-point fixture mismatch and unsuccessful WebKit row/hidden-button touch attempts remain in internal receipts. WebKit final opening uses native keyboard disclosure, not a claimed touch repair; History touch inspection passes. Six Chromium/Firefox cases ran before the runner added the disclosure-state assertion and WebKit keyboard-opening branch; their runtime source hashes are identical to the final two WebKit cases. This establishes presentation, not real PBS/VirtualBox collection, all reported #1723 drawer/metric outcomes, screen-reader speech, installed Tailscale Serve/SSE update or signed upgrade/rollback, protected integration or release availability.", + "notes": "All 12 final screenshots inspected. This repairs reproduced WebKit-emulated row touch, not physical iPhone/Safari acceptance, installed PBS telemetry, every #1723 metric outcome, screen-reader speech or a Tailscale Serve update. Initial harness working-directory/API-path/locator/output-wrapping failures and the instrumented diagnostic are retained in the output receipts; none is a release failure or passing uninstrumented baseline.", "prior_verifications": [ + { + "version": 1, + "base_sha": "00f82e2afc3cd75035c997e9e245c6e0eeffffa9", + "verified_at": "2026-10-01T01:42:43Z", + "result": "passed", + "changed_paths": [ + "frontend-modern/src/components/Workloads/GuestDrawerHistory.tsx", + "frontend-modern/src/components/Workloads/guestDrawerModel.ts" + ], + "content_sha256": { + "frontend-modern/src/components/Workloads/GuestDrawerHistory.tsx": "a49f3c6496e5515284a94c52b818f78f72d2e3530ae398794ea7432f251f48ec", + "frontend-modern/src/components/Workloads/guestDrawerModel.ts": "5ea99d3d995ddb2fa777273ed3f224b7fbb9e7b10a015bed52d61a8e10dc0b99" + }, + "routes": [ + "/browser-tests/pbs-identity-boundary.html (production PBS table, full ResourceDetailDrawer, shared History/query and CSS with synthetic VM/Agent APIs; not installed collection or complete #1723 acceptance)" + ], + "viewports": [ + { + "width": 1365, + "height": 900 + }, + { + "width": 390, + "height": 844 + } + ], + "states": [ + "light/dark short sample coverage stays at its true position in the fulfilled 24-hour window rather than stretching across each plot", + "all configured utilization/network/disk/thermal panels share dated endpoints, including lone and empty panels", + "three separately mapped synthetic PBS drawers read vm-one, vm-two and agent-three, not unrelated host identities", + "same-time observations occupy the same horizontal coordinate across groups with different sample coverage", + "failed same-source refresh retains samples and window with a sanitised warning; successful refresh advances the window", + "held 1-hour range replacement clears former endpoints, paths and inspection controls; matching data then uses the new window", + "empty window has labelled bounds without a synthetic trend; seven-day bounds remain dated; locked fourteen-day range makes no read" + ], + "interactions": [ + "desktop native keyboard disclosure and History tab; Chromium phone row touch; WebKit phone native keyboard disclosure (whole-row touch opening not established)", + "focus native observation input, use Home/ArrowRight/End and compare dated values and same-time markers between utilization and network", + "Chromium/WebKit phone-emulated touch taps on the at-least-44px native History input; inspection performs no extra API reads", + "fail a real query refresh; retry with advancing bounds; hold and fulfil a 1-hour replacement read; refresh empty, choose seven days and a licence-locked range" + ], + "command": "pulse-worker-browser frontend-modern/browser-tests/history-window.cjs --engine=chromium --theme=light; --engine=chromium --theme=dark; both themes with --engine=chromium --phone; both themes with --engine=firefox; both themes with --engine=webkit --phone. Each is a separate bounded command from the assigned workspace root. Vite 6.4.3 / Playwright 1.56.1; integration-lock parity checked.", + "browser_versions": { + "chromium": "141.0.7390.37", + "firefox": "142.0.1", + "webkit": "26.0" + }, + "artifacts": [ + "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-chromium-1365-dark/one-24h-dark.png", + "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-chromium-1365-dark/one-hour-dark.png", + "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-chromium-1365-dark/progress.json", + "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-chromium-1365-dark/result.json", + "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-chromium-1365-dark/retained-window-dark.png", + "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-chromium-1365-dark/three-24h-dark.png", + "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-chromium-1365-dark/two-24h-dark.png", + "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-chromium-1365-light/one-24h-light.png", + "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-chromium-1365-light/one-hour-light.png", + "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-chromium-1365-light/progress.json", + "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-chromium-1365-light/result.json", + "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-chromium-1365-light/retained-window-light.png", + "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-chromium-1365-light/three-24h-light.png", + "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-chromium-1365-light/two-24h-light.png", + "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-chromium-390-dark/one-24h-dark.png", + "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-chromium-390-dark/one-hour-dark.png", + "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-chromium-390-dark/progress.json", + "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-chromium-390-dark/result.json", + "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-chromium-390-dark/retained-window-dark.png", + "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-chromium-390-dark/three-24h-dark.png", + "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-chromium-390-dark/two-24h-dark.png", + "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-chromium-390-light/one-24h-light.png", + "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-chromium-390-light/one-hour-light.png", + "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-chromium-390-light/progress.json", + "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-chromium-390-light/result.json", + "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-chromium-390-light/retained-window-light.png", + "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-chromium-390-light/three-24h-light.png", + "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-chromium-390-light/two-24h-light.png", + "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-firefox-1365-dark/one-24h-dark.png", + "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-firefox-1365-dark/one-hour-dark.png", + "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-firefox-1365-dark/progress.json", + "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-firefox-1365-dark/result.json", + "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-firefox-1365-dark/retained-window-dark.png", + "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-firefox-1365-dark/three-24h-dark.png", + "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-firefox-1365-dark/two-24h-dark.png", + "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-firefox-1365-light/one-24h-light.png", + "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-firefox-1365-light/one-hour-light.png", + "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-firefox-1365-light/progress.json", + "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-firefox-1365-light/result.json", + "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-firefox-1365-light/retained-window-light.png", + "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-firefox-1365-light/three-24h-light.png", + "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-firefox-1365-light/two-24h-light.png", + "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-webkit-390-dark/one-24h-dark.png", + "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-webkit-390-dark/one-hour-dark.png", + "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-webkit-390-dark/progress.json", + "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-webkit-390-dark/result.json", + "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-webkit-390-dark/retained-window-dark.png", + "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-webkit-390-dark/three-24h-dark.png", + "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-webkit-390-dark/two-24h-dark.png", + "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-webkit-390-light/one-24h-light.png", + "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-webkit-390-light/one-hour-light.png", + "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-webkit-390-light/progress.json", + "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-webkit-390-light/result.json", + "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-webkit-390-light/retained-window-light.png", + "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-webkit-390-light/three-24h-light.png", + "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/browser-final/history-window-webkit-390-light/two-24h-light.png", + "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/receipts/browser-chromium-desktop-dark-final.log", + "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/receipts/browser-chromium-desktop-light-final.log", + "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/receipts/browser-chromium-phone-dark-final.log", + "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/receipts/browser-chromium-phone-light-final.log", + "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/receipts/browser-firefox-desktop-dark-final.log", + "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/receipts/browser-firefox-desktop-light-final.log", + "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/receipts/browser-webkit-phone-dark-native.log", + "/var/lib/pulse-maintainer/worker-outputs/web-product-6zj90pef/receipts/browser-webkit-phone-light-native.log" + ], + "notes": "All 40 final screenshots inspected. Eight engine/viewport/theme cases pass with no page errors or horizontal overflow and exact final runtime hashes. The ten-case exact-base source-fbz6bjig input fails all ten assertions; an inspected full PBS drawer baseline measures five minutes occupying 265/318 SVG plot units under 24 hours, versus about 1.10 units after repair. Initial navigation timeout, unknown browser-execution capability-unavailable exit, synthetic edge-point fixture mismatch and unsuccessful WebKit row/hidden-button touch attempts remain in internal receipts. WebKit final opening uses native keyboard disclosure, not a claimed touch repair; History touch inspection passes. Six Chromium/Firefox cases ran before the runner added the disclosure-state assertion and WebKit keyboard-opening branch; their runtime source hashes are identical to the final two WebKit cases. This establishes presentation, not real PBS/VirtualBox collection, all reported #1723 drawer/metric outcomes, screen-reader speech, installed Tailscale Serve/SSE update or signed upgrade/rollback, protected integration or release availability." + }, { "version": 1, "base_sha": "0dc858cb366a394b159d2c911c5d85b3020e7881", "verified_at": "2026-10-01T00:37:51Z", "result": "passed", - "changed_paths": ["frontend-modern/src/components/Workloads/GuestDrawerHistory.tsx"], + "changed_paths": [ + "frontend-modern/src/components/Workloads/GuestDrawerHistory.tsx" + ], "content_sha256": { "frontend-modern/src/components/Workloads/GuestDrawerHistory.tsx": "e1658b5843ab22e0ea83d1bf27e8c1565a16d9f528eb3832524a67318d281d6a" }, @@ -192,7 +257,9 @@ "base_sha": "0d3e5a801ab8fd1c1d224ef561cbf99e169c589f", "verified_at": "2026-09-30T23:28:26Z", "result": "passed", - "changed_paths": ["frontend-modern/src/components/Workloads/GuestDrawerHistory.tsx"], + "changed_paths": [ + "frontend-modern/src/components/Workloads/GuestDrawerHistory.tsx" + ], "content_sha256": { "frontend-modern/src/components/Workloads/GuestDrawerHistory.tsx": "327e75cabb99edffc712302c00645f06d92dc44dbebdd00aef08074e7fa53eb2" }, @@ -332,7 +399,9 @@ "base_sha": "21a35494aecb335755de773dd099e1364e65b3e0", "verified_at": "2026-09-30T20:12:09Z", "result": "passed", - "changed_paths": ["frontend-modern/src/components/Workloads/GuestDrawerHistory.tsx"], + "changed_paths": [ + "frontend-modern/src/components/Workloads/GuestDrawerHistory.tsx" + ], "content_sha256": { "frontend-modern/src/components/Workloads/GuestDrawerHistory.tsx": "b2b31441b19c2e50283ceedf319cd09a1f94791750cd424c052717a4957fdc67" }, @@ -387,7 +456,9 @@ "base_sha": "6ade01cbd4d02cd25b691f9ff5919206cceae764", "verified_at": "2026-09-30T01:40:13Z", "result": "passed", - "changed_paths": ["frontend-modern/src/features/proxmox/ProxmoxBackupServersTable.tsx"], + "changed_paths": [ + "frontend-modern/src/features/proxmox/ProxmoxBackupServersTable.tsx" + ], "content_sha256": { "frontend-modern/src/features/proxmox/ProxmoxBackupServersTable.tsx": "ed7207456ebbc990ada34b0c739e29565d90cfe1998415a781d87f3067d69c36" }, @@ -436,7 +507,9 @@ "base_sha": "c3e8b6dd6dff36b5396ba2eb147d4e58d23001a6", "verified_at": "2026-09-29T23:04:22Z", "result": "passed", - "changed_paths": ["frontend-modern/src/features/proxmox/ProxmoxBackupServersTable.tsx"], + "changed_paths": [ + "frontend-modern/src/features/proxmox/ProxmoxBackupServersTable.tsx" + ], "content_sha256": { "frontend-modern/src/features/proxmox/ProxmoxBackupServersTable.tsx": "7172690587f8e642ee30492481abbe76ee79d277aad290c3b50310dd1c0b282b" }, @@ -532,7 +605,9 @@ "base_sha": "fbf17b271abe70ad3e12042506b52f85b6cbc484", "verified_at": "2026-09-29T19:50:45Z", "result": "passed", - "changed_paths": ["frontend-modern/src/pages/Actions.tsx"], + "changed_paths": [ + "frontend-modern/src/pages/Actions.tsx" + ], "content_sha256": { "frontend-modern/src/pages/Actions.tsx": "30e862ecc5125f07cf93f0c1e0b5f58efa023f5fdb56fe4b01721bf9a453e7ce" }, @@ -573,7 +648,9 @@ "base_sha": "c2f7aa8b471d6cbb858e8ec71b4e664fded6549c", "verified_at": "2026-09-29T19:09:52Z", "result": "passed", - "changed_paths": ["frontend-modern/src/features/actions/ActionReviewDialog.tsx"], + "changed_paths": [ + "frontend-modern/src/features/actions/ActionReviewDialog.tsx" + ], "content_sha256": { "frontend-modern/src/features/actions/ActionReviewDialog.tsx": "9a4af522484b42e37b118e8bfde7c1673122037a54b8bfed249818ba644e6b81" }, diff --git a/frontend-modern/src/components/shared/Table.tsx b/frontend-modern/src/components/shared/Table.tsx index 6d2fdbb506..472263d205 100644 --- a/frontend-modern/src/components/shared/Table.tsx +++ b/frontend-modern/src/components/shared/Table.tsx @@ -63,11 +63,17 @@ export function TableBody(props: TableBodyProps) { export type TableRowProps = JSX.HTMLAttributes; +// WebKit touch needs a native click target on otherwise-static table rows. +// Keep actions document-delegated: nested controls must run first and retain +// their existing stopPropagation behaviour instead of opening the row too. +const nativeRowClickTarget = () => undefined; + export function TableRow(props: TableRowProps) { const [local, rest] = splitProps(props, ['class', 'children']); return ( {local.children} diff --git a/frontend-modern/src/components/shared/__tests__/PulseDataGrid.test.tsx b/frontend-modern/src/components/shared/__tests__/PulseDataGrid.test.tsx index 4691fbf0fa..23ac2ff5e9 100644 --- a/frontend-modern/src/components/shared/__tests__/PulseDataGrid.test.tsx +++ b/frontend-modern/src/components/shared/__tests__/PulseDataGrid.test.tsx @@ -103,6 +103,23 @@ describe('PulseDataGrid', () => { expect(onRowClick).toHaveBeenCalledTimes(1); }); + it('inherits the native touch click target from the shared TableRow', () => { + const nativeListener = vi.spyOn(HTMLTableRowElement.prototype, 'addEventListener'); + try { + render(() => ( + + data={[{ id: '1', name: 'Touch tower' }]} + columns={[{ key: 'name', label: 'Name' }]} + keyExtractor={(row) => row.id} + onRowClick={vi.fn()} + /> + )); + expect(nativeListener.mock.calls.some(([type]) => type === 'click')).toBe(true); + } finally { + nativeListener.mockRestore(); + } + }); + it('does not trigger the row handler when an interactive child is clicked', () => { const onRowClick = vi.fn(); const onRemove = vi.fn(); diff --git a/frontend-modern/src/components/shared/__tests__/Table.test.tsx b/frontend-modern/src/components/shared/__tests__/Table.test.tsx index 484cecc996..c81ce1dde0 100644 --- a/frontend-modern/src/components/shared/__tests__/Table.test.tsx +++ b/frontend-modern/src/components/shared/__tests__/Table.test.tsx @@ -1,5 +1,6 @@ -import { render, screen } from '@solidjs/testing-library'; -import { describe, expect, it } from 'vitest'; +import { cleanup, fireEvent, render, screen } from '@solidjs/testing-library'; +import { createSignal } from 'solid-js'; +import { afterEach, describe, expect, it, vi } from 'vitest'; import tableSource from '@/components/shared/Table.tsx?raw'; import { @@ -11,6 +12,128 @@ import { TableRow, } from '@/components/shared/Table'; +afterEach(() => { + cleanup(); + vi.restoreAllMocks(); +}); + +describe('TableRow touch activation', () => { + it('marks clickable rows as native click targets without changing row semantics', () => { + const nativeListener = vi.spyOn(HTMLTableRowElement.prototype, 'addEventListener'); + const onClick = vi.fn(); + render(() => ( + + + + Tap resource + + +
+ )); + expect(nativeListener.mock.calls.some(([type]) => type === 'click')).toBe(true); + const row = screen.getByRole('row'); + expect(row).not.toHaveAttribute('tabindex'); + expect(row).not.toHaveAttribute('aria-expanded'); + fireEvent.click(screen.getByText('Tap resource')); + expect(onClick).toHaveBeenCalledTimes(1); + fireEvent.keyDown(row, { key: 'Enter' }); + expect(onClick).toHaveBeenCalledTimes(1); + }); + + it('does not make static rows native click targets', () => { + const nativeListener = vi.spyOn(HTMLTableRowElement.prototype, 'addEventListener'); + render(() => ( + + + + Static resource + + +
+ )); + expect(nativeListener.mock.calls.some(([type]) => type === 'click')).toBe(false); + }); + + it('lets delegated child actions stop the row before its action runs', () => { + const onClick = vi.fn(); + const onChild = vi.fn(); + render(() => ( + + + + + + + + +
+ )); + fireEvent.click(screen.getByRole('button', { name: 'Child action' })); + expect(onChild).toHaveBeenCalledTimes(1); + expect(onClick).not.toHaveBeenCalled(); + }); + + it('removes the native target when the row action is withdrawn', () => { + const removeListener = vi.spyOn(HTMLTableRowElement.prototype, 'removeEventListener'); + const onClick = vi.fn(); + const [enabled, setEnabled] = createSignal(true); + render(() => ( + + + + Changing resource + + +
+ )); + fireEvent.click(screen.getByText('Changing resource')); + setEnabled(false); + expect(removeListener.mock.calls.some(([type]) => type === 'click')).toBe(true); + fireEvent.click(screen.getByText('Changing resource')); + expect(onClick).toHaveBeenCalledTimes(1); + setEnabled(true); + fireEvent.click(screen.getByText('Changing resource')); + expect(onClick).toHaveBeenCalledTimes(2); + }); + + it('preserves bound delegated handlers and caller-owned native handlers', () => { + const onClick = vi.fn(); + const onNative = vi.fn(); + render(() => ( + + + { + event.stopPropagation(); + onNative(); + }} + > + Native action + + + Bound action + + +
+ )); + fireEvent.click(screen.getByText('Native action')); + expect(onNative).toHaveBeenCalledTimes(1); + expect(onClick).not.toHaveBeenCalled(); + fireEvent.click(screen.getByText('Bound action')); + expect(onClick).toHaveBeenCalledTimes(1); + expect(onClick.mock.calls[0][0]).toBe('second-identity'); + }); +}); + describe('TableBody', () => { it('keeps the shared table wrapper CSP-safe', () => { expect(tableSource).toContain('touch-scroll');