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 }
|