Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 40 additions & 2 deletions .github/workflows/quality.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ on:

jobs:
quality-checks:
name: Quality Checks
name: Quality Checks + Smoke
runs-on: ubuntu-latest

steps:
Expand All @@ -32,4 +32,42 @@ jobs:
run: pnpm typecheck

- name: Run linting
run: pnpm lint
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
2 changes: 2 additions & 0 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
10 changes: 8 additions & 2 deletions frontend/src/hooks/useNotifications.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,18 +42,24 @@ export function useNotifications() {
const windowFocusedRef = useRef<boolean>(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;
Expand Down
122 changes: 122 additions & 0 deletions tests/electronApiMock.ts
Original file line number Diff line number Diff line change
@@ -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<string, unknown> = {}) =>
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,
},
});
});
}
121 changes: 0 additions & 121 deletions tests/git-status.spec.ts

This file was deleted.

9 changes: 7 additions & 2 deletions tests/health-check.spec.ts
Original file line number Diff line number Diff line change
@@ -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 }) => {
Expand All @@ -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' });
});
});
});
Loading
Loading