Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: thomasnordquist <7721625+thomasnordquist@users.noreply.github.com> Co-authored-by: Thomas Nordquist <thomasnordquist@users.noreply.github.com>
241 lines
9.2 KiB
TypeScript
241 lines
9.2 KiB
TypeScript
import 'mocha'
|
|
import { expect } from 'chai'
|
|
import { Browser, BrowserContext, ElectronApplication, Page, _electron as electron, chromium } from 'playwright'
|
|
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
|
|
*
|
|
* Tests the core UI functionality using a single connection.
|
|
* All topics are published before connecting, and tests run sequentially
|
|
* on the same connected application instance.
|
|
*
|
|
* Supports both Electron and Browser modes:
|
|
* - Electron mode: Default behavior, launches Electron app
|
|
* - Browser mode: Set BROWSER_MODE_URL environment variable to the server URL
|
|
*/
|
|
// tslint:disable:only-arrow-functions ter-prefer-arrow-callback no-unused-expression
|
|
describe('MQTT Explorer UI Tests', function () {
|
|
this.timeout(60000)
|
|
|
|
let electronApp: ElectronApplication | undefined
|
|
let browser: Browser | undefined
|
|
let browserContext: BrowserContext | undefined
|
|
let testMock: MqttClient
|
|
let page: Page
|
|
const isBrowserMode = !!process.env.BROWSER_MODE_URL
|
|
|
|
before(async function () {
|
|
this.timeout(90000)
|
|
|
|
console.log('Creating test-specific MQTT mock (no timers)...')
|
|
testMock = await createTestMock()
|
|
|
|
console.log('Publishing test topics...')
|
|
// Publish all test topics before connecting
|
|
testMock.publish('livingroom/lamp/state', 'on', { retain: true, qos: 0 })
|
|
testMock.publish('livingroom/lamp/brightness', '128', { retain: true, qos: 0 })
|
|
testMock.publish('livingroom/temperature', '21.0', { retain: true, qos: 0 })
|
|
|
|
const coffeeData = {
|
|
heater: 'on',
|
|
temperature: 92.5,
|
|
waterLevel: 0.5,
|
|
}
|
|
testMock.publish('kitchen/coffee_maker', JSON.stringify(coffeeData), { retain: true, qos: 2 })
|
|
testMock.publish('kitchen/lamp/state', 'off', { retain: true, qos: 0 })
|
|
testMock.publish('kitchen/temperature', '22.5', { retain: true, qos: 0 })
|
|
|
|
await sleep(2000) // Let MQTT messages propagate and get retained
|
|
|
|
if (isBrowserMode) {
|
|
console.log('Launching browser in browser mode...')
|
|
const browserUrl = process.env.BROWSER_MODE_URL
|
|
if (!browserUrl) {
|
|
throw new Error('BROWSER_MODE_URL environment variable must be set when running in browser mode')
|
|
}
|
|
console.log(`Browser URL: ${browserUrl}`)
|
|
|
|
// Launch Chromium browser
|
|
browser = await chromium.launch({
|
|
headless: true,
|
|
args: ['--no-sandbox', '--disable-dev-shm-usage'],
|
|
})
|
|
|
|
browserContext = await browser.newContext()
|
|
page = await browserContext.newPage()
|
|
|
|
// Listen for console messages
|
|
page.on('console', msg => console.log('Browser console:', msg.type(), msg.text()))
|
|
page.on('pageerror', error => console.error('Browser error:', error))
|
|
|
|
// Navigate to the browser mode URL
|
|
await page.goto(browserUrl, { timeout: 30000, waitUntil: 'networkidle' })
|
|
|
|
// Handle authentication if required
|
|
const username = process.env.MQTT_EXPLORER_USERNAME || 'test'
|
|
const password = process.env.MQTT_EXPLORER_PASSWORD || 'test123'
|
|
|
|
console.log('Waiting for page to initialize and auth check...')
|
|
await sleep(5000) // Wait longer for WebSocket connection attempt and auth error handling
|
|
|
|
console.log('Checking for login dialog...')
|
|
const loginDialog = page.locator('h2:has-text("Login to MQTT Explorer")')
|
|
let loginDialogVisible = false
|
|
try {
|
|
loginDialogVisible = await loginDialog.isVisible({ timeout: 10000 })
|
|
} catch (error) {
|
|
// Timeout is expected if dialog is not shown, not an error
|
|
console.log('Login dialog not found (timeout) - checking if auth is disabled')
|
|
}
|
|
|
|
// Debug: print page content to see what's rendered
|
|
if (!loginDialogVisible) {
|
|
const body = await page.locator('body').textContent().catch(() => 'Unable to read body')
|
|
console.log('Page body text:', body?.substring(0, 300))
|
|
}
|
|
|
|
if (loginDialogVisible) {
|
|
console.log('Login dialog detected, authenticating...')
|
|
await page.fill('[data-testid="username-input"] input', username)
|
|
await page.fill('[data-testid="password-input"] input', password)
|
|
await page.click('button:has-text("Login")')
|
|
await sleep(3000) // Wait for authentication to complete and reconnect
|
|
console.log('Authentication complete')
|
|
} else {
|
|
console.log('No login dialog detected - assuming auth is disabled')
|
|
}
|
|
|
|
// Wait for the connection dialog to appear
|
|
console.log('Waiting for MQTT connection dialog...')
|
|
try {
|
|
await page.locator('//label[contains(text(), "Host")]/..//input').waitFor({ timeout: 10000 })
|
|
} catch (error) {
|
|
console.log('Failed to find connection dialog, taking screenshot for debugging')
|
|
await page.screenshot({ path: 'browser-debug-screenshot.png', fullPage: true })
|
|
throw error
|
|
}
|
|
} else {
|
|
console.log('Launching Electron application...')
|
|
electronApp = await electron.launch({
|
|
args: [`${__dirname}/../../..`, '--runningUiTestOnCi', '--no-sandbox', '--disable-dev-shm-usage'],
|
|
timeout: 60000,
|
|
})
|
|
|
|
console.log('Getting application window...')
|
|
page = await electronApp.firstWindow({ timeout: 30000 })
|
|
await page.locator('//label[contains(text(), "Host")]/..//input').waitFor({ timeout: 10000 })
|
|
}
|
|
|
|
console.log('Connecting to MQTT broker...')
|
|
const brokerHost = process.env.MQTT_BROKER_HOST || '127.0.0.1'
|
|
await connectTo(brokerHost, page)
|
|
await sleep(3000) // Give time for topics to load
|
|
console.log('Setup complete')
|
|
})
|
|
|
|
after(async function () {
|
|
this.timeout(10000)
|
|
|
|
if (isBrowserMode) {
|
|
if (browserContext) {
|
|
await browserContext.close()
|
|
}
|
|
if (browser) {
|
|
await browser.close()
|
|
}
|
|
} else {
|
|
if (electronApp) {
|
|
await electronApp.close()
|
|
}
|
|
}
|
|
|
|
stopTestMock()
|
|
})
|
|
|
|
describe('Connection Management', () => {
|
|
it('should connect and expand livingroom/lamp topic', async function () {
|
|
// Given: Connected to broker with topics loaded
|
|
// When: Expand topic
|
|
await expandTopic('livingroom/lamp', page)
|
|
|
|
// Then: Should see lamp state topic
|
|
const stateTopic = page.locator('span[data-test-topic="state"]').first()
|
|
await stateTopic.waitFor({ state: 'visible', timeout: 5000 })
|
|
expect(await stateTopic.isVisible()).to.be.true
|
|
|
|
await page.screenshot({ path: 'test-screenshot-connection.png' })
|
|
})
|
|
})
|
|
|
|
describe('Topic Tree Structure', () => {
|
|
it('should expand and display kitchen/coffee_maker with JSON payload', async function () {
|
|
// Given: Connected to broker with kitchen/coffee_maker topic
|
|
// When: Expand topic
|
|
await expandTopic('kitchen/coffee_maker', page)
|
|
|
|
// Then: The topic should be visible and selected
|
|
const coffeeMakerTopic = page.locator('span[data-test-topic="coffee_maker"]').first()
|
|
await coffeeMakerTopic.waitFor({ state: 'visible', timeout: 5000 })
|
|
expect(await coffeeMakerTopic.isVisible()).to.be.true
|
|
|
|
await page.screenshot({ path: 'test-screenshot-kitchen-json.png' })
|
|
})
|
|
|
|
it('should expand nested topic livingroom/lamp/state', async function () {
|
|
// Given: Connected to broker with nested topics
|
|
// When: Expand to nested topic
|
|
await expandTopic('livingroom/lamp/state', page)
|
|
|
|
// Then: State topic should be visible and selected
|
|
const stateTopic = page.locator('span[data-test-topic="state"]').first()
|
|
await stateTopic.waitFor({ state: 'visible', timeout: 5000 })
|
|
expect(await stateTopic.isVisible()).to.be.true
|
|
|
|
await page.screenshot({ path: 'test-screenshot-nested-topic.png' })
|
|
})
|
|
})
|
|
|
|
describe('Search Functionality', () => {
|
|
it('should search for temperature and expand kitchen/temperature', async function () {
|
|
// Given: Connected to broker with temperature topics
|
|
// When: Search and expand
|
|
await searchTree('temp', page)
|
|
await sleep(1000)
|
|
await clearSearch(page)
|
|
await sleep(500)
|
|
await expandTopic('kitchen/temperature', page)
|
|
|
|
// Then: Temperature topic should be visible
|
|
const tempTopic = page.locator('span[data-test-topic="temperature"]').first()
|
|
await tempTopic.waitFor({ state: 'visible', timeout: 5000 })
|
|
expect(await tempTopic.isVisible()).to.be.true
|
|
|
|
await page.screenshot({ path: 'test-screenshot-search-temp.png' })
|
|
})
|
|
|
|
it('should search for lamp and expand kitchen/lamp', async function () {
|
|
// Given: Connected to broker with lamp topics
|
|
// When: Search and expand
|
|
await searchTree('kitchen/lamp', page)
|
|
await sleep(1000)
|
|
await clearSearch(page)
|
|
await sleep(500)
|
|
await expandTopic('kitchen/lamp', page)
|
|
|
|
// Then: Lamp topic should be visible
|
|
const lampTopic = page.locator('span[data-test-topic="lamp"]').first()
|
|
await lampTopic.waitFor({ state: 'visible', timeout: 5000 })
|
|
expect(await lampTopic.isVisible()).to.be.true
|
|
|
|
await page.screenshot({ path: 'test-screenshot-search-lamp.png' })
|
|
})
|
|
})
|
|
})
|