diff --git a/tests/course-package-store.spec.ts b/tests/course-package-store.spec.ts index aab877f..f12810d 100644 --- a/tests/course-package-store.spec.ts +++ b/tests/course-package-store.spec.ts @@ -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 }); }); });