/* Read-only browser smoke test of a running Rust server. * Set MBS_BASE_URL, MBS_TEST_USERNAME and MBS_TEST_PASSWORD explicitly. * Optional MBS_CHROMIUM points to an existing browser executable. */ const { chromium } = require('playwright'); const assert = require('node:assert/strict'); const fs = require('node:fs'); const path = require('node:path'); const base = process.env.MBS_BASE_URL; if (!base || !process.env.MBS_TEST_USERNAME || !process.env.MBS_TEST_PASSWORD) throw new Error('Set MBS_BASE_URL, MBS_TEST_USERNAME and MBS_TEST_PASSWORD'); (async () => { const browser = await chromium.launch({ headless: true, args: ['--no-sandbox'], ...(process.env.MBS_CHROMIUM ? { executablePath: process.env.MBS_CHROMIUM } : {}) }); const page = await browser.newPage({ viewport: { width: 1440, height: 1000 } }); const errors = [], failed = [], security = []; page.on('pageerror', error => errors.push(error.message)); page.on('console', message => { if (message.type() === 'error' && /Content Security|Refused to|CSP/i.test(message.text())) security.push(message.text()); }); page.on('response', response => { if (response.url().includes('/api/') && response.status() >= 400) failed.push(`${response.status()} ${new URL(response.url()).pathname}`); }); try { await page.goto(base); await page.locator('#login-form').waitFor(); assert.equal(await page.locator('.auth-layout header, .auth-layout svg').count(), 0); if (!await page.evaluate(() => window.MBS.config.allowRegistration)) assert.equal(await page.locator('a[href="/register"]').count(), 0); await page.fill('#login-form [name=username]', process.env.MBS_TEST_USERNAME); await page.fill('#login-form [name=password]', process.env.MBS_TEST_PASSWORD); await page.click('#login-form button[type=submit]'); await page.locator('.cards-grid').waitFor(); assert.equal(await page.locator('.metric svg, .topbar .brand svg, .topbar .brand-sub, .nav a[href="/change-password"]').count(), 0); assert.equal(await page.locator('.nav a[href="/schedule"]').count(), 0); assert.equal(await page.locator('.api-docs-link').getAttribute('href'), '/docs'); for (const route of ['/devices', '/files', '/diff-configs', '/logs', '/settings', '/schedule', '/change-password', '/']) { await page.evaluate(route => window.MBS.navigate(route), route); await page.locator('.loading').waitFor({ state: 'detached' }); assert.equal(await page.locator('.error-panel').count(), 0, `Failed route ${route}`); assert.equal(await page.locator('main h1').count(), 1, `Missing heading ${route}`); assert.ok(!/\b(?:routers|settings|dashboard|files|auth|ui)\.[a-zA-Z]+\b/.test(await page.locator('main').textContent()), `Untranslated key on ${route}`); if (route === '/change-password') { assert.equal(new URL(page.url()).pathname, '/settings'); assert.equal(new URL(page.url()).searchParams.get('tab'), 'account'); assert.equal(await page.locator('#password-form').isVisible(), true); } if (route === '/schedule') { assert.equal(new URL(page.url()).pathname, '/settings'); assert.equal(new URL(page.url()).searchParams.get('tab'), 'automation'); assert.equal(await page.locator('.settings-layout .card:visible').count(), 0); } console.log(`PASS live route ${route}`); } const routers = await page.evaluate(() => window.MBS.state.cache.routers || []); if (routers.length) { await page.evaluate(id => window.MBS.navigate(`/devices/${id}`), routers[0].id); await page.locator('#device-detail-body').waitFor(); const supported = routers[0].device_type !== 'switchos' && routers[0].supports_export !== false; assert.equal(await page.locator('.device-heading .metric-icon').count(), 0); if (!supported) assert.equal(await page.locator('[data-detail-tab=export], [data-action=device-export]').count(), 0); for (const tab of [...(supported ? ['export'] : []), 'binary', 'policy', 'overview']) await page.click(`[data-detail-tab=${tab}]`); console.log('PASS live device detail and tabs'); } const exports = await page.evaluate(() => (window.MBS.state.cache.backups || []).filter(file => file.backup_type === 'export')); if (exports.length >= 2) { await page.evaluate(ids => window.MBS.navigate(`/diff-configs?left=${ids[0]}&right=${ids[1]}`), exports.slice(0, 2).map(file => file.id)); await page.locator('.diff-split').waitFor(); assert.equal(await page.locator('.diff-pane').count(), 2); await page.click('[data-diff-mode=unified]'); await page.locator('.diff-unified').waitFor(); await page.click('[data-diff-mode=split]'); if (process.env.MBS_SCREENSHOT_DIR) { fs.mkdirSync(process.env.MBS_SCREENSHOT_DIR, { recursive: true }); await page.screenshot({ path: path.join(process.env.MBS_SCREENSHOT_DIR, 'live-diff.png'), fullPage: true, animations: 'disabled' }); } console.log('PASS live RSC diff split and unified'); } await page.evaluate(() => window.MBS.navigate('/')); if (process.env.MBS_SCREENSHOT_DIR) { fs.mkdirSync(process.env.MBS_SCREENSHOT_DIR, { recursive: true }); await page.screenshot({ path: path.join(process.env.MBS_SCREENSHOT_DIR, 'live-dashboard.png'), fullPage: true, animations: 'disabled' }); await page.evaluate(() => window.MBS.navigate('/settings')); await page.click('[data-settings-tab=automation]'); await page.screenshot({ path: path.join(process.env.MBS_SCREENSHOT_DIR, 'live-settings.png'), fullPage: true, animations: 'disabled' }); } await page.setViewportSize({ width: 390, height: 844 }); await page.evaluate(() => window.MBS.navigate('/')); assert.ok(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth + 1), 'Mobile horizontal overflow'); if (process.env.MBS_SCREENSHOT_DIR) await page.screenshot({ path: path.join(process.env.MBS_SCREENSHOT_DIR, 'live-mobile.png'), fullPage: true, animations: 'disabled' }); assert.deepEqual(errors, []); assert.deepEqual(failed, []); assert.deepEqual(security, []); await page.click('[data-action=logout]'); await page.locator('#login-form').waitFor(); console.log('PASS live responsive layout, CSP, no API errors, no JS errors and logout'); } finally { await browser.close(); } })().catch(error => { console.error(error); process.exitCode = 1; });