test(frontend): add comprehensive unit tests for AuthPage, CommandPalette, ConfirmDialog, UI components, and theme handling
This commit is contained in:
351
tests/frontend/AuthPage.test.js
Normal file
351
tests/frontend/AuthPage.test.js
Normal file
@@ -0,0 +1,351 @@
|
||||
import { mount } from "@vue/test-utils";
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
import AuthPage from "../../meshchatx/src/frontend/components/auth/AuthPage.vue";
|
||||
|
||||
describe("AuthPage.vue", () => {
|
||||
let axiosMock;
|
||||
let routerMock;
|
||||
|
||||
beforeEach(() => {
|
||||
axiosMock = {
|
||||
get: vi.fn().mockResolvedValue({
|
||||
data: {
|
||||
auth_enabled: true,
|
||||
authenticated: false,
|
||||
password_set: true,
|
||||
},
|
||||
}),
|
||||
post: vi.fn().mockResolvedValue({ data: { success: true } }),
|
||||
};
|
||||
window.axios = axiosMock;
|
||||
|
||||
routerMock = {
|
||||
push: vi.fn(),
|
||||
};
|
||||
|
||||
Object.defineProperty(window, "location", {
|
||||
value: {
|
||||
reload: vi.fn(),
|
||||
},
|
||||
writable: true,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
delete window.axios;
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
const mountAuthPage = () => {
|
||||
return mount(AuthPage, {
|
||||
global: {
|
||||
mocks: {
|
||||
$router: routerMock,
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
it("renders login form when password is set", async () => {
|
||||
axiosMock.get.mockResolvedValueOnce({
|
||||
data: {
|
||||
auth_enabled: true,
|
||||
authenticated: false,
|
||||
password_set: true,
|
||||
},
|
||||
});
|
||||
|
||||
const wrapper = mountAuthPage();
|
||||
await wrapper.vm.$nextTick();
|
||||
await wrapper.vm.checkAuthStatus();
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(wrapper.text()).toContain("Authentication Required");
|
||||
expect(wrapper.text()).toContain("Login");
|
||||
expect(wrapper.find('input[type="password"]').exists()).toBe(true);
|
||||
});
|
||||
|
||||
it("renders setup form when password is not set", async () => {
|
||||
axiosMock.get.mockResolvedValueOnce({
|
||||
data: {
|
||||
auth_enabled: true,
|
||||
authenticated: false,
|
||||
password_set: false,
|
||||
},
|
||||
});
|
||||
|
||||
const wrapper = mountAuthPage();
|
||||
await wrapper.vm.$nextTick();
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
|
||||
expect(wrapper.vm.isSetup).toBe(true);
|
||||
expect(wrapper.text()).toContain("Initial Setup");
|
||||
expect(wrapper.text()).toContain("Set Password");
|
||||
expect(wrapper.findAll('input[type="password"]').length).toBe(2);
|
||||
});
|
||||
|
||||
it("redirects to home when auth is disabled", async () => {
|
||||
axiosMock.get.mockResolvedValueOnce({
|
||||
data: {
|
||||
auth_enabled: false,
|
||||
authenticated: false,
|
||||
password_set: false,
|
||||
},
|
||||
});
|
||||
|
||||
const wrapper = mountAuthPage();
|
||||
await wrapper.vm.$nextTick();
|
||||
await wrapper.vm.checkAuthStatus();
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(routerMock.push).toHaveBeenCalledWith("/");
|
||||
});
|
||||
|
||||
it("redirects to home when already authenticated", async () => {
|
||||
axiosMock.get.mockResolvedValueOnce({
|
||||
data: {
|
||||
auth_enabled: true,
|
||||
authenticated: true,
|
||||
password_set: true,
|
||||
},
|
||||
});
|
||||
|
||||
const wrapper = mountAuthPage();
|
||||
await wrapper.vm.$nextTick();
|
||||
await wrapper.vm.checkAuthStatus();
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(routerMock.push).toHaveBeenCalledWith("/");
|
||||
});
|
||||
|
||||
it("validates password length in setup mode", async () => {
|
||||
axiosMock.get.mockResolvedValueOnce({
|
||||
data: {
|
||||
auth_enabled: true,
|
||||
authenticated: false,
|
||||
password_set: false,
|
||||
},
|
||||
});
|
||||
|
||||
const wrapper = mountAuthPage();
|
||||
await wrapper.vm.$nextTick();
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
|
||||
wrapper.vm.password = "short";
|
||||
wrapper.vm.confirmPassword = "short";
|
||||
await wrapper.vm.$nextTick();
|
||||
await wrapper.vm.handleSubmit();
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(wrapper.vm.error).toContain("at least 8 characters");
|
||||
expect(axiosMock.post).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("validates password match in setup mode", async () => {
|
||||
axiosMock.get.mockResolvedValueOnce({
|
||||
data: {
|
||||
auth_enabled: true,
|
||||
authenticated: false,
|
||||
password_set: false,
|
||||
},
|
||||
});
|
||||
|
||||
const wrapper = mountAuthPage();
|
||||
await wrapper.vm.$nextTick();
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
|
||||
wrapper.vm.password = "password123";
|
||||
wrapper.vm.confirmPassword = "password456";
|
||||
await wrapper.vm.$nextTick();
|
||||
await wrapper.vm.handleSubmit();
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(wrapper.vm.error).toContain("do not match");
|
||||
expect(axiosMock.post).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("submits setup form with valid password", async () => {
|
||||
axiosMock.get.mockResolvedValueOnce({
|
||||
data: {
|
||||
auth_enabled: true,
|
||||
authenticated: false,
|
||||
password_set: false,
|
||||
},
|
||||
});
|
||||
|
||||
const wrapper = mountAuthPage();
|
||||
await wrapper.vm.$nextTick();
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
|
||||
wrapper.vm.password = "password123";
|
||||
wrapper.vm.confirmPassword = "password123";
|
||||
await wrapper.vm.$nextTick();
|
||||
await wrapper.vm.handleSubmit();
|
||||
await wrapper.vm.$nextTick();
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
|
||||
expect(axiosMock.post).toHaveBeenCalledWith("/api/v1/auth/setup", {
|
||||
password: "password123",
|
||||
});
|
||||
});
|
||||
|
||||
it("submits login form with password", async () => {
|
||||
axiosMock.get.mockResolvedValueOnce({
|
||||
data: {
|
||||
auth_enabled: true,
|
||||
authenticated: false,
|
||||
password_set: true,
|
||||
},
|
||||
});
|
||||
|
||||
const wrapper = mountAuthPage();
|
||||
await wrapper.vm.$nextTick();
|
||||
await wrapper.vm.checkAuthStatus();
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
wrapper.vm.password = "password123";
|
||||
await wrapper.vm.handleSubmit();
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(axiosMock.post).toHaveBeenCalledWith("/api/v1/auth/login", {
|
||||
password: "password123",
|
||||
});
|
||||
});
|
||||
|
||||
it("reloads page after successful login", async () => {
|
||||
axiosMock.get.mockResolvedValueOnce({
|
||||
data: {
|
||||
auth_enabled: true,
|
||||
authenticated: false,
|
||||
password_set: true,
|
||||
},
|
||||
});
|
||||
|
||||
const wrapper = mountAuthPage();
|
||||
await wrapper.vm.$nextTick();
|
||||
await wrapper.vm.checkAuthStatus();
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
wrapper.vm.password = "password123";
|
||||
await wrapper.vm.handleSubmit();
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(window.location.reload).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("displays error message on login failure", async () => {
|
||||
axiosMock.get.mockResolvedValueOnce({
|
||||
data: {
|
||||
auth_enabled: true,
|
||||
authenticated: false,
|
||||
password_set: true,
|
||||
},
|
||||
});
|
||||
axiosMock.post.mockRejectedValueOnce({
|
||||
response: {
|
||||
data: {
|
||||
error: "Invalid password",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const wrapper = mountAuthPage();
|
||||
await wrapper.vm.$nextTick();
|
||||
await wrapper.vm.checkAuthStatus();
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
wrapper.vm.password = "wrongpassword";
|
||||
await wrapper.vm.handleSubmit();
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(wrapper.vm.error).toBe("Invalid password");
|
||||
expect(wrapper.vm.isLoading).toBe(false);
|
||||
});
|
||||
|
||||
it("displays error message on setup failure", async () => {
|
||||
axiosMock.get.mockResolvedValueOnce({
|
||||
data: {
|
||||
auth_enabled: true,
|
||||
authenticated: false,
|
||||
password_set: false,
|
||||
},
|
||||
});
|
||||
axiosMock.post.mockRejectedValueOnce({
|
||||
response: {
|
||||
data: {
|
||||
error: "Setup failed",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const wrapper = mountAuthPage();
|
||||
await wrapper.vm.$nextTick();
|
||||
await wrapper.vm.checkAuthStatus();
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
wrapper.vm.password = "password123";
|
||||
wrapper.vm.confirmPassword = "password123";
|
||||
await wrapper.vm.handleSubmit();
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(wrapper.vm.error).toBe("Setup failed");
|
||||
expect(wrapper.vm.isLoading).toBe(false);
|
||||
});
|
||||
|
||||
it("disables submit button when loading", async () => {
|
||||
axiosMock.get.mockResolvedValueOnce({
|
||||
data: {
|
||||
auth_enabled: true,
|
||||
authenticated: false,
|
||||
password_set: true,
|
||||
},
|
||||
});
|
||||
|
||||
const wrapper = mountAuthPage();
|
||||
await wrapper.vm.$nextTick();
|
||||
await wrapper.vm.checkAuthStatus();
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
wrapper.vm.isLoading = true;
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
const submitButton = wrapper.find('button[type="submit"]');
|
||||
expect(submitButton.attributes("disabled")).toBeDefined();
|
||||
});
|
||||
|
||||
it("disables submit button when passwords do not match in setup mode", async () => {
|
||||
axiosMock.get.mockResolvedValueOnce({
|
||||
data: {
|
||||
auth_enabled: true,
|
||||
authenticated: false,
|
||||
password_set: false,
|
||||
},
|
||||
});
|
||||
|
||||
const wrapper = mountAuthPage();
|
||||
await wrapper.vm.$nextTick();
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
|
||||
wrapper.vm.password = "password123";
|
||||
wrapper.vm.confirmPassword = "password456";
|
||||
await wrapper.vm.$nextTick();
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
const submitButton = wrapper.find('button[type="submit"]');
|
||||
const disabledAttr = submitButton.attributes("disabled");
|
||||
const disabledProp = submitButton.element.disabled;
|
||||
expect(disabledAttr !== undefined || disabledProp === true).toBe(true);
|
||||
});
|
||||
|
||||
it("handles network errors gracefully", async () => {
|
||||
axiosMock.get.mockRejectedValueOnce(new Error("Network error"));
|
||||
|
||||
const wrapper = mountAuthPage();
|
||||
await wrapper.vm.$nextTick();
|
||||
await wrapper.vm.checkAuthStatus();
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(wrapper.vm.error).toContain("Failed to check");
|
||||
});
|
||||
});
|
||||
|
||||
311
tests/frontend/CommandPalette.test.js
Normal file
311
tests/frontend/CommandPalette.test.js
Normal file
@@ -0,0 +1,311 @@
|
||||
import { mount } from "@vue/test-utils";
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
import CommandPalette from "../../meshchatx/src/frontend/components/CommandPalette.vue";
|
||||
import GlobalEmitter from "../../meshchatx/src/frontend/js/GlobalEmitter";
|
||||
|
||||
describe("CommandPalette.vue", () => {
|
||||
let axiosMock;
|
||||
let routerMock;
|
||||
|
||||
beforeEach(() => {
|
||||
axiosMock = {
|
||||
get: vi.fn().mockResolvedValue({
|
||||
data: {
|
||||
announces: [],
|
||||
contacts: [],
|
||||
},
|
||||
}),
|
||||
};
|
||||
window.axios = axiosMock;
|
||||
|
||||
routerMock = {
|
||||
push: vi.fn(),
|
||||
};
|
||||
|
||||
GlobalEmitter.off("sync-propagation-node");
|
||||
GlobalEmitter.off("toggle-orbit");
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
delete window.axios;
|
||||
GlobalEmitter.off("sync-propagation-node");
|
||||
GlobalEmitter.off("toggle-orbit");
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
const mountCommandPalette = () => {
|
||||
return mount(CommandPalette, {
|
||||
global: {
|
||||
mocks: {
|
||||
$t: (key) => key,
|
||||
$router: routerMock,
|
||||
},
|
||||
stubs: {
|
||||
MaterialDesignIcon: { template: '<div class="mdi"></div>' },
|
||||
LxmfUserIcon: { template: '<div class="lxmf-icon"></div>' },
|
||||
},
|
||||
directives: {
|
||||
"click-outside": {},
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
it("renders nothing when closed", () => {
|
||||
const wrapper = mountCommandPalette();
|
||||
expect(wrapper.find(".fixed").exists()).toBe(false);
|
||||
});
|
||||
|
||||
it("opens when Ctrl+K or Cmd+K is pressed", async () => {
|
||||
const wrapper = mountCommandPalette();
|
||||
|
||||
const event = new KeyboardEvent("keydown", {
|
||||
key: "k",
|
||||
ctrlKey: true,
|
||||
});
|
||||
window.dispatchEvent(event);
|
||||
|
||||
await wrapper.vm.$nextTick();
|
||||
expect(wrapper.vm.isOpen).toBe(true);
|
||||
});
|
||||
|
||||
it("opens and closes when toggle is called", async () => {
|
||||
const wrapper = mountCommandPalette();
|
||||
|
||||
await wrapper.vm.toggle();
|
||||
expect(wrapper.vm.isOpen).toBe(true);
|
||||
|
||||
await wrapper.vm.toggle();
|
||||
expect(wrapper.vm.isOpen).toBe(false);
|
||||
});
|
||||
|
||||
it("loads peers and contacts when opened", async () => {
|
||||
axiosMock.get
|
||||
.mockResolvedValueOnce({
|
||||
data: {
|
||||
announces: [
|
||||
{
|
||||
destination_hash: "peer1",
|
||||
display_name: "Peer 1",
|
||||
lxmf_user_icon: { icon_name: "account" },
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
data: [
|
||||
{
|
||||
id: 1,
|
||||
name: "Contact 1",
|
||||
remote_identity_hash: "contact1",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const wrapper = mountCommandPalette();
|
||||
await wrapper.vm.open();
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(axiosMock.get).toHaveBeenCalledWith("/api/v1/announces", {
|
||||
params: { aspect: "lxmf.delivery", limit: 20 },
|
||||
});
|
||||
expect(axiosMock.get).toHaveBeenCalledWith("/api/v1/telephone/contacts");
|
||||
});
|
||||
|
||||
it("filters results based on query", async () => {
|
||||
const wrapper = mountCommandPalette();
|
||||
wrapper.vm.isOpen = true;
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
wrapper.vm.query = "messages";
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
const results = wrapper.vm.filteredResults;
|
||||
expect(results.length).toBeGreaterThan(0);
|
||||
expect(results.some((r) => r.title.toLowerCase().includes("messages"))).toBe(true);
|
||||
});
|
||||
|
||||
it("shows navigation and action items when query is empty", async () => {
|
||||
const wrapper = mountCommandPalette();
|
||||
wrapper.vm.isOpen = true;
|
||||
wrapper.vm.query = "";
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
const results = wrapper.vm.filteredResults;
|
||||
const hasNavigation = results.some((r) => r.type === "navigation");
|
||||
const hasAction = results.some((r) => r.type === "action");
|
||||
expect(hasNavigation || hasAction).toBe(true);
|
||||
});
|
||||
|
||||
it("highlights first result when filtered results change", async () => {
|
||||
const wrapper = mountCommandPalette();
|
||||
wrapper.vm.isOpen = true;
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
const results = wrapper.vm.filteredResults;
|
||||
if (results.length > 0) {
|
||||
expect(wrapper.vm.highlightedId).toBe(results[0].id);
|
||||
}
|
||||
});
|
||||
|
||||
it("moves highlight up and down with arrow keys", async () => {
|
||||
const wrapper = mountCommandPalette();
|
||||
wrapper.vm.isOpen = true;
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
const initialHighlight = wrapper.vm.highlightedId;
|
||||
wrapper.vm.moveHighlight(1);
|
||||
expect(wrapper.vm.highlightedId).not.toBe(initialHighlight);
|
||||
|
||||
wrapper.vm.moveHighlight(-1);
|
||||
expect(wrapper.vm.highlightedId).toBe(initialHighlight);
|
||||
});
|
||||
|
||||
it("wraps highlight when moving past boundaries", async () => {
|
||||
const wrapper = mountCommandPalette();
|
||||
wrapper.vm.isOpen = true;
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
const results = wrapper.vm.filteredResults;
|
||||
if (results.length > 0) {
|
||||
wrapper.vm.highlightedId = results[results.length - 1].id;
|
||||
wrapper.vm.moveHighlight(1);
|
||||
expect(wrapper.vm.highlightedId).toBe(results[0].id);
|
||||
|
||||
wrapper.vm.highlightedId = results[0].id;
|
||||
wrapper.vm.moveHighlight(-1);
|
||||
expect(wrapper.vm.highlightedId).toBe(results[results.length - 1].id);
|
||||
}
|
||||
});
|
||||
|
||||
it("navigates to route when navigation result is executed", async () => {
|
||||
const wrapper = mountCommandPalette();
|
||||
wrapper.vm.isOpen = true;
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
const navResult = wrapper.vm.filteredResults.find((r) => r.type === "navigation");
|
||||
if (navResult) {
|
||||
wrapper.vm.executeResult(navResult);
|
||||
expect(routerMock.push).toHaveBeenCalledWith(navResult.route);
|
||||
expect(wrapper.vm.isOpen).toBe(false);
|
||||
}
|
||||
});
|
||||
|
||||
it("navigates to messages when peer result is executed", async () => {
|
||||
const wrapper = mountCommandPalette();
|
||||
wrapper.vm.isOpen = true;
|
||||
wrapper.vm.peers = [
|
||||
{
|
||||
destination_hash: "peer123",
|
||||
display_name: "Test Peer",
|
||||
},
|
||||
];
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
const peerResult = wrapper.vm.filteredResults.find((r) => r.type === "peer");
|
||||
if (peerResult) {
|
||||
wrapper.vm.executeResult(peerResult);
|
||||
expect(routerMock.push).toHaveBeenCalledWith({
|
||||
name: "messages",
|
||||
params: { destinationHash: "peer123" },
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
it("navigates to call when contact result is executed", async () => {
|
||||
const wrapper = mountCommandPalette();
|
||||
wrapper.vm.isOpen = true;
|
||||
wrapper.vm.contacts = [
|
||||
{
|
||||
id: 1,
|
||||
name: "Test Contact",
|
||||
remote_identity_hash: "contact123",
|
||||
},
|
||||
];
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
const contactResult = wrapper.vm.filteredResults.find((r) => r.type === "contact");
|
||||
if (contactResult) {
|
||||
wrapper.vm.executeResult(contactResult);
|
||||
expect(routerMock.push).toHaveBeenCalledWith({
|
||||
name: "call",
|
||||
query: { destination_hash: "contact123" },
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
it("emits sync event when sync action is executed", async () => {
|
||||
const wrapper = mountCommandPalette();
|
||||
const emitSpy = vi.spyOn(GlobalEmitter, "emit");
|
||||
wrapper.vm.isOpen = true;
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
const syncResult = wrapper.vm.filteredResults.find((r) => r.action === "sync");
|
||||
if (syncResult) {
|
||||
wrapper.vm.executeResult(syncResult);
|
||||
expect(emitSpy).toHaveBeenCalledWith("sync-propagation-node");
|
||||
}
|
||||
});
|
||||
|
||||
it("emits toggle-orbit event when orbit action is executed", async () => {
|
||||
const wrapper = mountCommandPalette();
|
||||
const emitSpy = vi.spyOn(GlobalEmitter, "emit");
|
||||
wrapper.vm.isOpen = true;
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
const orbitResult = wrapper.vm.filteredResults.find((r) => r.action === "toggle-orbit");
|
||||
if (orbitResult) {
|
||||
wrapper.vm.executeResult(orbitResult);
|
||||
expect(emitSpy).toHaveBeenCalledWith("toggle-orbit");
|
||||
}
|
||||
});
|
||||
|
||||
it("closes when ESC key is pressed", async () => {
|
||||
const wrapper = mountCommandPalette();
|
||||
wrapper.vm.isOpen = true;
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
const input = wrapper.find("input");
|
||||
await input.trigger("keydown.esc");
|
||||
|
||||
expect(wrapper.vm.isOpen).toBe(false);
|
||||
});
|
||||
|
||||
it("executes highlighted action when Enter is pressed", async () => {
|
||||
const wrapper = mountCommandPalette();
|
||||
wrapper.vm.isOpen = true;
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
const executeSpy = vi.spyOn(wrapper.vm, "executeAction");
|
||||
const input = wrapper.find("input");
|
||||
await input.trigger("keydown.enter");
|
||||
|
||||
expect(executeSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("groups results by type", async () => {
|
||||
const wrapper = mountCommandPalette();
|
||||
wrapper.vm.isOpen = true;
|
||||
wrapper.vm.peers = [
|
||||
{
|
||||
destination_hash: "peer1",
|
||||
display_name: "Peer 1",
|
||||
},
|
||||
];
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
const grouped = wrapper.vm.groupedResults;
|
||||
expect(Object.keys(grouped).length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("shows no results message when query has no matches", async () => {
|
||||
const wrapper = mountCommandPalette();
|
||||
wrapper.vm.isOpen = true;
|
||||
wrapper.vm.query = "nonexistentquery12345";
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
const results = wrapper.vm.filteredResults;
|
||||
expect(results.length).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
167
tests/frontend/ConfirmDialog.test.js
Normal file
167
tests/frontend/ConfirmDialog.test.js
Normal file
@@ -0,0 +1,167 @@
|
||||
import { mount } from "@vue/test-utils";
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
import ConfirmDialog from "../../meshchatx/src/frontend/components/ConfirmDialog.vue";
|
||||
import GlobalEmitter from "../../meshchatx/src/frontend/js/GlobalEmitter";
|
||||
|
||||
describe("ConfirmDialog.vue", () => {
|
||||
beforeEach(() => {
|
||||
GlobalEmitter.off("confirm");
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
GlobalEmitter.off("confirm");
|
||||
});
|
||||
|
||||
const mountConfirmDialog = () => {
|
||||
return mount(ConfirmDialog, {
|
||||
global: {
|
||||
stubs: {
|
||||
MaterialDesignIcon: { template: '<div class="mdi"></div>' },
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
it("renders nothing when no confirmation is pending", () => {
|
||||
const wrapper = mountConfirmDialog();
|
||||
expect(wrapper.find(".fixed").exists()).toBe(false);
|
||||
});
|
||||
|
||||
it("shows dialog when GlobalEmitter emits confirm event", async () => {
|
||||
const wrapper = mountConfirmDialog();
|
||||
const resolvePromise = vi.fn();
|
||||
|
||||
GlobalEmitter.emit("confirm", {
|
||||
message: "Are you sure?",
|
||||
resolve: resolvePromise,
|
||||
});
|
||||
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(wrapper.find(".fixed").exists()).toBe(true);
|
||||
expect(wrapper.text()).toContain("Are you sure?");
|
||||
expect(wrapper.text()).toContain("Confirm");
|
||||
expect(wrapper.text()).toContain("Cancel");
|
||||
});
|
||||
|
||||
it("calls resolve with true when confirm button is clicked", async () => {
|
||||
const wrapper = mountConfirmDialog();
|
||||
const resolvePromise = vi.fn();
|
||||
|
||||
GlobalEmitter.emit("confirm", {
|
||||
message: "Delete this item?",
|
||||
resolve: resolvePromise,
|
||||
});
|
||||
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
const buttons = wrapper.findAll("button");
|
||||
const confirmButton = buttons.find((btn) => btn.text().includes("Confirm"));
|
||||
await confirmButton.trigger("click");
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(resolvePromise).toHaveBeenCalledWith(true);
|
||||
expect(wrapper.find(".fixed").exists()).toBe(false);
|
||||
});
|
||||
|
||||
it("calls resolve with false when cancel button is clicked", async () => {
|
||||
const wrapper = mountConfirmDialog();
|
||||
const resolvePromise = vi.fn();
|
||||
|
||||
GlobalEmitter.emit("confirm", {
|
||||
message: "Delete this item?",
|
||||
resolve: resolvePromise,
|
||||
});
|
||||
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
const buttons = wrapper.findAll("button");
|
||||
const cancelButton = buttons.find((btn) => btn.text().includes("Cancel"));
|
||||
await cancelButton.trigger("click");
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(resolvePromise).toHaveBeenCalledWith(false);
|
||||
expect(wrapper.find(".fixed").exists()).toBe(false);
|
||||
});
|
||||
|
||||
it("calls resolve with false when clicking outside the dialog", async () => {
|
||||
const wrapper = mountConfirmDialog();
|
||||
const resolvePromise = vi.fn();
|
||||
|
||||
GlobalEmitter.emit("confirm", {
|
||||
message: "Delete this item?",
|
||||
resolve: resolvePromise,
|
||||
});
|
||||
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
const backdrop = wrapper.findAll(".fixed").find((el) => {
|
||||
const classes = el.classes();
|
||||
return classes.includes("inset-0") && !classes.includes("z-[200]");
|
||||
});
|
||||
|
||||
if (backdrop && backdrop.exists()) {
|
||||
await backdrop.trigger("click");
|
||||
await wrapper.vm.$nextTick();
|
||||
expect(resolvePromise).toHaveBeenCalledWith(false);
|
||||
expect(wrapper.find(".fixed").exists()).toBe(false);
|
||||
} else {
|
||||
wrapper.vm.cancel();
|
||||
await wrapper.vm.$nextTick();
|
||||
expect(resolvePromise).toHaveBeenCalledWith(false);
|
||||
}
|
||||
});
|
||||
|
||||
it("handles multiple confirmations sequentially", async () => {
|
||||
const wrapper = mountConfirmDialog();
|
||||
const resolve1 = vi.fn();
|
||||
const resolve2 = vi.fn();
|
||||
|
||||
GlobalEmitter.emit("confirm", {
|
||||
message: "First confirmation",
|
||||
resolve: resolve1,
|
||||
});
|
||||
|
||||
await wrapper.vm.$nextTick();
|
||||
expect(wrapper.text()).toContain("First confirmation");
|
||||
|
||||
const buttons1 = wrapper.findAll("button");
|
||||
const cancelButton1 = buttons1.find((btn) => btn.text().includes("Cancel"));
|
||||
await cancelButton1.trigger("click");
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(resolve1).toHaveBeenCalledWith(false);
|
||||
|
||||
GlobalEmitter.emit("confirm", {
|
||||
message: "Second confirmation",
|
||||
resolve: resolve2,
|
||||
});
|
||||
|
||||
await wrapper.vm.$nextTick();
|
||||
expect(wrapper.text()).toContain("Second confirmation");
|
||||
|
||||
const buttons2 = wrapper.findAll("button");
|
||||
const confirmButton2 = buttons2.find((btn) => btn.text().includes("Confirm"));
|
||||
await confirmButton2.trigger("click");
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(resolve2).toHaveBeenCalledWith(true);
|
||||
});
|
||||
|
||||
it("displays message with whitespace preserved", async () => {
|
||||
const wrapper = mountConfirmDialog();
|
||||
const resolvePromise = vi.fn();
|
||||
|
||||
const message = "Line 1\nLine 2\nLine 3";
|
||||
GlobalEmitter.emit("confirm", {
|
||||
message: message,
|
||||
resolve: resolvePromise,
|
||||
});
|
||||
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
const messageElement = wrapper.find(".whitespace-pre-wrap");
|
||||
expect(messageElement.exists()).toBe(true);
|
||||
expect(messageElement.text()).toContain("Line 1");
|
||||
});
|
||||
});
|
||||
581
tests/frontend/UIComponents.test.js
Normal file
581
tests/frontend/UIComponents.test.js
Normal file
@@ -0,0 +1,581 @@
|
||||
import { mount } from "@vue/test-utils";
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
import IconButton from "../../meshchatx/src/frontend/components/IconButton.vue";
|
||||
import SendMessageButton from "../../meshchatx/src/frontend/components/messages/SendMessageButton.vue";
|
||||
import Toggle from "../../meshchatx/src/frontend/components/forms/Toggle.vue";
|
||||
import FormLabel from "../../meshchatx/src/frontend/components/forms/FormLabel.vue";
|
||||
import FormSubLabel from "../../meshchatx/src/frontend/components/forms/FormSubLabel.vue";
|
||||
import DropDownMenu from "../../meshchatx/src/frontend/components/DropDownMenu.vue";
|
||||
import DropDownMenuItem from "../../meshchatx/src/frontend/components/DropDownMenuItem.vue";
|
||||
import SettingsPage from "../../meshchatx/src/frontend/components/settings/SettingsPage.vue";
|
||||
|
||||
vi.mock("../../meshchatx/src/frontend/js/WebSocketConnection", () => ({
|
||||
default: {
|
||||
on: vi.fn(),
|
||||
off: vi.fn(),
|
||||
emit: vi.fn(),
|
||||
send: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("../../meshchatx/src/frontend/js/ToastUtils", () => ({
|
||||
default: {
|
||||
success: vi.fn(),
|
||||
error: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("../../meshchatx/src/frontend/js/DialogUtils", () => ({
|
||||
default: {
|
||||
confirm: vi.fn().mockResolvedValue(true),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("../../meshchatx/src/frontend/js/KeyboardShortcuts", () => ({
|
||||
default: {
|
||||
getDefaultShortcuts: vi.fn(() => []),
|
||||
send: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("../../meshchatx/src/frontend/js/ElectronUtils", () => ({
|
||||
default: {
|
||||
isElectron: vi.fn(() => false),
|
||||
},
|
||||
}));
|
||||
|
||||
describe("IconButton Component", () => {
|
||||
it("renders with slot content", () => {
|
||||
const wrapper = mount(IconButton, {
|
||||
slots: {
|
||||
default: '<span class="test-content">Click me</span>',
|
||||
},
|
||||
});
|
||||
expect(wrapper.find(".test-content").exists()).toBe(true);
|
||||
expect(wrapper.text()).toContain("Click me");
|
||||
});
|
||||
|
||||
it("has correct button type attribute", () => {
|
||||
const wrapper = mount(IconButton);
|
||||
expect(wrapper.attributes("type")).toBe("button");
|
||||
});
|
||||
|
||||
it("emits click event when clicked", async () => {
|
||||
const wrapper = mount(IconButton);
|
||||
await wrapper.trigger("click");
|
||||
expect(wrapper.emitted("click")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("applies disabled state correctly", () => {
|
||||
const wrapper = mount(IconButton, {
|
||||
attrs: {
|
||||
disabled: true,
|
||||
},
|
||||
});
|
||||
expect(wrapper.attributes("disabled")).toBeDefined();
|
||||
});
|
||||
|
||||
it("applies custom classes", () => {
|
||||
const wrapper = mount(IconButton, {
|
||||
attrs: {
|
||||
class: "custom-class",
|
||||
},
|
||||
});
|
||||
expect(wrapper.classes()).toContain("custom-class");
|
||||
});
|
||||
});
|
||||
|
||||
describe("SendMessageButton Component", () => {
|
||||
it("renders send button with correct text when enabled", () => {
|
||||
const wrapper = mount(SendMessageButton, {
|
||||
props: {
|
||||
canSendMessage: true,
|
||||
isSendingMessage: false,
|
||||
deliveryMethod: null,
|
||||
},
|
||||
});
|
||||
expect(wrapper.text()).toContain("Send");
|
||||
});
|
||||
|
||||
it("shows sending state when isSendingMessage is true", () => {
|
||||
const wrapper = mount(SendMessageButton, {
|
||||
props: {
|
||||
canSendMessage: true,
|
||||
isSendingMessage: true,
|
||||
deliveryMethod: null,
|
||||
},
|
||||
});
|
||||
expect(wrapper.text()).toContain("Sending...");
|
||||
});
|
||||
|
||||
it("disables button when canSendMessage is false", () => {
|
||||
const wrapper = mount(SendMessageButton, {
|
||||
props: {
|
||||
canSendMessage: false,
|
||||
isSendingMessage: false,
|
||||
deliveryMethod: null,
|
||||
},
|
||||
});
|
||||
const button = wrapper.find("button");
|
||||
expect(button.attributes("disabled")).toBeDefined();
|
||||
});
|
||||
|
||||
it("shows delivery method in button text", () => {
|
||||
const wrapper = mount(SendMessageButton, {
|
||||
props: {
|
||||
canSendMessage: true,
|
||||
isSendingMessage: false,
|
||||
deliveryMethod: "direct",
|
||||
},
|
||||
});
|
||||
expect(wrapper.text()).toContain("Send (Direct)");
|
||||
});
|
||||
|
||||
it("emits send event when send button is clicked", async () => {
|
||||
const wrapper = mount(SendMessageButton, {
|
||||
props: {
|
||||
canSendMessage: true,
|
||||
isSendingMessage: false,
|
||||
deliveryMethod: null,
|
||||
},
|
||||
});
|
||||
const sendButton = wrapper.findAll("button")[0];
|
||||
await sendButton.trigger("click");
|
||||
expect(wrapper.emitted("send")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("opens dropdown menu when dropdown button is clicked", async () => {
|
||||
const wrapper = mount(SendMessageButton, {
|
||||
props: {
|
||||
canSendMessage: true,
|
||||
isSendingMessage: false,
|
||||
deliveryMethod: null,
|
||||
},
|
||||
});
|
||||
const dropdownButton = wrapper.findAll("button")[1];
|
||||
await dropdownButton.trigger("click");
|
||||
expect(wrapper.vm.isShowingMenu).toBe(true);
|
||||
});
|
||||
|
||||
it("emits delivery-method-changed when delivery method is selected", async () => {
|
||||
const wrapper = mount(SendMessageButton, {
|
||||
props: {
|
||||
canSendMessage: true,
|
||||
isSendingMessage: false,
|
||||
deliveryMethod: null,
|
||||
},
|
||||
});
|
||||
wrapper.vm.showMenu();
|
||||
await wrapper.vm.$nextTick();
|
||||
wrapper.vm.setDeliveryMethod("direct");
|
||||
expect(wrapper.emitted("delivery-method-changed")).toBeTruthy();
|
||||
expect(wrapper.emitted("delivery-method-changed")[0]).toEqual(["direct"]);
|
||||
});
|
||||
|
||||
it("closes menu after selecting delivery method", async () => {
|
||||
const wrapper = mount(SendMessageButton, {
|
||||
props: {
|
||||
canSendMessage: true,
|
||||
isSendingMessage: false,
|
||||
deliveryMethod: null,
|
||||
},
|
||||
});
|
||||
wrapper.vm.showMenu();
|
||||
expect(wrapper.vm.isShowingMenu).toBe(true);
|
||||
wrapper.vm.setDeliveryMethod("direct");
|
||||
expect(wrapper.vm.isShowingMenu).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Toggle Component", () => {
|
||||
it("renders with label when provided", () => {
|
||||
const wrapper = mount(Toggle, {
|
||||
props: {
|
||||
id: "test-toggle",
|
||||
label: "Enable Feature",
|
||||
},
|
||||
});
|
||||
expect(wrapper.text()).toContain("Enable Feature");
|
||||
});
|
||||
|
||||
it("emits update:modelValue when toggled", async () => {
|
||||
const wrapper = mount(Toggle, {
|
||||
props: {
|
||||
id: "test-toggle",
|
||||
modelValue: false,
|
||||
},
|
||||
});
|
||||
const input = wrapper.find("input");
|
||||
await input.setChecked(true);
|
||||
expect(wrapper.emitted("update:modelValue")).toBeTruthy();
|
||||
expect(wrapper.emitted("update:modelValue")[0]).toEqual([true]);
|
||||
});
|
||||
|
||||
it("reflects modelValue prop correctly", () => {
|
||||
const wrapper = mount(Toggle, {
|
||||
props: {
|
||||
id: "test-toggle",
|
||||
modelValue: true,
|
||||
},
|
||||
});
|
||||
expect(wrapper.find("input").element.checked).toBe(true);
|
||||
});
|
||||
|
||||
it("handles label prop correctly", () => {
|
||||
const wrapper = mount(Toggle, {
|
||||
props: {
|
||||
id: "test-toggle",
|
||||
modelValue: false,
|
||||
label: "Test Label",
|
||||
},
|
||||
});
|
||||
expect(wrapper.text()).toContain("Test Label");
|
||||
});
|
||||
|
||||
it("emits update:modelValue on input change", async () => {
|
||||
const wrapper = mount(Toggle, {
|
||||
props: {
|
||||
id: "test-toggle",
|
||||
modelValue: false,
|
||||
},
|
||||
});
|
||||
const input = wrapper.find("input");
|
||||
await input.trigger("change");
|
||||
expect(wrapper.emitted("update:modelValue")).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe("FormLabel Component", () => {
|
||||
it("renders label text", () => {
|
||||
const wrapper = mount(FormLabel, {
|
||||
props: {
|
||||
for: "test-input",
|
||||
},
|
||||
slots: {
|
||||
default: "Test Label",
|
||||
},
|
||||
});
|
||||
expect(wrapper.text()).toContain("Test Label");
|
||||
});
|
||||
|
||||
it("has correct for attribute", () => {
|
||||
const wrapper = mount(FormLabel, {
|
||||
props: {
|
||||
for: "test-input",
|
||||
},
|
||||
});
|
||||
expect(wrapper.attributes("for")).toBe("test-input");
|
||||
});
|
||||
});
|
||||
|
||||
describe("FormSubLabel Component", () => {
|
||||
it("renders sublabel text", () => {
|
||||
const wrapper = mount(FormSubLabel, {
|
||||
slots: {
|
||||
default: "This is a sublabel",
|
||||
},
|
||||
});
|
||||
expect(wrapper.text()).toContain("This is a sublabel");
|
||||
});
|
||||
});
|
||||
|
||||
describe("DropDownMenu Component", () => {
|
||||
it("toggles menu visibility on button click", async () => {
|
||||
const wrapper = mount(DropDownMenu, {
|
||||
slots: {
|
||||
button: "<button>Menu</button>",
|
||||
items: "<div>Item 1</div>",
|
||||
},
|
||||
});
|
||||
const button = wrapper.find("button");
|
||||
await button.trigger("click");
|
||||
expect(wrapper.vm.isShowingMenu).toBe(true);
|
||||
await button.trigger("click");
|
||||
expect(wrapper.vm.isShowingMenu).toBe(false);
|
||||
});
|
||||
|
||||
it("shows menu items when open", async () => {
|
||||
const wrapper = mount(DropDownMenu, {
|
||||
slots: {
|
||||
button: "<button>Menu</button>",
|
||||
items: '<div class="menu-item">Item 1</div>',
|
||||
},
|
||||
});
|
||||
wrapper.vm.showMenu();
|
||||
await wrapper.vm.$nextTick();
|
||||
expect(wrapper.find(".menu-item").exists()).toBe(true);
|
||||
});
|
||||
|
||||
it("hides menu when clicking outside", async () => {
|
||||
const wrapper = mount(DropDownMenu, {
|
||||
slots: {
|
||||
button: "<button>Menu</button>",
|
||||
items: "<div>Item 1</div>",
|
||||
},
|
||||
});
|
||||
wrapper.vm.showMenu();
|
||||
await wrapper.vm.$nextTick();
|
||||
wrapper.vm.onClickOutsideMenu({ preventDefault: vi.fn() });
|
||||
expect(wrapper.vm.isShowingMenu).toBe(false);
|
||||
});
|
||||
|
||||
it("closes menu when item is clicked", async () => {
|
||||
const wrapper = mount(DropDownMenu, {
|
||||
slots: {
|
||||
button: "<button>Menu</button>",
|
||||
items: '<div @click="hideMenu">Item 1</div>',
|
||||
},
|
||||
});
|
||||
wrapper.vm.showMenu();
|
||||
await wrapper.vm.$nextTick();
|
||||
const menu = wrapper.find(".absolute");
|
||||
await menu.trigger("click");
|
||||
expect(wrapper.vm.isShowingMenu).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("SettingsPage Component", () => {
|
||||
let axiosMock;
|
||||
let websocketMock;
|
||||
|
||||
beforeEach(() => {
|
||||
axiosMock = {
|
||||
get: vi.fn().mockResolvedValue({
|
||||
data: {
|
||||
config: {
|
||||
display_name: "Test User",
|
||||
identity_hash: "abc123",
|
||||
lxmf_address_hash: "def456",
|
||||
theme: "dark",
|
||||
is_transport_enabled: true,
|
||||
lxmf_local_propagation_node_enabled: false,
|
||||
auto_resend_failed_messages_when_announce_received: true,
|
||||
allow_auto_resending_failed_messages_with_attachments: false,
|
||||
auto_send_failed_messages_to_propagation_node: false,
|
||||
show_suggested_community_interfaces: true,
|
||||
lxmf_local_propagation_node_enabled: false,
|
||||
banished_effect_enabled: true,
|
||||
banished_text: "BANISHED",
|
||||
banished_color: "#dc2626",
|
||||
desktop_open_calls_in_separate_window: false,
|
||||
},
|
||||
},
|
||||
}),
|
||||
post: vi.fn().mockResolvedValue({ data: { success: true } }),
|
||||
patch: vi.fn().mockResolvedValue({ data: { success: true } }),
|
||||
};
|
||||
window.axios = axiosMock;
|
||||
|
||||
websocketMock = {
|
||||
on: vi.fn(),
|
||||
off: vi.fn(),
|
||||
emit: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
delete window.axios;
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
const mountSettingsPage = () => {
|
||||
return mount(SettingsPage, {
|
||||
global: {
|
||||
stubs: {
|
||||
MaterialDesignIcon: { template: '<div class="mdi"></div>' },
|
||||
Toggle: Toggle,
|
||||
ShortcutRecorder: { template: "<div></div>" },
|
||||
RouterLink: { template: "<a><slot /></a>" },
|
||||
},
|
||||
mocks: {
|
||||
$t: (key) => key,
|
||||
$router: {
|
||||
push: vi.fn(),
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
it("renders settings page with profile information", async () => {
|
||||
const wrapper = mountSettingsPage();
|
||||
await wrapper.vm.$nextTick();
|
||||
await wrapper.vm.getConfig();
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(wrapper.text()).toContain("Test User");
|
||||
});
|
||||
|
||||
it("renders copy buttons for identity and lxmf address", async () => {
|
||||
const wrapper = mountSettingsPage();
|
||||
await wrapper.vm.$nextTick();
|
||||
await wrapper.vm.getConfig();
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
const buttons = wrapper.findAll("button");
|
||||
const hasCopyButtons = buttons.some(
|
||||
(btn) => btn.text().includes("app.identity_hash") || btn.text().includes("app.lxmf_address")
|
||||
);
|
||||
expect(hasCopyButtons || buttons.length > 0).toBe(true);
|
||||
});
|
||||
|
||||
it("handles toggle changes for banished effect", async () => {
|
||||
const wrapper = mountSettingsPage();
|
||||
await wrapper.vm.$nextTick();
|
||||
await wrapper.vm.getConfig();
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
wrapper.vm.config.banished_effect_enabled = true;
|
||||
await wrapper.vm.$nextTick();
|
||||
wrapper.vm.onBanishedEffectEnabledChange(false);
|
||||
await wrapper.vm.$nextTick();
|
||||
expect(axiosMock.patch).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("updates banished text when input changes", async () => {
|
||||
const wrapper = mountSettingsPage();
|
||||
await wrapper.vm.$nextTick();
|
||||
await wrapper.vm.getConfig();
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
wrapper.vm.config.banished_effect_enabled = true;
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
const textInputs = wrapper.findAll('input[type="text"]');
|
||||
if (textInputs.length > 0) {
|
||||
const textInput =
|
||||
textInputs.find(
|
||||
(input) =>
|
||||
input.attributes("v-model")?.includes("banished_text") ||
|
||||
input.element.value === wrapper.vm.config.banished_text
|
||||
) || textInputs[0];
|
||||
await textInput.setValue("CUSTOM TEXT");
|
||||
await wrapper.vm.$nextTick();
|
||||
expect(wrapper.vm.config.banished_text).toBe("CUSTOM TEXT");
|
||||
}
|
||||
});
|
||||
|
||||
it("updates banished color when color picker changes", async () => {
|
||||
const wrapper = mountSettingsPage();
|
||||
await wrapper.vm.$nextTick();
|
||||
await wrapper.vm.getConfig();
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
wrapper.vm.config.banished_effect_enabled = true;
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
const colorInputs = wrapper.findAll('input[type="color"]');
|
||||
if (colorInputs.length > 0) {
|
||||
const colorInput = colorInputs[0];
|
||||
await colorInput.setValue("#ff0000");
|
||||
await wrapper.vm.$nextTick();
|
||||
expect(wrapper.vm.config.banished_color).toBe("#ff0000");
|
||||
}
|
||||
});
|
||||
|
||||
it("shows banished configuration when toggle is enabled", async () => {
|
||||
const wrapper = mountSettingsPage();
|
||||
await wrapper.vm.$nextTick();
|
||||
await wrapper.vm.getConfig();
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
wrapper.vm.config.banished_effect_enabled = true;
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
const hasBanishedConfig =
|
||||
wrapper.text().includes("app.banished") || wrapper.vm.config.banished_effect_enabled === true;
|
||||
expect(hasBanishedConfig).toBe(true);
|
||||
});
|
||||
|
||||
it("handles RNS reload button click", async () => {
|
||||
const wrapper = mountSettingsPage();
|
||||
await wrapper.vm.$nextTick();
|
||||
await wrapper.vm.getConfig();
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
if (wrapper.vm.reloadRns) {
|
||||
await wrapper.vm.reloadRns();
|
||||
expect(axiosMock.post).toHaveBeenCalledWith("/api/v1/reticulum/reload");
|
||||
}
|
||||
});
|
||||
|
||||
it("displays theme information correctly", async () => {
|
||||
const wrapper = mountSettingsPage();
|
||||
await wrapper.vm.$nextTick();
|
||||
await wrapper.vm.getConfig();
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(wrapper.text()).toContain("app.theme");
|
||||
});
|
||||
|
||||
it("displays transport status correctly", async () => {
|
||||
const wrapper = mountSettingsPage();
|
||||
await wrapper.vm.$nextTick();
|
||||
await wrapper.vm.getConfig();
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(wrapper.text()).toContain("app.transport");
|
||||
});
|
||||
|
||||
it("handles multiple toggle changes without errors", async () => {
|
||||
const wrapper = mountSettingsPage();
|
||||
await wrapper.vm.$nextTick();
|
||||
await wrapper.vm.getConfig();
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
if (!wrapper.vm.config) {
|
||||
wrapper.vm.config = {
|
||||
banished_effect_enabled: false,
|
||||
auto_resend_failed_messages_when_announce_received: false,
|
||||
};
|
||||
}
|
||||
|
||||
const toggles = wrapper.findAllComponents(Toggle);
|
||||
for (const toggle of toggles.slice(0, 2)) {
|
||||
try {
|
||||
await toggle.vm.$emit("update:modelValue", true);
|
||||
await wrapper.vm.$nextTick();
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
expect(axiosMock.patch).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Button Interactions and Accessibility", () => {
|
||||
it("IconButton is keyboard accessible", async () => {
|
||||
const wrapper = mount(IconButton, {
|
||||
attrs: {
|
||||
tabindex: "0",
|
||||
},
|
||||
});
|
||||
expect(wrapper.attributes("tabindex")).toBe("0");
|
||||
});
|
||||
|
||||
it("SendMessageButton respects disabled state for keyboard", () => {
|
||||
const wrapper = mount(SendMessageButton, {
|
||||
props: {
|
||||
canSendMessage: false,
|
||||
isSendingMessage: false,
|
||||
deliveryMethod: null,
|
||||
},
|
||||
});
|
||||
const buttons = wrapper.findAll("button");
|
||||
buttons.forEach((button) => {
|
||||
expect(button.attributes("disabled")).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
it("Toggle is keyboard accessible", async () => {
|
||||
const wrapper = mount(Toggle, {
|
||||
props: {
|
||||
id: "test-toggle",
|
||||
modelValue: false,
|
||||
},
|
||||
});
|
||||
const input = wrapper.find("input");
|
||||
expect(input.attributes("id")).toBe("test-toggle");
|
||||
await input.trigger("change");
|
||||
expect(wrapper.emitted("update:modelValue")).toBeTruthy();
|
||||
});
|
||||
});
|
||||
615
tests/frontend/UIThemeAndVisibility.test.js
Normal file
615
tests/frontend/UIThemeAndVisibility.test.js
Normal file
@@ -0,0 +1,615 @@
|
||||
import { mount } from "@vue/test-utils";
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
import App from "../../meshchatx/src/frontend/components/App.vue";
|
||||
import SettingsPage from "../../meshchatx/src/frontend/components/settings/SettingsPage.vue";
|
||||
import Toggle from "../../meshchatx/src/frontend/components/forms/Toggle.vue";
|
||||
import ConfirmDialog from "../../meshchatx/src/frontend/components/ConfirmDialog.vue";
|
||||
import ChangelogModal from "../../meshchatx/src/frontend/components/ChangelogModal.vue";
|
||||
import NotificationBell from "../../meshchatx/src/frontend/components/NotificationBell.vue";
|
||||
import LanguageSelector from "../../meshchatx/src/frontend/components/LanguageSelector.vue";
|
||||
|
||||
vi.mock("../../meshchatx/src/frontend/js/WebSocketConnection", () => ({
|
||||
default: {
|
||||
on: vi.fn(),
|
||||
off: vi.fn(),
|
||||
emit: vi.fn(),
|
||||
send: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("../../meshchatx/src/frontend/js/ToastUtils", () => ({
|
||||
default: {
|
||||
success: vi.fn(),
|
||||
error: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("../../meshchatx/src/frontend/js/GlobalState", () => ({
|
||||
default: {
|
||||
unreadConversationsCount: 0,
|
||||
activeCallTab: null,
|
||||
config: {},
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("../../meshchatx/src/frontend/js/GlobalEmitter", () => ({
|
||||
default: {
|
||||
on: vi.fn(),
|
||||
off: vi.fn(),
|
||||
emit: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("../../meshchatx/src/frontend/js/NotificationUtils", () => ({
|
||||
default: {
|
||||
showIncomingCallNotification: vi.fn(),
|
||||
showMissedCallNotification: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("../../meshchatx/src/frontend/js/KeyboardShortcuts", () => ({
|
||||
default: {
|
||||
getDefaultShortcuts: vi.fn(() => []),
|
||||
setShortcuts: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
const createRouterLinkStub = () => ({
|
||||
template:
|
||||
"<a><slot v-bind=\"{ href: typeof to === 'string' ? to : (to?.path || to?.name || '#'), navigate: () => {}, isActive: false }\" /></a>",
|
||||
props: ["to", "custom"],
|
||||
});
|
||||
|
||||
describe("Theme Switching", () => {
|
||||
let axiosMock;
|
||||
|
||||
beforeEach(() => {
|
||||
document.documentElement.classList.remove("dark");
|
||||
axiosMock = {
|
||||
get: vi.fn().mockResolvedValue({
|
||||
data: {
|
||||
config: {
|
||||
theme: "light",
|
||||
display_name: "Test User",
|
||||
},
|
||||
app_info: { is_reticulum_running: true },
|
||||
},
|
||||
}),
|
||||
post: vi.fn().mockResolvedValue({ data: {} }),
|
||||
};
|
||||
window.axios = axiosMock;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
document.documentElement.classList.remove("dark");
|
||||
delete window.axios;
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("applies dark class to root element when theme is dark", async () => {
|
||||
document.documentElement.classList.remove("dark");
|
||||
expect(document.documentElement.classList.contains("dark")).toBe(false);
|
||||
|
||||
const wrapper = mount(App, {
|
||||
global: {
|
||||
stubs: {
|
||||
RouterView: { template: "<div>Router View</div>" },
|
||||
RouterLink: createRouterLinkStub(),
|
||||
MaterialDesignIcon: { template: "<div></div>" },
|
||||
LanguageSelector: { template: "<div></div>" },
|
||||
NotificationBell: { template: "<div></div>" },
|
||||
SidebarLink: {
|
||||
template: '<div><slot name="icon"></slot><slot name="text"></slot></div>',
|
||||
props: ["to", "isCollapsed"],
|
||||
},
|
||||
},
|
||||
mocks: {
|
||||
$route: { name: "messages", meta: {}, query: {} },
|
||||
$router: { push: vi.fn() },
|
||||
$t: (key) => key,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
wrapper.vm.config = { theme: "dark" };
|
||||
document.documentElement.classList.add("dark");
|
||||
|
||||
expect(document.documentElement.classList.contains("dark")).toBe(true);
|
||||
});
|
||||
|
||||
it("removes dark class when theme is light", async () => {
|
||||
document.documentElement.classList.add("dark");
|
||||
|
||||
const wrapper = mount(App, {
|
||||
global: {
|
||||
stubs: {
|
||||
RouterView: { template: "<div>Router View</div>" },
|
||||
RouterLink: createRouterLinkStub(),
|
||||
MaterialDesignIcon: { template: "<div></div>" },
|
||||
LanguageSelector: { template: "<div></div>" },
|
||||
NotificationBell: { template: "<div></div>" },
|
||||
SidebarLink: {
|
||||
template: '<div><slot name="icon"></slot><slot name="text"></slot></div>',
|
||||
props: ["to", "isCollapsed"],
|
||||
},
|
||||
},
|
||||
mocks: {
|
||||
$route: { name: "messages", meta: {}, query: {} },
|
||||
$router: { push: vi.fn() },
|
||||
$t: (key) => key,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
wrapper.vm.config = { theme: "light" };
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(document.documentElement.classList.contains("dark")).toBe(false);
|
||||
});
|
||||
|
||||
it("toggles theme from light to dark", async () => {
|
||||
const WebSocketConnection = await import("../../meshchatx/src/frontend/js/WebSocketConnection");
|
||||
|
||||
const wrapper = mount(App, {
|
||||
global: {
|
||||
stubs: {
|
||||
RouterView: { template: "<div>Router View</div>" },
|
||||
RouterLink: createRouterLinkStub(),
|
||||
MaterialDesignIcon: { template: "<div></div>" },
|
||||
LanguageSelector: { template: "<div></div>" },
|
||||
NotificationBell: { template: "<div></div>" },
|
||||
SidebarLink: {
|
||||
template: '<div><slot name="icon"></slot><slot name="text"></slot></div>',
|
||||
props: ["to", "isCollapsed"],
|
||||
},
|
||||
},
|
||||
mocks: {
|
||||
$route: { name: "messages", meta: {}, query: {} },
|
||||
$router: { push: vi.fn() },
|
||||
$t: (key) => key,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
wrapper.vm.config = { theme: "light" };
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
await wrapper.vm.toggleTheme();
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(WebSocketConnection.default.send).toHaveBeenCalled();
|
||||
const sendCalls = WebSocketConnection.default.send.mock.calls;
|
||||
const configSetCall = sendCalls.find((call) => {
|
||||
try {
|
||||
const parsed = JSON.parse(call[0]);
|
||||
return parsed.type === "config.set";
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
expect(configSetCall).toBeDefined();
|
||||
if (configSetCall) {
|
||||
const callArgs = JSON.parse(configSetCall[0]);
|
||||
expect(callArgs.config.theme).toBe("dark");
|
||||
}
|
||||
});
|
||||
|
||||
it("toggles theme from dark to light", async () => {
|
||||
const wrapper = mount(App, {
|
||||
global: {
|
||||
stubs: {
|
||||
RouterView: { template: "<div>Router View</div>" },
|
||||
RouterLink: createRouterLinkStub(),
|
||||
MaterialDesignIcon: { template: "<div></div>" },
|
||||
LanguageSelector: { template: "<div></div>" },
|
||||
NotificationBell: { template: "<div></div>" },
|
||||
SidebarLink: {
|
||||
template: '<div><slot name="icon"></slot><slot name="text"></slot></div>',
|
||||
props: ["to", "isCollapsed"],
|
||||
},
|
||||
},
|
||||
mocks: {
|
||||
$route: { name: "messages", meta: {}, query: {} },
|
||||
$router: { push: vi.fn() },
|
||||
$t: (key) => key,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
wrapper.vm.config = { theme: "dark" };
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
await wrapper.vm.toggleTheme();
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(wrapper.vm.config.theme).toBe("light");
|
||||
});
|
||||
|
||||
it("shows correct icon for theme toggle button", async () => {
|
||||
const wrapper = mount(App, {
|
||||
global: {
|
||||
stubs: {
|
||||
RouterView: { template: "<div>Router View</div>" },
|
||||
RouterLink: createRouterLinkStub(),
|
||||
MaterialDesignIcon: {
|
||||
template: '<div :data-icon="iconName"></div>',
|
||||
props: ["iconName"],
|
||||
},
|
||||
LanguageSelector: { template: "<div></div>" },
|
||||
NotificationBell: { template: "<div></div>" },
|
||||
SidebarLink: {
|
||||
template: '<div><slot name="icon"></slot><slot name="text"></slot></div>',
|
||||
props: ["to", "isCollapsed"],
|
||||
},
|
||||
},
|
||||
mocks: {
|
||||
$route: { name: "messages", meta: {}, query: {} },
|
||||
$router: { push: vi.fn() },
|
||||
$t: (key) => key,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
wrapper.vm.config = { theme: "dark" };
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
const buttons = wrapper.findAll("button");
|
||||
expect(buttons.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Visibility Checks", () => {
|
||||
it("ConfirmDialog shows when pendingConfirm is set", async () => {
|
||||
const wrapper = mount(ConfirmDialog, {
|
||||
global: {
|
||||
stubs: {
|
||||
MaterialDesignIcon: { template: "<div></div>" },
|
||||
},
|
||||
mocks: {
|
||||
$t: (key) => key,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
wrapper.vm.pendingConfirm = { message: "Test message" };
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
const dialogElement = wrapper.find(".fixed");
|
||||
expect(dialogElement.exists()).toBe(true);
|
||||
expect(wrapper.text()).toContain("Confirm");
|
||||
});
|
||||
|
||||
it("ConfirmDialog hides when pendingConfirm is null", async () => {
|
||||
const wrapper = mount(ConfirmDialog, {
|
||||
global: {
|
||||
stubs: {
|
||||
MaterialDesignIcon: { template: "<div></div>" },
|
||||
},
|
||||
mocks: {
|
||||
$t: (key) => key,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
wrapper.vm.pendingConfirm = null;
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
const dialogElement = wrapper.find(".fixed");
|
||||
expect(dialogElement.exists()).toBe(false);
|
||||
});
|
||||
|
||||
it("ChangelogModal component renders correctly", () => {
|
||||
const wrapper = mount(ChangelogModal, {
|
||||
global: {
|
||||
stubs: {
|
||||
MaterialDesignIcon: { template: "<div></div>" },
|
||||
},
|
||||
mocks: {
|
||||
$t: (key) => key,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(wrapper.exists()).toBe(true);
|
||||
});
|
||||
|
||||
it("Toggle shows label when provided", () => {
|
||||
const wrapper = mount(Toggle, {
|
||||
props: {
|
||||
id: "test-toggle",
|
||||
label: "Show Label",
|
||||
modelValue: false,
|
||||
},
|
||||
});
|
||||
|
||||
expect(wrapper.text()).toContain("Show Label");
|
||||
});
|
||||
|
||||
it("Toggle hides label when not provided", () => {
|
||||
const wrapper = mount(Toggle, {
|
||||
props: {
|
||||
id: "test-toggle",
|
||||
modelValue: false,
|
||||
},
|
||||
});
|
||||
|
||||
expect(wrapper.text()).not.toContain("Show Label");
|
||||
});
|
||||
|
||||
it("SettingsPage shows banished config when toggle is enabled", async () => {
|
||||
const axiosMock = {
|
||||
get: vi.fn().mockResolvedValue({
|
||||
data: {
|
||||
config: {
|
||||
banished_effect_enabled: true,
|
||||
banished_text: "BANISHED",
|
||||
banished_color: "#dc2626",
|
||||
},
|
||||
},
|
||||
}),
|
||||
patch: vi.fn().mockResolvedValue({ data: {} }),
|
||||
};
|
||||
window.axios = axiosMock;
|
||||
|
||||
const wrapper = mount(SettingsPage, {
|
||||
global: {
|
||||
stubs: {
|
||||
MaterialDesignIcon: { template: "<div></div>" },
|
||||
Toggle: Toggle,
|
||||
ShortcutRecorder: { template: "<div></div>" },
|
||||
RouterLink: { template: "<a><slot /></a>" },
|
||||
},
|
||||
mocks: {
|
||||
$t: (key) => key,
|
||||
$router: { push: vi.fn() },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await wrapper.vm.$nextTick();
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
wrapper.vm.config.banished_effect_enabled = true;
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
const hasBanishedConfig =
|
||||
wrapper.text().includes("app.banished") || wrapper.findAll('input[type="text"]').length > 0;
|
||||
expect(hasBanishedConfig).toBe(true);
|
||||
|
||||
delete window.axios;
|
||||
});
|
||||
|
||||
it("SettingsPage hides banished config when toggle is disabled", async () => {
|
||||
const axiosMock = {
|
||||
get: vi.fn().mockResolvedValue({
|
||||
data: {
|
||||
config: {
|
||||
banished_effect_enabled: false,
|
||||
},
|
||||
},
|
||||
}),
|
||||
patch: vi.fn().mockResolvedValue({ data: {} }),
|
||||
};
|
||||
window.axios = axiosMock;
|
||||
|
||||
const wrapper = mount(SettingsPage, {
|
||||
global: {
|
||||
stubs: {
|
||||
MaterialDesignIcon: { template: "<div></div>" },
|
||||
Toggle: Toggle,
|
||||
ShortcutRecorder: { template: "<div></div>" },
|
||||
RouterLink: { template: "<a><slot /></a>" },
|
||||
},
|
||||
mocks: {
|
||||
$t: (key) => key,
|
||||
$router: { push: vi.fn() },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await wrapper.vm.$nextTick();
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
wrapper.vm.config.banished_effect_enabled = false;
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
const colorInputs = wrapper.findAll('input[type="color"]');
|
||||
expect(colorInputs.length).toBe(0);
|
||||
|
||||
delete window.axios;
|
||||
});
|
||||
});
|
||||
|
||||
describe("Conditional Rendering", () => {
|
||||
it("App shows emergency banner when emergency mode is active", async () => {
|
||||
const wrapper = mount(App, {
|
||||
global: {
|
||||
stubs: {
|
||||
RouterView: { template: "<div>Router View</div>" },
|
||||
RouterLink: createRouterLinkStub(),
|
||||
MaterialDesignIcon: { template: "<div></div>" },
|
||||
LanguageSelector: { template: "<div></div>" },
|
||||
NotificationBell: { template: "<div></div>" },
|
||||
SidebarLink: {
|
||||
template: '<div><slot name="icon"></slot><slot name="text"></slot></div>',
|
||||
props: ["to", "isCollapsed"],
|
||||
},
|
||||
},
|
||||
mocks: {
|
||||
$route: { name: "messages", meta: {}, query: {} },
|
||||
$router: { push: vi.fn() },
|
||||
$t: (key) => key,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
wrapper.vm.appInfo = { emergency: true };
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(wrapper.text()).toContain("app.emergency_mode_active");
|
||||
});
|
||||
|
||||
it("App hides emergency banner when emergency mode is inactive", async () => {
|
||||
const wrapper = mount(App, {
|
||||
global: {
|
||||
stubs: {
|
||||
RouterView: { template: "<div>Router View</div>" },
|
||||
RouterLink: createRouterLinkStub(),
|
||||
MaterialDesignIcon: { template: "<div></div>" },
|
||||
LanguageSelector: { template: "<div></div>" },
|
||||
NotificationBell: { template: "<div></div>" },
|
||||
SidebarLink: {
|
||||
template: '<div><slot name="icon"></slot><slot name="text"></slot></div>',
|
||||
props: ["to", "isCollapsed"],
|
||||
},
|
||||
},
|
||||
mocks: {
|
||||
$route: { name: "messages", meta: {}, query: {} },
|
||||
$router: { push: vi.fn() },
|
||||
$t: (key) => key,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
wrapper.vm.appInfo = { emergency: false };
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(wrapper.text()).not.toContain("app.emergency_mode_active");
|
||||
});
|
||||
|
||||
it("App shows sidebar toggle on mobile", async () => {
|
||||
const wrapper = mount(App, {
|
||||
global: {
|
||||
stubs: {
|
||||
RouterView: { template: "<div>Router View</div>" },
|
||||
RouterLink: createRouterLinkStub(),
|
||||
MaterialDesignIcon: { template: "<div></div>" },
|
||||
LanguageSelector: { template: "<div></div>" },
|
||||
NotificationBell: { template: "<div></div>" },
|
||||
SidebarLink: {
|
||||
template: '<div><slot name="icon"></slot><slot name="text"></slot></div>',
|
||||
props: ["to", "isCollapsed"],
|
||||
},
|
||||
},
|
||||
mocks: {
|
||||
$route: { name: "messages", meta: {}, query: {} },
|
||||
$router: { push: vi.fn() },
|
||||
$t: (key) => key,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const sidebarButton = wrapper.find("button.sm\\:hidden");
|
||||
expect(sidebarButton.exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Dark Mode Class Application", () => {
|
||||
it("App component applies dark class based on theme", async () => {
|
||||
const wrapper = mount(App, {
|
||||
global: {
|
||||
stubs: {
|
||||
RouterView: { template: "<div>Router View</div>" },
|
||||
RouterLink: createRouterLinkStub(),
|
||||
MaterialDesignIcon: { template: "<div></div>" },
|
||||
LanguageSelector: { template: "<div></div>" },
|
||||
NotificationBell: { template: "<div></div>" },
|
||||
SidebarLink: {
|
||||
template: '<div><slot name="icon"></slot><slot name="text"></slot></div>',
|
||||
props: ["to", "isCollapsed"],
|
||||
},
|
||||
},
|
||||
mocks: {
|
||||
$route: { name: "messages", meta: {}, query: {} },
|
||||
$router: { push: vi.fn() },
|
||||
$t: (key) => key,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
wrapper.vm.config = { theme: "dark" };
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(wrapper.classes()).toContain("dark");
|
||||
});
|
||||
|
||||
it("SettingsPage applies dark mode classes correctly", async () => {
|
||||
const axiosMock = {
|
||||
get: vi.fn().mockResolvedValue({
|
||||
data: {
|
||||
config: {
|
||||
theme: "dark",
|
||||
},
|
||||
},
|
||||
}),
|
||||
patch: vi.fn().mockResolvedValue({ data: {} }),
|
||||
};
|
||||
window.axios = axiosMock;
|
||||
|
||||
const wrapper = mount(SettingsPage, {
|
||||
global: {
|
||||
stubs: {
|
||||
MaterialDesignIcon: { template: "<div></div>" },
|
||||
Toggle: Toggle,
|
||||
ShortcutRecorder: { template: "<div></div>" },
|
||||
RouterLink: { template: "<a><slot /></a>" },
|
||||
},
|
||||
mocks: {
|
||||
$t: (key) => key,
|
||||
$router: { push: vi.fn() },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await wrapper.vm.$nextTick();
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
const hasDarkClasses = wrapper.html().includes("dark:") || wrapper.html().includes("dark:");
|
||||
expect(hasDarkClasses).toBe(true);
|
||||
|
||||
delete window.axios;
|
||||
});
|
||||
});
|
||||
|
||||
describe("Theme Persistence", () => {
|
||||
it("SettingsPage theme selector updates config", async () => {
|
||||
const axiosMock = {
|
||||
get: vi.fn().mockResolvedValue({
|
||||
data: {
|
||||
config: {
|
||||
theme: "light",
|
||||
},
|
||||
},
|
||||
}),
|
||||
patch: vi.fn().mockResolvedValue({ data: {} }),
|
||||
};
|
||||
window.axios = axiosMock;
|
||||
|
||||
const wrapper = mount(SettingsPage, {
|
||||
global: {
|
||||
stubs: {
|
||||
MaterialDesignIcon: { template: "<div></div>" },
|
||||
Toggle: Toggle,
|
||||
ShortcutRecorder: { template: "<div></div>" },
|
||||
RouterLink: { template: "<a><slot /></a>" },
|
||||
},
|
||||
mocks: {
|
||||
$t: (key) => key,
|
||||
$router: { push: vi.fn() },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await wrapper.vm.$nextTick();
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
const themeSelect = wrapper.find('select[v-model="config.theme"]');
|
||||
if (themeSelect.exists()) {
|
||||
await themeSelect.setValue("dark");
|
||||
await wrapper.vm.$nextTick();
|
||||
expect(wrapper.vm.config.theme).toBe("dark");
|
||||
}
|
||||
|
||||
delete window.axios;
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user