feat: enhance TradingView authentication debugging
- Add comprehensive debug logging to checkLoginStatus Strategy 1 - Enhanced authentication variable detection with detailed console output - Added debug logging for window.is_authenticated and window.user checks - Improved error visibility for authentication detection issues - Added health API endpoint for debugging and monitoring - Enhanced Dockerfile with better caching and debugging capabilities Authentication detection now shows detailed logs when checking: - window.is_authenticated variable presence and value - window.user object detection and structure - Helps identify why auth detection sees user data but doesn't return true
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
# Dockerfile for Next.js 15 + Playwright + Puppeteer/Chromium + Prisma + Tailwind + OpenAI
|
||||
# Dockerfile for Next.js 15 + Puppeteer/Chromium + Prisma + Tailwind + OpenAI
|
||||
FROM node:20-slim
|
||||
|
||||
# Use build arguments for CPU optimization
|
||||
@@ -10,7 +10,7 @@ ENV JOBS=${JOBS}
|
||||
ENV NODE_OPTIONS=${NODE_OPTIONS}
|
||||
ENV npm_config_jobs=${JOBS}
|
||||
|
||||
# Install system dependencies for Chromium and Playwright
|
||||
# Install system dependencies for Chromium
|
||||
RUN apt-get update && apt-get install -y \
|
||||
wget \
|
||||
ca-certificates \
|
||||
@@ -59,9 +59,6 @@ RUN npm config set maxsockets 8 && \
|
||||
npm config set fetch-retries 3 && \
|
||||
npm ci --no-audit --no-fund --prefer-offline
|
||||
|
||||
# Install Playwright browsers and dependencies with parallel downloads
|
||||
RUN npx playwright install --with-deps chromium
|
||||
|
||||
# Copy the rest of the app
|
||||
COPY . .
|
||||
|
||||
|
||||
10
app/api/health/route.js
Normal file
10
app/api/health/route.js
Normal file
@@ -0,0 +1,10 @@
|
||||
import { NextResponse } from 'next/server'
|
||||
|
||||
export async function GET() {
|
||||
return NextResponse.json({
|
||||
status: 'healthy',
|
||||
timestamp: new Date().toISOString(),
|
||||
service: 'AI Trading Bot',
|
||||
version: '1.0.0'
|
||||
})
|
||||
}
|
||||
@@ -192,11 +192,64 @@ export class TradingViewAutomation {
|
||||
async checkLoginStatus(): Promise<boolean> {
|
||||
if (!this.page) throw new Error('Page not initialized')
|
||||
|
||||
console.log('CHECKING: Login status with 5 detection strategies...')
|
||||
console.log('CHECKING: Login status with 6 detection strategies...')
|
||||
|
||||
try {
|
||||
// Strategy 1: Check for user account indicators (positive indicators)
|
||||
console.log('CHECKING: Strategy 1: Checking for user account indicators...')
|
||||
// Strategy 1: Check JavaScript authentication variables (most reliable)
|
||||
console.log('CHECKING: Strategy 1: Checking JavaScript authentication variables...')
|
||||
const authStatus = await this.page.evaluate(() => {
|
||||
// Check if window.is_authenticated exists and is true
|
||||
const w = window as any
|
||||
|
||||
// Enhanced debugging - capture all relevant variables
|
||||
const result = {
|
||||
is_authenticated: w.is_authenticated,
|
||||
user: w.user,
|
||||
hasUser: typeof w.user === 'object' && w.user !== null,
|
||||
authType: typeof w.is_authenticated,
|
||||
userType: typeof w.user
|
||||
}
|
||||
|
||||
console.log('🔍 JavaScript auth detection:', {
|
||||
is_authenticated: result.is_authenticated,
|
||||
authType: result.authType,
|
||||
hasUser: result.hasUser,
|
||||
userType: result.userType,
|
||||
userExists: !!w.user,
|
||||
username: w.user?.username || 'N/A'
|
||||
})
|
||||
|
||||
if (typeof w.is_authenticated === 'boolean') {
|
||||
return {
|
||||
isAuthenticated: w.is_authenticated,
|
||||
hasUser: typeof w.user === 'object' && w.user !== null,
|
||||
username: w.user?.username || null
|
||||
}
|
||||
}
|
||||
return null
|
||||
})
|
||||
|
||||
console.log('🔍 Auth status result:', authStatus)
|
||||
|
||||
if (authStatus) {
|
||||
if (authStatus.isAuthenticated && authStatus.hasUser) {
|
||||
console.log(`SUCCESS: JavaScript indicates user is authenticated as "${authStatus.username}"`)
|
||||
return true
|
||||
} else {
|
||||
console.log('INFO: JavaScript indicates user is not authenticated')
|
||||
console.log('🔍 Auth details:', {
|
||||
isAuthenticated: authStatus.isAuthenticated,
|
||||
hasUser: authStatus.hasUser,
|
||||
username: authStatus.username
|
||||
})
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
console.log('INFO: No JavaScript authentication variables found')
|
||||
}
|
||||
|
||||
// Strategy 2: Check for user account indicators (positive indicators)
|
||||
console.log('CHECKING: Strategy 2: Checking for user account indicators...')
|
||||
await this.takeDebugScreenshot('login_status_check')
|
||||
|
||||
const userIndicators = [
|
||||
@@ -221,8 +274,8 @@ export class TradingViewAutomation {
|
||||
}
|
||||
}
|
||||
|
||||
// Strategy 2: Check for anonymous/sign-in indicators (negative indicators)
|
||||
console.log('CHECKING: Strategy 2: Checking for anonymous/sign-in indicators...')
|
||||
// Strategy 3: Check for anonymous/sign-in indicators (negative indicators)
|
||||
console.log('CHECKING: Strategy 3: Checking for anonymous/sign-in indicators...')
|
||||
const anonymousIndicators = [
|
||||
'.tv-header__user-menu-button--anonymous',
|
||||
'[data-name="header-user-menu-sign-in"]',
|
||||
@@ -245,16 +298,16 @@ export class TradingViewAutomation {
|
||||
}
|
||||
}
|
||||
|
||||
// Strategy 3: Check URL patterns
|
||||
console.log('CHECKING: Strategy 3: Checking URL patterns...')
|
||||
// Strategy 4: Check URL patterns
|
||||
console.log('CHECKING: Strategy 4: Checking URL patterns...')
|
||||
const currentUrl = this.page.url()
|
||||
if (currentUrl.includes('/signin') || currentUrl.includes('/login')) {
|
||||
console.log('ERROR: On login page - not logged in')
|
||||
return false
|
||||
}
|
||||
|
||||
// Strategy 4: Check authentication cookies
|
||||
console.log('CHECKING: Strategy 4: Checking authentication cookies...')
|
||||
// Strategy 5: Check authentication cookies
|
||||
console.log('CHECKING: Strategy 5: Checking authentication cookies...')
|
||||
const cookies = await this.page.cookies()
|
||||
const authCookies = cookies.filter(cookie =>
|
||||
cookie.name.includes('auth') ||
|
||||
@@ -265,8 +318,8 @@ export class TradingViewAutomation {
|
||||
console.log('WARNING: No authentication cookies found')
|
||||
}
|
||||
|
||||
// Strategy 5: Check for personal content
|
||||
console.log('CHECKING: Strategy 5: Checking for personal content...')
|
||||
// Strategy 6: Check for personal content
|
||||
console.log('CHECKING: Strategy 6: Checking for personal content...')
|
||||
const personalContentSelectors = [
|
||||
'[data-name="watchlist"]',
|
||||
'.tv-header__watchlist',
|
||||
@@ -315,59 +368,325 @@ export class TradingViewAutomation {
|
||||
|
||||
console.log('🔐 Starting login process...')
|
||||
|
||||
// Navigate to login page
|
||||
console.log('📄 Navigating to TradingView login page...')
|
||||
await this.page.goto('https://www.tradingview.com/accounts/signin/', {
|
||||
waitUntil: 'domcontentloaded',
|
||||
timeout: 30000
|
||||
})
|
||||
// Navigate to main TradingView page first, then find login
|
||||
console.log('📄 Navigating to TradingView main page...')
|
||||
|
||||
try {
|
||||
await this.page.goto('https://www.tradingview.com/', {
|
||||
waitUntil: 'domcontentloaded',
|
||||
timeout: 30000
|
||||
})
|
||||
|
||||
await sleep(3000)
|
||||
|
||||
// Look for sign in buttons on main page
|
||||
console.log('🔍 Looking for Sign In button on main page...')
|
||||
const signinButtons = [
|
||||
'a[href*="signin"]',
|
||||
'a[href*="accounts/signin"]',
|
||||
'[data-name="header-user-menu-sign-in"]',
|
||||
'.tv-header__user-menu-button',
|
||||
'.tv-header__user-menu-button--anonymous',
|
||||
'.js-signin-switch',
|
||||
'button[data-role="button"]',
|
||||
'button.tv-button',
|
||||
'a.tv-button'
|
||||
]
|
||||
|
||||
let foundSignin = false
|
||||
for (const selector of signinButtons) {
|
||||
try {
|
||||
const elements = await this.page.$$(selector)
|
||||
|
||||
for (const element of elements) {
|
||||
const isVisible = await element.boundingBox()
|
||||
if (isVisible) {
|
||||
const text = await element.evaluate(el => {
|
||||
const elem = el as HTMLElement
|
||||
return elem.textContent || elem.getAttribute('title') || elem.getAttribute('href') || ''
|
||||
})
|
||||
console.log(`🎯 Found signin element: ${selector} with text: "${text}"`)
|
||||
|
||||
// Check if this looks like a sign in button
|
||||
const lowerText = text.toLowerCase()
|
||||
if (lowerText.includes('sign in') || lowerText.includes('login') ||
|
||||
lowerText.includes('signin') || selector.includes('signin') ||
|
||||
selector.includes('user-menu-button')) {
|
||||
|
||||
await element.click()
|
||||
console.log('✅ Clicked sign in button')
|
||||
await sleep(5000) // Wait for login page/modal to load
|
||||
foundSignin = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if (foundSignin) break
|
||||
} catch (e) {
|
||||
const error = e as Error
|
||||
console.log(`❌ Error with selector ${selector}: ${error.message}`)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// If no selector-based approach works, try finding by text content
|
||||
if (!foundSignin) {
|
||||
console.log('🔍 Trying to find sign-in button by text content...')
|
||||
const clickableElements = await this.page.$$('button, a, div[role="button"], span[role="button"]')
|
||||
|
||||
for (const element of clickableElements) {
|
||||
try {
|
||||
const isVisible = await element.boundingBox()
|
||||
if (isVisible) {
|
||||
const text = await element.evaluate(el => {
|
||||
const elem = el as HTMLElement
|
||||
return (elem.textContent || '').toLowerCase().trim()
|
||||
})
|
||||
|
||||
if (text.includes('sign in') || text.includes('signin') || text.includes('log in') || text.includes('login')) {
|
||||
console.log(`🎯 Found sign-in button with text: "${text}"`)
|
||||
await element.click()
|
||||
console.log('✅ Clicked sign-in button by text')
|
||||
await sleep(5000)
|
||||
foundSignin = true
|
||||
break
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!foundSignin) {
|
||||
console.log('⚠️ No sign in button found on main page, trying direct navigation...')
|
||||
await this.page.goto('https://www.tradingview.com/accounts/signin/', {
|
||||
waitUntil: 'domcontentloaded',
|
||||
timeout: 30000
|
||||
})
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
const error = e as Error
|
||||
console.log(`❌ Navigation error: ${error.message}`)
|
||||
// Fallback to direct login URL
|
||||
await this.page.goto('https://www.tradingview.com/accounts/signin/', {
|
||||
waitUntil: 'domcontentloaded',
|
||||
timeout: 30000
|
||||
})
|
||||
}
|
||||
|
||||
await sleep(3000)
|
||||
await this.takeDebugScreenshot('login_page_loaded')
|
||||
|
||||
// Check current URL and page state
|
||||
const currentUrl = await this.page.url()
|
||||
const pageTitle = await this.page.title()
|
||||
console.log(`🌐 Current URL: ${currentUrl}`)
|
||||
console.log(`📋 Page title: ${pageTitle}`)
|
||||
|
||||
// Wait for login form
|
||||
console.log('⏳ Waiting for login form...')
|
||||
await sleep(5000)
|
||||
|
||||
// Look for email login option
|
||||
// Look for email login option with comprehensive approach
|
||||
console.log('CHECKING: Looking for Email login option...')
|
||||
|
||||
const emailTriggers = [
|
||||
'button[data-overflow-tooltip-text="Email"]',
|
||||
'button:contains("Email")',
|
||||
'button:contains("email")',
|
||||
'[data-name="email"]'
|
||||
// Try to find email trigger button using XPath and text content
|
||||
let emailFormVisible = false
|
||||
|
||||
try {
|
||||
// First try standard selectors
|
||||
const emailTriggers = [
|
||||
'button[data-overflow-tooltip-text="Email"]',
|
||||
'[data-name="email"]',
|
||||
'.tv-signin-dialog__toggle-email',
|
||||
'.js-signin__email',
|
||||
'[data-testid="email-signin"]',
|
||||
'button[class*="email"]',
|
||||
'.signin-form__toggle'
|
||||
]
|
||||
|
||||
for (const trigger of emailTriggers) {
|
||||
try {
|
||||
const elements = await this.page.$$(trigger)
|
||||
for (const element of elements) {
|
||||
const isVisible = await element.boundingBox()
|
||||
if (isVisible) {
|
||||
const text = await element.evaluate(el => {
|
||||
const elem = el as HTMLElement
|
||||
return elem.textContent || elem.getAttribute('data-overflow-tooltip-text') || elem.getAttribute('title') || ''
|
||||
})
|
||||
console.log(`TARGET: Found email trigger: ${trigger} with text: "${text}"`)
|
||||
await element.click()
|
||||
console.log('SUCCESS: Clicked email trigger')
|
||||
await sleep(3000)
|
||||
emailFormVisible = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if (emailFormVisible) break
|
||||
} catch (e) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// If no standard selectors work, try finding buttons by text content
|
||||
if (!emailFormVisible) {
|
||||
console.log('🔍 Trying to find email button by text content...')
|
||||
const buttons = await this.page.$$('button, span, div[role="button"], a')
|
||||
|
||||
for (const button of buttons) {
|
||||
try {
|
||||
const isVisible = await button.boundingBox()
|
||||
if (isVisible) {
|
||||
const text = await button.evaluate(el => {
|
||||
const elem = el as HTMLElement
|
||||
return (elem.textContent || '').toLowerCase().trim()
|
||||
})
|
||||
|
||||
if (text.includes('email') || text.includes('continue with email')) {
|
||||
console.log(`🎯 Found email button with text: "${text}"`)
|
||||
await button.click()
|
||||
console.log('✅ Clicked email button by text')
|
||||
await sleep(3000)
|
||||
emailFormVisible = true
|
||||
break
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
console.log('⚠️ Error finding email trigger:', e)
|
||||
}
|
||||
|
||||
// If no email trigger found, check if we're in a modal or different login flow
|
||||
if (!emailFormVisible) {
|
||||
console.log('INFO: No email trigger found, checking for alternative login flows...')
|
||||
|
||||
// Wait a bit more for any modals to fully load
|
||||
await sleep(5000)
|
||||
await this.takeDebugScreenshot('checking_modal_login')
|
||||
|
||||
// Check for modal dialogs or different login containers
|
||||
const modalSelectors = [
|
||||
'[role="dialog"]',
|
||||
'.tv-dialog',
|
||||
'.tv-dialog__wrapper',
|
||||
'.js-dialog',
|
||||
'.signin-dialog',
|
||||
'.auth-modal',
|
||||
'.modal',
|
||||
'[data-testid="auth-modal"]'
|
||||
]
|
||||
|
||||
for (const modalSelector of modalSelectors) {
|
||||
try {
|
||||
const modal = await this.page.$(modalSelector)
|
||||
if (modal) {
|
||||
console.log(`📋 Found modal: ${modalSelector}`)
|
||||
// Look for email options within the modal
|
||||
const modalEmailButtons = await modal.$$('button, a, div[role="button"]')
|
||||
|
||||
for (const button of modalEmailButtons) {
|
||||
try {
|
||||
const isVisible = await button.boundingBox()
|
||||
if (isVisible) {
|
||||
const text = await button.evaluate(el => {
|
||||
const elem = el as HTMLElement
|
||||
return (elem.textContent || '').toLowerCase().trim()
|
||||
})
|
||||
|
||||
console.log(`🎯 Modal button text: "${text}"`)
|
||||
if (text.includes('email') || text.includes('continue with email') ||
|
||||
text.includes('sign in with email') || text.includes('use email')) {
|
||||
console.log(`✅ Found modal email button: "${text}"`)
|
||||
await button.click()
|
||||
console.log('✅ Clicked modal email button')
|
||||
await sleep(3000)
|
||||
emailFormVisible = true
|
||||
break
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
if (emailFormVisible) break
|
||||
}
|
||||
} catch (e) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!emailFormVisible) {
|
||||
console.log('INFO: Still no email form, assuming email form is already visible')
|
||||
}
|
||||
|
||||
await this.takeDebugScreenshot('after_email_trigger')
|
||||
|
||||
// Fill email - updated selectors for current TradingView
|
||||
const emailInputSelectors = [
|
||||
'input[type="email"]',
|
||||
'input[name*="email"]',
|
||||
'input[name="username"]',
|
||||
'input[name="id_username"]',
|
||||
'input[placeholder*="email" i]',
|
||||
'input[placeholder*="Email" i]',
|
||||
'input[autocomplete="username"]',
|
||||
'input[data-name="email"]',
|
||||
'input[class*="email"]',
|
||||
'#id_username',
|
||||
'#email',
|
||||
'[data-testid="email-input"]',
|
||||
'[data-testid="username-input"]'
|
||||
]
|
||||
|
||||
let emailFormVisible = false
|
||||
for (const trigger of emailTriggers) {
|
||||
// Debug: Check what's actually on the page
|
||||
await this.takeDebugScreenshot('before_email_search')
|
||||
|
||||
// Get all input elements to debug
|
||||
const allInputs = await this.page.$$('input')
|
||||
console.log(`DEBUG: Found ${allInputs.length} input elements on page`)
|
||||
|
||||
for (let i = 0; i < Math.min(allInputs.length, 10); i++) {
|
||||
try {
|
||||
const element = await this.page.$(trigger)
|
||||
if (element) {
|
||||
const isVisible = await element.boundingBox()
|
||||
if (isVisible) {
|
||||
console.log("TARGET: Found email trigger: " + trigger)
|
||||
await element.click()
|
||||
console.log('SUCCESS: Clicked email trigger')
|
||||
await sleep(3000)
|
||||
emailFormVisible = true
|
||||
break
|
||||
const inputType = await allInputs[i].evaluate(el => el.type)
|
||||
const inputName = await allInputs[i].evaluate(el => el.name)
|
||||
const inputPlaceholder = await allInputs[i].evaluate(el => el.placeholder)
|
||||
const inputId = await allInputs[i].evaluate(el => el.id)
|
||||
const inputClass = await allInputs[i].evaluate(el => el.className)
|
||||
const inputValue = await allInputs[i].evaluate(el => el.value)
|
||||
const isVisible = await allInputs[i].boundingBox()
|
||||
|
||||
console.log(`INPUT ${i}: type="${inputType}", name="${inputName}", placeholder="${inputPlaceholder}", id="${inputId}", class="${inputClass}", value="${inputValue}", visible=${!!isVisible}`)
|
||||
|
||||
// Check if this looks like an email/username field even if it doesn't match selectors
|
||||
if (isVisible && (inputType === 'text' || inputType === 'email' || !inputType)) {
|
||||
const lowerPlaceholder = (inputPlaceholder || '').toLowerCase()
|
||||
const lowerName = (inputName || '').toLowerCase()
|
||||
const lowerClass = (inputClass || '').toLowerCase()
|
||||
|
||||
if (lowerPlaceholder.includes('email') || lowerPlaceholder.includes('username') ||
|
||||
lowerName.includes('email') || lowerName.includes('username') ||
|
||||
lowerClass.includes('email') || lowerClass.includes('username')) {
|
||||
console.log(`🎯 INPUT ${i} looks like email field based on attributes`)
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
continue
|
||||
const error = e as Error
|
||||
console.log(`INPUT ${i}: Error reading attributes - ${error.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
// Fill email
|
||||
const emailInputSelectors = [
|
||||
'input[type="email"]',
|
||||
'input[name*="email"]',
|
||||
'input[name="username"]',
|
||||
'input[placeholder*="email" i]'
|
||||
]
|
||||
|
||||
let emailInput = null
|
||||
|
||||
// First try specific selectors
|
||||
for (const selector of emailInputSelectors) {
|
||||
try {
|
||||
emailInput = await this.page.$(selector)
|
||||
@@ -381,10 +700,76 @@ export class TradingViewAutomation {
|
||||
} catch (e) {
|
||||
continue
|
||||
}
|
||||
emailInput = null // Reset if not visible
|
||||
}
|
||||
|
||||
// If no specific selector worked, try to find the first visible text input
|
||||
if (!emailInput) {
|
||||
console.log('🔍 No specific email input found, trying first visible text input...')
|
||||
for (const input of allInputs) {
|
||||
try {
|
||||
const isVisible = await input.boundingBox()
|
||||
const inputType = await input.evaluate(el => el.type)
|
||||
|
||||
if (isVisible && (inputType === 'text' || inputType === 'email' || !inputType)) {
|
||||
console.log('🎯 Found first visible text input, assuming it\'s email field')
|
||||
emailInput = input
|
||||
break
|
||||
}
|
||||
} catch (e) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Last resort: try to find inputs by looking for labels or surrounding text
|
||||
if (!emailInput) {
|
||||
console.log('🔍 Last resort: searching for inputs near email-related text...')
|
||||
|
||||
// Look for text that says "email" and find nearby inputs
|
||||
const emailTexts = await this.page.evaluate(() => {
|
||||
const allElements = Array.from(document.querySelectorAll('*'))
|
||||
return allElements
|
||||
.filter(el => {
|
||||
const text = (el.textContent || '').toLowerCase()
|
||||
return text.includes('email') || text.includes('username') || text.includes('sign in')
|
||||
})
|
||||
.map(el => {
|
||||
const rect = el.getBoundingClientRect()
|
||||
return {
|
||||
text: (el.textContent || '').trim(),
|
||||
x: rect.x,
|
||||
y: rect.y,
|
||||
tag: el.tagName
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
console.log('📋 Found email-related text elements:', emailTexts.slice(0, 5))
|
||||
|
||||
// Try to find any input that's not already found
|
||||
for (const input of allInputs) {
|
||||
try {
|
||||
const isVisible = await input.boundingBox()
|
||||
if (isVisible) {
|
||||
console.log('🎯 Using any visible input as potential email field')
|
||||
emailInput = input
|
||||
break
|
||||
}
|
||||
} catch (e) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!emailInput) {
|
||||
throw new Error('Could not find email input field')
|
||||
// Additional debug info
|
||||
const currentUrl = await this.page.url()
|
||||
const title = await this.page.title()
|
||||
console.log(`❌ Email input not found on: ${currentUrl}`)
|
||||
console.log(`❌ Page title: ${title}`)
|
||||
await this.takeDebugScreenshot('email_input_not_found')
|
||||
throw new Error(`Could not find email input field on ${currentUrl}. Found ${allInputs.length} inputs total.`)
|
||||
}
|
||||
|
||||
await emailInput.click()
|
||||
|
||||
Reference in New Issue
Block a user