const { chromium } = require('playwright'); function launchBrowser() { const channel = process.env.MEGURI_BROWSER_CHANNEL || 'chrome'; const executablePath = process.env.MEGURI_BROWSER_EXECUTABLE; return chromium.launch({ headless: true, ...(executablePath ? { executablePath, args: ['--no-sandbox'] } : channel === 'chromium' ? {} : { channel }), }); } exports.launchBrowser = launchBrowser; const baseUrl = () => process.env.MEGURI_BROWSER_URL || 'http://127.0.0.1:3000'; exports.baseUrl = baseUrl; // The demo generates one password per server start and publishes it to the login screen. let password; exports.demoPassword = () => password ||= fetch(baseUrl() + '/api/session').then(r => r.json()).then(session => { if (!session.demo_lifecycle?.password) throw new Error('Browser tests require a disposable demo instance'); return session.demo_lifecycle.password; }); exports.signIn = async (page, username, secret) => { await page.goto(baseUrl()); await page.getByLabel('Username', { exact: true }).fill(username); await page.getByLabel('Password', { exact: true }).fill(secret ?? await exports.demoPassword()); await page.getByRole('button', { name: 'Sign in', exact: true }).click(); await page.waitForFunction(() => { const a = window.Alpine && Alpine.$data(document.body); return a && a.ready && a.user && !a.busy; }); }; // Minimal cookie-aware JSON client for test setup that must not go through the UI. exports.apiLogin = async (username, secret) => { const base = baseUrl(); const login = await fetch(base + '/api/login', { method: 'POST', headers: { Origin: base, 'Content-Type': 'application/json' }, body: JSON.stringify({ username, password: secret ?? await exports.demoPassword() }) }); if (!login.ok) throw new Error(`Login as ${username} failed: ${login.status}`); const cookie = login.headers.getSetCookie().map(c => c.split(';')[0]).find(c => c.startsWith('meguri_session=')); const { csrf } = await login.json(); const call = async (method, path, body) => { const response = await fetch(base + path, { method, headers: { Origin: base, 'Content-Type': 'application/json', Cookie: cookie, 'X-CSRF-Token': csrf }, ...(body === undefined ? {} : { body: JSON.stringify(body) }) }); const data = await response.json().catch(() => ({})); return { status: response.status, data }; }; const ok = async (method, path, body) => { const result = await call(method, path, body); if (result.status >= 300) throw new Error(`${method} ${path} -> ${result.status} ${JSON.stringify(result.data)}`); return result.data; }; return { call, ok, get: path => ok('GET', path), post: (path, body) => ok('POST', path, body), put: (path, body) => ok('PUT', path, body), csrf, cookie }; }; exports.adminApi = () => exports.apiLogin('admin'); // Mutating suites each own one empty sandbox workspace ("Sandbox N", demo started with // MEGURI_DEMO_SANDBOXES) whose groups are sandbox-N-leads (administrators) and sandbox-N-staff (employees). // Nobody is in a sandbox except accounts created here, so suites do not depend on each other or on order. exports.createUser = async (admin, { sandbox, role = 'admin', prefix, display }) => { const overview = await admin.get('/api/admin/overview'); const target = overview.workspaces.find(w => w.name === `Sandbox ${sandbox}` && !w.disabled_at); if (!target) throw new Error(`Start the demo with MEGURI_DEMO_SANDBOXES>=${sandbox}: Sandbox ${sandbox} not found`); const groups = role === 'admin' ? target.admin_groups : target.member_groups; const username = `${prefix}-${Date.now().toString(36)}${Math.random().toString(36).slice(2, 6)}`.toLowerCase(); const secret = 'local-browser-test-password'; await admin.post('/api/admin/users', { username, display_name: display, password: secret, groups }); return { username, password: secret, display, workspace: target }; }; // An administrator of exactly one sandbox, plus any employees (display names) created beside them. exports.isolatedUser = async ({ sandbox, prefix, display, employees = [] }) => { const admin = await exports.adminApi(); const user = await exports.createUser(admin, { sandbox, role: 'admin', prefix, display }); const staff = []; for (const name of employees) staff.push(await exports.createUser(admin, { sandbox, role: 'member', prefix: name.split(' ')[0], display: name })); return { admin, ...user, staff }; }; exports.openSetup = async (page, name) => { await page.waitForFunction(()=>window.Alpine && Alpine.$data(document.body).ready && Alpine.$data(document.body).workspace); const combined=['Schedules','Escalations & routing','Variables & rules'].includes(name); const target=page.getByRole('link',{name:combined?'Schedule setup':name,exact:true}); if(!await target.isVisible())await page.getByRole('button',{name:'Setup',exact:true}).click(); await target.click(); if(combined)await page.locator(name==='Schedules'?'#schedules':name==='Escalations & routing'?'#schedules':'#generation').scrollIntoViewIfNeeded(); };