Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 14 additions & 6 deletions shell/src/app/chat/chat-panel/chat-panel.ng.html
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,19 @@

<div class="preview-frame-container">
<!-- Loading/progress overlays status graphics panel -->
@if (
pipelineStatus() !== 'idle' && pipelineStatus() !== 'ready' && pipelineStatus() !== 'failed'
) {
<div class="pipeline-overlay" [class]="pipelineStatus()" (click)="dismissOverlay()">
@if (pipelineStatus() !== 'idle' && pipelineStatus() !== 'ready') {
<div
class="pipeline-overlay"
[class]="pipelineStatus()"
(click)="dismissOverlay()"
(keydown.enter)="dismissOverlay()"
(keydown.space)="$event.preventDefault(); dismissOverlay()"
role="button"
tabindex="0"
aria-label="Dismiss status overlay"
>
Comment thread
jgindin marked this conversation as resolved.
@if (pipelineStatus() === 'failed') {
<mat-icon class="status-icon error-icon">error</mat-icon>
<mat-icon class="status-icon error-icon" aria-hidden="true">error</mat-icon>
} @else {
<mat-spinner [diameter]="40"></mat-spinner>
}
Expand Down Expand Up @@ -71,7 +78,7 @@
[disabled]="isLocked()"
(click)="retryPrompt(turn.originalPrompt || '')"
>
<mat-icon>refresh</mat-icon>
<mat-icon aria-hidden="true">refresh</mat-icon>
Retry Request
</button>
</div>
Expand All @@ -88,6 +95,7 @@
<mat-form-field appearance="outline" subscriptSizing="dynamic" class="prompt-form-field">
<textarea
matInput
aria-label="Chat prompt"
[ngModel]="userPrompt()"
(ngModelChange)="userPrompt.set($event)"
[disabled]="isLocked()"
Expand Down
41 changes: 39 additions & 2 deletions shell/src/app/chat/chat-panel/chat-panel.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import {PipelineStatus} from '../pipeline-status/pipeline-status';
import {provideNoopAnimations} from '@angular/platform-browser/animations';
import {CatalogManagement} from '../../storage/catalog-management/catalog-management';
import {MatDialogHarness} from '@angular/material/dialog/testing';
import {MatInputHarness} from '@angular/material/input/testing';
import {Catalog} from '../../storage/models/catalog-storage.model';

class MockChatState {
Expand Down Expand Up @@ -347,10 +348,10 @@ describe('ChatPanel Gemini Dialogue Panel Integration', () => {
fixture.detectChanges();
expect(await harness.hasLoadingOverlay()).toBe(false);

// Milestone 6: Aborted/Failed turns (overlay is NOT shown, allowing sidebar to be interactive)
// Milestone 6: Aborted/Failed turns (overlay is shown with error status)
chatStateMock.pipelineStatus.set(PipelineStatus.FAILED);
fixture.detectChanges();
expect(await harness.hasLoadingOverlay()).toBe(false);
expect(await harness.hasLoadingOverlay()).toBe(true);
},
);

Expand Down Expand Up @@ -384,4 +385,40 @@ describe('ChatPanel Gemini Dialogue Panel Integration', () => {
// Send button must now be enabled
expect(await harness.isSubmitDisabled()).toBe(false);
});

it('applies the accessible name "Chat prompt" to the prompt textarea', async () => {
const loader = TestbedHarnessEnvironment.loader(fixture);
const input = await loader.getHarness(MatInputHarness);
const host = await input.host();
expect(await host.getAttribute('aria-label')).toBe('Chat prompt');
});

it('attaches structural accessibility attributes (role, tabindex, aria-label) to the pipeline dismiss overlay', async () => {
chatStateMock.pipelineStatus.set(PipelineStatus.RECEIVING_STREAM);
fixture.detectChanges();

const attrs = await harness.getPipelineOverlayAttributes();
expect(attrs.role).toBe('button');
expect(attrs.tabindex).toBe('0');
expect(attrs.ariaLabel).toBe('Dismiss status overlay');
});

it('applies aria-hidden attribute to purely decorative MatIcon elements across the chat panel', async () => {
const historyMocks: LlmMessage[] = [
{
role: MessageRole.ERROR,
content: 'error',
isRetryable: true,
originalPrompt: 'prompt',
},
];
chatStateMock.chatHistory.set(historyMocks);
fixture.detectChanges();

const hiddenAttrs = await harness.getIconsAriaHidden();
expect(hiddenAttrs.length).toBeGreaterThan(0);
hiddenAttrs.forEach(attr => {
expect(attr).toBe('true');
});
});
});
19 changes: 19 additions & 0 deletions shell/src/app/chat/chat-panel/test/chat-panel.harness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,4 +166,23 @@ export class ChatPanelHarness extends ComponentHarness {
}
await buttons[index].click();
}

