Add translations and testing framework.
This commit is contained in:
6
.gitignore
vendored
6
.gitignore
vendored
@@ -3,3 +3,9 @@ dist
|
|||||||
out
|
out
|
||||||
.DS_Store
|
.DS_Store
|
||||||
*.log*
|
*.log*
|
||||||
|
|
||||||
|
# Playwright
|
||||||
|
/test-results/
|
||||||
|
/playwright-report/
|
||||||
|
/blob-report/
|
||||||
|
/playwright/.cache/
|
||||||
|
|||||||
381
package-lock.json
generated
381
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
13
package.json
13
package.json
@@ -23,23 +23,28 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@electron-toolkit/preload": "^3.0.1",
|
"@electron-toolkit/preload": "^3.0.1",
|
||||||
"@electron-toolkit/utils": "^4.0.0",
|
"@electron-toolkit/utils": "^4.0.0",
|
||||||
"antd": "^5.24.3",
|
|
||||||
"chokidar": "^4.0.3",
|
"chokidar": "^4.0.3",
|
||||||
"dbffile": "^1.12.0",
|
"dbffile": "^1.12.0",
|
||||||
"electron-log": "^5.3.2",
|
"electron-log": "^5.3.2",
|
||||||
"electron-store": "^10.0.1",
|
"electron-store": "^10.0.1",
|
||||||
"electron-updater": "^6.3.9",
|
"electron-updater": "^6.3.9",
|
||||||
"firebase": "^11.4.0",
|
"i18next": "^24.2.2",
|
||||||
"lodash": "^4.17.21"
|
"lodash": "^4.17.21",
|
||||||
|
"playwright": "^1.51.0",
|
||||||
|
"react-error-boundary": "^5.0.0",
|
||||||
|
"react-i18next": "^15.4.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@ant-design/v5-patch-for-react-19": "^1.0.3",
|
||||||
"@electron-toolkit/eslint-config-prettier": "^3.0.0",
|
"@electron-toolkit/eslint-config-prettier": "^3.0.0",
|
||||||
"@electron-toolkit/eslint-config-ts": "^3.0.0",
|
"@electron-toolkit/eslint-config-ts": "^3.0.0",
|
||||||
"@electron-toolkit/tsconfig": "^1.0.1",
|
"@electron-toolkit/tsconfig": "^1.0.1",
|
||||||
|
"@playwright/test": "^1.51.0",
|
||||||
"@types/node": "^22.13.10",
|
"@types/node": "^22.13.10",
|
||||||
"@types/react": "^19.0.10",
|
"@types/react": "^19.0.10",
|
||||||
"@types/react-dom": "^19.0.4",
|
"@types/react-dom": "^19.0.4",
|
||||||
"@vitejs/plugin-react": "^4.3.4",
|
"@vitejs/plugin-react": "^4.3.4",
|
||||||
|
"antd": "^5.24.3",
|
||||||
"electron": "^35.0.1",
|
"electron": "^35.0.1",
|
||||||
"electron-builder": "^25.1.8",
|
"electron-builder": "^25.1.8",
|
||||||
"electron-vite": "^3.0.0",
|
"electron-vite": "^3.0.0",
|
||||||
@@ -47,9 +52,11 @@
|
|||||||
"eslint-plugin-react": "^7.37.4",
|
"eslint-plugin-react": "^7.37.4",
|
||||||
"eslint-plugin-react-hooks": "^5.2.0",
|
"eslint-plugin-react-hooks": "^5.2.0",
|
||||||
"eslint-plugin-react-refresh": "^0.4.19",
|
"eslint-plugin-react-refresh": "^0.4.19",
|
||||||
|
"firebase": "^11.4.0",
|
||||||
"prettier": "^3.5.3",
|
"prettier": "^3.5.3",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
|
"react-router": "^7.3.0",
|
||||||
"typescript": "^5.8.2",
|
"typescript": "^5.8.2",
|
||||||
"vite": "^6.2.1"
|
"vite": "^6.2.1"
|
||||||
}
|
}
|
||||||
|
|||||||
80
playwright.config.ts
Normal file
80
playwright.config.ts
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
import { defineConfig, devices } from "@playwright/test";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read environment variables from file.
|
||||||
|
* https://github.com/motdotla/dotenv
|
||||||
|
*/
|
||||||
|
// import dotenv from 'dotenv';
|
||||||
|
// import path from 'path';
|
||||||
|
// dotenv.config({ path: path.resolve(__dirname, '.env') });
|
||||||
|
|
||||||
|
/**
|
||||||
|
* See https://playwright.dev/docs/test-configuration.
|
||||||
|
*/
|
||||||
|
export default defineConfig({
|
||||||
|
//testDir: './tests/**/*',
|
||||||
|
//testMatch: '**/*.test.ts',
|
||||||
|
/* Run tests in files in parallel */
|
||||||
|
fullyParallel: true,
|
||||||
|
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
||||||
|
forbidOnly: !!process.env.CI,
|
||||||
|
/* Retry on CI only */
|
||||||
|
retries: process.env.CI ? 2 : 0,
|
||||||
|
/* Opt out of parallel tests on CI. */
|
||||||
|
workers: process.env.CI ? 1 : undefined,
|
||||||
|
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
||||||
|
reporter: "html",
|
||||||
|
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
||||||
|
use: {
|
||||||
|
/* Base URL to use in actions like `await page.goto('/')`. */
|
||||||
|
// baseURL: 'http://127.0.0.1:3000',
|
||||||
|
|
||||||
|
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
||||||
|
trace: "on-first-retry",
|
||||||
|
},
|
||||||
|
|
||||||
|
/* Configure projects for major browsers */
|
||||||
|
projects: [
|
||||||
|
{
|
||||||
|
name: "chromium",
|
||||||
|
use: { ...devices["Desktop Chrome"] },
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: "firefox",
|
||||||
|
use: { ...devices["Desktop Firefox"] },
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: "webkit",
|
||||||
|
use: { ...devices["Desktop Safari"] },
|
||||||
|
},
|
||||||
|
|
||||||
|
/* Test against mobile viewports. */
|
||||||
|
// {
|
||||||
|
// name: 'Mobile Chrome',
|
||||||
|
// use: { ...devices['Pixel 5'] },
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// name: 'Mobile Safari',
|
||||||
|
// use: { ...devices['iPhone 12'] },
|
||||||
|
// },
|
||||||
|
|
||||||
|
/* Test against branded browsers. */
|
||||||
|
// {
|
||||||
|
// name: 'Microsoft Edge',
|
||||||
|
// use: { ...devices['Desktop Edge'], channel: 'msedge' },
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// name: 'Google Chrome',
|
||||||
|
// use: { ...devices['Desktop Chrome'], channel: 'chrome' },
|
||||||
|
// },
|
||||||
|
],
|
||||||
|
|
||||||
|
/* Run your local dev server before starting the tests */
|
||||||
|
// webServer: {
|
||||||
|
// command: "npm run start",
|
||||||
|
// url: "http://127.0.0.1:3000",
|
||||||
|
// reuseExistingServer: !process.env.CI,
|
||||||
|
// },
|
||||||
|
});
|
||||||
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 { join } from "path";
|
||||||
import icon from "../../resources/icon.png?asset";
|
import icon from "../../resources/icon.png?asset";
|
||||||
import ErrorTypeCheck from "../util/errorTypeCheck";
|
import ErrorTypeCheck from "../util/errorTypeCheck";
|
||||||
|
import "./store/store";
|
||||||
|
log.initialize();
|
||||||
function createWindow(): void {
|
function createWindow(): void {
|
||||||
// Create the browser window.
|
// Create the browser window.
|
||||||
const mainWindow = new BrowserWindow({
|
const mainWindow = new BrowserWindow({
|
||||||
|
|||||||
@@ -1,6 +1,38 @@
|
|||||||
import { ipcMain } from "electron";
|
import { ipcMain } from "electron";
|
||||||
import ipcTypes from "../../util/ipcTypes.json";
|
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) =>
|
ipcMain.on(ipcTypes.toMain.test, (payload: any) =>
|
||||||
console.log("** Verify that ipcMain is loaded and working.", payload)
|
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,
|
enabled: false,
|
||||||
pollingInterval: 30000,
|
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 "@ant-design/v5-patch-for-react-19";
|
||||||
import log from "electron-log/renderer";
|
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 ipcTypes from "../../util/ipcTypes.json";
|
||||||
|
import NavigationHeader from "./components/NavigationHeader/Navigationheader";
|
||||||
import SignInForm from "./components/SignInForm/SignInForm";
|
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 (
|
return (
|
||||||
<>
|
<BrowserRouter>
|
||||||
<Button onClick={ipcHandle}>Send IPC</Button>
|
<ErrorBoundary FallbackComponent={ErrorBoundaryFallback}>
|
||||||
|
<Layout>
|
||||||
<Button onClick={ipcHandleWithType}>Send IPC written by me.</Button>
|
{!user ? (
|
||||||
{import.meta.env.VITE_FIREBASE_CONFIG}
|
<SignInForm />
|
||||||
<Versions></Versions>
|
) : (
|
||||||
<SignInForm />
|
<>
|
||||||
</>
|
<NavigationHeader />
|
||||||
|
<Routes>
|
||||||
|
<Route path="/" element={<div>AuthHome</div>} />
|
||||||
|
<Route path="settings" element={<div>Settings</div>} />
|
||||||
|
</Routes>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Layout>
|
||||||
|
</ErrorBoundary>
|
||||||
|
</BrowserRouter>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default App;
|
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 React from "react";
|
||||||
import ReactDOM from "react-dom/client";
|
import ReactDOM from "react-dom/client";
|
||||||
import App from "./App";
|
import App from "./App";
|
||||||
|
import "./util/i18n";
|
||||||
|
|
||||||
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
|
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
|
||||||
<React.StrictMode>
|
<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": {
|
"toMain": {
|
||||||
"test": "toMain_test"
|
"test": "toMain_test",
|
||||||
|
"authStateChanged": "toMain_authStateChanged"
|
||||||
},
|
},
|
||||||
"toRenderer": {
|
"toRenderer": {
|
||||||
"test": "toRenderer_test"
|
"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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
437
tests-examples/demo-todo-app.spec.ts
Normal file
437
tests-examples/demo-todo-app.spec.ts
Normal file
@@ -0,0 +1,437 @@
|
|||||||
|
import { test, expect, type Page } from '@playwright/test';
|
||||||
|
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
|
await page.goto('https://demo.playwright.dev/todomvc');
|
||||||
|
});
|
||||||
|
|
||||||
|
const TODO_ITEMS = [
|
||||||
|
'buy some cheese',
|
||||||
|
'feed the cat',
|
||||||
|
'book a doctors appointment'
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
test.describe('New Todo', () => {
|
||||||
|
test('should allow me to add todo items', async ({ page }) => {
|
||||||
|
// create a new todo locator
|
||||||
|
const newTodo = page.getByPlaceholder('What needs to be done?');
|
||||||
|
|
||||||
|
// Create 1st todo.
|
||||||
|
await newTodo.fill(TODO_ITEMS[0]);
|
||||||
|
await newTodo.press('Enter');
|
||||||
|
|
||||||
|
// Make sure the list only has one todo item.
|
||||||
|
await expect(page.getByTestId('todo-title')).toHaveText([
|
||||||
|
TODO_ITEMS[0]
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Create 2nd todo.
|
||||||
|
await newTodo.fill(TODO_ITEMS[1]);
|
||||||
|
await newTodo.press('Enter');
|
||||||
|
|
||||||
|
// Make sure the list now has two todo items.
|
||||||
|
await expect(page.getByTestId('todo-title')).toHaveText([
|
||||||
|
TODO_ITEMS[0],
|
||||||
|
TODO_ITEMS[1]
|
||||||
|
]);
|
||||||
|
|
||||||
|
await checkNumberOfTodosInLocalStorage(page, 2);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should clear text input field when an item is added', async ({ page }) => {
|
||||||
|
// create a new todo locator
|
||||||
|
const newTodo = page.getByPlaceholder('What needs to be done?');
|
||||||
|
|
||||||
|
// Create one todo item.
|
||||||
|
await newTodo.fill(TODO_ITEMS[0]);
|
||||||
|
await newTodo.press('Enter');
|
||||||
|
|
||||||
|
// Check that input is empty.
|
||||||
|
await expect(newTodo).toBeEmpty();
|
||||||
|
await checkNumberOfTodosInLocalStorage(page, 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should append new items to the bottom of the list', async ({ page }) => {
|
||||||
|
// Create 3 items.
|
||||||
|
await createDefaultTodos(page);
|
||||||
|
|
||||||
|
// create a todo count locator
|
||||||
|
const todoCount = page.getByTestId('todo-count')
|
||||||
|
|
||||||
|
// Check test using different methods.
|
||||||
|
await expect(page.getByText('3 items left')).toBeVisible();
|
||||||
|
await expect(todoCount).toHaveText('3 items left');
|
||||||
|
await expect(todoCount).toContainText('3');
|
||||||
|
await expect(todoCount).toHaveText(/3/);
|
||||||
|
|
||||||
|
// Check all items in one call.
|
||||||
|
await expect(page.getByTestId('todo-title')).toHaveText(TODO_ITEMS);
|
||||||
|
await checkNumberOfTodosInLocalStorage(page, 3);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe('Mark all as completed', () => {
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
|
await createDefaultTodos(page);
|
||||||
|
await checkNumberOfTodosInLocalStorage(page, 3);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.afterEach(async ({ page }) => {
|
||||||
|
await checkNumberOfTodosInLocalStorage(page, 3);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should allow me to mark all items as completed', async ({ page }) => {
|
||||||
|
// Complete all todos.
|
||||||
|
await page.getByLabel('Mark all as complete').check();
|
||||||
|
|
||||||
|
// Ensure all todos have 'completed' class.
|
||||||
|
await expect(page.getByTestId('todo-item')).toHaveClass(['completed', 'completed', 'completed']);
|
||||||
|
await checkNumberOfCompletedTodosInLocalStorage(page, 3);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should allow me to clear the complete state of all items', async ({ page }) => {
|
||||||
|
const toggleAll = page.getByLabel('Mark all as complete');
|
||||||
|
// Check and then immediately uncheck.
|
||||||
|
await toggleAll.check();
|
||||||
|
await toggleAll.uncheck();
|
||||||
|
|
||||||
|
// Should be no completed classes.
|
||||||
|
await expect(page.getByTestId('todo-item')).toHaveClass(['', '', '']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('complete all checkbox should update state when items are completed / cleared', async ({ page }) => {
|
||||||
|
const toggleAll = page.getByLabel('Mark all as complete');
|
||||||
|
await toggleAll.check();
|
||||||
|
await expect(toggleAll).toBeChecked();
|
||||||
|
await checkNumberOfCompletedTodosInLocalStorage(page, 3);
|
||||||
|
|
||||||
|
// Uncheck first todo.
|
||||||
|
const firstTodo = page.getByTestId('todo-item').nth(0);
|
||||||
|
await firstTodo.getByRole('checkbox').uncheck();
|
||||||
|
|
||||||
|
// Reuse toggleAll locator and make sure its not checked.
|
||||||
|
await expect(toggleAll).not.toBeChecked();
|
||||||
|
|
||||||
|
await firstTodo.getByRole('checkbox').check();
|
||||||
|
await checkNumberOfCompletedTodosInLocalStorage(page, 3);
|
||||||
|
|
||||||
|
// Assert the toggle all is checked again.
|
||||||
|
await expect(toggleAll).toBeChecked();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe('Item', () => {
|
||||||
|
|
||||||
|
test('should allow me to mark items as complete', async ({ page }) => {
|
||||||
|
// create a new todo locator
|
||||||
|
const newTodo = page.getByPlaceholder('What needs to be done?');
|
||||||
|
|
||||||
|
// Create two items.
|
||||||
|
for (const item of TODO_ITEMS.slice(0, 2)) {
|
||||||
|
await newTodo.fill(item);
|
||||||
|
await newTodo.press('Enter');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check first item.
|
||||||
|
const firstTodo = page.getByTestId('todo-item').nth(0);
|
||||||
|
await firstTodo.getByRole('checkbox').check();
|
||||||
|
await expect(firstTodo).toHaveClass('completed');
|
||||||
|
|
||||||
|
// Check second item.
|
||||||
|
const secondTodo = page.getByTestId('todo-item').nth(1);
|
||||||
|
await expect(secondTodo).not.toHaveClass('completed');
|
||||||
|
await secondTodo.getByRole('checkbox').check();
|
||||||
|
|
||||||
|
// Assert completed class.
|
||||||
|
await expect(firstTodo).toHaveClass('completed');
|
||||||
|
await expect(secondTodo).toHaveClass('completed');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should allow me to un-mark items as complete', async ({ page }) => {
|
||||||
|
// create a new todo locator
|
||||||
|
const newTodo = page.getByPlaceholder('What needs to be done?');
|
||||||
|
|
||||||
|
// Create two items.
|
||||||
|
for (const item of TODO_ITEMS.slice(0, 2)) {
|
||||||
|
await newTodo.fill(item);
|
||||||
|
await newTodo.press('Enter');
|
||||||
|
}
|
||||||
|
|
||||||
|
const firstTodo = page.getByTestId('todo-item').nth(0);
|
||||||
|
const secondTodo = page.getByTestId('todo-item').nth(1);
|
||||||
|
const firstTodoCheckbox = firstTodo.getByRole('checkbox');
|
||||||
|
|
||||||
|
await firstTodoCheckbox.check();
|
||||||
|
await expect(firstTodo).toHaveClass('completed');
|
||||||
|
await expect(secondTodo).not.toHaveClass('completed');
|
||||||
|
await checkNumberOfCompletedTodosInLocalStorage(page, 1);
|
||||||
|
|
||||||
|
await firstTodoCheckbox.uncheck();
|
||||||
|
await expect(firstTodo).not.toHaveClass('completed');
|
||||||
|
await expect(secondTodo).not.toHaveClass('completed');
|
||||||
|
await checkNumberOfCompletedTodosInLocalStorage(page, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should allow me to edit an item', async ({ page }) => {
|
||||||
|
await createDefaultTodos(page);
|
||||||
|
|
||||||
|
const todoItems = page.getByTestId('todo-item');
|
||||||
|
const secondTodo = todoItems.nth(1);
|
||||||
|
await secondTodo.dblclick();
|
||||||
|
await expect(secondTodo.getByRole('textbox', { name: 'Edit' })).toHaveValue(TODO_ITEMS[1]);
|
||||||
|
await secondTodo.getByRole('textbox', { name: 'Edit' }).fill('buy some sausages');
|
||||||
|
await secondTodo.getByRole('textbox', { name: 'Edit' }).press('Enter');
|
||||||
|
|
||||||
|
// Explicitly assert the new text value.
|
||||||
|
await expect(todoItems).toHaveText([
|
||||||
|
TODO_ITEMS[0],
|
||||||
|
'buy some sausages',
|
||||||
|
TODO_ITEMS[2]
|
||||||
|
]);
|
||||||
|
await checkTodosInLocalStorage(page, 'buy some sausages');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe('Editing', () => {
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
|
await createDefaultTodos(page);
|
||||||
|
await checkNumberOfTodosInLocalStorage(page, 3);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should hide other controls when editing', async ({ page }) => {
|
||||||
|
const todoItem = page.getByTestId('todo-item').nth(1);
|
||||||
|
await todoItem.dblclick();
|
||||||
|
await expect(todoItem.getByRole('checkbox')).not.toBeVisible();
|
||||||
|
await expect(todoItem.locator('label', {
|
||||||
|
hasText: TODO_ITEMS[1],
|
||||||
|
})).not.toBeVisible();
|
||||||
|
await checkNumberOfTodosInLocalStorage(page, 3);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should save edits on blur', async ({ page }) => {
|
||||||
|
const todoItems = page.getByTestId('todo-item');
|
||||||
|
await todoItems.nth(1).dblclick();
|
||||||
|
await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill('buy some sausages');
|
||||||
|
await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).dispatchEvent('blur');
|
||||||
|
|
||||||
|
await expect(todoItems).toHaveText([
|
||||||
|
TODO_ITEMS[0],
|
||||||
|
'buy some sausages',
|
||||||
|
TODO_ITEMS[2],
|
||||||
|
]);
|
||||||
|
await checkTodosInLocalStorage(page, 'buy some sausages');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should trim entered text', async ({ page }) => {
|
||||||
|
const todoItems = page.getByTestId('todo-item');
|
||||||
|
await todoItems.nth(1).dblclick();
|
||||||
|
await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill(' buy some sausages ');
|
||||||
|
await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).press('Enter');
|
||||||
|
|
||||||
|
await expect(todoItems).toHaveText([
|
||||||
|
TODO_ITEMS[0],
|
||||||
|
'buy some sausages',
|
||||||
|
TODO_ITEMS[2],
|
||||||
|
]);
|
||||||
|
await checkTodosInLocalStorage(page, 'buy some sausages');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should remove the item if an empty text string was entered', async ({ page }) => {
|
||||||
|
const todoItems = page.getByTestId('todo-item');
|
||||||
|
await todoItems.nth(1).dblclick();
|
||||||
|
await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill('');
|
||||||
|
await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).press('Enter');
|
||||||
|
|
||||||
|
await expect(todoItems).toHaveText([
|
||||||
|
TODO_ITEMS[0],
|
||||||
|
TODO_ITEMS[2],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should cancel edits on escape', async ({ page }) => {
|
||||||
|
const todoItems = page.getByTestId('todo-item');
|
||||||
|
await todoItems.nth(1).dblclick();
|
||||||
|
await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill('buy some sausages');
|
||||||
|
await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).press('Escape');
|
||||||
|
await expect(todoItems).toHaveText(TODO_ITEMS);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe('Counter', () => {
|
||||||
|
test('should display the current number of todo items', async ({ page }) => {
|
||||||
|
// create a new todo locator
|
||||||
|
const newTodo = page.getByPlaceholder('What needs to be done?');
|
||||||
|
|
||||||
|
// create a todo count locator
|
||||||
|
const todoCount = page.getByTestId('todo-count')
|
||||||
|
|
||||||
|
await newTodo.fill(TODO_ITEMS[0]);
|
||||||
|
await newTodo.press('Enter');
|
||||||
|
|
||||||
|
await expect(todoCount).toContainText('1');
|
||||||
|
|
||||||
|
await newTodo.fill(TODO_ITEMS[1]);
|
||||||
|
await newTodo.press('Enter');
|
||||||
|
await expect(todoCount).toContainText('2');
|
||||||
|
|
||||||
|
await checkNumberOfTodosInLocalStorage(page, 2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe('Clear completed button', () => {
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
|
await createDefaultTodos(page);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should display the correct text', async ({ page }) => {
|
||||||
|
await page.locator('.todo-list li .toggle').first().check();
|
||||||
|
await expect(page.getByRole('button', { name: 'Clear completed' })).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should remove completed items when clicked', async ({ page }) => {
|
||||||
|
const todoItems = page.getByTestId('todo-item');
|
||||||
|
await todoItems.nth(1).getByRole('checkbox').check();
|
||||||
|
await page.getByRole('button', { name: 'Clear completed' }).click();
|
||||||
|
await expect(todoItems).toHaveCount(2);
|
||||||
|
await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[2]]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should be hidden when there are no items that are completed', async ({ page }) => {
|
||||||
|
await page.locator('.todo-list li .toggle').first().check();
|
||||||
|
await page.getByRole('button', { name: 'Clear completed' }).click();
|
||||||
|
await expect(page.getByRole('button', { name: 'Clear completed' })).toBeHidden();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe('Persistence', () => {
|
||||||
|
test('should persist its data', async ({ page }) => {
|
||||||
|
// create a new todo locator
|
||||||
|
const newTodo = page.getByPlaceholder('What needs to be done?');
|
||||||
|
|
||||||
|
for (const item of TODO_ITEMS.slice(0, 2)) {
|
||||||
|
await newTodo.fill(item);
|
||||||
|
await newTodo.press('Enter');
|
||||||
|
}
|
||||||
|
|
||||||
|
const todoItems = page.getByTestId('todo-item');
|
||||||
|
const firstTodoCheck = todoItems.nth(0).getByRole('checkbox');
|
||||||
|
await firstTodoCheck.check();
|
||||||
|
await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[1]]);
|
||||||
|
await expect(firstTodoCheck).toBeChecked();
|
||||||
|
await expect(todoItems).toHaveClass(['completed', '']);
|
||||||
|
|
||||||
|
// Ensure there is 1 completed item.
|
||||||
|
await checkNumberOfCompletedTodosInLocalStorage(page, 1);
|
||||||
|
|
||||||
|
// Now reload.
|
||||||
|
await page.reload();
|
||||||
|
await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[1]]);
|
||||||
|
await expect(firstTodoCheck).toBeChecked();
|
||||||
|
await expect(todoItems).toHaveClass(['completed', '']);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe('Routing', () => {
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
|
await createDefaultTodos(page);
|
||||||
|
// make sure the app had a chance to save updated todos in storage
|
||||||
|
// before navigating to a new view, otherwise the items can get lost :(
|
||||||
|
// in some frameworks like Durandal
|
||||||
|
await checkTodosInLocalStorage(page, TODO_ITEMS[0]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should allow me to display active items', async ({ page }) => {
|
||||||
|
const todoItem = page.getByTestId('todo-item');
|
||||||
|
await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check();
|
||||||
|
|
||||||
|
await checkNumberOfCompletedTodosInLocalStorage(page, 1);
|
||||||
|
await page.getByRole('link', { name: 'Active' }).click();
|
||||||
|
await expect(todoItem).toHaveCount(2);
|
||||||
|
await expect(todoItem).toHaveText([TODO_ITEMS[0], TODO_ITEMS[2]]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should respect the back button', async ({ page }) => {
|
||||||
|
const todoItem = page.getByTestId('todo-item');
|
||||||
|
await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check();
|
||||||
|
|
||||||
|
await checkNumberOfCompletedTodosInLocalStorage(page, 1);
|
||||||
|
|
||||||
|
await test.step('Showing all items', async () => {
|
||||||
|
await page.getByRole('link', { name: 'All' }).click();
|
||||||
|
await expect(todoItem).toHaveCount(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
await test.step('Showing active items', async () => {
|
||||||
|
await page.getByRole('link', { name: 'Active' }).click();
|
||||||
|
});
|
||||||
|
|
||||||
|
await test.step('Showing completed items', async () => {
|
||||||
|
await page.getByRole('link', { name: 'Completed' }).click();
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(todoItem).toHaveCount(1);
|
||||||
|
await page.goBack();
|
||||||
|
await expect(todoItem).toHaveCount(2);
|
||||||
|
await page.goBack();
|
||||||
|
await expect(todoItem).toHaveCount(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should allow me to display completed items', async ({ page }) => {
|
||||||
|
await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check();
|
||||||
|
await checkNumberOfCompletedTodosInLocalStorage(page, 1);
|
||||||
|
await page.getByRole('link', { name: 'Completed' }).click();
|
||||||
|
await expect(page.getByTestId('todo-item')).toHaveCount(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should allow me to display all items', async ({ page }) => {
|
||||||
|
await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check();
|
||||||
|
await checkNumberOfCompletedTodosInLocalStorage(page, 1);
|
||||||
|
await page.getByRole('link', { name: 'Active' }).click();
|
||||||
|
await page.getByRole('link', { name: 'Completed' }).click();
|
||||||
|
await page.getByRole('link', { name: 'All' }).click();
|
||||||
|
await expect(page.getByTestId('todo-item')).toHaveCount(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should highlight the currently applied filter', async ({ page }) => {
|
||||||
|
await expect(page.getByRole('link', { name: 'All' })).toHaveClass('selected');
|
||||||
|
|
||||||
|
//create locators for active and completed links
|
||||||
|
const activeLink = page.getByRole('link', { name: 'Active' });
|
||||||
|
const completedLink = page.getByRole('link', { name: 'Completed' });
|
||||||
|
await activeLink.click();
|
||||||
|
|
||||||
|
// Page change - active items.
|
||||||
|
await expect(activeLink).toHaveClass('selected');
|
||||||
|
await completedLink.click();
|
||||||
|
|
||||||
|
// Page change - completed items.
|
||||||
|
await expect(completedLink).toHaveClass('selected');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
async function createDefaultTodos(page: Page) {
|
||||||
|
// create a new todo locator
|
||||||
|
const newTodo = page.getByPlaceholder('What needs to be done?');
|
||||||
|
|
||||||
|
for (const item of TODO_ITEMS) {
|
||||||
|
await newTodo.fill(item);
|
||||||
|
await newTodo.press('Enter');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkNumberOfTodosInLocalStorage(page: Page, expected: number) {
|
||||||
|
return await page.waitForFunction(e => {
|
||||||
|
return JSON.parse(localStorage['react-todos']).length === e;
|
||||||
|
}, expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkNumberOfCompletedTodosInLocalStorage(page: Page, expected: number) {
|
||||||
|
return await page.waitForFunction(e => {
|
||||||
|
return JSON.parse(localStorage['react-todos']).filter((todo: any) => todo.completed).length === e;
|
||||||
|
}, expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkTodosInLocalStorage(page: Page, title: string) {
|
||||||
|
return await page.waitForFunction(t => {
|
||||||
|
return JSON.parse(localStorage['react-todos']).map((todo: any) => todo.title).includes(t);
|
||||||
|
}, title);
|
||||||
|
}
|
||||||
18
tests/example.spec.ts
Normal file
18
tests/example.spec.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
|
||||||
|
test('has title', async ({ page }) => {
|
||||||
|
await page.goto('https://playwright.dev/');
|
||||||
|
|
||||||
|
// Expect a title "to contain" a substring.
|
||||||
|
await expect(page).toHaveTitle(/Playwright/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('get started link', async ({ page }) => {
|
||||||
|
await page.goto('https://playwright.dev/');
|
||||||
|
|
||||||
|
// Click the get started link.
|
||||||
|
await page.getByRole('link', { name: 'Get started' }).click();
|
||||||
|
|
||||||
|
// Expects page to have a heading with the name of Installation.
|
||||||
|
await expect(page.getByRole('heading', { name: 'Installation' })).toBeVisible();
|
||||||
|
});
|
||||||
112
translations.babel
Normal file
112
translations.babel
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
<babeledit_project be_version="2.7.1" version="1.2">
|
||||||
|
<!--
|
||||||
|
|
||||||
|
BabelEdit project file
|
||||||
|
https://www.codeandweb.com/babeledit
|
||||||
|
|
||||||
|
This file contains meta data for all translations, but not the translation texts itself.
|
||||||
|
They are stored in framework-specific message files (.json / .vue / .yaml / .properties)
|
||||||
|
|
||||||
|
-->
|
||||||
|
<preset_collections/>
|
||||||
|
<framework>i18next</framework>
|
||||||
|
<filename>translations.babel</filename>
|
||||||
|
<source_root_dir></source_root_dir>
|
||||||
|
<folder_node>
|
||||||
|
<name></name>
|
||||||
|
<children>
|
||||||
|
<file_node>
|
||||||
|
<name>main</name>
|
||||||
|
<children>
|
||||||
|
<folder_node>
|
||||||
|
<name>toolbar</name>
|
||||||
|
<children>
|
||||||
|
<concept_node>
|
||||||
|
<name>help</name>
|
||||||
|
<definition_loaded>false</definition_loaded>
|
||||||
|
<description></description>
|
||||||
|
<comment></comment>
|
||||||
|
<default_text></default_text>
|
||||||
|
<translations>
|
||||||
|
<translation>
|
||||||
|
<language>en-US</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
</translations>
|
||||||
|
</concept_node>
|
||||||
|
</children>
|
||||||
|
</folder_node>
|
||||||
|
</children>
|
||||||
|
<filename></filename>
|
||||||
|
</file_node>
|
||||||
|
<file_node>
|
||||||
|
<name>renderer</name>
|
||||||
|
<children>
|
||||||
|
<folder_node>
|
||||||
|
<name>translation</name>
|
||||||
|
<children>
|
||||||
|
<folder_node>
|
||||||
|
<name>navigation</name>
|
||||||
|
<children>
|
||||||
|
<concept_node>
|
||||||
|
<name>home</name>
|
||||||
|
<definition_loaded>false</definition_loaded>
|
||||||
|
<description></description>
|
||||||
|
<comment></comment>
|
||||||
|
<default_text></default_text>
|
||||||
|
<translations>
|
||||||
|
<translation>
|
||||||
|
<language>en-US</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
</translations>
|
||||||
|
</concept_node>
|
||||||
|
<concept_node>
|
||||||
|
<name>settings</name>
|
||||||
|
<definition_loaded>false</definition_loaded>
|
||||||
|
<description></description>
|
||||||
|
<comment></comment>
|
||||||
|
<default_text></default_text>
|
||||||
|
<translations>
|
||||||
|
<translation>
|
||||||
|
<language>en-US</language>
|
||||||
|
<approved>false</approved>
|
||||||
|
</translation>
|
||||||
|
</translations>
|
||||||
|
</concept_node>
|
||||||
|
</children>
|
||||||
|
</folder_node>
|
||||||
|
</children>
|
||||||
|
</folder_node>
|
||||||
|
</children>
|
||||||
|
<filename></filename>
|
||||||
|
</file_node>
|
||||||
|
</children>
|
||||||
|
</folder_node>
|
||||||
|
<isTemplateProject>false</isTemplateProject>
|
||||||
|
<languages>
|
||||||
|
<language>
|
||||||
|
<code>en-US</code>
|
||||||
|
<source_id></source_id>
|
||||||
|
<source_file>src/util/translations/en-US</source_file>
|
||||||
|
</language>
|
||||||
|
</languages>
|
||||||
|
<translation_files>
|
||||||
|
<translation_file>
|
||||||
|
<file>src/util/translations/en-US</file>
|
||||||
|
</translation_file>
|
||||||
|
</translation_files>
|
||||||
|
<editor_configuration>
|
||||||
|
<save_empty_translations>true</save_empty_translations>
|
||||||
|
<copy_templates>
|
||||||
|
<copy_template>'%1'</copy_template>
|
||||||
|
<copy_template>{ this.props.t('%1') }</copy_template>
|
||||||
|
<copy_template>{ t('%1') }</copy_template>
|
||||||
|
</copy_templates>
|
||||||
|
</editor_configuration>
|
||||||
|
<primary_language>en-US</primary_language>
|
||||||
|
<configuration>
|
||||||
|
<indent>tab</indent>
|
||||||
|
<format>namespaced-json</format>
|
||||||
|
</configuration>
|
||||||
|
</babeledit_project>
|
||||||
@@ -5,7 +5,8 @@
|
|||||||
"src/main/**/*",
|
"src/main/**/*",
|
||||||
"src/preload/**/*",
|
"src/preload/**/*",
|
||||||
"src/util/**/*",
|
"src/util/**/*",
|
||||||
"src/interfaces/**/*"
|
"src/interfaces/**/*",
|
||||||
|
"tests/index.spec.ts"
|
||||||
],
|
],
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"composite": true,
|
"composite": true,
|
||||||
|
|||||||
@@ -4,16 +4,15 @@
|
|||||||
"src/renderer/src/env.d.ts",
|
"src/renderer/src/env.d.ts",
|
||||||
"src/renderer/src/**/*",
|
"src/renderer/src/**/*",
|
||||||
"src/renderer/src/**/*.tsx",
|
"src/renderer/src/**/*.tsx",
|
||||||
"src/preload/*.d.ts"
|
"src/preload/*.d.ts",
|
||||||
|
"src/util/**/*"
|
||||||
],
|
],
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"composite": true,
|
"composite": true,
|
||||||
"jsx": "react-jsx",
|
"jsx": "react-jsx",
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
"paths": {
|
"paths": {
|
||||||
"@renderer/*": [
|
"@renderer/*": ["src/renderer/src/*"]
|
||||||
"src/renderer/src/*"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user