Run browser mode tests in Docker with authentication instead of Electron (#972)
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>
This commit is contained in:
12
.github/workflows/tests.yml
vendored
12
.github/workflows/tests.yml
vendored
@@ -21,7 +21,7 @@ jobs:
|
|||||||
- name: Test
|
- name: Test
|
||||||
run: yarn test
|
run: yarn test
|
||||||
|
|
||||||
electron-tests:
|
browser-ui-tests:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
container:
|
container:
|
||||||
image: ghcr.io/thomasnordquist/mqtt-explorer-ui-tests:latest
|
image: ghcr.io/thomasnordquist/mqtt-explorer-ui-tests:latest
|
||||||
@@ -34,16 +34,16 @@ jobs:
|
|||||||
ref: ${{ github.event.pull_request.head.sha }}
|
ref: ${{ github.event.pull_request.head.sha }}
|
||||||
- name: Install Packages
|
- name: Install Packages
|
||||||
run: yarn install --frozen-lockfile
|
run: yarn install --frozen-lockfile
|
||||||
- name: Build
|
- name: Build Browser Mode
|
||||||
run: yarn build
|
run: yarn build:server
|
||||||
- name: Run Electron UI Tests
|
- name: Run Browser UI Tests
|
||||||
timeout-minutes: 10
|
timeout-minutes: 10
|
||||||
run: ./scripts/runUiTests.sh
|
run: ./scripts/runBrowserTests.sh
|
||||||
- name: Upload Test Screenshots
|
- name: Upload Test Screenshots
|
||||||
if: always()
|
if: always()
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: electron-test-screenshots
|
name: browser-test-screenshots
|
||||||
path: |
|
path: |
|
||||||
test-screenshot-*.png
|
test-screenshot-*.png
|
||||||
retention-days: 30
|
retention-days: 30
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -18,5 +18,6 @@ test-mcp-introspection.js
|
|||||||
/data
|
/data
|
||||||
test-screenshot-*.png
|
test-screenshot-*.png
|
||||||
test-expand-*.png
|
test-expand-*.png
|
||||||
|
browser-debug-screenshot.png
|
||||||
|
|
||||||
app/.webpack-cache
|
app/.webpack-cache
|
||||||
@@ -68,6 +68,9 @@ export function BrowserAuthWrapper(props: BrowserAuthWrapperProps) {
|
|||||||
const errorMessage = event.detail?.message || 'Authentication failed'
|
const errorMessage = event.detail?.message || 'Authentication failed'
|
||||||
console.error('Authentication error:', errorMessage)
|
console.error('Authentication error:', errorMessage)
|
||||||
|
|
||||||
|
// Mark auth check as complete - we now know auth is required
|
||||||
|
setAuthCheckComplete(true)
|
||||||
|
|
||||||
// Clear authentication state
|
// Clear authentication state
|
||||||
setIsAuthenticated(false)
|
setIsAuthenticated(false)
|
||||||
setShowLogin(true)
|
setShowLogin(true)
|
||||||
|
|||||||
72
scripts/runBrowserTests.sh
Executable file
72
scripts/runBrowserTests.sh
Executable file
@@ -0,0 +1,72 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Browser Mode Test Runner
|
||||||
|
#
|
||||||
|
# This script runs UI tests against the browser mode server (instead of Electron).
|
||||||
|
# It starts a local mosquitto MQTT broker and the MQTT Explorer server, then runs
|
||||||
|
# the test suite using Playwright with a headless Chrome browser.
|
||||||
|
#
|
||||||
|
# Environment Variables:
|
||||||
|
# MQTT_EXPLORER_USERNAME - Username for browser authentication (default: test)
|
||||||
|
# MQTT_EXPLORER_PASSWORD - Password for browser authentication (default: test123)
|
||||||
|
# PORT - Server port (default: 3000)
|
||||||
|
# BROWSER_MODE_URL - URL for browser tests (set automatically)
|
||||||
|
# MQTT_BROKER_HOST - MQTT broker host for tests (default: 127.0.0.1)
|
||||||
|
# MQTT_BROKER_PORT - MQTT broker port for tests (default: 1883)
|
||||||
|
#
|
||||||
|
set -e
|
||||||
|
|
||||||
|
function finish {
|
||||||
|
set +e
|
||||||
|
echo "Exiting, cleaning up.."
|
||||||
|
|
||||||
|
if [[ ! -z "$PID_MOSQUITTO" ]]; then
|
||||||
|
echo "Stopping mosquitto ($PID_MOSQUITTO).."
|
||||||
|
kill "$PID_MOSQUITTO" || echo "Already stopped"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! -z "$PID_SERVER" ]]; then
|
||||||
|
echo "Stopping server ($PID_SERVER).."
|
||||||
|
kill "$PID_SERVER" || echo "Already stopped"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
trap finish EXIT
|
||||||
|
|
||||||
|
# Start mqtt broker
|
||||||
|
mosquitto &
|
||||||
|
export PID_MOSQUITTO=$!
|
||||||
|
sleep 1
|
||||||
|
|
||||||
|
# Set credentials for browser authentication (tests will use these to login)
|
||||||
|
export MQTT_EXPLORER_USERNAME=${MQTT_EXPLORER_USERNAME:-test}
|
||||||
|
export MQTT_EXPLORER_PASSWORD=${MQTT_EXPLORER_PASSWORD:-test123}
|
||||||
|
export PORT=${PORT:-3000}
|
||||||
|
|
||||||
|
# Start the browser mode server
|
||||||
|
node dist/src/server.js &
|
||||||
|
export PID_SERVER=$!
|
||||||
|
|
||||||
|
# Wait for server to be ready (max 60 seconds)
|
||||||
|
echo "Waiting for server to start..."
|
||||||
|
for i in {1..60}; do
|
||||||
|
if curl -f --connect-timeout 5 --max-time 10 http://localhost:${PORT} > /dev/null 2>&1; then
|
||||||
|
echo "Server started successfully after $i seconds"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
if [ $i -eq 60 ]; then
|
||||||
|
echo "Server failed to start within 60 seconds"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
|
||||||
|
# Run browser tests
|
||||||
|
export BROWSER_MODE_URL="http://localhost:${PORT}"
|
||||||
|
export MQTT_BROKER_HOST="127.0.0.1"
|
||||||
|
export MQTT_BROKER_PORT="1883"
|
||||||
|
|
||||||
|
yarn test:browser
|
||||||
|
TEST_EXIT_CODE=$?
|
||||||
|
|
||||||
|
echo "Browser tests exited with $TEST_EXIT_CODE"
|
||||||
|
exit $TEST_EXIT_CODE
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import 'mocha'
|
import 'mocha'
|
||||||
import { expect } from 'chai'
|
import { expect } from 'chai'
|
||||||
import { ElectronApplication, Page, _electron as electron } from 'playwright'
|
import { Browser, BrowserContext, ElectronApplication, Page, _electron as electron, chromium } from 'playwright'
|
||||||
import { createTestMock, stopTestMock } from './mock-mqtt-test'
|
import { createTestMock, stopTestMock } from './mock-mqtt-test'
|
||||||
import { default as MockSparkplug } from './mock-sparkplugb'
|
import { default as MockSparkplug } from './mock-sparkplugb'
|
||||||
import { sleep } from './util'
|
import { sleep } from './util'
|
||||||
@@ -15,14 +15,21 @@ import type { MqttClient } from 'mqtt'
|
|||||||
* Tests the core UI functionality using a single connection.
|
* Tests the core UI functionality using a single connection.
|
||||||
* All topics are published before connecting, and tests run sequentially
|
* All topics are published before connecting, and tests run sequentially
|
||||||
* on the same connected application instance.
|
* 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
|
// tslint:disable:only-arrow-functions ter-prefer-arrow-callback no-unused-expression
|
||||||
describe('MQTT Explorer UI Tests', function () {
|
describe('MQTT Explorer UI Tests', function () {
|
||||||
this.timeout(60000)
|
this.timeout(60000)
|
||||||
|
|
||||||
let electronApp: ElectronApplication
|
let electronApp: ElectronApplication | undefined
|
||||||
|
let browser: Browser | undefined
|
||||||
|
let browserContext: BrowserContext | undefined
|
||||||
let testMock: MqttClient
|
let testMock: MqttClient
|
||||||
let page: Page
|
let page: Page
|
||||||
|
const isBrowserMode = !!process.env.BROWSER_MODE_URL
|
||||||
|
|
||||||
before(async function () {
|
before(async function () {
|
||||||
this.timeout(90000)
|
this.timeout(90000)
|
||||||
@@ -47,18 +54,88 @@ describe('MQTT Explorer UI Tests', function () {
|
|||||||
|
|
||||||
await sleep(2000) // Let MQTT messages propagate and get retained
|
await sleep(2000) // Let MQTT messages propagate and get retained
|
||||||
|
|
||||||
console.log('Launching Electron application...')
|
if (isBrowserMode) {
|
||||||
electronApp = await electron.launch({
|
console.log('Launching browser in browser mode...')
|
||||||
args: [`${__dirname}/../../..`, '--runningUiTestOnCi', '--no-sandbox', '--disable-dev-shm-usage'],
|
const browserUrl = process.env.BROWSER_MODE_URL
|
||||||
timeout: 60000,
|
if (!browserUrl) {
|
||||||
})
|
throw new Error('BROWSER_MODE_URL environment variable must be set when running in browser mode')
|
||||||
|
}
|
||||||
|
console.log(`Browser URL: ${browserUrl}`)
|
||||||
|
|
||||||
console.log('Getting application window...')
|
// Launch Chromium browser
|
||||||
page = await electronApp.firstWindow({ timeout: 30000 })
|
browser = await chromium.launch({
|
||||||
await page.locator('//label[contains(text(), "Host")]/..//input').waitFor({ timeout: 10000 })
|
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...')
|
console.log('Connecting to MQTT broker...')
|
||||||
await connectTo('127.0.0.1', page)
|
const brokerHost = process.env.MQTT_BROKER_HOST || '127.0.0.1'
|
||||||
|
await connectTo(brokerHost, page)
|
||||||
await sleep(3000) // Give time for topics to load
|
await sleep(3000) // Give time for topics to load
|
||||||
console.log('Setup complete')
|
console.log('Setup complete')
|
||||||
})
|
})
|
||||||
@@ -66,8 +143,17 @@ describe('MQTT Explorer UI Tests', function () {
|
|||||||
after(async function () {
|
after(async function () {
|
||||||
this.timeout(10000)
|
this.timeout(10000)
|
||||||
|
|
||||||
if (electronApp) {
|
if (isBrowserMode) {
|
||||||
await electronApp.close()
|
if (browserContext) {
|
||||||
|
await browserContext.close()
|
||||||
|
}
|
||||||
|
if (browser) {
|
||||||
|
await browser.close()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (electronApp) {
|
||||||
|
await electronApp.close()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
stopTestMock()
|
stopTestMock()
|
||||||
|
|||||||
Reference in New Issue
Block a user