Update files
This commit is contained in:
@@ -120,14 +120,25 @@ async function selectAttachWorktree(page: Page, branchName: string) {
|
|||||||
await page.getByTestId('worktree-attach-toggle').click();
|
await page.getByTestId('worktree-attach-toggle').click();
|
||||||
const picker = page.getByTestId('worktree-attach-picker');
|
const picker = page.getByTestId('worktree-attach-picker');
|
||||||
await expect(picker).toBeVisible();
|
await expect(picker).toBeVisible();
|
||||||
|
|
||||||
|
// Wait a bit for the worktree list to load
|
||||||
|
await page.waitForTimeout(1000);
|
||||||
|
|
||||||
await picker.click();
|
await picker.click();
|
||||||
|
|
||||||
|
// Wait a bit for animation
|
||||||
|
await page.waitForTimeout(500);
|
||||||
|
|
||||||
const sheet = page.getByLabel('Bottom Sheet', { exact: true });
|
const sheet = page.getByLabel('Bottom Sheet', { exact: true });
|
||||||
const backdrop = page.getByRole('button', { name: 'Bottom sheet backdrop' }).first();
|
const backdrop = page.getByRole('button', { name: 'Bottom sheet backdrop' }).first();
|
||||||
|
|
||||||
await expect.poll(async () => {
|
await expect.poll(async () => {
|
||||||
const sheetVisible = await sheet.isVisible().catch(() => false);
|
const sheetVisible = await sheet.isVisible().catch(() => false);
|
||||||
const backdropVisible = await backdrop.isVisible().catch(() => false);
|
const backdropVisible = await backdrop.isVisible().catch(() => false);
|
||||||
return sheetVisible || backdropVisible;
|
// Also check if branch name is visible directly
|
||||||
}).toBeTruthy();
|
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 sheetVisible = await sheet.isVisible().catch(() => false);
|
||||||
const scope = sheetVisible ? sheet : page;
|
const scope = sheetVisible ? sheet : page;
|
||||||
const option = scope.getByText(branchName, { exact: true }).first();
|
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 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 setWorkingDirectory(page, repo.path);
|
||||||
await ensureHostSelected(page);
|
await ensureHostSelected(page);
|
||||||
await page.getByTestId('worktree-attach-toggle').click();
|
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[]>();
|
const consoleEntries = new WeakMap<Page, string[]>();
|
||||||
|
|
||||||
test.beforeEach(async ({ page }) => {
|
test.beforeEach(async ({ page }) => {
|
||||||
const daemonPort = process.env.E2E_DAEMON_PORT;
|
const daemonPort = process.env.E2E_DAEMON_PORT;
|
||||||
|
const metroPort = process.env.E2E_METRO_PORT;
|
||||||
if (!daemonPort) {
|
if (!daemonPort) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
'E2E_DAEMON_PORT is not set. Refusing to run e2e against the default daemon (e.g. localhost:6767). ' +
|
'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.'
|
'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.
|
// Hard guardrail: never allow tests to hit the developer's default daemon.
|
||||||
// This blocks both HTTP and WS attempts to :6767 (before any navigation).
|
// 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 daemonProcess: ChildProcess | null = null;
|
||||||
|
let metroProcess: ChildProcess | null = null;
|
||||||
let paseoHome: string | null = null;
|
let paseoHome: string | null = null;
|
||||||
let relayServer: RelayServer | null = null;
|
let relayServer: RelayServer | null = null;
|
||||||
|
|
||||||
@@ -74,11 +75,35 @@ function decodeOfferFromFragmentUrl(url: string): OfferPayload {
|
|||||||
export default async function globalSetup() {
|
export default async function globalSetup() {
|
||||||
const port = await getAvailablePort();
|
const port = await getAvailablePort();
|
||||||
const relayPort = await getAvailablePort();
|
const relayPort = await getAvailablePort();
|
||||||
|
const metroPort = await getAvailablePort();
|
||||||
paseoHome = await mkdtemp(path.join(tmpdir(), 'paseo-e2e-home-'));
|
paseoHome = await mkdtemp(path.join(tmpdir(), 'paseo-e2e-home-'));
|
||||||
|
|
||||||
relayServer = createRelayServer({ port: relayPort, host: '127.0.0.1' });
|
relayServer = createRelayServer({ port: relayPort, host: '127.0.0.1' });
|
||||||
await relayServer.start();
|
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 serverDir = path.resolve(__dirname, '../../..', 'packages/server');
|
||||||
const tsxBin = execSync('which tsx').toString().trim();
|
const tsxBin = execSync('which tsx').toString().trim();
|
||||||
|
|
||||||
@@ -95,7 +120,7 @@ export default async function globalSetup() {
|
|||||||
PASEO_HOME: paseoHome,
|
PASEO_HOME: paseoHome,
|
||||||
PASEO_LISTEN: `0.0.0.0:${port}`,
|
PASEO_LISTEN: `0.0.0.0:${port}`,
|
||||||
PASEO_RELAY_ENDPOINT: `127.0.0.1:${relayPort}`,
|
PASEO_RELAY_ENDPOINT: `127.0.0.1:${relayPort}`,
|
||||||
PASEO_CORS_ORIGINS: 'http://localhost:8081',
|
PASEO_CORS_ORIGINS: `http://localhost:${metroPort}`,
|
||||||
NODE_ENV: 'development',
|
NODE_ENV: 'development',
|
||||||
},
|
},
|
||||||
stdio: ['ignore', 'pipe', 'pipe'],
|
stdio: ['ignore', 'pipe', 'pipe'],
|
||||||
@@ -138,7 +163,11 @@ export default async function globalSetup() {
|
|||||||
console.error(`[daemon] ${data.toString().trim()}`);
|
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).
|
// Wait for daemon to emit a pairing offer (includes relay session ID).
|
||||||
await Promise.race([
|
await Promise.race([
|
||||||
@@ -155,13 +184,18 @@ export default async function globalSetup() {
|
|||||||
process.env.E2E_DAEMON_PORT = String(port);
|
process.env.E2E_DAEMON_PORT = String(port);
|
||||||
process.env.E2E_RELAY_PORT = String(relayPort);
|
process.env.E2E_RELAY_PORT = String(relayPort);
|
||||||
process.env.E2E_RELAY_SESSION_ID = offer.sessionId;
|
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 () => {
|
return async () => {
|
||||||
if (daemonProcess) {
|
if (daemonProcess) {
|
||||||
daemonProcess.kill('SIGTERM');
|
daemonProcess.kill('SIGTERM');
|
||||||
daemonProcess = null;
|
daemonProcess = null;
|
||||||
}
|
}
|
||||||
|
if (metroProcess) {
|
||||||
|
metroProcess.kill('SIGTERM');
|
||||||
|
metroProcess = null;
|
||||||
|
}
|
||||||
if (relayServer) {
|
if (relayServer) {
|
||||||
await relayServer.stop();
|
await relayServer.stop();
|
||||||
relayServer = null;
|
relayServer = null;
|
||||||
|
|||||||
@@ -213,6 +213,8 @@ export const setWorkingDirectory = async (page: Page, directory: string) => {
|
|||||||
const useOption = page.getByText(`Use "${directory}"`);
|
const useOption = page.getByText(`Use "${directory}"`);
|
||||||
await expect(useOption).toBeVisible();
|
await expect(useOption).toBeVisible();
|
||||||
await useOption.click({ force: true });
|
await useOption.click({ force: true });
|
||||||
|
// Wait for UI to update after clicking "Use"
|
||||||
|
await page.waitForTimeout(500);
|
||||||
const normalizedDirectory = directory.startsWith('/var/')
|
const normalizedDirectory = directory.startsWith('/var/')
|
||||||
? `/private${directory}`
|
? `/private${directory}`
|
||||||
: directory;
|
: directory;
|
||||||
@@ -220,7 +222,7 @@ export const setWorkingDirectory = async (page: Page, directory: string) => {
|
|||||||
await expect.poll(async () => {
|
await expect.poll(async () => {
|
||||||
const text = await workingDirectoryContainer.innerText();
|
const text = await workingDirectoryContainer.innerText();
|
||||||
return text.includes(directory) || text.includes(normalizedDirectory);
|
return text.includes(directory) || text.includes(normalizedDirectory);
|
||||||
}).toBe(true);
|
}, { timeout: 15000 }).toBe(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ensureHostSelected = async (page: Page) => {
|
export const ensureHostSelected = async (page: Page) => {
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { defineConfig, devices } from '@playwright/test';
|
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({
|
export default defineConfig({
|
||||||
testDir: './e2e',
|
testDir: './e2e',
|
||||||
@@ -24,4 +26,5 @@ export default defineConfig({
|
|||||||
use: { ...devices['Desktop Chrome'] },
|
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) {
|
if (!agent) {
|
||||||
return (
|
return (
|
||||||
<View style={styles.container}>
|
<View style={styles.container} testID="agent-not-found">
|
||||||
<MenuHeader title="Agent" />
|
<MenuHeader title="Agent" />
|
||||||
<View style={styles.errorContainer}>
|
<View style={styles.errorContainer}>
|
||||||
<Text style={styles.errorText}>Agent not found</Text>
|
<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 { useState, useCallback, useEffect, useId, useMemo, useRef, memo, type ReactElement } from "react";
|
||||||
|
import { useRouter } from "expo-router";
|
||||||
import {
|
import {
|
||||||
View,
|
View,
|
||||||
Text,
|
Text,
|
||||||
@@ -13,7 +14,6 @@ import { ScrollView, type ScrollView as ScrollViewType } from "react-native-gest
|
|||||||
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
||||||
import { ChevronRight, GitBranch } from "lucide-react-native";
|
import { ChevronRight, GitBranch } from "lucide-react-native";
|
||||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
import { useRouter } from "expo-router";
|
|
||||||
import { useSessionStore } from "@/stores/session-store";
|
import { useSessionStore } from "@/stores/session-store";
|
||||||
import {
|
import {
|
||||||
useCheckoutDiffQuery,
|
useCheckoutDiffQuery,
|
||||||
@@ -557,7 +557,6 @@ export function GitDiffPane({ serverId, agentId }: GitDiffPaneProps) {
|
|||||||
predicate: (query) =>
|
predicate: (query) =>
|
||||||
Array.isArray(query.queryKey) && query.queryKey[0] === "paseoWorktreeList",
|
Array.isArray(query.queryKey) && query.queryKey[0] === "paseoWorktreeList",
|
||||||
});
|
});
|
||||||
// Archiving a worktree also archives all its agents, so navigate to new agent screen
|
|
||||||
router.replace("/");
|
router.replace("/");
|
||||||
},
|
},
|
||||||
onError: (err) => {
|
onError: (err) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user