feat: implement session management and terminal handling with UI updates and backend support

This commit is contained in:
2026-09-25 20:22:56 +05:30
parent d202f9e2f8
commit caaea12163
11 changed files with 648 additions and 18 deletions
+101
View File
@@ -0,0 +1,101 @@
// 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; });