diff --git a/src/vs/workbench/browser/parts/notifications/notificationsToasts.ts b/src/vs/workbench/browser/parts/notifications/notificationsToasts.ts index 9a5807b6e74..0c9f25175c2 100644 --- a/src/vs/workbench/browser/parts/notifications/notificationsToasts.ts +++ b/src/vs/workbench/browser/parts/notifications/notificationsToasts.ts @@ -26,6 +26,7 @@ import { IntervalCounter } from '../../../../base/common/async.js'; import { assertReturnsDefined } from '../../../../base/common/types.js'; import { NotificationsToastsVisibleContext } from '../../../common/contextkeys.js'; import { mainWindow } from '../../../../base/browser/window.js'; +import { IWorkbenchEnvironmentService } from '../../../services/environment/common/environmentService.js'; interface INotificationToast { readonly item: INotificationViewItem; @@ -84,7 +85,8 @@ export class NotificationsToasts extends Themable implements INotificationsToast @IEditorGroupsService private readonly editorGroupService: IEditorGroupsService, @IContextKeyService contextKeyService: IContextKeyService, @ILifecycleService private readonly lifecycleService: ILifecycleService, - @IHostService private readonly hostService: IHostService + @IHostService private readonly hostService: IHostService, + @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService ) { super(themeService); @@ -137,6 +139,10 @@ export class NotificationsToasts extends Themable implements INotificationsToast return; // do not show toasts while notification center is visible } + if (this.environmentService.enableSmokeTestDriver) { + return; // disable in smoke tests to prevent covering elements + } + if (item.priority === NotificationPriority.SILENT) { return; // do not show toasts for silenced notifications } diff --git a/src/vs/workbench/contrib/chat/electron-browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/electron-browser/chat.contribution.ts index c6dc91591cd..3abfc386a4c 100644 --- a/src/vs/workbench/contrib/chat/electron-browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/electron-browser/chat.contribution.ts @@ -128,6 +128,7 @@ class ChatLifecycleHandler extends Disposable { @IChatWidgetService private readonly widgetService: IChatWidgetService, @IContextKeyService private readonly contextKeyService: IContextKeyService, @IExtensionService extensionService: IExtensionService, + @INativeWorkbenchEnvironmentService private readonly environmentService: INativeWorkbenchEnvironmentService, ) { super(); @@ -147,6 +148,10 @@ class ChatLifecycleHandler extends Disposable { } private shouldVetoShutdown(reason: ShutdownReason): boolean | Promise { + if (this.environmentService.enableSmokeTestDriver) { + return false; + } + if (!this.hasNonCloudSessionInProgress()) { return false; } diff --git a/test/automation/src/chat.ts b/test/automation/src/chat.ts index 80bd0a2b5c4..90ba336e4da 100644 --- a/test/automation/src/chat.ts +++ b/test/automation/src/chat.ts @@ -4,15 +4,17 @@ *--------------------------------------------------------------------------------------------*/ import { Code } from './code'; -import { Notification } from './notification'; const CHAT_VIEW = 'div[id="workbench.panel.chat"]'; const CHAT_EDITOR = `${CHAT_VIEW} .monaco-editor[role="code"]`; const CHAT_EDITOR_FOCUSED = `${CHAT_VIEW} .monaco-editor.focused[role="code"]`; +const CHAT_RESPONSE = `${CHAT_VIEW} .interactive-item-container.interactive-response`; +const CHAT_RESPONSE_COMPLETE = `${CHAT_RESPONSE}:not(.chat-response-loading)`; +const CHAT_FOOTER_DETAILS = `${CHAT_VIEW} .chat-footer-details`; export class Chat { - constructor(private code: Code, private notification: Notification) { } + constructor(private code: Code) { } private get chatInputSelector(): string { return `${CHAT_EDITOR} ${!this.code.editContextEnabled ? 'textarea' : '.native-edit-context'}`; @@ -27,9 +29,6 @@ export class Chat { } async sendMessage(message: string): Promise { - if (await this.notification.isNotificationVisible()) { - throw new Error('Notification is visible'); - } // Click on the chat input to focus it await this.code.waitAndClick(CHAT_EDITOR); @@ -44,4 +43,22 @@ export class Chat { // Submit the message await this.code.dispatchKeybinding('enter', () => Promise.resolve()); } + + async waitForResponse(retryCount?: number): Promise { + + // First wait for a response element to appear + await this.code.waitForElement(CHAT_RESPONSE, undefined, retryCount); + + // Then wait for it to complete (not loading) + await this.code.waitForElement(CHAT_RESPONSE_COMPLETE, undefined, retryCount); + } + + async waitForModelInFooter(modelName: string): Promise { + await this.code.waitForElements(CHAT_FOOTER_DETAILS, false, el => { + return el.some(el => { + const text = el && typeof el.textContent === 'string' ? el.textContent : ''; + return !!text && text.includes(modelName); + }); + }); + } } diff --git a/test/automation/src/index.ts b/test/automation/src/index.ts index 29fdf0fd54f..80318af7962 100644 --- a/test/automation/src/index.ts +++ b/test/automation/src/index.ts @@ -27,5 +27,4 @@ export * from './localization'; export * from './workbench'; export * from './task'; export * from './chat'; -export * from './notification'; export { getDevElectronPath, getBuildElectronPath, getBuildVersion } from './electron'; diff --git a/test/automation/src/notification.ts b/test/automation/src/notification.ts deleted file mode 100644 index 1145dc4a29b..00000000000 --- a/test/automation/src/notification.ts +++ /dev/null @@ -1,22 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { Code } from './code'; - -const NOTIFICATION_TOAST = '.notification-toast'; - -export class Notification { - - constructor(private code: Code) { } - - async isNotificationVisible(): Promise { - try { - await this.code.waitForElement(NOTIFICATION_TOAST, undefined, 1); - return true; - } catch { - return false; - } - } -} diff --git a/test/automation/src/workbench.ts b/test/automation/src/workbench.ts index 1c624db35d7..a01a7d362b6 100644 --- a/test/automation/src/workbench.ts +++ b/test/automation/src/workbench.ts @@ -23,7 +23,6 @@ import { Notebook } from './notebook'; import { Localization } from './localization'; import { Task } from './task'; import { Chat } from './chat'; -import { Notification } from './notification'; export interface Commands { runCommand(command: string, options?: { exactLabelMatch?: boolean }): Promise; @@ -50,7 +49,6 @@ export class Workbench { readonly localization: Localization; readonly task: Task; readonly chat: Chat; - readonly notification: Notification; constructor(code: Code) { this.editors = new Editors(code); @@ -71,7 +69,6 @@ export class Workbench { this.notebook = new Notebook(this.quickaccess, this.quickinput, code); this.localization = new Localization(code); this.task = new Task(code, this.editor, this.editors, this.quickaccess, this.quickinput, this.terminal); - this.notification = new Notification(code); - this.chat = new Chat(code, this.notification); + this.chat = new Chat(code); } } diff --git a/test/smoke/src/areas/chat/chatAnonymous.test.ts b/test/smoke/src/areas/chat/chatAnonymous.test.ts new file mode 100644 index 00000000000..b2d178f18b2 --- /dev/null +++ b/test/smoke/src/areas/chat/chatAnonymous.test.ts @@ -0,0 +1,37 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Application, Logger } from '../../../../automation'; +import { installAllHandlers } from '../../utils'; + +export function setup(logger: Logger) { + describe('Chat Anonymous', () => { + + // Shared before/after handling + installAllHandlers(logger); + + it('can send a chat message with anonymous access', async function () { + const app = this.app as Application; + + // Enable anonymous access + await app.workbench.settingsEditor.addUserSetting('chat.allowAnonymousAccess', 'true'); + + // Open chat view + await app.workbench.quickaccess.runCommand('workbench.action.chat.open'); + + // Wait for chat view to be visible + await app.workbench.chat.waitForChatView(); + + // Send a message + await app.workbench.chat.sendMessage('Hello'); + + // Wait for a response to complete + await app.workbench.chat.waitForResponse(); + + // Wait for model name to appear in footer + await app.workbench.chat.waitForModelInFooter('GPT-5 mini'); + }); + }); +} diff --git a/test/smoke/src/main.ts b/test/smoke/src/main.ts index 57a9ee5ba20..f82e297eaf5 100644 --- a/test/smoke/src/main.ts +++ b/test/smoke/src/main.ts @@ -27,6 +27,7 @@ import { setup as setupLaunchTests } from './areas/workbench/launch.test'; import { setup as setupTerminalTests } from './areas/terminal/terminal.test'; import { setup as setupTaskTests } from './areas/task/task.test'; import { setup as setupChatTests } from './areas/chat/chat.test'; +import { setup as setupChatAnonymousTests } from './areas/chat/chatAnonymous.test'; import { setup as setupAccessibilityTests } from './areas/accessibility/accessibility.test'; const rootPath = path.join(__dirname, '..', '..', '..'); @@ -406,5 +407,6 @@ describe(`VSCode Smoke Tests (${opts.web ? 'Web' : 'Electron'})`, () => { if (!opts.web && !opts.remote && quality !== Quality.Dev && quality !== Quality.OSS) { setupLocalizationTests(logger); } if (!opts.web && !opts.remote) { setupLaunchTests(logger); } if (!opts.web) { setupChatTests(logger); } + if (!opts.web && quality === Quality.Insiders) { setupChatAnonymousTests(logger); } setupAccessibilityTests(logger, opts); });