|
| 1 | +/** |
| 2 | + * Playwright globalSetup — authenticates once before any workers start. |
| 3 | + * |
| 4 | + * Performs CLI `auth login` with a dedicated temp dir, then stores the |
| 5 | + * path in E2E_AUTH_CONFIG_DIR so each worker can copy the session files |
| 6 | + * into its own isolated XDG dirs. |
| 7 | + */ |
| 8 | + |
| 9 | +/* eslint-disable no-restricted-imports, @shopify/cli/no-process-cwd */ |
| 10 | +import {createIsolatedEnv, executables, globalLog} from './env.js' |
| 11 | +import {CLI_TIMEOUT, BROWSER_TIMEOUT} from './constants.js' |
| 12 | +import {stripAnsi} from '../helpers/strip-ansi.js' |
| 13 | +import {waitForText} from '../helpers/wait-for-text.js' |
| 14 | +import {completeLogin} from '../helpers/browser-login.js' |
| 15 | +import {execa} from 'execa' |
| 16 | +import {chromium} from '@playwright/test' |
| 17 | +import * as path from 'path' |
| 18 | +import * as fs from 'fs' |
| 19 | + |
| 20 | +export default async function globalSetup() { |
| 21 | + const email = process.env.E2E_ACCOUNT_EMAIL |
| 22 | + const password = process.env.E2E_ACCOUNT_PASSWORD |
| 23 | + |
| 24 | + if (!email || !password) return |
| 25 | + |
| 26 | + const debug = process.env.DEBUG === '1' |
| 27 | + globalLog('auth', 'global setup starting') |
| 28 | + |
| 29 | + // Create a temp dir for the auth session |
| 30 | + const tmpBase = process.env.E2E_TEMP_DIR ?? path.join(process.cwd(), '.e2e-tmp') |
| 31 | + fs.mkdirSync(tmpBase, {recursive: true}) |
| 32 | + const {xdgEnv} = createIsolatedEnv(tmpBase) |
| 33 | + |
| 34 | + const processEnv: NodeJS.ProcessEnv = { |
| 35 | + ...process.env, |
| 36 | + ...xdgEnv, |
| 37 | + SHOPIFY_RUN_AS_USER: '0', |
| 38 | + NODE_OPTIONS: '', |
| 39 | + CI: '1', |
| 40 | + SHOPIFY_CLI_1P_DEV: undefined, |
| 41 | + SHOPIFY_FLAG_CLIENT_ID: undefined, |
| 42 | + } |
| 43 | + |
| 44 | + // Clear any existing session |
| 45 | + await execa('node', [executables.cli, 'auth', 'logout'], { |
| 46 | + env: processEnv, |
| 47 | + reject: false, |
| 48 | + }) |
| 49 | + |
| 50 | + // Spawn auth login via PTY |
| 51 | + const nodePty = await import('node-pty') |
| 52 | + const spawnEnv: {[key: string]: string} = {} |
| 53 | + for (const [key, value] of Object.entries(processEnv)) { |
| 54 | + if (value !== undefined) spawnEnv[key] = value |
| 55 | + } |
| 56 | + spawnEnv.CI = '' |
| 57 | + spawnEnv.CODESPACES = 'true' |
| 58 | + |
| 59 | + const ptyProcess = nodePty.spawn('node', [executables.cli, 'auth', 'login'], { |
| 60 | + name: 'xterm-color', |
| 61 | + cols: 120, |
| 62 | + rows: 30, |
| 63 | + env: spawnEnv, |
| 64 | + }) |
| 65 | + |
| 66 | + let output = '' |
| 67 | + ptyProcess.onData((data: string) => { |
| 68 | + output += data |
| 69 | + if (debug) process.stdout.write(data) |
| 70 | + }) |
| 71 | + |
| 72 | + await waitForText(() => output, 'Open this link to start the auth process', CLI_TIMEOUT.short) |
| 73 | + |
| 74 | + const stripped = stripAnsi(output) |
| 75 | + const urlMatch = stripped.match(/https:\/\/accounts\.shopify\.com\S+/) |
| 76 | + if (!urlMatch) { |
| 77 | + throw new Error(`[e2e] global-auth: could not find login URL in output:\n${stripped}`) |
| 78 | + } |
| 79 | + |
| 80 | + // Complete login in a headless browser |
| 81 | + const browser = await chromium.launch({headless: !process.env.E2E_HEADED}) |
| 82 | + const context = await browser.newContext({ |
| 83 | + extraHTTPHeaders: { |
| 84 | + 'X-Shopify-Loadtest-Bf8d22e7-120e-4b5b-906c-39ca9d5499a9': 'true', |
| 85 | + }, |
| 86 | + }) |
| 87 | + const page = await context.newPage() |
| 88 | + |
| 89 | + await completeLogin(page, urlMatch[0], email, password) |
| 90 | + |
| 91 | + await waitForText(() => output, 'Logged in', BROWSER_TIMEOUT.max) |
| 92 | + try { |
| 93 | + ptyProcess.kill() |
| 94 | + // eslint-disable-next-line no-catch-all/no-catch-all |
| 95 | + } catch (_error) { |
| 96 | + // Process may already be dead |
| 97 | + } |
| 98 | + |
| 99 | + // Visit admin.shopify.com and dev.shopify.com to establish session cookies |
| 100 | + // (completeLogin only authenticates on accounts.shopify.com) |
| 101 | + const orgId = (process.env.E2E_ORG_ID ?? '').trim() |
| 102 | + if (orgId) { |
| 103 | + // Establish admin.shopify.com cookies |
| 104 | + await page.goto('https://admin.shopify.com/', {waitUntil: 'domcontentloaded'}) |
| 105 | + await page.waitForTimeout(BROWSER_TIMEOUT.medium) |
| 106 | + |
| 107 | + // Handle account picker if shown |
| 108 | + if (page.url().includes('accounts.shopify.com')) { |
| 109 | + const accountButton = page.locator(`text=${email}`).first() |
| 110 | + if (await accountButton.isVisible({timeout: BROWSER_TIMEOUT.long}).catch(() => false)) { |
| 111 | + await accountButton.click() |
| 112 | + await page.waitForTimeout(BROWSER_TIMEOUT.medium) |
| 113 | + } |
| 114 | + } |
| 115 | + |
| 116 | + // Establish dev.shopify.com cookies |
| 117 | + await page.goto(`https://dev.shopify.com/dashboard/${orgId}/apps`, {waitUntil: 'domcontentloaded'}) |
| 118 | + await page.waitForTimeout(BROWSER_TIMEOUT.medium) |
| 119 | + |
| 120 | + if (page.url().includes('accounts.shopify.com')) { |
| 121 | + const accountButton = page.locator(`text=${email}`).first() |
| 122 | + if (await accountButton.isVisible({timeout: BROWSER_TIMEOUT.long}).catch(() => false)) { |
| 123 | + await accountButton.click() |
| 124 | + await page.waitForTimeout(BROWSER_TIMEOUT.medium) |
| 125 | + } |
| 126 | + } |
| 127 | + |
| 128 | + globalLog('auth', 'browser sessions established for admin + dev dashboard') |
| 129 | + } |
| 130 | + |
| 131 | + // Save browser cookies/storage so workers can reuse the session |
| 132 | + // Now includes cookies for both accounts.shopify.com AND admin.shopify.com |
| 133 | + const storageStatePath = path.join(tmpBase, 'browser-storage-state.json') |
| 134 | + await context.storageState({path: storageStatePath}) |
| 135 | + await browser.close() |
| 136 | + |
| 137 | + // Store paths so workers can copy CLI auth + load browser state |
| 138 | + /* eslint-disable require-atomic-updates */ |
| 139 | + process.env.E2E_AUTH_CONFIG_DIR = xdgEnv.XDG_CONFIG_HOME |
| 140 | + process.env.E2E_AUTH_DATA_DIR = xdgEnv.XDG_DATA_HOME |
| 141 | + process.env.E2E_AUTH_STATE_DIR = xdgEnv.XDG_STATE_HOME |
| 142 | + process.env.E2E_AUTH_CACHE_DIR = xdgEnv.XDG_CACHE_HOME |
| 143 | + process.env.E2E_BROWSER_STATE_PATH = storageStatePath |
| 144 | + /* eslint-enable require-atomic-updates */ |
| 145 | + |
| 146 | + globalLog('auth', `global setup done, config at ${xdgEnv.XDG_CONFIG_HOME}`) |
| 147 | +} |
0 commit comments