Files
2026-10-28 09:16:54 +01:00

89 lines
6.3 KiB
JavaScript

/* 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; });