mirror of
https://git.quad4.io/RNS-Things/MeshChatX.git
synced 2026-04-25 11:02:11 +00:00
208 lines
7.8 KiB
JavaScript
208 lines
7.8 KiB
JavaScript
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 GlobalState from "@/js/GlobalState";
|
|
import DialogUtils from "@/js/DialogUtils";
|
|
|
|
vi.mock("@/js/DialogUtils", () => ({
|
|
default: {
|
|
confirm: vi.fn(() => Promise.resolve(true)),
|
|
alert: vi.fn(() => Promise.resolve()),
|
|
},
|
|
}));
|
|
|
|
describe("MessageSendingFailures.test.js", () => {
|
|
let axiosMock;
|
|
|
|
beforeEach(() => {
|
|
GlobalState.config.theme = "light";
|
|
GlobalState.config.message_outbound_bubble_color = "#4f46e5";
|
|
GlobalState.config.message_waiting_bubble_color = "#e5e7eb";
|
|
WebSocketConnection.connect();
|
|
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("/lxmf-messages/conversation/"))
|
|
return Promise.resolve({ data: { lxmf_messages: [] } });
|
|
return Promise.resolve({ data: {} });
|
|
}),
|
|
post: vi.fn().mockImplementation(() => Promise.resolve({ data: { lxmf_message: { hash: "mock" } } })),
|
|
delete: vi.fn().mockResolvedValue({ data: {} }),
|
|
};
|
|
window.api = axiosMock;
|
|
|
|
// Mock URL.createObjectURL
|
|
window.URL.createObjectURL = vi.fn(() => "mock-url");
|
|
vi.spyOn(window, "open").mockImplementation(() => null);
|
|
});
|
|
|
|
afterEach(() => {
|
|
delete window.api;
|
|
vi.unstubAllGlobals();
|
|
WebSocketConnection.destroy();
|
|
});
|
|
|
|
const mountConversationViewer = (props = {}) => {
|
|
return mount(ConversationViewer, {
|
|
props: {
|
|
selectedPeer: { destination_hash: "test-hash", display_name: "Test Peer" },
|
|
myLxmfAddressHash: "my-hash",
|
|
conversations: [],
|
|
...props,
|
|
},
|
|
global: {
|
|
directives: { "click-outside": { mounted: () => {}, unmounted: () => {} } },
|
|
mocks: {
|
|
$t: (key) => key,
|
|
},
|
|
stubs: {
|
|
MaterialDesignIcon: true,
|
|
AddImageButton: true,
|
|
AddAudioButton: true,
|
|
SendMessageButton: true,
|
|
ConversationDropDownMenu: true,
|
|
PaperMessageModal: true,
|
|
AudioWaveformPlayer: true,
|
|
LxmfUserIcon: true,
|
|
},
|
|
},
|
|
});
|
|
};
|
|
|
|
it("handles API 503 failure when sending message", async () => {
|
|
const wrapper = mountConversationViewer();
|
|
wrapper.vm.newMessageText = "Hello failure";
|
|
|
|
axiosMock.post.mockRejectedValueOnce({
|
|
response: {
|
|
status: 503,
|
|
data: { message: "Sending failed" },
|
|
},
|
|
});
|
|
|
|
await wrapper.vm.sendMessage();
|
|
|
|
// Optimistic placeholder should be gone
|
|
const pendingItems = wrapper.vm.chatItems.filter((item) => item.lxmf_message.hash.startsWith("pending-"));
|
|
expect(pendingItems).toHaveLength(0);
|
|
|
|
// Alert should be shown
|
|
expect(DialogUtils.alert).toHaveBeenCalledWith("Sending failed");
|
|
});
|
|
|
|
it("updates UI when message state becomes failed via WebSocket", async () => {
|
|
const wrapper = mountConversationViewer();
|
|
const messageHash = "msg-123";
|
|
|
|
// Add a message that is currently "sending"
|
|
wrapper.vm.chatItems.push({
|
|
type: "lxmf_message",
|
|
is_outbound: true,
|
|
lxmf_message: {
|
|
hash: messageHash,
|
|
content: "Going to fail",
|
|
state: "sending",
|
|
progress: 50,
|
|
destination_hash: "test-hash",
|
|
source_hash: "my-hash",
|
|
fields: {},
|
|
},
|
|
});
|
|
|
|
// Simulate WebSocket update
|
|
wrapper.vm.onLxmfMessageUpdated({
|
|
hash: messageHash,
|
|
state: "failed",
|
|
progress: 50,
|
|
});
|
|
|
|
const updatedItem = wrapper.vm.chatItems.find((i) => i.lxmf_message.hash === messageHash);
|
|
expect(updatedItem.lxmf_message.state).toBe("failed");
|
|
|
|
await wrapper.vm.$nextTick();
|
|
// The retry button should be visible in the context menu if we were to open it
|
|
// (Testing the logic of onLxmfMessageUpdated is enough here as retry logic is tested elsewhere)
|
|
});
|
|
|
|
it("handles second image failure in multi-image send", async () => {
|
|
const wrapper = mountConversationViewer();
|
|
wrapper.vm.newMessageText = "Two images";
|
|
|
|
const image1 = new File([""], "image1.png", { type: "image/png" });
|
|
const image2 = new File([""], "image2.png", { type: "image/png" });
|
|
image1.arrayBuffer = vi.fn(() => Promise.resolve(new ArrayBuffer(8)));
|
|
image2.arrayBuffer = vi.fn(() => Promise.resolve(new ArrayBuffer(8)));
|
|
|
|
await wrapper.vm.onImageSelected(image1);
|
|
await wrapper.vm.onImageSelected(image2);
|
|
|
|
// First image succeeds, second fails
|
|
axiosMock.post
|
|
.mockResolvedValueOnce({
|
|
data: { lxmf_message: { hash: "hash-1", content: "Two images", state: "outbound" } },
|
|
})
|
|
.mockRejectedValueOnce({ response: { data: { message: "Second image failed" } } });
|
|
|
|
const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
|
|
await wrapper.vm.sendMessage();
|
|
|
|
// Both images should be processed, but second one logs an error
|
|
const sendCalls = axiosMock.post.mock.calls.filter((c) => c[0] === "/api/v1/lxmf-messages/send");
|
|
expect(sendCalls.length).toBe(2);
|
|
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining("Failed to send image 2"), expect.anything());
|
|
|
|
consoleSpy.mockRestore();
|
|
});
|
|
|
|
it("removes placeholder even if buildOutboundJobSnapshot fails", async () => {
|
|
const wrapper = mountConversationViewer();
|
|
wrapper.vm.newMessageText = "Fail early";
|
|
|
|
vi.spyOn(wrapper.vm, "buildOutboundJobSnapshot").mockRejectedValueOnce(new Error("Snapshot failed"));
|
|
|
|
await wrapper.vm.sendMessage();
|
|
|
|
expect(DialogUtils.alert).toHaveBeenCalledWith("Snapshot failed");
|
|
expect(wrapper.vm.chatItems).toHaveLength(0);
|
|
});
|
|
|
|
it("retrying a failed message sends it again", async () => {
|
|
const wrapper = mountConversationViewer();
|
|
const failedItem = {
|
|
type: "lxmf_message",
|
|
is_outbound: true,
|
|
lxmf_message: {
|
|
hash: "failed-hash",
|
|
state: "failed",
|
|
content: "retry me",
|
|
destination_hash: "test-hash",
|
|
source_hash: "my-hash",
|
|
fields: {},
|
|
},
|
|
};
|
|
wrapper.vm.chatItems = [failedItem];
|
|
|
|
axiosMock.post.mockResolvedValue({ data: { lxmf_message: { hash: "new-hash", state: "outbound" } } });
|
|
|
|
await wrapper.vm.retrySendingMessage(failedItem);
|
|
|
|
expect(axiosMock.post).toHaveBeenCalledWith(
|
|
"/api/v1/lxmf-messages/send",
|
|
expect.objectContaining({
|
|
lxmf_message: expect.objectContaining({
|
|
content: "retry me",
|
|
destination_hash: "test-hash",
|
|
}),
|
|
})
|
|
);
|
|
|
|
// Old item should be removed
|
|
expect(wrapper.vm.chatItems.find((i) => i.lxmf_message.hash === "failed-hash")).toBeUndefined();
|
|
// New item should be added
|
|
expect(wrapper.vm.chatItems.find((i) => i.lxmf_message.hash === "new-hash")).toBeDefined();
|
|
});
|
|
});
|