|
| 1 | +/*--------------------------------------------------------------------------------------------- |
| 2 | + * Copyright (c) Microsoft Corporation. All rights reserved. |
| 3 | + * Licensed under the MIT License. See License.txt in the project root for license information. |
| 4 | + *--------------------------------------------------------------------------------------------*/ |
| 5 | + |
| 6 | +import assert from 'assert'; |
| 7 | +import { addDisposableListener, EventType } from '../../../base/browser/dom.js'; |
| 8 | +import { mainWindow } from '../../../base/browser/window.js'; |
| 9 | +import { Event } from '../../../base/common/event.js'; |
| 10 | +import { DisposableStore } from '../../../base/common/lifecycle.js'; |
| 11 | +import { constObservable, IObservable } from '../../../base/common/observable.js'; |
| 12 | +import { isLinux } from '../../../base/common/platform.js'; |
| 13 | +import { URI } from '../../../base/common/uri.js'; |
| 14 | +import { mock } from '../../../base/test/common/mock.js'; |
| 15 | +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../base/test/common/utils.js'; |
| 16 | +import { ICommandService } from '../../../platform/commands/common/commands.js'; |
| 17 | +import { TestInstantiationService } from '../../../platform/instantiation/test/common/instantiationServiceMock.js'; |
| 18 | +import { workbenchInstantiationService } from '../../../workbench/test/browser/workbenchTestServices.js'; |
| 19 | +import { ChatCompositeBar } from '../../browser/parts/chatCompositeBar.js'; |
| 20 | +import { CLOSE_CHAT_COMMAND_ID } from '../../common/sessionCommands.js'; |
| 21 | +import { ISessionsProvidersService } from '../../services/sessions/browser/sessionsProvidersService.js'; |
| 22 | +import { ISessionsPartService } from '../../services/sessions/browser/sessionsPartService.js'; |
| 23 | +import { ISessionsService } from '../../services/sessions/browser/sessionsService.js'; |
| 24 | +import { ChatInteractivity, IChat, ISession, ISessionCapabilities, SessionStatus } from '../../services/sessions/common/session.js'; |
| 25 | +import { IActiveSession, ISessionsManagementService } from '../../services/sessions/common/sessionsManagement.js'; |
| 26 | + |
| 27 | +class TestCommandService extends mock<ICommandService>() { |
| 28 | + readonly calls: { readonly commandId: string; readonly args: readonly unknown[] }[] = []; |
| 29 | + |
| 30 | + override async executeCommand<T = unknown>(commandId: string, ...args: unknown[]): Promise<T | undefined> { |
| 31 | + this.calls.push({ commandId, args }); |
| 32 | + return undefined; |
| 33 | + } |
| 34 | +} |
| 35 | + |
| 36 | +class TestSessionsService extends mock<ISessionsService>() { |
| 37 | + readonly openedChats: URI[] = []; |
| 38 | + |
| 39 | + override async openChat(_session: ISession, chatUri: URI): Promise<void> { |
| 40 | + this.openedChats.push(chatUri); |
| 41 | + } |
| 42 | +} |
| 43 | + |
| 44 | +function createChat(id: string, title: string, status: SessionStatus = SessionStatus.Completed): IChat { |
| 45 | + const resource = URI.parse(`test-chat://${id}`); |
| 46 | + return new class extends mock<IChat>() { |
| 47 | + override readonly resource = resource; |
| 48 | + override readonly title: IObservable<string> = constObservable(title); |
| 49 | + override readonly status: IObservable<SessionStatus> = constObservable(status); |
| 50 | + override readonly isRead: IObservable<boolean> = constObservable(true); |
| 51 | + override readonly interactivity: IObservable<ChatInteractivity> = constObservable(ChatInteractivity.Full); |
| 52 | + }(); |
| 53 | +} |
| 54 | + |
| 55 | +function createSession(chats: readonly IChat[], activeChat: IChat): IActiveSession { |
| 56 | + const resource = URI.parse('test-session://session'); |
| 57 | + return new class extends mock<IActiveSession>() { |
| 58 | + override readonly sessionId = 'session'; |
| 59 | + override readonly resource = resource; |
| 60 | + override readonly providerId = 'test'; |
| 61 | + override readonly chats: IObservable<readonly IChat[]> = constObservable(chats); |
| 62 | + override readonly openChats: IObservable<readonly IChat[]> = constObservable(chats); |
| 63 | + override readonly closedChats: IObservable<readonly IChat[]> = constObservable([]); |
| 64 | + override readonly visibleChatTabs: IObservable<readonly IChat[]> = constObservable(chats); |
| 65 | + override readonly shouldShowChatTabs: IObservable<boolean> = constObservable(true); |
| 66 | + override readonly mainChat: IObservable<IChat> = constObservable(chats[0]); |
| 67 | + override readonly activeChat: IObservable<IChat> = constObservable(activeChat); |
| 68 | + override readonly capabilities: IObservable<ISessionCapabilities> = constObservable({ supportsMultipleChats: true }); |
| 69 | + override readonly isCreated: IObservable<boolean> = constObservable(true); |
| 70 | + override readonly isArchived: IObservable<boolean> = constObservable(false); |
| 71 | + }(); |
| 72 | +} |
| 73 | + |
| 74 | +interface IChatCompositeBarHarness { |
| 75 | + readonly store: DisposableStore; |
| 76 | + readonly instantiationService: TestInstantiationService; |
| 77 | + readonly commandService: TestCommandService; |
| 78 | + readonly sessionsService: TestSessionsService; |
| 79 | + readonly bar: ChatCompositeBar; |
| 80 | + readonly session: IActiveSession; |
| 81 | + readonly tabs: readonly HTMLElement[]; |
| 82 | +} |
| 83 | + |
| 84 | +function createHarness(disposables: Pick<DisposableStore, 'add'>): IChatCompositeBarHarness { |
| 85 | + const store = disposables.add(new DisposableStore()); |
| 86 | + const instantiationService = workbenchInstantiationService(undefined, store); |
| 87 | + const commandService = new TestCommandService(); |
| 88 | + const sessionsService = new TestSessionsService(); |
| 89 | + const mainChat = createChat('main', 'Main Chat'); |
| 90 | + const secondaryChat = createChat('secondary', 'Secondary Chat'); |
| 91 | + const session = createSession([mainChat, secondaryChat], mainChat); |
| 92 | + |
| 93 | + instantiationService.stub(ICommandService, commandService); |
| 94 | + instantiationService.stub(ISessionsService, sessionsService); |
| 95 | + instantiationService.stub(ISessionsManagementService, new class extends mock<ISessionsManagementService>() { |
| 96 | + override readonly onDidChangeSessions = Event.None; |
| 97 | + }()); |
| 98 | + instantiationService.stub(ISessionsPartService, new class extends mock<ISessionsPartService>() { }); |
| 99 | + instantiationService.stub(ISessionsProvidersService, new class extends mock<ISessionsProvidersService>() { |
| 100 | + override readonly onDidChangeProviders = Event.None; |
| 101 | + override getProvider() { return undefined; } |
| 102 | + }()); |
| 103 | + |
| 104 | + const bar = store.add(instantiationService.createInstance(ChatCompositeBar)); |
| 105 | + bar.setSession(session); |
| 106 | + const container = mainWindow.document.createElement('div'); |
| 107 | + container.appendChild(bar.element); |
| 108 | + const tabs = Array.from(bar.element.querySelectorAll<HTMLElement>('.chat-composite-bar-tab')); |
| 109 | + |
| 110 | + return { store, instantiationService, commandService, sessionsService, bar, session, tabs }; |
| 111 | +} |
| 112 | + |
| 113 | +suite('Sessions - ChatCompositeBar', () => { |
| 114 | + const disposables = ensureNoDisposablesAreLeakedInTestSuite(); |
| 115 | + |
| 116 | + test('middle-click closes the targeted inactive non-main chat', () => { |
| 117 | + const { store, commandService, sessionsService, bar, session, tabs } = createHarness(disposables); |
| 118 | + let bubbled = 0; |
| 119 | + store.add(addDisposableListener(bar.element, EventType.AUXCLICK, () => bubbled++)); |
| 120 | + const event = new MouseEvent(EventType.AUXCLICK, { bubbles: true, button: 1, cancelable: true }); |
| 121 | + |
| 122 | + const dispatchResult = tabs[1].dispatchEvent(event); |
| 123 | + |
| 124 | + assert.deepStrictEqual({ |
| 125 | + commandCalls: commandService.calls, |
| 126 | + openedChats: sessionsService.openedChats, |
| 127 | + defaultPrevented: event.defaultPrevented, |
| 128 | + dispatchResult, |
| 129 | + bubbled, |
| 130 | + }, { |
| 131 | + commandCalls: [{ |
| 132 | + commandId: CLOSE_CHAT_COMMAND_ID, |
| 133 | + args: [{ session, chat: session.visibleChatTabs.get()[1] }], |
| 134 | + }], |
| 135 | + openedChats: [], |
| 136 | + defaultPrevented: true, |
| 137 | + dispatchResult: false, |
| 138 | + bubbled: 0, |
| 139 | + }); |
| 140 | + }); |
| 141 | + |
| 142 | + test('middle-click does not close the main chat and other auxiliary clicks are ignored', () => { |
| 143 | + const { store, commandService, bar, tabs } = createHarness(disposables); |
| 144 | + let bubbled = 0; |
| 145 | + store.add(addDisposableListener(bar.element, EventType.AUXCLICK, () => bubbled++)); |
| 146 | + const mainMiddleClick = new MouseEvent(EventType.AUXCLICK, { bubbles: true, button: 1, cancelable: true }); |
| 147 | + const secondaryRightClick = new MouseEvent(EventType.AUXCLICK, { bubbles: true, button: 2, cancelable: true }); |
| 148 | + |
| 149 | + tabs[0].dispatchEvent(mainMiddleClick); |
| 150 | + tabs[1].dispatchEvent(secondaryRightClick); |
| 151 | + |
| 152 | + assert.deepStrictEqual({ |
| 153 | + commandCalls: commandService.calls, |
| 154 | + mainDefaultPrevented: mainMiddleClick.defaultPrevented, |
| 155 | + secondaryDefaultPrevented: secondaryRightClick.defaultPrevented, |
| 156 | + bubbled, |
| 157 | + }, { |
| 158 | + commandCalls: [], |
| 159 | + mainDefaultPrevented: true, |
| 160 | + secondaryDefaultPrevented: false, |
| 161 | + bubbled: 1, |
| 162 | + }); |
| 163 | + }); |
| 164 | + |
| 165 | + test('prevents native middle-button behavior on the scrollable tab container', () => { |
| 166 | + const { bar } = createHarness(disposables); |
| 167 | + const tabsContainer = bar.element.querySelector<HTMLElement>('.chat-composite-bar-tabs')!; |
| 168 | + const middleMouseDown = new MouseEvent(EventType.MOUSE_DOWN, { bubbles: true, button: 1, cancelable: true }); |
| 169 | + const leftMouseDown = new MouseEvent(EventType.MOUSE_DOWN, { bubbles: true, button: 0, cancelable: true }); |
| 170 | + const middleMouseUp = new MouseEvent(EventType.MOUSE_UP, { bubbles: true, button: 1, cancelable: true }); |
| 171 | + |
| 172 | + tabsContainer.dispatchEvent(middleMouseDown); |
| 173 | + tabsContainer.dispatchEvent(leftMouseDown); |
| 174 | + tabsContainer.dispatchEvent(middleMouseUp); |
| 175 | + |
| 176 | + assert.deepStrictEqual({ |
| 177 | + middleMouseDown: middleMouseDown.defaultPrevented, |
| 178 | + leftMouseDown: leftMouseDown.defaultPrevented, |
| 179 | + middleMouseUp: middleMouseUp.defaultPrevented, |
| 180 | + }, { |
| 181 | + middleMouseDown: true, |
| 182 | + leftMouseDown: false, |
| 183 | + middleMouseUp: isLinux, |
| 184 | + }); |
| 185 | + }); |
| 186 | + |
| 187 | + test('middle-click in the rename input does not close the chat', () => { |
| 188 | + const { commandService, tabs } = createHarness(disposables); |
| 189 | + tabs[1].dispatchEvent(new MouseEvent(EventType.DBLCLICK, { bubbles: true, button: 0, cancelable: true })); |
| 190 | + const input = tabs[1].querySelector<HTMLInputElement>('.chat-composite-bar-tab-input input')!; |
| 191 | + const mouseDownEvent = new MouseEvent(EventType.MOUSE_DOWN, { bubbles: true, button: 1, cancelable: true }); |
| 192 | + const mouseUpEvent = new MouseEvent(EventType.MOUSE_UP, { bubbles: true, button: 1, cancelable: true }); |
| 193 | + const auxClickEvent = new MouseEvent(EventType.AUXCLICK, { bubbles: true, button: 1, cancelable: true }); |
| 194 | + |
| 195 | + input.dispatchEvent(mouseDownEvent); |
| 196 | + input.dispatchEvent(mouseUpEvent); |
| 197 | + input.dispatchEvent(auxClickEvent); |
| 198 | + |
| 199 | + assert.deepStrictEqual({ |
| 200 | + commandCalls: commandService.calls, |
| 201 | + mouseDownDefaultPrevented: mouseDownEvent.defaultPrevented, |
| 202 | + mouseUpDefaultPrevented: mouseUpEvent.defaultPrevented, |
| 203 | + auxClickDefaultPrevented: auxClickEvent.defaultPrevented, |
| 204 | + }, { |
| 205 | + commandCalls: [], |
| 206 | + mouseDownDefaultPrevented: false, |
| 207 | + mouseUpDefaultPrevented: false, |
| 208 | + auxClickDefaultPrevented: false, |
| 209 | + }); |
| 210 | + }); |
| 211 | +}); |
0 commit comments