Skip to content

Commit

Permalink
Add jest tests
Browse files Browse the repository at this point in the history
  • Loading branch information
markmur committed Jan 30, 2024
1 parent f465808 commit b36ea87
Show file tree
Hide file tree
Showing 6 changed files with 114 additions and 32 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ class RCTShopifyCheckoutSheetKit: RCTEventEmitter, CheckoutDelegate {
func checkoutDidEmitWebPixelEvent(event: ShopifyCheckoutSheetKit.PixelEvent) {
if hasListeners {
var genericEvent: [String: Any]
switch(event) {
switch event {
case .standardEvent(let standardEvent):
genericEvent = mapToGenericEvent(standardEvent: standardEvent)
case .customEvent(let customEvent):
Expand Down Expand Up @@ -166,13 +166,12 @@ class RCTShopifyCheckoutSheetKit: RCTEventEmitter, CheckoutDelegate {
resolve(config)
}

/// MARK - Private
// MARK: - Private

private func stringToJSON(from value: String?) -> [String: Any]? {
guard let data = value?.data(using: .utf8, allowLossyConversion: false) else { return [:] }
do {
let jsonObject = try? JSONSerialization.jsonObject(with: data, options: .mutableContainers) as? [String: Any]
return jsonObject
return try? JSONSerialization.jsonObject(with: data, options: .mutableContainers) as? [String: Any]
} catch {
print("Failed to convert string to JSON: \(error)", value)
return [:]
Expand Down Expand Up @@ -208,15 +207,13 @@ class RCTShopifyCheckoutSheetKit: RCTEventEmitter, CheckoutDelegate {
}

private func decodeAndMap(event: CustomEvent, decoder: JSONDecoder = JSONDecoder()) throws -> [String: Any] {
let dictionary: [String: Any] = [
return [
"context": encodeToJSON(from: event.context),
"customData": stringToJSON(from: event.customData),
"id": event.id,
"name": event.name,
"timestamp": event.timestamp
]

return dictionary
] as [String: Any]
}
}

Expand Down
4 changes: 2 additions & 2 deletions modules/@shopify/checkout-sheet-kit/src/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ export interface Context {

export type CheckoutEvent = 'close' | 'completed' | 'error' | 'pixel';

export type PixelEventCallback = (event: string | PixelEvent) => void;
export type PixelEventCallback = (event: PixelEvent) => void;

export type CheckoutExceptionCallback = (error: CheckoutException) => void;

Expand All @@ -143,7 +143,7 @@ function addEventListener(

function addEventListener(
event: 'pixel',
callback: (event: PixelEvent) => void,
callback: (event?: PixelEvent) => void,
): Maybe<EmitterSubscription>;

function removeEventListeners(event: CheckoutEvent): void;
Expand Down
40 changes: 35 additions & 5 deletions modules/@shopify/checkout-sheet-kit/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ import {
type CheckoutEventCallback,
type Configuration,
type ShopifyCheckoutSheetKit,
PixelEventCallback,
} from './index.d';
import {type PixelEvent} from './pixels';

Expand Down Expand Up @@ -80,18 +79,35 @@ class ShopifyCheckoutSheet implements ShopifyCheckoutSheetKit {
callback: CheckoutEventCallback,
): EmitterSubscription | undefined {
if (event === 'pixel' && typeof callback === 'function') {
const eventHandler = callback as PixelEventCallback;
const eventHandler = callback as (data?: PixelEvent) => void;

/**
* Event data can be sent back as either a parsed Pixel Event object or a JSON string.
*/
const cb = (eventData: string | PixelEvent) => {
try {
if (typeof eventData === 'string') {
const parsed = JSON.parse(eventData);
eventHandler(parsed as PixelEvent);
try {
const parsed = JSON.parse(eventData);
eventHandler(parsed as PixelEvent);
} catch (error) {
throw new WebPixelsParseError(
'Failed to parse Web Pixel event data.',
{
cause: 'Invalid JSON',
},
);
}
} else if (eventData && typeof eventData === 'object') {
eventHandler(eventData);
}
} catch (error) {
eventHandler(eventData);
throw new WebPixelsParseError(
'Failed to parse Web Pixel event data.',
{
cause: 'Unknown',
},
);
}
};
return ShopifyCheckoutSheet.eventEmitter.addListener(event, cb);
Expand All @@ -105,6 +121,20 @@ class ShopifyCheckoutSheet implements ShopifyCheckoutSheetKit {
}
}

export class WebPixelsParseError extends Error {
constructor(
message?: string | undefined,
options?: ErrorOptions | undefined,
) {
super(message, options);
this.name = 'WebPixelsParseError';

if (Error.captureStackTrace) {
Error.captureStackTrace(this, WebPixelsParseError);
}
}
}

// API
export {
ShopifyCheckoutSheet,
Expand Down
77 changes: 66 additions & 11 deletions modules/@shopify/checkout-sheet-kit/tests/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/* eslint-disable no-new */

import {ShopifyCheckoutSheet} from '../src';
import {ShopifyCheckoutSheet, WebPixelsParseError} from '../src';
import {ColorScheme, type Configuration} from '../src';
import {NativeModules} from 'react-native';

Expand All @@ -10,9 +10,23 @@ const config: Configuration = {
};

jest.mock('react-native', () => {
let listeners: (typeof jest.fn)[] = [];

const NativeEventEmitter = jest.fn(() => ({
addListener: jest.fn(),
removeAllListeners: jest.fn(),
addListener: jest.fn((_, callback) => {
listeners.push(callback);
}),
removeAllListeners: jest.fn(() => {
listeners = [];
}),
emit: jest.fn((_, data: any) => {
for (const listener of listeners) {
listener(data);
}

// clear listeners
listeners = [];
}),
}));

const exampleConfig = {
Expand All @@ -31,6 +45,7 @@ jest.mock('react-native', () => {
};

return {
_listeners: listeners,
NativeEventEmitter,
NativeModules: {
ShopifyCheckoutSheetKit,
Expand All @@ -39,8 +54,16 @@ jest.mock('react-native', () => {
});

describe('ShopifyCheckoutSheetKit', () => {
// @ts-expect-error "eventEmitter is private"
const eventEmitter = ShopifyCheckoutSheet.eventEmitter;

afterEach(() => {
NativeModules.ShopifyCheckoutSheetKit.setConfig.mockReset();
NativeModules.ShopifyCheckoutSheetKit.eventEmitter.addListener.mockClear();
NativeModules.ShopifyCheckoutSheetKit.eventEmitter.removeAllListeners.mockClear();

// Clear mock listeners
NativeModules._listeners = [];
});

describe('instantiation', () => {
Expand Down Expand Up @@ -116,10 +139,45 @@ describe('ShopifyCheckoutSheetKit', () => {
const eventName = 'close';
const callback = jest.fn();
instance.addEventListener(eventName, callback);
expect(
// @ts-expect-error
ShopifyCheckoutSheet.eventEmitter.addListener,
).toHaveBeenCalledWith(eventName, callback);
expect(eventEmitter.addListener).toHaveBeenCalledWith(
eventName,
callback,
);
});

it('parses web pixel event JSON string data', () => {
const instance = new ShopifyCheckoutSheet();
const eventName = 'pixel';
const callback = jest.fn();
instance.addEventListener(eventName, callback);
NativeModules.ShopifyCheckoutSheetKit.addEventListener(
eventName,
callback,
);
expect(eventEmitter.addListener).toHaveBeenCalledWith(
'pixel',
expect.any(Function),
);
eventEmitter.emit('pixel', JSON.stringify({someAttribute: 123}));
expect(callback).toHaveBeenCalledWith({someAttribute: 123});
});

it('throws if the web pixel event contains invalid JSON data', () => {
const instance = new ShopifyCheckoutSheet();
const eventName = 'pixel';
const callback = jest.fn();
instance.addEventListener(eventName, callback);
NativeModules.ShopifyCheckoutSheetKit.addEventListener(
eventName,
callback,
);
expect(eventEmitter.addListener).toHaveBeenCalledWith(
'pixel',
expect.any(Function),
);
expect(() => eventEmitter.emit('pixel', '{"someAttribute": 123')).toThrow(
WebPixelsParseError,
);
});
});

Expand All @@ -129,10 +187,7 @@ describe('ShopifyCheckoutSheetKit', () => {
instance.addEventListener('close', () => {});
instance.addEventListener('close', () => {});
instance.removeEventListeners('close');
expect(
// @ts-expect-error
ShopifyCheckoutSheet.eventEmitter.removeAllListeners,
).toHaveBeenCalledWith('close');
expect(eventEmitter.removeAllListeners).toHaveBeenCalledWith('close');
});
});
});
10 changes: 5 additions & 5 deletions sample/ios/ReactNativeTests/ShopifyCheckoutSheetKitTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -170,10 +170,10 @@ class ShopifyCheckoutSheetKitTests: XCTestCase {
XCTAssertEqual(eventBody["id"] as? String, "test")
XCTAssertEqual(eventBody["name"] as? String, "test")
XCTAssertEqual(eventBody["timestamp"] as? String, "test")
XCTAssertEqual(eventBody["context"] as! [String : [String : String?]], [
XCTAssertEqual(eventBody["context"] as! [String: [String: String?]], [
"document": [
"characterSet": "utf8",
"referrer": "test",
"referrer": "test"
]
])
XCTAssertNil(eventBody["data"])
Expand All @@ -198,13 +198,13 @@ class ShopifyCheckoutSheetKitTests: XCTestCase {
XCTAssertEqual(eventBody["id"] as? String, "test")
XCTAssertEqual(eventBody["name"] as? String, "test")
XCTAssertEqual(eventBody["timestamp"] as? String, "test")
XCTAssertEqual(eventBody["context"] as! [String : [String : String?]], [
XCTAssertEqual(eventBody["context"] as! [String: [String: String?]], [
"document": [
"characterSet": "utf8",
"referrer": "test",
"referrer": "test"
]
])
XCTAssertEqual(eventBody["customData"] as! [String : [String : String]], [
XCTAssertEqual(eventBody["customData"] as! [String: [String: String]], [
"nestedData": [
"someAttribute": "456"
]
Expand Down
2 changes: 1 addition & 1 deletion sample/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ function AppWithContext({children}: PropsWithChildren) {
useEffect(() => {
const subscription = shopify.addEventListener('pixel', event => {
// eslint-disable-next-line no-console
console.log('[PixelEvent]', event.name, event);
console.log('[PixelEvent]', event?.name, event);
});

return () => subscription?.remove();
Expand Down

0 comments on commit b36ea87

Please sign in to comment.