Verify composed webhook instructions in both themes

Preserve the exact reviewed permission-help commit and the composed light-mode heading plus multiline service instructions. Record a fresh parent-bound browser receipt after inspecting eight Discord and Telegram captures across Chromium desktop and WebKit phone light/dark states. The fixture uses production form/state/CSS with source-exact public templates and no notification operation; earlier harness failures remain retained separately.

Contract-Neutral: Browser verification and offline fixture only; no runtime, schema, permission, notification, navigation or dependency contract change.
Change-source: pulse-maintainer
This commit is contained in:
pulse-triage[bot]
2026-10-05 05:11:04 +01:00
parent 5e967c49f5
commit 899414d8ca
4 changed files with 432 additions and 0 deletions
@@ -0,0 +1,350 @@
// Fresh proof of the composed heading and multiline instructions, not a replay
// of an old browser operation or installed notification-delivery acceptance.
const assert = require('node:assert/strict');
const crypto = require('node:crypto');
const fs = require('node:fs');
const path = require('node:path');
const { chromium, webkit } = require('playwright');
const root = '/workspace/frontend-modern';
const output = '/workspace/tmp/webhook-composed-proof-stable';
const hash = (file) => crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex');
const runtimePath = 'frontend-modern/src/components/Alerts/WebhookConfigForm.tsx';
const expectedHash = '0286a39d8405d214df78d8806f1792991a46e0583ab20d1b1fafb276b3c9160d';
const integration = '5e967c49f55d0fc2423e9d7ba63c7ed250069a87';
const sourceFiles = [
runtimePath,
'frontend-modern/src/components/Alerts/WebhookConfig.tsx',
'frontend-modern/src/components/Alerts/useWebhookConfigState.ts',
'frontend-modern/src/utils/alertWebhookPresentation.ts',
'frontend-modern/src/index.css',
'frontend-modern/package-lock.json',
'tests/integration/package-lock.json',
'internal/notifications/webhook_templates.go',
'frontend-modern/browser-tests/webhook-composed.html',
'frontend-modern/browser-tests/webhook-composed.tsx',
'frontend-modern/browser-tests/webhook-composed.cjs',
];
// Read the committed server catalogue, not a second copy of its instructions.
// This bounded extractor is only for the selected public fixture fields, not
// a Go evaluator or a substitute for a backend response/receiver test.
const templateSource = fs.readFileSync(
'/workspace/internal/notifications/webhook_templates.go',
'utf8',
);
const starts = [...templateSource.matchAll(/^\s*Service:\s*"([^"]+)"/gm)];
const templates = ['generic', 'discord', 'telegram'].map((service) => {
const i = starts.findIndex((match) => match[1] === service);
assert.ok(i >= 0, `catalogue service ${service}`);
const block = templateSource.slice(
starts[i].index,
starts[i + 1]?.index ?? templateSource.length,
);
const value = (field, optional = false) => {
const match = block.match(new RegExp(`^\\s*${field}:\\s*("(?:[^"\\\\]|\\\\.)*")`, 'm'));
assert.ok(optional || match, `catalogue field ${service}.${field}`);
return match ? JSON.parse(match[1]) : '';
};
const payload = block.match(/^\s*PayloadTemplate:\s*`([\s\S]*?)`,/m);
assert.ok(payload, `catalogue payload ${service}`);
assert.match(block, /Headers:\s*map\[string\]string\{"Content-Type": "application\/json"\}/);
return {
service,
label: value('Label'),
name: value('Name'),
description: value('Description'),
urlPattern: value('URLPattern'),
method: value('Method'),
headers: { 'Content-Type': 'application/json' },
mentionPlaceholder: value('MentionPlaceholder', true),
mentionHelp: value('MentionHelp', true),
payloadTemplate: payload[1],
instructions: value('Instructions'),
};
});
(async () => {
fs.mkdirSync(output, { recursive: true });
const playwrightVersion = require('playwright/package.json').version;
const declared = JSON.parse(fs.readFileSync('/workspace/tests/integration/package-lock.json'))
.packages['node_modules/@playwright/test'].version;
assert.equal(playwrightVersion, declared);
assert.equal(hash(path.join('/workspace', runtimePath)), expectedHash);
const files = Object.fromEntries(
sourceFiles.map((file) => [file, hash(path.join('/workspace', file))]),
);
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: path.join(output, 'vite-cache'),
optimizeDeps: { noDiscovery: true, include: [] },
server: { host: '127.0.0.1', port: 5307, strictPort: true, watch: null },
});
const origin = 'http://127.0.0.1:5307';
const results = [],
screenshots = [],
observations = [];
let browser,
phase = 'server',
completed = false;
try {
await server.listen();
for (const [name, engine, width, dark] of [
['chromium-desktop-light', chromium, 1440, false],
['chromium-desktop-dark', chromium, 1440, true],
['webkit-phone-light', webkit, 390, false],
['webkit-phone-dark', webkit, 390, true],
]) {
phase = name;
browser = await engine.launch(
engine === chromium
? { headless: true, channel: 'chromium', args: ['--no-sandbox'] }
: { headless: true },
);
const page = await browser.newPage({
viewport: { width, height: width < 500 ? 844 : 900 },
isMobile: width < 500,
hasTouch: width < 500,
locale: 'en-GB',
timezoneId: 'UTC',
});
page.setDefaultTimeout(10000);
page.setDefaultNavigationTimeout(45000);
const errors = [],
unexpectedRequests = [],
requests = [],
previewSockets = [],
checks = [];
page.on('pageerror', (error) => errors.push(error.message));
page.on('console', (message) => {
if (message.type() === 'error') errors.push(message.text());
});
await page.addInitScript((dark) => {
const apply = () => document.documentElement.classList.toggle('dark', dark);
if (document.documentElement) apply();
else document.addEventListener('DOMContentLoaded', apply, { once: true });
}, dark);
// Vite's own HMR client uses this fixed loopback endpoint. Closing it
// creates a real console error even though the form renders correctly.
// Permit that preview-only socket, not application/provider sockets;
// keep the zero-error assertion rather than filtering its diagnostic.
await page.routeWebSocket(/.*/, (socket) => {
const url = new URL(socket.url());
if (url.origin === 'ws://127.0.0.1:5307' && url.pathname === '/') {
previewSockets.push(url.pathname);
socket.connectToServer();
} else {
unexpectedRequests.push({ origin: url.origin, path: url.pathname, method: 'WebSocket' });
socket.close();
}
});
await page.route('**/*', async (route) => {
const request = route.request(),
url = new URL(request.url());
if (url.origin !== origin || request.method() !== 'GET') {
unexpectedRequests.push({ url: request.url(), method: request.method() });
return route.abort();
}
if (!url.pathname.startsWith('/api/')) return route.continue();
requests.push(url.pathname);
if (url.pathname === '/api/notifications/webhook-templates')
return route.fulfill({ json: templates });
unexpectedRequests.push({ url: request.url(), method: request.method() });
return route.abort();
});
await page.goto(`${origin}/browser-tests/webhook-composed.html`, {
waitUntil: 'networkidle',
});
await page.getByRole('button', { name: '+ Add Webhook', exact: true }).click();
await page
.getByRole('button', { name: 'Discord Discord server webhook', exact: true })
.waitFor();
for (const service of ['discord', 'telegram']) {
phase = `${name}: ${service}`;
const template = templates.find((candidate) => candidate.service === service);
if (service === 'telegram') {
const toggle = page.getByRole('button', { name: 'Discord →', exact: true });
await toggle.focus();
await page.keyboard.press('Enter');
}
const choice = page.getByRole('button', {
name: `${template.label} ${template.description}`,
exact: true,
});
if (width < 500) await choice.tap();
else {
await choice.focus();
await page.keyboard.press('Enter');
}
const heading = page.getByRole('heading', { name: 'Setup Instructions', exact: true });
await heading.waitFor();
const panel = heading.locator('..'),
instructions = panel.locator('p');
assert.equal(await instructions.textContent(), template.instructions);
assert.equal(
await page.getByLabel('Webhook URL', { exact: true }).getAttribute('placeholder'),
template.urlPattern,
);
await heading.evaluate((element) => element.scrollIntoView({ block: 'start' }));
await page.evaluate(async () => {
await document.fonts.ready;
await new Promise((resolve) =>
requestAnimationFrame(() => requestAnimationFrame(resolve)),
);
});
const layout = await panel.evaluate((element) => {
const heading = element.querySelector('h4'),
p = element.querySelector('p');
const style = getComputedStyle(heading),
pStyle = getComputedStyle(p);
const walker = document.createTreeWalker(p, NodeFilter.SHOW_TEXT);
const node = walker.nextNode();
if (!node || node.textContent !== p.textContent)
throw new Error('Instructions must remain one complete text node');
let offset = 0;
const linePositions = p.textContent.split('\n').map((line) => {
const range = document.createRange();
range.setStart(node, offset);
range.setEnd(node, offset + 2);
offset += line.length + 1;
const bounds = range.getBoundingClientRect();
const fragments = [...range.getClientRects()].map((rect) => ({
x: rect.x,
y: rect.y,
width: rect.width,
height: rect.height,
}));
// A union bound can include a zero-width newline on the preceding
// line. Measure the numbered glyphs, not that whitespace fragment.
const glyphs = fragments.filter((rect) => rect.width > 0 && rect.height > 0);
if (glyphs.length !== 1)
throw new Error('Each numbered prefix needs one painted rectangle');
return {
number: line.slice(0, 2),
x: glyphs[0].x,
y: glyphs[0].y,
bounds: { x: bounds.x, y: bounds.y, width: bounds.width, height: bounds.height },
fragments,
};
});
const canvas = document.createElement('canvas');
canvas.width = canvas.height = 1;
const context = canvas.getContext('2d');
context.fillStyle = style.color;
context.fillRect(0, 0, 1, 1);
const ancestorOpacity = [];
for (let current = heading; current; current = current.parentElement)
ancestorOpacity.push(getComputedStyle(current).opacity);
return {
headingClass: heading.className,
color: style.color,
alpha: context.getImageData(0, 0, 1, 1).data[3],
ancestorOpacity,
whiteSpace: pStyle.whiteSpace,
overflowWrap: pStyle.overflowWrap,
linePositions,
panelOverflow: element.scrollWidth - element.clientWidth,
documentOverflow: document.documentElement.scrollWidth - innerWidth,
};
});
observations.push({ name, service, layout });
const file = `${name}-${service}.png`;
await page.screenshot({ path: path.join(output, file), fullPage: true });
screenshots.push({ file, sha256: hash(path.join(output, file)), width, dark, service });
assert.ok(layout.headingClass.split(' ').includes('text-blue-900'));
assert.ok(layout.headingClass.split(' ').includes('dark:text-blue-100'));
assert.equal(layout.alpha, 255, 'heading colour is not translucent');
assert.ok(layout.ancestorOpacity.every((opacity) => opacity === '1'));
assert.equal(layout.whiteSpace, 'pre-line');
assert.equal(layout.overflowWrap, 'break-word');
assert.equal(layout.linePositions.length, service === 'discord' ? 4 : 5);
layout.linePositions.forEach((line, i, lines) => {
assert.equal(line.number, `${i + 1}.`);
if (i)
assert.ok(line.y > lines[i - 1].y + 5, 'numbered steps need separate rendered lines');
assert.ok(
Math.abs(line.x - lines[0].x) <= 1,
'each numbered step starts at the left edge',
);
});
assert.ok(layout.panelOverflow <= 1);
assert.ok(layout.documentOverflow <= 1);
assert.equal(await page.getByLabel('Notification operations').textContent(), '0');
checks.push({
service,
templateInstructionsExact: true,
layout,
noNotificationOperation: true,
});
}
await page.getByRole('button', { name: 'Cancel', exact: true }).click();
await page.getByRole('button', { name: '+ Add Webhook', exact: true }).waitFor();
assert.equal(
await page.getByRole('heading', { name: 'Setup Instructions', exact: true }).count(),
0,
);
assert.equal(await page.getByLabel('Notification operations').textContent(), '0');
assert.deepEqual(errors, []);
assert.deepEqual(unexpectedRequests, []);
results.push({
name,
browserVersion: browser.version(),
width,
dark,
phoneEmulated: width < 500,
checks,
cancelReturnsToAdd: true,
requests,
previewSockets,
errors,
unexpectedRequests,
});
await browser.close();
browser = undefined;
}
completed = true;
} catch (error) {
fs.writeFileSync(
path.join(output, 'failure.json'),
JSON.stringify(
{ phase, error: error.message, files, results, screenshots, observations },
null,
2,
),
);
throw error;
} finally {
if (browser) await browser.close();
await server.close();
}
if (completed) {
fs.writeFileSync(
path.join(output, 'result.json'),
JSON.stringify(
{
integration,
playwrightVersion,
declaredPlaywrightVersion: declared,
files,
results,
screenshots,
observations,
verifiedAt: new Date().toISOString(),
browserAndServerClosed: true,
limits:
'Production WebhookConfig/form/state/CSS in a standalone offline fixture. Source-exact public template fields, synthetic API. No full app navigation, real credentials, receiver, Test/save/delivery, native phone or installed acceptance.',
},
null,
2,
),
);
console.log(
`Verified ${results.length} theme/viewport states and ${screenshots.length} source-exact service captures; browser and server closed.`,
);
}
})().catch((error) => {
console.error(error);
process.exitCode = 1;
});
@@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Composed webhook instructions verification</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="./webhook-composed.tsx"></script>
</body>
</html>
@@ -0,0 +1,27 @@
// Production form and state, source-exact template text supplied by the runner.
// No real notification receiver, credentials, save or Test operation.
import { createSignal } from 'solid-js';
import { render } from 'solid-js/web';
import { WebhookConfig } from '../src/components/Alerts/WebhookConfig';
import '../src/index.css';
function Fixture() {
const [operations, setOperations] = createSignal(0);
const unexpectedOperation = () => setOperations((count) => count + 1);
return (
<main class="mx-auto min-h-screen max-w-4xl space-y-4 bg-surface p-4 text-base-content">
<h1 class="text-lg font-semibold">Webhook setup</h1>
<p class="text-sm text-muted">Offline presentation check. No notification is sent.</p>
<WebhookConfig
webhooks={[]}
onAdd={unexpectedOperation}
onUpdate={unexpectedOperation}
onDelete={unexpectedOperation}
onTest={unexpectedOperation}
/>
<output aria-label="Notification operations">{operations()}</output>
</main>
);
}
render(() => <Fixture />, document.getElementById('root')!);
@@ -0,0 +1,43 @@
{
"version": 1,
"base_sha": "5e967c49f55d0fc2423e9d7ba63c7ed250069a87",
"verified_at": "2026-10-05T04:08:20.487Z",
"result": "passed",
"changed_paths": [
"frontend-modern/src/components/Alerts/WebhookConfigForm.tsx"
],
"content_sha256": {
"frontend-modern/src/components/Alerts/WebhookConfigForm.tsx": "0286a39d8405d214df78d8806f1792991a46e0583ab20d1b1fafb276b3c9160d"
},
"routes": [
"/browser-tests/webhook-composed.html (production Notifications WebhookConfig/form/state/CSS; standalone offline fixture)"
],
"viewports": [
{
"width": 1440,
"height": 900
},
{
"width": 390,
"height": 844
}
],
"states": [
"Chromium 141.0.7390.37 desktop light and dark; WebKit 26.0 emulated phone light and dark, Playwright 1.56.1 matching integration lock",
"Discord four numbered instructions and Telegram five numbered instructions exactly match the committed server catalogue in each theme/layout",
"Setup Instructions heading is solid blue-900 in light mode and blue-100 in dark mode, alpha 255 and no ancestor opacity; both improvements are composed in the verified hash",
"Each numbered prefix has a distinct painted text rectangle and left edge; pre-line whitespace and break-word wrapping preserve long Telegram URLs without panel/document horizontal overflow",
"All eight full-page captures inspected: headings, step separation, long URLs and phone form layout readable; no page/console errors or unexpected API/provider requests"
],
"interactions": [
"Open Add Webhook and select Discord using keyboard on desktop or tap on emulated phone",
"Open the service selector using keyboard Enter and switch to Telegram; source-exact instructions and URL placeholder update, existing typed/default name is preserved",
"Cancel returns to Add Webhook and removes instructions; zero add/update/delete/Test callbacks or non-GET API requests throughout"
],
"proof": {
"script": "frontend-modern/browser-tests/webhook-composed.cjs",
"script_sha256": "99f74d9ef98188510a68d47cd9508ab7001673523c83d9f3ea54c542fc94313e",
"template_source_sha256": "07b04d7ccf37e15dac8fa333c487122d013f6a364cd8dc5ce5a7ecbbb20b5243",
"limits": "Production WebhookConfig/form/state/CSS in a standalone offline fixture. Source-exact public template fields, synthetic API. No full app navigation, real credentials, receiver, Test/save/delivery, native phone or installed acceptance."
}
}