Skip to content

Commit 28238d0

Browse files
committed
test(aria/combobox): check for incorrect usage of Combobox directives and log violations
1 parent 3dd9011 commit 28238d0

3 files changed

Lines changed: 103 additions & 1 deletion

File tree

src/aria/combobox/combobox-popup.ts

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,16 @@
66
* found in the LICENSE file at https://angular.dev/license
77
*/
88

9-
import {computed, Directive, inject, input, OnDestroy, OnInit, signal} from '@angular/core';
9+
import {
10+
computed,
11+
Directive,
12+
inject,
13+
input,
14+
OnDestroy,
15+
OnInit,
16+
signal,
17+
afterRenderEffect,
18+
} from '@angular/core';
1019
import {DeferredContent, ComboboxPopupPattern} from '@angular/aria/private';
1120
import type {Combobox} from './combobox';
1221
import type {ComboboxWidget} from './combobox-widget';
@@ -58,6 +67,21 @@ export class ComboboxPopup implements OnInit, OnDestroy {
5867
...this,
5968
});
6069

70+
constructor() {
71+
// Check for any violations after the DOM has been updated.
72+
afterRenderEffect({
73+
read: () => {
74+
if (typeof ngDevMode === 'undefined' || ngDevMode) {
75+
if (!this._widget()) {
76+
console.error(
77+
'ngComboboxPopup must contain an ngComboboxWidget to establish focus controls.',
78+
);
79+
}
80+
}
81+
},
82+
});
83+
}
84+
6185
ngOnInit() {
6286
this.combobox()._registerPopup(this);
6387
this._deferredContent.deferredContentAware.set(this.combobox());

src/aria/combobox/combobox.spec.ts

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
untracked,
77
viewChild,
88
afterRenderEffect,
9+
ChangeDetectionStrategy,
910
} from '@angular/core';
1011
import {ComponentFixture, TestBed} from '@angular/core/testing';
1112
import {By} from '@angular/platform-browser';
@@ -95,6 +96,45 @@ describe('Combobox', () => {
9596

9697
afterEach(async () => await runAccessibilityChecks(fixture.nativeElement));
9798

99+
describe('structural validations', () => {
100+
let consoleSpy: jasmine.Spy;
101+
102+
beforeEach(() => {
103+
consoleSpy = spyOn(console, 'error');
104+
});
105+
106+
afterEach(() => {
107+
TestBed.resetTestingModule();
108+
setupCombobox();
109+
});
110+
111+
it('should warn when ngCombobox is missing ngComboboxPopup', () => {
112+
TestBed.resetTestingModule();
113+
TestBed.configureTestingModule({
114+
imports: [ComboboxWithoutPopup],
115+
});
116+
const noPopupFixture = TestBed.createComponent(ComboboxWithoutPopup);
117+
noPopupFixture.detectChanges();
118+
119+
expect(consoleSpy).toHaveBeenCalledWith(
120+
'ngCombobox must have a corresponding ngComboboxPopup template to render.',
121+
);
122+
});
123+
124+
it('should warn when ngComboboxPopup is missing ngComboboxWidget', () => {
125+
TestBed.resetTestingModule();
126+
TestBed.configureTestingModule({
127+
imports: [ComboboxPopupWithoutWidget],
128+
});
129+
const noWidgetFixture = TestBed.createComponent(ComboboxPopupWithoutWidget);
130+
noWidgetFixture.detectChanges();
131+
132+
expect(consoleSpy).toHaveBeenCalledWith(
133+
'ngComboboxPopup must contain an ngComboboxWidget to establish focus controls.',
134+
);
135+
});
136+
});
137+
98138
describe('ARIA attributes and roles', () => {
99139
beforeEach(() => setupCombobox());
100140

@@ -1705,3 +1745,28 @@ class ComboboxListboxHighlightExample {
17051745
this.popupExpanded.set(false);
17061746
}
17071747
}
1748+
1749+
@Component({
1750+
template: `
1751+
<div ngCombobox>
1752+
<input />
1753+
</div>
1754+
`,
1755+
imports: [Combobox],
1756+
changeDetection: ChangeDetectionStrategy.Eager,
1757+
})
1758+
class ComboboxWithoutPopup {}
1759+
1760+
@Component({
1761+
template: `
1762+
<div ngCombobox #combobox="ngCombobox">
1763+
<input />
1764+
<ng-template ngComboboxPopup [combobox]="combobox">
1765+
<div class="plain-div">No Widget Inside</div>
1766+
</ng-template>
1767+
</div>
1768+
`,
1769+
imports: [Combobox, ComboboxPopup],
1770+
changeDetection: ChangeDetectionStrategy.Eager,
1771+
})
1772+
class ComboboxPopupWithoutWidget {}

src/aria/combobox/combobox.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,19 @@ export class Combobox extends DeferredContentAware implements OnInit {
123123
this._pattern.highlightEffect();
124124
});
125125
}
126+
127+
// Check for any violations after the DOM has been updated.
128+
afterRenderEffect({
129+
read: () => {
130+
if (typeof ngDevMode === 'undefined' || ngDevMode) {
131+
if (!this._popup()) {
132+
console.error(
133+
'ngCombobox must have a corresponding ngComboboxPopup template to render.',
134+
);
135+
}
136+
}
137+
},
138+
});
126139
}
127140

128141
ngOnInit() {

0 commit comments

Comments
 (0)