@@ -18,6 +18,7 @@ type LoginOptions = {
1818const BROWSER_WAIT_TIMEOUT_MS = 120000 ;
1919const BROWSER_FLOW_TIMEOUT_MS = 180000 ;
2020const BROWSER_LIFECYCLE_TIMEOUT_MS = 15 * 60 * 1000 ;
21+ const BROWSER_LOGIN_ATTEMPTS = 3 ;
2122const BROWSER_CLOSE_TIMEOUT_MS = 10000 ;
2223const BROWSER_CLICK_TIMEOUT_MS = 10000 ;
2324const BROWSER_POLL_TIMEOUT_MS = 1000 ;
@@ -72,6 +73,10 @@ const PASSWORD_SUBMIT_SELECTORS = submitControlSelectors(["Sign in", "Login", "C
7273export function shouldIgnoreHttpsErrors ( options : { resolveHosts ?: Record < string , string > ; ignoreHTTPSErrors ?: boolean } ) : boolean {
7374 return options . ignoreHTTPSErrors ?? Object . keys ( options . resolveHosts ?? { } ) . length > 0 ;
7475}
76+
77+ export function browserLifecycleTimeoutForLoginTargets ( targetCount : number ) : number {
78+ return BROWSER_FLOW_TIMEOUT_MS * BROWSER_LOGIN_ATTEMPTS * targetCount + BROWSER_WAIT_TIMEOUT_MS ;
79+ }
7580const CONSENT_SUBMIT_SELECTORS = [ "Allow" , "Authorize" , "Approve" , "Accept" , "Continue" , "Grant access" ] . flatMap ( ( label ) => [
7681 `button:has-text("${ label } ")` ,
7782 `[role="button"]:has-text("${ label } ")` ,
@@ -779,7 +784,7 @@ async function accountSelectionClickPoint(page: Page, userEmail: string): Promis
779784export async function withBrowser < T > (
780785 outputDir : string ,
781786 runFlow : ( browser : Browser ) => Promise < T > ,
782- options : { resolveHosts ?: Record < string , string > ; ignoreHTTPSErrors ?: boolean } = { }
787+ options : { resolveHosts ?: Record < string , string > ; ignoreHTTPSErrors ?: boolean ; lifecycleTimeoutMs ?: number } = { }
783788) : Promise < T > {
784789 mkdirSync ( outputDir , { recursive : true } ) ;
785790 const browser = await withTimeout (
@@ -792,7 +797,7 @@ export async function withBrowser<T>(
792797 "browser launch"
793798 ) ;
794799 try {
795- return await withTimeout ( runFlow ( browser ) , BROWSER_LIFECYCLE_TIMEOUT_MS , "browser lifecycle" ) ;
800+ return await withTimeout ( runFlow ( browser ) , options . lifecycleTimeoutMs ?? BROWSER_LIFECYCLE_TIMEOUT_MS , "browser lifecycle" ) ;
796801 } catch ( error ) {
797802 const diagnostics = await captureBrowserLifecycleDiagnostics ( browser , outputDir ) ;
798803 throw new Error ( `${ error instanceof Error ? error . message : String ( error ) } ${ diagnostics } ` ) ;
@@ -970,12 +975,12 @@ async function loginThroughZitadelWithBrowserRetry(
970975 options : LoginOptions
971976) : Promise < { finalUrl : string ; screenshotPath : string ; bodyText : string ; title : string } > {
972977 let lastError : unknown ;
973- for ( let attempt = 1 ; attempt <= 3 ; attempt += 1 ) {
978+ for ( let attempt = 1 ; attempt <= BROWSER_LOGIN_ATTEMPTS ; attempt += 1 ) {
974979 try {
975980 return await loginThroughZitadelWithBrowser ( browser , url , user , options ) ;
976981 } catch ( error ) {
977982 lastError = error ;
978- if ( attempt >= 3 || ! isRetryableBlankNavigationError ( error ) ) {
983+ if ( attempt >= BROWSER_LOGIN_ATTEMPTS || ! isRetryableBlankNavigationError ( error ) ) {
979984 throw error ;
980985 }
981986 }
@@ -1074,31 +1079,38 @@ export async function expectManagementUi(
10741079 : { } )
10751080 } ;
10761081 const ignoreHTTPSErrors = shouldIgnoreHttpsErrors ( { resolveHosts } ) ;
1077- await withBrowser ( outputDir , async ( browser ) => {
1078- const cockpit = await loginThroughZitadelWithBrowserRetry ( browser , manageUrl , user , {
1079- outputDir,
1080- postLoginBodyMarkers : COCKPIT_TEXT_MARKERS ,
1081- postLoginLabel : "Cockpit" ,
1082- ignoreHTTPSErrors
1083- } ) ;
1084- const cockpitFinal = new URL ( cockpit . finalUrl ) ;
1085- const cockpitTarget = new URL ( manageUrl ) ;
1086- if ( cockpitFinal . host !== cockpitTarget . host ) {
1087- throw new Error ( `unexpected post-login cockpit host: ${ cockpit . finalUrl } ` ) ;
1088- }
1089- assertUserFacingPageBody ( `${ cockpit . title } \n${ cockpit . bodyText } ` , COCKPIT_TEXT_MARKERS , "Cockpit" ) ;
1090-
1091- const proxy = await loginThroughZitadelWithBrowserRetry ( browser , proxyUrl , user , {
1092- outputDir,
1093- postLoginBodyMarkers : TRAEFIK_TEXT_MARKERS ,
1094- postLoginLabel : "Traefik dashboard" ,
1095- ignoreHTTPSErrors
1096- } ) ;
1097- if ( ! proxy . finalUrl . includes ( "/dashboard" ) ) {
1098- throw new Error ( `unexpected Traefik dashboard URL: ${ proxy . finalUrl } ` ) ;
1082+ await withBrowser (
1083+ outputDir ,
1084+ async ( browser ) => {
1085+ const cockpit = await loginThroughZitadelWithBrowserRetry ( browser , manageUrl , user , {
1086+ outputDir,
1087+ postLoginBodyMarkers : COCKPIT_TEXT_MARKERS ,
1088+ postLoginLabel : "Cockpit" ,
1089+ ignoreHTTPSErrors
1090+ } ) ;
1091+ const cockpitFinal = new URL ( cockpit . finalUrl ) ;
1092+ const cockpitTarget = new URL ( manageUrl ) ;
1093+ if ( cockpitFinal . host !== cockpitTarget . host ) {
1094+ throw new Error ( `unexpected post-login cockpit host: ${ cockpit . finalUrl } ` ) ;
1095+ }
1096+ assertUserFacingPageBody ( `${ cockpit . title } \n${ cockpit . bodyText } ` , COCKPIT_TEXT_MARKERS , "Cockpit" ) ;
1097+
1098+ const proxy = await loginThroughZitadelWithBrowserRetry ( browser , proxyUrl , user , {
1099+ outputDir,
1100+ postLoginBodyMarkers : TRAEFIK_TEXT_MARKERS ,
1101+ postLoginLabel : "Traefik dashboard" ,
1102+ ignoreHTTPSErrors
1103+ } ) ;
1104+ if ( ! proxy . finalUrl . includes ( "/dashboard" ) ) {
1105+ throw new Error ( `unexpected Traefik dashboard URL: ${ proxy . finalUrl } ` ) ;
1106+ }
1107+ assertUserFacingPageBody ( `${ proxy . title } \n${ proxy . bodyText } ` , TRAEFIK_TEXT_MARKERS , "Traefik dashboard" ) ;
1108+ } ,
1109+ {
1110+ lifecycleTimeoutMs : browserLifecycleTimeoutForLoginTargets ( 2 ) ,
1111+ resolveHosts : Object . keys ( resolveHosts ) . length > 0 ? resolveHosts : undefined
10991112 }
1100- assertUserFacingPageBody ( `${ proxy . title } \n${ proxy . bodyText } ` , TRAEFIK_TEXT_MARKERS , "Traefik dashboard" ) ;
1101- } , { resolveHosts : Object . keys ( resolveHosts ) . length > 0 ? resolveHosts : undefined } ) ;
1113+ ) ;
11021114}
11031115
11041116/** Verifies the Cockpit, Traefik, and LXD management surfaces in one browser lifecycle. */
@@ -1121,44 +1133,51 @@ export async function expectManagementSurfaces(
11211133 : { } )
11221134 } ;
11231135 const ignoreHTTPSErrors = shouldIgnoreHttpsErrors ( { resolveHosts } ) ;
1124- await withBrowser ( outputDir , async ( browser ) => {
1125- const cockpit = await loginThroughZitadelWithBrowserRetry ( browser , manageUrl , user , {
1126- outputDir,
1127- postLoginBodyMarkers : COCKPIT_TEXT_MARKERS ,
1128- postLoginLabel : "Cockpit" ,
1129- ignoreHTTPSErrors
1130- } ) ;
1131- const cockpitFinal = new URL ( cockpit . finalUrl ) ;
1132- const cockpitTarget = new URL ( manageUrl ) ;
1133- if ( cockpitFinal . host !== cockpitTarget . host ) {
1134- throw new Error ( `unexpected post-login cockpit host: ${ cockpit . finalUrl } ` ) ;
1135- }
1136- assertUserFacingPageBody ( `${ cockpit . title } \n${ cockpit . bodyText } ` , COCKPIT_TEXT_MARKERS , "Cockpit" ) ;
1137-
1138- const proxy = await loginThroughZitadelWithBrowserRetry ( browser , proxyUrl , user , {
1139- outputDir,
1140- postLoginBodyMarkers : TRAEFIK_TEXT_MARKERS ,
1141- postLoginLabel : "Traefik dashboard" ,
1142- ignoreHTTPSErrors
1143- } ) ;
1144- if ( ! proxy . finalUrl . includes ( "/dashboard" ) ) {
1145- throw new Error ( `unexpected Traefik dashboard URL: ${ proxy . finalUrl } ` ) ;
1146- }
1147- assertUserFacingPageBody ( `${ proxy . title } \n${ proxy . bodyText } ` , TRAEFIK_TEXT_MARKERS , "Traefik dashboard" ) ;
1148-
1149- const lxd = await loginThroughZitadelWithBrowserRetry ( browser , lxdUrl , user , {
1150- outputDir,
1151- postLoginBodyMarkers : LXD_TEXT_MARKERS ,
1152- postLoginLabel : "LXD UI" ,
1153- ignoreHTTPSErrors
1154- } ) ;
1155- const lxdFinal = new URL ( lxd . finalUrl ) ;
1156- const lxdTarget = new URL ( lxdUrl ) ;
1157- if ( lxdFinal . host !== lxdTarget . host ) {
1158- throw new Error ( `unexpected post-login LXD host: ${ lxd . finalUrl } ` ) ;
1136+ await withBrowser (
1137+ outputDir ,
1138+ async ( browser ) => {
1139+ const cockpit = await loginThroughZitadelWithBrowserRetry ( browser , manageUrl , user , {
1140+ outputDir,
1141+ postLoginBodyMarkers : COCKPIT_TEXT_MARKERS ,
1142+ postLoginLabel : "Cockpit" ,
1143+ ignoreHTTPSErrors
1144+ } ) ;
1145+ const cockpitFinal = new URL ( cockpit . finalUrl ) ;
1146+ const cockpitTarget = new URL ( manageUrl ) ;
1147+ if ( cockpitFinal . host !== cockpitTarget . host ) {
1148+ throw new Error ( `unexpected post-login cockpit host: ${ cockpit . finalUrl } ` ) ;
1149+ }
1150+ assertUserFacingPageBody ( `${ cockpit . title } \n${ cockpit . bodyText } ` , COCKPIT_TEXT_MARKERS , "Cockpit" ) ;
1151+
1152+ const proxy = await loginThroughZitadelWithBrowserRetry ( browser , proxyUrl , user , {
1153+ outputDir,
1154+ postLoginBodyMarkers : TRAEFIK_TEXT_MARKERS ,
1155+ postLoginLabel : "Traefik dashboard" ,
1156+ ignoreHTTPSErrors
1157+ } ) ;
1158+ if ( ! proxy . finalUrl . includes ( "/dashboard" ) ) {
1159+ throw new Error ( `unexpected Traefik dashboard URL: ${ proxy . finalUrl } ` ) ;
1160+ }
1161+ assertUserFacingPageBody ( `${ proxy . title } \n${ proxy . bodyText } ` , TRAEFIK_TEXT_MARKERS , "Traefik dashboard" ) ;
1162+
1163+ const lxd = await loginThroughZitadelWithBrowserRetry ( browser , lxdUrl , user , {
1164+ outputDir,
1165+ postLoginBodyMarkers : LXD_TEXT_MARKERS ,
1166+ postLoginLabel : "LXD UI" ,
1167+ ignoreHTTPSErrors
1168+ } ) ;
1169+ const lxdFinal = new URL ( lxd . finalUrl ) ;
1170+ const lxdTarget = new URL ( lxdUrl ) ;
1171+ if ( lxdFinal . host !== lxdTarget . host ) {
1172+ throw new Error ( `unexpected post-login LXD host: ${ lxd . finalUrl } ` ) ;
1173+ }
1174+ assertUserFacingPageBody ( `${ lxd . title } \n${ lxd . bodyText } ` , LXD_TEXT_MARKERS , "LXD UI" ) ;
1175+ } ,
1176+ {
1177+ lifecycleTimeoutMs : browserLifecycleTimeoutForLoginTargets ( 3 ) ,
1178+ resolveHosts : Object . keys ( resolveHosts ) . length > 0 ? resolveHosts : undefined
11591179 }
1160- assertUserFacingPageBody ( `${ lxd . title } \n${ lxd . bodyText } ` , LXD_TEXT_MARKERS , "LXD UI" ) ;
1161- } , { resolveHosts : Object . keys ( resolveHosts ) . length > 0 ? resolveHosts : undefined } ) ;
1180+ ) ;
11621181}
11631182
11641183/** Verifies that the public LXD UI completes OIDC login and renders an authenticated management view. */
0 commit comments