返回 DeepSeek-Reasonix
composer-image-admission.test.tsx
根目录 / desktop / frontend / src / __tests__ / composer-image-admission.test.tsx
1 import assert from "node:assert/strict";
2 import { JSDOM } from "jsdom";
3 import React, { act } from "react";
4 import { createRoot } from "react-dom/client";
5 import { Composer } from "../components/Composer";
6 import { LocaleProvider } from "../lib/i18n";
7 import { ToastProvider } from "../lib/toast";
8 import { installDesktopHostStub } from "./desktopHostStub";
9
10 const dom = new JSDOM("<div id='root'></div>", { url: "http://localhost/", pretendToBeVisual: true });
11 Object.assign(globalThis, {
12 window: dom.window,
13 document: dom.window.document,
14 localStorage: dom.window.localStorage,
15 IS_REACT_ACT_ENVIRONMENT: true,
16 requestAnimationFrame: dom.window.requestAnimationFrame.bind(dom.window),
17 cancelAnimationFrame: dom.window.cancelAnimationFrame.bind(dom.window),
18 ResizeObserver: class { observe() {} disconnect() {} unobserve() {} },
19 });
20 for (const name of ["Node", "HTMLElement", "HTMLTextAreaElement", "Event", "CustomEvent", "File", "FileReader", "MutationObserver"]) {
21 Object.defineProperty(globalThis, name, { configurable: true, value: Reflect.get(dom.window, name) });
22 }
23 Object.defineProperty(globalThis, "navigator", { configurable: true, value: dom.window.navigator });
24 Object.defineProperty(dom.window.HTMLElement.prototype, "attachEvent", { configurable: true, value() {} });
25 Object.defineProperty(dom.window.HTMLElement.prototype, "detachEvent", { configurable: true, value() {} });
26 window.matchMedia = (() => ({ matches: true, addEventListener() {}, removeEventListener() {} })) as typeof window.matchMedia;
27
28 installDesktopHostStub({
29 Commands: async () => [],
30 Models: async () => [],
31 ModelsForTab: async () => [],
32 ListDir: async () => [],
33 ListDirForTab: async () => [],
34 SearchFileRefs: async () => [],
35 SearchFileRefsForTab: async () => [],
36 StageImageForTab: async () => ({ draftId: "0123456789abcdef0123456789abcdef", displayName: "draft.png", mime: "image/png", width: 1, height: 1, bytes: 12 }),
37 CaptureAttachmentTarget: async () => ({token:"target-a",capabilities:["attachments-v2"]}),
38 ReleaseAttachmentTarget: async () => {},
39 StartTurnForAttachmentTarget: async () => ({turnId:"turn",status:"queued",submissionId:"submission"}),
40 StageImageForTarget: async () => ({ draftId: "0123456789abcdef0123456789abcdef", displayName: "draft.png", mime: "image/png", width: 1, height: 1, bytes: 12 }),
41 ReadDraftImageForTarget: async () => "data:image/png;base64,iVBORw0KGgo=",
42 ReadDraftImageForTab: async () => "data:image/png;base64,iVBORw0KGgo=",
43 SavePastedImageForTab: async () => ".reasonix/attachments/draft.png",
44 AttachmentDataURLForTab: async () => "data:image/png;base64,iVBORw0KGgo=",
45 });
46
47 const root = createRoot(document.getElementById("root")!);
48 const flush = () => new Promise<void>((resolve) => setTimeout(resolve, 0));
49 try {
50 await act(async () => {
51 root.render(<LocaleProvider><ToastProvider><Composer
52 running={false} collaborationMode="normal" toolApprovalMode="ask" goal="" cwd="/repo"
53 modelLabel="DeepSeek-R1" tabId="tab-a" sessionKey="session-a" ready
54 insertRequest={{ id: 1, text: "keep this draft", mode: "replace" }}
55 onSend={async () => { throw new Error("reasonix_error:image_attachment_unreadable"); }}
56 onCancel={async () => ({ discardedItemIds: [] })} onCycleMode={() => {}} onSetMode={() => {}}
57 onSetCollaborationMode={() => {}} onSetToolApprovalMode={() => {}} onClearGoal={() => {}}
58 onSwitchModel={() => {}} onSetEffort={() => {}}
59 /></ToastProvider></LocaleProvider>);
60 await flush();
61 });
62 const textarea = document.querySelector<HTMLTextAreaElement>("textarea")!;
63 const file = new File(["draft image"], "draft.png", { type: "image/png", lastModified: 1 });
64 const paste = new Event("paste", { bubbles: true, cancelable: true });
65 Object.defineProperty(paste, "clipboardData", {
66 configurable: true,
67 value: { files: [file], items: [], types: [], getData: () => "" },
68 });
69 await act(async () => { textarea.dispatchEvent(paste); await flush(); });
70 for (let attempt = 0; attempt < 10 && !document.querySelector(".composer-context__item"); attempt += 1) {
71 await act(async () => flush());
72 }
73 assert.equal(document.querySelectorAll(".composer-context__item").length, 1);
74 await act(async () => { document.querySelector<HTMLButtonElement>(".composer__btn--send")!.click(); await flush(); });
75 assert.equal(textarea.value, "keep this draft");
76 assert.equal(document.querySelectorAll(".composer-context__item").length, 1);
77 assert.match(document.body.textContent ?? "", /The image could not be read\. Re-add it or try again/);
78 console.log("composer image admission: rejected image keeps text and attachment for retry");
79 } finally {
80 await act(async () => root.unmount());
81 dom.window.close();
82 }
83
83 lines Plain Text