mirror of
https://git.quad4.io/RNS-Things/MeshChatX.git
synced 2026-04-26 17:27:50 +00:00
193 lines
6.8 KiB
JavaScript
193 lines
6.8 KiB
JavaScript
import { mount, flushPromises } from "@vue/test-utils";
|
|
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
import ReticulumConfigEditorPage from "@/components/tools/ReticulumConfigEditorPage.vue";
|
|
import DialogUtils from "@/js/DialogUtils";
|
|
import GlobalState from "@/js/GlobalState";
|
|
|
|
vi.mock("@/js/DialogUtils", () => ({
|
|
default: {
|
|
confirm: vi.fn(),
|
|
alert: vi.fn(),
|
|
prompt: vi.fn(),
|
|
},
|
|
}));
|
|
|
|
const SAMPLE_CONFIG =
|
|
"[reticulum]\n enable_transport = False\n\n[interfaces]\n [[Default Interface]]\n type = AutoInterface\n";
|
|
const DEFAULT_CONFIG =
|
|
"[reticulum]\n enable_transport = False\n\n[interfaces]\n [[Default Interface]]\n type = AutoInterface\n enabled = true\n";
|
|
const CONFIG_PATH = "/tmp/.reticulum/config";
|
|
|
|
describe("ReticulumConfigEditorPage.vue", () => {
|
|
let axiosMock;
|
|
|
|
beforeEach(() => {
|
|
axiosMock = {
|
|
get: vi.fn(),
|
|
put: vi.fn(),
|
|
post: vi.fn(),
|
|
};
|
|
window.api = axiosMock;
|
|
|
|
axiosMock.get.mockResolvedValue({
|
|
data: { content: SAMPLE_CONFIG, path: CONFIG_PATH },
|
|
});
|
|
axiosMock.put.mockResolvedValue({
|
|
data: { message: "Reticulum config saved", path: CONFIG_PATH },
|
|
});
|
|
axiosMock.post.mockImplementation((url) => {
|
|
if (url === "/api/v1/reticulum/config/reset") {
|
|
return Promise.resolve({
|
|
data: {
|
|
message: "Reticulum config restored to defaults",
|
|
content: DEFAULT_CONFIG,
|
|
path: CONFIG_PATH,
|
|
},
|
|
});
|
|
}
|
|
if (url === "/api/v1/reticulum/reload") {
|
|
return Promise.resolve({
|
|
data: { message: "Reticulum reloaded successfully" },
|
|
});
|
|
}
|
|
return Promise.resolve({ data: {} });
|
|
});
|
|
|
|
GlobalState.hasPendingInterfaceChanges = false;
|
|
if (!GlobalState.modifiedInterfaceNames) {
|
|
GlobalState.modifiedInterfaceNames = new Set();
|
|
}
|
|
GlobalState.modifiedInterfaceNames.clear();
|
|
});
|
|
|
|
afterEach(() => {
|
|
delete window.api;
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
const mountPage = () => {
|
|
return mount(ReticulumConfigEditorPage, {
|
|
global: {
|
|
mocks: {
|
|
$t: (key) => key,
|
|
},
|
|
stubs: {
|
|
MaterialDesignIcon: {
|
|
template: '<div class="mdi-stub" :data-icon-name="iconName"></div>',
|
|
props: ["iconName"],
|
|
},
|
|
},
|
|
},
|
|
});
|
|
};
|
|
|
|
it("loads the current config on mount and shows the path", async () => {
|
|
const wrapper = mountPage();
|
|
await flushPromises();
|
|
|
|
expect(axiosMock.get).toHaveBeenCalledWith("/api/v1/reticulum/config/raw");
|
|
expect(wrapper.vm.content).toBe(SAMPLE_CONFIG);
|
|
expect(wrapper.vm.originalContent).toBe(SAMPLE_CONFIG);
|
|
expect(wrapper.text()).toContain(CONFIG_PATH);
|
|
expect(wrapper.text()).toContain("tools.reticulum_config_editor.title");
|
|
});
|
|
|
|
it("marks the editor as dirty when the textarea changes", async () => {
|
|
const wrapper = mountPage();
|
|
await flushPromises();
|
|
expect(wrapper.vm.isDirty).toBe(false);
|
|
|
|
const textarea = wrapper.find("textarea");
|
|
await textarea.setValue(SAMPLE_CONFIG + "\n# my edit\n");
|
|
|
|
expect(wrapper.vm.isDirty).toBe(true);
|
|
expect(wrapper.text()).toContain("tools.reticulum_config_editor.unsaved");
|
|
});
|
|
|
|
it("saves config and shows the restart banner after success", async () => {
|
|
const wrapper = mountPage();
|
|
await flushPromises();
|
|
|
|
const newContent = SAMPLE_CONFIG + "\n# edit\n";
|
|
await wrapper.find("textarea").setValue(newContent);
|
|
await wrapper.vm.saveConfig();
|
|
await flushPromises();
|
|
|
|
expect(axiosMock.put).toHaveBeenCalledWith("/api/v1/reticulum/config/raw", { content: newContent });
|
|
expect(wrapper.vm.hasSavedChanges).toBe(true);
|
|
expect(wrapper.vm.showRestartReminder).toBe(true);
|
|
expect(GlobalState.hasPendingInterfaceChanges).toBe(true);
|
|
expect(wrapper.text()).toContain("tools.reticulum_config_editor.restart_required");
|
|
});
|
|
|
|
it("does not save when the editor is not dirty", async () => {
|
|
const wrapper = mountPage();
|
|
await flushPromises();
|
|
await wrapper.vm.saveConfig();
|
|
expect(axiosMock.put).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("restores defaults after confirmation", async () => {
|
|
DialogUtils.confirm.mockResolvedValue(true);
|
|
const wrapper = mountPage();
|
|
await flushPromises();
|
|
|
|
await wrapper.vm.restoreDefaults();
|
|
await flushPromises();
|
|
|
|
expect(DialogUtils.confirm).toHaveBeenCalled();
|
|
expect(axiosMock.post).toHaveBeenCalledWith("/api/v1/reticulum/config/reset");
|
|
expect(wrapper.vm.content).toBe(DEFAULT_CONFIG);
|
|
expect(wrapper.vm.originalContent).toBe(DEFAULT_CONFIG);
|
|
expect(wrapper.vm.hasSavedChanges).toBe(true);
|
|
expect(GlobalState.hasPendingInterfaceChanges).toBe(true);
|
|
});
|
|
|
|
it("does not restore defaults if the user cancels", async () => {
|
|
DialogUtils.confirm.mockResolvedValue(false);
|
|
const wrapper = mountPage();
|
|
await flushPromises();
|
|
|
|
await wrapper.vm.restoreDefaults();
|
|
await flushPromises();
|
|
|
|
expect(axiosMock.post).not.toHaveBeenCalledWith("/api/v1/reticulum/config/reset");
|
|
expect(wrapper.vm.content).toBe(SAMPLE_CONFIG);
|
|
});
|
|
|
|
it("reloads RNS and clears the restart banner", async () => {
|
|
const wrapper = mountPage();
|
|
await flushPromises();
|
|
wrapper.vm.hasSavedChanges = true;
|
|
await wrapper.vm.$nextTick();
|
|
|
|
await wrapper.vm.reloadRns();
|
|
await flushPromises();
|
|
|
|
expect(axiosMock.post).toHaveBeenCalledWith("/api/v1/reticulum/reload");
|
|
expect(wrapper.vm.hasSavedChanges).toBe(false);
|
|
expect(GlobalState.hasPendingInterfaceChanges).toBe(false);
|
|
});
|
|
|
|
it("discards unsaved changes back to the original content", async () => {
|
|
const wrapper = mountPage();
|
|
await flushPromises();
|
|
|
|
await wrapper.find("textarea").setValue("[reticulum]\n[interfaces]\n# changed");
|
|
expect(wrapper.vm.isDirty).toBe(true);
|
|
|
|
wrapper.vm.discardChanges();
|
|
expect(wrapper.vm.content).toBe(SAMPLE_CONFIG);
|
|
expect(wrapper.vm.isDirty).toBe(false);
|
|
});
|
|
|
|
it("shows an error toast when load fails", async () => {
|
|
axiosMock.get.mockRejectedValueOnce({
|
|
response: { data: { error: "boom" } },
|
|
});
|
|
const wrapper = mountPage();
|
|
await flushPromises();
|
|
expect(wrapper.vm.content).toBe("");
|
|
});
|
|
});
|