react-native-nami-sdk
Version:
React Native SDK for Nami - No-code paywall and onboarding flows with A/B testing.
98 lines (86 loc) • 3.89 kB
text/typescript
// Mock react-native BEFORE importing the SDK — `../Nami` resolves
// the native module via TurboModuleRegistry at import time.
const reset = jest.fn().mockResolvedValue(undefined);
// The wrapper reads `result?.success` (see Nami.ts), so the mock must
// return that shape — not a plain boolean.
const configure = jest.fn().mockResolvedValue({ success: true });
const sdkConfigured = jest.fn().mockResolvedValue(false);
const sdkVersion = jest.fn().mockResolvedValue('test');
jest.mock('react-native', () => ({
TurboModuleRegistry: {
getEnforcing: jest.fn(() => ({
reset,
configure,
sdkConfigured,
sdkVersion,
})),
},
NativeModules: {
RNNami: { reset, configure, sdkConfigured, sdkVersion },
},
}));
import { Nami } from '../Nami';
describe('Nami.reset (React Native bridge)', () => {
beforeEach(() => {
reset.mockClear();
configure.mockClear();
});
it('exposes reset() as an async function on the JS surface', () => {
expect(typeof Nami.reset).toBe('function');
});
it('delegates to the native RNNami.reset() turbomodule method', async () => {
await Nami.reset();
expect(reset).toHaveBeenCalledTimes(1);
});
it('returns a promise that resolves once the native call settles', async () => {
const result = Nami.reset();
expect(result).toBeInstanceOf(Promise);
await expect(result).resolves.toBeUndefined();
});
it('propagates native bridge rejections to the JS caller', async () => {
const err = new Error('native reset failed');
reset.mockRejectedValueOnce(err);
await expect(Nami.reset()).rejects.toThrow('native reset failed');
});
// NAM-1086: pre-fix, calling Nami.reset() before Nami.configure() on
// Android crashed inside `NamiEntitlementManager`'s class-init block,
// which read `Nami.refs.context` (a `lateinit`). The native bridge's
// `try { Nami.reset(...) } catch (e: Throwable) { promise.reject(...) }`
// turned that into a JS-observable `ExceptionInInitializerError`
// rejection of the `Nami.reset()` promise. These tests pin the JS-side
// contract: regardless of order or repetition, the bridge invocation
// must resolve cleanly and the host app must not see a rejection.
describe('order-of-calls (NAM-1086)', () => {
it('does not reject when called before configure', async () => {
// No prior `Nami.configure()` — simulates the cold-start path the
// RN host app exercises when wiping persisted state on launch.
await expect(Nami.reset()).resolves.toBeUndefined();
expect(reset).toHaveBeenCalledTimes(1);
expect(configure).not.toHaveBeenCalled();
});
it('does not reject when called after configure', async () => {
await Nami.configure({} as any);
await expect(Nami.reset()).resolves.toBeUndefined();
expect(configure).toHaveBeenCalledTimes(1);
expect(reset).toHaveBeenCalledTimes(1);
});
it('is idempotent across repeated cold calls', async () => {
// The host app may bind `Nami.reset()` to a "Clear Data" button
// that the user can tap multiple times in a row — each tap must
// resolve cleanly.
await expect(Nami.reset()).resolves.toBeUndefined();
await expect(Nami.reset()).resolves.toBeUndefined();
await expect(Nami.reset()).resolves.toBeUndefined();
expect(reset).toHaveBeenCalledTimes(3);
});
it('does not reject when interleaved with configure', async () => {
// Realistic flow: reset cold → configure → reset again to wipe
// post-configure state. Each step must resolve.
await expect(Nami.reset()).resolves.toBeUndefined();
await expect(Nami.configure({} as any)).resolves.toBe(true);
await expect(Nami.reset()).resolves.toBeUndefined();
expect(reset).toHaveBeenCalledTimes(2);
expect(configure).toHaveBeenCalledTimes(1);
});
});
});