mirror of
https://git.quad4.io/RNS-Things/MeshChatX.git
synced 2026-04-07 15:45:44 +00:00
317 lines
12 KiB
JavaScript
317 lines
12 KiB
JavaScript
import { mount } from "@vue/test-utils";
|
|
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
import MessagesPage from "@/components/messages/MessagesPage.vue";
|
|
|
|
describe("MessagesPage.vue", () => {
|
|
let axiosMock;
|
|
|
|
beforeEach(() => {
|
|
axiosMock = {
|
|
get: vi.fn(),
|
|
post: vi.fn(),
|
|
};
|
|
window.api = axiosMock;
|
|
|
|
axiosMock.get.mockImplementation((url) => {
|
|
if (url === "/api/v1/config")
|
|
return Promise.resolve({ data: { config: { lxmf_address_hash: "my-hash" } } });
|
|
if (url === "/api/v1/lxmf/conversations") return Promise.resolve({ data: { conversations: [] } });
|
|
if (url === "/api/v1/announces") return Promise.resolve({ data: { announces: [] } });
|
|
return Promise.resolve({ data: {} });
|
|
});
|
|
});
|
|
|
|
afterEach(() => {
|
|
delete window.api;
|
|
});
|
|
|
|
const mountMessagesPage = (props = { destinationHash: "" }) => {
|
|
return mount(MessagesPage, {
|
|
props,
|
|
global: {
|
|
mocks: {
|
|
$t: (key) => key,
|
|
$route: { query: {} },
|
|
$router: { replace: vi.fn() },
|
|
},
|
|
stubs: {
|
|
MaterialDesignIcon: true,
|
|
LoadingSpinner: true,
|
|
MessagesSidebar: {
|
|
template: '<div class="sidebar-stub"></div>',
|
|
props: ["conversations", "selectedDestinationHash"],
|
|
},
|
|
ConversationViewer: {
|
|
template: '<div class="viewer-stub"></div>',
|
|
props: ["selectedPeer", "myLxmfAddressHash"],
|
|
},
|
|
Modal: true,
|
|
},
|
|
},
|
|
});
|
|
};
|
|
|
|
it("fetches config and conversations on mount", async () => {
|
|
const wrapper = mountMessagesPage();
|
|
await wrapper.vm.$nextTick();
|
|
|
|
expect(axiosMock.get).toHaveBeenCalledWith("/api/v1/config");
|
|
expect(axiosMock.get).toHaveBeenCalledWith("/api/v1/lxmf/conversations", expect.any(Object));
|
|
});
|
|
|
|
it("opens ingest paper message modal", async () => {
|
|
const wrapper = mountMessagesPage();
|
|
await wrapper.vm.$nextTick();
|
|
|
|
// Find button to ingest paper message
|
|
const buttons = wrapper.findAll("button");
|
|
const ingestButton = buttons.find((b) => b.html().includes('icon-name="note-plus"'));
|
|
if (ingestButton) {
|
|
await ingestButton.trigger("click");
|
|
expect(wrapper.vm.isShowingIngestPaperMessageModal).toBe(true);
|
|
}
|
|
});
|
|
|
|
it("composes new message when destinationHash prop is provided", async () => {
|
|
const destHash = "0123456789abcdef0123456789abcdef";
|
|
axiosMock.get.mockImplementation((url) => {
|
|
if (url === "/api/v1/announces")
|
|
return Promise.resolve({
|
|
data: { announces: [{ destination_hash: destHash, display_name: "Test Peer" }] },
|
|
});
|
|
if (url === "/api/v1/lxmf/conversations") return Promise.resolve({ data: { conversations: [] } });
|
|
if (url === "/api/v1/config")
|
|
return Promise.resolve({ data: { config: { lxmf_address_hash: "my-hash" } } });
|
|
return Promise.resolve({ data: {} });
|
|
});
|
|
|
|
const wrapper = mountMessagesPage({ destinationHash: destHash });
|
|
// Ensure conversations is initialized as array to avoid filter error in watcher
|
|
wrapper.vm.conversations = [];
|
|
await wrapper.vm.$nextTick();
|
|
await wrapper.vm.$nextTick(); // Wait for fetch
|
|
|
|
expect(wrapper.vm.selectedPeer.destination_hash).toBe(destHash);
|
|
});
|
|
|
|
it("routes to compose when ingest result is lxma contact", async () => {
|
|
const wrapper = mountMessagesPage();
|
|
const composeSpy = vi.spyOn(wrapper.vm, "onComposeNewMessage").mockResolvedValue(undefined);
|
|
|
|
await wrapper.vm.onWebsocketMessage({
|
|
data: JSON.stringify({
|
|
type: "lxm.ingest_uri.result",
|
|
status: "success",
|
|
ingest_type: "lxma_contact",
|
|
destination_hash: "f".repeat(32),
|
|
}),
|
|
});
|
|
|
|
expect(composeSpy).toHaveBeenCalledWith("f".repeat(32));
|
|
});
|
|
|
|
it("updates existing conversation in-place without API call on lxmf_message_created", async () => {
|
|
const destHash = "a".repeat(32);
|
|
const wrapper = mountMessagesPage();
|
|
await wrapper.vm.$nextTick();
|
|
|
|
wrapper.vm.conversations = [
|
|
{
|
|
destination_hash: destHash,
|
|
display_name: "Test Peer",
|
|
latest_message_preview: "old msg",
|
|
updated_at: "2025-01-01T00:00:00Z",
|
|
},
|
|
];
|
|
|
|
axiosMock.get.mockClear();
|
|
|
|
await wrapper.vm.onWebsocketMessage({
|
|
data: JSON.stringify({
|
|
type: "lxmf_message_created",
|
|
lxmf_message: {
|
|
hash: "abc",
|
|
source_hash: "my-hash",
|
|
destination_hash: destHash,
|
|
is_incoming: false,
|
|
content: "new msg",
|
|
title: "",
|
|
timestamp: 1700000000,
|
|
},
|
|
}),
|
|
});
|
|
|
|
expect(wrapper.vm.conversations[0].latest_message_preview).toBe("new msg");
|
|
const convCalls = axiosMock.get.mock.calls.filter((c) => c[0] === "/api/v1/lxmf/conversations");
|
|
expect(convCalls).toHaveLength(0);
|
|
});
|
|
|
|
it("resolves display name for new conversation only", async () => {
|
|
const destHash = "d".repeat(32);
|
|
const wrapper = mountMessagesPage();
|
|
await wrapper.vm.$nextTick();
|
|
|
|
wrapper.vm.conversations = [];
|
|
wrapper.vm.selectedPeer = { destination_hash: destHash, display_name: "Anonymous Peer" };
|
|
|
|
axiosMock.get.mockClear();
|
|
axiosMock.get.mockImplementation((url) => {
|
|
if (url === "/api/v1/lxmf/conversations")
|
|
return Promise.resolve({
|
|
data: {
|
|
conversations: [
|
|
{
|
|
destination_hash: destHash,
|
|
display_name: "Resolved Name",
|
|
custom_display_name: null,
|
|
},
|
|
],
|
|
},
|
|
});
|
|
return Promise.resolve({ data: {} });
|
|
});
|
|
|
|
await wrapper.vm.onWebsocketMessage({
|
|
data: JSON.stringify({
|
|
type: "lxmf_message_created",
|
|
lxmf_message: {
|
|
hash: "new1",
|
|
source_hash: "my-hash",
|
|
destination_hash: destHash,
|
|
is_incoming: false,
|
|
content: "hello",
|
|
title: "",
|
|
timestamp: 1700000000,
|
|
},
|
|
}),
|
|
});
|
|
|
|
await wrapper.vm.$nextTick();
|
|
expect(wrapper.vm.conversations[0].display_name).toBe("Resolved Name");
|
|
expect(wrapper.vm.selectedPeer.display_name).toBe("Resolved Name");
|
|
|
|
const convCalls = axiosMock.get.mock.calls.filter((c) => c[0] === "/api/v1/lxmf/conversations");
|
|
expect(convCalls).toHaveLength(1);
|
|
expect(convCalls[0][1].params.search).toBe(destHash);
|
|
expect(convCalls[0][1].params.limit).toBe(1);
|
|
});
|
|
|
|
it("updates failed_messages_count on state transition to failed", async () => {
|
|
const destHash = "e".repeat(32);
|
|
const wrapper = mountMessagesPage();
|
|
await wrapper.vm.$nextTick();
|
|
|
|
wrapper.vm.conversations = [{ destination_hash: destHash, failed_messages_count: 0 }];
|
|
|
|
await wrapper.vm.onWebsocketMessage({
|
|
data: JSON.stringify({
|
|
type: "lxmf_message_state_updated",
|
|
lxmf_message: {
|
|
hash: "f1",
|
|
source_hash: "my-hash",
|
|
destination_hash: destHash,
|
|
is_incoming: false,
|
|
state: "failed",
|
|
},
|
|
}),
|
|
});
|
|
|
|
expect(wrapper.vm.conversations[0].failed_messages_count).toBe(1);
|
|
});
|
|
|
|
it("does not trigger API call on state updates", async () => {
|
|
const destHash = "f".repeat(32);
|
|
const wrapper = mountMessagesPage();
|
|
await wrapper.vm.$nextTick();
|
|
|
|
wrapper.vm.conversations = [{ destination_hash: destHash, failed_messages_count: 0 }];
|
|
|
|
axiosMock.get.mockClear();
|
|
|
|
for (const state of ["outbound", "sending", "sent", "delivered"]) {
|
|
await wrapper.vm.onWebsocketMessage({
|
|
data: JSON.stringify({
|
|
type: "lxmf_message_state_updated",
|
|
lxmf_message: {
|
|
hash: "s1",
|
|
source_hash: "my-hash",
|
|
destination_hash: destHash,
|
|
is_incoming: false,
|
|
state,
|
|
},
|
|
}),
|
|
});
|
|
}
|
|
|
|
const convCalls = axiosMock.get.mock.calls.filter((c) => c[0] === "/api/v1/lxmf/conversations");
|
|
expect(convCalls).toHaveLength(0);
|
|
});
|
|
|
|
it("mutates existing conversation object without replacing array entry", async () => {
|
|
const destHash = "a".repeat(32);
|
|
const wrapper = mountMessagesPage();
|
|
await wrapper.vm.$nextTick();
|
|
|
|
wrapper.vm.conversations = [
|
|
{
|
|
destination_hash: destHash,
|
|
display_name: "Peer",
|
|
latest_message_preview: "old",
|
|
updated_at: "2025-01-01T00:00:00Z",
|
|
},
|
|
];
|
|
|
|
const lengthBefore = wrapper.vm.conversations.length;
|
|
|
|
await wrapper.vm.onWebsocketMessage({
|
|
data: JSON.stringify({
|
|
type: "lxmf_message_created",
|
|
lxmf_message: {
|
|
hash: "abc",
|
|
source_hash: "my-hash",
|
|
destination_hash: destHash,
|
|
is_incoming: false,
|
|
content: "new",
|
|
title: "",
|
|
timestamp: 1700000000,
|
|
},
|
|
}),
|
|
});
|
|
|
|
expect(wrapper.vm.conversations.length).toBe(lengthBefore);
|
|
expect(wrapper.vm.conversations[0].latest_message_preview).toBe("new");
|
|
expect(wrapper.vm.conversations[0].display_name).toBe("Peer");
|
|
});
|
|
|
|
it("uses conversation display name instead of Unknown Peer when composing", async () => {
|
|
const destHash = "c".repeat(32);
|
|
axiosMock.get.mockImplementation((url) => {
|
|
if (url === "/api/v1/config")
|
|
return Promise.resolve({ data: { config: { lxmf_address_hash: "my-hash" } } });
|
|
if (url === "/api/v1/lxmf/conversations")
|
|
return Promise.resolve({
|
|
data: {
|
|
conversations: [
|
|
{
|
|
destination_hash: destHash,
|
|
display_name: "Existing Peer",
|
|
custom_display_name: null,
|
|
},
|
|
],
|
|
},
|
|
});
|
|
if (url === "/api/v1/announces") return Promise.resolve({ data: { announces: [] } });
|
|
return Promise.resolve({ data: {} });
|
|
});
|
|
|
|
const wrapper = mountMessagesPage();
|
|
wrapper.vm.conversations = [
|
|
{ destination_hash: destHash, display_name: "Existing Peer", custom_display_name: null },
|
|
];
|
|
await wrapper.vm.$nextTick();
|
|
|
|
await wrapper.vm.onComposeNewMessage(destHash);
|
|
expect(wrapper.vm.selectedPeer.display_name).toBe("Existing Peer");
|
|
});
|
|
});
|