Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 25 additions & 1 deletion .github/workflows/tests-pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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() }}
Expand Down
19 changes: 17 additions & 2 deletions packages/e2e/playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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'],
},
],
})
18 changes: 18 additions & 0 deletions packages/e2e/setup/auth-state.ts
Original file line number Diff line number Diff line change
@@ -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'),
},
}
}
36 changes: 12 additions & 24 deletions packages/e2e/setup/auth.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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
Expand All @@ -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
Expand Down
7 changes: 5 additions & 2 deletions packages/e2e/setup/browser.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -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} : {}),
})
Expand Down
6 changes: 6 additions & 0 deletions packages/e2e/setup/global-auth.setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import {prepareGlobalAuth} from './global-auth.js'
import {test as setup} from '@playwright/test'

setup('authenticate', async () => {
await prepareGlobalAuth()
})
36 changes: 9 additions & 27 deletions packages/e2e/setup/global-auth.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,20 @@
/**
* 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'
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 {
Expand All @@ -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

Expand All @@ -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,
Expand Down Expand Up @@ -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}`)
}

Expand Down
Loading