import { test, expect } from '@playwright/test'; import { E2E_BACKEND_URL } from '../playwright.config'; // Helper to create a tmux session via API async function createTmuxSession(request: import('@playwright/test').APIRequestContext) { const res = await request.post(`${E2E_BACKEND_URL}/api/sessions`, { data: { provider: 'tmux' } }); expect(res.ok()).toBeTruthy(); return await res.json(); } // Helper to delete a session async function deleteSession(request: import('@playwright/test').APIRequestContext, sessionId: string) { try { await request.delete(`${E2E_BACKEND_URL}/api/sessions/${encodeURIComponent(sessionId)}`); } catch (e) { console.log(`[Cleanup] Failed to delete session ${sessionId}:`, e); } } // Helper to wait for tmux session to load async function waitForTmuxSession(page: import('@playwright/test').Page) { // Wait for terminal badge to appear await expect(page.locator('text=TERMINAL')).toBeVisible({ timeout: 10000 }); } // Helper to click a session card by ID (triggers client-side navigation) async function clickSessionById(page: import('@playwright/test').Page, sessionId: string) { const card = page.locator(`a[href="/session/${encodeURIComponent(sessionId)}"]`); await expect(card).toBeVisible({ timeout: 10000 }); await card.click(); } // Helper to go back to home via the back button (client-side navigation) async function goBackToHome(page: import('@playwright/test').Page) { const backBtn = page.locator('button[aria-label="Go back"]'); await expect(backBtn).toBeVisible(); await backBtn.click(); await expect(page).toHaveTitle(/Spiceflow/i); } test.describe('Last Session Navigation', () => { test('navigating between sessions shows last session button', async ({ page, request }) => { // Create two sessions const sessionA = await createTmuxSession(request); const sessionB = await createTmuxSession(request); try { // Start at home page and wait for sessions to load await page.goto('/'); await expect(page).toHaveTitle(/Spiceflow/i); await expect(page.locator('a[href^="/session/"]').first()).toBeVisible({ timeout: 10000 }); // Click session A (client-side navigation) await clickSessionById(page, sessionA.id); await waitForTmuxSession(page); // Last session button should NOT be visible (no previous session) await expect(page.locator('button[title="Go to last session"]')).not.toBeVisible(); // Go back to home await goBackToHome(page); // Click session B (client-side navigation, sets lastSession = A) await clickSessionById(page, sessionB.id); await waitForTmuxSession(page); // Last session button SHOULD be visible now (previous was A) const lastSessionBtn = page.locator('button[title="Go to last session"]'); await expect(lastSessionBtn).toBeVisible({ timeout: 5000 }); // Click it to go back to A await lastSessionBtn.click(); await waitForTmuxSession(page); // Verify we're on session A await expect(page).toHaveURL(new RegExp(`/session/${encodeURIComponent(sessionA.id)}`)); // Now last session should still be visible (pointing to B) await expect(lastSessionBtn).toBeVisible({ timeout: 5000 }); } finally { await deleteSession(request, sessionA.id); await deleteSession(request, sessionB.id); } }); test('going home and re-entering same session preserves last session', async ({ page, request }) => { // Create two sessions const sessionA = await createTmuxSession(request); const sessionB = await createTmuxSession(request); try { // Start at home page await page.goto('/'); await expect(page.locator('a[href^="/session/"]').first()).toBeVisible({ timeout: 10000 }); // Click session A, then back to home, then click session B await clickSessionById(page, sessionA.id); await waitForTmuxSession(page); await goBackToHome(page); await clickSessionById(page, sessionB.id); await waitForTmuxSession(page); // Verify last session button is visible (points to A) const lastSessionBtn = page.locator('button[title="Go to last session"]'); await expect(lastSessionBtn).toBeVisible({ timeout: 5000 }); // Go back to home await goBackToHome(page); // Re-enter session B (same session) await clickSessionById(page, sessionB.id); await waitForTmuxSession(page); // Last session button should STILL be visible and point to A // (re-entering same session should NOT override the last session) await expect(lastSessionBtn).toBeVisible({ timeout: 5000 }); // Click it to verify it goes to A await lastSessionBtn.click(); await waitForTmuxSession(page); await expect(page).toHaveURL(new RegExp(`/session/${encodeURIComponent(sessionA.id)}`)); } finally { await deleteSession(request, sessionA.id); await deleteSession(request, sessionB.id); } }); test('last session button not shown when it equals current session', async ({ page, request }) => { // Create two sessions const sessionA = await createTmuxSession(request); const sessionB = await createTmuxSession(request); try { // Start at home await page.goto('/'); await expect(page.locator('a[href^="/session/"]').first()).toBeVisible({ timeout: 10000 }); // Go A -> home -> B (sets localStorage to A) await clickSessionById(page, sessionA.id); await waitForTmuxSession(page); await goBackToHome(page); await clickSessionById(page, sessionB.id); await waitForTmuxSession(page); // Go back to home await goBackToHome(page); // Now go to A (localStorage still has A, which equals current) await clickSessionById(page, sessionA.id); await waitForTmuxSession(page); // Last session button should NOT be visible (lastSession = current session = A) await expect(page.locator('button[title="Go to last session"]')).not.toBeVisible(); } finally { await deleteSession(request, sessionA.id); await deleteSession(request, sessionB.id); } }); test('last session via settings menu works', async ({ page, request }) => { // Create two sessions const sessionA = await createTmuxSession(request); const sessionB = await createTmuxSession(request); try { // Start at home await page.goto('/'); await expect(page.locator('a[href^="/session/"]').first()).toBeVisible({ timeout: 10000 }); // Navigate A -> home -> B await clickSessionById(page, sessionA.id); await waitForTmuxSession(page); await goBackToHome(page); await clickSessionById(page, sessionB.id); await waitForTmuxSession(page); // Open settings menu const settingsBtn = page.locator('button[aria-label="Session settings"]'); await expect(settingsBtn).toBeVisible(); await settingsBtn.click(); // Click "Last session" in the menu const lastSessionMenuItem = page.locator('button:has-text("Last session")'); await expect(lastSessionMenuItem).toBeVisible({ timeout: 5000 }); await lastSessionMenuItem.click(); // Should navigate to A await waitForTmuxSession(page); await expect(page).toHaveURL(new RegExp(`/session/${encodeURIComponent(sessionA.id)}`)); } finally { await deleteSession(request, sessionA.id); await deleteSession(request, sessionB.id); } }); // Skip: tmux session deletion/validation has timing issues in tests test.skip('deleting last session clears the reference', async ({ page, request }) => { // Create two sessions const sessionA = await createTmuxSession(request); const sessionB = await createTmuxSession(request); try { // Start at home await page.goto('/'); await expect(page.locator('a[href^="/session/"]').first()).toBeVisible({ timeout: 10000 }); // Navigate A -> home -> B await clickSessionById(page, sessionA.id); await waitForTmuxSession(page); await goBackToHome(page); await clickSessionById(page, sessionB.id); await waitForTmuxSession(page); // Last session button should be visible const lastSessionBtn = page.locator('button[title="Go to last session"]'); await expect(lastSessionBtn).toBeVisible({ timeout: 5000 }); // Delete session A via API await deleteSession(request, sessionA.id); // Go home and wait for session A to disappear from the list await goBackToHome(page); await expect(page.locator(`a[href="/session/${encodeURIComponent(sessionA.id)}"]`)).not.toBeVisible({ timeout: 10000 }); // Go back to B await clickSessionById(page, sessionB.id); await waitForTmuxSession(page); // Last session button should NOT be visible (A was deleted) await expect(lastSessionBtn).not.toBeVisible({ timeout: 5000 }); } finally { await deleteSession(request, sessionB.id); } }); });