From 8ed183f13c60a5b463d1f12b0d84fea97c069fb5 Mon Sep 17 00:00:00 2001 From: ValueOn AG Date: Tue, 17 Feb 2026 11:49:00 +0100 Subject: [PATCH] refactor: 2 path variants (Sign in / Join a meeting) with full page load waits Co-authored-by: Cursor --- src/bot/authTestProcedure.ts | 373 +++++++++++++++++++---------------- 1 file changed, 200 insertions(+), 173 deletions(-) diff --git a/src/bot/authTestProcedure.ts b/src/bot/authTestProcedure.ts index a766161..60cbaae 100644 --- a/src/bot/authTestProcedure.ts +++ b/src/bot/authTestProcedure.ts @@ -49,19 +49,14 @@ export interface AuthTestResults { const _VARIANTS: AuthTestVariant[] = [ { - id: 'chromiumClean', - name: 'Chromium Headful Clean', - description: 'Playwright Chromium headful, enhanced stealth + realistic devices, keine fake-flags', + id: 'signIn', + name: 'Pfad: Sign in', + description: 'Nach Login auf Teams-Landingpage "Sign in" klicken — was kommt?', }, { - id: 'chromiumNoAutomation', - name: 'Chromium No-Automation', - description: 'Chromium headful, zusaetzlich --disable-extensions, --no-first-run', - }, - { - id: 'rebrowserHeadful', - name: 'rebrowser-playwright Headful', - description: 'rebrowser-playwright headful, CDP-Leak-Fixes + realistic devices', + id: 'joinMeeting', + name: 'Pfad: Join a meeting', + description: 'Nach Login auf Teams-Landingpage "Join a meeting" klicken — was kommt?', }, ]; @@ -69,7 +64,7 @@ const _VARIANTS: AuthTestVariant[] = [ // CONSTANTS // ============================================================================ -const _VARIANT_TIMEOUT_MS = 60000; +const _VARIANT_TIMEOUT_MS = 120000; const _SCREENSHOT_QUALITY = 50; const _USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36 Edg/131.0.0.0'; @@ -96,8 +91,8 @@ const _BROWSER_ARGS_NO_AUTOMATION = [ // ============================================================================ /** - * Run auth detection test variants: login at teams.microsoft.com, screenshot where we land. - * Does NOT navigate to any meeting URL — only tests if Teams login works. + * Run auth tests: 2 variants — "Sign in" and "Join a meeting" on the Teams landing page. + * Both do full Microsoft login first, then click different buttons to see what page loads. */ export async function runAuthTests( meetingUrl: string, @@ -109,7 +104,6 @@ export async function runAuthTests( const results: AuthTestResult[] = []; for (const variant of _VARIANTS) { - // All variants require credentials if (!botAccountEmail || !botAccountPassword) { results.push(_createSkippedResult(variant, 'Keine Credentials angegeben')); continue; @@ -119,7 +113,7 @@ export async function runAuthTests( try { const result = await _runVariant(variant, meetingUrl, botAccountEmail, botAccountPassword); results.push(result); - logger.info(`[AuthTest] Variant ${variant.name}: pageType=${result.pageType}, url=${result.finalUrl.substring(0, 80)}`); + logger.info(`[AuthTest] Variant ${variant.name}: finalUrl=${result.finalUrl.substring(0, 80)}`); } catch (err) { logger.error(`[AuthTest] Variant ${variant.name} failed:`, err); results.push(_createErrorResult(variant, String(err))); @@ -161,39 +155,30 @@ async function _runVariant( }; try { - // Launch browser based on variant - const launchResult = await _launchBrowserForVariant(variant); + // Always use Chromium Headful Clean for both variants + const launchResult = await _launchChromiumHeadful(_BROWSER_ARGS_CLEAN); browser = launchResult.browser; context = launchResult.context; const page = launchResult.page; - // Collect browser console messages + // Collect browser console messages (only errors, to reduce noise) page.on('console', (msg) => { const type = msg.type(); - if (type === 'error' || type === 'warning') { + if (type === 'error') { variantLogs.push(`[CONSOLE:${type}] ${msg.text().substring(0, 200)}`); } }); - // Collect page errors page.on('pageerror', (err) => { variantLogs.push(`[PAGE_ERROR] ${String(err).substring(0, 200)}`); }); - // Track navigation/redirects page.on('framenavigated', (frame) => { if (frame === page.mainFrame()) { variantLogs.push(`[NAV] ${frame.url().substring(0, 150)}`); } }); - // Flow with screenshots at every step: - // 1. Navigate to teams.microsoft.com → wait for login redirect → screenshot - // 2. Login (email → password → stay signed in) → screenshot after auth - // 3. Wait for Teams app to load → screenshot - // 4. Click "Join a meeting" → screenshot - // 5. Navigate to meeting URL → screenshot (final) - let authAttempted = false; let authSuccess: boolean | null = null; const screenshots: StepScreenshot[] = []; @@ -206,111 +191,196 @@ async function _runVariant( } } - // --- Step 1: Navigate to teams.microsoft.com --- - _log('info', `Step 1: Navigate to teams.microsoft.com`); + // ===================================================================== + // STEP 1: Navigate to teams.microsoft.com + // ===================================================================== + _log('info', 'Step 1: Navigate to teams.microsoft.com'); await page.goto('https://teams.microsoft.com', { waitUntil: 'domcontentloaded', timeout: 30000, }); - // Wait for Teams to redirect to login.microsoftonline.com (up to 30s) - _log('info', 'Waiting for login redirect to login.microsoftonline.com...'); + // Wait for login redirect OR the Teams landing page to appear + _log('info', 'Waiting for login.microsoftonline.com redirect...'); try { await page.waitForURL('**/login.microsoftonline.com/**', { timeout: 30000 }); _log('info', `Redirected to login: ${page.url().substring(0, 150)}`); } catch { - _log('warn', `No login redirect after 30s, current URL: ${page.url().substring(0, 150)}`); + _log('warn', `No login redirect, current URL: ${page.url().substring(0, 150)}`); + } + + // Wait for the login page to FULLY RENDER (email input visible) + _log('info', 'Waiting for login page to fully render...'); + try { + await page.waitForSelector( + 'input[name="loginfmt"], input[type="email"], button:has-text("Sign in"), button:has-text("Join a meeting")', + { timeout: 15000, state: 'visible' }, + ); + await page.waitForTimeout(2000); + } catch { + _log('warn', 'Login page elements not found, taking screenshot anyway'); + await page.waitForTimeout(5000); } await _screenshotStep('1 - Login-Seite'); - // --- Step 2: Login --- + // ===================================================================== + // STEP 2: Microsoft Login + // ===================================================================== if (botAccountEmail && botAccountPassword) { authAttempted = true; - _log('info', `Authenticating as ${botAccountEmail} (skipNavigation=true to preserve OAuth context)`); - const authProcedure = new AuthProcedure(page, logger); - authSuccess = await authProcedure.authenticateWithMicrosoft(botAccountEmail, botAccountPassword, true); - _log(authSuccess ? 'info' : 'warn', `Auth result: ${authSuccess ? 'success' : 'failed'}`); + // Check if we're on login.microsoftonline.com or on the Teams landing page + const currentUrl = page.url(); + const onMsLogin = currentUrl.includes('login.microsoftonline.com') || currentUrl.includes('login.live.com'); - if (authSuccess) { - // Wait for redirect chain back to Teams - _log('info', 'Waiting for redirect back to Teams...'); - try { - await page.waitForURL('**/teams.microsoft.com/**', { timeout: 30000 }); - } catch { - _log('warn', `No Teams redirect after 30s, current URL: ${page.url().substring(0, 150)}`); - } - await page.waitForTimeout(5000); + if (onMsLogin) { + _log('info', `On MS login page, authenticating as ${botAccountEmail}`); + const authProcedure = new AuthProcedure(page, logger); + authSuccess = await authProcedure.authenticateWithMicrosoft(botAccountEmail, botAccountPassword, true); + _log(authSuccess ? 'info' : 'warn', `Auth result: ${authSuccess ? 'success' : 'failed'}`); + } else { + _log('info', 'Not on MS login page — Teams may already be loaded. Skipping auth step.'); + authSuccess = null; } - await _screenshotStep('2 - Nach Login'); } else { _log('info', 'No credentials provided — skipping auth'); } - // --- Step 3: Teams App geladen --- - _log('info', 'Waiting 10s for Teams app to fully load...'); - await page.waitForTimeout(10000); - await _screenshotStep('3 - Teams App'); + // ===================================================================== + // STEP 3: Wait for Teams landing page to fully render + // ===================================================================== + _log('info', 'Waiting for Teams landing page to fully render...'); - // --- Step 4: Click "Join a meeting" --- - _log('info', 'Step 4: Looking for "Join a meeting" button...'); - const joinMeetingSelectors = [ - 'button:has-text("Join a meeting")', - 'button:has-text("An Besprechung teilnehmen")', - 'a:has-text("Join a meeting")', - 'a:has-text("An Besprechung teilnehmen")', - '[data-tid="join-meeting-button"]', - '[data-tid="join-a-meeting-button"]', - 'button[title="Join a meeting"]', - 'button[aria-label="Join a meeting"]', - ]; - - let joinMeetingClicked = false; - for (const selector of joinMeetingSelectors) { + // Wait for redirect back to Teams (if we were on login page) + if (authSuccess) { try { - const btn = await page.waitForSelector(selector, { timeout: 5000, state: 'visible' }); - if (btn) { - await btn.click(); - _log('info', `Clicked "Join a meeting": ${selector}`); - joinMeetingClicked = true; - await page.waitForTimeout(3000); - break; - } + await page.waitForURL('**/teams.microsoft.com/**', { timeout: 30000 }); } catch { - // Try next selector + _log('warn', `No Teams redirect, current URL: ${page.url().substring(0, 150)}`); } } - if (!joinMeetingClicked) { - _log('warn', 'Could not find "Join a meeting" button — logging all visible buttons'); - try { - const buttons = await page.evaluate(() => { - const btns = document.querySelectorAll('button, a[role="button"], [role="menuitem"]'); - return Array.from(btns).slice(0, 30).map(b => { - const text = (b.textContent || '').trim().substring(0, 80); - const tid = b.getAttribute('data-tid') || ''; - const title = b.getAttribute('title') || ''; - return `[${b.tagName} tid="${tid}" title="${title}"] ${text}`; - }); + // Wait for either "Sign in" or "Join a meeting" button to be visible + // This is the Teams landing page ("Everyone together in Teams") + _log('info', 'Waiting for "Sign in" or "Join a meeting" button to appear...'); + try { + await page.waitForSelector( + 'button:has-text("Sign in"), button:has-text("Join a meeting"), button:has-text("Anmelden"), button:has-text("An Besprechung teilnehmen")', + { timeout: 30000, state: 'visible' }, + ); + await page.waitForTimeout(3000); + _log('info', 'Teams landing page loaded'); + } catch { + _log('warn', 'Landing page buttons not found after 30s'); + await page.waitForTimeout(5000); + } + await _screenshotStep('2 - Teams Landingpage'); + + // Log all visible buttons for debugging + try { + const buttons = await page.evaluate(() => { + const btns = document.querySelectorAll('button, a[role="button"]'); + return Array.from(btns).slice(0, 20).map(b => { + const text = (b.textContent || '').trim().substring(0, 80); + const tid = b.getAttribute('data-tid') || ''; + return `[${b.tagName} tid="${tid}"] ${text}`; }); - buttons.forEach(b => _log('info', ` Button: ${b}`)); - } catch (e) { - _log('warn', `Could not enumerate buttons: ${e}`); - } + }); + buttons.forEach(b => _log('info', ` Visible button: ${b}`)); + } catch { + // Ignore } - await _screenshotStep('4 - Join a meeting'); - // --- Step 5: Enter meeting URL --- - if (joinMeetingClicked) { - _log('info', `Step 5: Entering meeting URL: ${meetingUrl.substring(0, 80)}`); + // ===================================================================== + // STEP 4: Click variant-specific button + // ===================================================================== + if (variant.id === 'signIn') { + // --- VARIANT A: Click "Sign in" --- + _log('info', 'Step 3: Clicking "Sign in" on Teams landing page...'); + const signInSelectors = [ + 'button:has-text("Sign in")', + 'button:has-text("Anmelden")', + 'a:has-text("Sign in")', + 'a:has-text("Anmelden")', + ]; + + let clicked = false; + for (const selector of signInSelectors) { + try { + const btn = await page.waitForSelector(selector, { timeout: 5000, state: 'visible' }); + if (btn) { + await btn.click(); + _log('info', `Clicked: ${selector}`); + clicked = true; + break; + } + } catch { + // Try next + } + } + + if (!clicked) { + _log('warn', '"Sign in" button not found on landing page'); + } + + // Wait for the resulting page to FULLY load + _log('info', 'Waiting for resulting page to fully load...'); + await page.waitForTimeout(5000); + try { + await page.waitForLoadState('networkidle', { timeout: 20000 }); + } catch { + _log('warn', 'networkidle timeout, continuing'); + } + await page.waitForTimeout(5000); + await _screenshotStep('3 - Nach "Sign in"'); + + } else if (variant.id === 'joinMeeting') { + // --- VARIANT B: Click "Join a meeting" --- + _log('info', 'Step 3: Clicking "Join a meeting" on Teams landing page...'); + const joinSelectors = [ + 'button:has-text("Join a meeting")', + 'button:has-text("An Besprechung teilnehmen")', + 'a:has-text("Join a meeting")', + 'a:has-text("An Besprechung teilnehmen")', + ]; + + let clicked = false; + for (const selector of joinSelectors) { + try { + const btn = await page.waitForSelector(selector, { timeout: 5000, state: 'visible' }); + if (btn) { + await btn.click(); + _log('info', `Clicked: ${selector}`); + clicked = true; + break; + } + } catch { + // Try next + } + } + + if (!clicked) { + _log('warn', '"Join a meeting" button not found on landing page'); + } + + // Wait for the resulting page to FULLY load + _log('info', 'Waiting for resulting page to fully load...'); + await page.waitForTimeout(5000); + try { + await page.waitForLoadState('networkidle', { timeout: 20000 }); + } catch { + _log('warn', 'networkidle timeout, continuing'); + } + await page.waitForTimeout(5000); + await _screenshotStep('3 - Nach "Join a meeting"'); + + // If there's an input for meeting URL, enter it + _log('info', 'Looking for meeting URL input...'); const meetingInputSelectors = [ - 'input[placeholder*="meeting"]', - 'input[placeholder*="Besprechung"]', - 'input[placeholder*="code"]', - 'input[placeholder*="Code"]', - 'input[placeholder*="link"]', - 'input[placeholder*="Link"]', - 'input[data-tid="join-meeting-input"]', + 'input[placeholder*="meeting" i]', + 'input[placeholder*="code" i]', + 'input[placeholder*="link" i]', + 'input[placeholder*="ID" i]', 'input[type="text"]', 'input[type="url"]', ]; @@ -323,7 +393,7 @@ async function _runVariant( await input.fill(meetingUrl); _log('info', `Entered meeting URL in: ${selector}`); meetingInputFound = true; - await page.waitForTimeout(2000); + await page.waitForTimeout(3000); break; } } catch { @@ -331,70 +401,49 @@ async function _runVariant( } } - if (!meetingInputFound) { - _log('warn', 'Could not find meeting URL input field'); + if (meetingInputFound) { + await page.waitForTimeout(5000); + await _screenshotStep('4 - Meeting URL eingegeben'); + } else { + _log('warn', 'No meeting URL input field found'); } - - // Try clicking "Join" button after entering URL - const joinNowSelectors = [ - 'button:has-text("Join")', - 'button:has-text("Teilnehmen")', - 'button:has-text("Join now")', - 'button:has-text("Jetzt teilnehmen")', - '#prejoin-join-button', - 'button[data-tid="prejoin-join-button"]', - ]; - - for (const selector of joinNowSelectors) { - try { - const btn = await page.$(selector); - if (btn && await btn.isVisible()) { - _log('info', `Found Join button: ${selector} (NOT clicking — test only)`); - break; - } - } catch { - // Try next - } - } - - await page.waitForTimeout(5000); - await _screenshotStep('5 - Meeting URL eingegeben'); } - // --- Final: detect page type --- + // ===================================================================== + // FINAL: Log result + // ===================================================================== _log('info', `Final URL: ${page.url().substring(0, 150)}`); - const detection = await _detectPageType(page); - // Use last screenshot as the main screenshot for backward compatibility const screenshot = screenshots.length > 0 ? screenshots[screenshots.length - 1].data : undefined; return { variantId: variant.id, variantName: variant.name, success: true, - pageType: detection.pageType, + pageType: 'unknown', finalUrl: page.url(), - hasSignInLink: detection.hasSignInLink, - hasNameInput: detection.hasNameInput, - hasJoinButton: detection.hasJoinButton, + hasSignInLink: false, + hasNameInput: false, + hasJoinButton: false, authAttempted, authSuccess, screenshot, screenshots, durationMs: Date.now() - startTime, - detectedSignals: detection.signals, + detectedSignals: [], logs: variantLogs, }; } catch (err) { _log('error', `Variant failed: ${String(err).substring(0, 300)}`); - // Try to take an error screenshot let screenshot: string | undefined; + const screenshots: StepScreenshot[] = []; try { if (context) { const pages = context.pages(); if (pages.length > 0) { screenshot = await _takeScreenshot(pages[0]); + if (screenshot) screenshots.push({ label: 'Error', data: screenshot }); } } } catch { @@ -413,6 +462,7 @@ async function _runVariant( authAttempted: false, authSuccess: null, screenshot, + screenshots, durationMs: Date.now() - startTime, error: String(err), detectedSignals: [], @@ -438,22 +488,9 @@ interface LaunchResult { page: Page; } -async function _launchBrowserForVariant(variant: AuthTestVariant): Promise { - _requireDisplay(); // All variants are headful - - switch (variant.id) { - case 'chromiumClean': - return _launchChromiumHeadful(_BROWSER_ARGS_CLEAN); - - case 'chromiumNoAutomation': - return _launchChromiumHeadful(_BROWSER_ARGS_NO_AUTOMATION); - - case 'rebrowserHeadful': - return _launchRebrowserHeadful(); - - default: - return _launchChromiumHeadful(_BROWSER_ARGS_CLEAN); - } +async function _launchBrowserForVariant(_variant: AuthTestVariant): Promise { + _requireDisplay(); + return _launchChromiumHeadful(_BROWSER_ARGS_CLEAN); } /** @@ -1100,26 +1137,16 @@ function _createErrorResult(variant: AuthTestVariant, error: string): AuthTestRe * Generate a recommendation based on test results. */ function _generateRecommendation(results: AuthTestResult[]): string { - const v2Variants = results.filter(r => r.pageType === 'v2'); - const authSuccess = results.find(r => r.authAttempted && r.authSuccess === true); + const authSuccess = results.filter(r => r.authAttempted && r.authSuccess === true); + const failures = results.filter(r => !r.success); - if (authSuccess) { - return `Variante "${authSuccess.variantName}" hat Auth erfolgreich durchgefuehrt! ` + - `Auth-Flow kann mit dieser Konfiguration aktiviert werden.`; + if (authSuccess.length > 0) { + return `Microsoft-Login erfolgreich. Screenshots pruefen, um den besten Pfad zu waehlen.`; } - if (v2Variants.length > 0) { - const names = v2Variants.map(v => `"${v.variantName}"`).join(', '); - return `${v2Variants.length} Variante(n) haben /v2/ erhalten: ${names}. ` + - `Auth ist prinzipiell moeglich — Auth-Flow kann re-aktiviert werden.`; + if (failures.length === results.length) { + return 'Beide Varianten fehlgeschlagen. Logs und Screenshots pruefen.'; } - const successVariants = results.filter(r => r.success); - if (successVariants.every(r => r.pageType === 'lightMeetings')) { - return 'Alle Varianten wurden auf light-meetings umgeleitet. ' + - 'Teams blockiert den authentifizierten Join fuer alle getesteten Browser-Konfigurationen. ' + - 'Weitere Optionen: Graph API / Bot Framework oder andere Stealth-Ansaetze evaluieren.'; - } - - return 'Test abgeschlossen. Ergebnisse pruefen und naechste Schritte planen.'; + return 'Test abgeschlossen. Screenshots pruefen und naechste Schritte planen.'; }