diff --git a/src/components/Stepper/CheckoutStepperContainer.tsx b/src/components/Stepper/CheckoutStepperContainer.tsx index 86259bf5..c343ffb8 100644 --- a/src/components/Stepper/CheckoutStepperContainer.tsx +++ b/src/components/Stepper/CheckoutStepperContainer.tsx @@ -1,23 +1,36 @@ +import { getConfig } from '@edx/frontend-platform/config'; import { Col, Row, Stack, Stepper } from '@openedx/paragon'; import { ReactElement, useEffect } from 'react'; import { PurchaseSummary } from '@/components/PurchaseSummary'; import { StepperTitle } from '@/components/Stepper/StepperTitle'; -import { AccountDetails, BillingDetails, PlanDetails } from '@/components/Stepper/Steps'; +import { AccountDetails, BillingDetails, EssentialsAcademicSelection, PlanDetails } from '@/components/Stepper/Steps'; import { CheckoutSubstepKey } from '@/constants/checkout'; import useCurrentStep from '@/hooks/useCurrentStep'; +import { isFeatureEnabled } from '@/utils/common'; -const Steps = (): ReactElement => ( - <> - - - - -); +const Steps = (): ReactElement => { + const { + FEATURE_SELF_SERVICE_ESSENTIALS, + FEATURE_SELF_SERVICE_ESSENTIALS_KEY, + } = getConfig(); + return ( + <> + { + isFeatureEnabled( + FEATURE_SELF_SERVICE_ESSENTIALS, + FEATURE_SELF_SERVICE_ESSENTIALS_KEY, + ) && + } + + + + + ); +}; const CheckoutStepperContainer = (): ReactElement => { const { currentStepKey, currentSubstepKey } = useCurrentStep(); - useEffect(() => { const preventUnload = (e: BeforeUnloadEvent) => { if (currentSubstepKey !== CheckoutSubstepKey.Success) { diff --git a/src/components/Stepper/StepperContent/EssentialsAcademicSelectionContent.tsx b/src/components/Stepper/StepperContent/EssentialsAcademicSelectionContent.tsx new file mode 100644 index 00000000..99465115 --- /dev/null +++ b/src/components/Stepper/StepperContent/EssentialsAcademicSelectionContent.tsx @@ -0,0 +1,14 @@ +import type { UseFormReturn } from 'react-hook-form'; + +interface EssentialsAcademicSelectionContentProps { + form: UseFormReturn; +} + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +const EssentialsAcademicSelectionContent = ({ form: _form }: EssentialsAcademicSelectionContentProps) => ( + <> + Essentials Academic Selection + +); + +export default EssentialsAcademicSelectionContent; diff --git a/src/components/Stepper/StepperContent/index.ts b/src/components/Stepper/StepperContent/index.ts index 87da8570..920bcce0 100644 --- a/src/components/Stepper/StepperContent/index.ts +++ b/src/components/Stepper/StepperContent/index.ts @@ -4,3 +4,4 @@ export { default as PlanDetailsRegisterContent } from './PlanDetailsRegisterCont export { default as AccountDetailsContent } from './AccountDetailsContent'; export { default as BillingDetailsContent } from './BillingDetailsContent'; export { default as BillingDetailsSuccessContent } from './BillingDetailsSuccessContent'; +export { default as EssentialsAcademicSelectionContent } from './EssentialsAcademicSelectionContent'; diff --git a/src/components/Stepper/Steps/EssentialsAcademicSelection.tsx b/src/components/Stepper/Steps/EssentialsAcademicSelection.tsx new file mode 100644 index 00000000..127bc4b8 --- /dev/null +++ b/src/components/Stepper/Steps/EssentialsAcademicSelection.tsx @@ -0,0 +1,8 @@ +import { EssentialsAcademicSelectionPage } from '@/components/academic-selection-page'; + +// TODO: unnecessary layer of abstraction, just move component logic into this file. +const EssentialsAcademicSelection: React.FC = () => ( + +); + +export default EssentialsAcademicSelection; diff --git a/src/components/Stepper/Steps/hooks/useStepperContent.tsx b/src/components/Stepper/Steps/hooks/useStepperContent.tsx index a44b3756..090799f1 100644 --- a/src/components/Stepper/Steps/hooks/useStepperContent.tsx +++ b/src/components/Stepper/Steps/hooks/useStepperContent.tsx @@ -2,6 +2,7 @@ import { AccountDetailsContent, BillingDetailsContent, BillingDetailsSuccessContent, + EssentialsAcademicSelectionContent, PlanDetailsContent, PlanDetailsLoginContent, PlanDetailsRegisterContent, @@ -12,6 +13,7 @@ import useCurrentPage from '@/hooks/useCurrentPage'; type StepperContentComponent = React.FC<{ form?: any }>; const StepperContentByPage = { + EssentialsAcademicSelection: EssentialsAcademicSelectionContent, PlanDetails: PlanDetailsContent, PlanDetailsLogin: PlanDetailsLoginContent, PlanDetailsRegister: PlanDetailsRegisterContent, diff --git a/src/components/Stepper/Steps/index.ts b/src/components/Stepper/Steps/index.ts index f7006aa5..d77ee853 100644 --- a/src/components/Stepper/Steps/index.ts +++ b/src/components/Stepper/Steps/index.ts @@ -1,3 +1,4 @@ export { default as PlanDetails } from './PlanDetails'; export { default as AccountDetails } from './AccountDetails'; export { default as BillingDetails } from './BillingDetails'; +export { default as EssentialsAcademicSelection } from './EssentialsAcademicSelection'; diff --git a/src/components/academic-selection-page/EssentialsAcademicSelectionPage.tsx b/src/components/academic-selection-page/EssentialsAcademicSelectionPage.tsx new file mode 100644 index 00000000..2c2f59d2 --- /dev/null +++ b/src/components/academic-selection-page/EssentialsAcademicSelectionPage.tsx @@ -0,0 +1,45 @@ +import { zodResolver } from '@hookform/resolvers/zod'; +import { Stack, Stepper } from '@openedx/paragon'; +import { useMemo } from 'react'; +import { Helmet } from 'react-helmet'; +import { useForm } from 'react-hook-form'; + +import { useFormValidationConstraints } from '@/components/app/data'; +import { useStepperContent } from '@/components/Stepper/Steps/hooks'; +import { CheckoutStepKey, DataStoreKey } from '@/constants/checkout'; +import { useCheckoutFormStore, useCurrentPageDetails } from '@/hooks/index'; + +const EssentialsAcademicSelectionPage = () => { + const StepperContent = useStepperContent(); + const { data: formValidationConstraints } = useFormValidationConstraints(); + const eventKey = CheckoutStepKey.Essentials; + const { + formSchema, + } = useCurrentPageDetails(); + const essentialsFormData = useCheckoutFormStore((state) => state.formData[DataStoreKey.EssentialsAcademicSelection]); + + const essentialsAcademicSelectionSchema = useMemo(() => ( + formSchema(formValidationConstraints) + ), [formSchema, formValidationConstraints]); + + const form = useForm({ + mode: 'onTouched', + resolver: zodResolver(essentialsAcademicSelectionSchema), + defaultValues: essentialsFormData, + }); + + return ( + <> + + + + + + + + + + ); +}; + +export default EssentialsAcademicSelectionPage; diff --git a/src/components/academic-selection-page/index.ts b/src/components/academic-selection-page/index.ts new file mode 100644 index 00000000..090744cf --- /dev/null +++ b/src/components/academic-selection-page/index.ts @@ -0,0 +1 @@ +export { default as EssentialsAcademicSelectionPage } from './EssentialsAcademicSelectionPage'; diff --git a/src/components/app/routes/loaders/checkoutStepperLoader.ts b/src/components/app/routes/loaders/checkoutStepperLoader.ts index f87270f5..3e2f9f8a 100644 --- a/src/components/app/routes/loaders/checkoutStepperLoader.ts +++ b/src/components/app/routes/loaders/checkoutStepperLoader.ts @@ -8,6 +8,10 @@ import { CheckoutPageRoute, DataStoreKey } from '@/constants/checkout'; import { checkoutFormStore } from '@/hooks/useCheckoutFormStore'; import { extractPriceId, getCheckoutPageDetails, getStepFromParams } from '@/utils/checkout'; +async function essentialsAcademicSelectionLoader(): Promise { + return null; +} + /** * Route loader for Plan Details page. * @@ -169,6 +173,7 @@ async function billingDetailsSuccessLoader(queryClient: QueryClient): Promise Promise> = { + EssentialsAcademicSelection: essentialsAcademicSelectionLoader, PlanDetails: planDetailsLoader, PlanDetailsLogin: planDetailsLoginLoader, PlanDetailsRegister: planDetailsRegisterLoader, diff --git a/src/components/app/routes/loaders/rootLoader.ts b/src/components/app/routes/loaders/rootLoader.ts index fbc1e94f..2101a671 100644 --- a/src/components/app/routes/loaders/rootLoader.ts +++ b/src/components/app/routes/loaders/rootLoader.ts @@ -15,6 +15,7 @@ import { } from '@/components/app/routes/loaders/utils'; import { CheckoutPageRoute, EssentialsPageRoute } from '@/constants/checkout'; import { extractPriceId } from '@/utils/checkout'; +import { isFeatureEnabled } from '@/utils/common'; /** * Factory that creates the root route loader for the Enterprise Checkout MFE. @@ -87,14 +88,9 @@ const makeRootLoader = ( // Feature flag check if (routeFeatureKey) { - const sessionKey = sessionStorage.getItem(SSP_SESSION_KEY); + const isUnlocked = isFeatureEnabled(false, routeFeatureKey); - const isUnlockedBySiteKey = !!FEATURE_SELF_SERVICE_SITE_KEY - && sessionKey === FEATURE_SELF_SERVICE_SITE_KEY; - - const isUnlockedByRouteKey = sessionKey === routeFeatureKey; - - if (!isUnlockedBySiteKey && !isUnlockedByRouteKey) { + if (!isUnlocked) { const featureParam = new URL(request.url).searchParams.get('feature'); const paramIsSiteKey = !!FEATURE_SELF_SERVICE_SITE_KEY @@ -121,11 +117,11 @@ const makeRootLoader = ( * Essentials routes do not participate in checkout intent logic. * This check happens AFTER feature flag validation. */ - const isCheckoutRoute = !Object.values(EssentialsPageRoute).some(route => isPathMatch(currentPath, route)); - - if (!isCheckoutRoute) { - return null; - } + // const isCheckoutRoute = !Object.values(EssentialsPageRoute).some(route => isPathMatch(currentPath, route)); + // + // if (!isCheckoutRoute) { + // return null; + // } // Fetch basic info about authenticated user from JWT token, and also hydrate it with additional // information from the `/api/user/v1/accounts/` endpoint. We need access to the diff --git a/src/components/app/routes/loaders/utils.ts b/src/components/app/routes/loaders/utils.ts index 7cd2b0a6..c5d73f11 100644 --- a/src/components/app/routes/loaders/utils.ts +++ b/src/components/app/routes/loaders/utils.ts @@ -210,6 +210,7 @@ interface PrerequisiteCheck { * and the route that should be returned if that slice is invalid. */ export const prerequisiteSpec: Record>> = { + Essentials: [], PlanDetails: [], AccountDetails: [ { diff --git a/src/components/app/routes/tests/rootLoader.test.ts b/src/components/app/routes/tests/rootLoader.test.ts index 582bc18c..b5512ec5 100644 --- a/src/components/app/routes/tests/rootLoader.test.ts +++ b/src/components/app/routes/tests/rootLoader.test.ts @@ -197,7 +197,7 @@ describe('makeRootLoader (rootLoader) tests', () => { FEATURE_SELF_SERVICE_ESSENTIALS_KEY: 'essentials-key', }); - const result = getFeatureForPath(EssentialsPageRoute.AcademicSelection); + const result = getFeatureForPath(EssentialsPageRoute.EssentialsAcademicSelection); expect(result).toBe('essentials-key'); }); @@ -236,7 +236,7 @@ describe('makeRootLoader (rootLoader) tests', () => { const loader = makeRootLoader(queryClient); const result = await loader({ - request: makeRequest(`${EssentialsPageRoute.AcademicSelection}?feature=SSP_ESSENTIALS_CHECKOUT`), + request: makeRequest(`${EssentialsPageRoute.EssentialsAcademicSelection}?feature=SSP_ESSENTIALS_CHECKOUT`), } as any); expect(logInfo).toHaveBeenCalledWith( @@ -259,7 +259,7 @@ describe('makeRootLoader (rootLoader) tests', () => { const loader = makeRootLoader(queryClient); await expect( - loader({ request: makeRequest(EssentialsPageRoute.AcademicSelection) } as any), + loader({ request: makeRequest(EssentialsPageRoute.EssentialsAcademicSelection) } as any), ).rejects.toThrow('Self-service purchasing is not enabled'); expect(logError).toHaveBeenCalledWith('Self-service purchasing is not enabled'); @@ -277,7 +277,7 @@ describe('makeRootLoader (rootLoader) tests', () => { const loader = makeRootLoader(queryClient); const result = await loader({ - request: makeRequest(`${EssentialsPageRoute.AcademicSelection}?feature=SSP_ESSENTIALS_CHECKOUT`), + request: makeRequest(`${EssentialsPageRoute.EssentialsAcademicSelection}?feature=SSP_ESSENTIALS_CHECKOUT`), } as any); expect(logInfo).toHaveBeenCalledWith( @@ -301,7 +301,7 @@ describe('makeRootLoader (rootLoader) tests', () => { const result = await loader({ request: makeRequest( - `${EssentialsPageRoute.AcademicSelection}?feature=SSP_SITE_CHECKOUT`, + `${EssentialsPageRoute.EssentialsAcademicSelection}?feature=SSP_SITE_CHECKOUT`, ), } as any); diff --git a/src/components/plan-details-pages/PlanDetailsPage.tsx b/src/components/plan-details-pages/PlanDetailsPage.tsx index 5fe0f49a..34f22731 100644 --- a/src/components/plan-details-pages/PlanDetailsPage.tsx +++ b/src/components/plan-details-pages/PlanDetailsPage.tsx @@ -153,9 +153,9 @@ const PlanDetailsPage = () => { }, }); - const onSubmitCallbacks: { - [K in SubmitCallbacks]: (data: PlanDetailsData | PlanDetailsLoginPageData | PlanDetailsRegisterPageData) => void - } = { + const onSubmitCallbacks: Record void> = { + [SubmitCallbacks.EssentialsAcademicSelection]: () => {}, [SubmitCallbacks.PlanDetails]: async (data: PlanDetailsData) => { const { validationDecisions, isValid: isValidAdminEmailField } = await validateFieldDetailed( 'adminEmail', diff --git a/src/constants/checkout.ts b/src/constants/checkout.ts index fe701d92..86546d26 100644 --- a/src/constants/checkout.ts +++ b/src/constants/checkout.ts @@ -6,12 +6,14 @@ import { validateFieldDetailed } from '@/components/app/data/services/validation import { serverValidationError } from '@/utils/common'; export enum CheckoutStepKey { + Essentials = 'essentials', PlanDetails = 'plan-details', AccountDetails = 'account-details', BillingDetails = 'billing-details', } export enum CheckoutSubstepKey { + AcademicSelection = 'academic-selection', Login = 'login', Register = 'register', Success = 'success', @@ -279,9 +281,15 @@ export const BillingDetailsSchema = (constraints: CheckoutContextFieldConstraint // Simple empty schema - no validation needed for coming soon page // @ts-ignore // eslint-disable-next-line @typescript-eslint/no-unused-vars -export const AcademicSelectionSchema = (constraints: CheckoutContextFieldConstraints) => (z.object({})); +export const EssentialsAcademicSelectionSchema = (constraints: CheckoutContextFieldConstraints) => (z.object({})); + +// NEW ROUTES - Essentials flow +export const EssentialsPageRoute = { + EssentialsAcademicSelection: `/${CheckoutStepKey.Essentials}/${CheckoutSubstepKey.AcademicSelection}`, +} as const; export const CheckoutPageRoute = { + ...EssentialsPageRoute, PlanDetails: `/${CheckoutStepKey.PlanDetails}`, PlanDetailsLogin: `/${CheckoutStepKey.PlanDetails}/${CheckoutSubstepKey.Login}`, PlanDetailsRegister: `/${CheckoutStepKey.PlanDetails}/${CheckoutSubstepKey.Register}`, @@ -290,18 +298,13 @@ export const CheckoutPageRoute = { BillingDetailsSuccess: `/${CheckoutStepKey.BillingDetails}/${CheckoutSubstepKey.Success}`, } as const; -// NEW ROUTES - Essentials flow -export const EssentialsPageRoute = { - AcademicSelection: `/essentials/${EssentialsStepKey.AcademicSelection}`, -} as const; - // NEW PAGE DETAILS - Essentials flow export const EssentialsPageDetails = { - AcademicSelection: { - step: 'AcademicSelection', - substep: undefined, - formSchema: AcademicSelectionSchema, - route: EssentialsPageRoute.AcademicSelection, + EssentialsAcademicSelection: { + step: 'Essentials', + substep: 'AcademicSelection', + formSchema: EssentialsAcademicSelectionSchema, + route: EssentialsPageRoute.EssentialsAcademicSelection, title: defineMessages({ id: 'essentials.academicSelection.title', defaultMessage: 'Academic Selection', @@ -312,6 +315,7 @@ export const EssentialsPageDetails = { } as const; export const CheckoutPageDetails: { [K in CheckoutPage]: CheckoutPageDetails } = { + ...EssentialsPageDetails, PlanDetails: { step: 'PlanDetails', substep: undefined, @@ -413,12 +417,14 @@ export const authenticatedSteps = [ ] as const; export enum DataStoreKey { + EssentialsAcademicSelection = 'EssentialsAcademicSelection', PlanDetails = 'PlanDetails', AccountDetails = 'AccountDetails', BillingDetails = 'BillingDetails', } export enum SubmitCallbacks { + EssentialsAcademicSelection = 'EssentialsAcademicSelection', PlanDetails = 'PlanDetails', PlanDetailsLogin = 'PlanDetailsLogin', PlanDetailsRegister = 'PlanDetailsRegister', diff --git a/src/routes.tsx b/src/routes.tsx index 9ea48f09..347b4f80 100644 --- a/src/routes.tsx +++ b/src/routes.tsx @@ -10,8 +10,7 @@ import AppShell from '@/components/app/routes/AppShell'; import { makeCheckoutStepperLoader, makeRootLoader } from '@/components/app/routes/loaders'; import RouterFallback from '@/components/app/routes/RouterFallback'; import CheckoutPage from '@/components/checkout-page/CheckoutPage'; -import AcademicSelection from '@/components/essentials-page/AcademicSelection'; -import { authenticatedSteps, CheckoutStepKey, EssentialsStepKey } from '@/constants/checkout'; +import { authenticatedSteps, CheckoutStepKey } from '@/constants/checkout'; import { ErrorPage } from './components/ErrorPage'; @@ -99,34 +98,34 @@ export function getRoutes(queryClient: QueryClient) { loader: getRouteLoader(makeRootLoader, queryClient), errorElement: , children: [ - { - path: 'essentials', - element: ( - - }> - - - - ), - children: [ - { - index: true, - element: , - }, - { - path: EssentialsStepKey.AcademicSelection, - element: ( - - - - ), - }, - ], - }, - { - path: 'essentials/*', - element: , - }, + // { + // path: 'essentials', + // element: ( + // + // }> + // + // + // + // ), + // children: [ + // { + // index: true, + // element: , + // }, + // { + // path: EssentialsStepKey.AcademicSelection, + // element: ( + // + // + // + // ), + // }, + // ], + // }, + // { + // path: 'essentials/*', + // element: , + // }, { path: '/', loader: getRouteLoader(makeRootLoader, queryClient), diff --git a/src/types/types.d.ts b/src/types/types.d.ts index 98bf60a7..c2950d58 100644 --- a/src/types/types.d.ts +++ b/src/types/types.d.ts @@ -63,11 +63,11 @@ declare global { * ============================== */ - type CheckoutStep = 'PlanDetails' | 'AccountDetails' | 'BillingDetails'; + type CheckoutStep = 'Essentials' | 'PlanDetails' | 'AccountDetails' | 'BillingDetails'; - type CheckoutSubstep = 'Login' | 'Register' | 'Success'; + type CheckoutSubstep = 'AcademicSelection' | 'Login' | 'Register' | 'Success'; - type CheckoutPage = 'PlanDetails' | 'PlanDetailsLogin' | 'PlanDetailsRegister' | 'AccountDetails' | 'BillingDetails' | 'BillingDetailsSuccess'; + type CheckoutPage = 'EssentialsAcademicSelection' | 'PlanDetails' | 'PlanDetailsLogin' | 'PlanDetailsRegister' | 'AccountDetails' | 'BillingDetails' | 'BillingDetailsSuccess'; export type CheckoutPageRouteValue = (typeof CheckoutPageRoute)[keyof typeof CheckoutPageRoute]; @@ -88,6 +88,7 @@ declare global { /** * Form data types derived from Zod schemas */ + type EssentialAcademicSelectionData = z.infer; type PlanDetailsData = z.infer; type PlanDetailsLoginPageData = z.infer; type PlanDetailsRegisterPageData = z.infer; @@ -98,6 +99,7 @@ declare global { * Maps step names to their corresponding data types */ interface StepDataMap { + 'EssentialsAcademicSelection': Partial; 'PlanDetails': Partial; 'AccountDetails': Partial; 'BillingDetails': Partial; diff --git a/src/utils/common.ts b/src/utils/common.ts index a52c4bb3..876139ef 100644 --- a/src/utils/common.ts +++ b/src/utils/common.ts @@ -1,4 +1,5 @@ import { sendTrackEvent } from '@edx/frontend-platform/analytics'; +import { getConfig } from '@edx/frontend-platform/config'; import { logError } from '@edx/frontend-platform/logging'; import dayjs from 'dayjs'; @@ -109,6 +110,30 @@ const sendEnterpriseCheckoutTrackingEvent = ({ ); }; +/** + * Determines whether a feature is enabled. + * A feature is enabled if its configuration value is true, or if a valid + * feature key or site key is present in the session storage. + * + * @param {boolean} enabled - The configuration value for the feature. + * @param {string|null} featureKey - The specific override key for the feature. + * @returns {boolean} True if the feature is enabled; otherwise, false. + */ +const isFeatureEnabled = (enabled: boolean, featureKey?: string | null): boolean => { + const SSP_SESSION_KEY = 'edx.checkout.self-service-purchasing'; + const { FEATURE_SELF_SERVICE_SITE_KEY } = getConfig(); + const sessionKey = sessionStorage.getItem(SSP_SESSION_KEY); + + if (enabled) { + return true; + } + + const isUnlockedBySiteKey = !!FEATURE_SELF_SERVICE_SITE_KEY && sessionKey === FEATURE_SELF_SERVICE_SITE_KEY; + const isUnlockedByFeatureKey = !!featureKey && sessionKey === featureKey; + + return isUnlockedBySiteKey || isUnlockedByFeatureKey; +}; + export { defaultQueryClientRetryHandler, getComputedStylePropertyCSSVariable, @@ -117,4 +142,5 @@ export { serverValidationError, isExpired, sendEnterpriseCheckoutTrackingEvent, + isFeatureEnabled, }; diff --git a/src/utils/test/common.test.ts b/src/utils/test/common.test.ts index 37f564a2..bb244780 100644 --- a/src/utils/test/common.test.ts +++ b/src/utils/test/common.test.ts @@ -1,7 +1,19 @@ +import { getConfig } from '@edx/frontend-platform/config'; import dayjs from 'dayjs'; import { CheckoutErrorMessagesByField } from '@/constants/checkout'; -import { defaultQueryClientRetryHandler, isExpired, serverValidationError } from '@/utils/common'; +import { + defaultQueryClientRetryHandler, + isExpired, + isFeatureEnabled, + serverValidationError, +} from '@/utils/common'; + +jest.mock('@edx/frontend-platform/config', () => ({ + getConfig: jest.fn(() => ({ + PUBLIC_PATH: '/', + })), +})); describe('defaultQueryClientRetryHandler', () => { it.each([ @@ -107,3 +119,54 @@ describe('isExpired', () => { expect(isExpired(equalToNow)).toBe(false); }); }); + +describe('isFeatureEnabled', () => { + const SSP_SESSION_KEY = 'edx.checkout.self-service-purchasing'; + + beforeEach(() => { + sessionStorage.clear(); + jest.clearAllMocks(); + }); + + it('returns true when enabled is true', () => { + (getConfig as jest.Mock).mockReturnValue({}); + expect(isFeatureEnabled(true)).toBe(true); + }); + + it('returns false when enabled is false and no keys are in session', () => { + (getConfig as jest.Mock).mockReturnValue({ + FEATURE_SELF_SERVICE_SITE_KEY: 'site-key', + }); + expect(isFeatureEnabled(false, 'feature-key')).toBe(false); + }); + + it('returns true when enabled is false but featureKey matches session storage', () => { + (getConfig as jest.Mock).mockReturnValue({}); + sessionStorage.setItem(SSP_SESSION_KEY, 'feature-key'); + expect(isFeatureEnabled(false, 'feature-key')).toBe(true); + }); + + it('returns true when enabled is false but siteKey matches session storage', () => { + (getConfig as jest.Mock).mockReturnValue({ + FEATURE_SELF_SERVICE_SITE_KEY: 'site-key', + }); + sessionStorage.setItem(SSP_SESSION_KEY, 'site-key'); + expect(isFeatureEnabled(false, 'feature-key')).toBe(true); + }); + + it('returns false when session storage key does not match either featureKey or siteKey', () => { + (getConfig as jest.Mock).mockReturnValue({ + FEATURE_SELF_SERVICE_SITE_KEY: 'site-key', + }); + sessionStorage.setItem(SSP_SESSION_KEY, 'wrong-key'); + expect(isFeatureEnabled(false, 'feature-key')).toBe(false); + }); + + it('returns true if enabled is false but siteKey is matched, even if featureKey is null', () => { + (getConfig as jest.Mock).mockReturnValue({ + FEATURE_SELF_SERVICE_SITE_KEY: 'site-key', + }); + sessionStorage.setItem(SSP_SESSION_KEY, 'site-key'); + expect(isFeatureEnabled(false, null)).toBe(true); + }); +});