// Real React UI, mocked Tencent SDK/API: no SMS or chargeable verification calls. import assert from 'node:assert/strict'; import { createRequire } from 'node:module'; import { resolve } from 'node:path'; import { pathToFileURL } from 'node:url'; import { mkdir } from 'node:fs/promises'; const require = createRequire(process.env.PLAYWRIGHT_REQUIRE_FROM ?? import.meta.url); const { chromium } = require('playwright'); const { createServer } = await import(pathToFileURL(resolve('web/node_modules/vite/dist/node/index.js'))); const server = await createServer({ configFile: false, root: resolve('web'), oxc: { jsx: { runtime: 'automatic' } }, logLevel: 'error', server: { host: '127.0.0.1', port: 5325, strictPort: true }, plugins: [{ name: 'captcha-test-harness', resolveId(id) { if (id === '/captcha-harness.jsx') return id; }, load(id) { if (id === '/captcha-harness.jsx') return `import '/src/styles.css';import '/src/maintenance-console.css';import React from 'react';import {createRoot} from 'react-dom/client';import {CaptchaPolicySettings} from '/src/components/CaptchaPolicySettings.tsx';createRoot(document.getElementById('root')).render();`; } }] }); await server.listen(); let browser, currentPage; const errors = [], checks = []; try { browser = await chromium.launch({ headless: true, executablePath: process.env.CHROME_PATH ?? 'C:/Program Files/Google/Chrome/Application/chrome.exe' }); const page = await browser.newPage({ viewport: { width: 1280, height: 900 } }); currentPage = page; page.on('pageerror', e => errors.push(e.message)); await page.route('https://turing.captcha.qcloud.com/TJCaptcha.js', r => r.fulfill({ contentType: 'text/javascript', body: `window.TencentCaptcha=class{constructor(id,cb){window.lastCaptchaApp=id;this.cb=cb;}show(){setTimeout(()=>this.cb(window.captchaResult||{ret:0,ticket:'web-ticket',randstr:'random'}),0)}destroy(){}}` })); let generation = 0, sends = 0, passwords = 0, mode = 'tencent'; let expireNextCaptcha = true; let policy = { saas_mode: 'image', mini_mode: 'image', version: 1, saas_configured: true, mini_configured: false }, writes = 0; await page.route('**/api/v1/**', async route => { const req = route.request(), path = new URL(req.url()).pathname, body = req.method() === 'POST' ? req.postDataJSON() : undefined; let status = 200, code = 'OK', message = '', data = {}; if (path.endsWith('/account/session')) { status = 401; code = 'LOGIN_REQUIRED'; } else if (path.endsWith('/login/session')) data = { sms_enabled: true, csrf_token: 'login-csrf', password_enabled: true }; else if (path.endsWith('/login/captcha')) { data = { captcha_id: 'captcha-' + ++generation, mode, captcha_app_id: mode === 'tencent' ? '123' : undefined, image: mode === 'image' ? 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+jRZkAAAAASUVORK5CYII=' : undefined, expires_at_ms: Date.now() + (expireNextCaptcha ? -1000 : 120000) }; expireNextCaptcha = false; } else if (path.endsWith('/sms/send')) { assert.equal(body.captcha_ticket, 'web-ticket'); assert.equal(body.captcha_randstr, 'random'); assert.equal(body.captcha_code, undefined); assert.equal(body.captcha_id, 'captcha-' + generation); sends++; data = { sms_challenge_id: 'sms', expires_at_ms: Date.now() + 300000, resend_after_ms: 60000 }; } else if (path.endsWith('/login/password')) { assert.equal(body.captcha_ticket, 'web-ticket'); assert.equal(body.captcha_randstr, 'random'); passwords++; status = 401; code = 'PASSWORD_INVALID'; message = '手机号或密码不正确'; } else if (path === '/api/v1/ops/login-captcha-policy') { assert.equal(req.headers()['x-tenant-id'], 'test-tenant'); assert.equal(req.headers().authorization, 'Bearer test-token'); if (body) { assert.equal(body.version, policy.version); assert.equal(body.saas_mode, 'tencent'); assert.equal(body.mini_mode, 'image'); writes++; policy = { ...policy, ...body, version: policy.version + 1 }; } data = policy; } else throw new Error('Unexpected API ' + path); await route.fulfill({ status, contentType: 'application/json', body: JSON.stringify({ ok: status === 200, code, message, data }) }); }); await page.goto('http://127.0.0.1:5325/'); await page.getByLabel('手机号', { exact: true }).waitFor(); assert.equal(await page.locator('#customer-image-code').count(), 0); await page.getByLabel('手机号', { exact: true }).fill('13800000000'); assert.equal(await page.getByText('安全验证', { exact: true }).count(), 0); assert.equal(await page.getByRole('button', { name: '刷新验证', exact: true }).count(), 0); assert.equal(await page.evaluate(() => !!window.TencentCaptcha), false); const beforeExpiredRetry = generation; await page.evaluate(() => { window.captchaResult = { ret: 2 }; }); await page.getByRole('button', { name: '获取验证码', exact: true }).click(); await page.getByText('已取消验证', { exact: true }).waitFor(); assert.equal(sends, 0); assert(generation > beforeExpiredRetry, 'Expired hidden challenge is renewed when send is clicked'); checks.push('no security panel or SDK on initial render; clicking send renews an expired challenge and opens Tencent verification'); await page.evaluate(() => { window.captchaResult = { ret: 0, ticket: 'trerror_123', randstr: 'random' }; }); await page.getByRole('button', { name: '获取验证码', exact: true }).click(); await page.getByText('人机验证未通过,请重试', { exact: true }).waitFor(); assert.equal(sends, 0); await page.evaluate(() => { window.captchaResult = undefined; }); await page.getByRole('button', { name: '获取验证码', exact: true }).click(); await page.getByText(/验证码已发送/).waitFor(); assert.equal(sends, 1); assert.equal(await page.evaluate(() => window.lastCaptchaApp), '123'); checks.push('SaaS SDK ticket submitted only after successful verification; cancel and disaster tickets cannot send SMS'); await page.getByRole('button', { name: '密码登录', exact: true }).click(); await page.getByLabel('密码', { exact: true }).fill('WrongPassword123!'); await page.locator('.sms-login-form').getByRole('button', { name: /^登\s*录$/ }).click(); await page.getByText('手机号或密码不正确', { exact: true }).waitFor(); assert.equal(passwords, 1); checks.push('password login uses Tencent proof and refreshes after failure'); assert.equal(await page.getByRole('button', { name: '忘记密码', exact: true }).count(), 1); mode = 'image'; await page.reload(); await page.getByLabel('图片验证码', { exact: true }).waitFor(); checks.push('server policy switch returns numeric image UI without changing the client'); await page.route('**/__captcha-settings', r => r.fulfill({ contentType: 'text/html', body: '
' })); await page.goto('http://127.0.0.1:5325/__captcha-settings'); await page.getByText('登录与短信安全验证', { exact: true }).waitFor(); const web = page.getByRole('radiogroup', { name: 'SaaS / 管理端验证码方式' }), mini = page.getByRole('radiogroup', { name: '微信小程序验证码方式' }); assert(await mini.getByRole('radio', { name: /^腾讯云验证码/ }).isDisabled()); await web.getByRole('radio', { name: /^腾讯云验证码/ }).check(); await page.getByRole('button', { name: '保存验证码设置' }).click(); await page.getByText(/已保存。新验证立即生效/).waitFor(); assert.equal(writes, 1); await page.reload(); await page.getByText('登录与短信安全验证', { exact: true }).waitFor(); assert(await web.getByRole('radio', { name: /^腾讯云验证码/ }).isChecked()); await mkdir(resolve('.design-evidence/tencent-captcha'), { recursive: true }); await page.screenshot({ path: resolve('.design-evidence/tencent-captcha/settings.png'), fullPage: true }); await page.setViewportSize({ width: 375, height: 850 }); assert(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth + 1)); checks.push('admin settings keep clients independent, disable unconfigured Tencent mode, save and reload; mobile layout has no horizontal overflow'); assert.deepEqual(errors, []); console.log(JSON.stringify({ checks, errors })); } catch (error) { console.error((await currentPage?.locator('body').innerText())?.slice(0,2000)); console.error(await currentPage?.locator('button').evaluateAll(items=>items.map(x=>({text:x.textContent,html:x.outerHTML.slice(0,800)})))); console.error(await currentPage?.evaluate(()=>({sdk:window.TencentCaptcha?.toString().slice(0,150),result:window.captchaResult,body:document.body.getAttribute('aria-hidden')}))); console.error(errors); throw error; } finally { await browser?.close(); await server.close(); }