Skip to content
Closed
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
7 changes: 7 additions & 0 deletions RELEASE.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
Release Notes
=============

Version 0.63.4
--------------

- hide stay updated button on free products (#3201)
- Deindex unpublished test_mode resources, not contentfiles (#3192)
- feat: enable course access to staff user before the start date (#3182)

Version 0.63.3 (Released April 15, 2026)
--------------

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,9 +74,13 @@ const futureDashboardCourse: typeof mitxOnlineCourse = (...overrides) => {

const mitxUser = mitxonline.factories.user.user

const setupUserApis = () => {
const mitxUser = mitxonline.factories.user.user()
setMockResponse.get(mitxonline.urls.userMe.get(), mitxUser)
const setupUserApis = (overrides?: Parameters<typeof mitxUser>[0]) => {
const userData = mitxonline.factories.user.user({
is_staff: false,
...overrides,
})
setMockResponse.get(mitxonline.urls.userMe.get(), userData)
return userData
}

describe.each([
Expand Down Expand Up @@ -246,6 +250,36 @@ describe.each([
},
)

test("Courseware CTA is a navigable link for staff even when course has not started", async () => {
setupUserApis({ is_staff: true })
const coursewareUrl = faker.internet.url()
const course = futureDashboardCourse()
const enrollment = mitxonline.factories.enrollment.courseEnrollment({
enrollment_mode: EnrollmentMode.Audit,
grades: [],
certificate: null,
run: {
...course.courseruns[0],
course: course,
courseware_url: coursewareUrl,
},
})
renderWithProviders(
<DashboardCard
resource={{
type: DashboardType.CourseRunEnrollment,
data: enrollment,
}}
/>,
)
const card = getCard()
const coursewareCTA = await within(card).findByRole("link", {
name: "Continue",
})
expect(coursewareCTA).toBeEnabled()
expect(coursewareCTA).toHaveAttribute("href", coursewareUrl)
})

test("Courseware CTA is disabled when no enrollable runs exist", () => {
setupUserApis()
const course = mitxOnlineCourse({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -389,6 +389,7 @@ const useEnrollmentHandler = () => {
createEnrollment.isPending ||
createVerifiedProgramEnrollment.isPending ||
replaceBasketItem.isPending,
mitxOnlineUser: mitxOnlineUser.data,
}
}

Expand All @@ -402,6 +403,7 @@ type CoursewareButtonProps = {
noun: string
isProgram?: boolean
isPending?: boolean
isStaff?: boolean
"data-testid"?: string
onClick?: React.MouseEventHandler<HTMLButtonElement>
}
Expand Down Expand Up @@ -445,6 +447,7 @@ const CoursewareButton = styled(
isProgram,
onClick,
isPending,
isStaff,
...others
}: CoursewareButtonProps) => {
const coursewareText = getCoursewareTextAndIcon({
Expand All @@ -457,7 +460,12 @@ const CoursewareButton = styled(
const hasEnrolled = enrollmentStatus !== EnrollmentStatus.NotEnrolled

// Programs or enrolled courses with started runs: show link
if ((isProgram || hasEnrolled) && (hasStarted || !startDate) && href) {
// Staff can access courseware even before the course has started
if (
(isProgram || hasEnrolled) &&
(hasStarted || !startDate || isStaff) &&
href
) {
return (
<ButtonLink
size="small"
Expand All @@ -473,10 +481,12 @@ const CoursewareButton = styled(
}

// Determine if button should be disabled
// Staff can access courseware even before the course has started
const isDisabled = Boolean(
disabled ||
(!hasEnrolled && !onClick) || // Not enrolled and no click handler
(hasEnrolled && !!startDate && !hasStarted), // Enrolled but course hasn't started yet
(hasEnrolled && !href && !onClick) || // Enrolled but no action available
(hasEnrolled && !!startDate && !hasStarted && !isStaff), // Enrolled but course hasn't started yet
)

return (
Expand Down Expand Up @@ -664,6 +674,7 @@ const DashboardCard: React.FC<DashboardCardProps> = ({
onUpgradeError,
}) => {
const enrollment = useEnrollmentHandler()
const mitxOnlineUser = enrollment.mitxOnlineUser
const useProductPages = useFeatureFlagEnabled(
FeatureFlags.MitxOnlineProductPages,
)
Expand Down Expand Up @@ -835,6 +846,7 @@ const DashboardCard: React.FC<DashboardCardProps> = ({
isProgram={false}
disabled={disableEnrollment}
isPending={enrollment.isPending}
isStaff={mitxOnlineUser?.is_staff}
onClick={coursewareButtonClick}
/>
</>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -366,6 +366,7 @@ const useEnrollmentHandler = () => {
createEnrollment.isPending ||
createVerifiedProgramEnrollment.isPending ||
replaceBasketItem.isPending,
mitxOnlineUser: mitxOnlineUser.data,
}
}

Expand All @@ -376,6 +377,7 @@ type CoursewareButtonProps = {
href?: string | null
disabled?: boolean
className?: string
isStaff?: boolean
"data-testid"?: string
onClick?: React.MouseEventHandler<HTMLButtonElement>
}
Expand Down Expand Up @@ -413,6 +415,7 @@ const CoursewareButton = styled(
disabled,
className,
onClick,
isStaff,
...others
}: CoursewareButtonProps) => {
const coursewareText = getCoursewareButtonStyle({
Expand All @@ -422,7 +425,8 @@ const CoursewareButton = styled(
const hasStarted = startDate && isInPast(startDate)
const hasEnrolled = enrollmentStatus !== EnrollmentStatus.NotEnrolled

if (hasEnrolled && (hasStarted || !startDate) && href) {
// Staff can access courseware even before the course has started
if (hasEnrolled && (hasStarted || !startDate || isStaff) && href) {
return (
<StyledCoursewareButtonLink
size="small"
Expand All @@ -437,10 +441,12 @@ const CoursewareButton = styled(
}

// Determine if button should be disabled
// Staff can access courseware even before the course has started
const isDisabled = Boolean(
disabled ||
(!hasEnrolled && !onClick) || // Not enrolled and no click handler
(hasEnrolled && !!startDate && !hasStarted), // Enrolled but course hasn't started yet
(hasEnrolled && !href && !onClick) || // Enrolled but no action available
(hasEnrolled && !!startDate && !hasStarted && !isStaff), // Enrolled but course hasn't started yet
)

return (
Expand Down Expand Up @@ -699,6 +705,7 @@ const DashboardCourseCard: React.FC<DashboardCourseCardProps> = ({
onUpgradeError,
}) => {
const enrollment = useEnrollmentHandler()
const mitxOnlineUser = enrollment.mitxOnlineUser

const title = getTitle(resource)
const enrollmentStatus = getDashboardEnrollmentStatus(resource)
Expand Down Expand Up @@ -838,6 +845,7 @@ const DashboardCourseCard: React.FC<DashboardCourseCardProps> = ({
href={buttonHref ?? coursewareUrl}
endDate={courseRun?.end_date ?? enrollmentRun?.end_date}
disabled={disableEnrollment}
isStaff={mitxOnlineUser?.is_staff}
onClick={coursewareButtonClick}
/>
</CoursewareActionColumn>
Expand Down
93 changes: 93 additions & 0 deletions frontends/main/src/app-pages/ProductPages/CoursePage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { renderWithProviders, waitFor, screen, within } from "@/test-utils"
import CoursePage from "./CoursePage"
import { assertHeadings } from "ol-test-utilities"
import { notFound } from "next/navigation"
import { useStayUpdatedEnv } from "./test-utils/stayUpdated"

import { useFeatureFlagEnabled } from "posthog-js/react"
import { useFeatureFlagsLoaded } from "@/common/useFeatureFlagsLoaded"
Expand Down Expand Up @@ -267,4 +268,96 @@ describe("CoursePage", () => {
expect(notFound).toHaveBeenCalled()
})
})

describe("Stay Updated button", () => {
useStayUpdatedEnv()

test("Shows button when all course runs have only the verified enrollment mode", async () => {
const verifiedMode = factories.courses.enrollmentMode({
mode_slug: "verified",
})
const course = makeCourse({
courseruns: [
factories.courses.courseRun({ enrollment_modes: [verifiedMode] }),
factories.courses.courseRun({ enrollment_modes: [verifiedMode] }),
],
})
const page = makePage({ course_details: course })
setupApis({ course, page })
renderWithProviders(<CoursePage readableId={course.readable_id} />)

expect(
await screen.findByRole("button", { name: "Stay Updated" }),
).toBeInTheDocument()
})

test.each([
{
label: "one run has a non-verified mode",
buildRuns: () => [
factories.courses.courseRun({
enrollment_modes: [
factories.courses.enrollmentMode({ mode_slug: "verified" }),
],
}),
factories.courses.courseRun({
enrollment_modes: [
factories.courses.enrollmentMode({ mode_slug: "audit" }),
],
}),
],
},
{
label: "a run has mixed verified and non-verified modes",
buildRuns: () => [
factories.courses.courseRun({
enrollment_modes: [
factories.courses.enrollmentMode({ mode_slug: "verified" }),
factories.courses.enrollmentMode({ mode_slug: "audit" }),
],
}),
],
},
{
label: "a run has no enrollment modes",
buildRuns: () => [
factories.courses.courseRun({ enrollment_modes: [] }),
],
},
{
label: "the course has no runs",
buildRuns: () => [],
},
])("Hides button when $label", async ({ buildRuns }) => {
const course = makeCourse({ courseruns: buildRuns() })
const page = makePage({ course_details: course })
setupApis({ course, page })
renderWithProviders(<CoursePage readableId={course.readable_id} />)

await screen.findByRole("heading", { name: page.title })
expect(
screen.queryByRole("button", { name: "Stay Updated" }),
).not.toBeInTheDocument()
})

test("Hides button when Stay Updated form ID is not configured", async () => {
delete process.env.NEXT_PUBLIC_STAY_UPDATED_HUBSPOT_FORM_ID
const verifiedMode = factories.courses.enrollmentMode({
mode_slug: "verified",
})
const course = makeCourse({
courseruns: [
factories.courses.courseRun({ enrollment_modes: [verifiedMode] }),
],
})
const page = makePage({ course_details: course })
setupApis({ course, page })
renderWithProviders(<CoursePage readableId={course.readable_id} />)

await screen.findByRole("heading", { name: page.title })
expect(
screen.queryByRole("button", { name: "Stay Updated" }),
).not.toBeInTheDocument()
})
})
})
11 changes: 11 additions & 0 deletions frontends/main/src/app-pages/ProductPages/CoursePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import ProductPageTemplate from "./ProductPageTemplate"
import WhatYoullLearnSection from "./WhatYoullLearnSection"
import HowYoullLearnSection, { DEFAULT_HOW_DATA } from "./HowYoullLearnSection"
import { DEFAULT_RESOURCE_IMG } from "ol-utilities"
import { isVerifiedEnrollmentMode } from "@/common/mitxonline"
import { useFeatureFlagsLoaded } from "@/common/useFeatureFlagsLoaded"
import CourseInfoBox from "./InfoBoxCourse"
import CourseEnrollmentButton from "./CourseEnrollmentButton"
Expand Down Expand Up @@ -76,6 +77,16 @@ const CoursePage: React.FC<CoursePageProps> = ({ readableId }) => {
enrollmentAction={
<StyledCourseEnrollmentButton course={course} variant="bordered" />
}
showStayUpdated={
course.courseruns.length > 0 &&
course.courseruns.every(
(run) =>
run.enrollment_modes.length > 0 &&
run.enrollment_modes.every((mode) =>
isVerifiedEnrollmentMode(mode.mode_slug),
),
)
}
>
{page.about ? (
<AboutSection productNoun="Course" aboutHtml={page.about} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { renderWithProviders, screen } from "@/test-utils"
import ProductPageTemplate from "./ProductPageTemplate"
import { useHubspotFormDetail } from "api/hooks/hubspot"
import NiceModal from "@ebay/nice-modal-react"
import { STAY_UPDATED_FORM_ID } from "./test-utils/stayUpdated"

jest.mock("api/hooks/hubspot", () => ({
...jest.requireActual("api/hooks/hubspot"),
Expand All @@ -27,9 +28,11 @@ const mockedNiceModalShow = NiceModal.show as jest.MockedFunction<
typeof NiceModal.show
>

const STAY_UPDATED_FORM_ID = "4f423dc7-5b08-430b-a9fb-920b7f9597ed"

const renderProductPageTemplate = () => {
const renderProductPageTemplate = ({
showStayUpdated,
}: {
showStayUpdated?: boolean
} = {}) => {
setMockResponse.get(urls.userMe.get(), { is_authenticated: false })
renderWithProviders(
<ProductPageTemplate
Expand All @@ -39,6 +42,7 @@ const renderProductPageTemplate = () => {
imageSrc="/test-image.jpg"
infoBox={<div>Info box</div>}
enrollmentAction={<button type="button">Enroll</button>}
showStayUpdated={showStayUpdated}
>
<div>Page content</div>
</ProductPageTemplate>,
Expand Down Expand Up @@ -66,7 +70,9 @@ describe("ProductPageTemplate stay-updated trigger", () => {
expect(
screen.queryByRole("button", { name: "Stay Updated" }),
).not.toBeInTheDocument()
expect(mockedUseHubspotFormDetail).toHaveBeenCalledWith(undefined)
expect(mockedUseHubspotFormDetail).toHaveBeenCalledWith(undefined, {
enabled: false,
})
})

it("opens the modal when form id is configured even if the form is not yet fetched", () => {
Expand All @@ -76,7 +82,7 @@ describe("ProductPageTemplate stay-updated trigger", () => {
isError: false,
} as ReturnType<typeof useHubspotFormDetail>)

renderProductPageTemplate()
renderProductPageTemplate({ showStayUpdated: true })

const button = screen.getByRole("button", { name: "Stay Updated" })
expect(button).toBeInTheDocument()
Expand All @@ -93,7 +99,7 @@ describe("ProductPageTemplate stay-updated trigger", () => {
isError: true,
} as ReturnType<typeof useHubspotFormDetail>)

renderProductPageTemplate()
renderProductPageTemplate({ showStayUpdated: true })

const button = screen.getByRole("button", { name: "Stay Updated" })
expect(button).toBeInTheDocument()
Expand All @@ -110,7 +116,7 @@ describe("ProductPageTemplate stay-updated trigger", () => {
isError: false,
} as unknown as ReturnType<typeof useHubspotFormDetail>)

renderProductPageTemplate()
renderProductPageTemplate({ showStayUpdated: true })

const button = screen.getByRole("button", { name: "Stay Updated" })
expect(button).toBeInTheDocument()
Expand Down
Loading
Loading