UNPKG

@johnf/react-native-owl

Version:
189 lines (179 loc) 6.01 kB
"use strict"; 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