UNPKG

react-native-nami-sdk

Version:

React Native SDK for Nami - No-code paywall and onboarding flows with A/B testing.

203 lines (174 loc) 6.91 kB
// Mock react-native BEFORE importing the SDK — `../NamiFlowManager` resolves // the native module via TurboModuleRegistry at import time, and the // NativeEventEmitter constructor is called at module top-level. const registerStepHandoff = jest.fn(); const unregisterStepHandoff = jest.fn(); const registerEventHandler = jest.fn(); const unregisterEventHandler = jest.fn(); const completeHandoff = jest.fn(); const finishMock = jest.fn(); const isFlowOpenMock = jest.fn().mockResolvedValue(false); const resumeMock = jest.fn(); const pauseMock = jest.fn(); const subRemove = jest.fn(); // Capture the registered listener per event name so tests can fire native events. const listeners: Record<string, (event: unknown) => void> = {}; const addListener = jest.fn( (eventName: string, handler: (e: unknown) => void) => { listeners[eventName] = handler; return { remove: subRemove }; }, ); jest.mock('react-native', () => ({ TurboModuleRegistry: { getEnforcing: jest.fn(() => ({ registerStepHandoff, unregisterStepHandoff, registerEventHandler, unregisterEventHandler, completeHandoff, finish: finishMock, isFlowOpen: isFlowOpenMock, resume: resumeMock, pause: pauseMock, })), }, NativeModules: { RNNamiFlowManager: { registerStepHandoff, unregisterStepHandoff, registerEventHandler, unregisterEventHandler, completeHandoff, }, }, NativeEventEmitter: jest.fn().mockImplementation(() => ({ addListener, })), })); import { NamiFlowManager } from '../NamiFlowManager'; import type { NamiHandoffOutcome } from '../types'; /** Fire a native `Handoff` event into the captured emitter listener. */ function fireHandoff(event: { handoffId: string; handoffTag: string; handoffData?: Record<string, unknown>; }) { listeners.Handoff?.(event); } describe('NamiFlowManager (React Native bridge)', () => { beforeEach(() => { registerStepHandoff.mockClear(); unregisterStepHandoff.mockClear(); registerEventHandler.mockClear(); unregisterEventHandler.mockClear(); completeHandoff.mockClear(); resumeMock.mockClear(); subRemove.mockClear(); addListener.mockClear(); }); describe('registerStepHandoff', () => { it('returns a function and triggers native register', () => { const unsubscribe = NamiFlowManager.registerStepHandoff(() => {}); expect(typeof unsubscribe).toBe('function'); expect(registerStepHandoff).toHaveBeenCalledTimes(1); expect(addListener).toHaveBeenCalledTimes(1); }); it('returned unsubscribe removes the JS listener AND calls native unregister', () => { const unsubscribe = NamiFlowManager.registerStepHandoff(() => {}); unsubscribe(); expect(subRemove).toHaveBeenCalledTimes(1); expect(unregisterStepHandoff).toHaveBeenCalledTimes(1); }); }); describe('handoff outcome token bridge', () => { it('delivers (tag, data, complete) and correlates complete() back to the handoffId', () => { const handler = jest.fn( (_tag: string, _data: unknown, complete: () => void) => complete(), ); NamiFlowManager.registerStepHandoff(handler); fireHandoff({ handoffId: 'h1', handoffTag: 'signin', handoffData: { a: 1 }, }); expect(handler).toHaveBeenCalledWith( 'signin', { a: 1 }, expect.any(Function), ); // Bare complete() is normalized to {kind:'done'} before crossing the bridge. expect(completeHandoff).toHaveBeenCalledTimes(1); expect(completeHandoff).toHaveBeenCalledWith('h1', { kind: 'done' }); }); it('forwards an explicit outcome verbatim (incl. primitive login attributes)', () => { const outcome: NamiHandoffOutcome = { kind: 'login', result: 'success', details: { loginId: 'user-123', attributes: { subscriberStatus: 'active', seats: 3, trial: false }, }, }; NamiFlowManager.registerStepHandoff((_t, _d, complete) => complete(outcome), ); fireHandoff({ handoffId: 'h2', handoffTag: 'signin' }); expect(completeHandoff).toHaveBeenCalledWith('h2', outcome); }); it('is once-only per handoff — a second complete() warns and does not re-cross the bridge', () => { const warn = jest.spyOn(console, 'warn').mockImplementation(() => {}); NamiFlowManager.registerStepHandoff((_t, _d, complete) => { complete({ kind: 'purchase', result: 'cancelled' }); complete(); // duplicate — must be ignored }); fireHandoff({ handoffId: 'h3', handoffTag: 'buysku' }); expect(completeHandoff).toHaveBeenCalledTimes(1); expect(completeHandoff).toHaveBeenCalledWith('h3', { kind: 'purchase', result: 'cancelled', }); expect(warn).toHaveBeenCalledWith( expect.stringContaining('called more than once'), ); warn.mockRestore(); }); it('correlates concurrent handoffs to their own ids independently', () => { const completes: Record<string, () => void> = {}; NamiFlowManager.registerStepHandoff((tag, _d, complete) => { completes[tag] = complete; }); fireHandoff({ handoffId: 'hA', handoffTag: 'push' }); fireHandoff({ handoffId: 'hB', handoffTag: 'location' }); completes.location(); completes.push(); expect(completeHandoff).toHaveBeenCalledWith('hB', { kind: 'done' }); expect(completeHandoff).toHaveBeenCalledWith('hA', { kind: 'done' }); }); it('legacy (tag, data) handler that calls resume() still works (degrades natively, no completeHandoff)', () => { // Old-shape handler ignores the third arg and calls resume() directly. const legacy = (_tag: string, _data?: Record<string, unknown>) => { NamiFlowManager.resume(); }; // Cast at the call site only — the public signature is the 3-arg shape. NamiFlowManager.registerStepHandoff(legacy as never); fireHandoff({ handoffId: 'h4', handoffTag: 'complete' }); expect(resumeMock).toHaveBeenCalledTimes(1); expect(completeHandoff).not.toHaveBeenCalled(); }); }); describe('registerEventHandler', () => { it('returns a function and triggers native register', () => { const unsubscribe = NamiFlowManager.registerEventHandler(() => {}); expect(typeof unsubscribe).toBe('function'); expect(registerEventHandler).toHaveBeenCalledTimes(1); expect(addListener).toHaveBeenCalledTimes(1); }); it('returned unsubscribe removes the JS listener AND calls native unregister', () => { const unsubscribe = NamiFlowManager.registerEventHandler(() => {}); unsubscribe(); expect(subRemove).toHaveBeenCalledTimes(1); expect(unregisterEventHandler).toHaveBeenCalledTimes(1); }); }); });