From 23ac5c12b489780466d7f6360b8526c97d3b7ebb Mon Sep 17 00:00:00 2001 From: ParsaKhaz Date: Thu, 14 May 2026 11:20:11 -0700 Subject: [PATCH] Enforce maintained regression checks --- .github/workflows/quality.yml | 42 +++- frontend/src/App.tsx | 2 + frontend/src/hooks/useNotifications.ts | 10 +- tests/electronApiMock.ts | 122 +++++++++++ tests/git-status.spec.ts | 121 ---------- tests/health-check.spec.ts | 9 +- tests/permissions-ui-fixed.spec.ts | 85 ------- tests/permissions-ui.spec.ts | 86 -------- tests/permissions.spec.ts | 292 ------------------------- tests/setup.ts | 29 --- tests/smoke.spec.ts | 29 ++- 11 files changed, 196 insertions(+), 631 deletions(-) create mode 100644 tests/electronApiMock.ts delete mode 100644 tests/git-status.spec.ts delete mode 100644 tests/permissions-ui-fixed.spec.ts delete mode 100644 tests/permissions-ui.spec.ts delete mode 100644 tests/permissions.spec.ts delete mode 100644 tests/setup.ts diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml index a37d8129..9baacbe1 100644 --- a/.github/workflows/quality.yml +++ b/.github/workflows/quality.yml @@ -9,7 +9,7 @@ on: jobs: quality-checks: - name: Quality Checks + name: Quality Checks + Smoke runs-on: ubuntu-latest steps: @@ -32,4 +32,42 @@ jobs: run: pnpm typecheck - name: Run linting - run: pnpm lint \ No newline at end of file + run: pnpm lint + + - name: Install Playwright browser dependencies + run: pnpm exec playwright install --with-deps chromium + + - name: Run minimal functional smoke tests + run: xvfb-run -a pnpm test:ci:minimal + env: + PANE_DIR: ${{ runner.temp }}/pane-ci + + cross-os-main-tests: + name: Main Process Tests (${{ matrix.os }}) + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '22.15.1' + cache: 'pnpm' + + - name: Install dependencies + run: pnpm install + + - name: Run main process type checking + run: pnpm --filter main typecheck + + - name: Run main process unit tests + run: pnpm --filter main test diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 29c2db07..1ac5eeeb 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -204,6 +204,8 @@ function App() { // Detect unclean shutdown from previous session and notify user useEffect(() => { + if (!window.electronAPI?.events?.onUncleanShutdownDetected) return; + return window.electronAPI.events.onUncleanShutdownDetected(() => { showNotification( 'Pane didn\'t shut down cleanly', diff --git a/frontend/src/hooks/useNotifications.ts b/frontend/src/hooks/useNotifications.ts index 84307402..6ba2f243 100644 --- a/frontend/src/hooks/useNotifications.ts +++ b/frontend/src/hooks/useNotifications.ts @@ -42,18 +42,24 @@ export function useNotifications() { const windowFocusedRef = useRef(typeof document !== 'undefined' ? document.hasFocus() : true); useEffect(() => { + const electronWindow = window.electronAPI?.window; + const electronEvents = window.electronAPI?.events; + if (!electronWindow?.isFocused || !electronEvents?.onWindowFocusChanged) { + return; + } + // Pull authoritative initial state from the main process. document.hasFocus() // is a cold-start fallback; if DevTools or another Electron sub-window owns // DOM focus at mount time, document.hasFocus() returns false even though // BrowserWindow.isFocused() is true. Without this pull, no focus event // fires until the next focus change, and notifications misfire in between. - window.electronAPI.window.isFocused().then((focused) => { + electronWindow.isFocused().then((focused) => { windowFocusedRef.current = focused; }).catch(() => { // Leave the document.hasFocus() bootstrap in place on IPC failure. }); - const unsubscribe = window.electronAPI.events.onWindowFocusChanged((focused) => { + const unsubscribe = electronEvents.onWindowFocusChanged((focused) => { windowFocusedRef.current = focused; }); return unsubscribe; diff --git a/tests/electronApiMock.ts b/tests/electronApiMock.ts new file mode 100644 index 00000000..c3895fd0 --- /dev/null +++ b/tests/electronApiMock.ts @@ -0,0 +1,122 @@ +import type { Page } from '@playwright/test'; + +export async function installElectronApiMock(page: Page) { + await page.addInitScript(() => { + const success = (data: unknown = null) => Promise.resolve({ success: true, data }); + const unsubscribe = () => undefined; + const subscribe = () => unsubscribe; + + const namespace = (overrides: Record = {}) => + new Proxy(overrides, { + get(target, prop: string | symbol) { + if (prop in target) { + return target[prop as keyof typeof target]; + } + return () => success(); + }, + }); + + const events = new Proxy({}, { + get: () => subscribe, + }); + + const invoke = (channel: string) => { + if (channel === 'preferences:get') { + return success('true'); + } + if (channel === 'archive:get-progress') { + return success(null); + } + return success(); + }; + + const electronAPI = { + invoke, + events, + window: { + isFocused: () => Promise.resolve(true), + }, + getPlatform: () => Promise.resolve('linux'), + getVersionInfo: () => success({ + version: 'test', + current: 'test', + latest: 'test', + hasUpdate: false, + }), + isPackaged: () => Promise.resolve(false), + checkForUpdates: () => success({ hasUpdate: false }), + openExternal: () => undefined, + analytics: namespace({ + getIdentity: () => success({ distinctId: 'test', hasConsent: false }), + onMainEvent: subscribe, + syncDistinctId: () => undefined, + }), + cloud: namespace({ + getState: () => success({ status: 'idle' }), + onStateChanged: subscribe, + startPolling: () => success(), + stopPolling: () => success(), + }), + config: namespace({ + get: () => success({}), + getAvailableShells: () => success([]), + getMonospaceFonts: () => success([]), + getSessionPreferences: () => success({}), + }), + folders: namespace({ + getByProject: () => success([]), + }), + onboarding: namespace({ + detectEnvironment: () => success({}), + setupDefaultRepo: () => success({}), + starRepo: () => success({}), + }), + panels: namespace({ + getSessionPanels: () => success([]), + shouldAutoCreate: () => success(false), + }), + projects: namespace({ + getAll: () => success([]), + getActive: () => success(null), + refreshGitStatus: () => success(), + }), + prompts: namespace({ + getAll: () => success([]), + }), + ptyHost: namespace({ + ack: () => Promise.resolve(), + onData: subscribe, + onExit: subscribe, + }), + resourceMonitor: namespace({ + getSnapshot: () => success(null), + startActive: () => success(), + stopActive: () => success(), + }), + sessions: namespace({ + getAll: () => success([]), + getAllWithProjects: () => success([]), + getArchivedWithProjects: () => success([]), + getResumable: () => success([]), + }), + uiState: namespace({ + getExpanded: () => success([]), + saveSessionSortAscending: () => success(), + }), + }; + + Object.defineProperty(window, 'electronAPI', { + configurable: true, + value: electronAPI, + }); + + Object.defineProperty(window, 'electron', { + configurable: true, + value: { + invoke, + on: subscribe, + off: () => undefined, + }, + }); + }); +} diff --git a/tests/git-status.spec.ts b/tests/git-status.spec.ts deleted file mode 100644 index 1f90ed5d..00000000 --- a/tests/git-status.spec.ts +++ /dev/null @@ -1,121 +0,0 @@ -import { test, expect, Page } from '@playwright/test'; - -async function setupTest(page: Page): Promise { - // Navigate to the app - await page.goto('/', { waitUntil: 'domcontentloaded', timeout: 30000 }); - - // Close welcome dialog if present - const getStartedButton = page.locator('button:has-text("Get Started")'); - if (await getStartedButton.isVisible({ timeout: 1000 }).catch(() => false)) { - await getStartedButton.click(); - } - - // Wait for the UI to load - await page.waitForSelector('[data-testid="sidebar"], .sidebar, aside', { timeout: 10000 }); -} - -test.describe('Git Status Indicators - Smoke Test', () => { - test('should display git status indicator for sessions', async ({ page }) => { - await setupTest(page); - - // Check if there are any existing sessions with git status indicators - const existingStatusIndicators = page.locator('[data-testid$="-git-status"]'); - const existingCount = await existingStatusIndicators.count(); - - if (existingCount > 0) { - // Test existing sessions - const firstIndicator = existingStatusIndicators.first(); - await expect(firstIndicator).toBeVisible(); - - // Verify it has a git state attribute - const gitState = await firstIndicator.getAttribute('data-git-state'); - expect(gitState).toBeTruthy(); - expect(['clean', 'modified', 'ahead', 'behind', 'diverged', 'conflict', 'untracked', 'unknown']).toContain(gitState); - - // Check that it can show loading state when clicked - const sessionItem = page.locator('[data-testid^="session-"]').first(); - await sessionItem.click(); - - // Wait for either loading state to appear or git state to update - // This ensures we catch the transition properly without arbitrary delays - const indicatorTestId = await firstIndicator.getAttribute('data-testid'); - await page.waitForFunction( - (testId) => { - const indicator = document.querySelector(`[data-testid="${testId}"]`); - if (!indicator) return false; - - // Check if loading state is active or if git state has been updated - const isLoading = indicator.getAttribute('data-git-loading') === 'true'; - const hasGitState = indicator.hasAttribute('data-git-state'); - - // We're ready when either loading is shown or git state is present - return isLoading || hasGitState; - }, - indicatorTestId, - { timeout: 2000 } - ).catch(() => { - // If timeout, that's okay - the indicator might already have the state - }); - - // The indicator should still be visible (either in loading state or with status) - await expect(firstIndicator).toBeVisible(); - } else { - // No existing sessions found - verify the UI is still functional - // by checking that the sidebar is present (already checked in setupTest) - // This branch passing without errors indicates the test succeeded - } - - // Verify the UI is fully loaded by checking for project elements or sidebar - // The create session button appears on hover, so we'll check for the sidebar instead - const sidebar = page.locator('[data-testid="sidebar"], .sidebar, aside'); - await expect(sidebar).toBeVisible(); - }); - - test('should handle loading states gracefully', async ({ page }) => { - await setupTest(page); - - // If there are sessions, click on one to potentially trigger loading state - const sessionItems = page.locator('[data-testid^="session-"]'); - const sessionCount = await sessionItems.count(); - - if (sessionCount > 0) { - await sessionItems.first().click(); - - // Check if any git status indicators show loading state - const loadingIndicators = page.locator('[data-git-loading="true"]'); - - // Loading state might appear briefly or not at all if cached - if (await loadingIndicators.count() > 0) { - // Verify loading indicator has proper structure - const loader = loadingIndicators.first().locator('.animate-spin'); - await expect(loader).toBeVisible(); - } - - // Wait for any loading to complete by checking for loading state to disappear - // and git status to be populated - await page.waitForFunction( - () => { - // Check if any loading indicators are still present - const loadingElements = document.querySelectorAll('[data-git-loading="true"]'); - const hasLoadingState = loadingElements.length > 0; - - // Check if git status indicators have a valid state - const statusElements = document.querySelectorAll('[data-testid$="-git-status"][data-git-state]'); - const hasGitState = statusElements.length > 0; - - // Loading is complete when there are no loading states and we have git states - return !hasLoadingState && hasGitState; - }, - { timeout: 5000 } - ).catch(() => { - // If the wait times out, it might mean no git status is available, which is okay - }); - - // Verify indicators are still visible after loading - const statusIndicators = page.locator('[data-testid$="-git-status"]'); - if (await statusIndicators.count() > 0) { - await expect(statusIndicators.first()).toBeVisible(); - } - } - }); -}); \ No newline at end of file diff --git a/tests/health-check.spec.ts b/tests/health-check.spec.ts index 8a9da1a6..bc1c708b 100644 --- a/tests/health-check.spec.ts +++ b/tests/health-check.spec.ts @@ -1,4 +1,9 @@ import { test, expect } from '@playwright/test'; +import { installElectronApiMock } from './electronApiMock'; + +test.beforeEach(async ({ page }) => { + await installElectronApiMock(page); +}); test.describe('Health Check', () => { test('Electron app should start', async ({ page }) => { @@ -11,9 +16,9 @@ test.describe('Health Check', () => { // Check that the page has loaded const title = await page.title(); expect(title).toBeTruthy(); - + await expect(page.getByText('Something went wrong')).toHaveCount(0); // Take a screenshot for debugging await page.screenshot({ path: 'test-results/health-check.png' }); }); -}); \ No newline at end of file +}); diff --git a/tests/permissions-ui-fixed.spec.ts b/tests/permissions-ui-fixed.spec.ts deleted file mode 100644 index 390bdb6b..00000000 --- a/tests/permissions-ui-fixed.spec.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { test, expect } from '@playwright/test'; - -test.describe('Permission UI Elements', () => { - test.beforeEach(async ({ page }) => { - await page.goto('/'); - await page.waitForLoadState('networkidle'); - - // Close welcome dialog if present - const getStartedButton = page.locator('button:has-text("Get Started")'); - if (await getStartedButton.isVisible({ timeout: 1000 }).catch(() => false)) { - await getStartedButton.click(); - } - }); - - test('Settings should have permission mode option', async ({ page }) => { - // Click settings button with retry - const settingsButton = page.locator('[data-testid="settings-button"]'); - await expect(settingsButton).toBeVisible({ timeout: 10000 }); - await settingsButton.click(); - - // Wait for settings dialog with better selector - const settingsDialog = page.locator('div[role="dialog"]:has-text("Settings")'); - await expect(settingsDialog).toBeVisible({ timeout: 10000 }); - - // Check for permission mode section - await expect(page.locator('text="Default Permission Mode"')).toBeVisible({ timeout: 5000 }); - await expect(page.locator('text="Skip Permissions (Default)"')).toBeVisible(); - await expect(page.locator('text="Approve Actions"')).toBeVisible(); - - // Check radio buttons - await expect(page.locator('input[name="defaultPermissionMode"][value="ignore"]')).toBeVisible(); - await expect(page.locator('input[name="defaultPermissionMode"][value="approve"]')).toBeVisible(); - - // Default should be 'ignore' - await expect(page.locator('input[name="defaultPermissionMode"][value="ignore"]')).toBeChecked(); - }); - - test('Can change default permission mode', async ({ page }) => { - // Click settings button - const settingsButton = page.locator('[data-testid="settings-button"]'); - await expect(settingsButton).toBeVisible({ timeout: 10000 }); - await settingsButton.click(); - - // Wait for settings dialog - const settingsDialog = page.locator('div[role="dialog"]:has-text("Settings")'); - await expect(settingsDialog).toBeVisible({ timeout: 10000 }); - - // Click approve mode - const approveRadio = page.locator('input[name="defaultPermissionMode"][value="approve"]'); - await approveRadio.click(); - - // Save settings - click the Save button - const saveButton = page.locator('button[type="submit"]:has-text("Save")'); - await expect(saveButton).toBeVisible(); - await saveButton.click(); - - // Wait for settings to close - check dialog is gone - await expect(settingsDialog).toBeHidden({ timeout: 10000 }); - - // Give a moment for settings to persist - await page.waitForTimeout(500); - - // Re-open settings - await settingsButton.click(); - - // Wait for dialog again - await expect(settingsDialog).toBeVisible({ timeout: 10000 }); - - // Check that approve mode is now selected - await expect(page.locator('input[name="defaultPermissionMode"][value="approve"]')).toBeChecked(); - }); - - test('Permission dialog component renders correctly', async ({ page }) => { - // This test checks if the permission dialog component exists in the codebase - // For a real test, we'd need to trigger a permission request - - // Navigate to a page that might show permissions - await page.goto('/'); - - // For now, just check that the app loaded - await expect(page.locator('body')).toBeVisible(); - - // Could add more specific tests here when we know how to trigger permission dialogs - }); -}); \ No newline at end of file diff --git a/tests/permissions-ui.spec.ts b/tests/permissions-ui.spec.ts deleted file mode 100644 index 6ae4cce1..00000000 --- a/tests/permissions-ui.spec.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { test, expect } from '@playwright/test'; - -test.describe('Permission UI Elements', () => { - test.beforeEach(async ({ page }) => { - await page.goto('/'); - await page.waitForLoadState('networkidle'); - - // Close welcome dialog if present - const getStartedButton = page.locator('button:has-text("Get Started")'); - if (await getStartedButton.isVisible({ timeout: 1000 }).catch(() => false)) { - await getStartedButton.click(); - } - }); - - test('Settings should have permission mode option', async ({ page }) => { - // Click settings button - await page.click('[data-testid="settings-button"]'); - - // Wait for settings dialog - await page.waitForSelector('text="Settings"'); - - // Check for permission mode section - await expect(page.locator('text="Default Permission Mode"')).toBeVisible(); - await expect(page.locator('text="Skip Permissions (Default)"')).toBeVisible(); - await expect(page.locator('text="Approve Actions"')).toBeVisible(); - - // Check radio buttons - await expect(page.locator('input[name="defaultPermissionMode"][value="ignore"]')).toBeVisible(); - await expect(page.locator('input[name="defaultPermissionMode"][value="approve"]')).toBeVisible(); - - // Default should be 'ignore' - await expect(page.locator('input[name="defaultPermissionMode"][value="ignore"]')).toBeChecked(); - }); - - test('Can change default permission mode', async ({ page }) => { - // Click settings button - await page.click('[data-testid="settings-button"]'); - - // Wait for settings dialog - await page.waitForSelector('text="Settings"'); - - // Click approve mode - await page.click('input[name="defaultPermissionMode"][value="approve"]'); - - // Save settings - await page.click('button:has-text("Save")'); - - // Wait for settings to close - await page.waitForSelector('text="Settings"', { state: 'hidden' }); - - // Re-open settings - await page.click('[data-testid="settings-button"]'); - - // Verify it was saved - await expect(page.locator('input[name="defaultPermissionMode"][value="approve"]')).toBeChecked(); - }); - - test('Permission dialog component renders correctly', async ({ page }) => { - // Inject a mock permission dialog by evaluating in page context - await page.evaluate(() => { - // Create a temporary container for testing - const container = document.createElement('div'); - container.innerHTML = ` -
-
-
-

Permission Required

-

Claude wants to Execute shell commands

-
-
- - -
-
-
- `; - document.body.appendChild(container); - }); - - // Check that permission dialog elements are visible - await expect(page.locator('text="Permission Required"')).toBeVisible(); - await expect(page.locator('text="Claude wants to Execute shell commands"')).toBeVisible(); - await expect(page.locator('button:has-text("Allow")')).toBeVisible(); - await expect(page.locator('button:has-text("Deny")')).toBeVisible(); - }); -}); \ No newline at end of file diff --git a/tests/permissions.spec.ts b/tests/permissions.spec.ts deleted file mode 100644 index 301cfefa..00000000 --- a/tests/permissions.spec.ts +++ /dev/null @@ -1,292 +0,0 @@ -import { test, expect } from '@playwright/test'; -import { setupTestProject, cleanupTestProject } from './setup'; - -test.describe('Permission Flow', () => { - let testProjectPath: string; - - test.beforeAll(async () => { - testProjectPath = await setupTestProject(); - }); - - test.afterAll(async () => { - await cleanupTestProject(testProjectPath); - }); - // Helper to navigate to the app and set up a project - async function navigateToApp(page) { - await page.goto('/'); - // Wait for the app to load - await page.waitForLoadState('networkidle'); - - // Handle Welcome dialog if it appears - const getStartedButton = page.locator('button:has-text("Get Started")'); - if (await getStartedButton.isVisible({ timeout: 2000 }).catch(() => false)) { - await getStartedButton.click(); - // Wait for welcome dialog to close - await page.waitForSelector('text="Welcome to Pane"', { state: 'hidden' }); - } - - // Check if we need to select a project - const selectProjectButton = page.locator('button:has-text("Select Project")'); - if (await selectProjectButton.isVisible({ timeout: 2000 }).catch(() => false)) { - await selectProjectButton.click(); - - // Wait for project dialog - await page.waitForSelector('text="Select or Create Project"'); - - // Click create new project - await page.click('button:has-text("Create New Project")'); - - // Fill in project details - await page.fill('input[placeholder*="project name"]', 'Test Project'); - await page.fill('input[placeholder*="directory"]', testProjectPath); - - // Submit - await page.click('button[type="submit"]:has-text("Create")'); - - // Wait for dialog to close - await page.waitForSelector('text="Select or Create Project"', { state: 'hidden' }); - } - - // Wait for the sidebar to be visible - await page.waitForSelector('[data-testid="sidebar"]', { timeout: 30000 }); - } - - // Helper to create a session with permission mode - async function createSessionWithPermissions(page, prompt: string, permissionMode: 'approve' | 'ignore') { - // Click create session button - await page.click('[data-testid="create-session-button"]'); - - // Wait for dialog - await page.waitForSelector('[data-testid="create-session-dialog"]'); - - // Fill in prompt - await page.fill('textarea[id="prompt"]', prompt); - - // Select permission mode - await page.click(`input[name="permissionMode"][value="${permissionMode}"]`); - - // Submit form - await page.click('button[type="submit"]'); - - // Wait for dialog to close - await page.waitForSelector('[data-testid="create-session-dialog"]', { state: 'hidden' }); - } - - test('should show permission mode option in create session dialog', async ({ page }) => { - await navigateToApp(page); - - // Open create session dialog - await page.click('[data-testid="create-session-button"]'); - - // Check that permission mode options are visible - await expect(page.locator('input[name="permissionMode"][value="ignore"]')).toBeVisible(); - await expect(page.locator('input[name="permissionMode"][value="approve"]')).toBeVisible(); - - // Check default selection - await expect(page.locator('input[name="permissionMode"][value="ignore"]')).toBeChecked(); - }); - - test('should show permission mode in settings', async ({ page }) => { - await navigateToApp(page); - - // Open settings - await page.click('[data-testid="settings-button"]'); - - // Check that default permission mode options are visible - await expect(page.locator('input[name="defaultPermissionMode"][value="ignore"]')).toBeVisible(); - await expect(page.locator('input[name="defaultPermissionMode"][value="approve"]')).toBeVisible(); - }); - - test('should create session with skip permissions mode', async ({ page }) => { - await navigateToApp(page); - - await createSessionWithPermissions(page, 'Test skip permissions', 'ignore'); - - // Verify session was created - await expect(page.locator('text=Test skip permissions')).toBeVisible({ timeout: 10000 }); - }); - - test('should create session with approve permissions mode', async ({ page }) => { - await navigateToApp(page); - - await createSessionWithPermissions(page, 'Test approve permissions', 'approve'); - - // Verify session was created - await expect(page.locator('text=Test approve permissions')).toBeVisible({ timeout: 10000 }); - }); - - test('should show permission dialog when Claude requests permission', async ({ page }) => { - // This test would require mocking the Claude process to trigger a permission request - // For now, we'll test that the permission dialog component renders correctly - - await navigateToApp(page); - - // Inject a mock permission request - await page.evaluate(() => { - window.postMessage({ - type: 'permission:request', - data: { - id: 'test-request-1', - sessionId: 'test-session-1', - toolName: 'Bash', - input: { command: 'npm install', description: 'Install dependencies' }, - timestamp: Date.now() - } - }, '*'); - }); - - // Wait for permission dialog - await expect(page.locator('text=Permission Required')).toBeVisible({ timeout: 5000 }); - await expect(page.locator('text=Claude wants to Execute shell commands')).toBeVisible(); - await expect(page.locator('text=npm install')).toBeVisible(); - - // Check that Allow and Deny buttons are present - await expect(page.locator('button:has-text("Allow")')).toBeVisible(); - await expect(page.locator('button:has-text("Deny")')).toBeVisible(); - }); - - test('should handle allow permission response', async ({ page }) => { - await navigateToApp(page); - - // Inject a mock permission request - await page.evaluate(() => { - window.postMessage({ - type: 'permission:request', - data: { - id: 'test-request-2', - sessionId: 'test-session-2', - toolName: 'Write', - input: { file_path: '/tmp/test.txt', content: 'Hello World' }, - timestamp: Date.now() - } - }, '*'); - }); - - // Wait for permission dialog - await page.waitForSelector('text=Permission Required'); - - // Click Allow - await page.click('button:has-text("Allow")'); - - // Verify dialog is closed - await expect(page.locator('text=Permission Required')).not.toBeVisible(); - }); - - test('should handle deny permission response', async ({ page }) => { - await navigateToApp(page); - - // Inject a mock permission request - await page.evaluate(() => { - window.postMessage({ - type: 'permission:request', - data: { - id: 'test-request-3', - sessionId: 'test-session-3', - toolName: 'Delete', - input: { path: '/important/file.txt' }, - timestamp: Date.now() - } - }, '*'); - }); - - // Wait for permission dialog - await page.waitForSelector('text=Permission Required'); - - // Click Deny - await page.click('button:has-text("Deny")'); - - // Verify dialog is closed - await expect(page.locator('text=Permission Required')).not.toBeVisible(); - }); - - test('should show high risk warning for dangerous tools', async ({ page }) => { - await navigateToApp(page); - - // Inject a mock permission request for a dangerous tool - await page.evaluate(() => { - window.postMessage({ - type: 'permission:request', - data: { - id: 'test-request-4', - sessionId: 'test-session-4', - toolName: 'Bash', - input: { command: 'rm -rf /', description: 'Delete everything' }, - timestamp: Date.now() - } - }, '*'); - }); - - // Wait for permission dialog - await page.waitForSelector('text=Permission Required'); - - // Check for high risk warning - await expect(page.locator('text=High Risk Action')).toBeVisible(); - await expect(page.locator('text=This action could modify your system')).toBeVisible(); - }); - - test('should allow editing permission request input', async ({ page }) => { - await navigateToApp(page); - - // Inject a mock permission request - await page.evaluate(() => { - window.postMessage({ - type: 'permission:request', - data: { - id: 'test-request-5', - sessionId: 'test-session-5', - toolName: 'Write', - input: { file_path: '/tmp/test.txt', content: 'Original content' }, - timestamp: Date.now() - } - }, '*'); - }); - - // Wait for permission dialog - await page.waitForSelector('text=Permission Required'); - - // Click Edit button - await page.click('button:has-text("Edit")'); - - // Check that textarea is visible - const textarea = page.locator('textarea'); - await expect(textarea).toBeVisible(); - - // Verify original content is shown - const content = await textarea.inputValue(); - expect(content).toContain('Original content'); - - // Edit the content - await textarea.fill(JSON.stringify({ - file_path: '/tmp/test.txt', - content: 'Modified content' - }, null, 2)); - - // Click Allow - await page.click('button:has-text("Allow")'); - - // Verify dialog is closed - await expect(page.locator('text=Permission Required')).not.toBeVisible(); - }); - - test('should save default permission mode in settings', async ({ page }) => { - await navigateToApp(page); - - // Open settings - await page.click('[data-testid="settings-button"]'); - - // Select approve mode - await page.click('input[name="defaultPermissionMode"][value="approve"]'); - - // Save settings - await page.click('button:has-text("Save")'); - - // Wait for settings to close - await page.waitForSelector('text=Settings', { state: 'hidden' }); - - // Re-open settings to verify it was saved - await page.click('[data-testid="settings-button"]'); - - // Check that approve mode is selected - await expect(page.locator('input[name="defaultPermissionMode"][value="approve"]')).toBeChecked(); - }); -}); \ No newline at end of file diff --git a/tests/setup.ts b/tests/setup.ts deleted file mode 100644 index 9351fa84..00000000 --- a/tests/setup.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { chromium } from '@playwright/test'; -import * as fs from 'fs'; -import * as path from 'path'; -import * as os from 'os'; - -export async function setupTestProject() { - // Create a temporary test project directory - const testProjectPath = path.join(os.tmpdir(), `pane-test-${Date.now()}`); - fs.mkdirSync(testProjectPath, { recursive: true }); - - // Initialize git in the test directory - const { execSync } = require('child_process'); - execSync('git init -b main', { cwd: testProjectPath, stdio: 'pipe' }); - execSync('git config user.email "test@example.com"', { cwd: testProjectPath, stdio: 'pipe' }); - execSync('git config user.name "Test User"', { cwd: testProjectPath, stdio: 'pipe' }); - execSync('touch README.md', { cwd: testProjectPath }); - execSync('git add .', { cwd: testProjectPath }); - execSync('git commit -m "Initial commit"', { cwd: testProjectPath }); - - return testProjectPath; -} - -export async function cleanupTestProject(projectPath: string) { - try { - fs.rmSync(projectPath, { recursive: true, force: true }); - } catch (error) { - console.error('Failed to cleanup test project:', error); - } -} \ No newline at end of file diff --git a/tests/smoke.spec.ts b/tests/smoke.spec.ts index 756bb2da..9fc571a9 100644 --- a/tests/smoke.spec.ts +++ b/tests/smoke.spec.ts @@ -1,4 +1,9 @@ import { test, expect, Page } from '@playwright/test'; +import { installElectronApiMock } from './electronApiMock'; + +test.beforeEach(async ({ page }) => { + await installElectronApiMock(page); +}); async function dismissStartupDialogs(page: Page) { // Dismiss analytics consent dialog if present (shows before welcome) @@ -27,6 +32,7 @@ test.describe('Smoke Tests', () => { // Check that the page has loaded const title = await page.title(); expect(title).toBe('Pane'); + await expect(page.getByText('Something went wrong')).toHaveCount(0); // Take a screenshot for debugging await page.screenshot({ path: 'test-results/smoke-test.png' }); @@ -41,27 +47,26 @@ test.describe('Smoke Tests', () => { const sidebar = page.locator('[data-testid="sidebar"]').first(); await expect(sidebar).toBeVisible({ timeout: 10000 }); - // Settings button should exist (even if not immediately visible) - const settingsButton = page.locator('[data-testid="settings-button"]'); - await expect(settingsButton).toHaveCount(1); + const sidebarMenuButton = page.getByRole('button', { name: 'Sidebar menu' }); + await expect(sidebarMenuButton).toBeVisible(); }); - test('Settings button is clickable', async ({ page }) => { + test('Settings menu item is clickable', async ({ page }) => { await page.goto('/', { waitUntil: 'domcontentloaded', timeout: 30000 }); await dismissStartupDialogs(page); - // Wait for the settings button to be visible - const settingsButton = page.locator('[data-testid="settings-button"]'); - await expect(settingsButton).toBeVisible({ timeout: 5000 }); + const sidebarMenuButton = page.getByRole('button', { name: 'Sidebar menu' }); + await expect(sidebarMenuButton).toBeVisible({ timeout: 5000 }); - // Verify the button is enabled and clickable - await expect(settingsButton).toBeEnabled(); + await expect(sidebarMenuButton).toBeEnabled(); + await sidebarMenuButton.click(); - // Try to click it - await settingsButton.click(); + const settingsItem = page.getByRole('button', { name: 'Settings' }); + await expect(settingsItem).toBeVisible({ timeout: 5000 }); + await settingsItem.click(); // Small wait to ensure no errors are thrown await page.waitForTimeout(500); }); -}); \ No newline at end of file +});