mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-10 02:25:56 +00:00
Merge candidate 20260907T152524Z-web-product
Change-source: pulse-maintainer
This commit is contained in:
@@ -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."}
|
||||
@@ -0,0 +1 @@
|
||||
5cd23421409415dd8b25fa933bef261c8e3a11141ea225e0942e63fb9dcc6fa9 scripts/check-recovery-feedback.mjs
|
||||
@@ -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.
|
||||
|
||||
@@ -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 <OverviewTab overrides={[]} activeAlerts={{}} update
|
||||
showQuickTip={()=>false} dismissQuickTip={noop} showAcknowledged={()=>true}
|
||||
setShowAcknowledged={noop} alertsDisabled={()=>false}/>;}
|
||||
function Destinations() {
|
||||
const [pingUrl, setPingUrl] = createSignal('');
|
||||
window.unsaved = false;
|
||||
window.editedPingUrl = pingUrl;
|
||||
return <DestinationsTab
|
||||
emailConfig={()=>({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 <AlertsConfigurationSurface activeTab={()=>'destinations'}
|
||||
allResources={()=>[]} byType={()=>[]} children={()=>[]} activeAlerts={{}}
|
||||
removeAlerts={noop} setOverviewOverrides={noop}
|
||||
hasUnsavedChanges={unsaved} setHasUnsavedChanges={setUnsaved}
|
||||
alertsActivationState={()=>'active'} alertsActivationConfig={()=>({enabled:true})}/>;}
|
||||
|
||||
function Fixture() {return <main class="p-4"><ToastContainer/>{location.pathname.endsWith('destinations')?<Destinations/>:<Overview/>}</main>;}
|
||||
render(()=><Router><Route path="/qualification/*" component={Fixture}/></Router>,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 {
|
||||
|
||||
Reference in New Issue
Block a user