Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
6f98176
basic skeleton of loading program enrollments from v2 endpoint
gumaerc Nov 19, 2025
27c9472
Set up the basics for having a DashboardCard that supports programs w…
gumaerc Nov 19, 2025
1a2ffa7
Add individual program display to EnrollmentDisplay and toggle it on …
gumaerc Nov 20, 2025
5cb60b3
add "stacked" card style directly to DashboardCard as a variant
gumaerc Nov 20, 2025
c773d3f
rearrange HomeContent so recommendation carousels are still shown if …
gumaerc Nov 21, 2025
01de495
move program display to its own page route and component
gumaerc Nov 21, 2025
9ae298f
Fix program CTA text
gumaerc Nov 21, 2025
d2cf7fe
remove manual back button
gumaerc Nov 21, 2025
3932f07
Set up the program page to use req_tree when displaying the program's…
gumaerc Nov 22, 2025
6c07165
fix tests
gumaerc Nov 22, 2025
223a4f0
set up non-b2b course card click handler for enrolling
gumaerc Nov 22, 2025
352c30d
fix typescript errors
gumaerc Nov 22, 2025
478d73a
fix more tests
gumaerc Nov 22, 2025
2206a4d
add unit tests for new functionality and loading skeleton
gumaerc Nov 24, 2025
42dbb65
fix typescript problem
gumaerc Nov 24, 2025
f9f6e8f
no-op enrollment in non-b2b courses for now
gumaerc Nov 24, 2025
123004b
don't set link on unenrolled non-B2B course cards
gumaerc Nov 25, 2025
4c920cc
fix type errors
gumaerc Nov 25, 2025
b1c4eaa
simplify redundant ternary expression
gumaerc Nov 25, 2025
1867cff
use req_tree directly
gumaerc Nov 25, 2025
56a842f
remove unnecessary test var
gumaerc Nov 25, 2025
f17ec36
respect operator in req_tree
gumaerc Nov 25, 2025
9523114
use requirementtreebuilder
gumaerc Nov 25, 2025
c5ea9dd
filter program enrollments shown in enrollmentdisplay by their presen…
gumaerc Nov 26, 2025
37538fd
use h1 components for headers, use node id as key
gumaerc Nov 26, 2025
00b0b4c
add proper mocking for contracts
gumaerc Nov 26, 2025
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 env/shared.env
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,8 @@ APISIX_PORT=8065
KEYCLOAK_PORT=8066
KEYCLOAK_SSL_PORT=8067
REDIS_VIEW_CACHE_DURATION=0

# APISIX session cookie defaults (can be overridden in local env)
APISIX_COOKIE_SECURE=false
APISIX_COOKIE_SAMESITE=Lax
APISIX_REDIRECT_URI=http://host.docker.internal:8777/.apisix/redirect
6 changes: 3 additions & 3 deletions frontends/api/src/mitxonline/hooks/enrollment/queries.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { queryOptions } from "@tanstack/react-query"
import type {
CourseRunEnrollmentRequestV2,
UserProgramEnrollmentDetail,
V2UserProgramEnrollmentDetail,
} from "@mitodl/mitxonline-api-axios/v2"

