fix(e2e): 教学资源链从 /course/summary 建立上下文;fixtures 捕获 console/vConsole 与 OrgSwitchDebug
Made-with: Cursor
This commit is contained in:
@@ -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();
|
||||
|
||||
@@ -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,4 +1,4 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { expect, test } from './fixtures';
|
||||
|
||||
/**
|
||||
* 未登录(无 token、无有效 unionid 登录链):与 `huike-front` `index.vue` 一致,
|
||||
|
||||
@@ -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,4 +1,4 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { expect, test } from './fixtures';
|
||||
|
||||
const moicenUnionid = process.env.MOICEN_E2E_UNIONID?.trim();
|
||||
|
||||
|
||||
@@ -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
|
||||
/**
|
||||
* 教学资源相关任意壳层可见即可(`/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('请输入课程体系名称搜索')
|
||||
.waitFor({ state: 'visible', timeout: 90_000 }),
|
||||
page.locator('.van-search').first().waitFor({
|
||||
state: 'visible',
|
||||
timeout: 90_000,
|
||||
}),
|
||||
]);
|
||||
.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,4 +1,4 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { expect, test } from './fixtures';
|
||||
|
||||
const moicenUnionid = process.env.MOICEN_E2E_UNIONID?.trim();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user