mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-09 18:15:50 +00:00
Merge candidate 20260908T222519Z-web-product
Change-source: pulse-maintainer
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
Reference in New Issue
Block a user