diff --git a/Dockerfile b/Dockerfile index 07a6b3c323..b149ba5f05 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,15 +13,11 @@ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ libcairo2-dev \ poppler-utils \ default-jre && \ - # chromium 150.0.7871.46 crashes on startup in headless mode (Debian bug - # #1141488), breaking all chromedriver sessions. Pin the last working - # build from snapshot.debian.org; unpin once a fixed version ships. - echo "deb [check-valid-until=no] http://snapshot.debian.org/archive/debian-security/20260628T000000Z/ trixie-security main" >> /etc/apt/sources.list && \ + echo "deb http://deb.debian.org/debian/ sid main" >> /etc/apt/sources.list && \ apt-get update -qqy && \ apt-get install -qqy --no-install-recommends \ - chromium=149.0.7827.196-1~deb13u1 \ - chromium-common=149.0.7827.196-1~deb13u1 \ - chromium-driver=149.0.7827.196-1~deb13u1 + -o Dpkg::Options::="--force-confnew" \ + chromium chromium-driver FROM base AS deps diff --git a/RELEASE.rst b/RELEASE.rst index 1584bfb7b4..414c213944 100644 --- a/RELEASE.rst +++ b/RELEASE.rst @@ -1,6 +1,21 @@ Release Notes ============= +Version 0.78.5 +-------------- + +- fix(docker): force non-interactive conffile resolution for the sid chromium install (#3888) +- Trust X-Forwarded-Proto from the API gateway (#3876) +- fix: Make unenrolled module titles keyboard-focusable (#3871) +- Take the canonical page URL from learn_url (#3863) +- Bound Renovate to the service versions we actually run (#3865) +- Add refunds to the receipt page (#3823) +- vector search: add course metadata to context (#3852) +- Revert "Pin chromium to 149.0.7827.196 to avoid broken 150 headless build (#3584)" (#3878) +- Move the shared media players into page-components (#3875) +- vector search: embedding call request hedging (#3853) +- Update dependency youtube-transcript-api to >=1.2.4,<1.3 (#3861) + Version 0.78.2 (Released September 01, 2026) -------------- diff --git a/docker-compose.services.yml b/docker-compose.services.yml index 6ab1ac488e..0b7da9766b 100644 --- a/docker-compose.services.yml +++ b/docker-compose.services.yml @@ -56,7 +56,7 @@ services: start_period: 30s qdrant: - image: qdrant/qdrant:latest + image: qdrant/qdrant:v1.18.3 ports: - "6333:6333" volumes: diff --git a/frontends/api/package.json b/frontends/api/package.json index ee613bc0ee..4dab1000b7 100644 --- a/frontends/api/package.json +++ b/frontends/api/package.json @@ -36,7 +36,7 @@ }, "dependencies": { "@mitodl/mit-learn-api-axios": "2026.8.17", - "@mitodl/mitxonline-api-axios": "2026.8.31", + "@mitodl/mitxonline-api-axios": "2026.8.31-1", "@tanstack/react-query": "^5.66.0", "axios": "^1.12.2", "tiny-invariant": "^1.3.3" diff --git a/frontends/api/src/mitxonline/hooks/orders/index.ts b/frontends/api/src/mitxonline/hooks/orders/index.ts index 59d858ef60..d1a31f8af0 100644 --- a/frontends/api/src/mitxonline/hooks/orders/index.ts +++ b/frontends/api/src/mitxonline/hooks/orders/index.ts @@ -1,3 +1,27 @@ import { orderQueries, orderKeys } from "./queries" +import { useMutation, useQueryClient } from "@tanstack/react-query" +import { ordersApi } from "../../clients" +import type { RefundRequestRequest } from "@mitodl/mitxonline-api-axios/v2" +import type { MutationHookOptions } from "../../../mutations/mutationMeta" -export { orderQueries, orderKeys } +/** + * Submit a learner's refund request for an order. + * + * Invalidates the order's receipt, since a successful request moves its + * `refund_status` to `requested` and the card rendering it has to follow. + */ +const useCreateRefundRequest = ({ meta }: MutationHookOptions = {}) => { + const queryClient = useQueryClient() + return useMutation({ + mutationFn: (opts: RefundRequestRequest) => + ordersApi.ordersRefundRequestsCreate({ RefundRequestRequest: opts }), + onSettled: (_data, _error, opts) => { + queryClient.invalidateQueries({ + queryKey: orderKeys.receipt(opts.order), + }) + }, + meta, + }) +} + +export { orderQueries, orderKeys, useCreateRefundRequest } diff --git a/frontends/api/src/mitxonline/test-utils/factories/orders.ts b/frontends/api/src/mitxonline/test-utils/factories/orders.ts index e9ee249935..f7d1d4440d 100644 --- a/frontends/api/src/mitxonline/test-utils/factories/orders.ts +++ b/frontends/api/src/mitxonline/test-utils/factories/orders.ts @@ -10,8 +10,10 @@ import type { PaginatedOrderHistoryList, Product, RedeemedDiscount, + RefundRequest, TransactionLine, } from "@mitodl/mitxonline-api-axios/v2" +import { RefundStatusEnum } from "@mitodl/mitxonline-api-axios/v2" const transactionLine = ( overrides: Partial = {}, @@ -26,6 +28,8 @@ const transactionLine = ( total_paid: faker.commerce.price({ min: 50, max: 500 }), discount: "0.00", price: faker.commerce.price({ min: 50, max: 500 }), + // MITx Online course runs almost always carry an audit track. + has_free_audit: true, ...overrides, }) @@ -85,7 +89,13 @@ const order = (overrides: Partial = {}): Order => ({ lines: [transactionLine()], discounts: [], refunds: [], - refund_eligible: false, + // Default to the state a freshly purchased order is in, so tests opt into + // the other refund states rather than out of an arbitrary one. + refund_eligible: true, + refund_status: RefundStatusEnum.Eligible, + refund_deadline: faker.date.future().toISOString(), + refund_requested_on: null, + refund_reviewed_on: null, reference_number: faker.string.alphanumeric(10), created_on: faker.date.past().toISOString(), transactions: orderTransactions(), @@ -93,6 +103,16 @@ const order = (overrides: Partial = {}): Order => ({ ...overrides, }) +const refundRequest = ( + overrides: Partial = {}, +): RefundRequest => ({ + order: faker.number.int(), + refund_reason: "course_not_as_expected", + refund_reason_text: "", + consent_given: true, + ...overrides, +}) + /** * The default `purchasable_object` has only an `id`, which matches no variant — * pass a shaped object (with `course`, or neither `course` nor `run_tag`) when the @@ -135,7 +155,7 @@ const orderHistory = (overrides: Partial = {}): OrderHistory => ({ created_on: faker.date.past().toISOString(), titles: [], updated_on: faker.date.past().toISOString(), - refund_eligible: false, + refund_eligible: true, ...overrides, }) @@ -159,5 +179,6 @@ export { line, product, redeemedDiscount, + refundRequest, transactionLine, } diff --git a/frontends/api/src/mitxonline/test-utils/urls.ts b/frontends/api/src/mitxonline/test-utils/urls.ts index 6009de6460..953d1b3073 100644 --- a/frontends/api/src/mitxonline/test-utils/urls.ts +++ b/frontends/api/src/mitxonline/test-utils/urls.ts @@ -158,6 +158,7 @@ const orders = { `${getApiBaseUrl()}/api/v0/orders/receipt/${orderId}/`, historyList: (params?: OrdersApiOrdersHistoryListRequest) => `${getApiBaseUrl()}/api/v0/orders/history/${queryify(params)}`, + refundRequests: () => `${getApiBaseUrl()}/api/v0/orders/refund-requests/`, } const verifiedProgramEnrollments = { diff --git a/frontends/main/package.json b/frontends/main/package.json index 08a242128c..b6eae086d2 100644 --- a/frontends/main/package.json +++ b/frontends/main/package.json @@ -18,8 +18,8 @@ "@mitodl/arithmix": "^0.2.5", "@mitodl/course-search-utils": "^3.8.0", "@mitodl/hacksnack": "^0.1.2", - "@mitodl/mitxonline-api-axios": "2026.8.31", - "@mitodl/smoot-design": "6.33.4", + "@mitodl/mitxonline-api-axios": "2026.8.31-1", + "@mitodl/smoot-design": "6.34.0", "@mui/base": "5.0.0-beta.70", "@mui/material": "^6.4.5", "@mui/material-nextjs": "^6.4.3", diff --git a/frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/CardShared.tsx b/frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/CardShared.tsx index 70d67bc8af..01c79c72ec 100644 --- a/frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/CardShared.tsx +++ b/frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/CardShared.tsx @@ -80,17 +80,37 @@ const TitleLink = styled(Link)(({ theme }) => ({ ...theme.typography.subtitle1, })) -const TitleText = styled.h3<{ clickable?: boolean }>( - ({ theme, clickable }) => ({ - margin: 0, - ...theme.typography.subtitle1, - color: theme.custom.colors.darkGray2, - cursor: clickable ? "pointer" : "default", - [theme.breakpoints.down("sm")]: { - maxWidth: "calc(100% - 16px)", - }, - }), -) +const TitleText = styled.h3(({ theme }) => ({ + margin: 0, + ...theme.typography.subtitle1, + color: theme.custom.colors.darkGray2, + [theme.breakpoints.down("sm")]: { + maxWidth: "calc(100% - 16px)", + }, +})) + +/** + * A title rendered as an actual button (nested inside a TitleHeading), for + * cards whose title click triggers an action rather than navigation. Keeps + * the heading itself focusable-by-heading-role for AT heading navigation + * while making the title text a real, keyboard-operable control. + */ +const TitleButton = styled.button(({ theme }) => ({ + margin: 0, + padding: 0, + border: "none", + background: "none", + appearance: "none", + display: "block", + width: "100%", + textAlign: "left", + cursor: "pointer", + color: theme.custom.colors.darkGray2, + ...theme.typography.subtitle1, + [theme.breakpoints.down("sm")]: { + maxWidth: "calc(100% - 16px)", + }, +})) const SubtitleLinkRoot = styled.div(({ theme }) => ({ display: "flex", @@ -336,6 +356,7 @@ export { TitleHeading, TitleLink, TitleText, + TitleButton, SubtitleLinkRoot, SubtitleLink, MenuButton, diff --git a/frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/EnrolledCourseCard.tsx b/frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/EnrolledCourseCard.tsx index 87bcdf47d2..bbf0fb7af4 100644 --- a/frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/EnrolledCourseCard.tsx +++ b/frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/EnrolledCourseCard.tsx @@ -270,16 +270,14 @@ export const EnrolledCourseCard = ({ const mitxOnlineUser = useQuery(mitxUserQueries.me()) const isStaff = mitxOnlineUser.data?.is_staff /** - * Only verified enrollments can have a receipt, so the lookup is skipped - * entirely for audit ones. Every card shares one `orders/history` query (same - * cache key), so this is a single request for the whole dashboard rather than - * one per card. + * Enrollment mode does not decide this: a refund returns the learner to audit, + * and the receipt is where they confirm it went through. Whether an order + * exists is the only question, and the lookup answers it. + * + * Every card shares one `orders/history` query (same cache key), so this is a + * single request for the whole dashboard rather than one per card. */ - const receiptResolution = useOrderIdForRun( - isVerifiedEnrollmentMode(enrollment?.enrollment_mode) - ? (run?.id ?? null) - : null, - ) + const receiptResolution = useOrderIdForRun(run?.id ?? null) const title = isCompact ? course.title : run?.title || course.title const coursewareUrl = run?.courseware_url const certificateLink = getCertificateLink( @@ -484,7 +482,6 @@ export const EnrolledCourseCard = ({ ) const receiptMenuItem = getReceiptMenuItem( - enrollment?.enrollment_mode, receiptResolution, receiptByRunView(run.id), ) diff --git a/frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/ProgramAsCourseCard.test.tsx b/frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/ProgramAsCourseCard.test.tsx index 2c414ec898..c99242aa87 100644 --- a/frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/ProgramAsCourseCard.test.tsx +++ b/frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/ProgramAsCourseCard.test.tsx @@ -9,6 +9,7 @@ import { } from "@/test-utils" import { makeRequest } from "api/test-utils" import * as mitxonline from "api/mitxonline-test-utils" +import { setupOrderHistory } from "./test-utils" import { ProgramAsCourseCard } from "./ProgramAsCourseCard" import { waitFor } from "@testing-library/react" import invariant from "tiny-invariant" @@ -91,6 +92,9 @@ describe("ProgramAsCourseCard", () => { mitxonline.urls.userMe.get(), mitxonline.factories.user.user(), ) + // Every enrollment card looks up order history to decide whether to offer a + // Receipt item, regardless of enrollment mode. + setupOrderHistory() return { courseProgram: program, diff --git a/frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/ProgramEnrollmentCard.tsx b/frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/ProgramEnrollmentCard.tsx index a55713539e..680358f73a 100644 --- a/frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/ProgramEnrollmentCard.tsx +++ b/frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/ProgramEnrollmentCard.tsx @@ -61,12 +61,11 @@ export const ProgramEnrollmentCard = ({ programEnrollment.enrollment_mode, ) /** - * Skipped for audit enrollments, which never have a receipt. Shares one - * `orders/history` query with every other card on the dashboard. + * Not gated on enrollment mode: a refund returns the learner to audit, and the + * receipt is where they confirm it went through. Shares one `orders/history` + * query with every other card on the dashboard. */ - const receiptResolution = useOrderIdForProgram( - upgradedAndIncomplete ? programId : null, - ) + const receiptResolution = useOrderIdForProgram(programId) const displayMode = program.display_mode const titleSection = ( @@ -125,7 +124,6 @@ export const ProgramEnrollmentCard = ({ }) } const receiptMenuItem = getReceiptMenuItem( - programEnrollment.enrollment_mode, receiptResolution, receiptByProgramView(programId), ) diff --git a/frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/UnenrolledCourseCard.test.tsx b/frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/UnenrolledCourseCard.test.tsx index 5d799ce52b..209815ae59 100644 --- a/frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/UnenrolledCourseCard.test.tsx +++ b/frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/UnenrolledCourseCard.test.tsx @@ -43,9 +43,12 @@ describe.each([ setupLocationMock() - test("shows course title as clickable text (not link) when not enrolled", () => { + test("shows course title as a focusable button (not link) when not enrolled", () => { setupUserApis() - const run = mitxonline.factories.courses.courseRun({ b2b_contract: null }) + const run = mitxonline.factories.courses.courseRun({ + b2b_contract: null, + is_enrollable: true, + }) const course = mitxOnlineCourse({ title: run.title, // match so heading text is predictable courseruns: [run], @@ -59,13 +62,17 @@ describe.each([ expect( within(card).queryByRole("link", { name: run.title }), ).not.toBeInTheDocument() - // Should be clickable text wrapped in a heading + // Should be wrapped in a heading (for AT heading navigation)... expect( within(card).getByRole("heading", { name: run.title }), ).toBeInTheDocument() + // ...containing a real, keyboard-focusable button (hq#10262) + expect( + within(card).getByRole("button", { name: run.title }), + ).toBeInTheDocument() }) - test("shows course title as clickable text when B2B contract", () => { + test("shows course title as a focusable button when B2B contract", () => { setupUserApis() const b2bContractId = faker.number.int() const run = mitxonline.factories.courses.courseRun({ @@ -89,6 +96,9 @@ describe.each([ expect( within(card).getByRole("heading", { name: run.title }), ).toBeInTheDocument() + expect( + within(card).getByRole("button", { name: run.title }), + ).toBeInTheDocument() }) test("accepts a className and Component prop", () => { @@ -349,11 +359,11 @@ describe.each([ ) const card = getCard() - const titleHeading = within(card).getByRole("heading") + const titleButton = within(card).getByRole("button", { name: run.title }) const triggerElement = trigger === "button" ? within(card).getByTestId("courseware-button") - : titleHeading + : titleButton await user.click(triggerElement) @@ -393,7 +403,7 @@ describe.each([ const triggerElement = trigger === "button" ? within(card).getByTestId("courseware-button") - : within(card).getByRole("heading") + : within(card).getByRole("button", { name: run.title }) await user.click(triggerElement) @@ -501,7 +511,7 @@ describe.each([ const triggerElement = trigger === "button" ? within(card).getByTestId("courseware-button") - : within(card).getByRole("heading") + : within(card).getByRole("button", { name: run.title }) await user.click(triggerElement) @@ -542,7 +552,7 @@ describe.each([ const triggerElement = trigger === "button" ? within(card).getByTestId("courseware-button") - : within(card).getByRole("heading") + : within(card).getByRole("button", { name: run.title }) await user.click(triggerElement) @@ -593,7 +603,7 @@ describe.each([ const triggerElement = trigger === "button" ? within(card).getByTestId("courseware-button") - : within(card).getByRole("heading") + : within(card).getByRole("button", { name: run.title }) await user.click(triggerElement) @@ -651,7 +661,7 @@ describe.each([ const triggerElement = trigger === "button" ? within(card).getByTestId("courseware-button") - : within(card).getByRole("heading") + : within(card).getByRole("button", { name: run.title }) await user.click(triggerElement) diff --git a/frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/UnenrolledCourseCard.tsx b/frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/UnenrolledCourseCard.tsx index e56cdde0af..b92c88cdd6 100644 --- a/frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/UnenrolledCourseCard.tsx +++ b/frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/UnenrolledCourseCard.tsx @@ -9,7 +9,9 @@ import { CardRoot, CardTypeText, CoursewareButton, + TitleHeading, TitleText, + TitleButton, CourseDateSummary, Separator, } from "./CardShared" @@ -97,14 +99,14 @@ export const UnenrolledCourseCard = ({ handleEnrollmentClick() } const isCompact = layout === "compact" - const titleSection = ( - - {title} - + const titleSection = isDisabled ? ( + {title} + ) : ( + + + {title} + + ) const hasCourseDateText = getCourseDateText(courseRun?.start_date, courseRun?.end_date) !== null diff --git a/frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/receiptMenuItem.test.ts b/frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/receiptMenuItem.test.ts index 2df1ab94ba..e581c6dfe4 100644 --- a/frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/receiptMenuItem.test.ts +++ b/frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/receiptMenuItem.test.ts @@ -13,35 +13,15 @@ const resolution = ( }) describe("getReceiptMenuItem", () => { - test("returns null when enrollment mode is undefined", () => { - expect( - getReceiptMenuItem(undefined, resolution({ orderId: 87 }), RESOLVER_HREF), - ).toBeNull() - }) - - test("returns null for audit enrollments, since auditing is free", () => { - expect( - getReceiptMenuItem("audit", resolution({ orderId: 87 }), RESOLVER_HREF), - ).toBeNull() - }) - test("returns null while the order lookup is still pending", () => { expect( - getReceiptMenuItem( - "verified", - resolution({ isPending: true }), - RESOLVER_HREF, - ), + getReceiptMenuItem(resolution({ isPending: true }), RESOLVER_HREF), ).toBeNull() }) test("links straight to the resolved receipt", () => { expect( - getReceiptMenuItem( - "verified", - resolution({ orderId: 87 }), - RESOLVER_HREF, - ), + getReceiptMenuItem(resolution({ orderId: 87 }), RESOLVER_HREF), ).toEqual( expect.objectContaining({ key: "receipt", @@ -51,14 +31,23 @@ describe("getReceiptMenuItem", () => { ) }) + /** + * A refund moves the learner back to the audit track, and the receipt is where + * they see that it came through. Enrollment mode is not consulted at all; the + * order lookup is the only thing that decides. + */ + test("still links once the order has been refunded and the learner is auditing", () => { + expect( + getReceiptMenuItem(resolution({ orderId: 87 }), RESOLVER_HREF), + ).toEqual(expect.objectContaining({ href: "/receipt/87" })) + }) + /** * The lookup succeeded and found nothing — e.g. verified via a program purchase * that upgraded an existing audit enrollment, which creates no order. */ test("hides the item when the lookup found no order", () => { - expect( - getReceiptMenuItem("verified", resolution(), RESOLVER_HREF), - ).toBeNull() + expect(getReceiptMenuItem(resolution(), RESOLVER_HREF)).toBeNull() }) /** @@ -68,11 +57,7 @@ describe("getReceiptMenuItem", () => { */ test("falls back to the resolver route when the lookup failed", () => { expect( - getReceiptMenuItem( - "verified", - resolution({ isError: true }), - RESOLVER_HREF, - ), + getReceiptMenuItem(resolution({ isError: true }), RESOLVER_HREF), ).toEqual( expect.objectContaining({ key: "receipt", diff --git a/frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/receiptMenuItem.ts b/frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/receiptMenuItem.ts index 730d609562..30f007ad0d 100644 --- a/frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/receiptMenuItem.ts +++ b/frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/receiptMenuItem.ts @@ -1,5 +1,4 @@ import { SimpleMenuItem } from "ol-components" -import { isVerifiedEnrollmentMode } from "@/common/mitxonline" import { receiptView } from "@/common/urls" import type { OrderIdResolution } from "@/common/mitxonline/useOrderIdForResource" @@ -7,8 +6,11 @@ import type { OrderIdResolution } from "@/common/mitxonline/useOrderIdForResourc * The "Receipt" item for a dashboard card, or null when there is nothing to link * to. * - * Verified track alone is not enough — a program purchase can upgrade an existing - * audit enrollment without creating any order, leaving no receipt. + * Whether an order exists is the only thing that decides this. Enrollment mode + * does not: a refund moves the learner back to audit, and that is exactly when + * they want the receipt. A learner who reached the verified track without + * paying — a program purchase can upgrade an audit enrollment without creating + * an order — has no matching order, so the lookup below already hides the item. * * The three reasons `orderId` can be null are deliberately not equivalent: * @@ -20,11 +22,9 @@ import type { OrderIdResolution } from "@/common/mitxonline/useOrderIdForResourc * - looked up and found nothing — genuinely no receipt, so hide the item */ const getReceiptMenuItem = ( - enrollmentMode: string | null | undefined, resolution: OrderIdResolution, resolverHref: string, ): SimpleMenuItem | null => { - if (!enrollmentMode || !isVerifiedEnrollmentMode(enrollmentMode)) return null if (resolution.isPending) return null const href = diff --git a/frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/test-utils.ts b/frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/test-utils.ts index ec4cfb8cd4..a3b584f91d 100644 --- a/frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/test-utils.ts +++ b/frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/test-utils.ts @@ -26,10 +26,11 @@ const makeGrade = factories.enrollment.grade const makeContract = factories.contracts.contract /** - * Mock the order history that verified enrollment cards fetch to decide whether to - * show a "Receipt" item. Required in any suite rendering a verified enrollment, - * or the unmocked request fails the test. Defaults to an empty history (no - * receipt); pass `runId`/`programId` to make one resolve. + * Mock the order history that enrollment cards fetch to decide whether to show a + * "Receipt" item. Required in any suite rendering an enrollment card, whatever + * its mode — a refunded order leaves the learner auditing and still has a + * receipt — or the unmocked request fails the test. Defaults to an empty history + * (no receipt); pass `runId`/`programId` to make one resolve. */ const setupOrderHistory = ({ runId, diff --git a/frontends/main/src/app-pages/HomePage/VideoShortsModal.test.tsx b/frontends/main/src/app-pages/HomePage/VideoShortsModal.test.tsx index a9588145ee..98133e687b 100644 --- a/frontends/main/src/app-pages/HomePage/VideoShortsModal.test.tsx +++ b/frontends/main/src/app-pages/HomePage/VideoShortsModal.test.tsx @@ -42,7 +42,7 @@ const makeMockPlayer = () => ({ const mockHandles: MockPlayerHandle[] = [] -jest.mock("@/app-pages/VideoPlaylistCollectionPage/VideoJsPlayer", () => ({ +jest.mock("@/page-components/VideoPlayer/VideoJsPlayer", () => ({ __esModule: true, default: ({ sources, diff --git a/frontends/main/src/app-pages/HomePage/VideoShortsModal.tsx b/frontends/main/src/app-pages/HomePage/VideoShortsModal.tsx index 5386d74ce8..0be030a911 100644 --- a/frontends/main/src/app-pages/HomePage/VideoShortsModal.tsx +++ b/frontends/main/src/app-pages/HomePage/VideoShortsModal.tsx @@ -13,7 +13,7 @@ import { ActionButton, VisuallyHidden } from "@mitodl/smoot-design" import { useWindowDimensions } from "ol-utilities" import type { VideoResource } from "api/v1" import MITOpenLearningLogo from "@/public/images/mit-open-learning-logo.svg" -import VideoJsPlayer from "@/app-pages/VideoPlaylistCollectionPage/VideoJsPlayer" +import VideoJsPlayer from "@/page-components/VideoPlayer/VideoJsPlayer" import type Player from "video.js/dist/types/player" import { FocusTrap } from "@mui/base/FocusTrap" import { usePostHog } from "posthog-js/react" diff --git a/frontends/main/src/app-pages/PodcastPage/PodcastPlayer.tsx b/frontends/main/src/app-pages/PodcastPage/PodcastPlayer.tsx index edaf96be9b..277f0a1b9d 100644 --- a/frontends/main/src/app-pages/PodcastPage/PodcastPlayer.tsx +++ b/frontends/main/src/app-pages/PodcastPage/PodcastPlayer.tsx @@ -11,8 +11,14 @@ import { RiCloseLine, RiErrorWarningLine, } from "@remixicon/react" -import { useAudioPlayer, formatClockTime } from "./useAudioPlayer" -import { usePlaybackRecovery, RETRYING_STATUS } from "./usePlaybackRecovery" +import { + useAudioPlayer, + formatClockTime, +} from "@/page-components/PodcastEmbedPlayer/useAudioPlayer" +import { + usePlaybackRecovery, + RETRYING_STATUS, +} from "@/page-components/PodcastEmbedPlayer/usePlaybackRecovery" import { TrackInfo as TrackInfoBase, TrackTitle as TrackTitleBase, @@ -28,7 +34,7 @@ import { PlaybackError, PlaybackErrorText, RetryButton, -} from "./AudioPlayer.styled" +} from "@/page-components/PodcastEmbedPlayer/AudioPlayer.styled" // ─── Types ──────────────────────────────────────────────────────────────────── diff --git a/frontends/main/src/app-pages/PodcastPage/PodcastsListingPage/helpers.ts b/frontends/main/src/app-pages/PodcastPage/PodcastsListingPage/helpers.ts index a689791671..d888051b74 100644 --- a/frontends/main/src/app-pages/PodcastPage/PodcastsListingPage/helpers.ts +++ b/frontends/main/src/app-pages/PodcastPage/PodcastsListingPage/helpers.ts @@ -2,29 +2,13 @@ import moment from "moment" import { ResourceTypeEnum } from "api/v1" import type { LearningResource, PodcastEpisodeParent } from "api/v1" +// Defined in common/ so page-components can use it too; re-exported here to +// keep this module the single helper import for the podcast pages. +export { getEpisodeAudioUrl } from "@/common/podcasts" + export const formatApproxCount = (count: number): string => count >= 100 ? `${Math.floor(count / 100) * 100}+` : String(count) -/** - * The URL to play/link for an episode. - * - * Defaults to the direct `audio_url`, falling back to `episode_link`. Pass - * `{ allowEpisodeLink: false }` when the URL is fed straight into an `