diff --git a/tests/course-package.spec.ts b/tests/course-package.spec.ts index 8e1c4ca..2c66901 100644 --- a/tests/course-package.spec.ts +++ b/tests/course-package.spec.ts @@ -605,6 +605,254 @@ test.describe('音乐教室端(huike-front)课包 UI', () => { await expect(page).toHaveURL(/\/course\/group\/pick/, { timeout: 10_000 }); }); + test('课包新增:选择课节 → 确认 → 课节应持久保留在新增页', async ({ page, request }) => { + test.info().annotations.push({ type: 'issue', description: '选择课节后确认,回到新增页课节标签不显示,保存后未关联课节' }); + + // ---- Login (select "教师" role, not the first grid item) ---- + 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').filter({ hasText: /^教师$/ }).click(); + await page.waitForFunction( + () => !window.location.search.includes('status=2'), + { timeout: 30_000 }, + ); + } + // Extract JWT for API calls + const authToken = await page.evaluate(() => window.localStorage.getItem('Authorization')); + expect(authToken, '登录后应有 JWT').toBeTruthy(); + + const headers: Record = { + Authorization: authToken!, + HtySudoerToken: authToken!, + HtyHost: new URL(kcBase).hostname, + }; + + // ---- Create a course_group FIRST (before picker loads data) ---- + const groupName = `e2e-ui-picker-${Date.now()}`; + const createRes = await request.post(`${kcBase}/api/v1/ws/create_course_group`, { + headers: { ...headers, 'Content-Type': 'application/json' }, + data: { group_name: groupName, org_visible: true }, + }); + expect(createRes.ok(), `CREATE GROUP HTTP ${createRes.status()}`).toBeTruthy(); + const createBody = await createRes.json(); + expect(createBody.r, `CREATE GROUP failed: ${JSON.stringify(createBody)}`).toBe(true); + const groupId = createBody.d; + test.info().annotations.push({ type: 'group', description: `created groupId=${groupId}` }); + + // ---- Resolve org context ---- + const { resolveOrgContextForCoursePage } = await import('./helpers/music-room-session'); + await resolveOrgContextForCoursePage(page); + + let createdPkgId: string | undefined; + + try { + // ---- Navigate to course-package add page ---- + await page.goto('/course/course-package/add', { waitUntil: 'domcontentloaded', timeout: 60_000 }); + await expect(page.locator('#app')).toBeVisible({ timeout: 30_000 }); + + // ---- Fill package name (required for form submit) ---- + const pkgName = `e2e-ui-pkg-${Date.now()}`; + await page.locator('input[name="package_name"]').fill(pkgName); + + // ---- Click "包含课节" to open picker ---- + 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 }); + + // ---- Switch to "我的课节" tab (default is "机构可见课节" for course-package) ---- + await page.locator('.van-tab').filter({ hasText: '我的课节' }).click(); + await page.waitForTimeout(1000); + + // ---- Select the group we created — it should be in "我的课节" tab ---- + const groupItem = page.locator('.course-item').filter({ hasText: groupName }); + await expect(groupItem).toBeVisible({ timeout: 30_000 }); + const circleIcon = groupItem.locator('.van-icon-circle'); + await expect(circleIcon).toBeVisible({ timeout: 5000 }); + await circleIcon.click(); + await page.waitForTimeout(500); + + // ---- Verify selection visual feedback ---- + await expect(groupItem.locator('.van-icon-checked')).toBeVisible({ timeout: 5000 }); + + // ---- Click "确认" button and wait for navigation ---- + const confirmBtn = page.locator('.btn-pick'); + await expect(confirmBtn).toBeEnabled({ timeout: 5000 }); + + // Wait for navigation (confirmBtn click → router.back()) + await Promise.all([ + page.waitForURL(/\/course\/course-package\/add/, { timeout: 15_000 }), + confirmBtn.click(), + ]); + await page.waitForTimeout(500); + await page.waitForSelector('input[name="package_name"]', { timeout: 10_000 }); + + // ---- CRITICAL CHECK: Should see the selected group tag ---- + await expect( + page.locator('.van-tag').filter({ hasText: groupName }) + ).toBeVisible({ timeout: 10_000 }); + + // ---- Re-fill package name (component was re-created after navigation) ---- + await page.locator('input[name="package_name"]').fill(pkgName); + + // ---- Submit the form ---- + await page.getByText('保存').click(); + + // ---- Verify via API that package was created with this group ---- + await page.waitForTimeout(2000); + const myRes = await request.get( + `${kcBase}/api/v1/clazz/course-package/my-packages?page=1&page_size=50`, + { headers }, + ); + expect(myRes.ok()).toBeTruthy(); + const myBody = await myRes.json(); + const myList: any[] = myBody.d?.[0] ?? []; + const newPkg = myList.find((p: any) => p.package_name === pkgName); + expect(newPkg, '创建的课包应出现在我的课包列表中').toBeTruthy(); + createdPkgId = newPkg.id; + + const itemsRes = await request.get( + `${kcBase}/api/v1/clazz/course-package/item/list/${createdPkgId}`, + { headers }, + ); + expect(itemsRes.ok()).toBeTruthy(); + const itemsBody = await itemsRes.json(); + const items: any[] = itemsBody.d ?? []; + const matched = items.some((g: any) => g.course_group_id === groupId); + expect(matched, '保存后课包应包含选中的课节分组').toBe(true); + } finally { + // ---- Cleanup ---- + if (createdPkgId) { + await request.post(`${kcBase}/api/v1/clazz/course-package/delete/${createdPkgId}`, { headers }).catch(() => {}); + } + await request.post(`${kcBase}/api/v1/ws/delete_course_group/${groupId}`, { headers }).catch(() => {}); + } + }); + + test('课包编辑:加载已有课节 → 选择新增课节 → 保存后两组均应存在', async ({ page, request }) => { + 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').filter({ hasText: /^教师$/ }).click(); + await page.waitForFunction( + () => !window.location.search.includes('status=2'), + { timeout: 30_000 }, + ); + } + const authToken = await page.evaluate(() => window.localStorage.getItem('Authorization')); + expect(authToken).toBeTruthy(); + + const headers: Record = { + Authorization: authToken!, + HtySudoerToken: authToken!, + HtyHost: new URL(kcBase).hostname, + }; + + const groupName1 = `e2e-edit-g1-${Date.now()}`; + const groupName2 = `e2e-edit-g2-${Date.now()}`; + + const createG1 = await request.post(`${kcBase}/api/v1/ws/create_course_group`, { + headers: { ...headers, 'Content-Type': 'application/json' }, + data: { group_name: groupName1, org_visible: true }, + }); + const body1 = await createG1.json(); + expect(body1.r).toBe(true); + const groupId1 = body1.d; + + const createG2 = await request.post(`${kcBase}/api/v1/ws/create_course_group`, { + headers: { ...headers, 'Content-Type': 'application/json' }, + data: { group_name: groupName2, org_visible: true }, + }); + const body2 = await createG2.json(); + expect(body2.r).toBe(true); + const groupId2 = body2.d; + + const pkgName = `e2e-edit-pkg-${Date.now()}`; + const createPkg = await request.post(`${kcBase}/api/v1/clazz/course-package/create`, { + headers: { ...headers, 'Content-Type': 'application/json' }, + data: { package_name: pkgName }, + }); + const pkgBody = await createPkg.json(); + expect(pkgBody.r).toBe(true); + const pkgId = pkgBody.d.id; + + // Link group1 to package + 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: [groupId1] }, + }); + expect(syncRes.ok()).toBeTruthy(); + + try { + const { resolveOrgContextForCoursePage } = await import('./helpers/music-room-session'); + await resolveOrgContextForCoursePage(page); + + await page.goto(`/course/course-package/edit?id=${pkgId}`, { waitUntil: 'domcontentloaded', timeout: 60_000 }); + await expect(page.locator('#app')).toBeVisible({ timeout: 30_000 }); + await page.waitForSelector('input[name="package_name"]', { timeout: 10_000 }); + await page.waitForTimeout(1000); + + // Verify group1 shown (loaded from existing items) + await expect(page.locator('.van-tag').filter({ hasText: groupName1 })).toBeVisible({ timeout: 10_000 }); + + // Open picker + await page.locator('.van-cell').filter({ hasText: '包含课节' }).click(); + await expect(page).toHaveURL(/\/course\/group\/pick/, { timeout: 10_000 }); + + // Switch to "我的课节" tab and select group2 + await page.locator('.van-tab').filter({ hasText: '我的课节' }).click(); + await page.waitForTimeout(1000); + const group2Item = page.locator('.course-item').filter({ hasText: groupName2 }); + await expect(group2Item).toBeVisible({ timeout: 30_000 }); + await group2Item.locator('.van-icon-circle').click(); + await page.waitForTimeout(500); + await expect(group2Item.locator('.van-icon-checked')).toBeVisible({ timeout: 5000 }); + + // Confirm + const confirmBtn = page.locator('.btn-pick'); + await expect(confirmBtn).toBeEnabled({ timeout: 5000 }); + await Promise.all([ + page.waitForURL(/\/course\/course-package\/edit/, { timeout: 15_000 }), + confirmBtn.click(), + ]); + await page.waitForTimeout(500); + await page.waitForSelector('input[name="package_name"]', { timeout: 10_000 }); + + // Verify both groups shown + await expect(page.locator('.van-tag').filter({ hasText: groupName1 })).toBeVisible({ timeout: 10_000 }); + await expect(page.locator('.van-tag').filter({ hasText: groupName2 })).toBeVisible({ timeout: 10_000 }); + + // Submit + await page.getByText('保存').click(); + await page.waitForTimeout(2000); + + // Verify via API + const itemsRes = await request.get(`${kcBase}/api/v1/clazz/course-package/item/list/${pkgId}`, { headers }); + expect(itemsRes.ok()).toBeTruthy(); + const itemsBody = await itemsRes.json(); + const items: any[] = itemsBody.d ?? []; + const matchedIds = items.map((i: any) => i.course_group_id); + expect(matchedIds).toContain(groupId1); + expect(matchedIds).toContain(groupId2); + } finally { + await request.post(`${kcBase}/api/v1/clazz/course-package/delete/${pkgId}`, { headers }).catch(() => {}); + await request.post(`${kcBase}/api/v1/ws/delete_course_group/${groupId1}`, { headers }).catch(() => {}); + await request.post(`${kcBase}/api/v1/ws/delete_course_group/${groupId2}`, { headers }).catch(() => {}); + } + }); + test('课节新增页面:开放给机构开关可见', async ({ page }) => { const q = new URLSearchParams({ unionid: moicenUnionid!, status: '2' }); await page.goto(`/?${q.toString()}`, {