diff --git a/tests/frontend/ConversationViewer.test.js b/tests/frontend/ConversationViewer.test.js index e413acb..e317f71 100644 --- a/tests/frontend/ConversationViewer.test.js +++ b/tests/frontend/ConversationViewer.test.js @@ -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(); diff --git a/tests/frontend/ConversationViewerButtons.test.js b/tests/frontend/ConversationViewerButtons.test.js index 15ce199..1f2ddca 100644 --- a/tests/frontend/ConversationViewerButtons.test.js +++ b/tests/frontend/ConversationViewerButtons.test.js @@ -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(); + }); + }); });