async getPipelineOverlayAttributes(): Promise<{
role: string | null;
tabindex: string | null;
ariaLabel: string | null;
}> {
const overlay = await this.locatorForOptional('.pipeline-overlay')();
if (!overlay) return {role: null, tabindex: null, ariaLabel: null};
return {
role: await overlay.getAttribute('role'),
tabindex: await overlay.getAttribute('tabindex'),
ariaLabel: await overlay.getAttribute('aria-label'),
};
}

async getIconsAriaHidden(): Promise<(string | null)[]> {
const icons = await this.locatorForAll('mat-icon')();
return Promise.all(icons.map(i => i.getAttribute('aria-hidden')));
}
}
1 change: 1 addition & 0 deletions shell/src/app/debug/data-model/data-model.ng.html
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
<mat-form-field appearance="outline" subscriptSizing="dynamic" class="data-model-field">
<textarea
matInput
aria-label="Data model JSON"
[ngModel]="dataModelJson()"
(ngModelChange)="dataModelJson.set($event)"
placeholder="Enter data model JSON here..."
Expand Down
8 changes: 8 additions & 0 deletions shell/src/app/debug/data-model/data-model.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {signal, WritableSignal} from '@angular/core';
import {describe, it, expect, beforeEach, vi, afterEach} from 'vitest';
import {DataModel} from './data-model';
import {DataModelHarness} from './test/data-model.harness';
import {MatInputHarness} from '@angular/material/input/testing';
import {
HostCommunication,
MessageEnvelope,
Expand Down Expand Up @@ -173,4 +174,11 @@ describe('DataModel', () => {

expect(await harness.getModelText()).toBe('');
});

it('applies the accessible name "Data model JSON" to the data model textarea', async () => {
const loader = TestbedHarnessEnvironment.loader(fixture);
const input = await loader.getHarness(MatInputHarness);
const host = await input.host();
expect(await host.getAttribute('aria-label')).toBe('Data model JSON');
});
});
2 changes: 1 addition & 1 deletion shell/src/app/debug/errors/errors.ng.html
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@
(click)="toggleRow(element); $event.stopPropagation()"
aria-label="Toggle Stack Trace"
>
<mat-icon>{{
<mat-icon aria-hidden="true">{{
isRowExpanded(element) ? 'keyboard_arrow_up' : 'keyboard_arrow_down'
}}</mat-icon>
</button>
Expand Down
18 changes: 18 additions & 0 deletions shell/src/app/debug/errors/errors.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -277,4 +277,22 @@ describe('Errors', () => {
expect(await harness.hasPlaceholder()).toBe(true);
expect(fixture.componentInstance.expandedRows().size).toBe(0);
});

it('applies aria-hidden attribute to the purely decorative MatIcon element inside stack toggle buttons', async () => {
mockMessageStream.set({
type: PreviewBridgeMessageType.CONSOLE_LOG,
payload: {
level: 'error',
message: 'Exception with stack',
stack: 'Stack trace details here\n at file.ts:10',
},
origin: 'http://localhost',
timestamp: Date.now(),
});
fixture.detectChanges();

const hiddenAttrs = await harness.getIconsAriaHidden();
expect(hiddenAttrs.length).toBe(1);
expect(hiddenAttrs[0]).toBe('true');
});
});
5 changes: 5 additions & 0 deletions shell/src/app/debug/errors/test/errors.harness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,4 +83,9 @@ export class ErrorsHarness extends ComponentHarness {
}
return list[index].text();
}

