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,33 @@ 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+ } catch {
75+ // Browser check failed — fall through to re-authenticate
76+ } finally {
77+ await browser . close ( ) . catch ( ( ) => { } )
78+ }
79+ globalLog ( 'auth' , 'cached session expired, re-authenticating' )
80+ } else {
81+ globalLog ( 'auth' , 'no cached session found' )
82+ }
83+
84+ // Create fresh XDG dirs
85+ for ( const dir of Object . values ( xdgEnv ) ) {
86+ fs . mkdirSync ( dir , { recursive : true } )
87+ }
88+
5389 // Clear any existing session
5490 await execa ( 'node' , [ executables . cli , 'auth' , 'logout' ] , {
5591 env : processEnv ,
@@ -78,8 +114,6 @@ export default async function globalSetup() {
78114 if ( debug ) process . stdout . write ( data )
79115 } )
80116
81- const storageStatePath = path . join ( tmpBase , 'browser-storage-state.json' )
82-
83117 try {
84118 await waitForText ( ( ) => output , 'Open this link to start the auth process' , CLI_TIMEOUT . short )
85119
@@ -107,31 +141,8 @@ export default async function globalSetup() {
107141 // (completeLogin only authenticates on accounts.shopify.com)
108142 const orgId = ( process . env . E2E_ORG_ID ?? '' ) . trim ( )
109143 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-
144+ await visitAndHandleAccountPicker ( page , 'https://admin.shopify.com/' , email )
145+ await visitAndHandleAccountPicker ( page , `https://dev.shopify.com/dashboard/${ orgId } /apps` , email )
135146 globalLog ( 'auth' , 'browser sessions established for admin + dev dashboard' )
136147 }
137148
@@ -149,14 +160,29 @@ export default async function globalSetup() {
149160 }
150161 }
151162
152- // Store paths so workers can copy CLI auth + load browser state
163+ setAuthEnvVars ( xdgEnv , storageStatePath )
164+ globalLog ( 'auth' , `global setup done, config at ${ xdgEnv . XDG_CONFIG_HOME } ` )
165+ }
166+
167+ /** Navigate to a URL and dismiss the account picker if it appears. */
168+ async function visitAndHandleAccountPicker ( page : Page , url : string , email : string ) {
169+ await page . goto ( url , { waitUntil : 'domcontentloaded' } )
170+ await page . waitForTimeout ( BROWSER_TIMEOUT . medium )
171+ if ( isAccountsShopifyUrl ( page . url ( ) ) ) {
172+ const accountButton = page . locator ( `text=${ email } ` ) . first ( )
173+ if ( await accountButton . isVisible ( { timeout : BROWSER_TIMEOUT . long } ) . catch ( ( ) => false ) ) {
174+ await accountButton . click ( )
175+ await page . waitForTimeout ( BROWSER_TIMEOUT . medium )
176+ }
177+ }
178+ }
179+
180+ function setAuthEnvVars ( xdgEnv : Record < string , string > , storageStatePath : string ) : void {
153181 /* eslint-disable require-atomic-updates */
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
159187 /* eslint-enable require-atomic-updates */
160-
161- globalLog ( 'auth' , `global setup done, config at ${ xdgEnv . XDG_CONFIG_HOME } ` )
162188}
0 commit comments