From 3207abae4dfd2d4b79699c65af8d625e2cf6871f Mon Sep 17 00:00:00 2001 From: LightZirconite Date: Sun, 16 Nov 2025 19:59:38 +0100 Subject: [PATCH] fix: restore express-rate-limit dependency and enhance Fluent UI interaction methods for account creation --- package.json | 6 +- src/account-creation/AccountCreator.ts | 233 +++++++++++++++++++------ 2 files changed, 185 insertions(+), 54 deletions(-) diff --git a/package.json b/package.json index 6ecc58b..3a155f6 100644 --- a/package.json +++ b/package.json @@ -71,6 +71,7 @@ "chalk": "^4.1.2", "cheerio": "^1.0.0", "express": "^4.21.2", + "express-rate-limit": "^8.2.1", "fingerprint-generator": "^2.1.66", "fingerprint-injector": "^2.1.66", "http-proxy-agent": "^7.0.2", @@ -82,7 +83,6 @@ "rebrowser-playwright": "1.52.0", "socks-proxy-agent": "^8.0.5", "ts-node": "^10.9.2", - "ws": "^8.18.3", - "express-rate-limit": "^8.2.1" + "ws": "^8.18.3" } -} \ No newline at end of file +} diff --git a/src/account-creation/AccountCreator.ts b/src/account-creation/AccountCreator.ts index 7c70987..3afb71d 100644 --- a/src/account-creation/AccountCreator.ts +++ b/src/account-creation/AccountCreator.ts @@ -84,7 +84,8 @@ export class AccountCreator { operation: () => Promise, context: string, maxRetries: number = 3, - initialDelayMs: number = 1000 + initialDelayMs: number = 1000, + enableMicroGestures: boolean = true ): Promise { for (let attempt = 1; attempt <= maxRetries; attempt++) { try { @@ -102,7 +103,8 @@ export class AccountCreator { await this.page.waitForTimeout(Math.floor(humanDelay)) // IMPROVED: Random micro-gesture during retry (human frustration pattern) - if (Math.random() < 0.4) { + // CRITICAL: Can be disabled for sensitive operations (e.g., dropdowns) where scroll would break state + if (enableMicroGestures && Math.random() < 0.4) { await this.human.microGestures(`${context}_RETRY_${attempt}`) } } else { @@ -114,6 +116,30 @@ export class AccountCreator { return null } + /** + * IMPROVED: Fluent UI-compatible interaction method + * Uses focus + Enter (keyboard) which works better than mouse clicks for Fluent UI components + * Falls back to direct click if focus fails + * + * @param locator - Playwright locator for the element + * @param context - Context name for logging + * @returns Promise + */ + private async fluentUIClick(locator: ReturnType, context: string): Promise { + try { + // STRATEGY 1: Focus + Enter (most compatible with Fluent UI) + await locator.focus() + await this.humanDelay(300, 600) + await this.page.keyboard.press('Enter') + log(false, 'CREATOR', `[${context}] ✓ Pressed Enter on focused element`, 'log', 'cyan') + } catch (focusError) { + // STRATEGY 2: Fallback to direct click + log(false, 'CREATOR', `[${context}] ⚠️ Focus+Enter failed, using direct click`, 'warn', 'yellow') + await locator.click({ timeout: 5000 }) + log(false, 'CREATOR', `[${context}] ✓ Direct click executed`, 'log', 'cyan') + } + } + /** * CRITICAL: Wait for dropdown to be fully closed before continuing * IMPROVED: Detect Fluent UI dropdown states reliably @@ -1050,7 +1076,7 @@ export class AccountCreator { // CRITICAL: Get current URL before clicking const urlBeforeClick = this.page.url() - await nextBtn.click() + await this.fluentUIClick(nextBtn, 'EMAIL_NEXT') // OPTIMIZED: Reduced delay after clicking Next await this.humanDelay(1000, 1500) await this.waitForPageStable('AFTER_EMAIL_SUBMIT', 10000) @@ -1175,7 +1201,7 @@ export class AccountCreator { } const nextBtn = this.page.locator('button[data-testid="primaryButton"], button[type="submit"]').first() - await nextBtn.click() + await this.fluentUIClick(nextBtn, 'RETRY_EMAIL_NEXT') await this.humanDelay(2000, 3000) await this.waitForPageStable('RETRY_EMAIL', 15000) @@ -1298,8 +1324,8 @@ export class AccountCreator { log(false, 'CREATOR', `Selecting suggestion: ${cleanEmail}`, 'log', 'cyan') - // Click the suggestion - await firstButton.click() + // Click the suggestion using Fluent UI compatible method + await this.fluentUIClick(firstButton, 'EMAIL_SUGGESTION') await this.humanDelay(500, 1000) // REDUCED: Faster suggestion click (was 1500-2500) // Verify the email input was updated @@ -1378,7 +1404,7 @@ export class AccountCreator { // Get current URL and page state before clicking const urlBefore = this.page.url() - await nextBtn.click() + await this.fluentUIClick(nextBtn, `NEXT_${step.toUpperCase()}`) log(false, 'CREATOR', `✅ Clicked Next (${step})`, 'log', 'green') // CRITICAL: Wait for page to process the click @@ -1512,29 +1538,50 @@ export class AccountCreator { try { await this.humanDelay(2000, 3000) - // CRITICAL: Microsoft changed order - MONTH must be filled BEFORE DAY - // Detect order by checking which dropdown appears first in DOM - const monthFirst = await this.page.locator('button#BirthMonthDropdown').first().boundingBox().then(box => box?.y ?? 0).catch(() => 0) - const dayFirst = await this.page.locator('button#BirthDayDropdown').first().boundingBox().then(box => box?.y ?? 0).catch(() => 0) + // CRITICAL FIX: Microsoft UI has Month and Day side-by-side (same Y position) + // Cannot detect order by Y position. Checking DOM order instead. + // According to HTML structure analysis: Month ALWAYS comes before Day in DOM - const monthBeforeDay = monthFirst > 0 && monthFirst < dayFirst + const monthBox = await this.page.locator('button#BirthMonthDropdown').first().boundingBox().catch(() => null) + const dayBox = await this.page.locator('button#BirthDayDropdown').first().boundingBox().catch(() => null) + + const monthX = monthBox?.x ?? 0 + const dayX = dayBox?.x ?? 0 + const monthY = monthBox?.y ?? 0 + const dayY = dayBox?.y ?? 0 + + log(false, 'CREATOR', `[LAYOUT_DETECTION] Month (${monthX}, ${monthY}), Day (${dayX}, ${dayY})`, 'log', 'cyan') + + // Check if buttons are on same horizontal line (difference < 10px) + const sameLine = Math.abs(monthY - dayY) < 10 + + let monthBeforeDay = false + if (sameLine) { + // Horizontal layout - check X position (left-to-right) + monthBeforeDay = monthX > 0 && dayX > 0 && monthX < dayX + log(false, 'CREATOR', `[LAYOUT_DETECTION] Horizontal layout detected (Month X < Day X: ${monthBeforeDay})`, 'log', 'cyan') + } else { + // Vertical layout - check Y position (top-to-bottom) + monthBeforeDay = monthY > 0 && dayY > 0 && monthY < dayY + log(false, 'CREATOR', `[LAYOUT_DETECTION] Vertical layout detected (Month Y < Day Y: ${monthBeforeDay})`, 'log', 'cyan') + } if (monthBeforeDay) { - log(false, 'CREATOR', '🔄 Detected MONTH-FIRST layout (new Microsoft UI)', 'log', 'cyan') + log(false, 'CREATOR', '🔄 Detected MONTH-FIRST layout', 'log', 'cyan') } else { - log(false, 'CREATOR', '📅 Detected DAY-FIRST layout (old Microsoft UI)', 'log', 'cyan') + log(false, 'CREATOR', '📅 Detected DAY-FIRST layout', 'log', 'cyan') } // === FILL IN CORRECT ORDER === if (monthBeforeDay) { - // NEW ORDER: MONTH → DAY → YEAR + // MONTH → DAY → YEAR const monthResult = await this.fillMonthDropdown(birthdate.month) if (!monthResult) return null const dayResult = await this.fillDayDropdown(birthdate.day) if (!dayResult) return null } else { - // OLD ORDER: DAY → MONTH → YEAR + // DAY → MONTH → YEAR const dayResult = await this.fillDayDropdown(birthdate.day) if (!dayResult) return null @@ -1592,25 +1639,44 @@ export class AccountCreator { private async fillDayDropdown(day: number): Promise { try { // === DAY DROPDOWN === - // UPDATED: Microsoft changed HTML - new Fluent UI classes (___w2njya0, etc.) - const dayButton = this.page.locator('button#BirthDayDropdown, button[name="BirthDay"], button.fui-Dropdown__button[aria-label*="day"]').first() + // STRATEGY: Prioritize stable attributes (ID, name, aria-label) over volatile atomic classes + // Multiple fallbacks for Microsoft's frequent UI changes + const dayButton = this.page.locator('button#BirthDayDropdown, button[name="BirthDay"], button[aria-label*="Birth day"], button[aria-label*="day"][role="combobox"]').first() await dayButton.waitFor({ timeout: 15000, state: 'visible' }) + // Ensure button is in viewport + await dayButton.scrollIntoViewIfNeeded({ timeout: 3000 }).catch(() => { }) + await this.humanDelay(500, 1000) + + // CRITICAL: Wait for page to be fully interactive + await this.page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => { }) + await this.humanDelay(500, 1000) + log(false, 'CREATOR', 'Clicking day dropdown...', 'log') - // CRITICAL: Retry click if it fails + // CRITICAL: Retry click with better diagnostics const dayClickSuccess = await this.retryOperation( async () => { - // CRITICAL FIX: Use normal click (no force) to avoid bot detection - await dayButton.click({ timeout: 5000 }) - await this.humanDelay(1500, 2500) // INCREASED delay + // Verify button state before clicking + const visible = await dayButton.isVisible().catch(() => false) + const enabled = await dayButton.isEnabled().catch(() => false) - // Verify dropdown opened - // UPDATED: Check for Fluent UI dropdown container + if (!visible || !enabled) { + throw new Error(`Day button not ready (visible: ${visible}, enabled: ${enabled})`) + } + + // Use Fluent UI compatible click method + await this.fluentUIClick(dayButton, 'DAY_CLICK') + await this.humanDelay(1500, 2500) + + // Verify dropdown opened with multiple checks const dayOptionsContainer = this.page.locator('div[role="listbox"], ul[role="listbox"], div.fui-Listbox').first() const isOpen = await dayOptionsContainer.isVisible().catch(() => false) - if (!isOpen) { + const ariaExpanded = await dayButton.getAttribute('aria-expanded').catch(() => 'false') + const buttonExpanded = ariaExpanded === 'true' + + if (!isOpen && !buttonExpanded) { throw new Error('Day dropdown did not open') } @@ -1618,7 +1684,8 @@ export class AccountCreator { }, 'DAY_DROPDOWN_OPEN', 3, - 1000 + 1000, + false // Disable micro-gestures to avoid scroll interfering with dropdown ) if (!dayClickSuccess) { @@ -1633,16 +1700,18 @@ export class AccountCreator { // UPDATED: Fluent UI uses div[role="option"] with exact text matching const dayOption = this.page.locator(`div[role="option"]:text-is("${day}"), div[role="option"]:has-text("${day}"), li[role="option"]:has-text("${day}")`).first() await dayOption.waitFor({ timeout: 5000, state: 'visible' }) - await dayOption.click() + + // Try fluentUIClick for option selection (may fallback to direct click) + await this.fluentUIClick(dayOption, 'DAY_OPTION') await this.humanDelay(1500, 2500) // INCREASED delay // CRITICAL: Wait for dropdown to FULLY close await this.waitForDropdownClosed('DAY_DROPDOWN', 8000) - await this.humanDelay(3500, 5500) // IMPROVED: Longer delay (humans take time between dropdowns) + await this.humanDelay(2000, 3000) // Human-like pause between interactions // CRITICAL: Verify page is interactive (not animating) await this.waitForPageStable('AFTER_DAY_DROPDOWN', 5000) - await this.humanDelay(1500, 2500) // Additional reading pause + await this.humanDelay(800, 1500) // Brief reading pause return true } catch (error) { @@ -1658,47 +1727,104 @@ export class AccountCreator { private async fillMonthDropdown(month: number): Promise { try { // === MONTH DROPDOWN === - // UPDATED: Microsoft changed HTML - new Fluent UI classes - const monthButton = this.page.locator('button#BirthMonthDropdown, button[name="BirthMonth"], button.fui-Dropdown__button[aria-label*="month"]').first() + // STRATEGY: Prioritize stable attributes (ID, name, aria-label) over volatile atomic classes + // Multiple fallbacks for Microsoft's frequent UI changes + const monthButton = this.page.locator('button#BirthMonthDropdown, button[name="BirthMonth"], button[aria-label*="Birth month"], button[aria-label*="month"][role="combobox"]').first() await monthButton.waitFor({ timeout: 10000, state: 'visible' }) + // CRITICAL: Ensure button is visible and in viewport + await monthButton.scrollIntoViewIfNeeded({ timeout: 3000 }).catch(() => { }) + await this.humanDelay(500, 1000) + + // CRITICAL: Wait for any animations or JavaScript to complete + await this.page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => { }) + await this.humanDelay(800, 1500) + // CRITICAL: Verify button is actually clickable (not disabled, not covered) const monthEnabled = await monthButton.isEnabled().catch(() => false) if (!monthEnabled) { - log(false, 'CREATOR', 'Month button not enabled yet, waiting...', 'warn', 'yellow') - await this.humanDelay(3000, 5000) + log(false, 'CREATOR', '⚠️ Month button disabled, waiting for page to update...', 'warn', 'yellow') + await this.humanDelay(2000, 3000) + + // Re-check after waiting + const retryEnabled = await monthButton.isEnabled().catch(() => false) + if (!retryEnabled) { + log(false, 'CREATOR', '❌ Month button still disabled after wait', 'error') + return false + } } log(false, 'CREATOR', 'Clicking month dropdown...', 'log') - // CRITICAL: Retry click with RANDOMIZED delays (avoid pattern detection) + // CRITICAL: Retry click with better diagnostics const monthClickSuccess = await this.retryOperation( async () => { - // IMPROVED: Random micro-gesture before click (human-like) - await this.human.microGestures('MONTH_DROPDOWN_PRE_CLICK') - await this.humanDelay(500, 1200) + // Verify button is still visible and enabled before each attempt + const visible = await monthButton.isVisible().catch(() => false) + const enabled = await monthButton.isEnabled().catch(() => false) - // CRITICAL FIX: Use normal click (no force) to avoid bot detection - await monthButton.click({ timeout: 5000 }) + if (!visible) { + log(false, 'CREATOR', '[MONTH_CLICK] ❌ Button not visible', 'warn', 'yellow') + throw new Error('Month button not visible') + } + if (!enabled) { + log(false, 'CREATOR', '[MONTH_CLICK] ❌ Button not enabled', 'warn', 'yellow') + throw new Error('Month button not enabled') + } - // IMPROVED: Variable delay after click (avoid predictability) - const postClickDelay: [number, number] = Math.random() < 0.3 ? [2500, 4000] : [1500, 2500] - await this.humanDelay(postClickDelay[0], postClickDelay[1]) + // Get button position and log it + const box = await monthButton.boundingBox() + if (!box) { + log(false, 'CREATOR', '[MONTH_CLICK] ❌ Cannot get button position', 'warn', 'yellow') + throw new Error('Cannot get month button position') + } - // Verify dropdown opened - // UPDATED: Fluent UI listbox detection + log(false, 'CREATOR', `[MONTH_CLICK] Button at (${Math.round(box.x)}, ${Math.round(box.y)})`, 'log', 'cyan') + + // DIAGNOSTIC: Check if element is truly interactive + const computedStyle = await monthButton.evaluate((el) => { + const style = window.getComputedStyle(el) + return { + pointerEvents: style.pointerEvents, + opacity: style.opacity, + display: style.display, + visibility: style.visibility + } + }).catch(() => null) + + if (computedStyle) { + log(false, 'CREATOR', `[MONTH_CLICK] Style: ${JSON.stringify(computedStyle)}`, 'log', 'cyan') + } + + // Use Fluent UI compatible click method + await this.fluentUIClick(monthButton, 'MONTH_CLICK') + await this.humanDelay(1500, 2500) + + // Verify dropdown opened with multiple detection strategies const monthOptionsContainer = this.page.locator('div[role="listbox"], ul[role="listbox"], div.fui-Listbox').first() const isOpen = await monthOptionsContainer.isVisible().catch(() => false) - if (!isOpen) { + // Also check if button aria-expanded changed to true + const ariaExpanded = await monthButton.getAttribute('aria-expanded').catch(() => 'false') + const buttonExpanded = ariaExpanded === 'true' + + if (!isOpen && !buttonExpanded) { + log(false, 'CREATOR', '[MONTH_CLICK] ❌ Dropdown did not open (listbox not visible, aria-expanded=false)', 'warn', 'yellow') throw new Error('Month dropdown did not open') } + if (isOpen) { + log(false, 'CREATOR', '[MONTH_CLICK] ✓ Dropdown opened (listbox visible)', 'log', 'green') + } else if (buttonExpanded) { + log(false, 'CREATOR', '[MONTH_CLICK] ✓ Dropdown opened (aria-expanded=true)', 'log', 'green') + } + return true }, 'MONTH_DROPDOWN_OPEN', 3, - 1000 + 1200, + false // CRITICAL: Disable micro-gestures (scroll would lose button position) ) if (!monthClickSuccess) { @@ -1716,12 +1842,12 @@ export class AccountCreator { // Fallback: if data-value doesn't work, try by index const monthVisible = await monthOption.isVisible().catch(() => false) if (monthVisible) { - await monthOption.click() + await this.fluentUIClick(monthOption, 'MONTH_OPTION') log(false, 'CREATOR', '✅ Month selected', 'log', 'green') } else { log(false, 'CREATOR', `Fallback: selecting month by nth-child(${month})`, 'warn', 'yellow') const monthOptionByIndex = this.page.locator(`div[role="option"]:nth-child(${month}), li[role="option"]:nth-child(${month})`).first() - await monthOptionByIndex.click() + await this.fluentUIClick(monthOptionByIndex, 'MONTH_OPTION_INDEX') } await this.humanDelay(1500, 2500) // INCREASED delay @@ -1729,6 +1855,10 @@ export class AccountCreator { await this.waitForDropdownClosed('MONTH_DROPDOWN', 8000) await this.humanDelay(2000, 3000) // INCREASED safety delay + // CRITICAL: Verify page is interactive (not animating) - CONSISTENCY with day dropdown + await this.waitForPageStable('AFTER_MONTH_DROPDOWN', 5000) + await this.humanDelay(1500, 2500) // Additional reading pause + return true } catch (error) { const msg = error instanceof Error ? error.message : String(error) @@ -1743,8 +1873,9 @@ export class AccountCreator { private async fillYearInput(year: number): Promise { try { // === YEAR INPUT === - // UPDATED: Fluent UI year input (class fui-Input__input) - const yearInput = this.page.locator('input[name="BirthYear"], input[type="number"], input.fui-Input__input[aria-label*="year"]').first() + // STRATEGY: Prioritize stable attributes (name, type, aria-label) over volatile atomic classes + // Multiple fallbacks for Microsoft's frequent UI changes + const yearInput = this.page.locator('input[name="BirthYear"], input[type="number"][aria-label*="Birth year"], input[aria-label*="year"][inputmode="numeric"]').first() await yearInput.waitFor({ timeout: 10000, state: 'visible' }) log(false, 'CREATOR', `Filling year: ${year}`, 'log') @@ -1757,7 +1888,7 @@ export class AccountCreator { // Verify value was filled correctly const verified = await this.verifyInputValue( - 'input[name="BirthYear"], input[type="number"], input.fui-Input__input[aria-label*="year"]', + 'input[name="BirthYear"], input[type="number"][aria-label*="Birth year"], input[aria-label*="year"][inputmode="numeric"]', year.toString() )