diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index b895c83..41f58d1 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -21,7 +21,7 @@ jobs: - name: Test run: yarn test - electron-tests: + browser-ui-tests: runs-on: ubuntu-latest container: image: ghcr.io/thomasnordquist/mqtt-explorer-ui-tests:latest @@ -34,16 +34,16 @@ jobs: ref: ${{ github.event.pull_request.head.sha }} - name: Install Packages run: yarn install --frozen-lockfile - - name: Build - run: yarn build - - name: Run Electron UI Tests + - name: Build Browser Mode + run: yarn build:server + - name: Run Browser UI Tests timeout-minutes: 10 - run: ./scripts/runUiTests.sh + run: ./scripts/runBrowserTests.sh - name: Upload Test Screenshots if: always() uses: actions/upload-artifact@v4 with: - name: electron-test-screenshots + name: browser-test-screenshots path: | test-screenshot-*.png retention-days: 30 diff --git a/.gitignore b/.gitignore index efb8c6d..cc81c7d 100644 --- a/.gitignore +++ b/.gitignore @@ -18,5 +18,6 @@ test-mcp-introspection.js /data test-screenshot-*.png test-expand-*.png +browser-debug-screenshot.png app/.webpack-cache \ No newline at end of file diff --git a/app/src/components/BrowserAuthWrapper.tsx b/app/src/components/BrowserAuthWrapper.tsx index d39fb1d..7f4b6f6 100644 --- a/app/src/components/BrowserAuthWrapper.tsx +++ b/app/src/components/BrowserAuthWrapper.tsx @@ -68,6 +68,9 @@ export function BrowserAuthWrapper(props: BrowserAuthWrapperProps) { const errorMessage = event.detail?.message || 'Authentication failed' console.error('Authentication error:', errorMessage) + // Mark auth check as complete - we now know auth is required + setAuthCheckComplete(true) + // Clear authentication state setIsAuthenticated(false) setShowLogin(true) diff --git a/scripts/runBrowserTests.sh b/scripts/runBrowserTests.sh new file mode 100755 index 0000000..d6b0b59 --- /dev/null +++ b/scripts/runBrowserTests.sh @@ -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 diff --git a/src/spec/ui-tests.spec.ts b/src/spec/ui-tests.spec.ts index e103b88..48e1b4a 100644 --- a/src/spec/ui-tests.spec.ts +++ b/src/spec/ui-tests.spec.ts @@ -1,6 +1,6 @@ import 'mocha' 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 { default as MockSparkplug } from './mock-sparkplugb' import { sleep } from './util' @@ -15,14 +15,21 @@ import type { MqttClient } from 'mqtt' * 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 + 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) @@ -47,18 +54,88 @@ describe('MQTT Explorer UI Tests', function () { await sleep(2000) // Let MQTT messages propagate and get retained - console.log('Launching Electron application...') - electronApp = await electron.launch({ - args: [`${__dirname}/../../..`, '--runningUiTestOnCi', '--no-sandbox', '--disable-dev-shm-usage'], - timeout: 60000, - }) + 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}`) - console.log('Getting application window...') - page = await electronApp.firstWindow({ timeout: 30000 }) - await page.locator('//label[contains(text(), "Host")]/..//input').waitFor({ timeout: 10000 }) + // 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...') - 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 console.log('Setup complete') }) @@ -66,8 +143,17 @@ describe('MQTT Explorer UI Tests', function () { after(async function () { this.timeout(10000) - if (electronApp) { - await electronApp.close() + if (isBrowserMode) { + if (browserContext) { + await browserContext.close() + } + if (browser) { + await browser.close() + } + } else { + if (electronApp) { + await electronApp.close() + } } stopTestMock()