@@ -21,44 +21,56 @@ export interface TestContext {
2121 *
2222 * The browser's native fetch includes the laravel_token cookie (set by
2323 * CreateFreshApiToken during the dashboard page load), so authentication
24- * is handled by the browser's own cookie jar — no Playwright cookie sync
25- * issues. The returned Bearer token is then used for all subsequent API
26- * calls, making them completely independent of cookie state.
24+ * is handled by the browser's own cookie jar. The returned Bearer token is
25+ * then used for all subsequent API calls, making them independent of cookie state.
26+ *
27+ * If the first attempt returns 401 (Octane hasn't fully committed the session yet),
28+ * we reload the page to trigger a fresh CreateFreshApiToken and retry once.
2729 */
2830async function createApiToken ( page : Page ) : Promise < string > {
29- const result = await page . evaluate ( async ( baseUrl ) => {
30- const xsrfCookie = document . cookie
31- . split ( '; ' )
32- . find ( ( c ) => c . startsWith ( 'XSRF-TOKEN=' ) ) ;
33- const xsrfToken = xsrfCookie
34- ? decodeURIComponent ( xsrfCookie . split ( '=' ) . slice ( 1 ) . join ( '=' ) )
35- : '' ;
36-
37- const res = await fetch ( `${ baseUrl } /api/v1/users/me/api-tokens` , {
38- method : 'POST' ,
39- headers : {
40- 'Content-Type' : 'application/json' ,
41- Accept : 'application/json' ,
42- 'X-XSRF-TOKEN' : xsrfToken ,
43- } ,
44- body : JSON . stringify ( { name : 'playwright-test' } ) ,
45- } ) ;
31+ for ( let attempt = 0 ; attempt < 2 ; attempt ++ ) {
32+ const result = await page . evaluate ( async ( baseUrl ) => {
33+ const xsrfCookie = document . cookie . split ( '; ' ) . find ( ( c ) => c . startsWith ( 'XSRF-TOKEN=' ) ) ;
34+ const xsrfToken = xsrfCookie
35+ ? decodeURIComponent ( xsrfCookie . split ( '=' ) . slice ( 1 ) . join ( '=' ) )
36+ : '' ;
37+
38+ const res = await fetch ( `${ baseUrl } /api/v1/users/me/api-tokens` , {
39+ method : 'POST' ,
40+ headers : {
41+ 'Content-Type' : 'application/json' ,
42+ Accept : 'application/json' ,
43+ 'X-XSRF-TOKEN' : xsrfToken ,
44+ } ,
45+ body : JSON . stringify ( { name : 'playwright-test' } ) ,
46+ } ) ;
47+
48+ if ( ! res . ok ) {
49+ return null ;
50+ }
4651
47- if ( ! res . ok ) {
48- throw new Error ( `Failed to create API token: ${ res . status } ${ await res . text ( ) } ` ) ;
52+ const body = await res . json ( ) ;
53+ return body . data . access_token as string ;
54+ } , PLAYWRIGHT_BASE_URL ) ;
55+
56+ if ( result ) {
57+ return result ;
4958 }
5059
51- const body = await res . json ( ) ;
52- return body . data . access_token as string ;
53- } , PLAYWRIGHT_BASE_URL ) ;
60+ // Reload to get a fresh laravel_token cookie and retry
61+ await page . reload ( { waitUntil : 'domcontentloaded' } ) ;
62+ }
5463
55- return result ;
64+ throw new Error ( 'Failed to create API token after retry' ) ;
5665}
5766
58- function bearerHeaders ( token : string ) : Record < string , string > {
67+ function buildAuthHeaders ( token : string , xsrfToken : string ) : Record < string , string > {
5968 return {
6069 Accept : 'application/json' ,
6170 Authorization : `Bearer ${ token } ` ,
71+ // XSRF header is needed for web routes (e.g. PUT /teams) that go through
72+ // VerifyCsrfToken middleware. API routes ignore it but it doesn't hurt.
73+ ...( xsrfToken ? { 'X-XSRF-TOKEN' : xsrfToken } : { } ) ,
6274 } ;
6375}
6476
@@ -69,7 +81,11 @@ function bearerHeaders(token: string): Record<string, string> {
6981export async function setupTestContext ( page : Page ) : Promise < TestContext > {
7082 const token = await createApiToken ( page ) ;
7183 const request = page . request ;
72- const headers = bearerHeaders ( token ) ;
84+
85+ const cookies = await page . context ( ) . cookies ( ) ;
86+ const xsrfCookie = cookies . find ( ( c ) => c . name === 'XSRF-TOKEN' ) ;
87+ const xsrfToken = xsrfCookie ? decodeURIComponent ( xsrfCookie . value ) : '' ;
88+ const headers = buildAuthHeaders ( token , xsrfToken ) ;
7389
7490 const orgId = await getOrganizationId ( request , headers ) ;
7591 const memberId = await getCurrentMemberId ( request , orgId , headers ) ;
0 commit comments