Skip to content

Commit 9f917fb

Browse files
committed
test(aria/tabs): Generate additional tests for Tab directives, harness and patterns
1 parent ee8f1f8 commit 9f917fb

5 files changed

Lines changed: 166 additions & 2 deletions

File tree

src/aria/private/tabs/tabs.spec.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -420,4 +420,21 @@ describe('Tabs Pattern', () => {
420420
expect(tabPanelPatterns[2].labelledBy()).toBe('tab-3-id');
421421
});
422422
});
423+
424+
describe('ActiveDescendant mode', () => {
425+
beforeEach(() => {
426+
tabListInputs.focusMode.set('activedescendant');
427+
tabListPattern.setDefaultState();
428+
});
429+
430+
it('should update activeDescendant when navigating', () => {
431+
expect(tabListPattern.activeDescendant()).toBe('tab-1-id');
432+
433+
tabListPattern.onKeydown(right());
434+
expect(tabListPattern.activeDescendant()).toBe('tab-2-id');
435+
436+
tabListPattern.onKeydown(right());
437+
expect(tabListPattern.activeDescendant()).toBe('tab-3-id');
438+
});
439+
});
423440
});

src/aria/tabs/tabs.spec.ts

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -740,6 +740,99 @@ describe('Tabs', () => {
740740
expect(tabPanelElements[2].hasAttribute('inert')).toBe(true);
741741
});
742742
});
743+
744+
describe('Dynamic tabs', () => {
745+
beforeEach(() => {
746+
setupTestTabs();
747+
updateTabs({
748+
initialTabs: [
749+
{value: 'tab1', label: 'Tab 1', content: 'Content 1'},
750+
{value: 'tab2', label: 'Tab 2', content: 'Content 2'},
751+
{value: 'tab3', label: 'Tab 3', content: 'Content 3'},
752+
],
753+
selectedTab: 'tab2',
754+
});
755+
});
756+
757+
it('should update selection when active tab is removed', () => {
758+
expect(testComponent.selectedTab()).toBe('tab2');
759+
760+
testComponent.tabsData.set([
761+
{value: 'tab1', label: 'Tab 1', content: 'Content 1'},
762+
{value: 'tab3', label: 'Tab 3', content: 'Content 3'},
763+
]);
764+
fixture.detectChanges();
765+
defineTestVariables();
766+
767+
expect(testComponent.selectedTab()).toBeUndefined();
768+
});
769+
770+
it('should maintain selection when a new tab is added', () => {
771+
expect(testComponent.selectedTab()).toBe('tab2');
772+
773+
testComponent.tabsData.set([
774+
{value: 'tab1', label: 'Tab 1', content: 'Content 1'},
775+
{value: 'tab2', label: 'Tab 2', content: 'Content 2'},
776+
{value: 'tab3', label: 'Tab 3', content: 'Content 3'},
777+
{value: 'tab4', label: 'Tab 4', content: 'Content 4'},
778+
]);
779+
fixture.detectChanges();
780+
defineTestVariables();
781+
782+
expect(testComponent.selectedTab()).toBe('tab2');
783+
});
784+
});
785+
786+
describe('Content lazy rendering', () => {
787+
beforeEach(() => {
788+
setupTestTabs();
789+
updateTabs({
790+
initialTabs: [
791+
{value: 'tab1', label: 'Tab 1', content: 'Content 1'},
792+
{value: 'tab2', label: 'Tab 2', content: 'Content 2'},
793+
],
794+
selectedTab: 'tab1',
795+
});
796+
});
797+
798+
it('should not render content of unselected tabs', () => {
799+
expect(tabPanelElements[0].textContent?.trim()).toContain('Content 1');
800+
expect(tabPanelElements[1].textContent?.trim()).not.toContain('Content 2');
801+
});
802+
803+
it('should render content when tab becomes selected', () => {
804+
updateTabs({selectedTab: 'tab2'});
805+
806+
expect(tabPanelElements[0].textContent?.trim()).not.toContain('Content 1');
807+
expect(tabPanelElements[1].textContent?.trim()).toContain('Content 2');
808+
});
809+
});
810+
811+
describe('Custom IDs', () => {
812+
let customIdFixture: ComponentFixture<TestTabsCustomIdComponent>;
813+
let customIdComponent: TestTabsCustomIdComponent;
814+
815+
beforeEach(() => {
816+
TestBed.configureTestingModule({
817+
providers: [provideFakeDirectionality('ltr')],
818+
});
819+
customIdFixture = TestBed.createComponent(TestTabsCustomIdComponent);
820+
customIdComponent = customIdFixture.componentInstance;
821+
fixture = customIdFixture as any;
822+
customIdFixture.detectChanges();
823+
});
824+
825+
it('should use custom ID for tab and link to panel', async () => {
826+
const tabEl = customIdFixture.nativeElement.querySelector('#custom-tab-id');
827+
const panelEl = customIdFixture.nativeElement.querySelector('#custom-panel-id');
828+
829+
expect(tabEl).toBeTruthy();
830+
expect(panelEl).toBeTruthy();
831+
832+
expect(tabEl.getAttribute('aria-controls')).toBe('custom-panel-id');
833+
expect(panelEl.getAttribute('aria-labelledby')).toBe('custom-tab-id');
834+
});
835+
});
743836
});
744837

