diff --git a/frontends/main/src/app-pages/DashboardPage/OrganizationContent.test.tsx b/frontends/main/src/app-pages/DashboardPage/OrganizationContent.test.tsx index d41a6aff88..4ab71c50db 100644 --- a/frontends/main/src/app-pages/DashboardPage/OrganizationContent.test.tsx +++ b/frontends/main/src/app-pages/DashboardPage/OrganizationContent.test.tsx @@ -66,6 +66,31 @@ describe("OrganizationContent", () => { }) }) + it("displays courses in the correct order based on program.courseIds, regardless of API response order", async () => { + const { orgX, programA, coursesA } = setupProgramsAndCourses() + + // Mock API to return courses in reverse order from program.courseIds + const reversedCoursesA = [...coursesA].reverse() + setMockResponse.get( + expect.stringContaining( + `/api/v2/courses/?id=${programA.courses.join("%2C")}`, + ), + { results: reversedCoursesA }, + ) + + renderWithProviders() + + const programElement = await screen.findByTestId("org-program-root") + const cards = await within(programElement).findAllByTestId( + "enrollment-card-desktop", + ) + + // Verify courses appear in program.courseIds order, not API response order + coursesA.forEach((course, i) => { + expect(cards[i]).toHaveTextContent(course.title) + }) + }) + test("Shows correct enrollment status", async () => { const { orgX, programA, coursesA } = setupProgramsAndCourses() const enrollments = [ @@ -132,19 +157,16 @@ describe("OrganizationContent", () => { { results: [programB] }, ) - // Mock the courses API calls for programs in the collection - // Use dynamic matching since course IDs are randomly generated - setMockResponse.get( - expect.stringContaining( - `/api/v2/courses/?id=${programA.courses.join("%2C")}`, - ), - { results: coursesA }, - ) + // Mock the bulk course API call with first course from each program + const firstCourseA = coursesA.find((c) => c.id === programA.courses[0]) + const firstCourseB = coursesB.find((c) => c.id === programB.courses[0]) + const firstCourseIds = [programB.courses[0], programA.courses[0]] // B first, then A to match collection order + setMockResponse.get( expect.stringContaining( - `/api/v2/courses/?id=${programB.courses.join("%2C")}`, + `/api/v2/courses/?id=${firstCourseIds.join("%2C")}`, ), - { results: coursesB }, + { results: [firstCourseB, firstCourseA] }, // Response order should match request order ) renderWithProviders() @@ -168,8 +190,47 @@ describe("OrganizationContent", () => { // Verify the order matches the programCollection.programs array [programB.id, programA.id] const programCards = collection.getAllByTestId("enrollment-card-desktop") - expect(programCards[0]).toHaveTextContent(coursesB[0].title) - expect(programCards[1]).toHaveTextContent(coursesA[0].title) + expect(programCards[0]).toHaveTextContent(firstCourseB!.title) + expect(programCards[1]).toHaveTextContent(firstCourseA!.title) + }) + + test("Program collection displays the first course from each program", async () => { + const { orgX, programA, programCollection, coursesA } = + setupProgramsAndCourses() + + programCollection.programs = [programA.id] + setMockResponse.get(urls.programCollections.programCollectionsList(), { + results: [programCollection], + }) + + setMockResponse.get( + expect.stringContaining(`/api/v2/programs/?id=${programA.id}`), + { results: [programA] }, + ) + + // Mock bulk API call for the first course + const firstCourseId = programA.courses[0] + const firstCourse = coursesA.find((c) => c.id === firstCourseId) + setMockResponse.get( + expect.stringContaining(`/api/v2/courses/?id=${firstCourseId}`), + { results: [firstCourse] }, + ) + + renderWithProviders() + + const collection = await screen.findByTestId("org-program-collection-root") + + // Wait for program cards to be rendered + const programCards = await waitFor(() => { + const programCards = within(collection).getAllByTestId( + "enrollment-card-desktop", + ) + expect(programCards.length).toBeGreaterThan(0) + return programCards + }) + + // Should display the first course by program.courseIds order + expect(programCards[0]).toHaveTextContent(firstCourse!.title) }) test("Does not render a program separately if it is part of a collection", async () => { @@ -265,34 +326,32 @@ describe("OrganizationContent", () => { const { orgX, programA, programB, programCollection, coursesB } = setupProgramsAndCourses() + // Modify programA to have no courses to test "at least one program has courses" + const programANoCourses = { ...programA, courses: [] } + // Set up the collection to include both programs - programCollection.programs = [programA.id, programB.id] + programCollection.programs = [programANoCourses.id, programB.id] setMockResponse.get(urls.programCollections.programCollectionsList(), { results: [programCollection], }) // Mock individual program API calls for the collection setMockResponse.get( - expect.stringContaining(`/api/v2/programs/?id=${programA.id}`), - { results: [programA] }, + expect.stringContaining(`/api/v2/programs/?id=${programANoCourses.id}`), + { results: [programANoCourses] }, ) setMockResponse.get( expect.stringContaining(`/api/v2/programs/?id=${programB.id}`), { results: [programB] }, ) - // Mock programA to have no courses, programB to have courses - setMockResponse.get( - expect.stringContaining( - `/api/v2/courses/?id=${programA.courses.join("%2C")}`, - ), - { results: [] }, - ) + // Mock bulk course API call - only programB has courses, so only its first course should be included + const firstCourseBId = programB.courses[0] + const firstCourseB = coursesB.find((c) => c.id === firstCourseBId) + setMockResponse.get( - expect.stringContaining( - `/api/v2/courses/?id=${programB.courses.join("%2C")}`, - ), - { results: coursesB }, + expect.stringContaining(`/api/v2/courses/?id=${firstCourseBId}`), + { results: [firstCourseB] }, ) renderWithProviders() @@ -307,11 +366,11 @@ describe("OrganizationContent", () => { // Should see the collection header expect(collection.getByText(programCollection.title)).toBeInTheDocument() - // Should see programB's courses + // Should see programB's course await waitFor(() => { - expect(collection.getAllByText(coursesB[0].title).length).toBeGreaterThan( - 0, - ) + expect( + collection.getAllByText(firstCourseB!.title).length, + ).toBeGreaterThan(0) }) }) diff --git a/frontends/main/src/app-pages/DashboardPage/OrganizationContent.tsx b/frontends/main/src/app-pages/DashboardPage/OrganizationContent.tsx index 2612383a9f..ac9950533e 100644 --- a/frontends/main/src/app-pages/DashboardPage/OrganizationContent.tsx +++ b/frontends/main/src/app-pages/DashboardPage/OrganizationContent.tsx @@ -131,13 +131,9 @@ const ProgramDescription = styled(Typography)({ // Custom hook to handle multiple program queries and check if any have courses const useProgramCollectionCourses = (programIds: number[], orgId: number) => { const programQueries = useQueries({ - queries: programIds.map((programId) => ({ - ...programsQueries.programsList({ id: programId, org_id: orgId }), - queryKey: [ - ...programsQueries.programsList({ id: programId, org_id: orgId }) - .queryKey, - ], - })), + queries: programIds.map((programId) => + programsQueries.programsList({ id: programId, org_id: orgId }), + ), }) const isLoading = programQueries.some((query) => query.isLoading) @@ -175,6 +171,25 @@ const OrgProgramCollectionDisplay: React.FC<{ const sanitizedDescription = DOMPurify.sanitize(collection.description ?? "") const { isLoading, programsWithCourses, hasAnyCourses } = useProgramCollectionCourses(collection.programIds, orgId) + const firstCourseIds = programsWithCourses + .map((p) => p?.program.courseIds[0]) + .filter((id): id is number => id !== undefined) + const courses = useQuery({ + ...coursesQueries.coursesList({ + id: firstCourseIds, + org_id: orgId, + }), + enabled: firstCourseIds.length > 0, + }) + const rawCourses = + courses.data?.results.sort((a, b) => { + return firstCourseIds.indexOf(a.id) - firstCourseIds.indexOf(b.id) + }) ?? [] + const transformedCourses = transform.organizationCoursesWithContracts({ + courses: rawCourses, + contracts: contracts ?? [], + enrollments: enrollments ?? [], + }) const header = ( @@ -214,17 +229,26 @@ const OrgProgramCollectionDisplay: React.FC<{ {header} - {programsWithCourses.map((item) => - item ? ( - ( + - ) : null, - )} + ))} + {transformedCourses.map((course) => ( + + ))} ) @@ -260,8 +284,12 @@ const OrgProgramDisplay: React.FC<{ ) if (programLoading || courses.isLoading) return skeleton + const rawCourses = + courses.data?.results.sort((a, b) => { + return program.courseIds.indexOf(a.id) - program.courseIds.indexOf(b.id) + }) ?? [] const transformedCourses = transform.organizationCoursesWithContracts({ - courses: courses.data?.results ?? [], + courses: rawCourses, contracts: contracts ?? [], enrollments: courseRunEnrollments ?? [], }) @@ -307,59 +335,6 @@ const OrgProgramDisplay: React.FC<{ ) } -const ProgramCollectionItem: React.FC<{ - program: DashboardProgram - contracts?: ContractPage[] - enrollments?: CourseRunEnrollment[] - orgId: number -}> = ({ program, contracts, enrollments, orgId }) => { - return ( - - ) -} - -const ProgramCard: React.FC<{ - program: DashboardProgram - contracts?: ContractPage[] - enrollments?: CourseRunEnrollment[] - orgId: number -}> = ({ program, contracts, enrollments, orgId }) => { - const courses = useQuery( - coursesQueries.coursesList({ - id: program.courseIds, - org_id: orgId, - }), - ) - const skeleton = ( - - ) - if (courses.isLoading) return skeleton - const transformedCourses = transform.organizationCoursesWithContracts({ - courses: courses.data?.results ?? [], - contracts: contracts ?? [], - enrollments: enrollments ?? [], - }) - if (courses.isLoading || !transformedCourses.length) return skeleton - // For now we assume the first course is the main one for the program. - const course = transformedCourses[0] - return ( - - ) -} - const OrganizationRoot = styled.div({ display: "flex", flexDirection: "column",