Add UI tests
This commit is contained in:
77
src/spec/mock-mqtt.ts
Normal file
77
src/spec/mock-mqtt.ts
Normal 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
|
||||
15
src/spec/scenarios/clearOldTopics.ts
Normal file
15
src/spec/scenarios/clearOldTopics.ts
Normal 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)
|
||||
}
|
||||
}
|
||||
20
src/spec/scenarios/connect.ts
Normal file
20
src/spec/scenarios/connect.ts
Normal 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)
|
||||
}
|
||||
7
src/spec/scenarios/copyTopicToClipboard.ts
Normal file
7
src/spec/scenarios/copyTopicToClipboard.ts
Normal 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)
|
||||
}
|
||||
7
src/spec/scenarios/copyValueToClipboard.ts
Normal file
7
src/spec/scenarios/copyValueToClipboard.ts
Normal 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)
|
||||
}
|
||||
8
src/spec/scenarios/searchTree.ts
Normal file
8
src/spec/scenarios/searchTree.ts
Normal 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)
|
||||
}
|
||||
8
src/spec/scenarios/showJsonPreview.ts
Normal file
8
src/spec/scenarios/showJsonPreview.ts
Normal 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)
|
||||
}
|
||||
21
src/spec/scenarios/showMenu.ts
Normal file
21
src/spec/scenarios/showMenu.ts
Normal 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)
|
||||
}
|
||||
12
src/spec/scenarios/showNumericPlot.ts
Normal file
12
src/spec/scenarios/showNumericPlot.ts
Normal 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)
|
||||
}
|
||||
29
src/spec/util/expandTopic.ts
Normal file
29
src/spec/util/expandTopic.ts
Normal 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
109
src/spec/util/index.ts
Normal 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
57
src/spec/webdriverio.ts
Normal 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()
|
||||
Reference in New Issue
Block a user