745838
@Component({
@@ -798,3 +891,21 @@ class TestTabsComponent {
798891
focusMode = signal<'roving' | 'activedescendant'>('roving');
799892
selectionMode = signal<'follow' | 'explicit'>('follow');
800893
}
894+
895+
@Component({
896+
template: `
897+
<div ngTabs>
898+
<ul ngTabList [(selectedTab)]="selectedTab">
899+
<li ngTab value="tab1" id="custom-tab-id">Tab 1</li>
900+
</ul>
901+
<div ngTabPanel value="tab1" id="custom-panel-id">
902+
<ng-template ngTabContent>Content 1</ng-template>
903+
</div>
904+
</div>
905+
`,
906+
imports: [Tabs, TabList, Tab, TabPanel, TabContent],
907+
changeDetection: ChangeDetectionStrategy.Eager,
908+
})
909+
class TestTabsCustomIdComponent {
910+
selectedTab = signal('tab1');
911+
}

src/aria/tabs/testing/tabs-harness-filters.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,6 @@ export interface TabHarnessFilters extends BaseHarnessFilters {
1919
selected?: boolean;
2020
/** Only find instances that are disabled. */
2121
disabled?: boolean;
22+
/** Only find instances whose id matches the given value. */
23+
id?: string | RegExp;
2224
}

src/aria/tabs/testing/tabs-harness.spec.ts

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -126,14 +126,43 @@ describe('TabsHarness', () => {
126126
expect(filteredTabs.length).toBe(1);
127127
expect(await filteredTabs[0].getTitle()).toBe('Tab 3');
128128
});
129+
130+
it('should filter tabs by id', async () => {
131+
const tabs = await loader.getHarness(TabsHarness);
132+
const filteredTabs = await tabs.getTabs({id: 'custom-id-2'});
133+
134+
expect(filteredTabs.length).toBe(1);
135+
expect(await filteredTabs[0].getTitle()).toBe('Tab 2');
136+
});
137+
138+
it('should handle deferred content when collapsed', async () => {
139+
const tabs = await loader.getHarness(TabsHarness);
140+
const tabItems = await tabs.getTabs();
141+
142+
// Tab 2 is collapsed initially, content should not be available
143+
const contentHarness = await tabItems[1].getHarnessOrNull(TestContentHarness);
144+
expect(contentHarness).toBeNull();
145+
});
146+
147+
it('should handle deferred content when expanded', async () => {
148+
const tabs = await loader.getHarness(TabsHarness);
149+
const tabItems = await tabs.getTabs();
150+
151+
await tabItems[1].select(); // Expand Tab 2
152+
153+
// Now expanded, content should be available
154+
const contentHarness = await tabItems[1].getHarness(TestContentHarness);
155+
expect(contentHarness).toBeTruthy();
156+
expect(await contentHarness.getText()).toBe('Content 2');
157+
});
129158
});
130159

131160
@Component({
132161
template: `
133162
<div ngTabs>
134163
<ul ngTabList [selectedTab]="'tab1'">
135164
<li ngTab value="tab1">Tab 1</li>
136-
<li ngTab value="tab2">Tab 2</li>
165+
<li ngTab value="tab2" id="custom-id-2">Tab 2</li>
137166
<li ngTab value="tab3" [disabled]="true">Tab 3</li>
138167
</ul>
139168
@@ -144,7 +173,9 @@ describe('TabsHarness', () => {
144173
</ng-template>
145174
</div>
146175
<div ngTabPanel value="tab2">
147-
<ng-template ngTabContent>Content 2</ng-template>
176+
<ng-template ngTabContent>
177+
<div class="test-content">Content 2</div>
178+
</ng-template>
148179
</div>
149180
<div ngTabPanel value="tab3">
150181
<ng-template ngTabContent>Content 3</ng-template>

src/aria/tabs/testing/tabs-harness.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,9 @@ export class TabHarness extends ContentContainerComponentHarness {
3838
'disabled',
3939
options.disabled,
4040
async (harness, disabled) => (await harness.isDisabled()) === disabled,
41+
)
42+
.addOption('id', options.id, async (harness, id) =>
43+
HarnessPredicate.stringMatches((await harness.host()).getAttribute('id'), id),
4144
);
4245
}
4346

0 commit comments

Comments
 (0)