mirror of
https://git.quad4.io/RNS-Things/MeshChatX.git
synced 2026-04-02 10:05:43 +00:00
Add tests for ContactsPage and new ConversationViewer and NomadNetworkSidebar components
This commit is contained in:
@@ -41,6 +41,9 @@ describe("ContactsPage.vue", () => {
|
||||
});
|
||||
}
|
||||
if (url === "/api/v1/telephone/contacts") {
|
||||
return Promise.resolve({ data: { contacts: [], total_count: 0 } });
|
||||
}
|
||||
if (url === "/api/v1/telephone/contacts/export") {
|
||||
return Promise.resolve({ data: { contacts: [] } });
|
||||
}
|
||||
if (url.startsWith("/api/v1/telephone/contacts/check/")) {
|
||||
@@ -99,4 +102,47 @@ describe("ContactsPage.vue", () => {
|
||||
);
|
||||
expect(axiosMock.post).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("exports contacts via GET /api/v1/telephone/contacts/export", async () => {
|
||||
const wrapper = mountPage();
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
await wrapper.vm.exportContacts();
|
||||
|
||||
expect(axiosMock.get).toHaveBeenCalledWith("/api/v1/telephone/contacts/export");
|
||||
});
|
||||
|
||||
it("imports contacts via POST /api/v1/telephone/contacts/import", async () => {
|
||||
const wrapper = mountPage();
|
||||
await wrapper.vm.$nextTick();
|
||||
axiosMock.post.mockResolvedValue({ data: { added: 2, skipped: 0 } });
|
||||
|
||||
await wrapper.vm.importContacts([
|
||||
{ name: "A", remote_identity_hash: "a".repeat(32) },
|
||||
{ name: "B", remote_identity_hash: "b".repeat(32) },
|
||||
]);
|
||||
|
||||
expect(axiosMock.post).toHaveBeenCalledWith("/api/v1/telephone/contacts/import", {
|
||||
contacts: [
|
||||
{ name: "A", remote_identity_hash: "a".repeat(32) },
|
||||
{ name: "B", remote_identity_hash: "b".repeat(32) },
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it("mounts within 500ms", () => {
|
||||
const start = performance.now();
|
||||
const wrapper = mountPage();
|
||||
const elapsed = performance.now() - start;
|
||||
expect(wrapper.find("h1").exists()).toBe(true);
|
||||
expect(elapsed).toBeLessThan(500);
|
||||
});
|
||||
|
||||
it("export and import buttons are present", async () => {
|
||||
const wrapper = mountPage();
|
||||
await wrapper.vm.$nextTick();
|
||||
const html = wrapper.html();
|
||||
expect(html).toContain("contacts.export_contacts");
|
||||
expect(html).toContain("contacts.import_contacts");
|
||||
});
|
||||
});
|
||||
|
||||
217
tests/frontend/ConversationViewerButtons.test.js
Normal file
217
tests/frontend/ConversationViewerButtons.test.js
Normal file
@@ -0,0 +1,217 @@
|
||||
import { mount } from "@vue/test-utils";
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
import ConversationViewer from "@/components/messages/ConversationViewer.vue";
|
||||
import WebSocketConnection from "@/js/WebSocketConnection";
|
||||
import DialogUtils from "@/js/DialogUtils";
|
||||
import GlobalEmitter from "@/js/GlobalEmitter";
|
||||
import GlobalState from "@/js/GlobalState";
|
||||
|
||||
const RENDER_THRESHOLD_MS = 500;
|
||||
|
||||
vi.mock("@/js/DialogUtils", () => ({
|
||||
default: {
|
||||
confirm: vi.fn(() => Promise.resolve(true)),
|
||||
alert: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
describe("ConversationViewer.vue button interactions", () => {
|
||||
let axiosMock;
|
||||
|
||||
beforeEach(() => {
|
||||
WebSocketConnection.connect();
|
||||
vi.clearAllMocks();
|
||||
axiosMock = {
|
||||
get: vi.fn().mockImplementation((url) => {
|
||||
if (url.includes("/path")) return Promise.resolve({ data: { path: [] } });
|
||||
if (url.includes("/stamp-info")) return Promise.resolve({ data: { stamp_info: {} } });
|
||||
if (url.includes("/signal-metrics")) return Promise.resolve({ data: { signal_metrics: {} } });
|
||||
return Promise.resolve({ data: {} });
|
||||
}),
|
||||
post: vi.fn().mockResolvedValue({ data: {} }),
|
||||
delete: vi.fn().mockResolvedValue({ data: {} }),
|
||||
};
|
||||
window.axios = axiosMock;
|
||||
|
||||
GlobalState.blockedDestinations = [];
|
||||
GlobalState.config = { banished_effect_enabled: false };
|
||||
|
||||
vi.stubGlobal("localStorage", {
|
||||
getItem: vi.fn(),
|
||||
setItem: vi.fn(),
|
||||
removeItem: vi.fn(),
|
||||
});
|
||||
window.URL.createObjectURL = vi.fn(() => "mock-url");
|
||||
vi.stubGlobal("FileReader", vi.fn(() => ({
|
||||
readAsDataURL: vi.fn(function () {
|
||||
this.result = "data:image/png;base64,mock";
|
||||
this.onload?.({ target: { result: this.result } });
|
||||
}),
|
||||
})));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
delete window.axios;
|
||||
vi.unstubAllGlobals();
|
||||
WebSocketConnection.destroy();
|
||||
});
|
||||
|
||||
const mountViewer = (overrides = {}) =>
|
||||
mount(ConversationViewer, {
|
||||
props: {
|
||||
selectedPeer: { destination_hash: "a".repeat(32), display_name: "Test Peer" },
|
||||
myLxmfAddressHash: "b".repeat(32),
|
||||
conversations: [],
|
||||
...overrides,
|
||||
},
|
||||
global: {
|
||||
mocks: { $t: (key) => key },
|
||||
stubs: {
|
||||
MaterialDesignIcon: true,
|
||||
AddImageButton: true,
|
||||
AddAudioButton: true,
|
||||
SendMessageButton: true,
|
||||
ConversationDropDownMenu: true,
|
||||
PaperMessageModal: true,
|
||||
AudioWaveformPlayer: true,
|
||||
LxmfUserIcon: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
it("mounts within render threshold", () => {
|
||||
const start = performance.now();
|
||||
const wrapper = mountViewer();
|
||||
const elapsed = performance.now() - start;
|
||||
expect(wrapper.find(".flex").exists()).toBe(true);
|
||||
expect(elapsed).toBeLessThan(RENDER_THRESHOLD_MS);
|
||||
});
|
||||
|
||||
it("banish button calls API when confirmed", async () => {
|
||||
const emitSpy = vi.spyOn(GlobalEmitter, "emit");
|
||||
const wrapper = mountViewer();
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
await wrapper.vm.onBanishHeaderClick();
|
||||
|
||||
expect(DialogUtils.confirm).toHaveBeenCalled();
|
||||
expect(axiosMock.post).toHaveBeenCalledWith(
|
||||
"/api/v1/blocked-destinations",
|
||||
expect.objectContaining({ destination_hash: "a".repeat(32) })
|
||||
);
|
||||
expect(emitSpy).toHaveBeenCalledWith("block-status-changed");
|
||||
emitSpy.mockRestore();
|
||||
});
|
||||
|
||||
it("banish button hidden when peer is blocked", async () => {
|
||||
GlobalState.blockedDestinations = [{ destination_hash: "a".repeat(32) }];
|
||||
const wrapper = mountViewer();
|
||||
await wrapper.vm.$nextTick();
|
||||
wrapper.vm.checkIfSelectedPeerBlocked();
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
const banishBtn = wrapper.findAll("button").find((b) => b.attributes("title")?.includes("banish"));
|
||||
expect(banishBtn).toBeUndefined();
|
||||
});
|
||||
|
||||
it("telemetry history button opens modal", async () => {
|
||||
const wrapper = mountViewer();
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
const telemetryBtn = wrapper.findAll("button").find((b) => b.attributes("title") === "View Telemetry History");
|
||||
expect(telemetryBtn).toBeDefined();
|
||||
|
||||
await telemetryBtn.trigger("click");
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(wrapper.vm.isTelemetryHistoryModalOpen).toBe(true);
|
||||
});
|
||||
|
||||
it("close button emits close", async () => {
|
||||
const wrapper = mountViewer();
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
const closeBtn = wrapper.findAll("button").find((b) => b.attributes("title") === "Close");
|
||||
expect(closeBtn).toBeDefined();
|
||||
|
||||
await closeBtn.trigger("click");
|
||||
|
||||
expect(wrapper.emitted("close")).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("onMessageContextMenu opens menu and Reply works", async () => {
|
||||
const wrapper = mountViewer();
|
||||
const chatItem = {
|
||||
type: "lxmf_message",
|
||||
is_outbound: false,
|
||||
lxmf_message: {
|
||||
hash: "msg-1",
|
||||
content: "Hello",
|
||||
state: "delivered",
|
||||
fields: {},
|
||||
},
|
||||
};
|
||||
wrapper.vm.chatItems = [chatItem];
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
const replySpy = vi.spyOn(wrapper.vm, "replyToMessage");
|
||||
|
||||
wrapper.vm.onMessageContextMenu({ clientX: 100, clientY: 100 }, chatItem);
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(wrapper.vm.messageContextMenu.show).toBe(true);
|
||||
|
||||
const menuEl = Array.from(document.body.querySelectorAll(".fixed")).find(
|
||||
(el) => el.textContent?.includes("Reply") && el.textContent?.includes("Delete")
|
||||
);
|
||||
expect(menuEl).toBeTruthy();
|
||||
|
||||
const replyBtn = menuEl?.querySelector("button");
|
||||
expect(replyBtn?.textContent).toContain("Reply");
|
||||
|
||||
replyBtn?.click();
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(replySpy).toHaveBeenCalledWith(chatItem);
|
||||
});
|
||||
|
||||
it("message context menu Delete calls deleteChatItem", async () => {
|
||||
const wrapper = mountViewer();
|
||||
const chatItem = {
|
||||
type: "lxmf_message",
|
||||
is_outbound: false,
|
||||
lxmf_message: { hash: "msg-del", content: "Hi", state: "delivered", fields: {} },
|
||||
};
|
||||
wrapper.vm.chatItems = [chatItem];
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
const deleteSpy = vi.spyOn(wrapper.vm, "deleteChatItem");
|
||||
|
||||
wrapper.vm.messageContextMenu.chatItem = chatItem;
|
||||
wrapper.vm.messageContextMenu.show = true;
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
const menuEl = Array.from(document.body.querySelectorAll(".fixed")).find(
|
||||
(el) => el.textContent?.includes("Reply") && el.textContent?.includes("Delete")
|
||||
);
|
||||
const deleteBtn = menuEl ? Array.from(menuEl.querySelectorAll("button")).find((b) => b.textContent.includes("Delete")) : null;
|
||||
expect(deleteBtn).toBeTruthy();
|
||||
|
||||
deleteBtn?.click();
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(deleteSpy).toHaveBeenCalledWith(chatItem);
|
||||
});
|
||||
|
||||
it("call button exists and onStartCall is callable", async () => {
|
||||
const wrapper = mountViewer();
|
||||
expect(typeof wrapper.vm.onStartCall).toBe("function");
|
||||
await wrapper.vm.onStartCall();
|
||||
});
|
||||
|
||||
it("share contact button exists and openShareContactModal is callable", async () => {
|
||||
const wrapper = mountViewer();
|
||||
expect(typeof wrapper.vm.openShareContactModal).toBe("function");
|
||||
wrapper.vm.openShareContactModal();
|
||||
});
|
||||
});
|
||||
@@ -109,6 +109,14 @@ describe("NetworkVisualiser.vue", () => {
|
||||
}
|
||||
return Promise.resolve({ data: {} });
|
||||
}),
|
||||
post: vi.fn().mockImplementation((url) => {
|
||||
if (url.includes("/api/v1/path-table")) {
|
||||
return Promise.resolve({
|
||||
data: { path_table: [{ hash: "node1", interface: "eth0", hops: 1 }], total_count: 1 },
|
||||
});
|
||||
}
|
||||
return Promise.resolve({ data: {} });
|
||||
}),
|
||||
};
|
||||
window.axios = axiosMock;
|
||||
|
||||
|
||||
234
tests/frontend/NomadNetworkSidebar.test.js
Normal file
234
tests/frontend/NomadNetworkSidebar.test.js
Normal file
@@ -0,0 +1,234 @@
|
||||
import { mount } from "@vue/test-utils";
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
import NomadNetworkSidebar from "@/components/nomadnetwork/NomadNetworkSidebar.vue";
|
||||
import DialogUtils from "@/js/DialogUtils";
|
||||
import GlobalState from "@/js/GlobalState";
|
||||
import GlobalEmitter from "@/js/GlobalEmitter";
|
||||
|
||||
vi.mock("@/js/DialogUtils", () => ({
|
||||
default: {
|
||||
confirm: vi.fn(() => Promise.resolve(true)),
|
||||
alert: vi.fn(),
|
||||
prompt: vi.fn((msg, def) => Promise.resolve(def || "renamed")),
|
||||
},
|
||||
}));
|
||||
|
||||
describe("NomadNetworkSidebar.vue", () => {
|
||||
let axiosMock;
|
||||
|
||||
const defaultFavourite = {
|
||||
destination_hash: "a".repeat(32),
|
||||
display_name: "Test Favourite",
|
||||
};
|
||||
const defaultNode = {
|
||||
destination_hash: "b".repeat(32),
|
||||
identity_hash: "c".repeat(32),
|
||||
display_name: "Test Node",
|
||||
updated_at: new Date().toISOString(),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
axiosMock = {
|
||||
get: vi.fn().mockResolvedValue({ data: {} }),
|
||||
post: vi.fn().mockResolvedValue({ data: {} }),
|
||||
delete: vi.fn().mockResolvedValue({ data: {} }),
|
||||
};
|
||||
window.axios = axiosMock;
|
||||
|
||||
GlobalState.blockedDestinations = [];
|
||||
GlobalState.config = { banished_effect_enabled: false };
|
||||
|
||||
vi.stubGlobal("localStorage", {
|
||||
getItem: vi.fn(),
|
||||
setItem: vi.fn(),
|
||||
removeItem: vi.fn(),
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
delete window.axios;
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
const mountSidebar = (overrides = {}) =>
|
||||
mount(NomadNetworkSidebar, {
|
||||
props: {
|
||||
nodes: overrides.nodes ?? { [defaultNode.destination_hash]: defaultNode },
|
||||
favourites: overrides.favourites ?? [defaultFavourite],
|
||||
selectedDestinationHash: overrides.selectedDestinationHash ?? "",
|
||||
nodesSearchTerm: overrides.nodesSearchTerm ?? "",
|
||||
totalNodesCount: overrides.totalNodesCount ?? 1,
|
||||
isLoadingMoreNodes: overrides.isLoadingMoreNodes ?? false,
|
||||
hasMoreNodes: overrides.hasMoreNodes ?? false,
|
||||
},
|
||||
global: {
|
||||
mocks: { $t: (key) => key },
|
||||
stubs: {
|
||||
MaterialDesignIcon: {
|
||||
template: '<div class="mdi-stub" :data-icon-name="iconName"></div>',
|
||||
props: ["iconName"],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
it("renders favourites tab with favourite cards", async () => {
|
||||
const wrapper = mountSidebar();
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(wrapper.text()).toContain("nomadnet.favourites");
|
||||
expect(wrapper.text()).toContain("Test Favourite");
|
||||
});
|
||||
|
||||
it("3-dots on favourite card opens context menu", async () => {
|
||||
const wrapper = mountSidebar();
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
const favouriteCard = wrapper.find(".favourite-card");
|
||||
expect(favouriteCard.exists()).toBe(true);
|
||||
const dotsBtn = favouriteCard.findComponent({ name: "IconButton" });
|
||||
expect(dotsBtn.exists()).toBe(true);
|
||||
|
||||
await dotsBtn.trigger("click");
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(wrapper.vm.favouriteContextMenu.show).toBe(true);
|
||||
expect(wrapper.vm.favouriteContextMenu.targetHash).toBe(defaultFavourite.destination_hash);
|
||||
});
|
||||
|
||||
it("favourite context menu has rename, banish, remove options", async () => {
|
||||
const wrapper = mountSidebar();
|
||||
wrapper.vm.favouriteContextMenu = {
|
||||
show: true,
|
||||
targetHash: defaultFavourite.destination_hash,
|
||||
targetSectionId: "default",
|
||||
x: 100,
|
||||
y: 100,
|
||||
};
|
||||
wrapper.vm.sectionContextMenu.show = false;
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
const menuEls = document.body.querySelectorAll(".fixed");
|
||||
const menuEl = Array.from(menuEls).find((el) => el.textContent.includes("nomadnet.rename"));
|
||||
expect(menuEl).toBeTruthy();
|
||||
expect(menuEl.textContent).toContain("nomadnet.block_node");
|
||||
expect(menuEl.textContent).toContain("nomadnet.remove");
|
||||
});
|
||||
|
||||
it("rename from favourite context menu emits rename-favourite", async () => {
|
||||
const wrapper = mountSidebar();
|
||||
wrapper.vm.favouriteContextMenu = {
|
||||
show: true,
|
||||
targetHash: defaultFavourite.destination_hash,
|
||||
targetSectionId: "default",
|
||||
};
|
||||
await wrapper.vm.renameFavouriteFromContext();
|
||||
|
||||
expect(wrapper.emitted("rename-favourite")).toBeDefined();
|
||||
expect(wrapper.emitted("rename-favourite")).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("remove from favourite context menu emits remove-favourite", async () => {
|
||||
const wrapper = mountSidebar();
|
||||
wrapper.vm.favouriteContextMenu = {
|
||||
show: true,
|
||||
targetHash: defaultFavourite.destination_hash,
|
||||
targetSectionId: "default",
|
||||
};
|
||||
await wrapper.vm.removeFavouriteFromContext();
|
||||
|
||||
expect(wrapper.emitted("remove-favourite")).toBeDefined();
|
||||
expect(wrapper.emitted("remove-favourite")).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("banish from favourite context menu calls API and emits block-status-changed", async () => {
|
||||
const emitSpy = vi.spyOn(GlobalEmitter, "emit");
|
||||
const wrapper = mountSidebar();
|
||||
wrapper.vm.favouriteContextMenu = {
|
||||
show: true,
|
||||
targetHash: defaultFavourite.destination_hash,
|
||||
targetSectionId: "default",
|
||||
};
|
||||
wrapper.vm.sectionContextMenu.show = false;
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
const menuEls = document.body.querySelectorAll(".fixed");
|
||||
const menuEl = Array.from(menuEls).find((el) => el.textContent.includes("nomadnet.rename"));
|
||||
expect(menuEl).toBeTruthy();
|
||||
const banishBtn = Array.from(menuEl.querySelectorAll("button")).find((b) =>
|
||||
b.textContent.includes("nomadnet.block_node")
|
||||
);
|
||||
expect(banishBtn).toBeTruthy();
|
||||
await banishBtn.click();
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(DialogUtils.confirm).toHaveBeenCalled();
|
||||
expect(axiosMock.post).toHaveBeenCalledWith(
|
||||
"/api/v1/blocked-destinations",
|
||||
expect.objectContaining({ destination_hash: defaultFavourite.destination_hash })
|
||||
);
|
||||
expect(emitSpy).toHaveBeenCalledWith("block-status-changed");
|
||||
emitSpy.mockRestore();
|
||||
});
|
||||
|
||||
it("announces tab shows announce cards with 3-dots dropdown", async () => {
|
||||
const wrapper = mountSidebar();
|
||||
const announceTab = wrapper.findAll("button").find((b) => b.text().includes("nomadnet.announces"));
|
||||
await announceTab.trigger("click");
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(wrapper.text()).toContain("Test Node");
|
||||
const dropMenus = wrapper.findAllComponents({ name: "DropDownMenu" });
|
||||
expect(dropMenus.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("right-click on announce card opens context menu", async () => {
|
||||
const wrapper = mountSidebar();
|
||||
const announceTab = wrapper.findAll("button").find((b) => b.text().includes("nomadnet.announces"));
|
||||
await announceTab.trigger("click");
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
const announceCard = wrapper.find(".announce-card");
|
||||
expect(announceCard.exists()).toBe(true);
|
||||
|
||||
await announceCard.trigger("contextmenu");
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(wrapper.vm.announceContextMenu.show).toBe(true);
|
||||
expect(wrapper.vm.announceContextMenu.node).toEqual(defaultNode);
|
||||
});
|
||||
|
||||
it("add favourite from announce context menu emits add-favourite", async () => {
|
||||
const wrapper = mountSidebar({ favourites: [] });
|
||||
const announceTab = wrapper.findAll("button").find((b) => b.text().includes("nomadnet.announces"));
|
||||
await announceTab.trigger("click");
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
wrapper.vm.announceContextMenu = { show: true, node: defaultNode };
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
wrapper.vm.addFavouriteFromContext();
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(wrapper.emitted("add-favourite")).toHaveLength(1);
|
||||
expect(wrapper.emitted("add-favourite")[0][0]).toEqual(defaultNode);
|
||||
});
|
||||
|
||||
it("block from announce context menu calls API", async () => {
|
||||
const emitSpy = vi.spyOn(GlobalEmitter, "emit");
|
||||
const wrapper = mountSidebar();
|
||||
wrapper.vm.announceContextMenu = { show: true, node: defaultNode };
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
await wrapper.vm.blockAnnounceFromContext();
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(DialogUtils.confirm).toHaveBeenCalled();
|
||||
expect(axiosMock.post).toHaveBeenCalledWith(
|
||||
"/api/v1/blocked-destinations",
|
||||
expect.objectContaining({ destination_hash: defaultNode.identity_hash })
|
||||
);
|
||||
expect(emitSpy).toHaveBeenCalledWith("block-status-changed");
|
||||
emitSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user