feat(e2e): 核心全链路用例与辅助模块
新增 smoke-http、core-full-chain(访客深链守卫、登录黄金路径、可选健康 URL);抽出 music-room-session 辅助;Playwright 忽略本地 admin_debug;CI 注入可选 MOICEN_HEALTHCHECK_URL。 Made-with: Cursor
This commit is contained in:
@@ -6,3 +6,6 @@ HUIKE_FRONT_BASE_URL=https://music-room.moicen.com
|
||||
HUIKE_ADMIN_BASE_URL=https://admin.moicen.com
|
||||
MOICEN_ADMIN_USER=
|
||||
MOICEN_ADMIN_PASSWORD=
|
||||
|
||||
# 可选:任意 HTTP 200 健康检查 URL(CI 可在仓库 Settings → Variables 配置 MOICEN_HEALTHCHECK_URL)
|
||||
MOICEN_HEALTHCHECK_URL=
|
||||
|
||||
@@ -50,4 +50,6 @@ jobs:
|
||||
- name: Playwright
|
||||
env:
|
||||
MOICEN_E2E_UNIONID: ${{ secrets.MOICEN_E2E_UNIONID }}
|
||||
# 可选:Repository variables,例如后端 health/ping;未配置时对应用例 skip
|
||||
MOICEN_HEALTHCHECK_URL: ${{ vars.MOICEN_HEALTHCHECK_URL }}
|
||||
run: npx playwright test
|
||||
|
||||
@@ -18,6 +18,8 @@ npx playwright test
|
||||
|
||||
| 文件 | 内容 |
|
||||
|------|------|
|
||||
| `tests/smoke-http.spec.ts` | **request**:`GET /` 返回 HTML(网关 / TLS / 静态入口底线,不启浏览器) |
|
||||
| `tests/core-full-chain.spec.ts` | **访客**:未登录访问 `/course` 被送回 `/` 且见访客占位;**已登录**:黄金路径(会话→深链→课程体系壳层→JWT 与机构闭环);**可选** `MOICEN_HEALTHCHECK_URL` |
|
||||
| `tests/guest-onboarding.spec.ts` | 未登录:仅见「请返回微信小程序完成登录」,不出现已登录工作台 |
|
||||
| `tests/home-shell.spec.ts` | `#app`、伪造 unionid 不白屏、`page_path` 净化 |
|
||||
| `tests/logged-in-and-isolation.spec.ts` | 需 Secret:登录后非访客态;异主 unionid 剥离;多角色选身份后见「欢迎回来」(不校验姓名) |
|
||||
@@ -31,6 +33,8 @@ npx playwright test
|
||||
|
||||
在 **Settings → Secrets → Actions** 配置 **`MOICEN_E2E_UNIONID`** 即可。未配 unionid 时仅跑访客与壳层用例。
|
||||
|
||||
可选:在 **Settings → Secrets and variables → Actions → Variables** 配置 **`MOICEN_HEALTHCHECK_URL`**(完整 URL,返回 2xx),用于可选后端健康检查用例;未配置时该条自动 skip。
|
||||
|
||||
`workflow_dispatch` 可改目标 `base_url`;**默认定时:每天 06:30 UTC**(见 `.github/workflows/playwright-music-room.yml`)。
|
||||
|
||||
## 与 moicen 运维文档
|
||||
|
||||
@@ -6,6 +6,7 @@ loadEnv({ path: path.join(__dirname, '.env.e2e') });
|
||||
|
||||
export default defineConfig({
|
||||
testDir: './tests',
|
||||
testIgnore: ['**/admin_debug.spec.ts'],
|
||||
timeout: 120_000,
|
||||
workers: 1,
|
||||
expect: { timeout: 30_000 },
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
import { expect, test } from './fixtures';
|
||||
import {
|
||||
decodeJwtPayload,
|
||||
establishSession,
|
||||
extractOrgIdFromTokenPayload,
|
||||
resolveOrgContextForCoursePage,
|
||||
waitForCourseRealmVisible,
|
||||
} from './helpers/music-room-session';
|
||||
|
||||
const moicenUnionid = process.env.MOICEN_E2E_UNIONID?.trim();
|
||||
|
||||
/** 可选:仓库 Variables `MOICEN_HEALTHCHECK_URL`(如 `https://api.example/health`),未配置则 skip */
|
||||
const moicenHealthcheckUrl = process.env.MOICEN_HEALTHCHECK_URL?.trim();
|
||||
|
||||
test.describe('核心全链路(访客)', () => {
|
||||
/**
|
||||
* `main.ts`:未带 Registered 链时访问非 `/`、`/guest/*` 会 replace 到 `/`,最终应呈现访客占位。
|
||||
*/
|
||||
test('未登录深链 /course 最终回到访客首页', async ({ page }) => {
|
||||
await page.goto('/course', {
|
||||
waitUntil: 'domcontentloaded',
|
||||
timeout: 60_000,
|
||||
});
|
||||
await expect(page.locator('#app')).toBeVisible({ timeout: 60_000 });
|
||||
await page.waitForURL((u) => new URL(u).pathname === '/', {
|
||||
timeout: 60_000,
|
||||
});
|
||||
await expect(
|
||||
page.getByText('请返回微信小程序完成登录')
|
||||
).toBeVisible({ timeout: 60_000 });
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('核心全链路(已登录 → 教学资源)', () => {
|
||||
test.describe.configure({ timeout: 180_000 });
|
||||
|
||||
test.skip(!moicenUnionid, '需要 MOICEN_E2E_UNIONID(Secret 或 .env.e2e)');
|
||||
|
||||
test('黄金路径:会话建立 → 深链进入课程体系壳层 → JWT 与 CurrentOrgId 闭环', async ({
|
||||
page,
|
||||
}) => {
|
||||
await establishSession(page, moicenUnionid!);
|
||||
if (
|
||||
await page.getByText('请返回微信小程序完成登录').isVisible().catch(() => false)
|
||||
) {
|
||||
test.skip(true, '当前 unionid 会话未处于可用登录态');
|
||||
}
|
||||
|
||||
await resolveOrgContextForCoursePage(page);
|
||||
|
||||
const storage = await page.evaluate(() => ({
|
||||
currentOrgId: window.localStorage.getItem('CurrentOrgId'),
|
||||
authorization: window.localStorage.getItem('Authorization'),
|
||||
}));
|
||||
expect(storage.authorization, '登录深链后应有 Authorization').toBeTruthy();
|
||||
|
||||
const payload = decodeJwtPayload(storage.authorization!);
|
||||
expect(payload).not.toBeNull();
|
||||
const jwtOrgId = extractOrgIdFromTokenPayload(payload);
|
||||
expect(jwtOrgId).toBeTruthy();
|
||||
if (storage.currentOrgId) {
|
||||
expect(storage.currentOrgId).toBe(jwtOrgId);
|
||||
}
|
||||
|
||||
await page.goto('/course', {
|
||||
waitUntil: 'domcontentloaded',
|
||||
timeout: 90_000,
|
||||
});
|
||||
await expect(page.locator('#app')).toBeVisible({ timeout: 90_000 });
|
||||
await waitForCourseRealmVisible(page);
|
||||
|
||||
await expect(page.getByText('请求失败,点击重新加载')).toHaveCount(0, {
|
||||
timeout: 45_000,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('可选:MOICEN_HEALTHCHECK_URL 可达', async ({ request }) => {
|
||||
test.skip(!moicenHealthcheckUrl, '未配置 MOICEN_HEALTHCHECK_URL(仓库 Variables)');
|
||||
const res = await request.get(moicenHealthcheckUrl!);
|
||||
expect(
|
||||
res.ok(),
|
||||
`HTTP ${res.status()} ${res.statusText()} ${moicenHealthcheckUrl}`
|
||||
).toBeTruthy();
|
||||
});
|
||||
@@ -0,0 +1,183 @@
|
||||
import type { Page } from '@playwright/test';
|
||||
import { expect, test } from '../fixtures';
|
||||
|
||||
export function decodeJwtPayload(token: string): Record<string, unknown> | null {
|
||||
const raw = token.startsWith('Bearer ') ? token.slice(7).trim() : token;
|
||||
const parts = raw.split('.');
|
||||
if (parts.length !== 3) return null;
|
||||
try {
|
||||
const base64 = parts[1].replace(/-/g, '+').replace(/_/g, '/');
|
||||
const padded = base64 + '='.repeat((4 - (base64.length % 4)) % 4);
|
||||
const json = Buffer.from(padded, 'base64').toString('utf8');
|
||||
return JSON.parse(json) as Record<string, unknown>;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** 与 `huike-front/src/main.ts` 中 `parseCurrentOrgIdFromToken` 一致:claims 可能在顶层或嵌在 `sub` JSON 内 */
|
||||
export function extractOrgIdFromTokenPayload(
|
||||
payload: Record<string, unknown> | null
|
||||
): string | undefined {
|
||||
if (!payload) return undefined;
|
||||
const top = payload.current_org_id;
|
||||
if (typeof top === 'string' && top.length > 0) return top;
|
||||
const sub = payload.sub;
|
||||
if (typeof sub !== 'string' || sub.length === 0) return undefined;
|
||||
try {
|
||||
const inner = JSON.parse(sub) as Record<string, unknown>;
|
||||
const innerOrg = inner.current_org_id;
|
||||
if (typeof innerOrg === 'string' && innerOrg.length > 0) return innerOrg;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* 教学资源相关任意壳层可见即可(`/course/summary` 入口链或课程体系列表)。
|
||||
* CI 上 SPA 长连接会导致 networkidle 不可靠;此处只用 DOM 信号轮询。
|
||||
*/
|
||||
export async function waitForCourseRealmVisible(page: Page) {
|
||||
const deadline = Date.now() + 95_000;
|
||||
while (Date.now() < deadline) {
|
||||
const titleHit = await page.title().catch(() => '');
|
||||
if (/教学资源库|课程体系/.test(titleHit)) {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
await page.getByRole('link', { name: '课程体系' }).isVisible().catch(() => false)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
for (const label of ['课程体系', '课程', '课节']) {
|
||||
if (
|
||||
await page
|
||||
.locator('.van-cell')
|
||||
.filter({ hasText: new RegExp(label) })
|
||||
.first()
|
||||
.isVisible()
|
||||
.catch(() => false)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (
|
||||
await page
|
||||
.getByPlaceholder('请输入课程体系名称搜索')
|
||||
.isVisible()
|
||||
.catch(() => false)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
await page.locator('.van-search').first().isVisible().catch(() => false)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
await page.waitForTimeout(400);
|
||||
}
|
||||
throw new Error(`教学资源壳层未出现:${page.url()}`);
|
||||
}
|
||||
|
||||
export type OrgSelectState = 'ready' | 'empty' | 'guest' | 'timeout';
|
||||
|
||||
export async function detectOrgSelectState(
|
||||
page: Page,
|
||||
timeoutMs = 90_000
|
||||
): Promise<OrgSelectState> {
|
||||
const deadline = Date.now() + timeoutMs;
|
||||
while (Date.now() < deadline) {
|
||||
const state = await page.evaluate(() => {
|
||||
const appRoot = document.querySelector('#app');
|
||||
if (!appRoot) return 'pending';
|
||||
const rootText = appRoot.textContent ?? '';
|
||||
if (rootText.includes('请返回微信小程序完成登录')) return 'guest';
|
||||
if (rootText.includes('暂无可用机构')) return 'empty';
|
||||
if (rootText.includes('请选择机构')) return 'ready';
|
||||
if (appRoot.querySelector('.van-cell-group .van-cell')) return 'ready';
|
||||
return 'pending';
|
||||
});
|
||||
if (state === 'guest' || state === 'empty' || state === 'ready') {
|
||||
return state;
|
||||
}
|
||||
await page.waitForTimeout(500);
|
||||
}
|
||||
return 'timeout';
|
||||
}
|
||||
|
||||
export async function establishSession(page: Page, unionid: string) {
|
||||
const q = new URLSearchParams({
|
||||
unionid,
|
||||
status: '2',
|
||||
});
|
||||
await page.goto(`/?${q.toString()}`, {
|
||||
waitUntil: 'domcontentloaded',
|
||||
timeout: 60_000,
|
||||
});
|
||||
await expect(page.locator('#app')).toBeVisible({ 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 });
|
||||
}
|
||||
|
||||
/**
|
||||
* 停留在 `/` 时 main.ts 不会把 JWT 中的机构写入 CurrentOrgId。
|
||||
* 深链会触发守卫;多机构且无上下文时会先进入 `/org/select`,需点选机构后再继续。
|
||||
* 交替尝试 summary 与课程索引:部分账号在 CI 上只对其中之一稳定渲染壳层。
|
||||
*/
|
||||
export async function resolveOrgContextForCoursePage(page: Page) {
|
||||
const deepPaths = ['/course/summary', '/course'] as const;
|
||||
|
||||
for (let attempt = 0; attempt < 8; attempt++) {
|
||||
const targetPath = deepPaths[attempt % deepPaths.length];
|
||||
await page.goto(targetPath, {
|
||||
waitUntil: 'domcontentloaded',
|
||||
timeout: 90_000,
|
||||
});
|
||||
await expect(page.locator('#app')).toBeVisible({ timeout: 90_000 });
|
||||
|
||||
if (page.url().includes('/org/select')) {
|
||||
const orgSelectState = await detectOrgSelectState(page, 60_000);
|
||||
if (orgSelectState === 'guest') {
|
||||
test.skip(true, '深链进入机构页时会话退回访客态');
|
||||
}
|
||||
if (orgSelectState === 'empty') {
|
||||
test.skip(true, '账号无可用机构,跳过机构上下文用例');
|
||||
}
|
||||
if (orgSelectState === 'timeout') {
|
||||
throw new Error(`机构页未渲染就绪信号:${page.url()}`);
|
||||
}
|
||||
|
||||
const pickCells = page.locator('#app .van-cell-group .van-cell');
|
||||
const n = await pickCells.count();
|
||||
expect(n, '机构选择页应有可选机构').toBeGreaterThan(0);
|
||||
await pickCells.first().click();
|
||||
await expect(page.locator('#app')).toBeVisible({ timeout: 90_000 });
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
const path = new URL(page.url()).pathname || '';
|
||||
const onCourseRealm =
|
||||
path === '/course' || path.startsWith('/course/');
|
||||
if (onCourseRealm) {
|
||||
await waitForCourseRealmVisible(page);
|
||||
return;
|
||||
}
|
||||
} catch {
|
||||
/* URL 解析失败则继续重试 */
|
||||
}
|
||||
|
||||
// 守卫可能暂时送回首页「进入工作台」等,下一循环再深链
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
'resolveOrgContextForCoursePage: 无法在合理步数内进入教学资源链(/course/summary 等)'
|
||||
);
|
||||
}
|
||||
@@ -1,193 +1,22 @@
|
||||
import type { Page } from '@playwright/test';
|
||||
import { expect, test } from './fixtures';
|
||||
import {
|
||||
decodeJwtPayload,
|
||||
detectOrgSelectState,
|
||||
extractOrgIdFromTokenPayload,
|
||||
resolveOrgContextForCoursePage,
|
||||
establishSession,
|
||||
waitForCourseRealmVisible,
|
||||
} from './helpers/music-room-session';
|
||||
|
||||
const moicenUnionid = process.env.MOICEN_E2E_UNIONID?.trim();
|
||||
|
||||
function decodeJwtPayload(token: string): Record<string, unknown> | null {
|
||||
const raw = token.startsWith('Bearer ') ? token.slice(7).trim() : token;
|
||||
const parts = raw.split('.');
|
||||
if (parts.length !== 3) return null;
|
||||
try {
|
||||
const base64 = parts[1].replace(/-/g, '+').replace(/_/g, '/');
|
||||
const padded = base64 + '='.repeat((4 - (base64.length % 4)) % 4);
|
||||
const json = Buffer.from(padded, 'base64').toString('utf8');
|
||||
return JSON.parse(json) as Record<string, unknown>;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** 与 `huike-front/src/main.ts` 中 `parseCurrentOrgIdFromToken` 一致:claims 可能在顶层或嵌在 `sub` JSON 内 */
|
||||
function extractOrgIdFromTokenPayload(
|
||||
payload: Record<string, unknown> | null
|
||||
): string | undefined {
|
||||
if (!payload) return undefined;
|
||||
const top = payload.current_org_id;
|
||||
if (typeof top === 'string' && top.length > 0) return top;
|
||||
const sub = payload.sub;
|
||||
if (typeof sub !== 'string' || sub.length === 0) return undefined;
|
||||
try {
|
||||
const inner = JSON.parse(sub) as Record<string, unknown>;
|
||||
const innerOrg = inner.current_org_id;
|
||||
if (typeof innerOrg === 'string' && innerOrg.length > 0) return innerOrg;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* 教学资源相关任意壳层可见即可(`/course/summary` 入口链或课程体系列表)。
|
||||
* CI 上 SPA 长连接会导致 networkidle 不可靠;此处只用 DOM 信号轮询。
|
||||
*/
|
||||
async function waitForCourseRealmVisible(page: Page) {
|
||||
const deadline = Date.now() + 95_000;
|
||||
while (Date.now() < deadline) {
|
||||
const titleHit = await page.title().catch(() => '');
|
||||
if (/教学资源库|课程体系/.test(titleHit)) {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
await page.getByRole('link', { name: '课程体系' }).isVisible().catch(() => false)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
for (const label of ['课程体系', '课程', '课节']) {
|
||||
if (
|
||||
await page
|
||||
.locator('.van-cell')
|
||||
.filter({ hasText: new RegExp(label) })
|
||||
.first()
|
||||
.isVisible()
|
||||
.catch(() => false)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (
|
||||
await page
|
||||
.getByPlaceholder('请输入课程体系名称搜索')
|
||||
.isVisible()
|
||||
.catch(() => false)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
await page.locator('.van-search').first().isVisible().catch(() => false)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
await page.waitForTimeout(400);
|
||||
}
|
||||
throw new Error(`教学资源壳层未出现:${page.url()}`);
|
||||
}
|
||||
|
||||
type OrgSelectState = 'ready' | 'empty' | 'guest' | 'timeout';
|
||||
|
||||
async function detectOrgSelectState(page: Page, timeoutMs = 90_000): Promise<OrgSelectState> {
|
||||
const deadline = Date.now() + timeoutMs;
|
||||
while (Date.now() < deadline) {
|
||||
const state = await page.evaluate(() => {
|
||||
const appRoot = document.querySelector('#app');
|
||||
if (!appRoot) return 'pending';
|
||||
const rootText = appRoot.textContent ?? '';
|
||||
if (rootText.includes('请返回微信小程序完成登录')) return 'guest';
|
||||
if (rootText.includes('暂无可用机构')) return 'empty';
|
||||
if (rootText.includes('请选择机构')) return 'ready';
|
||||
if (appRoot.querySelector('.van-cell-group .van-cell')) return 'ready';
|
||||
return 'pending';
|
||||
});
|
||||
if (state === 'guest' || state === 'empty' || state === 'ready') {
|
||||
return state;
|
||||
}
|
||||
await page.waitForTimeout(500);
|
||||
}
|
||||
return 'timeout';
|
||||
}
|
||||
|
||||
async function establishSession(page: 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 });
|
||||
if (
|
||||
await page.getByText('请选择您的登录身份').isVisible().catch(() => false)
|
||||
) {
|
||||
await page.locator('.van-grid-item').first().click();
|
||||
}
|
||||
await expect(
|
||||
page.getByText(/请选择您的登录身份|欢迎回来|进入工作台/)
|
||||
).toBeVisible({ timeout: 120_000 });
|
||||
}
|
||||
|
||||
/**
|
||||
* 停留在 `/` 时 main.ts 不会把 JWT 中的机构写入 CurrentOrgId。
|
||||
* 深链会触发守卫;多机构且无上下文时会先进入 `/org/select`,需点选机构后再继续。
|
||||
* 交替尝试 summary 与课程索引:部分账号在 CI 上只对其中之一稳定渲染壳层。
|
||||
*/
|
||||
async function resolveOrgContextForCoursePage(page: Page) {
|
||||
const deepPaths = ['/course/summary', '/course'] as const;
|
||||
|
||||
for (let attempt = 0; attempt < 8; attempt++) {
|
||||
const targetPath = deepPaths[attempt % deepPaths.length];
|
||||
await page.goto(targetPath, {
|
||||
waitUntil: 'domcontentloaded',
|
||||
timeout: 90_000,
|
||||
});
|
||||
await expect(page.locator('#app')).toBeVisible({ timeout: 90_000 });
|
||||
|
||||
if (page.url().includes('/org/select')) {
|
||||
const orgSelectState = await detectOrgSelectState(page, 60_000);
|
||||
if (orgSelectState === 'guest') {
|
||||
test.skip(true, '深链进入机构页时会话退回访客态');
|
||||
}
|
||||
if (orgSelectState === 'empty') {
|
||||
test.skip(true, '账号无可用机构,跳过机构上下文用例');
|
||||
}
|
||||
if (orgSelectState === 'timeout') {
|
||||
throw new Error(`机构页未渲染就绪信号:${page.url()}`);
|
||||
}
|
||||
|
||||
const pickCells = page.locator('#app .van-cell-group .van-cell');
|
||||
const n = await pickCells.count();
|
||||
expect(n, '机构选择页应有可选机构').toBeGreaterThan(0);
|
||||
await pickCells.first().click();
|
||||
await expect(page.locator('#app')).toBeVisible({ timeout: 90_000 });
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
const path = new URL(page.url()).pathname || '';
|
||||
const onCourseRealm =
|
||||
path === '/course' || path.startsWith('/course/');
|
||||
if (onCourseRealm) {
|
||||
await waitForCourseRealmVisible(page);
|
||||
return;
|
||||
}
|
||||
} catch {
|
||||
/* URL 解析失败则继续重试 */
|
||||
}
|
||||
|
||||
// 守卫可能暂时送回首页「进入工作台」等,下一循环再深链
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
'resolveOrgContextForCoursePage: 无法在合理步数内进入教学资源链(/course/summary 等)'
|
||||
);
|
||||
}
|
||||
|
||||
test.describe('多机构数据隔离(会话与 token)', () => {
|
||||
test.describe.configure({ timeout: 180_000 });
|
||||
|
||||
test.skip(!moicenUnionid, '需要 MOICEN_E2E_UNIONID(Secret 或 .env.e2e)');
|
||||
|
||||
test('本地 CurrentOrgId 与 JWT current_org_id 一致', async ({ page }) => {
|
||||
await establishSession(page);
|
||||
await establishSession(page, moicenUnionid!);
|
||||
if (
|
||||
await page.getByText('请返回微信小程序完成登录').isVisible().catch(() => false)
|
||||
) {
|
||||
@@ -214,7 +43,7 @@ test.describe('多机构数据隔离(会话与 token)', () => {
|
||||
test('机构选择页存在多个机构时,切换后 CurrentOrgId 变化', async ({
|
||||
page,
|
||||
}) => {
|
||||
await establishSession(page);
|
||||
await establishSession(page, moicenUnionid!);
|
||||
if (
|
||||
await page.getByText('请返回微信小程序完成登录').isVisible().catch(() => false)
|
||||
) {
|
||||
@@ -302,7 +131,7 @@ test.describe('多机构数据隔离(会话与 token)', () => {
|
||||
test('课程体系页加载成功且不进入列表错误态(机构维度接口可用)', async ({
|
||||
page,
|
||||
}) => {
|
||||
await establishSession(page);
|
||||
await establishSession(page, moicenUnionid!);
|
||||
if (
|
||||
await page.getByText('请返回微信小程序完成登录').isVisible().catch(() => false)
|
||||
) {
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import { expect, test } from './fixtures';
|
||||
|
||||
function frontBaseUrl(): string {
|
||||
const raw =
|
||||
process.env.HUIKE_FRONT_BASE_URL?.trim() ||
|
||||
'https://music-room.moicen.com';
|
||||
return raw.replace(/\/$/, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* 不依赖浏览器:验证网关返回 H5 入口文档(部署/ DNS / TLS / Nginx 全链路底线)。
|
||||
*/
|
||||
test.describe('网关与 H5 文档(request)', () => {
|
||||
test('GET / 返回 HTML', async ({ request }) => {
|
||||
const res = await request.get(`${frontBaseUrl()}/`);
|
||||
expect(res.ok(), `HTTP ${res.status()} ${res.statusText()}`).toBeTruthy();
|
||||
const ct = res.headers()['content-type'] ?? '';
|
||||
expect(ct).toMatch(/text\/html/i);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user