From 75291986cf42532b0da7bd5392e1f80c9a313f08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=98=BF=E7=94=B7?= Date: Thu, 30 Apr 2026 08:58:15 +0800 Subject: [PATCH] test(course-package): verify against seeded data from moicen DB MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add pre-seeded course packages for 阿难 on moicen DB: 钢琴一对一课程, 声乐基础训练, 乐理知识速成 (INACTIVE). Update pagination tests to assert specific names, ACTIVE filtering, sort order, and INACTIVE exclusion. Extract shared loginAndGetJwt helper to reduce duplication. Co-Authored-By: Claude Opus 4.7 --- tests/course-package.spec.ts | 151 ++++++++++++++--------------------- 1 file changed, 61 insertions(+), 90 deletions(-) diff --git a/tests/course-package.spec.ts b/tests/course-package.spec.ts index f76df51..fd68656 100644 --- a/tests/course-package.spec.ts +++ b/tests/course-package.spec.ts @@ -33,6 +33,36 @@ function extractRoleKeys(payload: Record | null): string[] { .filter((k): k is string => typeof k === 'string' && k.length > 0); } +/** 共用的登录 + JWT 提取逻辑 */ +async function loginAndGetJwt(page: any): Promise { + 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 }); + if ( + await page.getByText('请选择您的登录身份').isVisible().catch(() => false) + ) { + await page.locator('.van-grid-item').first().click(); + } + await expect( + page.getByText(/请选择您的登录身份|欢迎回来|进入工作台/), + ).toBeVisible({ timeout: 120_000 }); + + if ( + await page.getByText('请返回微信小程序完成登录').isVisible().catch(() => false) + ) { + test.skip(true, '当前 unionid 会话未处于可用登录态'); + } + + const authToken = await page.evaluate(() => + window.localStorage.getItem('Authorization'), + ); + expect(authToken, '登录后应有 JWT').toBeTruthy(); + return authToken!; +} + test.describe('课包(course_package)', () => { test.describe.configure({ timeout: 180_000 }); @@ -41,34 +71,9 @@ test.describe('课包(course_package)', () => { test('权限校验:当前账号 JWT 应包含 TEACHER 或 SUPERVISOR 角色', 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 }); + const authToken = await loginAndGetJwt(page); - if ( - await page.getByText('请选择您的登录身份').isVisible().catch(() => false) - ) { - await page.locator('.van-grid-item').first().click(); - } - await expect( - page.getByText(/请选择您的登录身份|欢迎回来|进入工作台/) - ).toBeVisible({ timeout: 120_000 }); - - if ( - await page.getByText('请返回微信小程序完成登录').isVisible().catch(() => false) - ) { - test.skip(true, '当前 unionid 会话未处于可用登录态'); - } - - const authToken = await page.evaluate(() => - window.localStorage.getItem('Authorization') - ); - expect(authToken, '登录后应有 JWT').toBeTruthy(); - - const payload = decodeJwtPayload(authToken!); + const payload = decodeJwtPayload(authToken); expect(payload, 'JWT 应可解码').not.toBeNull(); const roles = extractRoleKeys(payload); @@ -85,36 +90,11 @@ test.describe('课包(course_package)', () => { page, request, }) => { - // ---- 登录获取 JWT ---- - 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 }); - if ( - await page.getByText('请选择您的登录身份').isVisible().catch(() => false) - ) { - await page.locator('.van-grid-item').first().click(); - } - await expect( - page.getByText(/请选择您的登录身份|欢迎回来|进入工作台/) - ).toBeVisible({ timeout: 120_000 }); - - if ( - await page.getByText('请返回微信小程序完成登录').isVisible().catch(() => false) - ) { - test.skip(true, '当前 unionid 会话未处于可用登录态'); - } - - const authToken = await page.evaluate(() => - window.localStorage.getItem('Authorization'), - ); - expect(authToken, '应有 Authorization JWT').toBeTruthy(); + const authToken = await loginAndGetJwt(page); const headers: Record = { - Authorization: authToken!, - HtySudoerToken: authToken!, + Authorization: authToken, + HtySudoerToken: authToken, HtyHost: new URL(kcBase).hostname, }; @@ -178,64 +158,55 @@ test.describe('课包(course_package)', () => { expect(deleteBody.r, `DELETE 业务失败: ${JSON.stringify(deleteBody)}`).toBe(true); }); - test('分页查询:my-packages / org-packages 接口正常', async ({ + test('分页查询:my-packages / org-packages 返回预置种子数据', async ({ page, request, }) => { - // ---- 登录获取 JWT ---- - 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 }); - if ( - await page.getByText('请选择您的登录身份').isVisible().catch(() => false) - ) { - await page.locator('.van-grid-item').first().click(); - } - await expect( - page.getByText(/请选择您的登录身份|欢迎回来|进入工作台/) - ).toBeVisible({ timeout: 120_000 }); - - if ( - await page.getByText('请返回微信小程序完成登录').isVisible().catch(() => false) - ) { - test.skip(true, '当前 unionid 会话未处于可用登录态'); - } - - const authToken = await page.evaluate(() => - window.localStorage.getItem('Authorization'), - ); - expect(authToken).toBeTruthy(); + const authToken = await loginAndGetJwt(page); const headers: Record = { - Authorization: authToken!, - HtySudoerToken: authToken!, + Authorization: authToken, + HtySudoerToken: authToken, HtyHost: new URL(kcBase).hostname, }; - // ---- my-packages(当前教师创建的课包)---- + // ---- my-packages(当前教师创建的课包,含 INACTIVE)---- const myRes = await request.get( - `${kcBase}/api/v1/clazz/course-package/my-packages?page=1&page_size=5`, + `${kcBase}/api/v1/clazz/course-package/my-packages?page=1&page_size=10`, { headers }, ); expect(myRes.ok(), `my-packages HTTP ${myRes.status()}`).toBeTruthy(); const myBody = await myRes.json(); expect(myBody.r, `my-packages 业务失败: ${JSON.stringify(myBody)}`).toBe(true); - expect(Array.isArray(myBody.d?.[0])).toBe(true); + const myList: any[] = myBody.d?.[0] ?? []; + expect(myList.length).toBeGreaterThanOrEqual(3); + // 应包含预置种子:钢琴一对一课程、声乐基础训练、乐理知识速成 + const myNames = myList.map((p: any) => p.package_name); + expect(myNames).toContain('钢琴一对一课程'); + expect(myNames).toContain('声乐基础训练'); + expect(myNames).toContain('乐理知识速成'); expect(typeof myBody.d?.[1] === 'number').toBe(true); // total pages expect(typeof myBody.d?.[2] === 'number').toBe(true); // total count - // ---- org-packages(机构下所有活跃课包)---- + // ---- org-packages(机构下活跃课包,仅 ACTIVE,按 sort_order 升序)---- const orgRes = await request.get( - `${kcBase}/api/v1/clazz/course-package/org-packages?page=1&page_size=5`, + `${kcBase}/api/v1/clazz/course-package/org-packages?page=1&page_size=10`, { headers }, ); expect(orgRes.ok(), `org-packages HTTP ${orgRes.status()}`).toBeTruthy(); const orgBody = await orgRes.json(); expect(orgBody.r, `org-packages 业务失败: ${JSON.stringify(orgBody)}`).toBe(true); - expect(Array.isArray(orgBody.d?.[0])).toBe(true); + const orgList: any[] = orgBody.d?.[0] ?? []; + // 应包含 2 个 ACTIVE 种子,不包含 INACTIVE 的「乐理知识速成」 + expect(orgList.length).toBeGreaterThanOrEqual(2); + const orgNames = orgList.map((p: any) => p.package_name); + expect(orgNames).toContain('钢琴一对一课程'); + expect(orgNames).toContain('声乐基础训练'); + expect(orgNames).not.toContain('乐理知识速成'); + // 按 sort_order 升序排列,钢琴一对一(sort=1)在前 + const pianoIdx = orgNames.indexOf('钢琴一对一课程'); + const vocalIdx = orgNames.indexOf('声乐基础训练'); + expect(pianoIdx).toBeLessThan(vocalIdx); expect(typeof orgBody.d?.[1] === 'number').toBe(true); expect(typeof orgBody.d?.[2] === 'number').toBe(true); });