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 adminUser = process.env.MOICEN_ADMIN_USER?.trim();
|
||||||
const adminPassword = process.env.MOICEN_ADMIN_PASSWORD?.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` 一致,
|
* 未登录(无 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 一致)
|
// 对已部署 H5:匿名、伪造 unionid、page_path 净化(与 huike-front main.ts 一致)
|
||||||
test.describe('music-room shell', () => {
|
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();
|
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();
|
const moicenUnionid = process.env.MOICEN_E2E_UNIONID?.trim();
|
||||||
|
|
||||||
@@ -35,17 +36,46 @@ function extractOrgIdFromTokenPayload(
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 课程体系列表壳:搜索框或 van-search(不同构建/加载时机下占位符可能尚未绑定) */
|
/**
|
||||||
async function waitForCourseListShell(page: Page) {
|
* 教学资源相关任意壳层可见即可(`/course/summary` 入口链或课程体系列表)。
|
||||||
await Promise.race([
|
* CI 上课程索引偶发长时间无搜索框;summary 页 Cell 更稳定。
|
||||||
page
|
*/
|
||||||
|
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('请输入课程体系名称搜索')
|
.getByPlaceholder('请输入课程体系名称搜索')
|
||||||
.waitFor({ state: 'visible', timeout: 90_000 }),
|
.isVisible()
|
||||||
page.locator('.van-search').first().waitFor({
|
.catch(() => false)
|
||||||
state: 'visible',
|
) {
|
||||||
timeout: 90_000,
|
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) {
|
async function establishSession(page: Page) {
|
||||||
@@ -74,7 +104,7 @@ async function establishSession(page: Page) {
|
|||||||
*/
|
*/
|
||||||
async function resolveOrgContextForCoursePage(page: Page) {
|
async function resolveOrgContextForCoursePage(page: Page) {
|
||||||
for (let attempt = 0; attempt < 6; attempt++) {
|
for (let attempt = 0; attempt < 6; attempt++) {
|
||||||
await page.goto('/course', {
|
await page.goto('/course/summary', {
|
||||||
waitUntil: 'domcontentloaded',
|
waitUntil: 'domcontentloaded',
|
||||||
timeout: 90_000,
|
timeout: 90_000,
|
||||||
});
|
});
|
||||||
@@ -108,17 +138,20 @@ async function resolveOrgContextForCoursePage(page: Page) {
|
|||||||
continue;
|
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 page.waitForLoadState('networkidle', { timeout: 60_000 }).catch(() => {});
|
||||||
await waitForCourseListShell(page);
|
await waitForCourseRealmVisible(page);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 守卫可能暂时送回首页「进入工作台」等,下一循环再深链 `/course`
|
// 守卫可能暂时送回首页「进入工作台」等,下一循环再深链
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new Error(
|
throw new Error(
|
||||||
'resolveOrgContextForCoursePage: 无法在合理步数内进入 /course(可能被重定向离开课程体系)'
|
'resolveOrgContextForCoursePage: 无法在合理步数内进入教学资源链(/course/summary 等)'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -234,7 +267,12 @@ test.describe('多机构数据隔离(会话与 token)', () => {
|
|||||||
|
|
||||||
await resolveOrgContextForCoursePage(page);
|
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(
|
await expect(
|
||||||
page.getByText('请求失败,点击重新加载')
|
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();
|
const moicenUnionid = process.env.MOICEN_E2E_UNIONID?.trim();
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user