拆分腾讯会话页面交互辅助逻辑
This commit is contained in:
@@ -0,0 +1,188 @@
|
|||||||
|
// @ts-nocheck
|
||||||
|
|
||||||
|
import fs from 'node:fs/promises'
|
||||||
|
import path from 'node:path'
|
||||||
|
|
||||||
|
import { isRetryableQrCaptureError, logBrowserSessionDebug } from './session-login-shared.js'
|
||||||
|
import { captureQqQrImage, ensureQqLoginReady, extractQqQrState } from './session-qq.js'
|
||||||
|
import { captureWxQrImage, ensureWxLoginReady, extractWxQrState } from './session-wx.js'
|
||||||
|
|
||||||
|
export async function captureSessionQr(
|
||||||
|
session,
|
||||||
|
{
|
||||||
|
ensureLoginReady = true,
|
||||||
|
ensureLoginTab: ensureLoginTabImpl = ensureLoginTab,
|
||||||
|
captureQrImageWithRetry: captureQrImageWithRetryImpl = captureQrImageWithRetry,
|
||||||
|
readFile = fs.readFile,
|
||||||
|
now = () => new Date().toISOString(),
|
||||||
|
} = {},
|
||||||
|
) {
|
||||||
|
if (ensureLoginReady) {
|
||||||
|
await ensureLoginTabImpl(session.page, session.loginType)
|
||||||
|
}
|
||||||
|
|
||||||
|
const qrImagePath = path.join(session.sessionDir, `${session.loginType}-qr.png`)
|
||||||
|
await captureQrImageWithRetryImpl(session.page, session.loginType, qrImagePath, {
|
||||||
|
ensureLoginTab: ensureLoginTabImpl,
|
||||||
|
})
|
||||||
|
|
||||||
|
session.qrImagePath = qrImagePath
|
||||||
|
session.qrImageBase64 = await readFile(qrImagePath, 'base64')
|
||||||
|
session.qrUpdatedAt = now()
|
||||||
|
session.updatedAt = session.qrUpdatedAt
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function captureQrImageWithRetry(
|
||||||
|
page,
|
||||||
|
loginType,
|
||||||
|
qrImagePath,
|
||||||
|
{
|
||||||
|
captureQqQrImage: captureQqQrImageImpl = captureQqQrImage,
|
||||||
|
captureWxQrImage: captureWxQrImageImpl = captureWxQrImage,
|
||||||
|
ensureLoginTab: ensureLoginTabImpl = ensureLoginTab,
|
||||||
|
isRetryableQrCaptureError: isRetryableQrCaptureErrorImpl = isRetryableQrCaptureError,
|
||||||
|
logBrowserSessionDebug: logBrowserSessionDebugImpl = logBrowserSessionDebug,
|
||||||
|
} = {},
|
||||||
|
) {
|
||||||
|
const maxAttempts = loginType === 'qq' ? 4 : 3
|
||||||
|
let lastError = null
|
||||||
|
|
||||||
|
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
||||||
|
try {
|
||||||
|
logBrowserSessionDebugImpl('captureQrImageWithRetry.start', {
|
||||||
|
loginType,
|
||||||
|
attempt,
|
||||||
|
maxAttempts,
|
||||||
|
qrImagePath,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (loginType === 'qq') {
|
||||||
|
await captureQqQrImageImpl(page, qrImagePath)
|
||||||
|
} else {
|
||||||
|
await captureWxQrImageImpl(page, qrImagePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
logBrowserSessionDebugImpl('captureQrImageWithRetry.success', {
|
||||||
|
loginType,
|
||||||
|
attempt,
|
||||||
|
qrImagePath,
|
||||||
|
})
|
||||||
|
|
||||||
|
return
|
||||||
|
} catch (error) {
|
||||||
|
lastError = error
|
||||||
|
logBrowserSessionDebugImpl('captureQrImageWithRetry.error', {
|
||||||
|
loginType,
|
||||||
|
attempt,
|
||||||
|
message: error instanceof Error ? error.message : String(error),
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!isRetryableQrCaptureErrorImpl(error) || attempt === maxAttempts) {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
|
await page.waitForTimeout(500)
|
||||||
|
await ensureLoginTabImpl(page, loginType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lastError) {
|
||||||
|
throw lastError
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error('二维码截图失败')
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function ensureLoginTab(
|
||||||
|
page,
|
||||||
|
loginType,
|
||||||
|
{
|
||||||
|
ensureQqLoginReady: ensureQqLoginReadyImpl = ensureQqLoginReady,
|
||||||
|
ensureWxLoginReady: ensureWxLoginReadyImpl = ensureWxLoginReady,
|
||||||
|
} = {},
|
||||||
|
) {
|
||||||
|
const tabSelector = loginType === 'wx' ? '.wx-tab' : '.qc-tab'
|
||||||
|
const loginButton = page.locator('#unlogin')
|
||||||
|
let interacted = false
|
||||||
|
|
||||||
|
if (await loginButton.count()) {
|
||||||
|
try {
|
||||||
|
await loginButton.first().click({ timeout: 2_000 })
|
||||||
|
interacted = true
|
||||||
|
} catch {
|
||||||
|
// ignore when the login layer is already open
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const tab = page.locator(tabSelector).first()
|
||||||
|
|
||||||
|
if (await tab.count()) {
|
||||||
|
try {
|
||||||
|
await tab.click({ timeout: 2_000 })
|
||||||
|
interacted = true
|
||||||
|
} catch {
|
||||||
|
// ignore tab click failures
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (interacted) {
|
||||||
|
await page.waitForTimeout(250)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loginType === 'qq') {
|
||||||
|
await ensureQqLoginReadyImpl(page)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loginType === 'wx') {
|
||||||
|
await ensureWxLoginReadyImpl(page)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function extractHostState(page) {
|
||||||
|
return page.evaluate(() => {
|
||||||
|
const isVisible = (element) => {
|
||||||
|
if (!element) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const style = window.getComputedStyle(element)
|
||||||
|
|
||||||
|
return style.display !== 'none' && style.visibility !== 'hidden' && style.opacity !== '0'
|
||||||
|
}
|
||||||
|
|
||||||
|
const unlogin = document.querySelector('#unlogin')
|
||||||
|
const logined = document.querySelector('#logined')
|
||||||
|
|
||||||
|
return {
|
||||||
|
unloginVisible: isVisible(unlogin),
|
||||||
|
loginedVisible: isVisible(logined),
|
||||||
|
loginText: String(logined?.textContent || '').trim(),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function reloadActivityPageForPresentation(page, activityUrl) {
|
||||||
|
try {
|
||||||
|
await page.reload({ waitUntil: 'domcontentloaded' })
|
||||||
|
} catch {
|
||||||
|
await page.goto(activityUrl, { waitUntil: 'domcontentloaded' })
|
||||||
|
}
|
||||||
|
|
||||||
|
await page.waitForTimeout(2_500)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function extractQrState(
|
||||||
|
page,
|
||||||
|
loginType,
|
||||||
|
{
|
||||||
|
extractQqQrState: extractQqQrStateImpl = extractQqQrState,
|
||||||
|
extractWxQrState: extractWxQrStateImpl = extractWxQrState,
|
||||||
|
} = {},
|
||||||
|
) {
|
||||||
|
if (loginType === 'qq') {
|
||||||
|
return extractQqQrStateImpl(page)
|
||||||
|
}
|
||||||
|
|
||||||
|
return extractWxQrStateImpl(page)
|
||||||
|
}
|
||||||
@@ -15,10 +15,7 @@ import {
|
|||||||
resolveBrowserLaunchOptions as resolveBrowserLaunchOptionsBase,
|
resolveBrowserLaunchOptions as resolveBrowserLaunchOptionsBase,
|
||||||
resolveDefaultViewport,
|
resolveDefaultViewport,
|
||||||
} from './session-browser-config.js'
|
} from './session-browser-config.js'
|
||||||
import {
|
import { logBrowserSessionDebug } from './session-login-shared.js'
|
||||||
isRetryableQrCaptureError,
|
|
||||||
logBrowserSessionDebug,
|
|
||||||
} from './session-login-shared.js'
|
|
||||||
import {
|
import {
|
||||||
loadCookieMapFromContext,
|
loadCookieMapFromContext,
|
||||||
} from './session-cookie-map.js'
|
} from './session-cookie-map.js'
|
||||||
@@ -26,10 +23,12 @@ import {
|
|||||||
buildSessionPayload as buildSessionPayloadBase,
|
buildSessionPayload as buildSessionPayloadBase,
|
||||||
} from './session-payload.js'
|
} from './session-payload.js'
|
||||||
import {
|
import {
|
||||||
captureQqQrImage,
|
captureSessionQr,
|
||||||
ensureQqLoginReady,
|
ensureLoginTab,
|
||||||
extractQqQrState,
|
extractHostState,
|
||||||
} from './session-qq.js'
|
extractQrState,
|
||||||
|
reloadActivityPageForPresentation,
|
||||||
|
} from './session-page.js'
|
||||||
import {
|
import {
|
||||||
saveRedeemArtifacts,
|
saveRedeemArtifacts,
|
||||||
} from './session-proof.js'
|
} from './session-proof.js'
|
||||||
@@ -45,9 +44,7 @@ import {
|
|||||||
submitRedeemInBrowser,
|
submitRedeemInBrowser,
|
||||||
} from './session-redeem.js'
|
} from './session-redeem.js'
|
||||||
import {
|
import {
|
||||||
captureWxQrImage,
|
|
||||||
ensureWxLoginReady,
|
ensureWxLoginReady,
|
||||||
extractWxQrState,
|
|
||||||
} from './session-wx.js'
|
} from './session-wx.js'
|
||||||
import { recognizeTencentCaptcha } from './ocr.js'
|
import { recognizeTencentCaptcha } from './ocr.js'
|
||||||
import {
|
import {
|
||||||
@@ -191,7 +188,7 @@ export async function reloadTencentBrowserSession(sessionId) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await reloadActivityPageForPresentation(session.page)
|
await reloadActivityPageForPresentation(session.page, ACTIVITY_URL)
|
||||||
|
|
||||||
const cookieMap = await loadCookieMapFromContext(session.browserContext)
|
const cookieMap = await loadCookieMapFromContext(session.browserContext)
|
||||||
const credentialReady = hasRedeemCredential(cookieMap)
|
const credentialReady = hasRedeemCredential(cookieMap)
|
||||||
@@ -278,7 +275,7 @@ export async function refreshTencentBrowserSession(sessionId, { includeQrImage =
|
|||||||
|
|
||||||
if (!session.qrImageBase64 || session.status === 'expired') {
|
if (!session.qrImageBase64 || session.status === 'expired') {
|
||||||
if (session.status === 'expired') {
|
if (session.status === 'expired') {
|
||||||
await reloadActivityPageForPresentation(session.page)
|
await reloadActivityPageForPresentation(session.page, ACTIVITY_URL)
|
||||||
await ensureLoginTab(session.page, session.loginType)
|
await ensureLoginTab(session.page, session.loginType)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -462,109 +459,6 @@ async function getRequiredSession(sessionId) {
|
|||||||
return session
|
return session
|
||||||
}
|
}
|
||||||
|
|
||||||
async function captureSessionQr(session, { ensureLoginReady = true } = {}) {
|
|
||||||
if (ensureLoginReady) {
|
|
||||||
await ensureLoginTab(session.page, session.loginType)
|
|
||||||
}
|
|
||||||
|
|
||||||
const qrImagePath = path.join(session.sessionDir, `${session.loginType}-qr.png`)
|
|
||||||
await captureQrImageWithRetry(session.page, session.loginType, qrImagePath)
|
|
||||||
|
|
||||||
session.qrImagePath = qrImagePath
|
|
||||||
session.qrImageBase64 = await fs.readFile(qrImagePath, 'base64')
|
|
||||||
session.qrUpdatedAt = new Date().toISOString()
|
|
||||||
session.updatedAt = new Date().toISOString()
|
|
||||||
}
|
|
||||||
|
|
||||||
async function captureQrImageWithRetry(page, loginType, qrImagePath) {
|
|
||||||
const maxAttempts = loginType === 'qq' ? 4 : 3
|
|
||||||
let lastError = null
|
|
||||||
|
|
||||||
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
|
||||||
try {
|
|
||||||
logBrowserSessionDebug('captureQrImageWithRetry.start', {
|
|
||||||
loginType,
|
|
||||||
attempt,
|
|
||||||
maxAttempts,
|
|
||||||
qrImagePath,
|
|
||||||
})
|
|
||||||
|
|
||||||
if (loginType === 'qq') {
|
|
||||||
await captureQqQrImage(page, qrImagePath)
|
|
||||||
} else {
|
|
||||||
await captureWxQrImage(page, qrImagePath)
|
|
||||||
}
|
|
||||||
|
|
||||||
logBrowserSessionDebug('captureQrImageWithRetry.success', {
|
|
||||||
loginType,
|
|
||||||
attempt,
|
|
||||||
qrImagePath,
|
|
||||||
})
|
|
||||||
|
|
||||||
return
|
|
||||||
} catch (error) {
|
|
||||||
lastError = error
|
|
||||||
logBrowserSessionDebug('captureQrImageWithRetry.error', {
|
|
||||||
loginType,
|
|
||||||
attempt,
|
|
||||||
message: error instanceof Error ? error.message : String(error),
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!isRetryableQrCaptureError(error) || attempt === maxAttempts) {
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
|
|
||||||
await page.waitForTimeout(500)
|
|
||||||
await ensureLoginTab(page, loginType)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (lastError) {
|
|
||||||
throw lastError
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error('二维码截图失败')
|
|
||||||
}
|
|
||||||
|
|
||||||
async function ensureLoginTab(page, loginType) {
|
|
||||||
const tabSelector = loginType === 'wx' ? '.wx-tab' : '.qc-tab'
|
|
||||||
const loginButton = page.locator('#unlogin')
|
|
||||||
let interacted = false
|
|
||||||
|
|
||||||
if (await loginButton.count()) {
|
|
||||||
try {
|
|
||||||
await loginButton.first().click({ timeout: 2_000 })
|
|
||||||
interacted = true
|
|
||||||
} catch {
|
|
||||||
// ignore when the login layer is already open
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const tab = page.locator(tabSelector).first()
|
|
||||||
|
|
||||||
if (await tab.count()) {
|
|
||||||
try {
|
|
||||||
await tab.click({ timeout: 2_000 })
|
|
||||||
interacted = true
|
|
||||||
} catch {
|
|
||||||
// ignore tab click failures
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (interacted) {
|
|
||||||
await page.waitForTimeout(250)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (loginType === 'qq') {
|
|
||||||
await ensureQqLoginReady(page)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (loginType === 'wx') {
|
|
||||||
await ensureWxLoginReady(page)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function applyInitialQrSessionState(session) {
|
function applyInitialQrSessionState(session) {
|
||||||
resetTencentSessionAutoClose(session)
|
resetTencentSessionAutoClose(session)
|
||||||
Object.assign(session, resolveInitialQrSessionState(session.loginType))
|
Object.assign(session, resolveInitialQrSessionState(session.loginType))
|
||||||
@@ -594,29 +488,6 @@ function resetTencentSessionAutoClose(session) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function extractHostState(page) {
|
|
||||||
return page.evaluate(() => {
|
|
||||||
const isVisible = (element) => {
|
|
||||||
if (!element) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
const style = window.getComputedStyle(element)
|
|
||||||
|
|
||||||
return style.display !== 'none' && style.visibility !== 'hidden' && style.opacity !== '0'
|
|
||||||
}
|
|
||||||
|
|
||||||
const unlogin = document.querySelector('#unlogin')
|
|
||||||
const logined = document.querySelector('#logined')
|
|
||||||
|
|
||||||
return {
|
|
||||||
unloginVisible: isVisible(unlogin),
|
|
||||||
loginedVisible: isVisible(logined),
|
|
||||||
loginText: String(logined?.textContent || '').trim(),
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
async function ensureLoggedInPresentation(
|
async function ensureLoggedInPresentation(
|
||||||
session,
|
session,
|
||||||
{ hostState = null, credentialReady = false } = {},
|
{ hostState = null, credentialReady = false } = {},
|
||||||
@@ -638,7 +509,7 @@ async function ensureLoggedInPresentation(
|
|||||||
}
|
}
|
||||||
|
|
||||||
session.presentationSyncAttempts += 1
|
session.presentationSyncAttempts += 1
|
||||||
await reloadActivityPageForPresentation(session.page)
|
await reloadActivityPageForPresentation(session.page, ACTIVITY_URL)
|
||||||
nextHostState = await extractHostState(session.page)
|
nextHostState = await extractHostState(session.page)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -647,24 +518,6 @@ async function ensureLoggedInPresentation(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function reloadActivityPageForPresentation(page) {
|
|
||||||
try {
|
|
||||||
await page.reload({ waitUntil: 'domcontentloaded' })
|
|
||||||
} catch {
|
|
||||||
await page.goto(ACTIVITY_URL, { waitUntil: 'domcontentloaded' })
|
|
||||||
}
|
|
||||||
|
|
||||||
await page.waitForTimeout(2_500)
|
|
||||||
}
|
|
||||||
|
|
||||||
async function extractQrState(page, loginType) {
|
|
||||||
if (loginType === 'qq') {
|
|
||||||
return extractQqQrState(page)
|
|
||||||
}
|
|
||||||
|
|
||||||
return extractWxQrState(page)
|
|
||||||
}
|
|
||||||
|
|
||||||
async function ensureActivityInfoReady(page, { triggerRender = false, timeoutMs = 8_000 } = {}) {
|
async function ensureActivityInfoReady(page, { triggerRender = false, timeoutMs = 8_000 } = {}) {
|
||||||
if (triggerRender) {
|
if (triggerRender) {
|
||||||
await triggerActivityRoleRender(page)
|
await triggerActivityRoleRender(page)
|
||||||
|
|||||||
@@ -6,6 +6,10 @@ import {
|
|||||||
parseViewportSpec,
|
parseViewportSpec,
|
||||||
} from './session-browser-config.js'
|
} from './session-browser-config.js'
|
||||||
import { normalizeTencentCookieMap } from './session-cookie-map.js'
|
import { normalizeTencentCookieMap } from './session-cookie-map.js'
|
||||||
|
import {
|
||||||
|
captureQrImageWithRetry,
|
||||||
|
ensureLoginTab,
|
||||||
|
} from './session-page.js'
|
||||||
import {
|
import {
|
||||||
buildArtifactsPayload,
|
buildArtifactsPayload,
|
||||||
buildRedeemPayload,
|
buildRedeemPayload,
|
||||||
@@ -206,3 +210,86 @@ test('ensureReviewScreenshot writes screenshot state and skips unchanged signatu
|
|||||||
assert.equal(await ensureReviewScreenshot(session, activityInfo), false)
|
assert.equal(await ensureReviewScreenshot(session, activityInfo), false)
|
||||||
assert.equal(screenshotCalls.length, 1)
|
assert.equal(screenshotCalls.length, 1)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('ensureLoginTab clicks login UI and delegates to qq readiness', async () => {
|
||||||
|
const calls = []
|
||||||
|
const makeLocator = (name) => ({
|
||||||
|
async count() {
|
||||||
|
return 1
|
||||||
|
},
|
||||||
|
first() {
|
||||||
|
const firstLocator = {
|
||||||
|
async count() {
|
||||||
|
return 1
|
||||||
|
},
|
||||||
|
async click(options) {
|
||||||
|
calls.push({ type: 'click', name, options })
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return firstLocator
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const page = {
|
||||||
|
locator(selector) {
|
||||||
|
if (selector === '#unlogin') {
|
||||||
|
return makeLocator('loginButton')
|
||||||
|
}
|
||||||
|
if (selector === '.qc-tab') {
|
||||||
|
return makeLocator('qqTab')
|
||||||
|
}
|
||||||
|
throw new Error(`unexpected selector: ${selector}`)
|
||||||
|
},
|
||||||
|
async waitForTimeout(ms) {
|
||||||
|
calls.push({ type: 'wait', ms })
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
await ensureLoginTab(page, 'qq', {
|
||||||
|
async ensureQqLoginReady() {
|
||||||
|
calls.push({ type: 'ensure', name: 'qq' })
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.deepEqual(calls, [
|
||||||
|
{ type: 'click', name: 'loginButton', options: { timeout: 2_000 } },
|
||||||
|
{ type: 'click', name: 'qqTab', options: { timeout: 2_000 } },
|
||||||
|
{ type: 'wait', ms: 250 },
|
||||||
|
{ type: 'ensure', name: 'qq' },
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('captureQrImageWithRetry retries once after retryable error', async () => {
|
||||||
|
const calls = []
|
||||||
|
const page = {
|
||||||
|
async waitForTimeout(ms) {
|
||||||
|
calls.push({ type: 'wait', ms })
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
let attempt = 0
|
||||||
|
await captureQrImageWithRetry(page, 'wx', '/tmp/wx-qr.png', {
|
||||||
|
async captureWxQrImage() {
|
||||||
|
attempt += 1
|
||||||
|
calls.push({ type: 'capture', attempt })
|
||||||
|
if (attempt === 1) {
|
||||||
|
throw new Error('Timeout while waiting for qr')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async ensureLoginTab(_page, loginType) {
|
||||||
|
calls.push({ type: 'ensureLoginTab', loginType })
|
||||||
|
},
|
||||||
|
isRetryableQrCaptureError(error) {
|
||||||
|
return String(error.message).includes('Timeout')
|
||||||
|
},
|
||||||
|
logBrowserSessionDebug() {},
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.equal(attempt, 2)
|
||||||
|
assert.deepEqual(calls, [
|
||||||
|
{ type: 'capture', attempt: 1 },
|
||||||
|
{ type: 'wait', ms: 500 },
|
||||||
|
{ type: 'ensureLoginTab', loginType: 'wx' },
|
||||||
|
{ type: 'capture', attempt: 2 },
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user