Files
ledgerly-docs/scripts/ts/browser.ts
T
rosercode d2e4a51f2b feat: 补充图片记账、交易渠道管理与前端偏好文档
- 新增「交易渠道管理」指南与「前端用户偏好」开发文档,并加入侧边栏导航
- 图片记账:补充多图上传、实时识别进度、结果勾选导入与「来源图片」回看,AI 配置新增视觉模型说明
- 数据模型补充 t_channel、t_ai_config/t_ai_image 等表与「类型+金额+时间」去重键说明,表数更新为 21 张
- API 文档补充 /api/channel、/api/transaction/batch、/api/ai/record/image(/stream)、/api/ai/image、/api/ai/channel 等接口
- 截图脚本新增渠道页、图片记账、来源图片与渠道 AI 辅助截图,并放宽导航超时
2026-09-21 22:19:55 +08:00

115 lines
3.6 KiB
TypeScript

// 浏览器自动化公共模块(Playwright + Chromium)与 HTTP 公共方法(axios)。
import axios from 'axios'
import { chromium, type Browser, type BrowserContext, type Page } from 'playwright-core'
import { API_URL, BASE_URL, CHROMIUM_BIN, PASSWORD, USERNAME } from './config.js'
/** 默认视口尺寸 */
export const DEFAULT_VIEWPORT = { width: 1440, height: 1000 }
/**
* 启动 headless Chromium(复用 Playwright 缓存的浏览器)。
*
* @returns Browser 实例
*/
export async function launchBrowser(): Promise<Browser> {
return chromium.launch({
executablePath: CHROMIUM_BIN,
headless: true,
args: ['--no-sandbox', '--disable-dev-shm-usage'],
})
}
/**
* 创建浏览器上下文。
*
* @param browser Browser 实例
* @param viewport 视口尺寸
* @returns BrowserContext 实例
*/
export async function newContext(
browser: Browser,
viewport: { width: number; height: number } = DEFAULT_VIEWPORT,
): Promise<BrowserContext> {
const context = await browser.newContext({ viewport })
// 首次访问需等待 Vite 按需编译,放宽默认导航超时,避免冷启动误判失败
context.setDefaultNavigationTimeout(120000)
return context
}
/**
* 在上下文中注入登录态与中文语言偏好(对之后打开的页面生效)。
*
* @param context BrowserContext 实例
* @param token 访问令牌
*/
export async function injectSession(context: BrowserContext, token: string): Promise<void> {
await context.addInitScript(
([t]) => {
localStorage.setItem('token', t as string)
localStorage.setItem('locale', 'zh-CN')
},
[token],
)
}
/**
* 等待页面渲染完成:先等文档就绪,再等关键元素可见,最后做短暂稳定等待。
*
* @param page Page 实例
* @param options.selector 关键元素选择器(可选)
* @param options.settle 元素出现后的稳定等待毫秒数,默认 600ms
*/
export async function waitPage(
page: Page,
options: { selector?: string; settle?: number } = {},
): Promise<void> {
await page.waitForLoadState('domcontentloaded').catch(() => undefined)
if (options.selector) {
await page
.locator(options.selector)
.first()
.waitFor({ state: 'visible', timeout: 20000 })
.catch(() => undefined)
}
// 等待异步数据请求基本完成后再截图,避免表格/图表空白;
// SSE 页面(运行日志)不会进入空闲,超时忽略即可。
await page.waitForLoadState('networkidle', { timeout: 8000 }).catch(() => undefined)
await page.waitForTimeout(options.settle ?? 900)
}
/**
* 按可见文本点击按钮。
*
* @param page Page 实例
* @param text 按钮文本
* @returns 是否点击成功
*/
export async function clickText(page: Page, text: string): Promise<boolean> {
const button = page.locator('button', { hasText: text }).first()
try {
// 按钮可能随异步配置(如 AI 启用状态)延迟渲染,等待其出现
await button.waitFor({ state: 'visible', timeout: 15000 })
} catch {
return false
}
// 部分 Naive UI 按钮对 Playwright 合成鼠标事件不响应,改用 DOM 原生 click
await button.evaluate((el) => (el as HTMLElement).click())
return true
}
/**
* 登录测试账号并返回 JWT。
*
* @returns 访问令牌
*/
export async function login(): Promise<string> {
const res = await axios.post(`${API_URL}/user/login`, { username: USERNAME, password: PASSWORD })
return res.data.data.token
}
/** 打开首页并等待(用于非截图场景的简单访问) */
export async function openHome(page: Page): Promise<void> {
await page.goto(BASE_URL)
await page.waitForTimeout(1500)
}