async getIconsAriaHidden(): Promise<(string | null)[]> {
const icons = await this.locatorForAll('mat-icon')();
return Promise.all(icons.map(i => i.getAttribute('aria-hidden')));
}
}
1 change: 1 addition & 0 deletions shell/src/app/preview/raw/raw-frame.ng.html
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
<mat-form-field appearance="outline" subscriptSizing="dynamic" class="raw-json-field">
<textarea
matInput
aria-label="Raw layout JSON"
[ngModel]="layoutJson()"
(ngModelChange)="onLayoutChange($event)"
[readOnly]="isLocked()"
Expand Down
9 changes: 9 additions & 0 deletions shell/src/app/preview/raw/raw-frame.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {TestBed} from '@angular/core/testing';
import {RawFrame} from './raw-frame';
import {TestbedHarnessEnvironment} from '@angular/cdk/testing/testbed';
import {RawFrameHarness} from './test/raw-frame.harness';
import {MatInputHarness} from '@angular/material/input/testing';
import {describe, it, expect, vi, beforeEach, afterEach} from 'vitest';
import {provideNoopAnimations} from '@angular/platform-browser/animations';
import {IS_EXTENSION_MODE} from '../../shell/environment-tokens/environment-tokens';
Expand Down Expand Up @@ -303,4 +304,12 @@ describe('RawFrame JSON Source Editor View', () => {
fixture.detectChanges();
expect(await harness.isReadOnly()).toBe(false);
});

it('applies the accessible name "Raw layout JSON" to the raw layout textarea', async () => {
const {fixture} = await setup(false);
const loader = TestbedHarnessEnvironment.loader(fixture);
const input = await loader.getHarness(MatInputHarness);
const host = await input.host();
expect(await host.getAttribute('aria-label')).toBe('Raw layout JSON');
});
});
8 changes: 5 additions & 3 deletions shell/src/app/settings/settings-view/settings.ng.html
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
<h3>Renderer Application URL</h3>
@if (isLocked()) {
<div class="locked-notice">
<mat-icon color="warn">lock</mat-icon>
<mat-icon color="warn" aria-hidden="true">lock</mat-icon>
<span
>Active URL configuration is locked by enterprise policy (allowOverrides:
false).</span
Expand Down Expand Up @@ -73,7 +73,9 @@ <h3>Gemini API Provisioning</h3>
(click)="hideApiKey.set(!hideApiKey())"
[attr.aria-label]="hideApiKey() ? 'Show API key' : 'Hide API key'"
>
<mat-icon>{{ hideApiKey() ? 'visibility' : 'visibility_off' }}</mat-icon>
<mat-icon aria-hidden="true">{{
hideApiKey() ? 'visibility' : 'visibility_off'
}}</mat-icon>
</button>
@if (settingsForm.controls.apiKey.hasError('required')) {
<mat-error>Gemini API key is required in external environments.</mat-error>
Expand All @@ -92,7 +94,7 @@ <h3>Developer Authentication Overrides</h3>
</p>
@if (isLocked()) {
<div class="locked-notice auth-locked-notice">
<mat-icon color="warn">lock</mat-icon>
<mat-icon color="warn" aria-hidden="true">lock</mat-icon>
<span>Authentication mode overrides are locked by enterprise policy.</span>
</div>
}
Expand Down
13 changes: 13 additions & 0 deletions shell/src/app/settings/settings-view/settings.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -434,4 +434,17 @@ describe('Settings', () => {

expect(locationAssign).toHaveBeenCalledWith(expect.anything(), '/');
});

it('applies aria-hidden attribute to purely decorative MatIcon elements across settings', async () => {
mockStartupResolution.isContextLocked.mockReturnValue(true);
mockStartupResolution.isThirdPartyEnvironment.mockReturnValue(true);
const {fixture, harness} = await setupComponent();
fixture.detectChanges();

const hiddenAttrs = await harness.getIconsAriaHidden();
expect(hiddenAttrs.length).toBe(3);
hiddenAttrs.forEach(attr => {
expect(attr).toBe('true');
});
});
});
5 changes: 5 additions & 0 deletions shell/src/app/settings/settings-view/test/settings.harness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,4 +168,9 @@ export class SettingsHarness extends ComponentHarness {
const chip = await this.getCatalogBadge();
return chip.text();
}

