Add mobile compatibility concept, Pixel 6 demo video infrastructure, and CI/CD workflow (#1006)

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: thomasnordquist <7721625+thomasnordquist@users.noreply.github.com>
This commit is contained in:
Copilot
2025-12-24 18:02:17 +01:00
committed by GitHub
parent a3de71d939
commit 1453934e29
13 changed files with 868 additions and 2 deletions

View File

@@ -22,6 +22,16 @@ export type SceneNames =
| 'keyboard_shortcuts'
| 'sparkplugb-decoding'
| 'end'
| 'mobile_intro'
| 'mobile_connect'
| 'mobile_browse_topics'
| 'mobile_search'
| 'mobile_view_message'
| 'mobile_json_view'
| 'mobile_clipboard'
| 'mobile_plots'
| 'mobile_menu'
| 'mobile_end'
export const SCENE_TITLES: Record<SceneNames, string> = {
connect: 'Connecting to MQTT Broker',
@@ -39,6 +49,16 @@ export const SCENE_TITLES: Record<SceneNames, string> = {
keyboard_shortcuts: 'Keyboard Shortcuts',
'sparkplugb-decoding': 'SparkplugB Decoding',
end: 'The End',
mobile_intro: 'MQTT Explorer on Mobile',
mobile_connect: 'Connect to MQTT Broker',
mobile_browse_topics: 'Browse Topic Tree',
mobile_search: 'Search Topics',
mobile_view_message: 'View Message Details',
mobile_json_view: 'JSON Message Formatting',
mobile_clipboard: 'Copy to Clipboard',
mobile_plots: 'View Numeric Plots',
mobile_menu: 'Settings & Menu',
mobile_end: 'Mobile-Friendly MQTT Explorer',
}
export class SceneBuilder {

230
src/spec/demoVideoMobile.ts Normal file
View File

@@ -0,0 +1,230 @@
import * as fs from 'fs'
import * as os from 'os'
import * as path from 'path'
import { Browser, BrowserContext, Page, chromium } from 'playwright'
import mockMqtt, { stop as stopMqtt } from './mock-mqtt'
import { default as MockSparkplug } from './mock-sparkplugb'
import { clearSearch, searchTree } from './scenarios/searchTree'
import { clickOnHistory, createFakeMousePointer, hideText, showText, sleep } from './util'
import { connectTo } from './scenarios/connect'
import { copyTopicToClipboard } from './scenarios/copyTopicToClipboard'
import { copyValueToClipboard } from './scenarios/copyValueToClipboard'
import { disconnect } from './scenarios/disconnect'
import { publishTopic } from './scenarios/publishTopic'
import { Scene, SceneBuilder } from './SceneBuilder'
import { showAdvancedConnectionSettings } from './scenarios/showAdvancedConnectionSettings'
import { showJsonPreview } from './scenarios/showJsonPreview'
import { showMenu } from './scenarios/showMenu'
import { showNumericPlot } from './scenarios/showNumericPlot'
import { showOffDiffCapability } from './scenarios/showOffDiffCapability'
/**
* Mobile Demo Video - Pixel 6 viewport
*
* This demo showcases MQTT Explorer running in a mobile browser viewport
* simulating a Google Pixel 6 (412x915px portrait mode)
*/
/**
* A convenience method that handles gracefully cleaning up the test run.
*/
const cleanUp = async (scenes: SceneBuilder, browser: Browser) => {
// Exit app.
fs.writeFileSync('scenes-mobile.json', JSON.stringify(scenes.scenes, undefined, ' '))
await browser.close()
}
process.on('unhandledRejection' as any, (error: Error | any) => {
console.error('unhandledRejection', error.message, error.stack)
process.exit(1)
})
setTimeout(
() => {
console.error('Timeout reached')
process.exit(1)
},
60 * 10 * 1000
)
async function doStuff() {
const brokerHost = process.env.TESTS_MQTT_BROKER_HOST || '127.0.0.1'
const brokerPort = process.env.TESTS_MQTT_BROKER_PORT || '1883'
console.log(`Waiting for MQTT Broker at ${brokerHost}:${brokerPort} (no auth)`)
await mockMqtt()
console.log('Starting playwright/chromium in mobile mode (Pixel 6)')
// Launch Chromium browser with mobile emulation
const browser = await chromium.launch({
headless: true,
args: ['--no-sandbox', '--disable-dev-shm-usage'],
})
// Create browser context with Pixel 6 viewport
const context = await browser.newContext({
viewport: {
width: 412,
height: 915,
},
deviceScaleFactor: 2.625,
isMobile: true,
hasTouch: true,
userAgent: 'Mozilla/5.0 (Linux; Android 12; Pixel 6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.45 Mobile Safari/537.36',
})
const page = await context.newPage()
// Navigate to the browser mode server
const serverUrl = process.env.BROWSER_MODE_URL || 'http://localhost:3000'
console.log(`Navigating to ${serverUrl}`)
await page.goto(serverUrl, { waitUntil: 'networkidle' })
// Print the title
console.log(await page.title())
// Capture a screenshot
await page.screenshot({ path: 'intro-mobile.png' })
// Direct console to Node terminal
page.on('console', console.log)
// Handle authentication if required
const username = process.env.MQTT_EXPLORER_USERNAME || 'admin'
const password = process.env.MQTT_EXPLORER_PASSWORD || 'password'
console.log('Waiting for page to initialize...')
await sleep(3000)
// Check for login dialog
const loginDialog = page.locator('h2:has-text("Login to MQTT Explorer")')
let loginDialogVisible = false
try {
loginDialogVisible = await loginDialog.isVisible({ timeout: 5000 })
} catch (error) {
console.log('Login dialog not found - auth may be disabled')
}
if (loginDialogVisible) {
console.log('Handling authentication...')
const usernameInput = page.locator('input[name="username"]')
const passwordInput = page.locator('input[name="password"]')
const loginButton = page.locator('button:has-text("Login")')
await usernameInput.fill(username)
await passwordInput.fill(password)
await loginButton.click()
await sleep(2000)
}
// Wait for the connection UI to be visible
await page.locator('//label[contains(text(), "Host")]/..//input').waitFor({ timeout: 10000 })
const scenes = new SceneBuilder()
await scenes.record('mobile_intro', async () => {
await showText('MQTT Explorer on Mobile', 2000, page, 'middle')
await sleep(2500)
await showText('Google Pixel 6 (412x915)', 1500, page, 'middle')
await sleep(2000)
await hideText(page)
})
await scenes.record('mobile_connect', async () => {
await showText('Connect to MQTT Broker', 1500, page, 'top')
await connectTo(brokerHost, page)
await MockSparkplug.run() // Start sparkplug client after connect
await sleep(2000)
await hideText(page)
})
await scenes.record('mobile_browse_topics', async () => {
await showText('Browse Topic Tree', 1500, page, 'top')
await sleep(1500)
// Try to expand a topic in the tree
const firstTopic = page.locator('[data-testid="tree-node"]').first()
if (await firstTopic.isVisible()) {
await firstTopic.click()
await sleep(1000)
}
await sleep(1500)
await hideText(page)
})
await scenes.record('mobile_search', async () => {
await showText('Search Topics', 1500, page, 'top')
await searchTree('temp', page)
await sleep(1500)
await showText('Filter Results', 1000, page, 'top')
await sleep(1500)
await clearSearch(page)
await sleep(1000)
await hideText(page)
})
await scenes.record('mobile_view_message', async () => {
await showText('View Message Details', 1500, page, 'top')
await sleep(1000)
// Click on a topic to view details in sidebar
const topicNode = page.locator('[data-testid="tree-node"]').first()
if (await topicNode.isVisible()) {
await topicNode.click()
await sleep(2000)
}
await hideText(page)
})
await scenes.record('mobile_json_view', async () => {
await showText('JSON Message Formatting', 1500, page, 'top')
await showJsonPreview(page)
await sleep(2000)
await hideText(page)
})
await scenes.record('mobile_clipboard', async () => {
await showText('Copy to Clipboard', 1500, page, 'top')
await copyTopicToClipboard(page)
await sleep(1000)
await copyValueToClipboard(page)
await sleep(1500)
await hideText(page)
})
await scenes.record('mobile_plots', async () => {
await showText('View Numeric Plots', 1500, page, 'top')
await showNumericPlot(page)
await sleep(2500)
await hideText(page)
})
await scenes.record('mobile_menu', async () => {
await showText('Settings & Menu', 1500, page, 'top')
await showMenu(page)
await sleep(2000)
await hideText(page)
})
await scenes.record('mobile_end', async () => {
await showText('Mobile-Friendly MQTT Explorer', 2000, page, 'middle')
await sleep(2500)
await showText('Ready for Optimization', 1500, page, 'middle')
await sleep(2000)
})
setTimeout(() => {
console.log('Forced quit')
process.exit(0)
}, 10 * 1000)
stopMqtt()
console.log('Stopped mqtt client')
await cleanUp(scenes, browser)
// Force exit since there appear to be open handles
process.exit(0)
}
doStuff()