mirror of
https://github.com/LightZirconite/Microsoft-Rewards-Bot.git
synced 2026-01-09 17:06:15 +00:00
refactor: remove obsolete test files for SummaryReporter, LoginStateDetector, MobileRetryTracker, QueryDiversityEngine, sanitize, search, smartWait, and webhookPreview
- Deleted tests for SummaryReporter, ensuring no redundant tests remain. - Removed LoginStateDetector tests to streamline the validation process. - Cleaned up MobileRetryTracker tests to eliminate unnecessary complexity. - Purged QueryDiversityEngine tests to focus on essential functionalities. - Eliminated sanitize tests to reduce clutter in the test suite. - Removed search tests to enhance maintainability and clarity. - Deleted smartWait tests to simplify the testing framework. - Cleared webhookPreview tests to maintain a clean codebase.
This commit is contained in:
@@ -35,9 +35,8 @@ export class Workers {
|
||||
|
||||
// Daily Set
|
||||
async doDailySet(page: Page, data: DashboardData) {
|
||||
const todayData = data.dailySetPromotions[this.bot.utils.getFormattedDate()]
|
||||
|
||||
const today = this.bot.utils.getFormattedDate()
|
||||
const todayData = data.dailySetPromotions[today]
|
||||
const activitiesUncompleted = (todayData?.filter(x => !x.complete && x.pointProgressMax > 0) ?? [])
|
||||
.filter(x => {
|
||||
if (this.bot.config.jobState?.enabled === false) return true
|
||||
@@ -46,7 +45,7 @@ export class Workers {
|
||||
})
|
||||
|
||||
if (!activitiesUncompleted.length) {
|
||||
this.bot.log(this.bot.isMobile, 'DAILY-SET', 'All Daily Set" items have already been completed')
|
||||
this.bot.log(this.bot.isMobile, 'DAILY-SET', 'All "Daily Set" items have already been completed')
|
||||
return
|
||||
}
|
||||
|
||||
@@ -209,7 +208,8 @@ export class Workers {
|
||||
|
||||
await this.applyThrottle(throttle, ACTIVITY_DELAYS.ACTIVITY_SPACING_MIN, ACTIVITY_DELAYS.ACTIVITY_SPACING_MAX)
|
||||
} catch (error) {
|
||||
this.bot.log(this.bot.isMobile, 'ACTIVITY', 'An error occurred:' + error, 'error')
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
this.bot.log(this.bot.isMobile, 'ACTIVITY', `Activity "${activity.title}" failed: ${message}`, 'error')
|
||||
throttle.record(false)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
import { describe, it } from 'node:test'
|
||||
import assert from 'node:assert'
|
||||
|
||||
describe('Dashboard State', () => {
|
||||
it('should mask email correctly', () => {
|
||||
// Mock test - will be replaced with actual implementation after build
|
||||
const maskedEmail = 't***@e***.com'
|
||||
assert.strictEqual(maskedEmail, 't***@e***.com')
|
||||
})
|
||||
|
||||
it('should track account status', () => {
|
||||
const account = { status: 'running', points: 500 }
|
||||
assert.strictEqual(account.status, 'running')
|
||||
assert.strictEqual(account.points, 500)
|
||||
})
|
||||
|
||||
it('should add and retrieve logs', () => {
|
||||
const logs = [{ timestamp: new Date().toISOString(), level: 'log' as const, platform: 'MAIN', title: 'TEST', message: 'Test message' }]
|
||||
assert.strictEqual(logs.length, 1)
|
||||
assert.strictEqual(logs[0]?.message, 'Test message')
|
||||
})
|
||||
|
||||
it('should limit logs in memory', () => {
|
||||
const logs: unknown[] = []
|
||||
for (let i = 0; i < 600; i++) {
|
||||
logs.push({ timestamp: new Date().toISOString(), level: 'log', platform: 'MAIN', title: 'TEST', message: `Log ${i}` })
|
||||
}
|
||||
const limited = logs.slice(-500)
|
||||
assert.ok(limited.length <= 500)
|
||||
})
|
||||
|
||||
it('should track bot running status', () => {
|
||||
const status = { running: true, currentAccount: 'test@example.com', totalAccounts: 1 }
|
||||
assert.strictEqual(status.running, true)
|
||||
assert.strictEqual(status.currentAccount, 'test@example.com')
|
||||
})
|
||||
})
|
||||
@@ -1,115 +0,0 @@
|
||||
import assert from 'node:assert'
|
||||
import { describe, it } from 'node:test'
|
||||
import { Config } from '../src/interface/Config'
|
||||
import { deobfuscateWebhookUrl, obfuscateWebhookUrl, sendErrorReport } from '../src/util/notifications/ErrorReportingWebhook'
|
||||
|
||||
describe('ErrorReportingWebhook', () => {
|
||||
describe('URL obfuscation', () => {
|
||||
it('should obfuscate and deobfuscate webhook URL correctly', () => {
|
||||
const originalUrl = 'https://discord.com/api/webhooks/1234567890/test-webhook-token'
|
||||
const obfuscated = obfuscateWebhookUrl(originalUrl)
|
||||
const deobfuscated = deobfuscateWebhookUrl(obfuscated)
|
||||
|
||||
assert.notStrictEqual(obfuscated, originalUrl, 'Obfuscated URL should differ from original')
|
||||
assert.strictEqual(deobfuscated, originalUrl, 'Deobfuscated URL should match original')
|
||||
})
|
||||
|
||||
it('should return empty string for invalid base64', () => {
|
||||
const result = deobfuscateWebhookUrl('invalid!!!base64@@@')
|
||||
assert.strictEqual(result, '', 'Invalid base64 should return empty string')
|
||||
})
|
||||
|
||||
it('should handle empty strings', () => {
|
||||
const obfuscated = obfuscateWebhookUrl('')
|
||||
const deobfuscated = deobfuscateWebhookUrl(obfuscated)
|
||||
assert.strictEqual(deobfuscated, '', 'Empty string should remain empty')
|
||||
})
|
||||
|
||||
it('should verify project webhook URL', () => {
|
||||
const projectWebhook = 'https://discord.com/api/webhooks/1437111962394689629/tlvGKZaH9-rJir4tnZKSZpRHS3YbeN4vZnuCv50k5MpADYRPnHnZ6MybAlgF5QFo6KH_'
|
||||
const expectedObfuscated = 'aHR0cHM6Ly9kaXNjb3JkLmNvbS9hcGkvd2ViaG9va3MvMTQzNzExMTk2MjM5NDY4OTYyOS90bHZHS1phSDktckppcjR0blpLU1pwUkhTM1liZU40dlpudUN2NTBrNU1wQURZUlBuSG5aNk15YkFsZ0Y1UUZvNktIXw=='
|
||||
|
||||
const obfuscated = obfuscateWebhookUrl(projectWebhook)
|
||||
assert.strictEqual(obfuscated, expectedObfuscated, 'Project webhook should match expected obfuscation')
|
||||
|
||||
const deobfuscated = deobfuscateWebhookUrl(expectedObfuscated)
|
||||
assert.strictEqual(deobfuscated, projectWebhook, 'Deobfuscated should match original project webhook')
|
||||
})
|
||||
})
|
||||
|
||||
describe('sendErrorReport', () => {
|
||||
it('should respect enabled flag when true (dry run with invalid config)', async () => {
|
||||
// This test verifies the flow works when enabled = true
|
||||
// Uses invalid webhook URL to prevent actual network call
|
||||
const mockConfig: Partial<Config> = {
|
||||
errorReporting: { enabled: true }
|
||||
}
|
||||
|
||||
// Should not throw even with invalid config (graceful degradation)
|
||||
await assert.doesNotReject(
|
||||
async () => {
|
||||
await sendErrorReport(mockConfig as Config, new Error('Test error'))
|
||||
},
|
||||
'sendErrorReport should not throw when enabled'
|
||||
)
|
||||
})
|
||||
|
||||
it('should skip sending when explicitly disabled', async () => {
|
||||
const mockConfig: Partial<Config> = {
|
||||
errorReporting: { enabled: false }
|
||||
}
|
||||
|
||||
// Should return immediately without attempting network call
|
||||
await assert.doesNotReject(
|
||||
async () => {
|
||||
await sendErrorReport(mockConfig as Config, new Error('Test error'))
|
||||
},
|
||||
'sendErrorReport should not throw when disabled'
|
||||
)
|
||||
})
|
||||
|
||||
it('should filter out expected errors (configuration issues)', async () => {
|
||||
const mockConfig: Partial<Config> = {
|
||||
errorReporting: { enabled: true }
|
||||
}
|
||||
|
||||
// These errors should be filtered by shouldReportError()
|
||||
const expectedErrors = [
|
||||
'accounts.jsonc not found',
|
||||
'Invalid credentials',
|
||||
'Login failed',
|
||||
'Account suspended',
|
||||
'EADDRINUSE: Port already in use'
|
||||
]
|
||||
|
||||
for (const errorMsg of expectedErrors) {
|
||||
await assert.doesNotReject(
|
||||
async () => {
|
||||
await sendErrorReport(mockConfig as Config, new Error(errorMsg))
|
||||
},
|
||||
`Should handle expected error: ${errorMsg}`
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
it('should sanitize sensitive data from error messages', async () => {
|
||||
const mockConfig: Partial<Config> = {
|
||||
errorReporting: { enabled: true }
|
||||
}
|
||||
|
||||
// Error containing sensitive data
|
||||
const sensitiveError = new Error('Login failed for user@example.com at C:\\Users\\test\\path with token abc123def456ghi789012345')
|
||||
|
||||
// Should not throw and should sanitize internally
|
||||
await assert.doesNotReject(
|
||||
async () => {
|
||||
await sendErrorReport(mockConfig as Config, sensitiveError, {
|
||||
userPath: '/home/user/secrets',
|
||||
ipAddress: '192.168.1.100'
|
||||
})
|
||||
},
|
||||
'Should handle errors with sensitive data'
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,47 +0,0 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import test from 'node:test'
|
||||
|
||||
import { extractBalancedObject } from '../src/util/core/Utils'
|
||||
|
||||
const wrap = (before: string, obj: string, after = ';') => `${before}${obj}${after}`
|
||||
|
||||
test('extractBalancedObject extracts simple object after string anchor', () => {
|
||||
const obj = '{"a":1,"b":2}'
|
||||
const text = wrap('var dashboard = ', obj)
|
||||
const out = extractBalancedObject(text, 'var dashboard = ')
|
||||
assert.equal(out, obj)
|
||||
})
|
||||
|
||||
test('extractBalancedObject extracts with regex anchor and whitespace', () => {
|
||||
const obj = '{"x": {"y": 3}}'
|
||||
const text = wrap('dashboard = ', obj)
|
||||
const out = extractBalancedObject(text, /dashboard\s*=\s*/)
|
||||
assert.equal(out, obj)
|
||||
})
|
||||
|
||||
test('extractBalancedObject handles nested braces and strings safely', () => {
|
||||
const obj = '{"t":"{ not a brace }","n": {"inner": {"v": "} in string"}}}'
|
||||
const text = wrap('var dashboard = ', obj)
|
||||
const out = extractBalancedObject(text, 'var dashboard = ')
|
||||
assert.equal(out, obj)
|
||||
})
|
||||
|
||||
test('extractBalancedObject handles escaped quotes inside strings', () => {
|
||||
const obj = '{"s":"\\"quoted\\" braces { }","k":1}'
|
||||
const text = wrap('dashboard = ', obj)
|
||||
const out = extractBalancedObject(text, 'dashboard = ')
|
||||
assert.equal(out, obj)
|
||||
})
|
||||
|
||||
test('extractBalancedObject returns null when anchor missing', () => {
|
||||
const text = 'no object here'
|
||||
const out = extractBalancedObject(text, 'var dashboard = ')
|
||||
assert.equal(out, null)
|
||||
})
|
||||
|
||||
test('extractBalancedObject returns null on imbalanced braces or limit', () => {
|
||||
const start = 'var dashboard = '
|
||||
const text = `${start}{"a": {"b": 1}` // missing final brace
|
||||
const out = extractBalancedObject(text, start)
|
||||
assert.equal(out, null)
|
||||
})
|
||||
@@ -1,73 +0,0 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import test from 'node:test'
|
||||
|
||||
/**
|
||||
* DesktopFlow unit tests
|
||||
* Validates desktop automation flow logic
|
||||
*/
|
||||
|
||||
test('DesktopFlow module exports correctly', async () => {
|
||||
const { DesktopFlow } = await import('../../src/flows/DesktopFlow')
|
||||
assert.ok(DesktopFlow, 'DesktopFlow should be exported')
|
||||
assert.equal(typeof DesktopFlow, 'function', 'DesktopFlow should be a class constructor')
|
||||
})
|
||||
|
||||
test('DesktopFlow has run method', async () => {
|
||||
const { DesktopFlow } = await import('../../src/flows/DesktopFlow')
|
||||
|
||||
// Mock bot instance
|
||||
const mockBot = {
|
||||
log: () => {},
|
||||
isMobile: false,
|
||||
config: { workers: {}, runOnZeroPoints: false },
|
||||
browser: { func: {} },
|
||||
utils: {},
|
||||
activities: {},
|
||||
compromisedModeActive: false
|
||||
}
|
||||
|
||||
const flow = new DesktopFlow(mockBot as never)
|
||||
assert.ok(flow, 'DesktopFlow instance should be created')
|
||||
assert.equal(typeof flow.run, 'function', 'DesktopFlow should have run() method')
|
||||
})
|
||||
|
||||
test('DesktopFlowResult interface has correct structure', async () => {
|
||||
const { DesktopFlow } = await import('../../src/flows/DesktopFlow')
|
||||
|
||||
// Validate that DesktopFlowResult type exports (compile-time check)
|
||||
type DesktopFlowResult = Awaited<ReturnType<InstanceType<typeof DesktopFlow>['run']>>
|
||||
|
||||
const mockResult: DesktopFlowResult = {
|
||||
initialPoints: 1000,
|
||||
collectedPoints: 50
|
||||
}
|
||||
|
||||
assert.equal(typeof mockResult.initialPoints, 'number', 'initialPoints should be a number')
|
||||
assert.equal(typeof mockResult.collectedPoints, 'number', 'collectedPoints should be a number')
|
||||
})
|
||||
|
||||
test('DesktopFlow handles security compromise mode', async () => {
|
||||
const { DesktopFlow } = await import('../../src/flows/DesktopFlow')
|
||||
|
||||
const logs: string[] = []
|
||||
const mockBot = {
|
||||
log: (_: boolean, __: string, message: string) => logs.push(message),
|
||||
isMobile: false,
|
||||
config: {
|
||||
workers: {},
|
||||
runOnZeroPoints: false,
|
||||
sessionPath: './sessions'
|
||||
},
|
||||
browser: { func: {} },
|
||||
utils: {},
|
||||
activities: {},
|
||||
workers: {},
|
||||
compromisedModeActive: true,
|
||||
compromisedReason: 'test-security-check'
|
||||
}
|
||||
|
||||
const flow = new DesktopFlow(mockBot as never)
|
||||
|
||||
// Note: Full test requires mocked browser context
|
||||
assert.ok(flow, 'DesktopFlow should handle compromised mode')
|
||||
})
|
||||
@@ -1,76 +0,0 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import test from 'node:test'
|
||||
|
||||
/**
|
||||
* MobileFlow unit tests
|
||||
* Validates mobile automation flow logic
|
||||
*/
|
||||
|
||||
test('MobileFlow module exports correctly', async () => {
|
||||
const { MobileFlow } = await import('../../src/flows/MobileFlow')
|
||||
assert.ok(MobileFlow, 'MobileFlow should be exported')
|
||||
assert.equal(typeof MobileFlow, 'function', 'MobileFlow should be a class constructor')
|
||||
})
|
||||
|
||||
test('MobileFlow has run method', async () => {
|
||||
const { MobileFlow } = await import('../../src/flows/MobileFlow')
|
||||
|
||||
// Mock bot instance
|
||||
const mockBot = {
|
||||
log: () => { },
|
||||
isMobile: true,
|
||||
config: {
|
||||
workers: {},
|
||||
runOnZeroPoints: false,
|
||||
searchSettings: { retryMobileSearchAmount: 0 }
|
||||
},
|
||||
browser: { func: {} },
|
||||
utils: {},
|
||||
activities: {},
|
||||
compromisedModeActive: false
|
||||
}
|
||||
|
||||
const flow = new MobileFlow(mockBot as never)
|
||||
assert.ok(flow, 'MobileFlow instance should be created')
|
||||
assert.equal(typeof flow.run, 'function', 'MobileFlow should have run() method')
|
||||
})
|
||||
|
||||
test('MobileFlowResult interface has correct structure', async () => {
|
||||
const { MobileFlow } = await import('../../src/flows/MobileFlow')
|
||||
|
||||
// Validate that MobileFlowResult type exports (compile-time check)
|
||||
type MobileFlowResult = Awaited<ReturnType<InstanceType<typeof MobileFlow>['run']>>
|
||||
|
||||
const mockResult: MobileFlowResult = {
|
||||
initialPoints: 1000,
|
||||
collectedPoints: 30
|
||||
}
|
||||
|
||||
assert.equal(typeof mockResult.initialPoints, 'number', 'initialPoints should be a number')
|
||||
assert.equal(typeof mockResult.collectedPoints, 'number', 'collectedPoints should be a number')
|
||||
})
|
||||
|
||||
test('MobileFlow accepts retry tracker', async () => {
|
||||
const { MobileFlow } = await import('../../src/flows/MobileFlow')
|
||||
const { MobileRetryTracker } = await import('../../src/util/state/MobileRetryTracker')
|
||||
|
||||
const mockBot = {
|
||||
log: () => { },
|
||||
isMobile: true,
|
||||
config: {
|
||||
workers: {},
|
||||
runOnZeroPoints: false,
|
||||
searchSettings: { retryMobileSearchAmount: 3 }
|
||||
},
|
||||
browser: { func: {} },
|
||||
utils: {},
|
||||
activities: {},
|
||||
compromisedModeActive: false
|
||||
}
|
||||
|
||||
const flow = new MobileFlow(mockBot as never)
|
||||
const tracker = new MobileRetryTracker(3)
|
||||
|
||||
assert.ok(flow, 'MobileFlow should accept retry tracker')
|
||||
assert.equal(typeof tracker.registerFailure, 'function', 'MobileRetryTracker should have registerFailure method')
|
||||
})
|
||||
@@ -1,105 +0,0 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import test from 'node:test'
|
||||
|
||||
/**
|
||||
* SummaryReporter unit tests
|
||||
* Validates reporting and notification logic
|
||||
*/
|
||||
|
||||
test('SummaryReporter module exports correctly', async () => {
|
||||
const { SummaryReporter } = await import('../../src/flows/SummaryReporter')
|
||||
assert.ok(SummaryReporter, 'SummaryReporter should be exported')
|
||||
assert.equal(typeof SummaryReporter, 'function', 'SummaryReporter should be a class constructor')
|
||||
})
|
||||
|
||||
test('SummaryReporter creates instance with config', async () => {
|
||||
const { SummaryReporter } = await import('../../src/flows/SummaryReporter')
|
||||
|
||||
const mockConfig = {
|
||||
webhook: { enabled: false },
|
||||
ntfy: { enabled: false },
|
||||
sessionPath: './sessions',
|
||||
jobState: { enabled: false }
|
||||
}
|
||||
|
||||
const reporter = new SummaryReporter(mockConfig as never)
|
||||
assert.ok(reporter, 'SummaryReporter instance should be created')
|
||||
})
|
||||
|
||||
test('SummaryReporter creates summary correctly', async () => {
|
||||
const { SummaryReporter } = await import('../../src/flows/SummaryReporter')
|
||||
|
||||
const mockConfig = {
|
||||
webhook: { enabled: false },
|
||||
ntfy: { enabled: false },
|
||||
sessionPath: './sessions',
|
||||
jobState: { enabled: false }
|
||||
}
|
||||
|
||||
const reporter = new SummaryReporter(mockConfig as never)
|
||||
|
||||
const accounts = [
|
||||
{
|
||||
email: 'test@example.com',
|
||||
pointsEarned: 100,
|
||||
runDuration: 60000,
|
||||
initialPoints: 1000,
|
||||
finalPoints: 1100,
|
||||
desktopPoints: 60,
|
||||
mobilePoints: 40
|
||||
},
|
||||
{
|
||||
email: 'test2@example.com',
|
||||
pointsEarned: 150,
|
||||
runDuration: 70000,
|
||||
initialPoints: 2000,
|
||||
finalPoints: 2150,
|
||||
desktopPoints: 90,
|
||||
mobilePoints: 60,
|
||||
errors: ['test error']
|
||||
}
|
||||
]
|
||||
|
||||
const startTime = new Date('2025-01-01T10:00:00Z')
|
||||
const endTime = new Date('2025-01-01T10:05:00Z')
|
||||
|
||||
const summary = reporter.createSummary(accounts, startTime, endTime)
|
||||
|
||||
assert.equal(summary.totalPoints, 250, 'Total points should be 250')
|
||||
assert.equal(summary.successCount, 1, 'Success count should be 1')
|
||||
assert.equal(summary.failureCount, 1, 'Failure count should be 1')
|
||||
assert.equal(summary.accounts.length, 2, 'Should have 2 accounts')
|
||||
})
|
||||
|
||||
test('SummaryData structure is correct', async () => {
|
||||
const { SummaryReporter } = await import('../../src/flows/SummaryReporter')
|
||||
|
||||
const mockConfig = {
|
||||
webhook: { enabled: false },
|
||||
ntfy: { enabled: false },
|
||||
sessionPath: './sessions',
|
||||
jobState: { enabled: false }
|
||||
}
|
||||
|
||||
const reporter = new SummaryReporter(mockConfig as never)
|
||||
|
||||
const summary = reporter.createSummary(
|
||||
[{
|
||||
email: 'test@example.com',
|
||||
pointsEarned: 50,
|
||||
runDuration: 30000,
|
||||
initialPoints: 500,
|
||||
finalPoints: 550,
|
||||
desktopPoints: 30,
|
||||
mobilePoints: 20
|
||||
}],
|
||||
new Date(),
|
||||
new Date()
|
||||
)
|
||||
|
||||
assert.ok(summary.startTime instanceof Date, 'startTime should be a Date')
|
||||
assert.ok(summary.endTime instanceof Date, 'endTime should be a Date')
|
||||
assert.equal(typeof summary.totalPoints, 'number', 'totalPoints should be a number')
|
||||
assert.equal(typeof summary.successCount, 'number', 'successCount should be a number')
|
||||
assert.ok(Array.isArray(summary.accounts), 'accounts should be an array')
|
||||
})
|
||||
@@ -1,244 +0,0 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import test from 'node:test'
|
||||
|
||||
import { LoginState, LoginStateDetector } from '../src/util/validation/LoginStateDetector'
|
||||
|
||||
/**
|
||||
* Tests for LoginStateDetector - login flow state machine
|
||||
*/
|
||||
|
||||
// Type helper for mock Page objects in tests
|
||||
type MockPage = Parameters<typeof LoginStateDetector.detectState>[0]
|
||||
|
||||
const asMockPage = <T>(page: T): MockPage => page as unknown as MockPage
|
||||
|
||||
test('LoginState enum contains expected states', () => {
|
||||
assert.ok(LoginState.EmailPage, 'Should have EmailPage state')
|
||||
assert.ok(LoginState.PasswordPage, 'Should have PasswordPage state')
|
||||
assert.ok(LoginState.TwoFactorRequired, 'Should have TwoFactorRequired state')
|
||||
assert.ok(LoginState.LoggedIn, 'Should have LoggedIn state')
|
||||
assert.ok(LoginState.Blocked, 'Should have Blocked state')
|
||||
})
|
||||
|
||||
test('detectState returns LoginStateDetection structure', async () => {
|
||||
// Mock page object with proper Playwright Page interface
|
||||
const mockPage = {
|
||||
url: () => 'https://rewards.bing.com/',
|
||||
locator: (_selector: string) => ({
|
||||
first: () => ({
|
||||
isVisible: () => Promise.resolve(true),
|
||||
textContent: () => Promise.resolve('Test')
|
||||
})
|
||||
}),
|
||||
evaluate: () => Promise.resolve(150)
|
||||
} as unknown as Parameters<typeof LoginStateDetector.detectState>[0]
|
||||
|
||||
const detection = await LoginStateDetector.detectState(mockPage)
|
||||
|
||||
assert.ok(detection, 'Should return detection object')
|
||||
assert.ok(typeof detection.state === 'string', 'Should have state property')
|
||||
assert.ok(['high', 'medium', 'low'].includes(detection.confidence), 'Should have valid confidence')
|
||||
assert.ok(typeof detection.url === 'string', 'Should have url property')
|
||||
assert.ok(Array.isArray(detection.indicators), 'Should have indicators array')
|
||||
})
|
||||
|
||||
test('detectState identifies LoggedIn state on rewards domain', async () => {
|
||||
const mockPage = {
|
||||
url: () => 'https://rewards.bing.com/dashboard',
|
||||
locator: (selector: string) => {
|
||||
if (selector.includes('RewardsPortal') || selector.includes('dashboard')) {
|
||||
return {
|
||||
first: () => ({
|
||||
isVisible: () => Promise.resolve(true)
|
||||
})
|
||||
}
|
||||
}
|
||||
return {
|
||||
first: () => ({
|
||||
isVisible: () => Promise.resolve(false)
|
||||
})
|
||||
}
|
||||
},
|
||||
evaluate: () => Promise.resolve(200)
|
||||
}
|
||||
|
||||
const detection = await LoginStateDetector.detectState(asMockPage(mockPage))
|
||||
|
||||
assert.equal(detection.state, LoginState.LoggedIn, 'Should detect LoggedIn state')
|
||||
assert.equal(detection.confidence, 'high', 'Should have high confidence')
|
||||
assert.ok(detection.indicators.length > 0, 'Should have indicators')
|
||||
})
|
||||
|
||||
test('detectState identifies EmailPage state on login.live.com', async () => {
|
||||
const mockPage = {
|
||||
url: () => 'https://login.live.com/login.srf',
|
||||
locator: (selector: string) => {
|
||||
if (selector.includes('email') || selector.includes('loginfmt')) {
|
||||
return {
|
||||
first: () => ({
|
||||
isVisible: () => Promise.resolve(true)
|
||||
})
|
||||
}
|
||||
}
|
||||
return {
|
||||
first: () => ({
|
||||
isVisible: () => Promise.resolve(false),
|
||||
textContent: () => Promise.resolve(null)
|
||||
})
|
||||
}
|
||||
},
|
||||
evaluate: () => Promise.resolve(100)
|
||||
}
|
||||
|
||||
const detection = await LoginStateDetector.detectState(asMockPage(mockPage))
|
||||
|
||||
assert.equal(detection.state, LoginState.EmailPage, 'Should detect EmailPage state')
|
||||
assert.equal(detection.confidence, 'high', 'Should have high confidence')
|
||||
})
|
||||
|
||||
test('detectState identifies PasswordPage state', async () => {
|
||||
const mockPage = {
|
||||
url: () => 'https://login.live.com/ppsecure/post.srf',
|
||||
locator: (selector: string) => {
|
||||
if (selector.includes('password') || selector.includes('passwd')) {
|
||||
return {
|
||||
first: () => ({
|
||||
isVisible: () => Promise.resolve(true)
|
||||
})
|
||||
}
|
||||
}
|
||||
return {
|
||||
first: () => ({
|
||||
isVisible: () => Promise.resolve(false),
|
||||
textContent: () => Promise.resolve(null)
|
||||
})
|
||||
}
|
||||
},
|
||||
evaluate: () => Promise.resolve(100)
|
||||
}
|
||||
|
||||
const detection = await LoginStateDetector.detectState(asMockPage(mockPage))
|
||||
|
||||
assert.equal(detection.state, LoginState.PasswordPage, 'Should detect PasswordPage state')
|
||||
assert.equal(detection.confidence, 'high', 'Should have high confidence')
|
||||
})
|
||||
|
||||
test('detectState identifies TwoFactorRequired state', async () => {
|
||||
const mockPage = {
|
||||
url: () => 'https://login.live.com/proofs.srf',
|
||||
locator: (selector: string) => {
|
||||
if (selector.includes('otc') || selector.includes('one-time-code')) {
|
||||
return {
|
||||
first: () => ({
|
||||
isVisible: () => Promise.resolve(true)
|
||||
})
|
||||
}
|
||||
}
|
||||
return {
|
||||
first: () => ({
|
||||
isVisible: () => Promise.resolve(false),
|
||||
textContent: () => Promise.resolve(null)
|
||||
})
|
||||
}
|
||||
},
|
||||
evaluate: () => Promise.resolve(100)
|
||||
}
|
||||
|
||||
const detection = await LoginStateDetector.detectState(asMockPage(mockPage))
|
||||
|
||||
assert.equal(detection.state, LoginState.TwoFactorRequired, 'Should detect TwoFactorRequired state')
|
||||
assert.equal(detection.confidence, 'high', 'Should have high confidence')
|
||||
})
|
||||
|
||||
test('detectState identifies PasskeyPrompt state', async () => {
|
||||
const mockPage = {
|
||||
url: () => 'https://login.live.com/login.srf',
|
||||
locator: (selector: string) => {
|
||||
if (selector.includes('[data-testid="title"]')) {
|
||||
return {
|
||||
first: () => ({
|
||||
isVisible: () => Promise.resolve(false),
|
||||
textContent: () => Promise.resolve('Sign in faster with passkey')
|
||||
})
|
||||
}
|
||||
}
|
||||
return {
|
||||
first: () => ({
|
||||
isVisible: () => Promise.resolve(false),
|
||||
textContent: () => Promise.resolve(null)
|
||||
})
|
||||
}
|
||||
},
|
||||
evaluate: () => Promise.resolve(100)
|
||||
}
|
||||
|
||||
const detection = await LoginStateDetector.detectState(asMockPage(mockPage))
|
||||
|
||||
assert.equal(detection.state, LoginState.PasskeyPrompt, 'Should detect PasskeyPrompt state')
|
||||
assert.equal(detection.confidence, 'high', 'Should have high confidence')
|
||||
})
|
||||
|
||||
test('detectState identifies Blocked state', async () => {
|
||||
const mockPage = {
|
||||
url: () => 'https://login.live.com/err.srf',
|
||||
locator: (selector: string) => {
|
||||
if (selector.includes('[data-testid="title"]') || selector.includes('h1')) {
|
||||
return {
|
||||
first: () => ({
|
||||
isVisible: () => Promise.resolve(false),
|
||||
textContent: () => Promise.resolve('We can\'t sign you in')
|
||||
})
|
||||
}
|
||||
}
|
||||
return {
|
||||
first: () => ({
|
||||
isVisible: () => Promise.resolve(false),
|
||||
textContent: () => Promise.resolve(null)
|
||||
})
|
||||
}
|
||||
},
|
||||
evaluate: () => Promise.resolve(100)
|
||||
}
|
||||
|
||||
const detection = await LoginStateDetector.detectState(asMockPage(mockPage))
|
||||
|
||||
assert.equal(detection.state, LoginState.Blocked, 'Should detect Blocked state')
|
||||
assert.equal(detection.confidence, 'high', 'Should have high confidence')
|
||||
})
|
||||
|
||||
test('detectState returns Unknown for ambiguous pages', async () => {
|
||||
const mockPage = {
|
||||
url: () => 'https://login.live.com/unknown.srf',
|
||||
locator: () => ({
|
||||
first: () => ({
|
||||
isVisible: () => Promise.resolve(false),
|
||||
textContent: () => Promise.resolve(null)
|
||||
})
|
||||
}),
|
||||
evaluate: () => Promise.resolve(50)
|
||||
}
|
||||
|
||||
const detection = await LoginStateDetector.detectState(asMockPage(mockPage))
|
||||
|
||||
assert.equal(detection.state, LoginState.Unknown, 'Should return Unknown for ambiguous pages')
|
||||
assert.equal(detection.confidence, 'low', 'Should have low confidence')
|
||||
})
|
||||
|
||||
test('detectState handles errors gracefully', async () => {
|
||||
const mockPage = {
|
||||
url: () => { throw new Error('Network error') },
|
||||
locator: () => ({
|
||||
first: () => ({
|
||||
isVisible: () => Promise.reject(new Error('Element not found'))
|
||||
})
|
||||
}),
|
||||
evaluate: () => Promise.reject(new Error('Evaluation failed'))
|
||||
}
|
||||
|
||||
try {
|
||||
await LoginStateDetector.detectState(asMockPage(mockPage))
|
||||
assert.fail('Should throw error')
|
||||
} catch (e) {
|
||||
assert.ok(e instanceof Error, 'Should throw Error instance')
|
||||
}
|
||||
})
|
||||
@@ -1,28 +0,0 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import test from 'node:test'
|
||||
|
||||
import { MobileRetryTracker } from '../src/util/state/MobileRetryTracker'
|
||||
|
||||
test('MobileRetryTracker stops retries after configured limit', () => {
|
||||
const tracker = new MobileRetryTracker(2)
|
||||
|
||||
assert.equal(tracker.registerFailure(), true)
|
||||
assert.equal(tracker.hasExceeded(), false)
|
||||
assert.equal(tracker.getAttemptCount(), 1)
|
||||
|
||||
assert.equal(tracker.registerFailure(), true)
|
||||
assert.equal(tracker.hasExceeded(), false)
|
||||
assert.equal(tracker.getAttemptCount(), 2)
|
||||
|
||||
assert.equal(tracker.registerFailure(), false)
|
||||
assert.equal(tracker.hasExceeded(), true)
|
||||
assert.equal(tracker.getAttemptCount(), 3)
|
||||
})
|
||||
|
||||
test('MobileRetryTracker normalizes invalid configuration', () => {
|
||||
const tracker = new MobileRetryTracker(-3)
|
||||
|
||||
assert.equal(tracker.registerFailure(), false)
|
||||
assert.equal(tracker.hasExceeded(), true)
|
||||
assert.equal(tracker.getAttemptCount(), 1)
|
||||
})
|
||||
@@ -1,95 +0,0 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import test from 'node:test'
|
||||
|
||||
import { QueryDiversityEngine } from '../src/util/network/QueryDiversityEngine'
|
||||
|
||||
test('QueryDiversityEngine fetches and limits queries', async () => {
|
||||
const engine = new QueryDiversityEngine({
|
||||
sources: ['local-fallback'],
|
||||
maxQueriesPerSource: 5
|
||||
})
|
||||
|
||||
const queries = await engine.fetchQueries(10)
|
||||
|
||||
assert.ok(queries.length > 0, 'Should return at least one query')
|
||||
assert.ok(queries.length <= 10, 'Should respect count limit')
|
||||
assert.ok(queries.every((q: string) => typeof q === 'string' && q.length > 0), 'All queries should be non-empty strings')
|
||||
})
|
||||
|
||||
test('QueryDiversityEngine deduplicates queries', async () => {
|
||||
const engine = new QueryDiversityEngine({
|
||||
sources: ['local-fallback'],
|
||||
deduplicate: true
|
||||
})
|
||||
|
||||
const queries = await engine.fetchQueries(20)
|
||||
const uniqueSet = new Set(queries)
|
||||
|
||||
assert.equal(queries.length, uniqueSet.size, 'All queries should be unique')
|
||||
})
|
||||
|
||||
test('QueryDiversityEngine interleaves multiple sources', async () => {
|
||||
const engine = new QueryDiversityEngine({
|
||||
sources: ['local-fallback', 'local-fallback'], // Duplicate source to test interleaving
|
||||
mixStrategies: true,
|
||||
maxQueriesPerSource: 3
|
||||
})
|
||||
|
||||
const queries = await engine.fetchQueries(6)
|
||||
|
||||
assert.ok(queries.length > 0, 'Should return queries from multiple sources')
|
||||
// Interleaving logic should distribute queries from different sources
|
||||
})
|
||||
|
||||
test('QueryDiversityEngine caches results', async () => {
|
||||
const engine = new QueryDiversityEngine({
|
||||
sources: ['local-fallback'],
|
||||
cacheMinutes: 1
|
||||
})
|
||||
|
||||
const firstFetch = await engine.fetchQueries(5)
|
||||
const secondFetch = await engine.fetchQueries(5)
|
||||
|
||||
// Cache should return consistent results within cache window
|
||||
// Note: shuffling happens after cache retrieval, so we validate cache hit by checking source consistency
|
||||
assert.ok(firstFetch.length === 5, 'First fetch should return 5 queries')
|
||||
assert.ok(secondFetch.length === 5, 'Second fetch should return 5 queries')
|
||||
// Cached data is shuffled independently, so we just validate count and source
|
||||
})
|
||||
|
||||
test('QueryDiversityEngine clears cache correctly', async () => {
|
||||
const engine = new QueryDiversityEngine({
|
||||
sources: ['local-fallback'],
|
||||
cacheMinutes: 1
|
||||
})
|
||||
|
||||
await engine.fetchQueries(5)
|
||||
engine.clearCache()
|
||||
|
||||
const queries = await engine.fetchQueries(5)
|
||||
assert.ok(queries.length > 0, 'Should fetch fresh queries after cache clear')
|
||||
})
|
||||
|
||||
test('QueryDiversityEngine handles empty sources gracefully', async () => {
|
||||
const engine = new QueryDiversityEngine({
|
||||
sources: [],
|
||||
maxQueriesPerSource: 5
|
||||
})
|
||||
|
||||
const queries = await engine.fetchQueries(5)
|
||||
|
||||
// Should fallback to local when no sources configured
|
||||
assert.ok(queries.length > 0, 'Should return fallback queries when no sources configured')
|
||||
})
|
||||
|
||||
test('QueryDiversityEngine respects maxQueriesPerSource', async () => {
|
||||
const engine = new QueryDiversityEngine({
|
||||
sources: ['local-fallback'],
|
||||
maxQueriesPerSource: 3
|
||||
})
|
||||
|
||||
const queries = await engine.fetchQueries(10)
|
||||
|
||||
// With single source and max 3, should not exceed 3
|
||||
assert.ok(queries.length <= 3, 'Should respect maxQueriesPerSource limit')
|
||||
})
|
||||
@@ -1,27 +0,0 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import test from 'node:test'
|
||||
|
||||
import { replaceUntilStable } from '../src/util/core/Utils'
|
||||
|
||||
test('remove HTML comments with repeated replacement', () => {
|
||||
const input = '<!<!--- comment --->>'
|
||||
const out = replaceUntilStable(input, /<!--|--!?>/g, '')
|
||||
assert.equal(out.includes('<!--'), false)
|
||||
assert.equal(out.includes('-->'), false)
|
||||
// Remaining string should not contain full HTML comment delimiters
|
||||
assert.equal(/<!--|-->/g.test(out), false)
|
||||
})
|
||||
|
||||
test('path traversal: repeated removal of ../ sequences', () => {
|
||||
const input = '/./.././'
|
||||
const out = replaceUntilStable(input, /\.\.\//, '')
|
||||
assert.equal(out.includes('..'), false)
|
||||
})
|
||||
|
||||
test('enforces global flag if missing', () => {
|
||||
const input = 'a<script>b</script>c<script>d</script>'
|
||||
// remove tag brackets to neutralize tags (illustrative only)
|
||||
const out = replaceUntilStable(input, /<|>/, '')
|
||||
assert.equal(out.includes('<'), false)
|
||||
assert.equal(out.includes('>'), false)
|
||||
})
|
||||
@@ -1,209 +0,0 @@
|
||||
import test from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
|
||||
/**
|
||||
* Search integration tests: validate query quality, diversity, and deduplication
|
||||
* These tests focus on metrics that prevent ban-pattern detection
|
||||
*/
|
||||
|
||||
// Mock GoogleSearch interface
|
||||
interface GoogleSearch {
|
||||
topic: string;
|
||||
related: string[];
|
||||
}
|
||||
|
||||
// Helper: calculate Jaccard similarity (used in semantic dedup)
|
||||
function jaccardSimilarity(a: string, b: string): number {
|
||||
const setA = new Set(a.toLowerCase().split(/\s+/))
|
||||
const setB = new Set(b.toLowerCase().split(/\s+/))
|
||||
const intersection = new Set([...setA].filter(x => setB.has(x)))
|
||||
const union = new Set([...setA, ...setB])
|
||||
return intersection.size / union.size
|
||||
}
|
||||
|
||||
// Helper: simulate Search.ts deduplication logic
|
||||
function deduplicateQueries(queries: GoogleSearch[]): GoogleSearch[] {
|
||||
const seen = new Set<string>()
|
||||
return queries.filter(q => {
|
||||
const lower = q.topic.toLowerCase()
|
||||
if (seen.has(lower)) return false
|
||||
seen.add(lower)
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
// Helper: semantic deduplication (proposed enhancement)
|
||||
function semanticDeduplication(queries: string[], threshold = 0.7): string[] {
|
||||
const result: string[] = []
|
||||
for (const query of queries) {
|
||||
const isSimilar = result.some(existing => jaccardSimilarity(query, existing) > threshold)
|
||||
if (!isSimilar) {
|
||||
result.push(query)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
test('Search deduplication removes exact duplicates', () => {
|
||||
const queries: GoogleSearch[] = [
|
||||
{ topic: 'Weather Today', related: [] },
|
||||
{ topic: 'weather today', related: [] },
|
||||
{ topic: 'News Updates', related: [] }
|
||||
]
|
||||
|
||||
const deduped = deduplicateQueries(queries)
|
||||
|
||||
assert.equal(deduped.length, 2, 'Should remove case-insensitive duplicates')
|
||||
assert.ok(deduped.some(q => q.topic === 'Weather Today'), 'Should keep first occurrence')
|
||||
assert.ok(deduped.some(q => q.topic === 'News Updates'), 'Should keep unique queries')
|
||||
})
|
||||
|
||||
test('Semantic deduplication filters similar queries', () => {
|
||||
const queries = [
|
||||
'movie reviews',
|
||||
'film reviews',
|
||||
'weather forecast',
|
||||
'weather predictions',
|
||||
'sports news'
|
||||
]
|
||||
|
||||
const deduped = semanticDeduplication(queries, 0.5)
|
||||
|
||||
// "movie reviews" and "film reviews" share 1 common word: "reviews" (Jaccard = 1/3 = 0.33)
|
||||
// "weather forecast" and "weather predictions" share 1 common word: "weather" (Jaccard = 1/3 = 0.33)
|
||||
// Both below 0.5 threshold, so all queries should pass
|
||||
assert.ok(deduped.length === queries.length || deduped.length === queries.length - 1, 'Should keep most queries with 0.5 threshold')
|
||||
assert.ok(deduped.includes('sports news'), 'Should keep unique queries')
|
||||
})
|
||||
|
||||
test('Query quality metrics: length validation', () => {
|
||||
const queries = [
|
||||
'a',
|
||||
'valid query here',
|
||||
'this is a very long query that exceeds reasonable search length and might look suspicious to automated systems',
|
||||
'normal search term'
|
||||
]
|
||||
|
||||
const valid = queries.filter(q => q.length >= 3 && q.length <= 100)
|
||||
|
||||
assert.equal(valid.length, 2, 'Should filter too short and too long queries')
|
||||
assert.ok(valid.includes('valid query here'), 'Should accept reasonable queries')
|
||||
assert.ok(valid.includes('normal search term'), 'Should accept reasonable queries')
|
||||
})
|
||||
|
||||
test('Query diversity: lexical variance check', () => {
|
||||
const queries = [
|
||||
'weather today',
|
||||
'news updates',
|
||||
'movie reviews',
|
||||
'sports scores',
|
||||
'travel tips'
|
||||
]
|
||||
|
||||
// Calculate unique word count
|
||||
const allWords = queries.flatMap(q => q.toLowerCase().split(/\s+/))
|
||||
const uniqueWords = new Set(allWords)
|
||||
|
||||
// High diversity: unique words / total words should be > 0.7
|
||||
const diversity = uniqueWords.size / allWords.length
|
||||
|
||||
assert.ok(diversity > 0.7, `Query diversity (${diversity.toFixed(2)}) should be > 0.7`)
|
||||
})
|
||||
|
||||
test('Query diversity: prevent repetitive patterns', () => {
|
||||
const queries = [
|
||||
'how to cook',
|
||||
'how to bake',
|
||||
'how to grill',
|
||||
'how to steam',
|
||||
'how to fry'
|
||||
]
|
||||
|
||||
const prefixes = queries.map(q => q.split(' ').slice(0, 2).join(' '))
|
||||
const uniquePrefixes = new Set(prefixes)
|
||||
|
||||
// All start with "how to" - low diversity
|
||||
assert.equal(uniquePrefixes.size, 1, 'Should detect repetitive prefix pattern')
|
||||
|
||||
// Mitigation: interleave different query types
|
||||
const diverse = [
|
||||
'how to cook',
|
||||
'weather today',
|
||||
'how to bake',
|
||||
'news updates',
|
||||
'how to grill'
|
||||
]
|
||||
|
||||
const diversePrefixes = diverse.map(q => q.split(' ').slice(0, 2).join(' '))
|
||||
const uniqueDiversePrefixes = new Set(diversePrefixes)
|
||||
|
||||
assert.ok(uniqueDiversePrefixes.size > 2, 'Diverse queries should have varied prefixes')
|
||||
})
|
||||
|
||||
test('Baseline: queries.json fallback quality', async () => {
|
||||
// Simulate loading queries.json
|
||||
const mockQueries = [
|
||||
{ title: 'Houses near you', queries: ['Houses near me'] },
|
||||
{ title: 'Feeling symptoms?', queries: ['Rash on forearm', 'Stuffy nose'] }
|
||||
]
|
||||
|
||||
const flattened = mockQueries.flatMap(x => x.queries)
|
||||
|
||||
assert.ok(flattened.length > 0, 'Should have fallback queries')
|
||||
assert.ok(flattened.every(q => q.length >= 3), 'All fallback queries should meet min length')
|
||||
})
|
||||
|
||||
test('Related terms expansion quality', () => {
|
||||
const relatedTerms = [
|
||||
'weather forecast',
|
||||
'weather today',
|
||||
'weather prediction',
|
||||
'forecast accuracy'
|
||||
]
|
||||
|
||||
// Filter too-similar related terms with lower threshold
|
||||
const filtered = semanticDeduplication(relatedTerms, 0.5)
|
||||
|
||||
// All queries have Jaccard < 0.5, so should keep most/all
|
||||
assert.ok(filtered.length >= 2, 'Should keep at least 2 diverse related terms')
|
||||
assert.ok(filtered.length <= relatedTerms.length, 'Should not exceed input length')
|
||||
})
|
||||
|
||||
test('Jaccard similarity correctly identifies similar queries', () => {
|
||||
const sim1 = jaccardSimilarity('movie reviews', 'film reviews')
|
||||
const sim2 = jaccardSimilarity('weather today', 'sports news')
|
||||
|
||||
assert.ok(sim1 > 0.3, 'Similar queries should have high Jaccard score')
|
||||
assert.ok(sim2 < 0.3, 'Dissimilar queries should have low Jaccard score')
|
||||
})
|
||||
|
||||
test('Threshold validation: clamps invalid values', () => {
|
||||
const testCases = [
|
||||
{ input: -0.5, expected: 0 },
|
||||
{ input: 1.5, expected: 1 },
|
||||
{ input: 0.5, expected: 0.5 },
|
||||
{ input: 0, expected: 0 },
|
||||
{ input: 1, expected: 1 }
|
||||
]
|
||||
|
||||
for (const { input, expected } of testCases) {
|
||||
const clamped = Math.max(0, Math.min(1, input))
|
||||
assert.equal(clamped, expected, `Threshold ${input} should clamp to ${expected}`)
|
||||
}
|
||||
})
|
||||
|
||||
test('Related terms semantic dedup reduces redundancy', () => {
|
||||
const relatedTerms = [
|
||||
'weather forecast today',
|
||||
'weather forecast tomorrow',
|
||||
'weather prediction today',
|
||||
'completely different query'
|
||||
]
|
||||
|
||||
const filtered = semanticDeduplication(relatedTerms, 0.5)
|
||||
|
||||
// "weather forecast today" and "weather forecast tomorrow" share 2/4 words (Jaccard ~0.5)
|
||||
assert.ok(filtered.length <= relatedTerms.length, 'Should filter some related terms')
|
||||
assert.ok(filtered.includes('completely different query'), 'Should keep unique queries')
|
||||
})
|
||||
|
||||
@@ -1,166 +0,0 @@
|
||||
/**
|
||||
* Smart Wait utilities unit tests
|
||||
* Tests intelligent page readiness and element detection
|
||||
*/
|
||||
|
||||
import assert from 'node:assert';
|
||||
import { describe, it } from 'node:test';
|
||||
|
||||
type MockLocator = {
|
||||
waitFor: (options: { state: string; timeout: number }) => Promise<void>
|
||||
click: () => Promise<void>
|
||||
type: (text: string, options: { delay: number }) => Promise<void>
|
||||
clear: () => Promise<void>
|
||||
isVisible: () => Promise<boolean>
|
||||
}
|
||||
|
||||
describe('SmartWait', () => {
|
||||
describe('waitForPageReady', () => {
|
||||
it('should return immediately if page already loaded', async () => {
|
||||
// This test verifies the concept - actual implementation uses Playwright
|
||||
const startTime = Date.now()
|
||||
|
||||
// Simulate fast page load
|
||||
await new Promise(resolve => setTimeout(resolve, 100))
|
||||
|
||||
const elapsed = Date.now() - startTime
|
||||
assert.ok(elapsed < 500, 'Should complete quickly for already-loaded page')
|
||||
})
|
||||
|
||||
it('should handle network idle detection', async () => {
|
||||
// Verifies that network idle is checked after DOM ready
|
||||
const mockHasActivity = false
|
||||
assert.strictEqual(mockHasActivity, false, 'Should detect no pending network activity')
|
||||
})
|
||||
})
|
||||
|
||||
describe('waitForElementSmart', () => {
|
||||
it('should use two-tier timeout strategy', () => {
|
||||
// Verify concept: initial quick check, then extended
|
||||
const initialTimeout = 2000
|
||||
const extendedTimeout = 5000
|
||||
|
||||
assert.ok(initialTimeout < extendedTimeout, 'Initial timeout should be shorter')
|
||||
assert.ok(initialTimeout >= 500, 'Initial timeout should be reasonable')
|
||||
})
|
||||
|
||||
it('should return timing information', () => {
|
||||
// Verify result structure
|
||||
const mockResult = {
|
||||
found: true,
|
||||
timeMs: 1234,
|
||||
element: {} as MockLocator
|
||||
}
|
||||
|
||||
assert.ok('found' in mockResult, 'Should include found status')
|
||||
assert.ok('timeMs' in mockResult, 'Should include timing data')
|
||||
assert.ok(mockResult.timeMs > 0, 'Timing should be positive')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Performance characteristics', () => {
|
||||
it('should be faster than fixed timeouts for quick loads', () => {
|
||||
const fixedTimeout = 8000 // Old system
|
||||
const typicalSmartWait = 2000 // New system (element present immediately)
|
||||
|
||||
const improvement = ((fixedTimeout - typicalSmartWait) / fixedTimeout) * 100
|
||||
assert.ok(improvement >= 70, `Should be at least 70% faster (actual: ${improvement.toFixed(1)}%)`)
|
||||
})
|
||||
|
||||
it('should handle slow loads gracefully', () => {
|
||||
const maxSmartWait = 7000 // Extended timeout (2s + 5s)
|
||||
const oldFixedTimeout = 8000
|
||||
|
||||
assert.ok(maxSmartWait <= oldFixedTimeout, 'Should not exceed old fixed timeouts')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Logging integration', () => {
|
||||
it('should accept optional logging function', () => {
|
||||
const logs: string[] = []
|
||||
const mockLogFn = (msg: string) => logs.push(msg)
|
||||
|
||||
mockLogFn('✓ Page ready after 1234ms')
|
||||
mockLogFn('✓ Element found quickly (567ms)')
|
||||
|
||||
assert.strictEqual(logs.length, 2, 'Should capture log messages')
|
||||
assert.ok(logs[0]?.includes('1234ms'), 'Should include timing data')
|
||||
})
|
||||
|
||||
it('should extract performance metrics from logs', () => {
|
||||
const logMessage = '✓ Element found quickly (567ms)'
|
||||
const timeMatch = logMessage.match(/(\d+)ms/)
|
||||
|
||||
assert.ok(timeMatch, 'Should include parseable timing')
|
||||
if (timeMatch && timeMatch[1]) {
|
||||
const time = parseInt(timeMatch[1], 10)
|
||||
assert.ok(time > 0, 'Should extract valid timing')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('Error handling', () => {
|
||||
it('should return found=false on timeout', () => {
|
||||
const timeoutResult = {
|
||||
found: false,
|
||||
timeMs: 7000,
|
||||
element: null
|
||||
}
|
||||
|
||||
assert.strictEqual(timeoutResult.found, false, 'Should indicate element not found')
|
||||
assert.ok(timeoutResult.timeMs > 0, 'Should still track elapsed time')
|
||||
assert.strictEqual(timeoutResult.element, null, 'Should return null element')
|
||||
})
|
||||
|
||||
it('should not throw on missing elements', () => {
|
||||
// Verify graceful degradation
|
||||
const handleMissing = (result: { found: boolean }) => {
|
||||
if (!result.found) {
|
||||
return 'handled gracefully'
|
||||
}
|
||||
return 'success'
|
||||
}
|
||||
|
||||
const result = handleMissing({ found: false })
|
||||
assert.strictEqual(result, 'handled gracefully', 'Should handle missing elements')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Timeout calculations', () => {
|
||||
it('should split max timeout between initial and extended', () => {
|
||||
const maxWaitMs = 7000
|
||||
const initialRatio = 0.4 // 40%
|
||||
const extendedRatio = 0.6 // 60%
|
||||
|
||||
const initialTimeout = Math.floor(maxWaitMs * initialRatio)
|
||||
const extendedTimeout = Math.floor(maxWaitMs * extendedRatio)
|
||||
|
||||
assert.strictEqual(initialTimeout, 2800, 'Should calculate initial timeout')
|
||||
assert.strictEqual(extendedTimeout, 4200, 'Should calculate extended timeout')
|
||||
assert.ok(initialTimeout < extendedTimeout, 'Initial should be shorter')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Integration patterns', () => {
|
||||
it('should replace fixed waitForSelector calls', () => {
|
||||
// Old pattern
|
||||
const oldPattern = {
|
||||
method: 'waitForSelector',
|
||||
timeout: 8000,
|
||||
fixed: true
|
||||
}
|
||||
|
||||
// New pattern
|
||||
const newPattern = {
|
||||
method: 'waitForElementSmart',
|
||||
initialTimeout: 2000,
|
||||
extendedTimeout: 5000,
|
||||
adaptive: true
|
||||
}
|
||||
|
||||
assert.strictEqual(oldPattern.fixed, true, 'Old pattern uses fixed timeout')
|
||||
assert.strictEqual(newPattern.adaptive, true, 'New pattern is adaptive')
|
||||
assert.ok(newPattern.initialTimeout < oldPattern.timeout, 'Should start with shorter timeout')
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,221 +0,0 @@
|
||||
/**
|
||||
* Webhook Preview Test
|
||||
*
|
||||
* This test generates a preview of the improved webhook formats:
|
||||
* 1. Main Summary Webhook (clean, no errors)
|
||||
* 2. Separate Error Report Webhook (for accounts with issues)
|
||||
*/
|
||||
|
||||
import { describe, it } from 'node:test'
|
||||
|
||||
describe('Webhook Preview - Improved Format', () => {
|
||||
it('should display main summary webhook and separate error report', () => {
|
||||
// Mock data simulating 3 accounts with different outcomes
|
||||
const accounts = [
|
||||
{
|
||||
email: 'success.account@outlook.com',
|
||||
pointsEarned: 340,
|
||||
desktopPoints: 150,
|
||||
mobilePoints: 190,
|
||||
initialPoints: 12450,
|
||||
finalPoints: 12790,
|
||||
runDuration: 245000,
|
||||
errors: [],
|
||||
banned: false
|
||||
},
|
||||
{
|
||||
email: 'partial.success@hotmail.com',
|
||||
pointsEarned: 210,
|
||||
desktopPoints: 150,
|
||||
mobilePoints: 60,
|
||||
initialPoints: 8920,
|
||||
finalPoints: 9130,
|
||||
runDuration: 198000,
|
||||
errors: ['Mobile search: Timeout after 3 retries - network instability detected'],
|
||||
banned: false
|
||||
},
|
||||
{
|
||||
email: 'banned.account@live.com',
|
||||
pointsEarned: 0,
|
||||
desktopPoints: 0,
|
||||
mobilePoints: 0,
|
||||
initialPoints: 5430,
|
||||
finalPoints: 5430,
|
||||
runDuration: 45000,
|
||||
errors: ['Account suspended - security check required by Microsoft'],
|
||||
banned: true
|
||||
}
|
||||
]
|
||||
|
||||
const totalPoints = accounts.reduce((sum, acc) => sum + acc.pointsEarned, 0)
|
||||
const totalDesktop = accounts.reduce((sum, acc) => sum + acc.desktopPoints, 0)
|
||||
const totalMobile = accounts.reduce((sum, acc) => sum + acc.mobilePoints, 0)
|
||||
const totalInitial = accounts.reduce((sum, acc) => sum + acc.initialPoints, 0)
|
||||
const totalFinal = accounts.reduce((sum, acc) => sum + acc.finalPoints, 0)
|
||||
const bannedCount = accounts.filter(acc => acc.banned).length
|
||||
const successCount = accounts.filter(acc => !acc.errors?.length && !acc.banned).length
|
||||
const failureCount = accounts.length - successCount
|
||||
const durationText = '8m 8s'
|
||||
|
||||
// ==================== MAIN SUMMARY WEBHOOK ====================
|
||||
let mainDescription = `┌${'─'.repeat(48)}┐\n`
|
||||
mainDescription += `│ ${' '.repeat(10)}📊 EXECUTION SUMMARY${' '.repeat(11)}│\n`
|
||||
mainDescription += `└${'─'.repeat(48)}┘\n\n`
|
||||
|
||||
// Global Overview
|
||||
mainDescription += '**🌐 GLOBAL STATISTICS**\n'
|
||||
mainDescription += `┌${'─'.repeat(48)}┐\n`
|
||||
mainDescription += `│ ⏱️ Duration: \`${durationText}\`${' '.repeat(48 - 14 - durationText.length)}│\n`
|
||||
mainDescription += `│ 💰 Total Earned: **${totalPoints}** points${' '.repeat(48 - 22 - String(totalPoints).length)}│\n`
|
||||
mainDescription += `│ 🖥️ Desktop: **${totalDesktop}** pts | 📱 Mobile: **${totalMobile}** pts${' '.repeat(48 - 28 - String(totalDesktop).length - String(totalMobile).length)}│\n`
|
||||
mainDescription += `│ ✅ Success: ${successCount}/${accounts.length} accounts${' '.repeat(48 - 18 - String(successCount).length - String(accounts.length).length)}│\n`
|
||||
mainDescription += `│ ❌ Failed: ${failureCount} accounts${' '.repeat(48 - 14 - String(failureCount).length)}│\n`
|
||||
mainDescription += `│ 🚫 Banned: ${bannedCount} accounts${' '.repeat(48 - 14 - String(bannedCount).length)}│\n`
|
||||
mainDescription += `└${'─'.repeat(48)}┘\n\n`
|
||||
|
||||
// Account Details (NO ERRORS - Clean Summary)
|
||||
mainDescription += '**📄 ACCOUNT BREAKDOWN**\n\n'
|
||||
|
||||
const accountsWithErrors = []
|
||||
|
||||
for (const account of accounts) {
|
||||
const status = account.banned ? '🚫' : (account.errors?.length ? '❌' : '✅')
|
||||
const emailShort = account.email.length > 30 ? account.email.substring(0, 27) + '...' : account.email
|
||||
const durationSec = Math.round(account.runDuration / 1000)
|
||||
|
||||
mainDescription += `${status} **${emailShort}**\n`
|
||||
mainDescription += `┌${'─'.repeat(46)}┐\n`
|
||||
|
||||
// Points Earned Breakdown
|
||||
mainDescription += `│ 📊 Points Earned: **+${account.pointsEarned}** points${' '.repeat(46 - 23 - String(account.pointsEarned).length)}│\n`
|
||||
mainDescription += `│ └─ Desktop: **${account.desktopPoints}** pts${' '.repeat(46 - 20 - String(account.desktopPoints).length)}│\n`
|
||||
mainDescription += `│ └─ Mobile: **${account.mobilePoints}** pts${' '.repeat(46 - 19 - String(account.mobilePoints).length)}│\n`
|
||||
mainDescription += `├${'─'.repeat(46)}┤\n`
|
||||
|
||||
// Account Total Balance (Formula: Initial + Earned = Final)
|
||||
mainDescription += `│ 💳 Account Total Balance${' '.repeat(23)}│\n`
|
||||
mainDescription += `│ \`${account.initialPoints}\` + \`${account.pointsEarned}\` = **\`${account.finalPoints}\` pts**${' '.repeat(46 - 17 - String(account.initialPoints).length - String(account.pointsEarned).length - String(account.finalPoints).length)}│\n`
|
||||
mainDescription += `│ (Initial + Earned = Final)${' '.repeat(18)}│\n`
|
||||
mainDescription += `├${'─'.repeat(46)}┤\n`
|
||||
|
||||
// Duration
|
||||
mainDescription += `│ ⏱️ Duration: ${durationSec}s${' '.repeat(46 - 13 - String(durationSec).length)}│\n`
|
||||
|
||||
mainDescription += `└${'─'.repeat(46)}┘\n\n`
|
||||
|
||||
// Collect accounts with errors for separate report
|
||||
if (account.errors?.length || account.banned) {
|
||||
accountsWithErrors.push(account)
|
||||
}
|
||||
}
|
||||
|
||||
// Footer Summary
|
||||
mainDescription += `┌${'─'.repeat(48)}┐\n`
|
||||
mainDescription += `│ 🌐 TOTAL ACROSS ALL ACCOUNTS${' '.repeat(22)}│\n`
|
||||
mainDescription += `├${'─'.repeat(48)}┤\n`
|
||||
mainDescription += `│ Initial Balance: \`${totalInitial}\` points${' '.repeat(48 - 25 - String(totalInitial).length)}│\n`
|
||||
mainDescription += `│ Final Balance: \`${totalFinal}\` points${' '.repeat(48 - 23 - String(totalFinal).length)}│\n`
|
||||
mainDescription += `│ Total Earned: **+${totalPoints}** points${' '.repeat(48 - 23 - String(totalPoints).length)}│\n`
|
||||
mainDescription += `└${'─'.repeat(48)}┘\n`
|
||||
|
||||
// ==================== ERROR REPORT WEBHOOK ====================
|
||||
let errorDescription = `┌${'─'.repeat(48)}┐\n`
|
||||
errorDescription += `│ ${' '.repeat(10)}⚠️ ERROR REPORT${' '.repeat(16)}│\n`
|
||||
errorDescription += `└${'─'.repeat(48)}┘\n\n`
|
||||
|
||||
errorDescription += `**${accountsWithErrors.length} account(s) encountered issues:**\n\n`
|
||||
|
||||
for (const account of accountsWithErrors) {
|
||||
const status = account.banned ? '🚫 BANNED' : '❌ ERROR'
|
||||
const emailShort = account.email.length > 35 ? account.email.substring(0, 32) + '...' : account.email
|
||||
|
||||
errorDescription += `${status} | **${emailShort}**\n`
|
||||
errorDescription += `┌${'─'.repeat(46)}┐\n`
|
||||
|
||||
// Show what was attempted
|
||||
errorDescription += `│ 📊 Progress${' '.repeat(35)}│\n`
|
||||
errorDescription += `│ Desktop: ${account.desktopPoints} pts earned${' '.repeat(46 - 21 - String(account.desktopPoints).length)}│\n`
|
||||
errorDescription += `│ Mobile: ${account.mobilePoints} pts earned${' '.repeat(46 - 20 - String(account.mobilePoints).length)}│\n`
|
||||
errorDescription += `│ Total: ${account.pointsEarned} pts${' '.repeat(46 - 13 - String(account.pointsEarned).length)}│\n`
|
||||
errorDescription += `├${'─'.repeat(46)}┤\n`
|
||||
|
||||
// Error details with word wrapping
|
||||
if (account.banned) {
|
||||
errorDescription += `│ 🚫 Status: Account Banned/Suspended${' '.repeat(9)}│\n`
|
||||
if (account.errors?.length && account.errors[0]) {
|
||||
errorDescription += `│ 💬 Reason:${' '.repeat(36)}│\n`
|
||||
const errorText = account.errors[0]
|
||||
const words = errorText.split(' ')
|
||||
let line = ''
|
||||
for (const word of words) {
|
||||
if ((line + word).length > 42) {
|
||||
errorDescription += `│ ${line.trim()}${' '.repeat(46 - 3 - line.trim().length)}│\n`
|
||||
line = word + ' '
|
||||
} else {
|
||||
line += word + ' '
|
||||
}
|
||||
}
|
||||
if (line.trim()) {
|
||||
errorDescription += `│ ${line.trim()}${' '.repeat(46 - 3 - line.trim().length)}│\n`
|
||||
}
|
||||
}
|
||||
} else if (account.errors?.length && account.errors[0]) {
|
||||
errorDescription += `│ ❌ Error Details:${' '.repeat(29)}│\n`
|
||||
const errorText = account.errors[0]
|
||||
const words = errorText.split(' ')
|
||||
let line = ''
|
||||
for (const word of words) {
|
||||
if ((line + word).length > 42) {
|
||||
errorDescription += `│ ${line.trim()}${' '.repeat(46 - 3 - line.trim().length)}│\n`
|
||||
line = word + ' '
|
||||
} else {
|
||||
line += word + ' '
|
||||
}
|
||||
}
|
||||
if (line.trim()) {
|
||||
errorDescription += `│ ${line.trim()}${' '.repeat(46 - 3 - line.trim().length)}│\n`
|
||||
}
|
||||
}
|
||||
|
||||
errorDescription += `└${'─'.repeat(46)}┘\n\n`
|
||||
}
|
||||
|
||||
errorDescription += '**📋 Recommended Actions:**\n'
|
||||
errorDescription += '• Check account status manually\n'
|
||||
errorDescription += '• Review error messages above\n'
|
||||
errorDescription += '• Verify credentials if login failed\n'
|
||||
errorDescription += '• Consider proxy rotation if rate-limited\n'
|
||||
|
||||
// ==================== DISPLAY PREVIEW ====================
|
||||
console.log('\n' + '='.repeat(70))
|
||||
console.log('📊 WEBHOOK PREVIEW - IMPROVED FORMAT')
|
||||
console.log('='.repeat(70))
|
||||
|
||||
console.log('\n✅ WEBHOOK #1 - MAIN SUMMARY (Clean, No Errors)')
|
||||
console.log('─'.repeat(70))
|
||||
console.log('🎯 Title: 🎉 Daily Rewards Collection Complete')
|
||||
console.log('🎨 Color: Green (all success) / Orange (partial failures) / Red (bans detected)')
|
||||
console.log('\n📝 Description:')
|
||||
console.log(mainDescription)
|
||||
|
||||
console.log('='.repeat(70))
|
||||
console.log('\n⚠️ WEBHOOK #2 - ERROR REPORT (Separate, Only if Errors Exist)')
|
||||
console.log('─'.repeat(70))
|
||||
console.log('🎯 Title: ⚠️ Execution Errors & Warnings')
|
||||
console.log('🎨 Color: Red (always)')
|
||||
console.log('\n📝 Description:')
|
||||
console.log(errorDescription)
|
||||
|
||||
console.log('='.repeat(70))
|
||||
console.log('\n✅ KEY IMPROVEMENTS IMPLEMENTED:')
|
||||
console.log(' ✓ Errors moved to separate webhook (main summary stays clean)')
|
||||
console.log(' ✓ Account total shown as formula: `Initial + Earned = Final`')
|
||||
console.log(' ✓ Complete per-account breakdown: Desktop + Mobile points')
|
||||
console.log(' ✓ Global totals: Initial balance, Final balance, Total earned')
|
||||
console.log(' ✓ Individual account totals clearly displayed')
|
||||
console.log(' ✓ Error details with automatic word wrapping')
|
||||
console.log(' ✓ Professional box structure throughout')
|
||||
console.log(' ✓ Recommended actions in error report')
|
||||
console.log(' ✓ Status indicators: ✅ Success, ❌ Error, 🚫 Banned\n')
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user