Files
paseo/packages/app/e2e/helpers/terminal-dsl.ts
T

150 lines
4.4 KiB
TypeScript

import type { Page } from "@playwright/test";
import type { TerminalActivity, TerminalActivityState } from "@getpaseo/protocol/terminal-activity";
import { createTempGitRepo } from "./workspace";
import { navigateToTerminal, setupDeterministicPrompt } from "./terminal-perf";
import { connectSeedClient, type SeedDaemonClient } from "./seed-client";
interface TempRepo {
path: string;
cleanup: () => Promise<void>;
}
export interface TerminalInstance {
id: string;
name: string;
cwd: string;
}
interface CreateTerminalInput {
name: string;
command?: string;
args?: string[];
}
function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
export class TerminalE2EHarness {
readonly client: SeedDaemonClient;
readonly tempRepo: TempRepo;
readonly workspaceId: string;
private constructor(input: {
client: SeedDaemonClient;
tempRepo: TempRepo;
workspaceId: string;
}) {
this.client = input.client;
this.tempRepo = input.tempRepo;
this.workspaceId = input.workspaceId;
}
static async create(input: { tempPrefix: string }): Promise<TerminalE2EHarness> {
const tempRepo = await createTempGitRepo(input.tempPrefix);
const client = await connectSeedClient();
const seedResult = await client.openProject(tempRepo.path);
if (!seedResult.workspace) {
await client.close().catch(() => {});
await tempRepo.cleanup().catch(() => {});
throw new Error(seedResult.error ?? "Failed to seed workspace");
}
return new TerminalE2EHarness({
client,
tempRepo,
workspaceId: seedResult.workspace.id,
});
}
async cleanup(): Promise<void> {
await this.client.close().catch(() => {});
await this.tempRepo.cleanup().catch(() => {});
}
async createTerminal(input: CreateTerminalInput): Promise<TerminalInstance> {
const options =
input.command || input.args
? {
command: input.command,
args: input.args,
workspaceId: this.workspaceId,
}
: { workspaceId: this.workspaceId };
const result = await this.client.createTerminal(
this.tempRepo.path,
input.name,
undefined,
options,
);
if (!result.terminal) {
throw new Error(`Failed to create terminal: ${result.error}`);
}
return result.terminal;
}
async waitForTerminalActivity(input: {
terminalId: string;
state: TerminalActivityState | null;
attentionReason?: TerminalActivity["attentionReason"] | null;
timeoutMs?: number;
}): Promise<void> {
const timeoutMs = input.timeoutMs ?? 10_000;
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
const result = await this.client.listTerminals(this.tempRepo.path, undefined, {
workspaceId: this.workspaceId,
});
const terminal = result.terminals.find((entry) => entry.id === input.terminalId);
const activity = terminal?.activity ?? null;
const attentionMatches =
input.attentionReason === undefined ||
(activity?.attentionReason ?? null) === input.attentionReason;
if ((activity?.state ?? null) === input.state && attentionMatches) {
return;
}
await sleep(50);
}
const attentionSuffix =
input.attentionReason === undefined
? ""
: ` with attention ${input.attentionReason ?? "none"}`;
throw new Error(
`Timed out waiting for terminal ${input.terminalId} activity state ${input.state ?? "unknown"}${attentionSuffix}`,
);
}
async killTerminal(terminalId: string): Promise<void> {
await this.client.killTerminal(terminalId).catch(() => {});
}
async openTerminal(page: Page, input: { terminalId: string }): Promise<void> {
await navigateToTerminal(page, {
workspaceId: this.workspaceId,
terminalId: input.terminalId,
});
}
terminalSurface(page: Page) {
return page.locator('[data-testid="terminal-surface"]');
}
async setupPrompt(page: Page, sentinel?: string): Promise<void> {
await setupDeterministicPrompt(page, sentinel);
}
}
export async function withTerminalInApp<T>(
page: Page,
harness: TerminalE2EHarness,
input: { name: string },
fn: (terminal: TerminalInstance) => Promise<T>,
): Promise<T> {
const terminal = await harness.createTerminal({ name: input.name });
try {
await harness.openTerminal(page, { terminalId: terminal.id });
return await fn(terminal);
} finally {
await harness.killTerminal(terminal.id);
}
}