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 @@
@@ -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);
});
});