Add translations and testing framework.
This commit is contained in:
21
src/main/index.test.ts
Normal file
21
src/main/index.test.ts
Normal 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();
|
||||
});
|
||||
@@ -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({
|
||||
|
||||
@@ -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();
|
||||
|
||||
14
src/main/ipc/ipcMainHandler.user.ts
Normal file
14
src/main/ipc/ipcMainHandler.user.ts
Normal 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 };
|
||||
@@ -8,6 +8,7 @@ const store = new Store({
|
||||
enabled: false,
|
||||
pollingInterval: 30000,
|
||||
},
|
||||
user: null,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
93
src/renderer/src/App.test.tsx
Normal file
93
src/renderer/src/App.test.tsx
Normal 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();
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
|
||||
18
src/renderer/src/util/i18n.ts
Normal file
18
src/renderer/src/util/i18n.ts
Normal 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;
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"toMain": {
|
||||
"test": "toMain_test"
|
||||
"test": "toMain_test",
|
||||
"authStateChanged": "toMain_authStateChanged"
|
||||
},
|
||||
"toRenderer": {
|
||||
"test": "toRenderer_test"
|
||||
|
||||
5
src/util/translations/en-US/main.json
Normal file
5
src/util/translations/en-US/main.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"toolbar": {
|
||||
"help": "Help"
|
||||
}
|
||||
}
|
||||
8
src/util/translations/en-US/renderer.json
Normal file
8
src/util/translations/en-US/renderer.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"translation": {
|
||||
"navigation": {
|
||||
"home": "Home",
|
||||
"settings": "Settings"
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user