Files
huike-e2e-moicen/tests/department-single-transparent.spec.ts
T
weli d470e2e95e fix(department-e2e): use waitFor for role selector to avoid race condition
Replace one-shot isVisible() with waitFor() to handle the timing gap
where the SPA route guard is still resolving roles asynchronously.
The role selector renders after the route guard's read() call completes,
and isVisible() may return false if called before that.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-03 07:57:54 +08:00

199 lines
8.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { expect, test } from './fixtures';
/**
* 部门默认透明测试
*
* 测试 CI unionid 测试用户,其所有机构都只有 1 个 active 部门(默认部门),
* 因此前端不应出现部门选择入口:
* - localStorage 自动写入 CurrentDepartmentId
* - 页面路径不包含 department 参数
* - 角色选择、排课列表、课包商店等已有页面不受影响
*
* 注意:CI 用户可能有多机构,需要先选机构,再触发部门自动选中。
*/
const moicenUnionid = process.env.MOICEN_E2E_UNIONID?.trim();
/**
* 登录 → 依次处理机构选择 + 身份选择 → 到达首页
* 适用于多机构用户的场景
*/
async function loginAndDismissSelectors(page: any) {
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 });
// 等待 auth tokens
await expect(async () => {
const auth = await page.evaluate(() =>
window.localStorage.getItem('Authorization')
);
expect(auth).toBeTruthy();
}).toPass({ timeout: 30_000 });
// 逐层处理选择页:机构选择 → 身份选择(可能有 0~n 层)
for (let i = 0; i < 5; i++) {
const path = await page.evaluate(() => window.location.pathname);
console.log(`[DeptTest] iteration ${i}: path=${path}`);
console.log(`[DeptTest] has Authorization:`, !!await page.evaluate(() => window.localStorage.getItem('Authorization')));
console.log(`[DeptTest] has HtySudoerToken:`, !!await page.evaluate(() => window.localStorage.getItem('HtySudoerToken')));
if (path === '/org/select') {
await page.locator('.van-cell').first().click();
console.log('[DeptTest] clicked org cell');
await page.waitForTimeout(3_000);
} else if (path === '/') {
// 用 waitFor 避免 SPA 路由守卫异步加载角色时的竞态(isVisible 可能返回 false
const rs = page.getByText('请选择您的登录身份');
try {
await rs.waitFor({ state: 'visible', timeout: 10_000 });
await page.locator('.van-grid-item').first().click();
console.log('[DeptTest] clicked role');
await page.waitForTimeout(3_000);
} catch {
console.log('[DeptTest] at / but no role selector');
// 无角色选择器 → 当前用户角色已自动选中(1个活跃角色)。
// chooseRole 内部调用 router.push('/role/profile') 是异步的,
// 等待重定向完成,让该 navigtion 的 route guard 走完 org/dept 加载。
// 避免后续 page.goto 的完整页面加载冲掉未完成的 SPA 导航。
try {
await page.waitForFunction(
() => window.location.pathname !== '/',
{ timeout: 10_000 }
);
const newPath = await page.evaluate(() => window.location.pathname);
console.log('[DeptTest] role auto-redirected to', newPath);
// route guard 已在这个导航中完成 org/dept 加载,CurrentDepartmentId 已写入
} catch {
// 10s 内未重定向(可能没有活跃角色),不做处理
console.log('[DeptTest] no redirect within 10s (0 roles?)');
}
break;
}
} else {
console.log('[DeptTest] unexpected path, break');
break;
}
}
}
test.describe('单部门透明', () => {
test.skip(!moicenUnionid, '需要 MOICEN_E2E_UNIONIDSecret 或 .env.e2e');
function decodeJwtPayloadCompact(token: string): Record<string, unknown> {
const parts = token.split('.');
if (parts.length < 2) return {};
const raw = parts[1].replace(/-/g, '+').replace(/_/g, '/');
const padded = raw.padEnd(Math.ceil(raw.length / 4) * 4, '=');
const decoded = decodeURIComponent(
atob(padded)
.split('')
.map(c => `%${(`00${c.charCodeAt(0).toString(16)}`).slice(-2)}`)
.join('')
);
return JSON.parse(decoded);
}
test('自动选中默认部门,不出现部门选择 UI', async ({ page }) => {
test.setTimeout(180_000);
await loginAndDismissSelectors(page);
// 检查登录后的 JWT
const loginJwt = await page.evaluate(() => window.localStorage.getItem('Authorization'));
if (loginJwt) {
const payload = decodeJwtPayloadCompact(loginJwt);
let sub: Record<string, unknown> = {};
if (typeof payload.sub === 'string') sub = JSON.parse(payload.sub);
console.log('[DeptTest JWT before] top-level keys:', Object.keys(payload));
console.log('[DeptTest JWT before] payload.current_org_id:', payload.current_org_id);
console.log('[DeptTest JWT before] sub.current_org_id:', (sub as any).current_org_id);
console.log('[DeptTest JWT before] sub.current_department_id:', (sub as any).current_department_id);
}
// 导航到排课页触发路由守卫中的 org 加载 + 部门自动选中
await page.goto('/clazz', {
waitUntil: 'networkidle',
timeout: 90_000,
});
await expect(page.locator('#app')).toBeVisible({ timeout: 60_000 });
// 打印当前页面状态
const pageState = await page.evaluate(() => {
const ls = (k: string) => window.localStorage.getItem(k);
return {
pathname: window.location.pathname,
CurrentOrgId: ls('CurrentOrgId'),
CurrentDepartmentId: ls('CurrentDepartmentId'),
hasAuth: !!ls('Authorization'),
hasSudo: !!ls('HtySudoerToken'),
guardError: ls('__guardError'),
orgSwitchDebug: ls('OrgSwitchDebug'),
};
});
console.log('[DeptTest] state after goto:', JSON.stringify(pageState, null, 2));
console.log('[DeptTest] deptId after goto:', pageState.CurrentDepartmentId);
// 如果部门尚未设置,给 route guard 额外时间完成异步加载
if (!pageState.CurrentDepartmentId) {
console.log('[DeptTest] waiting extra for department loading...');
await page.waitForTimeout(8_000);
const retry = await page.evaluate(() => window.localStorage.getItem('CurrentDepartmentId'));
console.log('[DeptTest] CurrentDepartmentId after extra wait:', retry);
if (retry) {
pageState.CurrentDepartmentId = retry;
pageState.pathname = await page.evaluate(() => window.location.pathname);
}
}
// 验证 localStorage 写入了 CurrentDepartmentId
const deptId = pageState.CurrentDepartmentId;
expect(deptId).toBeTruthy();
expect(deptId).toContain('dept_default_');
// 验证页面路径不包含 department 相关参数
expect(pageState.pathname).not.toContain('department');
// 验证 token 中包含 department_id
const authToken = await page.evaluate(() =>
window.localStorage.getItem('Authorization')
);
expect(authToken).toBeTruthy();
if (authToken) {
const parts = authToken.split('.');
expect(parts.length).toBe(3);
const payloadRaw = parts[1].replace(/-/g, '+').replace(/_/g, '/');
const payload = JSON.parse(atob(payloadRaw));
console.log('[JWT Debug] top-level keys:', Object.keys(payload));
console.log('[JWT Debug] payload.current_department_id:', payload.current_department_id);
console.log('[JWT Debug] payload.current_org_id:', payload.current_org_id);
let subjectPayload: Record<string, unknown> = {};
if (typeof payload.sub === 'string') {
subjectPayload = JSON.parse(payload.sub);
console.log('[JWT Debug] sub keys:', Object.keys(subjectPayload));
console.log('[JWT Debug] sub.current_department_id:', subjectPayload.current_department_id);
console.log('[JWT Debug] sub.current_org_id:', subjectPayload.current_org_id);
console.log('[JWT Debug] sub.current_org_id type:', typeof subjectPayload.current_org_id);
}
const deptIdInJwt = payload.current_department_id || subjectPayload.current_department_id;
expect(deptIdInJwt).toBeTruthy();
expect(typeof deptIdInJwt).toBe('string');
}
});
test('已有部门上下文时不重复加载部门列表', async ({ page }) => {
await loginAndDismissSelectors(page);
// 导航到排课页触发 org 加载 + 部门自动选中,验证页面正常渲染
await page.goto('/clazz', {
waitUntil: 'domcontentloaded',
timeout: 60_000,
});
await expect(page.locator('#app')).toBeVisible({ timeout: 60_000 });
});
});