225 lines
8.9 KiB
TypeScript
225 lines
8.9 KiB
TypeScript
import { test, expect } from '@playwright/test';
|
|
import { E2E_BACKEND_URL } from '../playwright.config';
|
|
|
|
// Helper to clean up any existing tmux sessions via UI
|
|
async function cleanupTmuxSessions(page: import('@playwright/test').Page) {
|
|
await page.goto('/');
|
|
|
|
// Delete all tmux session cards
|
|
while (true) {
|
|
const tmuxCards = page.locator('a.card').filter({ has: page.locator('span:text-is("tmux")') });
|
|
const count = await tmuxCards.count();
|
|
if (count === 0) break;
|
|
|
|
// Set up one-time dialog handler for this deletion
|
|
page.once('dialog', async (dialog) => {
|
|
await dialog.accept();
|
|
});
|
|
|
|
const deleteButton = tmuxCards.first().locator('button[title="Delete session"]');
|
|
await deleteButton.click();
|
|
await page.waitForTimeout(500); // Wait for deletion to process
|
|
}
|
|
}
|
|
|
|
test.describe('Tmux Terminal Session', () => {
|
|
test('create tmux session and run shell commands', async ({ page }) => {
|
|
// Clean up any stale tmux sessions first
|
|
await cleanupTmuxSessions(page);
|
|
|
|
// Enable console logging for debugging
|
|
page.on('console', (msg) => {
|
|
console.log(`[Browser ${msg.type()}]`, msg.text());
|
|
});
|
|
|
|
// Log network requests to /api
|
|
page.on('request', (request) => {
|
|
if (request.url().includes('/api')) {
|
|
console.log(`[Request] ${request.method()} ${request.url()}`);
|
|
}
|
|
});
|
|
page.on('response', (response) => {
|
|
if (response.url().includes('/api')) {
|
|
console.log(`[Response] ${response.status()} ${response.url()}`);
|
|
}
|
|
});
|
|
|
|
// 1. Navigate to homepage
|
|
await page.goto('/');
|
|
await expect(page).toHaveTitle(/Spiceflow/i);
|
|
|
|
// 2. Click the + button to open new session menu
|
|
const createButton = page.locator('button[title="New Session"]');
|
|
await expect(createButton).toBeVisible();
|
|
await createButton.click();
|
|
|
|
// 3. Select Terminal (tmux) from the dropdown
|
|
const tmuxOption = page.locator('button:has-text("Terminal (tmux)")');
|
|
await expect(tmuxOption).toBeVisible();
|
|
await tmuxOption.click();
|
|
|
|
// 4. Wait for navigation to session page
|
|
await page.waitForURL(/\/session\/.+/);
|
|
const sessionUrl = page.url();
|
|
const sessionId = decodeURIComponent(sessionUrl.split('/session/')[1]);
|
|
console.log('[Test] Navigated to session page:', sessionUrl);
|
|
|
|
// 5. Wait for terminal view to load (should see the terminal pre element)
|
|
const terminalOutput = page.locator('pre.text-green-400');
|
|
await expect(terminalOutput).toBeVisible({ timeout: 10000 });
|
|
console.log('[Test] Terminal view loaded');
|
|
|
|
// 6. Verify the session status indicator shows active
|
|
const statusIndicator = page.locator('.bg-green-500').first();
|
|
await expect(statusIndicator).toBeVisible({ timeout: 5000 });
|
|
console.log('[Test] Session is active');
|
|
|
|
// 7. Verify the terminal badge shows "TERMINAL"
|
|
const terminalBadge = page.locator('text=TERMINAL');
|
|
await expect(terminalBadge).toBeVisible();
|
|
|
|
// 8. Find the command input
|
|
const commandInput = page.locator('input[placeholder*="Type command"]');
|
|
await expect(commandInput).toBeVisible();
|
|
await expect(commandInput).toBeEnabled();
|
|
|
|
// 9. Run `pwd` command
|
|
await commandInput.fill('pwd');
|
|
await commandInput.press('Enter');
|
|
console.log('[Test] Sent pwd command');
|
|
|
|
// 10. Wait for output and verify it contains a path (starts with /)
|
|
await page.waitForTimeout(1000); // Give time for command to execute
|
|
let terminalContent = await terminalOutput.textContent();
|
|
console.log('[Test] Terminal content after pwd:', terminalContent);
|
|
|
|
// The output should contain a path starting with /
|
|
await expect(async () => {
|
|
terminalContent = await terminalOutput.textContent();
|
|
expect(terminalContent).toMatch(/\/[a-zA-Z]/);
|
|
}).toPass({ timeout: 5000 });
|
|
console.log('[Test] pwd output verified - contains path');
|
|
|
|
// 11. Run `ls -al` command
|
|
await commandInput.fill('ls -al');
|
|
await commandInput.press('Enter');
|
|
console.log('[Test] Sent ls -al command');
|
|
|
|
// 12. Wait for output and verify it contains typical ls -al output
|
|
await page.waitForTimeout(1000); // Give time for command to execute
|
|
|
|
await expect(async () => {
|
|
terminalContent = await terminalOutput.textContent();
|
|
// ls -al output typically contains "total" at the start and permission strings like "drwx"
|
|
expect(terminalContent).toMatch(/total \d+/);
|
|
}).toPass({ timeout: 5000 });
|
|
console.log('[Test] ls -al output verified - contains "total" line');
|
|
|
|
// 13. Verify the output contains directory entries with permissions
|
|
await expect(async () => {
|
|
terminalContent = await terminalOutput.textContent();
|
|
// Should see permission patterns like drwx or -rw-
|
|
expect(terminalContent).toMatch(/[d-][rwx-]{9}/);
|
|
}).toPass({ timeout: 5000 });
|
|
console.log('[Test] ls -al output verified - contains permission strings');
|
|
|
|
// 14. Verify the output contains the . and .. directory entries
|
|
terminalContent = await terminalOutput.textContent();
|
|
// The . entry appears at end of line as " .\n" or " ." followed by newline
|
|
expect(terminalContent).toMatch(/ \.$/m);
|
|
console.log('[Test] ls -al output verified - contains current directory entry');
|
|
|
|
console.log('[Test] Tmux terminal test completed successfully');
|
|
|
|
// 15. Cleanup: Delete the session via UI
|
|
await page.goto('/');
|
|
await expect(page).toHaveTitle(/Spiceflow/i);
|
|
|
|
// Find the tmux session card and delete it
|
|
const tmuxCards = page.locator('a.card').filter({ has: page.locator('span:text-is("tmux")') });
|
|
await expect(tmuxCards).toHaveCount(1);
|
|
|
|
// Set up one-time dialog handler
|
|
page.once('dialog', async (dialog) => {
|
|
await dialog.accept();
|
|
});
|
|
|
|
const deleteButton = tmuxCards.first().locator('button[title="Delete session"]');
|
|
await deleteButton.click();
|
|
|
|
// Wait for no tmux cards to remain
|
|
await expect(tmuxCards).toHaveCount(0, { timeout: 5000 });
|
|
console.log('[Test] Cleanup: Session deleted');
|
|
});
|
|
|
|
test('deleting tmux session kills the tmux process', async ({ page, request }) => {
|
|
// Clean up any stale tmux sessions first
|
|
await cleanupTmuxSessions(page);
|
|
|
|
// 1. Navigate to homepage and create a tmux session
|
|
await page.goto('/');
|
|
await expect(page).toHaveTitle(/Spiceflow/i);
|
|
|
|
const createButton = page.locator('button[title="New Session"]');
|
|
await createButton.click();
|
|
|
|
const tmuxOption = page.locator('button:has-text("Terminal (tmux)")');
|
|
await tmuxOption.click();
|
|
|
|
// Wait for navigation to session page
|
|
await page.waitForURL(/\/session\/.+/);
|
|
const sessionUrl = page.url();
|
|
// URL decode the session ID since it contains special characters
|
|
const sessionId = decodeURIComponent(sessionUrl.split('/session/')[1]);
|
|
console.log('[Test] Created session:', sessionId);
|
|
|
|
// 2. Wait for terminal to load and session to be active
|
|
const terminalOutput = page.locator('pre.text-green-400');
|
|
await expect(terminalOutput).toBeVisible({ timeout: 10000 });
|
|
|
|
// 3. For ephemeral tmux sessions, the session ID IS the tmux session name
|
|
const tmuxSessionName = sessionId;
|
|
console.log('[Test] Tmux session name:', tmuxSessionName);
|
|
|
|
// 4. Verify the tmux session exists by running a command
|
|
const commandInput = page.locator('input[placeholder*="Type command"]');
|
|
await commandInput.fill('echo "session-alive"');
|
|
await commandInput.press('Enter');
|
|
await page.waitForTimeout(500);
|
|
|
|
// 5. Go back to home page
|
|
await page.goto('/');
|
|
await expect(page).toHaveTitle(/Spiceflow/i);
|
|
|
|
// 6. Find and delete the session by provider badge (more generic than session ID)
|
|
const tmuxCards = page.locator('a.card').filter({ has: page.locator('span:text-is("tmux")') });
|
|
await expect(tmuxCards).toHaveCount(1);
|
|
|
|
// Set up one-time dialog handler to accept the confirmation
|
|
page.once('dialog', async (dialog) => {
|
|
console.log('[Test] Dialog appeared:', dialog.message());
|
|
await dialog.accept();
|
|
});
|
|
|
|
// Click the delete button (X icon) on the session card
|
|
const deleteButton = tmuxCards.first().locator('button[title="Delete session"]');
|
|
await deleteButton.click();
|
|
|
|
// 7. Wait for session to be deleted (no more tmux cards)
|
|
await expect(tmuxCards).toHaveCount(0, { timeout: 5000 });
|
|
console.log('[Test] Session deleted from UI');
|
|
|
|
// 8. Verify the tmux session was killed by checking the API
|
|
// The session should no longer exist
|
|
const sessionResponse = await request.get(`${E2E_BACKEND_URL}/api/sessions/${sessionId}`);
|
|
expect(sessionResponse.status()).toBe(404);
|
|
console.log('[Test] Session no longer exists in API');
|
|
|
|
// 9. Verify the tmux session is no longer alive
|
|
// We can check this by trying to get terminal content - it should fail
|
|
const terminalCheckResponse = await request.get(`${E2E_BACKEND_URL}/api/sessions/${sessionId}/terminal`);
|
|
expect(terminalCheckResponse.status()).toBe(404);
|
|
console.log('[Test] Tmux session properly cleaned up');
|
|
});
|
|
});
|