Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
dc3c1b8
elaborate frontend testing prompt
ChristopherChudzicki Dec 9, 2025
c75f5c4
dialog tweaks
ChristopherChudzicki Dec 9, 2025
48c4c01
improve select types
ChristopherChudzicki Dec 9, 2025
0b7cec5
set session_cookie to .odl.local by default
ChristopherChudzicki Dec 9, 2025
c5bc45b
set default jest tz
ChristopherChudzicki Dec 9, 2025
7996ff7
add course enrollment dialog
ChristopherChudzicki Dec 9, 2025
ca82e0c
rough mobile designs
ChristopherChudzicki Dec 9, 2025
7204584
mobilify
ChristopherChudzicki Dec 9, 2025
121f97c
slightly better mobile styles
ChristopherChudzicki Dec 10, 2025
b864dc4
update env
ChristopherChudzicki Dec 10, 2025
a48beda
remove unversioned/v1 urls
ChristopherChudzicki Dec 10, 2025
f273300
add program course enrollment dialog
ChristopherChudzicki Dec 10, 2025
3679dc6
consolidate location faking
ChristopherChudzicki Dec 10, 2025
aa0c33b
add program enrollment tests
ChristopherChudzicki Dec 10, 2025
ca8ded0
update prompt instructions
ChristopherChudzicki Dec 10, 2025
7aee9f5
add enrollment indicator for program pages
ChristopherChudzicki Dec 10, 2025
9cf3435
test enrollmentbutton
ChristopherChudzicki Dec 11, 2025
f25ebe7
delete instructions file, move to separate pr
ChristopherChudzicki Dec 11, 2025
6c2eff6
add signup popover to enrollmentbutton
ChristopherChudzicki Dec 11, 2025
ad206a8
add signup popover for course pages
ChristopherChudzicki Dec 11, 2025
671f623
revert theme change
ChristopherChudzicki Dec 11, 2025
7ac4d8e
restore original frontend-tests instructions
ChristopherChudzicki Dec 11, 2025
a0c69f0
fix test
ChristopherChudzicki Dec 12, 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
1 change: 1 addition & 0 deletions env/backend.env
Original file line number Diff line number Diff line change
Expand Up @@ -68,3 +68,4 @@ USERINFO_URL=http://kc.ol.local:8066/realms/ol-local/protocol/openid-connect/use

# Disable all celery tasks by default in local dev
CELERY_BEAT_DISABLED=True
SESSION_COOKIE_DOMAIN="odl.local"
17 changes: 16 additions & 1 deletion frontends/api/src/mitxonline/hooks/enrollment/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@ import { useMutation, useQueryClient } from "@tanstack/react-query"
import { b2bApi, courseRunEnrollmentsApi } from "../../clients"
import {
B2bApiB2bEnrollCreateRequest,
EnrollmentsApiUserEnrollmentsCreateV2Request,
EnrollmentsApiEnrollmentsPartialUpdateRequest,
} from "@mitodl/mitxonline-api-axios/v2"

