From f590f1ba726e61c46d0ee735663009f3b4b8c2a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=98=BF=E7=94=B7?= Date: Tue, 28 Apr 2026 08:45:12 +0800 Subject: [PATCH] =?UTF-8?q?fix(e2e):=20=E6=95=99=E5=AD=A6=E8=B5=84?= =?UTF-8?q?=E6=BA=90=E9=93=BE=E4=BB=8E=20/course/summary=20=E5=BB=BA?= =?UTF-8?q?=E7=AB=8B=E4=B8=8A=E4=B8=8B=E6=96=87=EF=BC=9Bfixtures=20?= =?UTF-8?q?=E6=8D=95=E8=8E=B7=20console/vConsole=20=E4=B8=8E=20OrgSwitchDe?= =?UTF-8?q?bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Made-with: Cursor --- tests/admin-tasks.spec.ts | 2 +- tests/fixtures.ts | 76 +++++++++++++++++++++++++++ tests/guest-onboarding.spec.ts | 2 +- tests/home-shell.spec.ts | 2 +- tests/logged-in-and-isolation.spec.ts | 2 +- tests/org-data-isolation.spec.ts | 74 +++++++++++++++++++------- tests/org-multi-tenant.spec.ts | 2 +- 7 files changed, 137 insertions(+), 23 deletions(-) create mode 100644 tests/fixtures.ts diff --git a/tests/admin-tasks.spec.ts b/tests/admin-tasks.spec.ts index 11d37c7..7861c99 100644 --- a/tests/admin-tasks.spec.ts +++ b/tests/admin-tasks.spec.ts @@ -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(); diff --git a/tests/fixtures.ts b/tests/fixtures.ts new file mode 100644 index 0000000..c56d427 --- /dev/null +++ b/tests/fixtures.ts @@ -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; + } +) { + 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 }; diff --git a/tests/guest-onboarding.spec.ts b/tests/guest-onboarding.spec.ts index 2c3b58f..c2d33b3 100644 --- a/tests/guest-onboarding.spec.ts +++ b/tests/guest-onboarding.spec.ts @@ -1,4 +1,4 @@ -import { test, expect } from '@playwright/test'; +import { expect, test } from './fixtures'; /** * 未登录(无 token、无有效 unionid 登录链):与 `huike-front` `index.vue` 一致, diff --git a/tests/home-shell.spec.ts b/tests/home-shell.spec.ts index 04ab184..a320873 100644 --- a/tests/home-shell.spec.ts +++ b/tests/home-shell.spec.ts @@ -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', () => { diff --git a/tests/logged-in-and-isolation.spec.ts b/tests/logged-in-and-isolation.spec.ts index 10f8973..4b585c0 100644 --- a/tests/logged-in-and-isolation.spec.ts +++ b/tests/logged-in-and-isolation.spec.ts @@ -1,4 +1,4 @@ -import { test, expect } from '@playwright/test'; +import { expect, test } from './fixtures'; const moicenUnionid = process.env.MOICEN_E2E_UNIONID?.trim(); diff --git a/tests/org-data-isolation.spec.ts b/tests/org-data-isolation.spec.ts index a4dd29b..2ec4d3e 100644 --- a/tests/org-data-isolation.spec.ts +++ b/tests/org-data-isolation.spec.ts @@ -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('请求失败,点击重新加载') diff --git a/tests/org-multi-tenant.spec.ts b/tests/org-multi-tenant.spec.ts index 615fc99..828b8cb 100644 --- a/tests/org-multi-tenant.spec.ts +++ b/tests/org-multi-tenant.spec.ts @@ -1,4 +1,4 @@ -import { test, expect } from '@playwright/test'; +import { expect, test } from './fixtures'; const moicenUnionid = process.env.MOICEN_E2E_UNIONID?.trim();