// Optional integration test: requires Playwright + Chromium and the project's test extra. const assert = require('node:assert/strict'); const { spawn } = require('node:child_process'); const { mkdtemp, rm } = require('node:fs/promises'); const os = require('node:os'); const path = require('node:path'); const { chromium } = require('playwright'); (async () => { const temporary = await mkdtemp(path.join(os.tmpdir(), 'nest-browser-')); const port = process.env.NEST_BROWSER_TEST_PORT || '18765'; const origin = `http://127.0.0.1:${port}`; const server = spawn(process.env.NEST_TEST_PYTHON || 'python3', ['-B', '-m', 'tests.browser_session_server', '--base', temporary, '--port', port], { cwd: path.resolve(__dirname, '..'), stdio: ['ignore', 'pipe', 'pipe'] }); let logs = ''; server.stdout.on('data', (data) => { logs = (logs + data).slice(-16000); }); server.stderr.on('data', (data) => { logs = (logs + data).slice(-16000); }); let browser; let page; try { let ready = false; for (let attempt = 0; attempt < 100; attempt++) { if (server.exitCode !== null) throw new Error(`Fixture server exited: ${logs}`); try { const response = await fetch(`${origin}/health`); if (response.ok) { ready = true; break; } } catch {} await new Promise((resolve) => setTimeout(resolve, 100)); } assert.ok(ready, `Fixture server did not start: ${logs}`); browser = await chromium.launch({ headless: true }); page = await browser.newPage({ viewport: { width: 1440, height: 1100 } }); const errors = []; page.on('pageerror', (error) => errors.push(error.message)); const connected = () => page.waitForFunction(() => document.querySelector('#session-status').textContent === 'Connected'); const output = (value) => page.waitForFunction((text) => document.querySelector('#session-terminal').textContent.includes(text), value); await page.goto(`${origin}/dashboard#vm/vm-test`); await page.locator('#token-input').fill('browser-test-token'); await page.locator('#auth-btn').click(); await page.locator('#detail-actions [data-action="launch-session"]').click(); await connected(); const firstUrl = page.url(); assert.match(firstUrl, /#vm\/vm-test\/sessions\/session-/); await page.keyboard.type('echo alpha'); await page.keyboard.press('Enter'); await output('result:echo alpha'); await page.reload(); await connected(); await output('result:echo alpha'); assert.equal(await page.locator('.session-item').count(), 1); await page.locator('#detail-actions [data-action="launch-session"]').click(); await page.waitForURL((url) => url.href !== firstUrl); await connected(); assert.equal(await page.locator('.session-item').count(), 2); assert.ok(!(await page.locator('#session-terminal').textContent()).includes('result:echo alpha')); await page.keyboard.type('echo beta'); await page.keyboard.press('Enter'); await output('result:echo beta'); await page.locator('.session-item').filter({ hasText: 'Session 1' }).click(); await connected(); await output('result:echo alpha'); assert.ok(!(await page.locator('#session-terminal').textContent()).includes('result:echo beta')); await page.goto(`${origin}/dashboard#vm/vm-other/sessions`); await page.locator('#vm-sessions-panel').waitFor({ state: 'visible' }); await page.waitForFunction(() => document.querySelector('#vm-session-count').textContent === '0'); assert.equal(await page.locator('.session-item').count(), 0); assert.ok(await page.locator('#detail-actions [data-action="launch-session"]').isDisabled()); assert.ok(!(await page.locator('#session-terminal').textContent()).includes('alpha')); await page.goto(firstUrl); await connected(); await output('result:echo alpha'); await page.locator('#session-end').click(); await page.locator('#confirm-ok').click(); await page.waitForFunction(() => document.querySelector('#session-status').textContent.startsWith('closed')); await page.reload(); await output('result:echo alpha'); assert.ok(await page.locator('#session-end').isDisabled()); await page.setViewportSize({ width: 390, height: 844 }); await page.waitForTimeout(300); const fits = await page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth); assert.ok(fits, 'mobile layout overflows the viewport'); assert.deepEqual(errors, []); if (process.env.NEST_BROWSER_SCREENSHOT) await page.screenshot({ path: process.env.NEST_BROWSER_SCREENSHOT, fullPage: true }); console.log('Browser sessions passed: launch, terminal input, reload/reconnect, multiple sessions, VM isolation, closed history, and mobile layout.'); } catch (error) { if (page) console.error('Browser state:', await page.locator('#sessions-error').textContent(), await page.locator('#session-status').textContent()); console.error(logs); throw error; } finally { await browser?.close(); server.kill('SIGTERM'); if (server.exitCode === null) await new Promise((resolve) => server.once('exit', resolve)); await rm(temporary, { recursive: true, force: true }); } })().catch((error) => { console.error(error); process.exitCode = 1; });