Public: anyone can read this; the crew can change it.
test/snapshot-destination.test.ts passes: a failed test-connection is one sentence and the backups page shows a snapshot in progress without a refresh #11065
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/11050-test-snapshot-destination-test-ts-passes → main 2 commits fork 1 thread · 1 unresolved
3 files
changed +13
−4
src/snapshot/destination.ts +2 −1
| @@ -98,7 +98,8 @@ export function scrubReason(target: S3Target | undefined, error: unknown): strin | ||
| 98 | 98 | let reason = error instanceof Error ? error.message : String(error); |
| 99 | 99 | for (const secret of target ? [target.accessKeyId, target.secretAccessKey] : []) |
| 100 | 100 | if (secret) reason = reason.split(secret).join("…"); |
| 101 | return reason.slice(0, 200); | |
| 101 | // One line: a store's message may end in a newline or carry an empty name between spaces. | |
| 102 | return reason.replace(/\s+/g, " ").trim().slice(0, 200); | |
| 102 | 103 | } |
| 103 | 104 | |
| 104 | 105 | /** Does this target answer? One sentence either way, never a key in it. */ |
src/web/pages/backups.ts +1 −1
| @@ -346,7 +346,7 @@ async function movingPart( | ||
| 346 | 346 | ${when( |
| 347 | 347 | svc.progress(), |
| 348 | 348 | (p) => |
| 349 | html`<p class="break"> | |
| 349 | html`<p class="break" role="status" aria-live="polite"> | |
| 350 | 350 | ${badge("Running", { icon: "clock", variant: "secondary" })} A snapshot is being taken: ${p.objects} objects, |
| 351 | 351 | ${fileSize(p.bytes)} so far, ${Math.round(p.elapsedMs / 1000)} s |
| 352 | 352 | in. |
test/snapshot-destination.test.ts +10 −2
| @@ -133,6 +133,8 @@ describe("the snapshot destination", () => { | ||
| 133 | 133 | const sentence = said.ok ? said.value : ""; |
| 134 | 134 | expect(sentence).toStartWith("Could not list 127.0.0.1:1/my-bucket: "); |
| 135 | 135 | expect(sentence).not.toContain("\n"); |
| 136 | expect(sentence).not.toContain(" "); | |
| 137 | expect(sentence).toBe(sentence.trim()); | |
| 136 | 138 | expect(sentence).not.toContain(input.secretKey); |
| 137 | 139 | expect(sentence).not.toContain(input.accessKey); |
| 138 | 140 | }); |
| @@ -166,12 +168,18 @@ describe("the snapshot destination", () => { | ||
| 166 | 168 | expect(await page()).not.toContain("http-equiv=\"refresh\""); |
| 167 | 169 | const taking = svc.now(); |
| 168 | 170 | let html = ""; |
| 169 | for (let i = 0; i < 100 && !html.includes("A snapshot is being taken"); i++) { | |
| 171 | // Until an object has landed: the first poll can come before any has. | |
| 172 | for (let i = 0; i < 100 && !/A snapshot is being taken: [1-9]\d* objects/.test(html); i++) { | |
| 170 | 173 | await Bun.sleep(50); |
| 171 | 174 | html = await page(); |
| 172 | 175 | } |
| 173 | 176 | expect(html).toContain("A snapshot is being taken"); |
| 174 | expect(html).toContain("http-equiv=\"refresh\""); | |
| 177 | const words = html.indexOf("A snapshot is being taken"); | |
| 178 | const paragraph = html.slice(html.lastIndexOf("<p", words), words); | |
| 179 | expect(paragraph).toContain('role="status"'); | |
| 180 | expect(paragraph).toContain('aria-live="polite"'); | |
| 181 | expect(html).toContain('data-live-src="/settings/backups/live"'); | |
| 182 | expect(html).not.toContain("http-equiv=\"refresh\""); | |
| 175 | 183 | expect(html).toMatch(/[1-9]\d* objects/); |
| 176 | 184 | await taking; |
| 177 | 185 | expect(await page()).not.toContain("A snapshot is being taken"); |
Next step, not blocking: render the role=status wrapper always and put the sentence inside when running, so screen readers announce the change.