chore: commit remaining local changes
This commit is contained in:
@@ -153,6 +153,10 @@ export default async function globalSetup() {
|
||||
PASEO_LISTEN: `0.0.0.0:${port}`,
|
||||
PASEO_RELAY_ENDPOINT: `127.0.0.1:${relayPort}`,
|
||||
PASEO_CORS_ORIGINS: `http://localhost:${metroPort}`,
|
||||
// Keep e2e bootstrap fast and deterministic; terminal/sidebar tests do not need speech.
|
||||
PASEO_DICTATION_ENABLED: "0",
|
||||
PASEO_VOICE_MODE_ENABLED: "0",
|
||||
PASEO_LOCAL_AUTO_DOWNLOAD: "0",
|
||||
NODE_ENV: 'development',
|
||||
},
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
|
||||
@@ -168,9 +168,10 @@ export const openSettings = async (page: Page) => {
|
||||
};
|
||||
|
||||
export const setWorkingDirectory = async (page: Page, directory: string) => {
|
||||
const workingDirectoryLabel = page.getByText('WORKING DIRECTORY', { exact: true }).first();
|
||||
await expect(workingDirectoryLabel).toBeVisible();
|
||||
const workingDirectorySelect = page.getByTestId('working-directory-select').first();
|
||||
const workingDirectorySelect = page
|
||||
.locator('[data-testid="working-directory-select"]:visible')
|
||||
.first();
|
||||
await expect(workingDirectorySelect).toBeVisible({ timeout: 30000 });
|
||||
|
||||
const input = page.getByRole('textbox', { name: '/path/to/project' });
|
||||
const worktreePicker = page.getByTestId('worktree-attach-picker');
|
||||
@@ -333,11 +334,9 @@ export const createAgent = async (page: Page, message: string) => {
|
||||
|
||||
// Expo Router navigations can be "same-document" updates, so avoid waiting for a full `load`.
|
||||
await page.waitForURL(/\/agent\//, { waitUntil: 'commit' });
|
||||
const userBubble = page
|
||||
.getByRole('button', { name: 'Copy message' })
|
||||
.filter({ hasText: message })
|
||||
.first();
|
||||
await expect(userBubble).toBeVisible();
|
||||
await expect(page.getByText(message, { exact: true }).first()).toBeVisible({
|
||||
timeout: 30000,
|
||||
});
|
||||
};
|
||||
|
||||
export interface AgentConfig {
|
||||
|
||||
@@ -0,0 +1,370 @@
|
||||
import { test, expect, type Page } from "./fixtures";
|
||||
import {
|
||||
createAgent,
|
||||
ensureHostSelected,
|
||||
gotoHome,
|
||||
setWorkingDirectory,
|
||||
} from "./helpers/app";
|
||||
import { createTempGitRepo } from "./helpers/workspace";
|
||||
|
||||
function parseAgentFromUrl(url: string): { serverId: string; agentId: string } {
|
||||
const pathname = (() => {
|
||||
try {
|
||||
return new URL(url).pathname;
|
||||
} catch {
|
||||
return url;
|
||||
}
|
||||
})();
|
||||
|
||||
const modernMatch = pathname.match(/\/h\/([^/]+)\/agent\/([^/?#]+)/);
|
||||
if (modernMatch) {
|
||||
return {
|
||||
serverId: decodeURIComponent(modernMatch[1]),
|
||||
agentId: decodeURIComponent(modernMatch[2]),
|
||||
};
|
||||
}
|
||||
|
||||
const legacyMatch = pathname.match(/\/agent\/([^/]+)\/([^/?#]+)/);
|
||||
if (legacyMatch) {
|
||||
return {
|
||||
serverId: decodeURIComponent(legacyMatch[1]),
|
||||
agentId: decodeURIComponent(legacyMatch[2]),
|
||||
};
|
||||
}
|
||||
|
||||
throw new Error(`Expected /h/:serverId/agent/:agentId URL, got ${url}`);
|
||||
}
|
||||
|
||||
async function openAgentFromSidebar(page: Page, serverId: string, agentId: string): Promise<void> {
|
||||
await gotoHome(page);
|
||||
const row = page.getByTestId(`agent-row-${serverId}-${agentId}`).first();
|
||||
await expect(row).toBeVisible({ timeout: 30000 });
|
||||
await row.click();
|
||||
await expect
|
||||
.poll(
|
||||
() => {
|
||||
try {
|
||||
const parsed = parseAgentFromUrl(page.url());
|
||||
return `${parsed.serverId}:${parsed.agentId}`;
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
},
|
||||
{ timeout: 30000 }
|
||||
)
|
||||
.toBe(`${serverId}:${agentId}`);
|
||||
}
|
||||
|
||||
async function openNewAgentDraft(page: Page): Promise<void> {
|
||||
await gotoHome(page);
|
||||
const newAgentButton = page.getByTestId("sidebar-new-agent").first();
|
||||
await expect(newAgentButton).toBeVisible({ timeout: 30000 });
|
||||
await newAgentButton.click();
|
||||
await expect(page).toHaveURL(/\/agent\/?$/, { timeout: 30000 });
|
||||
await expect(
|
||||
page.locator('[data-testid="working-directory-select"]:visible').first()
|
||||
).toBeVisible({
|
||||
timeout: 30000,
|
||||
});
|
||||
}
|
||||
|
||||
async function openTerminalsPanel(page: Page): Promise<void> {
|
||||
let header = page.locator('[data-testid="explorer-header"]:visible').first();
|
||||
if (!(await header.isVisible().catch(() => false))) {
|
||||
const toggle = page.getByRole("button", {
|
||||
name: /open explorer|close explorer|toggle explorer/i,
|
||||
});
|
||||
if (await toggle.first().isVisible().catch(() => false)) {
|
||||
await toggle.first().click();
|
||||
}
|
||||
}
|
||||
|
||||
header = page.locator('[data-testid="explorer-header"]:visible').first();
|
||||
await expect(header).toBeVisible({ timeout: 30000 });
|
||||
|
||||
const terminalsTab = page.getByTestId("explorer-tab-terminals").first();
|
||||
await expect(terminalsTab).toBeVisible({ timeout: 30000 });
|
||||
await terminalsTab.click();
|
||||
|
||||
await expect(page.getByTestId("terminals-header").first()).toBeVisible({
|
||||
timeout: 30000,
|
||||
});
|
||||
|
||||
await expect(page.getByTestId("terminal-surface").first()).toBeVisible({
|
||||
timeout: 30000,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
async function selectNewestTerminalTab(page: Page): Promise<void> {
|
||||
const tabs = page.locator('[data-testid^="terminal-tab-"]');
|
||||
await expect(tabs.first()).toBeVisible({ timeout: 30000 });
|
||||
await expect
|
||||
.poll(async () => await tabs.count(), { timeout: 30000 })
|
||||
.toBeGreaterThanOrEqual(2);
|
||||
await tabs.last().click();
|
||||
}
|
||||
|
||||
async function runTerminalCommand(page: Page, command: string, expectedText: string): Promise<void> {
|
||||
const surface = page.getByTestId("terminal-surface").first();
|
||||
await expect(surface).toBeVisible({ timeout: 30000 });
|
||||
await surface.click({ force: true });
|
||||
await page.keyboard.type(command, { delay: 1 });
|
||||
await page.keyboard.press("Enter");
|
||||
await expect(surface).toContainText(expectedText, {
|
||||
timeout: 30000,
|
||||
});
|
||||
}
|
||||
|
||||
async function runTerminalCommandWithPreEnterEcho(
|
||||
page: Page,
|
||||
command: string,
|
||||
expectedText: string
|
||||
): Promise<void> {
|
||||
const surface = page.getByTestId("terminal-surface").first();
|
||||
await expect(surface).toBeVisible({ timeout: 30000 });
|
||||
await surface.click({ force: true });
|
||||
await page.keyboard.type(command, { delay: 1 });
|
||||
await expect(surface).toContainText(command, {
|
||||
timeout: 30000,
|
||||
});
|
||||
await page.keyboard.press("Enter");
|
||||
await expect(surface).toContainText(expectedText, {
|
||||
timeout: 30000,
|
||||
});
|
||||
}
|
||||
|
||||
async function expectAnsiColorApplied(page: Page, marker: string): Promise<void> {
|
||||
await expect
|
||||
.poll(
|
||||
async () =>
|
||||
await page.evaluate((target) => {
|
||||
const terminal = (window as any).__paseoTerminal;
|
||||
if (!terminal?.buffer?.active?.getLine || !terminal?.buffer?.active?.getNullCell) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const buffer = terminal.buffer.active;
|
||||
const nullCell = buffer.getNullCell();
|
||||
const lineCount = buffer.length ?? 0;
|
||||
const cols = terminal.cols ?? 0;
|
||||
|
||||
for (let y = 0; y < lineCount; y += 1) {
|
||||
const line = buffer.getLine(y);
|
||||
if (!line) continue;
|
||||
const lineText = line.translateToString(true);
|
||||
const index = lineText.indexOf(target);
|
||||
if (index === -1) continue;
|
||||
|
||||
for (let x = index; x < index + target.length && x < cols; x += 1) {
|
||||
const cell = line.getCell(x, nullCell);
|
||||
if (!cell) continue;
|
||||
if (!cell.isFgDefault()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}, marker),
|
||||
{ timeout: 30000 }
|
||||
)
|
||||
.toBe(true);
|
||||
}
|
||||
|
||||
test("Terminals tab creates multiple terminals and streams command output", async ({ page }) => {
|
||||
const repo = await createTempGitRepo("paseo-e2e-terminals-");
|
||||
|
||||
try {
|
||||
await openNewAgentDraft(page);
|
||||
await setWorkingDirectory(page, repo.path);
|
||||
await ensureHostSelected(page);
|
||||
await createAgent(page, "Reply with exactly: terminal smoke");
|
||||
|
||||
await openTerminalsPanel(page);
|
||||
await expect(page.locator('[data-testid^="terminal-tab-"]').first()).toBeVisible({
|
||||
timeout: 30000,
|
||||
});
|
||||
|
||||
const preEnterEchoMarker = `typed-echo-${Date.now()}`;
|
||||
await runTerminalCommandWithPreEnterEcho(
|
||||
page,
|
||||
`echo ${preEnterEchoMarker}`,
|
||||
preEnterEchoMarker
|
||||
);
|
||||
|
||||
const ansiMarker = `ansi-red-${Date.now()}`;
|
||||
await runTerminalCommand(
|
||||
page,
|
||||
`printf '\\033[31m${ansiMarker}\\033[0m\\n'`,
|
||||
ansiMarker
|
||||
);
|
||||
await expectAnsiColorApplied(page, ansiMarker);
|
||||
|
||||
const markerOne = `terminal-smoke-one-${Date.now()}`;
|
||||
await runTerminalCommand(page, `echo ${markerOne}`, markerOne);
|
||||
|
||||
await page.getByTestId("terminals-create-button").first().click();
|
||||
await selectNewestTerminalTab(page);
|
||||
|
||||
const markerTwo = `terminal-smoke-two-${Date.now()}`;
|
||||
await runTerminalCommand(page, `echo ${markerTwo}`, markerTwo);
|
||||
} finally {
|
||||
await repo.cleanup();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
test("terminals are shared by agents on the same cwd", async ({ page }) => {
|
||||
const repo = await createTempGitRepo("paseo-e2e-terminal-share-");
|
||||
|
||||
try {
|
||||
await openNewAgentDraft(page);
|
||||
await setWorkingDirectory(page, repo.path);
|
||||
await ensureHostSelected(page);
|
||||
await createAgent(page, "Agent one");
|
||||
const first = parseAgentFromUrl(page.url());
|
||||
|
||||
await openNewAgentDraft(page);
|
||||
await setWorkingDirectory(page, repo.path);
|
||||
await ensureHostSelected(page);
|
||||
await createAgent(page, "Agent two");
|
||||
const second = parseAgentFromUrl(page.url());
|
||||
|
||||
expect(first.serverId).toBe(second.serverId);
|
||||
expect(first.agentId).not.toBe(second.agentId);
|
||||
|
||||
await openAgentFromSidebar(page, first.serverId, first.agentId);
|
||||
await openTerminalsPanel(page);
|
||||
await page.getByTestId("terminals-create-button").first().click();
|
||||
await selectNewestTerminalTab(page);
|
||||
|
||||
await openAgentFromSidebar(page, second.serverId, second.agentId);
|
||||
await openTerminalsPanel(page);
|
||||
await selectNewestTerminalTab(page);
|
||||
|
||||
const sharedMarker = `shared-terminal-${Date.now()}`;
|
||||
await runTerminalCommand(page, `echo ${sharedMarker}`, sharedMarker);
|
||||
|
||||
await openAgentFromSidebar(page, first.serverId, first.agentId);
|
||||
await openTerminalsPanel(page);
|
||||
await selectNewestTerminalTab(page);
|
||||
await expect(page.getByTestId("terminal-surface").first()).toContainText(sharedMarker, {
|
||||
timeout: 30000,
|
||||
});
|
||||
} finally {
|
||||
await repo.cleanup();
|
||||
}
|
||||
});
|
||||
|
||||
async function getTerminalRows(page: Page): Promise<number> {
|
||||
return await page.evaluate(() => {
|
||||
const terminal = (window as { __paseoTerminal?: { rows?: unknown } }).__paseoTerminal;
|
||||
return typeof terminal?.rows === "number" ? terminal.rows : 0;
|
||||
});
|
||||
}
|
||||
|
||||
async function setExplorerContentBottomPadding(page: Page, padding: number): Promise<void> {
|
||||
await page.evaluate((nextPadding) => {
|
||||
const container = document.querySelector<HTMLElement>(
|
||||
'[data-testid="explorer-content-area"]'
|
||||
);
|
||||
if (!container) {
|
||||
return;
|
||||
}
|
||||
container.style.boxSizing = "border-box";
|
||||
container.style.paddingBottom = nextPadding + "px";
|
||||
}, padding);
|
||||
}
|
||||
|
||||
async function getTerminalScrollbackDistance(page: Page): Promise<number> {
|
||||
return await page.evaluate(() => {
|
||||
const terminal = (
|
||||
window as {
|
||||
__paseoTerminal?: {
|
||||
buffer?: { active?: { baseY?: unknown; viewportY?: unknown } };
|
||||
};
|
||||
}
|
||||
).__paseoTerminal;
|
||||
const baseY = terminal?.buffer?.active?.baseY;
|
||||
const viewportY = terminal?.buffer?.active?.viewportY;
|
||||
if (typeof baseY !== "number" || typeof viewportY !== "number") {
|
||||
return 0;
|
||||
}
|
||||
return Math.max(0, baseY - viewportY);
|
||||
});
|
||||
}
|
||||
|
||||
test("terminal viewport resizes and uses xterm scrollback", async ({ page }) => {
|
||||
const repo = await createTempGitRepo("paseo-e2e-terminal-viewport-");
|
||||
|
||||
try {
|
||||
await openNewAgentDraft(page);
|
||||
await setWorkingDirectory(page, repo.path);
|
||||
await ensureHostSelected(page);
|
||||
await createAgent(page, "Viewport and scrollback test");
|
||||
|
||||
await openTerminalsPanel(page);
|
||||
|
||||
const initialViewport = page.viewportSize();
|
||||
if (!initialViewport) {
|
||||
throw new Error("Expected a viewport size");
|
||||
}
|
||||
|
||||
await expect
|
||||
.poll(() => getTerminalRows(page), { timeout: 30000 })
|
||||
.toBeGreaterThan(0);
|
||||
const initialRows = await getTerminalRows(page);
|
||||
|
||||
await setExplorerContentBottomPadding(page, 220);
|
||||
await expect
|
||||
.poll(() => getTerminalRows(page), { timeout: 30000 })
|
||||
.toBeLessThan(initialRows);
|
||||
|
||||
await setExplorerContentBottomPadding(page, 0);
|
||||
await expect
|
||||
.poll(() => getTerminalRows(page), { timeout: 30000 })
|
||||
.toBeGreaterThanOrEqual(initialRows);
|
||||
|
||||
const reducedHeight = Math.max(520, initialViewport.height - 220);
|
||||
await page.setViewportSize({
|
||||
width: initialViewport.width,
|
||||
height: reducedHeight,
|
||||
});
|
||||
|
||||
await expect
|
||||
.poll(() => getTerminalRows(page), { timeout: 30000 })
|
||||
.toBeLessThan(initialRows);
|
||||
|
||||
await page.setViewportSize(initialViewport);
|
||||
|
||||
await expect
|
||||
.poll(() => getTerminalRows(page), { timeout: 30000 })
|
||||
.toBeGreaterThanOrEqual(initialRows);
|
||||
|
||||
const scrollbackMarker = `scrollback-${Date.now()}`;
|
||||
await runTerminalCommand(
|
||||
page,
|
||||
`for i in $(seq 1 180); do echo ${scrollbackMarker}-$i; done`,
|
||||
`${scrollbackMarker}-180`
|
||||
);
|
||||
|
||||
const surface = page.getByTestId("terminal-surface").first();
|
||||
await surface.hover();
|
||||
await page.mouse.wheel(0, -3000);
|
||||
|
||||
await expect
|
||||
.poll(() => getTerminalScrollbackDistance(page), { timeout: 30000 })
|
||||
.toBeGreaterThan(0);
|
||||
|
||||
const distanceAfterScrollUp = await getTerminalScrollbackDistance(page);
|
||||
|
||||
await surface.hover();
|
||||
await page.mouse.wheel(0, 3000);
|
||||
|
||||
await expect
|
||||
.poll(() => getTerminalScrollbackDistance(page), { timeout: 30000 })
|
||||
.toBeLessThan(distanceAfterScrollUp);
|
||||
} finally {
|
||||
await repo.cleanup();
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user