fix the setup

This commit is contained in:
Lifei Zhou
2026-02-25 20:29:57 +11:00
parent ca40ef227f
commit ebf0832fcc
7 changed files with 109 additions and 61 deletions
+2 -2
View File
@@ -22,8 +22,8 @@
"bundle:intel": "node scripts/prepare-platform-binaries.js && npm run make -- --arch=x64 && cd out/Goose-darwin-x64 && ditto -c -k --sequesterRsrc --keepParent Goose.app Goose_intel_mac.zip",
"debug": "echo 'run --remote-debugging-port=8315' && lldb out/Goose-darwin-arm64/Goose.app",
"test-e2e": "npm run generate-api && playwright test",
"test-e2e:packaged": "npm run generate-api && NO_TEST_OVERLAY=1 playwright test tests/e2e/app.spec.ts",
"test-e2e:packaged:video": "npm run playwright:install && npm run generate-api && PW_ELECTRON_VIDEO=1 NO_TEST_OVERLAY=1 playwright test tests/e2e/app.spec.ts",
"test-e2e:non-video": "npm run generate-api && NO_TEST_OVERLAY=1 playwright test tests/e2e/app.spec.ts",
"test-e2e:video": "npm run playwright:install && npm run generate-api && PW_ELECTRON_VIDEO=1 NO_TEST_OVERLAY=1 playwright test tests/e2e/app.spec.ts",
"test-e2e:dev": "npm run generate-api && playwright test --reporter=list --retries=0 --max-failures=1",
"test-e2e:ui": "npm run generate-api && playwright test --ui",
"test-e2e:debug": "npm run generate-api && playwright test --debug",
@@ -54,12 +54,14 @@ export const SessionsList: React.FC<SessionsListProps> = ({
exit={{ height: 0, opacity: 0 }}
transition={{ duration: 0.2 }}
className="overflow-hidden mt-[2px]"
data-testid="chat-sessions-list"
>
<div className="bg-background-primary rounded-lg py-1 flex flex-col gap-[2px]">
{/* New Chat button as first item */}
{onNewChat && (
<div
onClick={onNewChat}
data-testid="chat-start-new"
className={cn(
'w-full text-left py-1.5 px-2 text-xs rounded-md',
'hover:bg-background-tertiary transition-colors',
@@ -126,6 +128,7 @@ export const SessionsList: React.FC<SessionsListProps> = ({
{onShowAll && sessions.length > 0 && (
<div
onClick={onShowAll}
data-testid="chat-show-all"
className={cn(
'w-full text-left py-1.5 px-2 text-xs rounded-md',
'hover:bg-background-tertiary transition-colors',
@@ -98,6 +98,7 @@ export const AlertBox = ({ alert, className }: AlertBoxProps) => {
return (
<div
data-testid="alert-box"
className={cn('flex flex-col gap-2 px-3 py-3', alertStyles[alert.type], className)}
onMouseDown={(e) => {
// Prevent popover from closing when clicking inside the alert box
@@ -251,14 +252,17 @@ export const AlertBox = ({ alert, className }: AlertBoxProps) => {
</div>
<div className="flex justify-between items-baseline text-[11px]">
<div className="flex gap-1 items-baseline">
<span className={'dark:text-black/60 text-white/60'}>
<span
className={'dark:text-black/60 text-white/60'}
data-testid="alert-progress-current"
>
{formatTokenCount(alert.progress!.current)}
</span>
<span className={'dark:text-black/40 text-white/40'}>
{Math.round((alert.progress!.current / alert.progress!.total) * 100)}%
</span>
</div>
<span className={'dark:text-black/60 text-white/60'}>
<span className={'dark:text-black/60 text-white/60'} data-testid="alert-progress-total">
{formatTokenCount(alert.progress!.total)}
</span>
</div>
@@ -205,6 +205,7 @@ export default function BottomMenuAlertPopover({ alerts }: AlertPopoverProps) {
<div className="relative">
<button
ref={triggerRef}
data-testid="bottom-menu-alert-trigger"
className="cursor-pointer flex items-center justify-center min-w-5 min-h-5 rounded hover:bg-background-secondary"
onClick={() => {
setIsOpen(true);
+24 -6
View File
@@ -98,8 +98,8 @@ async function selectProvider(mainWindow: Page, provider: Provider) {
// Navigate to home/chat after provider configuration
console.log('Navigating to home/chat...');
const homeButton = mainWindow.getByTestId('sidebar-home-button');
if (await homeButton.isVisible().catch(() => false)) {
const homeButton = mainWindow.getByRole('button', { name: /^home$/i }).first();
if (await homeButton.isVisible({ timeout: 3000 }).catch(() => false)) {
await homeButton.click();
}
@@ -108,6 +108,24 @@ async function selectProvider(mainWindow: Page, provider: Provider) {
}
async function openSettings(mainWindow: Page) {
const settingsButton = mainWindow.getByRole('button', { name: /^settings$/i }).first();
await expect(settingsButton).toBeVisible({ timeout: DEFAULT_TIMEOUT });
await settingsButton.click();
}
async function openHome(mainWindow: Page) {
const homeButton = mainWindow.getByRole('button', { name: /^home$/i }).first();
await expect(homeButton).toBeVisible({ timeout: DEFAULT_TIMEOUT });
await homeButton.click();
}
async function openExtensions(mainWindow: Page) {
const extensionsButton = mainWindow.getByRole('button', { name: /^extensions$/i }).first();
await expect(extensionsButton).toBeVisible({ timeout: DEFAULT_TIMEOUT });
await extensionsButton.click();
}
test.describe('Goose App', () => {
test.describe('General UI', () => {
@@ -117,7 +135,7 @@ test.describe('Goose App', () => {
await expect(mainWindow.getByTestId('chat-input')).toBeVisible({ timeout: DEFAULT_TIMEOUT });
// Navigate to Settings via sidebar
await mainWindow.getByTestId('sidebar-settings-button').click();
await openSettings(mainWindow);
// Navigate to App tab
await mainWindow.getByTestId('settings-app-tab').click();
@@ -147,7 +165,7 @@ test.describe('Goose App', () => {
await lightModeButton.click();
// Navigate back to home
await mainWindow.getByTestId('sidebar-home-button').click();
await openHome(mainWindow);
});
});
@@ -207,7 +225,7 @@ test.describe('Goose App', () => {
test('MCP integration - add extension and use tool', async () => {
// Navigate to Extensions via sidebar
await mainWindow.getByTestId('sidebar-extensions-button').click();
await openExtensions(mainWindow);
// Add custom extension
await mainWindow.getByRole('button', { name: 'Add custom extension' }).click();
@@ -228,7 +246,7 @@ test.describe('Goose App', () => {
).toBeVisible({ timeout: DEFAULT_TIMEOUT });
// Navigate back to home
await mainWindow.getByTestId('sidebar-home-button').click();
await openHome(mainWindow);
// Send a message requesting a running quote
const chatInput = mainWindow.getByTestId('chat-input');
@@ -157,16 +157,25 @@ function withVisualDelayPage(page: Page): Page {
export const test = base.extend<GooseTestFixtures>({
goosePage: async ({}, use, testInfo) => {
testInfo.setTimeout(Math.max(testInfo.timeout, 120000));
let electronApp: Awaited<ReturnType<typeof electron.launch>> | null = null;
let page: Page | null = null;
let videoDir: string | undefined;
let videoTrimStartMs = 0;
const appRoot = join(__dirname, '../..');
const executablePath = resolvePackagedExecutable(appRoot);
const executablePath = resolveDirectElectronExecutable(appRoot);
if (!fs.existsSync(executablePath)) {
throw new Error(
`Packaged app executable not found at ${executablePath}. Build it first (e.g. "cd ui/desktop && npm run package"), or set GOOSE_PACKAGED_EXECUTABLE.`
`Electron executable not found at ${executablePath}. Install dependencies in ui/desktop to ensure node_modules/electron exists.`
);
}
const viteMainPath = join(appRoot, '.vite', 'build', 'main.js');
if (!fs.existsSync(viteMainPath)) {
throw new Error(
`Direct Electron mode requires Vite build output at ${viteMainPath}. Run "cd ui/desktop && npm run package" (or another build step that generates .vite/build) first.`
);
}
@@ -175,18 +184,21 @@ export const test = base.extend<GooseTestFixtures>({
try {
videoDir = isVideoRecording() ? testInfo.outputPath('videos') : undefined;
const launchOptions = buildLaunchOptions(executablePath, tempDir, videoDir);
debugLog(`Launching packaged Electron for test: ${testInfo.title}`);
debugLog(`Using packaged executable: ${executablePath}`);
debugLog(`Launching direct Electron for test: ${testInfo.title}`);
debugLog(`Using Electron executable: ${executablePath}`);
electronApp = await electron.launch(launchOptions);
attachAppDebugLogs(electronApp);
await electronApp.firstWindow({ timeout: 30000 });
await electronApp.firstWindow({ timeout: 20000 });
const recordingStartMs = Date.now();
await waitForRootWindow(electronApp, 60000);
const rootWindow = await waitForRootWindow(electronApp, 8000).catch(async () => {
debugLog('Root-ready window not found quickly; falling back to first window.');
return await electronApp!.firstWindow({ timeout: 5000 });
});
const rootReadyElapsedMs = Date.now() - recordingStartMs;
page = await waitForReadyAppWindow(electronApp, 60000);
await page.waitForLoadState('domcontentloaded', { timeout: 10000 }).catch(() => {});
page = rootWindow;
await page.waitForLoadState('domcontentloaded', { timeout: 5000 }).catch(() => {});
debugLog(`Selected app window URL: ${page.url()}`);
attachPageDebugLogs(page);
if (isVideoRecording()) {
@@ -218,37 +230,6 @@ export const test = base.extend<GooseTestFixtures>({
},
});
async function waitForReadyAppWindow(
electronApp: Awaited<ReturnType<typeof electron.launch>>,
timeoutMs: number
): Promise<Page> {
const start = Date.now();
while (Date.now() - start < timeoutMs) {
const windows = electronApp.windows();
for (const page of windows) {
try {
const isReady = await page.evaluate(() => {
const root = document.getElementById('root');
if (!root) {
return false;
}
const chatInput = document.querySelector('[data-testid="chat-input"]');
return !!chatInput;
});
if (isReady) {
return page;
}
} catch {
// Window may not be ready for evaluation yet.
}
}
await new Promise((resolve) => setTimeout(resolve, 200));
}
const urls = electronApp.windows().map((w) => w.url() || '<empty>');
throw new Error(`No app-ready window found within ${timeoutMs}ms. Window URLs: ${urls.join(', ')}`);
}
async function waitForRootWindow(
electronApp: Awaited<ReturnType<typeof electron.launch>>,
timeoutMs: number
@@ -273,20 +254,14 @@ async function waitForRootWindow(
throw new Error(`No root-ready window found within ${timeoutMs}ms. Window URLs: ${urls.join(', ')}`);
}
function resolvePackagedExecutable(appRoot: string): string {
if (process.env.GOOSE_PACKAGED_EXECUTABLE) {
return process.env.GOOSE_PACKAGED_EXECUTABLE;
}
function resolveDirectElectronExecutable(appRoot: string): string {
if (process.platform === 'darwin') {
return join(appRoot, 'out', 'Goose-darwin-arm64', 'Goose.app', 'Contents', 'MacOS', 'Goose');
return join(appRoot, 'node_modules', 'electron', 'dist', 'Electron.app', 'Contents', 'MacOS', 'Electron');
}
if (process.platform === 'win32') {
return join(appRoot, 'out', 'Goose-win32-x64', 'Goose.exe');
return join(appRoot, 'node_modules', 'electron', 'dist', 'electron.exe');
}
return join(appRoot, 'out', 'goose-linux-x64', 'goose');
return join(appRoot, 'node_modules', 'electron', 'dist', 'electron');
}
function createIsolatedGoosePathRoot(): string {
@@ -319,10 +294,11 @@ function buildLaunchOptions(
tempDir: string,
videoDir?: string
): Parameters<typeof electron.launch>[0] {
const appRoot = join(__dirname, '../..');
const launchOptions: Parameters<typeof electron.launch>[0] = {
executablePath,
args: [],
timeout: 60000,
args: [appRoot],
timeout: 30000,
env: {
...process.env,
GOOSE_ALLOWLIST_BYPASS: 'true',
+46
View File
@@ -0,0 +1,46 @@
import { test, expect } from './fixtures.electron.packaged';
const DEFAULT_TIMEOUT = 10000;
test.describe('Goose App', () => {
test('history is empty before first session', async ({ goosePage }) => {
const mainWindow = goosePage;
const showAll = mainWindow.getByText('Show All', { exact: true }).first();
if (!(await showAll.isVisible().catch(() => false))) {
const chatButton = mainWindow.getByRole('button', { name: /^chat$/i }).first();
await expect(chatButton).toBeVisible({ timeout: DEFAULT_TIMEOUT });
await chatButton.click();
}
await expect(mainWindow.getByTestId('chat-sessions-list')).toBeVisible({
timeout: DEFAULT_TIMEOUT,
});
await expect(mainWindow.getByTestId('chat-start-new')).toBeVisible({ timeout: DEFAULT_TIMEOUT });
await expect(mainWindow.getByTestId('chat-show-all')).toHaveCount(0);
const chatInput = mainWindow.getByTestId('chat-input');
if (!(await chatInput.isVisible().catch(() => false))) {
const sidebarHomeButton = mainWindow.getByTestId('sidebar-home-button');
if (await sidebarHomeButton.isVisible().catch(() => false)) {
await sidebarHomeButton.click();
} else {
const homeButton = mainWindow.getByRole('button', { name: /^home$/i }).first();
await expect(homeButton).toBeVisible({ timeout: DEFAULT_TIMEOUT });
await homeButton.click();
}
}
await expect(chatInput).toBeVisible({ timeout: DEFAULT_TIMEOUT });
const alertTrigger = mainWindow.getByTestId('bottom-menu-alert-trigger');
await expect(alertTrigger).toBeVisible({ timeout: DEFAULT_TIMEOUT });
await alertTrigger.click();
const contextWindowAlert = mainWindow
.getByTestId('alert-box')
.filter({ has: mainWindow.getByText('Context window', { exact: true }) })
.first();
await expect(contextWindowAlert).toBeVisible({ timeout: DEFAULT_TIMEOUT });
await expect(contextWindowAlert.getByTestId('alert-progress-current')).toHaveText(/^0$/);
});
});