From ab1be8ea2d07929717586036430b5689fae65b85 Mon Sep 17 00:00:00 2001 From: Ivan Date: Thu, 16 Apr 2026 21:35:12 -0500 Subject: [PATCH] feat(propagation-nodes): propagation node management with new UI elements and API integration - Added functionality to request paths for preferred propagation nodes during sync. - Introduced new Material Design icons for better visual representation of node states. - Updated settings to allow transfer limits in megabytes, improving user experience. - Enhanced localization support for new features in multiple languages. - Improved tests to cover new propagation node functionalities and UI interactions. --- meshchatx/src/frontend/components/App.vue | 4 + .../components/MaterialDesignIcon.vue | 8 +- .../PropagationNodesPage.vue | 421 +++++++++++++++--- .../components/settings/SettingsPage.vue | 71 ++- .../frontend/components/tools/ToolsPage.vue | 8 + meshchatx/src/frontend/locales/de.json | 16 +- meshchatx/src/frontend/locales/en.json | 16 +- meshchatx/src/frontend/locales/it.json | 16 +- meshchatx/src/frontend/locales/ru.json | 16 +- tests/backend/fixtures/http_api_routes.json | 8 + tests/frontend/AppPropagationSync.test.js | 8 + tests/frontend/PropagationNodesPage.test.js | 139 +++++- .../SettingsPage.config-persistence.test.js | 6 +- tests/frontend/ToolsPage.test.js | 5 +- 14 files changed, 666 insertions(+), 76 deletions(-) diff --git a/meshchatx/src/frontend/components/App.vue b/meshchatx/src/frontend/components/App.vue index b7f3615..d40aee6 100644 --- a/meshchatx/src/frontend/components/App.vue +++ b/meshchatx/src/frontend/components/App.vue @@ -1343,6 +1343,10 @@ export default { // request sync try { + const preferredHash = this.config?.lxmf_preferred_propagation_node_destination_hash; + if (preferredHash) { + await window.api.post(`/api/v1/destination/${preferredHash}/request-path`); + } await window.api.get("/api/v1/lxmf/propagation-node/sync"); } catch (e) { const errorMessage = e.response?.data?.message ?? this.$t("app.sync_error_generic"); diff --git a/meshchatx/src/frontend/components/MaterialDesignIcon.vue b/meshchatx/src/frontend/components/MaterialDesignIcon.vue index aeb2f09..5cb2932 100644 --- a/meshchatx/src/frontend/components/MaterialDesignIcon.vue +++ b/meshchatx/src/frontend/components/MaterialDesignIcon.vue @@ -19,6 +19,11 @@ diff --git a/meshchatx/src/frontend/components/settings/SettingsPage.vue b/meshchatx/src/frontend/components/settings/SettingsPage.vue index bed992e..a518ccd 100644 --- a/meshchatx/src/frontend/components/settings/SettingsPage.vue +++ b/meshchatx/src/frontend/components/settings/SettingsPage.vue @@ -1995,39 +1995,51 @@
- Delivery transfer limit (bytes) + Delivery transfer limit (MB)
+
+ {{ formatByteSize(config.lxmf_delivery_transfer_limit_in_bytes) }} +
- Propagation transfer limit (bytes) + Propagation transfer limit (MB)
+
+ {{ formatByteSize(config.lxmf_propagation_transfer_limit_in_bytes) }} +
- Propagation sync limit (bytes) + Propagation sync limit (MB)
+
+ {{ formatByteSize(config.lxmf_propagation_sync_limit_in_bytes) }} +
@@ -2236,6 +2248,9 @@ export default { nomad_default_page_path: "/page/index.mu", }, saveTimeouts: {}, + lxmfDeliveryTransferLimitInputMb: 10, + lxmfPropagationTransferLimitInputMb: 0.256, + lxmfPropagationSyncLimitInputMb: 10.24, lastRememberedInboundStampCost: 8, shortcuts: [], reloadingRns: false, @@ -2544,6 +2559,7 @@ export default { if (json.config) { this.config = { ...this.config, ...json.config }; this.sanitizeColorConfigFields(); + this.syncLxmfTransferLimitInputs(); } break; } @@ -2575,6 +2591,7 @@ export default { if (merged) { this.config = merged; normalizeConfigColors(this.config); + this.syncLxmfTransferLimitInputs(); const inbound = Number(this.config.lxmf_inbound_stamp_cost); if (inbound > 0) { this.lastRememberedInboundStampCost = Math.min(254, inbound); @@ -2613,6 +2630,7 @@ export default { const newConfig = await patchServerConfig(config, window.api); this.config = newConfig; normalizeConfigColors(this.config); + this.syncLxmfTransferLimitInputs(); if (label) { ToastUtils.success(this.$t("app.setting_auto_saved", { label: this.$t(`app.${label}`) })); } @@ -2621,6 +2639,35 @@ export default { console.log(e); } }, + syncLxmfTransferLimitInputs() { + this.lxmfDeliveryTransferLimitInputMb = this.bytesToMb(this.config.lxmf_delivery_transfer_limit_in_bytes); + this.lxmfPropagationTransferLimitInputMb = this.bytesToMb( + this.config.lxmf_propagation_transfer_limit_in_bytes + ); + this.lxmfPropagationSyncLimitInputMb = this.bytesToMb(this.config.lxmf_propagation_sync_limit_in_bytes); + }, + bytesToMb(value) { + const n = Number(value); + if (!Number.isFinite(n) || n <= 0) { + return 0; + } + return Math.max(0.001, Math.round((n / 1000000) * 1000) / 1000); + }, + mbToBytes(value) { + const n = Number(value); + if (!Number.isFinite(n) || n <= 0) { + return 1000; + } + return Math.max(1000, Math.round(n * 1000000)); + }, + formatByteSize(bytes) { + const value = Number(bytes); + if (!Number.isFinite(value) || value < 0) return "0 B"; + if (value < 1000) return `${Math.round(value)} B`; + if (value < 1000 * 1000) return `${(value / 1000).toFixed(1)} KB`; + if (value < 1000 * 1000 * 1000) return `${(value / (1000 * 1000)).toFixed(2)} MB`; + return `${(value / (1000 * 1000 * 1000)).toFixed(2)} GB`; + }, sanitizeColorConfigFields() { if (!this.config) return; normalizeConfigColors(this.config); @@ -2870,7 +2917,7 @@ export default { } this.saveTimeouts.delivery_transfer_limit = setTimeout(async () => { await this.updateConfig({ - lxmf_delivery_transfer_limit_in_bytes: this.config.lxmf_delivery_transfer_limit_in_bytes, + lxmf_delivery_transfer_limit_in_bytes: this.mbToBytes(this.lxmfDeliveryTransferLimitInputMb), }); }, 1000); }, @@ -2880,7 +2927,7 @@ export default { } this.saveTimeouts.propagation_transfer_limit = setTimeout(async () => { await this.updateConfig({ - lxmf_propagation_transfer_limit_in_bytes: this.config.lxmf_propagation_transfer_limit_in_bytes, + lxmf_propagation_transfer_limit_in_bytes: this.mbToBytes(this.lxmfPropagationTransferLimitInputMb), }); }, 1000); }, @@ -2890,7 +2937,7 @@ export default { } this.saveTimeouts.propagation_sync_limit = setTimeout(async () => { await this.updateConfig({ - lxmf_propagation_sync_limit_in_bytes: this.config.lxmf_propagation_sync_limit_in_bytes, + lxmf_propagation_sync_limit_in_bytes: this.mbToBytes(this.lxmfPropagationSyncLimitInputMb), }); }, 1000); }, diff --git a/meshchatx/src/frontend/components/tools/ToolsPage.vue b/meshchatx/src/frontend/components/tools/ToolsPage.vue index d79e223..49e5da6 100644 --- a/meshchatx/src/frontend/components/tools/ToolsPage.vue +++ b/meshchatx/src/frontend/components/tools/ToolsPage.vue @@ -193,6 +193,14 @@ export default { titleKey: "tools.bots.title", descriptionKey: "tools.bots.description", }, + { + name: "propagation-nodes", + route: { name: "propagation-nodes" }, + icon: "mailbox", + iconBg: "tool-card__icon bg-cyan-50 text-cyan-500 dark:bg-cyan-900/30 dark:text-cyan-200", + title: "Propagation Nodes", + description: "Manage preferred and local propagation nodes with live stats and path checks.", + }, { name: "forwarder", route: { name: "forwarder" }, diff --git a/meshchatx/src/frontend/locales/de.json b/meshchatx/src/frontend/locales/de.json index 06e52f1..518e3da 100644 --- a/meshchatx/src/frontend/locales/de.json +++ b/meshchatx/src/frontend/locales/de.json @@ -25,7 +25,21 @@ "export_identity": "Identität exportieren", "bot_deleted": "Bot erfolgreich gelöscht", "failed_to_delete": "Bot konnte nicht gelöscht werden", - "more_bots_coming": "Weitere Bots folgen in Kürze!" + "more_bots_coming": "Weitere Bots folgen in Kürze!", + "chat_with_bot": "Chat", + "lxmf_address": "LXMF-Adresse", + "last_announce": "Letzter Announce", + "never_announced": "Auf diesem Knoten noch nicht gesehen", + "address_pending": "Erst nach dem ersten Start verfügbar", + "status_running": "Läuft", + "status_stopped": "Gestoppt", + "force_announce": "Jetzt ankündigen", + "edit_name": "Namen bearbeiten", + "bot_renamed": "Name gespeichert", + "rename_failed": "Name konnte nicht gespeichert werden", + "name_required": "Namen eingeben", + "announce_triggered": "Ankündigung angefordert", + "announce_failed": "Ankündigung konnte nicht angefordert werden" }, "app": { "name": "Reticulum MeshChatX", diff --git a/meshchatx/src/frontend/locales/en.json b/meshchatx/src/frontend/locales/en.json index a3db46d..db6bba3 100644 --- a/meshchatx/src/frontend/locales/en.json +++ b/meshchatx/src/frontend/locales/en.json @@ -25,7 +25,21 @@ "export_identity": "Export Identity", "bot_deleted": "Bot deleted successfully", "failed_to_delete": "Failed to delete bot", - "more_bots_coming": "More bots coming soon!" + "more_bots_coming": "More bots coming soon!", + "chat_with_bot": "Chat", + "lxmf_address": "LXMF address", + "last_announce": "Last announce", + "never_announced": "Not seen on this node yet", + "address_pending": "Not available until the bot has started once", + "status_running": "Running", + "status_stopped": "Stopped", + "force_announce": "Announce now", + "edit_name": "Edit name", + "bot_renamed": "Bot name saved", + "rename_failed": "Could not save name", + "name_required": "Enter a name", + "announce_triggered": "Announce requested", + "announce_failed": "Could not request announce" }, "app": { "name": "Reticulum MeshChatX", diff --git a/meshchatx/src/frontend/locales/it.json b/meshchatx/src/frontend/locales/it.json index fc7113f..8928ee2 100644 --- a/meshchatx/src/frontend/locales/it.json +++ b/meshchatx/src/frontend/locales/it.json @@ -25,7 +25,21 @@ "export_identity": "Esporta Identità", "bot_deleted": "Bot eliminato con successo", "failed_to_delete": "Impossibile eliminare il bot", - "more_bots_coming": "Altri bot in arrivo!" + "more_bots_coming": "Altri bot in arrivo!", + "chat_with_bot": "Chat", + "lxmf_address": "Indirizzo LXMF", + "last_announce": "Ultimo announce", + "never_announced": "Non ancora visto su questo nodo", + "address_pending": "Disponibile dopo il primo avvio del bot", + "status_running": "In esecuzione", + "status_stopped": "Fermato", + "force_announce": "Annuncia ora", + "edit_name": "Modifica nome", + "bot_renamed": "Nome salvato", + "rename_failed": "Impossibile salvare il nome", + "name_required": "Inserisci un nome", + "announce_triggered": "Annuncio richiesto", + "announce_failed": "Impossibile richiedere l'annuncio" }, "app": { "name": "Reticulum MeshChatX", diff --git a/meshchatx/src/frontend/locales/ru.json b/meshchatx/src/frontend/locales/ru.json index a8aec98..65f6b90 100644 --- a/meshchatx/src/frontend/locales/ru.json +++ b/meshchatx/src/frontend/locales/ru.json @@ -25,7 +25,21 @@ "export_identity": "Экспорт личности", "bot_deleted": "Бот успешно удален", "failed_to_delete": "Не удалось удалить бота", - "more_bots_coming": "Скоро появятся новые боты!" + "more_bots_coming": "Скоро появятся новые боты!", + "chat_with_bot": "Чат", + "lxmf_address": "Адрес LXMF", + "last_announce": "Последний announce", + "never_announced": "На этом узле ещё не виден", + "address_pending": "Появится после первого запуска бота", + "status_running": "Запущен", + "status_stopped": "Остановлен", + "force_announce": "Объявить сейчас", + "edit_name": "Изменить имя", + "bot_renamed": "Имя бота сохранено", + "rename_failed": "Не удалось сохранить имя", + "name_required": "Введите имя", + "announce_triggered": "Запрос announce отправлен", + "announce_failed": "Не удалось запросить announce" }, "app": { "name": "Reticulum MeshChatX", diff --git a/tests/backend/fixtures/http_api_routes.json b/tests/backend/fixtures/http_api_routes.json index c0651ce..4fa22ab 100644 --- a/tests/backend/fixtures/http_api_routes.json +++ b/tests/backend/fixtures/http_api_routes.json @@ -64,6 +64,10 @@ "method": "DELETE", "path": "/api/v1/blocked-destinations/{destination_hash}" }, + { + "method": "POST", + "path": "/api/v1/bots/announce" + }, { "method": "POST", "path": "/api/v1/bots/delete" @@ -84,6 +88,10 @@ "method": "GET", "path": "/api/v1/bots/status" }, + { + "method": "PATCH", + "path": "/api/v1/bots/update" + }, { "method": "POST", "path": "/api/v1/bots/stop" diff --git a/tests/frontend/AppPropagationSync.test.js b/tests/frontend/AppPropagationSync.test.js index 21197d2..4e9b993 100644 --- a/tests/frontend/AppPropagationSync.test.js +++ b/tests/frontend/AppPropagationSync.test.js @@ -22,6 +22,9 @@ const syncingStates = [ function makeSyncContext(axiosMock, tOverrides = {}) { return { + config: { + lxmf_preferred_propagation_node_destination_hash: "deadbeef", + }, propagationNodeStatus: null, _propagationSyncPollTimer: null, propagationSyncLiveToastMessage: App.methods.propagationSyncLiveToastMessage, @@ -77,6 +80,7 @@ function makeSyncContext(axiosMock, tOverrides = {}) { describe("App propagation sync", () => { const axiosMock = { get: vi.fn(), + post: vi.fn(), }; beforeEach(() => { @@ -91,6 +95,7 @@ describe("App propagation sync", () => { }); it("shows detailed success toast with stored, confirmations and hidden counts", async () => { + axiosMock.post.mockResolvedValue({ data: { message: "ok" } }); axiosMock.get.mockImplementation((url) => { if (url === "/api/v1/lxmf/propagation-node/sync") { return Promise.resolve({ data: { message: "Sync is starting" } }); @@ -122,10 +127,12 @@ describe("App propagation sync", () => { expect(ToastUtils.success).toHaveBeenCalledWith( "Sync complete. 8 messages received. (3 stored, 2 confirmations, 3 hidden)" ); + expect(axiosMock.post).toHaveBeenCalledWith("/api/v1/destination/deadbeef/request-path"); expect(ToastUtils.error).not.toHaveBeenCalled(); }); it("polls status while syncing and updates live loading toast", async () => { + axiosMock.post.mockResolvedValue({ data: { message: "ok" } }); let statusCalls = 0; axiosMock.get.mockImplementation((url) => { if (url === "/api/v1/lxmf/propagation-node/sync") { @@ -178,6 +185,7 @@ describe("App propagation sync", () => { }); it("uses translated status in error toast when sync ends in a failure state", async () => { + axiosMock.post.mockResolvedValue({ data: { message: "ok" } }); axiosMock.get.mockImplementation((url) => { if (url === "/api/v1/lxmf/propagation-node/sync") { return Promise.resolve({ data: { message: "Sync is starting" } }); diff --git a/tests/frontend/PropagationNodesPage.test.js b/tests/frontend/PropagationNodesPage.test.js index d70bc8b..0d5b24b 100644 --- a/tests/frontend/PropagationNodesPage.test.js +++ b/tests/frontend/PropagationNodesPage.test.js @@ -11,7 +11,9 @@ vi.mock("../../meshchatx/src/frontend/js/ToastUtils", () => ({ describe("PropagationNodesPage", () => { const axiosMock = { + get: vi.fn(), post: vi.fn(), + patch: vi.fn(), }; beforeEach(() => { @@ -39,20 +41,42 @@ describe("PropagationNodesPage", () => { const ctx = { localPropagationNode: { destination_hash: "local-node" }, usePropagationNode: vi.fn(), + requestPathForNode: vi.fn(), }; await PropagationNodesPage.methods.useLocalPropagationNode.call(ctx); expect(ctx.usePropagationNode).toHaveBeenCalledWith("local-node"); + expect(ctx.requestPathForNode).toHaveBeenCalledWith("local-node"); + }); + + it("prefers runtime local node state for running indicator", () => { + const runningByStats = PropagationNodesPage.computed.localNodeIsRunning.call({ + localPropagationNode: { + is_propagation_enabled: true, + local_node_stats: { is_running: false }, + }, + }); + expect(runningByStats).toBe(false); + }); + + it("formats storage usage with limit when available", () => { + const ctx = { + formatByteSize: PropagationNodesPage.methods.formatByteSize, + }; + const text = PropagationNodesPage.methods.formatStorageUsage.call(ctx, { + messagestore_bytes: 76500, + messagestore_limit_bytes: 10240000, + }); + expect(text).toBe("76.5 KB / 10.24 MB"); }); it("debounces propagation transfer limit save", async () => { const ctx = { - config: { - lxmf_propagation_transfer_limit_in_bytes: 123456, - }, + propagationLimitInputMb: 1.234, saveTimeouts: { propagationLimit: null, }, + mbToBytes: PropagationNodesPage.methods.mbToBytes, updateConfig: vi.fn().mockResolvedValue(undefined), }; @@ -61,7 +85,24 @@ describe("PropagationNodesPage", () => { await vi.advanceTimersByTimeAsync(500); expect(ctx.updateConfig).toHaveBeenCalledWith({ - lxmf_propagation_transfer_limit_in_bytes: 123456, + lxmf_propagation_transfer_limit_in_bytes: 1234000, + }); + }); + + it("debounces propagation stamp cost save with bounds", async () => { + const ctx = { + config: { + lxmf_propagation_node_stamp_cost: 3, + }, + saveTimeouts: { + propagationStampCost: null, + }, + updateConfig: vi.fn().mockResolvedValue(undefined), + }; + await PropagationNodesPage.methods.onPropagationStampCostChange.call(ctx); + await vi.advanceTimersByTimeAsync(500); + expect(ctx.updateConfig).toHaveBeenCalledWith({ + lxmf_propagation_node_stamp_cost: 13, }); }); @@ -70,6 +111,7 @@ describe("PropagationNodesPage", () => { const ctx = { getConfig: vi.fn().mockResolvedValue(undefined), loadPropagationNodes: vi.fn().mockResolvedValue(undefined), + refreshPriorityNodePaths: vi.fn().mockResolvedValue(undefined), $t: (k) => k, }; @@ -80,4 +122,93 @@ describe("PropagationNodesPage", () => { expect(axiosMock.post).toHaveBeenCalledWith("/api/v1/lxmf/propagation-node/restart"); expect(ToastUtils.success).toHaveBeenCalledTimes(2); }); + + it("triggers announce via icon action", async () => { + axiosMock.get.mockResolvedValue({ data: {} }); + const ctx = { + loadPropagationNodes: vi.fn().mockResolvedValue(undefined), + refreshPriorityNodePaths: vi.fn().mockResolvedValue(undefined), + $t: (k) => k, + }; + await PropagationNodesPage.methods.announceNow.call(ctx); + expect(axiosMock.get).toHaveBeenCalledWith("/api/v1/announce"); + expect(ToastUtils.success).toHaveBeenCalledWith("Announce triggered"); + }); + + it("resets local node display name to Anonymous Peer", async () => { + const ctx = { + localNodeDisplayNameDraft: "Custom Name", + saveLocalNodeDisplayName: vi.fn().mockResolvedValue(undefined), + }; + await PropagationNodesPage.methods.resetLocalNodeDisplayName.call(ctx); + expect(ctx.localNodeDisplayNameDraft).toBe("Anonymous Peer"); + expect(ctx.saveLocalNodeDisplayName).toHaveBeenCalledTimes(1); + }); + + it("uses collapsed manager on small screens", () => { + const originalMatchMedia = window.matchMedia; + window.matchMedia = vi.fn().mockReturnValue({ matches: true }); + const ctx = { + isLocalManagerCollapsed: false, + getConfig: vi.fn(), + loadPropagationNodes: vi.fn(), + refreshPriorityNodePaths: vi.fn(), + }; + PropagationNodesPage.mounted.call(ctx); + expect(ctx.isLocalManagerCollapsed).toBe(true); + window.matchMedia = originalMatchMedia; + }); + + it("saves local display name and announces immediately", async () => { + axiosMock.patch.mockResolvedValue({ + data: { + config: { + display_name: "Friendly Node", + lxmf_delivery_transfer_limit_in_bytes: 10000000, + lxmf_propagation_transfer_limit_in_bytes: 256000, + lxmf_propagation_sync_limit_in_bytes: 10240000, + }, + }, + }); + axiosMock.get.mockResolvedValue({ data: {} }); + + const ctx = { + localNodeDisplayNameDraft: " Friendly Node ", + config: { + lxmf_delivery_transfer_limit_in_bytes: 10000000, + lxmf_propagation_transfer_limit_in_bytes: 256000, + lxmf_propagation_sync_limit_in_bytes: 10240000, + }, + syncManagerInputsFromConfig: vi.fn(), + loadPropagationNodes: vi.fn().mockResolvedValue(undefined), + refreshPriorityNodePaths: vi.fn().mockResolvedValue(undefined), + announceNow: PropagationNodesPage.methods.announceNow, + updateConfig: PropagationNodesPage.methods.updateConfig, + $t: (k) => k, + }; + + await PropagationNodesPage.methods.saveLocalNodeDisplayName.call(ctx); + + expect(axiosMock.patch).toHaveBeenCalledWith("/api/v1/config", { + display_name: "Friendly Node", + }); + expect(axiosMock.get).toHaveBeenCalledWith("/api/v1/announce"); + expect(ToastUtils.success).toHaveBeenCalledWith("Name saved and announced"); + }); + + it("fetches path for a destination hash", async () => { + axiosMock.get.mockResolvedValueOnce({ + data: { + path: { hops: 2, next_hop_interface: "TCP Client" }, + }, + }); + const ctx = { + nodePathsByHash: {}, + }; + await PropagationNodesPage.methods.requestPathForNode.call(ctx, "abcd"); + expect(axiosMock.get).toHaveBeenCalledWith("/api/v1/destination/abcd/path", { + params: { request: "1", timeout: 4 }, + }); + expect(ctx.nodePathsByHash.abcd).toEqual({ hops: 2, next_hop_interface: "TCP Client" }); + }); }); diff --git a/tests/frontend/SettingsPage.config-persistence.test.js b/tests/frontend/SettingsPage.config-persistence.test.js index 7bbb3a1..66de439 100644 --- a/tests/frontend/SettingsPage.config-persistence.test.js +++ b/tests/frontend/SettingsPage.config-persistence.test.js @@ -258,21 +258,21 @@ describe("SettingsPage — config persistence (PATCH and related)", () => { it("LXMF transfer/sync limits PATCH after debounce", async () => { const w = await mountSettingsPage(api); - w.vm.config.lxmf_delivery_transfer_limit_in_bytes = 9_000_000; + w.vm.lxmfDeliveryTransferLimitInputMb = 9; await w.vm.onLxmfDeliveryTransferLimitChange(); await vi.advanceTimersByTimeAsync(1000); expect(api.patch).toHaveBeenCalledWith("/api/v1/config", { lxmf_delivery_transfer_limit_in_bytes: 9_000_000, }); - w.vm.config.lxmf_propagation_transfer_limit_in_bytes = 300_000; + w.vm.lxmfPropagationTransferLimitInputMb = 0.3; await w.vm.onLxmfPropagationTransferLimitChange(); await vi.advanceTimersByTimeAsync(1000); expect(api.patch).toHaveBeenCalledWith("/api/v1/config", { lxmf_propagation_transfer_limit_in_bytes: 300_000, }); - w.vm.config.lxmf_propagation_sync_limit_in_bytes = 9_000_000; + w.vm.lxmfPropagationSyncLimitInputMb = 9; await w.vm.onLxmfPropagationSyncLimitChange(); await vi.advanceTimersByTimeAsync(1000); expect(api.patch).toHaveBeenCalledWith("/api/v1/config", { diff --git a/tests/frontend/ToolsPage.test.js b/tests/frontend/ToolsPage.test.js index af51ae5..091b668 100644 --- a/tests/frontend/ToolsPage.test.js +++ b/tests/frontend/ToolsPage.test.js @@ -15,6 +15,7 @@ describe("ToolsPage.vue", () => { { path: "/rnpath-trace", name: "rnpath-trace", component: { template: "div" } }, { path: "/translator", name: "translator", component: { template: "div" } }, { path: "/bots", name: "bots", component: { template: "div" } }, + { path: "/propagation-nodes", name: "propagation-nodes", component: { template: "div" } }, { path: "/forwarder", name: "forwarder", component: { template: "div" } }, { path: "/documentation", name: "documentation", component: { template: "div" } }, { path: "/micron-editor", name: "micron-editor", component: { template: "div" } }, @@ -51,7 +52,7 @@ describe("ToolsPage.vue", () => { it("renders all tool rows", () => { const wrapper = mountToolsPage(); const toolRows = wrapper.findAll(".tool-row"); - expect(toolRows.length).toBe(17); + expect(toolRows.length).toBe(18); }); it("filters tools based on search query", async () => { @@ -76,6 +77,6 @@ describe("ToolsPage.vue", () => { await clearButton.trigger("click"); expect(wrapper.vm.searchQuery).toBe(""); - expect(wrapper.vm.filteredTools.length).toBe(17); + expect(wrapper.vm.filteredTools.length).toBe(18); }); });