Merge candidate 20260908T222519Z-web-product

Change-source: pulse-maintainer
This commit is contained in:
pulse-triage[bot]
2026-09-08 23:48:43 +01:00
5 changed files with 136 additions and 14 deletions
@@ -2828,3 +2828,14 @@ The presentation unit tests cover the label and terminal guidance.
cards in Chromium at desktop, tablet and phone widths, asserting the copy,
absence of page errors and horizontal overflow. Scripted props are component
evidence only, not installed notification delivery.
### Webhook test/save custom-field parity
The webhook form applies the same service-specific custom-field normalisation
when testing unsaved data as when saving it. In particular, manually entered
Pushover app_token/user_token aliases become token/user with the existing
canonical-value precedence. Existing-field editing remains normalised on load.
This changes neither delivery scheduling nor the meaning of a successful test.
The registered WebhookConfig regression covers alias test/save payload equality;
the browser fixture exercises the real form at desktop and phone widths with
synthetic callbacks, not a hosted Pushover destination or installed delivery.
+11 -13
View File
@@ -1,16 +1,16 @@
{
"version": 1,
"base_sha": "d242055f9877a47d31b08133e3e8588cbff65f40",
"verified_at": "2026-09-08T20:59:34.058403Z",
"base_sha": "6f3547bc5fbb10ddf313a8cd2f184b6359173a59",
"verified_at": "2026-09-08T22:29:31.643744Z",
"result": "passed",
"changed_paths": [
"frontend-modern/src/utils/proxmoxUpdateEvidence.ts"
"frontend-modern/src/components/Alerts/useWebhookConfigState.ts"
],
"content_sha256": {
"frontend-modern/src/utils/proxmoxUpdateEvidence.ts": "b99d8b5411e0e894f294f106d970978c8573915ff7ffe0ed60c451128e22d34a"
"frontend-modern/src/components/Alerts/useWebhookConfigState.ts": "14f0dceb069fe52b9a47100c50998ae53a2c5c8a29a524e342f7458546cb4a19"
},
"routes": [
"/qualification.html (production-built real NodeDrawerOverview with synthetic reactive props)"
"/qualification (real WebhookConfigForm and hook; synthetic callbacks)"
],
"viewports": [
{
@@ -27,15 +27,13 @@
}
],
"states": [
"Unavailable permission denial",
"Retained stale count with permission-denial title",
"Checked zero",
"Checked positive count",
"Not-checked permission-denial reason"
"Unsaved manually entered Pushover aliases",
"Test callback payload",
"Save callback payload"
],
"interactions": [
"At each width select unavailable, stale, zero, positive, not_checked, then unavailable again; assert text, stale title, no obsolete Sys.Audit prescription, no page errors or horizontal page overflow."
"Click Test then Add Webhook at each width; assert both canonical token/user payloads, no page errors or horizontal overflow."
],
"notes": "Built isolated real component with Vite esnext target, served compiled assets to Chromium. Desktop stale and phone unavailable screenshots visually inspected. Phone uses existing ellipsis for long values; full reason retained in title. No installed backend or reporter credential reproduction. Initial fixture build attempts failed on default target and virtual HTML path; corrected fixture build and all checks passed. Screenshots /tmp/pulse-update-access-copy; log /tmp/web-205503/browser-built.log.",
"command": "pulse-heavy-run -- node scripts/check-update-access-copy.mjs"
"notes": "Real component with Vite dev transform in Chromium, not production build or installed/provider delivery. Phone screenshot visually inspected; synthetic credentials only. Screenshots /tmp/pulse-webhook-parity.",
"command": "pulse-heavy-run -- node scripts/check-webhook-test-save-parity.mjs"
}
@@ -1,3 +1,5 @@
import { createRoot } from 'solid-js';
import { useWebhookConfigState } from './useWebhookConfigState';
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { cleanup, fireEvent, render, screen, waitFor } from '@solidjs/testing-library';
import webhookConfigSource from '@/components/Alerts/WebhookConfig.tsx?raw';
@@ -1580,3 +1582,29 @@ describe('WebhookConfig', () => {
expect(payloadTextareaAfter.value).toBe('');
});
});
describe('webhook test/save parity', () => {
it('normalises manually entered Pushover aliases identically for test and save', () => {
createRoot((dispose) => {
try {
const onTest = vi.fn();
const onAdd = vi.fn();
const state = useWebhookConfigState({
webhooks: [], onTest, onAdd, onUpdate: vi.fn(), onDelete: vi.fn(),
});
state.openAddForm();
state.setFormData((data) => ({ ...data, name: 'Example', url: 'https://example.invalid', service: 'pushover' }));
for (const [index, key] of ['app_token', 'user_token'].entries()) {
state.addCustomFieldInput();
state.updateCustomFieldInput(index, { key, value: `synthetic-${index}` });
}
state.testWebhookForm();
state.saveWebhook();
expect(onAdd.mock.calls[0][0].customFields).toEqual({ token: 'synthetic-0', user: 'synthetic-1' });
expect(onTest.mock.calls[0][1].customFields).toEqual(onAdd.mock.calls[0][0].customFields);
} finally {
dispose();
}
});
});
});
@@ -284,7 +284,7 @@ export function useWebhookConfigState(props: WebhookConfigProps): WebhookConfigS
const testPayload = {
...restFormData,
headers,
customFields,
customFields: normalizeAlertWebhookCustomFields(data.service, customFields),
template: payloadTemplate ?? restFormData.template ?? '',
};
const tempId = editingId() || 'temp-new-webhook';
@@ -0,0 +1,85 @@
// Isolated real-browser component qualification; no installed backend or delivery claim.
import { createServer } from "../frontend-modern/node_modules/vite/dist/node/index.js";
import solid from "../frontend-modern/node_modules/vite-plugin-solid/dist/esm/index.mjs";
import { chromium } from "@playwright/test";
import { resolve } from "node:path";
import { mkdirSync } from "node:fs";
import assert from "node:assert/strict";
const root = resolve("frontend-modern");
process.chdir(root);
const fixture = `
import { render } from 'solid-js/web';
import { useWebhookConfigState } from '/src/components/Alerts/useWebhookConfigState';
import { WebhookConfigForm } from '/src/components/Alerts/WebhookConfigForm';
import '/src/index.css';
render(() => {
const state = useWebhookConfigState({webhooks:[], onAdd: data => window.saved = data, onUpdate:()=>{}, onDelete:()=>{}, onTest: (_id,data) => window.tested = data});
state.openAddForm();
state.setFormData(data => ({...data,name:'Synthetic destination',url:'https://example.invalid',service:'pushover'}));
for (const [index,key] of ['app_token','user_token'].entries()) {
state.addCustomFieldInput();
state.updateCustomFieldInput(index,{key,value:'synthetic-'+index});
}
return <main class="p-4"><WebhookConfigForm {...state}/></main>;
}, document.getElementById('root'));
`;
const server = await createServer({
root,
configFile: false,
optimizeDeps: {
noDiscovery: true,
entries: [],
esbuildOptions: { target: "esnext" },
},
esbuild: { target: "esnext" },
plugins: [
solid(),
{
name: "ordering-fixture",
configureServer(s) {
s.middlewares.use((req, res, next) => {
if (req.url === "/qualification") {
res.setHeader("Content-Type", "text/html");
res.end(
'<div id="root"></div><script type="module" src="/ordering-fixture.tsx"></script>',
);
} else next();
});
},
resolveId(id) {
if (id === "/ordering-fixture.tsx") return id;
},
load(id) {
if (id === "/ordering-fixture.tsx") return fixture;
},
},
],
resolve: { alias: { "@": resolve(root, "src") } },
server: { host: "127.0.0.1", port: 5197, strictPort: true },
});
let browser;
try {
await server.listen();
browser = await chromium.launch({ headless: true });
mkdirSync("/tmp/pulse-webhook-parity", { recursive: true });
for (const width of [1440, 900, 390]) {
const page = await browser.newPage({ viewport: { width, height: 900 } });
const errors = [];
page.on('pageerror', error => errors.push(error.message));
await page.goto("http://127.0.0.1:5197/qualification");
await page.getByRole('button', {name:'Test', exact:true}).click();
assert.deepEqual(await page.evaluate(() => window.tested.customFields), {token:'synthetic-0',user:'synthetic-1'});
await page.screenshot({path: `/tmp/pulse-webhook-parity/${width}.png`, fullPage:true});
await page.getByRole('button', {name:'Add Webhook', exact:true}).click();
assert.deepEqual(await page.evaluate(() => window.saved.customFields), {token:'synthetic-0',user:'synthetic-1'});
assert.equal(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth), true);
assert.deepEqual(errors, []);
await page.close();
}
console.log(JSON.stringify({result:'passed',viewports:[1440,900,390],scope:'Real webhook form, Chromium, scripted props; no installed delivery claim'}));
} finally {
await browser?.close();
await server.close();
}