From d4c43ce087eb5cf6e064343a4c46743aeb69edf0 Mon Sep 17 00:00:00 2001 From: Michael Huynh Date: Wed, 16 Oct 2024 14:35:51 -0700 Subject: [PATCH] feat: proof of concept appium testing --- test/app/appium/config.js | 20 + test/app/appium/outgoingCall.js | 18 + test/app/index.js | 4 +- test/app/package.json | 3 +- test/app/src/e2e/OutgoingCall.tsx | 85 ++ test/app/yarn.lock | 1264 ++++++++++++++++++++++++++++- 6 files changed, 1363 insertions(+), 31 deletions(-) create mode 100644 test/app/appium/config.js create mode 100644 test/app/appium/outgoingCall.js create mode 100644 test/app/src/e2e/OutgoingCall.tsx diff --git a/test/app/appium/config.js b/test/app/appium/config.js new file mode 100644 index 00000000..8a6379dd --- /dev/null +++ b/test/app/appium/config.js @@ -0,0 +1,20 @@ +const capabilities = { + 'platformName': 'Android', + 'appium:automationName': 'UiAutomator2', + 'appium:deviceName': 'Android', + 'appium:appPackage': 'com.example.twiliovoicereactnative', + 'appium:appActivity': '.MainActivity', + 'appium:autoGrantPermissions': 'true', +}; + +const webdriverioOptions = { + hostname: process.env.APPIUM_HOST || 'localhost', + port: parseInt(process.env.APPIUM_PORT, 10) || 4723, + logLevel: 'info', + capabilities, +}; + +module.exports = { + capabilities, + webdriverioOptions, +}; diff --git a/test/app/appium/outgoingCall.js b/test/app/appium/outgoingCall.js new file mode 100644 index 00000000..630d972b --- /dev/null +++ b/test/app/appium/outgoingCall.js @@ -0,0 +1,18 @@ +const { remote } = require('webdriverio'); +const { webdriverioOptions } = require('./config'); + +async function runTest() { + const driver = await remote(webdriverioOptions); + + try { + await driver.pause(5000); + const runTestButton = driver.$('~runTest'); + await runTestButton.click(); + await driver.pause(10000); + } finally { + await driver.pause(1000); + await driver.deleteSession(); + } +} + +runTest().catch(console.error); diff --git a/test/app/index.js b/test/app/index.js index 752e7ee3..9355fe9d 100644 --- a/test/app/index.js +++ b/test/app/index.js @@ -3,7 +3,7 @@ */ import { AppRegistry } from 'react-native'; -import App from './src/App'; +import OutgoingCallTest from './src/e2e/OutgoingCall'; import { name as appName } from './app.json'; -AppRegistry.registerComponent(appName, () => App); +AppRegistry.registerComponent(appName, () => OutgoingCallTest); diff --git a/test/app/package.json b/test/app/package.json index ae14fb46..53fc8cbe 100644 --- a/test/app/package.json +++ b/test/app/package.json @@ -33,7 +33,8 @@ "metro-react-native-babel-preset": "0.76.8", "prettier": "^2.4.1", "react-test-renderer": "18.2.0", - "typescript": "4.8.4" + "typescript": "4.8.4", + "webdriverio": "^9.2.1" }, "engines": { "node": ">=16" diff --git a/test/app/src/e2e/OutgoingCall.tsx b/test/app/src/e2e/OutgoingCall.tsx new file mode 100644 index 00000000..0264dfd9 --- /dev/null +++ b/test/app/src/e2e/OutgoingCall.tsx @@ -0,0 +1,85 @@ +import * as React from 'react'; +import { Button, Text, View } from 'react-native'; +import { Call, Voice } from '@twilio/voice-react-native-sdk'; +import { generateAccessToken } from '../tokenUtility'; + +type TestStatus = 'not-started' | 'running' | 'success' | 'failed'; + +type TestHook = () => { status: TestStatus; run: () => Promise }; + +async function settlePromise( + p: Promise +): Promise<{ result: 'ok' | 'err'; value: T }> { + return p + .then((value) => ({ result: 'ok' as const, value })) + .catch((value) => ({ result: 'err' as const, value })); +} + +const useOutgoingCallTest: TestHook = () => { + const testId = React.useMemo(() => Date.now(), []); + const voice = React.useMemo(() => new Voice(), []); + const accessToken = React.useMemo(() => generateAccessToken(), []); + + const [status, setStatus] = React.useState(() => 'not-started'); + + const run = React.useCallback(async () => { + setStatus('running'); + + const call = await voice.connect(accessToken); + + const connectedPromise = new Promise<'connected'>((resolve) => { + call.on(Call.Event.Connected, () => { + console.log(testId, 'call event connected'); + resolve('connected'); + }); + }); + + const connectFailurePromise = new Promise<'connectFailure'>((resolve) => { + call.on(Call.Event.ConnectFailure, (error) => { + console.log(testId, 'call event connectFailure', error); + resolve('connectFailure'); + }); + }); + + const callStatus = await Promise.any([ + connectedPromise, + connectFailurePromise, + ]); + + if (callStatus === 'connectFailure') { + setStatus('failed'); + return; + } + + await new Promise((resolve) => { + setTimeout(resolve, 5000); + }); + + const disconnectResult = await settlePromise(call.disconnect()); + if (disconnectResult.result === 'err') { + setStatus('failed'); + console.log(testId, 'disconnect promise failed', disconnectResult.value); + return; + } + + setStatus('success'); + }, [accessToken, testId, voice]); + + return { run, status }; +}; + +export default function OutgoingCallTest() { + const { run, status } = useOutgoingCallTest(); + + return ( + + + outgoingCallTest + +