mirror of
https://git.quad4.io/RNS-Things/MeshChatX.git
synced 2026-07-20 19:51:07 +00:00
feat(tests): update testing framework for Electron and backend integrity checks, add new tests for loading status and main helpers
This commit is contained in:
@@ -50,7 +50,7 @@ jobs:
|
||||
timeout: 45
|
||||
- name: Backend tests
|
||||
id: backend-tests
|
||||
timeout: 55
|
||||
timeout: 120
|
||||
- name: Localization tests
|
||||
id: lang-tests
|
||||
timeout: 20
|
||||
@@ -93,13 +93,11 @@ jobs:
|
||||
poetry run ruff format --check .
|
||||
;;
|
||||
frontend-tests)
|
||||
pnpm run test -- --exclude tests/frontend/i18n.test.js
|
||||
pnpm exec vitest run --exclude tests/frontend/LoadTimePerformance.test.js --exclude tests/frontend/i18n.test.js
|
||||
pnpm exec vitest run --config vitest.electron.config.js
|
||||
;;
|
||||
backend-tests)
|
||||
poetry run python -m pytest tests/backend -n auto \
|
||||
--ignore=tests/backend/test_performance_hotpaths.py \
|
||||
--ignore=tests/backend/test_memory_profiling.py \
|
||||
--ignore=tests/backend/test_performance_bottlenecks.py \
|
||||
--cov=meshchatx/src/backend
|
||||
;;
|
||||
lang-tests)
|
||||
|
||||
@@ -100,6 +100,7 @@ docker-compose.override.yml
|
||||
|
||||
.coverage
|
||||
coverage/
|
||||
coverage-electron/
|
||||
|
||||
test-results/
|
||||
playwright-report/
|
||||
|
||||
@@ -40,7 +40,7 @@ lint:
|
||||
|
||||
test:
|
||||
pnpm run test
|
||||
poetry run python -m pytest tests/backend -n auto --ignore=tests/backend/test_performance_hotpaths.py --ignore=tests/backend/test_memory_profiling.py --ignore=tests/backend/test_performance_bottlenecks.py --cov=meshchatx/src/backend
|
||||
poetry run python -m pytest tests/backend -n auto --cov=meshchatx/src/backend
|
||||
|
||||
test-be-perf:
|
||||
poetry run python -m pytest tests/backend/test_performance_hotpaths.py tests/backend/test_performance_bottlenecks.py
|
||||
|
||||
+9
-8
@@ -161,22 +161,22 @@ tasks:
|
||||
- "{{.NPM}} run test:e2e"
|
||||
|
||||
test:be:
|
||||
desc: Run Python tests (pytest; excludes timing-sensitive perf and memory profiling — use test:be:perf / profile:mem locally)
|
||||
desc: Run Python tests (pytest; includes perf/memory profiling — same as GitHub CI)
|
||||
cmds:
|
||||
- poetry run pytest tests/backend -n auto --ignore=tests/backend/test_performance_hotpaths.py --ignore=tests/backend/test_memory_profiling.py --ignore=tests/backend/test_performance_bottlenecks.py --cov=meshchatx/src/backend
|
||||
- poetry run pytest tests/backend -n auto --cov=meshchatx/src/backend
|
||||
|
||||
test:be:cov:
|
||||
desc: Run Python tests with detailed coverage (excludes performance hot-path, bottleneck timing, and memory profiling tests)
|
||||
desc: Run Python tests with detailed coverage (includes performance and memory profiling tests)
|
||||
cmds:
|
||||
- poetry run pytest tests/backend -n auto --ignore=tests/backend/test_performance_hotpaths.py --ignore=tests/backend/test_memory_profiling.py --ignore=tests/backend/test_performance_bottlenecks.py --cov=meshchatx/src/backend --cov-report=term-missing
|
||||
- poetry run pytest tests/backend -n auto --cov=meshchatx/src/backend --cov-report=term-missing
|
||||
|
||||
test:be:perf:
|
||||
desc: Backend performance regression tests (not run in CI; run locally when needed)
|
||||
desc: Backend performance regression tests only (hot paths + bottlenecks)
|
||||
cmds:
|
||||
- poetry run pytest tests/backend/test_performance_hotpaths.py tests/backend/test_performance_bottlenecks.py
|
||||
|
||||
test:be:full:
|
||||
desc: All backend tests including performance hot paths (local)
|
||||
desc: Same as test:be (full backend suite)
|
||||
cmds:
|
||||
- poetry run pytest tests/backend -n auto --cov=meshchatx/src/backend
|
||||
|
||||
@@ -186,9 +186,10 @@ tasks:
|
||||
- poetry run mutmut run "meshchatx.src.backend.meshchat_utils*"
|
||||
|
||||
test:fe:
|
||||
desc: Run frontend tests (vitest; excludes i18n — see test:lang; excludes LoadTimePerformance — use test:fe:loadtime locally)
|
||||
desc: Run frontend + Electron shell unit tests (vitest; excludes i18n — see test:lang; excludes LoadTimePerformance — use test:fe:loadtime locally)
|
||||
cmds:
|
||||
- "{{.NPM}} run test -- --exclude tests/frontend/i18n.test.js"
|
||||
- "{{.NPM}} exec vitest run --exclude tests/frontend/LoadTimePerformance.test.js --exclude tests/frontend/i18n.test.js"
|
||||
- "{{.NPM}} exec vitest run --config vitest.electron.config.js"
|
||||
|
||||
test:fe:loadtime:
|
||||
desc: Optional frontend load-time / sidebar performance tests (not run in CI)
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
"use strict";
|
||||
|
||||
const crypto = require("crypto");
|
||||
const fs = require("fs");
|
||||
const path = require("node:path");
|
||||
|
||||
/**
|
||||
* Verify SHA-256 hashes of backend files against backend-manifest.json next to the executable.
|
||||
* @param {string} exeDir Directory containing the backend binary and manifest.
|
||||
* @returns {{ ok: boolean, issues: string[] }}
|
||||
*/
|
||||
function verifyBackendIntegrity(exeDir) {
|
||||
const manifestPath = path.join(exeDir, "backend-manifest.json");
|
||||
if (!fs.existsSync(manifestPath)) {
|
||||
return { ok: true, issues: ["Manifest missing"] };
|
||||
}
|
||||
|
||||
try {
|
||||
const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
|
||||
const issues = [];
|
||||
|
||||
const filesToVerify = manifest.files || manifest;
|
||||
const metadata = manifest._metadata || {};
|
||||
|
||||
for (const [relPath, expectedHash] of Object.entries(filesToVerify)) {
|
||||
const fullPath = path.join(exeDir, relPath);
|
||||
if (!fs.existsSync(fullPath)) {
|
||||
issues.push(`Missing: ${relPath}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const fileBuffer = fs.readFileSync(fullPath);
|
||||
const actualHash = crypto.createHash("sha256").update(fileBuffer).digest("hex");
|
||||
if (actualHash !== expectedHash) {
|
||||
issues.push(`Modified: ${relPath}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (issues.length > 0 && metadata.date && metadata.time) {
|
||||
issues.unshift(`Backend build timestamp: ${metadata.date} ${metadata.time}`);
|
||||
}
|
||||
|
||||
return {
|
||||
ok: issues.length === 0,
|
||||
issues: issues,
|
||||
};
|
||||
} catch (error) {
|
||||
return { ok: false, issues: [error.message] };
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { verifyBackendIntegrity };
|
||||
+11
-78
@@ -17,7 +17,8 @@ const { spawn } = require("child_process");
|
||||
const fs = require("fs");
|
||||
const path = require("node:path");
|
||||
|
||||
const crypto = require("crypto");
|
||||
const { verifyBackendIntegrity } = require("./backendIntegrity");
|
||||
const { getUserProvidedArguments, formatRenderProcessGoneDetails, isLocalBackendUrl } = require("./mainHelpers");
|
||||
|
||||
// remember main window
|
||||
var mainWindow = null;
|
||||
@@ -121,50 +122,6 @@ app.on("open-url", (event, url) => {
|
||||
}
|
||||
});
|
||||
|
||||
function verifyBackendIntegrity(exeDir) {
|
||||
const manifestPath = path.join(exeDir, "backend-manifest.json");
|
||||
if (!fs.existsSync(manifestPath)) {
|
||||
log("Backend integrity manifest missing, skipping check.");
|
||||
return { ok: true, issues: ["Manifest missing"] };
|
||||
}
|
||||
|
||||
try {
|
||||
const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
|
||||
const issues = [];
|
||||
|
||||
const filesToVerify = manifest.files || manifest;
|
||||
const metadata = manifest._metadata || {};
|
||||
|
||||
// The exeDir is build/exe when running or unpacked
|
||||
// we only care about files in the manifest
|
||||
for (const [relPath, expectedHash] of Object.entries(filesToVerify)) {
|
||||
const fullPath = path.join(exeDir, relPath);
|
||||
if (!fs.existsSync(fullPath)) {
|
||||
issues.push(`Missing: ${relPath}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const fileBuffer = fs.readFileSync(fullPath);
|
||||
const actualHash = crypto.createHash("sha256").update(fileBuffer).digest("hex");
|
||||
if (actualHash !== expectedHash) {
|
||||
issues.push(`Modified: ${relPath}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (issues.length > 0 && metadata.date && metadata.time) {
|
||||
issues.unshift(`Backend build timestamp: ${metadata.date} ${metadata.time}`);
|
||||
}
|
||||
|
||||
return {
|
||||
ok: issues.length === 0,
|
||||
issues: issues,
|
||||
};
|
||||
} catch (error) {
|
||||
log(`Backend integrity check failed: ${error.message}`);
|
||||
return { ok: false, issues: [error.message] };
|
||||
}
|
||||
}
|
||||
|
||||
// allow fetching app version via ipc
|
||||
ipcMain.handle("app-version", () => {
|
||||
return app.getVersion();
|
||||
@@ -217,13 +174,8 @@ ipcMain.handle("set-power-save-blocker", (event, enabled) => {
|
||||
// ignore ssl errors
|
||||
app.commandLine.appendSwitch("ignore-certificate-errors");
|
||||
|
||||
function getUserProvidedArguments() {
|
||||
const ignoredArguments = ["--no-sandbox", "--ozone-platform-hint=auto"];
|
||||
return process.argv.slice(1).filter((arg) => !ignoredArguments.includes(arg));
|
||||
}
|
||||
|
||||
ipcMain.handle("backend-http-only", () => {
|
||||
return getUserProvidedArguments().includes("--no-https");
|
||||
return getUserProvidedArguments(process.argv).includes("--no-https");
|
||||
});
|
||||
|
||||
ipcMain.handle("backend-runtime-state", () => {
|
||||
@@ -437,20 +389,6 @@ function log(message) {
|
||||
mainWindow.webContents.send("log", message);
|
||||
}
|
||||
|
||||
function formatRenderProcessGoneDetails(details) {
|
||||
if (!details) {
|
||||
return "no details";
|
||||
}
|
||||
return JSON.stringify(
|
||||
{
|
||||
reason: details.reason || "unknown",
|
||||
exitCode: details.exitCode,
|
||||
},
|
||||
null,
|
||||
2
|
||||
);
|
||||
}
|
||||
|
||||
function getDefaultStorageDir() {
|
||||
// if we are running a windows portable exe, we want to use .reticulum-meshchat in the portable exe dir
|
||||
// e.g if we launch "E:\Some\Path\MeshChat.exe" we want to use "E:\Some\Path\.reticulum-meshchat"
|
||||
@@ -483,18 +421,6 @@ function getAppIconPath() {
|
||||
return fs.existsSync(iconPath) ? iconPath : fallbackIconPath;
|
||||
}
|
||||
|
||||
function isLocalBackendUrl(url) {
|
||||
if (!url || typeof url !== "string") {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
url.startsWith("http://127.0.0.1:9337") ||
|
||||
url.startsWith("https://127.0.0.1:9337") ||
|
||||
url.startsWith("http://localhost:9337") ||
|
||||
url.startsWith("https://localhost:9337")
|
||||
);
|
||||
}
|
||||
|
||||
function createTray() {
|
||||
tray = new Tray(getAppIconPath());
|
||||
const contextMenu = Menu.buildFromTemplate([
|
||||
@@ -569,7 +495,7 @@ app.whenReady().then(async () => {
|
||||
createTray();
|
||||
|
||||
// get arguments passed to application, and remove the provided application path
|
||||
const userProvidedArguments = getUserProvidedArguments();
|
||||
const userProvidedArguments = getUserProvidedArguments(process.argv);
|
||||
const shouldLaunchHeadless = userProvidedArguments.includes("--headless");
|
||||
|
||||
if (!shouldLaunchHeadless) {
|
||||
@@ -739,6 +665,13 @@ app.whenReady().then(async () => {
|
||||
// Verify backend integrity before spawning
|
||||
const exeDir = path.dirname(exe);
|
||||
integrityStatus.backend = verifyBackendIntegrity(exeDir);
|
||||
if (
|
||||
integrityStatus.backend.ok &&
|
||||
integrityStatus.backend.issues.length === 1 &&
|
||||
integrityStatus.backend.issues[0] === "Manifest missing"
|
||||
) {
|
||||
log("Backend integrity manifest missing, skipping check.");
|
||||
}
|
||||
if (!integrityStatus.backend.ok) {
|
||||
log(`INTEGRITY WARNING: Backend tampering detected! Issues: ${integrityStatus.backend.issues.join(", ")}`);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
"use strict";
|
||||
|
||||
const IGNORED_CLI_ARGUMENTS = new Set(["--no-sandbox", "--ozone-platform-hint=auto"]);
|
||||
|
||||
/**
|
||||
* Arguments after argv[0], excluding known Chromium/Electron noise flags.
|
||||
* @param {string[]} argv Typically process.argv
|
||||
* @returns {string[]}
|
||||
*/
|
||||
function getUserProvidedArguments(argv) {
|
||||
const list = Array.isArray(argv) ? argv : [];
|
||||
return list.slice(1).filter((arg) => !IGNORED_CLI_ARGUMENTS.has(arg));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {unknown} details Electron render-process-gone details
|
||||
* @returns {string}
|
||||
*/
|
||||
function formatRenderProcessGoneDetails(details) {
|
||||
if (!details) {
|
||||
return "no details";
|
||||
}
|
||||
return JSON.stringify(
|
||||
{
|
||||
reason: details.reason || "unknown",
|
||||
exitCode: details.exitCode,
|
||||
},
|
||||
null,
|
||||
2
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the URL is the MeshChatX local backend origin (loading / API checks).
|
||||
* @param {unknown} url
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function isLocalBackendUrl(url) {
|
||||
if (!url || typeof url !== "string") {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
url.startsWith("http://127.0.0.1:9337") ||
|
||||
url.startsWith("https://127.0.0.1:9337") ||
|
||||
url.startsWith("http://localhost:9337") ||
|
||||
url.startsWith("https://localhost:9337")
|
||||
);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getUserProvidedArguments,
|
||||
formatRenderProcessGoneDetails,
|
||||
isLocalBackendUrl,
|
||||
};
|
||||
+2
-2
@@ -18,8 +18,8 @@
|
||||
"lint": "eslint .",
|
||||
"lint:fix": "eslint . --fix",
|
||||
"format": "prettier --write .",
|
||||
"test": "vitest run --exclude tests/frontend/LoadTimePerformance.test.js",
|
||||
"test:coverage": "vitest run --coverage --exclude tests/frontend/LoadTimePerformance.test.js",
|
||||
"test": "vitest run --exclude tests/frontend/LoadTimePerformance.test.js && vitest run --config vitest.electron.config.js",
|
||||
"test:coverage": "vitest run --coverage --exclude tests/frontend/LoadTimePerformance.test.js && vitest run --config vitest.electron.config.js --coverage",
|
||||
"test:fuzz": "vitest run -t fuzzing",
|
||||
"test:watch": "vitest --exclude tests/frontend/LoadTimePerformance.test.js",
|
||||
"test:loadtime": "vitest run tests/frontend/LoadTimePerformance.test.js",
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
# SPDX-License-Identifier: 0BSD
|
||||
|
||||
import unittest
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from meshchatx.src.backend.announce_handler import AnnounceHandler
|
||||
|
||||
|
||||
class TestAnnounceHandler(unittest.TestCase):
|
||||
def test_forwards_to_callback_with_aspect_filter(self):
|
||||
cb = MagicMock()
|
||||
handler = AnnounceHandler("test.aspect", cb)
|
||||
handler.received_announce(b"d", b"id", b"app", b"hash")
|
||||
cb.assert_called_once_with("test.aspect", b"d", b"id", b"app", b"hash")
|
||||
|
||||
def test_swallows_callback_exception(self):
|
||||
def bad_cb(_aspect, *_args):
|
||||
raise RuntimeError("simulated handler failure")
|
||||
|
||||
handler = AnnounceHandler("a", bad_cb)
|
||||
handler.received_announce(b"d", b"id", b"app", b"h")
|
||||
@@ -2,9 +2,8 @@
|
||||
|
||||
"""Wall-clock database throughput tests (large seeds + strict ms ceilings).
|
||||
|
||||
Excluded from default `task test:be` / CI (like test_performance_hotpaths.py and
|
||||
test_memory_profiling.py). Run locally: `task test:be:perf` or
|
||||
`pytest tests/backend/test_performance_bottlenecks.py`.
|
||||
Included in the full backend suite (`task test:be`, GitHub CI). For perf-only:
|
||||
`task test:be:perf` or `pytest tests/backend/test_performance_bottlenecks.py`.
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
"""Performance regression tests for the critical hot paths.
|
||||
|
||||
Excluded from default `task test:be` / CI; run locally with `task test:be:perf`
|
||||
or `task test:be:full` (see Taskfile.yml).
|
||||
Run as part of the full backend suite (`task test:be`, `make test`, GitHub CI).
|
||||
For perf-only: `task test:be:perf`.
|
||||
|
||||
Focus areas (user priority):
|
||||
- NomadNet browser: load announces, search announces, favourites
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
import fs from "fs";
|
||||
import os from "os";
|
||||
import path from "path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { createRequire } from "module";
|
||||
import crypto from "crypto";
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
const { verifyBackendIntegrity } = require("../../electron/backendIntegrity.js");
|
||||
|
||||
describe("electron/backendIntegrity", () => {
|
||||
it("returns ok when manifest is absent", () => {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "mcx-int-"));
|
||||
try {
|
||||
const r = verifyBackendIntegrity(dir);
|
||||
expect(r.ok).toBe(true);
|
||||
expect(r.issues).toContain("Manifest missing");
|
||||
} finally {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("detects missing file from manifest", () => {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "mcx-int-"));
|
||||
try {
|
||||
const manifest = { files: { "missing.bin": "abc" } };
|
||||
fs.writeFileSync(path.join(dir, "backend-manifest.json"), JSON.stringify(manifest), "utf8");
|
||||
const r = verifyBackendIntegrity(dir);
|
||||
expect(r.ok).toBe(false);
|
||||
expect(r.issues.some((i) => i.includes("Missing:"))).toBe(true);
|
||||
} finally {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("detects hash mismatch", () => {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "mcx-int-"));
|
||||
try {
|
||||
const fileRel = "blob.bin";
|
||||
const full = path.join(dir, fileRel);
|
||||
fs.writeFileSync(full, "hello", "utf8");
|
||||
const wrongHash = "0".repeat(64);
|
||||
const manifest = { files: { [fileRel]: wrongHash } };
|
||||
fs.writeFileSync(path.join(dir, "backend-manifest.json"), JSON.stringify(manifest), "utf8");
|
||||
const r = verifyBackendIntegrity(dir);
|
||||
expect(r.ok).toBe(false);
|
||||
expect(r.issues.some((i) => i.includes("Modified:"))).toBe(true);
|
||||
} finally {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("accepts matching manifest", () => {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "mcx-int-"));
|
||||
try {
|
||||
const fileRel = "blob.bin";
|
||||
const full = path.join(dir, fileRel);
|
||||
const data = Buffer.from("integrity-test", "utf8");
|
||||
fs.writeFileSync(full, data);
|
||||
const hash = crypto.createHash("sha256").update(data).digest("hex");
|
||||
const manifest = {
|
||||
files: { [fileRel]: hash },
|
||||
_metadata: { date: "2020-01-01", time: "12:00:00" },
|
||||
};
|
||||
fs.writeFileSync(path.join(dir, "backend-manifest.json"), JSON.stringify(manifest), "utf8");
|
||||
const r = verifyBackendIntegrity(dir);
|
||||
expect(r.ok).toBe(true);
|
||||
expect(r.issues).toHaveLength(0);
|
||||
} finally {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,35 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { createRequire } from "module";
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
const notice = require("../../electron/loadingStatusNotice.js");
|
||||
|
||||
describe("electron/loadingStatusNotice", () => {
|
||||
it("classifyConnectionIssue prefers backend-exited when runtime not running", () => {
|
||||
const r = notice.classifyConnectionIssue([], {
|
||||
running: false,
|
||||
lastExitCode: 1,
|
||||
});
|
||||
expect(r.reason).toBe("backend-exited");
|
||||
});
|
||||
|
||||
it("classifyConnectionIssue flags loopback blocked", () => {
|
||||
const r = notice.classifyConnectionIssue([{ kind: "address-unreachable" }], { running: true });
|
||||
expect(r.reason).toBe("loopback-blocked");
|
||||
});
|
||||
|
||||
it("classifyConnectionIssue uses starting before network threshold", () => {
|
||||
const r = notice.classifyConnectionIssue([{ kind: "network-error" }], { running: true }, { attemptCount: 1 });
|
||||
expect(r.reason).toBe("starting");
|
||||
});
|
||||
|
||||
it("classifyFetchError maps address unreachable", () => {
|
||||
expect(notice.classifyFetchError({ name: "Error", message: "net::ERR_ADDRESS_UNREACHABLE" })).toBe(
|
||||
"address-unreachable"
|
||||
);
|
||||
});
|
||||
|
||||
it("classifyFetchError defaults to network-error", () => {
|
||||
expect(notice.classifyFetchError({ name: "TypeError", message: "fetch failed" })).toBe("network-error");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,34 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { createRequire } from "module";
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
const {
|
||||
getUserProvidedArguments,
|
||||
formatRenderProcessGoneDetails,
|
||||
isLocalBackendUrl,
|
||||
} = require("../../electron/mainHelpers.js");
|
||||
|
||||
describe("electron/mainHelpers", () => {
|
||||
it("getUserProvidedArguments filters ignored flags and skips argv[0]", () => {
|
||||
const argv = ["/app/electron", "--no-https", "--no-sandbox", "--ozone-platform-hint=auto", "--port", "1"];
|
||||
expect(getUserProvidedArguments(argv)).toEqual(["--no-https", "--port", "1"]);
|
||||
});
|
||||
|
||||
it("formatRenderProcessGoneDetails handles null/undefined", () => {
|
||||
expect(formatRenderProcessGoneDetails(null)).toBe("no details");
|
||||
expect(formatRenderProcessGoneDetails(undefined)).toBe("no details");
|
||||
});
|
||||
|
||||
it("formatRenderProcessGoneDetails serializes reason and exitCode", () => {
|
||||
const s = formatRenderProcessGoneDetails({ reason: "crashed", exitCode: 5 });
|
||||
expect(s).toContain("crashed");
|
||||
expect(s).toContain("5");
|
||||
});
|
||||
|
||||
it("isLocalBackendUrl matches localhost backends only", () => {
|
||||
expect(isLocalBackendUrl("https://127.0.0.1:9337/api")).toBe(true);
|
||||
expect(isLocalBackendUrl("http://localhost:9337/")).toBe(true);
|
||||
expect(isLocalBackendUrl("https://example.com")).toBe(false);
|
||||
expect(isLocalBackendUrl("")).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,81 @@
|
||||
import { createRequire } from "module";
|
||||
import path from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const preloadPath = path.resolve(__dirname, "../../electron/preload.js");
|
||||
const rootRequire = createRequire(import.meta.url);
|
||||
const nodeModule = rootRequire("module");
|
||||
|
||||
function loadPreloadWithElectronMock(mockElectron) {
|
||||
const orig = nodeModule.prototype.require;
|
||||
nodeModule.prototype.require = function patchedRequire(id) {
|
||||
if (id === "electron") {
|
||||
return mockElectron;
|
||||
}
|
||||
return orig.apply(this, arguments);
|
||||
};
|
||||
try {
|
||||
delete rootRequire.cache[preloadPath];
|
||||
rootRequire(preloadPath);
|
||||
} finally {
|
||||
nodeModule.prototype.require = orig;
|
||||
}
|
||||
}
|
||||
|
||||
describe("electron/preload", () => {
|
||||
afterEach(() => {
|
||||
delete rootRequire.cache[preloadPath];
|
||||
});
|
||||
|
||||
it("registers contextBridge API and forwards invoke to ipcRenderer", async () => {
|
||||
const exposeInMainWorld = vi.fn();
|
||||
const invoke = vi.fn();
|
||||
const on = vi.fn();
|
||||
const mockElectron = {
|
||||
contextBridge: { exposeInMainWorld },
|
||||
ipcRenderer: { invoke, on },
|
||||
};
|
||||
loadPreloadWithElectronMock(mockElectron);
|
||||
expect(exposeInMainWorld).toHaveBeenCalledWith("electron", expect.any(Object));
|
||||
const api = exposeInMainWorld.mock.calls[0][1];
|
||||
invoke.mockResolvedValueOnce("9.9.9");
|
||||
await expect(api.appVersion()).resolves.toBe("9.9.9");
|
||||
expect(invoke).toHaveBeenCalledWith("app-version");
|
||||
|
||||
invoke.mockResolvedValueOnce(true);
|
||||
await expect(api.isHardwareAccelerationEnabled()).resolves.toBe(true);
|
||||
expect(invoke).toHaveBeenCalledWith("is-hardware-acceleration-enabled");
|
||||
|
||||
api.showNotification("t", "b", true);
|
||||
expect(invoke).toHaveBeenCalledWith("show-notification", { title: "t", body: "b", silent: true });
|
||||
});
|
||||
|
||||
it("onProtocolLink registers ipc listener for open-protocol-link", () => {
|
||||
const exposeInMainWorld = vi.fn();
|
||||
const invoke = vi.fn();
|
||||
const on = vi.fn();
|
||||
loadPreloadWithElectronMock({
|
||||
contextBridge: { exposeInMainWorld },
|
||||
ipcRenderer: { invoke, on },
|
||||
});
|
||||
const api = exposeInMainWorld.mock.calls[0][1];
|
||||
const cb = vi.fn();
|
||||
api.onProtocolLink(cb);
|
||||
const handler = on.mock.calls.find((c) => c[0] === "open-protocol-link")?.[1];
|
||||
expect(handler).toEqual(expect.any(Function));
|
||||
handler({}, "rns://x");
|
||||
expect(cb).toHaveBeenCalledWith("rns://x");
|
||||
});
|
||||
|
||||
it("subscribes to log channel on load", () => {
|
||||
const exposeInMainWorld = vi.fn();
|
||||
const on = vi.fn();
|
||||
loadPreloadWithElectronMock({
|
||||
contextBridge: { exposeInMainWorld },
|
||||
ipcRenderer: { invoke: vi.fn(), on },
|
||||
});
|
||||
expect(on).toHaveBeenCalledWith("log", expect.any(Function));
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,16 @@
|
||||
import { defineConfig } from "vitest/config";
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
globals: true,
|
||||
environment: "node",
|
||||
include: ["tests/electron/**/*.test.js"],
|
||||
coverage: {
|
||||
provider: "v8",
|
||||
reporter: ["text", "json-summary"],
|
||||
reportsDirectory: "./coverage-electron",
|
||||
include: ["electron/**/*.js"],
|
||||
exclude: ["electron/assets/**", "electron/main-legacy.js"],
|
||||
},
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user