Create test-specific mock and rewrite all tests to use expandTopic with per-test data mocking

Co-authored-by: thomasnordquist <7721625+thomasnordquist@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2025-12-20 10:55:14 +00:00
co-authored by thomasnordquist
parent ea9026e8e7
commit 76d74e11cb
2 changed files with 125 additions and 62 deletions
+40
View File
@@ -0,0 +1,40 @@
import * as mqtt from 'mqtt'
/**
* Test-specific MQTT mock (no timers)
*
* This mock connects to the broker but doesn't publish any messages automatically.
* Each test should publish only the messages it needs via the returned client.
*
* This is different from the demo video mock which uses timers.
*/
let mqttClient: mqtt.MqttClient | null = null
export async function createTestMock(): Promise<mqtt.MqttClient> {
if (mqttClient) {
return mqttClient
}
return new Promise(resolve => {
const client = mqtt.connect('mqtt://127.0.0.1:1883', {
username: '',
password: '',
})
client.once('connect', () => {
mqttClient = client
resolve(client)
})
})
}
export function stopTestMock() {
if (mqttClient) {
try {
mqttClient.end()
mqttClient = null
} catch {
// ignore
}
}
}
+85 -62
View File
@@ -1,31 +1,38 @@
import 'mocha'
import { expect } from 'chai'
import { ElectronApplication, Page, _electron as electron } from 'playwright'
import mockMqtt, { stop as stopMqtt } from './mock-mqtt'
import { createTestMock, stopTestMock } from './mock-mqtt-test'
import { default as MockSparkplug } from './mock-sparkplugb'
import { sleep } from './util'
import { connectTo } from './scenarios/connect'
import { searchTree, clearSearch } from './scenarios/searchTree'
import { expandTopic } from './util/expandTopic'
import type { MqttClient } from 'mqtt'
/**
* MQTT Explorer UI Tests - Fully Isolated Test Suite
*
* Each test is completely independent with its own page reload for clean state.
* This ensures no test interference or state carryover.
* Each test:
* 1. Gets fresh page
* 2. Mocks only the MQTT messages it needs (no timers)
* 3. Connects to broker
* 4. Tests functionality using expandTopic
* 5. Reloads page for next test
*
* This ensures complete test isolation with no state carryover.
*/
// tslint:disable:only-arrow-functions ter-prefer-arrow-callback no-unused-expression
describe('MQTT Explorer UI Tests', function () {
this.timeout(60000)
let electronApp: ElectronApplication
let mqttClientStarted = false
let testMock: MqttClient
before(async function () {
this.timeout(30000)
this.timeout(90000)
console.log('Starting MQTT mock broker...')
await mockMqtt()
mqttClientStarted = true
console.log('Creating test-specific MQTT mock (no timers)...')
testMock = await createTestMock()
console.log('Launching Electron application...')
electronApp = await electron.launch({
@@ -41,9 +48,7 @@ describe('MQTT Explorer UI Tests', function () {
await electronApp.close()
}
if (mqttClientStarted) {
stopMqtt()
}
stopTestMock()
})
// Helper function to get a fresh page
@@ -54,64 +59,74 @@ describe('MQTT Explorer UI Tests', function () {
}
describe('Connection Management', () => {
it('should connect to MQTT broker successfully', async function () {
// Given: Fresh page
it('should connect and expand livingroom/lamp topic', async function () {
// Given: Fresh page and mocked topic
const page = await getFreshPage()
testMock.publish('livingroom/lamp/state', 'on', { retain: true, qos: 0 })
await sleep(500) // Let MQTT message propagate
// When: Connect to MQTT broker
// When: Connect and expand topic
await connectTo('127.0.0.1', page)
await sleep(1000)
await sleep(2000)
await expandTopic(page, 'livingroom/lamp')
await MockSparkplug.run()
await sleep(1000)
// Then: Disconnect button should be visible
const disconnectButton = await page.locator('//button/span[contains(text(),"Disconnect")]')
await disconnectButton.waitFor({ state: 'visible', timeout: 5000 })
const isVisible = await disconnectButton.isVisible()
expect(isVisible).to.be.true
// Then: Should see lamp state
const stateTopic = await page.locator('span[data-test-topic="state"]')
await stateTopic.waitFor({ state: 'visible', timeout: 5000 })
expect(await stateTopic.isVisible()).to.be.true
await page.screenshot({ path: 'test-screenshot-connection.png' })
// Clean up: Reload page for next test
// Clean up: Reload page
await page.reload()
})
})
describe('Topic Tree Structure', () => {
it('should display kitchen topic from mock data', async function () {
// Given: Fresh page and connected
it('should expand and display kitchen/coffee_maker with JSON payload', async function () {
// Given: Fresh page and mocked JSON message
const page = await getFreshPage()
const coffeeData = {
heater: 'on',
temperature: 92.5,
waterLevel: 0.5,
}
testMock.publish('kitchen/coffee_maker', JSON.stringify(coffeeData), { retain: true, qos: 2 })
await sleep(500)
// When: Connect and expand topic
await connectTo('127.0.0.1', page)
await sleep(2000)
await expandTopic(page, 'kitchen/coffee_maker')
// Then: Kitchen topic should be visible
const kitchenTopic = await page.locator('span[data-test-topic="kitchen"]')
await kitchenTopic.waitFor({ state: 'visible', timeout: 5000 })
expect(await kitchenTopic.isVisible()).to.be.true
// Then: JSON content should be visible (check for heater key)
const valueDisplay = await page.locator('text="heater"')
await valueDisplay.waitFor({ state: 'visible', timeout: 5000 })
expect(await valueDisplay.isVisible()).to.be.true
await page.screenshot({ path: 'test-screenshot-kitchen.png' })
await page.screenshot({ path: 'test-screenshot-kitchen-json.png' })
// Clean up: Reload page
await page.reload()
})
it('should display root topics from mock data', async function () {
// Given: Fresh page and connected
it('should expand nested topic livingroom/lamp/brightness', async function () {
// Given: Fresh page and nested mocked topic
const page = await getFreshPage()
testMock.publish('livingroom/lamp/brightness', '128', { retain: true, qos: 0 })
await sleep(500)
// When: Connect and expand to nested topic
await connectTo('127.0.0.1', page)
await sleep(2000)
await expandTopic(page, 'livingroom/lamp/brightness')
// Then: All root topics should be visible
const rootTopics = ['livingroom', 'kitchen', 'garden']
for (const topicName of rootTopics) {
const topic = await page.locator(`span[data-test-topic="${topicName}"]`)
await topic.waitFor({ state: 'visible', timeout: 5000 })
const visible = await topic.isVisible()
expect(visible).to.be.true
}
// Then: Brightness topic should be visible and selected
const brightnessTopic = await page.locator('span[data-test-topic="brightness"]')
await brightnessTopic.waitFor({ state: 'visible', timeout: 5000 })
expect(await brightnessTopic.isVisible()).to.be.true
await page.screenshot({ path: 'test-screenshot-root-topics.png' })
await page.screenshot({ path: 'test-screenshot-nested-topic.png' })
// Clean up: Reload page
await page.reload()
@@ -119,47 +134,55 @@ describe('MQTT Explorer UI Tests', function () {
})
describe('Search Functionality', () => {
it('should search and filter topics containing "temp"', async function () {
// Given: Fresh page and connected
it('should search for temperature and expand kitchen/temperature', async function () {
// Given: Fresh page and mocked temperature topics
const page = await getFreshPage()
testMock.publish('kitchen/temperature', '22.5', { retain: true, qos: 0 })
testMock.publish('livingroom/temperature', '21.0', { retain: true, qos: 0 })
await sleep(500)
// When: Connect, search, and expand
await connectTo('127.0.0.1', page)
await sleep(2000)
// When: Search for "temp"
await searchTree('temp', page)
await sleep(1000)
await clearSearch(page)
await sleep(500)
await expandTopic(page, 'kitchen/temperature')
// Then: Search field should contain "temp" and temperature topic should be visible
const searchField = await page.locator('//input[contains(@placeholder, "Search")]')
const searchValue = await searchField.inputValue()
expect(searchValue).to.equal('temp')
const tempTopic = await page.locator('span[data-test-topic="temperature"]').first()
// Then: Temperature topic should be visible
const tempTopic = await page.locator('span[data-test-topic="temperature"]')
await tempTopic.waitFor({ state: 'visible', timeout: 5000 })
expect(await tempTopic.isVisible()).to.be.true
await page.screenshot({ path: 'test-screenshot-search.png' })
await page.screenshot({ path: 'test-screenshot-search-temp.png' })
// Clean up: Reload page
await page.reload()
})
it('should search for specific topic path like "kitchen/lamp"', async function () {
// Given: Fresh page and connected
it('should search for lamp and expand kitchen/lamp', async function () {
// Given: Fresh page and mocked lamp topics
const page = await getFreshPage()
testMock.publish('kitchen/lamp/state', 'off', { retain: true, qos: 0 })
testMock.publish('livingroom/lamp/state', 'on', { retain: true, qos: 0 })
await sleep(500)
// When: Connect, search, and expand
await connectTo('127.0.0.1', page)
await sleep(2000)
// When: Search for "kitchen/lamp"
await searchTree('kitchen/lamp', page)
await sleep(1000)
await clearSearch(page)
await sleep(500)
await expandTopic(page, 'kitchen/lamp')
// Then: Search field should contain "kitchen/lamp"
const searchField = await page.locator('//input[contains(@placeholder, "Search")]')
const searchValue = await searchField.inputValue()
expect(searchValue).to.equal('kitchen/lamp')
// Then: Lamp topic should be visible
const lampTopic = await page.locator('span[data-test-topic="lamp"]')
await lampTopic.waitFor({ state: 'visible', timeout: 5000 })
expect(await lampTopic.isVisible()).to.be.true
await page.screenshot({ path: 'test-screenshot-search-path.png' })
await page.screenshot({ path: 'test-screenshot-search-lamp.png' })
// Clean up: Reload page
await page.reload()