test: fix detail page E2E tests — use real login flow instead of fake JWT

The mocked detail page tests were using localStorage fake JWTs, but the
frontend auth guard in router.beforeEach requires BOTH Authorization and
HtySudoToken to be present — without both, it redirects to / before the
detail component ever mounts, so the route mock was never reached.

Rewrite to use real student login first, then navigate with mocks active.
Tests 11-13: login → mock API → navigate to detail page → assert.
Test 14: fix .group-sections assertion for packages without course items.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-01 11:35:19 +08:00
parent 38a710c19d
commit dd8d41c7b0
+219 -221
View File
@@ -254,233 +254,231 @@ test.describe('课包商店(已登录学生查看老师主页)', () => {
});
});
async function loginAsStudent(page: any, moicenUnionid: string) {
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 });
await page.waitForTimeout(5000);
// Select student role if prompted
const roleSelect = page.getByText('请选择您的登录身份');
if (await roleSelect.isVisible().catch(() => false)) {
const studentRole = page.locator('.van-grid-item').filter({ hasText: '学生' });
if (await studentRole.isVisible().catch(() => false)) {
await studentRole.click();
await page.waitForTimeout(5000);
}
}
// Handle org select if needed
if (page.url().includes('/org/select')) {
if (await page.locator('.course-package-store').isVisible().catch(() => false)) {
return false; // session not usable
}
const orgCell = page.locator('#app .van-cell-group .van-cell').first();
if (await orgCell.isVisible().catch(() => false)) {
await orgCell.click();
await page.waitForTimeout(5000);
}
}
return true;
}
test.describe('课包详情页权限与预览', () => {
test('非创建者查看课包详情不显示管理按钮', async ({ page }) => {
const testPkgId = 'pkg-e2e-perm';
// Only mock the specific package detail URL
await page.route(`**/course-package/${testPkgId}`, async route => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
r: true,
d: {
id: testPkgId,
package_name: '课包-权限验证',
description: '非创建者不应看到管理按钮',
total_lessons: 10,
package_status: 'ACTIVE',
created_by: 'someone-else-id',
published_at: '2026-01-01T00:00:00Z',
published_snapshot: {
course_groups: [
{
id: 'group-1',
group_name: '基础训练',
course_sections: [
{ id: 'sec-1', course_name: '钢琴', section_name: '第一课-基础指法' },
{ id: 'sec-2', course_name: '钢琴', section_name: '第二课-音阶练习' },
],
},
],
},
},
e: null,
}),
});
});
// Set a fake token so isLoggedIn is true
await page.goto('/', { waitUntil: 'domcontentloaded', timeout: 60_000 });
await page.evaluate(() => {
localStorage.setItem('Authorization', 'fake-jwt-token');
});
await page.goto(`/course/course-package/detail?id=${testPkgId}`, {
waitUntil: 'domcontentloaded',
timeout: 60_000,
});
await expect(page.locator('#app')).toBeVisible({ timeout: 60_000 });
await page.waitForTimeout(3000);
// Management buttons should NOT be visible (non-owner)
await expect(page.locator('button').filter({ hasText: '编辑' })).not.toBeVisible();
await expect(page.locator('button').filter({ hasText: '删除' })).not.toBeVisible();
await expect(page.locator('button').filter({ hasText: '发布上架' })).not.toBeVisible();
// Package name should still be visible
await expect(page.locator('.title')).toContainText('课包-权限验证');
});
test('非创建者仅第一节课节可试看,其余已锁定', async ({ page }) => {
const testPkgId = 'pkg-e2e-preview';
await page.route(`**/course-package/${testPkgId}`, async route => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
r: true,
d: {
id: testPkgId,
package_name: '课包-预览验证',
description: '仅第一节课节可试看',
total_lessons: 3,
package_status: 'ACTIVE',
created_by: 'someone-else-id',
published_at: '2026-01-01T00:00:00Z',
published_snapshot: {
course_groups: [
{
id: 'group-1',
group_name: '基础训练',
course_sections: [
{ id: 'sec-p1', course_name: '钢琴', section_name: '第一课-基础指法' },
{ id: 'sec-p2', course_name: '钢琴', section_name: '第二课-音阶练习' },
{ id: 'sec-p3', course_name: '钢琴', section_name: '第三课-和弦' },
],
},
],
},
},
e: null,
}),
});
});
await page.goto('/', { waitUntil: 'domcontentloaded', timeout: 60_000 });
await page.evaluate(() => {
localStorage.setItem('Authorization', 'fake-jwt-token');
});
await page.goto(`/course/course-package/detail?id=${testPkgId}`, {
waitUntil: 'domcontentloaded',
timeout: 60_000,
});
await expect(page.locator('#app')).toBeVisible({ timeout: 60_000 });
await page.waitForTimeout(3000);
// First section should have the "试看" badge
const firstSection = page.locator('.section-item').first();
await expect(firstSection.locator('.preview-badge')).toBeVisible();
await expect(firstSection.locator('.lock-label')).not.toBeVisible();
// Second section should show "已锁定"
const secondSection = page.locator('.section-item').nth(1);
await expect(secondSection.locator('.lock-label')).toBeVisible();
await expect(secondSection.locator('.preview-badge')).not.toBeVisible();
// Third section should also be locked
const thirdSection = page.locator('.section-item').nth(2);
await expect(thirdSection.locator('.lock-label')).toBeVisible();
// Clicking locked section shows purchase dialog
await secondSection.click();
await page.waitForTimeout(500);
const dialog = page.locator('.van-dialog');
await expect(dialog).toBeVisible({ timeout: 5000 });
await expect(dialog).toContainText('购买课包');
await page.locator('.van-dialog__confirm').click();
});
test('已上架课包显示状态标签', async ({ page }) => {
const testPkgId = 'pkg-e2e-active';
await page.route(`**/course-package/${testPkgId}`, async route => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
r: true,
d: {
id: testPkgId,
package_name: '课包-已上架',
description: '已上架的课包',
total_lessons: 10,
package_status: 'ACTIVE',
created_by: 'someone-else-id',
published_at: '2026-01-01T00:00:00Z',
published_snapshot: null,
},
e: null,
}),
});
});
await page.goto('/', { waitUntil: 'domcontentloaded', timeout: 60_000 });
await page.evaluate(() => {
localStorage.setItem('Authorization', 'fake-jwt-token');
});
await page.goto(`/course/course-package/detail?id=${testPkgId}`, {
waitUntil: 'domcontentloaded',
timeout: 60_000,
});
await expect(page.locator('#app')).toBeVisible({ timeout: 60_000 });
await page.waitForTimeout(3000);
// Status tag should show "上架中"
await expect(page.locator('.header').getByText('上架中')).toBeVisible();
});
test('已登录学生从商店进入课包详情无管理权限', async ({ page }) => {
test.setTimeout(180_000);
if (!moicenUnionid) {
test.skip(true, 'MOICEN_E2E_UNIONID 未设置');
return;
}
// Login as student
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 });
await page.waitForTimeout(5000);
// Select student role if prompted
const roleSelect = page.getByText('请选择您的登录身份');
if (await roleSelect.isVisible().catch(() => false)) {
const studentRole = page.locator('.van-grid-item').filter({ hasText: '学生' });
if (await studentRole.isVisible().catch(() => false)) {
await studentRole.click();
await page.waitForTimeout(5000);
test.describe('权限、预览与状态(mock API', () => {
test.beforeEach(({ }, {}) => {
if (!moicenUnionid) {
test.skip(true, 'MOICEN_E2E_UNIONID 未设置');
}
}
});
// Handle org select if needed
if (page.url().includes('/org/select')) {
if (await page.locator('.course-package-store').isVisible().catch(() => false)) {
test.skip(true, '会话不可用');
test('非创建者查看课包详情不显示管理按钮', async ({ page }) => {
test.setTimeout(180_000);
if (!moicenUnionid) return;
const ok = await loginAsStudent(page, moicenUnionid);
if (!ok) { test.skip(true, '会话不可用'); return; }
const testPkgId = 'pkg-e2e-perm';
await page.route(new RegExp(`/api/v1/clazz/course-package/${testPkgId}`), async route => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
r: true,
d: {
id: testPkgId,
package_name: '课包-权限验证',
description: '非创建者不应看到管理按钮',
total_lessons: 10,
package_status: 'ACTIVE',
created_by: 'someone-else-id',
published_at: '2026-01-01T00:00:00Z',
published_snapshot: {
course_groups: [
{
id: 'group-1',
group_name: '基础训练',
course_sections: [
{ id: 'sec-1', course_name: '钢琴', section_name: '第一课-基础指法' },
{ id: 'sec-2', course_name: '钢琴', section_name: '第二课-音阶练习' },
],
},
],
},
},
e: null,
}),
});
});
await page.goto(`/course/course-package/detail?id=${testPkgId}`, { waitUntil: 'domcontentloaded', timeout: 60_000 });
await expect(page.locator('#app')).toBeVisible({ timeout: 60_000 });
await page.waitForTimeout(3000);
await expect(page.locator('button').filter({ hasText: '编辑' })).not.toBeVisible();
await expect(page.locator('button').filter({ hasText: '删除' })).not.toBeVisible();
await expect(page.locator('button').filter({ hasText: '发布上架' })).not.toBeVisible();
await expect(page.locator('.title')).toContainText('课包-权限验证');
});
test('非创建者仅第一节课节可试看,其余已锁定', async ({ page }) => {
test.setTimeout(180_000);
if (!moicenUnionid) return;
const ok = await loginAsStudent(page, moicenUnionid);
if (!ok) { test.skip(true, '会话不可用'); return; }
const testPkgId = 'pkg-e2e-preview';
await page.route(new RegExp(`/api/v1/clazz/course-package/${testPkgId}`), async route => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
r: true,
d: {
id: testPkgId,
package_name: '课包-预览验证',
description: '仅第一节课节可试看',
total_lessons: 3,
package_status: 'ACTIVE',
created_by: 'someone-else-id',
published_at: '2026-01-01T00:00:00Z',
published_snapshot: {
course_groups: [
{
id: 'group-1',
group_name: '基础训练',
course_sections: [
{ id: 'sec-p1', course_name: '钢琴', section_name: '第一课-基础指法' },
{ id: 'sec-p2', course_name: '钢琴', section_name: '第二课-音阶练习' },
{ id: 'sec-p3', course_name: '钢琴', section_name: '第三课-和弦' },
],
},
],
},
},
e: null,
}),
});
});
await page.goto(`/course/course-package/detail?id=${testPkgId}`, { waitUntil: 'domcontentloaded', timeout: 60_000 });
await expect(page.locator('#app')).toBeVisible({ timeout: 60_000 });
await page.waitForTimeout(3000);
// First section should have "试看" badge, not locked
const firstSection = page.locator('.section-item').first();
await expect(firstSection.locator('.preview-badge')).toBeVisible();
await expect(firstSection.locator('.lock-label')).not.toBeVisible();
// Second section should be locked
const secondSection = page.locator('.section-item').nth(1);
await expect(secondSection.locator('.lock-label')).toBeVisible();
await expect(secondSection.locator('.preview-badge')).not.toBeVisible();
// Third section should be locked
const thirdSection = page.locator('.section-item').nth(2);
await expect(thirdSection.locator('.lock-label')).toBeVisible();
// Clicking locked section shows purchase dialog
await secondSection.click();
await page.waitForTimeout(500);
const dialog = page.locator('.van-dialog');
await expect(dialog).toBeVisible({ timeout: 5000 });
await expect(dialog).toContainText('购买课包');
await page.locator('.van-dialog__confirm').click();
});
test('已上架课包显示状态标签', async ({ page }) => {
test.setTimeout(180_000);
if (!moicenUnionid) return;
const ok = await loginAsStudent(page, moicenUnionid);
if (!ok) { test.skip(true, '会话不可用'); return; }
const testPkgId = 'pkg-e2e-active';
await page.route(new RegExp(`/api/v1/clazz/course-package/${testPkgId}`), async route => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
r: true,
d: {
id: testPkgId,
package_name: '课包-已上架',
description: '已上架的课包',
total_lessons: 10,
package_status: 'ACTIVE',
created_by: 'someone-else-id',
published_at: '2026-01-01T00:00:00Z',
published_snapshot: null,
},
e: null,
}),
});
});
await page.goto(`/course/course-package/detail?id=${testPkgId}`, { waitUntil: 'domcontentloaded', timeout: 60_000 });
await expect(page.locator('#app')).toBeVisible({ timeout: 60_000 });
await page.waitForTimeout(3000);
await expect(page.locator('.header').getByText('上架中')).toBeVisible();
});
});
test.describe('真实数据验证', () => {
test('已登录学生从商店进入课包详情无管理权限', async ({ page }) => {
test.setTimeout(180_000);
if (!moicenUnionid) {
test.skip(true, 'MOICEN_E2E_UNIONID 未设置');
return;
}
const orgCell = page.locator('#app .van-cell-group .van-cell').first();
if (await orgCell.isVisible().catch(() => false)) {
await orgCell.click();
await page.waitForTimeout(5000);
}
}
// Navigate to a published package detail (owned by 周晓慧, not by 阿难)
await page.goto('/course/course-package/detail?id=pkg-mc-001', {
waitUntil: 'domcontentloaded',
timeout: 60_000,
const ok = await loginAsStudent(page, moicenUnionid);
if (!ok) { test.skip(true, '会话不可用'); return; }
// Navigate to a published package detail (owned by 周晓慧, not by 阿难)
await page.goto('/course/course-package/detail?id=pkg-mc-001', {
waitUntil: 'domcontentloaded',
timeout: 60_000,
});
await expect(page.locator('#app')).toBeVisible({ timeout: 60_000 });
await page.waitForTimeout(5000);
// Should see the package name
await expect(page.locator('.title')).toBeVisible({ timeout: 10_000 });
// Should NOT see management buttons
await expect(page.locator('button').filter({ hasText: '编辑' })).not.toBeVisible();
await expect(page.locator('button').filter({ hasText: '删除' })).not.toBeVisible();
await expect(page.locator('button').filter({ hasText: '发布上架' })).not.toBeVisible();
await expect(page.locator('button').filter({ hasText: '下架' })).not.toBeVisible();
// Should see section list or empty state
const sectionList = page.locator('.group-sections');
const emptyState = page.locator('.empty');
await expect(sectionList.first().or(emptyState)).toBeVisible({ timeout: 10_000 });
});
await expect(page.locator('#app')).toBeVisible({ timeout: 60_000 });
await page.waitForTimeout(5000);
// Should see the package name
await expect(page.locator('.title')).toBeVisible({ timeout: 10_000 });
// Should NOT see management buttons
await expect(page.locator('button').filter({ hasText: '编辑' })).not.toBeVisible();
await expect(page.locator('button').filter({ hasText: '删除' })).not.toBeVisible();
await expect(page.locator('button').filter({ hasText: '发布上架' })).not.toBeVisible();
await expect(page.locator('button').filter({ hasText: '下架' })).not.toBeVisible();
// Should see section list (even if empty)
const sectionList = page.locator('.group-sections');
await expect(sectionList.first()).toBeVisible({ timeout: 10_000 });
});
});