import { courseRunEnrollmentsApi, programEnrollmentsApi } from "../../clients"
Expand Down Expand Up @@ -35,9 +35,9 @@ const enrollmentQueries = {
programEnrollmentsList: (opts?: RawAxiosRequestConfig) =>
queryOptions({
queryKey: enrollmentKeys.programEnrollmentsList(opts),
queryFn: async (): Promise<UserProgramEnrollmentDetail[]> => {
queryFn: async (): Promise<V2UserProgramEnrollmentDetail[]> => {
return programEnrollmentsApi
.programEnrollmentsList(opts)
.v2ProgramEnrollmentsList(opts)
.then((res) => res.data)
},
}),
Expand Down
27 changes: 26 additions & 1 deletion frontends/api/src/mitxonline/test-utils/factories/enrollment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type {
CourseRunEnrollmentRequestV2,
CourseRunGrade,
UserProgramEnrollmentDetail,
V2UserProgramEnrollmentDetail,
} from "@mitodl/mitxonline-api-axios/v2"
import { UniqueEnforcer } from "enforce-unique"
import { factories } from ".."
Expand Down Expand Up @@ -193,9 +194,33 @@ const programEnrollment: PartialFactory<UserProgramEnrollmentDetail> = (
return mergeOverrides<UserProgramEnrollmentDetail>(defaults, overrides)
}

const programEnrollmentV2: PartialFactory<V2UserProgramEnrollmentDetail> = (
overrides = {},
): V2UserProgramEnrollmentDetail => {
const program = factories.programs.program()
const hasCertificate = faker.datatype.boolean()
const defaults: V2UserProgramEnrollmentDetail = {
certificate: hasCertificate
? {
uuid: faker.string.uuid(),
link: `/certificate/program/${faker.string.uuid()}/`,
}
: null,
program: program,
enrollments: [courseEnrollment()],
}
return mergeOverrides<V2UserProgramEnrollmentDetail>(defaults, overrides)
}

// Not paginated
const courseEnrollments = (count: number): CourseRunEnrollmentRequestV2[] => {
return new Array(count).fill(null).map(() => courseEnrollment())
}

export { courseEnrollment, courseEnrollments, grade, programEnrollment }
export {
courseEnrollment,
courseEnrollments,
grade,
programEnrollment,
programEnrollmentV2,
}
1 change: 1 addition & 0 deletions frontends/api/src/mitxonline/test-utils/urls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ const enrollment = {

const programEnrollments = {
enrollmentsList: () => `${API_BASE_URL}/api/v1/program_enrollments/`,
enrollmentsListV2: () => `${API_BASE_URL}/api/v2/program_enrollments/`,
}

const b2b = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
import * as mitxonline from "api/mitxonline-test-utils"
import { mockAxiosInstance } from "api/test-utils"
import { DashboardCard, getDefaultContextMenuItems } from "./DashboardCard"
import { dashboardCourse } from "./test-utils"
import { dashboardCourse, dashboardProgram } from "./test-utils"
import { faker } from "@faker-js/faker/locale/en"
import moment from "moment"
import { EnrollmentMode, EnrollmentStatus } from "./types"
Expand Down Expand Up @@ -80,10 +80,13 @@ describe.each([
})
})

test("It shows course title and links to marketingUrl if titleAction is marketing", async () => {
test("It shows course title and links to marketingUrl if titleAction is marketing and enrolled", async () => {
setupUserApis()
const course = dashboardCourse({
marketingUrl: "?some-marketing-url",
enrollment: {
status: EnrollmentStatus.Enrolled,
},
})
renderWithProviders(
<DashboardCard titleAction="marketing" dashboardResource={course} />,
Expand All @@ -97,9 +100,39 @@ describe.each([
expect(courseLink).toHaveAttribute("href", course.marketingUrl)
})

test("It shows course title and links to courseware if titleAction is courseware", async () => {
test("It shows course title as clickable text (not link) if titleAction is marketing and not enrolled (non-B2B)", async () => {
setupUserApis()
const course = dashboardCourse()
const course = dashboardCourse({
marketingUrl: "?some-marketing-url",
enrollment: {
status: EnrollmentStatus.NotEnrolled,
},
run: {
b2bContractId: null,
},
})
renderWithProviders(
<DashboardCard titleAction="marketing" dashboardResource={course} />,
)

const card = getCard()

// Should not be a link
expect(
within(card).queryByRole("link", { name: course.title }),
).not.toBeInTheDocument()
// Should be clickable text
const titleText = within(card).getByText(course.title)
expect(titleText).toBeInTheDocument()
})

test("It shows course title and links to courseware if titleAction is courseware and enrolled", async () => {
setupUserApis()
const course = dashboardCourse({
enrollment: {
status: EnrollmentStatus.Enrolled,
},
})
renderWithProviders(
<DashboardCard titleAction="courseware" dashboardResource={course} />,
)
Expand All @@ -112,6 +145,56 @@ describe.each([
expect(courseLink).toHaveAttribute("href", course.run.coursewareUrl)
})

test("It shows course title as clickable text (not link) if titleAction is courseware and not enrolled (non-B2B)", async () => {
setupUserApis()
const course = dashboardCourse({
enrollment: {
status: EnrollmentStatus.NotEnrolled,
},
run: {
b2bContractId: null,
},
})
renderWithProviders(
<DashboardCard titleAction="courseware" dashboardResource={course} />,
)

const card = getCard()

// Should not be a link
expect(
within(card).queryByRole("link", { name: course.title }),
).not.toBeInTheDocument()
// Should be clickable text
const titleText = within(card).getByText(course.title)
expect(titleText).toBeInTheDocument()
})

test("It shows course title as link if not enrolled but has B2B contract", async () => {
setupUserApis()
const b2bContractId = faker.number.int()
const course = dashboardCourse({
enrollment: {
status: EnrollmentStatus.NotEnrolled,
b2b_contract_id: b2bContractId,
},
run: {
b2bContractId: b2bContractId,
},
})
renderWithProviders(
<DashboardCard titleAction="courseware" dashboardResource={course} />,
)

const card = getCard()

// Should be a link for B2B courses
const courseLink = within(card).getByRole("link", {
name: course.title,
})
expect(courseLink).toHaveAttribute("href", course.run.coursewareUrl)
})

test("Accepts a classname", () => {
setupUserApis()
const course = dashboardCourse()
Expand All @@ -137,12 +220,22 @@ describe.each([

test.each([
{
course: pastDashboardCourse(),
course: pastDashboardCourse({
enrollment: {
status: EnrollmentStatus.Enrolled,
mode: EnrollmentMode.Audit,
},
}),
expected: { enabled: true },
case: "past",
},
{
course: currentDashboardCourse(),
course: currentDashboardCourse({
enrollment: {
status: EnrollmentStatus.Enrolled,
mode: EnrollmentMode.Audit,
},
}),
expected: { enabled: true },
case: "current",
},
Expand Down Expand Up @@ -221,7 +314,7 @@ describe.each([
view.rerender(
<DashboardCard
titleAction="marketing"
courseNoun={courseNoun}
noun={courseNoun}
dashboardResource={course}
/>,
)
Expand All @@ -236,7 +329,7 @@ describe.each([
`${expected.label} ${courseNoun}`,
)
} else {
// "Continue" doesn't use courseNoun
// "Continue" doesn't use noun
expect(coursewareCTA).toHaveTextContent(expected.label)
}
},
Expand Down Expand Up @@ -605,8 +698,15 @@ describe.each([
"Enrollment for complete profile bypasses just-in-time dialog",
async ({ trigger }) => {
const userData = mitxUser()
const b2bContractId = faker.number.int()
const course = dashboardCourse({
enrollment: { status: EnrollmentStatus.NotEnrolled },
enrollment: {
status: EnrollmentStatus.NotEnrolled,
b2b_contract_id: b2bContractId,
},
run: {
b2bContractId: b2bContractId,
},
})
const { enrollmentUrl } = setupEnrollmentApis({ user: userData, course })
renderWithProviders(
Expand Down Expand Up @@ -634,8 +734,15 @@ describe.each([
)(
"Enrollment for complete profile bypasses just-in-time dialog",
async ({ trigger, userData }) => {
const b2bContractId = faker.number.int()
const course = dashboardCourse({
enrollment: { status: EnrollmentStatus.NotEnrolled },
enrollment: {
status: EnrollmentStatus.NotEnrolled,
b2b_contract_id: b2bContractId,
},
run: {
b2bContractId: b2bContractId,
},
})
setupEnrollmentApis({ user: userData, course })
renderWithProviders(
Expand All @@ -655,4 +762,90 @@ describe.each([
)
},
)

describe("Stacked Variant", () => {
test("applies stacked variant styling", () => {
setupUserApis()
const course = dashboardCourse()
renderWithProviders(
<DashboardCard
variant="stacked"
titleAction="marketing"
dashboardResource={course}
/>,
)

const card = getCard()
expect(card).toBeInTheDocument()
// Successfully renders a stacked card - the variant prop controls styling via styled-components
})

test("renders multiple stacked cards correctly", () => {
setupUserApis()
const courses = [
dashboardCourse({ title: "First Stacked Course" }),
dashboardCourse({ title: "Second Stacked Course" }),
dashboardCourse({ title: "Third Stacked Course" }),
]

renderWithProviders(
<div>
{courses.map((course) => (
<DashboardCard
key={course.key}
variant="stacked"
titleAction="marketing"
dashboardResource={course}
/>
))}
</div>,
)

const allCards = screen.getAllByTestId(testId)
expect(allCards).toHaveLength(3)
expect(
within(allCards[0]).getByText("First Stacked Course"),
).toBeInTheDocument()
expect(
within(allCards[1]).getByText("Second Stacked Course"),
).toBeInTheDocument()
expect(
within(allCards[2]).getByText("Third Stacked Course"),
).toBeInTheDocument()
})
})

describe("Program Cards", () => {
test("renders program card with title", () => {
setupUserApis()
const program = dashboardProgram({
title: "Test Program Title",
})

renderWithProviders(
<DashboardCard titleAction="marketing" dashboardResource={program} />,
)

const card = getCard()
expect(within(card).getByText("Test Program Title")).toBeInTheDocument()
})

test("program card does not show course-specific elements", () => {
setupUserApis()
const program = dashboardProgram({
title: "Test Program",
})

renderWithProviders(
<DashboardCard titleAction="marketing" dashboardResource={program} />,
)

const card = getCard()
// Programs don't show enrollment status or courseware buttons
expect(
within(card).queryByTestId("courseware-button"),
).not.toBeInTheDocument()
expect(within(card).queryByTestId("upgrade-root")).not.toBeInTheDocument()
})
})
})
Loading
Loading