diff --git a/package-lock.json b/package-lock.json
index b6a6c09..c4d6074 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -60,6 +60,7 @@
"lucide-react": "^0.553.0",
"react": "^19.2.0",
"react-dom": "^19.2.0",
+ "react-error-boundary": "^6.1.0",
"react-i18next": "^16.2.4",
"reflect-metadata": "^0.2.2",
"rxjs": "^7.8.2",
@@ -26393,6 +26394,15 @@
"react": "^19.2.0"
}
},
+ "node_modules/react-error-boundary": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmmirror.com/react-error-boundary/-/react-error-boundary-6.1.0.tgz",
+ "integrity": "sha512-02k9WQ/mUhdbXir0tC1NiMesGzRPaCsJEWU/4bcFrbY1YMZOtHShtZP6zw0SJrBWA/31H0KT9/FgdL8+sPKgHA==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": "^18.0.0 || ^19.0.0"
+ }
+ },
"node_modules/react-i18next": {
"version": "16.3.5",
"resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-16.3.5.tgz",
diff --git a/package.json b/package.json
index c33dae8..399a0c5 100644
--- a/package.json
+++ b/package.json
@@ -139,6 +139,7 @@
"lucide-react": "^0.553.0",
"react": "^19.2.0",
"react-dom": "^19.2.0",
+ "react-error-boundary": "^6.1.0",
"react-i18next": "^16.2.4",
"reflect-metadata": "^0.2.2",
"rxjs": "^7.8.2",
diff --git a/src/components/CloudAccountList.tsx b/src/components/CloudAccountList.tsx
index 906a45b..0e66733 100644
--- a/src/components/CloudAccountList.tsx
+++ b/src/components/CloudAccountList.tsx
@@ -306,11 +306,19 @@ export function CloudAccountList() {
if (isError) {
return (
-
+
{t('cloud.error.loadFailed')}
{t('action.retry')}
-
diff --git a/src/main.ts b/src/main.ts
index 2c368b9..dd5085a 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -199,7 +199,7 @@ function createWindow({ startHidden }: { startHidden: boolean }) {
ipcContext.setMainWindow(mainWindow);
logger.info('createWindow: setMainWindow done');
- if (MAIN_WINDOW_VITE_DEV_SERVER_URL) {
+ if (inDevelopment && MAIN_WINDOW_VITE_DEV_SERVER_URL) {
logger.info(`createWindow: loading URL ${MAIN_WINDOW_VITE_DEV_SERVER_URL}`);
mainWindow.loadURL(MAIN_WINDOW_VITE_DEV_SERVER_URL);
} else {
diff --git a/src/tests/e2e/app.spec.ts b/src/tests/e2e/app.spec.ts
index dfd8feb..874fd6a 100644
--- a/src/tests/e2e/app.spec.ts
+++ b/src/tests/e2e/app.spec.ts
@@ -2,13 +2,72 @@ import { test, expect, ElectronApplication } from '@playwright/test';
import { _electron as electron } from 'playwright';
import path from 'path';
+const injectCloudAccountsFailureScript = `
+(() => {
+ const START_ORPC_SERVER = 'start-orpc-server';
+ const originalPostMessage = window.postMessage.bind(window);
+
+ window.postMessage = function (message, targetOrigin, transfer) {
+ if (message === START_ORPC_SERVER && Array.isArray(transfer) && transfer[0]) {
+ const serverPort = transfer[0];
+ if (typeof serverPort.start === 'function') {
+ serverPort.start();
+ }
+
+ serverPort.onmessage = (event) => {
+ try {
+ const request = typeof event.data === 'string' ? JSON.parse(event.data) : event.data;
+ const requestId = request && (request.i || request.id);
+ const requestUrl = (request && request.p && request.p.u) || '';
+
+ if (!requestId) {
+ return;
+ }
+
+ if (requestUrl.includes('/cloud/listCloudAccounts')) {
+ serverPort.postMessage(
+ JSON.stringify({
+ i: requestId,
+ p: { s: 500, b: { json: { message: 'internal server error' } } },
+ }),
+ );
+ return;
+ }
+
+ let result = null;
+ if (requestUrl.includes('/process/isProcessRunning')) {
+ result = false;
+ } else if (requestUrl.includes('/cloud/getAutoSwitchEnabled')) {
+ result = false;
+ }
+
+ serverPort.postMessage(
+ JSON.stringify({
+ i: requestId,
+ p: { b: { json: result } },
+ }),
+ );
+ } catch (_error) {
+ // Ignore malformed messages in E2E bridge stub.
+ }
+ };
+
+ return;
+ }
+
+ return originalPostMessage(message, targetOrigin, transfer);
+ };
+})();
+`;
+
test.describe('Antigravity Manager', () => {
let electronApp: ElectronApplication;
+ const electronMainPath = path.join(__dirname, '../../../.vite/build/main.js');
test.beforeAll(async () => {
// Launch Electron app
electronApp = await electron.launch({
- args: [path.join(__dirname, '../../../.vite/build/main.js')],
+ args: [electronMainPath],
});
});
@@ -23,8 +82,8 @@ test.describe('Antigravity Manager', () => {
const title = await window.title();
expect(title).toBe('Antigravity Manager');
- // Check for main elements - h2 is present (contains i18n content)
- await expect(window.locator('h2').first()).toBeVisible();
+ await expect(window.getByRole('main')).toBeVisible();
+ await expect(window.locator('a[href="/settings"]').first()).toBeVisible();
});
test('should navigate to settings', async () => {
@@ -38,6 +97,24 @@ test.describe('Antigravity Manager', () => {
await expect(window.locator('h2').first()).toBeVisible();
});
+ test('should show fallback UI when cloud accounts loading fails', async () => {
+ await electronApp.close();
+
+ electronApp = await electron.launch({
+ args: [electronMainPath],
+ });
+
+ const page = await electronApp.firstWindow();
+ await page.addInitScript(injectCloudAccountsFailureScript);
+ await page.reload();
+ await page.waitForLoadState('domcontentloaded');
+ const mainContent = page.getByRole('main');
+ await expect(mainContent.getByTestId('cloud-load-error-fallback')).toBeVisible({
+ timeout: 15000,
+ });
+ await expect(mainContent.getByTestId('cloud-load-error-retry')).toBeVisible();
+ });
+
// More detailed tests would require mocking IPC or having a real environment
// For now, we verify basic navigation and rendering
});