@@ -157,6 +157,43 @@ async function inputVisible(page: Page, selectors: readonly string[]): Promise<b
157157 return false ;
158158}
159159
160+ async function firstIdentityInput ( page : Page , selectors : readonly string [ ] , targetHost : string ) : Promise < string > {
161+ const deadline = Date . now ( ) + BROWSER_WAIT_TIMEOUT_MS ;
162+ let lastBody = "" ;
163+
164+ while ( Date . now ( ) < deadline ) {
165+ const currentUrl = page . url ( ) ;
166+ if ( isTargetLoginOrOauthPlumbingPage ( currentUrl , targetHost ) ) {
167+ await clickOidcStartIfNeeded ( page , targetHost ) ;
168+ }
169+
170+ if ( isIdentityLoginInputPage ( page . url ( ) , targetHost ) ) {
171+ for ( const selector of selectors ) {
172+ const locator = page . locator ( selector ) . first ( ) ;
173+ if ( await locatorVisible ( locator ) ) {
174+ return selector ;
175+ }
176+ }
177+ }
178+
179+ if ( await reloadBlankLoginDocumentIfNeeded ( page ) ) {
180+ await page . waitForTimeout ( 1000 ) ;
181+ continue ;
182+ }
183+
184+ lastBody = ( await maybeWithTimeout ( page . locator ( "body" ) . innerText ( { timeout : 1000 } ) . catch ( ( ) => "" ) , 2000 ) ) ?? "" ;
185+ await page . waitForTimeout ( 500 ) ;
186+ }
187+
188+ throw new Error (
189+ [
190+ `none of the identity selectors were visible: ${ selectors . join ( ", " ) } ` ,
191+ `current URL: ${ page . url ( ) } ` ,
192+ `body:\n${ bodySnippetForError ( lastBody ) || "<empty>" } `
193+ ] . join ( "\n" )
194+ ) ;
195+ }
196+
160197async function clickOidcStartIfNeeded ( page : Page , targetHost : string ) : Promise < void > {
161198 const deadline = Date . now ( ) + BROWSER_OIDC_START_TIMEOUT_MS ;
162199 let reloadedBlankTargetLoginPage = false ;
@@ -184,6 +221,14 @@ async function clickOidcStartIfNeeded(page: Page, targetHost: string): Promise<v
184221 continue ;
185222 }
186223
224+ const lxdOidcLoginUrl = lxdOidcLoginUrlForSsoPage ( page . url ( ) , lastBody , targetHost ) ;
225+ if ( lxdOidcLoginUrl && Date . now ( ) - lastStartClick > BROWSER_OIDC_RECLICK_INTERVAL_MS ) {
226+ lastStartClick = Date . now ( ) ;
227+ await maybeWithTimeout ( page . goto ( lxdOidcLoginUrl , { waitUntil : "commit" , timeout : BROWSER_CLICK_TIMEOUT_MS } ) . catch ( ( ) => undefined ) , 12000 ) ;
228+ await waitForOidcStartHandoff ( page , targetHost ) ;
229+ continue ;
230+ }
231+
187232 const startSelectors = [
188233 'button:has-text("Login with SSO")' ,
189234 'a:has-text("Login with SSO")' ,
@@ -801,7 +846,7 @@ async function loginThroughZitadelWithBrowser(
801846 stage = "waiting for identity login document" ;
802847 await waitForIdentityLoginDocument ( page , targetHost ) ;
803848 stage = "waiting for username input" ;
804- const emailSelector = await firstVisible ( page , [ ... USERNAME_INPUT_SELECTORS ] ) ;
849+ const emailSelector = await firstIdentityInput ( page , USERNAME_INPUT_SELECTORS , targetHost ) ;
805850 stage = "entering username" ;
806851 await typeInto ( page , emailSelector , user . email ) ;
807852 stage = "waiting for username submit" ;
@@ -1180,6 +1225,20 @@ export function isIdentityLoginInputPage(currentUrl: string, targetHost: string)
11801225 return Boolean ( parsed && ! ( parsed . host === targetHost && isLoginOrOauthCallbackPlumbingPath ( parsed . pathname ) ) ) ;
11811226}
11821227
1228+ export function lxdOidcLoginUrlForSsoPage ( currentUrl : string , body : string , targetHost : string ) : string | undefined {
1229+ const parsed = parseBrowserUrl ( currentUrl ) ;
1230+ if ( ! parsed || parsed . host !== targetHost || ! parsed . pathname . toLowerCase ( ) . startsWith ( "/ui/login" ) ) {
1231+ return undefined ;
1232+ }
1233+
1234+ const normalizedBody = body . toLowerCase ( ) ;
1235+ if ( ! normalizedBody . includes ( "login with sso" ) || ! normalizedBody . includes ( "canonical lxd" ) ) {
1236+ return undefined ;
1237+ }
1238+
1239+ return `${ parsed . origin } /oidc/login` ;
1240+ }
1241+
11831242export function formatDeniedTargetRouteFailure ( finalUrl : string , body : string ) : string {
11841243 return [
11851244 "expected denied protected route to show a denial page, but the browser reached the target host without denial text" ,
0 commit comments