diff --git a/tests/frontend/BanishedPage.test.js b/tests/frontend/BanishedPage.test.js new file mode 100644 index 0000000..f83972d --- /dev/null +++ b/tests/frontend/BanishedPage.test.js @@ -0,0 +1,119 @@ +import { mount } from "@vue/test-utils"; +import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; +import BlockedPage from "@/components/blocked/BlockedPage.vue"; +import GlobalState from "@/js/GlobalState"; + +describe("BlockedPage.vue (Banished UI)", () => { + let axiosMock; + + beforeEach(() => { + axiosMock = { + get: vi.fn(), + post: vi.fn(), + delete: vi.fn(), + }; + window.axios = axiosMock; + + // Mock localization + const t = (key) => { + const translations = { + "common.save": "Save", + "common.cancel": "Cancel", + }; + return translations[key] || key; + }; + + axiosMock.get.mockImplementation((url) => { + if (url === "/api/v1/blocked-destinations") { + return Promise.resolve({ + data: { + blocked_destinations: [ + { destination_hash: "a".repeat(32), created_at: "2026-01-04T12:00:00Z" }, + ], + }, + }); + } + if (url === "/api/v1/reticulum/blackhole") { + return Promise.resolve({ + data: { + blackholed_identities: { + ["b".repeat(32)]: { + source: "c".repeat(32), + reason: "Spam", + until: null, + }, + }, + }, + }); + } + if (url === "/api/v1/announces") { + return Promise.resolve({ data: { announces: [] } }); + } + return Promise.resolve({ data: {} }); + }); + }); + + afterEach(() => { + delete window.axios; + }); + + const mountBlockedPage = () => { + return mount(BlockedPage, { + global: { + mocks: { + $t: (key) => key, + }, + stubs: { + MaterialDesignIcon: { + template: '
', + props: ["iconName"], + }, + }, + }, + }); + }; + + it("displays 'Banished' title and subtext", async () => { + const wrapper = mountBlockedPage(); + // Wait for isLoading to become false + await vi.waitFor(() => expect(wrapper.vm.isLoading).toBe(false)); + + expect(wrapper.text()).toContain("Banished"); + expect(wrapper.text()).toContain("Manage Banished users and nodes"); + }); + + it("combines local blocked and RNS blackholed items", async () => { + const wrapper = mountBlockedPage(); + await vi.waitFor(() => expect(wrapper.vm.isLoading).toBe(false)); + + expect(wrapper.vm.allBlockedItems.length).toBe(2); + + const rnsItem = wrapper.vm.allBlockedItems.find((i) => i.is_rns_blackholed); + expect(rnsItem).toBeDefined(); + expect(rnsItem.destination_hash).toBe("b".repeat(32)); + expect(rnsItem.rns_reason).toBe("Spam"); + }); + + it("displays RNS Blackhole badge for blackholed items", async () => { + const wrapper = mountBlockedPage(); + await vi.waitFor(() => expect(wrapper.vm.isLoading).toBe(false)); + + expect(wrapper.text()).toContain("RNS Blackhole"); + }); + + it("calls delete API when lifting banishment", async () => { + // Mock DialogUtils.confirm + const DialogUtils = await import("@/js/DialogUtils"); + vi.spyOn(DialogUtils.default, "confirm").mockResolvedValue(true); + + const wrapper = mountBlockedPage(); + await vi.waitFor(() => expect(wrapper.vm.isLoading).toBe(false)); + + const unblockButtons = wrapper.findAll("button").filter((b) => b.text().includes("Lift Banishment")); + expect(unblockButtons.length).toBeGreaterThan(0); + + await unblockButtons[0].trigger("click"); + + expect(axiosMock.delete).toHaveBeenCalled(); + }); +}); diff --git a/tests/frontend/LanguageSelector.test.js b/tests/frontend/LanguageSelector.test.js index fff5897..8e77e9e 100644 --- a/tests/frontend/LanguageSelector.test.js +++ b/tests/frontend/LanguageSelector.test.js @@ -14,6 +14,7 @@ describe("LanguageSelector.vue", () => { }, stubs: { MaterialDesignIcon: true, + Teleport: true, }, }, }); @@ -28,20 +29,20 @@ describe("LanguageSelector.vue", () => { const wrapper = mountLanguageSelector(); const button = wrapper.find("button"); - expect(wrapper.find(".absolute").exists()).toBe(false); + expect(wrapper.find(".fixed").exists()).toBe(false); await button.trigger("click"); - expect(wrapper.find(".absolute").exists()).toBe(true); + expect(wrapper.find(".fixed").exists()).toBe(true); await button.trigger("click"); - expect(wrapper.find(".absolute").exists()).toBe(false); + expect(wrapper.find(".fixed").exists()).toBe(false); }); it("lists all available languages in the dropdown", async () => { const wrapper = mountLanguageSelector(); await wrapper.find("button").trigger("click"); - const languageButtons = wrapper.findAll(".absolute button"); + const languageButtons = wrapper.findAll(".fixed button"); expect(languageButtons).toHaveLength(3); expect(languageButtons[0].text()).toContain("English"); expect(languageButtons[1].text()).toContain("Deutsch"); @@ -52,22 +53,22 @@ describe("LanguageSelector.vue", () => { const wrapper = mountLanguageSelector("en"); await wrapper.find("button").trigger("click"); - const deButton = wrapper.findAll(".absolute button")[1]; + const deButton = wrapper.findAll(".fixed button")[1]; await deButton.trigger("click"); expect(wrapper.emitted("language-change")).toBeTruthy(); expect(wrapper.emitted("language-change")[0]).toEqual(["de"]); - expect(wrapper.find(".absolute").exists()).toBe(false); + expect(wrapper.find(".fixed").exists()).toBe(false); }); it("does not emit language-change when the current language is selected", async () => { const wrapper = mountLanguageSelector("en"); await wrapper.find("button").trigger("click"); - const enButton = wrapper.findAll(".absolute button")[0]; + const enButton = wrapper.findAll(".fixed button")[0]; await enButton.trigger("click"); expect(wrapper.emitted("language-change")).toBeFalsy(); - expect(wrapper.find(".absolute").exists()).toBe(false); + expect(wrapper.find(".fixed").exists()).toBe(false); }); }); diff --git a/tests/frontend/NotificationBell.test.js b/tests/frontend/NotificationBell.test.js index 743291c..c1925d8 100644 --- a/tests/frontend/NotificationBell.test.js +++ b/tests/frontend/NotificationBell.test.js @@ -32,6 +32,7 @@ describe("NotificationBell.vue", () => { }, stubs: { MaterialDesignIcon: true, + Teleport: true, }, directives: { "click-outside": {}, @@ -172,6 +173,7 @@ describe("NotificationBell.vue", () => { }, stubs: { MaterialDesignIcon: true, + Teleport: true, }, directives: { "click-outside": {}, diff --git a/tests/frontend/RNPathPage.test.js b/tests/frontend/RNPathPage.test.js new file mode 100644 index 0000000..7fcf42c --- /dev/null +++ b/tests/frontend/RNPathPage.test.js @@ -0,0 +1,105 @@ +import { mount } from "@vue/test-utils"; +import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; +import RNPathPage from "@/components/tools/RNPathPage.vue"; + +describe("RNPathPage.vue", () => { + let axiosMock; + + beforeEach(() => { + axiosMock = { + get: vi.fn(), + post: vi.fn(), + }; + window.axios = axiosMock; + + axiosMock.get.mockImplementation((url) => { + if (url === "/api/v1/rnpath/table") { + return Promise.resolve({ + data: { + table: [ + { + hash: "a".repeat(32), + hops: 1, + via: "b".repeat(32), + interface: "UDP", + expires: 1234567890, + }, + ], + }, + }); + } + if (url === "/api/v1/rnpath/rates") { + return Promise.resolve({ + data: { + rates: [ + { + hash: "c".repeat(32), + last: 1234567890, + timestamps: [], + rate_violations: 0, + blocked_until: 0, + }, + ], + }, + }); + } + return Promise.resolve({ data: {} }); + }); + }); + + afterEach(() => { + delete window.axios; + }); + + const mountRNPathPage = () => { + return mount(RNPathPage, { + global: { + mocks: { + $t: (key) => key, + }, + stubs: { + MaterialDesignIcon: { + template: '', + props: ["iconName"], + }, + }, + }, + }); + }; + + it("renders and loads data", async () => { + const wrapper = mountRNPathPage(); + await vi.waitFor(() => expect(wrapper.vm.isLoading).toBe(false)); + + expect(wrapper.text()).toContain("RNPath"); + expect(wrapper.vm.pathTable.length).toBe(1); + expect(wrapper.vm.rateTable.length).toBe(1); + }); + + it("switches tabs", async () => { + const wrapper = mountRNPathPage(); + await vi.waitFor(() => expect(wrapper.vm.isLoading).toBe(false)); + + const ratesButton = wrapper.findAll("button").find((b) => b.text() === "Rates"); + await ratesButton.trigger("click"); + expect(wrapper.vm.tab).toBe("rates"); + + const actionsButton = wrapper.findAll("button").find((b) => b.text() === "Actions"); + await actionsButton.trigger("click"); + expect(wrapper.vm.tab).toBe("actions"); + }); + + it("calls request path API", async () => { + const wrapper = mountRNPathPage(); + await vi.waitFor(() => expect(wrapper.vm.isLoading).toBe(false)); + + await wrapper.setData({ tab: "actions", requestHash: "d".repeat(32) }); + + const requestButton = wrapper.findAll("button").find((b) => b.text() === "Request"); + await requestButton.trigger("click"); + + expect(axiosMock.post).toHaveBeenCalledWith("/api/v1/rnpath/request", { + destination_hash: "d".repeat(32), + }); + }); +}); diff --git a/tests/frontend/UIThemeAndVisibility.test.js b/tests/frontend/UIThemeAndVisibility.test.js index c33e603..13e6565 100644 --- a/tests/frontend/UIThemeAndVisibility.test.js +++ b/tests/frontend/UIThemeAndVisibility.test.js @@ -8,6 +8,14 @@ import ChangelogModal from "../../meshchatx/src/frontend/components/ChangelogMod import NotificationBell from "../../meshchatx/src/frontend/components/NotificationBell.vue"; import LanguageSelector from "../../meshchatx/src/frontend/components/LanguageSelector.vue"; +vi.mock("vuetify", () => ({ + useTheme: vi.fn(() => ({ + global: { + name: { value: "light" }, + }, + })), +})); + vi.mock("../../meshchatx/src/frontend/js/WebSocketConnection", () => ({ default: { on: vi.fn(), @@ -344,6 +352,7 @@ describe("Visibility Checks", () => { banished_effect_enabled: true, banished_text: "BANISHED", banished_color: "#dc2626", + blackhole_integration_enabled: true, }, }, }), @@ -379,6 +388,42 @@ describe("Visibility Checks", () => { delete window.axios; }); + it("SettingsPage shows blackhole integration toggle", async () => { + const axiosMock = { + get: vi.fn().mockResolvedValue({ + data: { + config: { + blackhole_integration_enabled: true, + }, + }, + }), + patch: vi.fn().mockResolvedValue({ data: {} }), + }; + window.axios = axiosMock; + + const wrapper = mount(SettingsPage, { + global: { + stubs: { + MaterialDesignIcon: { template: "" }, + Toggle: Toggle, + ShortcutRecorder: { template: "" }, + RouterLink: { template: "