From d4ec9c6eee182c459a5baa9dc6434f18049b806b Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Fri, 28 Aug 2026 15:51:57 +1000 Subject: [PATCH] fix(desktop): let the clear-all-data spinner be optional at both ends `Delete account from swarm` clicks Clear and then waits on the spinner, but the work it covers restarts the app: on devnet the delete can finish before we look, and when we do catch the spinner it leaves with the window, which Playwright reports as a target-closed error rather than a hidden element. Neither is a failure of the thing under test. The appearance wait was already tolerant, so this adds the other end: `windowMayClose` treats a closed window as the loader being gone. Opt-in, since everywhere else a window disappearing mid-wait is a crash worth failing on. --- run/desktop/DesktopWrapper.ts | 5 ++-- run/desktop/utils.ts | 25 ++++++++++++++++--- run/test/specs/desktop/delete_account.spec.ts | 11 ++++---- 3 files changed, 30 insertions(+), 11 deletions(-) diff --git a/run/desktop/DesktopWrapper.ts b/run/desktop/DesktopWrapper.ts index 2a6b8cc82..cb6fdf484 100644 --- a/run/desktop/DesktopWrapper.ts +++ b/run/desktop/DesktopWrapper.ts @@ -1114,10 +1114,9 @@ export class DesktopWrapper implements IBaseDeviceWrapper { /** Returns immediately if the loader never shows — see the helper for why that is a pass. */ public async waitForLoadingAnimationToFinish( loader: DataTestId, - appearWithinMs?: number, - finishWithinMs?: number + options?: { appearWithinMs?: number; finishWithinMs?: number; windowMayClose?: boolean } ): Promise { - await waitForLoadingAnimationToFinish(this.page, loader, appearWithinMs, finishWithinMs); + await waitForLoadingAnimationToFinish(this.page, loader, options); } public async clickOnTextMessage( diff --git a/run/desktop/utils.ts b/run/desktop/utils.ts index eaeb38762..2b4e982dc 100644 --- a/run/desktop/utils.ts +++ b/run/desktop/utils.ts @@ -212,12 +212,18 @@ export async function waitForMatchingPlaceholder( * `network_page.spec.ts` grew its own `networkDataLoaded` rather than use this: a loader that never * cleared used to hang until the test timed out with nothing to go on. Now it fails against this * selector, which says what was still spinning. + * + * `windowMayClose` is for the work that restarts the app (clearing all data): the loader goes away + * with the window, which Playwright reports as a target-closed error rather than a hidden element. */ export async function waitForLoadingAnimationToFinish( window: Page, loader: DataTestId, - appearWithinMs = 2_000, - finishWithinMs = 60_000 + { + appearWithinMs = 2_000, + finishWithinMs = 60_000, + windowMayClose = false, + }: { appearWithinMs?: number; finishWithinMs?: number; windowMayClose?: boolean } = {} ) { const selector = buildSelectorEscapeText({ strategy: 'data-testid', selector: loader }); @@ -234,10 +240,23 @@ export async function waitForLoadingAnimationToFinish( } console.info(`${loader} was found, waiting for it to be gone`); - await window.waitForSelector(selector, { timeout: finishWithinMs, state: 'hidden' }); + try { + await window.waitForSelector(selector, { timeout: finishWithinMs, state: 'hidden' }); + } catch (e) { + if (!windowMayClose || !windowWentAway(window, e as Error)) { + throw e; + } + console.info(`${loader} went away with its window — the app restarted`); + return; + } console.info('Loading animation has finished'); } +/** `isClosed()` does not always flip before the pending wait rejects, hence the message check too. */ +function windowWentAway(window: Page, e: Error): boolean { + return window.isClosed() || /closed/i.test(e.message); +} + /** * The absence counterpart to `waitForElement`. `hidden` covers never-attached, so it also suits an * element that should never have rendered. diff --git a/run/test/specs/desktop/delete_account.spec.ts b/run/test/specs/desktop/delete_account.spec.ts index 628f156c2..3607e0f12 100644 --- a/run/test/specs/desktop/delete_account.spec.ts +++ b/run/test/specs/desktop/delete_account.spec.ts @@ -34,11 +34,12 @@ sessionTestTwoWindows('Delete account from swarm', async ([windowA, windowB]) => // Confirm deletion by clicking Clear, twice await windowA.clickOnMatchingText(tStripped('clear')); await windowA.clickOnMatchingText(tStripped('clear')); - await windowA.waitForLoadingAnimationToFinish(Global.loadingSpinner.selector); - // await sleepFor(7500); - // Wait for window to close and reopen - - // await windowA.close(); + // The delete restarts the app, so the spinner is optional at both ends: on a fast network it + // can be gone before we look, and when we do catch it, it leaves with the window. + await windowA.waitForLoadingAnimationToFinish(Global.loadingSpinner.selector, { + appearWithinMs: 1_000, + windowMayClose: true, + }); restoringWindows = await openAppsAndWaitWindows(1); // not using sessionTest here as we need to close and reopen one of the window const [restoringWindowPage] = restoringWindows; const restoringWindow = new DesktopWrapper(restoringWindowPage, 'alice-restoring');