Skip to content

Commit 72241d2

Browse files
committed
test(aria/tabs): generate additional tests for Tab directives, harness and patterns
1 parent f8027a1 commit 72241d2

6 files changed

Lines changed: 176 additions & 13 deletions

File tree

goldens/aria/tabs/testing/index.api.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ export class TabHarness extends ContentContainerComponentHarness {
2727
// @public
2828
export interface TabHarnessFilters extends BaseHarnessFilters {
2929
disabled?: boolean;
30+
id?: string | RegExp;
3031
selected?: boolean;
3132
title?: string | RegExp;
3233
}

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: 120 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -495,23 +495,23 @@ describe('Tabs', () => {
495495
],
496496
});
497497

498-
const tabsDebugElement = fixture.debugElement.query(By.directive(Tabs));
499-
const tabsDirective = tabsDebugElement.injector.get(Tabs);
500-
501-
let orderedItems = tabsDirective._collection.orderedItems();
502-
expect(orderedItems.length).toBe(3);
503-
expect(orderedItems[0].value()).toBe('tab1');
504-
expect(orderedItems[2].value()).toBe('tab3');
498+
// Verify initial DOM order
499+
expect(tabElements.length).toBe(3);
500+
expect(tabElements[0].textContent?.trim()).toBe('Tab 1');
501+
expect(tabElements[2].textContent?.trim()).toBe('Tab 3');
505502

503+
// Shuffle (reverse) data
506504
const items = testComponent.tabsData().reverse();
507505
testComponent.tabsData.set([...items]);
508506
fixture.detectChanges();
509507
await waitForMicrotasks();
510508

511-
orderedItems = tabsDirective._collection.orderedItems();
512-
expect(orderedItems.length).toBe(3);
513-
expect(orderedItems[0].value()).toBe('tab3');
514-
expect(orderedItems[2].value()).toBe('tab1');
509+
// Re-query elements to check new DOM order
510+
defineTestVariables();
511+
512+
expect(tabElements.length).toBe(3);
513+
expect(tabElements[0].textContent?.trim()).toBe('Tab 3');
514+
expect(tabElements[2].textContent?.trim()).toBe('Tab 1');
515515
});
516516
});
517517

@@ -740,6 +740,97 @@ 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+
814+
beforeEach(() => {
815+
TestBed.configureTestingModule({
816+
providers: [provideFakeDirectionality('ltr')],
817+
});
818+
customIdFixture = TestBed.createComponent(TestTabsCustomIdComponent);
819+
fixture = customIdFixture as any;
820+
customIdFixture.detectChanges();
821+
});
822+
823+
it('should use custom ID for tab and link to panel', async () => {
824+
const tabEl = customIdFixture.nativeElement.querySelector('#custom-tab-id');
825+
const panelEl = customIdFixture.nativeElement.querySelector('#custom-panel-id');
826+
827+
expect(tabEl).toBeTruthy();
828+
expect(panelEl).toBeTruthy();
829+
830+
expect(tabEl.getAttribute('aria-controls')).toBe('custom-panel-id');
831+
expect(panelEl.getAttribute('aria-labelledby')).toBe('custom-tab-id');
832+
});
833+
});
743834
});
744835

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

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)