From fc3ebefd8668100f368a6c8e6d8095084b090ae6 Mon Sep 17 00:00:00 2001 From: "pulse-triage[bot]" <249995291+pulse-triage[bot]@users.noreply.github.com> Date: Mon, 7 Sep 2026 16:39:11 +0100 Subject: [PATCH] test(web): verify recovery preserves real settings-parent edits The previous recovery browser fixture owned the edited URL in a synthetic parent, leaving the production configuration-state integration untested. Mount the real Alerts settings surface and assert edits and dirty state survive recovery without reload or implicit save, then reach the explicit save boundary intact. Two serialized Chromium runs passed 12 cases each, including six real settings-parent cases. Record the exact script hash and final result; retain earlier harness failures and distinguish mocked API evidence from persistence, installed recovery and recipient receipt. Change-source: pulse-maintainer --- .../settings-parent-result.json | 1 + .../settings-parent-source.sha256 | 1 + .../unsaved-edit-retention.md | 59 +++++++++++---- scripts/check-recovery-feedback.mjs | 75 +++++++++++++++---- 4 files changed, 107 insertions(+), 29 deletions(-) create mode 100644 docs/qualification/recovery-feedback/settings-parent-result.json create mode 100644 docs/qualification/recovery-feedback/settings-parent-source.sha256 diff --git a/docs/qualification/recovery-feedback/settings-parent-result.json b/docs/qualification/recovery-feedback/settings-parent-result.json new file mode 100644 index 000000000..c011bd0ab --- /dev/null +++ b/docs/qualification/recovery-feedback/settings-parent-result.json @@ -0,0 +1 @@ +{"result":"passed","cases":12,"unsavedEditCases":6,"viewports":[1440,900,390],"scope":"Real OverviewTab and AlertsConfigurationSurface with configuration/destination state, shared toast and feedback in Chromium; scripted API only, not installed delivery or recipient receipt."} diff --git a/docs/qualification/recovery-feedback/settings-parent-source.sha256 b/docs/qualification/recovery-feedback/settings-parent-source.sha256 new file mode 100644 index 000000000..51548cbfa --- /dev/null +++ b/docs/qualification/recovery-feedback/settings-parent-source.sha256 @@ -0,0 +1 @@ +5cd23421409415dd8b25fa933bef261c8e3a11141ea225e0942e63fb9dcc6fa9 scripts/check-recovery-feedback.mjs diff --git a/docs/qualification/recovery-feedback/unsaved-edit-retention.md b/docs/qualification/recovery-feedback/unsaved-edit-retention.md index 91523592e..b1e0a6ce5 100644 --- a/docs/qualification/recovery-feedback/unsaved-edit-retention.md +++ b/docs/qualification/recovery-feedback/unsaved-edit-retention.md @@ -1,25 +1,54 @@ # Unsaved edit retention — 7 September 2026 -`scripts/check-recovery-feedback.mjs` now gives the Destinations fixture a -reactive ping URL and an observable unsaved flag instead of no-op setters. -Each Destinations case enters a synthetic `example.invalid` URL and checks -both the rendered input and parent signal, plus the unsaved flag, after: +## Settings-parent qualification + +`scripts/check-recovery-feedback.mjs` now mounts the real +`AlertsConfigurationSurface` for Destinations. Its +`useAlertsConfigurationState`, configuration snapshot, overrides and +destination hooks are unmocked. Only the API boundary and external resource / +activation inputs are scripted; the enclosing fixture supplies the dirty +signal as the application shell would. + +The test waits for initial configuration loading, enters a synthetic +`example.invalid` ping URL, and checks the rendered value, dirty signal and +real unsaved-changes banner survive: - rejected retry and toast expiry; - cancelled dismissal; - accepted retry followed by unavailable health refresh; - subsequent recovery, failed dismissal, healthy-card removal and message clearing. -Validation: `pulse-heavy-run -- node scripts/check-recovery-feedback.mjs` passed -12 Chromium cases: Overview and Destinations at 1440, 900 and 390 pixels in -light and dark themes. Six cases include the unsaved edit assertions. +At each checkpoint, configuration was read only once and neither the global +configuration nor ping URL was saved. Finally, clicking the real Save Changes +button sends the exact edited URL through the real destination save path to +the scripted API, writes global configuration once and clears dirty state. +This verifies parent-owned retention rather than just an isolated input. -This is component qualification with scripted APIs. It does not establish -full settings-parent integration, saved configuration, installed recovery, -recipient receipt or assistive-technology announcements. Existing screenshots -are not refreshed by this evidence record; no new visual acceptance is claimed. +Validation: two serialized Chromium runs passed all 12 cases (Overview and +Destinations at 1440, 900 and 390 pixels in light/dark), including six +settings-parent edit/save cases. The final formatted-script result is +`settings-parent-result.json`; `settings-parent-source.sha256` identifies the +script. `node --check` and `git diff --check` passed. -Independent rationale: W3C's [redundant-entry guidance](https://www.w3.org/WAI/WCAG22/Understanding/redundant-entry.html), -retrieved through search on 7 September (direct retrieval returned HTTP 429), -describes the burden and error risk of re-entering information. This supports -protecting input retention, not a claim of WCAG conformance or new product demand. +Earlier attempts are not passes: the first could not start because this +isolated workspace lacked Vite dependencies; lockfile installs resolved that. +The next timed out because the test looked for an input of type URL, whereas +the existing field is masked. Selecting its existing ID prefix resolved the +fixture error. No runtime code was changed. + +## Boundaries + +The previous fixture used a synthetic reactive parent directly around +DestinationsTab. This replaces that limitation with the real settings surface +and state, but does not mount the entire Alerts application shell or exercise +navigation, organisation switching, concurrent configuration loads, backend +persistence, installed recovery, history retention, recipient receipt or +assistive-technology announcements. No release qualification is claimed. +Historical screenshots and older receipts are preserved, not refreshed or +presented as visual acceptance for this test-only change. + +Independent rationale: W3C's +[redundant-entry guidance](https://www.w3.org/WAI/WCAG22/Understanding/redundant-entry.html), +successfully retrieved directly on 7 September, explains the effort and error +risk of requiring people to recall and re-enter information. This supports +input-retention testing, not new product demand or a WCAG conformance claim. diff --git a/scripts/check-recovery-feedback.mjs b/scripts/check-recovery-feedback.mjs index d9b5f5817..b91179339 100644 --- a/scripts/check-recovery-feedback.mjs +++ b/scripts/check-recovery-feedback.mjs @@ -14,11 +14,20 @@ import { Router, Route } from '@solidjs/router'; import { AlertsAPI } from '/src/api/alerts'; import { NotificationsAPI } from '/src/api/notifications'; import { OverviewTab } from '/src/features/alerts/OverviewTab'; -import { DestinationsTab } from '/src/features/alerts/tabs/DestinationsTab'; +import { AlertsConfigurationSurface } from '/src/features/alerts/AlertsConfigurationSurface'; import { ToastContainer } from '/src/components/Toast/Toast'; import '/src/index.css'; window.health = 'degraded'; window.action = 'reject'; window.mutations = 0; +window.configReads = 0; window.configWrites = []; window.savedPingUrls = []; +AlertsAPI.getConfig = async () => {window.configReads++; return {overrides:{}};}; +AlertsAPI.getDeadManConfig = async () => ({pingUrl:'https://example.invalid/original'}); +NotificationsAPI.getEmailConfig = async () => ({enabled:false,to:[]}); +NotificationsAPI.getAppriseConfig = async () => ({enabled:false,targets:[]}); +AlertsAPI.updateConfig = async (value) => {window.configWrites.push(value); return {success:true};}; +NotificationsAPI.updateEmailConfig = async () => ({}); +NotificationsAPI.updateAppriseConfig = async (value) => value; +AlertsAPI.updateDeadManConfig = async (value) => {window.savedPingUrls.push(value); return {};}; NotificationsAPI.getHealth = async () => { if(window.health === 'error') throw new Error('scripted offline'); return {queue:{status:window.health, attentionRequired:window.health==='healthy'?0:2, failed:2,deadLetter:0}}; @@ -37,15 +46,14 @@ function Overview() {return false} dismissQuickTip={noop} showAcknowledged={()=>true} setShowAcknowledged={noop} alertsDisabled={()=>false}/>;} function Destinations() { -const [pingUrl, setPingUrl] = createSignal(''); -window.unsaved = false; -window.editedPingUrl = pingUrl; -return ({enabled:false,provider:'smtp',from:'',server:'',username:'',password:'',port:587,to:[],tls:true,startTLS:true,replyTo:'',maxRetries:3,retryDelay:5,rateLimit:60})} setEmailConfig={noop} -appriseConfig={()=>({enabled:false,mode:'cli',targetsText:'',configKey:'',serverUrl:'',timeoutSeconds:30,apiKey:'',apiKeyHeader:'X-API-KEY',hasApiKey:false,skipTlsVerify:false,cliPath:'apprise'})} setAppriseConfig={noop} -configLoadError={()=>null} isRetrying={()=>false} isLoadingDestinations={()=>false} onRetryLoad={noop} -webhooks={()=>[]} setHasUnsavedChanges={(value)=>{window.unsaved=value;}} deadManPingUrl={pingUrl} setDeadManPingUrl={setPingUrl} -pushMinimumSeverity={()=>'all'} setPushMinimumSeverity={noop}/>;} +const [unsaved, setUnsaved] = createSignal(false); +window.unsaved = unsaved; +return 'destinations'} +allResources={()=>[]} byType={()=>[]} children={()=>[]} activeAlerts={{}} +removeAlerts={noop} setOverviewOverrides={noop} +hasUnsavedChanges={unsaved} setHasUnsavedChanges={setUnsaved} +alertsActivationState={()=>'active'} alertsActivationConfig={()=>({enabled:true})}/>;} + function Fixture() {return
{location.pathname.endsWith('destinations')?:}
;} render(()=>,document.getElementById('root')); `; @@ -141,7 +149,7 @@ try { }); throw error; }); - // A synthetic unsaved value, never submitted to a backend. + // Synthetic value owned by the real settings parent; APIs remain scripted. const editedUrl = "https://example.invalid/unsaved-recovery-check"; const pingInput = page.getByLabel( "Healthchecks-compatible success ping URL", @@ -150,10 +158,25 @@ try { const assertEditRetained = async () => { if (surface !== "destinations") return; assert.equal(await pingInput.inputValue(), editedUrl); - assert.equal(await page.evaluate(() => window.editedPingUrl()), editedUrl); - assert.equal(await page.evaluate(() => window.unsaved), true); + assert.equal(await page.evaluate(() => window.unsaved()), true); + assert.equal( + await page + .getByText("You have unsaved changes", { exact: true }) + .count(), + 1, + ); + assert.equal(await page.evaluate(() => window.configReads), 1); + assert.deepEqual(await page.evaluate(() => window.configWrites), []); + assert.deepEqual(await page.evaluate(() => window.savedPingUrls), []); }; if (surface === "destinations") { + // Wait for the real configuration/destination load before editing. + await page.waitForFunction( + () => + document.querySelector('input[id^="alert-deadman-url-"]') + ?.value === "https://example.invalid/original", + ); + assert.equal(await page.evaluate(() => window.unsaved()), false); await pingInput.fill(editedUrl); await assertEditRetained(); } @@ -268,6 +291,30 @@ try { true, ); await assertEditRetained(); + if (surface === "destinations") { + // Prove the retained value reaches the real parent's save path, only + // after an explicit Save. This is not backend persistence evidence. + await page + .getByRole("button", { name: "Save Changes", exact: true }) + .click(); + await page.waitForFunction( + () => window.savedPingUrls.length === 1 && !window.unsaved(), + ); + assert.deepEqual(await page.evaluate(() => window.savedPingUrls), [ + editedUrl, + ]); + assert.equal( + await page.evaluate(() => window.configWrites.length), + 1, + ); + assert.equal(await pingInput.inputValue(), editedUrl); + assert.equal( + await page + .getByText("You have unsaved changes", { exact: true }) + .count(), + 0, + ); + } assert.deepEqual(errors, []); await page.close(); cases++; @@ -280,7 +327,7 @@ try { unsavedEditCases, viewports: [1440, 900, 390], scope: - "Real OverviewTab and DestinationsTab, shared toast and feedback in Chromium; scripted API only, not installed delivery or recipient receipt.", + "Real OverviewTab and AlertsConfigurationSurface with configuration/destination state, shared toast and feedback in Chromium; scripted API only, not installed delivery or recipient receipt.", }), ); } finally {