From 2a44de3c218cdef76922e5c52be030f70b0caeac Mon Sep 17 00:00:00 2001 From: Phyllis Wu Date: Mon, 13 Apr 2026 13:59:10 -0400 Subject: [PATCH 1/6] E2E: one-time global auth before test start --- packages/e2e/playwright.config.ts | 1 + packages/e2e/setup/auth.ts | 58 ++++++++++-- packages/e2e/setup/browser.ts | 2 + packages/e2e/setup/env.ts | 7 ++ packages/e2e/setup/global-auth.ts | 147 ++++++++++++++++++++++++++++++ 5 files changed, 205 insertions(+), 10 deletions(-) create mode 100644 packages/e2e/setup/global-auth.ts diff --git a/packages/e2e/playwright.config.ts b/packages/e2e/playwright.config.ts index a46b602c7d..2c33df99bb 100644 --- a/packages/e2e/playwright.config.ts +++ b/packages/e2e/playwright.config.ts @@ -8,6 +8,7 @@ config() const isCI = Boolean(process.env.CI) export default defineConfig({ + globalSetup: './setup/global-auth.ts', testDir: './tests', fullyParallel: false, forbidOnly: isCI, diff --git a/packages/e2e/setup/auth.ts b/packages/e2e/setup/auth.ts index 9a8bb1054b..f1ac0c93ca 100644 --- a/packages/e2e/setup/auth.ts +++ b/packages/e2e/setup/auth.ts @@ -1,17 +1,39 @@ -import {CLI_TIMEOUT, BROWSER_TIMEOUT} from './constants.js' +/* eslint-disable no-restricted-imports */ import {browserFixture} from './browser.js' -import {executables} from './env.js' +import {CLI_TIMEOUT, BROWSER_TIMEOUT} from './constants.js' +import {globalLog, executables} from './env.js' import {stripAnsi} from '../helpers/strip-ansi.js' import {waitForText} from '../helpers/wait-for-text.js' import {completeLogin} from '../helpers/browser-login.js' import {execa} from 'execa' +import * as fs from 'fs' +import * as path from 'path' + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const log = {log: (_ctx: any, msg: string) => globalLog('auth', msg)} /** - * Worker-scoped fixture that performs OAuth login using the shared browser page. + * Copy directory contents recursively. + */ +function copyDirSync(src: string, dest: string): void { + fs.mkdirSync(dest, {recursive: true}) + for (const entry of fs.readdirSync(src, {withFileTypes: true})) { + const srcPath = path.join(src, entry.name) + const destPath = path.join(dest, entry.name) + if (entry.isDirectory()) { + copyDirSync(srcPath, destPath) + } else { + fs.copyFileSync(srcPath, destPath) + } + } +} + +/** + * Worker-scoped fixture that provides an authenticated CLI session. * - * Extends browserFixture — the browser is already running when auth starts. - * After login, the CLI session is stored in XDG dirs and the browser page - * remains available for other browser-based actions (dashboard navigation, etc.). + * If globalSetup already ran auth (E2E_AUTH_CONFIG_DIR is set), copies the + * pre-authenticated session files into this worker's isolated XDG dirs. + * Otherwise falls back to running auth login directly (single-worker mode). * * Fixture chain: envFixture → cliFixture → browserFixture → authFixture */ @@ -26,22 +48,38 @@ export const authFixture = browserFixture.extend<{}, {authLogin: void}>({ return } - process.stdout.write('[e2e] Authenticating automatically — no action required.\n') + const authConfigDir = process.env.E2E_AUTH_CONFIG_DIR + const authDataDir = process.env.E2E_AUTH_DATA_DIR + const authStateDir = process.env.E2E_AUTH_STATE_DIR + const authCacheDir = process.env.E2E_AUTH_CACHE_DIR + + if (authConfigDir && authDataDir && authStateDir && authCacheDir) { + // Copy pre-authenticated session from global setup + log.log(env, 'copying session from global setup') + + copyDirSync(authConfigDir, env.processEnv.XDG_CONFIG_HOME!) + copyDirSync(authDataDir, env.processEnv.XDG_DATA_HOME!) + copyDirSync(authStateDir, env.processEnv.XDG_STATE_HOME!) + copyDirSync(authCacheDir, env.processEnv.XDG_CACHE_HOME!) + + await use() + return + } + + // Fallback: run auth login directly (single-worker / no global setup) + log.log(env, ' authenticating automatically') - // Clear any existing session await execa('node', [executables.cli, 'auth', 'logout'], { env: env.processEnv, reject: false, }) - // Spawn auth login via PTY (must not have CI=1) const nodePty = await import('node-pty') const spawnEnv: {[key: string]: string} = {} for (const [key, value] of Object.entries(env.processEnv)) { if (value !== undefined) spawnEnv[key] = value } spawnEnv.CI = '' - // Print login URL directly instead of opening system browser spawnEnv.CODESPACES = 'true' const ptyProcess = nodePty.spawn('node', [executables.cli, 'auth', 'login'], { diff --git a/packages/e2e/setup/browser.ts b/packages/e2e/setup/browser.ts index ff3fa20dc7..e0270fc711 100644 --- a/packages/e2e/setup/browser.ts +++ b/packages/e2e/setup/browser.ts @@ -28,10 +28,12 @@ export const browserFixture = cliFixture.extend<{}, {browserPage: Page}>({ // eslint-disable-next-line no-empty-pattern async ({}, use) => { const browser = await chromium.launch({headless: !process.env.E2E_HEADED}) + const storageStatePath = process.env.E2E_BROWSER_STATE_PATH const context = await browser.newContext({ extraHTTPHeaders: { 'X-Shopify-Loadtest-Bf8d22e7-120e-4b5b-906c-39ca9d5499a9': 'true', }, + ...(storageStatePath ? {storageState: storageStatePath} : {}), }) context.setDefaultTimeout(BROWSER_TIMEOUT.max) context.setDefaultNavigationTimeout(BROWSER_TIMEOUT.max) diff --git a/packages/e2e/setup/env.ts b/packages/e2e/setup/env.ts index a6a4a2c9ac..0d862a8299 100644 --- a/packages/e2e/setup/env.ts +++ b/packages/e2e/setup/env.ts @@ -72,6 +72,13 @@ export function requireEnv(env: E2EEnv, ...keys: (keyof Pick { + output += data + if (debug) process.stdout.write(data) + }) + + await waitForText(() => output, 'Open this link to start the auth process', CLI_TIMEOUT.short) + + const stripped = stripAnsi(output) + const urlMatch = stripped.match(/https:\/\/accounts\.shopify\.com\S+/) + if (!urlMatch) { + throw new Error(`[e2e] global-auth: could not find login URL in output:\n${stripped}`) + } + + // Complete login in a headless browser + const browser = await chromium.launch({headless: !process.env.E2E_HEADED}) + const context = await browser.newContext({ + extraHTTPHeaders: { + 'X-Shopify-Loadtest-Bf8d22e7-120e-4b5b-906c-39ca9d5499a9': 'true', + }, + }) + const page = await context.newPage() + + await completeLogin(page, urlMatch[0], email, password) + + await waitForText(() => output, 'Logged in', BROWSER_TIMEOUT.max) + try { + ptyProcess.kill() + // eslint-disable-next-line no-catch-all/no-catch-all + } catch (_error) { + // Process may already be dead + } + + // Visit admin.shopify.com and dev.shopify.com to establish session cookies + // (completeLogin only authenticates on accounts.shopify.com) + const orgId = (process.env.E2E_ORG_ID ?? '').trim() + if (orgId) { + // Establish admin.shopify.com cookies + await page.goto('https://admin.shopify.com/', {waitUntil: 'domcontentloaded'}) + await page.waitForTimeout(BROWSER_TIMEOUT.medium) + + // Handle account picker if shown + if (page.url().includes('accounts.shopify.com')) { + const accountButton = page.locator(`text=${email}`).first() + if (await accountButton.isVisible({timeout: BROWSER_TIMEOUT.long}).catch(() => false)) { + await accountButton.click() + await page.waitForTimeout(BROWSER_TIMEOUT.medium) + } + } + + // Establish dev.shopify.com cookies + await page.goto(`https://dev.shopify.com/dashboard/${orgId}/apps`, {waitUntil: 'domcontentloaded'}) + await page.waitForTimeout(BROWSER_TIMEOUT.medium) + + if (page.url().includes('accounts.shopify.com')) { + const accountButton = page.locator(`text=${email}`).first() + if (await accountButton.isVisible({timeout: BROWSER_TIMEOUT.long}).catch(() => false)) { + await accountButton.click() + await page.waitForTimeout(BROWSER_TIMEOUT.medium) + } + } + + globalLog('auth', 'browser sessions established for admin + dev dashboard') + } + + // Save browser cookies/storage so workers can reuse the session + // Now includes cookies for both accounts.shopify.com AND admin.shopify.com + const storageStatePath = path.join(tmpBase, 'browser-storage-state.json') + await context.storageState({path: storageStatePath}) + await browser.close() + + // Store paths so workers can copy CLI auth + load browser state + /* eslint-disable require-atomic-updates */ + process.env.E2E_AUTH_CONFIG_DIR = xdgEnv.XDG_CONFIG_HOME + process.env.E2E_AUTH_DATA_DIR = xdgEnv.XDG_DATA_HOME + process.env.E2E_AUTH_STATE_DIR = xdgEnv.XDG_STATE_HOME + process.env.E2E_AUTH_CACHE_DIR = xdgEnv.XDG_CACHE_HOME + process.env.E2E_BROWSER_STATE_PATH = storageStatePath + /* eslint-enable require-atomic-updates */ + + globalLog('auth', `global setup done, config at ${xdgEnv.XDG_CONFIG_HOME}`) +} From bf32de232d4025b42cc7588dabb2e884a45376b4 Mon Sep 17 00:00:00 2001 From: Phyllis Wu Date: Mon, 13 Apr 2026 14:27:26 -0400 Subject: [PATCH 2/6] fix: use URL hostname check instead of substring match for accounts.shopify.com --- packages/e2e/setup/global-auth.ts | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/packages/e2e/setup/global-auth.ts b/packages/e2e/setup/global-auth.ts index 18d5e681dc..9373ec1b39 100644 --- a/packages/e2e/setup/global-auth.ts +++ b/packages/e2e/setup/global-auth.ts @@ -17,6 +17,15 @@ import {chromium} from '@playwright/test' import * as path from 'path' import * as fs from 'fs' +function isAccountsShopifyUrl(rawUrl: string): boolean { + try { + return new URL(rawUrl).hostname === 'accounts.shopify.com' + // eslint-disable-next-line no-catch-all/no-catch-all + } catch { + return false + } +} + export default async function globalSetup() { const email = process.env.E2E_ACCOUNT_EMAIL const password = process.env.E2E_ACCOUNT_PASSWORD @@ -105,7 +114,7 @@ export default async function globalSetup() { await page.waitForTimeout(BROWSER_TIMEOUT.medium) // Handle account picker if shown - if (page.url().includes('accounts.shopify.com')) { + if (isAccountsShopifyUrl(page.url())) { const accountButton = page.locator(`text=${email}`).first() if (await accountButton.isVisible({timeout: BROWSER_TIMEOUT.long}).catch(() => false)) { await accountButton.click() @@ -117,7 +126,7 @@ export default async function globalSetup() { await page.goto(`https://dev.shopify.com/dashboard/${orgId}/apps`, {waitUntil: 'domcontentloaded'}) await page.waitForTimeout(BROWSER_TIMEOUT.medium) - if (page.url().includes('accounts.shopify.com')) { + if (isAccountsShopifyUrl(page.url())) { const accountButton = page.locator(`text=${email}`).first() if (await accountButton.isVisible({timeout: BROWSER_TIMEOUT.long}).catch(() => false)) { await accountButton.click() From ea083c7a8038ae383125f11d501a6c1a12ff8eda Mon Sep 17 00:00:00 2001 From: Phyllis Wu Date: Mon, 13 Apr 2026 15:13:24 -0400 Subject: [PATCH 3/6] replace custom copyDirSync with fs.cpSync --- packages/e2e/setup/auth.ts | 24 ++++-------------------- 1 file changed, 4 insertions(+), 20 deletions(-) diff --git a/packages/e2e/setup/auth.ts b/packages/e2e/setup/auth.ts index f1ac0c93ca..b87468b2a7 100644 --- a/packages/e2e/setup/auth.ts +++ b/packages/e2e/setup/auth.ts @@ -7,26 +7,10 @@ import {waitForText} from '../helpers/wait-for-text.js' import {completeLogin} from '../helpers/browser-login.js' import {execa} from 'execa' import * as fs from 'fs' -import * as path from 'path' // eslint-disable-next-line @typescript-eslint/no-explicit-any const log = {log: (_ctx: any, msg: string) => globalLog('auth', msg)} -/** - * Copy directory contents recursively. - */ -function copyDirSync(src: string, dest: string): void { - fs.mkdirSync(dest, {recursive: true}) - for (const entry of fs.readdirSync(src, {withFileTypes: true})) { - const srcPath = path.join(src, entry.name) - const destPath = path.join(dest, entry.name) - if (entry.isDirectory()) { - copyDirSync(srcPath, destPath) - } else { - fs.copyFileSync(srcPath, destPath) - } - } -} /** * Worker-scoped fixture that provides an authenticated CLI session. @@ -57,10 +41,10 @@ export const authFixture = browserFixture.extend<{}, {authLogin: void}>({ // Copy pre-authenticated session from global setup log.log(env, 'copying session from global setup') - copyDirSync(authConfigDir, env.processEnv.XDG_CONFIG_HOME!) - copyDirSync(authDataDir, env.processEnv.XDG_DATA_HOME!) - copyDirSync(authStateDir, env.processEnv.XDG_STATE_HOME!) - copyDirSync(authCacheDir, env.processEnv.XDG_CACHE_HOME!) + fs.cpSync(authConfigDir, env.processEnv.XDG_CONFIG_HOME!, {recursive: true}) + fs.cpSync(authDataDir, env.processEnv.XDG_DATA_HOME!, {recursive: true}) + fs.cpSync(authStateDir, env.processEnv.XDG_STATE_HOME!, {recursive: true}) + fs.cpSync(authCacheDir, env.processEnv.XDG_CACHE_HOME!, {recursive: true}) await use() return From cf3afe93dce0db29d7067d1fc169cc627924b934 Mon Sep 17 00:00:00 2001 From: Phyllis Wu Date: Mon, 13 Apr 2026 15:18:09 -0400 Subject: [PATCH 4/6] remove redundant blank space --- packages/e2e/setup/auth.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/e2e/setup/auth.ts b/packages/e2e/setup/auth.ts index b87468b2a7..3e286b9486 100644 --- a/packages/e2e/setup/auth.ts +++ b/packages/e2e/setup/auth.ts @@ -51,7 +51,7 @@ export const authFixture = browserFixture.extend<{}, {authLogin: void}>({ } // Fallback: run auth login directly (single-worker / no global setup) - log.log(env, ' authenticating automatically') + log.log(env, 'authenticating automatically') await execa('node', [executables.cli, 'auth', 'logout'], { env: env.processEnv, From 2cc2eb9bb74f90425e72eefc953a6c9174efe585 Mon Sep 17 00:00:00 2001 From: Phyllis Wu Date: Mon, 13 Apr 2026 15:37:10 -0400 Subject: [PATCH 5/6] use the same directories.root to keep temp files in one place --- packages/e2e/setup/auth.ts | 2 -- packages/e2e/setup/global-auth.ts | 6 +++--- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/e2e/setup/auth.ts b/packages/e2e/setup/auth.ts index 3e286b9486..21bbe65151 100644 --- a/packages/e2e/setup/auth.ts +++ b/packages/e2e/setup/auth.ts @@ -1,4 +1,3 @@ -/* eslint-disable no-restricted-imports */ import {browserFixture} from './browser.js' import {CLI_TIMEOUT, BROWSER_TIMEOUT} from './constants.js' import {globalLog, executables} from './env.js' @@ -11,7 +10,6 @@ import * as fs from 'fs' // eslint-disable-next-line @typescript-eslint/no-explicit-any const log = {log: (_ctx: any, msg: string) => globalLog('auth', msg)} - /** * Worker-scoped fixture that provides an authenticated CLI session. * diff --git a/packages/e2e/setup/global-auth.ts b/packages/e2e/setup/global-auth.ts index 9373ec1b39..d09472522b 100644 --- a/packages/e2e/setup/global-auth.ts +++ b/packages/e2e/setup/global-auth.ts @@ -6,8 +6,8 @@ * into its own isolated XDG dirs. */ -/* eslint-disable no-restricted-imports, @shopify/cli/no-process-cwd */ -import {createIsolatedEnv, executables, globalLog} from './env.js' +/* eslint-disable no-restricted-imports */ +import {createIsolatedEnv, directories, executables, globalLog} from './env.js' import {CLI_TIMEOUT, BROWSER_TIMEOUT} from './constants.js' import {stripAnsi} from '../helpers/strip-ansi.js' import {waitForText} from '../helpers/wait-for-text.js' @@ -36,7 +36,7 @@ export default async function globalSetup() { globalLog('auth', 'global setup starting') // Create a temp dir for the auth session - const tmpBase = process.env.E2E_TEMP_DIR ?? path.join(process.cwd(), '.e2e-tmp') + const tmpBase = process.env.E2E_TEMP_DIR ?? path.join(directories.root, '.e2e-tmp') fs.mkdirSync(tmpBase, {recursive: true}) const {xdgEnv} = createIsolatedEnv(tmpBase) From 3c8cf0e9412d3613901812ce15324ea523b6a351 Mon Sep 17 00:00:00 2001 From: Phyllis Wu Date: Tue, 14 Apr 2026 12:36:44 -0400 Subject: [PATCH 6/6] fix: validate global auth artifacts and ensure resource cleanup --- packages/e2e/setup/auth.ts | 9 +++ packages/e2e/setup/browser.ts | 4 +- packages/e2e/setup/global-auth.ts | 114 ++++++++++++++++-------------- 3 files changed, 72 insertions(+), 55 deletions(-) diff --git a/packages/e2e/setup/auth.ts b/packages/e2e/setup/auth.ts index 21bbe65151..e4b88992db 100644 --- a/packages/e2e/setup/auth.ts +++ b/packages/e2e/setup/auth.ts @@ -39,6 +39,15 @@ export const authFixture = browserFixture.extend<{}, {authLogin: void}>({ // Copy pre-authenticated session from global setup log.log(env, 'copying session from global setup') + if ( + !fs.existsSync(authConfigDir) || + !fs.existsSync(authDataDir) || + !fs.existsSync(authStateDir) || + !fs.existsSync(authCacheDir) + ) { + throw new Error('Global auth dirs missing — global setup may not have completed successfully') + } + fs.cpSync(authConfigDir, env.processEnv.XDG_CONFIG_HOME!, {recursive: true}) fs.cpSync(authDataDir, env.processEnv.XDG_DATA_HOME!, {recursive: true}) fs.cpSync(authStateDir, env.processEnv.XDG_STATE_HOME!, {recursive: true}) diff --git a/packages/e2e/setup/browser.ts b/packages/e2e/setup/browser.ts index e0270fc711..af9814eff7 100644 --- a/packages/e2e/setup/browser.ts +++ b/packages/e2e/setup/browser.ts @@ -1,6 +1,7 @@ import {cliFixture} from './cli.js' import {BROWSER_TIMEOUT} from './constants.js' import {chromium, type Page} from '@playwright/test' +import * as fs from 'fs' // --------------------------------------------------------------------------- // Shared browser context type @@ -29,11 +30,12 @@ export const browserFixture = cliFixture.extend<{}, {browserPage: Page}>({ async ({}, use) => { const browser = await chromium.launch({headless: !process.env.E2E_HEADED}) const storageStatePath = process.env.E2E_BROWSER_STATE_PATH + const hasValidStorageState = storageStatePath && fs.existsSync(storageStatePath) const context = await browser.newContext({ extraHTTPHeaders: { 'X-Shopify-Loadtest-Bf8d22e7-120e-4b5b-906c-39ca9d5499a9': 'true', }, - ...(storageStatePath ? {storageState: storageStatePath} : {}), + ...(hasValidStorageState ? {storageState: storageStatePath} : {}), }) context.setDefaultTimeout(BROWSER_TIMEOUT.max) context.setDefaultNavigationTimeout(BROWSER_TIMEOUT.max) diff --git a/packages/e2e/setup/global-auth.ts b/packages/e2e/setup/global-auth.ts index d09472522b..5abad32523 100644 --- a/packages/e2e/setup/global-auth.ts +++ b/packages/e2e/setup/global-auth.ts @@ -78,71 +78,77 @@ export default async function globalSetup() { if (debug) process.stdout.write(data) }) - await waitForText(() => output, 'Open this link to start the auth process', CLI_TIMEOUT.short) - - const stripped = stripAnsi(output) - const urlMatch = stripped.match(/https:\/\/accounts\.shopify\.com\S+/) - if (!urlMatch) { - throw new Error(`[e2e] global-auth: could not find login URL in output:\n${stripped}`) - } - - // Complete login in a headless browser - const browser = await chromium.launch({headless: !process.env.E2E_HEADED}) - const context = await browser.newContext({ - extraHTTPHeaders: { - 'X-Shopify-Loadtest-Bf8d22e7-120e-4b5b-906c-39ca9d5499a9': 'true', - }, - }) - const page = await context.newPage() - - await completeLogin(page, urlMatch[0], email, password) + const storageStatePath = path.join(tmpBase, 'browser-storage-state.json') - await waitForText(() => output, 'Logged in', BROWSER_TIMEOUT.max) try { - ptyProcess.kill() - // eslint-disable-next-line no-catch-all/no-catch-all - } catch (_error) { - // Process may already be dead - } + await waitForText(() => output, 'Open this link to start the auth process', CLI_TIMEOUT.short) - // Visit admin.shopify.com and dev.shopify.com to establish session cookies - // (completeLogin only authenticates on accounts.shopify.com) - const orgId = (process.env.E2E_ORG_ID ?? '').trim() - if (orgId) { - // Establish admin.shopify.com cookies - await page.goto('https://admin.shopify.com/', {waitUntil: 'domcontentloaded'}) - await page.waitForTimeout(BROWSER_TIMEOUT.medium) - - // Handle account picker if shown - if (isAccountsShopifyUrl(page.url())) { - const accountButton = page.locator(`text=${email}`).first() - if (await accountButton.isVisible({timeout: BROWSER_TIMEOUT.long}).catch(() => false)) { - await accountButton.click() - await page.waitForTimeout(BROWSER_TIMEOUT.medium) - } + const stripped = stripAnsi(output) + const urlMatch = stripped.match(/https:\/\/accounts\.shopify\.com\S+/) + if (!urlMatch) { + throw new Error(`[e2e] global-auth: could not find login URL in output:\n${stripped}`) } - // Establish dev.shopify.com cookies - await page.goto(`https://dev.shopify.com/dashboard/${orgId}/apps`, {waitUntil: 'domcontentloaded'}) - await page.waitForTimeout(BROWSER_TIMEOUT.medium) + // Complete login in a headless browser + const browser = await chromium.launch({headless: !process.env.E2E_HEADED}) + try { + const context = await browser.newContext({ + extraHTTPHeaders: { + 'X-Shopify-Loadtest-Bf8d22e7-120e-4b5b-906c-39ca9d5499a9': 'true', + }, + }) + const page = await context.newPage() + + await completeLogin(page, urlMatch[0], email, password) + + await waitForText(() => output, 'Logged in', BROWSER_TIMEOUT.max) + + // Visit admin.shopify.com and dev.shopify.com to establish session cookies + // (completeLogin only authenticates on accounts.shopify.com) + const orgId = (process.env.E2E_ORG_ID ?? '').trim() + if (orgId) { + // Establish admin.shopify.com cookies + await page.goto('https://admin.shopify.com/', {waitUntil: 'domcontentloaded'}) + await page.waitForTimeout(BROWSER_TIMEOUT.medium) - if (isAccountsShopifyUrl(page.url())) { - const accountButton = page.locator(`text=${email}`).first() - if (await accountButton.isVisible({timeout: BROWSER_TIMEOUT.long}).catch(() => false)) { - await accountButton.click() + // Handle account picker if shown + if (isAccountsShopifyUrl(page.url())) { + const accountButton = page.locator(`text=${email}`).first() + if (await accountButton.isVisible({timeout: BROWSER_TIMEOUT.long}).catch(() => false)) { + await accountButton.click() + await page.waitForTimeout(BROWSER_TIMEOUT.medium) + } + } + + // Establish dev.shopify.com cookies + await page.goto(`https://dev.shopify.com/dashboard/${orgId}/apps`, {waitUntil: 'domcontentloaded'}) await page.waitForTimeout(BROWSER_TIMEOUT.medium) + + if (isAccountsShopifyUrl(page.url())) { + const accountButton = page.locator(`text=${email}`).first() + if (await accountButton.isVisible({timeout: BROWSER_TIMEOUT.long}).catch(() => false)) { + await accountButton.click() + await page.waitForTimeout(BROWSER_TIMEOUT.medium) + } + } + + globalLog('auth', 'browser sessions established for admin + dev dashboard') } - } - globalLog('auth', 'browser sessions established for admin + dev dashboard') + // Save browser cookies/storage so workers can reuse the session + await context.storageState({path: storageStatePath}) + } finally { + await browser.close() + } + } finally { + try { + ptyProcess.kill() + // eslint-disable-next-line no-catch-all/no-catch-all + } catch (_error) { + // Process may already be dead + } } - // Save browser cookies/storage so workers can reuse the session - // Now includes cookies for both accounts.shopify.com AND admin.shopify.com - const storageStatePath = path.join(tmpBase, 'browser-storage-state.json') - await context.storageState({path: storageStatePath}) - await browser.close() - // Store paths so workers can copy CLI auth + load browser state /* eslint-disable require-atomic-updates */ process.env.E2E_AUTH_CONFIG_DIR = xdgEnv.XDG_CONFIG_HOME