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
2 changes: 1 addition & 1 deletion frontends/main/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
"next-nprogress-bar": "^2.4.2",
"ol-components": "0.0.0",
"ol-utilities": "0.0.0",
"posthog-js": "^1.157.2",
"posthog-js": "^1.296.1",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-slick": "^0.30.2",
Expand Down
20 changes: 15 additions & 5 deletions frontends/main/src/app-pages/ProductPages/CoursePage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,13 @@ import { assertHeadings } from "ol-test-utilities"
import { notFound } from "next/navigation"

import { useFeatureFlagEnabled } from "posthog-js/react"
import { useFeatureFlagsLoaded } from "@/common/useFeatureFlagsLoaded"
import invariant from "tiny-invariant"

jest.mock("posthog-js/react")
const mockedUseFeatureFlagEnabled = jest.mocked(useFeatureFlagEnabled)
jest.mock("@/common/useFeatureFlagsLoaded")
const mockedUseFeatureFlagsLoaded = jest.mocked(useFeatureFlagsLoaded)

const makeCourse = factories.courses.course
const makePage = factories.pages.coursePageItem
Expand Down Expand Up @@ -46,12 +49,19 @@ const setupApis = ({
describe("CoursePage", () => {
beforeEach(() => {
mockedUseFeatureFlagEnabled.mockReturnValue(true)
mockedUseFeatureFlagsLoaded.mockReturnValue(true)
})

test.each([true, false])(
test.each([
{ hasLoaded: true, isEnabled: true, shouldNotFound: false },
{ hasLoaded: true, isEnabled: false, shouldNotFound: true },
{ hasLoaded: false, isEnabled: true, shouldNotFound: false },
{ hasLoaded: false, isEnabled: false, shouldNotFound: false },
])(
"Calls noFound if and only the feature flag is disabled",
async (isEnabled) => {
async ({ isEnabled, hasLoaded, shouldNotFound }) => {
mockedUseFeatureFlagEnabled.mockReturnValue(isEnabled)
mockedUseFeatureFlagsLoaded.mockReturnValue(hasLoaded)

const course = makeCourse()
const page = makePage({ course_details: course })
Expand All @@ -60,10 +70,10 @@ describe("CoursePage", () => {
url: `/courses/${course.readable_id}/`,
})

if (isEnabled) {
expect(notFound).not.toHaveBeenCalled()
} else {
if (shouldNotFound) {
expect(notFound).toHaveBeenCalled()
} else {
expect(notFound).not.toHaveBeenCalled()
}
},
)
Expand Down
6 changes: 4 additions & 2 deletions frontends/main/src/app-pages/ProductPages/CoursePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import ProductPageTemplate, {
} from "./ProductPageTemplate"
import { CoursePageItem } from "@mitodl/mitxonline-api-axios/v2"
import { DEFAULT_RESOURCE_IMG } from "ol-utilities"
import { useFeatureFlagsLoaded } from "@/common/useFeatureFlagsLoaded"

type CoursePageProps = {
readableId: string
Expand Down Expand Up @@ -76,10 +77,11 @@ const CoursePage: React.FC<CoursePageProps> = ({ readableId }) => {
const page = pages.data?.items[0]
const course = courses.data?.results?.[0]
const enabled = useFeatureFlagEnabled(FeatureFlags.ProductPageCourse)
if (enabled === false) {
const flagsLoaded = useFeatureFlagsLoaded()
if (!flagsLoaded) return null
if (!enabled) {
return notFound()
}
if (!enabled) return

const doneLoading = pages.isSuccess && courses.isSuccess

Expand Down
20 changes: 15 additions & 5 deletions frontends/main/src/app-pages/ProductPages/ProgramPage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,12 @@ import { notFound } from "next/navigation"
import { useFeatureFlagEnabled } from "posthog-js/react"
import invariant from "tiny-invariant"
import { ResourceTypeEnum } from "api"
import { useFeatureFlagsLoaded } from "@/common/useFeatureFlagsLoaded"

jest.mock("posthog-js/react")
const mockedUseFeatureFlagEnabled = jest.mocked(useFeatureFlagEnabled)
jest.mock("@/common/useFeatureFlagsLoaded")
const mockedUseFeatureFlagsLoaded = jest.mocked(useFeatureFlagsLoaded)

const makeProgram = factories.programs.program
const makePage = factories.pages.programPageItem
Expand Down Expand Up @@ -65,12 +68,19 @@ const setupApis = ({
describe("ProgramPage", () => {
beforeEach(() => {
mockedUseFeatureFlagEnabled.mockReturnValue(true)
mockedUseFeatureFlagsLoaded.mockReturnValue(true)
})

test.each([true, false])(
test.each([
{ hasLoaded: true, isEnabled: true, shouldNotFound: false },
{ hasLoaded: true, isEnabled: false, shouldNotFound: true },
{ hasLoaded: false, isEnabled: true, shouldNotFound: false },
{ hasLoaded: false, isEnabled: false, shouldNotFound: false },
])(
"Calls noFound if and only the feature flag is disabled",
async (isEnabled) => {
async ({ isEnabled, hasLoaded, shouldNotFound }) => {
mockedUseFeatureFlagEnabled.mockReturnValue(isEnabled)
mockedUseFeatureFlagsLoaded.mockReturnValue(hasLoaded)

const program = makeProgram()
const page = makePage({ program_details: program })
Expand All @@ -79,10 +89,10 @@ describe("ProgramPage", () => {
url: `/programs/${program.readable_id}/`,
})

if (isEnabled) {
expect(notFound).not.toHaveBeenCalled()
} else {
if (shouldNotFound) {
expect(notFound).toHaveBeenCalled()
} else {
expect(notFound).not.toHaveBeenCalled()
}
},
)
Expand Down
6 changes: 4 additions & 2 deletions frontends/main/src/app-pages/ProductPages/ProgramPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { ProgramSummary } from "./ProductSummary"
import { DEFAULT_RESOURCE_IMG } from "ol-utilities"
import { learningResourceQueries } from "api/hooks/learningResources"
import { ResourceTypeEnum } from "api"
import { useFeatureFlagsLoaded } from "@/common/useFeatureFlagsLoaded"

type ProgramPageProps = {
readableId: string
Expand Down Expand Up @@ -89,10 +90,11 @@ const ProgramPage: React.FC<ProgramPageProps> = ({ readableId }) => {
const program = programs.data?.results?.[0]
const programResource = programResources.data?.results?.[0]
const enabled = useFeatureFlagEnabled(FeatureFlags.ProductPageCourse)
if (enabled === false) {
const flagsLoaded = useFeatureFlagsLoaded()
if (!flagsLoaded) return null
if (!enabled) {
return notFound()
}
if (!enabled) return

const isLoading =
pages.isLoading || programs.isLoading || programResources.isLoading
Expand Down
90 changes: 90 additions & 0 deletions frontends/main/src/common/useFeatureFlagsLoaded.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { renderHook, waitFor } from "@testing-library/react"
import { act } from "react"
import { usePostHog } from "posthog-js/react"
import type { PostHog } from "posthog-js"

// Import the real implementation, not the mock
const { useFeatureFlagsLoaded } = jest.requireActual("./useFeatureFlagsLoaded")

jest.mock("posthog-js/react", () => {
return {
__esModule: true,
usePostHog: jest.fn(),
}
})

const mockUsePostHog = jest.mocked(usePostHog)

describe("useFeatureFlagsLoaded", () => {
let onFeatureFlagsCallback:
| ((flags: string[], variants: Record<string, string | boolean>) => void)
| null = null

const createPostHogMock = (hasLoadedFlags: boolean) => {
return {
featureFlags: {
hasLoadedFlags,
},
onFeatureFlags: jest.fn((callback) => {
onFeatureFlagsCallback = callback
return () => {} // Return cleanup function
}),
} as unknown as PostHog
}

beforeEach(() => {
onFeatureFlagsCallback = null
})

afterEach(() => {
jest.clearAllMocks()
})

test("Returns false when flags have not loaded yet", () => {
mockUsePostHog.mockReturnValue(createPostHogMock(false))

const { result } = renderHook(() => useFeatureFlagsLoaded())

expect(result.current).toBe(false)
})

test("Returns true when flags have already loaded", () => {
mockUsePostHog.mockReturnValue(createPostHogMock(true))

const { result } = renderHook(() => useFeatureFlagsLoaded())

expect(result.current).toBe(true)
})

test("Returned value is reactive and re-renders when onFeatureFlags callback runs", async () => {
mockUsePostHog.mockReturnValue(createPostHogMock(false))

const { result } = renderHook(() => useFeatureFlagsLoaded())

// Initially should be false
expect(result.current).toBe(false)

// Simulate flags loading by calling the callback
expect(onFeatureFlagsCallback).not.toBeNull()
act(() => {
onFeatureFlagsCallback!([], {})
})

// Wait for the state to update
await waitFor(() => {
expect(result.current).toBe(true)
})
})

test("onFeatureFlags callback is registered on mount", () => {
const mockPostHog = createPostHogMock(false)
mockUsePostHog.mockReturnValue(mockPostHog)

renderHook(() => useFeatureFlagsLoaded())

expect(mockPostHog.onFeatureFlags).toHaveBeenCalledTimes(1)
expect(mockPostHog.onFeatureFlags).toHaveBeenCalledWith(
expect.any(Function),
)
})
})
17 changes: 17 additions & 0 deletions frontends/main/src/common/useFeatureFlagsLoaded.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { usePostHog } from "posthog-js/react"
import { useState, useEffect } from "react"

const useFeatureFlagsLoaded = () => {
const posthog = usePostHog()
const [hasLoaded, setHasLoaded] = useState(
posthog.featureFlags.hasLoadedFlags,
)
useEffect(() => {
posthog.onFeatureFlags(() => {
setHasLoaded(true)
})
}, [posthog])
return hasLoaded
}

export { useFeatureFlagsLoaded }
40 changes: 29 additions & 11 deletions yarn.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading