import { act, renderHook, waitFor } from '@testing-library/react'; import { registerCheckoutOutlet, resetCheckoutPiecesForTests } from '@akinon/pz-theme/src/chrome/checkout-pieces'; import settingsModule from '../../../../settings'; import { useResolvedCheckoutConfig } from '../use-resolved-config'; // Same module the hook imports as `@theme/settings` — jest keys mocks by // resolved path, and the `@theme/*` alias is not resolvable from a test file. // Mocking it keeps the suite off the real storefront settings (and its // runtime-only requires) while letting each case state its own baseline. jest.mock('../../../../settings', () => ({ __esModule: true, default: {} })); const mockSettings = settingsModule as { checkout?: Record }; const themeSettingsResponse = (body: unknown) => { (global.fetch as jest.Mock).mockResolvedValue({ ok: true, json: async () => body }); }; const registerFlowOutlet = (flowVariant?: unknown, id = 'outlet-1') => { registerCheckoutOutlet('flow', { id, node: document.createElement('div'), flowVariant: flowVariant as never }); }; /** * Three layers resolve the checkout layout — settings.js, the backend * theme-settings widget, and the composition's flow outlet — and the order * matters in both directions: a composition must be able to say "this page is * the one-page checkout", and nothing may be able to say "no checkout at all". * These tests pin the precedence, the closed mapping (an unrecognised * preference always falls through to the inherited variant), and the live * latch that keeps a shopper's flow from swapping mid-session. */ describe('useResolvedCheckoutConfig', () => { beforeEach(() => { resetCheckoutPiecesForTests(); delete mockSettings.checkout; delete window.__themeEditorDesigner; global.fetch = jest .fn() .mockResolvedValue({ ok: true, json: async () => ({}) }); }); afterEach(() => { jest.resetAllMocks(); }); it('falls back to the step-by-step flow when nothing is configured', async () => { const { result } = renderHook(() => useResolvedCheckoutConfig()); expect(result.current.variantId).toBe('multi-step'); expect(result.current.options).toEqual({}); await act(async () => undefined); }); it('uses the settings.js baseline when the backend has no override', async () => { mockSettings.checkout = { variant: 'one-page', options: { showTrustBadges: false } }; const { result } = renderHook(() => useResolvedCheckoutConfig()); expect(result.current.variantId).toBe('one-page'); expect(result.current.options).toEqual({ showTrustBadges: false }); await act(async () => undefined); }); it('lets the theme-settings response override settings.js', async () => { mockSettings.checkout = { variant: 'multi-step' }; themeSettingsResponse({ checkout: { variant: 'one-page' } }); const { result } = renderHook(() => useResolvedCheckoutConfig()); await waitFor(() => expect(result.current.variantId).toBe('one-page')); }); it('coerces options from both layers, dropping unknown and mistyped keys', async () => { mockSettings.checkout = { options: { autoAdvance: false, compactMode: true } }; themeSettingsResponse({ checkout: { options: { compactMode: 'yes', gtmEnabled: false, madeUpKey: 'x' } } }); const { result } = renderHook(() => useResolvedCheckoutConfig()); await waitFor(() => expect(result.current.options).toEqual({ autoAdvance: false, compactMode: true, gtmEnabled: false }) ); }); it('lets a composed flow outlet beat settings.js and the backend', async () => { mockSettings.checkout = { variant: 'multi-step' }; themeSettingsResponse({ checkout: { variant: 'multi-step' } }); registerFlowOutlet('one-page'); const { result } = renderHook(() => useResolvedCheckoutConfig()); expect(result.current.variantId).toBe('one-page'); // The later theme-settings arrival must not displace the latched layout. await act(async () => undefined); expect(result.current.variantId).toBe('one-page'); }); it('maps step-by-step to the multi-step variant', async () => { mockSettings.checkout = { variant: 'one-page' }; registerFlowOutlet('step-by-step'); const { result } = renderHook(() => useResolvedCheckoutConfig()); expect(result.current.variantId).toBe('multi-step'); await act(async () => undefined); }); it.each([ 'accordion', 'basket-and-checkout', 'constructor', 'toString', '', 42, null ])( 'falls through to the inherited variant for the preference %p', async (preference) => { mockSettings.checkout = { variant: 'one-page' }; registerFlowOutlet(preference); const { result } = renderHook(() => useResolvedCheckoutConfig()); expect(result.current.variantId).toBe('one-page'); await act(async () => undefined); } ); it('never lets a composition add options of its own', async () => { mockSettings.checkout = { options: { stickyMobileCta: false } }; registerFlowOutlet('one-page'); const { result } = renderHook(() => useResolvedCheckoutConfig()); expect(result.current.options).toEqual({ stickyMobileCta: false }); await act(async () => undefined); }); it('ignores an outlet that registers after the first commit', async () => { mockSettings.checkout = { variant: 'multi-step' }; const { result } = renderHook(() => useResolvedCheckoutConfig()); expect(result.current.variantId).toBe('multi-step'); act(() => registerFlowOutlet('one-page')); expect(result.current.variantId).toBe('multi-step'); await act(async () => undefined); }); it('follows the live claim on the designer canvas', async () => { window.__themeEditorDesigner = true; mockSettings.checkout = { variant: 'multi-step' }; const { result } = renderHook(() => useResolvedCheckoutConfig()); expect(result.current.variantId).toBe('multi-step'); act(() => registerFlowOutlet('one-page')); expect(result.current.variantId).toBe('one-page'); await act(async () => undefined); }); it('resolves exactly like the two-layer storefront when nothing registers', async () => { mockSettings.checkout = { variant: 'multi-step', options: { showOrderReview: false } }; themeSettingsResponse({ checkout: { variant: 'one-page', options: { enableEditMode: false } } }); const { result } = renderHook(() => useResolvedCheckoutConfig()); await waitFor(() => expect(result.current).toEqual({ variantId: 'one-page', options: { showOrderReview: false, enableEditMode: false } }) ); }); });