@theia/plugin-ext 1.75.0-next.42 → 1.75.0-next.46

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,160 @@
1
+ // *****************************************************************************
2
+ // Copyright (C) 2026 EclipseSource GmbH and others.
3
+ //
4
+ // This program and the accompanying materials are made available under the
5
+ // terms of the Eclipse Public License v. 2.0 which is available at
6
+ // http://www.eclipse.org/legal/epl-2.0.
7
+ //
8
+ // This Source Code may also be made available under the following Secondary
9
+ // Licenses when the conditions for such availability set forth in the Eclipse
10
+ // Public License v. 2.0 are satisfied: GNU General Public License, version 2
11
+ // with the GNU Classpath Exception which is available at
12
+ // https://www.gnu.org/software/classpath/license.html.
13
+ //
14
+ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
15
+ // *****************************************************************************
16
+
17
+ import { FrontendApplicationConfigProvider } from '@theia/core/lib/browser/frontend-application-config-provider';
18
+ import { enableJSDOM } from '@theia/core/lib/browser/test/jsdom';
19
+ // `webviews-main` transitively pulls in Lumino and several frontend modules that touch
20
+ // `document` and the frontend application config at module-load time.
21
+ const disableJSDOM = enableJSDOM();
22
+ try { FrontendApplicationConfigProvider.set({}); } catch { /* already set by a sibling spec */ }
23
+ // xterm.js (pulled in transitively via the terminal package) calls HTMLCanvasElement.prototype.getContext
24
+ // at module-load time. JSDOM's default impl throws 'Not implemented' without the optional `canvas`
25
+ // package; replace it with a no-op so the module graph evaluates. The tests below never render a terminal.
26
+ const canvasProto = (globalThis as { HTMLCanvasElement?: { prototype: { getContext?: unknown } } }).HTMLCanvasElement?.prototype;
27
+ if (canvasProto) {
28
+ canvasProto.getContext = () => undefined;
29
+ }
30
+
31
+ import { expect } from 'chai';
32
+ import { WebviewsMainImpl } from './webviews-main';
33
+ import { WebviewWidget } from './webview/webview';
34
+
35
+ after(() => disableJSDOM());
36
+
37
+ interface FakeWebviewWidget {
38
+ isDisposed: boolean;
39
+ title: { label: string };
40
+ html?: string;
41
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
42
+ contentOptions?: any;
43
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
44
+ iconUrl?: any;
45
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
46
+ messages: any[];
47
+ }
48
+
49
+ function createWebviewWidget(): FakeWebviewWidget {
50
+ const widget: FakeWebviewWidget = { isDisposed: false, title: { label: '' }, messages: [] };
51
+ return Object.assign(widget, {
52
+ setHTML: (value: string) => { widget.html = value; },
53
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
54
+ setContentOptions: (options: any) => { widget.contentOptions = options; },
55
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
56
+ setIconUrl: (iconUrl: any) => { widget.iconUrl = iconUrl; },
57
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
58
+ sendMessage: (value: any) => { widget.messages.push(value); }
59
+ });
60
+ }
61
+
62
+ /**
63
+ * Bypasses the constructor's RPC/container wiring. The methods under test only resolve
64
+ * the webview widget through the widget manager, so stubbing that is enough.
65
+ */
66
+ function createWebviewsMain(widgets: Map<string, FakeWebviewWidget>): WebviewsMainImpl {
67
+ const impl = Object.create(WebviewsMainImpl.prototype) as WebviewsMainImpl;
68
+ (impl as unknown as Record<string, unknown>).widgetManager = {
69
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
70
+ findWidget: async (factoryId: string, predicate: (options?: any) => boolean) => {
71
+ if (factoryId !== WebviewWidget.FACTORY_ID) {
72
+ return undefined;
73
+ }
74
+ for (const [id, widget] of widgets) {
75
+ if (predicate({ id })) {
76
+ return widget;
77
+ }
78
+ }
79
+ return undefined;
80
+ },
81
+ getWidget: async () => undefined
82
+ };
83
+ return impl;
84
+ }
85
+
86
+ describe('WebviewsMainImpl - late calls for a disposed webview', () => {
87
+
88
+ const handle = 'webview-handle';
89
+ let widgets: Map<string, FakeWebviewWidget>;
90
+ let webviewsMain: WebviewsMainImpl;
91
+
92
+ beforeEach(() => {
93
+ widgets = new Map();
94
+ webviewsMain = createWebviewsMain(widgets);
95
+ });
96
+
97
+ // The plugin host declares these methods as returning `void` and never attaches a rejection
98
+ // handler, so throwing on an unknown handle would surface as an unhandled promise rejection.
99
+ describe('with an unknown handle', () => {
100
+
101
+ it('should not reject in $setHtml', async () => {
102
+ await webviewsMain.$setHtml(handle, '<html></html>');
103
+ });
104
+
105
+ it('should not reject in $setOptions', async () => {
106
+ await webviewsMain.$setOptions(handle, { enableScripts: true });
107
+ });
108
+
109
+ it('should not reject in $setTitle', async () => {
110
+ await webviewsMain.$setTitle(handle, 'Title');
111
+ });
112
+
113
+ it('should not reject in $setIconPath', async () => {
114
+ await webviewsMain.$setIconPath(handle, undefined);
115
+ });
116
+
117
+ it('should not reject in $reveal', async () => {
118
+ await webviewsMain.$reveal(handle, {});
119
+ });
120
+
121
+ it('should resolve to false in $postMessage', async () => {
122
+ expect(await webviewsMain.$postMessage(handle, 'message')).to.be.false;
123
+ });
124
+ });
125
+
126
+ describe('with a known handle', () => {
127
+
128
+ let widget: FakeWebviewWidget;
129
+
130
+ beforeEach(() => {
131
+ widget = createWebviewWidget();
132
+ widgets.set(handle, widget);
133
+ });
134
+
135
+ it('should apply the html in $setHtml', async () => {
136
+ await webviewsMain.$setHtml(handle, '<html></html>');
137
+ expect(widget.html).to.equal('<html></html>');
138
+ });
139
+
140
+ it('should apply the content options in $setOptions', async () => {
141
+ await webviewsMain.$setOptions(handle, { enableScripts: true, enableForms: false, enableCommandUris: ['a.command'] });
142
+ expect(widget.contentOptions).to.deep.equal({
143
+ allowScripts: true,
144
+ allowForms: false,
145
+ localResourceRoots: undefined,
146
+ enableCommandUris: ['a.command']
147
+ });
148
+ });
149
+
150
+ it('should apply the title in $setTitle', async () => {
151
+ await webviewsMain.$setTitle(handle, 'Title');
152
+ expect(widget.title.label).to.equal('Title');
153
+ });
154
+
155
+ it('should send the message in $postMessage', async () => {
156
+ expect(await webviewsMain.$postMessage(handle, 'message')).to.be.true;
157
+ expect(widget.messages).to.deep.equal(['message']);
158
+ });
159
+ });
160
+ });
@@ -135,8 +135,8 @@ export class WebviewsMainImpl implements WebviewsMain, Disposable {
135
135
  }
