@johnf/react-native-owl
Version:
Visual regression testing for React Native
189 lines (179 loc) • 6.01 kB
JavaScript
;
import React from 'react';
import { Platform } from 'react-native';
import { Logger } from "../logger.js";
import { CHECK_INTERVAL, MAX_CHECK_TIMEOUT, SOCKET_WAIT_TIMEOUT } from "./constants.js";
import { initWebSocket } from "./websocket.js";
import { add, get } from "./trackedElements.js";
import { handleAction } from "./handleAction.js";
// NOTE: On new arch the module isn't autoloaded
import "../NativeOwl.js";
const logger = new Logger(true);
let isReactUpdating = true;
let owlClient;
export const initClient = () => {
logger.info('[OWL - Client] Initialising OWL client');
patchReact();
waitForWebSocket();
};
/**
* Based on an elements props, store element tracking data and return updated props
*/
export const applyElementTracking = (props, isJsx = false) => {
if (isJsx) {
applyJsxChildrenElementTracking(props);
}
const testID = props?.testID;
const returnProps = {
...props,
showsHorizontalScrollIndicator: false,
showsVerticalScrollIndicator: false
};
if (!testID) {
return returnProps;
}
const existingTrackedElement = get(testID);
const ref = props?.ref || existingTrackedElement?.ref || /*#__PURE__*/React.createRef();
const trackData = {
ref: existingTrackedElement?.ref || ref,
onPress: existingTrackedElement?.onPress || props?.onPress,
onLongPress: existingTrackedElement?.onLongPress || props?.onLongPress,
onChangeText: existingTrackedElement?.onChangeText || props?.onChangeText
};
add(logger, testID, trackData);
return {
...returnProps,
ref
};
};
/**
* To get access to the prop callbacks when the element is created, we need to check the children
*/
export const applyJsxChildrenElementTracking = props => {
if (props.children && Array.isArray(props.children)) {
props.children.forEach(child => {
const testID = child?.props?.testID;
if (!testID) {
return;
}
const existingTrackedElement = get(testID);
const ref = child?.props?.ref || existingTrackedElement?.ref || /*#__PURE__*/React.createRef();
const trackData = {
ref: existingTrackedElement?.ref || ref,
onPress: existingTrackedElement?.onPress || child?.props?.onPress,
onLongPress: existingTrackedElement?.onLongPress || child?.props?.onLongPress,
onChangeText: existingTrackedElement?.onChangeText || child?.props?.onChangeText
};
add(logger, testID, trackData);
});
}
};
/**
* We patch react so that we can maintain a list of elements that have testID's
* We can then use this list to find the element when we receive an action
*/
export const patchReact = () => {
const originalReactCreateElement = React.createElement;
let automateTimeout;
if (parseInt(React.version.split('.')?.[0] || '0', 10) >= 18) {
const jsxRuntime = require('react/jsx-runtime');
const origJsx = jsxRuntime.jsx;
// @ts-ignore
jsxRuntime.jsx = (type, config, maybeKey) => {
const newProps = applyElementTracking(config, true);
clearTimeout(automateTimeout);
automateTimeout = setTimeout(() => {
isReactUpdating = false;
}, CHECK_INTERVAL);
isReactUpdating = true;
return origJsx(type, newProps, maybeKey);
};
}
// @ts-ignore
React.createElement = (type, props, ...children) => {
const newProps = applyElementTracking(props);
clearTimeout(automateTimeout);
automateTimeout = setTimeout(() => {
isReactUpdating = false;
}, CHECK_INTERVAL);
isReactUpdating = true;
return originalReactCreateElement(type, newProps, ...children);
};
};
/**
* The app might launch before the OWL server starts, so we need to keep trying...
*/
export const waitForWebSocket = async () => {
try {
owlClient = await initWebSocket(logger, Platform.OS === 'android' ? 'android' : 'ios', handleMessage);
logger.info('[OWL - Websocket] Connection established');
} catch {
setTimeout(waitForWebSocket, SOCKET_WAIT_TIMEOUT);
}
};
/**
* When we receive a message, we need to find the element that corresponds to the testID,
* then attempt to handle the requested action on it.
*/
export const handleMessage = async message => {
const socketEvent = JSON.parse(message);
const testID = socketEvent.testID;
let element;
try {
element = await getElementByTestId(testID);
} catch (error) {
sendNotFound(testID);
}
if (element) {
try {
if (socketEvent.type === 'ACTION') {
handleAction(logger, testID, element, socketEvent.action, socketEvent.value);
setTimeout(sendDone, 1000);
} else {
sendDone();
}
} catch (error) {
let message = 'Unknown error';
if (error instanceof Error) {
message = error.message;
}
sendError(testID, message);
}
}
};
const sendEvent = event => owlClient.send(JSON.stringify(event));
const sendNotFound = testID => sendEvent({
type: 'NOT_FOUND',
testID
});
const sendDone = () => sendEvent({
type: 'DONE'
});
const sendError = (testID, message) => sendEvent({
type: 'ERROR',
testID,
message
});
/**
* This function resolves the tracked element by its testID, so that we can handle events on it.
* If the element is not immedietly available, we wait for it to be available for some time.
*/
const getElementByTestId = async testID => new Promise((resolve, reject) => {
logger.info(`[OWL - Client] Looking for Element with testID ${testID}`);
const rejectTimeout = setTimeout(() => {
logger.error(`[OWL - Client] ❌ not found`);
clearInterval(checkInterval);
reject(new Error(`Element with testID ${testID} not found`));
}, MAX_CHECK_TIMEOUT);
const checkInterval = setInterval(() => {
const element = get(testID);
if (isReactUpdating || !element) {
return;
}
logger.info(`[OWL - Client] ✓ found`);
clearInterval(checkInterval);
clearTimeout(rejectTimeout);
resolve(element);
}, CHECK_INTERVAL);
});
//# sourceMappingURL=client.js.map