const useCreateEnrollment = () => {
const useCreateB2bEnrollment = () => {
const queryClient = useQueryClient()
return useMutation({
mutationFn: (opts: B2bApiB2bEnrollCreateRequest) =>
Expand All @@ -19,6 +20,19 @@ const useCreateEnrollment = () => {
})
}

const useCreateEnrollment = () => {
const queryClient = useQueryClient()
return useMutation({
mutationFn: (opts: EnrollmentsApiUserEnrollmentsCreateV2Request) =>
courseRunEnrollmentsApi.userEnrollmentsCreateV2(opts),
onSettled: () => {
queryClient.invalidateQueries({
queryKey: enrollmentKeys.courseRunEnrollmentsList(),
})
},
})
}

const useUpdateEnrollment = () => {
const queryClient = useQueryClient()
return useMutation({
Expand Down Expand Up @@ -48,6 +62,7 @@ const useDestroyEnrollment = () => {
export {
enrollmentQueries,
enrollmentKeys,
useCreateB2bEnrollment,
useCreateEnrollment,
useUpdateEnrollment,
useDestroyEnrollment,
Expand Down
5 changes: 4 additions & 1 deletion frontends/api/src/mitxonline/test-utils/factories/courses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import type {
} from "@mitodl/mitxonline-api-axios/v2"
import { faker } from "@faker-js/faker/locale/en"
import { UniqueEnforcer } from "enforce-unique"
import { has } from "lodash"

const uniqueCourseId = new UniqueEnforcer()
const uniqueCourseRunId = new UniqueEnforcer()
Expand Down Expand Up @@ -122,7 +123,9 @@ const course: PartialFactory<CourseWithCourseRunsSerializerV2> = (
Array.from({ length: faker.number.int({ min: 1, max: 3 }) }).map(() =>
courseRun(),
)
const nextRunId = overrides.next_run_id ?? faker.helpers.arrayElement(runs).id
const nextRunId = has(overrides, "next_run_id")
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With the original version of this factory, it was impossible to specify null next_run_id, even thought that is possible via the api.

? (overrides.next_run_id ?? null)
: faker.helpers.arrayElement(runs).id
const defaults: CourseWithCourseRunsSerializerV2 = {
id: uniqueCourseId.enforce(() => faker.number.int()),
title: faker.lorem.words(3),
Expand Down
4 changes: 3 additions & 1 deletion frontends/api/src/mitxonline/test-utils/urls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ const countries = {
}

const enrollment = {
enrollmentsList: () => `${API_BASE_URL}/api/v1/enrollments/`,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We weren't using this; I removed the mock from some tests and they all still passed.

/**
* @deprecated
*/
courseEnrollment: (id?: number) =>
`${API_BASE_URL}/api/v1/enrollments/${id ? `${id}/` : ""}`,
enrollmentsListV2: () => `${API_BASE_URL}/api/v2/enrollments/`,
Expand Down
9 changes: 7 additions & 2 deletions frontends/jest-shared-setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,19 @@ import "cross-fetch/polyfill"
import { resetAllWhenMocks } from "jest-when"
import * as matchers from "jest-extended"
import { mockRouter } from "ol-test-utilities/mocks/nextNavigation"
import { setDefaultTimezone } from "ol-test-utilities"

expect.extend(matchers)

setDefaultTimezone("UTC")

// env vars
process.env.NEXT_PUBLIC_MITOL_API_BASE_URL =
"http://api.test.learn.odl.local:8063"
"http://api.test.learn.odl.local:8065"
process.env.NEXT_PUBLIC_MITX_ONLINE_BASE_URL =
"http://api.test.mitxonline.odl.local:8053"
"http://api.test.learn.odl.local:8065/mitxonline"
process.env.NEXT_PUBLIC_MITX_ONLINE_LEGACY_BASE_URL =
"http://mitxonline.odl.local:8065"
process.env.NEXT_PUBLIC_ORIGIN = "http://test.learn.odl.local:8062"
process.env.NEXT_PUBLIC_EMBEDLY_KEY = "fake-embedly-key"

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
renderWithProviders,
screen,
setMockResponse,
setupLocationMock,
user,
within,
} from "@/test-utils"
Expand Down Expand Up @@ -62,23 +63,7 @@ describe.each([
])("DashboardCard $display", ({ testId }) => {
const getCard = () => screen.getByTestId(testId)

const originalLocation = window.location

beforeAll(() => {
Object.defineProperty(window, "location", {
configurable: true,
enumerable: true,
value: { ...originalLocation, assign: jest.fn() },
})
})

afterAll(() => {
Object.defineProperty(window, "location", {
configurable: true,
enumerable: true,
value: originalLocation,
})
})
setupLocationMock()

test("It shows course title and links to marketingUrl if titleAction is marketing and enrolled", async () => {
setupUserApis()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ import {
UnenrollDialog,
} from "./DashboardDialogs"
import NiceModal from "@ebay/nice-modal-react"
import { useCreateEnrollment } from "api/mitxonline-hooks/enrollment"
import { useCreateB2bEnrollment } from "api/mitxonline-hooks/enrollment"
import { mitxUserQueries } from "api/mitxonline-hooks/user"
import { useQuery } from "@tanstack/react-query"
import { programView } from "@/common/urls"
Expand Down Expand Up @@ -177,7 +177,7 @@ const getDefaultContextMenuItems = (

const useOneClickEnroll = () => {
const mitxOnlineUser = useQuery(mitxUserQueries.me())
const createEnrollment = useCreateEnrollment()
const createEnrollment = useCreateB2bEnrollment()
const userCountry = mitxOnlineUser.data?.legal_address?.country
const userYearOfBirth = mitxOnlineUser.data?.user_profile?.year_of_birth
const showJustInTimeDialog = !userCountry || !userYearOfBirth
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
renderWithProviders,
screen,
setMockResponse,
setupLocationMock,
user,
within,
} from "@/test-utils"
Expand Down Expand Up @@ -157,23 +158,7 @@ describe("JustInTimeDialog", () => {
}
}

const originalLocation = window.location

beforeAll(() => {
Object.defineProperty(window, "location", {
configurable: true,
enumerable: true,
value: { ...originalLocation, assign: jest.fn() },
})
})

afterAll(() => {
Object.defineProperty(window, "location", {
configurable: true,
enumerable: true,
value: originalLocation,
})
})
setupLocationMock()

type SetupJitOptions = {
userOverrides?: PartialDeep<MitxUser>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { useQuery } from "@tanstack/react-query"
import NiceModal, { muiDialogV5 } from "@ebay/nice-modal-react"
import { useFormik } from "formik"
import {
useCreateEnrollment,
useCreateB2bEnrollment,
useDestroyEnrollment,
useUpdateEnrollment,
} from "api/mitxonline-hooks/enrollment"
Expand Down Expand Up @@ -196,7 +196,7 @@ const JustInTimeDialogInner: React.FC<{ href: string; readableId: string }> = ({
}) => {
const { data: countries } = useQuery(mitxUserQueries.countries())
const updateUser = useUpdateUserMutation()
const createEnrollment = useCreateEnrollment()
const createEnrollment = useCreateB2bEnrollment()
const user = useQuery(mitxUserQueries.me())
const modal = NiceModal.useModal()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,6 @@ function setupOrgDashboardMocks(
)

// Empty defaults
setMockResponse.get(mitxonline.urls.enrollment.enrollmentsList(), [])
setMockResponse.get(mitxonline.urls.programEnrollments.enrollmentsList(), [])
setMockResponse.get(
mitxonline.urls.programEnrollments.enrollmentsListV2(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ const makeGrade = factories.enrollment.grade

describe("OrganizationContent", () => {
beforeEach(() => {
setMockResponse.get(urls.enrollment.enrollmentsList(), [])
setMockResponse.get(urls.enrollment.enrollmentsListV2(), [])
setMockResponse.get(urls.programEnrollments.enrollmentsList(), [])
setMockResponse.get(urls.programEnrollments.enrollmentsListV2(), [])
Expand Down Expand Up @@ -145,7 +144,6 @@ describe("OrganizationContent", () => {
}),
]
// Override the default empty enrollments for this test
setMockResponse.get(urls.enrollment.enrollmentsList(), enrollments)
setMockResponse.get(urls.enrollment.enrollmentsListV2(), enrollments)

renderWithProviders(<OrganizationContent orgSlug={orgX.slug} />)
Expand All @@ -170,11 +168,11 @@ describe("OrganizationContent", () => {

// Check based on the actual enrollment status, not array position
if (course.enrollment?.status === EnrollmentStatus.Enrolled) {
expect(indicator).toHaveTextContent("Enrolled")
expect(indicator).toHaveTextContent(/^Enrolled$/)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As originally written, a bunch of tests in this file passed irrespective of whether the text was "Enrolled" or "Not Enrolled". toHaveTextContent is only an exact match if you use regex to specify start/stop. If you pass a string, it just checks for inclusion.

} else if (course.enrollment?.status === EnrollmentStatus.Completed) {
expect(indicator).toHaveTextContent("Completed")
expect(indicator).toHaveTextContent(/^Completed$/)
} else {
expect(indicator).toHaveTextContent("Not Enrolled")
expect(indicator).toHaveTextContent(/^Not Enrolled$/)
}
})
})
Expand Down Expand Up @@ -754,9 +752,25 @@ describe("OrganizationContent", () => {
)?.id,
course: { id: courses[0].id, title: courses[0].title },
},
grades: [], // No grades = enrolled but not completed
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Notes:

  • By default, enrollment factory creates a certificate. (We might want to revisit that choice.)
  • Subsequent to fix dashboard card enrollment association and display #2792, the comment grades: [], // No grades = enrolled but not completed is innacurate. The enrollment in this test SHOULD be "Completed" since it has a cert but no grades.

b2b_contract_id: contracts[0].id,
b2b_organization_id: contracts[0].organization,
certificate: { uuid: faker.string.uuid(), link: faker.internet.url() },
}),
factories.enrollment.courseEnrollment({
run: {
id: courses[1].courseruns.find(
(r) => r.b2b_contract === contractIds[0],
)?.id,
course: { id: courses[1].id, title: courses[1].title },
},
b2b_contract_id: contracts[0].id,
b2b_organization_id: contracts[0].organization,
certificate: null,
grades: [],
}),
]
// Override enrollments for this test
setMockResponse.get(urls.enrollment.enrollmentsListV2(), enrollments)

const program = factories.programs.program({
courses: courses.map((c) => c.id),
Expand All @@ -771,24 +785,22 @@ describe("OrganizationContent", () => {
contracts,
)

// Override enrollments for this test
setMockResponse.get(urls.enrollment.enrollmentsList(), enrollments)
Comment on lines -774 to -775
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On main, this test was not behaving as intended.

  • if you remove all enrollments and unset this mock, it still passes
  • The enrollments were not being matched to the org because enrollment-level b2b props were missing
  • The assertions still passed for aforementioned reason, namely toHaveTextContent("Enrolled") was matching "Not Enrolled".


renderWithProviders(<OrganizationContent orgSlug={orgX.slug} />)

const cards = await within(
await screen.findByTestId("org-program-root"),
).findAllByTestId("enrollment-card-desktop")

expect(cards.length).toBe(3)
// First card should show enrolled status
const firstCardStatus = within(cards[0]).getByTestId("enrollment-status")
expect(firstCardStatus).toHaveTextContent("Enrolled")
const cardStatus0 = within(cards[0]).getByTestId("enrollment-status")
expect(cardStatus0).toHaveTextContent(/^Completed$/)

// Remaining cards should show not enrolled
for (let i = 1; i < cards.length; i++) {
const cardStatus = within(cards[i]).getByTestId("enrollment-status")
expect(cardStatus).toHaveTextContent("Not Enrolled")
}
const cardStatus1 = within(cards[1]).getByTestId("enrollment-status")
expect(cardStatus1).toHaveTextContent(/^Enrolled$/)

const cardStatus2 = within(cards[2]).getByTestId("enrollment-status")
expect(cardStatus2).toHaveTextContent(/^Not Enrolled$/)
})

test("shows the not found screen if the organization is not found by orgSlug", async () => {
Expand Down Expand Up @@ -987,7 +999,6 @@ describe("OrganizationContent", () => {
contracts,
)

setMockResponse.get(urls.enrollment.enrollmentsList(), [enrollment])
setMockResponse.get(urls.enrollment.enrollmentsListV2(), [enrollment])

renderWithProviders(<OrganizationContent orgSlug={orgX.slug} />)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ describe("OrganizationRedirect", () => {
beforeEach(() => {
mockReplace.mockClear()
localStorage.clear()
setMockResponse.get(urls.enrollment.enrollmentsList(), [])
setMockResponse.get(urls.programEnrollments.enrollmentsList(), [])
setMockResponse.get(urls.contracts.contractsList(), [])
})
Expand Down
Loading
Loading