@@ -225,70 +225,70 @@ export async function findAppOnDevDashboard(page: Page, appName: string, orgId?:
225225 return null
226226}
227227
228- /** Delete an app from its dev dashboard settings page. Returns true if deleted, false if not. */
228+ /**
229+ * Delete an app from its dev dashboard settings page. Returns true if deleted.
230+ * Mirrors the resilience patterns from scripts/cleanup-apps.ts:deleteApp:
231+ * - Outer 3-attempt retry loop around the whole flow (500/502 recovery)
232+ * - scrollIntoViewIfNeeded on the "Delete app" button
233+ * - Conditional "type DELETE" confirmation (some orgs/modals require it)
234+ * - Verify via settings-page reload → expect 404
235+ */
229236export async function deleteAppFromDevDashboard ( page : Page , appUrl : string ) : Promise < boolean > {
230- // Step 1: Navigate to settings page
231- await page . goto ( `${ appUrl } /settings` , { waitUntil : 'domcontentloaded' } )
232- await page . waitForTimeout ( BROWSER_TIMEOUT . medium )
233- await refreshIfPageError ( page )
234-
235- // Step 2: Wait for "Delete app" button to be enabled, then click (retry with error check)
236- const deleteAppBtn = page . locator ( 'button:has-text("Delete app")' ) . first ( )
237237 for ( let attempt = 1 ; attempt <= 3 ; attempt ++ ) {
238- if ( await refreshIfPageError ( page ) ) continue
239- const isDisabled = await deleteAppBtn . getAttribute ( 'disabled' ) . catch ( ( ) => 'true' )
240- if ( ! isDisabled ) break
241- await page . reload ( { waitUntil : 'domcontentloaded' } )
242- await page . waitForTimeout ( BROWSER_TIMEOUT . medium )
243- }
244-
245- // Click the delete button — if it's not found, the page didn't load properly
246- const deleteClicked = await deleteAppBtn
247- . click ( { timeout : BROWSER_TIMEOUT . long } )
248- . then ( ( ) => true )
249- . catch ( ( ) => false )
250- if ( ! deleteClicked ) return false
251- await page . waitForTimeout ( BROWSER_TIMEOUT . medium )
252-
253- // Step 3: Click confirm "Delete" in the modal (retry step 2+3 if not visible)
254- // The dev dashboard modal has a submit button with class "critical" inside a form
255- const confirmAppBtn = page . locator ( 'button.critical[type="submit"]' )
256- for ( let attempt = 1 ; attempt <= 3 ; attempt ++ ) {
257- if ( await confirmAppBtn . isVisible ( { timeout : BROWSER_TIMEOUT . medium } ) . catch ( ( ) => false ) ) break
258- if ( attempt === 3 ) return false
259- // Retry: re-click the delete button to reopen modal
260- await page . keyboard . press ( 'Escape' )
261- await page . waitForTimeout ( BROWSER_TIMEOUT . short )
262- const retryClicked = await deleteAppBtn
263- . click ( { timeout : BROWSER_TIMEOUT . long } )
264- . then ( ( ) => true )
265- . catch ( ( ) => false )
266- if ( ! retryClicked ) return false
267- await page . waitForTimeout ( BROWSER_TIMEOUT . medium )
268- }
238+ try {
239+ await page . goto ( `${ appUrl } /settings` , { waitUntil : 'domcontentloaded' } )
240+ await page . waitForTimeout ( BROWSER_TIMEOUT . medium )
269241
270- const urlBefore = page . url ( )
271- const confirmClicked = await confirmAppBtn
272- . click ( { timeout : BROWSER_TIMEOUT . long } )
273- . then ( ( ) => true )
274- . catch ( ( ) => false )
275- if ( ! confirmClicked ) return false
276-
277- // Wait for page to navigate away after deletion
278- try {
279- await page . waitForURL ( ( url ) => url . toString ( ) !== urlBefore , { timeout : BROWSER_TIMEOUT . max } )
280- // eslint-disable-next-line no-catch-all/no-catch-all
281- } catch ( _err ) {
282- // URL didn't change — check if page error occurred during redirect
283- if ( await refreshIfPageError ( page ) ) {
284- // After refresh, 404 means the app was deleted (settings page no longer exists)
242+ // 404 before we even click = app already gone. 500/502 = throw to retry.
285243 const bodyText = ( await page . textContent ( 'body' ) ) ?? ''
286244 if ( bodyText . includes ( '404: Not Found' ) ) return true
287- return false
245+ if ( bodyText . includes ( '500: Internal Server Error' ) || bodyText . includes ( '502 Bad Gateway' ) ) {
246+ throw new Error ( 'Server error loading app settings page' )
247+ }
248+ await refreshIfPageError ( page )
249+
250+ // Wait for "Delete app" button to be enabled. Button can be below the
251+ // fold, so scrollIntoView before each check.
252+ const deleteBtn = page . locator ( 'button:has-text("Delete app")' ) . first ( )
253+ for ( let i = 1 ; i <= 5 ; i ++ ) {
254+ await deleteBtn . scrollIntoViewIfNeeded ( )
255+ const isDisabled = await deleteBtn . getAttribute ( 'disabled' )
256+ if ( ! isDisabled ) break
257+ await page . waitForTimeout ( BROWSER_TIMEOUT . long )
258+ await page . reload ( { waitUntil : 'domcontentloaded' } )
259+ await page . waitForTimeout ( BROWSER_TIMEOUT . medium )
260+ }
261+
262+ await deleteBtn . click ( { timeout : BROWSER_TIMEOUT . max } )
263+ await page . waitForTimeout ( BROWSER_TIMEOUT . medium )
264+
265+ // Some confirmation modals require typing "DELETE". Fill if present.
266+ const confirmInput = page . locator ( 'input[type="text"]' ) . last ( )
267+ if ( await confirmInput . isVisible ( { timeout : BROWSER_TIMEOUT . medium } ) . catch ( ( ) => false ) ) {
268+ await confirmInput . fill ( 'DELETE' )
269+ await page . waitForTimeout ( BROWSER_TIMEOUT . short )
270+ }
271+
272+ // Confirm button is the second "Delete app" button on the page (inside modal).
273+ const confirmBtn = page . locator ( 'button:has-text("Delete app")' ) . last ( )
274+ await confirmBtn . click ( { timeout : BROWSER_TIMEOUT . long } )
275+ await page . waitForTimeout ( BROWSER_TIMEOUT . medium )
276+
277+ // Verify by reloading settings — 404 means deleted.
278+ await page . goto ( `${ appUrl } /settings` , { waitUntil : 'domcontentloaded' } )
279+ await page . waitForTimeout ( BROWSER_TIMEOUT . short )
280+ const afterText = ( await page . textContent ( 'body' ) ) ?? ''
281+ if ( afterText . includes ( '404: Not Found' ) ) return true
282+ if ( afterText . includes ( '500: Internal Server Error' ) || afterText . includes ( '502 Bad Gateway' ) ) {
283+ throw new Error ( 'Server error verifying app deletion' )
284+ }
285+ // eslint-disable-next-line no-catch-all/no-catch-all
286+ } catch ( _err ) {
287+ if ( attempt === 3 ) return false
288+ await page . waitForTimeout ( BROWSER_TIMEOUT . medium )
288289 }
289- await page . waitForTimeout ( BROWSER_TIMEOUT . medium )
290290 }
291- return page . url ( ) !== urlBefore
291+ return false
292292}
293293
294294// ---------------------------------------------------------------------------
0 commit comments