test(e2e): 前后台联调 Playwright 与 admin 冒烟
ci / h5 (push) Canceled after 0s
ci / api (push) Canceled after 0s
ci / ess-docs (push) Canceled after 0s

新增 e2e-live 真 API 一致性全路径,并为 admin-h5 增加 mock 浏览器冒烟。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
jackyu66git
2026-08-06 22:41:43 +08:00
co-authored by Cursor
parent 879bf70cb7
commit 7e9023f0a8
12 changed files with 400 additions and 1 deletions
+3
View File
@@ -0,0 +1,3 @@
test-results/
playwright-report/
blob-report/
+14
View File
@@ -0,0 +1,14 @@
# e2e-live — 前后台联调(真 API
需要:
1. `npm run deps:up`
2. API`cd apps/api && go run ./cmd/server``:8080`,含 admin bootstrap
3. 本套件会自动拉起 user-h5 `:5173` 与 admin-h5 `:5174`(已占用则复用)
```bash
npm install
npm run test:e2e:live
```
覆盖:画像 → 后台用户一致 → 前台开通会员 ↔ 后台订单/会员 → 后台授年卡 ↔ 前台年卡+审计 → 关系 → Ask → 量表 → 多页烟测。
+13
View File
@@ -0,0 +1,13 @@
{
"name": "@yuxingu/e2e-live",
"version": "0.1.0",
"private": true,
"description": "前后台联调 Playwright(真 API",
"scripts": {
"test": "playwright test",
"test:headed": "playwright test --headed"
},
"devDependencies": {
"@playwright/test": "^1.50.1"
}
}
+43
View File
@@ -0,0 +1,43 @@
import { defineConfig, devices } from '@playwright/test'
/**
* Live full-stack e2e: real Go API + user-h5 + admin-h5.
* Prerequisites: `npm run deps:up` and API on :8080 (or started via webServer).
*/
export default defineConfig({
testDir: './tests',
fullyParallel: false,
workers: 1,
timeout: 180_000,
expect: { timeout: 20_000 },
forbidOnly: !!process.env.CI,
retries: 0,
use: {
trace: 'retain-on-failure',
screenshot: 'only-on-failure',
video: 'retain-on-failure',
},
projects: [
{
name: 'chromium',
use: {
...devices['Desktop Chrome'],
...(process.env.PW_CHANNEL ? { channel: process.env.PW_CHANNEL } : {}),
},
},
],
webServer: [
{
command: 'npm run dev -w @yuxingu/user-h5 -- --host 127.0.0.1 --port 5173',
url: 'http://127.0.0.1:5173/psy/',
reuseExistingServer: true,
timeout: 120_000,
},
{
command: 'npm run dev -w @yuxingu/admin-h5 -- --host 127.0.0.1 --port 5174',
url: 'http://127.0.0.1:5174/',
reuseExistingServer: true,
timeout: 120_000,
},
],
})
@@ -0,0 +1,169 @@
import { test, expect, type Page, type BrowserContext } from '@playwright/test'
const USER = 'http://127.0.0.1:5173'
const ADMIN = 'http://127.0.0.1:5174'
const API = 'http://127.0.0.1:8080'
test.describe.configure({ mode: 'serial' })
test.describe('前后台联调 · 真 API 全路径', () => {
let userCtx: BrowserContext
let userPage: Page
let adminPage: Page
let userId = ''
let marker = ''
test.beforeAll(async ({ browser }) => {
const health = await fetch(`${API}/api/v1/healthz`)
if (!health.ok) {
throw new Error('API :8080 不可用。先 npm run deps:up && npm run dev:api')
}
marker = `L${Date.now().toString(36)}`
userCtx = await browser.newContext()
userPage = await userCtx.newPage()
adminPage = await browser.newPage()
})
test.afterAll(async () => {
await userCtx?.close()
await adminPage?.context().close()
})
test('0 · 运营后台可登录', async () => {
await adminPage.goto(`${ADMIN}/login`)
await adminPage.locator('input[type="password"]').fill('change-me')
await adminPage.getByRole('button', { name: '登录' }).click()
await expect(adminPage.getByRole('heading', { name: '用户' })).toBeVisible()
})
test('1 · 用户侧生成个人画像', async () => {
const portraitResp = userPage.waitForResponse(
(r) => r.url().includes('/reports/portrait') && r.request().method() === 'POST' && r.ok(),
)
await userPage.goto(`${USER}/psy/portrait`)
await expect(userPage.getByRole('heading', { name: '愈心解码' }).first()).toBeVisible()
const inputs = userPage.locator('.decode-inputs input')
await inputs.nth(0).fill('1992')
await inputs.nth(1).fill('7')
await inputs.nth(2).fill('18')
await userPage.getByRole('button', { name: '开始解码' }).click()
const resp = await portraitResp
const body = await resp.json()
userId = body?.data?.user_id || ''
expect(userId, 'portrait 响应应含 user_id').toMatch(/^[0-9a-f-]{36}$/i)
await expect(userPage.getByRole('link', { name: /在报告页打开/ })).toBeVisible({ timeout: 60_000 })
await expect(userPage.getByRole('button', { name: /解锁完整分析/ }).first()).toBeVisible()
})
test('2 · 后台能看到该用户,并与前台档案一致', async () => {
expect(userId).toBeTruthy()
await adminPage.goto(`${ADMIN}/users/${userId}`)
await expect(adminPage.getByRole('heading', { name: '用户详情' })).toBeVisible()
await expect(adminPage.getByText(userId).first()).toBeVisible()
await expect(adminPage.getByText('self').first()).toBeVisible()
})
test('3 · 前台开通会员 → 后台订单与会员状态一致', async () => {
await userPage.goto(`${USER}/psy/membership`)
await expect(userPage.getByRole('heading', { name: '成长会员' })).toBeVisible()
await userPage.getByRole('button', { name: /月卡/ }).click()
await expect(userPage.getByText('会员有效')).toBeVisible({ timeout: 30_000 })
await adminPage.goto(`${ADMIN}/users/${userId}`)
await expect(adminPage.getByText('有效').first()).toBeVisible({ timeout: 15_000 })
await expect(adminPage.getByText(/month|月/i).first()).toBeVisible()
await adminPage.goto(`${ADMIN}/orders`)
await expect(adminPage.getByRole('heading', { name: '订单' })).toBeVisible()
await expect(adminPage.getByText(userId).first()).toBeVisible()
await expect(adminPage.getByText('membership').first()).toBeVisible()
await expect(adminPage.getByText('paid').first()).toBeVisible()
})
test('4 · 后台授予年卡 → 前台会员与审计一致', async () => {
await adminPage.goto(`${ADMIN}/users/${userId}`)
await adminPage.locator('select').selectOption('year')
await adminPage.getByRole('button', { name: '授予 / 延长' }).click()
await expect(adminPage.getByText('已授予')).toBeVisible()
await adminPage.goto(`${ADMIN}/audit`)
await expect(adminPage.getByText('membership.grant').first()).toBeVisible()
await expect(adminPage.getByText(userId).first()).toBeVisible()
await userPage.goto(`${USER}/psy/membership`)
await expect(userPage.getByText('会员有效 · 年卡')).toBeVisible({ timeout: 15_000 })
})
test('5 · 成长报告列表可打开(会员后)', async () => {
await userPage.goto(`${USER}/psy/reports`)
await userPage.waitForLoadState('networkidle')
await expect(userPage.locator('.who-static')).toContainText('成长报告', { timeout: 20_000 })
await expect(userPage.getByText('愈心解码').first()).toBeVisible()
await userPage.getByText('查看报告').first().click()
await expect(userPage).toHaveURL(/\/reports\//)
})
test('6 · 关系理解', async () => {
await userPage.goto(`${USER}/psy/relation`)
await expect(userPage.getByRole('heading', { name: '看见彼此的相处模样' })).toBeVisible()
await userPage.getByPlaceholder('例如:伴侣').fill(marker.slice(0, 6))
const nums = userPage.locator('input[type="number"]')
await nums.nth(0).fill('1991')
await nums.nth(1).fill('3')
await nums.nth(2).fill('9')
await userPage.getByRole('button', { name: '去匹配' }).click()
await expect(userPage.getByRole('button', { name: '生成中…' })).toBeHidden({ timeout: 60_000 })
await expect(userPage.getByText(/默契|相处|沟通|匹配|性格/).first()).toBeVisible({ timeout: 60_000 })
})
test('7 · AI 问答', async () => {
await userPage.goto(`${USER}/psy/ask`)
const box = userPage.getByPlaceholder('让我来解答你的问题吧')
await expect(box).toBeVisible({ timeout: 30_000 })
const q = `联调提问${marker}`
await box.fill(q)
await userPage.getByRole('button', { name: '发送' }).click()
await expect(userPage.getByText(q)).toBeVisible()
await expect(userPage.locator('body')).toContainText(/你|建议|可以|了解|次数|会员/, { timeout: 60_000 })
})
test('8 · 探索测试完整提交', async () => {
await userPage.goto(`${USER}/psy/scales/communication-style`)
await expect(userPage.getByRole('button', { name: '开始' })).toBeVisible({ timeout: 20_000 })
await userPage.getByRole('button', { name: '开始' }).click()
for (let i = 0; i < 3; i++) {
await userPage.locator('label.opt').first().click()
const nextLabel = i === 2 ? '查看探索结果' : '下一题'
await userPage.getByRole('button', { name: nextLabel }).click()
}
await expect(userPage.getByText(/沟通|探索|结果|偏好/).first()).toBeVisible({ timeout: 30_000 })
})
test('9 · 其余核心页可打开(烟测)', async () => {
for (const path of [
'/psy/',
'/psy/explore',
'/psy/companion',
'/psy/mine',
'/psy/reports',
'/psy/profile',
'/psy/star',
'/psy/rhythm',
'/psy/cards',
'/psy/synastry',
]) {
await userPage.goto(`${USER}${path}`)
await expect(userPage.locator('#app')).toBeVisible()
await expect(userPage.getByText(/失败|unavailable|Internal Server/i)).toHaveCount(0)
}
})
test('10 · 后台用户详情仍可看到档案与近订单', async () => {
await adminPage.goto(`${ADMIN}/users/${userId}`)
await expect(adminPage.getByText('self').first()).toBeVisible()
await expect(adminPage.getByText('有效').first()).toBeVisible()
})
})