11/**
22 * Playwright globalSetup — authenticates once before any workers start.
33 *
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.
4+ * Uses a stable `global-auth/` dir for session caching across runs.
5+ * On subsequent runs, validates the cached browser session before
6+ * re-authenticating. Workers copy the session files into their own
7+ * isolated XDG dirs via E2E_AUTH_* env vars.
78 */
89
910/* eslint-disable no-restricted-imports */
10- import { createIsolatedEnv , directories , executables , globalLog } from './env.js'
11+ import { directories , executables , globalLog } from './env.js'
1112import { CLI_TIMEOUT , BROWSER_TIMEOUT } from './constants.js'
1213import { stripAnsi } from '../helpers/strip-ansi.js'
1314import { waitForText } from '../helpers/wait-for-text.js'
1415import { completeLogin } from '../helpers/browser-login.js'
1516import { execa } from 'execa'
16- import { chromium } from '@playwright/test'
17+ import { chromium , type Page } from '@playwright/test'
1718import * as path from 'path'
1819import * as fs from 'fs'
1920
@@ -35,10 +36,18 @@ export default async function globalSetup() {
3536 const debug = process . env . DEBUG === '1'
3637 globalLog ( 'auth' , 'global setup starting' )
3738
38- // Create a temp dir for the auth session
39+ // Use a stable auth dir (reused across runs for session caching)
3940 const tmpBase = process . env . E2E_TEMP_DIR ?? path . join ( directories . root , '.e2e-tmp' )
4041 fs . mkdirSync ( tmpBase , { recursive : true } )
41- const { xdgEnv} = createIsolatedEnv ( tmpBase )
42+ const authDir = path . join ( tmpBase , 'global-auth' )
43+ const storageStatePath = path . join ( authDir , 'browser-storage-state.json' )
44+
45+ const xdgEnv = {
46+ XDG_DATA_HOME : path . join ( authDir , 'XDG_DATA_HOME' ) ,
47+ XDG_CONFIG_HOME : path . join ( authDir , 'XDG_CONFIG_HOME' ) ,
48+ XDG_STATE_HOME : path . join ( authDir , 'XDG_STATE_HOME' ) ,
49+ XDG_CACHE_HOME : path . join ( authDir , 'XDG_CACHE_HOME' ) ,
50+ }
4251
4352 const processEnv : NodeJS . ProcessEnv = {
4453 ...process . env ,
@@ -50,6 +59,34 @@ export default async function globalSetup() {
5059 SHOPIFY_FLAG_CLIENT_ID : undefined ,
5160 }
5261
62+ // Check if cached session from a previous run is still valid
63+ if ( fs . existsSync ( storageStatePath ) ) {
64+ const browser = await chromium . launch ( { headless : true } )
65+ try {
66+ const context = await browser . newContext ( { storageState : storageStatePath } )
67+ const page = await context . newPage ( )
68+ await page . goto ( 'https://admin.shopify.com/' , { waitUntil : 'domcontentloaded' , timeout : BROWSER_TIMEOUT . long } )
69+ if ( ! isAccountsShopifyUrl ( page . url ( ) ) ) {
70+ globalLog ( 'auth' , 'reusing cached session' )
71+ setAuthEnvVars ( xdgEnv , storageStatePath )
72+ return
73+ }
74+ // eslint-disable-next-line no-catch-all/no-catch-all
75+ } catch ( _error ) {
76+ // Browser check failed — fall through to re-authenticate
77+ } finally {
78+ await browser . close ( ) . catch ( ( ) => { } )
79+ }
80+ globalLog ( 'auth' , 'cached session expired, re-authenticating' )
81+ } else {
82+ globalLog ( 'auth' , 'no cached session found' )
83+ }
84+
85+ // Create fresh XDG dirs
86+ for ( const dir of Object . values ( xdgEnv ) ) {
87+ fs . mkdirSync ( dir , { recursive : true } )
88+ }
89+
5390 // Clear any existing session
5491 await execa ( 'node' , [ executables . cli , 'auth' , 'logout' ] , {
5592 env : processEnv ,
@@ -78,8 +115,6 @@ export default async function globalSetup() {
78115 if ( debug ) process . stdout . write ( data )
79116 } )
80117
81- const storageStatePath = path . join ( tmpBase , 'browser-storage-state.json' )
82-
83118 try {
84119 await waitForText ( ( ) => output , 'Open this link to start the auth process' , CLI_TIMEOUT . short )
85120
@@ -107,31 +142,8 @@ export default async function globalSetup() {
107142 // (completeLogin only authenticates on accounts.shopify.com)
108143 const orgId = ( process . env . E2E_ORG_ID ?? '' ) . trim ( )
109144 if ( orgId ) {
110- // Establish admin.shopify.com cookies
111- await page . goto ( 'https://admin.shopify.com/' , { waitUntil : 'domcontentloaded' } )
112- await page . waitForTimeout ( BROWSER_TIMEOUT . medium )
113-
114- // Handle account picker if shown
115- if ( isAccountsShopifyUrl ( page . url ( ) ) ) {
116- const accountButton = page . locator ( `text=${ email } ` ) . first ( )
117- if ( await accountButton . isVisible ( { timeout : BROWSER_TIMEOUT . long } ) . catch ( ( ) => false ) ) {
118- await accountButton . click ( )
119- await page . waitForTimeout ( BROWSER_TIMEOUT . medium )
120- }
121- }
122-
123- // Establish dev.shopify.com cookies
124- await page . goto ( `https://dev.shopify.com/dashboard/${ orgId } /apps` , { waitUntil : 'domcontentloaded' } )
125- await page . waitForTimeout ( BROWSER_TIMEOUT . medium )
126-
127- if ( isAccountsShopifyUrl ( page . url ( ) ) ) {
128- const accountButton = page . locator ( `text=${ email } ` ) . first ( )
129- if ( await accountButton . isVisible ( { timeout : BROWSER_TIMEOUT . long } ) . catch ( ( ) => false ) ) {
130- await accountButton . click ( )
131- await page . waitForTimeout ( BROWSER_TIMEOUT . medium )
132- }
133- }
134-
145+ await visitAndHandleAccountPicker ( page , 'https://admin.shopify.com/' , email )
146+ await visitAndHandleAccountPicker ( page , `https://dev.shopify.com/dashboard/${ orgId } /apps` , email )
135147 globalLog ( 'auth' , 'browser sessions established for admin + dev dashboard' )
136148 }
137149
@@ -149,14 +161,27 @@ export default async function globalSetup() {
149161 }
150162 }
151163
152- // Store paths so workers can copy CLI auth + load browser state
153- /* eslint-disable require-atomic-updates */
164+ setAuthEnvVars ( xdgEnv , storageStatePath )
165+ globalLog ( 'auth' , `global setup done, config at ${ xdgEnv . XDG_CONFIG_HOME } ` )
166+ }
167+
168+ /** Navigate to a URL and dismiss the account picker if it appears. */
169+ async function visitAndHandleAccountPicker ( page : Page , url : string , email : string ) {
170+ await page . goto ( url , { waitUntil : 'domcontentloaded' } )
171+ await page . waitForTimeout ( BROWSER_TIMEOUT . medium )
172+ if ( isAccountsShopifyUrl ( page . url ( ) ) ) {
173+ const accountButton = page . locator ( `text=${ email } ` ) . first ( )
174+ if ( await accountButton . isVisible ( { timeout : BROWSER_TIMEOUT . long } ) . catch ( ( ) => false ) ) {
175+ await accountButton . click ( )
176+ await page . waitForTimeout ( BROWSER_TIMEOUT . medium )
177+ }
178+ }
179+ }
180+
181+ function setAuthEnvVars ( xdgEnv : Record < string , string > , storageStatePath : string ) : void {
154182 process . env . E2E_AUTH_CONFIG_DIR = xdgEnv . XDG_CONFIG_HOME
155183 process . env . E2E_AUTH_DATA_DIR = xdgEnv . XDG_DATA_HOME
156184 process . env . E2E_AUTH_STATE_DIR = xdgEnv . XDG_STATE_HOME
157185 process . env . E2E_AUTH_CACHE_DIR = xdgEnv . XDG_CACHE_HOME
158186 process . env . E2E_BROWSER_STATE_PATH = storageStatePath
159- /* eslint-enable require-atomic-updates */
160-
161- globalLog ( 'auth' , `global setup done, config at ${ xdgEnv . XDG_CONFIG_HOME } ` )
162187}
0 commit comments