mirror of
https://github.com/getpaseo/paseo.git
synced 2026-07-29 12:01:31 +00:00
Merge branch 'archive-worktree-redirect'
This commit is contained in:
@@ -120,14 +120,25 @@ async function selectAttachWorktree(page: Page, branchName: string) {
|
||||
await page.getByTestId('worktree-attach-toggle').click();
|
||||
const picker = page.getByTestId('worktree-attach-picker');
|
||||
await expect(picker).toBeVisible();
|
||||
|
||||
// Wait a bit for the worktree list to load
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
await picker.click();
|
||||
|
||||
// Wait a bit for animation
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
const sheet = page.getByLabel('Bottom Sheet', { exact: true });
|
||||
const backdrop = page.getByRole('button', { name: 'Bottom sheet backdrop' }).first();
|
||||
|
||||
await expect.poll(async () => {
|
||||
const sheetVisible = await sheet.isVisible().catch(() => false);
|
||||
const backdropVisible = await backdrop.isVisible().catch(() => false);
|
||||
return sheetVisible || backdropVisible;
|
||||
}).toBeTruthy();
|
||||
// Also check if branch name is visible directly
|
||||
const branchVisible = await page.getByText(branchName, { exact: true }).first().isVisible().catch(() => false);
|
||||
return sheetVisible || backdropVisible || branchVisible;
|
||||
}, { timeout: 10000 }).toBeTruthy();
|
||||
const sheetVisible = await sheet.isVisible().catch(() => false);
|
||||
const scope = sheetVisible ? sheet : page;
|
||||
const option = scope.getByText(branchName, { exact: true }).first();
|
||||
@@ -294,7 +305,8 @@ test('checkout-first Changes panel ship loop', async ({ page }) => {
|
||||
});
|
||||
|
||||
await getChangesActionButton(page, 'Archive').click();
|
||||
await page.getByTestId('sidebar-new-agent').click();
|
||||
// Archiving a worktree deletes agents and redirects to home
|
||||
await expect(page).toHaveURL(/\/$/, { timeout: 30000 });
|
||||
await setWorkingDirectory(page, repo.path);
|
||||
await ensureHostSelected(page);
|
||||
await page.getByTestId('worktree-attach-toggle').click();
|
||||
|
||||
@@ -1,9 +1,21 @@
|
||||
import { test, expect, type Page } from '@playwright/test';
|
||||
import { test as base, expect, type Page } from '@playwright/test';
|
||||
|
||||
// Extend base test to provide dynamic baseURL from global-setup
|
||||
const test = base.extend({
|
||||
baseURL: async ({}, use) => {
|
||||
const metroPort = process.env.E2E_METRO_PORT;
|
||||
if (!metroPort) {
|
||||
throw new Error('E2E_METRO_PORT not set - globalSetup must run first');
|
||||
}
|
||||
await use(`http://localhost:${metroPort}`);
|
||||
},
|
||||
});
|
||||
|
||||
const consoleEntries = new WeakMap<Page, string[]>();
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
const daemonPort = process.env.E2E_DAEMON_PORT;
|
||||
const metroPort = process.env.E2E_METRO_PORT;
|
||||
if (!daemonPort) {
|
||||
throw new Error(
|
||||
'E2E_DAEMON_PORT is not set. Refusing to run e2e against the default daemon (e.g. localhost:6767). ' +
|
||||
@@ -16,6 +28,11 @@ test.beforeEach(async ({ page }) => {
|
||||
'Fix Playwright globalSetup to start an isolated test daemon and export its port.'
|
||||
);
|
||||
}
|
||||
if (!metroPort) {
|
||||
throw new Error(
|
||||
'E2E_METRO_PORT is not set. Ensure Playwright `globalSetup` starts Metro and exports E2E_METRO_PORT.'
|
||||
);
|
||||
}
|
||||
|
||||
// Hard guardrail: never allow tests to hit the developer's default daemon.
|
||||
// This blocks both HTTP and WS attempts to :6767 (before any navigation).
|
||||
|
||||
@@ -41,6 +41,7 @@ async function waitForServer(port: number, timeout = 15000): Promise<void> {
|
||||
}
|
||||
|
||||
let daemonProcess: ChildProcess | null = null;
|
||||
let metroProcess: ChildProcess | null = null;
|
||||
let paseoHome: string | null = null;
|
||||
let relayServer: RelayServer | null = null;
|
||||
|
||||
@@ -74,11 +75,35 @@ function decodeOfferFromFragmentUrl(url: string): OfferPayload {
|
||||
export default async function globalSetup() {
|
||||
const port = await getAvailablePort();
|
||||
const relayPort = await getAvailablePort();
|
||||
const metroPort = await getAvailablePort();
|
||||
paseoHome = await mkdtemp(path.join(tmpdir(), 'paseo-e2e-home-'));
|
||||
|
||||
relayServer = createRelayServer({ port: relayPort, host: '127.0.0.1' });
|
||||
await relayServer.start();
|
||||
|
||||
// Start Metro bundler on dynamic port
|
||||
const appDir = path.resolve(__dirname, '..');
|
||||
metroProcess = spawn('npx', ['expo', 'start', '--web', '--port', String(metroPort)], {
|
||||
cwd: appDir,
|
||||
env: {
|
||||
...process.env,
|
||||
BROWSER: 'none', // Don't auto-open browser
|
||||
},
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
detached: false,
|
||||
});
|
||||
|
||||
metroProcess.stdout?.on('data', (data: Buffer) => {
|
||||
const lines = data.toString().split('\n').filter((l) => l.trim());
|
||||
for (const line of lines) {
|
||||
console.log(`[metro] ${line}`);
|
||||
}
|
||||
});
|
||||
|
||||
metroProcess.stderr?.on('data', (data: Buffer) => {
|
||||
console.error(`[metro] ${data.toString().trim()}`);
|
||||
});
|
||||
|
||||
const serverDir = path.resolve(__dirname, '../../..', 'packages/server');
|
||||
const tsxBin = execSync('which tsx').toString().trim();
|
||||
|
||||
@@ -95,7 +120,7 @@ export default async function globalSetup() {
|
||||
PASEO_HOME: paseoHome,
|
||||
PASEO_LISTEN: `0.0.0.0:${port}`,
|
||||
PASEO_RELAY_ENDPOINT: `127.0.0.1:${relayPort}`,
|
||||
PASEO_CORS_ORIGINS: 'http://localhost:8081',
|
||||
PASEO_CORS_ORIGINS: `http://localhost:${metroPort}`,
|
||||
NODE_ENV: 'development',
|
||||
},
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
@@ -138,7 +163,11 @@ export default async function globalSetup() {
|
||||
console.error(`[daemon] ${data.toString().trim()}`);
|
||||
});
|
||||
|
||||
await waitForServer(port);
|
||||
// Wait for both daemon and Metro to be ready
|
||||
await Promise.all([
|
||||
waitForServer(port),
|
||||
waitForServer(metroPort, 120000), // Metro can take longer to start
|
||||
]);
|
||||
|
||||
// Wait for daemon to emit a pairing offer (includes relay session ID).
|
||||
await Promise.race([
|
||||
@@ -155,13 +184,18 @@ export default async function globalSetup() {
|
||||
process.env.E2E_DAEMON_PORT = String(port);
|
||||
process.env.E2E_RELAY_PORT = String(relayPort);
|
||||
process.env.E2E_RELAY_SESSION_ID = offer.sessionId;
|
||||
console.log(`[e2e] Test daemon started on port ${port}, home: ${paseoHome}`);
|
||||
process.env.E2E_METRO_PORT = String(metroPort);
|
||||
console.log(`[e2e] Test daemon started on port ${port}, Metro on port ${metroPort}, home: ${paseoHome}`);
|
||||
|
||||
return async () => {
|
||||
if (daemonProcess) {
|
||||
daemonProcess.kill('SIGTERM');
|
||||
daemonProcess = null;
|
||||
}
|
||||
if (metroProcess) {
|
||||
metroProcess.kill('SIGTERM');
|
||||
metroProcess = null;
|
||||
}
|
||||
if (relayServer) {
|
||||
await relayServer.stop();
|
||||
relayServer = null;
|
||||
|
||||
@@ -213,6 +213,8 @@ export const setWorkingDirectory = async (page: Page, directory: string) => {
|
||||
const useOption = page.getByText(`Use "${directory}"`);
|
||||
await expect(useOption).toBeVisible();
|
||||
await useOption.click({ force: true });
|
||||
// Wait for UI to update after clicking "Use"
|
||||
await page.waitForTimeout(500);
|
||||
const normalizedDirectory = directory.startsWith('/var/')
|
||||
? `/private${directory}`
|
||||
: directory;
|
||||
@@ -220,7 +222,7 @@ export const setWorkingDirectory = async (page: Page, directory: string) => {
|
||||
await expect.poll(async () => {
|
||||
const text = await workingDirectoryContainer.innerText();
|
||||
return text.includes(directory) || text.includes(normalizedDirectory);
|
||||
}).toBe(true);
|
||||
}, { timeout: 15000 }).toBe(true);
|
||||
};
|
||||
|
||||
export const ensureHostSelected = async (page: Page) => {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { defineConfig, devices } from '@playwright/test';
|
||||
|
||||
const baseURL = process.env.E2E_BASE_URL ?? 'http://localhost:8081';
|
||||
// E2E_METRO_PORT is set dynamically by global-setup.ts after finding a free port
|
||||
// This allows multiple test runs in parallel across different worktrees
|
||||
const baseURL = process.env.E2E_BASE_URL ?? `http://localhost:${process.env.E2E_METRO_PORT ?? '8081'}`;
|
||||
|
||||
export default defineConfig({
|
||||
testDir: './e2e',
|
||||
@@ -24,4 +26,5 @@ export default defineConfig({
|
||||
use: { ...devices['Desktop Chrome'] },
|
||||
},
|
||||
],
|
||||
// Note: Metro is started by global-setup.ts on a dynamic port to allow parallel test runs
|
||||
});
|
||||
|
||||
@@ -562,7 +562,7 @@ function AgentScreenContent({
|
||||
|
||||
if (!agent) {
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<View style={styles.container} testID="agent-not-found">
|
||||
<MenuHeader title="Agent" />
|
||||
<View style={styles.errorContainer}>
|
||||
<Text style={styles.errorText}>Agent not found</Text>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useState, useCallback, useEffect, useId, useMemo, useRef, memo, type ReactElement } from "react";
|
||||
import { useRouter } from "expo-router";
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
@@ -345,6 +346,7 @@ interface GitDiffPaneProps {
|
||||
|
||||
export function GitDiffPane({ serverId, agentId }: GitDiffPaneProps) {
|
||||
const { theme } = useUnistyles();
|
||||
const router = useRouter();
|
||||
const queryClient = useQueryClient();
|
||||
const client = useSessionStore(
|
||||
(state) => state.sessions[serverId]?.client ?? null
|
||||
@@ -555,6 +557,7 @@ export function GitDiffPane({ serverId, agentId }: GitDiffPaneProps) {
|
||||
predicate: (query) =>
|
||||
Array.isArray(query.queryKey) && query.queryKey[0] === "paseoWorktreeList",
|
||||
});
|
||||
router.replace("/");
|
||||
},
|
||||
onError: (err) => {
|
||||
const message = err instanceof Error ? err.message : "Failed to archive worktree";
|
||||
|
||||
Reference in New Issue
Block a user