fix: captions enabling for authenticated Teams UI - add submenu path, reduce timeouts, add debug logging

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
ValueOn AG 2026-02-17 20:14:51 +01:00
parent 04abaf9402
commit 50f1f1977e

View file

@ -58,37 +58,24 @@ export class CaptionsProcedure {
/** /**
* Open the "More actions" (...) menu in the call controls. * Open the "More actions" (...) menu in the call controls.
* Primary selector: button[id="callingButtons-showMoreBtn"] (confirmed by Recall.ai). * Works for both anonymous (light-meetings) and authenticated (full Teams) UI.
*/ */
private async _openMoreMenu(): Promise<void> { private async _openMoreMenu(): Promise<void> {
// Primary selector - confirmed by Recall.ai (Jan 2025) const allSelectors = [
const primarySelector = 'button[id="callingButtons-showMoreBtn"]'; 'button[id="callingButtons-showMoreBtn"]',
try {
await this._page.waitForSelector(primarySelector, { timeout: 120000 });
this._logger.info('Found "More" button');
await this._page.click(primarySelector);
this._logger.info('Clicked "More" button');
return;
} catch {
this._logger.info('Primary more button selector not found, trying fallbacks...');
}
// Fallback selectors
const fallbackSelectors = [
'[data-tid="callingButtons-showMoreBtn"]', '[data-tid="callingButtons-showMoreBtn"]',
'button[aria-label*="More actions"]', 'button[aria-label*="More actions"]',
'button[aria-label*="More"]', 'button[aria-label*="More"]',
'[data-tid="more-button"]', '[data-tid="more-button"]',
]; ];
for (const selector of fallbackSelectors) { for (const selector of allSelectors) {
try { try {
const button = await this._page.$(selector); const button = await this._page.$(selector);
if (button) { if (button) {
await button.click(); await button.click();
this._logger.info(`Clicked "More" button: ${selector}`);
await this._page.waitForTimeout(1000); await this._page.waitForTimeout(1000);
this._logger.info(`Opened more menu (fallback: ${selector})`);
return; return;
} }
} catch { } catch {
@ -96,44 +83,44 @@ export class CaptionsProcedure {
} }
} }
// Last resort: wait for the primary selector with a short timeout
try {
await this._page.waitForSelector(allSelectors[0], { timeout: 10000 });
await this._page.click(allSelectors[0]);
this._logger.info('Found "More" button (after wait)');
await this._page.waitForTimeout(1000);
return;
} catch {
// Continue
}
throw new Error('Could not find More actions menu'); throw new Error('Could not find More actions menu');
} }
/** /**
* Click the captions button in the menu. * Click the captions button in the menu.
* Primary selector: div[id="closed-captions-button"] (confirmed by Recall.ai). * Handles two UI variants:
* - Anonymous (light-meetings): direct div[id="closed-captions-button"]
* - Authenticated (full Teams): submenu "Captions & transcripts" "Turn on live captions"
*/ */
private async _clickEnableCaptions(): Promise<void> { private async _clickEnableCaptions(): Promise<void> {
// Primary selector - confirmed by Recall.ai (Jan 2025) // Log visible menu items for debugging
const primarySelector = 'div[id="closed-captions-button"]'; await this._logVisibleMenuItems();
try { // Strategy 1: Direct captions button (anonymous/light-meetings UI)
await this._page.waitForSelector(primarySelector, { timeout: 120000 }); const directSelectors = [
this._logger.info('Found "Captions" button'); 'div[id="closed-captions-button"]',
await this._page.click(primarySelector); '[data-tid="closed-captions-button"]',
this._logger.info('Clicked "Captions" button');
return;
} catch {
this._logger.info('Primary captions button selector not found, trying fallbacks...');
}
// Fallback selectors
const fallbackSelectors = [
'button:has-text("Turn on live captions")',
'button:has-text("Live captions")',
'[data-tid="captions-toggle"]', '[data-tid="captions-toggle"]',
'button[aria-label*="captions" i]',
'[role="menuitem"]:has-text("captions")',
'[role="menuitemcheckbox"]:has-text("captions")',
]; ];
for (const selector of fallbackSelectors) { for (const selector of directSelectors) {
try { try {
const button = await this._page.$(selector); const button = await this._page.$(selector);
if (button) { if (button) {
await button.click(); await button.click();
this._logger.info(`Clicked direct captions button: ${selector}`);
await this._page.waitForTimeout(1000); await this._page.waitForTimeout(1000);
this._logger.info(`Clicked enable captions (fallback: ${selector})`);
return; return;
} }
} catch { } catch {
@ -141,24 +128,163 @@ export class CaptionsProcedure {
} }
} }
// Try clicking away to close menu if captions not found // Strategy 2: Authenticated Teams UI — "Captions & transcripts" submenu first
const submenuSelectors = [
'[data-tid="captions-and-transcripts-button"]',
'[role="menuitem"]:has-text("Captions & transcripts")',
'[role="menuitem"]:has-text("Captions and transcripts")',
'[role="menuitem"]:has-text("Untertitel und Transkripte")',
'[role="menuitem"]:has-text("Untertitel")',
'button:has-text("Captions & transcripts")',
'button:has-text("Captions and transcripts")',
];
for (const selector of submenuSelectors) {
try {
const item = await this._page.$(selector);
if (item) {
await item.click();
this._logger.info(`Clicked captions submenu: ${selector}`);
await this._page.waitForTimeout(1500);
// Now look for "Turn on live captions" inside the submenu/panel
const enableSelectors = [
'button:has-text("Turn on live captions")',
'button:has-text("Live captions")',
'button:has-text("Live-Untertitel aktivieren")',
'button:has-text("Liveuntertitel")',
'[role="menuitem"]:has-text("Turn on live captions")',
'[role="menuitem"]:has-text("Live captions")',
'[role="menuitemcheckbox"]:has-text("captions")',
'[data-tid="toggle-captions"]',
];
for (const enableSel of enableSelectors) {
try {
const enableBtn = await this._page.$(enableSel);
if (enableBtn) {
await enableBtn.click();
this._logger.info(`Clicked enable captions: ${enableSel}`);
await this._page.waitForTimeout(1000);
return;
}
} catch {
// Continue
}
}
this._logger.info('Opened captions submenu but could not find enable button');
break;
}
} catch {
// Continue
}
}
// Strategy 3: Generic text-based fallbacks
const textFallbacks = [
'button:has-text("Turn on live captions")',
'button:has-text("Live captions")',
'button[aria-label*="captions" i]',
'[role="menuitem"]:has-text("captions")',
'[role="menuitemcheckbox"]:has-text("captions")',
];
for (const selector of textFallbacks) {
try {
const button = await this._page.$(selector);
if (button) {
await button.click();
this._logger.info(`Clicked captions (text fallback): ${selector}`);
await this._page.waitForTimeout(1000);
return;
}
} catch {
// Continue
}
}
// Strategy 4: DOM scan — find any element mentioning "caption" in the open menu
const found = await this._page.evaluate(() => {
const keywords = ['caption', 'captions', 'untertitel', 'live caption'];
const candidates = document.querySelectorAll(
'[role="menuitem"], [role="menuitemcheckbox"], [role="menuitemradio"], button, li, div[role="option"]'
);
const results: string[] = [];
for (let i = 0; i < candidates.length; i++) {
const el = candidates[i] as HTMLElement;
const text = el.innerText?.toLowerCase()?.trim() || '';
if (text && keywords.some(kw => text.includes(kw))) {
results.push(text.substring(0, 60));
el.click();
return { clicked: text.substring(0, 60), allMatches: results };
}
}
return { clicked: null, allMatches: results };
});
if (found.clicked) {
this._logger.info(`Clicked captions via DOM scan: "${found.clicked}"`);
await this._page.waitForTimeout(1500);
// Check if this opened a submenu — look for "Turn on" or "enable" inside
const turnOnBtn = await this._page.$('button:has-text("Turn on"), [role="menuitem"]:has-text("Turn on")');
if (turnOnBtn) {
await turnOnBtn.click();
this._logger.info('Clicked "Turn on" in captions submenu');
await this._page.waitForTimeout(1000);
}
return;
}
// Nothing found
await this._page.keyboard.press('Escape'); await this._page.keyboard.press('Escape');
this._logger.warn('Could not find captions option - may already be enabled or not available'); this._logger.warn(`Could not find captions option. DOM scan matches: ${JSON.stringify(found.allMatches)}`);
}
/**
* Log visible menu items for debugging when captions button is not found.
*/
private async _logVisibleMenuItems(): Promise<void> {
try {
const menuItems = await this._page.evaluate(() => {
const items = document.querySelectorAll(
'[role="menuitem"], [role="menuitemcheckbox"], [role="menuitemradio"]'
);
return Array.from(items).map(el => {
const text = (el as HTMLElement).innerText?.trim()?.substring(0, 50) || '';
const tid = el.getAttribute('data-tid') || '';
const id = el.id || '';
return `[${tid || id || 'no-id'}] ${text}`;
}).filter(t => t.length > 5);
});
this._logger.info(`Visible menu items (${menuItems.length}): ${JSON.stringify(menuItems)}`);
} catch {
// Not critical
}
} }
/** /**
* Wait for the captions container to become visible after enabling. * Wait for the captions container to become visible after enabling.
* Primary selector: div[data-tid="closed-caption-renderer-wrapper"] (confirmed by Recall.ai).
*/ */
private async _waitForCaptionsContainer(): Promise<void> { private async _waitForCaptionsContainer(): Promise<void> {
const containerSelector = 'div[data-tid="closed-caption-renderer-wrapper"]'; const containerSelectors = [
'div[data-tid="closed-caption-renderer-wrapper"]',
'div[data-tid="live-captions-renderer"]',
'[data-tid="caption-area"]',
];
try { for (const selector of containerSelectors) {
await this._page.waitForSelector(containerSelector, { timeout: 120000 }); try {
this._logger.info('Found captions container'); await this._page.waitForSelector(selector, { timeout: 15000 });
} catch { this._logger.info(`Found captions container: ${selector}`);
this._logger.warn('Could not find captions container - captions may not have enabled'); return;
} catch {
// Try next
}
} }
this._logger.warn('Could not find captions container - captions may not have enabled or may use a different selector');
} }
/** /**
@ -553,11 +679,24 @@ export class CaptionsProcedure {
}); });
// Verify captions container is present // Verify captions container is present
const containerSelector = 'div[data-tid="closed-caption-renderer-wrapper"]'; const containerSelectors = [
try { 'div[data-tid="closed-caption-renderer-wrapper"]',
await this._page.waitForSelector(containerSelector, { timeout: 120000 }); 'div[data-tid="live-captions-renderer"]',
} catch { '[data-tid="caption-area"]',
this._logger.warn('Captions container not found, subscribing anyway'); ];
let containerFound = false;
for (const sel of containerSelectors) {
try {
await this._page.waitForSelector(sel, { timeout: 10000 });
containerFound = true;
this._logger.info(`Captions container found: ${sel}`);
break;
} catch {
// Try next
}
}
if (!containerFound) {
this._logger.warn('Captions container not found with known selectors, subscribing anyway');
} }
this._logger.info('Setting up MutationObserver for captions...'); this._logger.info('Setting up MutationObserver for captions...');