Travis ui tests (#57)

* Prepare travis is tests

* Fix ffmpeg travis source

* Trying xenial

* Move shell scripts

* Upload video assets

* Upload video assets

* Change text input method

* Add ui test docker support

* Fix travis docker build

* Fix asset uploader

* Fix dockerfile

* Update dockerfile

* Change writeText behavior

* Fix type error

* Fix exit codes

* Fix types

* fix upload

* Fix writeText

* Fix argument name

* Add test scenarios

* Enable vnc and change mqtt host
This commit is contained in:
Thomas Nordquist
2019-01-30 03:13:19 -08:00
committed by GitHub
parent 0114d938bd
commit d64e085247
22 changed files with 397 additions and 1819 deletions

View File

@@ -1,7 +1,7 @@
language: node_js
services:
- docker
- xvfb
cache:
directories:
@@ -17,12 +17,19 @@ os:
- linux
- osx
dist: xenial
services:
- docker
install:
- yarn install
- if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then docker build docker --tag uitest; fi;
script:
- yarn run build
- yarn test
- if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then docker run -e GH_TOKEN=$GH_TOKEN -e GIT_TAG=$TRAVIS_TAG --rm -v `pwd`:/app uitest sh -c "cd app && docker/testMounted.sh"; fi
- if [[ "$TRAVIS_TAG" != "" ]]; then yarn run prepare-release; fi
- if [[ "$TRAVIS_OS_NAME" == "linux" ]] && [[ "$TRAVIS_TAG" != "" ]]; then yarn run package -- linux; fi
- if [[ "$TRAVIS_OS_NAME" == "osx" ]] && [[ "$TRAVIS_TAG" != "" ]]; then yarn run package -- mac; fi

View File

@@ -144,6 +144,7 @@ class Publish extends React.Component<Props, State> {
size="small"
color="primary"
onClick={this.publish}
id="publish-button"
>
<Navigation style={{ marginRight: '8px' }} /> Publish
</Button>

17
docker/Dockerfile Normal file
View File

@@ -0,0 +1,17 @@
FROM node:11-stretch
RUN DEBIAN_FRONTEND="noninteractive" apt-get update \
&& apt-get install -y --no-install-recommends nano ffmpeg xvfb git-core tmux locales mosquitto x11vnc
RUN apt-get install -yq --no-install-recommends libasound2 libatk1.0-0 libc6 libcairo2 libcups2 libdbus-1-3 libexpat1 libfontconfig1 libgcc1 libgconf-2-4 libgdk-pixbuf2.0-0 libglib2.0-0 libgtk-3-0 libnspr4 libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 libxtst6 libnss3
# Generate locales for TMUX
RUN sed -i -e 's/# en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen && locale-gen
ENV LANG en_US.UTF-8
ENV LANGUAGE en_US:en
ENV LC_ALL en_US.UTF-8
CMD /bin/bash
COPY cloneBuildAndTest.sh ./
VOLUME /app
EXPOSE 5900

12
docker/cloneBuildAndTest.sh Executable file
View File

@@ -0,0 +1,12 @@
#!/bin/bash
set -e
git clone https://github.com/thomasnordquist/MQTT-Explorer.git /app
cd /app
git checkout travis-ui-tests
yarn
yarn build
yarn ui-test
yarn upload-video-artifacts

5
docker/testMounted.sh Executable file
View File

@@ -0,0 +1,5 @@
#!/bin/bash
set -e
yarn ui-test
yarn upload-video-artifacts

View File

@@ -9,9 +9,11 @@
"install": "cd app; yarn; cd ..",
"build": "tsc && cd app && yarn run build && cd ..",
"test-backend": "cd backend && yarn run test && cd ..",
"prepare-release": "./prepare-release.sh",
"prepare-release": "./scripts/prepare-release.sh",
"package": "ts-node package.ts",
"package-with-docker": "./package-with-docker.sh"
"ui-test": "./scripts/uiTests.sh",
"upload-video-artifacts": "./scripts/uploadVideoAsset.ts ui-test.mp4 ui-test.gif",
"package-with-docker": "./scripts/package-with-docker.sh"
},
"repository": {
"type": "git",
@@ -42,6 +44,7 @@
"license": "ISC",
"devDependencies": {
"@types/chai": "^4.1.7",
"@types/mime": "^2.0.0",
"@types/mocha": "^5.2.5",
"@types/mustache": "^0.8.32",
"@types/node": "^10.12.18",
@@ -51,6 +54,7 @@
"chai": "^4.2.0",
"electron": "^4.0.2",
"electron-builder": "^20.38.4",
"mime": "^2.4.0",
"mocha": "^5.2.0",
"mustache": "^3.0.1",
"nyc": "^13.1.0",

View File

@@ -1,14 +1,11 @@
#!/bin/bash
cp app.mp4 original.mp4
# The video starts with a few blank frames, we want to know when the stop
ffprobe -f lavfi -i "movie=app.mp4,blackdetect[out0]" -show_entries tags=lavfi.black_start,lavfi.black_end -of default=nw=1 -v quiet > ffmpeg_info
END_OF_BLACK=`cat ffmpeg_info | grep end | head -n1 | cut -d'=' -f2`
# Find crop values for black bars
CROP=`ffmpeg -i app.mp4 -t 1 -vf cropdetect -f null - 2>&1 | awk '/crop/ { print $NF }' | head -n1`
# Crop and trim black frames at start
ffmpeg -ss $END_ONF_BLACK -i app.mp4 -vf "$CROP" app2.mp4
# Trim black frames at start
ffmpeg -ss $END_OF_BLACK -i app.mp4 app2.mp4
mv app2.mp4 app.mp4
# Generate gif palette
@@ -19,3 +16,6 @@ ffmpeg -i app.mp4 -i palette.png -filter_complex "fps=10,scale=720:-1:flags=lanc
# Clean up
rm ffmpeg_info palette.png
mv app.mp4 ui-test.mp4
mv app.gif ui-test.gif

View File

@@ -1,22 +1,26 @@
#!/bin/bash
function finish {
echo "UUUUPS I crashed"
echo "Exiting, cleaning up"
tmux send-keys -t record q || echo "No tmux was running"
echo kill $PID_XVFB $PID_CHROMEDRIVER $PID_MOSQUITTO
kill $PID_XVFB $PID_CHROMEDRIVER $PID_MOSQUITTO
#echo kill $PID_XVFB $PID_CHROMEDRIVER $PID_MOSQUITTO
#kill $PID_XVFB $PID_CHROMEDRIVER $PID_MOSQUITTO
}
trap finish EXIT
set -e
DIMENSIONS="1024x700"
SCR=99
# Start new window manager
Xvfb :$SCR -screen 0 1024x800x24 -ac &
Xvfb :$SCR -screen 0 "$DIMENSIONS"x24 -ac &
export PID_XVFB=$!
sleep 2
# Debug with VNC
while [ "$TEST_EXIT_CODE" = "" ]; do x11vnc -passwd "bierbier" -display :$SCR; done &
export PID_VNC=$!
# Start mqtt broker
mosquitto &
export PID_MOSQUITTO=$!
@@ -28,10 +32,12 @@ sleep 2
rm ./app.mp4 || echo no need to delete ./app.mp4
# Start recoring in tmux
tmux new-session -d -s record ffmpeg -f x11grab -draw_mouse 0 -video_size 1024x800 -i :$SCR -codec:v libx264 -r 20 ./app.mp4
tmux new-session -d -s record ffmpeg -f x11grab -draw_mouse 0 -video_size $DIMENSIONS -i :$SCR -codec:v libx264 -r 20 ./app.mp4
# Start tests
node dist/src/spec/webdriverio.js
TEST_EXIT_CODE=$?
echo "Webriver exitet with $TEST_EXIT_CODE"
# Stop recording
tmux send-keys -t record q
@@ -40,4 +46,6 @@ tmux send-keys -t record q
sleep 5
# Process the video
./prepareVideo.sh
./scripts/prepareVideo.sh
exit $TEST_EXIT_CODE

86
scripts/uploadVideoAsset.ts Executable file
View File

@@ -0,0 +1,86 @@
#!node_modules/.bin/ts-node
import axios from 'axios'
import * as fs from 'fs'
import * as path from 'path'
import * as mime from 'mime'
const githubToken = process.env.GH_TOKEN
async function tagUrl(tag: string): Promise<string | undefined> {
const response = await axios.get(`https://api.github.com/repos/thomasnordquist/mqtt-explorer/releases?access_token=${githubToken}`)
const tagRelease = response.data.find((release: any) => release.tag_name === tag)
return tagRelease ? cleanUploadUrl(tagRelease.upload_url) : undefined
}
async function createDraft(tag: string) {
console.log('create draft')
const response = await axios({
method: 'post',
url: `https://api.github.com/repos/thomasnordquist/mqtt-explorer/releases?access_token=${githubToken}`,
data: {
tag_name: tag,
name: tag.slice(1),
draft: true,
},
})
return cleanUploadUrl(response.data.upload_url)
}
function cleanUploadUrl(url: string) {
return url.match(/(.*){/)![1]
}
async function uploadAsset() {
const tag: string | undefined = process.env.GIT_TAG
const files = process.argv.slice(2)
if (!tag || files.length === 0) {
console.log('Nothing to do')
return
}
let uploadUrl: string | undefined
try {
uploadUrl = await tagUrl(tag)
if (!uploadUrl) {
console.log('tag does not exist')
try {
uploadUrl = await createDraft(tag)
} catch (error) {
console.error('failed to create draft', error.stack)
process.exit(1)
}
}
} catch (error) {
console.error('failed to find tag release', error.stack)
process.exit(1)
}
if (uploadUrl) {
console.log(uploadUrl)
for (const file of files) {
console.log('uploading file', file)
await uploadFile(uploadUrl, file)
console.log('upload completed')
}
}
}
async function uploadFile(uploadUrl: string, file: string) {
const data = fs.readFileSync(file)
const mimeType = mime.getType(path.extname(file))
return await axios({
data,
method: 'post',
url: `${uploadUrl}?name=${path.basename(file)}&access_token=${githubToken}`,
headers: {
'Content-Type': mimeType,
},
})
}
uploadAsset()

View File

@@ -14,6 +14,8 @@ if (!isDev) {
}
const isDebugEnabled = Boolean(process.argv.find(arg => arg === 'debug'))
const isFullscreen = Boolean(process.argv.find(arg => arg === '--fullscreen'))
require('electron-debug')({ enabled: isDebugEnabled })
autoUpdater.logger = log
@@ -41,7 +43,10 @@ function createWindow() {
})
mainWindow.once('ready-to-show', () => {
mainWindow && mainWindow.show()
if (mainWindow) {
isFullscreen && mainWindow.setFullScreen(true)
mainWindow.show()
}
})
console.log('icon path', iconPath)

View File

@@ -17,7 +17,7 @@ function startServer(): Promise<mqtt.MqttClient> {
function connectMqtt(): Promise<mqtt.MqttClient> {
return new Promise((resolve) => {
const client = mqtt.connect('mqtt://localhost:1883', { username: 'thomas', password: 'bierbier' })
const client = mqtt.connect('mqtt://127.0.0.1:1883', { username: 'thomas', password: 'bierbier' })
client.once('connect', () => {
resolve(client)
})
@@ -52,6 +52,15 @@ function generateData(client: mqtt.MqttClient) {
client.publish('livingroom/lamp-1/brightness', '48', { retain: true, qos: 0 })
client.publish('livingroom/lamp-2/state', 'off', { retain: true, qos: 0 })
client.publish('livingroom/lamp-2/brightness', '48', { retain: true, qos: 0 })
client.publish('kitchen/lamp/state', 'off', { retain: true, qos: 0 })
client.subscribe('kitchen/lamp/set')
client.on('message', (topic, payload) => {
if (topic === 'kitchen/lamp/set') {
setTimeout(() => client.publish('kitchen/lamp/state', payload, { retain: true, qos: 0 }), 500)
}
})
intervals.push(setInterval(() => client.publish('kitchen/temperature', temperature()), 1500))
intervals.push(setInterval(() => client.publish('kitchen/humidity', temperature(60, -5, 0)), 1800))
@@ -66,12 +75,32 @@ function generateData(client: mqtt.MqttClient) {
// Used for demonstrating "clean up"
client.publish('test 123', 'Hello world', { retain: true, qos: 0 })
client.publish('hello', 'sunshine', { retain: true, qos: 0 })
// Just stuff
client.publish('01-80-C2-00-00-0F/LWT', 'offline', { retain: true, qos: 0 })
intervals.push(setInterval(() => {
client.publish('3d-printer/OctoPrint/temperature/bed', '{"_timestamp":1548589083,"actual":25.9,"target":0}')
client.publish('3d-printer/OctoPrint/temperature/tool0', '{"_timestamp":1548589093,"actual":26.4,"target":0}')
}, 3333))
let state = true
intervals.push(setInterval(() => {
state = !state
const enitityId = Math.round(Math.random() * 3000)
client.publish(
'actuality/showcase', `{
"tags":{
"entityId":${enitityId},
"entityType":"person",
"host":"d44ad81e10f9",
"server":"${state ? 'http://localhost/dataActuality' : 'http://localhost/dataStorage' }",
"status":"${state ? 'live' : 'inactive'}"},
"timestamp":${Date.now()}
}`.replace(/\s/g, ''),
{ retain: true, qos: 0 },
)
}, 2102))
}
export default startServer

View File

@@ -0,0 +1,14 @@
import { clickOn, sleep, writeText, expandTopic, hideText } from '../util'
import { Browser } from 'webdriverio'
// Expects a topic with at least two messages to be selected
export async function compareJsonSideBySide(browser: Browser<void>) {
const firstEntry = await browser.$('//span[contains(text(), "History")]/../../div/div[1]/div')
const secondEntry = await browser.$('//span[contains(text(), "History")]/../../div/div[2]/div')
await clickOn(secondEntry, browser)
await sleep(2000)
await clickOn(firstEntry, browser)
await sleep(2000)
await clickOn(firstEntry, browser)
await sleep(1000)
}

View File

@@ -0,0 +1,7 @@
import { clickOn } from '../util'
import { Browser } from 'webdriverio'
export async function disconnect(browser: Browser<void>) {
const connectButton = await browser.$('//button/span[contains(text(),"Disconnect")]')
clickOn(connectButton, browser)
}

View File

@@ -0,0 +1,23 @@
import { clickOn, sleep, writeText, delteTextWithBackspaces, expandTopic, moveToCenterOfElement, showText } from '../util'
import { Browser } from 'webdriverio'
export async function publishTopic(browser: Browser<void>) {
await expandTopic('kitchen/lamp/state', browser)
const topicInput = await browser.$('//textarea[contains(text(),"kitchen/lamp/state")][2]')
await clickOn(topicInput, browser)
await delteTextWithBackspaces(topicInput, browser, 120, 5)
await writeText('set', browser, 300)
const payloadInput = await browser.$('//*[contains(@class, "ace_text-input")]')
await payloadInput.setValue('o')
await sleep(300)
await payloadInput.setValue('n')
await sleep(700)
const publishButton = await browser.$('#publish-button')
await moveToCenterOfElement(publishButton, browser)
await showText('Lamp turns on', 1000, browser, 'top')
await sleep(500)
await clickOn(publishButton, browser)
}

View File

@@ -1,8 +1,15 @@
import { clickOn, sleep, writeText } from '../util'
import { clickOn, sleep, writeText, delteTextWithBackspaces, showText } from '../util'
import { Browser } from 'webdriverio'
export async function searchTree(browser: Browser<void>) {
export async function searchTree(text: string, browser: Browser<void>) {
const searchField = await browser.$('//input[contains(@placeholder, "Search")]')
await clickOn(searchField, browser, 1)
writeText('temp', browser)
await writeText(text, browser, 100)
await sleep(1500)
}
export async function clearSearch(browser: Browser<void>) {
const searchField = await browser.$('//input[contains(@placeholder, "Search")]')
await clickOn(searchField, browser, 1)
await delteTextWithBackspaces(searchField, browser, 100)
}

View File

@@ -2,7 +2,7 @@ import { clickOn, sleep, writeText, expandTopic } from '../util'
import { Browser } from 'webdriverio'
export async function showJsonPreview(browser: Browser<void>) {
await expandTopic('3d-printer/OctoPrint/temperature/bed', browser)
await expandTopic('actuality/showcase', browser)
await sleep(1000)
}

View File

@@ -1,11 +1,10 @@
import { clickOn, sleep, writeText, expandTopic } from '../util'
import { clickOn, sleep, writeText, expandTopic, clickOnHistory } from '../util'
import { Browser } from 'webdriverio'
export async function showNumericPlot(browser: Browser<void>) {
await expandTopic('livingroom/temperature', browser)
const messageHistory = await browser.$('//span/*[contains(text(), "History")]')
await clickOn(messageHistory, browser, 1)
await clickOnHistory(browser)
await sleep(1000)
await expandTopic('livingroom/humidity', browser)

View File

@@ -1,17 +1,29 @@
import { Element, Browser } from 'webdriverio'
export { expandTopic } from './expandTopic'
export function sleep(ms: number) {
export function sleep(ms: number, required = false) {
return new Promise((resolve) => {
setTimeout(resolve, ms)
if (required) {
setTimeout(resolve, ms)
} else {
setTimeout(resolve, ms)
}
})
}
export function writeText(text: string, to: Browser<void>) {
text.split('').forEach(async (c) => {
await to.keys([c])
await sleep(50)
})
export async function writeText(text: string, browser: Browser<void>, delay = 0) {
for (const c of text.split('')) {
await browser.keys([c])
await sleep(delay)
}
}
export async function delteTextWithBackspaces(element: Element<void>, browser: Browser<void>, delay = 0, count = 0) {
const length = count > 0 ? count : (await element.getValue()).length
for (let i = 0; i < length; i += 1) {
await browser.keys(['Backspace'])
await sleep(delay)
}
}
export async function moveToCenterOfElement(element: Element<void>, browser: Browser<void>) {
@@ -37,22 +49,32 @@ export async function moveToCenterOfElement(element: Element<void>, browser: Bro
const stepY = deltaY / steps
let currentStep = 0
function getCloser() {
e.style.left = String(left + (stepX * currentStep)) + 5 + 'px'
e.style.top = String(top + (stepY * currentStep)) + 5 + 'px'
e.style.left = String(left + (stepX * currentStep)) + 'px'
e.style.top = String(top + (stepY * currentStep)) + 'px'
if (currentStep < steps) {
setTimeout(() => {
currentStep += 1
getCloser()
currentStep += 1
if (currentStep === steps) {
e.style.left = String(targetX + 5) + 'px'
e.style.top = String(targetY + 1) + 'px'
} else {
getCloser()
}
}, duration/steps)
}
}
getCloser()
}`
await browser.execute(js)
await sleep(550)
await sleep(550, true)
await element.moveTo()
}
export async function clickOnHistory(browser: Browser<void>) {
const messageHistory = await browser.$('//span/*[contains(text(), "History")]')
await clickOn(messageHistory, browser)
}
export async function clickOn(element: Element<void>, browser: Browser<void>, clicks = 1) {
await moveToCenterOfElement(element, browser)
for (let i = 0; i < clicks; i += 1) {
@@ -71,25 +93,24 @@ export async function createFakeMousePointer(browser: Browser<void>) {
+ 'document.body.appendChild(i)'
await browser.execute(addCursorImage)
const onMouseMove = `document.onmousemove = (event) => {
const e = document.getElementById('bier')
e.style.left = (event.pageX+1) + 'px'
e.style.top = event.pageY + 'px'
}`
await browser.execute(onMouseMove)
}
export async function showText(text: string, duration: number = 0, browser: Browser<void>) {
export async function showText(text: string, duration: number = 0, browser: Browser<void>, location: 'top' | 'bottom' | 'middle' = 'bottom') {
const positions = {
top: 0,
bottom: -65,
middle: -32,
}
const js = `
const postition = "${positions[location]}vh"
let previousDiv = document.getElementById('tests-text-overlay')
previousDiv && previousDiv.remove()
let div = document.createElement('div')
div.id = "tests-text-overlay"
div.style = "background-color: rgba(0, 0, 0, 0.8);position: fixed;left: 5vw;z-index: 1000000;margin: 30vw auto 50vw;border-radius: 16px;right: 5vw;bottom: -65vh;"
div.style = "background-color: rgba(0, 0, 0, 0.8);position: fixed;left: 5vw;z-index: 1000000;margin: 30vw auto 50vw;border-radius: 16px;right: 5vw;bottom: "+ postition +";"
let div2 = document.createElement('div')
div2.style = "text-align: center;font-size: 4em;color: white;"
div2.innerHTML = "${text}"
div2.innerHTML = \`${text}\`
div.appendChild(div2)
document.body.appendChild(div)
if (${duration} > 0) {

View File

@@ -1,22 +1,28 @@
process.on('unhandledRejection', (error: Error) => {
console.error('unhandledRejection', error.message, error.stack);
console.error('unhandledRejection', error.message, error.stack)
process.exit(1)
});
})
import * as webdriverio from 'webdriverio'
import * as os from 'os'
import mockMqtt, { stop } from './mock-mqtt'
import { connectTo } from './scenarios/connect'
import { disconnect } from './scenarios/disconnect'
import { showNumericPlot } from './scenarios/showNumericPlot'
import { showJsonPreview } from './scenarios/showJsonPreview'
import { compareJsonSideBySide } from './scenarios/compareJsonSideBySide'
import { searchTree, clearSearch } from './scenarios/searchTree'
import { copyTopicToClipboard } from './scenarios/copyTopicToClipboard'
import { copyValueToClipboard } from './scenarios/copyValueToClipboard'
import { publishTopic } from './scenarios/publishTopic'
import { clearOldTopics } from './scenarios/clearOldTopics'
import { showMenu } from './scenarios/showMenu'
import { createFakeMousePointer, sleep, showText, hideText } from './util'
import { createFakeMousePointer, sleep, showText, hideText, clickOnHistory } from './util'
const binary = os.platform() === 'darwin' ? 'Electron.app/Contents/MacOS/Electron' : 'electron'
const fullscreen = os.platform() === 'darwin' ? [] : ['--fullscreen']
const options = {
host: 'localhost', // Use localhost as chrome driver server
port: 9515, // "9515" is the port opened by chrome driver.
@@ -24,7 +30,7 @@ const options = {
browserName: 'electron',
chromeOptions: {
binary: `${__dirname}/../../../node_modules/electron/dist/${binary}`,
args: [`--app=${__dirname}/../../..`, '--force-device-scale-factor=1', '--no-sandbox', '--disable-dev-shm-usage', '--disable-extensions'],
args: [`--app=${__dirname}/../../..`, '--force-device-scale-factor=1', '--no-sandbox', '--disable-dev-shm-usage', '--disable-extensions'].concat(fullscreen),
},
windowTypes: ['app', 'webview'],
},
@@ -35,28 +41,58 @@ async function doStuff() {
const browser = await webdriverio.remote(options)
await createFakeMousePointer(browser)
await connectTo('localhost', browser)
await sleep(2000) // Allow some topics to pour in
await showText('Plotting topics', 0, browser)
await connectTo('127.0.0.1', browser)
await sleep(1000)
await sleep(1000)
await showText('Overview of topics', 2000, browser, 'top')
await sleep(2000)
await showText('Indicates which topics change', 2000, browser, 'bottom')
await sleep(3000)
await showText('Plot topics', 2000, browser)
await showNumericPlot(browser)
await sleep(2000)
await hideText(browser)
await showText('JSON preview', 0, browser)
await showText('Formatted messages', 2000, browser, 'top')
await showJsonPreview(browser)
await sleep(2000)
await showText('Compare messages', 2000, browser, 'top')
await compareJsonSideBySide(browser)
await hideText(browser)
await showText('Copy&Paste data', 2000, browser)
await copyTopicToClipboard(browser)
await showText('Publish topics', 2000, browser, 'top')
await clickOnHistory(browser)
await publishTopic(browser)
await sleep(1000)
await showText('Copy to Clipboard', 2000, browser)
await copyTopicToClipboard(browser)
await hideText(browser)
await copyValueToClipboard(browser)
await sleep(1000)
await showText('Search topic hierarchy', 0, browser, 'middle')
await searchTree('temp', browser)
await hideText(browser)
await showText('Topics containing "temp"', 1500, browser)
await sleep(1500)
await clearSearch(browser)
await sleep(1000)
await hideText(browser)
await showText('Delete retained topics', 0, browser)
await clearOldTopics(browser)
await hideText(browser)
await showText('Settings', 3000, browser)
await showText('Display Options', 2000, browser)
await showMenu(browser)
await sleep(2000)
await disconnect(browser)
await sleep(3000)
browser.closeWindow()
stop()

1815
yarn.lock

File diff suppressed because it is too large Load Diff