From e86050a8bfbea08df5060f52457bfd10615d7fc5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Isaac=20Rold=C3=A1n?= Date: Fri, 26 Jun 2026 12:17:04 +0200 Subject: [PATCH 1/2] Fix e2e cleanup --- packages/e2e/scripts/cleanup-apps.ts | 58 +++++++++++++++++++++++++--- packages/e2e/setup/browser.ts | 6 ++- 2 files changed, 57 insertions(+), 7 deletions(-) diff --git a/packages/e2e/scripts/cleanup-apps.ts b/packages/e2e/scripts/cleanup-apps.ts index 620452a3d7e..e9852ef36b9 100644 --- a/packages/e2e/scripts/cleanup-apps.ts +++ b/packages/e2e/scripts/cleanup-apps.ts @@ -25,7 +25,7 @@ import * as path from 'path' import {fileURLToPath} from 'url' import {chromium} from '@playwright/test' import {BROWSER_TIMEOUT} from '../setup/constants.js' -import {navigateToDashboard, refreshIfPageError, trackMainFrameStatus} from '../setup/browser.js' +import {getLastPageStatus, navigateToDashboard, refreshIfPageError, trackMainFrameStatus} from '../setup/browser.js' import {deleteAppFromDevDashboard} from '../setup/app.js' import {uninstallAppFromStore} from '../setup/store.js' import {completeLogin} from '../helpers/browser-login.js' @@ -74,6 +74,11 @@ interface CleanupStats { failed: number } +const APP_CARD_SELECTOR = 'a[href*="/apps/"]' +const EMPTY_APPS_PATTERN = + /(no apps matched your search|don't have any apps|do not have any apps|haven't created any apps)/i +const DASHBOARD_ERROR_PATTERN = /(unprocessable entity|request can't be processed|server error|something went wrong)/i + /** * Find and delete all E2E test apps matching a pattern. * Handles browser login, dashboard navigation, uninstall, and deletion. @@ -121,7 +126,7 @@ export async function cleanupAllApps(opts: CleanupOptions = {}): Promise { // Step 2: Navigate to dashboard (retry on 500/502). // navigateToDashboard already refreshes once on error; this loop is extra resilience. console.log('[cleanup-apps] Navigating to dashboard...') - await navigateToDashboard({browserPage: page, email, orgId}) + await navigateToDashboard({browserPage: page, email, orgId, searchTerm: pattern}) for (let attempt = 1; attempt <= 3; attempt++) { if (!(await refreshIfPageError(page))) break if (attempt === 3) throw new Error('Dashboard returned server error after 3 attempts, aborting cleanup') @@ -170,6 +175,7 @@ async function cleanupAppsPageByPage(opts: { // eslint-disable-next-line no-constant-condition while (true) { await recoverFromAppsPageError(page) + await waitForAppsIndex(page) const nextUrl = await nextAppsPageUrl(page) const {seen, matches} = await findAppsOnCurrentDashboardPage(page, pattern, handledAppUrls) const matchOffset = stats.found @@ -196,7 +202,7 @@ async function cleanupAppsPageByPage(opts: { // for an org with matches spread across many pages: re-scanning from the top // avoids stale next_cursor links that point past now-deleted apps. Don't // "optimize" this into carrying the cursor across mutations. - await navigateToDashboard({browserPage: page, email, orgId}) + await navigateToDashboard({browserPage: page, email, orgId, searchTerm: pattern}) totalSeen = 0 pageNumber = 1 continue @@ -229,7 +235,7 @@ async function cleanupApp(opts: { try { if (attempt > 1) { console.log(` (${attempt}/3) retrying...`) - await navigateToDashboard({browserPage: page, email, orgId}) + await navigateToDashboard({browserPage: page, email, orgId, searchTerm: app.name}) } if (mode === 'full' || mode === 'uninstall') { @@ -299,6 +305,48 @@ async function recoverFromAppsPageError(page: Page): Promise { } } +async function waitForAppsIndex(page: Page): Promise { + for (let attempt = 1; attempt <= 3; attempt++) { + const status = getLastPageStatus(page) + if (status !== undefined && status >= 400) { + await retryAppsIndex(page, attempt, `HTTP ${status}`) + continue + } + + const hasAppCards = await page + .locator(APP_CARD_SELECTOR) + .first() + .waitFor({state: 'attached', timeout: BROWSER_TIMEOUT.max}) + .then(() => true) + .catch(() => false) + if (hasAppCards) return + + const bodyText = await page + .locator('body') + .innerText({timeout: BROWSER_TIMEOUT.short}) + .catch(() => '') + if (EMPTY_APPS_PATTERN.test(bodyText)) return + + const reason = DASHBOARD_ERROR_PATTERN.test(bodyText) + ? `dashboard error page: ${compactPageText(bodyText)}` + : 'no app cards or empty state' + await retryAppsIndex(page, attempt, reason) + } +} + +async function retryAppsIndex(page: Page, attempt: number, reason: string): Promise { + if (attempt === 3) { + throw new Error(`Apps page did not render a usable app list (${reason}; url: ${page.url()})`) + } + console.log(`[cleanup-apps] ...apps page not ready (${reason}), retrying`) + await page.reload({waitUntil: 'domcontentloaded'}) + await page.waitForTimeout(BROWSER_TIMEOUT.medium) +} + +function compactPageText(text: string): string { + return text.replace(/\s+/g, ' ').trim().slice(0, 120) +} + async function findAppsOnCurrentDashboardPage( page: Page, namePattern: string, @@ -306,7 +354,7 @@ async function findAppsOnCurrentDashboardPage( ): Promise<{seen: number; matches: DashboardApp[]}> { const matches: DashboardApp[] = [] let seen = 0 - const appCards = await page.locator('a[href*="/apps/"]').all() + const appCards = await page.locator(APP_CARD_SELECTOR).all() for (const card of appCards) { const href = await card.getAttribute('href') diff --git a/packages/e2e/setup/browser.ts b/packages/e2e/setup/browser.ts index 0683e5bf25d..455bffb4bcf 100644 --- a/packages/e2e/setup/browser.ts +++ b/packages/e2e/setup/browser.ts @@ -116,12 +116,14 @@ export async function navigateToDashboard( ctx: BrowserContext & { email?: string orgId?: string + searchTerm?: string }, ): Promise { const {browserPage} = ctx const orgId = ctx.orgId ?? (process.env.E2E_ORG_ID ?? '').trim() - const dashboardUrl = orgId ? `https://dev.shopify.com/dashboard/${orgId}/apps` : 'https://dev.shopify.com/dashboard' - await browserPage.goto(dashboardUrl, {waitUntil: 'domcontentloaded'}) + const dashboardUrl = new URL(orgId ? `https://dev.shopify.com/dashboard/${orgId}/apps` : 'https://dev.shopify.com/dashboard') + if (ctx.searchTerm) dashboardUrl.searchParams.set('search_term', ctx.searchTerm) + await browserPage.goto(dashboardUrl.toString(), {waitUntil: 'domcontentloaded'}) await browserPage.waitForTimeout(BROWSER_TIMEOUT.medium) // Retry on server errors From 3fdcc111fbb131f7619c88269abe00d2fc753d7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Isaac=20Rold=C3=A1n?= Date: Fri, 26 Jun 2026 14:18:11 +0200 Subject: [PATCH 2/2] Fix e2e cleanup CI Assisted-By: devx/6d93e11e-762b-48c5-9bcf-265de5035498 --- .../cli-kit/src/cli/api/graphql/admin/generated/types.d.ts | 2 ++ packages/e2e/setup/browser.ts | 4 +++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/cli-kit/src/cli/api/graphql/admin/generated/types.d.ts b/packages/cli-kit/src/cli/api/graphql/admin/generated/types.d.ts index 2da991ce827..9ce2584b9d6 100644 --- a/packages/cli-kit/src/cli/api/graphql/admin/generated/types.d.ts +++ b/packages/cli-kit/src/cli/api/graphql/admin/generated/types.d.ts @@ -232,6 +232,8 @@ export type OnlineStoreThemeFilesUserErrorsCode = | 'LESS_THAN_OR_EQUAL_TO' /** The record with the ID used as the input value couldn't be found. */ | 'NOT_FOUND' + /** Theme contextualization and condition types are not compatible with each other. */ + | 'THEME_CONTEXTUALIZATION_NOT_COMPATIBLE_WITH_CONDITION_TYPES' /** There are theme files with conflicts. */ | 'THEME_FILES_CONFLICT' /** This action is not available on your current plan. Please upgrade to access theme editing features. */ diff --git a/packages/e2e/setup/browser.ts b/packages/e2e/setup/browser.ts index 455bffb4bcf..7497bba3fbd 100644 --- a/packages/e2e/setup/browser.ts +++ b/packages/e2e/setup/browser.ts @@ -121,7 +121,9 @@ export async function navigateToDashboard( ): Promise { const {browserPage} = ctx const orgId = ctx.orgId ?? (process.env.E2E_ORG_ID ?? '').trim() - const dashboardUrl = new URL(orgId ? `https://dev.shopify.com/dashboard/${orgId}/apps` : 'https://dev.shopify.com/dashboard') + const dashboardUrl = new URL( + orgId ? `https://dev.shopify.com/dashboard/${orgId}/apps` : 'https://dev.shopify.com/dashboard', + ) if (ctx.searchTerm) dashboardUrl.searchParams.set('search_term', ctx.searchTerm) await browserPage.goto(dashboardUrl.toString(), {waitUntil: 'domcontentloaded'}) await browserPage.waitForTimeout(BROWSER_TIMEOUT.medium)