From de058d1e5e12bc3ddf97d103d0d99091c4972ff5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=98=BF=E7=94=B7?= Date: Thu, 30 Apr 2026 14:31:25 +0800 Subject: [PATCH] test(course-package): add org_visible and course_package_item sync E2E tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - API test: course_group org_visible toggle (create → visible → list → invisible) - API test: course_package_item sync with SUPERVISOR permission check - UI test: "包含课节" picker link on course-package add page - UI test: "开放给机构" switch on course-group add page Co-Authored-By: Claude Opus 4.7 --- tests/course-package.spec.ts | 225 +++++++++++++++++++++++++++++++++++ 1 file changed, 225 insertions(+) diff --git a/tests/course-package.spec.ts b/tests/course-package.spec.ts index 9c85ffc..69ba7dc 100644 --- a/tests/course-package.spec.ts +++ b/tests/course-package.spec.ts @@ -213,6 +213,159 @@ test.describe('课包(course_package)', () => { expect(typeof orgBody.d?.[2] === 'number').toBe(true); }); + test('course_group org_visible 开关全链路:创建 → 设为机构可见 → 查询可见列表 → 取消可见 → 验证移除', async ({ + page, + request, + }) => { + const authToken = await loginAndGetJwt(page); + const headers: Record = { + Authorization: authToken, + HtySudoerToken: authToken, + HtyHost: new URL(kcBase).hostname, + }; + + const groupName = `e2e-test-org-visible-${Date.now()}`; + let groupId: string; + + // ---- CREATE course_group ---- + const createRes = await request.post(`${kcBase}/api/v1/ws/create_course_group`, { + headers: { ...headers, 'Content-Type': 'application/json' }, + data: { group_name: groupName }, + }); + expect(createRes.ok(), `CREATE GROUP HTTP ${createRes.status()}`).toBeTruthy(); + const createBody = await createRes.json(); + expect(createBody.r, `CREATE GROUP 业务失败: ${JSON.stringify(createBody)}`).toBe(true); + expect(createBody.d?.id).toBeTruthy(); + expect(createBody.d?.group_name).toBe(groupName); + groupId = createBody.d.id; + + try { + // ---- UPDATE org_visible=true ---- + const updateVisibleRes = await request.post(`${kcBase}/api/v1/ws/update_course_group`, { + headers: { ...headers, 'Content-Type': 'application/json' }, + data: { id: groupId, group_name: groupName, org_visible: true }, + }); + expect(updateVisibleRes.ok(), `UPDATE VISIBLE HTTP ${updateVisibleRes.status()}`).toBeTruthy(); + const updateVisibleBody = await updateVisibleRes.json(); + expect(updateVisibleBody.r, `UPDATE VISIBLE 业务失败: ${JSON.stringify(updateVisibleBody)}`).toBe(true); + + // ---- FIND org-visible groups - should include our group ---- + const visibleRes = await request.get(`${kcBase}/api/v1/ws/find_org_visible_course_groups`, { + headers, + }); + expect(visibleRes.ok(), `FIND VISIBLE HTTP ${visibleRes.status()}`).toBeTruthy(); + const visibleBody = await visibleRes.json(); + expect(visibleBody.r, `FIND VISIBLE 业务失败: ${JSON.stringify(visibleBody)}`).toBe(true); + const visibleList: any[] = visibleBody.d ?? []; + const foundVisible = visibleList.some((g: any) => g.id === groupId); + expect(foundVisible, 'org_visible=true 的分组应出现在机构可见列表中').toBe(true); + + // ---- UPDATE org_visible=false ---- + const updateInvisibleRes = await request.post(`${kcBase}/api/v1/ws/update_course_group`, { + headers: { ...headers, 'Content-Type': 'application/json' }, + data: { id: groupId, group_name: groupName, org_visible: false }, + }); + expect(updateInvisibleRes.ok(), `UPDATE INVISIBLE HTTP ${updateInvisibleRes.status()}`).toBeTruthy(); + + // ---- FIND org-visible groups - should NOT include our group ---- + const invisibleRes = await request.get(`${kcBase}/api/v1/ws/find_org_visible_course_groups`, { + headers, + }); + expect(invisibleRes.ok(), `FIND INVISIBLE HTTP ${invisibleRes.status()}`).toBeTruthy(); + const invisibleBody = await invisibleRes.json(); + expect(invisibleBody.r, `FIND INVISIBLE 业务失败: ${JSON.stringify(invisibleBody)}`).toBe(true); + const invisibleList: any[] = invisibleBody.d ?? []; + const foundInvisible = invisibleList.some((g: any) => g.id === groupId); + expect(foundInvisible, 'org_visible=false 的分组不应出现在机构可见列表中').toBe(false); + } finally { + // ---- CLEANUP: delete course_group ---- + await request.post(`${kcBase}/api/v1/ws/delete_course_group/${groupId}`, { headers }); + } + }); + + test('course_package_item sync 全链路(SUPERVISOR):创建分组 → 设 org_visible → 创建课包 → sync → 列表验证', async ({ + page, + request, + }) => { + const authToken = await loginAndGetJwt(page); + const payload = decodeJwtPayload(authToken); + const roles = extractRoleKeys(payload); + const isSupervisor = roles.includes('SUPERVISOR'); + test.skip(!isSupervisor, '当前账号无 SUPERVISOR 权限,跳过 sync 测试'); + + const headers: Record = { + Authorization: authToken, + HtySudoerToken: authToken, + HtyHost: new URL(kcBase).hostname, + }; + + const ts = Date.now(); + const groupName = `e2e-test-sync-group-${ts}`; + const pkgName = `e2e-test-sync-pkg-${ts}`; + let groupId: string; + let pkgId: string; + + // ---- CREATE course_group with org_visible=true ---- + const createGroupRes = await request.post(`${kcBase}/api/v1/ws/create_course_group`, { + headers: { ...headers, 'Content-Type': 'application/json' }, + data: { group_name: groupName }, + }); + expect(createGroupRes.ok(), `CREATE GROUP HTTP ${createGroupRes.status()}`).toBeTruthy(); + const createGroupBody = await createGroupRes.json(); + expect(createGroupBody.r, `CREATE GROUP 业务失败: ${JSON.stringify(createGroupBody)}`).toBe(true); + groupId = createGroupBody.d.id; + + await request.post(`${kcBase}/api/v1/ws/update_course_group`, { + headers: { ...headers, 'Content-Type': 'application/json' }, + data: { id: groupId, group_name: groupName, org_visible: true }, + }); + + try { + // ---- CREATE course_package ---- + const createPkgRes = await request.post(`${kcBase}/api/v1/clazz/course-package/create`, { + headers: { ...headers, 'Content-Type': 'application/json' }, + data: { + package_name: pkgName, + description: 'e2e test sync', + package_status: 'ACTIVE', + }, + }); + expect(createPkgRes.ok(), `CREATE PKG HTTP ${createPkgRes.status()}`).toBeTruthy(); + const createPkgBody = await createPkgRes.json(); + expect(createPkgBody.r, `CREATE PKG 业务失败: ${JSON.stringify(createPkgBody)}`).toBe(true); + expect(createPkgBody.d?.id).toBeTruthy(); + pkgId = createPkgBody.d.id; + + // ---- SYNC items ---- + const syncRes = await request.post(`${kcBase}/api/v1/clazz/course-package/item/sync`, { + headers: { ...headers, 'Content-Type': 'application/json' }, + data: { package_id: pkgId, course_group_ids: [groupId] }, + }); + expect(syncRes.ok(), `SYNC HTTP ${syncRes.status()}`).toBeTruthy(); + const syncBody = await syncRes.json(); + expect(syncBody.r, `SYNC 业务失败: ${JSON.stringify(syncBody)}`).toBe(true); + + // ---- LIST items ---- + const listRes = await request.get(`${kcBase}/api/v1/clazz/course-package/item/list/${pkgId}`, { + headers, + }); + expect(listRes.ok(), `LIST ITEMS HTTP ${listRes.status()}`).toBeTruthy(); + const listBody = await listRes.json(); + expect(listBody.r, `LIST ITEMS 业务失败: ${JSON.stringify(listBody)}`).toBe(true); + const items: any[] = listBody.d ?? []; + const matched = items.some((g: any) => g.id === groupId); + expect(matched, 'sync 后列表应包含该分组').toBe(true); + } finally { + // ---- CLEANUP ---- + if (pkgId) { + await request.post(`${kcBase}/api/v1/clazz/course-package/delete/${pkgId}`, { headers }).catch(() => {}); + } + if (groupId) { + await request.post(`${kcBase}/api/v1/ws/delete_course_group/${groupId}`, { headers }).catch(() => {}); + } + } + }); + test('教师端 UI:导航栏出现"课包"入口,页面加载种子数据', async ({ page, }) => { @@ -413,4 +566,76 @@ test.describe('音乐教室端(huike-front)课包 UI', () => { // 验证提交按钮 await expect(page.getByText('保存')).toBeVisible({ timeout: 10_000 }); }); + + test('课包新增页面:包含课节选择器可见', async ({ page }) => { + 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.waitForFunction( + () => !window.location.search.includes('status=2'), + { timeout: 90_000 }, + ); + if (await page.getByText('请选择您的登录身份').isVisible().catch(() => false)) { + await page.locator('.van-grid-item').first().click(); + await page.waitForFunction( + () => !window.location.search.includes('status=2'), + { timeout: 30_000 }, + ); + } + + const { resolveOrgContextForCoursePage } = await import('./helpers/music-room-session'); + await resolveOrgContextForCoursePage(page); + + await page.goto('/course/course-package/add', { + waitUntil: 'domcontentloaded', + timeout: 60_000, + }); + await expect(page.locator('#app')).toBeVisible({ timeout: 30_000 }); + + // 验证"包含课节"选择器链接可见 + const pickerCell = page.locator('.van-cell').filter({ hasText: '包含课节' }); + await expect(pickerCell).toBeVisible({ timeout: 10_000 }); + // 验证点击后跳转到选择页 + await pickerCell.click(); + await expect(page).toHaveURL(/\/course\/group\/pick/, { timeout: 10_000 }); + }); + + test('课节新增页面:开放给机构开关可见', async ({ page }) => { + 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.waitForFunction( + () => !window.location.search.includes('status=2'), + { timeout: 90_000 }, + ); + if (await page.getByText('请选择您的登录身份').isVisible().catch(() => false)) { + await page.locator('.van-grid-item').first().click(); + await page.waitForFunction( + () => !window.location.search.includes('status=2'), + { timeout: 30_000 }, + ); + } + + const { resolveOrgContextForCoursePage } = await import('./helpers/music-room-session'); + await resolveOrgContextForCoursePage(page); + + await page.goto('/course/group/add', { + waitUntil: 'domcontentloaded', + timeout: 60_000, + }); + await expect(page.locator('#app')).toBeVisible({ timeout: 30_000 }); + + // 验证"开放给机构"切换开关可见 + const orgVisibleCell = page.locator('.van-cell').filter({ hasText: '开放给机构' }); + await expect(orgVisibleCell).toBeVisible({ timeout: 10_000 }); + // 验证开关组件存在 + const switchEl = orgVisibleCell.locator('.van-switch'); + await expect(switchEl).toBeVisible({ timeout: 10_000 }); + }); });