async getIconsAriaHidden(): Promise<(string | null)[]> {
const icons = await this.locatorForAll('mat-icon')();
return Promise.all(icons.map(i => i.getAttribute('aria-hidden')));
}
}
4 changes: 2 additions & 2 deletions shell/src/app/shell/composer-shell/composer-shell.ng.html
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
(click)="sidenav.toggle()"
aria-label="Toggle sidenav"
>
<mat-icon>menu</mat-icon>
<mat-icon aria-hidden="true">menu</mat-icon>
</button>
<span class="header-title" [matTooltip]="activeCatalogDescription() || ''"
>A2UI Composer{{ activeCatalogTitle() ? ' - ' + activeCatalogTitle() : '' }}</span
Expand All @@ -37,7 +37,7 @@
(click)="toggleTheme()"
[attr.aria-label]="isDarkTheme() ? 'Switch to light theme' : 'Switch to dark theme'"
>
<mat-icon>{{ isDarkTheme() ? 'light_mode' : 'dark_mode' }}</mat-icon>
<mat-icon aria-hidden="true">{{ isDarkTheme() ? 'light_mode' : 'dark_mode' }}</mat-icon>
</button>
</mat-toolbar>

Expand Down
8 changes: 8 additions & 0 deletions shell/src/app/shell/composer-shell/composer-shell.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -190,4 +190,12 @@ describe('ComposerShell Layout', () => {
await harness.clickThemeToggleButton();
expect(configProviderMock.setThemePreference).toHaveBeenCalledWith('light');
});

it('applies aria-hidden attribute to purely decorative MatIcon elements across the composer shell', async () => {
const hiddenAttrs = await harness.getIconsAriaHidden();
expect(hiddenAttrs.length).toBe(2);
hiddenAttrs.forEach(attr => {
expect(attr).toBe('true');
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -58,4 +58,9 @@ export class ComposerShellHarness extends ComponentHarness {
const sidenav = await this.getSidenav();
return sidenav.isOpen();
}

async getIconsAriaHidden(): Promise<(string | null)[]> {
const icons = await this.locatorForAll('mat-icon')();
return Promise.all(icons.map(i => i.getAttribute('aria-hidden')));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
matTooltip="Clear all log tabs"
matTooltipPosition="left"
>
<mat-icon>delete</mat-icon>
<mat-icon aria-hidden="true">delete</mat-icon>
</button>
<button
mat-icon-button
Expand All @@ -55,7 +55,7 @@
[matTooltip]="isDebugCollapsed() ? 'Expand Debug Panel' : 'Collapse Debug Panel'"
matTooltipPosition="left"
>
<mat-icon>{{
<mat-icon aria-hidden="true">{{
isDebugCollapsed() ? 'keyboard_arrow_up' : 'keyboard_arrow_down'
}}</mat-icon>
</button>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -353,4 +353,12 @@ describe('ComposerWorkspace Dashboard', () => {
expect(await newHarness.isDebugSectionCollapsed()).toBe(true);
},
);

it('applies aria-hidden attribute to purely decorative MatIcon elements across the workspace header controls', async () => {
const hiddenAttrs = await harness.getIconsAriaHidden();
expect(hiddenAttrs.length).toBe(2);
hiddenAttrs.forEach(attr => {
expect(attr).toBe('true');
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -51,4 +51,9 @@ export class ComposerWorkspaceHarness extends ComponentHarness {
const section = await this.locatorFor('.debug-section')();
return section.hasClass('collapsed');
}

async getIconsAriaHidden(): Promise<(string | null)[]> {
const icons = await this.locatorForAll('.debug-header-controls mat-icon')();
return Promise.all(icons.map(i => i.getAttribute('aria-hidden')));
}
}
Loading