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[] = [
{
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<LaunchResult> {
_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<LaunchResult> {
_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.';
}