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:
+219
-221
@@ -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 });
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user