From 2c6a1f1d3860a2865723ceca86ab53baf755615e Mon Sep 17 00:00:00 2001
From: ValueOn AG
Date: Mon, 16 Feb 2026 13:14:15 +0100
Subject: [PATCH] fix: use Teams v2 stable selectors (#prejoin-join-button,
data-tid) with 20s waitForSelector for Join button
Co-authored-by: Cursor
---
src/bot/joinProcedure.ts | 35 +++++++++++++---------
src/bot/orchestrator.ts | 65 ++++++++++++++++++++++++++++++----------
2 files changed, 70 insertions(+), 30 deletions(-)
diff --git a/src/bot/joinProcedure.ts b/src/bot/joinProcedure.ts
index 062ea38..f10cf24 100644
--- a/src/bot/joinProcedure.ts
+++ b/src/bot/joinProcedure.ts
@@ -203,26 +203,33 @@ export class JoinProcedure {
// First, dismiss any "no audio/video" modal that may be blocking
await this._dismissNoAudioVideoModal();
- // Primary selector - confirmed working by Recall.ai (Jan 2025)
- const primarySelector = 'button:has-text("Join now")';
+ // 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(primarySelector, { timeout: 15000 });
- await this._page.click(primarySelector);
- this._logger.info('Clicked "Join now" button');
-
- // After clicking Join, Teams may show the modal again. Dismiss if present.
- await this._page.waitForTimeout(2000);
- await this._dismissNoAudioVideoModal();
- return;
+ 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('Primary join button selector not found, trying fallbacks...');
+ this._logger.info('Join button not found with combined selectors, trying text fallbacks...');
}
- // Fallback selectors
+ // Last resort fallback: any button with "Join" text
const fallbackSelectors = [
- 'button[data-tid="prejoin-join-button"]',
- 'button:has-text("Join meeting")',
'button:has-text("Join")',
'[data-tid="joinButton"]',
];
diff --git a/src/bot/orchestrator.ts b/src/bot/orchestrator.ts
index 38ce7a2..29ee879 100644
--- a/src/bot/orchestrator.ts
+++ b/src/bot/orchestrator.ts
@@ -218,22 +218,55 @@ export class BotOrchestrator {
const postAuthUrl = this._page!.url();
this._logger.info(`Post-auth URL: ${postAuthUrl.substring(0, 100)}`);
- // After login, Microsoft may redirect to M365/Office instead of back to the meeting.
- // We need to navigate back to the meeting URL -- now with the auth session active.
- // This time, Teams should recognize the auth and show the authenticated pre-join page.
- if (!postAuthUrl.includes('teams.microsoft.com/v2') || !postAuthUrl.includes('meet')) {
- this._logger.info('Not on Teams meeting page after auth - navigating back to meeting URL...');
- await this._page!.goto(this._meetingUrl, {
- waitUntil: 'domcontentloaded',
- timeout: 30000,
- });
-
- // Handle launcher dialog if it appears again
- await this._joinProcedure!.handleLauncherIfPresent();
- await this._page!.waitForTimeout(5000);
-
- const meetingPageUrl = this._page!.url();
- this._logger.info(`After re-navigation to meeting: ${meetingPageUrl.substring(0, 100)}`);
+ // After login, Microsoft redirects to M365 (m365.cloud.microsoft/chat/).
+ // The "Continue on this browser" launcher ALWAYS leads to anonymous mode.
+ // Instead, navigate to teams.cloud.microsoft and use the meeting join from there.
+ // teams.cloud.microsoft is the new Teams domain where the auth session lives.
+ const { parseMeetingUrl } = await import('./meetingUrlParser');
+ const parsed = parseMeetingUrl(this._meetingUrl);
+ const meetingId = parsed.meetingId || '';
+ const passcode = parsed.passcode || '';
+
+ // Navigate to Teams on the cloud.microsoft domain (where auth cookies are)
+ // and try to join the meeting from within the authenticated app
+ const teamsCloudUrls = [
+ // Direct meeting URL on new Teams domain
+ `https://teams.cloud.microsoft/meet/${meetingId}${passcode ? '?p=' + passcode : ''}`,
+ // Original meeting URL (teams.microsoft.com)
+ this._meetingUrl,
+ ];
+
+ for (const url of teamsCloudUrls) {
+ this._logger.info(`Trying authenticated meeting URL: ${url.substring(0, 80)}`);
+ try {
+ await this._page!.goto(url, {
+ waitUntil: 'domcontentloaded',
+ timeout: 20000,
+ });
+ await this._page!.waitForTimeout(5000);
+
+ const resultUrl = this._page!.url();
+ const pageContent = await this._page!.evaluate(() => document.body?.innerText?.substring(0, 500) || '');
+ this._logger.info(`Result URL: ${resultUrl.substring(0, 100)}`);
+ this._logger.info(`Page content: ${pageContent.substring(0, 200)}`);
+
+ // Check if we have the authenticated pre-join (no "Type your name", has "Join now")
+ if (pageContent.includes('Join now') && !pageContent.includes('Type your name') && !pageContent.includes('Enter the name')) {
+ this._logger.info('Found authenticated pre-join page!');
+ break;
+ }
+
+ // If launcher appears, DON'T click "Continue on this browser" (leads to anon)
+ // Instead try the next URL
+ if (pageContent.includes('Continue on this browser')) {
+ this._logger.info('Launcher appeared - skipping (would lead to anonymous)');
+ continue;
+ }
+
+ } catch (navErr) {
+ this._logger.warn(`Navigation to ${url.substring(0, 60)} failed: ${navErr}`);
+ continue;
+ }
}
// Verify we're on the authenticated pre-join page