test: cover the public pay SDK contract#38
Conversation
There was a problem hiding this comment.
Code Review
This pull request replaces a placeholder test with a comprehensive test suite for the React Native SDK contract, verifying result codes, payment/setup flow forwarding, and linking error handling. The feedback suggests parameterizing the test suite to cover both the Legacy Bridge and TurboModule (New Architecture) execution paths, rather than only testing the legacy path.
| function loadSdk(nativeModule?: NativeModule) { | ||
| jest.resetModules(); | ||
| // @ts-expect-error test-only override | ||
| global.__turboModuleProxy = null; | ||
|
|
||
| jest.doMock('react-native', () => ({ | ||
| NativeModules: nativeModule ? { DojoReactNativePaySdk: nativeModule } : {}, | ||
| Platform: { | ||
| select: (options: Record<string, string>) => | ||
| options.ios ?? options.default ?? '', | ||
| }, | ||
| TurboModuleRegistry: { | ||
| getEnforcing: jest.fn(() => nativeModule), | ||
| }, | ||
| })); | ||
|
|
||
| return require('../index') as typeof import('../index'); | ||
| } |
There was a problem hiding this comment.
The loadSdk helper currently hardcodes global.__turboModuleProxy = null, which means the TurboModule (New Architecture) path is never tested. We can add an optional isTurboModuleEnabled parameter to allow testing both legacy and TurboModule paths.
| function loadSdk(nativeModule?: NativeModule) { | |
| jest.resetModules(); | |
| // @ts-expect-error test-only override | |
| global.__turboModuleProxy = null; | |
| jest.doMock('react-native', () => ({ | |
| NativeModules: nativeModule ? { DojoReactNativePaySdk: nativeModule } : {}, | |
| Platform: { | |
| select: (options: Record<string, string>) => | |
| options.ios ?? options.default ?? '', | |
| }, | |
| TurboModuleRegistry: { | |
| getEnforcing: jest.fn(() => nativeModule), | |
| }, | |
| })); | |
| return require('../index') as typeof import('../index'); | |
| } | |
| function loadSdk(nativeModule?: NativeModule, isTurboModuleEnabled = false) { | |
| jest.resetModules(); | |
| // @ts-expect-error test-only override | |
| global.__turboModuleProxy = isTurboModuleEnabled ? {} : null; | |
| jest.doMock('react-native', () => ({ | |
| NativeModules: nativeModule ? { DojoReactNativePaySdk: nativeModule } : {}, | |
| Platform: { | |
| select: (options: Record<string, string>) => | |
| options.ios ?? options.default ?? '', | |
| }, | |
| TurboModuleRegistry: { | |
| getEnforcing: jest.fn(() => nativeModule), | |
| }, | |
| })); | |
| return require('../index') as typeof import('../index'); | |
| } |
| describe('public SDK contract', () => { | ||
| afterEach(() => { | ||
| jest.resetModules(); | ||
| jest.clearAllMocks(); | ||
| jest.dontMock('react-native'); | ||
| // @ts-expect-error test-only cleanup | ||
| delete global.__turboModuleProxy; | ||
| }); | ||
|
|
||
| it('exposes the full ResultCode enum without missing values', () => { | ||
| const { ResultCode } = loadSdk({ | ||
| startPaymentFlow: jest.fn(), | ||
| startSetupFlow: jest.fn(), | ||
| }); | ||
|
|
||
| const actualEntries = Object.entries(ResultCode).filter( | ||
| (entry): entry is [string, number] => typeof entry[1] === 'number' | ||
| ); | ||
|
|
||
| expect(Object.fromEntries(actualEntries)).toEqual(EXPECTED_RESULT_CODES); | ||
| expect(actualEntries).toHaveLength(14); | ||
| }); | ||
|
|
||
| it('forwards payment details to startPaymentFlow and resolves with the native result code', async () => { | ||
| const nativeModule = { | ||
| startPaymentFlow: jest.fn().mockResolvedValue(3), | ||
| startSetupFlow: jest.fn(), | ||
| }; | ||
| const { startPaymentFlow, ResultCode } = loadSdk(nativeModule); | ||
|
|
||
| await expect(startPaymentFlow(PAYMENT_DETAILS)).resolves.toBe( | ||
| ResultCode.authorizing | ||
| ); | ||
| expect(nativeModule.startPaymentFlow).toHaveBeenCalledWith(PAYMENT_DETAILS); | ||
| }); | ||
|
|
||
| it('forwards payment details to startSetupFlow and resolves with the native result code', async () => { | ||
| const nativeModule = { | ||
| startPaymentFlow: jest.fn(), | ||
| startSetupFlow: jest.fn().mockResolvedValue(0), | ||
| }; | ||
| const { startSetupFlow, ResultCode } = loadSdk(nativeModule); | ||
|
|
||
| await expect(startSetupFlow(PAYMENT_DETAILS)).resolves.toBe( | ||
| ResultCode.successful | ||
| ); | ||
| expect(nativeModule.startSetupFlow).toHaveBeenCalledWith(PAYMENT_DETAILS); | ||
| }); | ||
|
|
||
| it('throws the linking error when startPaymentFlow is unavailable', () => { | ||
| const { startPaymentFlow } = loadSdk(); | ||
|
|
||
| expect(() => startPaymentFlow(PAYMENT_DETAILS)).toThrow( | ||
| "@dojo-engineering/react-native-pay-sdk' doesn't seem to be linked" | ||
| ); | ||
| expect(() => startPaymentFlow(PAYMENT_DETAILS)).toThrow( | ||
| "You have run 'pod install'" | ||
| ); | ||
| }); | ||
|
|
||
| it('throws the linking error when startSetupFlow is unavailable', () => { | ||
| const { startSetupFlow } = loadSdk(); | ||
|
|
||
| expect(() => startSetupFlow(PAYMENT_DETAILS)).toThrow( | ||
| "@dojo-engineering/react-native-pay-sdk' doesn't seem to be linked" | ||
| ); | ||
| expect(() => startSetupFlow(PAYMENT_DETAILS)).toThrow( | ||
| 'You rebuilt the app after installing the package' | ||
| ); | ||
| }); | ||
| }); |
There was a problem hiding this comment.
To ensure full coverage of both the Legacy Bridge and TurboModule architectures, we can parameterize the entire test suite using describe.each and pass the isTurboModuleEnabled flag to loadSdk.
describe.each([
{ name: 'Legacy Bridge', isTurboModuleEnabled: false },
{ name: 'TurboModule', isTurboModuleEnabled: true },
])('public SDK contract ($name)', ({ isTurboModuleEnabled }) => {
afterEach(() => {
jest.resetModules();
jest.clearAllMocks();
jest.dontMock('react-native');
// @ts-expect-error test-only cleanup
delete global.__turboModuleProxy;
});
it('exposes the full ResultCode enum without missing values', () => {
const { ResultCode } = loadSdk({
startPaymentFlow: jest.fn(),
startSetupFlow: jest.fn(),
}, isTurboModuleEnabled);
const actualEntries = Object.entries(ResultCode).filter(
(entry): entry is [string, number] => typeof entry[1] === 'number'
);
expect(Object.fromEntries(actualEntries)).toEqual(EXPECTED_RESULT_CODES);
expect(actualEntries).toHaveLength(14);
});
it('forwards payment details to startPaymentFlow and resolves with the native result code', async () => {
const nativeModule = {
startPaymentFlow: jest.fn().mockResolvedValue(3),
startSetupFlow: jest.fn(),
};
const { startPaymentFlow, ResultCode } = loadSdk(nativeModule, isTurboModuleEnabled);
await expect(startPaymentFlow(PAYMENT_DETAILS)).resolves.toBe(
ResultCode.authorizing
);
expect(nativeModule.startPaymentFlow).toHaveBeenCalledWith(PAYMENT_DETAILS);
});
it('forwards payment details to startSetupFlow and resolves with the native result code', async () => {
const nativeModule = {
startPaymentFlow: jest.fn(),
startSetupFlow: jest.fn().mockResolvedValue(0),
};
const { startSetupFlow, ResultCode } = loadSdk(nativeModule, isTurboModuleEnabled);
await expect(startSetupFlow(PAYMENT_DETAILS)).resolves.toBe(
ResultCode.successful
);
expect(nativeModule.startSetupFlow).toHaveBeenCalledWith(PAYMENT_DETAILS);
});
it('throws the linking error when startPaymentFlow is unavailable', () => {
const { startPaymentFlow } = loadSdk(undefined, isTurboModuleEnabled);
expect(() => startPaymentFlow(PAYMENT_DETAILS)).toThrow(
"@dojo-engineering/react-native-pay-sdk' doesn't seem to be linked"
);
expect(() => startPaymentFlow(PAYMENT_DETAILS)).toThrow(
"You have run 'pod install'"
);
});
it('throws the linking error when startSetupFlow is unavailable', () => {
const { startSetupFlow } = loadSdk(undefined, isTurboModuleEnabled);
expect(() => startSetupFlow(PAYMENT_DETAILS)).toThrow(
"@dojo-engineering/react-native-pay-sdk' doesn't seem to be linked"
);
expect(() => startSetupFlow(PAYMENT_DETAILS)).toThrow(
'You rebuilt the app after installing the package'
);
});
})
|
Addressed the Gemini suggestions in 18cdbce by covering both Legacy Bridge and TurboModule paths in the public SDK contract tests. Validated locally with |
Summary
ResultCodeenum value and confirm the native bridge returns those values through both flowsPaymentDetailsand raise the documented linking error when the native module is unavailableCloses #35
Testing
npm test -- --runInBandnpm run lint -- src/__tests__/index.test.tsxnpx tsc --noEmit -p tsconfig.build.json