Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/test/browser/componentFixtures/fixtureUtils.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
// This should be the only place that is allowed to import from @vscode/component-explorer
7
// eslint-disable-next-line local/code-import-patterns
8
import { defineFixture, defineFixtureGroup, defineFixtureVariants } from '@vscode/component-explorer';
9
import { DisposableStore, toDisposable } from '../../../../base/common/lifecycle.js';
10
import { URI } from '../../../../base/common/uri.js';
11
// eslint-disable-next-line local/code-import-patterns
12
import '../../../../../../build/vite/style.css';
13
import '../../../browser/media/style.css';
14
15
// Theme
16
import { IEnvironmentService } from '../../../../platform/environment/common/environment.js';
17
import { Registry } from '../../../../platform/registry/common/platform.js';
18
import { getIconsStyleSheet } from '../../../../platform/theme/browser/iconsStyleSheet.js';
19
import { ColorScheme } from '../../../../platform/theme/common/theme.js';
20
import { IColorTheme, IThemeService, IThemingRegistry, Extensions as ThemingExtensions } from '../../../../platform/theme/common/themeService.js';
21
import { generateColorThemeCSS } from '../../../services/themes/browser/colorThemeCss.js';
22
import { ColorThemeData } from '../../../services/themes/common/colorThemeData.js';
23
import { COLOR_THEME_DARK_INITIAL_COLORS, COLOR_THEME_LIGHT_INITIAL_COLORS } from '../../../services/themes/common/workbenchThemeService.js';
24
25
// Instantiation
26
import { SyncDescriptor } from '../../../../platform/instantiation/common/descriptors.js';
27
import { ServiceIdentifier } from '../../../../platform/instantiation/common/instantiation.js';
28
import { ServiceCollection } from '../../../../platform/instantiation/common/serviceCollection.js';
29
import { TestInstantiationService } from '../../../../platform/instantiation/test/common/instantiationServiceMock.js';
30
31
// Test service implementations
32
import { Emitter, Event } from '../../../../base/common/event.js';
33
import { mock } from '../../../../base/test/common/mock.js';
34
import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js';
35
import { IInlineCompletionsService, InlineCompletionsService } from '../../../../editor/browser/services/inlineCompletionsService.js';
36
import { ILanguageService } from '../../../../editor/common/languages/language.js';
37
import { ILanguageConfigurationService } from '../../../../editor/common/languages/languageConfigurationRegistry.js';
38
import { IEditorWorkerService } from '../../../../editor/common/services/editorWorker.js';
39
import { ILanguageFeatureDebounceService, LanguageFeatureDebounceService } from '../../../../editor/common/services/languageFeatureDebounce.js';
40
import { ILanguageFeaturesService } from '../../../../editor/common/services/languageFeatures.js';
41
import { LanguageFeaturesService } from '../../../../editor/common/services/languageFeaturesService.js';
42
import { LanguageService } from '../../../../editor/common/services/languageService.js';
43
import { IModelService } from '../../../../editor/common/services/model.js';
44
import { ModelService } from '../../../../editor/common/services/modelService.js';
45
import { ITextResourcePropertiesService } from '../../../../editor/common/services/textResourceConfiguration.js';
46
import { ITreeSitterLibraryService } from '../../../../editor/common/services/treeSitter/treeSitterLibraryService.js';
47
import { ICodeLensCache } from '../../../../editor/contrib/codelens/browser/codeLensCache.js';
48
import { TestCodeEditorService, TestCommandService } from '../../../../editor/test/browser/editorTestServices.js';
49
import { TestLanguageConfigurationService } from '../../../../editor/test/common/modes/testLanguageConfigurationService.js';
50
import { TestEditorWorkerService } from '../../../../editor/test/common/services/testEditorWorkerService.js';
51
import { TestTextResourcePropertiesService } from '../../../../editor/test/common/services/testTextResourcePropertiesService.js';
52
import { TestTreeSitterLibraryService } from '../../../../editor/test/common/services/testTreeSitterLibraryService.js';
53
import { IAccessibilityService } from '../../../../platform/accessibility/common/accessibility.js';
54
import { TestAccessibilityService } from '../../../../platform/accessibility/test/common/testAccessibilityService.js';
55
import { IActionViewItemService, NullActionViewItemService } from '../../../../platform/actions/browser/actionViewItemService.js';
56
import { IMenuService } from '../../../../platform/actions/common/actions.js';
57
import { IClipboardService } from '../../../../platform/clipboard/common/clipboardService.js';
58
import { TestClipboardService } from '../../../../platform/clipboard/test/common/testClipboardService.js';
59
import { ICommandService } from '../../../../platform/commands/common/commands.js';
60
import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
61
import { TestConfigurationService } from '../../../../platform/configuration/test/common/testConfigurationService.js';
62
import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';
63
import { IContextMenuService, IContextViewService } from '../../../../platform/contextview/browser/contextView.js';
64
import { IDataChannelService, NullDataChannelService } from '../../../../platform/dataChannel/common/dataChannel.js';
65
import { IDefaultAccountService } from '../../../../platform/defaultAccount/common/defaultAccount.js';
66
import { IDialogService } from '../../../../platform/dialogs/common/dialogs.js';
67
import { TestDialogService } from '../../../../platform/dialogs/test/common/testDialogService.js';
68
import { IHoverService } from '../../../../platform/hover/browser/hover.js';
69
import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js';
70
import { MockContextKeyService, MockKeybindingService } from '../../../../platform/keybinding/test/common/mockKeybindingService.js';
71
import { ILabelService } from '../../../../platform/label/common/label.js';
72
import { ILoggerService, ILogService, NullLoggerService, NullLogService } from '../../../../platform/log/common/log.js';
73
import { INotificationService } from '../../../../platform/notification/common/notification.js';
74
import { TestNotificationService } from '../../../../platform/notification/test/common/testNotificationService.js';
75
import { IOpenerService } from '../../../../platform/opener/common/opener.js';
76
import { NullOpenerService } from '../../../../platform/opener/test/common/nullOpenerService.js';
77
import { IApplicationSharedStorageValueChangeEvent, IApplicationStorageValueChangeEvent, IProfileStorageValueChangeEvent, IStorageEntry, IStorageService, IStorageTargetChangeEvent, IStorageValueChangeEvent, IWillSaveStateEvent, IWorkspaceStorageValueChangeEvent, StorageScope, StorageTarget, WillSaveStateReason } from '../../../../platform/storage/common/storage.js';
78
import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js';
79
import { NullTelemetryServiceShape } from '../../../../platform/telemetry/common/telemetryUtils.js';
80
import { TestThemeService } from '../../../../platform/theme/test/common/testThemeService.js';
81
import { IUndoRedoService } from '../../../../platform/undoRedo/common/undoRedo.js';
82
import { UndoRedoService } from '../../../../platform/undoRedo/common/undoRedoService.js';
83
import { IUserDataProfile } from '../../../../platform/userDataProfile/common/userDataProfile.js';
84
import { IUserInteractionService, MockUserInteractionService } from '../../../../platform/userInteraction/browser/userInteractionService.js';
85
import { IAnyWorkspaceIdentifier } from '../../../../platform/workspace/common/workspace.js';
86
import { TestMenuService } from '../workbenchTestServices.js';
87
import { IAccessibilitySignalService } from '../../../../platform/accessibilitySignal/browser/accessibilitySignalService.js';
88
import { ITextModelService } from '../../../../editor/common/services/resolverService.js';
89
// eslint-disable-next-line local/code-import-patterns
90
import { IAgentFeedbackService } from '../../../../sessions/contrib/agentFeedback/browser/agentFeedbackService.js';
91
import { IChatEditingService } from '../../../contrib/chat/common/editing/chatEditingService.js';
92
// eslint-disable-next-line local/code-import-patterns
93
import { ISessionsManagementService } from '../../../../sessions/services/sessions/common/sessionsManagement.js';
94
// eslint-disable-next-line local/code-import-patterns
95
import { ICodeReviewService, CodeReviewStateKind, PRReviewStateKind } from '../../../../sessions/contrib/codeReview/browser/codeReviewService.js';
96
import { constObservable } from '../../../../base/common/observable.js';
97
98
// Editor
99
import { ITextModel } from '../../../../editor/common/model.js';
100
101
import './fixtures.css';
102
103
// Import color registrations to ensure colors are available
104
import { IdleDeadline, installFakeRunWhenIdle } from '../../../../base/common/async.js';
105
import { AsyncSchedulerProcessor, TimeTravelScheduler, captureGlobalTimeApi, createLoggingTimeApi, createVirtualTimeApi, overwriteGlobalTimeApi } from '../../../../base/test/common/timeTravelScheduler.js';
106
import '../../../../platform/theme/common/colors/baseColors.js';
107
import '../../../../platform/theme/common/colors/editorColors.js';
108
import '../../../../platform/theme/common/colors/listColors.js';
109
import '../../../../platform/theme/common/colors/miscColors.js';
110
import '../../../common/theme.js';
111
112
// eslint-disable-next-line local/code-import-patterns
113
import sourceMapSupport from 'source-map-support';
114
sourceMapSupport.install({
115
environment: 'browser',
116
handleUncaughtExceptions: false,
117
retrieveSourceMap: (source: string) => {
118
const mapUrl = source + '.map';
119
try {
120
const xhr = new XMLHttpRequest();
121
xhr.open('GET', mapUrl, false);
122
xhr.send();
123
if (xhr.status === 200) {
124
return { url: null as never, map: xhr.responseText };
125
}
126
} catch { }
127
return null;
128
},
129
});
130
131
/**
132
* A storage service that never stores anything and always returns the default/fallback value.
133
* This is useful for fixtures where we want consistent behavior without persisted state.
134
*/
135
class NullStorageService implements IStorageService {
136
137
declare readonly _serviceBrand: undefined;
138
139
private readonly _onDidChangeValue = new Emitter<IStorageValueChangeEvent>();
140
onDidChangeValue(scope: StorageScope.WORKSPACE, key: string | undefined, disposable: DisposableStore): Event<IWorkspaceStorageValueChangeEvent>;
141
onDidChangeValue(scope: StorageScope.PROFILE, key: string | undefined, disposable: DisposableStore): Event<IProfileStorageValueChangeEvent>;
142
onDidChangeValue(scope: StorageScope.APPLICATION, key: string | undefined, disposable: DisposableStore): Event<IApplicationStorageValueChangeEvent>;
143
onDidChangeValue(scope: StorageScope.APPLICATION_SHARED, key: string | undefined, disposable: DisposableStore): Event<IApplicationSharedStorageValueChangeEvent>;
144
onDidChangeValue(scope: StorageScope, key: string | undefined, disposable: DisposableStore): Event<IStorageValueChangeEvent> {
145
return Event.filter(this._onDidChangeValue.event, e => e.scope === scope && (key === undefined || e.key === key), disposable);
146
}
147
148
private readonly _onDidChangeTarget = new Emitter<IStorageTargetChangeEvent>();
149
readonly onDidChangeTarget: Event<IStorageTargetChangeEvent> = this._onDidChangeTarget.event;
150
151
private readonly _onWillSaveState = new Emitter<IWillSaveStateEvent>();
152
readonly onWillSaveState: Event<IWillSaveStateEvent> = this._onWillSaveState.event;
153
154
get(key: string, scope: StorageScope, fallbackValue: string): string;
155
get(key: string, scope: StorageScope, fallbackValue?: string): string | undefined;
156
get(_key: string, _scope: StorageScope, fallbackValue?: string): string | undefined {
157
return fallbackValue;
158
}
159
160
getBoolean(key: string, scope: StorageScope, fallbackValue: boolean): boolean;
161
getBoolean(key: string, scope: StorageScope, fallbackValue?: boolean): boolean | undefined;
162
getBoolean(_key: string, _scope: StorageScope, fallbackValue?: boolean): boolean | undefined {
163
return fallbackValue;
164
}
165
166
getNumber(key: string, scope: StorageScope, fallbackValue: number): number;
167
getNumber(key: string, scope: StorageScope, fallbackValue?: number): number | undefined;
168
getNumber(_key: string, _scope: StorageScope, fallbackValue?: number): number | undefined {
169
return fallbackValue;
170
}
171
172
getObject<T extends object>(key: string, scope: StorageScope, fallbackValue: T): T;
173
getObject<T extends object>(key: string, scope: StorageScope, fallbackValue?: T): T | undefined;
174
getObject<T extends object>(_key: string, _scope: StorageScope, fallbackValue?: T): T | undefined {
175
return fallbackValue;
176
}
177
178
store(_key: string, _value: string | boolean | number | undefined | null, _scope: StorageScope, _target: StorageTarget): void {
179
// no-op
180
}
181
182
storeAll(_entries: IStorageEntry[], _external: boolean): void {
183
// no-op
184
}
185
186
remove(_key: string, _scope: StorageScope): void {
187
// no-op
188
}
189
190
isNew(_scope: StorageScope): boolean {
191
return true;
192
}
193
194
flush(_reason?: WillSaveStateReason): Promise<void> {
195
return Promise.resolve();
196
}
197
198
optimize(_scope: StorageScope): Promise<void> {
199
return Promise.resolve();
200
}
201
202
log(): void {
203
// no-op
204
}
205
206
keys(_scope: StorageScope, _target: StorageTarget): string[] {
207
return [];
208
}
209
210
switch(): Promise<void> {
211
return Promise.resolve();
212
}
213
214
hasScope(_scope: IAnyWorkspaceIdentifier | IUserDataProfile): boolean {
215
return false;
216
}
217
}
218
219
220
// ============================================================================
221
// Themes
222
// ============================================================================
223
224
const themingRegistry = Registry.as<IThemingRegistry>(ThemingExtensions.ThemingContribution);
225
const mockEnvironmentService: IEnvironmentService = Object.create(null);
226
227
export const darkTheme = ColorThemeData.createUnloadedThemeForThemeType(
228
ColorScheme.DARK,
229
COLOR_THEME_DARK_INITIAL_COLORS
230
);
231
232
export const lightTheme = ColorThemeData.createUnloadedThemeForThemeType(
233
ColorScheme.LIGHT,
234
COLOR_THEME_LIGHT_INITIAL_COLORS
235
);
236
237
let globalStyleSheet: CSSStyleSheet | undefined;
238
let iconsStyleSheetCache: CSSStyleSheet | undefined;
239
let darkThemeStyleSheet: CSSStyleSheet | undefined;
240
let lightThemeStyleSheet: CSSStyleSheet | undefined;
241
242
function getGlobalStyleSheet(): CSSStyleSheet {
243
if (!globalStyleSheet) {
244
globalStyleSheet = new CSSStyleSheet();
245
const globalRules: string[] = [];
246
for (const sheet of Array.from(document.styleSheets)) {
247
try {
248
for (const rule of Array.from(sheet.cssRules)) {
249
globalRules.push(rule.cssText);
250
}
251
} catch {
252
// Cross-origin stylesheets can't be read
253
}
254
}
255
globalStyleSheet.replaceSync(globalRules.join('\n'));
256
}
257
return globalStyleSheet;
258
}
259
260
function getIconsStyleSheetCached(): CSSStyleSheet {
261
if (!iconsStyleSheetCache) {
262
iconsStyleSheetCache = new CSSStyleSheet();
263
const iconsSheet = getIconsStyleSheet(undefined);
264
iconsStyleSheetCache.replaceSync(iconsSheet.getCSS() as string);
265
iconsSheet.dispose();
266
}
267
return iconsStyleSheetCache;
268
}
269
270
function getThemeStyleSheet(theme: ColorThemeData): CSSStyleSheet {
271
const isDark = theme.type === ColorScheme.DARK;
272
if (isDark && darkThemeStyleSheet) {
273
return darkThemeStyleSheet;
274
}
275
if (!isDark && lightThemeStyleSheet) {
276
return lightThemeStyleSheet;
277
}
278
279
const scopeSelector = '.' + theme.classNames[0];
280
const sheet = new CSSStyleSheet();
281
const css = generateColorThemeCSS(
282
theme,
283
scopeSelector,
284
themingRegistry.getThemingParticipants(),
285
mockEnvironmentService
286
);
287
sheet.replaceSync(css.code);
288
289
if (isDark) {
290
darkThemeStyleSheet = sheet;
291
} else {
292
lightThemeStyleSheet = sheet;
293
}
294
return sheet;
295
}
296
297
let globalStylesInstalled = false;
298
299
function installGlobalStyles(): void {
300
if (globalStylesInstalled) {
301
return;
302
}
303
globalStylesInstalled = true;
304
document.adoptedStyleSheets = [
305
...document.adoptedStyleSheets,
306
getGlobalStyleSheet(),
307
getIconsStyleSheetCached(),
308
getThemeStyleSheet(darkTheme),
309
getThemeStyleSheet(lightTheme),
310
];
311
}
312
313
export function setupTheme(container: HTMLElement, theme: ColorThemeData): void {
314
installGlobalStyles();
315
container.classList.add('monaco-workbench', getPlatformClass(), 'disable-animations', ...theme.classNames);
316
}
317
318
function getPlatformClass(): string {
319
const alwaysUseMac = true;
320
if (alwaysUseMac) {
321
return 'mac';
322
} else {
323
const ua = navigator.userAgent;
324
if (ua.includes('Macintosh')) {
325
return 'mac';
326
}
327
if (ua.includes('Linux')) {
328
return 'linux';
329
}
330
return 'windows';
331
}
332
}
333
334
335
// ============================================================================
336
// Services
337
// ============================================================================
338
339
export interface ServiceRegistration {
340
define<T>(id: ServiceIdentifier<T>, ctor: new (...args: never[]) => T): void;
341
defineInstance<T>(id: ServiceIdentifier<T>, instance: T): void;
342
/** Like defineInstance but accepts a partial mock - provides type checking on provided properties */
343
definePartialInstance<T>(id: ServiceIdentifier<T>, instance: Partial<T>): void;
344
}
345
346
export interface CreateServicesOptions {
347
/**
348
* The color theme to use for the theme service.
349
*/
350
colorTheme?: IColorTheme;
351
/**
352
* Additional services to register after the base editor services.
353
*/
354
additionalServices?: (registration: ServiceRegistration) => void;
355
}
356
357
/**
358
* Creates a TestInstantiationService with all services needed for CodeEditorWidget.
359
* Additional services can be registered via the options callback.
360
*/
361
export function createEditorServices(disposables: DisposableStore, options?: CreateServicesOptions): TestInstantiationService {
362
const services = new ServiceCollection();
363
// eslint-disable-next-line @typescript-eslint/no-explicit-any
364
const serviceIdentifiers: ServiceIdentifier<any>[] = [];
365
366
// eslint-disable-next-line @typescript-eslint/no-explicit-any
367
const define = <T>(id: ServiceIdentifier<T>, ctor: new (...args: any[]) => T) => {
368
if (!services.has(id)) {
369
services.set(id, new SyncDescriptor(ctor));
370
}
371
serviceIdentifiers.push(id);
372
};
373
374
const defineInstance = <T>(id: ServiceIdentifier<T>, instance: T) => {
375
if (!services.has(id)) {
376
services.set(id, instance);
377
}
378
serviceIdentifiers.push(id);
379
};
380
381
const definePartialInstance = <T>(id: ServiceIdentifier<T>, instance: Partial<T>) => {
382
defineInstance(id, instance as T);
383
};
384
385
// Base editor services
386
define(IAccessibilityService, TestAccessibilityService);
387
define(IKeybindingService, MockKeybindingService);
388
define(IClipboardService, TestClipboardService);
389
define(IEditorWorkerService, TestEditorWorkerService);
390
defineInstance(IOpenerService, NullOpenerService);
391
define(INotificationService, TestNotificationService);
392
define(IDialogService, TestDialogService);
393
define(IUndoRedoService, UndoRedoService);
394
define(ILanguageService, LanguageService);
395
define(ILanguageConfigurationService, TestLanguageConfigurationService);
396
define(IConfigurationService, TestConfigurationService);
397
define(ITextResourcePropertiesService, TestTextResourcePropertiesService);
398
defineInstance(IStorageService, new NullStorageService());
399
if (options?.colorTheme) {
400
defineInstance(IThemeService, new TestThemeService(options.colorTheme));
401
} else {
402
define(IThemeService, TestThemeService);
403
}
404
define(ILogService, NullLogService);
405
define(IModelService, ModelService);
406
define(ICodeEditorService, TestCodeEditorService);
407
define(IContextKeyService, MockContextKeyService);
408
define(ICommandService, TestCommandService);
409
define(ITelemetryService, NullTelemetryServiceShape);
410
define(ILoggerService, NullLoggerService);
411
define(IDataChannelService, NullDataChannelService);
412
define(IEnvironmentService, class extends mock<IEnvironmentService>() {
413
declare readonly _serviceBrand: undefined;
414
override isBuilt: boolean = true;
415
override isExtensionDevelopment: boolean = false;
416
});
417
define(ILanguageFeatureDebounceService, LanguageFeatureDebounceService);
418
define(ILanguageFeaturesService, LanguageFeaturesService);
419
define(ITreeSitterLibraryService, TestTreeSitterLibraryService);
420
define(IInlineCompletionsService, InlineCompletionsService);
421
defineInstance(ICodeLensCache, {
422
_serviceBrand: undefined,
423
put: () => { },
424
get: () => undefined,
425
delete: () => { },
426
});
427
defineInstance(IHoverService, {
428
_serviceBrand: undefined,
429
showDelayedHover: () => undefined,
430
setupDelayedHover: () => ({ dispose: () => { } }),
431
setupDelayedHoverAtMouse: () => ({ dispose: () => { } }),
432
showInstantHover: () => undefined,
433
hideHover: () => { },
434
showAndFocusLastHover: () => { },
435
setupManagedHover: () => ({ dispose: () => { }, show: () => { }, hide: () => { }, update: () => { } }),
436
showManagedHover: () => { },
437
});
438
defineInstance(IDefaultAccountService, {
439
_serviceBrand: undefined,
440
onDidChangeDefaultAccount: new Emitter<null>().event,
441
onDidChangePolicyData: new Emitter<null>().event,
442
policyData: null,
443
currentDefaultAccount: null,
444
copilotTokenInfo: null,
445
onDidChangeCopilotTokenInfo: new Emitter<null>().event,
446
getDefaultAccount: async () => null,
447
getDefaultAccountAuthenticationProvider: () => ({ id: 'test', name: 'Test', scopes: [], enterprise: false }),
448
setDefaultAccountProvider: () => { },
449
refresh: async () => null,
450
signIn: async () => null,
451
signOut: async () => { },
452
});
453
454
// User interaction service with focus simulation enabled (all elements appear focused in fixtures)
455
defineInstance(IUserInteractionService, new MockUserInteractionService(true, false));
456
457
defineInstance(IAccessibilitySignalService, {
458
_serviceBrand: undefined,
459
playSignal: async () => { },
460
playSignals: async () => { },
461
playSignalLoop: () => ({ dispose: () => { } }),
462
getEnabledState: () => ({ value: false, onDidChange: Event.None, onChange: () => ({ dispose: () => { } }) }),
463
getDelayMs: () => 0,
464
playSound: async () => { },
465
isSoundEnabled: () => false,
466
isAnnouncementEnabled: () => false,
467
onSoundEnabledChanged: () => Event.None,
468
});
469
470
definePartialInstance(ITextModelService, {
471
_serviceBrand: undefined,
472
registerTextModelContentProvider: () => ({ dispose: () => { } }),
473
canHandleResource: () => false,
474
// eslint-disable-next-line local/code-no-any-casts, @typescript-eslint/no-explicit-any
475
createModelReference: async () => ({ object: { textEditorModel: null }, dispose() { } } as any),
476
});
477
478
defineInstance(IAgentFeedbackService, {
479
_serviceBrand: undefined,
480
onDidChangeFeedback: Event.None,
481
onDidChangeNavigation: Event.None,
482
addFeedback: () => undefined!,
483
removeFeedback: () => { },
484
updateFeedback: () => { },
485
getFeedback: () => [],
486
getMostRecentSessionForResource: () => undefined,
487
revealFeedback: async () => { },
488
revealSessionComment: async () => { },
489
getNextFeedback: () => undefined,
490
getNextNavigableItem: () => undefined,
491
setNavigationAnchor: () => { },
492
getNavigationBearing: () => ({ activeIdx: -1, totalCount: 0 }),
493
clearFeedback: () => { },
494
addFeedbackAndSubmit: async () => { },
495
});
496
497
definePartialInstance(IChatEditingService, {
498
_serviceBrand: undefined,
499
editingSessionsObs: constObservable([]),
500
startOrContinueGlobalEditingSession: () => undefined!,
501
getEditingSession: () => undefined,
502
});
503
504
definePartialInstance(ISessionsManagementService, {
505
_serviceBrand: undefined,
506
activeSession: constObservable(undefined),
507
getSession: () => undefined,
508
getSessions: () => [],
509
});
510
511
definePartialInstance(ICodeReviewService, {
512
_serviceBrand: undefined,
513
getReviewState: () => constObservable({ kind: CodeReviewStateKind.Idle }),
514
getPRReviewState: () => constObservable({ kind: PRReviewStateKind.None }),
515
hasReview: () => false,
516
requestReview: () => { },
517
removeComment: () => { },
518
updateComment: () => { },
519
dismissReview: () => { },
520
resolvePRReviewThread: async () => { },
521
markPRReviewCommentConverted: () => { },
522
});
523
524
// Allow additional services to override defaults
525
options?.additionalServices?.({
526
// eslint-disable-next-line @typescript-eslint/no-explicit-any
527
define: <T>(id: ServiceIdentifier<T>, ctor: new (...args: any[]) => T) => {
528
services.set(id, new SyncDescriptor(ctor));
529
serviceIdentifiers.push(id);
530
},
531
defineInstance: <T>(id: ServiceIdentifier<T>, instance: T) => {
532
services.set(id, instance);
533
serviceIdentifiers.push(id);
534
},
535
definePartialInstance: <T>(id: ServiceIdentifier<T>, instance: Partial<T>) => {
536
services.set(id, instance as T);
537
serviceIdentifiers.push(id);
538
},
539
});
540
541
const instantiationService = disposables.add(new TestInstantiationService(services, true));
542
543
disposables.add(toDisposable(() => {
544
for (const id of serviceIdentifiers) {
545
const instanceOrDescriptor = services.get(id);
546
if (typeof instanceOrDescriptor?.dispose === 'function') {
547
instanceOrDescriptor.dispose();
548
}
549
}
550
}));
551
552
return instantiationService;
553
}
554
555
/**
556
* Registers additional services needed by workbench components (merge editor, etc.).
557
* Use with createEditorServices additionalServices option.
558
*/
559
export function registerWorkbenchServices(registration: ServiceRegistration): void {
560
registration.defineInstance(IContextMenuService, {
561
showContextMenu: () => { },
562
onDidShowContextMenu: () => ({ dispose: () => { } }),
563
onDidHideContextMenu: () => ({ dispose: () => { } }),
564
_serviceBrand: undefined,
565
});
566
567
registration.defineInstance(IContextViewService, {
568
showContextView: () => ({ close: () => { } }),
569
hideContextView: () => { },
570
getContextViewElement: () => { throw new Error('Not implemented'); },
571
layout: () => { },
572
anchorAlignment: 0,
573
_serviceBrand: undefined,
574
});
575
576
registration.defineInstance(ILabelService, {
577
getUriLabel: (uri: URI) => uri.path,
578
getUriBasenameLabel: (uri: URI) => uri.path.split('/').pop() ?? '',
579
getWorkspaceLabel: () => '',
580
getHostLabel: () => '',
581
getSeparator: () => '/',
582
registerFormatter: () => ({ dispose: () => { } }),
583
onDidChangeFormatters: () => ({ dispose: () => { } }),
584
registerCachedFormatter: () => ({ dispose: () => { } }),
585
_serviceBrand: undefined,
586
getHostTooltip: () => '',
587
});
588
589
registration.define(IMenuService, TestMenuService);
590
registration.define(IActionViewItemService, NullActionViewItemService);
591
}
592
593
594
// ============================================================================
595
// Text Models
596
// ============================================================================
597
598
/**
599
* Creates a text model using the ModelService.
600
*/
601
export function createTextModel(
602
instantiationService: TestInstantiationService,
603
text: string,
604
uri: URI,
605
languageId?: string
606
): ITextModel {
607
const modelService = instantiationService.get(IModelService);
608
const languageService = instantiationService.get(ILanguageService);
609
const languageSelection = languageId ? languageService.createById(languageId) : null;
610
return modelService.createModel(text, languageSelection, uri);
611
}
612
613
614
// ============================================================================
615
// Fixture Adapters
616
// ============================================================================
617
618
export interface ThemedFixtureGroupLabels {
619
readonly kind?: 'screenshot' | 'animated';
620
readonly blocksCi?: true;
621
readonly flaky?: true;
622
}
623
624
function resolveLabels(labels: ThemedFixtureGroupLabels | undefined): string[] {
625
const result: string[] = [];
626
if (labels?.kind === 'screenshot') {
627
result.push('.screenshot');
628
} else if (labels?.kind === 'animated') {
629
result.push('animated');
630
}
631
if (labels?.blocksCi) {
632
result.push('blocks-ci');
633
}
634
if (labels?.flaky) {
635
result.push('flaky');
636
}
637
return result;
638
}
639
640
export interface ComponentFixtureContext {
641
container: HTMLElement;
642
disposableStore: DisposableStore;
643
theme: ColorThemeData;
644
}
645
646
export interface ComponentFixtureOptions {
647
render: (context: ComponentFixtureContext) => void | Promise<void>;
648
labels?: ThemedFixtureGroupLabels;
649
}
650
651
type ThemedFixtures = ReturnType<typeof defineFixtureVariants>;
652
653
// Permanent logging layer that detects real timer API usage.
654
// Includes handler source for identification since bundled stack traces are not useful.
655
const realTimeApi = captureGlobalTimeApi();
656
const loggingTimeApi = createLoggingTimeApi(realTimeApi, (name, stack, handler) => {
657
const handlerStr = typeof handler === 'function' ? handler.toString().slice(0, 500) : String(handler);
658
console.warn(`[ComponentFixture] Real ${name} called outside of virtual time.\nHandler: ${handlerStr}\nStack: ${stack}`);
659
});
660
overwriteGlobalTimeApi(loggingTimeApi);
661
662
/**
663
* Creates Dark and Light fixture variants from a single render function.
664
* The render function receives a context with container and disposableStore.
665
*
666
* Note: If render returns a Promise, the async work will run in background.
667
* Component-explorer waits 2 animation frames after sync render returns,
668
* which should be sufficient for most async setup, but timing is not guaranteed.
669
*/
670
export function defineComponentFixture(options: ComponentFixtureOptions): ThemedFixtures {
671
const createFixture = (theme: typeof darkTheme | typeof lightTheme) => defineFixture({
672
isolation: 'none',
673
displayMode: { type: 'component' },
674
background: theme === darkTheme ? 'dark' : 'light',
675
render: async (container: HTMLElement, context) => {
676
const disposableStore = context.addDisposable(new DisposableStore());
677
678
const schedulerStore = disposableStore.add(new DisposableStore());
679
const scheduler = new TimeTravelScheduler(Date.now());
680
const p = schedulerStore.add(new AsyncSchedulerProcessor(scheduler, {
681
maxTaskCount: 100,
682
realTimeApi,
683
}));
684
685
async function actualRender() {
686
687
setupTheme(container, theme);
688
689
const virtualTimeApi = createVirtualTimeApi(scheduler, { fakeRequestAnimationFrame: true });
690
schedulerStore.add(overwriteGlobalTimeApi(virtualTimeApi));
691
disposableStore.add(installFakeRunWhenIdle((_targetWindow, callback, _timeout?) => {
692
return scheduler.schedule({
693
time: scheduler.now,
694
run: () => {
695
const deadline: IdleDeadline = {
696
didTimeout: true,
697
timeRemaining: () => 50,
698
};
699
callback(deadline);
700
},
701
source: {
702
toString() { return 'runWhenIdle'; },
703
stackTrace: undefined,
704
},
705
});
706
}));
707
708
const result = options.render({ container, disposableStore, theme });
709
710
const p2 = p.runForVirtualTimeMs(1000);
711
712
await Promise.all([
713
result instanceof Promise ? result : Promise.resolve(),
714
p2,
715
]);
716
}
717
718
await actualRender();
719
},
720
});
721
722
const labels = resolveLabels(options.labels);
723
return defineFixtureVariants(labels.length > 0 ? { labels } : {}, {
724
Dark: createFixture(darkTheme),
725
Light: createFixture(lightTheme),
726
});
727
}
728
729
interface ThemedFixtureGroupOptions {
730
readonly path?: string;
731
readonly labels?: ThemedFixtureGroupLabels;
732
}
733
734
type ThemedFixtureGroupFixtures = Record<string, ThemedFixtures>;
735
736
/**
737
* Creates a nested fixture group from themed fixtures.
738
* E.g., { MergeEditor: { Dark: ..., Light: ... } } becomes a nested group: MergeEditor > Dark/Light
739
*/
740
export function defineThemedFixtureGroup(options: ThemedFixtureGroupOptions, fixtures: ThemedFixtureGroupFixtures): ReturnType<typeof defineFixtureGroup>;
741
export function defineThemedFixtureGroup(fixtures: ThemedFixtureGroupFixtures): ReturnType<typeof defineFixtureGroup>;
742
export function defineThemedFixtureGroup(optionsOrFixtures: ThemedFixtureGroupOptions | ThemedFixtureGroupFixtures, fixtures?: ThemedFixtureGroupFixtures): ReturnType<typeof defineFixtureGroup> {
743
if (fixtures) {
744
const options = optionsOrFixtures as ThemedFixtureGroupOptions;
745
return defineFixtureGroup({
746
labels: resolveLabels(options.labels),
747
path: options.path,
748
}, fixtures as ThemedFixtureGroupFixtures);
749
}
750
return defineFixtureGroup(optionsOrFixtures as ThemedFixtureGroupFixtures);
751
}
752
753