mirror of
https://git.quad4.io/RNS-Things/MeshChatX.git
synced 2026-04-27 12:55:54 +00:00
feat(electron, frontend, tests): update Linux app name handling, add UI transparency options, and improve message scrolling tests; refactor icon path retrieval and update license artifact generation logic
This commit is contained in:
+12
-4
@@ -68,6 +68,10 @@ if (process.argv.includes("--disable-gpu") || process.argv.includes("--disable-s
|
||||
app.disableHardwareAcceleration();
|
||||
}
|
||||
|
||||
if (process.platform === "linux") {
|
||||
app.setName("reticulum-meshchatx");
|
||||
}
|
||||
|
||||
// Protocol registration
|
||||
if (process.defaultApp) {
|
||||
if (process.argv.length >= 2) {
|
||||
@@ -411,7 +415,7 @@ function formatRenderProcessGoneDetails(details) {
|
||||
exitCode: details.exitCode,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
2
|
||||
);
|
||||
}
|
||||
|
||||
@@ -441,12 +445,14 @@ function getDefaultReticulumConfigDir() {
|
||||
return path.join(app.getPath("home"), ".reticulum");
|
||||
}
|
||||
|
||||
function createTray() {
|
||||
function getAppIconPath() {
|
||||
const iconPath = path.join(__dirname, "build", "icon.png");
|
||||
const fallbackIconPath = path.join(__dirname, "assets", "images", "logo.png");
|
||||
const trayIcon = fs.existsSync(iconPath) ? iconPath : fallbackIconPath;
|
||||
return fs.existsSync(iconPath) ? iconPath : fallbackIconPath;
|
||||
}
|
||||
|
||||
tray = new Tray(trayIcon);
|
||||
function createTray() {
|
||||
tray = new Tray(getAppIconPath());
|
||||
const contextMenu = Menu.buildFromTemplate([
|
||||
{
|
||||
label: "Show App",
|
||||
@@ -523,10 +529,12 @@ app.whenReady().then(async () => {
|
||||
const shouldLaunchHeadless = userProvidedArguments.includes("--headless");
|
||||
|
||||
if (!shouldLaunchHeadless) {
|
||||
const appIconPath = getAppIconPath();
|
||||
// create browser window
|
||||
mainWindow = new BrowserWindow({
|
||||
width: 1500,
|
||||
height: 800,
|
||||
icon: appIconPath,
|
||||
webPreferences: {
|
||||
// used to inject logging over ipc
|
||||
preload: path.join(__dirname, "preload.js"),
|
||||
|
||||
@@ -24,6 +24,9 @@ const globalState = reactive({
|
||||
nomad_render_html_enabled: true,
|
||||
nomad_render_plaintext_enabled: true,
|
||||
nomad_default_page_path: "/page/index.mu",
|
||||
ui_transparency: 0,
|
||||
ui_glass_enabled: true,
|
||||
message_list_virtualization: true,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -21,6 +21,8 @@ export const MESHCHAT_THEME_VARIABLES_LIGHT = {
|
||||
"--mc-focus-border": "#60a5fa",
|
||||
"--mc-accent": "#2563eb",
|
||||
"--mc-accent-hover": "#3b82f6",
|
||||
"--mc-action-primary": "#2563eb",
|
||||
"--mc-action-primary-hover": "#3b82f6",
|
||||
"--mc-scrollbar-thumb": "#94a3b8",
|
||||
"--mc-scrollbar-track": "#e2e8f0",
|
||||
"--mc-scrollbar-thumb-border": "#e2e8f0",
|
||||
@@ -72,10 +74,12 @@ export const MESHCHAT_THEME_VARIABLES_DARK = {
|
||||
"--mc-text-secondary": "#ffffff",
|
||||
"--mc-text-muted": "#9ca3af",
|
||||
"--mc-text-label": "#e5e7eb",
|
||||
"--mc-focus": "#60a5fa",
|
||||
"--mc-focus-border": "#60a5fa",
|
||||
"--mc-accent": "#2563eb",
|
||||
"--mc-focus": "#3b82f6",
|
||||
"--mc-focus-border": "#3b82f6",
|
||||
"--mc-accent": "#60a5fa",
|
||||
"--mc-accent-hover": "#3b82f6",
|
||||
"--mc-action-primary": "#2563eb",
|
||||
"--mc-action-primary-hover": "#3b82f6",
|
||||
"--mc-scrollbar-thumb": "#52525b",
|
||||
"--mc-scrollbar-track": "#18181b",
|
||||
"--mc-scrollbar-thumb-border": "#18181b",
|
||||
@@ -133,6 +137,8 @@ export function tailwindSemanticColorExtend() {
|
||||
"fg-label": "var(--mc-text-label)",
|
||||
accent: "var(--mc-accent)",
|
||||
"accent-hover": "var(--mc-accent-hover)",
|
||||
"action-primary": "var(--mc-action-primary)",
|
||||
"action-primary-hover": "var(--mc-action-primary-hover)",
|
||||
focus: "var(--mc-focus)",
|
||||
glass: "var(--mc-glass-surface)",
|
||||
"surface-muted": "var(--mc-surface-muted)",
|
||||
@@ -190,7 +196,7 @@ export function vuetifyThemesFromTokens() {
|
||||
colors: {
|
||||
background: L["--mc-canvas"],
|
||||
surface: L["--mc-surface"],
|
||||
primary: L["--mc-accent"],
|
||||
primary: L["--mc-action-primary"],
|
||||
secondary: "#475569",
|
||||
error: L["--mc-error"],
|
||||
info: L["--mc-info"],
|
||||
@@ -203,7 +209,7 @@ export function vuetifyThemesFromTokens() {
|
||||
colors: {
|
||||
background: D["--mc-canvas"],
|
||||
surface: D["--mc-surface"],
|
||||
primary: D["--mc-accent"],
|
||||
primary: D["--mc-action-primary"],
|
||||
secondary: "#94a3b8",
|
||||
error: D["--mc-error"],
|
||||
info: D["--mc-info"],
|
||||
|
||||
+78
-17
@@ -59,6 +59,71 @@ function generateManifest(buildDir, manifestPath) {
|
||||
console.log(`Manifest saved to ${manifestPath} (${Object.keys(manifest.files).length} files)`);
|
||||
}
|
||||
|
||||
function failOnSpawnResult(stepName, result) {
|
||||
if (result.error) {
|
||||
throw result.error;
|
||||
}
|
||||
|
||||
if (result.signal) {
|
||||
console.error(`${stepName} was terminated by signal ${result.signal}.`);
|
||||
if (result.signal === "SIGKILL") {
|
||||
console.error(
|
||||
"Build process was force-killed (often OOM killer). This is likely memory pressure, not a normal script error."
|
||||
);
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (result.status !== 0) {
|
||||
console.error(`${stepName} exited with status ${result.status}.`);
|
||||
process.exit(result.status || 1);
|
||||
}
|
||||
}
|
||||
|
||||
function fileMtimeMs(filePath) {
|
||||
if (!fs.existsSync(filePath)) {
|
||||
return null;
|
||||
}
|
||||
return fs.statSync(filePath).mtimeMs;
|
||||
}
|
||||
|
||||
function shouldRefreshLicenseArtifacts(repoRoot) {
|
||||
const forceRefresh =
|
||||
process.env.MESHCHATX_FORCE_LICENSE_ARTIFACTS === "1" ||
|
||||
process.env.MESHCHATX_FORCE_LICENSE_ARTIFACTS === "true";
|
||||
if (forceRefresh) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const dataDir = path.join(repoRoot, "meshchatx", "src", "backend", "data");
|
||||
const noticesPath = path.join(dataDir, "THIRD_PARTY_NOTICES.txt");
|
||||
const frontendLicensesPath = path.join(dataDir, "licenses_frontend.json");
|
||||
|
||||
const outputTimes = [fileMtimeMs(noticesPath), fileMtimeMs(frontendLicensesPath)];
|
||||
if (outputTimes.some((t) => t == null)) {
|
||||
return true;
|
||||
}
|
||||
const oldestOutput = Math.min(...outputTimes);
|
||||
|
||||
const inputFiles = [
|
||||
path.join(repoRoot, "pyproject.toml"),
|
||||
path.join(repoRoot, "poetry.lock"),
|
||||
path.join(repoRoot, "package.json"),
|
||||
path.join(repoRoot, "pnpm-lock.yaml"),
|
||||
path.join(repoRoot, "meshchatx", "src", "backend", "licenses_collector.py"),
|
||||
];
|
||||
|
||||
let newestInput = 0;
|
||||
for (const inputFile of inputFiles) {
|
||||
const mtime = fileMtimeMs(inputFile);
|
||||
if (mtime != null && mtime > newestInput) {
|
||||
newestInput = mtime;
|
||||
}
|
||||
}
|
||||
|
||||
return newestInput >= oldestOutput;
|
||||
}
|
||||
|
||||
try {
|
||||
const platform = process.env.PLATFORM || process.platform;
|
||||
const arch = process.env.ARCH || process.arch;
|
||||
@@ -103,17 +168,17 @@ try {
|
||||
spawnArgs = ["-x86_64", cmd, ...licensesArgs];
|
||||
}
|
||||
|
||||
console.log("Generating embedded third-party license artifacts...");
|
||||
const licensesResult = spawnSync(spawnCmd, spawnArgs, {
|
||||
stdio: "inherit",
|
||||
shell: false,
|
||||
env: env,
|
||||
});
|
||||
if (licensesResult.error) {
|
||||
throw licensesResult.error;
|
||||
}
|
||||
if (licensesResult.status !== 0) {
|
||||
process.exit(licensesResult.status || 1);
|
||||
const repoRoot = path.join(__dirname, "..");
|
||||
if (shouldRefreshLicenseArtifacts(repoRoot)) {
|
||||
console.log("Generating embedded third-party license artifacts...");
|
||||
const licensesResult = spawnSync(spawnCmd, spawnArgs, {
|
||||
stdio: "inherit",
|
||||
shell: false,
|
||||
env: env,
|
||||
});
|
||||
failOnSpawnResult("License artifact generation", licensesResult);
|
||||
} else {
|
||||
console.log("Skipping license artifact generation (artifacts are up to date).");
|
||||
}
|
||||
|
||||
spawnCmd = cmd;
|
||||
@@ -123,17 +188,13 @@ try {
|
||||
spawnArgs = ["-x86_64", cmd, ...args];
|
||||
}
|
||||
|
||||
console.log("Running cx_Freeze backend build...");
|
||||
const result = spawnSync(spawnCmd, spawnArgs, {
|
||||
stdio: "inherit",
|
||||
shell: false,
|
||||
env: env,
|
||||
});
|
||||
if (result.error) {
|
||||
throw result.error;
|
||||
}
|
||||
if (result.status !== 0) {
|
||||
process.exit(result.status || 1);
|
||||
}
|
||||
failOnSpawnResult("Backend build", result);
|
||||
|
||||
if (fs.existsSync(buildDir)) {
|
||||
if (isDarwin) {
|
||||
|
||||
@@ -133,7 +133,11 @@ def test_lxst_switch_profile_updates_codec_and_frame_time(monkeypatch):
|
||||
|
||||
telephone = LXSTTelephony.Telephone(identity)
|
||||
telephone.call_status = LXSTTelephony.Signalling.STATUS_ESTABLISHED
|
||||
telephone.active_call = SimpleNamespace(profile=LXSTTelephony.Profiles.QUALITY_MEDIUM, filters=[], packetizer=MagicMock())
|
||||
telephone.active_call = SimpleNamespace(
|
||||
profile=LXSTTelephony.Profiles.QUALITY_MEDIUM,
|
||||
filters=[],
|
||||
packetizer=MagicMock(),
|
||||
)
|
||||
telephone.transmit_mixer = _FakeMixer(target_frame_ms=60, gain=0.0)
|
||||
telephone.audio_input = _FakeLineSource()
|
||||
telephone.transmit_pipeline = _FakePipeline()
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import os
|
||||
import json
|
||||
import shutil
|
||||
import tempfile
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
@@ -253,7 +254,7 @@ async def test_reload_reticulum_failure_recovery(mock_rns, temp_dir):
|
||||
# or just mock a method inside the try block to raise.
|
||||
with patch.object(
|
||||
app,
|
||||
"teardown_identity",
|
||||
"_teardown_all_contexts_for_reload",
|
||||
side_effect=Exception("Reload failed"),
|
||||
):
|
||||
result = await app.reload_reticulum()
|
||||
@@ -312,3 +313,378 @@ async def test_hotswap_identity(mock_rns, temp_dir):
|
||||
broadcast_call = app.websocket_broadcast.call_args[0][0]
|
||||
assert "identity_switched" in broadcast_call
|
||||
app.teardown_identity()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_reload_reticulum_restores_same_identity(mock_rns, temp_dir):
|
||||
with (
|
||||
patch("meshchatx.src.backend.identity_context.Database"),
|
||||
patch("meshchatx.src.backend.identity_context.ConfigManager"),
|
||||
patch("meshchatx.src.backend.identity_context.MessageHandler"),
|
||||
patch("meshchatx.src.backend.identity_context.AnnounceManager"),
|
||||
patch("meshchatx.src.backend.identity_context.ArchiverManager"),
|
||||
patch("meshchatx.src.backend.identity_context.MapManager"),
|
||||
patch("meshchatx.src.backend.identity_context.TelephoneManager"),
|
||||
patch("meshchatx.src.backend.identity_context.VoicemailManager"),
|
||||
patch("meshchatx.src.backend.identity_context.RingtoneManager"),
|
||||
patch("meshchatx.src.backend.identity_context.RNCPHandler"),
|
||||
patch("meshchatx.src.backend.identity_context.RNStatusHandler"),
|
||||
patch("meshchatx.src.backend.identity_context.RNProbeHandler"),
|
||||
patch("meshchatx.src.backend.identity_context.TranslatorHandler"),
|
||||
patch("LXMF.LXMRouter"),
|
||||
patch("asyncio.sleep", return_value=None),
|
||||
patch("socket.socket") as mock_socket,
|
||||
):
|
||||
mock_sock_inst = MagicMock()
|
||||
mock_socket.return_value = mock_sock_inst
|
||||
|
||||
app = ReticulumMeshChat(
|
||||
identity=mock_rns["id_instance"],
|
||||
storage_dir=temp_dir,
|
||||
reticulum_config_dir=temp_dir,
|
||||
)
|
||||
original_identity = app.identity
|
||||
app.setup_identity = MagicMock()
|
||||
app.cleanup_rns_state_for_identity = MagicMock()
|
||||
|
||||
result = await app.reload_reticulum()
|
||||
|
||||
assert result is True
|
||||
app.cleanup_rns_state_for_identity.assert_called_with(original_identity.hash)
|
||||
app.setup_identity.assert_called_with(original_identity)
|
||||
app.teardown_identity()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_transport_enable_endpoint_reloads_rns(mock_rns, temp_dir):
|
||||
with (
|
||||
patch("meshchatx.src.backend.identity_context.Database"),
|
||||
patch("meshchatx.src.backend.identity_context.ConfigManager"),
|
||||
patch("meshchatx.src.backend.identity_context.MessageHandler"),
|
||||
patch("meshchatx.src.backend.identity_context.AnnounceManager"),
|
||||
patch("meshchatx.src.backend.identity_context.ArchiverManager"),
|
||||
patch("meshchatx.src.backend.identity_context.MapManager"),
|
||||
patch("meshchatx.src.backend.identity_context.TelephoneManager"),
|
||||
patch("meshchatx.src.backend.identity_context.VoicemailManager"),
|
||||
patch("meshchatx.src.backend.identity_context.RingtoneManager"),
|
||||
patch("meshchatx.src.backend.identity_context.RNCPHandler"),
|
||||
patch("meshchatx.src.backend.identity_context.RNStatusHandler"),
|
||||
patch("meshchatx.src.backend.identity_context.RNProbeHandler"),
|
||||
patch("meshchatx.src.backend.identity_context.TranslatorHandler"),
|
||||
patch("LXMF.LXMRouter"),
|
||||
):
|
||||
app = ReticulumMeshChat(
|
||||
identity=mock_rns["id_instance"],
|
||||
storage_dir=temp_dir,
|
||||
reticulum_config_dir=temp_dir,
|
||||
)
|
||||
app.reload_reticulum = AsyncMock(return_value=True)
|
||||
|
||||
handler = None
|
||||
for route in app.get_routes():
|
||||
if (
|
||||
route.path == "/api/v1/reticulum/enable-transport"
|
||||
and route.method == "POST"
|
||||
):
|
||||
handler = route.handler
|
||||
break
|
||||
|
||||
assert handler is not None
|
||||
|
||||
response = await handler(MagicMock())
|
||||
payload = json.loads(response.body)
|
||||
|
||||
assert response.status == 200
|
||||
assert (
|
||||
payload["message"]
|
||||
== "Transport mode enabled and RNS restarted successfully."
|
||||
)
|
||||
app.reload_reticulum.assert_awaited_once()
|
||||
app.teardown_identity()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_transport_disable_endpoint_reloads_rns(mock_rns, temp_dir):
|
||||
with (
|
||||
patch("meshchatx.src.backend.identity_context.Database"),
|
||||
patch("meshchatx.src.backend.identity_context.ConfigManager"),
|
||||
patch("meshchatx.src.backend.identity_context.MessageHandler"),
|
||||
patch("meshchatx.src.backend.identity_context.AnnounceManager"),
|
||||
patch("meshchatx.src.backend.identity_context.ArchiverManager"),
|
||||
patch("meshchatx.src.backend.identity_context.MapManager"),
|
||||
patch("meshchatx.src.backend.identity_context.TelephoneManager"),
|
||||
patch("meshchatx.src.backend.identity_context.VoicemailManager"),
|
||||
patch("meshchatx.src.backend.identity_context.RingtoneManager"),
|
||||
patch("meshchatx.src.backend.identity_context.RNCPHandler"),
|
||||
patch("meshchatx.src.backend.identity_context.RNStatusHandler"),
|
||||
patch("meshchatx.src.backend.identity_context.RNProbeHandler"),
|
||||
patch("meshchatx.src.backend.identity_context.TranslatorHandler"),
|
||||
patch("LXMF.LXMRouter"),
|
||||
):
|
||||
app = ReticulumMeshChat(
|
||||
identity=mock_rns["id_instance"],
|
||||
storage_dir=temp_dir,
|
||||
reticulum_config_dir=temp_dir,
|
||||
)
|
||||
app.reload_reticulum = AsyncMock(return_value=True)
|
||||
|
||||
handler = None
|
||||
for route in app.get_routes():
|
||||
if (
|
||||
route.path == "/api/v1/reticulum/disable-transport"
|
||||
and route.method == "POST"
|
||||
):
|
||||
handler = route.handler
|
||||
break
|
||||
|
||||
assert handler is not None
|
||||
|
||||
response = await handler(MagicMock())
|
||||
payload = json.loads(response.body)
|
||||
|
||||
assert response.status == 200
|
||||
assert (
|
||||
payload["message"]
|
||||
== "Transport mode disabled and RNS restarted successfully."
|
||||
)
|
||||
app.reload_reticulum.assert_awaited_once()
|
||||
app.teardown_identity()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_transport_enable_endpoint_reload_failure(mock_rns, temp_dir):
|
||||
with (
|
||||
patch("meshchatx.src.backend.identity_context.Database"),
|
||||
patch("meshchatx.src.backend.identity_context.ConfigManager"),
|
||||
patch("meshchatx.src.backend.identity_context.MessageHandler"),
|
||||
patch("meshchatx.src.backend.identity_context.AnnounceManager"),
|
||||
patch("meshchatx.src.backend.identity_context.ArchiverManager"),
|
||||
patch("meshchatx.src.backend.identity_context.MapManager"),
|
||||
patch("meshchatx.src.backend.identity_context.TelephoneManager"),
|
||||
patch("meshchatx.src.backend.identity_context.VoicemailManager"),
|
||||
patch("meshchatx.src.backend.identity_context.RingtoneManager"),
|
||||
patch("meshchatx.src.backend.identity_context.RNCPHandler"),
|
||||
patch("meshchatx.src.backend.identity_context.RNStatusHandler"),
|
||||
patch("meshchatx.src.backend.identity_context.RNProbeHandler"),
|
||||
patch("meshchatx.src.backend.identity_context.TranslatorHandler"),
|
||||
patch("LXMF.LXMRouter"),
|
||||
):
|
||||
app = ReticulumMeshChat(
|
||||
identity=mock_rns["id_instance"],
|
||||
storage_dir=temp_dir,
|
||||
reticulum_config_dir=temp_dir,
|
||||
)
|
||||
app.reload_reticulum = AsyncMock(return_value=False)
|
||||
|
||||
handler = None
|
||||
for route in app.get_routes():
|
||||
if (
|
||||
route.path == "/api/v1/reticulum/enable-transport"
|
||||
and route.method == "POST"
|
||||
):
|
||||
handler = route.handler
|
||||
break
|
||||
|
||||
assert handler is not None
|
||||
|
||||
response = await handler(MagicMock())
|
||||
payload = json.loads(response.body)
|
||||
|
||||
assert response.status == 500
|
||||
assert (
|
||||
payload["message"]
|
||||
== "Transport mode was enabled in config, but RNS reload failed."
|
||||
)
|
||||
app.reload_reticulum.assert_awaited_once()
|
||||
app.teardown_identity()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_transport_disable_endpoint_reload_failure(mock_rns, temp_dir):
|
||||
with (
|
||||
patch("meshchatx.src.backend.identity_context.Database"),
|
||||
patch("meshchatx.src.backend.identity_context.ConfigManager"),
|
||||
patch("meshchatx.src.backend.identity_context.MessageHandler"),
|
||||
patch("meshchatx.src.backend.identity_context.AnnounceManager"),
|
||||
patch("meshchatx.src.backend.identity_context.ArchiverManager"),
|
||||
patch("meshchatx.src.backend.identity_context.MapManager"),
|
||||
patch("meshchatx.src.backend.identity_context.TelephoneManager"),
|
||||
patch("meshchatx.src.backend.identity_context.VoicemailManager"),
|
||||
patch("meshchatx.src.backend.identity_context.RingtoneManager"),
|
||||
patch("meshchatx.src.backend.identity_context.RNCPHandler"),
|
||||
patch("meshchatx.src.backend.identity_context.RNStatusHandler"),
|
||||
patch("meshchatx.src.backend.identity_context.RNProbeHandler"),
|
||||
patch("meshchatx.src.backend.identity_context.TranslatorHandler"),
|
||||
patch("LXMF.LXMRouter"),
|
||||
):
|
||||
app = ReticulumMeshChat(
|
||||
identity=mock_rns["id_instance"],
|
||||
storage_dir=temp_dir,
|
||||
reticulum_config_dir=temp_dir,
|
||||
)
|
||||
app.reload_reticulum = AsyncMock(return_value=False)
|
||||
|
||||
handler = None
|
||||
for route in app.get_routes():
|
||||
if (
|
||||
route.path == "/api/v1/reticulum/disable-transport"
|
||||
and route.method == "POST"
|
||||
):
|
||||
handler = route.handler
|
||||
break
|
||||
|
||||
assert handler is not None
|
||||
|
||||
response = await handler(MagicMock())
|
||||
payload = json.loads(response.body)
|
||||
|
||||
assert response.status == 500
|
||||
assert (
|
||||
payload["message"]
|
||||
== "Transport mode was disabled in config, but RNS reload failed."
|
||||
)
|
||||
app.reload_reticulum.assert_awaited_once()
|
||||
app.teardown_identity()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_reticulum_reload_endpoint_success(mock_rns, temp_dir):
|
||||
with (
|
||||
patch("meshchatx.src.backend.identity_context.Database"),
|
||||
patch("meshchatx.src.backend.identity_context.ConfigManager"),
|
||||
patch("meshchatx.src.backend.identity_context.MessageHandler"),
|
||||
patch("meshchatx.src.backend.identity_context.AnnounceManager"),
|
||||
patch("meshchatx.src.backend.identity_context.ArchiverManager"),
|
||||
patch("meshchatx.src.backend.identity_context.MapManager"),
|
||||
patch("meshchatx.src.backend.identity_context.TelephoneManager"),
|
||||
patch("meshchatx.src.backend.identity_context.VoicemailManager"),
|
||||
patch("meshchatx.src.backend.identity_context.RingtoneManager"),
|
||||
patch("meshchatx.src.backend.identity_context.RNCPHandler"),
|
||||
patch("meshchatx.src.backend.identity_context.RNStatusHandler"),
|
||||
patch("meshchatx.src.backend.identity_context.RNProbeHandler"),
|
||||
patch("meshchatx.src.backend.identity_context.TranslatorHandler"),
|
||||
patch("LXMF.LXMRouter"),
|
||||
):
|
||||
app = ReticulumMeshChat(
|
||||
identity=mock_rns["id_instance"],
|
||||
storage_dir=temp_dir,
|
||||
reticulum_config_dir=temp_dir,
|
||||
)
|
||||
app.reload_reticulum = AsyncMock(return_value=True)
|
||||
|
||||
handler = None
|
||||
for route in app.get_routes():
|
||||
if route.path == "/api/v1/reticulum/reload" and route.method == "POST":
|
||||
handler = route.handler
|
||||
break
|
||||
|
||||
assert handler is not None
|
||||
|
||||
response = await handler(MagicMock())
|
||||
payload = json.loads(response.body)
|
||||
|
||||
assert response.status == 200
|
||||
assert payload["message"] == "Reticulum reloaded successfully"
|
||||
app.reload_reticulum.assert_awaited_once()
|
||||
app.teardown_identity()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_reticulum_reload_endpoint_failure(mock_rns, temp_dir):
|
||||
with (
|
||||
patch("meshchatx.src.backend.identity_context.Database"),
|
||||
patch("meshchatx.src.backend.identity_context.ConfigManager"),
|
||||
patch("meshchatx.src.backend.identity_context.MessageHandler"),
|
||||
patch("meshchatx.src.backend.identity_context.AnnounceManager"),
|
||||
patch("meshchatx.src.backend.identity_context.ArchiverManager"),
|
||||
patch("meshchatx.src.backend.identity_context.MapManager"),
|
||||
patch("meshchatx.src.backend.identity_context.TelephoneManager"),
|
||||
patch("meshchatx.src.backend.identity_context.VoicemailManager"),
|
||||
patch("meshchatx.src.backend.identity_context.RingtoneManager"),
|
||||
patch("meshchatx.src.backend.identity_context.RNCPHandler"),
|
||||
patch("meshchatx.src.backend.identity_context.RNStatusHandler"),
|
||||
patch("meshchatx.src.backend.identity_context.RNProbeHandler"),
|
||||
patch("meshchatx.src.backend.identity_context.TranslatorHandler"),
|
||||
patch("LXMF.LXMRouter"),
|
||||
):
|
||||
app = ReticulumMeshChat(
|
||||
identity=mock_rns["id_instance"],
|
||||
storage_dir=temp_dir,
|
||||
reticulum_config_dir=temp_dir,
|
||||
)
|
||||
app.reload_reticulum = AsyncMock(return_value=False)
|
||||
|
||||
handler = None
|
||||
for route in app.get_routes():
|
||||
if route.path == "/api/v1/reticulum/reload" and route.method == "POST":
|
||||
handler = route.handler
|
||||
break
|
||||
|
||||
assert handler is not None
|
||||
|
||||
response = await handler(MagicMock())
|
||||
payload = json.loads(response.body)
|
||||
|
||||
assert response.status == 500
|
||||
assert payload["error"] == "Failed to reload Reticulum"
|
||||
app.reload_reticulum.assert_awaited_once()
|
||||
app.teardown_identity()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_reload_teardown_stops_all_context_services(mock_rns, temp_dir):
|
||||
with (
|
||||
patch("meshchatx.src.backend.identity_context.Database"),
|
||||
patch("meshchatx.src.backend.identity_context.ConfigManager"),
|
||||
patch("meshchatx.src.backend.identity_context.MessageHandler"),
|
||||
patch("meshchatx.src.backend.identity_context.AnnounceManager"),
|
||||
patch("meshchatx.src.backend.identity_context.ArchiverManager"),
|
||||
patch("meshchatx.src.backend.identity_context.MapManager"),
|
||||
patch("meshchatx.src.backend.identity_context.TelephoneManager"),
|
||||
patch("meshchatx.src.backend.identity_context.VoicemailManager"),
|
||||
patch("meshchatx.src.backend.identity_context.RingtoneManager"),
|
||||
patch("meshchatx.src.backend.identity_context.RNCPHandler"),
|
||||
patch("meshchatx.src.backend.identity_context.RNStatusHandler"),
|
||||
patch("meshchatx.src.backend.identity_context.RNProbeHandler"),
|
||||
patch("meshchatx.src.backend.identity_context.TranslatorHandler"),
|
||||
patch("LXMF.LXMRouter"),
|
||||
):
|
||||
app = ReticulumMeshChat(
|
||||
identity=mock_rns["id_instance"],
|
||||
storage_dir=temp_dir,
|
||||
reticulum_config_dir=temp_dir,
|
||||
)
|
||||
|
||||
identity_a = MagicMock()
|
||||
identity_a.hash = b"a" * 16
|
||||
identity_b = MagicMock()
|
||||
identity_b.hash = b"b" * 16
|
||||
|
||||
ctx_a = MagicMock()
|
||||
ctx_a.identity = identity_a
|
||||
ctx_a.bot_handler = MagicMock()
|
||||
ctx_a.identity_hash = identity_a.hash.hex()
|
||||
|
||||
ctx_b = MagicMock()
|
||||
ctx_b.identity = identity_b
|
||||
ctx_b.bot_handler = MagicMock()
|
||||
ctx_b.identity_hash = identity_b.hash.hex()
|
||||
|
||||
app.contexts = {
|
||||
ctx_a.identity_hash: ctx_a,
|
||||
ctx_b.identity_hash: ctx_b,
|
||||
}
|
||||
app.current_context = ctx_a
|
||||
app.stop_local_propagation_node = MagicMock()
|
||||
app.page_node_manager.teardown = MagicMock()
|
||||
|
||||
app._teardown_all_contexts_for_reload()
|
||||
|
||||
ctx_a.bot_handler.stop_all.assert_called_once()
|
||||
ctx_b.bot_handler.stop_all.assert_called_once()
|
||||
assert app.stop_local_propagation_node.call_count == 2
|
||||
app.page_node_manager.teardown.assert_called_once()
|
||||
ctx_a.teardown.assert_called_once()
|
||||
ctx_b.teardown.assert_called_once()
|
||||
assert app.contexts == {}
|
||||
assert app.current_context is None
|
||||
|
||||
@@ -40,9 +40,17 @@ async def test_initiate_retries_path_requests_during_lookup(telephone_manager):
|
||||
)
|
||||
|
||||
with (
|
||||
patch("meshchatx.src.backend.telephone_manager.RNS.Identity.recall", return_value=MagicMock()),
|
||||
patch("meshchatx.src.backend.telephone_manager.RNS.Transport.has_path", side_effect=has_path),
|
||||
patch("meshchatx.src.backend.telephone_manager.RNS.Transport.request_path") as request_path,
|
||||
patch(
|
||||
"meshchatx.src.backend.telephone_manager.RNS.Identity.recall",
|
||||
return_value=MagicMock(),
|
||||
),
|
||||
patch(
|
||||
"meshchatx.src.backend.telephone_manager.RNS.Transport.has_path",
|
||||
side_effect=has_path,
|
||||
),
|
||||
patch(
|
||||
"meshchatx.src.backend.telephone_manager.RNS.Transport.request_path"
|
||||
) as request_path,
|
||||
):
|
||||
await telephone_manager.initiate(destination_hash, timeout_seconds=1)
|
||||
|
||||
@@ -60,14 +68,22 @@ async def test_initiate_cancels_quickly_while_finding_path_identity(telephone_ma
|
||||
telephone_manager._update_initiation_status(None, None)
|
||||
|
||||
with (
|
||||
patch("meshchatx.src.backend.telephone_manager.RNS.Identity.recall", return_value=None),
|
||||
patch("meshchatx.src.backend.telephone_manager.RNS.Transport.has_path", return_value=False),
|
||||
patch(
|
||||
"meshchatx.src.backend.telephone_manager.RNS.Identity.recall",
|
||||
return_value=None,
|
||||
),
|
||||
patch(
|
||||
"meshchatx.src.backend.telephone_manager.RNS.Transport.has_path",
|
||||
return_value=False,
|
||||
),
|
||||
patch(
|
||||
"meshchatx.src.backend.telephone_manager.RNS.Transport.request_path",
|
||||
side_effect=request_path_and_cancel,
|
||||
),
|
||||
):
|
||||
task = asyncio.create_task(telephone_manager.initiate(destination_hash, timeout_seconds=5))
|
||||
task = asyncio.create_task(
|
||||
telephone_manager.initiate(destination_hash, timeout_seconds=5)
|
||||
)
|
||||
result = await asyncio.wait_for(task, timeout=0.3)
|
||||
|
||||
assert result is None
|
||||
@@ -84,12 +100,23 @@ async def test_initiate_cancels_quickly_while_dialling(telephone_manager):
|
||||
telephone_manager.telephone.call.side_effect = blocking_call
|
||||
|
||||
with (
|
||||
patch("meshchatx.src.backend.telephone_manager.RNS.Identity.recall", return_value=MagicMock()),
|
||||
patch("meshchatx.src.backend.telephone_manager.RNS.Transport.has_path", return_value=True),
|
||||
patch(
|
||||
"meshchatx.src.backend.telephone_manager.RNS.Identity.recall",
|
||||
return_value=MagicMock(),
|
||||
),
|
||||
patch(
|
||||
"meshchatx.src.backend.telephone_manager.RNS.Transport.has_path",
|
||||
return_value=True,
|
||||
),
|
||||
):
|
||||
task = asyncio.create_task(telephone_manager.initiate(destination_hash, timeout_seconds=5))
|
||||
task = asyncio.create_task(
|
||||
telephone_manager.initiate(destination_hash, timeout_seconds=5)
|
||||
)
|
||||
for _ in range(200):
|
||||
if telephone_manager.initiation_status in ("Establishing link...", "Calling..."):
|
||||
if telephone_manager.initiation_status in (
|
||||
"Establishing link...",
|
||||
"Calling...",
|
||||
):
|
||||
break
|
||||
await asyncio.sleep(0)
|
||||
|
||||
@@ -149,7 +176,9 @@ async def test_cancel_after_path_found_before_dialling_stabilizes(telephone_mana
|
||||
return_value=True,
|
||||
),
|
||||
):
|
||||
task = asyncio.create_task(telephone_manager.initiate(destination_hash, timeout_seconds=2))
|
||||
task = asyncio.create_task(
|
||||
telephone_manager.initiate(destination_hash, timeout_seconds=2)
|
||||
)
|
||||
for _ in range(200):
|
||||
if telephone_manager.initiation_status == "Establishing link...":
|
||||
break
|
||||
@@ -254,7 +283,9 @@ async def test_call_thread_exception_surfaces_without_hanging(telephone_manager)
|
||||
"meshchatx.src.backend.telephone_manager.RNS.Transport.has_path",
|
||||
return_value=True,
|
||||
),
|
||||
patch("meshchatx.src.backend.telephone_manager.asyncio.sleep", side_effect=no_wait),
|
||||
patch(
|
||||
"meshchatx.src.backend.telephone_manager.asyncio.sleep", side_effect=no_wait
|
||||
),
|
||||
):
|
||||
result = await asyncio.wait_for(
|
||||
telephone_manager.initiate(destination_hash, timeout_seconds=1),
|
||||
@@ -274,6 +305,7 @@ async def test_inconsistent_call_status_finishes_within_timeout(telephone_manage
|
||||
return None
|
||||
|
||||
telephone_manager.telephone.call.side_effect = inconsistent_call
|
||||
|
||||
async def no_wait(_seconds):
|
||||
return None
|
||||
|
||||
@@ -286,7 +318,9 @@ async def test_inconsistent_call_status_finishes_within_timeout(telephone_manage
|
||||
"meshchatx.src.backend.telephone_manager.RNS.Transport.has_path",
|
||||
return_value=True,
|
||||
),
|
||||
patch("meshchatx.src.backend.telephone_manager.asyncio.sleep", side_effect=no_wait),
|
||||
patch(
|
||||
"meshchatx.src.backend.telephone_manager.asyncio.sleep", side_effect=no_wait
|
||||
),
|
||||
):
|
||||
result = await asyncio.wait_for(
|
||||
telephone_manager.initiate(destination_hash, timeout_seconds=0.2),
|
||||
@@ -339,8 +373,10 @@ async def test_lxst_busy_and_rejected_end_without_stuck_status(telephone_manager
|
||||
for terminal_state in (0, 1):
|
||||
telephone_manager._status_events.clear()
|
||||
telephone_manager.telephone.call_status = 3
|
||||
telephone_manager.telephone.call.side_effect = lambda _identity, state=terminal_state: setattr(
|
||||
telephone_manager.telephone, "call_status", state
|
||||
telephone_manager.telephone.call.side_effect = (
|
||||
lambda _identity, state=terminal_state: setattr(
|
||||
telephone_manager.telephone, "call_status", state
|
||||
)
|
||||
)
|
||||
|
||||
with (
|
||||
@@ -386,7 +422,9 @@ async def test_rapid_dial_cancel_soak_has_bounded_memory(telephone_manager):
|
||||
tracemalloc.start()
|
||||
for _ in range(loops):
|
||||
telephone_manager.telephone.call_status = 3
|
||||
task = asyncio.create_task(telephone_manager.initiate(destination_hash, timeout_seconds=1))
|
||||
task = asyncio.create_task(
|
||||
telephone_manager.initiate(destination_hash, timeout_seconds=1)
|
||||
)
|
||||
await asyncio.sleep(0.005)
|
||||
telephone_manager._update_initiation_status(None, None)
|
||||
await asyncio.wait_for(task, timeout=0.5)
|
||||
|
||||
@@ -1,8 +1,81 @@
|
||||
const crypto = require("crypto");
|
||||
const { expect } = require("@playwright/test");
|
||||
|
||||
const E2E_BACKEND_PORT = process.env.E2E_BACKEND_PORT || "18079";
|
||||
const E2E_BACKEND_ORIGIN = `http://127.0.0.1:${E2E_BACKEND_PORT}`;
|
||||
|
||||
const E2E_SCROLL_PEER_HASH = `e2e0${"0".repeat(28)}`;
|
||||
|
||||
function buildE2eLxmfRow({ peerHash, localHash, index, total, inbound }) {
|
||||
const hash = crypto.randomBytes(16).toString("hex");
|
||||
const baseTs = Math.floor(Date.now() / 1000) - total;
|
||||
return {
|
||||
hash,
|
||||
source_hash: inbound ? peerHash : localHash,
|
||||
destination_hash: inbound ? localHash : peerHash,
|
||||
peer_hash: peerHash,
|
||||
state: "delivered",
|
||||
progress: 1.0,
|
||||
is_incoming: inbound ? 1 : 0,
|
||||
method: "direct",
|
||||
delivery_attempts: 1,
|
||||
next_delivery_attempt_at: null,
|
||||
title: "",
|
||||
content: `E2E scroll seed ${String(index).padStart(3, "0")} ${"x".repeat(64)}`,
|
||||
fields: "{}",
|
||||
timestamp: baseTs + index,
|
||||
rssi: null,
|
||||
snr: null,
|
||||
quality: null,
|
||||
is_spam: 0,
|
||||
reply_to_hash: null,
|
||||
attachments_stripped: 0,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').APIRequestContext} request
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
async function getE2eLocalLxmfHash(request) {
|
||||
const cfgRes = await request.get(`${E2E_BACKEND_ORIGIN}/api/v1/config`);
|
||||
expect(cfgRes.ok()).toBeTruthy();
|
||||
const cfgBody = await cfgRes.json();
|
||||
const localHash = cfgBody.config?.lxmf_address_hash;
|
||||
expect(localHash && String(localHash).length === 32).toBeTruthy();
|
||||
return localHash;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts LXMF rows via maintenance import so the messages UI has a long thread for scroll tests.
|
||||
* @param {import('@playwright/test').APIRequestContext} request
|
||||
* @param {{ messageCount?: number }} [opts]
|
||||
* @returns {Promise<{ peerHash: string, localHash: string }>}
|
||||
*/
|
||||
async function seedE2eLongConversationThread(request, opts = {}) {
|
||||
const messageCount = opts.messageCount ?? 45;
|
||||
const localHash = await getE2eLocalLxmfHash(request);
|
||||
|
||||
const peerHash = E2E_SCROLL_PEER_HASH;
|
||||
const messages = [];
|
||||
for (let i = 0; i < messageCount; i++) {
|
||||
messages.push(
|
||||
buildE2eLxmfRow({
|
||||
peerHash,
|
||||
localHash,
|
||||
index: i,
|
||||
total: messageCount,
|
||||
inbound: i % 2 === 0,
|
||||
})
|
||||
);
|
||||
}
|
||||
const imp = await request.post(`${E2E_BACKEND_ORIGIN}/api/v1/maintenance/messages/import`, {
|
||||
data: { messages },
|
||||
});
|
||||
expect(imp.ok()).toBeTruthy();
|
||||
return { peerHash, localHash };
|
||||
}
|
||||
|
||||
const PALETTE_PLACEHOLDER = /Search commands,\s*(routes|navigate),\s*or peers\.{0,3}/i;
|
||||
|
||||
/**
|
||||
@@ -51,8 +124,11 @@ async function dismissMapOnboardingTooltip(page) {
|
||||
|
||||
module.exports = {
|
||||
E2E_BACKEND_ORIGIN,
|
||||
E2E_SCROLL_PEER_HASH,
|
||||
PALETTE_PLACEHOLDER,
|
||||
dismissMapOnboardingTooltip,
|
||||
openCommandPalette,
|
||||
prepareE2eSession,
|
||||
getE2eLocalLxmfHash,
|
||||
seedE2eLongConversationThread,
|
||||
};
|
||||
|
||||
@@ -82,9 +82,18 @@ test.describe("Messages conversation scroll", () => {
|
||||
test("starts near bottom with a long thread", async ({ page }) => {
|
||||
await page.goto("/#/messages");
|
||||
await expect(page.getByText("Conversations", { exact: true }).first()).toBeVisible({ timeout: 25000 });
|
||||
await page.locator(".conversation-item").filter({ hasText: /E2E scroll seed/ }).first().click();
|
||||
await page
|
||||
.locator(".conversation-item")
|
||||
.filter({ hasText: /E2E scroll seed/ })
|
||||
.first()
|
||||
.click();
|
||||
await expect(page.locator("#messages")).toBeVisible({ timeout: 25000 });
|
||||
await expect(page.locator("#messages").getByText(/E2E scroll seed 119/).first()).toBeVisible({
|
||||
await expect(
|
||||
page
|
||||
.locator("#messages")
|
||||
.getByText(/E2E scroll seed 119/)
|
||||
.first()
|
||||
).toBeVisible({
|
||||
timeout: 25000,
|
||||
});
|
||||
|
||||
@@ -98,7 +107,11 @@ test.describe("Messages conversation scroll", () => {
|
||||
const localHash = await getE2eLocalLxmfHash(request);
|
||||
await page.goto("/#/messages");
|
||||
await expect(page.getByText("Conversations", { exact: true }).first()).toBeVisible({ timeout: 25000 });
|
||||
await page.locator(".conversation-item").filter({ hasText: /E2E scroll seed/ }).first().click();
|
||||
await page
|
||||
.locator(".conversation-item")
|
||||
.filter({ hasText: /E2E scroll seed/ })
|
||||
.first()
|
||||
.click();
|
||||
await expect(page.locator("#messages")).toBeVisible({ timeout: 25000 });
|
||||
await waitForMessagesOverflow(page);
|
||||
|
||||
@@ -145,7 +158,11 @@ test.describe("Messages conversation scroll", () => {
|
||||
const localHash = await getE2eLocalLxmfHash(request);
|
||||
await page.goto("/#/messages");
|
||||
await expect(page.getByText("Conversations", { exact: true }).first()).toBeVisible({ timeout: 25000 });
|
||||
await page.locator(".conversation-item").filter({ hasText: /E2E scroll seed/ }).first().click();
|
||||
await page
|
||||
.locator(".conversation-item")
|
||||
.filter({ hasText: /E2E scroll seed/ })
|
||||
.first()
|
||||
.click();
|
||||
await expect(page.locator("#messages")).toBeVisible({ timeout: 25000 });
|
||||
await waitForMessagesOverflow(page);
|
||||
|
||||
@@ -183,7 +200,11 @@ test.describe("Messages conversation scroll", () => {
|
||||
test("preserves scroll anchor when loading older messages from the top", async ({ page }) => {
|
||||
await page.goto("/#/messages");
|
||||
await expect(page.getByText("Conversations", { exact: true }).first()).toBeVisible({ timeout: 25000 });
|
||||
await page.locator(".conversation-item").filter({ hasText: /E2E scroll seed/ }).first().click();
|
||||
await page
|
||||
.locator(".conversation-item")
|
||||
.filter({ hasText: /E2E scroll seed/ })
|
||||
.first()
|
||||
.click();
|
||||
await expect(page.locator("#messages")).toBeVisible({ timeout: 25000 });
|
||||
await waitForMessagesOverflow(page);
|
||||
|
||||
|
||||
@@ -104,77 +104,65 @@ describe("ConversationViewer performance baselines", () => {
|
||||
expect(sig[0]).toMatch(/^single:msg_/);
|
||||
});
|
||||
|
||||
it(
|
||||
"bulk chatItems update: baseline ceiling (detect regressions)",
|
||||
async () => {
|
||||
const wrapper = mountViewer();
|
||||
const n = 800;
|
||||
const items = makeChatItems(n, myLxmfAddressHash, peerHash);
|
||||
it("bulk chatItems update: baseline ceiling (detect regressions)", async () => {
|
||||
const wrapper = mountViewer();
|
||||
const n = 800;
|
||||
const items = makeChatItems(n, myLxmfAddressHash, peerHash);
|
||||
|
||||
const t0 = performance.now();
|
||||
await wrapper.setData({ chatItems: items });
|
||||
await wrapper.vm.$nextTick();
|
||||
const ms = performance.now() - t0;
|
||||
const t0 = performance.now();
|
||||
await wrapper.setData({ chatItems: items });
|
||||
await wrapper.vm.$nextTick();
|
||||
const ms = performance.now() - t0;
|
||||
|
||||
expect(wrapper.vm.selectedPeerChatDisplayGroups.length).toBe(n);
|
||||
expect(ms).toBeLessThan(15000);
|
||||
},
|
||||
60_000
|
||||
);
|
||||
expect(wrapper.vm.selectedPeerChatDisplayGroups.length).toBe(n);
|
||||
expect(ms).toBeLessThan(15000);
|
||||
}, 60_000);
|
||||
|
||||
it(
|
||||
"incremental append: baseline ceiling when thread already large",
|
||||
async () => {
|
||||
const wrapper = mountViewer();
|
||||
const n = 600;
|
||||
await wrapper.setData({ chatItems: makeChatItems(n, myLxmfAddressHash, peerHash) });
|
||||
await wrapper.vm.$nextTick();
|
||||
it("incremental append: baseline ceiling when thread already large", async () => {
|
||||
const wrapper = mountViewer();
|
||||
const n = 600;
|
||||
await wrapper.setData({ chatItems: makeChatItems(n, myLxmfAddressHash, peerHash) });
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
const newMsg = {
|
||||
type: "lxmf_message",
|
||||
is_outbound: true,
|
||||
lxmf_message: {
|
||||
hash: "newmsg".padEnd(32, "0"),
|
||||
source_hash: myLxmfAddressHash,
|
||||
destination_hash: peerHash,
|
||||
content: "New",
|
||||
created_at: new Date().toISOString(),
|
||||
state: "delivered",
|
||||
method: "direct",
|
||||
progress: 1.0,
|
||||
delivery_attempts: 1,
|
||||
id: n,
|
||||
},
|
||||
};
|
||||
const newMsg = {
|
||||
type: "lxmf_message",
|
||||
is_outbound: true,
|
||||
lxmf_message: {
|
||||
hash: "newmsg".padEnd(32, "0"),
|
||||
source_hash: myLxmfAddressHash,
|
||||
destination_hash: peerHash,
|
||||
content: "New",
|
||||
created_at: new Date().toISOString(),
|
||||
state: "delivered",
|
||||
method: "direct",
|
||||
progress: 1.0,
|
||||
delivery_attempts: 1,
|
||||
id: n,
|
||||
},
|
||||
};
|
||||
|
||||
const t0 = performance.now();
|
||||
wrapper.vm.chatItems.push(newMsg);
|
||||
await wrapper.vm.$nextTick();
|
||||
const ms = performance.now() - t0;
|
||||
const t0 = performance.now();
|
||||
wrapper.vm.chatItems.push(newMsg);
|
||||
await wrapper.vm.$nextTick();
|
||||
const ms = performance.now() - t0;
|
||||
|
||||
expect(wrapper.vm.selectedPeerChatDisplayGroups.length).toBe(n + 1);
|
||||
expect(ms).toBeLessThan(8000);
|
||||
},
|
||||
60_000
|
||||
);
|
||||
expect(wrapper.vm.selectedPeerChatDisplayGroups.length).toBe(n + 1);
|
||||
expect(ms).toBeLessThan(8000);
|
||||
}, 60_000);
|
||||
|
||||
it(
|
||||
"display groups computation alone stays bounded for large n",
|
||||
async () => {
|
||||
const wrapper = mountViewer();
|
||||
const n = 2000;
|
||||
await wrapper.setData({ chatItems: makeChatItems(n, myLxmfAddressHash, peerHash) });
|
||||
await wrapper.vm.$nextTick();
|
||||
it("display groups computation alone stays bounded for large n", async () => {
|
||||
const wrapper = mountViewer();
|
||||
const n = 2000;
|
||||
await wrapper.setData({ chatItems: makeChatItems(n, myLxmfAddressHash, peerHash) });
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
const t0 = performance.now();
|
||||
for (let k = 0; k < 20; k++) {
|
||||
void wrapper.vm.selectedPeerChatDisplayGroups;
|
||||
}
|
||||
const ms = performance.now() - t0;
|
||||
const t0 = performance.now();
|
||||
for (let k = 0; k < 20; k++) {
|
||||
void wrapper.vm.selectedPeerChatDisplayGroups;
|
||||
}
|
||||
const ms = performance.now() - t0;
|
||||
|
||||
expect(wrapper.vm.selectedPeerChatDisplayGroups.length).toBe(n);
|
||||
expect(ms).toBeLessThan(2000);
|
||||
},
|
||||
60_000
|
||||
);
|
||||
expect(wrapper.vm.selectedPeerChatDisplayGroups.length).toBe(n);
|
||||
expect(ms).toBeLessThan(2000);
|
||||
}, 60_000);
|
||||
});
|
||||
|
||||
@@ -126,7 +126,7 @@ describe("KeyboardShortcuts", () => {
|
||||
type: "keyboard_shortcuts.set",
|
||||
action: "nav_messages",
|
||||
keys: ["alt", "q"],
|
||||
}),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
@@ -136,7 +136,7 @@ describe("KeyboardShortcuts", () => {
|
||||
JSON.stringify({
|
||||
type: "keyboard_shortcuts.delete",
|
||||
action: "nav_map",
|
||||
}),
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -300,7 +300,7 @@ describe("SettingsPage — config persistence (PATCH and related)", () => {
|
||||
message_inbound_bubble_color: null,
|
||||
message_failed_bubble_color: "#ef4444",
|
||||
message_waiting_bubble_color: "#e5e7eb",
|
||||
}),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
@@ -330,7 +330,7 @@ describe("SettingsPage — config persistence (PATCH and related)", () => {
|
||||
expect.objectContaining({
|
||||
announce_max_stored_lxmf_delivery: 900,
|
||||
discovered_interfaces_max_return: 500,
|
||||
}),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
@@ -341,21 +341,21 @@ describe("SettingsPage — config persistence (PATCH and related)", () => {
|
||||
"/api/v1/config",
|
||||
expect.objectContaining({
|
||||
auto_resend_failed_messages_when_announce_received: true,
|
||||
}),
|
||||
})
|
||||
);
|
||||
await w.vm.onAllowAutoResendingFailedMessagesWithAttachmentsChange();
|
||||
expect(api.patch).toHaveBeenCalledWith(
|
||||
"/api/v1/config",
|
||||
expect.objectContaining({
|
||||
allow_auto_resending_failed_messages_with_attachments: false,
|
||||
}),
|
||||
})
|
||||
);
|
||||
await w.vm.onAutoSendFailedMessagesToPropagationNodeChange();
|
||||
expect(api.patch).toHaveBeenCalledWith(
|
||||
"/api/v1/config",
|
||||
expect.objectContaining({
|
||||
auto_send_failed_messages_to_propagation_node: false,
|
||||
}),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
@@ -507,7 +507,7 @@ describe("SettingsPage — config persistence (PATCH and related)", () => {
|
||||
await vi.advanceTimersByTimeAsync(1000);
|
||||
expect(api.patch).toHaveBeenCalledWith(
|
||||
"/api/v1/config",
|
||||
expect.objectContaining({ banished_text: "OUT", banished_color: "#ff0000" }),
|
||||
expect.objectContaining({ banished_text: "OUT", banished_color: "#ff0000" })
|
||||
);
|
||||
});
|
||||
|
||||
@@ -574,7 +574,7 @@ describe("SettingsPage — config persistence (PATCH and related)", () => {
|
||||
expect.objectContaining({
|
||||
gitea_base_url: "https://gitea.example",
|
||||
docs_download_urls: "https://docs.example",
|
||||
}),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
@@ -592,7 +592,7 @@ describe("SettingsPage — config persistence (PATCH and related)", () => {
|
||||
expect.objectContaining({
|
||||
csp_extra_connect_src: "wss://a.example",
|
||||
csp_extra_style_src: "https://css.example",
|
||||
}),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
@@ -772,9 +772,7 @@ describe("SettingsPage — maintenance, exports, telemetry trust, RNS reload", (
|
||||
it("flushArchivedPages sends websocket flush after confirm", async () => {
|
||||
const w = await mountSettingsPage(api);
|
||||
await w.vm.flushArchivedPages();
|
||||
expect(WebSocketConnection.send).toHaveBeenCalledWith(
|
||||
JSON.stringify({ type: "nomadnet.page.archive.flush" }),
|
||||
);
|
||||
expect(WebSocketConnection.send).toHaveBeenCalledWith(JSON.stringify({ type: "nomadnet.page.archive.flush" }));
|
||||
});
|
||||
|
||||
it("revokeTelemetryTrust PATCHes contact telemetry flag", async () => {
|
||||
|
||||
Reference in New Issue
Block a user