diff --git a/.github/workflows/tests-pr.yml b/.github/workflows/tests-pr.yml index 3165c915e66..9a2d043e9c4 100644 --- a/.github/workflows/tests-pr.yml +++ b/.github/workflows/tests-pr.yml @@ -221,6 +221,30 @@ jobs: if: needs.unit-tests.result != 'success' run: exit 1 + e2e-local-tests: + name: 'E2E local tests' + needs: [type-check, bundle] + runs-on: ubuntu-latest + timeout-minutes: 20 + continue-on-error: true + steps: + - uses: actions/checkout@v6 + with: + repository: ${{ github.event.pull_request.head.repo.full_name || github.event.repository.full_name }} + ref: ${{ github.event.pull_request.head.ref || github.event.merge_group.head_ref }} + fetch-depth: 1 + - name: Setup deps + uses: ./.github/actions/setup-cli-deps + with: + node-version: ${{ env.PLAYWRIGHT_NODE_VERSION }} + - name: Build + run: pnpm nx run-many --all --target=build --output-style=stream + - name: Rebuild node-pty + run: pnpm rebuild node-pty + - name: Run local E2E tests + working-directory: packages/e2e + run: pnpm exec playwright test --project local + e2e-tests: name: "E2E tests (shard ${{ matrix.shard }})" if: github.event_name == 'merge_group' || github.event.pull_request.head.repo.full_name == github.repository @@ -259,7 +283,7 @@ jobs: E2E_STORE_FQDN: ${{ secrets.E2E_STORE_FQDN }} E2E_ORG_ID: ${{ secrets.E2E_ORG_ID }} E2E_LOADTEST_HEADER: ${{ secrets.E2E_LOADTEST_HEADER }} - run: pnpm exec playwright test --shard ${{ matrix.shard }} + run: pnpm exec playwright test --project remote --shard ${{ matrix.shard }} - name: Upload Playwright report uses: actions/upload-artifact@v4 if: ${{ !cancelled() }} diff --git a/packages/e2e/playwright.config.ts b/packages/e2e/playwright.config.ts index e10b229cd3d..a6592818b3e 100644 --- a/packages/e2e/playwright.config.ts +++ b/packages/e2e/playwright.config.ts @@ -8,8 +8,7 @@ config() const isCI = Boolean(process.env.CI) export default defineConfig({ - globalSetup: './setup/global-auth.ts', - testDir: './tests', + testDir: '.', fullyParallel: true, forbidOnly: isCI, retries: 0, @@ -24,4 +23,20 @@ export default defineConfig({ screenshot: isCI ? 'on' : 'off', video: 'off', }, + projects: [ + { + name: 'local', + testMatch: ['tests/smoke.spec.ts', 'tests/smoke-pty.spec.ts', 'tests/fixture-toml.spec.ts'], + }, + { + name: 'remote-auth', + testMatch: 'setup/global-auth.setup.ts', + }, + { + name: 'remote', + testMatch: 'tests/*.spec.ts', + testIgnore: ['tests/smoke.spec.ts', 'tests/smoke-pty.spec.ts', 'tests/fixture-toml.spec.ts'], + dependencies: ['remote-auth'], + }, + ], }) diff --git a/packages/e2e/setup/auth-state.ts b/packages/e2e/setup/auth-state.ts new file mode 100644 index 00000000000..6dd30322e1a --- /dev/null +++ b/packages/e2e/setup/auth-state.ts @@ -0,0 +1,18 @@ +/* eslint-disable no-restricted-imports */ +import {directories} from './env.js' +import * as path from 'path' + +export function authStatePaths() { + const authDir = path.join(process.env.E2E_TEMP_DIR ?? path.join(directories.root, '.e2e-tmp'), 'global-auth') + + return { + authDir, + storageStatePath: path.join(authDir, 'browser-storage-state.json'), + xdgEnv: { + XDG_DATA_HOME: path.join(authDir, 'XDG_DATA_HOME'), + XDG_CONFIG_HOME: path.join(authDir, 'XDG_CONFIG_HOME'), + XDG_STATE_HOME: path.join(authDir, 'XDG_STATE_HOME'), + XDG_CACHE_HOME: path.join(authDir, 'XDG_CACHE_HOME'), + }, + } +} diff --git a/packages/e2e/setup/auth.ts b/packages/e2e/setup/auth.ts index 93caef60194..fa87c1663db 100644 --- a/packages/e2e/setup/auth.ts +++ b/packages/e2e/setup/auth.ts @@ -1,4 +1,5 @@ import {browserFixture} from './browser.js' +import {authStatePaths} from './auth-state.js' import {CLI_TIMEOUT, BROWSER_TIMEOUT} from './constants.js' import {globalLog, executables} from './env.js' import {stripAnsi} from '../helpers/strip-ansi.js' @@ -13,8 +14,8 @@ const log = {log: (_ctx: any, msg: string) => globalLog('auth', msg)} /** * Worker-scoped fixture that provides an authenticated CLI session. * - * If globalSetup already ran auth (E2E_AUTH_CONFIG_DIR is set), copies the - * pre-authenticated session files into this worker's isolated XDG dirs. + * If the remote project's auth setup completed, 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 @@ -30,28 +31,15 @@ export const authFixture = browserFixture.extend<{}, {authLogin: void}>({ return } - 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') - - 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}) - fs.cpSync(authCacheDir, env.processEnv.XDG_CACHE_HOME!, {recursive: true}) + const {xdgEnv: authXdgEnv} = authStatePaths() + const authDirs = Object.values(authXdgEnv) + + if (authDirs.every((directory) => fs.existsSync(directory))) { + log.log(env, 'copying session from auth setup') + fs.cpSync(authXdgEnv.XDG_CONFIG_HOME, env.processEnv.XDG_CONFIG_HOME!, {recursive: true}) + fs.cpSync(authXdgEnv.XDG_DATA_HOME, env.processEnv.XDG_DATA_HOME!, {recursive: true}) + fs.cpSync(authXdgEnv.XDG_STATE_HOME, env.processEnv.XDG_STATE_HOME!, {recursive: true}) + fs.cpSync(authXdgEnv.XDG_CACHE_HOME, env.processEnv.XDG_CACHE_HOME!, {recursive: true}) await use() return diff --git a/packages/e2e/setup/browser.ts b/packages/e2e/setup/browser.ts index 92e9b7be2d7..e18df20d34e 100644 --- a/packages/e2e/setup/browser.ts +++ b/packages/e2e/setup/browser.ts @@ -1,4 +1,5 @@ import {cliFixture} from './cli.js' +import {authStatePaths} from './auth-state.js' import {BROWSER_TIMEOUT} from './constants.js' import {addLoadtestHeader} from '../helpers/loadtest-header.js' import {chromium, type Locator, type Page} from '@playwright/test' @@ -57,8 +58,10 @@ 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 hasValidStorageState = storageStatePath && fs.existsSync(storageStatePath) + const storageStatePath = authStatePaths().storageStatePath + const hasValidStorageState = Boolean( + process.env.E2E_ACCOUNT_EMAIL && process.env.E2E_ACCOUNT_PASSWORD && fs.existsSync(storageStatePath), + ) const context = await browser.newContext({ ...(hasValidStorageState ? {storageState: storageStatePath} : {}), }) diff --git a/packages/e2e/setup/global-auth.setup.ts b/packages/e2e/setup/global-auth.setup.ts new file mode 100644 index 00000000000..a7d6654080a --- /dev/null +++ b/packages/e2e/setup/global-auth.setup.ts @@ -0,0 +1,6 @@ +import {prepareGlobalAuth} from './global-auth.js' +import {test as setup} from '@playwright/test' + +setup('authenticate', async () => { + await prepareGlobalAuth() +}) diff --git a/packages/e2e/setup/global-auth.ts b/packages/e2e/setup/global-auth.ts index d232aa611f7..ccfb8200483 100644 --- a/packages/e2e/setup/global-auth.ts +++ b/packages/e2e/setup/global-auth.ts @@ -1,13 +1,13 @@ /** - * Playwright globalSetup — authenticates once before any workers start. + * Authenticates once before the remote Playwright project starts. * * Auth artifacts are stored in a stable `global-auth/` dir. Workers copy - * the session files into their own isolated XDG dirs via E2E_AUTH_* env vars. + * the session files into their own isolated XDG dirs. */ -/* eslint-disable no-restricted-imports */ import {isVisibleWithin} from './browser.js' -import {directories, executables, globalLog} from './env.js' +import {executables, globalLog} from './env.js' +import {authStatePaths} from './auth-state.js' import {CLI_TIMEOUT, BROWSER_TIMEOUT} from './constants.js' import {stripAnsi} from '../helpers/strip-ansi.js' import {waitForText} from '../helpers/wait-for-text.js' @@ -15,7 +15,6 @@ import {completeLogin} from '../helpers/browser-login.js' import {addLoadtestHeader} from '../helpers/loadtest-header.js' import {execa} from 'execa' import {chromium, type Page} from '@playwright/test' -import * as path from 'path' import * as fs from 'fs' function isAccountsShopifyUrl(rawUrl: string): boolean { @@ -27,7 +26,10 @@ function isAccountsShopifyUrl(rawUrl: string): boolean { } } -export default async function globalSetup() { +export async function prepareGlobalAuth() { + const {authDir, storageStatePath, xdgEnv} = authStatePaths() + fs.rmSync(authDir, {recursive: true, force: true}) + const email = process.env.E2E_ACCOUNT_EMAIL const password = process.env.E2E_ACCOUNT_PASSWORD @@ -36,18 +38,7 @@ export default async function globalSetup() { const debug = process.env.DEBUG === '1' globalLog('auth', 'global setup starting') - // All auth artifacts stored in a stable dir - const tmpBase = process.env.E2E_TEMP_DIR ?? path.join(directories.root, '.e2e-tmp') - fs.mkdirSync(tmpBase, {recursive: true}) - const authDir = path.join(tmpBase, 'global-auth') - const storageStatePath = path.join(authDir, 'browser-storage-state.json') - - const xdgEnv = { - XDG_DATA_HOME: path.join(authDir, 'XDG_DATA_HOME'), - XDG_CONFIG_HOME: path.join(authDir, 'XDG_CONFIG_HOME'), - XDG_STATE_HOME: path.join(authDir, 'XDG_STATE_HOME'), - XDG_CACHE_HOME: path.join(authDir, 'XDG_CACHE_HOME'), - } + fs.mkdirSync(authDir, {recursive: true}) const processEnv: NodeJS.ProcessEnv = { ...process.env, @@ -142,15 +133,6 @@ export default async function globalSetup() { } } - // 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}`) }