From 7fdf75260c2692875110bde64af8f6f1660e3087 Mon Sep 17 00:00:00 2001 From: Alessandro Senese Date: Wed, 16 Mar 2022 16:27:43 +0000 Subject: [PATCH 001/119] fix import --- example/index.js | 7 +++++-- example/metro.config.js | 18 +++++++++++++++++- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/example/index.js b/example/index.js index a850d031..cbaea119 100644 --- a/example/index.js +++ b/example/index.js @@ -2,8 +2,11 @@ * @format */ -import {AppRegistry} from 'react-native'; +import { AppRegistry } from 'react-native'; import App from './App'; -import {name as appName} from './app.json'; +import { name as appName } from './app.json'; +import { initClient } from 'react-native-owl/dist/client'; + +initClient(); AppRegistry.registerComponent(appName, () => App); diff --git a/example/metro.config.js b/example/metro.config.js index e91aba93..dec17694 100644 --- a/example/metro.config.js +++ b/example/metro.config.js @@ -4,14 +4,30 @@ * * @format */ +const path = require('path'); + +const extraNodeModules = { + 'react-native-owl': path.resolve(__dirname + '/..'), +}; +const watchFolders = [path.resolve(__dirname + '/..')]; module.exports = { transformer: { getTransformOptions: async () => ({ transform: { experimentalImportSupport: false, - inlineRequires: true, + inlineRequires: false, }, }), }, + resolver: { + extraNodeModules: new Proxy(extraNodeModules, { + get: (target, name) => + //redirects dependencies referenced from common/ to local node_modules + name in target + ? target[name] + : path.join(process.cwd(), `node_modules/${name}`), + }), + }, + watchFolders, }; From 64fb28d0fdb24cec46fbc716597c3fa299b240ef Mon Sep 17 00:00:00 2001 From: Alessandro Senese Date: Wed, 16 Mar 2022 17:14:55 +0000 Subject: [PATCH 002/119] add `initClient` --- example/__tests__/App.owl.tsx | 4 +- lib/client/client.ts | 80 ++++++++++++++++++++++++++++++++++ lib/client/constants.ts | 2 + lib/client/index.ts | 1 + lib/client/rn-websocket.ts | 30 +++++++++++++ lib/client/tracked-elements.ts | 18 ++++++++ lib/constants.ts | 1 + lib/index.ts | 2 + lib/websocket.ts | 5 +-- 9 files changed, 139 insertions(+), 4 deletions(-) create mode 100644 lib/client/client.ts create mode 100644 lib/client/constants.ts create mode 100644 lib/client/index.ts create mode 100644 lib/client/rn-websocket.ts create mode 100644 lib/client/tracked-elements.ts create mode 100644 lib/constants.ts diff --git a/example/__tests__/App.owl.tsx b/example/__tests__/App.owl.tsx index ed9b47be..d7a0177d 100644 --- a/example/__tests__/App.owl.tsx +++ b/example/__tests__/App.owl.tsx @@ -1,9 +1,11 @@ -import { takeScreenshot } from 'react-native-owl'; +import { takeScreenshot, tapOn } from 'react-native-owl'; describe('App.tsx', () => { it('takes a screenshot of the first screen', async () => { const screen = await takeScreenshot('homescreen'); + await tapOn('testMe'); + expect(screen).toMatchBaseline(); }); }); diff --git a/lib/client/client.ts b/lib/client/client.ts new file mode 100644 index 00000000..b554f61d --- /dev/null +++ b/lib/client/client.ts @@ -0,0 +1,80 @@ +// @ts-ignore +import React from 'react'; +import { Logger } from '../logger'; +import { CHECK_TIMEOUT, MAX_TIMEOUT } from './constants'; +import { initWebSocket } from './rn-websocket'; + +import { get, exists, add, ElementActions } from './tracked-elements'; + +// @ts-ignore +const logger = new Logger(true); // !!(process.env.OWL_DEBUG === 'true') || __DEV__); + +let automateTimeout: NodeJS.Timeout; +let isReactUpdating = true; + +export const initClient = async () => { + logger.info('Initialising OWL client'); + + // @ts-ignore + global.__owl_client = initWebSocket(logger); + patchReacth(); +}; + +const patchReacth = () => { + // @ts-ignore + const originalReactCreateElement = React.createElement; + + // @ts-ignore + React.createElement = (...args) => { + if (args[1]?.testID) { + const testID = args[1].testID; + + if (!exists(testID)) { + // @ts-ignore + const newRef = React.createRef(); + + args[1].ref = newRef; + // args[1].onLayout = (e) => testOnLayout(testID, e); + + add(testID, { + ref: newRef, + onPress: args[1].onPress, + // onChangeText: args[1].onChangeText, + }); + } + } + + clearTimeout(automateTimeout); + + automateTimeout = setTimeout(() => { + isReactUpdating = false; + }, CHECK_TIMEOUT); + + isReactUpdating = true; + + return originalReactCreateElement(...args); + }; +}; + +export const getElementByTestId = async ( + testID: string +): Promise => { + return new Promise((resolve, reject) => { + const rejectTimeout = setTimeout(() => { + const message = `Element with testID ${testID} not found`; + + clearInterval(checkInterval); + reject(new Error(message)); + }, MAX_TIMEOUT); + + const checkInterval = setInterval(() => { + if (isReactUpdating || get(testID) == null) { + return; + } + + clearInterval(checkInterval); + clearTimeout(rejectTimeout); + resolve(get(testID)); + }, CHECK_TIMEOUT); + }); +}; diff --git a/lib/client/constants.ts b/lib/client/constants.ts new file mode 100644 index 00000000..a2867b38 --- /dev/null +++ b/lib/client/constants.ts @@ -0,0 +1,2 @@ +export const MAX_TIMEOUT = 10 * 1000; +export const CHECK_TIMEOUT = 32; diff --git a/lib/client/index.ts b/lib/client/index.ts new file mode 100644 index 00000000..d525a1d3 --- /dev/null +++ b/lib/client/index.ts @@ -0,0 +1 @@ +export { initClient } from './client' \ No newline at end of file diff --git a/lib/client/rn-websocket.ts b/lib/client/rn-websocket.ts new file mode 100644 index 00000000..a8615715 --- /dev/null +++ b/lib/client/rn-websocket.ts @@ -0,0 +1,30 @@ +import { WEBSOCKET_PORT } from "../constants"; + +import { Logger } from "../logger"; + +export const initWebSocket = (logger: Logger) => { + // @ts-ignore + const ws = new WebSocket(`ws://localhost:${WEBSOCKET_PORT}`); + + ws.onopen = () => { + // connection opened + ws.send('something'); // send a message + }; + + ws.onmessage = (e: { data: any }) => { + // a message was received + logger.info(`[OWL] Websocket onMessage: ${e.data}`); + }; + + ws.onerror = (e: { message: string }) => { + // an error occurred + logger.error(`[OWL] Websocket onError: ${e.message}`); + }; + + ws.onclose = (e: { message: string }) => { + // connection closed + logger.error(`[OWL] Websocket onError: ${e.message}`); + }; + + return ws; +}; diff --git a/lib/client/tracked-elements.ts b/lib/client/tracked-elements.ts new file mode 100644 index 00000000..b1f1532c --- /dev/null +++ b/lib/client/tracked-elements.ts @@ -0,0 +1,18 @@ +export type ElementActions = { + ref: any; + onPress: Function; +}; + +const trackedElements: Record = {}; + +export const get = (ID: string) => { + return trackedElements[ID]; +}; + +export const exists = (ID: string) => { + return get(ID) !== undefined; +}; + +export const add = (ID: string, data: ElementActions) => { + trackedElements[ID] = data; +} diff --git a/lib/constants.ts b/lib/constants.ts new file mode 100644 index 00000000..c7415df1 --- /dev/null +++ b/lib/constants.ts @@ -0,0 +1 @@ +export const WEBSOCKET_PORT = 8123; diff --git a/lib/index.ts b/lib/index.ts index 5a32e1af..8bb10d86 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -1,3 +1,5 @@ import './matchers'; export { takeScreenshot } from './take-screenshot'; +export { tapOn } from './actions/actions'; +export { initClient } from './client/client'; diff --git a/lib/websocket.ts b/lib/websocket.ts index 97c4ff71..c5249a54 100644 --- a/lib/websocket.ts +++ b/lib/websocket.ts @@ -1,13 +1,12 @@ import WebSocket from 'ws'; +import { WEBSOCKET_PORT } from './constants'; import { Logger } from './logger'; -const port = 8123; - export const startWebSocketServer = async ( logger: Logger ): Promise => { - const wss = new WebSocket.Server({ port }); + const wss = new WebSocket.Server({ port: WEBSOCKET_PORT }); return new Promise((resolve) => { wss.on('connection', (ws) => { From be3af775e97962dd382983fa29f3abd76cba3c96 Mon Sep 17 00:00:00 2001 From: Alessandro Senese Date: Mon, 21 Mar 2022 17:39:59 +0000 Subject: [PATCH 003/119] start client-server communication --- lib/actions/actions.ts | 14 ++++++++ lib/cli/run.ts | 8 ++--- lib/client/actionHandler.ts | 69 +++++++++++++++++++++++++++++++++++++ 3 files changed, 87 insertions(+), 4 deletions(-) create mode 100644 lib/actions/actions.ts create mode 100644 lib/client/actionHandler.ts diff --git a/lib/actions/actions.ts b/lib/actions/actions.ts new file mode 100644 index 00000000..31624199 --- /dev/null +++ b/lib/actions/actions.ts @@ -0,0 +1,14 @@ +import WebSocket from "ws"; + +export const tapOn = async(testId: string) => { + const client = new WebSocket('wss://localhost:8123'); + + console.info("sending event", client) + // client.onopen = (event) => { + // console.info({event}) + // client.send("hello world") + // } + + return new Promise(resolve => { + }) +} \ No newline at end of file diff --git a/lib/cli/run.ts b/lib/cli/run.ts index 2cfa7e22..a793aac8 100644 --- a/lib/cli/run.ts +++ b/lib/cli/run.ts @@ -20,7 +20,7 @@ export const runIOS = async (config: Config, logger: Logger) => { const simulator = config.ios!.device.replace(/([ /])/g, '\\$1'); const { stdout: bundleId } = await execa.command( - `./PlistBuddy -c 'Print CFBundleIdentifier' ${plistPath}`, + `/usr/libexec/PlistBuddy -c 'Print CFBundleIdentifier' ${plistPath}`, { shell: true, cwd: '/usr/libexec' } ); @@ -55,9 +55,9 @@ export const runAndroid = async (config: Config, logger: Logger) => { const appPath = path.join(cwd, appFilename); const { packageName } = config.android!; - const SIMULATOR_TIME = '0941'; - const setTimeCommand = `adb shell date 0101${SIMULATOR_TIME}`; - await execa.command(setTimeCommand, { stdio }); + // const SIMULATOR_TIME = '0941'; + // const setTimeCommand = `adb shell date 0101${SIMULATOR_TIME}`; + // await execa.command(setTimeCommand, { stdio }); const installCommand = `adb install -r ${appPath}`; await execa.command(installCommand, { stdio }); diff --git a/lib/client/actionHandler.ts b/lib/client/actionHandler.ts new file mode 100644 index 00000000..76205439 --- /dev/null +++ b/lib/client/actionHandler.ts @@ -0,0 +1,69 @@ + +// /** +// * +// * This can be used to know the scrollview position & height and therefore +// * take a fullscreen screenshot +// */ +// // function testOnLayout(testID, e) { +// // // scrollViewLatyous[testID] = e.nativeEvent.layout; + +// // console.info(e.nativeEvent.layout); +// // } + +// export const tapOn = async (testID: string) => { +// debug('Tapping on element with testID', testID); + +// const element = await getElementByTestId(testID); + +// // @ts-ignore +// element.onPress(); +// }; + +// // export const focusInput = async (testID: string) => { +// // debug('Focusing on element with testID', testID); + +// // const element = await getElementByTestId(testID); + +// // element.ref.current.focus(); +// // }; + +// // export const fillInput = async (testID: string, value: string) => { +// // debug(`Filling field testID ${testID}`, `with value ${value}`); + +// // const element = await getElementByTestId(testID); + +// // element.onChangeText(value); +// // }; + +// // export const getScrollViewLayout = async (testID: string) => { +// // debug('Getting ScrollView layout for testID', testID); + +// // await getElementByTestId(testID); + +// // return scrollViewLatyous[testID]; +// // }; + +// // export const scrollViewTo = async (testID: string, y: number) => { +// // debug(`scrolling ScrollView: ${testID}`, `to value ${y}`); + +// // const element = await getElementByTestId(testID); + +// // element.ref.current.scrollTo({ y }); +// // }; + +// // export const scrollViewToEnd = async (testID: string) => { +// // debug(`scrolling ScrollView: ${testID}`, 'to value end'); + +// // const element = await getElementByTestId(testID); + +// // element.ref.current.scrollToEnd(); +// // }; + +// // export const elementToBeVisible = async (testID: string) => { +// // await getElementByTestId(testID); +// // }; + +// const debug = (action: string, value: string) => { +// console.debug(`DIRTY HACK: ${action}`, value); +// }; + \ No newline at end of file From c8ff7847a0877f3e828a48a4e2f6bd3f6bee4e51 Mon Sep 17 00:00:00 2001 From: Alessandro Senese Date: Mon, 21 Mar 2022 17:40:12 +0000 Subject: [PATCH 004/119] client-server communication --- .gitignore | 3 +++ example/__tests__/App.owl.tsx | 6 +++-- example/package.json | 2 +- lib/actions/actions.ts | 34 ++++++++++++++++++--------- lib/actions/types.ts | 1 + lib/client/client.ts | 37 ++++++++++++++++++++++++++---- lib/client/rn-websocket.ts | 43 +++++++++++++++++++---------------- lib/index.ts | 1 - lib/websocket.ts | 2 +- 9 files changed, 88 insertions(+), 41 deletions(-) create mode 100644 lib/actions/types.ts diff --git a/.gitignore b/.gitignore index ef03bf2c..66e83c2b 100644 --- a/.gitignore +++ b/.gitignore @@ -58,3 +58,6 @@ typings/ # generated js dist/ + +# IntellJ IDEA +.idea \ No newline at end of file diff --git a/example/__tests__/App.owl.tsx b/example/__tests__/App.owl.tsx index d7a0177d..698099f7 100644 --- a/example/__tests__/App.owl.tsx +++ b/example/__tests__/App.owl.tsx @@ -1,11 +1,13 @@ import { takeScreenshot, tapOn } from 'react-native-owl'; +jest.setTimeout(30000); + describe('App.tsx', () => { it('takes a screenshot of the first screen', async () => { - const screen = await takeScreenshot('homescreen'); - await tapOn('testMe'); + const screen = await takeScreenshot('homescreen'); + expect(screen).toMatchBaseline(); }); }); diff --git a/example/package.json b/example/package.json index ed5ecb1d..eaea1c0c 100644 --- a/example/package.json +++ b/example/package.json @@ -9,7 +9,7 @@ "owl:build:ios": "yarn owl build --platform ios", "owl:test:ios": "yarn owl test --platform ios", "owl:build:android": "yarn owl build --platform android", - "owl:test:android": "yarn owl test --platform android", + "owl:test:android": "adb reverse tcp:8123 tcp:8123 && yarn owl test --platform android", "start": "react-native start", "test": "jest", "lint": "eslint . --ext .js,.jsx,.ts,.tsx" diff --git a/lib/actions/actions.ts b/lib/actions/actions.ts index 31624199..20ef1667 100644 --- a/lib/actions/actions.ts +++ b/lib/actions/actions.ts @@ -1,14 +1,26 @@ -import WebSocket from "ws"; +import WebSocket from 'ws'; +import { Logger } from '../logger'; +import { createWebSocketClient } from '../websocket'; +import { ACTION } from './types'; -export const tapOn = async(testId: string) => { - const client = new WebSocket('wss://localhost:8123'); +const logger = new Logger(true); // !!(process.env.OWL_DEBUG === 'true') || __DEV__); - console.info("sending event", client) - // client.onopen = (event) => { - // console.info({event}) - // client.send("hello world") - // } +let actionsClient: WebSocket; - return new Promise(resolve => { - }) -} \ No newline at end of file +const sendEvent = async (action: ACTION, message?: string) => { + if (actionsClient === undefined) { + actionsClient = await createWebSocketClient(logger, handleMessage); + } + + actionsClient.send(JSON.stringify({ action, message })); +}; + +const handleMessage = (message: string) => { + console.info('response received', message); +}; + +export const tapOn = async (testId: string) => { + await sendEvent('TAP', testId); + + return new Promise(() => {}); +}; diff --git a/lib/actions/types.ts b/lib/actions/types.ts new file mode 100644 index 00000000..738d377f --- /dev/null +++ b/lib/actions/types.ts @@ -0,0 +1 @@ +export type ACTION = 'TAP' \ No newline at end of file diff --git a/lib/client/client.ts b/lib/client/client.ts index b554f61d..77d8b212 100644 --- a/lib/client/client.ts +++ b/lib/client/client.ts @@ -3,6 +3,7 @@ import React from 'react'; import { Logger } from '../logger'; import { CHECK_TIMEOUT, MAX_TIMEOUT } from './constants'; import { initWebSocket } from './rn-websocket'; +import { ACTION } from '../actions/types'; import { get, exists, add, ElementActions } from './tracked-elements'; @@ -11,13 +12,13 @@ const logger = new Logger(true); // !!(process.env.OWL_DEBUG === 'true') || __DE let automateTimeout: NodeJS.Timeout; let isReactUpdating = true; +const SOCKET_WAIT_TIMEOUT = 300; export const initClient = async () => { logger.info('Initialising OWL client'); - // @ts-ignore - global.__owl_client = initWebSocket(logger); patchReacth(); + waitForWebSocket(); }; const patchReacth = () => { @@ -56,10 +57,36 @@ const patchReacth = () => { }; }; -export const getElementByTestId = async ( - testID: string -): Promise => { +/** + * The app might launch before the OWL server starts, so we need to keep trying... + */ +const waitForWebSocket = async () => { + try { + const client = await initWebSocket(logger, handleMessage); + + logger.info('[OWL] Connection established'); + + // @ts-ignore + global.__owl_client = client; + } catch { + setTimeout(waitForWebSocket, SOCKET_WAIT_TIMEOUT); + } +}; + +const handleMessage = async (message: string) => { + const { action, testID } = JSON.parse(message); + const element = await getElementByTestId(testID); + + switch (action as ACTION) { + case 'TAP': + element.onPress(); + } +}; + +const getElementByTestId = async (testID: string): Promise => { return new Promise((resolve, reject) => { + logger.info(`Looking for Element with testID ${testID}`); + const rejectTimeout = setTimeout(() => { const message = `Element with testID ${testID} not found`; diff --git a/lib/client/rn-websocket.ts b/lib/client/rn-websocket.ts index a8615715..fe55407f 100644 --- a/lib/client/rn-websocket.ts +++ b/lib/client/rn-websocket.ts @@ -1,30 +1,33 @@ -import { WEBSOCKET_PORT } from "../constants"; +import { WEBSOCKET_PORT } from '../constants'; -import { Logger } from "../logger"; +import { Logger } from '../logger'; -export const initWebSocket = (logger: Logger) => { +export const initWebSocket = ( + logger: Logger, + onMessage: (message: string) => void +) => { // @ts-ignore const ws = new WebSocket(`ws://localhost:${WEBSOCKET_PORT}`); - ws.onopen = () => { - // connection opened - ws.send('something'); // send a message - }; + return new Promise((resolve, reject) => { + ws.onopen = () => { + ws.send('OWL Client Connected!'); + resolve(ws); + }; - ws.onmessage = (e: { data: any }) => { - // a message was received - logger.info(`[OWL] Websocket onMessage: ${e.data}`); - }; + ws.onmessage = (e: { data: any }) => { + logger.info(`[OWL] Websocket onMessage: ${e.data}`); - ws.onerror = (e: { message: string }) => { - // an error occurred - logger.error(`[OWL] Websocket onError: ${e.message}`); - }; + onMessage(e.data.toString()); + }; - ws.onclose = (e: { message: string }) => { - // connection closed - logger.error(`[OWL] Websocket onError: ${e.message}`); - }; + ws.onerror = (e: { message: string }) => { + logger.info(`[OWL] Websocket onError: ${e.message}`); + }; - return ws; + ws.onclose = (e: { message: string }) => { + logger.info(`[OWL] Websocket onClose: ${e.message}`); + reject(e); + }; + }); }; diff --git a/lib/index.ts b/lib/index.ts index 8bb10d86..ccd4052b 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -2,4 +2,3 @@ import './matchers'; export { takeScreenshot } from './take-screenshot'; export { tapOn } from './actions/actions'; -export { initClient } from './client/client'; diff --git a/lib/websocket.ts b/lib/websocket.ts index c5249a54..4af87251 100644 --- a/lib/websocket.ts +++ b/lib/websocket.ts @@ -41,7 +41,7 @@ export const createWebSocketClient = async ( logger: Logger, onMessage: (message: string) => void ): Promise => { - const wsClient = new WebSocket(`ws://localhost:${port}`); + const wsClient = new WebSocket(`ws://localhost:${WEBSOCKET_PORT}`); return new Promise((resolve) => { wsClient.on('open', () => { From 4ef0eb55334748bef908750ab91d5dc23070788e Mon Sep 17 00:00:00 2001 From: Alessandro Senese Date: Fri, 25 Mar 2022 09:05:33 +0000 Subject: [PATCH 005/119] actions --- example/App.tsx | 170 +++++++++++++-------------------- example/index.js | 2 +- example/package.json | 6 +- example/yarn.lock | 109 ++++++++++++++++++++- lib/actions/actions.ts | 22 ++++- lib/actions/types.ts | 10 +- lib/client/client.ts | 72 ++++++++------ lib/client/index.owl.js | 8 ++ lib/client/rn-websocket.ts | 11 ++- lib/client/tracked-elements.ts | 12 ++- 10 files changed, 271 insertions(+), 151 deletions(-) create mode 100644 lib/client/index.owl.js diff --git a/example/App.tsx b/example/App.tsx index 800cebb6..7279e7c2 100644 --- a/example/App.tsx +++ b/example/App.tsx @@ -1,119 +1,77 @@ -/** - * Sample React Native App - * https://github.com/facebook/react-native - * - * Generated with the TypeScript template - * https://github.com/react-native-community/react-native-template-typescript - * - * @format - */ +import * as React from 'react'; +import { View, Text, Button, TextInput, Pressable } from 'react-native'; +import { NavigationContainer, useNavigation } from '@react-navigation/native'; +import { createNativeStackNavigator } from '@react-navigation/native-stack'; -import React from 'react'; -import { - SafeAreaView, - ScrollView, - StatusBar, - StyleSheet, - Text, - useColorScheme, - View, -} from 'react-native'; - -import { - Colors, - DebugInstructions, - Header, - LearnMoreLinks, - ReloadInstructions, -} from 'react-native/Libraries/NewAppScreen'; - -const Section: React.FC<{ - title: string; -}> = ({ children, title }) => { - const isDarkMode = useColorScheme() === 'dark'; +function HomeScreen({ navigation }) { return ( - - + Welcome to OWL Demo + + navigation.navigate('DetailsScreen')} > - {title} - - - {children} - + + View Details + + ); -}; +} -const App = () => { - const isDarkMode = useColorScheme() === 'dark'; +function DetailsScreen({ navigation }) { + const [text, setText] = React.useState(''); + const [isTrue, setTrue] = React.useState(false); - const backgroundStyle = { - backgroundColor: isDarkMode ? Colors.darker : Colors.lighter, - }; + return ( + + Details Screen + + setTrue(true)}> + + Reveal secret + + + {isTrue ? ( + setText(newText)} + defaultValue={text} + /> + ) : null} + + ); +} +const Stack = createNativeStackNavigator(); + +export function App() { return ( - - - -
- + + + , }} - > -
- Edit App.tsx to change this - screen and then come back to see your edits. -
-
- -
-
- -
-
- Read the docs to discover what to do next: -
- -
- - + /> + + ); -}; +} -const styles = StyleSheet.create({ - sectionContainer: { - marginTop: 32, - paddingHorizontal: 24, - }, - sectionTitle: { - fontSize: 24, - fontWeight: '600', - }, - sectionDescription: { - marginTop: 8, - fontSize: 18, - fontWeight: '400', - }, - highlight: { - fontWeight: '700', - }, -}); +const HeaderLeft = () => { + const navigation = useNavigation(); -export default App; + return ( +