diff --git a/src/bot/joinProcedure.ts b/src/bot/joinProcedure.ts index f10cf24..28f29ec 100644 --- a/src/bot/joinProcedure.ts +++ b/src/bot/joinProcedure.ts @@ -129,10 +129,11 @@ export class JoinProcedure { this._logger.info(`Starting lobby join flow... (authenticated: ${this._isAuthenticated})`); if (this._isAuthenticated) { - // Authenticated join: name comes from Microsoft account, no name input needed - // Wait for the pre-join page to load (look for Join now button) - this._logger.info('Authenticated join - skipping name input, waiting for Join button...'); - await this._page.waitForTimeout(3000); + // Authenticated join: wait for the authenticated pre-join page. + // Proof that we're on the RIGHT page: no name input field exists + // (the anonymous page has input[placeholder="Type your name"]). + // Retry up to 5 times, every 5 seconds. + await this._waitForAuthenticatedPreJoinPage(); } else { // Anonymous join: enter bot name in the name input field await this._enterBotName(); @@ -142,6 +143,41 @@ export class JoinProcedure { await this._clickJoinNow(); } + /** + * Wait for the authenticated pre-join page to be ready. + * + * Verification: The authenticated page does NOT have a name input field. + * If a name input (placeholder="Type your name") exists, we're still on the + * anonymous page and need to wait for the redirect to complete. + * + * Retries 5 times, every 5 seconds. + */ + private async _waitForAuthenticatedPreJoinPage(): Promise { + const maxRetries = 5; + const retryIntervalMs = 5000; + + for (let attempt = 1; attempt <= maxRetries; attempt++) { + const url = this._page.url(); + const hasNameInput = await this._page.$('input[placeholder="Type your name"]'); + const hasJoinButton = await this._page.$('#prejoin-join-button, button[data-tid="prejoin-join-button"], button:has-text("Join now")'); + + if (hasJoinButton && !hasNameInput) { + this._logger.info(`Authenticated pre-join page confirmed (attempt ${attempt}/${maxRetries}): Join button present, no name input. URL: ${url.substring(0, 100)}`); + return; + } + + const nameStatus = hasNameInput ? 'name input FOUND (wrong page)' : 'no name input'; + const joinStatus = hasJoinButton ? 'Join button found' : 'no Join button'; + this._logger.info(`Waiting for authenticated pre-join page (attempt ${attempt}/${maxRetries}): ${nameStatus}, ${joinStatus}. URL: ${url.substring(0, 100)}`); + + if (attempt < maxRetries) { + await this._page.waitForTimeout(retryIntervalMs); + } + } + + this._logger.warn('Could not confirm authenticated pre-join page after all retries. Proceeding anyway...'); + } + /** * Enter the bot name in the name input field. * Primary selector: input[placeholder="Type your name"] (confirmed by Recall.ai). @@ -200,56 +236,61 @@ export class JoinProcedure { private async _clickJoinNow(): Promise { this._logger.info('Clicking Join now...'); - // First, dismiss any "no audio/video" modal that may be blocking - await this._dismissNoAudioVideoModal(); + const maxRetries = 5; + const retryIntervalMs = 5000; - // Teams v2 uses stable IDs for the join button. Wait with multiple selectors. - // The button may take time to render in the Teams v2 SPA after auth redirect. const joinSelectors = [ '#prejoin-join-button', // Teams v2 stable ID 'button[data-tid="prejoin-join-button"]', // Teams v2 data-tid 'button:has-text("Join now")', // Text-based (light-meetings) 'button:has-text("Join meeting")', // Alternative text ]; - const combinedSelector = joinSelectors.join(', '); - try { - await this._page.waitForSelector(combinedSelector, { timeout: 20000, state: 'visible' }); - const button = await this._page.$(combinedSelector); - if (button) { - await button.click(); - this._logger.info('Clicked "Join now" button'); - await this._page.waitForTimeout(2000); - await this._dismissNoAudioVideoModal(); - return; - } - } catch { - this._logger.info('Join button not found with combined selectors, trying text fallbacks...'); - } + for (let attempt = 1; attempt <= maxRetries; attempt++) { + // Dismiss any "no audio/video" modal that may be blocking + await this._dismissNoAudioVideoModal(); - // Last resort fallback: any button with "Join" text - const fallbackSelectors = [ - 'button:has-text("Join")', - '[data-tid="joinButton"]', - ]; - - for (const selector of fallbackSelectors) { try { - const button = await this._page.$(selector); + await this._page.waitForSelector(combinedSelector, { timeout: 5000, state: 'visible' }); + const button = await this._page.$(combinedSelector); if (button) { await button.click(); - this._logger.info(`Clicked join button (fallback: ${selector})`); + this._logger.info(`Clicked "Join now" button (attempt ${attempt}/${maxRetries})`); await this._page.waitForTimeout(2000); await this._dismissNoAudioVideoModal(); return; } } catch { - // Continue + // Button not found this attempt + } + + // Fallback: any button with "Join" text + const fallbackSelectors = ['button:has-text("Join")', '[data-tid="joinButton"]']; + for (const selector of fallbackSelectors) { + try { + const button = await this._page.$(selector); + if (button && await button.isVisible()) { + await button.click(); + this._logger.info(`Clicked join button fallback: ${selector} (attempt ${attempt}/${maxRetries})`); + await this._page.waitForTimeout(2000); + await this._dismissNoAudioVideoModal(); + return; + } + } catch { + // Continue + } + } + + const url = this._page.url(); + this._logger.info(`Join button not found (attempt ${attempt}/${maxRetries}). URL: ${url.substring(0, 100)}`); + + if (attempt < maxRetries) { + await this._page.waitForTimeout(retryIntervalMs); } } - // Diagnostic info for debugging + // All retries exhausted — throw with diagnostic info const currentUrl = this._page.url(); const title = await this._page.title(); const bodyText = await this._page.evaluate(() =>