Skip to content

Commit 0e615ef

Browse files
committed
--amend
1 parent d7cf4e7 commit 0e615ef

4 files changed

Lines changed: 188 additions & 154 deletions

File tree

packages/e2e/setup/app.ts

Lines changed: 31 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/* eslint-disable no-restricted-imports, no-await-in-loop */
22
import {authFixture} from './auth.js'
3-
import {navigateToDashboard} from './browser.js'
3+
import {navigateToDashboard, refreshIfPageError} from './browser.js'
44
import {CLI_TIMEOUT, BROWSER_TIMEOUT} from './constants.js'
55
import {updateTomlValues} from '@shopify/toml-patch'
66
import * as toml from '@iarna/toml'
@@ -201,7 +201,6 @@ export async function findAppOnDevDashboard(page: Page, appName: string, orgId?:
201201
await navigateToDashboard({browserPage: page, email, orgId: org})
202202

203203
// Scan current page + pagination for the app
204-
205204
while (true) {
206205
const allLinks = await page.locator('a[href*="/apps/"]').all()
207206
for (const link of allLinks) {
@@ -220,6 +219,7 @@ export async function findAppOnDevDashboard(page: Page, appName: string, orgId?:
220219
const nextUrl = nextHref.startsWith('http') ? nextHref : `https://dev.shopify.com${nextHref}`
221220
await page.goto(nextUrl, {waitUntil: 'domcontentloaded'})
222221
await page.waitForTimeout(BROWSER_TIMEOUT.medium)
222+
await refreshIfPageError(page)
223223
}
224224

225225
return null
@@ -230,41 +230,62 @@ export async function deleteAppFromDevDashboard(page: Page, appUrl: string): Pro
230230
// Step 1: Navigate to settings page
231231
await page.goto(`${appUrl}/settings`, {waitUntil: 'domcontentloaded'})
232232
await page.waitForTimeout(BROWSER_TIMEOUT.medium)
233+
await refreshIfPageError(page)
233234

234-
// Step 2: Wait for "Delete app" button to be enabled, then click (retry step 1+2 on failure)
235+
// Step 2: Wait for "Delete app" button to be enabled, then click (retry with error check)
235236
const deleteAppBtn = page.locator('button:has-text("Delete app")').first()
236-
for (let attempt = 1; attempt <= 5; attempt++) {
237+
for (let attempt = 1; attempt <= 3; attempt++) {
238+
if (await refreshIfPageError(page)) continue
237239
const isDisabled = await deleteAppBtn.getAttribute('disabled').catch(() => 'true')
238240
if (!isDisabled) break
239-
await page.waitForTimeout(BROWSER_TIMEOUT.long)
240241
await page.reload({waitUntil: 'domcontentloaded'})
241242
await page.waitForTimeout(BROWSER_TIMEOUT.medium)
242243
}
243244

244-
await deleteAppBtn.click({timeout: BROWSER_TIMEOUT.long})
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
245251
await page.waitForTimeout(BROWSER_TIMEOUT.medium)
246252

247-
// Step 3: Click confirm "Delete app" in the modal (retry step 2+3 if not visible)
253+
// Step 3: Click confirm "Delete" in the modal (retry step 2+3 if not visible)
248254
// The dev dashboard modal has a submit button with class "critical" inside a form
249255
const confirmAppBtn = page.locator('button.critical[type="submit"]')
250256
for (let attempt = 1; attempt <= 3; attempt++) {
251257
if (await confirmAppBtn.isVisible({timeout: BROWSER_TIMEOUT.medium}).catch(() => false)) break
252-
if (attempt === 3) break
258+
if (attempt === 3) return false
253259
// Retry: re-click the delete button to reopen modal
254260
await page.keyboard.press('Escape')
255261
await page.waitForTimeout(BROWSER_TIMEOUT.short)
256-
await deleteAppBtn.click({timeout: BROWSER_TIMEOUT.long})
262+
const retryClicked = await deleteAppBtn
263+
.click({timeout: BROWSER_TIMEOUT.long})
264+
.then(() => true)
265+
.catch(() => false)
266+
if (!retryClicked) return false
257267
await page.waitForTimeout(BROWSER_TIMEOUT.medium)
258268
}
259269

260270
const urlBefore = page.url()
261-
await confirmAppBtn.click({timeout: BROWSER_TIMEOUT.long})
271+
const confirmClicked = await confirmAppBtn
272+
.click({timeout: BROWSER_TIMEOUT.long})
273+
.then(() => true)
274+
.catch(() => false)
275+
if (!confirmClicked) return false
262276

263277
// Wait for page to navigate away after deletion
264278
try {
265279
await page.waitForURL((url) => url.toString() !== urlBefore, {timeout: BROWSER_TIMEOUT.max})
266280
// eslint-disable-next-line no-catch-all/no-catch-all
267281
} 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)
285+
const bodyText = (await page.textContent('body')) ?? ''
286+
if (bodyText.includes('404: Not Found')) return true
287+
return false
288+
}
268289
await page.waitForTimeout(BROWSER_TIMEOUT.medium)
269290
}
270291
return page.url() !== urlBefore

packages/e2e/setup/browser.ts

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,22 @@ export const browserFixture = cliFixture.extend<{}, {browserPage: Page}>({
4848
})
4949

5050
// ---------------------------------------------------------------------------
51-
// Browser helpers — generic dashboard navigation
51+
// Browser helpers
5252
// ---------------------------------------------------------------------------
53+
54+
/**
55+
* Check if the current page shows a server error (500, 502). If so, refresh and return true.
56+
* Call this in retry loops when a selector isn't found — the page might be an error page.
57+
*/
58+
export async function refreshIfPageError(page: Page): Promise<boolean> {
59+
const pageText = (await page.textContent('body')) ?? ''
60+
if (!pageText.includes('Internal Server Error') && !pageText.includes('502 Bad Gateway')) return false
61+
//if (process.env.DEBUG === '1') process.stdout.write(' page refreshing...\n')
62+
await page.reload({waitUntil: 'domcontentloaded'})
63+
await page.waitForTimeout(BROWSER_TIMEOUT.medium)
64+
return true
65+
}
66+
5367
/** Navigate to the dev dashboard for the configured org. */
5468
export async function navigateToDashboard(
5569
ctx: BrowserContext & {
@@ -63,6 +77,9 @@ export async function navigateToDashboard(
6377
await browserPage.goto(dashboardUrl, {waitUntil: 'domcontentloaded'})
6478
await browserPage.waitForTimeout(BROWSER_TIMEOUT.medium)
6579

80+
// Retry on server errors
81+
await refreshIfPageError(browserPage)
82+
6683
// Handle account picker (skip if email not provided)
6784
if (ctx.email) {
6885
const accountButton = browserPage.locator(`text=${ctx.email}`).first()
@@ -71,12 +88,4 @@ export async function navigateToDashboard(
7188
await browserPage.waitForTimeout(BROWSER_TIMEOUT.medium)
7289
}
7390
}
74-
75-
// Retry on 500 errors
76-
for (let attempt = 1; attempt <= 3; attempt++) {
77-
const pageText = (await browserPage.textContent('body')) ?? '' // eslint-disable-line no-await-in-loop
78-
if (!pageText.includes('500: Internal Server Error') && !pageText.includes('Internal Server Error')) break
79-
await browserPage.waitForTimeout(BROWSER_TIMEOUT.medium) // eslint-disable-line no-await-in-loop
80-
await browserPage.reload({waitUntil: 'domcontentloaded'}) // eslint-disable-line no-await-in-loop
81-
}
8291
}

packages/e2e/setup/store.ts

Lines changed: 50 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -125,26 +125,29 @@ export async function createDevStore(
125125
// Store admin browser actions — uninstall apps and delete stores
126126
// ---------------------------------------------------------------------------
127127

128+
/** Dismiss the Dev Console panel if visible on a store admin page. */
129+
export async function dismissDevConsole(page: Page): Promise<void> {
130+
const devConsole = page.locator('h2:has-text("Dev Console")')
131+
if (!(await devConsole.isVisible({timeout: BROWSER_TIMEOUT.medium}).catch(() => false))) return
132+
133+
const hideBtn = page.locator('button[aria-label="hide"]').first()
134+
if (await hideBtn.isVisible({timeout: BROWSER_TIMEOUT.short}).catch(() => false)) {
135+
await hideBtn.click()
136+
await page.waitForTimeout(BROWSER_TIMEOUT.short)
137+
}
138+
}
139+
128140
/**
129141
* Uninstall an app from a store's admin settings page.
130142
* Navigates to /settings/apps, finds the app by name, uninstalls it, and verifies removal.
131143
* Returns true if app is confirmed gone, false if still present.
132144
*/
133-
export async function uninstallAppFromStoreAdmin(page: Page, storeSlug: string, appName: string): Promise<boolean> {
145+
export async function uninstallAppFromStore(page: Page, storeSlug: string, appName: string): Promise<boolean> {
134146
await page.goto(`https://admin.shopify.com/store/${storeSlug}/settings/apps`, {
135147
waitUntil: 'domcontentloaded',
136148
})
137149
await page.waitForTimeout(BROWSER_TIMEOUT.long)
138-
139-
// Dismiss any Dev Console dialog
140-
const cancelBtn = page.locator('button:has-text("Cancel")')
141-
if (await cancelBtn.isVisible({timeout: BROWSER_TIMEOUT.medium}).catch(() => false)) {
142-
await cancelBtn.click()
143-
await page.waitForTimeout(BROWSER_TIMEOUT.short)
144-
}
145-
146-
// Check if already uninstalled
147-
if (await isAppsPageEmpty(page)) return true
150+
await dismissDevConsole(page)
148151

149152
const appSpan = page.locator(`span:has-text("${appName}"):not([class*="Polaris"])`).first()
150153
if (!(await appSpan.isVisible({timeout: BROWSER_TIMEOUT.long}).catch(() => false))) return true
@@ -164,23 +167,24 @@ export async function uninstallAppFromStoreAdmin(page: Page, storeSlug: string,
164167
await page.waitForTimeout(BROWSER_TIMEOUT.medium)
165168
}
166169

167-
// Verify: reload and check app is gone
168-
await page.reload({waitUntil: 'domcontentloaded'})
169-
await page.waitForTimeout(BROWSER_TIMEOUT.long)
170+
// Verify: check the specific app is gone
171+
const check = async () =>
172+
page
173+
.locator(`span:has-text("${appName}"):not([class*="Polaris"])`)
174+
.first()
175+
.isVisible({timeout: BROWSER_TIMEOUT.medium})
176+
.catch(() => false)
170177

171-
if (await isAppsPageEmpty(page)) return true
178+
if (!(await check())) return true
172179

173-
// App name no longer visible = success even if other apps remain
174-
const stillVisible = await page
175-
.locator(`span:has-text("${appName}"):not([class*="Polaris"])`)
176-
.first()
177-
.isVisible({timeout: BROWSER_TIMEOUT.medium})
178-
.catch(() => false)
179-
return !stillVisible
180+
// If still visible — reload and check again
181+
await page.reload({waitUntil: 'domcontentloaded'})
182+
await page.waitForTimeout(BROWSER_TIMEOUT.long)
183+
return !(await check())
180184
}
181185

182-
/** Check if the store apps page shows the empty state (zero apps installed). */
183-
async function isAppsPageEmpty(page: Page): Promise<boolean> {
186+
/** Check if the current page shows the empty state (zero apps installed). Caller must navigate first. */
187+
export async function isStoreAppsEmpty(page: Page): Promise<boolean> {
184188
// "Add apps to your store" empty state is the definitive zero-apps signal
185189
const emptyState = page.locator('text=Add apps to your store')
186190
if (await emptyState.isVisible({timeout: BROWSER_TIMEOUT.medium}).catch(() => false)) return true
@@ -191,29 +195,11 @@ async function isAppsPageEmpty(page: Page): Promise<boolean> {
191195
}
192196

193197
/**
194-
* Delete a store from the admin settings plan page.
195-
* Verifies no apps are installed first — refuses to delete if apps remain.
196-
* Returns true if deleted, false if skipped.
198+
* Delete a store via the admin settings plan page.
199+
* Caller must verify no apps are installed before calling.
200+
* Returns true if deleted, false if not.
197201
*/
198-
export async function deleteStoreFromAdmin(page: Page, storeSlug: string): Promise<boolean> {
199-
// Verify no apps are installed before deleting
200-
await page.goto(`https://admin.shopify.com/store/${storeSlug}/settings/apps`, {
201-
waitUntil: 'domcontentloaded',
202-
})
203-
await page.waitForTimeout(BROWSER_TIMEOUT.long)
204-
205-
const cancelBtn = page.locator('button:has-text("Cancel")')
206-
if (await cancelBtn.isVisible({timeout: BROWSER_TIMEOUT.medium}).catch(() => false)) {
207-
await cancelBtn.click()
208-
await page.waitForTimeout(BROWSER_TIMEOUT.short)
209-
}
210-
211-
if (!(await isAppsPageEmpty(page))) {
212-
// Apps still installed — refuse to delete
213-
return false
214-
}
215-
216-
// Delete the store
202+
export async function deleteStore(page: Page, storeSlug: string): Promise<boolean> {
217203
// Step 1: Navigate to plan page and click delete button to open modal (retry navigation on failure)
218204
const planUrl = `https://admin.shopify.com/store/${storeSlug}/settings/plan`
219205
const deleteButton = page.locator('s-internal-button[tone="critical"]').locator('button')
@@ -222,12 +208,13 @@ export async function deleteStoreFromAdmin(page: Page, storeSlug: string): Promi
222208
try {
223209
await page.goto(planUrl, {waitUntil: 'domcontentloaded'})
224210
await page.waitForTimeout(BROWSER_TIMEOUT.long)
211+
// If redirected to access_account, store is already deleted
212+
if (page.url().includes('access_account')) return true
225213
await deleteButton.click({timeout: BROWSER_TIMEOUT.long})
226214
break
227215
// eslint-disable-next-line no-catch-all/no-catch-all
228216
} catch (_err) {
229217
if (attempt === 3) return false
230-
await page.waitForTimeout(BROWSER_TIMEOUT.medium)
231218
}
232219
}
233220
await page.waitForTimeout(BROWSER_TIMEOUT.medium)
@@ -264,15 +251,24 @@ export async function deleteStoreFromAdmin(page: Page, storeSlug: string): Promi
264251
await page.waitForTimeout(BROWSER_TIMEOUT.short)
265252
}
266253

267-
await confirmButton.click({force: true})
268-
await page.waitForURL(/access_account/, {timeout: BROWSER_TIMEOUT.max})
269-
270-
// Verify: "Your plan was canceled" confirms the store is deleted
271-
const canceled = page.locator('text=Your plan was canceled')
272-
const verified = await canceled.isVisible({timeout: BROWSER_TIMEOUT.long}).catch(() => false)
273-
return verified || page.url().includes('access_account')
254+
const confirmClicked = await confirmButton
255+
.click({force: true})
256+
.then(() => true)
257+
.catch(() => false)
258+
if (!confirmClicked) return false
259+
260+
// Verify: URL reaching access_account confirms store is deleted
261+
try {
262+
await page.waitForURL(/access_account/, {timeout: BROWSER_TIMEOUT.max})
263+
return true
264+
// eslint-disable-next-line no-catch-all/no-catch-all
265+
} catch (_err) {
266+
return false
267+
}
274268
}
275269

270+
271+
276272
// ---------------------------------------------------------------------------
277273
// Fixture — per-test dev store for tests that need `app dev`
278274
// ---------------------------------------------------------------------------

0 commit comments

Comments
 (0)