feat(tests): update ConversationViewer tests with clipboard handling, file attachment functionality, and message translation

This commit is contained in:
Ivan
2026-04-13 18:46:51 -05:00
parent 76cb936582
commit 329d1df1aa
2 changed files with 207 additions and 1 deletions
+66
View File
@@ -105,6 +105,72 @@ describe("ConversationViewer.vue", () => {
expect(wrapper.vm.newMessageImages).toHaveLength(1);
});
it("onMessagePaste ignores non-image clipboard files (e.g. PDF) and does not prevent default", () => {
const wrapper = mountConversationViewer();
const file = new File([""], "doc.pdf", { type: "application/pdf" });
const event = {
preventDefault: vi.fn(),
clipboardData: {
items: [
{
kind: "file",
type: "application/pdf",
getAsFile: () => file,
},
],
},
};
wrapper.vm.onMessagePaste(event);
expect(event.preventDefault).not.toHaveBeenCalled();
expect(wrapper.vm.newMessageImages).toHaveLength(0);
});
it("onMessagePaste does nothing when clipboard has no image file items", () => {
const wrapper = mountConversationViewer();
const event = {
preventDefault: vi.fn(),
clipboardData: {
items: [{ kind: "string", type: "text/plain", getAsString: () => "hi" }],
},
};
wrapper.vm.onMessagePaste(event);
expect(event.preventDefault).not.toHaveBeenCalled();
expect(wrapper.vm.newMessageImages).toHaveLength(0);
});
it("onMessagePaste adds multiple images from a single paste event", () => {
const wrapper = mountConversationViewer();
const f1 = new File([""], "a.png", { type: "image/png" });
const f2 = new File([""], "b.png", { type: "image/png" });
const event = {
preventDefault: vi.fn(),
clipboardData: {
items: [
{ kind: "file", type: "image/png", getAsFile: () => f1 },
{ kind: "file", type: "image/png", getAsFile: () => f2 },
],
},
};
wrapper.vm.onMessagePaste(event);
expect(event.preventDefault).toHaveBeenCalled();
expect(wrapper.vm.newMessageImages).toHaveLength(2);
});
it("pasteFromClipboard inserts text at the message input selection", async () => {
const readText = vi.fn(() => Promise.resolve("pasted-text"));
vi.stubGlobal("navigator", {
...navigator,
clipboard: { readText },
});
const wrapper = mountConversationViewer();
const ta = wrapper.find("#message-input").element;
ta.selectionStart = 0;
ta.selectionEnd = 0;
wrapper.vm.newMessageText = "";
await wrapper.vm.pasteFromClipboard();
expect(wrapper.vm.newMessageText).toBe("pasted-text");
});
it("adds multiple images and renders previews", async () => {
const wrapper = mountConversationViewer();
@@ -15,6 +15,15 @@ vi.mock("@/js/DialogUtils", () => ({
},
}));
vi.mock("@/js/ToastUtils", () => ({
default: {
success: vi.fn(),
error: vi.fn(),
loading: vi.fn(),
info: vi.fn(),
},
}));
describe("ConversationViewer.vue button interactions", () => {
let axiosMock;
@@ -68,7 +77,8 @@ describe("ConversationViewer.vue button interactions", () => {
...overrides,
},
global: {
mocks: { $t: (key) => key },
directives: { "click-outside": { mounted: () => {}, unmounted: () => {} } },
mocks: { $t: (key) => key, $i18n: { locale: "en" } },
stubs: {
MaterialDesignIcon: true,
AddImageButton: true,
@@ -216,4 +226,134 @@ describe("ConversationViewer.vue button interactions", () => {
expect(typeof wrapper.vm.openShareContactModal).toBe("function");
wrapper.vm.openShareContactModal();
});
describe("compose area: clipboard, file attachments, and toolbar actions", () => {
it("add files button triggers the hidden file input click", async () => {
const wrapper = mountViewer();
await wrapper.vm.$nextTick();
const fileInput = wrapper.find('input[type="file"]');
const clickSpy = vi.spyOn(fileInput.element, "click").mockImplementation(() => {});
const actionButtons = wrapper.findAll(".attachment-action-button");
await actionButtons[0].trigger("click");
expect(clickSpy).toHaveBeenCalled();
clickSpy.mockRestore();
});
it("onFileInputChange appends selected files to newMessageFiles", () => {
const wrapper = mountViewer();
const f = new File(["x"], "attach.txt", { type: "text/plain" });
wrapper.vm.onFileInputChange({ target: { files: [f] } });
expect(wrapper.vm.newMessageFiles).toContain(f);
});
it("removeFileAttachment removes one file from newMessageFiles", () => {
const wrapper = mountViewer();
const f1 = new File(["a"], "a.txt", { type: "text/plain" });
const f2 = new File(["b"], "b.txt", { type: "text/plain" });
wrapper.vm.newMessageFiles = [f1, f2];
wrapper.vm.removeFileAttachment(f1);
expect(wrapper.vm.newMessageFiles).toEqual([f2]);
});
it("paste toolbar button reads clipboard text into the message field", async () => {
const readText = vi.fn(() => Promise.resolve("from-clipboard"));
vi.stubGlobal("navigator", {
...navigator,
clipboard: { readText },
});
const wrapper = mountViewer();
await wrapper.vm.$nextTick();
const ta = wrapper.find("#message-input").element;
ta.selectionStart = 0;
ta.selectionEnd = 0;
wrapper.vm.newMessageText = "";
const actionButtons = wrapper.findAll(".attachment-action-button");
await actionButtons[1].trigger("click");
await vi.waitFor(() => expect(wrapper.vm.newMessageText).toBe("from-clipboard"));
});
it("translateMessage replaces text when the translator API succeeds", async () => {
axiosMock.post.mockImplementation((url) => {
if (url.includes("/translator/translate")) {
return Promise.resolve({ data: { translated_text: "translated" } });
}
return Promise.resolve({ data: {} });
});
const wrapper = mountViewer();
wrapper.vm.newMessageText = "hello";
await wrapper.vm.translateMessage();
expect(wrapper.vm.newMessageText).toBe("translated");
expect(axiosMock.post).toHaveBeenCalledWith(
"/api/v1/translator/translate",
expect.objectContaining({
text: "hello",
target_lang: "en",
})
);
});
it("generatePaperMessageFromComposition sends lxm.generate_paper_uri over the websocket", async () => {
const sendSpy = vi.spyOn(WebSocketConnection, "send").mockImplementation(() => {});
const wrapper = mountViewer();
wrapper.vm.newMessageText = "paper body";
await wrapper.vm.generatePaperMessageFromComposition();
expect(sendSpy).toHaveBeenCalled();
const payload = JSON.parse(sendSpy.mock.calls[0][0]);
expect(payload.type).toBe("lxm.generate_paper_uri");
expect(payload.destination_hash).toBe("a".repeat(32));
expect(payload.content).toBe("paper body");
sendSpy.mockRestore();
});
it("requestLocation posts a telemetry request command", async () => {
const wrapper = mountViewer();
await wrapper.vm.requestLocation();
expect(axiosMock.post).toHaveBeenCalledWith(
"/api/v1/lxmf-messages/send",
expect.objectContaining({
lxmf_message: expect.objectContaining({
destination_hash: "a".repeat(32),
fields: {
commands: [{ "0x01": expect.any(Number) }],
},
}),
})
);
});
it("shareLocation with manual coordinates sets telemetry and calls sendMessage", async () => {
const wrapper = mountViewer({
config: {
location_source: "manual",
location_manual_lat: "12.34",
location_manual_lon: "56.78",
location_manual_alt: "9",
},
});
const sendSpy = vi.spyOn(wrapper.vm, "sendMessage").mockResolvedValue(undefined);
await wrapper.vm.shareLocation();
expect(sendSpy).toHaveBeenCalled();
expect(wrapper.vm.newMessageTelemetry).toMatchObject({
latitude: 12.34,
longitude: 56.78,
altitude: 9,
});
sendSpy.mockRestore();
});
it("openConversationPopout opens a popout URL with the peer hash", () => {
const openSpy = vi.spyOn(window, "open").mockImplementation(() => null);
const wrapper = mountViewer();
wrapper.vm.openConversationPopout();
expect(openSpy).toHaveBeenCalledWith(
expect.stringContaining(encodeURIComponent("a".repeat(32))),
"_blank",
expect.stringContaining("width=")
);
openSpy.mockRestore();
});
});
});