Record final Patrol reversal browser checks

Bind the six changed runtime files to completed production-component desktop and narrow checks, including scoped reversal, uncertainty and long-reason scrolling. Retain synthetic and unsupported-input limits without claiming installed acceptance.

Change-source: pulse-maintainer
This commit is contained in:
pulse-triage[bot]
2026-10-05 19:28:09 +01:00
parent e90b10f9f2
commit 486d27bf75
4 changed files with 192 additions and 1 deletions
@@ -0,0 +1,39 @@
// Final affected confirmation content: real phone reversal, desktop viewport,
// and long-reason pointer/keyboard review. No parent or stopped test replay.
const fs = require('node:fs');
const path = require('node:path');
const {
journey,
report,
output,
} = require('/workspace/frontend-modern/browser-tests/patrol-rule-removal.cjs');
(async () => {
try {
await journey('/workspace/frontend-modern', 'webkit', 320);
await journey('/workspace/frontend-modern', 'chromium', 1365, false, true, true);
await journey('/workspace/frontend-modern', 'webkit', 320, false, true, 'long');
report.result = 'passed';
} catch (error) {
report.result = 'failed';
report.failure = error.message;
throw error;
} finally {
report.cleanup = {
all_cases_closed: report.cases.every(
(c) => c.cleanup.browser_closed && c.cleanup.server_closed,
),
};
fs.writeFileSync(path.join(output, 'final-result.json'), JSON.stringify(report, null, 2));
}
console.log(
JSON.stringify({
result: report.result,
cases: report.cases.length,
captures: report.captures.length,
cleanup: report.cleanup,
}),
);
})().catch((error) => {
console.error(error.stack);
process.exitCode = 1;
});
@@ -0,0 +1,38 @@
// Complete the previously unmeasured scroll controls only. Mobile WebKit admits
// keyboard scrolling, not mouse wheel; desktop Chromium admits pointer wheel.
const fs = require('node:fs');
const path = require('node:path');
const {
journey,
report,
output,
} = require('/workspace/frontend-modern/browser-tests/patrol-rule-removal.cjs');
(async () => {
try {
await journey('/workspace/frontend-modern', 'webkit', 320, false, true, 'long');
await journey('/workspace/frontend-modern', 'chromium', 1365, false, true, 'long');
report.result = 'passed';
} catch (error) {
report.result = 'failed';
report.failure = error.message;
throw error;
} finally {
report.cleanup = {
all_cases_closed: report.cases.every(
(c) => c.cleanup.browser_closed && c.cleanup.server_closed,
),
};
fs.writeFileSync(path.join(output, 'long-final-result.json'), JSON.stringify(report, null, 2));
}
console.log(
JSON.stringify({
result: report.result,
cases: report.cases.length,
captures: report.captures.length,
cleanup: report.cleanup,
}),
);
})().catch((error) => {
console.error(error.stack);
process.exitCode = 1;
});
@@ -326,7 +326,10 @@ async function journey(root, engine, width, parent = false, resume = false, visu
return route.fulfill({ json });
});
const capture = async (name) => {
const file = path.join(output, `${parent ? 'parent' : engine}-${width}-${name}.png`);
const file = path.join(
output,
`${parent ? 'parent' : engine}-${width}-${visualOnly === 'long' ? 'long-' : ''}${name}.png`,
);
await page.screenshot({
path: file,
fullPage: !visualOnly,
@@ -403,6 +406,65 @@ async function journey(root, engine, width, parent = false, resume = false, visu
actions.y + actions.height <= 901,
'Confirmation action must not be clipped: ' + JSON.stringify({ box, actions }),
);
if (visualOnly === 'long') {
const region = dialog.getByRole('region', { name: 'Rule scope and reason' });
const initial = await region.evaluate((e) => ({
top: e.scrollTop,
height: e.clientHeight,
scrollHeight: e.scrollHeight,
}));
assert.equal(initial.top, 0, 'Safe autofocus must not scroll past the rule scope');
assert(
initial.scrollHeight > initial.height,
'Long reason needs a real scrolling region',
);
for (const text of ['Keep VM', 'fixture-keep', 'capacity', expected]) {
const scopeBox = await region.getByText(text, { exact: true }).boundingBox();
const regionBox = await region.boundingBox();
assert(
scopeBox &&
regionBox &&
scopeBox.y >= regionBox.y &&
scopeBox.y + scopeBox.height <= regionBox.y + regionBox.height + 1,
'The scope and exact ID are initially in view: ' + text,
);
}
const input =
engine === 'webkit' && width === 320 ? 'keyboard PageDown' : 'pointer wheel';
if (input === 'keyboard PageDown') {
// Playwright mobile WebKit does not implement mouse wheel. Use
// its supported native keyboard scroll on the focusable region.
await region.focus();
await page.keyboard.press('PageDown');
} else {
await region.hover();
await page.mouse.wheel(0, 3000);
}
await page.waitForFunction(
() => document.querySelector('[aria-label="Rule scope and reason"]').scrollTop > 0,
);
const scrolled = await region.evaluate((e) => e.scrollTop);
await capture('long-reason-scrolled');
const footerAfter = await dialog
.getByRole('button', { name: 'Remove this rule' })
.boundingBox();
assert.deepEqual(footerAfter, actions, 'The action footer must not move with the reason');
await region.focus();
await page.keyboard.press('Home');
await page.waitForFunction(
() => document.querySelector('[aria-label="Rule scope and reason"]').scrollTop === 0,
);
result.longReason = {
initial,
scrollInput: input,
scrollPosition: scrolled,
keyboardHomeReturned: true,
stableFooter: true,
};
result.checks.push(
'Long reason supports the recorded scroll input and keyboard Home, with initial scope/ID visible and a stable safe footer.',
);
}
result.dialogs.push({ name, ruleId: expected, box, viewport: { width, height: 900 } });
await page.keyboard.press('Escape');
await dialog.waitFor({ state: 'detached' });
@@ -0,0 +1,52 @@
{
"version": 1,
"base_sha": "e90b10f9f2a8b75249eb78327d302ae27c39b528",
"verified_at": "2026-10-05T18:27:23Z",
"result": "passed",
"changed_paths": [
"frontend-modern/src/api/patrol.ts",
"frontend-modern/src/components/AI/FindingsPanel.tsx",
"frontend-modern/src/features/patrol/PatrolAttentionWorkbench.tsx",
"frontend-modern/src/features/patrol/PatrolIntelligenceSurface.tsx",
"frontend-modern/src/features/patrol/PatrolSuppressionRules.tsx",
"frontend-modern/src/utils/apiClient.ts"
],
"content_sha256": {
"frontend-modern/src/api/patrol.ts": "5c58ef600ede08b04ccb8acd70d14d70ade52cb048f25e1075df328fe2e8324f",
"frontend-modern/src/components/AI/FindingsPanel.tsx": "ae16913c0761aee84ec808a4be9d2782576e7afea78f64ea0b134d5d9e43f375",
"frontend-modern/src/features/patrol/PatrolAttentionWorkbench.tsx": "fa21aa6871c0930e23531bfa27614644871a73a353fd1971aebca1f4dcd1bc0e",
"frontend-modern/src/features/patrol/PatrolIntelligenceSurface.tsx": "f011f93aa9950cf10ce93317f5baf9e25161b39b03ec1a124da6fdbfc20cc249",
"frontend-modern/src/features/patrol/PatrolSuppressionRules.tsx": "6efd4b42234cdc432507786bb3c014262dbc02be400b9029f9af1e5bac871f35",
"frontend-modern/src/utils/apiClient.ts": "56dc80b15c0e7854ee16ece8b902c9de519e54f6110d55dc2c7e735d72214295"
},
"routes": [
"/patrol",
"/patrol/activity#patrol-suppression-rules"
],
"viewports": [
{
"width": 1365,
"height": 900
},
{
"width": 320,
"height": 900
}
],
"states": [
"Production Patrol surface, clients and Router mounted with real CSS in the offline browser fixture; API responses and tenant/session inputs are synthetic. No installed or reporter deletion is claimed.",
"Assigned parent 53ec5fae creates a permanent narrow rule but has no manual-rule reversal surface. Existing Inbox and Finding options creator links lead to Activity in the repaired source.",
"Final-content WebKit 320: manual list, scoped confirmation, cancel, exact-ID removal/readback, preserved unrelated/finding-backed decisions, wildcard confirmation, uncertain response, explicit reload, revoked access and organisation invalidation.",
"Final-content Chromium 1365 viewport: exact manual identity and all-resource/all-category confirmation are visible with initial Cancel focus. Desktop deletion and strict transport checks were completed before the scroll-only markup repair; their client/handler content is unchanged.",
"Long multiline reason at 320 dark WebKit and 1365 light Chromium: heading and action footer stay visible, initial resource/category/rule ID remain in view, reason scrolls independently. WebKit native keyboard PageDown and Home; Chromium pointer wheel and keyboard Home.",
"Full-page screenshots omitted fixed confirmation panels. Separately inspected viewport captures and bounding boxes establish visual readability. No horizontal overflow, page errors or off-origin calls; browser/server closed in all cases."
],
"interactions": [
"Written reason required by the real Inbox Create rule form; narrow POST with synthetic CSRF and exact tenant, then link opens the collection even if Router state precedes pushState.",
"Escape/Cancel performs no DELETE and returns focus. Confirm verifies the same manual row with a fresh GET, sends one exact-ID DELETE and requires readback absence; unrelated rules and historic dismissals remain.",
"Finding options Create rule from this has a labelled reason field, Reopen distinction and the same Activity link without creating a second rule.",
"Lost DELETE response never retries or reports success; explicit reload reconciles. 403 fresh read stops before DELETE; org switch invalidates confirmation and old list with no cross-tenant action.",
"Measured final confirmation boxes within desktop and narrow viewports; finished finite entrance animations for viewport inspection. Long-reason scroll leaves the footer fixed, and keyboard Home restores the initial scope.",
"Playwright 1.56.1 matches the integration lock; Chromium 141.0.7390.37 and WebKit 26.0. Mobile WebKit wheel is unsupported; its failed aggregate remains evidence, with completed cases retained and the outstanding scroll check completed using supported keyboard input, without replaying their mutations."
]
}