mirror of
https://github.com/microsoft/vscode.git
synced 2026-07-09 09:24:12 -05:00
Fixes component fixture errors (#314164)
* Fixes component fixture errors * refactor: replace empty mock with MockChatModeService in chat fixture services
This commit is contained in:
committed by
GitHub
parent
e4e0fa64ea
commit
83b3a75bba
33
.github/workflows/screenshot-test.yml
vendored
33
.github/workflows/screenshot-test.yml
vendored
@@ -26,10 +26,10 @@ jobs:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
# Depth 2 gives us the test-merge commit plus its parents, which is
|
||||
# enough for the merge-base lookup below (the target-branch tip is a
|
||||
# direct parent). Full clone would be wasteful for this large repo.
|
||||
fetch-depth: 2
|
||||
# Need enough history for the merge-base lookup below to succeed even
|
||||
# when the target branch has advanced since the PR was opened. Full
|
||||
# clone would be wasteful for this large repo, so cap at 50.
|
||||
fetch-depth: 50
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
@@ -63,7 +63,8 @@ jobs:
|
||||
- name: Capture screenshots
|
||||
run: ./node_modules/.bin/component-explorer render --project ./test/componentFixtures/component-explorer.json
|
||||
|
||||
- name: Log fixture errors
|
||||
- name: Check fixture errors
|
||||
id: fixture_errors
|
||||
if: always()
|
||||
run: |
|
||||
MANIFEST="test/componentFixtures/.screenshots/current/manifest.json"
|
||||
@@ -71,7 +72,11 @@ jobs:
|
||||
echo "::warning::No manifest found — render may have failed entirely"
|
||||
exit 0
|
||||
fi
|
||||
ERRORS=$(node -e "
|
||||
# Log per-fixture errors but do not fail here — let later steps run
|
||||
# (artifact upload, diff, PR comment) so failures are debuggable.
|
||||
# The final "Fail if fixtures had errors" step turns this into a
|
||||
# job failure.
|
||||
if node -e "
|
||||
const m = require('./$MANIFEST');
|
||||
const errs = m.fixtures.filter(f => f.hasError);
|
||||
if (!errs.length) { console.log('No fixture errors.'); process.exit(0); }
|
||||
@@ -84,8 +89,12 @@ jobs:
|
||||
for (const e of f.events) { console.log(' event: ' + JSON.stringify(e)); }
|
||||
}
|
||||
}
|
||||
")
|
||||
echo "$ERRORS"
|
||||
process.exit(1);
|
||||
"; then
|
||||
echo "has_errors=false" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "has_errors=true" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Check blocks-ci screenshots
|
||||
id: blocks-ci
|
||||
@@ -127,7 +136,7 @@ jobs:
|
||||
# the target-branch tip at PR creation time and can be stale,
|
||||
# causing unrelated target-branch commits to show up as diffs.
|
||||
TARGET_REF="origin/${{ github.event.pull_request.base.ref }}"
|
||||
git fetch --no-tags --depth=1 origin "${{ github.event.pull_request.base.ref }}"
|
||||
git fetch --no-tags --depth=50 origin "${{ github.event.pull_request.base.ref }}"
|
||||
BASE_SHA=$(git merge-base "${{ github.sha }}" "$TARGET_REF")
|
||||
else
|
||||
# For push events, diff against the parent commit.
|
||||
@@ -319,6 +328,12 @@ jobs:
|
||||
diff -u test/componentFixtures/blocks-ci-screenshots.md /tmp/blocks-ci-updated.md || true
|
||||
exit 1
|
||||
|
||||
- name: Fail if fixtures had errors
|
||||
if: always() && steps.fixture_errors.outputs.has_errors == 'true'
|
||||
run: |
|
||||
echo "::error::One or more component fixtures failed to render. See the 'Check fixture errors' step for details."
|
||||
exit 1
|
||||
|
||||
# - name: Prepare explorer artifact
|
||||
# run: |
|
||||
# mkdir -p /tmp/explorer-artifact/screenshot-report
|
||||
|
||||
@@ -207,32 +207,32 @@ export class AgentFeedbackEditorWidget extends Disposable implements IOverlayWid
|
||||
|
||||
const itemActions: ICommentItemActions = { editAction: undefined!, convertAction: undefined, removeAction: undefined! };
|
||||
|
||||
itemActions.editAction = new Action(
|
||||
itemActions.editAction = this._eventStore.add(new Action(
|
||||
'agentFeedback.widget.edit',
|
||||
nls.localize('editComment', "Edit"),
|
||||
ThemeIcon.asClassName(Codicon.edit),
|
||||
true,
|
||||
(): void => { this._startEditing(comment, text, itemActions); },
|
||||
);
|
||||
));
|
||||
actionBar.push(itemActions.editAction, { icon: true, label: false });
|
||||
|
||||
if (comment.canConvertToAgentFeedback) {
|
||||
itemActions.convertAction = new Action(
|
||||
itemActions.convertAction = this._eventStore.add(new Action(
|
||||
'agentFeedback.widget.convert',
|
||||
nls.localize('convertComment', "Convert to Agent Feedback"),
|
||||
ThemeIcon.asClassName(Codicon.check),
|
||||
true,
|
||||
() => this._convertToAgentFeedback(comment),
|
||||
);
|
||||
));
|
||||
actionBar.push(itemActions.convertAction, { icon: true, label: false });
|
||||
}
|
||||
itemActions.removeAction = new Action(
|
||||
itemActions.removeAction = this._eventStore.add(new Action(
|
||||
'agentFeedback.widget.remove',
|
||||
nls.localize('removeComment', "Remove"),
|
||||
ThemeIcon.asClassName(Codicon.close),
|
||||
true,
|
||||
() => this._removeComment(comment),
|
||||
);
|
||||
));
|
||||
actionBar.push(itemActions.removeAction, { icon: true, label: false });
|
||||
|
||||
itemHeader.appendChild(actionBarContainer);
|
||||
|
||||
@@ -0,0 +1,213 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Event } from '../../../../../base/common/event.js';
|
||||
import { IObservable, observableValue } from '../../../../../base/common/observable.js';
|
||||
import { mock } from '../../../../../base/test/common/mock.js';
|
||||
import { ICommandService } from '../../../../../platform/commands/common/commands.js';
|
||||
import { IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js';
|
||||
import { IMenu, IMenuItem, IMenuService, MenuId, MenuItemAction } from '../../../../../platform/actions/common/actions.js';
|
||||
import { IFileDialogService } from '../../../../../platform/dialogs/common/dialogs.js';
|
||||
import { IFileService } from '../../../../../platform/files/common/files.js';
|
||||
import { IListService, ListService } from '../../../../../platform/list/browser/listService.js';
|
||||
import { IActionWidgetService } from '../../../../../platform/actionWidget/browser/actionWidget.js';
|
||||
import { IProductService } from '../../../../../platform/product/common/productService.js';
|
||||
import { IUpdateService, StateType } from '../../../../../platform/update/common/update.js';
|
||||
import { IUriIdentityService } from '../../../../../platform/uriIdentity/common/uriIdentity.js';
|
||||
import { ISharedWebContentExtractorService } from '../../../../../platform/webContentExtractor/common/webContentExtractor.js';
|
||||
import { IAccessibleViewService } from '../../../../../platform/accessibility/browser/accessibleView.js';
|
||||
import { IMarkdownRendererService, MarkdownRendererService } from '../../../../../platform/markdown/browser/markdownRenderer.js';
|
||||
import { IWorkspace, IWorkspaceContextService } from '../../../../../platform/workspace/common/workspace.js';
|
||||
import { IDecorationsService } from '../../../../services/decorations/common/decorations.js';
|
||||
import { ITextFileService } from '../../../../services/textfile/common/textfiles.js';
|
||||
import { IExtensionService } from '../../../../services/extensions/common/extensions.js';
|
||||
import { IEditorService } from '../../../../services/editor/common/editorService.js';
|
||||
import { IPathService } from '../../../../services/path/common/pathService.js';
|
||||
import { IWorkbenchAssignmentService } from '../../../../services/assignment/common/assignmentService.js';
|
||||
import { IWorkbenchLayoutService } from '../../../../services/layout/browser/layoutService.js';
|
||||
import { IWorkbenchEnvironmentService } from '../../../../services/environment/common/environmentService.js';
|
||||
import { IChatEntitlementService } from '../../../../services/chat/common/chatEntitlementService.js';
|
||||
import { INotebookDocumentService } from '../../../../services/notebook/common/notebookDocumentService.js';
|
||||
import { IViewDescriptorService } from '../../../../common/views.js';
|
||||
import { ISCMService } from '../../../../contrib/scm/common/scm.js';
|
||||
import { IAgentSessionsService } from '../../../../contrib/chat/browser/agentSessions/agentSessionsService.js';
|
||||
import { IChatAccessibilityService, IChatWidget, IChatWidgetService } from '../../../../contrib/chat/browser/chat.js';
|
||||
import { IChatAttachmentResolveService } from '../../../../contrib/chat/browser/attachments/chatAttachmentResolveService.js';
|
||||
import { IChatAttachmentWidgetRegistry } from '../../../../contrib/chat/browser/attachments/chatAttachmentWidgetRegistry.js';
|
||||
import { IChatContextPickService } from '../../../../contrib/chat/browser/attachments/chatContextPickService.js';
|
||||
import { IChatContextService } from '../../../../contrib/chat/browser/contextContrib/chatContextService.js';
|
||||
import { IChatImageCarouselService } from '../../../../contrib/chat/browser/chatImageCarouselService.js';
|
||||
import { IChatInputNotificationService } from '../../../../contrib/chat/browser/widget/input/chatInputNotificationService.js';
|
||||
import { IChatMarkdownAnchorService } from '../../../../contrib/chat/browser/widget/chatContentParts/chatMarkdownAnchorService.js';
|
||||
import { IChatWidgetHistoryService } from '../../../../contrib/chat/common/widget/chatWidgetHistoryService.js';
|
||||
import { IChatModeService } from '../../../../contrib/chat/common/chatModes.js';
|
||||
import { MockChatModeService } from '../../../../contrib/chat/test/common/mockChatModeService.js';
|
||||
import { IChatService } from '../../../../contrib/chat/common/chatService/chatService.js';
|
||||
import { IChatSessionsService } from '../../../../contrib/chat/common/chatSessionsService.js';
|
||||
import { Target } from '../../../../contrib/chat/common/promptSyntax/promptTypes.js';
|
||||
import { ILanguageModelsService } from '../../../../contrib/chat/common/languageModels.js';
|
||||
import { ChatAgentService, IChatAgent, IChatAgentNameService, IChatAgentService } from '../../../../contrib/chat/common/participants/chatAgents.js';
|
||||
import { MockChatService } from '../../../../contrib/chat/test/common/chatService/mockChatService.js';
|
||||
import { ILanguageModelToolsService } from '../../../../contrib/chat/common/tools/languageModelToolsService.js';
|
||||
import { IArtifactSourceGroup, IChatArtifacts, IChatArtifactsService } from '../../../../contrib/chat/common/tools/chatArtifactsService.js';
|
||||
import { IChatTodo, IChatTodoListService } from '../../../../contrib/chat/common/tools/chatTodoListService.js';
|
||||
import { ServiceRegistration, registerWorkbenchServices } from '../fixtureUtils.js';
|
||||
|
||||
/**
|
||||
* A minimal IMenuService implementation backed by an in-memory map. Tests can
|
||||
* register menu items with addItem() before the component renders the menu.
|
||||
*/
|
||||
export class FixtureMenuService implements IMenuService {
|
||||
declare readonly _serviceBrand: undefined;
|
||||
private readonly _items = new Map<string, IMenuItem[]>();
|
||||
constructor(
|
||||
@IContextKeyService private readonly _contextKeyService: IContextKeyService,
|
||||
@ICommandService private readonly _commandService: ICommandService,
|
||||
) { }
|
||||
addItem(menuId: MenuId, item: IMenuItem): void {
|
||||
const key = menuId.id;
|
||||
let items = this._items.get(key);
|
||||
if (!items) {
|
||||
items = [];
|
||||
this._items.set(key, items);
|
||||
}
|
||||
items.push(item);
|
||||
}
|
||||
createMenu(id: MenuId): IMenu {
|
||||
const actions: [string, MenuItemAction[]][] = [];
|
||||
for (const item of this._items.get(id.id) ?? []) {
|
||||
const group = item.group ?? '';
|
||||
let entry = actions.find(a => a[0] === group);
|
||||
if (!entry) {
|
||||
entry = [group, []];
|
||||
actions.push(entry);
|
||||
}
|
||||
entry[1].push(new MenuItemAction(item.command, item.alt, {}, undefined, undefined, this._contextKeyService, this._commandService));
|
||||
}
|
||||
return { onDidChange: Event.None, dispose() { }, getActions: () => actions };
|
||||
}
|
||||
getMenuActions() { return []; }
|
||||
getMenuContexts() { return new Set<string>(); }
|
||||
resetHiddenStates() { }
|
||||
}
|
||||
|
||||
export interface IChatFixtureServicesOptions {
|
||||
/** Observable backing IChatArtifactsService.getArtifacts().artifactGroups. */
|
||||
readonly artifactGroups?: IObservable<readonly IArtifactSourceGroup[]>;
|
||||
/** Initial todos returned from IChatTodoListService.getTodos(). */
|
||||
readonly todos?: readonly IChatTodo[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers the wide set of service mocks needed to instantiate chat widgets
|
||||
* (input part, list widget, content parts). All of these are no-op mocks
|
||||
* suitable for fixtures.
|
||||
*
|
||||
* Callers can override any service by registering it again after this call.
|
||||
*/
|
||||
export function registerChatFixtureServices(reg: ServiceRegistration, options: IChatFixtureServicesOptions = {}): void {
|
||||
registerWorkbenchServices(reg);
|
||||
reg.define(IMenuService, FixtureMenuService);
|
||||
reg.define(IMarkdownRendererService, MarkdownRendererService);
|
||||
reg.define(IListService, ListService);
|
||||
|
||||
reg.defineInstance(IDecorationsService, new class extends mock<IDecorationsService>() { override onDidChangeDecorations = Event.None; }());
|
||||
reg.defineInstance(ITextFileService, new class extends mock<ITextFileService>() { override readonly untitled = new class extends mock<ITextFileService['untitled']>() { override readonly onDidChangeLabel = Event.None; }(); }());
|
||||
reg.defineInstance(IFileService, new class extends mock<IFileService>() { override onDidFilesChange = Event.None; override onDidRunOperation = Event.None; override hasProvider() { return false; } }());
|
||||
reg.defineInstance(IEditorService, new class extends mock<IEditorService>() { override onDidActiveEditorChange = Event.None; }());
|
||||
reg.defineInstance(IExtensionService, new class extends mock<IExtensionService>() { override readonly onDidChangeExtensions = Event.None; }());
|
||||
reg.defineInstance(IPathService, new class extends mock<IPathService>() { }());
|
||||
reg.defineInstance(IWorkbenchAssignmentService, new class extends mock<IWorkbenchAssignmentService>() { override async getCurrentExperiments() { return []; } override async getTreatment() { return undefined; } override onDidRefetchAssignments = Event.None; }());
|
||||
reg.defineInstance(IWorkspaceContextService, new class extends mock<IWorkspaceContextService>() { override onDidChangeWorkspaceFolders = Event.None; override getWorkspace(): IWorkspace { return { id: '', folders: [], configuration: undefined }; } }());
|
||||
reg.defineInstance(IWorkbenchLayoutService, new class extends mock<IWorkbenchLayoutService>() { override onDidChangePartVisibility = Event.None; override onDidChangeWindowMaximized = Event.None; override isVisible() { return true; } }());
|
||||
reg.defineInstance(IViewDescriptorService, new class extends mock<IViewDescriptorService>() { override onDidChangeLocation = Event.None; }());
|
||||
reg.defineInstance(INotebookDocumentService, new class extends mock<INotebookDocumentService>() { }());
|
||||
reg.defineInstance(ISCMService, new class extends mock<ISCMService>() {
|
||||
override readonly onDidAddRepository = Event.None;
|
||||
override readonly onDidRemoveRepository = Event.None;
|
||||
override readonly repositories = [];
|
||||
override readonly repositoryCount = 0;
|
||||
}());
|
||||
reg.defineInstance(IFileDialogService, new class extends mock<IFileDialogService>() { }());
|
||||
reg.defineInstance(IProductService, new class extends mock<IProductService>() { }());
|
||||
reg.defineInstance(IUpdateService, new class extends mock<IUpdateService>() { override onStateChange = Event.None; override get state() { return { type: StateType.Uninitialized as const }; } }());
|
||||
reg.defineInstance(IUriIdentityService, new class extends mock<IUriIdentityService>() { }());
|
||||
reg.defineInstance(IActionWidgetService, new class extends mock<IActionWidgetService>() { override show() { } override hide() { } override get isVisible() { return false; } }());
|
||||
reg.defineInstance(ISharedWebContentExtractorService, new class extends mock<ISharedWebContentExtractorService>() { }());
|
||||
reg.defineInstance(IAccessibleViewService, new class extends mock<IAccessibleViewService>() { override getOpenAriaHint() { return null; } }());
|
||||
|
||||
// Chat services
|
||||
reg.define(IChatAgentService, class FixtureChatAgentService extends ChatAgentService {
|
||||
override getDefaultAgent(): IChatAgent {
|
||||
// eslint-disable-next-line local/code-no-dangerous-type-assertions
|
||||
return { fullName: 'GitHub Copilot', id: 'githubCopilot' } as unknown as IChatAgent;
|
||||
}
|
||||
});
|
||||
reg.defineInstance(IChatAgentNameService, new class extends mock<IChatAgentNameService>() {
|
||||
override getAgentNameRestriction() { return true; }
|
||||
}());
|
||||
reg.define(IChatService, MockChatService);
|
||||
reg.defineInstance(IChatWidgetService, new class extends mock<IChatWidgetService>() {
|
||||
override readonly lastFocusedWidget = undefined;
|
||||
override readonly onDidAddWidget = Event.None;
|
||||
override readonly onDidBackgroundSession = Event.None;
|
||||
override readonly onDidChangeFocusedWidget = Event.None;
|
||||
override readonly onDidChangeFocusedSession = Event.None;
|
||||
override getAllWidgets(): readonly IChatWidget[] { return []; }
|
||||
override getWidgetByInputUri() { return undefined; }
|
||||
override getWidgetBySessionResource() { return undefined; }
|
||||
override getWidgetsByLocations() { return []; }
|
||||
override register() { return { dispose() { } }; }
|
||||
}());
|
||||
reg.defineInstance(IChatAccessibilityService, new class extends mock<IChatAccessibilityService>() {
|
||||
override acceptRequest() { }
|
||||
override disposeRequest() { }
|
||||
override acceptResponse() { }
|
||||
override acceptElicitation() { }
|
||||
}());
|
||||
reg.defineInstance(IWorkbenchEnvironmentService, new class extends mock<IWorkbenchEnvironmentService>() {
|
||||
override readonly isExtensionDevelopment = false;
|
||||
override readonly isBuilt = true;
|
||||
override readonly isSessionsWindow = false;
|
||||
}());
|
||||
reg.defineInstance(IChatSessionsService, new class extends mock<IChatSessionsService>() { override getAllChatSessionContributions() { return []; } override readonly onDidChangeSessionOptions = Event.None; override readonly onDidChangeOptionGroups = Event.None; override readonly onDidChangeAvailability = Event.None; override getCustomAgentTargetForSessionType() { return Target.Undefined; } override requiresCustomModelsForSessionType() { return false; } override getOptionGroupsForSessionType() { return []; } }());
|
||||
reg.defineInstance(IChatEntitlementService, new class extends mock<IChatEntitlementService>() { }());
|
||||
reg.defineInstance(IChatModeService, new MockChatModeService());
|
||||
reg.defineInstance(ILanguageModelsService, new class extends mock<ILanguageModelsService>() { override onDidChangeLanguageModels = Event.None; override getLanguageModelIds() { return []; } }());
|
||||
reg.defineInstance(ILanguageModelToolsService, new class extends mock<ILanguageModelToolsService>() { override onDidChangeTools = Event.None; override onDidPrepareToolCallBecomeUnresponsive = Event.None; override getTools() { return []; } }());
|
||||
reg.defineInstance(IChatContextService, new class extends mock<IChatContextService>() { }());
|
||||
reg.defineInstance(IChatContextPickService, new class extends mock<IChatContextPickService>() { }());
|
||||
reg.defineInstance(IChatAttachmentWidgetRegistry, new class extends mock<IChatAttachmentWidgetRegistry>() { }());
|
||||
reg.defineInstance(IChatAttachmentResolveService, new class extends mock<IChatAttachmentResolveService>() { }());
|
||||
reg.defineInstance(IChatWidgetHistoryService, new class extends mock<IChatWidgetHistoryService>() { override getHistory() { return []; } override readonly onDidChangeHistory = Event.None; }());
|
||||
reg.defineInstance(IChatImageCarouselService, new class extends mock<IChatImageCarouselService>() { }());
|
||||
reg.defineInstance(IChatMarkdownAnchorService, new class extends mock<IChatMarkdownAnchorService>() { override register() { return { dispose() { } }; } }());
|
||||
reg.defineInstance(IChatInputNotificationService, new class extends mock<IChatInputNotificationService>() {
|
||||
override readonly onDidChange = Event.None;
|
||||
override getActiveNotification() { return undefined; }
|
||||
}());
|
||||
reg.defineInstance(IAgentSessionsService, new class extends mock<IAgentSessionsService>() { override readonly model = new class extends mock<IAgentSessionsService['model']>() { override readonly onDidChangeSessions = Event.None; }(); }());
|
||||
|
||||
const artifactGroups = options.artifactGroups ?? observableValue<readonly IArtifactSourceGroup[]>('artifactGroups', []);
|
||||
reg.defineInstance(IChatArtifactsService, new class extends mock<IChatArtifactsService>() {
|
||||
override getArtifacts(): IChatArtifacts {
|
||||
return new class extends mock<IChatArtifacts>() {
|
||||
override readonly artifactGroups = artifactGroups;
|
||||
override setAgentArtifacts() { }
|
||||
override clearAgentArtifacts() { }
|
||||
override clearSubagentArtifacts() { }
|
||||
override migrate() { }
|
||||
}();
|
||||
}
|
||||
}());
|
||||
|
||||
const todos = [...(options.todos ?? [])];
|
||||
reg.defineInstance(IChatTodoListService, new class extends mock<IChatTodoListService>() {
|
||||
override readonly onDidUpdateTodos = Event.None;
|
||||
override getTodos() { return [...todos]; }
|
||||
override setTodos() { }
|
||||
override migrateTodos() { }
|
||||
}());
|
||||
}
|
||||
@@ -8,90 +8,20 @@ import { observableValue } from '../../../../../base/common/observable.js';
|
||||
import { URI } from '../../../../../base/common/uri.js';
|
||||
import { mock } from '../../../../../base/test/common/mock.js';
|
||||
import { Codicon } from '../../../../../base/common/codicons.js';
|
||||
import { IMenuService, IMenu, MenuId, MenuItemAction, IMenuItem } from '../../../../../platform/actions/common/actions.js';
|
||||
import { ICommandService } from '../../../../../platform/commands/common/commands.js';
|
||||
import { IMenuService, MenuId } from '../../../../../platform/actions/common/actions.js';
|
||||
import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js';
|
||||
import { TestConfigurationService } from '../../../../../platform/configuration/test/common/testConfigurationService.js';
|
||||
import { IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js';
|
||||
|
||||
import { IFileDialogService } from '../../../../../platform/dialogs/common/dialogs.js';
|
||||
import { IFileService } from '../../../../../platform/files/common/files.js';
|
||||
import { ISharedWebContentExtractorService } from '../../../../../platform/webContentExtractor/common/webContentExtractor.js';
|
||||
import { IDecorationsService } from '../../../../services/decorations/common/decorations.js';
|
||||
import { ITextFileService } from '../../../../services/textfile/common/textfiles.js';
|
||||
import { IExtensionService } from '../../../../services/extensions/common/extensions.js';
|
||||
import { IPathService } from '../../../../services/path/common/pathService.js';
|
||||
import { IChatWidgetHistoryService } from '../../../../contrib/chat/common/widget/chatWidgetHistoryService.js';
|
||||
import { IChatContextPickService } from '../../../../contrib/chat/browser/attachments/chatContextPickService.js';
|
||||
import { IWorkspaceContextService, IWorkspace } from '../../../../../platform/workspace/common/workspace.js';
|
||||
import { IViewDescriptorService } from '../../../../common/views.js';
|
||||
import { IChatWidget } from '../../../../contrib/chat/browser/chat.js';
|
||||
import { IAgentSessionsService } from '../../../../contrib/chat/browser/agentSessions/agentSessionsService.js';
|
||||
import { IChatAttachmentResolveService } from '../../../../contrib/chat/browser/attachments/chatAttachmentResolveService.js';
|
||||
import { IChatAttachmentWidgetRegistry } from '../../../../contrib/chat/browser/attachments/chatAttachmentWidgetRegistry.js';
|
||||
import { IChatContextService } from '../../../../contrib/chat/browser/contextContrib/chatContextService.js';
|
||||
import { IChatImageCarouselService } from '../../../../contrib/chat/browser/chatImageCarouselService.js';
|
||||
import { ChatInputPart, IChatInputPartOptions, IChatInputStyles } from '../../../../contrib/chat/browser/widget/input/chatInputPart.js';
|
||||
import { IArtifactSourceGroup, IChatArtifacts, IChatArtifactsService } from '../../../../contrib/chat/common/tools/chatArtifactsService.js';
|
||||
import { IArtifactSourceGroup } from '../../../../contrib/chat/common/tools/chatArtifactsService.js';
|
||||
import { ChatEditingSessionState, IChatEditingSession, IModifiedFileEntry, ModifiedFileEntryState } from '../../../../contrib/chat/common/editing/chatEditingService.js';
|
||||
import { IChatRequestDisablement } from '../../../../contrib/chat/common/model/chatModel.js';
|
||||
import { IChatTodo, IChatTodoListService } from '../../../../contrib/chat/common/tools/chatTodoListService.js';
|
||||
import { IChatTodo } from '../../../../contrib/chat/common/tools/chatTodoListService.js';
|
||||
import { ChatAgentLocation, ChatConfiguration } from '../../../../contrib/chat/common/constants.js';
|
||||
import { IChatEntitlementService } from '../../../../services/chat/common/chatEntitlementService.js';
|
||||
import { IChatModeService } from '../../../../contrib/chat/common/chatModes.js';
|
||||
import { IChatService } from '../../../../contrib/chat/common/chatService/chatService.js';
|
||||
import { IChatSessionsService } from '../../../../contrib/chat/common/chatSessionsService.js';
|
||||
import { ILanguageModelsService } from '../../../../contrib/chat/common/languageModels.js';
|
||||
import { IChatAgentService } from '../../../../contrib/chat/common/participants/chatAgents.js';
|
||||
import { ILanguageModelToolsService } from '../../../../contrib/chat/common/tools/languageModelToolsService.js';
|
||||
import { IWorkbenchAssignmentService } from '../../../../services/assignment/common/assignmentService.js';
|
||||
import { IEditorService } from '../../../../services/editor/common/editorService.js';
|
||||
import { IWorkbenchLayoutService } from '../../../../services/layout/browser/layoutService.js';
|
||||
import { IActionWidgetService } from '../../../../../platform/actionWidget/browser/actionWidget.js';
|
||||
import { IProductService } from '../../../../../platform/product/common/productService.js';
|
||||
import { IUpdateService, StateType } from '../../../../../platform/update/common/update.js';
|
||||
import { IUriIdentityService } from '../../../../../platform/uriIdentity/common/uriIdentity.js';
|
||||
import { IListService, ListService } from '../../../../../platform/list/browser/listService.js';
|
||||
import { INotebookDocumentService } from '../../../../services/notebook/common/notebookDocumentService.js';
|
||||
import { ISCMService } from '../../../../contrib/scm/common/scm.js';
|
||||
import { ComponentFixtureContext, createEditorServices, defineComponentFixture, defineThemedFixtureGroup, registerWorkbenchServices } from '../fixtureUtils.js';
|
||||
import { ComponentFixtureContext, createEditorServices, defineComponentFixture, defineThemedFixtureGroup } from '../fixtureUtils.js';
|
||||
import { FixtureMenuService, registerChatFixtureServices } from './chatFixtureUtils.js';
|
||||
|
||||
import '../../../../contrib/chat/browser/widget/media/chat.css';
|
||||
import { MockChatModeService } from '../../../../contrib/chat/test/common/mockChatModeService.js';
|
||||
|
||||
class FixtureMenuService implements IMenuService {
|
||||
declare readonly _serviceBrand: undefined;
|
||||
private readonly _items = new Map<string, IMenuItem[]>();
|
||||
constructor(
|
||||
@IContextKeyService private readonly _contextKeyService: IContextKeyService,
|
||||
@ICommandService private readonly _commandService: ICommandService,
|
||||
) { }
|
||||
addItem(menuId: MenuId, item: IMenuItem): void {
|
||||
const key = menuId.id;
|
||||
let items = this._items.get(key);
|
||||
if (!items) {
|
||||
items = [];
|
||||
this._items.set(key, items);
|
||||
}
|
||||
items.push(item);
|
||||
}
|
||||
createMenu(id: MenuId): IMenu {
|
||||
const actions: [string, MenuItemAction[]][] = [];
|
||||
for (const item of this._items.get(id.id) ?? []) {
|
||||
const group = item.group ?? '';
|
||||
let entry = actions.find(a => a[0] === group);
|
||||
if (!entry) {
|
||||
entry = [group, []];
|
||||
actions.push(entry);
|
||||
}
|
||||
entry[1].push(new MenuItemAction(item.command, item.alt, {}, undefined, undefined, this._contextKeyService, this._commandService));
|
||||
}
|
||||
return { onDidChange: Event.None, dispose() { }, getActions: () => actions };
|
||||
}
|
||||
getMenuActions() { return []; }
|
||||
getMenuContexts() { return new Set<string>(); }
|
||||
resetHiddenStates() { }
|
||||
}
|
||||
|
||||
interface ChatInputFixtureOptions {
|
||||
readonly artifacts?: readonly { label: string; uri: string; type: 'devServer' | 'screenshot' | 'plan' | undefined }[];
|
||||
@@ -108,63 +38,7 @@ async function renderChatInput(context: ComponentFixtureContext, fixtureOptions:
|
||||
const instantiationService = createEditorServices(disposableStore, {
|
||||
colorTheme: context.theme,
|
||||
additionalServices: (reg) => {
|
||||
registerWorkbenchServices(reg);
|
||||
reg.define(IMenuService, FixtureMenuService);
|
||||
reg.defineInstance(IDecorationsService, new class extends mock<IDecorationsService>() { override onDidChangeDecorations = Event.None; }());
|
||||
reg.defineInstance(ITextFileService, new class extends mock<ITextFileService>() { override readonly untitled = new class extends mock<ITextFileService['untitled']>() { override readonly onDidChangeLabel = Event.None; }(); }());
|
||||
reg.defineInstance(ILanguageModelsService, new class extends mock<ILanguageModelsService>() { override onDidChangeLanguageModels = Event.None; override getLanguageModelIds() { return []; } }());
|
||||
reg.defineInstance(IFileService, new class extends mock<IFileService>() { override onDidFilesChange = Event.None; override onDidRunOperation = Event.None; }());
|
||||
reg.defineInstance(IEditorService, new class extends mock<IEditorService>() { override onDidActiveEditorChange = Event.None; }());
|
||||
reg.defineInstance(IChatAgentService, new class extends mock<IChatAgentService>() { override onDidChangeAgents = Event.None; override getAgents() { return []; } override getActivatedAgents() { return []; } }());
|
||||
reg.defineInstance(ISharedWebContentExtractorService, new class extends mock<ISharedWebContentExtractorService>() { }());
|
||||
reg.defineInstance(IWorkbenchAssignmentService, new class extends mock<IWorkbenchAssignmentService>() { override async getCurrentExperiments() { return []; } override async getTreatment() { return undefined; } override onDidRefetchAssignments = Event.None; }());
|
||||
reg.defineInstance(IChatEntitlementService, new class extends mock<IChatEntitlementService>() { }());
|
||||
reg.defineInstance(IChatModeService, new MockChatModeService());
|
||||
reg.defineInstance(ILanguageModelToolsService, new class extends mock<ILanguageModelToolsService>() { override onDidChangeTools = Event.None; override getTools() { return []; } }());
|
||||
reg.defineInstance(IChatService, new class extends mock<IChatService>() { override onDidSubmitRequest = Event.None; }());
|
||||
reg.defineInstance(IChatSessionsService, new class extends mock<IChatSessionsService>() { override getAllChatSessionContributions() { return []; } override readonly onDidChangeSessionOptions = Event.None; override readonly onDidChangeOptionGroups = Event.None; override readonly onDidChangeAvailability = Event.None; }());
|
||||
reg.defineInstance(IChatContextService, new class extends mock<IChatContextService>() { }());
|
||||
reg.defineInstance(IAgentSessionsService, new class extends mock<IAgentSessionsService>() { override readonly model = new class extends mock<IAgentSessionsService['model']>() { override readonly onDidChangeSessions = Event.None; }(); }());
|
||||
reg.defineInstance(IWorkspaceContextService, new class extends mock<IWorkspaceContextService>() { override onDidChangeWorkspaceFolders = Event.None; override getWorkspace(): IWorkspace { return { id: '', folders: [], configuration: undefined }; } }());
|
||||
reg.defineInstance(IWorkbenchLayoutService, new class extends mock<IWorkbenchLayoutService>() { override onDidChangePartVisibility = Event.None; override onDidChangeWindowMaximized = Event.None; override isVisible() { return true; } }());
|
||||
reg.defineInstance(IViewDescriptorService, new class extends mock<IViewDescriptorService>() { override onDidChangeLocation = Event.None; }());
|
||||
reg.defineInstance(IChatAttachmentWidgetRegistry, new class extends mock<IChatAttachmentWidgetRegistry>() { }());
|
||||
reg.defineInstance(IChatAttachmentResolveService, new class extends mock<IChatAttachmentResolveService>() { }());
|
||||
reg.defineInstance(IExtensionService, new class extends mock<IExtensionService>() { override readonly onDidChangeExtensions = Event.None; }());
|
||||
reg.defineInstance(IPathService, new class extends mock<IPathService>() { }());
|
||||
reg.defineInstance(IChatWidgetHistoryService, new class extends mock<IChatWidgetHistoryService>() { override getHistory() { return []; } override readonly onDidChangeHistory = Event.None; }());
|
||||
reg.defineInstance(IChatContextPickService, new class extends mock<IChatContextPickService>() { }());
|
||||
reg.defineInstance(IListService, new ListService());
|
||||
reg.defineInstance(INotebookDocumentService, new class extends mock<INotebookDocumentService>() { }());
|
||||
reg.defineInstance(ISCMService, new class extends mock<ISCMService>() {
|
||||
override readonly onDidAddRepository = Event.None;
|
||||
override readonly onDidRemoveRepository = Event.None;
|
||||
override readonly repositories = [];
|
||||
override readonly repositoryCount = 0;
|
||||
}());
|
||||
reg.defineInstance(IActionWidgetService, new class extends mock<IActionWidgetService>() { override show() { } override hide() { } override get isVisible() { return false; } }());
|
||||
reg.defineInstance(IFileDialogService, new class extends mock<IFileDialogService>() { }());
|
||||
reg.defineInstance(IProductService, new class extends mock<IProductService>() { }());
|
||||
reg.defineInstance(IChatImageCarouselService, new class extends mock<IChatImageCarouselService>() { }());
|
||||
reg.defineInstance(IUpdateService, new class extends mock<IUpdateService>() { override onStateChange = Event.None; override get state() { return { type: StateType.Uninitialized as const }; } }());
|
||||
reg.defineInstance(IUriIdentityService, new class extends mock<IUriIdentityService>() { }());
|
||||
reg.defineInstance(IChatArtifactsService, new class extends mock<IChatArtifactsService>() {
|
||||
override getArtifacts(): IChatArtifacts {
|
||||
return new class extends mock<IChatArtifacts>() {
|
||||
override readonly artifactGroups = artifactsObs;
|
||||
override setAgentArtifacts() { }
|
||||
override clearAgentArtifacts() { }
|
||||
override clearSubagentArtifacts() { }
|
||||
override migrate() { }
|
||||
}();
|
||||
}
|
||||
}());
|
||||
reg.defineInstance(IChatTodoListService, new class extends mock<IChatTodoListService>() {
|
||||
override readonly onDidUpdateTodos = Event.None;
|
||||
override getTodos() { return [...todos]; }
|
||||
override setTodos() { }
|
||||
override migrateTodos() { }
|
||||
}());
|
||||
registerChatFixtureServices(reg, { artifactGroups: artifactsObs, todos });
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,303 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as dom from '../../../../../base/browser/dom.js';
|
||||
import { Emitter, Event } from '../../../../../base/common/event.js';
|
||||
import { MarkdownString } from '../../../../../base/common/htmlContent.js';
|
||||
import { mock } from '../../../../../base/test/common/mock.js';
|
||||
import { Codicon } from '../../../../../base/common/codicons.js';
|
||||
import { generateUuid } from '../../../../../base/common/uuid.js';
|
||||
import { OffsetRange } from '../../../../../editor/common/core/ranges/offsetRange.js';
|
||||
import { Range } from '../../../../../editor/common/core/range.js';
|
||||
import { IMenuService, MenuId } from '../../../../../platform/actions/common/actions.js';
|
||||
import { ChatRequestTextPart } from '../../../../contrib/chat/common/requestParser/chatParserTypes.js';
|
||||
import { ChatModel } from '../../../../contrib/chat/common/model/chatModel.js';
|
||||
import { ChatViewModel } from '../../../../contrib/chat/common/model/chatViewModel.js';
|
||||
import { ChatListWidget } from '../../../../contrib/chat/browser/widget/chatListWidget.js';
|
||||
import { ChatInputPart, IChatInputPartOptions, IChatInputStyles } from '../../../../contrib/chat/browser/widget/input/chatInputPart.js';
|
||||
import { IChatWidget, IChatWidgetService } from '../../../../contrib/chat/browser/chat.js';
|
||||
import { IChatService } from '../../../../contrib/chat/common/chatService/chatService.js';
|
||||
import { ChatToolInvocation } from '../../../../contrib/chat/common/model/chatProgressTypes/chatToolInvocation.js';
|
||||
import { IToolData, ToolDataSource } from '../../../../contrib/chat/common/tools/languageModelToolsService.js';
|
||||
import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js';
|
||||
import { TestConfigurationService } from '../../../../../platform/configuration/test/common/testConfigurationService.js';
|
||||
import { ChatAgentLocation, ChatConfiguration, ChatModeKind } from '../../../../contrib/chat/common/constants.js';
|
||||
import { MockChatService } from '../../../../contrib/chat/test/common/chatService/mockChatService.js';
|
||||
import { ComponentFixtureContext, createEditorServices, defineComponentFixture, defineThemedFixtureGroup } from '../fixtureUtils.js';
|
||||
import { FixtureMenuService, registerChatFixtureServices } from './chatFixtureUtils.js';
|
||||
|
||||
import '../../../../contrib/chat/browser/widget/media/chat.css';
|
||||
|
||||
interface IFixtureMessage {
|
||||
readonly user: string; // user prompt text
|
||||
readonly assistant?: ReadonlyArray<
|
||||
| { kind: 'markdown'; text: string }
|
||||
| { kind: 'progress'; text: string }
|
||||
| { kind: 'terminalConfirmation'; command: string; title?: string }
|
||||
>;
|
||||
readonly responseComplete?: boolean;
|
||||
}
|
||||
|
||||
interface IChatWidgetFixtureOptions {
|
||||
readonly messages: ReadonlyArray<IFixtureMessage>;
|
||||
readonly withInput?: boolean;
|
||||
}
|
||||
|
||||
function makeUserMessage(text: string) {
|
||||
return {
|
||||
text,
|
||||
parts: [new ChatRequestTextPart(new OffsetRange(0, text.length), new Range(1, 1, 1, text.length + 1), text)],
|
||||
};
|
||||
}
|
||||
|
||||
async function renderChatWidget(context: ComponentFixtureContext, options: IChatWidgetFixtureOptions): Promise<void> {
|
||||
const { container, disposableStore } = context;
|
||||
|
||||
const widgetHolder: { current: IChatWidget | undefined } = { current: undefined };
|
||||
|
||||
const instantiationService = createEditorServices(disposableStore, {
|
||||
colorTheme: context.theme,
|
||||
additionalServices: (reg) => {
|
||||
registerChatFixtureServices(reg);
|
||||
// Override widget service so the chat list renderer can route tool
|
||||
// confirmations to the carousel attached to our input part.
|
||||
reg.defineInstance(IChatWidgetService, new class extends mock<IChatWidgetService>() {
|
||||
override readonly lastFocusedWidget = undefined;
|
||||
override readonly onDidAddWidget = Event.None;
|
||||
override readonly onDidBackgroundSession = Event.None;
|
||||
override readonly onDidChangeFocusedWidget = Event.None;
|
||||
override readonly onDidChangeFocusedSession = Event.None;
|
||||
override getAllWidgets() { return widgetHolder.current ? [widgetHolder.current] : []; }
|
||||
override getWidgetByInputUri() { return undefined; }
|
||||
override getWidgetBySessionResource() { return widgetHolder.current; }
|
||||
override getWidgetsByLocations() { return []; }
|
||||
override register() { return { dispose() { } }; }
|
||||
}());
|
||||
},
|
||||
});
|
||||
|
||||
const configService = instantiationService.get(IConfigurationService) as TestConfigurationService;
|
||||
await configService.setUserConfiguration('chat', {
|
||||
editor: { fontSize: 13, fontFamily: 'default', fontWeight: 'default', lineHeight: 0, wordWrap: 'off' },
|
||||
});
|
||||
await configService.setUserConfiguration('editor', { fontFamily: 'monospace', fontLigatures: false });
|
||||
configService.setUserConfiguration(ChatConfiguration.ToolConfirmationCarousel, true);
|
||||
|
||||
// Build a real ChatModel populated with hand-crafted requests/responses, then drive a
|
||||
// real ChatViewModel + ChatListWidget — the same components used in production.
|
||||
const chatService = instantiationService.get(IChatService) as MockChatService;
|
||||
const model = disposableStore.add(instantiationService.createInstance(
|
||||
ChatModel,
|
||||
undefined,
|
||||
{ initialLocation: ChatAgentLocation.Chat, canUseTools: true }
|
||||
));
|
||||
chatService.addSession(model);
|
||||
|
||||
const fixtureToolData: IToolData = {
|
||||
id: 'fixture.terminalTool',
|
||||
displayName: 'Terminal',
|
||||
modelDescription: 'Run a command in the terminal',
|
||||
source: ToolDataSource.Internal,
|
||||
};
|
||||
|
||||
for (const message of options.messages) {
|
||||
const request = model.addRequest(makeUserMessage(message.user), { variables: [] }, 0);
|
||||
const response = request.response!;
|
||||
for (const part of message.assistant ?? []) {
|
||||
if (part.kind === 'markdown') {
|
||||
model.acceptResponseProgress(request, { kind: 'markdownContent', content: new MarkdownString(part.text) });
|
||||
} else if (part.kind === 'progress') {
|
||||
model.acceptResponseProgress(request, { kind: 'progressMessage', content: new MarkdownString(part.text) });
|
||||
} else if (part.kind === 'terminalConfirmation') {
|
||||
const title = part.title ?? `Run pwsh command?`;
|
||||
const toolInvocation = new ChatToolInvocation(
|
||||
{
|
||||
invocationMessage: new MarkdownString(`Running \`${part.command}\``),
|
||||
pastTenseMessage: new MarkdownString(`Ran \`${part.command}\``),
|
||||
confirmationMessages: { title, message: new MarkdownString(`\`${part.command}\``) },
|
||||
toolSpecificData: {
|
||||
kind: 'terminal',
|
||||
commandLine: { original: part.command },
|
||||
language: 'pwsh',
|
||||
},
|
||||
},
|
||||
fixtureToolData,
|
||||
generateUuid(),
|
||||
undefined,
|
||||
{ command: part.command },
|
||||
);
|
||||
model.acceptResponseProgress(request, toolInvocation);
|
||||
}
|
||||
}
|
||||
if (message.responseComplete !== false) {
|
||||
response.complete();
|
||||
}
|
||||
}
|
||||
|
||||
const viewModel = disposableStore.add(instantiationService.createInstance(ChatViewModel, model, undefined));
|
||||
|
||||
container.style.width = '720px';
|
||||
container.style.height = '600px';
|
||||
container.style.backgroundColor = 'var(--vscode-sideBar-background, var(--vscode-editor-background))';
|
||||
container.classList.add('monaco-workbench');
|
||||
|
||||
// Mirror the product DOM ancestry: the chat widget lives inside
|
||||
// `.part.auxiliarybar > .content`, where auxiliaryBarPart.css recolors
|
||||
// inline editors with `--vscode-sideBar-background` (used by the carousel).
|
||||
const auxBar = dom.$('.part.auxiliarybar');
|
||||
auxBar.style.width = '100%';
|
||||
auxBar.style.height = '100%';
|
||||
const auxContent = dom.$('.content');
|
||||
auxContent.style.width = '100%';
|
||||
auxContent.style.height = '100%';
|
||||
auxBar.appendChild(auxContent);
|
||||
container.appendChild(auxBar);
|
||||
|
||||
const session = dom.$('.interactive-session');
|
||||
auxContent.appendChild(session);
|
||||
|
||||
// Build the input part FIRST so the widget (with its inputPart) is registered
|
||||
// in IChatWidgetService before the list widget renders. The renderer queries
|
||||
// the service synchronously when routing tool confirmations to the carousel.
|
||||
let inputPart: ChatInputPart | undefined;
|
||||
if (options.withInput) {
|
||||
const menuService = instantiationService.get(IMenuService) as FixtureMenuService;
|
||||
menuService.addItem(MenuId.ChatInput, { command: { id: 'workbench.action.chat.attachContext', title: '+', icon: Codicon.add }, group: 'navigation', order: -1 });
|
||||
menuService.addItem(MenuId.ChatInput, { command: { id: 'workbench.action.chat.openModePicker', title: 'Agent' }, group: 'navigation', order: 1 });
|
||||
menuService.addItem(MenuId.ChatInput, { command: { id: 'workbench.action.chat.openModelPicker', title: 'GPT-5.3-Codex' }, group: 'navigation', order: 3 });
|
||||
menuService.addItem(MenuId.ChatInput, { command: { id: 'workbench.action.chat.configureTools', title: '', icon: Codicon.settingsGear }, group: 'navigation', order: 100 });
|
||||
menuService.addItem(MenuId.ChatExecute, { command: { id: 'workbench.action.chat.submit', title: 'Send', icon: Codicon.arrowUp }, group: 'navigation', order: 4 });
|
||||
menuService.addItem(MenuId.ChatInputSecondary, { command: { id: 'workbench.action.chat.openSessionTargetPicker', title: 'Local' }, group: 'navigation', order: 0 });
|
||||
menuService.addItem(MenuId.ChatInputSecondary, { command: { id: 'workbench.action.chat.openPermissionPicker', title: 'Default Approvals' }, group: 'navigation', order: 10 });
|
||||
|
||||
const inputOptions: IChatInputPartOptions = {
|
||||
renderFollowups: false,
|
||||
renderInputToolbarBelowInput: false,
|
||||
renderWorkingSet: false,
|
||||
menus: { executeToolbar: MenuId.ChatExecute, telemetrySource: 'fixture' },
|
||||
widgetViewKindTag: 'view',
|
||||
inputEditorMinLines: 2,
|
||||
};
|
||||
const inputStyles: IChatInputStyles = {
|
||||
overlayBackground: 'var(--vscode-editor-background)',
|
||||
listForeground: 'var(--vscode-foreground)',
|
||||
listBackground: 'var(--vscode-editor-background)',
|
||||
};
|
||||
|
||||
inputPart = disposableStore.add(instantiationService.createInstance(ChatInputPart, ChatAgentLocation.Chat, inputOptions, inputStyles, false));
|
||||
}
|
||||
|
||||
const fixtureWidget = new class extends mock<IChatWidget>() {
|
||||
override readonly onDidChangeViewModel = new Emitter<never>().event;
|
||||
override readonly viewModel = viewModel;
|
||||
override readonly contribs = [];
|
||||
override readonly location = ChatAgentLocation.Chat;
|
||||
override readonly viewContext = {};
|
||||
override readonly inputPart = inputPart!;
|
||||
}();
|
||||
widgetHolder.current = fixtureWidget;
|
||||
|
||||
if (inputPart) {
|
||||
inputPart.render(session, '', fixtureWidget);
|
||||
inputPart.layout(720);
|
||||
await new Promise(r => setTimeout(r, 50));
|
||||
inputPart.layout(720);
|
||||
}
|
||||
|
||||
const listContainer = dom.$('.interactive-list');
|
||||
listContainer.style.flex = '1 1 auto';
|
||||
listContainer.style.minHeight = '0';
|
||||
listContainer.style.position = 'relative';
|
||||
// Prepend the list before the input so the visual order matches production.
|
||||
session.insertBefore(listContainer, session.firstChild);
|
||||
|
||||
const listWidget = disposableStore.add(instantiationService.createInstance(
|
||||
ChatListWidget,
|
||||
listContainer,
|
||||
{
|
||||
currentChatMode: () => ChatModeKind.Agent,
|
||||
defaultElementHeight: 120,
|
||||
renderStyle: 'compact',
|
||||
styles: {
|
||||
listForeground: 'var(--vscode-foreground)',
|
||||
listBackground: 'var(--vscode-editor-background)',
|
||||
},
|
||||
location: ChatAgentLocation.Chat,
|
||||
rendererOptions: {
|
||||
progressMessageAtBottomOfResponse: mode => mode !== ChatModeKind.Ask,
|
||||
},
|
||||
},
|
||||
));
|
||||
listWidget.setViewModel(viewModel);
|
||||
listWidget.setVisible(true);
|
||||
listWidget.refresh();
|
||||
|
||||
const listHeight = options.withInput ? 420 : 600;
|
||||
listWidget.layout(listHeight, 720);
|
||||
|
||||
// Allow the renderer to flush its async progressive rendering pass.
|
||||
await new Promise(r => setTimeout(r, 100));
|
||||
listWidget.layout(listHeight, 720);
|
||||
listWidget.scrollTop = 0;
|
||||
}
|
||||
|
||||
const SIMPLE_QA: IFixtureMessage[] = [
|
||||
{
|
||||
user: 'Add a fibonacci function to fibon.ts',
|
||||
assistant: [
|
||||
{ kind: 'markdown', text: 'I added a recursive `fibonacci(n)` to `fibon.ts`. Note that recursion is exponential — for large `n` consider an iterative version.' },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const PENDING_TOOL_APPROVAL: IFixtureMessage[] = [
|
||||
{
|
||||
user: 'run git init',
|
||||
assistant: [
|
||||
{ kind: 'terminalConfirmation', command: 'git init' },
|
||||
],
|
||||
responseComplete: false,
|
||||
},
|
||||
];
|
||||
|
||||
const STREAMING: IFixtureMessage[] = [
|
||||
{
|
||||
user: 'Search the workspace for TODO comments',
|
||||
assistant: [
|
||||
{ kind: 'progress', text: 'Searching workspace for `TODO` comments...' },
|
||||
],
|
||||
responseComplete: false,
|
||||
},
|
||||
];
|
||||
|
||||
const MULTI_TURN: IFixtureMessage[] = [
|
||||
{
|
||||
user: 'What does this project do?',
|
||||
assistant: [
|
||||
{ kind: 'markdown', text: 'This project is **Visual Studio Code**, a free source-code editor made by Microsoft for Windows, Linux and macOS.' },
|
||||
],
|
||||
},
|
||||
{
|
||||
user: 'Where is the entrypoint?',
|
||||
assistant: [
|
||||
{ kind: 'markdown', text: 'The desktop entrypoint is in `src/vs/code/electron-main/main.ts`. The browser/server entrypoints live under `src/vs/server/`.' },
|
||||
],
|
||||
},
|
||||
{
|
||||
user: 'Thanks!',
|
||||
assistant: [
|
||||
{ kind: 'markdown', text: 'You are welcome — let me know if you have more questions.' },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export default defineThemedFixtureGroup({ path: 'chat/widget/' }, {
|
||||
SimpleQA: defineComponentFixture({ render: ctx => renderChatWidget(ctx, { messages: SIMPLE_QA }) }),
|
||||
Streaming: defineComponentFixture({ labels: { kind: 'animated' }, render: ctx => renderChatWidget(ctx, { messages: STREAMING }) }),
|
||||
PendingToolApproval: defineComponentFixture({ render: ctx => renderChatWidget(ctx, { messages: PENDING_TOOL_APPROVAL }) }),
|
||||
PendingToolApprovalWithInput: defineComponentFixture({ render: ctx => renderChatWidget(ctx, { messages: PENDING_TOOL_APPROVAL, withInput: true }) }),
|
||||
MultiTurn: defineComponentFixture({ render: ctx => renderChatWidget(ctx, { messages: MULTI_TURN }) }),
|
||||
WithInput: defineComponentFixture({ render: ctx => renderChatWidget(ctx, { messages: MULTI_TURN, withInput: true }) }),
|
||||
});
|
||||
@@ -8,12 +8,17 @@ import { Event, ValueWithChangeEvent } from '../../../../../base/common/event.js
|
||||
import { DisposableStore, toDisposable } from '../../../../../base/common/lifecycle.js';
|
||||
import { URI } from '../../../../../base/common/uri.js';
|
||||
import { mock } from '../../../../../base/test/common/mock.js';
|
||||
import { CancellationToken } from '../../../../../base/common/cancellation.js';
|
||||
import { createTimeout, timeout } from '../../../../../base/common/async.js';
|
||||
import { MultiDiffEditorWidget } from '../../../../../editor/browser/widget/multiDiffEditor/multiDiffEditorWidget.js';
|
||||
import { IDocumentDiffItem, IMultiDiffEditorModel } from '../../../../../editor/browser/widget/multiDiffEditor/model.js';
|
||||
import { IResourceLabel as IMultiDiffResourceLabel, IWorkbenchUIElementFactory } from '../../../../../editor/browser/widget/multiDiffEditor/workbenchUIElementFactory.js';
|
||||
import { RefCounted } from '../../../../../editor/browser/widget/diffEditor/utils.js';
|
||||
import { IDiffProviderFactoryService } from '../../../../../editor/browser/widget/diffEditor/diffProviderFactoryService.js';
|
||||
import { TestDiffProviderFactoryService } from '../../../../../editor/test/browser/diff/testDiffProviderFactoryService.js';
|
||||
import { IDocumentDiff, IDocumentDiffProvider, IDocumentDiffProviderOptions } from '../../../../../editor/common/diff/documentDiffProvider.js';
|
||||
import { linesDiffComputers } from '../../../../../editor/common/diff/linesDiffComputers.js';
|
||||
import { ITextModel } from '../../../../../editor/common/model.js';
|
||||
import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';
|
||||
import { IEditorProgressService } from '../../../../../platform/progress/common/progress.js';
|
||||
import { IWorkspaceContextService, IWorkspace } from '../../../../../platform/workspace/common/workspace.js';
|
||||
@@ -22,6 +27,7 @@ import { IDecorationsService } from '../../../../services/decorations/common/dec
|
||||
import { INotebookDocumentService } from '../../../../services/notebook/common/notebookDocumentService.js';
|
||||
import { ITextFileService } from '../../../../services/textfile/common/textfiles.js';
|
||||
import { ComponentFixtureContext, createEditorServices, createTextModel, defineComponentFixture, defineThemedFixtureGroup, registerWorkbenchServices } from '../fixtureUtils.js';
|
||||
import { TestInstantiationService } from '../../../../../platform/instantiation/test/common/instantiationServiceMock.js';
|
||||
|
||||
class FixtureWorkbenchUIElementFactory implements IWorkbenchUIElementFactory {
|
||||
constructor(
|
||||
@@ -110,10 +116,57 @@ function renderMultiDiffEditor({ container, disposableStore, theme }: ComponentF
|
||||
container.style.height = '600px';
|
||||
container.style.border = '1px solid var(--vscode-editorWidget-border)';
|
||||
|
||||
const instantiationService = createEditorServices(disposableStore, {
|
||||
const instantiationService = createCommonServices(disposableStore, theme, new TestDiffProviderFactoryService());
|
||||
const { widget, textModels } = createWidget(instantiationService, disposableStore, container);
|
||||
const { doc1, doc2, doc3 } = createDocuments(instantiationService, textModels);
|
||||
|
||||
const model: IMultiDiffEditorModel = {
|
||||
documents: ValueWithChangeEvent.const([doc1, doc2, doc3]),
|
||||
};
|
||||
|
||||
const viewModel = widget.createViewModel(model);
|
||||
widget.setViewModel(viewModel);
|
||||
widget.layout(new Dimension(800, 600));
|
||||
}
|
||||
|
||||
class DelayedDiffProviderFactoryService implements IDiffProviderFactoryService {
|
||||
declare readonly _serviceBrand: undefined;
|
||||
constructor(private readonly _delayMs: number) { }
|
||||
createDiffProvider(): IDocumentDiffProvider {
|
||||
return new DelayedDocumentDiffProvider(this._delayMs);
|
||||
}
|
||||
}
|
||||
|
||||
class DelayedDocumentDiffProvider implements IDocumentDiffProvider {
|
||||
readonly onDidChange: Event<void> = () => toDisposable(() => { });
|
||||
constructor(private readonly _delayMs: number) { }
|
||||
|
||||
async computeDiff(original: ITextModel, modified: ITextModel, options: IDocumentDiffProviderOptions, _cancellationToken: CancellationToken): Promise<IDocumentDiff> {
|
||||
await timeout(this._delayMs);
|
||||
if (_cancellationToken.isCancellationRequested || original.isDisposed() || modified.isDisposed()) {
|
||||
return ({
|
||||
changes: [],
|
||||
quitEarly: true,
|
||||
identical: false,
|
||||
moves: [],
|
||||
|
||||
});
|
||||
}
|
||||
const result = linesDiffComputers.getDefault().computeDiff(original.getLinesContent(), modified.getLinesContent(), options);
|
||||
return {
|
||||
changes: result.changes,
|
||||
quitEarly: result.hitTimeout,
|
||||
identical: original.getValue() === modified.getValue(),
|
||||
moves: result.moves,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function createCommonServices(disposableStore: DisposableStore, theme: ComponentFixtureContext['theme'], diffProviderFactory: IDiffProviderFactoryService) {
|
||||
return createEditorServices(disposableStore, {
|
||||
colorTheme: theme,
|
||||
additionalServices: (reg) => {
|
||||
reg.define(IDiffProviderFactoryService, TestDiffProviderFactoryService);
|
||||
reg.defineInstance(IDiffProviderFactoryService, diffProviderFactory);
|
||||
reg.definePartialInstance(IEditorProgressService, {
|
||||
show: () => ({ total: () => { }, worked: () => { }, done: () => { } }),
|
||||
});
|
||||
@@ -124,47 +177,108 @@ function renderMultiDiffEditor({ container, disposableStore, theme }: ComponentF
|
||||
registerWorkbenchServices(reg);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function createWidget(instantiationService: IInstantiationService, disposableStore: DisposableStore, container: HTMLElement) {
|
||||
const uiFactory = instantiationService.createInstance(FixtureWorkbenchUIElementFactory);
|
||||
|
||||
const widget = disposableStore.add(instantiationService.createInstance(
|
||||
MultiDiffEditorWidget,
|
||||
container,
|
||||
uiFactory,
|
||||
));
|
||||
|
||||
// Text models must be disposed after the widget releases its references.
|
||||
// DisposableStore disposes in insertion order, so we add a cleanup disposable
|
||||
// after the widget that first clears the view model, then disposes text models.
|
||||
const textModels = new DisposableStore();
|
||||
disposableStore.add(toDisposable(() => {
|
||||
widget.setViewModel(undefined);
|
||||
textModels.dispose();
|
||||
}));
|
||||
return { widget, textModels };
|
||||
}
|
||||
|
||||
function createDocuments(instantiationService: TestInstantiationService, textModels: DisposableStore) {
|
||||
const original1 = textModels.add(createTextModel(instantiationService, ORIGINAL_CODE_1, URI.parse('inmemory://original/greet.ts'), 'typescript'));
|
||||
const modified1 = textModels.add(createTextModel(instantiationService, MODIFIED_CODE_1, URI.parse('inmemory://modified/greet.ts'), 'typescript'));
|
||||
|
||||
const original2 = textModels.add(createTextModel(instantiationService, ORIGINAL_CODE_2, URI.parse('inmemory://original/config.ts'), 'typescript'));
|
||||
const modified2 = textModels.add(createTextModel(instantiationService, MODIFIED_CODE_2, URI.parse('inmemory://modified/config.ts'), 'typescript'));
|
||||
|
||||
const original3 = textModels.add(createTextModel(instantiationService, ORIGINAL_CODE_3, URI.parse('inmemory://original/server.ts'), 'typescript'));
|
||||
const modified3 = textModels.add(createTextModel(instantiationService, MODIFIED_CODE_3, URI.parse('inmemory://modified/server.ts'), 'typescript'));
|
||||
|
||||
const documents: RefCounted<IDocumentDiffItem>[] = [
|
||||
RefCounted.createOfNonDisposable<IDocumentDiffItem>({ original: original1, modified: modified1 }, { dispose() { } }),
|
||||
RefCounted.createOfNonDisposable<IDocumentDiffItem>({ original: original2, modified: modified2 }, { dispose() { } }),
|
||||
RefCounted.createOfNonDisposable<IDocumentDiffItem>({ original: original3, modified: modified3 }, { dispose() { } }),
|
||||
];
|
||||
|
||||
const model: IMultiDiffEditorModel = {
|
||||
documents: ValueWithChangeEvent.const(documents),
|
||||
return {
|
||||
doc1: RefCounted.createOfNonDisposable<IDocumentDiffItem>({ original: original1, modified: modified1 }, { dispose() { } }),
|
||||
doc2: RefCounted.createOfNonDisposable<IDocumentDiffItem>({ original: original2, modified: modified2 }, { dispose() { } }),
|
||||
doc3: RefCounted.createOfNonDisposable<IDocumentDiffItem>({ original: original3, modified: modified3 }, { dispose() { } }),
|
||||
};
|
||||
}
|
||||
|
||||
const viewModel = widget.createViewModel(model);
|
||||
widget.setViewModel(viewModel);
|
||||
function renderMultiDiffEditorIncrementalUpdate() {
|
||||
return ({ container, disposableStore, theme }: ComponentFixtureContext) => {
|
||||
container.style.width = '800px';
|
||||
container.style.height = '600px';
|
||||
container.style.border = '1px solid var(--vscode-editorWidget-border)';
|
||||
|
||||
widget.layout(new Dimension(800, 600));
|
||||
// First file: sync diffs (already resolved). Files 2+3: 800ms delay.
|
||||
const delayedFactory = new DelayedDiffProviderFactoryService(800);
|
||||
const instantiationService = createCommonServices(disposableStore, theme, delayedFactory);
|
||||
const { widget, textModels } = createWidget(instantiationService, disposableStore, container);
|
||||
const { doc1, doc2, doc3 } = createDocuments(instantiationService, textModels);
|
||||
|
||||
// Start with only doc1 — its diff resolves immediately (800ms virtual)
|
||||
const documents = new ValueWithChangeEvent<readonly RefCounted<IDocumentDiffItem>[]>([doc1]);
|
||||
const model: IMultiDiffEditorModel = { documents };
|
||||
const viewModel = widget.createViewModel(model);
|
||||
widget.setViewModel(viewModel);
|
||||
widget.layout(new Dimension(800, 600));
|
||||
|
||||
|
||||
// At T=900ms: add doc2 and doc3. Their diffs take 800ms (resolve at T=1700ms).
|
||||
// The 1s gate means they appear at min(T=1700ms, T=1900ms) = T=1700ms.
|
||||
disposableStore.add(createTimeout(900, () => {
|
||||
documents.value = [doc1, doc2, doc3];
|
||||
}));
|
||||
};
|
||||
}
|
||||
|
||||
function renderMultiDiffEditorDocumentSwap() {
|
||||
return ({ container, disposableStore, theme }: ComponentFixtureContext) => {
|
||||
container.style.width = '800px';
|
||||
container.style.height = '600px';
|
||||
container.style.border = '1px solid var(--vscode-editorWidget-border)';
|
||||
|
||||
const delayedFactory = new DelayedDiffProviderFactoryService(800);
|
||||
const instantiationService = createCommonServices(disposableStore, theme, delayedFactory);
|
||||
const { widget, textModels } = createWidget(instantiationService, disposableStore, container);
|
||||
|
||||
const makeDoc = (origText: string, modText: string, name: string) => {
|
||||
const original = textModels.add(createTextModel(instantiationService, origText, URI.parse(`inmemory://original/${name}`), 'typescript'));
|
||||
const modified = textModels.add(createTextModel(instantiationService, modText, URI.parse(`inmemory://modified/${name}`), 'typescript'));
|
||||
return RefCounted.createOfNonDisposable<IDocumentDiffItem>({ original, modified }, { dispose() { } });
|
||||
};
|
||||
|
||||
// Each document has exactly one line change.
|
||||
const codeA_orig = 'const greeting = "hello";';
|
||||
const codeA_mod = 'const greeting = "hi";';
|
||||
const codeB_orig = 'const port = 3000;';
|
||||
const codeB_mod = 'const port = 8080;';
|
||||
const codeD_orig = 'const env = "development";';
|
||||
const codeD_mod = 'const env = "production";';
|
||||
|
||||
const docA = makeDoc(codeA_orig, codeA_mod, 'greet.ts');
|
||||
const docB = makeDoc(codeB_orig, codeB_mod, 'config.ts');
|
||||
|
||||
// Start with A and B
|
||||
const documents = new ValueWithChangeEvent<readonly RefCounted<IDocumentDiffItem>[]>([docA, docB]);
|
||||
const model: IMultiDiffEditorModel = { documents };
|
||||
const viewModel = widget.createViewModel(model);
|
||||
widget.setViewModel(viewModel);
|
||||
widget.layout(new Dimension(800, 600));
|
||||
|
||||
// At T=900ms: replace with A, C, D.
|
||||
// C has the same content as B but a different URI.
|
||||
// D is a new document.
|
||||
disposableStore.add(createTimeout(900, () => {
|
||||
const docC = makeDoc(codeB_orig, codeB_mod, 'config-v2.ts');
|
||||
const docD = makeDoc(codeD_orig, codeD_mod, 'server.ts');
|
||||
documents.value = [docA, docC, docD];
|
||||
}));
|
||||
};
|
||||
}
|
||||
|
||||
export default defineThemedFixtureGroup({ path: 'editor/' }, {
|
||||
@@ -172,4 +286,34 @@ export default defineThemedFixtureGroup({ path: 'editor/' }, {
|
||||
labels: { kind: 'screenshot' },
|
||||
render: (context) => renderMultiDiffEditor(context),
|
||||
}),
|
||||
MultiDiffEditorIncrementalPending: defineComponentFixture({
|
||||
labels: { kind: 'screenshot' },
|
||||
virtualTime: { enabled: true, durationMs: 1200 },
|
||||
render: renderMultiDiffEditorIncrementalUpdate(),
|
||||
}),
|
||||
MultiDiffEditorIncrementalResolved: defineComponentFixture({
|
||||
labels: { kind: 'screenshot' },
|
||||
virtualTime: { enabled: true, durationMs: 2000 },
|
||||
render: renderMultiDiffEditorIncrementalUpdate(),
|
||||
}),
|
||||
MultiDiffEditorIncrementalResolvedRealtime: defineComponentFixture({
|
||||
labels: { kind: 'animated' },
|
||||
virtualTime: { enabled: false },
|
||||
render: renderMultiDiffEditorIncrementalUpdate(),
|
||||
}),
|
||||
MultiDiffEditorDocumentSwapBefore: defineComponentFixture({
|
||||
labels: { kind: 'screenshot' },
|
||||
virtualTime: { enabled: true, durationMs: 100 },
|
||||
render: renderMultiDiffEditorDocumentSwap(),
|
||||
}),
|
||||
MultiDiffEditorDocumentSwapAfter: defineComponentFixture({
|
||||
labels: { kind: 'screenshot' },
|
||||
virtualTime: { enabled: true, durationMs: 2000 },
|
||||
render: renderMultiDiffEditorDocumentSwap(),
|
||||
}),
|
||||
MultiDiffEditorDocumentSwapRealtime: defineComponentFixture({
|
||||
labels: { kind: 'animated' },
|
||||
virtualTime: { enabled: false },
|
||||
render: renderMultiDiffEditorDocumentSwap(),
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -433,63 +433,6 @@ interface IRenderEditorOptions {
|
||||
readonly editorDisplayMode?: 'preview' | 'raw';
|
||||
}
|
||||
|
||||
async function waitForAnimationFrames(count: number): Promise<void> {
|
||||
for (let i = 0; i < count; i++) {
|
||||
await new Promise<void>(resolve => mainWindow.requestAnimationFrame(() => resolve()));
|
||||
}
|
||||
}
|
||||
|
||||
function getVisibleEditorSignature(container: HTMLElement): string {
|
||||
const sectionCounts = [...container.querySelectorAll('.section-list-item')].map(item => item.textContent?.replace(/\s+/g, ' ').trim() ?? '').join('|');
|
||||
const visibleContent = [...container.querySelectorAll('.prompts-content-container, .mcp-content-container, .plugin-content-container')]
|
||||
.find(node => node instanceof HTMLElement && node.style.display !== 'none');
|
||||
const visibleRows = visibleContent
|
||||
? [...visibleContent.querySelectorAll('.monaco-list-row')].map(row => row.textContent?.replace(/\s+/g, ' ').trim() ?? '').join('|')
|
||||
: '';
|
||||
|
||||
return `${sectionCounts}@@${visibleRows}`;
|
||||
}
|
||||
|
||||
async function waitForEditorToSettle(container: HTMLElement): Promise<void> {
|
||||
let previousSignature = '';
|
||||
let stableIterations = 0;
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 150));
|
||||
|
||||
for (let i = 0; i < 20; i++) {
|
||||
await waitForAnimationFrames(2);
|
||||
await new Promise(resolve => setTimeout(resolve, 25));
|
||||
|
||||
const signature = getVisibleEditorSignature(container);
|
||||
if (signature && signature === previousSignature) {
|
||||
stableIterations++;
|
||||
if (stableIterations >= 2) {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
stableIterations = 0;
|
||||
previousSignature = signature;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function waitForVisibleScrollbarsToFade(container: HTMLElement): Promise<void> {
|
||||
const deadline = Date.now() + 4000;
|
||||
|
||||
while (Date.now() < deadline) {
|
||||
const hasVisibleScrollbar = [...container.querySelectorAll<HTMLElement>('.scrollbar.vertical')].some(scrollbar => {
|
||||
const style = mainWindow.getComputedStyle(scrollbar);
|
||||
return scrollbar.classList.contains('visible') && style.opacity !== '0';
|
||||
});
|
||||
|
||||
if (!hasVisibleScrollbar) {
|
||||
return;
|
||||
}
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
}
|
||||
}
|
||||
|
||||
function renderFixtureMarkdown(markdown: string): HTMLElement {
|
||||
const container = DOM.$('div.fixture-rendered-markdown');
|
||||
const lines = markdown.split(/\r?\n/);
|
||||
@@ -770,13 +713,8 @@ async function renderEditor(ctx: ComponentFixtureContext, options: IRenderEditor
|
||||
editor.selectSectionById(options.selectedSection);
|
||||
}
|
||||
|
||||
await waitForEditorToSettle(ctx.container);
|
||||
|
||||
if (options.scrollToBottom) {
|
||||
editor.revealLastItem();
|
||||
await waitForAnimationFrames(2);
|
||||
await new Promise(resolve => setTimeout(resolve, 2400));
|
||||
await waitForVisibleScrollbarsToFade(ctx.container);
|
||||
}
|
||||
|
||||
if (options.openFirstItem) {
|
||||
@@ -791,15 +729,10 @@ async function renderEditor(ctx: ComponentFixtureContext, options: IRenderEditor
|
||||
rowToOpen.dispatchEvent(new MouseEvent('mousedown', { bubbles: true, button: 0 }));
|
||||
rowToOpen.dispatchEvent(new MouseEvent('mouseup', { bubbles: true, button: 0 }));
|
||||
rowToOpen.dispatchEvent(new MouseEvent('click', { bubbles: true, button: 0 }));
|
||||
// Allow any async setInput to settle.
|
||||
await waitForAnimationFrames(2);
|
||||
await new Promise(resolve => setTimeout(resolve, 250));
|
||||
|
||||
if (options.editorDisplayMode === 'raw') {
|
||||
const modeButton = ctx.container.querySelector('.editor-mode-button') as HTMLButtonElement | undefined;
|
||||
modeButton?.click();
|
||||
await waitForAnimationFrames(2);
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user