Skip to content

Commit 320dd80

Browse files
authored
Move common mocks to __mocks__ directory (#239)
1 parent 527eee7 commit 320dd80

File tree

5 files changed

+125
-131
lines changed

5 files changed

+125
-131
lines changed

__mocks__/react-native.ts

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
type Listener = {eventName: string; callback: (data: any) => void};
2+
3+
function createMockEmitter() {
4+
let listeners: Listener[] = [];
5+
6+
const addListener = jest.fn(
7+
(eventName: string, callback: (data: any) => void) => {
8+
listeners.push({eventName, callback});
9+
return {remove: jest.fn()};
10+
},
11+
);
12+
13+
const removeAllListeners = jest.fn((eventName?: string) => {
14+
if (eventName) {
15+
listeners = listeners.filter(l => l.eventName !== eventName);
16+
} else {
17+
listeners = [];
18+
}
19+
});
20+
21+
const emit = jest.fn((eventName: string, data: any) => {
22+
const callbacks = listeners
23+
.filter(l => l.eventName === eventName)
24+
.map(l => l.callback);
25+
callbacks.forEach(cb => cb(data));
26+
// Clear listeners after emit to avoid cross-test leakage
27+
listeners = [];
28+
});
29+
30+
return {addListener, removeAllListeners, emit};
31+
}
32+
33+
const requireNativeComponent = (..._args: any[]) => {
34+
const React = require('react');
35+
return (props: any) => React.createElement('View', props);
36+
};
37+
38+
const exampleConfig = {preloading: true};
39+
40+
const ShopifyCheckoutSheetKit = {
41+
version: '0.7.0',
42+
preload: jest.fn(),
43+
present: jest.fn(),
44+
dismiss: jest.fn(),
45+
invalidateCache: jest.fn(),
46+
getConfig: jest.fn(async () => exampleConfig),
47+
setConfig: jest.fn(),
48+
addEventListener: jest.fn(),
49+
removeEventListeners: jest.fn(),
50+
initiateGeolocationRequest: jest.fn(),
51+
configureAcceleratedCheckouts: jest.fn(),
52+
isAcceleratedCheckoutAvailable: jest.fn(),
53+
};
54+
55+
// CommonJS export for Jest manual mock resolution
56+
module.exports = {
57+
Platform: {OS: 'ios'},
58+
PermissionsAndroid: {
59+
requestMultiple: jest.fn(async () => ({})),
60+
},
61+
NativeEventEmitter: jest.fn(() => createMockEmitter()),
62+
requireNativeComponent,
63+
NativeModules: {
64+
ShopifyCheckoutSheetKit: {
65+
...ShopifyCheckoutSheetKit,
66+
eventEmitter: createMockEmitter(),
67+
},
68+
},
69+
};

jest.config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
module.exports = {
22
preset: 'react-native',
33
modulePathIgnorePatterns: ['modules/@shopify/checkout-sheet-kit/lib'],
4+
setupFiles: ['<rootDir>/jest.setup.ts'],
45
transform: {
56
'\\.[jt]sx?$': 'babel-jest',
67
},

jest.setup.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
/**
2+
* Ensure Jest picks up manual mocks written in TypeScript.
3+
* Jest resolves __mocks__/react-native automatically when a test calls jest.mock('react-native')
4+
* or when the module is required and a manual mock exists. No runtime code needed here.
5+
* This file exists to ensure TypeScript is part of Jest's setupFiles and compiled.
6+
*/
7+
8+
export {};

modules/@shopify/checkout-sheet-kit/tests/context.test.tsx

Lines changed: 5 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -12,57 +12,8 @@ const config: Configuration = {
1212
colorScheme: ColorScheme.automatic,
1313
};
1414

15-
jest.mock('react-native', () => {
16-
let listeners: (typeof jest.fn)[] = [];
17-
18-
const NativeEventEmitter = jest.fn(() => ({
19-
addListener: jest.fn((_, callback) => {
20-
listeners.push(callback);
21-
return {remove: jest.fn()};
22-
}),
23-
removeAllListeners: jest.fn(() => {
24-
listeners = [];
25-
}),
26-
emit: jest.fn((_, data: any) => {
27-
for (const listener of listeners) {
28-
listener(data);
29-
}
30-
listeners = [];
31-
}),
32-
}));
33-
34-
const exampleConfig = {
35-
preloading: true,
36-
};
37-
38-
const ShopifyCheckoutSheetKit = {
39-
eventEmitter: NativeEventEmitter(),
40-
version: '0.7.0',
41-
preload: jest.fn(),
42-
present: jest.fn(),
43-
dismiss: jest.fn(),
44-
invalidateCache: jest.fn(),
45-
getConfig: jest.fn(async () => exampleConfig),
46-
setConfig: jest.fn(),
47-
addEventListener: jest.fn(),
48-
removeEventListeners: jest.fn(),
49-
initiateGeolocationRequest: jest.fn(),
50-
};
51-
52-
return {
53-
Platform: {
54-
OS: 'ios',
55-
},
56-
PermissionsAndroid: {
57-
requestMultiple: jest.fn(),
58-
},
59-
_listeners: listeners,
60-
NativeEventEmitter,
61-
NativeModules: {
62-
ShopifyCheckoutSheetKit,
63-
},
64-
};
65-
});
15+
// Use the shared manual mock. Individual tests can override if needed.
16+
jest.mock('react-native');
6617

6718
// Helper component to test the hook
6819
const HookTestComponent = ({
@@ -394,7 +345,9 @@ describe('ShopifyCheckoutSheetContext without provider', () => {
394345
// Test all the noop functions to ensure they don't throw
395346
expect(() => hookValue.addEventListener('close', jest.fn())).not.toThrow();
396347
expect(() => hookValue.removeEventListeners('close')).not.toThrow();
397-
expect(() => hookValue.setConfig({colorScheme: ColorScheme.automatic})).not.toThrow();
348+
expect(() =>
349+
hookValue.setConfig({colorScheme: ColorScheme.automatic}),
350+
).not.toThrow();
398351
expect(() => hookValue.preload('test-url')).not.toThrow();
399352
expect(() => hookValue.present('test-url')).not.toThrow();
400353
expect(() => hookValue.invalidate()).not.toThrow();

modules/@shopify/checkout-sheet-kit/tests/index.test.ts

Lines changed: 42 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -19,58 +19,8 @@ const config: Configuration = {
1919
colorScheme: ColorScheme.automatic,
2020
};
2121

22-
jest.mock('react-native', () => {
23-
let listeners: (typeof jest.fn)[] = [];
24-
25-
const NativeEventEmitter = jest.fn(() => ({
26-
addListener: jest.fn((_, callback) => {
27-
listeners.push(callback);
28-
}),
29-
removeAllListeners: jest.fn(() => {
30-
listeners = [];
31-
}),
32-
emit: jest.fn((_, data: any) => {
33-
for (const listener of listeners) {
34-
listener(data);
35-
}
36-
37-
// clear listeners
38-
listeners = [];
39-
}),
40-
}));
41-
42-
const exampleConfig = {
43-
preloading: true,
44-
};
45-
46-
const ShopifyCheckoutSheetKit = {
47-
eventEmitter: NativeEventEmitter(),
48-
version: '0.7.0',
49-
preload: jest.fn(),
50-
present: jest.fn(),
51-
dismiss: jest.fn(),
52-
invalidateCache: jest.fn(),
53-
getConfig: jest.fn(async () => exampleConfig),
54-
setConfig: jest.fn(),
55-
addEventListener: jest.fn(),
56-
removeEventListeners: jest.fn(),
57-
initiateGeolocationRequest: jest.fn(),
58-
};
59-
60-
return {
61-
Platform: {
62-
OS: 'ios',
63-
},
64-
PermissionsAndroid: {
65-
requestMultiple: jest.fn(),
66-
},
67-
_listeners: listeners,
68-
NativeEventEmitter,
69-
NativeModules: {
70-
ShopifyCheckoutSheetKit,
71-
},
72-
};
73-
});
22+
// Use the shared manual mock. Individual tests can override if needed.
23+
jest.mock('react-native');
7424

7525
global.console = {
7626
...global.console,
@@ -429,27 +379,36 @@ describe('ShopifyCheckoutSheetKit', () => {
429379
{error: clientError, constructor: CheckoutClientError},
430380
{error: networkError, constructor: CheckoutHTTPError},
431381
{error: expiredError, constructor: CheckoutExpiredError},
432-
])(`correctly parses error $error`, ({error, constructor}) => {
433-
const instance = new ShopifyCheckoutSheet();
434-
const eventName = 'error';
435-
const callback = jest.fn();
436-
instance.addEventListener(eventName, callback);
437-
NativeModules.ShopifyCheckoutSheetKit.addEventListener(
438-
eventName,
439-
callback,
440-
);
441-
expect(eventEmitter.addListener).toHaveBeenCalledWith(
442-
'error',
443-
expect.any(Function),
444-
);
445-
eventEmitter.emit('error', error);
446-
const calledWith = callback.mock.calls[0][0];
447-
expect(calledWith).toBeInstanceOf(constructor);
448-
expect(calledWith).not.toHaveProperty('__typename');
449-
expect(calledWith).toHaveProperty('code');
450-
expect(calledWith).toHaveProperty('message');
451-
expect(calledWith).toHaveProperty('recoverable');
452-
});
382+
])(
383+
`correctly parses error $error`,
384+
({
385+
error,
386+
constructor,
387+
}: {
388+
error: any;
389+
constructor: new (...args: any[]) => any;
390+
}) => {
391+
const instance = new ShopifyCheckoutSheet();
392+
const eventName = 'error';
393+
const callback = jest.fn();
394+
instance.addEventListener(eventName, callback);
395+
NativeModules.ShopifyCheckoutSheetKit.addEventListener(
396+
eventName,
397+
callback,
398+
);
399+
expect(eventEmitter.addListener).toHaveBeenCalledWith(
400+
'error',
401+
expect.any(Function),
402+
);
403+
eventEmitter.emit('error', error);
404+
const calledWith = callback.mock.calls[0][0];
405+
expect(calledWith).toBeInstanceOf(constructor);
406+
expect(calledWith).not.toHaveProperty('__typename');
407+
expect(calledWith).toHaveProperty('code');
408+
expect(calledWith).toHaveProperty('message');
409+
expect(calledWith).toHaveProperty('recoverable');
410+
},
411+
);
453412

454413
it('returns an unknown generic error if the error cannot be parsed', () => {
455414
const instance = new ShopifyCheckoutSheet();
@@ -535,9 +494,11 @@ describe('ShopifyCheckoutSheetKit', () => {
535494
'android.permission.ACCESS_FINE_LOCATION': 'denied',
536495
};
537496

538-
(PermissionsAndroid.requestMultiple as jest.Mock).mockResolvedValue(
539-
mockPermissions,
540-
);
497+
(
498+
PermissionsAndroid.requestMultiple as unknown as {
499+
mockResolvedValue: (v: any) => void;
500+
}
501+
).mockResolvedValue(mockPermissions);
541502

542503
new ShopifyCheckoutSheet();
543504

@@ -558,9 +519,11 @@ describe('ShopifyCheckoutSheetKit', () => {
558519
'android.permission.ACCESS_FINE_LOCATION': 'denied',
559520
};
560521

561-
(PermissionsAndroid.requestMultiple as jest.Mock).mockResolvedValue(
562-
mockPermissions,
563-
);
522+
(
523+
PermissionsAndroid.requestMultiple as unknown as {
524+
mockResolvedValue: (v: any) => void;
525+
}
526+
).mockResolvedValue(mockPermissions);
564527

565528
new ShopifyCheckoutSheet();
566529

0 commit comments

Comments
 (0)