fix: robust meeting admission for auth join, fix spoken language selectors for Teams UI
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
31c196978b
commit
ba8dab5a98
2 changed files with 114 additions and 39 deletions
|
|
@ -194,17 +194,19 @@ export class CaptionsProcedure {
|
|||
// Wait a moment for the captions UI to stabilize
|
||||
await this._page.waitForTimeout(2000);
|
||||
|
||||
// Strategy 1: Try "Caption settings" button near the captions area
|
||||
// This is typically a gear icon or "..." button in the captions banner
|
||||
let settingsOpened = false;
|
||||
|
||||
// Strategy 1: Try "Caption settings" gear button near the captions area
|
||||
const captionSettingsSelectors = [
|
||||
'button[aria-label*="Caption settings"]',
|
||||
'button[aria-label*="caption settings"]',
|
||||
'button[aria-label*="Captions settings"]',
|
||||
'button[aria-label*="Caption settings" i]',
|
||||
'button[aria-label*="Captions settings" i]',
|
||||
'button[aria-label*="Untertiteleinstellungen" i]',
|
||||
'button[data-tid="caption-settings-button"]',
|
||||
'button[id="caption-settings-button"]',
|
||||
// Teams 2025+: settings icon inside the captions banner
|
||||
'button[aria-label*="Settings" i][data-tid*="caption" i]',
|
||||
];
|
||||
|
||||
let settingsOpened = false;
|
||||
for (const selector of captionSettingsSelectors) {
|
||||
try {
|
||||
const button = await this._page.$(selector);
|
||||
|
|
@ -220,27 +222,30 @@ export class CaptionsProcedure {
|
|||
}
|
||||
}
|
||||
|
||||
// Strategy 2: If no caption settings button found, try More menu > Language and speech
|
||||
// Strategy 2: More menu > "Language and speech" / "Captions & transcripts"
|
||||
if (!settingsOpened) {
|
||||
this._logger.info('Caption settings button not found, trying More menu > Language and speech...');
|
||||
|
||||
await this._openMoreMenu();
|
||||
await this._page.waitForTimeout(500);
|
||||
await this._page.waitForTimeout(1000);
|
||||
|
||||
// Look for "Language and speech" or "Captions & transcripts" menu items
|
||||
// Teams has renamed this menu multiple times across versions
|
||||
// All selectors must have an element prefix for Playwright
|
||||
const languageMenuSelectors = [
|
||||
'[data-tid="captions-and-transcripts-button"]',
|
||||
':has-text("Captions & transcripts")',
|
||||
':has-text("Captions and transcripts")',
|
||||
':has-text("Untertitel und Transkripte")',
|
||||
':has-text("Language and speech")',
|
||||
':has-text("Spoken language")',
|
||||
':has-text("Sprache und Spracheingabe")',
|
||||
':has-text("Gesprochene Sprache")',
|
||||
'[data-tid="language-and-speech-button"]',
|
||||
'button:has-text("Language")',
|
||||
'button:has-text("Sprache")',
|
||||
'div[role="menuitem"]:has-text("Captions & transcripts")',
|
||||
'div[role="menuitem"]:has-text("Captions and transcripts")',
|
||||
'div[role="menuitem"]:has-text("Untertitel und Transkripte")',
|
||||
'div[role="menuitem"]:has-text("Language and speech")',
|
||||
'div[role="menuitem"]:has-text("Sprache und Spracheingabe")',
|
||||
'button:has-text("Captions & transcripts")',
|
||||
'button:has-text("Captions and transcripts")',
|
||||
'button:has-text("Language and speech")',
|
||||
'button:has-text("Sprache und Spracheingabe")',
|
||||
'li:has-text("Captions")',
|
||||
'li:has-text("Language")',
|
||||
'li:has-text("Untertitel")',
|
||||
'li:has-text("Sprache")',
|
||||
];
|
||||
|
||||
for (const selector of languageMenuSelectors) {
|
||||
|
|
@ -259,20 +264,54 @@ export class CaptionsProcedure {
|
|||
}
|
||||
}
|
||||
|
||||
// Strategy 3: Search all visible menu items by evaluating text content
|
||||
if (!settingsOpened) {
|
||||
this._logger.info('Standard selectors failed, scanning menu items by text...');
|
||||
|
||||
const found = await this._page.evaluate(() => {
|
||||
const keywords = [
|
||||
'caption', 'captions', 'untertitel',
|
||||
'language', 'sprache', 'spoken',
|
||||
];
|
||||
// Search all menu items, buttons, and clickable elements
|
||||
const candidates = document.querySelectorAll(
|
||||
'[role="menuitem"], [role="menuitemcheckbox"], [role="menuitemradio"], button, li'
|
||||
);
|
||||
for (const el of candidates) {
|
||||
const text = (el as HTMLElement).innerText?.toLowerCase() || '';
|
||||
if (keywords.some(kw => text.includes(kw))) {
|
||||
(el as HTMLElement).click();
|
||||
return text;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
if (found) {
|
||||
this._logger.info(`Clicked menu item by text scan: "${found}"`);
|
||||
settingsOpened = true;
|
||||
await this._page.waitForTimeout(1000);
|
||||
}
|
||||
}
|
||||
|
||||
if (!settingsOpened) {
|
||||
this._logger.warn('Could not open language settings - captions will use default language (English)');
|
||||
return;
|
||||
}
|
||||
|
||||
// Now look for the "Language settings" / "Change spoken language" sub-option if needed
|
||||
// Look for sub-options like "Change spoken language" / "Language settings"
|
||||
const langSettingsSelectors = [
|
||||
':has-text("Change spoken language")',
|
||||
':has-text("Gesprochene Sprache ändern")',
|
||||
':has-text("Language settings")',
|
||||
':has-text("Spracheinstellungen")',
|
||||
'button:has-text("Change spoken language")',
|
||||
'button:has-text("Gesprochene Sprache ändern")',
|
||||
'button:has-text("Language settings")',
|
||||
'button:has-text("Spracheinstellungen")',
|
||||
'button:has-text("Spoken language")',
|
||||
'button:has-text("Gesprochene Sprache")',
|
||||
'div[role="menuitem"]:has-text("Change spoken language")',
|
||||
'div[role="menuitem"]:has-text("Spoken language")',
|
||||
'div[role="menuitem"]:has-text("Gesprochene Sprache")',
|
||||
'a:has-text("Change spoken language")',
|
||||
'a:has-text("Spoken language")',
|
||||
];
|
||||
|
||||
for (const selector of langSettingsSelectors) {
|
||||
|
|
@ -289,63 +328,85 @@ export class CaptionsProcedure {
|
|||
}
|
||||
}
|
||||
|
||||
// Look for the spoken language dropdown
|
||||
// Look for the spoken language dropdown/combobox
|
||||
let languageSet = false;
|
||||
const dropdownSelectors = [
|
||||
'select[aria-label*="spoken language" i]',
|
||||
'select[aria-label*="Meeting spoken language" i]',
|
||||
'select[aria-label*="Gesprochene Sprache" i]',
|
||||
'[data-tid="spoken-language-dropdown"]',
|
||||
'div[role="combobox"]',
|
||||
'div[role="listbox"]',
|
||||
'select', // Generic fallback
|
||||
];
|
||||
|
||||
for (const selector of dropdownSelectors) {
|
||||
if (languageSet) break;
|
||||
try {
|
||||
const dropdown = await this._page.$(selector);
|
||||
if (dropdown) {
|
||||
const tagName = await dropdown.evaluate(el => el.tagName.toLowerCase());
|
||||
|
||||
if (tagName === 'select') {
|
||||
// Native select element - try to select by text
|
||||
// Native select element
|
||||
for (const name of targetNames) {
|
||||
try {
|
||||
await this._page.selectOption(selector, { label: name });
|
||||
this._logger.info(`Selected spoken language: ${name}`);
|
||||
languageSet = true;
|
||||
break;
|
||||
} catch {
|
||||
// Try next name variant
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Fluent UI dropdown - click and select from options
|
||||
// Fluent UI dropdown/combobox
|
||||
await dropdown.click();
|
||||
await this._page.waitForTimeout(500);
|
||||
|
||||
for (const name of targetNames) {
|
||||
try {
|
||||
const option = await this._page.$(`[role="option"]:has-text("${name}")`);
|
||||
// Try role="option" first, then generic text search
|
||||
const optionSelectors = [
|
||||
`[role="option"]:has-text("${name}")`,
|
||||
`li:has-text("${name}")`,
|
||||
`div[role="option"]:has-text("${name}")`,
|
||||
`span:has-text("${name}")`,
|
||||
];
|
||||
for (const optSel of optionSelectors) {
|
||||
const option = await this._page.$(optSel);
|
||||
if (option) {
|
||||
await option.click();
|
||||
this._logger.info(`Selected spoken language: ${name}`);
|
||||
this._logger.info(`Selected spoken language: ${name} (via ${optSel})`);
|
||||
languageSet = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (languageSet) break;
|
||||
} catch {
|
||||
// Try next name variant
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
if (languageSet) break;
|
||||
}
|
||||
} catch {
|
||||
// Continue
|
||||
}
|
||||
}
|
||||
|
||||
// Click "Update" or "Apply" button
|
||||
if (!languageSet) {
|
||||
this._logger.warn('Could not find/select spoken language in dropdown');
|
||||
}
|
||||
|
||||
// Click "Update" / "Apply" / "Confirm" button
|
||||
const updateSelectors = [
|
||||
'button:has-text("Update")',
|
||||
'button:has-text("Apply")',
|
||||
'button:has-text("Confirm")',
|
||||
'button:has-text("Aktualisieren")',
|
||||
'button:has-text("Übernehmen")',
|
||||
'button:has-text("Bestätigen")',
|
||||
'button[data-tid="language-update-button"]',
|
||||
];
|
||||
|
||||
|
|
@ -365,7 +426,7 @@ export class CaptionsProcedure {
|
|||
|
||||
// Close any open dialogs/menus
|
||||
await this._page.keyboard.press('Escape');
|
||||
this._logger.info('Spoken language setting attempt completed');
|
||||
this._logger.info(`Spoken language setting attempt completed (set: ${languageSet})`);
|
||||
|
||||
} catch (error) {
|
||||
this._logger.warn(`Could not set spoken language to ${this._language}: ${error}`);
|
||||
|
|
|
|||
|
|
@ -514,6 +514,8 @@ export class BotOrchestrator {
|
|||
private async _waitForMeetingAdmission(): Promise<void> {
|
||||
const startTime = Date.now();
|
||||
const timeout = config.timeouts.lobbyWait;
|
||||
let consecutiveNoSignal = 0;
|
||||
const maxNoSignal = 5; // Allow several cycles with no lobby/meeting signal before giving up
|
||||
|
||||
while (Date.now() - startTime < timeout) {
|
||||
// Check if we're in the meeting
|
||||
|
|
@ -524,12 +526,24 @@ export class BotOrchestrator {
|
|||
|
||||
// Check if still in lobby
|
||||
const inLobby = await this._joinProcedure!.isInMeetingLobby({ waitForSeconds: 2 });
|
||||
if (!inLobby) {
|
||||
// Might have been rejected or meeting ended
|
||||
throw new Error('Bot was removed from lobby or meeting ended');
|
||||
if (inLobby) {
|
||||
consecutiveNoSignal = 0;
|
||||
this._logger.info('Still waiting in lobby...');
|
||||
continue;
|
||||
}
|
||||
|
||||
this._logger.info('Still waiting in lobby...');
|
||||
// Neither in meeting nor in lobby — this can happen legitimately:
|
||||
// - Authenticated users skip lobby, but meeting UI takes seconds to load
|
||||
// - Page is transitioning between states
|
||||
// Only give up after several consecutive cycles with no signal
|
||||
consecutiveNoSignal++;
|
||||
this._logger.info(`No lobby/meeting signal detected (attempt ${consecutiveNoSignal}/${maxNoSignal}), waiting...`);
|
||||
|
||||
if (consecutiveNoSignal >= maxNoSignal) {
|
||||
// Take a screenshot for debugging before giving up
|
||||
await this._takeScreenshot('no-meeting-signal');
|
||||
throw new Error('Bot was removed from lobby or meeting ended');
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error('Timeout waiting to be admitted from lobby');
|
||||
|
|
|
|||
Loading…
Reference in a new issue