Add UI tests

This commit is contained in:
Thomas Nordquist
2019-01-28 22:40:52 +01:00
parent 2822f98103
commit d11337fda2
17 changed files with 2835 additions and 63 deletions

BIN
app/cursor.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

View File

@@ -64,7 +64,7 @@ class MessageHistory extends React.Component<Props, State> {
badgeContent={this.props.items.length} badgeContent={this.props.items.length}
color="primary" color="primary"
> >
{this.state.collapsed ? '▶' : '▼'} History {this.state.collapsed ? '▶ History' : '▼ History'}
</Badge> </Badge>
<div style={{ float: 'right' }}>{this.state.collapsed ? this.props.contentTypeIndicator : null}</div> <div style={{ float: 'right' }}>{this.state.collapsed ? this.props.contentTypeIndicator : null}</div>
</Typography> </Typography>

View File

@@ -53,16 +53,19 @@
"electron": "^4.0.2", "electron": "^4.0.2",
"electron-builder": "^20.38.4", "electron-builder": "^20.38.4",
"mocha": "^5.2.0", "mocha": "^5.2.0",
"mosca": "^2.8.3",
"mustache": "^3.0.1", "mustache": "^3.0.1",
"nyc": "^13.1.0", "nyc": "^13.1.0",
"redux-thunk": "^2.3.0", "redux-thunk": "^2.3.0",
"source-map-support": "^0.5.9", "source-map-support": "^0.5.9",
"spectron": "^5.0.0",
"ts-node": "^7.0.1", "ts-node": "^7.0.1",
"tslint": "^5.12.0", "tslint": "^5.12.0",
"tslint-config-airbnb": "^5.11.1", "tslint-config-airbnb": "^5.11.1",
"tslint-react": "^3.6.0", "tslint-react": "^3.6.0",
"tslint-strict-null-checks": "^1.0.1", "tslint-strict-null-checks": "^1.0.1",
"typescript": "^3.2.2" "typescript": "^3.2.2",
"webdriverio": "5.4"
}, },
"dependencies": { "dependencies": {
"electron-debug": "^2.0.0", "electron-debug": "^2.0.0",

77
src/spec/mock-mqtt.ts Normal file
View File

@@ -0,0 +1,77 @@
import * as mqtt from 'mqtt'
const settings = {
port: 1883,
}
let client: mqtt.MqttClient
function startServer(): Promise<mqtt.MqttClient> {
return new Promise(async (resolve) => {
// const server = new mosca.Server(settings)
// await new Promise(resolve => server.once('ready', resolve))
client = await connectMqtt()
generateData(client)
resolve(client)
})
}
function connectMqtt(): Promise<mqtt.MqttClient> {
return new Promise((resolve) => {
const client = mqtt.connect('mqtt://localhost:1883', { username: 'thomas', password: 'bierbier' })
client.once('connect', () => {
resolve(client)
})
})
}
function temperature(base = 18, sineCoefficient = 2, offset = 0) {
const temp = base + Math.sin(Date.now() / 1000 / 5 + offset) * sineCoefficient + Math.random()
return String(Math.round(temp * 100) / 100)
}
export function stop() {
for (const interval of intervals) {
clearInterval(interval)
}
try {
client && client.end()
} catch {}
}
const intervals: any = []
function generateData(client: mqtt.MqttClient) {
client.publish('livingroom/lamp/state', 'on', { retain: true, qos: 0 })
client.publish('livingroom/lamp/brightness', '128', { retain: true, qos: 0 })
client.publish('livingroom/thermostat/targetTemperature', '20°C', { retain: true, qos: 0 })
intervals.push(setInterval(() => client.publish('livingroom/temperature', temperature()), 1000))
intervals.push(setInterval(() => client.publish('livingroom/humidity', temperature(60, -2, 0)), 1000))
client.publish('livingroom/lamp-1/state', 'on', { retain: true, qos: 0 })
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 })
intervals.push(setInterval(() => client.publish('kitchen/temperature', temperature()), 1500))
intervals.push(setInterval(() => client.publish('kitchen/humidity', temperature(60, -5, 0)), 1800))
client.publish('garden/pump/state', 'off', { retain: true, qos: 0 })
client.publish('garden/water/level', '70%', { retain: true, qos: 0 })
client.publish('garden/lamps/state', 'off', { retain: true, qos: 0 })
client.publish('garden/lamps/state', 'off', { retain: true, qos: 0 })
client.publish('zigbee2mqtt/bridge/state', 'online', { retain: true, qos: 0 })
client.publish('ble2mqtt/bridge/state', 'online', { retain: true, qos: 0 })
// Used for demonstrating "clean up"
client.publish('test 123', 'Hello world', { retain: true, qos: 0 })
client.publish('hello', 'sunshine', { retain: true, qos: 0 })
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))
}
export default startServer

