Path: blob/main/src/vs/workbench/test/browser/componentFixtures/fixtureUtils.ts
13401 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45// This should be the only place that is allowed to import from @vscode/component-explorer6// eslint-disable-next-line local/code-import-patterns7import { defineFixture, defineFixtureGroup, defineFixtureVariants } from '@vscode/component-explorer';8import { DisposableStore, toDisposable } from '../../../../base/common/lifecycle.js';9import { URI } from '../../../../base/common/uri.js';10// eslint-disable-next-line local/code-import-patterns11import '../../../../../../build/vite/style.css';12import '../../../browser/media/style.css';1314// Theme15import { IEnvironmentService } from '../../../../platform/environment/common/environment.js';16import { Registry } from '../../../../platform/registry/common/platform.js';17import { getIconsStyleSheet } from '../../../../platform/theme/browser/iconsStyleSheet.js';18import { ColorScheme } from '../../../../platform/theme/common/theme.js';19import { IColorTheme, IThemeService, IThemingRegistry, Extensions as ThemingExtensions } from '../../../../platform/theme/common/themeService.js';20import { generateColorThemeCSS } from '../../../services/themes/browser/colorThemeCss.js';21import { ColorThemeData } from '../../../services/themes/common/colorThemeData.js';22import { COLOR_THEME_DARK_INITIAL_COLORS, COLOR_THEME_LIGHT_INITIAL_COLORS } from '../../../services/themes/common/workbenchThemeService.js';2324// Instantiation25import { SyncDescriptor } from '../../../../platform/instantiation/common/descriptors.js';26import { ServiceIdentifier } from '../../../../platform/instantiation/common/instantiation.js';27import { ServiceCollection } from '../../../../platform/instantiation/common/serviceCollection.js';28import { TestInstantiationService } from '../../../../platform/instantiation/test/common/instantiationServiceMock.js';2930// Test service implementations31import { Emitter, Event } from '../../../../base/common/event.js';32import { mock } from '../../../../base/test/common/mock.js';33import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js';34import { IInlineCompletionsService, InlineCompletionsService } from '../../../../editor/browser/services/inlineCompletionsService.js';35import { ILanguageService } from '../../../../editor/common/languages/language.js';36import { ILanguageConfigurationService } from '../../../../editor/common/languages/languageConfigurationRegistry.js';37import { IEditorWorkerService } from '../../../../editor/common/services/editorWorker.js';38import { ILanguageFeatureDebounceService, LanguageFeatureDebounceService } from '../../../../editor/common/services/languageFeatureDebounce.js';39import { ILanguageFeaturesService } from '../../../../editor/common/services/languageFeatures.js';40import { LanguageFeaturesService } from '../../../../editor/common/services/languageFeaturesService.js';41import { LanguageService } from '../../../../editor/common/services/languageService.js';42import { IModelService } from '../../../../editor/common/services/model.js';43import { ModelService } from '../../../../editor/common/services/modelService.js';44import { ITextResourcePropertiesService } from '../../../../editor/common/services/textResourceConfiguration.js';45import { ITreeSitterLibraryService } from '../../../../editor/common/services/treeSitter/treeSitterLibraryService.js';46import { ICodeLensCache } from '../../../../editor/contrib/codelens/browser/codeLensCache.js';47import { TestCodeEditorService, TestCommandService } from '../../../../editor/test/browser/editorTestServices.js';48import { TestLanguageConfigurationService } from '../../../../editor/test/common/modes/testLanguageConfigurationService.js';49import { TestEditorWorkerService } from '../../../../editor/test/common/services/testEditorWorkerService.js';50import { TestTextResourcePropertiesService } from '../../../../editor/test/common/services/testTextResourcePropertiesService.js';51import { TestTreeSitterLibraryService } from '../../../../editor/test/common/services/testTreeSitterLibraryService.js';52import { IAccessibilityService } from '../../../../platform/accessibility/common/accessibility.js';53import { TestAccessibilityService } from '../../../../platform/accessibility/test/common/testAccessibilityService.js';54import { IActionViewItemService, NullActionViewItemService } from '../../../../platform/actions/browser/actionViewItemService.js';55import { IMenuService } from '../../../../platform/actions/common/actions.js';56import { IClipboardService } from '../../../../platform/clipboard/common/clipboardService.js';57import { TestClipboardService } from '../../../../platform/clipboard/test/common/testClipboardService.js';58import { ICommandService } from '../../../../platform/commands/common/commands.js';59import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';60import { TestConfigurationService } from '../../../../platform/configuration/test/common/testConfigurationService.js';61import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';62import { IContextMenuService, IContextViewService } from '../../../../platform/contextview/browser/contextView.js';63import { IDataChannelService, NullDataChannelService } from '../../../../platform/dataChannel/common/dataChannel.js';64import { IDefaultAccountService } from '../../../../platform/defaultAccount/common/defaultAccount.js';65import { IDialogService } from '../../../../platform/dialogs/common/dialogs.js';66import { TestDialogService } from '../../../../platform/dialogs/test/common/testDialogService.js';67import { IHoverService } from '../../../../platform/hover/browser/hover.js';68import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js';69import { MockContextKeyService, MockKeybindingService } from '../../../../platform/keybinding/test/common/mockKeybindingService.js';70import { ILabelService } from '../../../../platform/label/common/label.js';71import { ILoggerService, ILogService, NullLoggerService, NullLogService } from '../../../../platform/log/common/log.js';72import { INotificationService } from '../../../../platform/notification/common/notification.js';73import { TestNotificationService } from '../../../../platform/notification/test/common/testNotificationService.js';74import { IOpenerService } from '../../../../platform/opener/common/opener.js';75import { NullOpenerService } from '../../../../platform/opener/test/common/nullOpenerService.js';76import { IApplicationSharedStorageValueChangeEvent, IApplicationStorageValueChangeEvent, IProfileStorageValueChangeEvent, IStorageEntry, IStorageService, IStorageTargetChangeEvent, IStorageValueChangeEvent, IWillSaveStateEvent, IWorkspaceStorageValueChangeEvent, StorageScope, StorageTarget, WillSaveStateReason } from '../../../../platform/storage/common/storage.js';77import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js';78import { NullTelemetryServiceShape } from '../../../../platform/telemetry/common/telemetryUtils.js';79import { TestThemeService } from '../../../../platform/theme/test/common/testThemeService.js';80import { IUndoRedoService } from '../../../../platform/undoRedo/common/undoRedo.js';81import { UndoRedoService } from '../../../../platform/undoRedo/common/undoRedoService.js';82import { IUserDataProfile } from '../../../../platform/userDataProfile/common/userDataProfile.js';83import { IUserInteractionService, MockUserInteractionService } from '../../../../platform/userInteraction/browser/userInteractionService.js';84import { IAnyWorkspaceIdentifier } from '../../../../platform/workspace/common/workspace.js';85import { TestMenuService } from '../workbenchTestServices.js';86import { IAccessibilitySignalService } from '../../../../platform/accessibilitySignal/browser/accessibilitySignalService.js';87import { ITextModelService } from '../../../../editor/common/services/resolverService.js';88// eslint-disable-next-line local/code-import-patterns89import { IAgentFeedbackService } from '../../../../sessions/contrib/agentFeedback/browser/agentFeedbackService.js';90import { IChatEditingService } from '../../../contrib/chat/common/editing/chatEditingService.js';91// eslint-disable-next-line local/code-import-patterns92import { ISessionsManagementService } from '../../../../sessions/services/sessions/common/sessionsManagement.js';93// eslint-disable-next-line local/code-import-patterns94import { ICodeReviewService, CodeReviewStateKind, PRReviewStateKind } from '../../../../sessions/contrib/codeReview/browser/codeReviewService.js';95import { constObservable } from '../../../../base/common/observable.js';9697// Editor98import { ITextModel } from '../../../../editor/common/model.js';99100import './fixtures.css';101102// Import color registrations to ensure colors are available103import { IdleDeadline, installFakeRunWhenIdle } from '../../../../base/common/async.js';104import { AsyncSchedulerProcessor, TimeTravelScheduler, captureGlobalTimeApi, createLoggingTimeApi, createVirtualTimeApi, overwriteGlobalTimeApi } from '../../../../base/test/common/timeTravelScheduler.js';105import '../../../../platform/theme/common/colors/baseColors.js';106import '../../../../platform/theme/common/colors/editorColors.js';107import '../../../../platform/theme/common/colors/listColors.js';108import '../../../../platform/theme/common/colors/miscColors.js';109import '../../../common/theme.js';110111// eslint-disable-next-line local/code-import-patterns112import sourceMapSupport from 'source-map-support';113sourceMapSupport.install({114environment: 'browser',115handleUncaughtExceptions: false,116retrieveSourceMap: (source: string) => {117const mapUrl = source + '.map';118try {119const xhr = new XMLHttpRequest();120xhr.open('GET', mapUrl, false);121xhr.send();122if (xhr.status === 200) {123return { url: null as never, map: xhr.responseText };124}125} catch { }126return null;127},128});129130/**131* A storage service that never stores anything and always returns the default/fallback value.132* This is useful for fixtures where we want consistent behavior without persisted state.133*/134class NullStorageService implements IStorageService {135136declare readonly _serviceBrand: undefined;137138private readonly _onDidChangeValue = new Emitter<IStorageValueChangeEvent>();139onDidChangeValue(scope: StorageScope.WORKSPACE, key: string | undefined, disposable: DisposableStore): Event<IWorkspaceStorageValueChangeEvent>;140onDidChangeValue(scope: StorageScope.PROFILE, key: string | undefined, disposable: DisposableStore): Event<IProfileStorageValueChangeEvent>;141onDidChangeValue(scope: StorageScope.APPLICATION, key: string | undefined, disposable: DisposableStore): Event<IApplicationStorageValueChangeEvent>;142onDidChangeValue(scope: StorageScope.APPLICATION_SHARED, key: string | undefined, disposable: DisposableStore): Event<IApplicationSharedStorageValueChangeEvent>;143onDidChangeValue(scope: StorageScope, key: string | undefined, disposable: DisposableStore): Event<IStorageValueChangeEvent> {144return Event.filter(this._onDidChangeValue.event, e => e.scope === scope && (key === undefined || e.key === key), disposable);145}146147private readonly _onDidChangeTarget = new Emitter<IStorageTargetChangeEvent>();148readonly onDidChangeTarget: Event<IStorageTargetChangeEvent> = this._onDidChangeTarget.event;149150private readonly _onWillSaveState = new Emitter<IWillSaveStateEvent>();151readonly onWillSaveState: Event<IWillSaveStateEvent> = this._onWillSaveState.event;152153get(key: string, scope: StorageScope, fallbackValue: string): string;154get(key: string, scope: StorageScope, fallbackValue?: string): string | undefined;155get(_key: string, _scope: StorageScope, fallbackValue?: string): string | undefined {156return fallbackValue;157}158159getBoolean(key: string, scope: StorageScope, fallbackValue: boolean): boolean;160getBoolean(key: string, scope: StorageScope, fallbackValue?: boolean): boolean | undefined;161getBoolean(_key: string, _scope: StorageScope, fallbackValue?: boolean): boolean | undefined {162return fallbackValue;163}164165getNumber(key: string, scope: StorageScope, fallbackValue: number): number;166getNumber(key: string, scope: StorageScope, fallbackValue?: number): number | undefined;167getNumber(_key: string, _scope: StorageScope, fallbackValue?: number): number | undefined {168return fallbackValue;169}170171getObject<T extends object>(key: string, scope: StorageScope, fallbackValue: T): T;172getObject<T extends object>(key: string, scope: StorageScope, fallbackValue?: T): T | undefined;173getObject<T extends object>(_key: string, _scope: StorageScope, fallbackValue?: T): T | undefined {174return fallbackValue;175}176177store(_key: string, _value: string | boolean | number | undefined | null, _scope: StorageScope, _target: StorageTarget): void {178// no-op179}180181storeAll(_entries: IStorageEntry[], _external: boolean): void {182// no-op183}184185remove(_key: string, _scope: StorageScope): void {186// no-op187}188189isNew(_scope: StorageScope): boolean {190return true;191}192193flush(_reason?: WillSaveStateReason): Promise<void> {194return Promise.resolve();195}196197optimize(_scope: StorageScope): Promise<void> {198return Promise.resolve();199}200201log(): void {202// no-op203}204205keys(_scope: StorageScope, _target: StorageTarget): string[] {206return [];207}208209switch(): Promise<void> {210return Promise.resolve();211}212213hasScope(_scope: IAnyWorkspaceIdentifier | IUserDataProfile): boolean {214return false;215}216}217218219// ============================================================================220// Themes221// ============================================================================222223const themingRegistry = Registry.as<IThemingRegistry>(ThemingExtensions.ThemingContribution);224const mockEnvironmentService: IEnvironmentService = Object.create(null);225226export const darkTheme = ColorThemeData.createUnloadedThemeForThemeType(227ColorScheme.DARK,228COLOR_THEME_DARK_INITIAL_COLORS229);230231export const lightTheme = ColorThemeData.createUnloadedThemeForThemeType(232ColorScheme.LIGHT,233COLOR_THEME_LIGHT_INITIAL_COLORS234);235236let globalStyleSheet: CSSStyleSheet | undefined;237let iconsStyleSheetCache: CSSStyleSheet | undefined;238let darkThemeStyleSheet: CSSStyleSheet | undefined;239let lightThemeStyleSheet: CSSStyleSheet | undefined;240241function getGlobalStyleSheet(): CSSStyleSheet {242if (!globalStyleSheet) {243globalStyleSheet = new CSSStyleSheet();244const globalRules: string[] = [];245for (const sheet of Array.from(document.styleSheets)) {246try {247for (const rule of Array.from(sheet.cssRules)) {248globalRules.push(rule.cssText);249}250} catch {251// Cross-origin stylesheets can't be read252}253}254globalStyleSheet.replaceSync(globalRules.join('\n'));255}256return globalStyleSheet;257}258259function getIconsStyleSheetCached(): CSSStyleSheet {260if (!iconsStyleSheetCache) {261iconsStyleSheetCache = new CSSStyleSheet();262const iconsSheet = getIconsStyleSheet(undefined);263iconsStyleSheetCache.replaceSync(iconsSheet.getCSS() as string);264iconsSheet.dispose();265}266return iconsStyleSheetCache;267}268269function getThemeStyleSheet(theme: ColorThemeData): CSSStyleSheet {270const isDark = theme.type === ColorScheme.DARK;271if (isDark && darkThemeStyleSheet) {272return darkThemeStyleSheet;273}274if (!isDark && lightThemeStyleSheet) {275return lightThemeStyleSheet;276}277278const scopeSelector = '.' + theme.classNames[0];279const sheet = new CSSStyleSheet();280const css = generateColorThemeCSS(281theme,282scopeSelector,283themingRegistry.getThemingParticipants(),284mockEnvironmentService285);286sheet.replaceSync(css.code);287288if (isDark) {289darkThemeStyleSheet = sheet;290} else {291lightThemeStyleSheet = sheet;292}293return sheet;294}295296let globalStylesInstalled = false;297298function installGlobalStyles(): void {299if (globalStylesInstalled) {300return;301}302globalStylesInstalled = true;303document.adoptedStyleSheets = [304...document.adoptedStyleSheets,305getGlobalStyleSheet(),306getIconsStyleSheetCached(),307getThemeStyleSheet(darkTheme),308getThemeStyleSheet(lightTheme),309];310}311312export function setupTheme(container: HTMLElement, theme: ColorThemeData): void {313installGlobalStyles();314container.classList.add('monaco-workbench', getPlatformClass(), 'disable-animations', ...theme.classNames);315}316317function getPlatformClass(): string {318const alwaysUseMac = true;319if (alwaysUseMac) {320return 'mac';321} else {322const ua = navigator.userAgent;323if (ua.includes('Macintosh')) {324return 'mac';325}326if (ua.includes('Linux')) {327return 'linux';328}329return 'windows';330}331}332333334// ============================================================================335// Services336// ============================================================================337338export interface ServiceRegistration {339define<T>(id: ServiceIdentifier<T>, ctor: new (...args: never[]) => T): void;340defineInstance<T>(id: ServiceIdentifier<T>, instance: T): void;341/** Like defineInstance but accepts a partial mock - provides type checking on provided properties */342definePartialInstance<T>(id: ServiceIdentifier<T>, instance: Partial<T>): void;343}344345export interface CreateServicesOptions {346/**347* The color theme to use for the theme service.348*/349colorTheme?: IColorTheme;350/**351* Additional services to register after the base editor services.352*/353additionalServices?: (registration: ServiceRegistration) => void;354}355356/**357* Creates a TestInstantiationService with all services needed for CodeEditorWidget.358* Additional services can be registered via the options callback.359*/360export function createEditorServices(disposables: DisposableStore, options?: CreateServicesOptions): TestInstantiationService {361const services = new ServiceCollection();362// eslint-disable-next-line @typescript-eslint/no-explicit-any363const serviceIdentifiers: ServiceIdentifier<any>[] = [];364365// eslint-disable-next-line @typescript-eslint/no-explicit-any366const define = <T>(id: ServiceIdentifier<T>, ctor: new (...args: any[]) => T) => {367if (!services.has(id)) {368services.set(id, new SyncDescriptor(ctor));369}370serviceIdentifiers.push(id);371};372373const defineInstance = <T>(id: ServiceIdentifier<T>, instance: T) => {374if (!services.has(id)) {375services.set(id, instance);376}377serviceIdentifiers.push(id);378};379380const definePartialInstance = <T>(id: ServiceIdentifier<T>, instance: Partial<T>) => {381defineInstance(id, instance as T);382};383384// Base editor services385define(IAccessibilityService, TestAccessibilityService);386define(IKeybindingService, MockKeybindingService);387define(IClipboardService, TestClipboardService);388define(IEditorWorkerService, TestEditorWorkerService);389defineInstance(IOpenerService, NullOpenerService);390define(INotificationService, TestNotificationService);391define(IDialogService, TestDialogService);392define(IUndoRedoService, UndoRedoService);393define(ILanguageService, LanguageService);394define(ILanguageConfigurationService, TestLanguageConfigurationService);395define(IConfigurationService, TestConfigurationService);396define(ITextResourcePropertiesService, TestTextResourcePropertiesService);397defineInstance(IStorageService, new NullStorageService());398if (options?.colorTheme) {399defineInstance(IThemeService, new TestThemeService(options.colorTheme));400} else {401define(IThemeService, TestThemeService);402}403define(ILogService, NullLogService);404define(IModelService, ModelService);405define(ICodeEditorService, TestCodeEditorService);406define(IContextKeyService, MockContextKeyService);407define(ICommandService, TestCommandService);408define(ITelemetryService, NullTelemetryServiceShape);409define(ILoggerService, NullLoggerService);410define(IDataChannelService, NullDataChannelService);411define(IEnvironmentService, class extends mock<IEnvironmentService>() {412declare readonly _serviceBrand: undefined;413override isBuilt: boolean = true;414override isExtensionDevelopment: boolean = false;415});416define(ILanguageFeatureDebounceService, LanguageFeatureDebounceService);417define(ILanguageFeaturesService, LanguageFeaturesService);418define(ITreeSitterLibraryService, TestTreeSitterLibraryService);419define(IInlineCompletionsService, InlineCompletionsService);420defineInstance(ICodeLensCache, {421_serviceBrand: undefined,422put: () => { },423get: () => undefined,424delete: () => { },425});426defineInstance(IHoverService, {427_serviceBrand: undefined,428showDelayedHover: () => undefined,429setupDelayedHover: () => ({ dispose: () => { } }),430setupDelayedHoverAtMouse: () => ({ dispose: () => { } }),431showInstantHover: () => undefined,432hideHover: () => { },433showAndFocusLastHover: () => { },434setupManagedHover: () => ({ dispose: () => { }, show: () => { }, hide: () => { }, update: () => { } }),435showManagedHover: () => { },436});437defineInstance(IDefaultAccountService, {438_serviceBrand: undefined,439onDidChangeDefaultAccount: new Emitter<null>().event,440onDidChangePolicyData: new Emitter<null>().event,441policyData: null,442currentDefaultAccount: null,443copilotTokenInfo: null,444onDidChangeCopilotTokenInfo: new Emitter<null>().event,445getDefaultAccount: async () => null,446getDefaultAccountAuthenticationProvider: () => ({ id: 'test', name: 'Test', scopes: [], enterprise: false }),447setDefaultAccountProvider: () => { },448refresh: async () => null,449signIn: async () => null,450signOut: async () => { },451});452453// User interaction service with focus simulation enabled (all elements appear focused in fixtures)454defineInstance(IUserInteractionService, new MockUserInteractionService(true, false));455456defineInstance(IAccessibilitySignalService, {457_serviceBrand: undefined,458playSignal: async () => { },459playSignals: async () => { },460playSignalLoop: () => ({ dispose: () => { } }),461getEnabledState: () => ({ value: false, onDidChange: Event.None, onChange: () => ({ dispose: () => { } }) }),462getDelayMs: () => 0,463playSound: async () => { },464isSoundEnabled: () => false,465isAnnouncementEnabled: () => false,466onSoundEnabledChanged: () => Event.None,467});468469definePartialInstance(ITextModelService, {470_serviceBrand: undefined,471registerTextModelContentProvider: () => ({ dispose: () => { } }),472canHandleResource: () => false,473// eslint-disable-next-line local/code-no-any-casts, @typescript-eslint/no-explicit-any474createModelReference: async () => ({ object: { textEditorModel: null }, dispose() { } } as any),475});476477defineInstance(IAgentFeedbackService, {478_serviceBrand: undefined,479onDidChangeFeedback: Event.None,480onDidChangeNavigation: Event.None,481addFeedback: () => undefined!,482removeFeedback: () => { },483updateFeedback: () => { },484getFeedback: () => [],485getMostRecentSessionForResource: () => undefined,486revealFeedback: async () => { },487revealSessionComment: async () => { },488getNextFeedback: () => undefined,489getNextNavigableItem: () => undefined,490setNavigationAnchor: () => { },491getNavigationBearing: () => ({ activeIdx: -1, totalCount: 0 }),492clearFeedback: () => { },493addFeedbackAndSubmit: async () => { },494});495496definePartialInstance(IChatEditingService, {497_serviceBrand: undefined,498editingSessionsObs: constObservable([]),499startOrContinueGlobalEditingSession: () => undefined!,500getEditingSession: () => undefined,501});502503definePartialInstance(ISessionsManagementService, {504_serviceBrand: undefined,505activeSession: constObservable(undefined),506getSession: () => undefined,507getSessions: () => [],508});509510definePartialInstance(ICodeReviewService, {511_serviceBrand: undefined,512getReviewState: () => constObservable({ kind: CodeReviewStateKind.Idle }),513getPRReviewState: () => constObservable({ kind: PRReviewStateKind.None }),514hasReview: () => false,515requestReview: () => { },516removeComment: () => { },517updateComment: () => { },518dismissReview: () => { },519resolvePRReviewThread: async () => { },520markPRReviewCommentConverted: () => { },521});522523// Allow additional services to override defaults524options?.additionalServices?.({525// eslint-disable-next-line @typescript-eslint/no-explicit-any526define: <T>(id: ServiceIdentifier<T>, ctor: new (...args: any[]) => T) => {527services.set(id, new SyncDescriptor(ctor));528serviceIdentifiers.push(id);529},530defineInstance: <T>(id: ServiceIdentifier<T>, instance: T) => {531services.set(id, instance);532serviceIdentifiers.push(id);533},534definePartialInstance: <T>(id: ServiceIdentifier<T>, instance: Partial<T>) => {535services.set(id, instance as T);536serviceIdentifiers.push(id);537},538});539540const instantiationService = disposables.add(new TestInstantiationService(services, true));541542disposables.add(toDisposable(() => {543for (const id of serviceIdentifiers) {544const instanceOrDescriptor = services.get(id);545if (typeof instanceOrDescriptor?.dispose === 'function') {546instanceOrDescriptor.dispose();547}548}549}));550551return instantiationService;552}553554/**555* Registers additional services needed by workbench components (merge editor, etc.).556* Use with createEditorServices additionalServices option.557*/558export function registerWorkbenchServices(registration: ServiceRegistration): void {559registration.defineInstance(IContextMenuService, {560showContextMenu: () => { },561onDidShowContextMenu: () => ({ dispose: () => { } }),562onDidHideContextMenu: () => ({ dispose: () => { } }),563_serviceBrand: undefined,564});565566registration.defineInstance(IContextViewService, {567showContextView: () => ({ close: () => { } }),568hideContextView: () => { },569getContextViewElement: () => { throw new Error('Not implemented'); },570layout: () => { },571anchorAlignment: 0,572_serviceBrand: undefined,573});574575registration.defineInstance(ILabelService, {576getUriLabel: (uri: URI) => uri.path,577getUriBasenameLabel: (uri: URI) => uri.path.split('/').pop() ?? '',578getWorkspaceLabel: () => '',579getHostLabel: () => '',580getSeparator: () => '/',581registerFormatter: () => ({ dispose: () => { } }),582onDidChangeFormatters: () => ({ dispose: () => { } }),583registerCachedFormatter: () => ({ dispose: () => { } }),584_serviceBrand: undefined,585getHostTooltip: () => '',586});587588registration.define(IMenuService, TestMenuService);589registration.define(IActionViewItemService, NullActionViewItemService);590}591592593// ============================================================================594// Text Models595// ============================================================================596597/**598* Creates a text model using the ModelService.599*/600export function createTextModel(601instantiationService: TestInstantiationService,602text: string,603uri: URI,604languageId?: string605): ITextModel {606const modelService = instantiationService.get(IModelService);607const languageService = instantiationService.get(ILanguageService);608const languageSelection = languageId ? languageService.createById(languageId) : null;609return modelService.createModel(text, languageSelection, uri);610}611612613// ============================================================================614// Fixture Adapters615// ============================================================================616617export interface ThemedFixtureGroupLabels {618readonly kind?: 'screenshot' | 'animated';619readonly blocksCi?: true;620readonly flaky?: true;621}622623function resolveLabels(labels: ThemedFixtureGroupLabels | undefined): string[] {624const result: string[] = [];625if (labels?.kind === 'screenshot') {626result.push('.screenshot');627} else if (labels?.kind === 'animated') {628result.push('animated');629}630if (labels?.blocksCi) {631result.push('blocks-ci');632}633if (labels?.flaky) {634result.push('flaky');635}636return result;637}638639export interface ComponentFixtureContext {640container: HTMLElement;641disposableStore: DisposableStore;642theme: ColorThemeData;643}644645export interface ComponentFixtureOptions {646render: (context: ComponentFixtureContext) => void | Promise<void>;647labels?: ThemedFixtureGroupLabels;648}649650type ThemedFixtures = ReturnType<typeof defineFixtureVariants>;651652// Permanent logging layer that detects real timer API usage.653// Includes handler source for identification since bundled stack traces are not useful.654const realTimeApi = captureGlobalTimeApi();655const loggingTimeApi = createLoggingTimeApi(realTimeApi, (name, stack, handler) => {656const handlerStr = typeof handler === 'function' ? handler.toString().slice(0, 500) : String(handler);657console.warn(`[ComponentFixture] Real ${name} called outside of virtual time.\nHandler: ${handlerStr}\nStack: ${stack}`);658});659overwriteGlobalTimeApi(loggingTimeApi);660661/**662* Creates Dark and Light fixture variants from a single render function.663* The render function receives a context with container and disposableStore.664*665* Note: If render returns a Promise, the async work will run in background.666* Component-explorer waits 2 animation frames after sync render returns,667* which should be sufficient for most async setup, but timing is not guaranteed.668*/669export function defineComponentFixture(options: ComponentFixtureOptions): ThemedFixtures {670const createFixture = (theme: typeof darkTheme | typeof lightTheme) => defineFixture({671isolation: 'none',672displayMode: { type: 'component' },673background: theme === darkTheme ? 'dark' : 'light',674render: async (container: HTMLElement, context) => {675const disposableStore = context.addDisposable(new DisposableStore());676677const schedulerStore = disposableStore.add(new DisposableStore());678const scheduler = new TimeTravelScheduler(Date.now());679const p = schedulerStore.add(new AsyncSchedulerProcessor(scheduler, {680maxTaskCount: 100,681realTimeApi,682}));683684async function actualRender() {685686setupTheme(container, theme);687688const virtualTimeApi = createVirtualTimeApi(scheduler, { fakeRequestAnimationFrame: true });689schedulerStore.add(overwriteGlobalTimeApi(virtualTimeApi));690disposableStore.add(installFakeRunWhenIdle((_targetWindow, callback, _timeout?) => {691return scheduler.schedule({692time: scheduler.now,693run: () => {694const deadline: IdleDeadline = {695didTimeout: true,696timeRemaining: () => 50,697};698callback(deadline);699},700source: {701toString() { return 'runWhenIdle'; },702stackTrace: undefined,703},704});705}));706707const result = options.render({ container, disposableStore, theme });708709const p2 = p.runForVirtualTimeMs(1000);710711await Promise.all([712result instanceof Promise ? result : Promise.resolve(),713p2,714]);715}716717await actualRender();718},719});720721const labels = resolveLabels(options.labels);722return defineFixtureVariants(labels.length > 0 ? { labels } : {}, {723Dark: createFixture(darkTheme),724Light: createFixture(lightTheme),725});726}727728interface ThemedFixtureGroupOptions {729readonly path?: string;730readonly labels?: ThemedFixtureGroupLabels;731}732733type ThemedFixtureGroupFixtures = Record<string, ThemedFixtures>;734735/**736* Creates a nested fixture group from themed fixtures.737* E.g., { MergeEditor: { Dark: ..., Light: ... } } becomes a nested group: MergeEditor > Dark/Light738*/739export function defineThemedFixtureGroup(options: ThemedFixtureGroupOptions, fixtures: ThemedFixtureGroupFixtures): ReturnType<typeof defineFixtureGroup>;740export function defineThemedFixtureGroup(fixtures: ThemedFixtureGroupFixtures): ReturnType<typeof defineFixtureGroup>;741export function defineThemedFixtureGroup(optionsOrFixtures: ThemedFixtureGroupOptions | ThemedFixtureGroupFixtures, fixtures?: ThemedFixtureGroupFixtures): ReturnType<typeof defineFixtureGroup> {742if (fixtures) {743const options = optionsOrFixtures as ThemedFixtureGroupOptions;744return defineFixtureGroup({745labels: resolveLabels(options.labels),746path: options.path,747}, fixtures as ThemedFixtureGroupFixtures);748}749return defineFixtureGroup(optionsOrFixtures as ThemedFixtureGroupFixtures);750}751752753