返回 DeepSeek-Reasonix
fakeGuestViews.ts
根目录 / desktop / electron / src / main / browser / fakeGuestViews.ts
1 import type { KeyboardInputEvent, MouseInputEvent, MouseWheelInputEvent, Rectangle, WebPreferences } from "electron";
2 import type { GuestDebugger, GuestFrame, GuestImage, GuestPage, GuestView, GuestViewEvents, GuestViewFactory } from "./guestView.js";
3
4 // In-memory stand-ins for WebContentsView so the browser modules run under
5 // plain node --test. Scripts are answered by the injected `run` callback.
6
7 export type ScriptRunner = (code: string, frame: FakeFrame) => unknown;
8
9 export class FakeFrame implements GuestFrame {
10 detached = false;
11 parent: FakeFrame | null = null;
12 children: FakeFrame[] = [];
13
14 constructor(readonly frameTreeNodeId: number, public url: string, private readonly run: ScriptRunner) {}
15
16 get framesInSubtree(): GuestFrame[] {
17 return [this, ...this.children.flatMap((child) => child.framesInSubtree)];
18 }
19
20 async executeJavaScript(code: string): Promise<unknown> {
21 return this.run(code, this);
22 }
23 }
24
25 export class FakeDebugger implements GuestDebugger {
26 attached = false;
27 readonly commands: Array<{ method: string; params: unknown }> = [];
28 private readonly listeners = new Set<(event: unknown, method: string, params: unknown) => void>();
29 respond: (method: string, params: unknown) => unknown = () => ({});
30
31 isAttached(): boolean {
32 return this.attached;
33 }
34
35 attach(): void {
36 this.attached = true;
37 }
38
39 detach(): void {
40 this.attached = false;
41 }
42
43 async sendCommand(method: string, params?: unknown): Promise<unknown> {
44 this.commands.push({ method, params });
45 return this.respond(method, params);
46 }
47
48 emit(method: string, params: unknown): void {
49 for (const listener of [...this.listeners]) listener({}, method, params);
50 }
51
52 on(_event: "message", listener: (event: unknown, method: string, params: unknown) => void): void {
53 this.listeners.add(listener);
54 }
55
56 removeListener(_event: "message", listener: (event: unknown, method: string, params: unknown) => void): void {
57 this.listeners.delete(listener);
58 }
59 }
60
61 export class FakePage implements GuestPage {
62 url = "about:blank";
63 title = "";
64 loading = false;
65 destroyed = false;
66 zoom = 1;
67 devtools = false;
68 back = false;
69 forward = false;
70 readonly calls: string[] = [];
71 readonly inputs: Array<MouseInputEvent | MouseWheelInputEvent | KeyboardInputEvent> = [];
72 readonly inserted: string[] = [];
73 readonly mainFrame: FakeFrame;
74 readonly debugger = new FakeDebugger();
75 loadResult: Promise<void> = Promise.resolve();
76 image: GuestImage = { toPNG: () => Buffer.from("png"), getSize: () => ({ width: 8, height: 6 }) };
77 readonly capturedRects: Array<Rectangle | undefined> = [];
78
79 constructor(readonly id: number, public run: ScriptRunner = () => undefined) {
80 this.mainFrame = new FakeFrame(id * 100, this.url, (code, frame) => this.run(code, frame));
81 }
82
83 readonly navigationHistory = {
84 canGoBack: () => this.back,
85 canGoForward: () => this.forward,
86 goBack: () => this.calls.push("back"),
87 goForward: () => this.calls.push("forward"),
88 };
89
90 isDestroyed(): boolean {
91 return this.destroyed;
92 }
93
94 loadURL(url: string): Promise<void> {
95 this.calls.push(`load:${url}`);
96 this.url = url;
97 return this.loadResult;
98 }
99
100 getURL(): string {
101 return this.url;
102 }
103
104 getTitle(): string {
105 return this.title;
106 }
107
108 isLoading(): boolean {
109 return this.loading;
110 }
111
112 reload(): void {
113 this.calls.push("reload");
114 }
115
116 stop(): void {
117 this.calls.push("stop");
118 }
119
120 getZoomFactor(): number {
121 return this.zoom;
122 }
123
124 setZoomFactor(factor: number): void {
125 this.zoom = factor;
126 }
127
128 isDevToolsOpened(): boolean {
129 return this.devtools;
130 }
131
132 openDevTools(): void {
133 this.devtools = true;
134 }
135
136 closeDevTools(): void {
137 this.devtools = false;
138 }
139
140 focus(): void {
141 this.calls.push("focus");
142 }
143
144 async executeJavaScriptInIsolatedWorld(_worldId: number, scripts: { code: string }[]): Promise<unknown> {
145 return this.run(scripts[0]?.code ?? "", this.mainFrame);
146 }
147
148 sendInputEvent(event: MouseInputEvent | MouseWheelInputEvent | KeyboardInputEvent): void {
149 this.inputs.push(event);
150 }
151
152 async insertText(text: string): Promise<void> {
153 this.inserted.push(text);
154 }
155
156 async capturePage(rect?: Rectangle): Promise<GuestImage> {
157 this.capturedRects.push(rect);
158 return this.image;
159 }
160 }
161
162 export class FakeGuestView implements GuestView {
163 readonly page: FakePage;
164 events: GuestViewEvents | null = null;
165 bounds: Rectangle | null = null;
166 visible = false;
167 destroyed = false;
168
169 constructor(id: number, readonly partition: string, readonly inherited?: WebPreferences) {
170 this.page = new FakePage(id);
171 }
172
173 bind(events: GuestViewEvents): void {
174 this.events = events;
175 }
176
177 setBounds(bounds: Rectangle): void {
178 this.bounds = bounds;
179 }
180
181 setVisible(visible: boolean): void {
182 this.visible = visible;
183 }
184
185 destroy(): void {
186 this.destroyed = true;
187 this.page.destroyed = true;
188 }
189
190 fire(): GuestViewEvents {
191 if (!this.events) throw new Error("view is not bound");
192 return this.events;
193 }
194 }
195
196 export class FakeViewFactory implements GuestViewFactory {
197 readonly views: FakeGuestView[] = [];
198 private nextId = 1;
199
200 create(partition: string, inherited?: WebPreferences): GuestView {
201 const view = new FakeGuestView(this.nextId++, partition, inherited);
202 this.views.push(view);
203 return view;
204 }
205 }
206
207 export const silentLog = { info() {}, warn() {}, error() {} };
208
208 lines TYPESCRIPT