numerous improvements

This commit is contained in:
2026-01-05 11:47:35 -06:00
parent 5694c1ee67
commit fda9187e95
104 changed files with 4567 additions and 1070 deletions

View File

@@ -182,13 +182,13 @@ describe("AboutPage.vue", () => {
});
mountAboutPage();
expect(axiosMock.get).toHaveBeenCalledTimes(4); // info, config, health, snapshots
expect(axiosMock.get).toHaveBeenCalledTimes(5); // info, config, health, snapshots, backups
vi.advanceTimersByTime(5000);
expect(axiosMock.get).toHaveBeenCalledTimes(5);
expect(axiosMock.get).toHaveBeenCalledTimes(6); // +1 from updateInterval
vi.advanceTimersByTime(5000);
expect(axiosMock.get).toHaveBeenCalledTimes(6);
expect(axiosMock.get).toHaveBeenCalledTimes(7); // +2 from updateInterval
});
it("handles vacuum database action", async () => {

View File

@@ -0,0 +1,86 @@
import { mount } from "@vue/test-utils";
import { describe, it, expect, vi, beforeEach } from "vitest";
import AddInterfacePage from "../../meshchatx/src/frontend/components/interfaces/AddInterfacePage.vue";
// mocks
const mockAxios = {
get: vi.fn(),
post: vi.fn(),
};
window.axios = mockAxios;
vi.mock("../../meshchatx/src/frontend/js/DialogUtils", () => ({
default: {
alert: vi.fn(),
},
}));
vi.mock("../../meshchatx/src/frontend/js/ToastUtils", () => ({
default: {
success: vi.fn(),
error: vi.fn(),
},
}));
describe("AddInterfacePage.vue discovery", () => {
beforeEach(() => {
vi.clearAllMocks();
mockAxios.get.mockResolvedValue({ data: {} });
mockAxios.post.mockResolvedValue({ data: { message: "ok" } });
});
it("adds discovery fields when interface is discoverable", async () => {
const wrapper = mount(AddInterfacePage, {
global: {
mocks: {
$route: { query: {} },
$router: { push: vi.fn() },
$t: (msg) => msg,
},
stubs: ["RouterLink", "MaterialDesignIcon", "Toggle", "ExpandingSection", "FormLabel", "FormSubLabel"],
},
});
// required interface fields
wrapper.vm.newInterfaceName = "TestIface";
wrapper.vm.newInterfaceType = "TCPClientInterface";
wrapper.vm.newInterfaceTargetHost = "example.com";
wrapper.vm.newInterfaceTargetPort = "4242";
// discovery fields
wrapper.vm.discovery.discoverable = true;
wrapper.vm.discovery.discovery_name = "Region A";
wrapper.vm.discovery.announce_interval = 720;
wrapper.vm.discovery.reachable_on = "/usr/local/bin/ip.sh";
wrapper.vm.discovery.discovery_stamp_value = 22;
wrapper.vm.discovery.discovery_encrypt = true;
wrapper.vm.discovery.publish_ifac = true;
wrapper.vm.discovery.latitude = 1.23;
wrapper.vm.discovery.longitude = 4.56;
wrapper.vm.discovery.height = 7;
wrapper.vm.discovery.discovery_frequency = 915000000;
wrapper.vm.discovery.discovery_bandwidth = 125000;
wrapper.vm.discovery.discovery_modulation = "LoRa";
await wrapper.vm.addInterface();
expect(mockAxios.post).toHaveBeenCalledWith(
"/api/v1/reticulum/interfaces/add",
expect.objectContaining({
discoverable: "yes",
discovery_name: "Region A",
announce_interval: 720,
reachable_on: "/usr/local/bin/ip.sh",
discovery_stamp_value: 22,
discovery_encrypt: true,
publish_ifac: true,
latitude: 1.23,
longitude: 4.56,
height: 7,
discovery_frequency: 915000000,
discovery_bandwidth: 125000,
discovery_modulation: "LoRa",
})
);
});
});

View File

@@ -7,6 +7,7 @@ import GlobalState from "../../meshchatx/src/frontend/js/GlobalState";
const mockAxios = {
get: vi.fn(),
post: vi.fn(),
patch: vi.fn(),
};
window.axios = mockAxios;
@@ -110,4 +111,85 @@ describe("InterfacesPage.vue", () => {
expect(wrapper.vm.modifiedInterfaceNames.size).toBe(0);
expect(mockAxios.post).toHaveBeenCalledWith("/api/v1/reticulum/reload");
});
it("loads and saves discovery config", async () => {
mockAxios.get.mockImplementation((url) => {
if (url === "/api/v1/reticulum/interfaces") {
return Promise.resolve({ data: { interfaces: [] } });
}
if (url === "/api/v1/app/info") {
return Promise.resolve({ data: { app_info: { is_reticulum_running: true } } });
}
if (url === "/api/v1/reticulum/discovery") {
return Promise.resolve({
data: {
discovery: {
discover_interfaces: "true",
interface_discovery_sources: "abc",
required_discovery_value: "16",
autoconnect_discovered_interfaces: "3",
network_identity: "/tmp/netid",
},
},
});
}
return Promise.reject();
});
mockAxios.patch.mockResolvedValue({
data: {
discovery: {
discover_interfaces: false,
interface_discovery_sources: null,
required_discovery_value: 18,
autoconnect_discovered_interfaces: 5,
network_identity: "/tmp/new",
},
},
});
const wrapper = mount(InterfacesPage, {
global: {
mocks: {
$route: mockRoute,
$router: mockRouter,
$t: (msg) => msg,
},
stubs: [
"RouterLink",
"MaterialDesignIcon",
"IconButton",
"Interface",
"ImportInterfacesModal",
"Toggle",
],
},
});
await wrapper.vm.$nextTick();
await wrapper.vm.$nextTick();
expect(wrapper.vm.discoveryConfig.discover_interfaces).toBe(true);
expect(wrapper.vm.discoveryConfig.interface_discovery_sources).toBe("abc");
expect(wrapper.vm.discoveryConfig.required_discovery_value).toBe(16);
expect(wrapper.vm.discoveryConfig.autoconnect_discovered_interfaces).toBe(3);
expect(wrapper.vm.discoveryConfig.network_identity).toBe("/tmp/netid");
wrapper.vm.discoveryConfig.discover_interfaces = false;
wrapper.vm.discoveryConfig.interface_discovery_sources = "";
wrapper.vm.discoveryConfig.required_discovery_value = 18;
wrapper.vm.discoveryConfig.autoconnect_discovered_interfaces = 5;
wrapper.vm.discoveryConfig.network_identity = "/tmp/new";
await wrapper.vm.saveDiscoveryConfig();
expect(mockAxios.patch).toHaveBeenCalledWith("/api/v1/reticulum/discovery", {
discover_interfaces: false,
interface_discovery_sources: null,
required_discovery_value: 18,
autoconnect_discovered_interfaces: 5,
network_identity: "/tmp/new",
});
expect(wrapper.vm.savingDiscovery).toBe(false);
});
});

