test: guest onboarding, logged-in isolation, optional real_name

Add guest-onboarding and logged-in-and-isolation specs; remove moicen-unionid-chain;
MOICEN_E2E_EXPECTED_REAL_NAME for 欢迎回来+姓名; CI secret; workers=1; longer goto timeouts.

Made-with: Cursor
This commit is contained in:
2026-04-26 17:22:41 +08:00
parent bc85888359
commit 1e7337c239
8 changed files with 137 additions and 35 deletions
+2
View File
@@ -1,3 +1,5 @@
# 复制为 .env.e2e(已 gitignore
MOICEN_E2E_UNIONID=
# 可选:与 UC 中 real_name 一致,用于断言「欢迎回来,{姓名}」(如 阿难)
MOICEN_E2E_EXPECTED_REAL_NAME=
HUIKE_FRONT_BASE_URL=https://music-room.moicen.com
@@ -50,4 +50,5 @@ jobs:
- name: Playwright
env:
MOICEN_E2E_UNIONID: ${{ secrets.MOICEN_E2E_UNIONID }}
MOICEN_E2E_EXPECTED_REAL_NAME: ${{ secrets.MOICEN_E2E_EXPECTED_REAL_NAME }}
run: npx playwright test
+12 -2
View File
@@ -12,11 +12,21 @@ npx playwright test
走代理安装(默认 `http://localhost:7890`):`npm run install:with-proxy`
可选:复制 `.env.e2e.example``.env.e2e`,填写 `MOICEN_E2E_UNIONID`(勿提交 `.env.e2e`)。未配置时「真实 unionid」用例自动 skip
可选:复制 `.env.e2e.example``.env.e2e`,填写 **`MOICEN_E2E_UNIONID`**;若要断言首页展示 **本人姓名**,再加 **`MOICEN_E2E_EXPECTED_REAL_NAME`**(与库 `hty_users.real_name` 一致,如 `阿难`)。勿提交 `.env.e2e`
用例概览:
| 文件 | 内容 |
|------|------|
| `tests/guest-onboarding.spec.ts` | 未登录:仅见「请返回微信小程序完成登录」,不出现已登录工作台 |
| `tests/home-shell.spec.ts` | `#app`、伪造 unionid 不白屏、`page_path` 净化 |
| `tests/logged-in-and-isolation.spec.ts` | 需 Secret:登录后非访客态;URL 异主 unionid 剥离(串号防护);可选姓名断言 |
未配置 `MOICEN_E2E_UNIONID` 时,`logged-in-and-isolation` 内用例全部 skip。
## GitHub Actions
仓库 **Settings → Secrets → Actions** 配置 **`MOICEN_E2E_UNIONID`**(测试用户 `union_id`后,全链路用例才会执行;否则仅跑壳层与伪造 unionid 用例。
**Settings → Secrets → Actions** 配置 **`MOICEN_E2E_UNIONID`**(测试用户 `union_id`。可选再配 **`MOICEN_E2E_EXPECTED_REAL_NAME`**,否则「欢迎回来 + 姓名」用例 skip。未配 unionid 时仅跑访客与壳层用例。
`workflow_dispatch` 可改目标 `base_url`**默认定时:每天 06:30 UTC**(见 `.github/workflows/playwright-music-room.yml`)。
+1
View File
@@ -7,6 +7,7 @@ loadEnv({ path: path.join(__dirname, '.env.e2e') });
export default defineConfig({
testDir: './tests',
timeout: 120_000,
workers: 1,
expect: { timeout: 30_000 },
use: {
baseURL:
+34
View File
@@ -0,0 +1,34 @@
import { test, expect } from '@playwright/test';
/**
* 未登录(无 token、无有效 unionid 登录链):与 `huike-front` `index.vue` 一致,
* 根路径展示「请返回微信小程序完成登录」占位(产品侧常称欢迎/引导注册流程)。
*/
test.describe('未登录访客', () => {
test('新浏览器上下文访问 / 只看到小程序登录引导,不出现已登录工作台', async ({
browser,
}) => {
const context = await browser.newContext();
const page = await context.newPage();
await page.goto('/', { waitUntil: 'domcontentloaded', timeout: 60_000 });
await expect(page.locator('#app')).toBeVisible({ timeout: 60_000 });
await expect(
page.getByText('请返回微信小程序完成登录')
).toBeVisible({ timeout: 60_000 });
await expect(page.getByText('欢迎回来')).not.toBeVisible();
await expect(page.getByText('请选择您的登录身份')).not.toBeVisible();
await context.close();
});
test('未带 status=2 时访问 / 仍为访客引导(非 Registered 链)', async ({
browser,
}) => {
const context = await browser.newContext();
const page = await context.newPage();
await page.goto('/', { waitUntil: 'domcontentloaded', timeout: 60_000 });
await expect(
page.getByText('请返回微信小程序完成登录')
).toBeVisible({ timeout: 60_000 });
await context.close();
});
});
+6 -6
View File
@@ -3,16 +3,16 @@ import { test, expect } from '@playwright/test';
// 对已部署 H5:匿名、伪造 unionid、page_path 净化(与 huike-front main.ts 一致)
test.describe('music-room shell', () => {
test('根路径挂载 Vue 根节点', async ({ page }) => {
await page.goto('/');
await expect(page.locator('#app')).toBeVisible();
await page.goto('/', { waitUntil: 'domcontentloaded', timeout: 60_000 });
await expect(page.locator('#app')).toBeVisible({ timeout: 60_000 });
});
test('带伪造 unionid/status 的入口不应导致白屏', async ({ page }) => {
await page.goto('/?unionid=fake-wx-unionid-e2e&status=2', {
waitUntil: 'domcontentloaded',
timeout: 30_000,
timeout: 60_000,
});
await expect(page.locator('#app')).toBeVisible({ timeout: 30_000 });
await expect(page.locator('#app')).toBeVisible({ timeout: 60_000 });
});
test('page_path 内嵌他人 unionid 时应被剥离(最终 URL 不含该串)', async ({ page }) => {
@@ -20,9 +20,9 @@ test.describe('music-room shell', () => {
const pagePath = encodeURIComponent(`/?unionid=${poison}&status=2`);
await page.goto(`/?page_path=${pagePath}`, {
waitUntil: 'domcontentloaded',
timeout: 30_000,
timeout: 60_000,
});
await page.waitForURL((u) => !u.toString().includes(poison), { timeout: 30_000 });
await page.waitForURL((u) => !u.toString().includes(poison), { timeout: 60_000 });
await expect(page.locator('#app')).toBeVisible();
});
});
+81
View File
@@ -0,0 +1,81 @@
import { test, expect } from '@playwright/test';
const moicenUnionid = process.env.MOICEN_E2E_UNIONID?.trim();
const expectedRealName = process.env.MOICEN_E2E_EXPECTED_REAL_NAME?.trim();
test.describe('已登录用户与串号防护', () => {
test.skip(!moicenUnionid, '需要 MOICEN_E2E_UNIONIDSecret 或 .env.e2e');
test('login2_with_unionid 后不应停留在未登录占位,应出现身份或工作台', async ({
page,
}) => {
const q = new URLSearchParams({
unionid: moicenUnionid!,
status: '2',
});
await page.goto(`/?${q.toString()}`, {
waitUntil: 'domcontentloaded',
timeout: 60_000,
});
await expect(page.locator('#app')).toBeVisible({ timeout: 60_000 });
await expect(
page.getByText('请返回微信小程序完成登录')
).not.toBeVisible({ timeout: 90_000 });
await expect(
page.getByText(/请选择您的登录身份|欢迎回来|进入工作台/)
).toBeVisible({ timeout: 90_000 });
});
test('已登录后 URL 中异主 unionid 应被剥离(不串号)', async ({ page }) => {
const q = new URLSearchParams({
unionid: moicenUnionid!,
status: '2',
});
await page.goto(`/?${q.toString()}`, {
waitUntil: 'domcontentloaded',
timeout: 60_000,
});
await expect(
page.getByText(/请选择您的登录身份|欢迎回来|进入工作台/)
).toBeVisible({ timeout: 90_000 });
const poisonUnionid = 'e2eStrangerUnionidNotCurrentUser001';
const pathBefore = new URL(page.url()).pathname || '/';
await page.goto(
`${pathBefore}?unionid=${encodeURIComponent(poisonUnionid)}&status=2`,
{ waitUntil: 'domcontentloaded', timeout: 60_000 }
);
await page.waitForURL(
(u) => !u.toString().includes(poisonUnionid),
{ timeout: 45_000 }
);
expect(page.url()).not.toContain(poisonUnionid);
await expect(
page.getByText('请返回微信小程序完成登录')
).not.toBeVisible();
});
test('可选:工作台「欢迎回来」展示配置的真实姓名(自己的数据)', async ({
page,
}) => {
test.skip(
!expectedRealName,
'未设置 MOICEN_E2E_EXPECTED_REAL_NAME 时跳过(与库中 real_name 一致,如 阿难)'
);
const q = new URLSearchParams({
unionid: moicenUnionid!,
status: '2',
});
await page.goto(`/?${q.toString()}`, {
waitUntil: 'domcontentloaded',
timeout: 60_000,
});
if (await page.getByText('请选择您的登录身份').isVisible().catch(() => false)) {
await page.locator('.van-grid-item').first().click();
}
await expect(page.getByText('欢迎回来')).toBeVisible({ timeout: 120_000 });
await expect(page.getByText(expectedRealName!)).toBeVisible({
timeout: 30_000,
});
});
});
-27
View File
@@ -1,27 +0,0 @@
import { test, expect } from '@playwright/test';
/**
* 依赖目标站上已存在的用户与 `login2_with_unionid`。
* 未设 MOICEN_E2E_UNIONID 时 skipCI 无 Secret 不失败)。
*/
const moicenUnionid = process.env.MOICEN_E2E_UNIONID?.trim();
test.describe('全链路(真实 unionid → UC 换 session', () => {
test('带 status=2 进入后应出现已登录侧 UI(多角色选身份 / 单角色欢迎语)', async ({
page,
}) => {
test.skip(!moicenUnionid, '未设置 MOICEN_E2E_UNIONID(本地 .env.e2e 或 GitHub Secret');
const q = new URLSearchParams({
unionid: moicenUnionid!,
status: '2',
});
await page.goto(`/?${q.toString()}`, {
waitUntil: 'domcontentloaded',
timeout: 30_000,
});
await expect(page.locator('#app')).toBeVisible({ timeout: 30_000 });
await expect(
page.getByText(/请选择您的登录身份|欢迎回来/)
).toBeVisible({ timeout: 90_000 });
});
});