diff --git a/tests/course-package-store.spec.ts b/tests/course-package-store.spec.ts index 885f745..aab877f 100644 --- a/tests/course-package-store.spec.ts +++ b/tests/course-package-store.spec.ts @@ -209,7 +209,7 @@ test.describe('课包商店(已登录学生查看老师主页)', () => { // Handle org select if needed if (page.url().includes('/org/select')) { - if (await page.getByText('请返回微信小程序完成登录').isVisible().catch(() => false)) { + if (await page.locator('.course-package-store').isVisible().catch(() => false)) { test.skip(true, '会话不可用'); return; } @@ -253,3 +253,234 @@ test.describe('课包商店(已登录学生查看老师主页)', () => { expect(await previewBtns.count()).toBe(cardCount); }); }); + +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); + } + } + + // Handle org select if needed + if (page.url().includes('/org/select')) { + if (await page.locator('.course-package-store').isVisible().catch(() => false)) { + test.skip(true, '会话不可用'); + 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, + }); + 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 }); + }); +});