fix(e2e): 教学资源链从 /course/summary 建立上下文;fixtures 捕获 console/vConsole 与 OrgSwitchDebug

Made-with: Cursor
This commit is contained in:
2026-04-28 08:45:12 +08:00
parent 3305a60a33
commit f590f1ba72
7 changed files with 137 additions and 23 deletions
+1 -1
View File
@@ -1,4 +1,4 @@
import { test, expect } from '@playwright/test';
import { expect, test } from './fixtures';
const adminUser = process.env.MOICEN_ADMIN_USER?.trim();
const adminPassword = process.env.MOICEN_ADMIN_PASSWORD?.trim();
+76
View File
@@ -0,0 +1,76 @@
import { test as base, expect, type Page } from '@playwright/test';
async function attachFrontFailureArtifacts(
page: Page,
testInfo: {
status?: string;
attach: (name: string, body: Buffer) => Promise<void>;
}
) {
const st = testInfo.status;
if (st === 'passed' || st === 'skipped') return;
try {
const snap = await page.evaluate(() => ({
OrgSwitchDebug: window.localStorage.getItem('OrgSwitchDebug'),
CurrentOrgId: window.localStorage.getItem('CurrentOrgId'),
path: window.location.pathname,
}));
await testInfo.attach(
'front-localstorage-snapshot.json',
Buffer.from(JSON.stringify(snap, null, 2), 'utf8')
);
} catch {
/* 页面可能已关闭 */
}
}
/**
* auto fixture:挂载 console / PageError / requestfailed(含 vConsole 镜像到 console 的输出),
* 仅在用例失败时附加 browser-console-tail.txt 与 OrgSwitchDebug 快照。
*/
export const test = base.extend<{ _frontLogs: void }>({
_frontLogs: [
async ({ page }, use, testInfo) => {
const lines: string[] = [];
const push = (line: string) => {
lines.push(line);
if (lines.length > 400) lines.splice(0, lines.length - 400);
};
page.on('console', (msg) => {
const loc = msg.location();
const where =
loc.url && loc.lineNumber != null
? ` ${loc.url}:${loc.lineNumber}`
: '';
push(`[browser:${msg.type()}] ${msg.text()}${where}`);
});
page.on('pageerror', (err) => {
push(`[pageerror] ${err.stack || err.message}`);
});
page.on('requestfailed', (req) => {
push(
`[requestfailed] ${req.method()} ${req.url()} ${req.failure()?.errorText ?? ''}`
);
});
await use(undefined);
await attachFrontFailureArtifacts(page, testInfo);
if (testInfo.status !== 'passed' && testInfo.status !== 'skipped') {
const tail = lines.slice(-200).join('\n');
if (tail.length > 0) {
await testInfo.attach(
'browser-console-tail.txt',
Buffer.from(tail, 'utf8')
);
}
}
},
{ auto: true },
],
});
export { expect };
+1 -1
View File
@@ -1,4 +1,4 @@
import { test, expect } from '@playwright/test';
import { expect, test } from './fixtures';
/**
* 未登录(无 token、无有效 unionid 登录链):与 `huike-front` `index.vue` 一致,
+1 -1
View File
@@ -1,4 +1,4 @@
import { test, expect } from '@playwright/test';
import { expect, test } from './fixtures';
// 对已部署 H5:匿名、伪造 unionid、page_path 净化(与 huike-front main.ts 一致)
test.describe('music-room shell', () => {
+1 -1
View File
@@ -1,4 +1,4 @@
import { test, expect } from '@playwright/test';
import { expect, test } from './fixtures';
const moicenUnionid = process.env.MOICEN_E2E_UNIONID?.trim();
+56 -18
View File
@@ -1,4 +1,5 @@
import { test, expect, type Page } from '@playwright/test';
import type { Page } from '@playwright/test';
import { expect, test } from './fixtures';
const moicenUnionid = process.env.MOICEN_E2E_UNIONID?.trim();
@@ -35,17 +36,46 @@ function extractOrgIdFromTokenPayload(
return undefined;
}
/** 课程体系列表壳:搜索框或 van-search(不同构建/加载时机下占位符可能尚未绑定) */
async function waitForCourseListShell(page: Page) {
await Promise.race([
page
.getByPlaceholder('请输入课程体系名称搜索')
.waitFor({ state: 'visible', timeout: 90_000 }),
page.locator('.van-search').first().waitFor({
state: 'visible',
timeout: 90_000,
}),
]);
/**
* 教学资源相关任意壳层可见即可(`/course/summary` 入口链或课程体系列表)。
* CI 上课程索引偶发长时间无搜索框;summary 页 Cell 更稳定。
*/
async function waitForCourseRealmVisible(page: Page) {
const deadline = Date.now() + 95_000;
while (Date.now() < deadline) {
if (
await page.getByRole('link', { name: '课程体系' }).isVisible().catch(() => false)
) {
return;
}
if (
await page
.locator('.van-cell')
.filter({ hasText: /^课程体系$/ })
.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()}`
);
}
async function establishSession(page: Page) {
@@ -74,7 +104,7 @@ async function establishSession(page: Page) {
*/
async function resolveOrgContextForCoursePage(page: Page) {
for (let attempt = 0; attempt < 6; attempt++) {
await page.goto('/course', {
await page.goto('/course/summary', {
waitUntil: 'domcontentloaded',
timeout: 90_000,
});
@@ -108,17 +138,20 @@ async function resolveOrgContextForCoursePage(page: Page) {
continue;
}
if (page.url().includes('/course')) {
if (
page.url().includes('/course/summary') ||
page.url().includes('/course')
) {
await page.waitForLoadState('networkidle', { timeout: 60_000 }).catch(() => {});
await waitForCourseListShell(page);
await waitForCourseRealmVisible(page);
return;
}
// 守卫可能暂时送回首页「进入工作台」等,下一循环再深链 `/course`
// 守卫可能暂时送回首页「进入工作台」等,下一循环再深链
}
throw new Error(
'resolveOrgContextForCoursePage: 无法在合理步数内进入 /course(可能被重定向离开课程体系'
'resolveOrgContextForCoursePage: 无法在合理步数内进入教学资源链(/course/summary 等'
);
}
@@ -234,7 +267,12 @@ test.describe('多机构数据隔离(会话与 token', () => {
await resolveOrgContextForCoursePage(page);
await waitForCourseListShell(page);
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('请求失败,点击重新加载')
+1 -1
View File
@@ -1,4 +1,4 @@
import { test, expect } from '@playwright/test';
import { expect, test } from './fixtures';
const moicenUnionid = process.env.MOICEN_E2E_UNIONID?.trim();