fix: Language and speech opens SUBMENU with closed-captions-button menuitemcheckbox, not a panel with switches
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
b0dffb49dd
commit
8f0b739308
1 changed files with 111 additions and 149 deletions
|
|
@ -106,44 +106,134 @@ export class CaptionsProcedure {
|
||||||
* Enable captions or transcription from the "More" menu.
|
* Enable captions or transcription from the "More" menu.
|
||||||
*
|
*
|
||||||
* Strategies in priority order:
|
* Strategies in priority order:
|
||||||
* 1. Direct captions button (anonymous / light-meetings UI)
|
* 1. Direct captions button (anonymous / light-meetings UI — closed-captions-button in main menu)
|
||||||
* 2. "Record and transcribe" → "Start transcription" (authenticated Teams 2025+)
|
* 2. "Language and speech" → SUBMENU → "Show live captions" (authenticated Teams 2025+)
|
||||||
* → triggers spoken-language-selection-dialog handled by _handleLanguageDialog()
|
* The submenu item is a menuitemcheckbox with id="closed-captions-button"
|
||||||
* → then "Show transcript" to open scraping panel
|
* aria-checked="false" + "Show live captions" → click to enable
|
||||||
* 3. "Captions & transcripts" submenu (older authenticated Teams)
|
* aria-checked="true" + "Hide live captions" → already on, don't click
|
||||||
* 4. "Language and speech" panel toggle (fallback)
|
* 3. "Record and transcribe" → "Start transcription" (fallback with transcript panel)
|
||||||
* 5. Generic text / DOM scan fallback
|
* 4. Generic text / DOM scan fallback
|
||||||
*/
|
*/
|
||||||
private async _clickEnableCaptions(): Promise<void> {
|
private async _clickEnableCaptions(): Promise<void> {
|
||||||
await this._logVisibleMenuItems();
|
await this._logVisibleMenuItems();
|
||||||
|
|
||||||
// ── Strategy 1: Direct captions button (anonymous / light-meetings UI) ──
|
// ── Strategy 1: Direct captions button (anonymous / light-meetings UI) ──
|
||||||
const directSelectors = [
|
// In anonymous mode, closed-captions-button appears directly in the More menu
|
||||||
'div[id="closed-captions-button"]',
|
const directBtn = await this._page.$('#closed-captions-button');
|
||||||
'[data-tid="closed-captions-button"]',
|
if (directBtn) {
|
||||||
'[data-tid="captions-toggle"]',
|
const ariaChecked = await directBtn.getAttribute('aria-checked');
|
||||||
|
const text = await directBtn.evaluate(el => el.textContent?.trim() || '');
|
||||||
|
this._logger.info(`Direct captions button found: aria-checked="${ariaChecked}", text="${text}"`);
|
||||||
|
|
||||||
|
if (ariaChecked === 'true' || text.toLowerCase().includes('hide')) {
|
||||||
|
this._logger.info('Live captions already ON (found "Hide live captions")');
|
||||||
|
await this._page.keyboard.press('Escape');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await directBtn.click();
|
||||||
|
this._logger.info('Clicked "Show live captions" (direct button)');
|
||||||
|
await this._page.waitForTimeout(1000);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Strategy 2: "Language and speech" → submenu → "Show live captions" ──
|
||||||
|
// Authenticated Teams 2025+: "Language and speech" has aria-haspopup="menu"
|
||||||
|
// Opening it reveals a submenu with closed-captions-button as menuitemcheckbox
|
||||||
|
const langSpeechSelectors = [
|
||||||
|
'#LanguageSpeechMenuControl-id',
|
||||||
|
'[data-tid="LanguageSpeechMenuControl-id"]',
|
||||||
|
'div[role="menuitem"]:has-text("Language and speech")',
|
||||||
|
'div[role="menuitem"]:has-text("Sprache und Spracheingabe")',
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const selector of directSelectors) {
|
for (const selector of langSpeechSelectors) {
|
||||||
try {
|
try {
|
||||||
const button = await this._page.$(selector);
|
const item = await this._page.$(selector);
|
||||||
if (button) {
|
if (item) {
|
||||||
await button.click();
|
await item.click();
|
||||||
this._logger.info(`Clicked direct captions button: ${selector}`);
|
this._logger.info(`Clicked "Language and speech": ${selector}`);
|
||||||
await this._page.waitForTimeout(1000);
|
await this._page.waitForTimeout(1500);
|
||||||
return;
|
|
||||||
|
// Now look for closed-captions-button in the submenu
|
||||||
|
const captionsBtn = await this._page.$('#closed-captions-button');
|
||||||
|
if (captionsBtn) {
|
||||||
|
const ariaChecked = await captionsBtn.getAttribute('aria-checked');
|
||||||
|
const btnText = await captionsBtn.evaluate(el => el.textContent?.trim() || '');
|
||||||
|
this._logger.info(`Submenu captions button: aria-checked="${ariaChecked}", text="${btnText}"`);
|
||||||
|
|
||||||
|
if (ariaChecked === 'true' || btnText.toLowerCase().includes('hide')) {
|
||||||
|
this._logger.info('Live captions already ON (found "Hide live captions")');
|
||||||
|
await this._page.keyboard.press('Escape');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await captionsBtn.click();
|
||||||
|
this._logger.info('Clicked "Show live captions" in Language and speech submenu');
|
||||||
|
await this._page.waitForTimeout(1000);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: try menuitemcheckbox with captions-related text
|
||||||
|
const fallbackSelectors = [
|
||||||
|
'[role="menuitemcheckbox"]:has-text("captions")',
|
||||||
|
'[role="menuitemcheckbox"]:has-text("Untertitel")',
|
||||||
|
'[role="menuitemcheckbox"]:has-text("caption")',
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const fbSel of fallbackSelectors) {
|
||||||
|
try {
|
||||||
|
const fbBtn = await this._page.$(fbSel);
|
||||||
|
if (fbBtn) {
|
||||||
|
const ariaChecked = await fbBtn.getAttribute('aria-checked');
|
||||||
|
const fbText = await fbBtn.evaluate(el => el.textContent?.trim() || '');
|
||||||
|
this._logger.info(`Fallback captions button: aria-checked="${ariaChecked}", text="${fbText}"`);
|
||||||
|
|
||||||
|
if (ariaChecked === 'true' || fbText.toLowerCase().includes('hide')) {
|
||||||
|
this._logger.info('Live captions already ON');
|
||||||
|
await this._page.keyboard.press('Escape');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await fbBtn.click();
|
||||||
|
this._logger.info(`Clicked captions button: ${fbSel}`);
|
||||||
|
await this._page.waitForTimeout(1000);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this._logger.warn('Language and speech submenu opened but no captions button found');
|
||||||
|
await this._page.keyboard.press('Escape');
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// Continue
|
// Continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Strategy 2: "Record and transcribe" → "Start transcription" + "Show transcript" ──
|
// ── Strategy 3 (fallback): "Record and transcribe" → "Start transcription" + panel ──
|
||||||
|
this._logger.info('Live captions not found, trying transcription fallback...');
|
||||||
|
|
||||||
|
// Clean menu state first
|
||||||
|
await this._page.keyboard.press('Escape');
|
||||||
|
await this._page.waitForTimeout(500);
|
||||||
|
await this._page.keyboard.press('Escape');
|
||||||
|
await this._page.waitForTimeout(500);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this._openMoreMenu();
|
||||||
|
} catch {
|
||||||
|
this._logger.warn('Could not re-open More menu for transcription fallback');
|
||||||
|
}
|
||||||
|
|
||||||
const recordMenuSelectors = [
|
const recordMenuSelectors = [
|
||||||
'[data-tid="RecordingMenuControl-id"]',
|
'[data-tid="RecordingMenuControl-id"]',
|
||||||
|
'#RecordingMenuControl-id',
|
||||||
'div[role="menuitem"]:has-text("Record and transcribe")',
|
'div[role="menuitem"]:has-text("Record and transcribe")',
|
||||||
'div[role="menuitem"]:has-text("Aufzeichnen und transkribieren")',
|
'div[role="menuitem"]:has-text("Aufzeichnen und transkribieren")',
|
||||||
'div[role="menuitem"]:has-text("Aufnehmen und transkribieren")',
|
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const selector of recordMenuSelectors) {
|
for (const selector of recordMenuSelectors) {
|
||||||
|
|
@ -179,13 +269,10 @@ export class CaptionsProcedure {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!alreadyRunning) {
|
if (!alreadyRunning) {
|
||||||
// Click "Start transcription" (only explicit "Start" selectors)
|
|
||||||
const startSelectors = [
|
const startSelectors = [
|
||||||
'[data-tid="call-transcript-button"]:has-text("Start")',
|
'[data-tid="call-transcript-button"]:has-text("Start")',
|
||||||
'[role="menuitem"]:has-text("Start transcription")',
|
'[role="menuitem"]:has-text("Start transcription")',
|
||||||
'[role="menuitem"]:has-text("Transkription starten")',
|
'[role="menuitem"]:has-text("Transkription starten")',
|
||||||
'button:has-text("Start transcription")',
|
|
||||||
'button:has-text("Transkription starten")',
|
|
||||||
];
|
];
|
||||||
|
|
||||||
let started = false;
|
let started = false;
|
||||||
|
|
@ -230,7 +317,6 @@ export class CaptionsProcedure {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close any remaining menu overlay
|
|
||||||
await this._page.keyboard.press('Escape');
|
await this._page.keyboard.press('Escape');
|
||||||
await this._page.waitForTimeout(500);
|
await this._page.waitForTimeout(500);
|
||||||
return;
|
return;
|
||||||
|
|
@ -240,124 +326,7 @@ export class CaptionsProcedure {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Strategy 3: "Captions & transcripts" submenu (older Teams) ──
|
// ── Strategy 4: DOM scan for anything containing "caption" / "transcri" ──
|
||||||
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")',
|
|
||||||
];
|
|
||||||
|
|
||||||
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);
|
|
||||||
|
|
||||||
const enableSelectors = [
|
|
||||||
'button:has-text("Turn on live captions")',
|
|
||||||
'button:has-text("Live captions")',
|
|
||||||
'button:has-text("Live-Untertitel aktivieren")',
|
|
||||||
'[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 4 (fallback): "Language and speech" panel toggle ──
|
|
||||||
this._logger.info('Trying "Language and speech" as fallback...');
|
|
||||||
|
|
||||||
// Ensure clean menu state: close any open panels/menus first
|
|
||||||
await this._page.keyboard.press('Escape');
|
|
||||||
await this._page.waitForTimeout(500);
|
|
||||||
await this._page.keyboard.press('Escape');
|
|
||||||
await this._page.waitForTimeout(500);
|
|
||||||
|
|
||||||
try {
|
|
||||||
await this._openMoreMenu();
|
|
||||||
} catch {
|
|
||||||
this._logger.warn('Could not re-open More menu for Language and speech fallback');
|
|
||||||
}
|
|
||||||
|
|
||||||
const langSpeechSelectors = [
|
|
||||||
'[data-tid="LanguageSpeechMenuControl-id"]',
|
|
||||||
'div[role="menuitem"]:has-text("Language and speech")',
|
|
||||||
'div[role="menuitem"]:has-text("Sprache und Spracheingabe")',
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const selector of langSpeechSelectors) {
|
|
||||||
try {
|
|
||||||
const item = await this._page.$(selector);
|
|
||||||
if (item) {
|
|
||||||
await item.click();
|
|
||||||
this._logger.info(`Clicked "Language and speech": ${selector}`);
|
|
||||||
await this._page.waitForTimeout(2000);
|
|
||||||
|
|
||||||
const toggleResult = await this._page.evaluate(() => {
|
|
||||||
const switches = document.querySelectorAll(
|
|
||||||
'input[role="switch"], [role="switch"], input[type="checkbox"]'
|
|
||||||
);
|
|
||||||
for (const sw of Array.from(switches)) {
|
|
||||||
const label = (sw.getAttribute('aria-label') || '').toLowerCase();
|
|
||||||
const tid = (sw.getAttribute('data-tid') || '').toLowerCase();
|
|
||||||
const parentEl = sw.closest('div, label, span') as HTMLElement;
|
|
||||||
const nearText = (parentEl?.textContent || '').toLowerCase();
|
|
||||||
const isCaptions =
|
|
||||||
label.includes('caption') || label.includes('untertitel') ||
|
|
||||||
tid.includes('caption') || tid.includes('subtitle') ||
|
|
||||||
nearText.includes('live caption') || nearText.includes('liveuntertitel');
|
|
||||||
if (isCaptions) {
|
|
||||||
if (!(sw as HTMLInputElement).checked) {
|
|
||||||
(sw as HTMLElement).click();
|
|
||||||
return { found: true, clicked: true, info: label || tid || nearText.substring(0, 60) };
|
|
||||||
}
|
|
||||||
return { found: true, clicked: false, info: `already on: ${label || tid}` };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return { found: false, clicked: false, info: '' };
|
|
||||||
});
|
|
||||||
|
|
||||||
this._logger.info(`Captions toggle result: ${JSON.stringify(toggleResult)}`);
|
|
||||||
if (toggleResult.found && toggleResult.clicked) {
|
|
||||||
await this._page.waitForTimeout(1500);
|
|
||||||
}
|
|
||||||
await this._page.keyboard.press('Escape');
|
|
||||||
if (toggleResult.found) return;
|
|
||||||
|
|
||||||
this._logger.warn('Language panel opened but no captions toggle found');
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// Continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Strategy 5: DOM scan for anything containing "caption" / "transcri" ──
|
|
||||||
const found = await this._page.evaluate(() => {
|
const found = await this._page.evaluate(() => {
|
||||||
const keywords = ['caption', 'captions', 'untertitel', 'live caption', 'transcri', 'transkri'];
|
const keywords = ['caption', 'captions', 'untertitel', 'live caption', 'transcri', 'transkri'];
|
||||||
const candidates = document.querySelectorAll(
|
const candidates = document.querySelectorAll(
|
||||||
|
|
@ -379,13 +348,6 @@ export class CaptionsProcedure {
|
||||||
if (found.clicked) {
|
if (found.clicked) {
|
||||||
this._logger.info(`Clicked via DOM scan: "${found.clicked}"`);
|
this._logger.info(`Clicked via DOM scan: "${found.clicked}"`);
|
||||||
await this._page.waitForTimeout(1500);
|
await this._page.waitForTimeout(1500);
|
||||||
|
|
||||||
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 submenu');
|
|
||||||
await this._page.waitForTimeout(1000);
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue