// 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 }); const context = await browser.newContext({ viewport: { width: 1440, height: 1100 } }); page = await context.newPage(); 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); // Rename is cancellable, validated, persisted, and preserves the live widget. const firstId = new URL(firstUrl).hash.split('/').at(-1); const firstRow = () => page.locator(`.session-item[data-session-id="${firstId}"]`); await page.locator('#session-rename').click(); await page.locator('#session-name-input').fill('Discarded name'); await page.locator('#session-rename-cancel').click(); assert.equal(await firstRow().locator('strong').textContent(), 'Session 1'); await page.evaluate(() => { window.originalTerminal = document.querySelector('#session-terminal .xterm'); }); await page.locator('#session-rename').click(); await page.locator('#session-name-input').fill(' '); await page.locator('#session-rename-save').click(); assert.ok(await page.locator('#session-rename-dialog').isVisible()); assert.match(await page.locator('#session-rename-error').textContent(), /Enter a session name/); await page.locator('#session-name-input').fill(' Deployment '); await page.locator('#session-rename-save').click(); await page.locator('#session-rename-dialog').waitFor({ state: 'hidden' }); await page.waitForFunction(() => document.querySelector('#session-title').textContent.startsWith('Deployment')); assert.equal(page.url(), firstUrl); assert.ok(await page.evaluate(() => window.originalTerminal === document.querySelector('#session-terminal .xterm'))); await page.reload(); await connected(); await output('result:echo alpha'); assert.equal(await firstRow().locator('strong').textContent(), 'Deployment'); await page.locator('#detail-actions [data-action="launch-session"]').click(); await page.waitForURL((url) => url.href !== firstUrl); await connected(); const secondUrl = page.url(); 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'); // History actions also work without selecting the affected session. await firstRow().locator('[data-session-action="rename"]').click(); await page.locator('#session-name-input').fill('Operations '); await page.locator('#session-rename-save').click(); await page.locator('#session-rename-dialog').waitFor({ state: 'hidden' }); assert.equal(page.url(), secondUrl); assert.equal(await firstRow().locator('strong').textContent(), 'Operations '); assert.equal(await firstRow().locator('alpha').count(), 0); await output('result:echo beta'); await firstRow().locator('.session-link').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.locator('#session-rename').click(); await page.locator('#session-name-input').fill('Archived run'); await page.locator('#session-rename-save').click(); await page.locator('#session-rename-dialog').waitFor({ state: 'hidden' }); await page.reload(); await output('result:echo alpha'); assert.equal(await firstRow().locator('strong').textContent(), 'Archived run'); 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'); if (process.env.NEST_BROWSER_SCREENSHOT) await page.screenshot({ path: process.env.NEST_BROWSER_SCREENSHOT, fullPage: true }); // Cancel preserves history; confirmation removes an ended session and clears selection. await page.locator('#session-delete').click(); await page.locator('#confirm-dialog button[value="cancel"]').click(); assert.equal(await page.locator('.session-item').count(), 2); await firstRow().locator('[data-session-action="delete"]').click(); await page.locator('#confirm-ok').click(); await page.waitForURL((url) => url.hash === '#vm/vm-test/sessions'); await page.waitForFunction(() => document.querySelector('#vm-session-count').textContent === '1'); assert.ok(await page.locator('#session-terminal').isHidden()); await page.reload(); await page.waitForFunction(() => document.querySelector('#vm-session-count').textContent === '1'); assert.equal(await firstRow().count(), 0); // Deleting an active session clears other attached browser views as well. await page.goto(secondUrl); await connected(); await output('result:echo beta'); const observer = await page.context().newPage(); observer.on('pageerror', (error) => errors.push(error.message)); await observer.goto(secondUrl); await observer.locator('#token-input').fill('browser-test-token'); await observer.locator('#auth-btn').click(); await observer.waitForFunction(() => document.querySelector('#session-status').textContent === 'Connected'); await page.locator('#session-rename').click(); await page.locator('#session-name-input').fill('Second workspace'); await page.locator('#session-rename-save').click(); await page.locator('#session-rename-dialog').waitFor({ state: 'hidden' }); await observer.waitForFunction(() => document.querySelector('#session-title').textContent.startsWith('Second workspace')); await page.locator('#session-delete').click(); await page.locator('#confirm-ok').click(); await page.waitForFunction(() => document.querySelector('#vm-session-count').textContent === '0'); await observer.waitForURL((url) => url.hash === '#vm/vm-test/sessions'); assert.ok(await observer.locator('#session-terminal').isHidden()); await page.reload(); await page.waitForFunction(() => document.querySelector('#vm-session-count').textContent === '0'); assert.equal(await page.locator('.session-item').count(), 0); assert.deepEqual(errors, []); await observer.close(); console.log('Browser sessions passed: terminal/reconnect, VM isolation, persistent rename, active/ended deletion, cancellation, live browser updates, 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; });