mirror of
https://git.quad4.io/RNS-Things/MeshChatX.git
synced 2026-05-11 16:04:42 +00:00
427 lines
16 KiB
JavaScript
427 lines
16 KiB
JavaScript
import { mount } from "@vue/test-utils";
|
|
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
import AboutPage from "@/components/about/AboutPage.vue";
|
|
import ElectronUtils from "@/js/ElectronUtils";
|
|
import DialogUtils from "@/js/DialogUtils";
|
|
import ToastUtils from "@/js/ToastUtils";
|
|
|
|
vi.mock("@/js/ToastUtils", () => ({
|
|
default: {
|
|
success: vi.fn(),
|
|
error: vi.fn(),
|
|
loading: vi.fn(),
|
|
dismiss: vi.fn(),
|
|
},
|
|
}));
|
|
|
|
describe("AboutPage.vue", () => {
|
|
let axiosMock;
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
vi.useFakeTimers();
|
|
axiosMock = {
|
|
get: vi.fn().mockImplementation(() => Promise.resolve({ data: {} })),
|
|
post: vi.fn().mockImplementation(() => Promise.resolve({ data: {} })),
|
|
};
|
|
window.api = axiosMock;
|
|
window.URL.createObjectURL = vi.fn();
|
|
window.URL.revokeObjectURL = vi.fn();
|
|
|
|
// Default electron mock
|
|
window.electron = {
|
|
getMemoryUsage: vi.fn().mockResolvedValue(null),
|
|
electronVersion: vi.fn().mockReturnValue("1.0.0"),
|
|
chromeVersion: vi.fn().mockReturnValue("1.0.0"),
|
|
nodeVersion: vi.fn().mockReturnValue("1.0.0"),
|
|
appVersion: vi.fn().mockResolvedValue("1.0.0"),
|
|
};
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.useRealTimers();
|
|
delete window.api;
|
|
delete window.electron;
|
|
});
|
|
|
|
const mountAboutPage = () => {
|
|
return mount(AboutPage, {
|
|
global: {
|
|
mocks: {
|
|
$t: (key, params) => {
|
|
if (params) {
|
|
return `${key} ${JSON.stringify(params)}`;
|
|
}
|
|
return key;
|
|
},
|
|
},
|
|
stubs: {
|
|
MaterialDesignIcon: true,
|
|
},
|
|
},
|
|
});
|
|
};
|
|
|
|
it("fetches app info and config on mount", async () => {
|
|
const appInfo = {
|
|
version: "1.0.0",
|
|
rns_version: "0.1.0",
|
|
lxmf_version: "0.2.0",
|
|
python_version: "3.11.0",
|
|
reticulum_config_path: "/path/to/config",
|
|
database_path: "/path/to/db",
|
|
database_file_size: 1024,
|
|
dependencies: {
|
|
aiohttp: "3.8.1",
|
|
cryptography: "3.4.8",
|
|
},
|
|
};
|
|
const config = {
|
|
identity_hash: "hash1",
|
|
lxmf_address_hash: "hash2",
|
|
};
|
|
|
|
axiosMock.get.mockImplementation((url) => {
|
|
if (url === "/api/v1/app/info") return Promise.resolve({ data: { app_info: appInfo } });
|
|
if (url === "/api/v1/config") return Promise.resolve({ data: { config: config } });
|
|
if (url === "/api/v1/database/health")
|
|
return Promise.resolve({
|
|
data: {
|
|
database: {
|
|
quick_check: "ok",
|
|
journal_mode: "wal",
|
|
page_size: 4096,
|
|
page_count: 100,
|
|
freelist_pages: 5,
|
|
estimated_free_bytes: 20480,
|
|
},
|
|
},
|
|
});
|
|
if (url === "/api/v1/database/snapshots") return Promise.resolve({ data: [] });
|
|
return Promise.reject(new Error("Not found"));
|
|
});
|
|
|
|
const wrapper = mountAboutPage();
|
|
wrapper.vm.showAdvanced = true;
|
|
await vi.runOnlyPendingTimers();
|
|
await wrapper.vm.$nextTick();
|
|
await wrapper.vm.$nextTick(); // Extra tick for multiple async calls
|
|
await wrapper.vm.$nextTick();
|
|
await wrapper.vm.$nextTick();
|
|
|
|
expect(axiosMock.get).toHaveBeenCalledWith("/api/v1/app/info");
|
|
expect(axiosMock.get).toHaveBeenCalledWith("/api/v1/config");
|
|
|
|
expect(wrapper.text()).toContain("about.app_name");
|
|
expect(wrapper.text()).toContain("about.tagline_link");
|
|
expect(wrapper.text()).toContain("hash1");
|
|
expect(wrapper.text()).toContain("hash2");
|
|
|
|
expect(wrapper.text()).toContain("about.dependency_chain");
|
|
expect(wrapper.text()).toContain("about.dep_lxmf_subtitle");
|
|
expect(wrapper.text()).toContain("about.dep_rns_subtitle");
|
|
|
|
expect(wrapper.text()).toContain("about.backend_stack");
|
|
expect(wrapper.text()).toContain("aiohttp");
|
|
expect(wrapper.text()).toContain("3.8.1");
|
|
});
|
|
|
|
it("displays Electron memory usage when running in Electron", async () => {
|
|
vi.spyOn(ElectronUtils, "isElectron").mockReturnValue(true);
|
|
const getMemoryUsageSpy = vi.spyOn(ElectronUtils, "getMemoryUsage").mockResolvedValue({
|
|
private: 1000,
|
|
residentSet: 2000,
|
|
});
|
|
|
|
const appInfo = {
|
|
version: "1.0.0",
|
|
};
|
|
|
|
axiosMock.get.mockImplementation((url) => {
|
|
if (url === "/api/v1/app/info") return Promise.resolve({ data: { app_info: appInfo } });
|
|
if (url === "/api/v1/config") return Promise.resolve({ data: { config: {} } });
|
|
if (url === "/api/v1/database/health") return Promise.resolve({ data: { database: {} } });
|
|
if (url === "/api/v1/database/snapshots") return Promise.resolve({ data: [] });
|
|
return Promise.reject(new Error("Not found"));
|
|
});
|
|
|
|
const wrapper = mountAboutPage();
|
|
wrapper.vm.showAdvanced = true;
|
|
await wrapper.vm.$nextTick();
|
|
await wrapper.vm.$nextTick();
|
|
await wrapper.vm.$nextTick();
|
|
await wrapper.vm.$nextTick();
|
|
|
|
expect(getMemoryUsageSpy).toHaveBeenCalled();
|
|
expect(wrapper.vm.electronMemoryUsage).not.toBeNull();
|
|
expect(wrapper.text()).toContain("about.environment_information");
|
|
});
|
|
|
|
it("handles shutdown action", async () => {
|
|
const confirmSpy = vi.spyOn(DialogUtils, "confirm").mockResolvedValue(true);
|
|
const axiosPostSpy = axiosMock.post.mockResolvedValue({ data: { message: "Shutting down..." } });
|
|
const shutdownSpy = vi.spyOn(ElectronUtils, "shutdown").mockImplementation(() => {});
|
|
vi.spyOn(ElectronUtils, "isElectron").mockReturnValue(true);
|
|
|
|
const wrapper = mountAboutPage();
|
|
wrapper.vm.appInfo = { version: "1.0.0" };
|
|
await wrapper.vm.$nextTick();
|
|
|
|
await wrapper.vm.shutdown();
|
|
|
|
expect(confirmSpy).toHaveBeenCalled();
|
|
expect(axiosPostSpy).toHaveBeenCalledWith("/api/v1/app/shutdown");
|
|
expect(shutdownSpy).toHaveBeenCalled();
|
|
});
|
|
|
|
it("restartRns posts reticulum reload endpoint", async () => {
|
|
const wrapper = mountAboutPage();
|
|
wrapper.vm.appInfo = { version: "1.0.0" };
|
|
await wrapper.vm.$nextTick();
|
|
|
|
axiosMock.post.mockResolvedValueOnce({ data: { message: "RNS restarted" } });
|
|
await wrapper.vm.restartRns();
|
|
|
|
expect(ToastUtils.loading).toHaveBeenCalledWith("app.reloading_rns", 0, "about-rns-reload");
|
|
expect(axiosMock.post).toHaveBeenCalledWith("/api/v1/reticulum/reload");
|
|
expect(ToastUtils.dismiss).toHaveBeenCalledWith("about-rns-reload");
|
|
});
|
|
|
|
it("updates app info periodically", async () => {
|
|
axiosMock.get.mockResolvedValue({
|
|
data: {
|
|
app_info: {},
|
|
config: {},
|
|
database: {
|
|
quick_check: "ok",
|
|
journal_mode: "wal",
|
|
page_size: 4096,
|
|
page_count: 100,
|
|
freelist_pages: 5,
|
|
estimated_free_bytes: 20480,
|
|
},
|
|
},
|
|
});
|
|
mountAboutPage();
|
|
|
|
expect(axiosMock.get).toHaveBeenCalledTimes(5); // info, config, health, snapshots, backups
|
|
|
|
vi.advanceTimersByTime(5000);
|
|
expect(axiosMock.get).toHaveBeenCalledTimes(6); // +1 from updateInterval
|
|
|
|
vi.advanceTimersByTime(5000);
|
|
expect(axiosMock.get).toHaveBeenCalledTimes(7); // +2 from updateInterval
|
|
});
|
|
|
|
it("handles vacuum database action and shows success toast", async () => {
|
|
axiosMock.get.mockResolvedValue({
|
|
data: {
|
|
app_info: {},
|
|
config: {},
|
|
database: {
|
|
quick_check: "ok",
|
|
journal_mode: "wal",
|
|
page_size: 4096,
|
|
page_count: 100,
|
|
freelist_pages: 5,
|
|
estimated_free_bytes: 20480,
|
|
},
|
|
},
|
|
});
|
|
axiosMock.post.mockResolvedValue({ data: { message: "Vacuum success" } });
|
|
|
|
const wrapper = mountAboutPage();
|
|
await wrapper.vm.$nextTick();
|
|
|
|
await wrapper.vm.vacuumDatabase();
|
|
|
|
expect(axiosMock.post).toHaveBeenCalledWith("/api/v1/database/vacuum");
|
|
expect(wrapper.vm.databaseActionMessage).toBe("Vacuum success");
|
|
expect(ToastUtils.success).toHaveBeenCalledWith("about.vacuum_complete");
|
|
});
|
|
|
|
it("shows error toast when vacuum fails", async () => {
|
|
axiosMock.get.mockResolvedValue({
|
|
data: {
|
|
app_info: {},
|
|
config: {},
|
|
database: {},
|
|
},
|
|
});
|
|
const apiErr = new Error("vacuum failed");
|
|
apiErr.response = { data: { message: "Failed to vacuum database: locked" } };
|
|
axiosMock.post.mockRejectedValue(apiErr);
|
|
|
|
const wrapper = mountAboutPage();
|
|
await wrapper.vm.$nextTick();
|
|
|
|
await wrapper.vm.vacuumDatabase();
|
|
|
|
expect(ToastUtils.error).toHaveBeenCalledWith("Failed to vacuum database: locked");
|
|
expect(wrapper.vm.databaseActionError).toBe("about.vacuum_failed");
|
|
});
|
|
|
|
it("handles database recovery and shows success toast", async () => {
|
|
vi.spyOn(DialogUtils, "confirm").mockResolvedValue(true);
|
|
axiosMock.get.mockResolvedValue({
|
|
data: {
|
|
app_info: {},
|
|
config: {},
|
|
database: {
|
|
quick_check: "ok",
|
|
journal_mode: "wal",
|
|
page_count: 1,
|
|
estimated_free_bytes: 0,
|
|
},
|
|
},
|
|
});
|
|
axiosMock.post.mockImplementation((url) => {
|
|
if (url === "/api/v1/database/recover") {
|
|
return Promise.resolve({
|
|
data: {
|
|
message: "Database recovery routine completed",
|
|
database: {
|
|
health: {
|
|
quick_check: "ok",
|
|
journal_mode: "wal",
|
|
page_count: 2,
|
|
estimated_free_bytes: 100,
|
|
},
|
|
actions: [{ step: "wal_checkpoint", result: [] }],
|
|
},
|
|
},
|
|
});
|
|
}
|
|
return Promise.resolve({ data: {} });
|
|
});
|
|
|
|
const wrapper = mountAboutPage();
|
|
await wrapper.vm.$nextTick();
|
|
|
|
await wrapper.vm.runRecovery();
|
|
|
|
expect(DialogUtils.confirm).toHaveBeenCalledWith("about.recovery_confirm");
|
|
expect(axiosMock.post).toHaveBeenCalledWith("/api/v1/database/recover");
|
|
expect(ToastUtils.success).toHaveBeenCalledWith("about.recovery_complete");
|
|
expect(wrapper.vm.databaseRecoveryActions.length).toBe(1);
|
|
});
|
|
|
|
it("does not run recovery when user cancels the confirm dialog", async () => {
|
|
vi.spyOn(DialogUtils, "confirm").mockResolvedValue(false);
|
|
axiosMock.get.mockResolvedValue({
|
|
data: { app_info: {}, config: {}, database: {} },
|
|
});
|
|
|
|
const wrapper = mountAboutPage();
|
|
await wrapper.vm.$nextTick();
|
|
|
|
await wrapper.vm.runRecovery();
|
|
|
|
expect(DialogUtils.confirm).toHaveBeenCalledWith("about.recovery_confirm");
|
|
expect(axiosMock.post).not.toHaveBeenCalledWith("/api/v1/database/recover");
|
|
expect(ToastUtils.success).not.toHaveBeenCalledWith("about.recovery_complete");
|
|
});
|
|
|
|
it("shows error toast when recovery fails", async () => {
|
|
vi.spyOn(DialogUtils, "confirm").mockResolvedValue(true);
|
|
axiosMock.get.mockResolvedValue({
|
|
data: { app_info: {}, config: {}, database: {} },
|
|
});
|
|
const apiErr = new Error("recover failed");
|
|
apiErr.response = { data: { message: "Failed to recover database: corrupt" } };
|
|
axiosMock.post.mockRejectedValue(apiErr);
|
|
|
|
const wrapper = mountAboutPage();
|
|
await wrapper.vm.$nextTick();
|
|
|
|
await wrapper.vm.runRecovery();
|
|
|
|
expect(ToastUtils.error).toHaveBeenCalledWith("Failed to recover database: corrupt");
|
|
expect(wrapper.vm.databaseActionError).toBe("about.recovery_failed");
|
|
});
|
|
|
|
it("displays Free Space from database health", async () => {
|
|
axiosMock.get.mockImplementation((url) => {
|
|
if (url === "/api/v1/app/info") return Promise.resolve({ data: { app_info: { version: "1.0.0" } } });
|
|
if (url === "/api/v1/config") return Promise.resolve({ data: { config: {} } });
|
|
if (url === "/api/v1/database/health")
|
|
return Promise.resolve({
|
|
data: {
|
|
database: {
|
|
quick_check: "ok",
|
|
journal_mode: "wal",
|
|
page_size: 4096,
|
|
page_count: 100,
|
|
freelist_pages: 0,
|
|
estimated_free_bytes: 1073741824,
|
|
},
|
|
},
|
|
});
|
|
if (url === "/api/v1/database/snapshots") return Promise.resolve({ data: [] });
|
|
return Promise.reject(new Error("Not found"));
|
|
});
|
|
|
|
const wrapper = mountAboutPage();
|
|
wrapper.vm.showAdvanced = true;
|
|
await vi.runOnlyPendingTimers();
|
|
await wrapper.vm.$nextTick();
|
|
await wrapper.vm.$nextTick();
|
|
|
|
expect(wrapper.text()).toContain("about.free_space");
|
|
expect(wrapper.text()).toContain("1 GB");
|
|
});
|
|
|
|
it("displays 0 Bytes when database health has no estimated_free_bytes", async () => {
|
|
axiosMock.get.mockImplementation((url) => {
|
|
if (url === "/api/v1/app/info") return Promise.resolve({ data: { app_info: { version: "1.0.0" } } });
|
|
if (url === "/api/v1/config") return Promise.resolve({ data: { config: {} } });
|
|
if (url === "/api/v1/database/health")
|
|
return Promise.resolve({
|
|
data: { database: { quick_check: "ok", journal_mode: "wal" } },
|
|
});
|
|
if (url === "/api/v1/database/snapshots") return Promise.resolve({ data: [] });
|
|
return Promise.reject(new Error("Not found"));
|
|
});
|
|
|
|
const wrapper = mountAboutPage();
|
|
wrapper.vm.showAdvanced = true;
|
|
await vi.runOnlyPendingTimers();
|
|
await wrapper.vm.$nextTick();
|
|
await wrapper.vm.$nextTick();
|
|
|
|
expect(wrapper.text()).toContain("about.free_space");
|
|
expect(wrapper.text()).toContain("0 Bytes");
|
|
});
|
|
|
|
it("shows unknown fallbacks for missing environment paths", async () => {
|
|
axiosMock.get.mockImplementation((url) => {
|
|
if (url === "/api/v1/app/info")
|
|
return Promise.resolve({
|
|
data: {
|
|
app_info: {
|
|
version: "1.0.0",
|
|
python_version: "3.11.0",
|
|
lxmf_version: "0.2.0",
|
|
rns_version: "0.1.0",
|
|
reticulum_config_path: null,
|
|
database_path: null,
|
|
},
|
|
},
|
|
});
|
|
if (url === "/api/v1/config") return Promise.resolve({ data: { config: {} } });
|
|
if (url === "/api/v1/database/health") return Promise.resolve({ data: { database: {} } });
|
|
if (url === "/api/v1/database/snapshots") return Promise.resolve({ data: [] });
|
|
return Promise.reject(new Error("Not found"));
|
|
});
|
|
|
|
const wrapper = mountAboutPage();
|
|
await vi.runOnlyPendingTimers();
|
|
await wrapper.vm.$nextTick();
|
|
await wrapper.vm.$nextTick();
|
|
|
|
expect(wrapper.text()).toContain("about.reticulum_config");
|
|
expect(wrapper.text()).toContain("about.database_path");
|
|
expect(wrapper.text()).toContain("about.path_unknown");
|
|
});
|
|
});
|