View File

@@ -0,0 +1,15 @@
import { clickOn, sleep, writeText, expandTopic, moveToCenterOfElement } from '../util'
import { Browser } from 'webdriverio'
export async function clearOldTopics(browser: Browser<void>) {
const topics = ['hello', 'test 123']
for (const topic of topics) {
await expandTopic(topic, browser)
await sleep(1000)
const deleteButton = await browser.$('//button[contains(@title, "Delete retained topic")]')
await moveToCenterOfElement(deleteButton, browser)
await clickOn(deleteButton, browser)
await sleep(700)
}
}

View File

@@ -0,0 +1,20 @@
import { clickOn, sleep, writeText } from '../util'
import { Browser } from 'webdriverio'
export async function connectTo(host: string, browser: Browser<void>) {
await writeTextToInput('Host', host, browser)
await writeTextToInput('Username', 'thomas', browser, false)
await writeTextToInput('Password', 'bierbier', browser, false)
const connectButton = await browser.$('//button/span[contains(text(),"Connect")]')
clickOn(connectButton, browser)
}
async function writeTextToInput(name: string, text: string, browser: Browser<void>, wait: boolean = true) {
const input = await browser.$(`//label[contains(text(), "${name}")]/..//input`)
await clickOn(input, browser, 1)
wait && await sleep(500)
input.clearValue()
wait && await sleep(300)
await writeText(text, browser)
}

View File

@@ -0,0 +1,7 @@
import { clickOn, sleep, writeText, expandTopic } from '../util'
import { Browser } from 'webdriverio'
export async function copyTopicToClipboard(browser: Browser<void>) {
const copyButton = await browser.$('//p[contains(text(), "Topic")]/span')
await clickOn(copyButton, browser, 1)
}

View File

@@ -0,0 +1,7 @@
import { clickOn, sleep, writeText, expandTopic } from '../util'
import { Browser } from 'webdriverio'
export async function copyValueToClipboard(browser: Browser<void>) {
const copyButton = await browser.$('//p[contains(text(), "Value")]/span')
await clickOn(copyButton, browser, 1)
}

View File

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

View File

@@ -0,0 +1,8 @@
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 sleep(1000)
}

View File

@@ -0,0 +1,21 @@
import { clickOn, sleep, writeText, expandTopic, moveToCenterOfElement } from '../util'
import { Browser } from 'webdriverio'
export async function showMenu(browser: Browser<void>) {
const menuButton = await browser.$('//button[contains(@aria-label, "Menu")]')
await clickOn(menuButton, browser)
const brokerStatistics = await browser.$('//div[contains(@class, "BrokerStatistics")]/div[4]')
moveToCenterOfElement(brokerStatistics, browser)
await sleep(2000)
const topicOrder = await browser.$('#select-node-order')
await clickOn(topicOrder, browser)
await sleep(1000)
const alphabetically = await browser.$('//li[contains(@data-value, "abc")]')
await clickOn(alphabetically, browser)
await sleep(2000)
await clickOn(menuButton, browser)
}

View File

@@ -0,0 +1,12 @@
import { clickOn, sleep, writeText, expandTopic } 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 sleep(1000)
await expandTopic('livingroom/humidity', browser)
}

View File

@@ -0,0 +1,29 @@
import { Browser } from 'webdriverio'
import { clickOn } from './'
export async function expandTopic(path: string, browser: Browser<void>) {
const originalTopics = path.split('/')
let topics = path.split('/')
while (topics.length > 0 && !await topicMatches(topics, browser)) {
topics = topics.slice(0, topics.length - 1)
}
if (topics.length === 0) {
throw Error('could not expand topics, no match found')
}
while (topics.length <= originalTopics.length) {
const match = await browser.$(topicSelector(topics))
await clickOn(match, browser)
topics.push(originalTopics[topics.length])
}
}
async function topicMatches(topics: string[], browser: Browser<void>) {
const result = await browser.$(topicSelector(topics))
return result.isExisting()
}
function topicSelector(topics: string[]) {
const suffix = topics.map(topic => `*[contains(text(), "${topic}")]`).join('/../../..//')
return `//${suffix}`
}

109
src/spec/util/index.ts Normal file
View File

@@ -0,0 +1,109 @@
import { Element, Browser } from 'webdriverio'
export { expandTopic } from './expandTopic'
export function sleep(ms: number) {
return new Promise((resolve) => {
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 moveToCenterOfElement(element: Element<void>, browser: Browser<void>) {
const { x, y } = await element.getLocation()
const { width, height } = await element.getSize()
const js = `{
const targetX = ${x + width / 2}
const targetY = ${y + height / 2}
const duration = 500
const maxStepSize = 10
const e = document.getElementById("bier")
const top = parseFloat(e.style.top)
const left = parseFloat(e.style.left)
const deltaY = targetY - top
const deltaX = targetX - left
const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY)
const steps = Math.ceil(distance / maxStepSize)
const stepX = deltaX / steps
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'
if (currentStep < steps) {
setTimeout(() => {
currentStep += 1
getCloser()
}, duration/steps)
}
}
getCloser()
}`
await browser.execute(js)
await sleep(550)
await element.moveTo()
}
export async function clickOn(element: Element<void>, browser: Browser<void>, clicks = 1) {
await moveToCenterOfElement(element, browser)
for (let i = 0; i < clicks; i += 1) {
await element.click()
await sleep(50)
}
}
export async function createFakeMousePointer(browser: Browser<void>) {
const addCursorImage = 'const i=document.createElement("img");'
+ 'i.src="../cursor.png";'
+ 'i.width="32";'
+ 'i.height="32";'
+ 'i.id="bier";'
+ 'i.style="position: fixed; z-index:10000000; filter: invert(100%);left: 0px; top: 0px;";'
+ '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>) {
const js = `
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;"
let div2 = document.createElement('div')
div2.style = "text-align: center;font-size: 4em;color: white;"
div2.innerHTML = "${text}"
div.appendChild(div2)
document.body.appendChild(div)
if (${duration} > 0) {
setTimeout(() => div.remove(), ${duration})
}
`
browser.execute(js)
}
export async function hideText(browser: Browser<void>) {
const js = `
let previousDiv = document.getElementById('tests-text-overlay')
previousDiv && previousDiv.remove()
`
browser.execute(js)
await sleep(600)
}

57
src/spec/webdriverio.ts Normal file
View File

@@ -0,0 +1,57 @@
import * as webdriverio from 'webdriverio'
import mockMqtt, { stop } from './mock-mqtt'
import { connectTo } from './scenarios/connect'
import { showNumericPlot } from './scenarios/showNumericPlot'
import { showJsonPreview } from './scenarios/showJsonPreview'
import { copyTopicToClipboard } from './scenarios/copyTopicToClipboard'
import { copyValueToClipboard } from './scenarios/copyValueToClipboard'
import { clearOldTopics } from './scenarios/clearOldTopics'
import { showMenu } from './scenarios/showMenu'
import { createFakeMousePointer, sleep, showText, hideText } from './util'
const options = {
host: 'localhost', // Use localhost as chrome driver server
port: 9515, // "9515" is the port opened by chrome driver.
capabilities: {
browserName: 'electron',
chromeOptions: {
binary: `${__dirname}/../../../node_modules/electron/dist/Electron.app/Contents/MacOS/Electron`,
args: [`--app=${__dirname}/../../..`, '--force-device-scale-factor=1', '--no-sandbox', '--disable-dev-shm-usage', '--disable-extensions'],
},
windowTypes: ['app', 'webview'],
},
}
async function doStuff() {
await mockMqtt()
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 showNumericPlot(browser)
await sleep(2000)
await hideText(browser)
await showText('JSON preview', 0, browser)
await showJsonPreview(browser)
await sleep(2000)
await hideText(browser)
await showText('Copy&Paste data', 2000, browser)
await copyTopicToClipboard(browser)
await sleep(1000)
await hideText(browser)
await copyValueToClipboard(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 showMenu(browser)
browser.closeWindow()
stop()
}
doStuff()

View File

@@ -11,5 +11,5 @@
"lib": ["es2017", "dom"], "lib": ["es2017", "dom"],
"sourceMap": true "sourceMap": true
}, },
"include": ["src/electron.ts"] "include": ["src/electron.ts", "src/spec/electron.ts", "src/spec/webdriverio.ts"]
} }

2519
yarn.lock

File diff suppressed because it is too large Load Diff