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

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;