Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/api/test/browser/extHostBrowsers.test.ts
13401 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import type * as vscode from 'vscode';
7
import assert from 'assert';
8
import { mock } from '../../../../base/test/common/mock.js';
9
import { BrowserTabDto, MainThreadBrowsersShape } from '../../common/extHost.protocol.js';
10
import { ExtHostBrowsers } from '../../common/extHostBrowsers.js';
11
import { SingleProxyRPCProtocol } from '../common/testRPCProtocol.js';
12
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js';
13
14
suite('ExtHostBrowsers', () => {
15
16
const store = ensureNoDisposablesAreLeakedInTestSuite();
17
18
const defaultDto: BrowserTabDto = {
19
id: 'browser-1',
20
url: 'https://example.com',
21
title: 'Example',
22
favicon: undefined,
23
};
24
25
function createDto(overrides?: Partial<BrowserTabDto>): BrowserTabDto {
26
return { ...defaultDto, ...overrides };
27
}
28
29
function createExtHostBrowsers(overrides?: Partial<MainThreadBrowsersShape>): ExtHostBrowsers {
30
const proxy = new class extends mock<MainThreadBrowsersShape>() {
31
override $openBrowserTab(): Promise<BrowserTabDto> { return Promise.resolve(createDto()); }
32
override $startCDPSession(): Promise<void> { return Promise.resolve(); }
33
override $closeCDPSession(): Promise<void> { return Promise.resolve(); }
34
override $sendCDPMessage(): Promise<void> { return Promise.resolve(); }
35
override $closeBrowserTab(): Promise<void> { return Promise.resolve(); }
36
};
37
if (overrides) {
38
Object.assign(proxy, overrides);
39
}
40
return store.add(new ExtHostBrowsers(SingleProxyRPCProtocol(proxy)));
41
}
42
43
// #region browserTabs
44
45
test('browserTabs populates from $onDidOpenBrowserTab', () => {
46
const extHost = createExtHostBrowsers();
47
extHost.$onDidOpenBrowserTab(createDto({ id: 'b1', url: 'https://one.com', title: 'One' }));
48
extHost.$onDidOpenBrowserTab(createDto({ id: 'b2', url: 'https://two.com', title: 'Two' }));
49
50
const tabs = extHost.browserTabs;
51
assert.strictEqual(tabs.length, 2);
52
assert.strictEqual(tabs[0].url, 'https://one.com');
53
assert.strictEqual(tabs[1].url, 'https://two.com');
54
});
55
56
test('browserTabs returns a snapshot, not a live array', () => {
57
const extHost = createExtHostBrowsers();
58
extHost.$onDidOpenBrowserTab(createDto({ id: 'b1' }));
59
const snapshot1 = extHost.browserTabs;
60
61
extHost.$onDidOpenBrowserTab(createDto({ id: 'b2' }));
62
const snapshot2 = extHost.browserTabs;
63
64
assert.notStrictEqual(snapshot1, snapshot2);
65
assert.strictEqual(snapshot1.length, 1);
66
assert.strictEqual(snapshot2.length, 2);
67
});
68
69
// #endregion
70
71
// #region activeBrowserTab
72
73
test('activeBrowserTab updates via $onDidChangeActiveBrowserTab', () => {
74
const extHost = createExtHostBrowsers();
75
const dto = createDto({ id: 'b1', url: 'https://active.com' });
76
extHost.$onDidOpenBrowserTab(dto);
77
extHost.$onDidChangeActiveBrowserTab('b1');
78
79
assert.strictEqual(extHost.activeBrowserTab?.url, 'https://active.com');
80
});
81
82
test('activeBrowserTab becomes undefined when cleared', () => {
83
const extHost = createExtHostBrowsers();
84
const dto = createDto({ id: 'b1' });
85
extHost.$onDidOpenBrowserTab(dto);
86
extHost.$onDidChangeActiveBrowserTab('b1');
87
assert.ok(extHost.activeBrowserTab);
88
89
extHost.$onDidChangeActiveBrowserTab(undefined);
90
assert.strictEqual(extHost.activeBrowserTab, undefined);
91
});
92
93
test('$onDidChangeActiveBrowserTab with unknown tab returns undefined', () => {
94
const extHost = createExtHostBrowsers();
95
96
extHost.$onDidChangeActiveBrowserTab('non-existent');
97
98
assert.strictEqual(extHost.activeBrowserTab, undefined);
99
});
100
101
// #endregion
102
103
// #region openBrowserTab
104
105
test('openBrowserTab returns a BrowserTab with correct properties', async () => {
106
const dto = createDto({ id: 'opened', url: 'https://opened.com', title: 'Opened' });
107
const extHost = createExtHostBrowsers({
108
$openBrowserTab: () => Promise.resolve(dto),
109
});
110
111
const tab = await extHost.openBrowserTab('https://opened.com');
112
assert.strictEqual(tab.url, 'https://opened.com');
113
assert.strictEqual(tab.title, 'Opened');
114
});
115
116
test('openBrowserTab fires onDidOpenBrowserTab for new tabs', async () => {
117
const extHost = createExtHostBrowsers({
118
$openBrowserTab: () => Promise.resolve(createDto({ id: 'new-tab' })),
119
});
120
const opened: vscode.BrowserTab[] = [];
121
store.add(extHost.onDidOpenBrowserTab(tab => opened.push(tab)));
122
123
await extHost.openBrowserTab('https://example.com');
124
125
assert.strictEqual(opened.length, 1);
126
assert.strictEqual(opened[0].url, 'https://example.com');
127
});
128
129
test('openBrowserTab reuses existing tab when IDs match', async () => {
130
const extHost = createExtHostBrowsers({
131
$openBrowserTab: () => Promise.resolve(createDto({ id: 'same', url: 'https://updated.com' })),
132
});
133
134
extHost.$onDidOpenBrowserTab(createDto({ id: 'same', url: 'https://original.com' }));
135
const tab = await extHost.openBrowserTab('https://updated.com');
136
137
assert.strictEqual(extHost.browserTabs.length, 1);
138
assert.strictEqual(tab.url, 'https://updated.com');
139
});
140
141
test('openBrowserTab forwards options to proxy', async () => {
142
let capturedViewColumn: number | undefined;
143
let capturedOptions: { preserveFocus?: boolean; inactive?: boolean } | undefined;
144
const extHost = createExtHostBrowsers({
145
$openBrowserTab: (_url: string, viewColumn?: number, options?: { preserveFocus?: boolean; inactive?: boolean }) => {
146
capturedViewColumn = viewColumn;
147
capturedOptions = options;
148
return Promise.resolve(createDto({ id: 'opts' }));
149
},
150
});
151
152
await extHost.openBrowserTab('https://example.com', { viewColumn: 2, preserveFocus: true, background: true });
153
154
// ViewColumn.from converts API viewColumn (1-based) to EditorGroupColumn (0-based)
155
assert.strictEqual(capturedViewColumn, 1);
156
assert.strictEqual(capturedOptions?.preserveFocus, true);
157
assert.strictEqual(capturedOptions?.inactive, true);
158
});
159
160
// #endregion
161
162
// #region $onDidOpenBrowserTab
163
164
test('$onDidOpenBrowserTab fires event', () => {
165
const extHost = createExtHostBrowsers();
166
const opened: vscode.BrowserTab[] = [];
167
store.add(extHost.onDidOpenBrowserTab(tab => opened.push(tab)));
168
169
extHost.$onDidOpenBrowserTab(createDto({ id: 'b1', url: 'https://opened.com' }));
170
171
assert.strictEqual(opened.length, 1);
172
assert.strictEqual(opened[0].url, 'https://opened.com');
173
});
174
175
// #endregion
176
177
// #region $onDidCloseBrowserTab
178
179
test('$onDidCloseBrowserTab removes tab and fires event', () => {
180
const extHost = createExtHostBrowsers();
181
const changes: vscode.BrowserTab[] = [];
182
store.add(extHost.onDidChangeBrowserTabState(tab => changes.push(tab)));
183
184
extHost.$onDidOpenBrowserTab(createDto({ id: 'b1', url: 'https://old.com' }));
185
extHost.$onDidChangeBrowserTabState(createDto({ id: 'b1', url: 'https://new.com' }));
186
187
assert.strictEqual(changes.length, 1);
188
assert.strictEqual(changes[0].url, 'https://new.com');
189
});
190
191
test('$onDidChangeBrowserTabState does not fire when data is unchanged', () => {
192
const extHost = createExtHostBrowsers();
193
extHost.$onDidOpenBrowserTab(createDto({ id: 'b1', url: 'https://example.com', title: 'Old Title' }));
194
195
extHost.$onDidChangeBrowserTabState(createDto({ id: 'b1', url: 'https://example.com', title: 'New Title' }));
196
197
assert.strictEqual(extHost.browserTabs[0].url, 'https://example.com');
198
assert.strictEqual(extHost.browserTabs[0].title, 'New Title');
199
});
200
201
// #endregion
202
203
// #region $onDidChangeActiveBrowserTab event
204
205
test('$onDidChangeActiveBrowserTab fires event', () => {
206
const extHost = createExtHostBrowsers();
207
const activeChanges: (string | undefined)[] = [];
208
store.add(extHost.onDidChangeActiveBrowserTab(tab => activeChanges.push(tab?.url)));
209
210
const dto = createDto({ id: 'b1' });
211
extHost.$onDidOpenBrowserTab(dto);
212
extHost.$onDidChangeActiveBrowserTab('b1');
213
extHost.$onDidChangeActiveBrowserTab(undefined);
214
215
assert.deepStrictEqual(activeChanges, ['https://example.com', undefined]);
216
});
217
218
// #endregion
219
220
// #region BrowserTab icon
221
222
test('icon is globe ThemeIcon when no favicon', () => {
223
const extHost = createExtHostBrowsers();
224
extHost.$onDidOpenBrowserTab(createDto({ id: 'b1', favicon: undefined }));
225
226
assert.strictEqual((extHost.browserTabs[0].icon as { id: string }).id, 'globe');
227
});
228
229
test('icon is URI when favicon is provided', () => {
230
const extHost = createExtHostBrowsers();
231
extHost.$onDidOpenBrowserTab(createDto({ id: 'b1', favicon: 'https://example.com/favicon.ico' }));
232
233
assert.strictEqual(String(extHost.browserTabs[0].icon), 'https://example.com/favicon.ico');
234
});
235
236
test('icon updates when favicon changes', () => {
237
const extHost = createExtHostBrowsers();
238
extHost.$onDidOpenBrowserTab(createDto({ id: 'b1', favicon: undefined }));
239
assert.strictEqual((extHost.browserTabs[0].icon as { id: string }).id, 'globe');
240
241
extHost.$onDidChangeBrowserTabState(createDto({ id: 'b1', favicon: 'https://example.com/new.ico' }));
242
assert.strictEqual(String(extHost.browserTabs[0].icon), 'https://example.com/new.ico');
243
});
244
245
test('icon reverts to globe when favicon is cleared', () => {
246
const extHost = createExtHostBrowsers();
247
extHost.$onDidOpenBrowserTab(createDto({ id: 'b1', favicon: 'https://example.com/icon.ico' }));
248
assert.strictEqual(String(extHost.browserTabs[0].icon), 'https://example.com/icon.ico');
249
250
extHost.$onDidChangeBrowserTabState(createDto({ id: 'b1', favicon: undefined }));
251
assert.strictEqual((extHost.browserTabs[0].icon as { id: string }).id, 'globe');
252
});
253
254
// #endregion
255
256
// #region BrowserTab readonly properties
257
258
test('tab properties are not directly writable', () => {
259
const extHost = createExtHostBrowsers();
260
extHost.$onDidOpenBrowserTab(createDto({ id: 'b1', url: 'https://example.com', title: 'Title' }));
261
const tab = extHost.browserTabs[0];
262
263
// Attempting to assign to getter-only properties should either throw or be silently ignored
264
assert.throws(() => { (tab as unknown as Record<string, unknown>).url = 'https://hacked.com'; });
265
assert.throws(() => { (tab as unknown as Record<string, unknown>).title = 'Hacked'; });
266
assert.strictEqual(tab.url, 'https://example.com');
267
assert.strictEqual(tab.title, 'Title');
268
});
269
270
test('startCDPSession calls $startCDPSession on proxy', async () => {
271
let capturedBrowserId: string | undefined;
272
const extHost = createExtHostBrowsers({
273
$startCDPSession: (_sessionId: string, browserId: string) => {
274
capturedBrowserId = browserId;
275
return Promise.resolve();
276
},
277
});
278
279
extHost.$onDidOpenBrowserTab(createDto({ id: 'b1' }));
280
const session = await extHost.browserTabs[0].startCDPSession();
281
282
assert.ok(session);
283
assert.strictEqual(capturedBrowserId, 'b1');
284
});
285
286
test('sendMessage validates message structure', async () => {
287
const extHost = createExtHostBrowsers();
288
extHost.$onDidOpenBrowserTab(createDto({ id: 'b1' }));
289
const session = await extHost.browserTabs[0].startCDPSession();
290
291
// Valid message succeeds
292
await session.sendMessage({ id: 1, method: 'Page.enable' });
293
294
// Invalid messages are rejected
295
await assert.rejects(Promise.resolve().then(() => session.sendMessage(null as never)), /must be an object/);
296
await assert.rejects(Promise.resolve().then(() => session.sendMessage({ method: 'Foo' } as never)), /numeric id/);
297
await assert.rejects(Promise.resolve().then(() => session.sendMessage({ id: 1 } as never)), /method string/);
298
});
299
300
test('sendMessage forwards valid message to proxy', async () => {
301
const sentMessages: unknown[] = [];
302
const extHost = createExtHostBrowsers({
303
$sendCDPMessage: (_sid: string, message: unknown) => {
304
sentMessages.push(message);
305
return Promise.resolve();
306
},
307
});
308
309
extHost.$onDidOpenBrowserTab(createDto({ id: 'b1' }));
310
const session = await extHost.browserTabs[0].startCDPSession();
311
await session.sendMessage({ id: 1, method: 'Page.enable', params: {} });
312
313
assert.strictEqual(sentMessages.length, 1);
314
assert.deepStrictEqual(sentMessages[0], { id: 1, method: 'Page.enable', params: {}, sessionId: undefined });
315
});
316
317
test('sendMessage rejects after session is closed', async () => {
318
const extHost = createExtHostBrowsers();
319
extHost.$onDidOpenBrowserTab(createDto({ id: 'b1' }));
320
const session = await extHost.browserTabs[0].startCDPSession();
321
322
await session.close();
323
await assert.rejects(Promise.resolve().then(() => session.sendMessage({ id: 1, method: 'Foo' })), /closed/);
324
});
325
326
test('$onCDPSessionMessage delivers to correct session', async () => {
327
const capturedIds: string[] = [];
328
const extHost = createExtHostBrowsers({
329
$startCDPSession: (sessionId: string) => {
330
capturedIds.push(sessionId);
331
return Promise.resolve();
332
},
333
});
334
335
extHost.$onDidOpenBrowserTab(createDto({ id: 'b1' }));
336
const session1 = await extHost.browserTabs[0].startCDPSession();
337
const session2 = await extHost.browserTabs[0].startCDPSession();
338
339
const received1: unknown[] = [];
340
const received2: unknown[] = [];
341
store.add(session1.onDidReceiveMessage(m => received1.push(m)));
342
store.add(session2.onDidReceiveMessage(m => received2.push(m)));
343
344
extHost.$onCDPSessionMessage(capturedIds[1], { id: 1, result: { data: 'hello' } });
345
346
assert.deepStrictEqual(received1, []);
347
assert.deepStrictEqual(received2, [{ id: 1, result: { data: 'hello' } }]);
348
});
349
350
test('$onCDPSessionClosed fires onDidClose', async () => {
351
const capturedIds: string[] = [];
352
const extHost = createExtHostBrowsers({
353
$startCDPSession: (sessionId: string) => {
354
capturedIds.push(sessionId);
355
return Promise.resolve();
356
},
357
});
358
359
extHost.$onDidOpenBrowserTab(createDto({ id: 'b1' }));
360
const session = await extHost.browserTabs[0].startCDPSession();
361
362
let closeFired = false;
363
store.add(session.onDidClose(() => { closeFired = true; }));
364
365
extHost.$onCDPSessionClosed(capturedIds[0]);
366
assert.ok(closeFired);
367
});
368
369
// #endregion
370
371
// #region Reference stability
372
373
test('tab object reference is stable across updates', () => {
374
const extHost = createExtHostBrowsers();
375
extHost.$onDidOpenBrowserTab(createDto({ id: 'b1', url: 'https://old.com', title: 'Old' }));
376
const tabBefore = extHost.browserTabs[0];
377
378
extHost.$onDidChangeBrowserTabState(createDto({ id: 'b1', url: 'https://new.com', title: 'New' }));
379
const tabAfter = extHost.browserTabs[0];
380
381
assert.strictEqual(tabBefore, tabAfter);
382
assert.strictEqual(tabAfter.url, 'https://new.com');
383
});
384
385
test('openBrowserTab returns same reference as browserTabs entry', async () => {
386
const extHost = createExtHostBrowsers({
387
$openBrowserTab: () => Promise.resolve(createDto({ id: 'ref-test' })),
388
});
389
390
const returned = await extHost.openBrowserTab('https://example.com');
391
const fromArray = extHost.browserTabs[0];
392
393
assert.strictEqual(returned, fromArray);
394
});
395
396
// #endregion
397
398
// #region Multiple tabs tracked independently
399
400
test('closing one tab does not affect others', () => {
401
const extHost = createExtHostBrowsers();
402
extHost.$onDidOpenBrowserTab(createDto({ id: 'b1', url: 'https://one.com' }));
403
extHost.$onDidOpenBrowserTab(createDto({ id: 'b2', url: 'https://two.com' }));
404
extHost.$onDidOpenBrowserTab(createDto({ id: 'b3', url: 'https://three.com' }));
405
406
extHost.$onDidCloseBrowserTab('b2');
407
408
assert.strictEqual(extHost.browserTabs.length, 2);
409
assert.deepStrictEqual(extHost.browserTabs.map(t => t.url), ['https://one.com', 'https://three.com']);
410
});
411
412
test('closing active tab clears activeBrowserTab', () => {
413
const extHost = createExtHostBrowsers();
414
const dto = createDto({ id: 'b1' });
415
extHost.$onDidOpenBrowserTab(dto);
416
extHost.$onDidChangeActiveBrowserTab('b1');
417
assert.ok(extHost.activeBrowserTab);
418
419
extHost.$onDidCloseBrowserTab('b1');
420
assert.strictEqual(extHost.activeBrowserTab, undefined);
421
});
422
423
// #endregion
424
});
425
426