feat: Playwright against deployed music-room (shell + optional unionid chain)

No Rust/Compose; GitHub Actions with MOICEN_E2E_UNIONID secret; dotenv .env.e2e; proxy install script.

Made-with: Cursor
This commit is contained in:
2026-04-26 17:01:52 +08:00
commit c5d054789e
10 changed files with 277 additions and 0 deletions
+3
View File
@@ -0,0 +1,3 @@
# 复制为 .env.e2e(已 gitignore
MOICEN_E2E_UNIONID=
HUIKE_FRONT_BASE_URL=https://music-room.moicen.com
@@ -0,0 +1,52 @@
# 仅 npm + Playwright,不 build Rust / 不 Compose;测已部署 music-room H5。
name: music-room Playwright
on:
push:
branches: [master, main]
pull_request:
workflow_dispatch:
inputs:
base_url:
description: H5 基址(含协议,无末尾斜杠)
required: false
default: "https://music-room.moicen.com"
type: string
schedule:
- cron: "30 6 * * 2"
concurrency:
group: huike-e2e-moicen-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
playwright:
runs-on: ubuntu-latest
timeout-minutes: 30
permissions:
contents: read
steps:
- uses: actions/checkout@v4
- name: 解析 H5 基址
run: |
URL="${{ inputs.base_url }}"
if [ -z "$URL" ]; then URL="https://music-room.moicen.com"; fi
echo "HUIKE_FRONT_BASE_URL=$URL" >> "$GITHUB_ENV"
echo "Using HUIKE_FRONT_BASE_URL=$URL"
- uses: actions/setup-node@v4
with:
node-version: "20"
cache: "npm"
cache-dependency-path: package-lock.json
- name: 依赖与 Chromium
run: |
npm ci
npx playwright install chromium --with-deps
- name: Playwright
env:
MOICEN_E2E_UNIONID: ${{ secrets.MOICEN_E2E_UNIONID }}
run: npx playwright test
+6
View File
@@ -0,0 +1,6 @@
node_modules/
playwright-report/
test-results/
.env.e2e
*.log
.DS_Store
+25
View File
@@ -0,0 +1,25 @@
# huike-e2e-moicen
针对 **已部署**`music-room.moicen.com`(或任意 `HUIKE_FRONT_BASE_URL`)跑 **Playwright**,不 clone `huike-front`、不起 Postgres、不编译 Rust。全栈本地 E2E 见 **[alchemy-studio/huike-e2e](https://github.com/alchemy-studio/huike-e2e)**。
## 本机
```bash
npm ci
npx playwright install chromium
npx playwright test
```
走代理安装(默认 `http://localhost:7890`):`npm run install:with-proxy`
可选:复制 `.env.e2e.example``.env.e2e`,填写 `MOICEN_E2E_UNIONID`(勿提交 `.env.e2e`)。未配置时「真实 unionid」用例自动 skip。
## GitHub Actions
在仓库 **Settings → Secrets → Actions** 配置 **`MOICEN_E2E_UNIONID`**(测试用户 `union_id`)后,全链路用例才会执行;否则仅跑壳层与伪造 unionid 用例。
`workflow_dispatch` 可改目标 `base_url`;默认定时见 workflow 文件。
## 与 moicen 运维文档
浏览器联调、在 UC 库查 `union_id` 等:见团队内 **`plan_skills/moicen/moicen-music-room-browser-test-runbook.md`**(与 huiwing-migration / 运维仓同路径即可)。
+90
View File
@@ -0,0 +1,90 @@
{
"name": "huike-e2e-moicen",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "huike-e2e-moicen",
"devDependencies": {
"@playwright/test": "^1.49.0",
"dotenv": "^16.4.5"
}
},
"node_modules/@playwright/test": {
"version": "1.59.1",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.59.1.tgz",
"integrity": "sha512-PG6q63nQg5c9rIi4/Z5lR5IVF7yU5MqmKaPOe0HSc0O2cX1fPi96sUQu5j7eo4gKCkB2AnNGoWt7y4/Xx3Kcqg==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"playwright": "1.59.1"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=18"
}
},
"node_modules/dotenv": {
"version": "16.6.1",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
"integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==",
"dev": true,
"license": "BSD-2-Clause",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://dotenvx.com"
}
},
"node_modules/fsevents": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/playwright": {
"version": "1.59.1",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.59.1.tgz",
"integrity": "sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"playwright-core": "1.59.1"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=18"
},
"optionalDependencies": {
"fsevents": "2.3.2"
}
},
"node_modules/playwright-core": {
"version": "1.59.1",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.59.1.tgz",
"integrity": "sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"playwright-core": "cli.js"
},
"engines": {
"node": ">=18"
}
}
}
}
+12
View File
@@ -0,0 +1,12 @@
{
"name": "huike-e2e-moicen",
"private": true,
"scripts": {
"test": "playwright test",
"install:with-proxy": "bash scripts/install-with-proxy.sh"
},
"devDependencies": {
"@playwright/test": "^1.49.0",
"dotenv": "^16.4.5"
}
}
+17
View File
@@ -0,0 +1,17 @@
import path from 'node:path';
import { config as loadEnv } from 'dotenv';
import { defineConfig } from '@playwright/test';
loadEnv({ path: path.join(__dirname, '.env.e2e') });
export default defineConfig({
testDir: './tests',
timeout: 120_000,
expect: { timeout: 30_000 },
use: {
baseURL:
process.env.HUIKE_FRONT_BASE_URL || 'https://music-room.moicen.com',
trace: 'on-first-retry',
},
reporter: [['list']],
});
+17
View File
@@ -0,0 +1,17 @@
#!/usr/bin/env bash
# 经本机 HTTP 代理安装 npm 与 Playwright Chromium(默认 http://localhost:7890
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
PROXY_URL="${HTTP_PROXY:-${HTTPS_PROXY:-http://localhost:7890}}"
export HTTP_PROXY="$PROXY_URL"
export HTTPS_PROXY="$PROXY_URL"
export ALL_PROXY="${ALL_PROXY:-$PROXY_URL}"
export NO_PROXY="${NO_PROXY:-127.0.0.1,localhost}"
echo "Using proxy: $HTTP_PROXY"
cd "$ROOT"
npm ci
npx playwright install chromium --with-deps
+28
View File
@@ -0,0 +1,28 @@
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();
});
test('带伪造 unionid/status 的入口不应导致白屏', async ({ page }) => {
await page.goto('/?unionid=fake-wx-unionid-e2e&status=2', {
waitUntil: 'domcontentloaded',
timeout: 30_000,
});
await expect(page.locator('#app')).toBeVisible({ timeout: 30_000 });
});
test('page_path 内嵌他人 unionid 时应被剥离(最终 URL 不含该串)', async ({ page }) => {
const poison = 'attacker-unionid-e2e-marker';
const pagePath = encodeURIComponent(`/?unionid=${poison}&status=2`);
await page.goto(`/?page_path=${pagePath}`, {
waitUntil: 'domcontentloaded',
timeout: 30_000,
});
await page.waitForURL((u) => !u.toString().includes(poison), { timeout: 30_000 });
await expect(page.locator('#app')).toBeVisible();
});
});
+27
View File
@@ -0,0 +1,27 @@
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 });
});
});