View File

@@ -45,6 +45,10 @@ vi.mock("ol/Map", () => ({
forEachFeatureAtPixel: vi.fn(),
setTarget: vi.fn(),
updateSize: vi.fn(),
getViewport: vi.fn().mockReturnValue({
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
}),
})),
}));
@@ -63,6 +67,7 @@ vi.mock("ol/source/Vector", () => ({
addFeature: vi.fn(),
addFeatures: vi.fn(),
getFeatures: vi.fn().mockReturnValue([]),
on: vi.fn(),
})),
}));
vi.mock("ol/proj", () => ({
@@ -75,11 +80,30 @@ vi.mock("ol/control", () => ({
vi.mock("ol/interaction/Draw", () => ({
default: vi.fn().mockImplementation(() => ({
on: vi.fn(),
setActive: vi.fn(),
})),
}));
vi.mock("ol/interaction/Modify", () => ({
default: vi.fn().mockImplementation(() => ({
on: vi.fn(),
setActive: vi.fn(),
})),
}));
vi.mock("ol/interaction/Select", () => ({
default: vi.fn().mockImplementation(() => ({
on: vi.fn(),
setActive: vi.fn(),
getFeatures: vi.fn().mockReturnValue({
getArray: vi.fn().mockReturnValue([]),
clear: vi.fn(),
push: vi.fn(),
}),
})),
}));
vi.mock("ol/interaction/Translate", () => ({
default: vi.fn().mockImplementation(() => ({
on: vi.fn(),
setActive: vi.fn(),
})),
}));
vi.mock("ol/interaction/Snap", () => ({
@@ -106,6 +130,29 @@ vi.mock("ol/format/GeoJSON", () => ({
readFeatures: vi.fn().mockReturnValue([]),
})),
}));
vi.mock("ol/style", () => ({
Style: vi.fn().mockImplementation(() => ({})),
Text: vi.fn().mockImplementation(() => ({})),
Fill: vi.fn().mockImplementation(() => ({})),
Stroke: vi.fn().mockImplementation(() => ({})),
Circle: vi.fn().mockImplementation(() => ({})),
Icon: vi.fn().mockImplementation(() => ({})),
}));
vi.mock("ol/sphere", () => ({
getArea: vi.fn(),
getLength: vi.fn(),
}));
vi.mock("ol/geom", () => ({
LineString: vi.fn(),
Polygon: vi.fn(),
Circle: vi.fn(),
}));
vi.mock("ol/geom/Polygon", () => ({
fromCircle: vi.fn(),
}));
vi.mock("ol/Observable", () => ({
unByKey: vi.fn(),
}));
describe("MapPage.vue - Drawing and Measurement Tools", () => {
let axiosMock;
@@ -196,13 +243,18 @@ describe("MapPage.vue - Drawing and Measurement Tools", () => {
const wrapper = mountMapPage();
await wrapper.vm.$nextTick();
await new Promise((resolve) => setTimeout(resolve, 50)); // wait for initMap
await wrapper.vm.$nextTick();
expect(wrapper.vm.map).toBeDefined();
const pointTool = wrapper.find('button[title="map.tool_point"]');
await pointTool.trigger("click");
await wrapper.vm.$nextTick();
expect(wrapper.vm.drawType).toBe("Point");
expect(wrapper.vm.draw).not.toBeNull();
await pointTool.trigger("click");
await wrapper.vm.$nextTick();
expect(wrapper.vm.drawType).toBeNull();
expect(wrapper.vm.draw).toBeNull();
});
@@ -211,13 +263,18 @@ describe("MapPage.vue - Drawing and Measurement Tools", () => {
const wrapper = mountMapPage();
await wrapper.vm.$nextTick();
await new Promise((resolve) => setTimeout(resolve, 50)); // wait for initMap
await wrapper.vm.$nextTick();
expect(wrapper.vm.map).toBeDefined();
const measureTool = wrapper.find('button[title="map.tool_measure"]');
await measureTool.trigger("click");
await wrapper.vm.$nextTick();
expect(wrapper.vm.isMeasuring).toBe(true);
expect(wrapper.vm.drawType).toBe("LineString");
await measureTool.trigger("click");
await wrapper.vm.$nextTick();
expect(wrapper.vm.isMeasuring).toBe(false);
expect(wrapper.vm.drawType).toBeNull();
});

View File

@@ -45,6 +45,10 @@ vi.mock("ol/Map", () => ({
forEachFeatureAtPixel: vi.fn(),
setTarget: vi.fn(),
updateSize: vi.fn(),
getViewport: vi.fn().mockReturnValue({
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
}),
})),
}));
@@ -63,6 +67,7 @@ vi.mock("ol/source/Vector", () => ({
addFeature: vi.fn(),
addFeatures: vi.fn(),
getFeatures: vi.fn().mockReturnValue([]),
on: vi.fn(),
})),
}));
vi.mock("ol/proj", () => ({
@@ -75,11 +80,30 @@ vi.mock("ol/control", () => ({
vi.mock("ol/interaction/Draw", () => ({
default: vi.fn().mockImplementation(() => ({
on: vi.fn(),
setActive: vi.fn(),
})),
}));
vi.mock("ol/interaction/Modify", () => ({
default: vi.fn().mockImplementation(() => ({
on: vi.fn(),
setActive: vi.fn(),
})),
}));
vi.mock("ol/interaction/Select", () => ({
default: vi.fn().mockImplementation(() => ({
on: vi.fn(),
setActive: vi.fn(),
getFeatures: vi.fn().mockReturnValue({
getArray: vi.fn().mockReturnValue([]),
clear: vi.fn(),
push: vi.fn(),
}),
})),
}));
vi.mock("ol/interaction/Translate", () => ({
default: vi.fn().mockImplementation(() => ({
on: vi.fn(),
setActive: vi.fn(),
})),
}));
vi.mock("ol/interaction/Snap", () => ({
@@ -106,6 +130,29 @@ vi.mock("ol/format/GeoJSON", () => ({
readFeatures: vi.fn().mockReturnValue([]),
})),
}));
vi.mock("ol/style", () => ({
Style: vi.fn().mockImplementation(() => ({})),
Text: vi.fn().mockImplementation(() => ({})),
Fill: vi.fn().mockImplementation(() => ({})),
Stroke: vi.fn().mockImplementation(() => ({})),
Circle: vi.fn().mockImplementation(() => ({})),
Icon: vi.fn().mockImplementation(() => ({})),
}));
vi.mock("ol/sphere", () => ({
getArea: vi.fn(),
getLength: vi.fn(),
}));
vi.mock("ol/geom", () => ({
LineString: vi.fn(),
Polygon: vi.fn(),
Circle: vi.fn(),
}));
vi.mock("ol/geom/Polygon", () => ({
fromCircle: vi.fn(),
}));
vi.mock("ol/Observable", () => ({
unByKey: vi.fn(),
}));
import MapPage from "@/components/map/MapPage.vue";