Add translations and testing framework.

This commit is contained in:
Patrick Fic
2025-03-12 14:53:02 -07:00
parent e0cec62e13
commit 776d152d88
23 changed files with 1334 additions and 33 deletions

21
src/main/index.test.ts Normal file
View File

@@ -0,0 +1,21 @@
import { _electron as electron } from "playwright";
import { test, expect } from "@playwright/test";
test("example test", async () => {
const electronApp = await electron.launch({ args: ["."] });
const isPackaged = await electronApp.evaluate(async ({ app }) => {
// This runs in Electron's main process, parameter here is always
// the result of the require('electron') in the main app script.
return app.isPackaged;
});
expect(isPackaged).toBe(false);
// Wait for the first BrowserWindow to open
// and return its Page object
const window = await electronApp.firstWindow();
await window.screenshot({ path: "intro.png" });
// close app
await electronApp.close();
});

View File

@@ -4,7 +4,8 @@ import log from "electron-log/main";
import { join } from "path";
import icon from "../../resources/icon.png?asset";
import ErrorTypeCheck from "../util/errorTypeCheck";
import "./store/store";
log.initialize();
function createWindow(): void {
// Create the browser window.
const mainWindow = new BrowserWindow({

View File

@@ -1,6 +1,38 @@
import { ipcMain } from "electron";
import ipcTypes from "../../util/ipcTypes.json";
import log from "electron-log/main";
import { ipcMainHandleAuthStateChanged } from "./ipcMainHandler.user";
// Log all IPC messages and their payloads
const logIpcMessages = () => {
// Get all message types from ipcTypes.toMain
Object.keys(ipcTypes.toMain).forEach((key) => {
const messageType = ipcTypes.toMain[key];
// Wrap the original handler with our logging
const originalHandler = ipcMain.listeners(messageType)[0];
if (originalHandler) {
ipcMain.removeAllListeners(messageType);
}
ipcMain.on(messageType, (event, payload) => {
log.info(
`%c[IPC Main]%c${messageType}`,
"color: red",
"color: green",
payload
);
// Call original handler if it existed
if (originalHandler) {
originalHandler(event, payload);
}
});
});
};
ipcMain.on(ipcTypes.toMain.test, (payload: any) =>
console.log("** Verify that ipcMain is loaded and working.", payload)
);
ipcMain.on(ipcTypes.toMain.authStateChanged, ipcMainHandleAuthStateChanged);
logIpcMessages();

View File

@@ -0,0 +1,14 @@
import { IpcMainEvent } from "electron";
import Store from "../store/store";
import { User } from "firebase/auth";
import log from "electron-log/main";
const ipcMainHandleAuthStateChanged = async (
event: IpcMainEvent,
user: User | null
) => {
Store.set("user", user);
log.log(Store.get("user"));
};
export { ipcMainHandleAuthStateChanged };

View File

@@ -8,6 +8,7 @@ const store = new Store({
enabled: false,
pollingInterval: 30000,
},
user: null,
},
});

View File

@@ -0,0 +1,93 @@
import { test, expect } from '@playwright/test';
import { Page } from '@playwright/test';
// src/renderer/src/App.test.tsx
// Mock data
const mockUser = {
uid: 'test123',
email: 'test@example.com',
displayName: 'Test User',
toJSON: () => ({
uid: 'test123',
email: 'test@example.com',
displayName: 'Test User'
})
};
test.describe('App Component', () => {
let page: Page;
test.beforeEach(async ({ browser }) => {
page = await browser.newPage();
// Mock Firebase Auth
await page.addInitScript(() => {
window.mockAuthState = null;
// Mock the firebase auth module
jest.mock('./util/firebase', () => ({
auth: {
onAuthStateChanged: (callback) => {
callback(window.mockAuthState);
// Return mock unsubscribe function
return () => {};
}
}
}));
// Mock electron IPC
window.electron = {
ipcRenderer: {
send: jest.fn()
}
};
});
await page.goto('/');
});
test('should show SignInForm when user is not authenticated', async () => {
await page.evaluate(() => {
window.mockAuthState = null;
});
await page.reload();
// Check if SignInForm is visible
const signInForm = await page.locator('form').filter({ hasText: 'Sign In' });
await expect(signInForm).toBeVisible();
});
test('should show routes when user is authenticated', async () => {
await page.evaluate((user) => {
window.mockAuthState = user;
}, mockUser);
await page.reload();
// Check if AuthHome is visible
const authHome = await page.locator('div:text("AuthHome")');
await expect(authHome).toBeVisible();
// Check that electron IPC was called with auth state
await expect(page.evaluate(() => {
return window.electron.ipcRenderer.send.mock.calls.length > 0;
})).resolves.toBe(true);
});
test('should navigate to settings page when authenticated', async () => {
await page.evaluate((user) => {
window.mockAuthState = user;
}, mockUser);
await page.reload();
// Navigate to settings
await page.click('a[href="/settings"]');
// Check if Settings page is visible
const settingsPage = await page.locator('div:text("Settings")');
await expect(settingsPage).toBeVisible();
});
});

View File

@@ -1,25 +1,49 @@
import { Button } from "antd";
import log from "electron-log/renderer";
import "@ant-design/v5-patch-for-react-19";
import { Layout } from "antd";
import { User } from "firebase/auth";
import { useState } from "react";
import { BrowserRouter, Route, Routes } from "react-router";
import ipcTypes from "../../util/ipcTypes.json";
import NavigationHeader from "./components/NavigationHeader/Navigationheader";
import SignInForm from "./components/SignInForm/SignInForm";
import Versions from "./components/Versions";
import { auth } from "./util/firebase";
import {} from "react-error-boundary";
import { ErrorBoundary } from "react-error-boundary";
import ErrorBoundaryFallback from "./components/ErrorBoundaryFallback/ErrorBoundaryFallback";
const App: React.FC = () => {
const [user, setUser] = useState<User | null>(null);
auth.onAuthStateChanged((user) => {
setUser(user);
//Send back to the main process so that it knows we are authenticated.
if (user) {
window.electron.ipcRenderer.send(
ipcTypes.toMain.authStateChanged,
user.toJSON()
);
}
});
function App(): JSX.Element {
const ipcHandle = (): void => window.electron.ipcRenderer.send("ping");
const ipcHandleWithType = (): void => {
log.error("Test from renderer.");
window.electron.ipcRenderer.send(ipcTypes.toMain.test, { test: "test" });
};
return (
<>
<Button onClick={ipcHandle}>Send IPC</Button>
<Button onClick={ipcHandleWithType}>Send IPC written by me.</Button>
{import.meta.env.VITE_FIREBASE_CONFIG}
<Versions></Versions>
<SignInForm />
</>
<BrowserRouter>
<ErrorBoundary FallbackComponent={ErrorBoundaryFallback}>
<Layout>
{!user ? (
<SignInForm />
) : (
<>
<NavigationHeader />
<Routes>
<Route path="/" element={<div>AuthHome</div>} />
<Route path="settings" element={<div>Settings</div>} />
</Routes>
</>
)}
</Layout>
</ErrorBoundary>
</BrowserRouter>
);
}
};
export default App;

View File

@@ -0,0 +1,20 @@
import { Button, Result } from "antd";
import { FallbackProps } from "react-error-boundary";
import { useTranslation } from "react-i18next";
const ErrorBoundaryFallback: React.FC<FallbackProps> = ({
error,
resetErrorBoundary,
}) => {
const { t } = useTranslation();
return (
<Result
status={"500"}
title={t("app.errors.errorboundary")}
subTitle={error?.message}
extra={[<Button onClick={resetErrorBoundary}>Try again</Button>]}
/>
);
};
export default ErrorBoundaryFallback;

View File

@@ -0,0 +1,29 @@
import { Layout, Menu } from "antd";
import { MenuItemType } from "antd/es/menu/interface";
import { useTranslation } from "react-i18next";
import { NavLink } from "react-router";
const NavigationHeader: React.FC = () => {
const { t } = useTranslation();
const menuItems: MenuItemType[] = [
{ label: <NavLink to="/">{t("navigation.home")}</NavLink>, key: "home" },
{
label: <NavLink to="/settings">{t("navigation.settings")}</NavLink>,
key: "settings",
},
];
return (
<Layout.Header style={{ display: "flex", alignItems: "center" }}>
<div className="demo-logo" />
<Menu
theme="dark"
mode="horizontal"
defaultSelectedKeys={["2"]}
items={menuItems}
style={{ flex: 1, minWidth: 0 }}
/>
</Layout.Header>
);
};
export default NavigationHeader;

View File

@@ -1,6 +1,7 @@
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
import "./util/i18n";
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
<React.StrictMode>

View File

@@ -0,0 +1,18 @@
import i18n from "i18next";
import { initReactI18next } from "react-i18next";
import enTranslations from "../../../util/translations/en-US/renderer.json";
const resources = {
en: enTranslations,
};
i18n.use(initReactI18next).init({
resources,
debug: import.meta.env.DEV,
lng: "en",
interpolation: {
escapeValue: false,
},
});
export default i18n;

View File

@@ -1,6 +1,7 @@
{
"toMain": {
"test": "toMain_test"
"test": "toMain_test",
"authStateChanged": "toMain_authStateChanged"
},
"toRenderer": {
"test": "toRenderer_test"

View File

@@ -0,0 +1,5 @@
{
"toolbar": {
"help": "Help"
}
}

View File

@@ -0,0 +1,8 @@
{
"translation": {
"navigation": {
"home": "Home",
"settings": "Settings"
}
}
}