mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-23 20:25:56 +00:00
Integrate reviewed shared-table touch activation repair
Preserve the exact Web candidate, parent-bound browser receipt and lint correction. Restore first-touch disclosure while retaining delegated nested controls and keyboard behaviour. Change-source: pulse-maintainer
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
@@ -0,0 +1,12 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Shared table touch verification</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/browser-tests/table-row-touch.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -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 (
|
||||
<main class="space-y-4 p-4">
|
||||
<h1>Shared table touch verification</h1>
|
||||
<button type="button" onClick={() => setEnabled((value) => !value)}>
|
||||
Toggle row action
|
||||
</button>
|
||||
<output aria-label="Action state" class="block break-all text-xs">
|
||||
{JSON.stringify({
|
||||
expanded: expanded(),
|
||||
rowActions: rowActions(),
|
||||
childActions: childActions(),
|
||||
enabled: enabled(),
|
||||
gridActions: gridActions(),
|
||||
})}
|
||||
</output>
|
||||
<Table>
|
||||
<TableBody>
|
||||
<TableRow data-testid="control-row" onClick={enabled() ? toggle : undefined}>
|
||||
<TableCell>
|
||||
<SummaryRowActionButton
|
||||
kind="disclosure"
|
||||
subjectLabel="shared resource"
|
||||
expanded={expanded()}
|
||||
hideWhenRowTappableOnMobile={false}
|
||||
onAction={toggle}
|
||||
/>
|
||||
<span>Shared resource name</span>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<button type="button" onClick={child}>
|
||||
Child action
|
||||
</button>
|
||||
<a href="#native-link" onClick={child}>
|
||||
Native link
|
||||
</a>
|
||||
<input
|
||||
type="checkbox"
|
||||
aria-label="Child checkbox"
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>Static row</TableCell>
|
||||
<TableCell>No action</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
<PulseDataGrid<{ id: string; name: string }>
|
||||
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: () => (
|
||||
<button type="button" onClick={child}>
|
||||
Grid child action
|
||||
</button>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
render(() => <Fixture />, document.getElementById('root')!);
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -63,11 +63,17 @@ export function TableBody(props: TableBodyProps) {
|
||||
|
||||
export type TableRowProps = JSX.HTMLAttributes<HTMLTableRowElement>;
|
||||
|
||||
// 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 (
|
||||
<tr
|
||||
class={`group transition-colors duration-150 hover:bg-surface-hover ${local.class || ''}`}
|
||||
on:click={rest.onClick ? nativeRowClickTarget : undefined}
|
||||
{...rest}
|
||||
>
|
||||
{local.children}
|
||||
|
||||
@@ -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(() => (
|
||||
<PulseDataGrid<TestRow>
|
||||
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();
|
||||
|
||||
@@ -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,127 @@ 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(() => (
|
||||
<Table>
|
||||
<TableBody>
|
||||
<TableRow onClick={onClick}>
|
||||
<TableCell>Tap resource</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
));
|
||||
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(() => (
|
||||
<Table>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableCell>Static resource</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
));
|
||||
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(() => (
|
||||
<Table>
|
||||
<TableBody>
|
||||
<TableRow onClick={onClick}>
|
||||
<TableCell>
|
||||
<button
|
||||
type="button"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
onChild();
|
||||
}}
|
||||
>
|
||||
Child action
|
||||
</button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
));
|
||||
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(() => (
|
||||
<Table>
|
||||
<TableBody>
|
||||
<TableRow onClick={enabled() ? onClick : undefined}>
|
||||
<TableCell>Changing resource</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
));
|
||||
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(() => (
|
||||
<Table>
|
||||
<TableBody>
|
||||
<TableRow
|
||||
on:click={(event) => {
|
||||
event.stopPropagation();
|
||||
onNative();
|
||||
}}
|
||||
>
|
||||
<TableCell>Native action</TableCell>
|
||||
</TableRow>
|
||||
<TableRow onClick={[onClick, 'second-identity']}>
|
||||
<TableCell>Bound action</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
));
|
||||
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');
|
||||
|
||||
Reference in New Issue
Block a user