From 1e7337c23962ab3861e40a9e19c18f603edcaba6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=98=BF=E7=94=B7?= Date: Sun, 26 Apr 2026 17:22:41 +0800 Subject: [PATCH] test: guest onboarding, logged-in isolation, optional real_name MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .env.e2e.example | 2 + .github/workflows/playwright-music-room.yml | 1 + README.md | 14 +++- playwright.config.ts | 1 + tests/guest-onboarding.spec.ts | 34 +++++++++ tests/home-shell.spec.ts | 12 +-- tests/logged-in-and-isolation.spec.ts | 81 +++++++++++++++++++++ tests/moicen-unionid-chain.spec.ts | 27 ------- 8 files changed, 137 insertions(+), 35 deletions(-) create mode 100644 tests/guest-onboarding.spec.ts create mode 100644 tests/logged-in-and-isolation.spec.ts delete mode 100644 tests/moicen-unionid-chain.spec.ts diff --git a/.env.e2e.example b/.env.e2e.example index 11647b4..27805e9 100644 --- a/.env.e2e.example +++ b/.env.e2e.example @@ -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 diff --git a/.github/workflows/playwright-music-room.yml b/.github/workflows/playwright-music-room.yml index abcc1e1..1f9fb2c 100644 --- a/.github/workflows/playwright-music-room.yml +++ b/.github/workflows/playwright-music-room.yml @@ -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 diff --git a/README.md b/README.md index 14cf212..a65dcb3 100644 --- a/README.md +++ b/README.md @@ -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`)。 diff --git a/playwright.config.ts b/playwright.config.ts index bec7864..7ce4db6 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -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: diff --git a/tests/guest-onboarding.spec.ts b/tests/guest-onboarding.spec.ts new file mode 100644 index 0000000..2c3b58f --- /dev/null +++ b/tests/guest-onboarding.spec.ts @@ -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(); + }); +}); diff --git a/tests/home-shell.spec.ts b/tests/home-shell.spec.ts index fbf5627..04ab184 100644 --- a/tests/home-shell.spec.ts +++ b/tests/home-shell.spec.ts @@ -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(); }); }); diff --git a/tests/logged-in-and-isolation.spec.ts b/tests/logged-in-and-isolation.spec.ts new file mode 100644 index 0000000..1a39b09 --- /dev/null +++ b/tests/logged-in-and-isolation.spec.ts @@ -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_UNIONID(Secret 或 .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, + }); + }); +}); diff --git a/tests/moicen-unionid-chain.spec.ts b/tests/moicen-unionid-chain.spec.ts deleted file mode 100644 index 53459c1..0000000 --- a/tests/moicen-unionid-chain.spec.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { test, expect } from '@playwright/test'; - -/** - * 依赖目标站上已存在的用户与 `login2_with_unionid`。 - * 未设 MOICEN_E2E_UNIONID 时 skip(CI 无 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 }); - }); -});