mirror of
https://github.com/LightZirconite/Microsoft-Rewards-Bot.git
synced 2026-01-10 01:06:17 +00:00
fix: restore express-rate-limit dependency and enhance Fluent UI interaction methods for account creation
This commit is contained in:
@@ -71,6 +71,7 @@
|
|||||||
"chalk": "^4.1.2",
|
"chalk": "^4.1.2",
|
||||||
"cheerio": "^1.0.0",
|
"cheerio": "^1.0.0",
|
||||||
"express": "^4.21.2",
|
"express": "^4.21.2",
|
||||||
|
"express-rate-limit": "^8.2.1",
|
||||||
"fingerprint-generator": "^2.1.66",
|
"fingerprint-generator": "^2.1.66",
|
||||||
"fingerprint-injector": "^2.1.66",
|
"fingerprint-injector": "^2.1.66",
|
||||||
"http-proxy-agent": "^7.0.2",
|
"http-proxy-agent": "^7.0.2",
|
||||||
@@ -82,7 +83,6 @@
|
|||||||
"rebrowser-playwright": "1.52.0",
|
"rebrowser-playwright": "1.52.0",
|
||||||
"socks-proxy-agent": "^8.0.5",
|
"socks-proxy-agent": "^8.0.5",
|
||||||
"ts-node": "^10.9.2",
|
"ts-node": "^10.9.2",
|
||||||
"ws": "^8.18.3",
|
"ws": "^8.18.3"
|
||||||
"express-rate-limit": "^8.2.1"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -84,7 +84,8 @@ export class AccountCreator {
|
|||||||
operation: () => Promise<T>,
|
operation: () => Promise<T>,
|
||||||
context: string,
|
context: string,
|
||||||
maxRetries: number = 3,
|
maxRetries: number = 3,
|
||||||
initialDelayMs: number = 1000
|
initialDelayMs: number = 1000,
|
||||||
|
enableMicroGestures: boolean = true
|
||||||
): Promise<T | null> {
|
): Promise<T | null> {
|
||||||
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
||||||
try {
|
try {
|
||||||
@@ -102,7 +103,8 @@ export class AccountCreator {
|
|||||||
await this.page.waitForTimeout(Math.floor(humanDelay))
|
await this.page.waitForTimeout(Math.floor(humanDelay))
|
||||||
|
|
||||||
// IMPROVED: Random micro-gesture during retry (human frustration pattern)
|
// 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}`)
|
await this.human.microGestures(`${context}_RETRY_${attempt}`)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -114,6 +116,30 @@ export class AccountCreator {
|
|||||||
return null
|
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<void>
|
||||||
|
*/
|
||||||
|
private async fluentUIClick(locator: ReturnType<typeof this.page.locator>, context: string): Promise<void> {
|
||||||
|
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
|
* CRITICAL: Wait for dropdown to be fully closed before continuing
|
||||||
* IMPROVED: Detect Fluent UI dropdown states reliably
|
* IMPROVED: Detect Fluent UI dropdown states reliably
|
||||||
@@ -1050,7 +1076,7 @@ export class AccountCreator {
|
|||||||
// CRITICAL: Get current URL before clicking
|
// CRITICAL: Get current URL before clicking
|
||||||
const urlBeforeClick = this.page.url()
|
const urlBeforeClick = this.page.url()
|
||||||
|
|
||||||
await nextBtn.click()
|
await this.fluentUIClick(nextBtn, 'EMAIL_NEXT')
|
||||||
// OPTIMIZED: Reduced delay after clicking Next
|
// OPTIMIZED: Reduced delay after clicking Next
|
||||||
await this.humanDelay(1000, 1500)
|
await this.humanDelay(1000, 1500)
|
||||||
await this.waitForPageStable('AFTER_EMAIL_SUBMIT', 10000)
|
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()
|
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.humanDelay(2000, 3000)
|
||||||
await this.waitForPageStable('RETRY_EMAIL', 15000)
|
await this.waitForPageStable('RETRY_EMAIL', 15000)
|
||||||
|
|
||||||
@@ -1298,8 +1324,8 @@ export class AccountCreator {
|
|||||||
|
|
||||||
log(false, 'CREATOR', `Selecting suggestion: ${cleanEmail}`, 'log', 'cyan')
|
log(false, 'CREATOR', `Selecting suggestion: ${cleanEmail}`, 'log', 'cyan')
|
||||||
|
|
||||||
// Click the suggestion
|
// Click the suggestion using Fluent UI compatible method
|
||||||
await firstButton.click()
|
await this.fluentUIClick(firstButton, 'EMAIL_SUGGESTION')
|
||||||
await this.humanDelay(500, 1000) // REDUCED: Faster suggestion click (was 1500-2500)
|
await this.humanDelay(500, 1000) // REDUCED: Faster suggestion click (was 1500-2500)
|
||||||
|
|
||||||
// Verify the email input was updated
|
// Verify the email input was updated
|
||||||
@@ -1378,7 +1404,7 @@ export class AccountCreator {
|
|||||||
// Get current URL and page state before clicking
|
// Get current URL and page state before clicking
|
||||||
const urlBefore = this.page.url()
|
const urlBefore = this.page.url()
|
||||||
|
|
||||||
await nextBtn.click()
|
await this.fluentUIClick(nextBtn, `NEXT_${step.toUpperCase()}`)
|
||||||
log(false, 'CREATOR', `✅ Clicked Next (${step})`, 'log', 'green')
|
log(false, 'CREATOR', `✅ Clicked Next (${step})`, 'log', 'green')
|
||||||
|
|
||||||
// CRITICAL: Wait for page to process the click
|
// CRITICAL: Wait for page to process the click
|
||||||
@@ -1512,29 +1538,50 @@ export class AccountCreator {
|
|||||||
try {
|
try {
|
||||||
await this.humanDelay(2000, 3000)
|
await this.humanDelay(2000, 3000)
|
||||||
|
|
||||||
// CRITICAL: Microsoft changed order - MONTH must be filled BEFORE DAY
|
// CRITICAL FIX: Microsoft UI has Month and Day side-by-side (same Y position)
|
||||||
// Detect order by checking which dropdown appears first in DOM
|
// Cannot detect order by Y position. Checking DOM order instead.
|
||||||
const monthFirst = await this.page.locator('button#BirthMonthDropdown').first().boundingBox().then(box => box?.y ?? 0).catch(() => 0)
|
// According to HTML structure analysis: Month ALWAYS comes before Day in DOM
|
||||||
const dayFirst = await this.page.locator('button#BirthDayDropdown').first().boundingBox().then(box => box?.y ?? 0).catch(() => 0)
|
|
||||||
|
|
||||||
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) {
|
if (monthBeforeDay) {
|
||||||
log(false, 'CREATOR', '🔄 Detected MONTH-FIRST layout (new Microsoft UI)', 'log', 'cyan')
|
log(false, 'CREATOR', '🔄 Detected MONTH-FIRST layout', 'log', 'cyan')
|
||||||
} else {
|
} 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 ===
|
// === FILL IN CORRECT ORDER ===
|
||||||
if (monthBeforeDay) {
|
if (monthBeforeDay) {
|
||||||
// NEW ORDER: MONTH → DAY → YEAR
|
// MONTH → DAY → YEAR
|
||||||
const monthResult = await this.fillMonthDropdown(birthdate.month)
|
const monthResult = await this.fillMonthDropdown(birthdate.month)
|
||||||
if (!monthResult) return null
|
if (!monthResult) return null
|
||||||
|
|
||||||
const dayResult = await this.fillDayDropdown(birthdate.day)
|
const dayResult = await this.fillDayDropdown(birthdate.day)
|
||||||
if (!dayResult) return null
|
if (!dayResult) return null
|
||||||
} else {
|
} else {
|
||||||
// OLD ORDER: DAY → MONTH → YEAR
|
// DAY → MONTH → YEAR
|
||||||
const dayResult = await this.fillDayDropdown(birthdate.day)
|
const dayResult = await this.fillDayDropdown(birthdate.day)
|
||||||
if (!dayResult) return null
|
if (!dayResult) return null
|
||||||
|
|
||||||
@@ -1592,25 +1639,44 @@ export class AccountCreator {
|
|||||||
private async fillDayDropdown(day: number): Promise<boolean> {
|
private async fillDayDropdown(day: number): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
// === DAY DROPDOWN ===
|
// === DAY DROPDOWN ===
|
||||||
// UPDATED: Microsoft changed HTML - new Fluent UI classes (___w2njya0, etc.)
|
// STRATEGY: Prioritize stable attributes (ID, name, aria-label) over volatile atomic classes
|
||||||
const dayButton = this.page.locator('button#BirthDayDropdown, button[name="BirthDay"], button.fui-Dropdown__button[aria-label*="day"]').first()
|
// 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' })
|
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')
|
log(false, 'CREATOR', 'Clicking day dropdown...', 'log')
|
||||||
|
|
||||||
// CRITICAL: Retry click if it fails
|
// CRITICAL: Retry click with better diagnostics
|
||||||
const dayClickSuccess = await this.retryOperation(
|
const dayClickSuccess = await this.retryOperation(
|
||||||
async () => {
|
async () => {
|
||||||
// CRITICAL FIX: Use normal click (no force) to avoid bot detection
|
// Verify button state before clicking
|
||||||
await dayButton.click({ timeout: 5000 })
|
const visible = await dayButton.isVisible().catch(() => false)
|
||||||
await this.humanDelay(1500, 2500) // INCREASED delay
|
const enabled = await dayButton.isEnabled().catch(() => false)
|
||||||
|
|
||||||
// Verify dropdown opened
|
if (!visible || !enabled) {
|
||||||
// UPDATED: Check for Fluent UI dropdown container
|
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 dayOptionsContainer = this.page.locator('div[role="listbox"], ul[role="listbox"], div.fui-Listbox').first()
|
||||||
const isOpen = await dayOptionsContainer.isVisible().catch(() => false)
|
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')
|
throw new Error('Day dropdown did not open')
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1618,7 +1684,8 @@ export class AccountCreator {
|
|||||||
},
|
},
|
||||||
'DAY_DROPDOWN_OPEN',
|
'DAY_DROPDOWN_OPEN',
|
||||||
3,
|
3,
|
||||||
1000
|
1000,
|
||||||
|
false // Disable micro-gestures to avoid scroll interfering with dropdown
|
||||||
)
|
)
|
||||||
|
|
||||||
if (!dayClickSuccess) {
|
if (!dayClickSuccess) {
|
||||||
@@ -1633,16 +1700,18 @@ export class AccountCreator {
|
|||||||
// UPDATED: Fluent UI uses div[role="option"] with exact text matching
|
// 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()
|
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.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
|
await this.humanDelay(1500, 2500) // INCREASED delay
|
||||||
|
|
||||||
// CRITICAL: Wait for dropdown to FULLY close
|
// CRITICAL: Wait for dropdown to FULLY close
|
||||||
await this.waitForDropdownClosed('DAY_DROPDOWN', 8000)
|
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)
|
// CRITICAL: Verify page is interactive (not animating)
|
||||||
await this.waitForPageStable('AFTER_DAY_DROPDOWN', 5000)
|
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
|
return true
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -1658,47 +1727,104 @@ export class AccountCreator {
|
|||||||
private async fillMonthDropdown(month: number): Promise<boolean> {
|
private async fillMonthDropdown(month: number): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
// === MONTH DROPDOWN ===
|
// === MONTH DROPDOWN ===
|
||||||
// UPDATED: Microsoft changed HTML - new Fluent UI classes
|
// STRATEGY: Prioritize stable attributes (ID, name, aria-label) over volatile atomic classes
|
||||||
const monthButton = this.page.locator('button#BirthMonthDropdown, button[name="BirthMonth"], button.fui-Dropdown__button[aria-label*="month"]').first()
|
// 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' })
|
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)
|
// CRITICAL: Verify button is actually clickable (not disabled, not covered)
|
||||||
const monthEnabled = await monthButton.isEnabled().catch(() => false)
|
const monthEnabled = await monthButton.isEnabled().catch(() => false)
|
||||||
if (!monthEnabled) {
|
if (!monthEnabled) {
|
||||||
log(false, 'CREATOR', 'Month button not enabled yet, waiting...', 'warn', 'yellow')
|
log(false, 'CREATOR', '⚠️ Month button disabled, waiting for page to update...', 'warn', 'yellow')
|
||||||
await this.humanDelay(3000, 5000)
|
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')
|
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(
|
const monthClickSuccess = await this.retryOperation(
|
||||||
async () => {
|
async () => {
|
||||||
// IMPROVED: Random micro-gesture before click (human-like)
|
// Verify button is still visible and enabled before each attempt
|
||||||
await this.human.microGestures('MONTH_DROPDOWN_PRE_CLICK')
|
const visible = await monthButton.isVisible().catch(() => false)
|
||||||
await this.humanDelay(500, 1200)
|
const enabled = await monthButton.isEnabled().catch(() => false)
|
||||||
|
|
||||||
// CRITICAL FIX: Use normal click (no force) to avoid bot detection
|
if (!visible) {
|
||||||
await monthButton.click({ timeout: 5000 })
|
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)
|
// Get button position and log it
|
||||||
const postClickDelay: [number, number] = Math.random() < 0.3 ? [2500, 4000] : [1500, 2500]
|
const box = await monthButton.boundingBox()
|
||||||
await this.humanDelay(postClickDelay[0], postClickDelay[1])
|
if (!box) {
|
||||||
|
log(false, 'CREATOR', '[MONTH_CLICK] ❌ Cannot get button position', 'warn', 'yellow')
|
||||||
|
throw new Error('Cannot get month button position')
|
||||||
|
}
|
||||||
|
|
||||||
// Verify dropdown opened
|
log(false, 'CREATOR', `[MONTH_CLICK] Button at (${Math.round(box.x)}, ${Math.round(box.y)})`, 'log', 'cyan')
|
||||||
// UPDATED: Fluent UI listbox detection
|
|
||||||
|
// 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 monthOptionsContainer = this.page.locator('div[role="listbox"], ul[role="listbox"], div.fui-Listbox').first()
|
||||||
const isOpen = await monthOptionsContainer.isVisible().catch(() => false)
|
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')
|
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
|
return true
|
||||||
},
|
},
|
||||||
'MONTH_DROPDOWN_OPEN',
|
'MONTH_DROPDOWN_OPEN',
|
||||||
3,
|
3,
|
||||||
1000
|
1200,
|
||||||
|
false // CRITICAL: Disable micro-gestures (scroll would lose button position)
|
||||||
)
|
)
|
||||||
|
|
||||||
if (!monthClickSuccess) {
|
if (!monthClickSuccess) {
|
||||||
@@ -1716,12 +1842,12 @@ export class AccountCreator {
|
|||||||
// Fallback: if data-value doesn't work, try by index
|
// Fallback: if data-value doesn't work, try by index
|
||||||
const monthVisible = await monthOption.isVisible().catch(() => false)
|
const monthVisible = await monthOption.isVisible().catch(() => false)
|
||||||
if (monthVisible) {
|
if (monthVisible) {
|
||||||
await monthOption.click()
|
await this.fluentUIClick(monthOption, 'MONTH_OPTION')
|
||||||
log(false, 'CREATOR', '✅ Month selected', 'log', 'green')
|
log(false, 'CREATOR', '✅ Month selected', 'log', 'green')
|
||||||
} else {
|
} else {
|
||||||
log(false, 'CREATOR', `Fallback: selecting month by nth-child(${month})`, 'warn', 'yellow')
|
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()
|
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
|
await this.humanDelay(1500, 2500) // INCREASED delay
|
||||||
|
|
||||||
@@ -1729,6 +1855,10 @@ export class AccountCreator {
|
|||||||
await this.waitForDropdownClosed('MONTH_DROPDOWN', 8000)
|
await this.waitForDropdownClosed('MONTH_DROPDOWN', 8000)
|
||||||
await this.humanDelay(2000, 3000) // INCREASED safety delay
|
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
|
return true
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const msg = error instanceof Error ? error.message : String(error)
|
const msg = error instanceof Error ? error.message : String(error)
|
||||||
@@ -1743,8 +1873,9 @@ export class AccountCreator {
|
|||||||
private async fillYearInput(year: number): Promise<boolean> {
|
private async fillYearInput(year: number): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
// === YEAR INPUT ===
|
// === YEAR INPUT ===
|
||||||
// UPDATED: Fluent UI year input (class fui-Input__input)
|
// STRATEGY: Prioritize stable attributes (name, type, aria-label) over volatile atomic classes
|
||||||
const yearInput = this.page.locator('input[name="BirthYear"], input[type="number"], input.fui-Input__input[aria-label*="year"]').first()
|
// 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' })
|
await yearInput.waitFor({ timeout: 10000, state: 'visible' })
|
||||||
|
|
||||||
log(false, 'CREATOR', `Filling year: ${year}`, 'log')
|
log(false, 'CREATOR', `Filling year: ${year}`, 'log')
|
||||||
@@ -1757,7 +1888,7 @@ export class AccountCreator {
|
|||||||
|
|
||||||
// Verify value was filled correctly
|
// Verify value was filled correctly
|
||||||
const verified = await this.verifyInputValue(
|
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()
|
year.toString()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user