refactor: 2 path variants (Sign in / Join a meeting) with full page load waits

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
ValueOn AG 2026-02-17 11:49:00 +01:00
parent c892c93215
commit 8ed183f13c

View file

@ -49,19 +49,14 @@ export interface AuthTestResults {
const _VARIANTS: AuthTestVariant[] = [ const _VARIANTS: AuthTestVariant[] = [
{ {
id: 'chromiumClean', id: 'signIn',
name: 'Chromium Headful Clean', name: 'Pfad: Sign in',
description: 'Playwright Chromium headful, enhanced stealth + realistic devices, keine fake-flags', description: 'Nach Login auf Teams-Landingpage "Sign in" klicken — was kommt?',
}, },
{ {
id: 'chromiumNoAutomation', id: 'joinMeeting',
name: 'Chromium No-Automation', name: 'Pfad: Join a meeting',
description: 'Chromium headful, zusaetzlich --disable-extensions, --no-first-run', description: 'Nach Login auf Teams-Landingpage "Join a meeting" klicken — was kommt?',
},
{
id: 'rebrowserHeadful',
name: 'rebrowser-playwright Headful',
description: 'rebrowser-playwright headful, CDP-Leak-Fixes + realistic devices',
}, },
]; ];
@ -69,7 +64,7 @@ const _VARIANTS: AuthTestVariant[] = [
// CONSTANTS // CONSTANTS
// ============================================================================ // ============================================================================
const _VARIANT_TIMEOUT_MS = 60000; const _VARIANT_TIMEOUT_MS = 120000;
const _SCREENSHOT_QUALITY = 50; 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'; 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. * Run auth tests: 2 variants "Sign in" and "Join a meeting" on the Teams landing page.
* Does NOT navigate to any meeting URL only tests if Teams login works. * Both do full Microsoft login first, then click different buttons to see what page loads.
*/ */
export async function runAuthTests( export async function runAuthTests(
meetingUrl: string, meetingUrl: string,
@ -109,7 +104,6 @@ export async function runAuthTests(
const results: AuthTestResult[] = []; const results: AuthTestResult[] = [];
for (const variant of _VARIANTS) { for (const variant of _VARIANTS) {
// All variants require credentials
if (!botAccountEmail || !botAccountPassword) { if (!botAccountEmail || !botAccountPassword) {
results.push(_createSkippedResult(variant, 'Keine Credentials angegeben')); results.push(_createSkippedResult(variant, 'Keine Credentials angegeben'));
continue; continue;
@ -119,7 +113,7 @@ export async function runAuthTests(
try { try {
const result = await _runVariant(variant, meetingUrl, botAccountEmail, botAccountPassword); const result = await _runVariant(variant, meetingUrl, botAccountEmail, botAccountPassword);
results.push(result); 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) { } catch (err) {
logger.error(`[AuthTest] Variant ${variant.name} failed:`, err); logger.error(`[AuthTest] Variant ${variant.name} failed:`, err);
results.push(_createErrorResult(variant, String(err))); results.push(_createErrorResult(variant, String(err)));
@ -161,39 +155,30 @@ async function _runVariant(
}; };
try { try {
// Launch browser based on variant // Always use Chromium Headful Clean for both variants
const launchResult = await _launchBrowserForVariant(variant); const launchResult = await _launchChromiumHeadful(_BROWSER_ARGS_CLEAN);
browser = launchResult.browser; browser = launchResult.browser;
context = launchResult.context; context = launchResult.context;
const page = launchResult.page; const page = launchResult.page;
// Collect browser console messages // Collect browser console messages (only errors, to reduce noise)
page.on('console', (msg) => { page.on('console', (msg) => {
const type = msg.type(); const type = msg.type();
if (type === 'error' || type === 'warning') { if (type === 'error') {
variantLogs.push(`[CONSOLE:${type}] ${msg.text().substring(0, 200)}`); variantLogs.push(`[CONSOLE:${type}] ${msg.text().substring(0, 200)}`);
} }
}); });
// Collect page errors
page.on('pageerror', (err) => { page.on('pageerror', (err) => {
variantLogs.push(`[PAGE_ERROR] ${String(err).substring(0, 200)}`); variantLogs.push(`[PAGE_ERROR] ${String(err).substring(0, 200)}`);
}); });
// Track navigation/redirects
page.on('framenavigated', (frame) => { page.on('framenavigated', (frame) => {
if (frame === page.mainFrame()) { if (frame === page.mainFrame()) {
variantLogs.push(`[NAV] ${frame.url().substring(0, 150)}`); 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 authAttempted = false;
let authSuccess: boolean | null = null; let authSuccess: boolean | null = null;
const screenshots: StepScreenshot[] = []; 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', { await page.goto('https://teams.microsoft.com', {
waitUntil: 'domcontentloaded', waitUntil: 'domcontentloaded',
timeout: 30000, timeout: 30000,
}); });
// Wait for Teams to redirect to login.microsoftonline.com (up to 30s) // Wait for login redirect OR the Teams landing page to appear
_log('info', 'Waiting for login redirect to login.microsoftonline.com...'); _log('info', 'Waiting for login.microsoftonline.com redirect...');
try { try {
await page.waitForURL('**/login.microsoftonline.com/**', { timeout: 30000 }); await page.waitForURL('**/login.microsoftonline.com/**', { timeout: 30000 });
_log('info', `Redirected to login: ${page.url().substring(0, 150)}`); _log('info', `Redirected to login: ${page.url().substring(0, 150)}`);
} catch { } 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'); await _screenshotStep('1 - Login-Seite');
// --- Step 2: Login --- // =====================================================================
// STEP 2: Microsoft Login
// =====================================================================
if (botAccountEmail && botAccountPassword) { if (botAccountEmail && botAccountPassword) {
authAttempted = true; authAttempted = true;
_log('info', `Authenticating as ${botAccountEmail} (skipNavigation=true to preserve OAuth context)`);
const authProcedure = new AuthProcedure(page, logger); // Check if we're on login.microsoftonline.com or on the Teams landing page
authSuccess = await authProcedure.authenticateWithMicrosoft(botAccountEmail, botAccountPassword, true); const currentUrl = page.url();
_log(authSuccess ? 'info' : 'warn', `Auth result: ${authSuccess ? 'success' : 'failed'}`); const onMsLogin = currentUrl.includes('login.microsoftonline.com') || currentUrl.includes('login.live.com');
if (authSuccess) { if (onMsLogin) {
// Wait for redirect chain back to Teams _log('info', `On MS login page, authenticating as ${botAccountEmail}`);
_log('info', 'Waiting for redirect back to Teams...'); const authProcedure = new AuthProcedure(page, logger);
try { authSuccess = await authProcedure.authenticateWithMicrosoft(botAccountEmail, botAccountPassword, true);
await page.waitForURL('**/teams.microsoft.com/**', { timeout: 30000 }); _log(authSuccess ? 'info' : 'warn', `Auth result: ${authSuccess ? 'success' : 'failed'}`);
} catch { } else {
_log('warn', `No Teams redirect after 30s, current URL: ${page.url().substring(0, 150)}`); _log('info', 'Not on MS login page — Teams may already be loaded. Skipping auth step.');
} authSuccess = null;
await page.waitForTimeout(5000);
} }
await _screenshotStep('2 - Nach Login');
} else { } else {
_log('info', 'No credentials provided — skipping auth'); _log('info', 'No credentials provided — skipping auth');
} }
// --- Step 3: Teams App geladen --- // =====================================================================
_log('info', 'Waiting 10s for Teams app to fully load...'); // STEP 3: Wait for Teams landing page to fully render
await page.waitForTimeout(10000); // =====================================================================
await _screenshotStep('3 - Teams App'); _log('info', 'Waiting for Teams landing page to fully render...');
// --- Step 4: Click "Join a meeting" --- // Wait for redirect back to Teams (if we were on login page)
_log('info', 'Step 4: Looking for "Join a meeting" button...'); if (authSuccess) {
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) {
try { try {
const btn = await page.waitForSelector(selector, { timeout: 5000, state: 'visible' }); await page.waitForURL('**/teams.microsoft.com/**', { timeout: 30000 });
if (btn) {
await btn.click();
_log('info', `Clicked "Join a meeting": ${selector}`);
joinMeetingClicked = true;
await page.waitForTimeout(3000);
break;
}
} catch { } catch {
// Try next selector _log('warn', `No Teams redirect, current URL: ${page.url().substring(0, 150)}`);
} }
} }
if (!joinMeetingClicked) { // Wait for either "Sign in" or "Join a meeting" button to be visible
_log('warn', 'Could not find "Join a meeting" button — logging all visible buttons'); // This is the Teams landing page ("Everyone together in Teams")
try { _log('info', 'Waiting for "Sign in" or "Join a meeting" button to appear...');
const buttons = await page.evaluate(() => { try {
const btns = document.querySelectorAll('button, a[role="button"], [role="menuitem"]'); await page.waitForSelector(
return Array.from(btns).slice(0, 30).map(b => { 'button:has-text("Sign in"), button:has-text("Join a meeting"), button:has-text("Anmelden"), button:has-text("An Besprechung teilnehmen")',
const text = (b.textContent || '').trim().substring(0, 80); { timeout: 30000, state: 'visible' },
const tid = b.getAttribute('data-tid') || ''; );
const title = b.getAttribute('title') || ''; await page.waitForTimeout(3000);
return `[${b.tagName} tid="${tid}" title="${title}"] ${text}`; _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) { buttons.forEach(b => _log('info', ` Visible button: ${b}`));
_log('warn', `Could not enumerate buttons: ${e}`); } catch {
} // Ignore
} }
await _screenshotStep('4 - Join a meeting');
// --- Step 5: Enter meeting URL --- // =====================================================================
if (joinMeetingClicked) { // STEP 4: Click variant-specific button
_log('info', `Step 5: Entering meeting URL: ${meetingUrl.substring(0, 80)}`); // =====================================================================
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 = [ const meetingInputSelectors = [
'input[placeholder*="meeting"]', 'input[placeholder*="meeting" i]',
'input[placeholder*="Besprechung"]', 'input[placeholder*="code" i]',
'input[placeholder*="code"]', 'input[placeholder*="link" i]',
'input[placeholder*="Code"]', 'input[placeholder*="ID" i]',
'input[placeholder*="link"]',
'input[placeholder*="Link"]',
'input[data-tid="join-meeting-input"]',
'input[type="text"]', 'input[type="text"]',
'input[type="url"]', 'input[type="url"]',
]; ];
@ -323,7 +393,7 @@ async function _runVariant(
await input.fill(meetingUrl); await input.fill(meetingUrl);
_log('info', `Entered meeting URL in: ${selector}`); _log('info', `Entered meeting URL in: ${selector}`);
meetingInputFound = true; meetingInputFound = true;
await page.waitForTimeout(2000); await page.waitForTimeout(3000);
break; break;
} }
} catch { } catch {
@ -331,70 +401,49 @@ async function _runVariant(
} }
} }
if (!meetingInputFound) { if (meetingInputFound) {
_log('warn', 'Could not find meeting URL input field'); 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)}`); _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; const screenshot = screenshots.length > 0 ? screenshots[screenshots.length - 1].data : undefined;
return { return {
variantId: variant.id, variantId: variant.id,
variantName: variant.name, variantName: variant.name,
success: true, success: true,
pageType: detection.pageType, pageType: 'unknown',
finalUrl: page.url(), finalUrl: page.url(),
hasSignInLink: detection.hasSignInLink, hasSignInLink: false,
hasNameInput: detection.hasNameInput, hasNameInput: false,
hasJoinButton: detection.hasJoinButton, hasJoinButton: false,
authAttempted, authAttempted,
authSuccess, authSuccess,
screenshot, screenshot,
screenshots, screenshots,
durationMs: Date.now() - startTime, durationMs: Date.now() - startTime,
detectedSignals: detection.signals, detectedSignals: [],
logs: variantLogs, logs: variantLogs,
}; };
} catch (err) { } catch (err) {
_log('error', `Variant failed: ${String(err).substring(0, 300)}`); _log('error', `Variant failed: ${String(err).substring(0, 300)}`);
// Try to take an error screenshot
let screenshot: string | undefined; let screenshot: string | undefined;
const screenshots: StepScreenshot[] = [];
try { try {
if (context) { if (context) {
const pages = context.pages(); const pages = context.pages();
if (pages.length > 0) { if (pages.length > 0) {
screenshot = await _takeScreenshot(pages[0]); screenshot = await _takeScreenshot(pages[0]);
if (screenshot) screenshots.push({ label: 'Error', data: screenshot });
} }
} }
} catch { } catch {
@ -413,6 +462,7 @@ async function _runVariant(
authAttempted: false, authAttempted: false,
authSuccess: null, authSuccess: null,
screenshot, screenshot,
screenshots,
durationMs: Date.now() - startTime, durationMs: Date.now() - startTime,
error: String(err), error: String(err),
detectedSignals: [], detectedSignals: [],
@ -438,22 +488,9 @@ interface LaunchResult {
page: Page; page: Page;
} }
async function _launchBrowserForVariant(variant: AuthTestVariant): Promise<LaunchResult> { async function _launchBrowserForVariant(_variant: AuthTestVariant): Promise<LaunchResult> {
_requireDisplay(); // All variants are headful _requireDisplay();
return _launchChromiumHeadful(_BROWSER_ARGS_CLEAN);
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);
}
} }
/** /**
@ -1100,26 +1137,16 @@ function _createErrorResult(variant: AuthTestVariant, error: string): AuthTestRe
* Generate a recommendation based on test results. * Generate a recommendation based on test results.
*/ */
function _generateRecommendation(results: AuthTestResult[]): string { function _generateRecommendation(results: AuthTestResult[]): string {
const v2Variants = results.filter(r => r.pageType === 'v2'); const authSuccess = results.filter(r => r.authAttempted && r.authSuccess === true);
const authSuccess = results.find(r => r.authAttempted && r.authSuccess === true); const failures = results.filter(r => !r.success);
if (authSuccess) { if (authSuccess.length > 0) {
return `Variante "${authSuccess.variantName}" hat Auth erfolgreich durchgefuehrt! ` + return `Microsoft-Login erfolgreich. Screenshots pruefen, um den besten Pfad zu waehlen.`;
`Auth-Flow kann mit dieser Konfiguration aktiviert werden.`;
} }
if (v2Variants.length > 0) { if (failures.length === results.length) {
const names = v2Variants.map(v => `"${v.variantName}"`).join(', '); return 'Beide Varianten fehlgeschlagen. Logs und Screenshots pruefen.';
return `${v2Variants.length} Variante(n) haben /v2/ erhalten: ${names}. ` +
`Auth ist prinzipiell moeglich — Auth-Flow kann re-aktiviert werden.`;
} }
const successVariants = results.filter(r => r.success); return 'Test abgeschlossen. Screenshots pruefen und naechste Schritte planen.';
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.';
} }