test(course-package): add picker persistence and edit scenario tests

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-04-30 21:10:23 +08:00
parent bc8122f4d4
commit 4d594c97ab
+248
View File
@@ -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<string, string> = {
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<string, string> = {
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()}`, {