Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions RELEASE.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
Release Notes
=============

Version 0.55.5
--------------

- program dashboard course enrollment (#2961)

Version 0.55.4 (Released February 24, 2026)
--------------

Expand Down
2 changes: 1 addition & 1 deletion frontends/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
"ol-test-utilities": "0.0.0"
},
"dependencies": {
"@mitodl/mitxonline-api-axios": "^2026.2.5",
"@mitodl/mitxonline-api-axios": "^2026.2.19",
"@tanstack/react-query": "^5.66.0",
"axios": "^1.12.2",
"tiny-invariant": "^1.3.3"
Expand Down
3 changes: 3 additions & 0 deletions frontends/api/src/mitxonline/hooks/enrollment/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ const useCreateEnrollment = () => {
queryClient.invalidateQueries({
queryKey: enrollmentKeys.courseRunEnrollmentsList(),
})
queryClient.invalidateQueries({
queryKey: enrollmentKeys.programEnrollmentsList(),
})
},
})
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,7 @@ const programEnrollmentV3: PartialFactory<V3UserProgramEnrollment> = (
link: `/certificate/program/${faker.string.uuid()}/`,
}
: null,
enrollment_mode: faker.helpers.arrayElement(["audit", "verified", null]),
program: program,
}
return mergeOverrides<V3UserProgramEnrollment>(defaults, overrides)
Expand Down
2 changes: 1 addition & 1 deletion frontends/main/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
"@emotion/styled": "^11.11.0",
"@floating-ui/react": "^0.27.16",
"@mitodl/course-search-utils": "^3.5.0",
"@mitodl/mitxonline-api-axios": "^2026.2.5",
"@mitodl/mitxonline-api-axios": "^2026.2.19",
"@mitodl/smoot-design": "^6.24.0",
"@mui/material": "^6.4.5",
"@mui/material-nextjs": "^6.4.3",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1161,6 +1161,179 @@ describe.each([
},
)

describe("B2C (non-B2B) Enrollment", () => {
test.each(ENROLLMENT_TRIGGERS)(
"Clicking $trigger on non-B2B course opens CourseEnrollmentDialog",
async ({ trigger }) => {
const userData = mitxUser()
setMockResponse.get(mitxonline.urls.userMe.get(), userData)

const run = mitxonline.factories.courses.courseRun({
b2b_contract: null, // Non-B2B course
is_enrollable: true,
})
const course = dashboardCourse({
courseruns: [run],
next_run_id: run.id,
})

renderWithProviders(
<DashboardCard
resource={{ type: DashboardType.Course, data: course }}
/>,
)

const card = getCard()
const triggerElement =
trigger === "button"
? within(card).getByTestId("courseware-button")
: within(card).getByText(course.title)

await user.click(triggerElement)

// Should open the CourseEnrollmentDialog, not JustInTimeDialog
await screen.findByRole("dialog", { name: course.title })
expect(
screen.queryByRole("dialog", { name: "Just a Few More Details" }),
).not.toBeInTheDocument()
},
)
})

describe("Verified Program Enrollment", () => {
test.each(ENROLLMENT_TRIGGERS)(
"Clicking $trigger on course in verified program does one-click enrollment",
async ({ trigger }) => {
const userData = mitxUser()
setMockResponse.get(mitxonline.urls.userMe.get(), userData)

const run = mitxonline.factories.courses.courseRun({
b2b_contract: null,
is_enrollable: true,
courseware_url: faker.internet.url(),
})
const course = dashboardCourse({
courseruns: [run],
next_run_id: run.id,
})

const programEnrollment =
mitxonline.factories.enrollment.programEnrollmentV3({
enrollment_mode: "verified",
})

// Mock the enrollment endpoint
setMockResponse.post(mitxonline.urls.enrollment.enrollmentsListV1(), {})

renderWithProviders(
<DashboardCard
resource={{ type: DashboardType.Course, data: course }}
programEnrollment={programEnrollment}
/>,
)

const card = getCard()
const triggerElement =
trigger === "button"
? within(card).getByTestId("courseware-button")
: within(card).getByText(course.title)

await user.click(triggerElement)

// Should call enrollment endpoint
await waitFor(() => {
expect(mockAxiosInstance.request).toHaveBeenCalledWith(
expect.objectContaining({
method: "POST",
url: mitxonline.urls.enrollment.enrollmentsListV1(),
}),
)
})

expect(
screen.queryByRole("dialog", { name: course.title }),
).not.toBeInTheDocument()
expect(
screen.queryByRole("dialog", { name: "Just a Few More Details" }),
).not.toBeInTheDocument()
},
)

test("Audit program enrollment opens CourseEnrollmentDialog", async () => {
const userData = mitxUser()
setMockResponse.get(mitxonline.urls.userMe.get(), userData)

const run = mitxonline.factories.courses.courseRun({
b2b_contract: null,
is_enrollable: true,
})
const course = dashboardCourse({
courseruns: [run],
next_run_id: run.id,
})

const programEnrollment =
mitxonline.factories.enrollment.programEnrollmentV3({
enrollment_mode: "audit", // Audit, not verified
})

renderWithProviders(
<DashboardCard
resource={{ type: DashboardType.Course, data: course }}
programEnrollment={programEnrollment}
/>,
)

const card = getCard()
const button = within(card).getByTestId("courseware-button")

await user.click(button)

// Should open the CourseEnrollmentDialog for audit enrollments
await screen.findByRole("dialog", { name: course.title })
})
})

describe("CourseEnrollmentDialog", () => {
test("shows course runs as options in dialog", async () => {
const userData = mitxUser()
setMockResponse.get(mitxonline.urls.userMe.get(), userData)

const run1 = mitxonline.factories.courses.courseRun({
b2b_contract: null,
is_enrollable: true,
title: "Fall 2025",
})
const run2 = mitxonline.factories.courses.courseRun({
b2b_contract: null,
is_enrollable: true,
title: "Spring 2026",
})
const course = dashboardCourse({
title: "Test Course Title",
courseruns: [run1, run2],
next_run_id: run1.id,
})

renderWithProviders(
<DashboardCard
resource={{ type: DashboardType.Course, data: course }}
/>,
)

const card = getCard()
const startButton = within(card).getByTestId("courseware-button")
await user.click(startButton)

const dialog = await screen.findByRole("dialog", {
name: "Test Course Title",
})

// Dialog should show course runs as options
expect(within(dialog).getByText("Choose a date:")).toBeInTheDocument()
})
})

describe("Stacked Variant", () => {
test("applies stacked variant styling", () => {
setupUserApis()
Expand Down
Loading
Loading