后端: - 新增 leai 模块:认证、Webhook、数据同步、定时对账 - 新增 LeaiConfig/RestTemplateConfig/SchedulingConfig 配置 - 新增 FlywayRepairConfig 处理迁移修复 - 新增 V5__leai_integration.sql 迁移脚本 - 扩展所有实体类添加 tenantId 等字段 - 更新 SecurityConfig 放行 leai 公开接口 - 添加 application-test.yml 测试环境配置 前端: - 添加乐读派认证 API (public.ts) - 优化 Generating.vue 生成页 - 添加 Playwright E2E 测试配置及依赖 - 添加测试 fixtures、utils、mock-h5.html - 添加 leai 模块完整 E2E 测试套件 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
124 lines
3.3 KiB
TypeScript
124 lines
3.3 KiB
TypeScript
import { test as base, expect, request as requestFactory, type Page, type APIRequestContext } from '@playwright/test'
|
||
|
||
/**
|
||
* 认证 Fixture
|
||
* 提供已登录的浏览器上下文和 JWT Token
|
||
*/
|
||
|
||
/** 测试账户(通过环境变量覆盖) */
|
||
export const AUTH_CONFIG = {
|
||
username: process.env.TEST_USERNAME || 'demo',
|
||
password: process.env.TEST_PASSWORD || 'demo123456',
|
||
tenantCode: process.env.TEST_TENANT_CODE || 'gdlib',
|
||
/** 后端 API 地址 */
|
||
apiBase: process.env.API_BASE_URL || 'http://localhost:8580/api',
|
||
/** 登录接口路径 */
|
||
loginPath: '/public/auth/login',
|
||
}
|
||
|
||
/** 登录页路径 */
|
||
export function loginPath() {
|
||
return '/p/login'
|
||
}
|
||
|
||
/** 创作页路径 */
|
||
export function createPath() {
|
||
return '/p/create'
|
||
}
|
||
|
||
/** 作品列表路径 */
|
||
export function worksPath() {
|
||
return '/p/works'
|
||
}
|
||
|
||
/**
|
||
* 通过 API 直接获取 JWT Token(绕过 UI 登录)
|
||
*/
|
||
export async function fetchJwtToken(request: APIRequestContext): Promise<string> {
|
||
const resp = await request.post(`${AUTH_CONFIG.apiBase}${AUTH_CONFIG.loginPath}`, {
|
||
data: {
|
||
username: AUTH_CONFIG.username,
|
||
password: AUTH_CONFIG.password,
|
||
tenantCode: AUTH_CONFIG.tenantCode,
|
||
},
|
||
})
|
||
const json = await resp.json()
|
||
if (json.code !== 200 || !json.data?.token) {
|
||
throw new Error(`登录API失败: ${JSON.stringify(json)}`)
|
||
}
|
||
return json.data.token as string
|
||
}
|
||
|
||
/**
|
||
* 在浏览器页面中执行 UI 登录
|
||
*/
|
||
export async function doLogin(page: Page): Promise<Page> {
|
||
await page.goto(loginPath())
|
||
|
||
// 等待登录表单渲染
|
||
await page.waitForSelector('input[type="password"]', { timeout: 10_000 })
|
||
|
||
// 填写用户名(尝试多种选择器兼容不同 UI)
|
||
const usernameSelectors = [
|
||
'input[placeholder*="用户名"]',
|
||
'input[placeholder*="账号"]',
|
||
'input#username',
|
||
'input[name="username"]',
|
||
'input:not([type="password"]):not([type="hidden"])',
|
||
]
|
||
for (const sel of usernameSelectors) {
|
||
const input = page.locator(sel).first()
|
||
if (await input.count() > 0 && await input.isVisible()) {
|
||
await input.fill(AUTH_CONFIG.username)
|
||
break
|
||
}
|
||
}
|
||
|
||
// 填写密码
|
||
await page.locator('input[type="password"]').first().fill(AUTH_CONFIG.password)
|
||
|
||
// 点击登录按钮
|
||
const loginBtn = page.locator('button[type="submit"], button:has-text("登录"), button:has-text("登 录")').first()
|
||
await loginBtn.click()
|
||
|
||
// 等待跳转离开登录页
|
||
await page.waitForURL((url) => !url.pathname.includes('/login'), { timeout: 15_000 })
|
||
|
||
return page
|
||
}
|
||
|
||
/**
|
||
* 扩展 Playwright test fixture
|
||
*/
|
||
type AuthFixtures = {
|
||
/** 已登录的页面(浏览器模式) */
|
||
loggedInPage: Page
|
||
/** JWT token 字符串 */
|
||
authToken: string
|
||
/** 带 token 的 API 请求上下文 */
|
||
authedApi: APIRequestContext
|
||
}
|
||
|
||
export const test = base.extend<AuthFixtures>({
|
||
loggedInPage: async ({ page }, use) => {
|
||
await doLogin(page)
|
||
await use(page)
|
||
},
|
||
|
||
authToken: async ({ request }, use) => {
|
||
const token = await fetchJwtToken(request)
|
||
await use(token)
|
||
},
|
||
|
||
authedApi: async ({ request }, use) => {
|
||
const token = await fetchJwtToken(request)
|
||
const context = await requestFactory.newContext({
|
||
baseURL: AUTH_CONFIG.apiBase,
|
||
extraHTTPHeaders: { Authorization: `Bearer ${token}` },
|
||
})
|
||
await use(context)
|
||
},
|
||
})
|
||
|
||
export { expect }
|