diff --git a/src/vs/base/browser/ui/button/button.ts b/src/vs/base/browser/ui/button/button.ts index 5b32ddc9d85..8b7fc2d9b33 100644 --- a/src/vs/base/browser/ui/button/button.ts +++ b/src/vs/base/browser/ui/button/button.ts @@ -351,10 +351,10 @@ export class Button extends Disposable implements IButton { set checked(value: boolean) { if (value) { this._element.classList.add('checked'); - this._element.setAttribute('aria-checked', 'true'); + this._element.setAttribute('aria-pressed', 'true'); } else { this._element.classList.remove('checked'); - this._element.setAttribute('aria-checked', 'false'); + this._element.setAttribute('aria-pressed', 'false'); } } diff --git a/src/vs/workbench/browser/parts/compositeBarActions.ts b/src/vs/workbench/browser/parts/compositeBarActions.ts index 337ec9a0d4e..fe7bce2f083 100644 --- a/src/vs/workbench/browser/parts/compositeBarActions.ts +++ b/src/vs/workbench/browser/parts/compositeBarActions.ts @@ -237,11 +237,11 @@ export class CompositeBarActionViewItem extends BaseActionViewItem { this.container.classList.add('icon'); } + // Use 'tab' inside tablist, 'button' for popup items outside tablist + const role = this.options.isTabList || !this.options.hasPopup ? 'tab' : 'button'; + this.container.setAttribute('role', role); if (this.options.hasPopup) { - this.container.setAttribute('role', 'button'); this.container.setAttribute('aria-haspopup', 'true'); - } else { - this.container.setAttribute('role', 'tab'); } // Try hard to prevent keyboard only focus feedback when using mouse @@ -479,7 +479,7 @@ export class CompositeOverflowActivityActionViewItem extends CompositeBarActionV @IConfigurationService configurationService: IConfigurationService, @IKeybindingService keybindingService: IKeybindingService, ) { - super(action, { icon: true, colors, hasPopup: true, hoverOptions }, () => true, themeService, hoverService, configurationService, keybindingService); + super(action, { icon: true, colors, hasPopup: true, hoverOptions, isTabList: true }, () => true, themeService, hoverService, configurationService, keybindingService); } showMenu(): void { diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/experiments/agentTitleBarStatusWidget.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/experiments/agentTitleBarStatusWidget.ts index b4d2442497f..d44024fa6de 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/experiments/agentTitleBarStatusWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/experiments/agentTitleBarStatusWidget.ts @@ -749,7 +749,7 @@ export class AgentTitleBarStatusWidget extends BaseActionViewItem { // Create dropdown action (empty label prevents default tooltip - we have our own hover) const dropdownAction = toAction({ id: 'agentStatus.sparkle.dropdown', - label: '', + label: localize('agentStatus.sparkle.dropdown', "More Actions"), run() { } }); diff --git a/test/smoke/src/areas/accessibility/accessibility.test.ts b/test/smoke/src/areas/accessibility/accessibility.test.ts index 9472218a82f..d6f02874590 100644 --- a/test/smoke/src/areas/accessibility/accessibility.test.ts +++ b/test/smoke/src/areas/accessibility/accessibility.test.ts @@ -3,14 +3,14 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Application, Logger } from '../../../../automation'; +import { Application, Logger, Quality } from '../../../../automation'; import { installAllHandlers } from '../../utils'; -export function setup(logger: Logger, opts: { web?: boolean }) { - describe.skip('Accessibility', function () { +export function setup(logger: Logger, opts: { web?: boolean }, quality: Quality) { + describe('Accessibility', function () { // Increase timeout for accessibility scans - this.timeout(30 * 1000); + this.timeout(2 * 60 * 1000); // Retry tests to minimize flakiness this.retries(2); @@ -38,7 +38,9 @@ export function setup(logger: Logger, opts: { web?: boolean }) { // Monaco lists use aria-multiselectable on role="list" and aria-setsize/aria-posinset/aria-selected on role="dialog" rows // These violations appear intermittently when notification lists or other dynamic lists are visible // Note: patterns match against HTML string, not CSS selectors, so no leading dots - 'aria-allowed-attr': ['monaco-list', 'monaco-list-row'] + 'aria-allowed-attr': ['monaco-list', 'monaco-list-row'], + // Monaco lists may temporarily contain dialog children during extension activation errors + 'aria-required-children': ['monaco-list'] } }); }); @@ -69,7 +71,7 @@ export function setup(logger: Logger, opts: { web?: boolean }) { }); // Chat is not available in web mode - if (!opts.web) { + if (quality !== Quality.Dev && quality !== Quality.OSS && !opts.web) { describe('Chat', function () { it('chat panel has no accessibility violations', async function () { @@ -87,6 +89,79 @@ export function setup(logger: Logger, opts: { web?: boolean }) { } }); }); + + // Chat response test requires gallery service which is only available in non-Dev/OSS builds + it('chat response has no accessibility violations', async function () { + // Disable retries for this test - it modifies settings and retries cause issues + this.retries(0); + // Extend timeout for this test since AI responses can take a while + this.timeout(3 * 60 * 1000); + + // Enable anonymous chat access + await app.workbench.settingsEditor.addUserSetting('chat.allowAnonymousAccess', 'true'); + + // Open chat panel + await app.workbench.quickaccess.runCommand('workbench.action.chat.open'); + + // Wait for chat view to be visible + await app.workbench.chat.waitForChatView(); + + // Send a simple message + await app.workbench.chat.sendMessage('Create a simple hello.txt file with the text "Hello World"'); + + // Wait for the response to complete (1500 retries ~= 150 seconds at 100ms per retry) + await app.workbench.chat.waitForResponse(1500); + + // Run accessibility check on the chat panel with the response + await app.code.driver.assertNoAccessibilityViolations({ + selector: 'div[id="workbench.panel.chat"]', + excludeRules: { + // Links in chat welcome view show underline on hover/focus which axe-core static analysis cannot detect + 'link-in-text-block': ['command:workbench.action.chat.generateInstructions'], + // Monaco lists use aria-multiselectable on role="list" and aria-selected on role="listitem" + // These are used intentionally for selection semantics even though technically not spec-compliant + 'aria-allowed-attr': ['monaco-list', 'monaco-list-row'], + // Some icon buttons have empty aria-label during rendering + 'aria-command-name': ['codicon-plus'] + } + }); + }); + + it('chat terminal tool response has no accessibility violations', async function () { + // Disable retries for this test + this.retries(0); + // Extend timeout for this test since AI responses can take a while + this.timeout(3 * 60 * 1000); + + // Enable auto-approve for tools so terminal commands run automatically + await app.workbench.settingsEditor.addUserSetting('chat.tools.global.autoApprove', 'true'); + + // Open chat panel + await app.workbench.quickaccess.runCommand('workbench.action.chat.open'); + + // Wait for chat view to be visible + await app.workbench.chat.waitForChatView(); + + // Send a terminal command request + await app.workbench.chat.sendMessage('Run ls in the terminal'); + + // Wait for the response to complete (1500 retries ~= 150 seconds at 100ms per retry) + await app.workbench.chat.waitForResponse(1500); + + // Run accessibility check on the chat panel with the response + await app.code.driver.assertNoAccessibilityViolations({ + selector: 'div[id="workbench.panel.chat"]', + excludeRules: { + // Links in chat welcome view show underline on hover/focus which axe-core static analysis cannot detect + 'link-in-text-block': ['command:workbench.action.chat.generateInstructions'], + // Monaco lists use aria-multiselectable on role="list" and aria-selected on role="listitem" + // These are used intentionally for selection semantics even though technically not spec-compliant + 'aria-allowed-attr': ['monaco-list', 'monaco-list-row'], + // Some icon buttons have empty aria-label during rendering + 'aria-command-name': ['codicon-plus'] + } + }); + }); }); } }); diff --git a/test/smoke/src/main.ts b/test/smoke/src/main.ts index 2360381fd52..15279bbd5a1 100644 --- a/test/smoke/src/main.ts +++ b/test/smoke/src/main.ts @@ -408,5 +408,5 @@ describe(`VSCode Smoke Tests (${opts.web ? 'Web' : 'Electron'})`, () => { if (!opts.web && !opts.remote) { setupLaunchTests(logger); } if (!opts.web) { setupChatTests(logger); } if (!opts.web && quality === Quality.Insiders) { setupChatAnonymousTests(logger); } - setupAccessibilityTests(logger, opts); + setupAccessibilityTests(logger, opts, quality); });