136
136
 
137
137
  async $reveal(handle: string, showOptions: WebviewPanelShowOptions): Promise<void> {
138
- const widget = await this.getWebview(handle);
139
- if (widget.isDisposed) {
138
+ const widget = await this.tryGetWebview(handle);
139
+ if (!widget || widget.isDisposed) {
140
140
  return;
141
141
  }
142
142
  if ((showOptions.viewColumn !== undefined && showOptions.viewColumn !== widget.viewState.position) || showOptions.area !== undefined) {
@@ -156,22 +156,27 @@ export class WebviewsMainImpl implements WebviewsMain, Disposable {
156
156
  }
157
157
 
158
158
  async $setTitle(handle: string, value: string): Promise<void> {
159
- const webview = await this.getWebview(handle);
160
- webview.title.label = value;
159
+ const webview = await this.tryGetWebview(handle);
160
+ if (webview) {
161
+ webview.title.label = value;
162
+ }
161
163
  }
162
164
 
163
165
  async $setIconPath(handle: string, iconUrl: IconUrl | ThemeIcon | undefined): Promise<void> {
164
- const webview = await this.getWebview(handle);
165
- webview.setIconUrl(iconUrl);
166
+ const webview = await this.tryGetWebview(handle);
167
+ webview?.setIconUrl(iconUrl);
166
168
  }
167
169
 
168
170
  async $setHtml(handle: string, value: string): Promise<void> {
169
- const webview = await this.getWebview(handle);
170
- webview.setHTML(value);
171
+ const webview = await this.tryGetWebview(handle);
172
+ webview?.setHTML(value);
171
173
  }
172
174
 
173
175
  async $setOptions(handle: string, options: WebviewOptions): Promise<void> {
174
- const webview = await this.getWebview(handle);
176
+ const webview = await this.tryGetWebview(handle);
177
+ if (!webview) {
178
+ return;
179
+ }
175
180
  const { enableScripts, enableForms, localResourceRoots, ...contentOptions } = options;
176
181
  webview.setContentOptions({
177
182
  allowScripts: enableScripts,
@@ -183,8 +188,6 @@ export class WebviewsMainImpl implements WebviewsMain, Disposable {
183
188
 
184
189
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
185
190
  async $postMessage(handle: string, value: any): Promise<boolean> {
186
- // Due to async nature of $postMessage, the webview may have been disposed in the meantime.
187
- // Therefore, don't throw an error if the webview is not found, but return false in this case.
188
191
  const webview = await this.tryGetWebview(handle);
189
192
  if (!webview) {
190
193
  return false;
@@ -257,14 +260,14 @@ export class WebviewsMainImpl implements WebviewsMain, Disposable {
257
260
  this.proxy.$onDidChangeWebviewPanelViewState(widget.identifier.id, widget.viewState);
258
261
  }
259
262
 
260
- private async getWebview(viewId: string): Promise<WebviewWidget> {
261
- const webview = await this.tryGetWebview(viewId);
262
- if (!webview) {
263
- throw new Error(`Unknown Webview: ${viewId}`);
264
- }
265
- return webview;
266
- }
267
-
263
+ /**
264
+ * Looks up the webview widget for the given handle.
265
+ *
266
+ * All `$`-methods of this class are invoked over RPC and resolve the widget asynchronously, so the webview may already
267
+ * have been disposed by the time they run. The plugin host declares most of them as returning `void` and therefore never
268
+ * attaches a rejection handler, so throwing here surfaces as an unhandled promise rejection in the plugin host. Callers
269
+ * must instead treat a missing webview as a no-op, the same way VS Code's `MainThreadWebviews` does.
270
+ */
268
271
  private async tryGetWebview(id: string): Promise<WebviewWidget | undefined> {
269
272
  const webview = await this.widgetManager.findWidget<WebviewWidget>(WebviewWidget.FACTORY_ID, options => {
270
273
  if (options) {