Skip to content
Draft
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
88 changes: 87 additions & 1 deletion src/aria/accordion/accordion.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,12 @@ describe('AccordionGroup', () => {
});

describe('with shuffled items', () => {
beforeEach(() => {
triggerElements[1].focus();
fixture.detectChanges();
expect(isTriggerActive(1)).toBeTrue();
});

it('should focus on new last trigger with End', () => {
const items = testComponent.items().reverse();
testComponent.items.set([...items]);
Expand All @@ -311,6 +317,8 @@ describe('AccordionGroup', () => {
content: 'Item 0 Content',
disabled: signal(false),
expanded: signal(false),
preserveContent: signal(false),
customId: signal<string | undefined>(undefined),
});
testComponent.items.set([...items]);
setupTriggerAndPanels();
Expand All @@ -328,6 +336,8 @@ describe('AccordionGroup', () => {
content: 'Item 4 Content',
disabled: signal(false),
expanded: signal(false),
preserveContent: signal(false),
customId: signal<string | undefined>(undefined),
});
testComponent.items.set([...items]);
setupTriggerAndPanels();
Expand All @@ -345,11 +355,12 @@ describe('AccordionGroup', () => {
content: 'Item 2a Content',
disabled: signal(false),
expanded: signal(false),
preserveContent: signal(false),
customId: signal<string | undefined>(undefined),
});
testComponent.items.set([...items]);
setupTriggerAndPanels();

downArrowKey();
downArrowKey();
expect(isTriggerActive(2)).toBeTrue();
expect(triggerElements[2].textContent?.trim()).toBe('Item 2a Header');
Expand Down Expand Up @@ -442,6 +453,73 @@ describe('AccordionGroup', () => {
expect(isTriggerExpanded(0)).toBeFalse();
});
});

describe('AccordionPanel specific behaviors', () => {
it('should expand via expand()', () => {
const panelDirective = fixture.debugElement
.queryAll(By.directive(AccordionPanel))[0]
.injector.get(AccordionPanel);
expect(panelDirective.visible()).toBeFalse();
panelDirective.expand();
fixture.detectChanges();
expect(panelDirective.visible()).toBeTrue();
});

it('should collapse via collapse()', () => {
const panelDirective = fixture.debugElement
.queryAll(By.directive(AccordionPanel))[0]
.injector.get(AccordionPanel);
panelDirective.expand();
fixture.detectChanges();
expect(panelDirective.visible()).toBeTrue();

panelDirective.collapse();
fixture.detectChanges();
expect(panelDirective.visible()).toBeFalse();
});

it('should toggle via toggle()', () => {
const panelDirective = fixture.debugElement
.queryAll(By.directive(AccordionPanel))[0]
.injector.get(AccordionPanel);
expect(panelDirective.visible()).toBeFalse();
panelDirective.toggle();
fixture.detectChanges();
expect(panelDirective.visible()).toBeTrue();

panelDirective.toggle();
fixture.detectChanges();
expect(panelDirective.visible()).toBeFalse();
});

it('should use custom ID when provided', () => {
testComponent.items()[0].customId.set('my-custom-id');
fixture.detectChanges();
expect(panelElements[0].getAttribute('id')).toBe('my-custom-id');
});

it('should destroy content when collapsed and preserveContent is false', () => {
testComponent.items()[0].preserveContent.set(false);
fixture.detectChanges();

click(triggerElements[0]); // Expand
expect(panelElements[0].textContent?.trim()).toContain('Item 1 Content');

click(triggerElements[0]); // Collapse
expect(panelElements[0].textContent?.trim()).not.toContain('Item 1 Content');
});

it('should preserve content when collapsed and preserveContent is true', () => {
testComponent.items()[0].preserveContent.set(true);
fixture.detectChanges();

click(triggerElements[0]); // Expand
expect(panelElements[0].textContent?.trim()).toContain('Item 1 Content');

click(triggerElements[0]); // Collapse
expect(panelElements[0].textContent?.trim()).toContain('Item 1 Content');
});
});
});

describe('using an if', () => {
Expand Down Expand Up @@ -547,6 +625,8 @@ describe('AccordionGroup', () => {
<div
ngAccordionPanel
#panel="ngAccordionPanel"
[attr.id]="item.customId()"
[preserveContent]="item.preserveContent()"
>
<ng-template ngAccordionContent>
{{ item.content }}
Expand All @@ -567,20 +647,26 @@ class AccordionGroupWithLoop {
content: 'Item 1 Content',
disabled: signal(false),
expanded: signal(false),
preserveContent: signal(false),
customId: signal<string | undefined>(undefined),
},
{
panelId: 'item-2',
header: 'Item 2 Header',
content: 'Item 2 Content',
disabled: signal(false),
expanded: signal(false),
preserveContent: signal(false),
customId: signal<string | undefined>(undefined),
},
{
panelId: 'item-3',
header: 'Item 3 Header',
content: 'Item 3 Content',
disabled: signal(false),
expanded: signal(false),
preserveContent: signal(false),
customId: signal<string | undefined>(undefined),
},
]);

Expand Down
2 changes: 2 additions & 0 deletions src/aria/accordion/testing/accordion-harness-filters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ export interface AccordionHarnessFilters extends BaseHarnessFilters {
expanded?: boolean;
/** Only find instances whose disabled state matches the given value. */
disabled?: boolean;
/** Only find instances whose id matches the given value. */
id?: string | RegExp;
}

/** Filters for locating an `AccordionGroupHarness`. */
Expand Down
56 changes: 51 additions & 5 deletions src/aria/accordion/testing/accordion-harness.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,13 @@
* found in the LICENSE file at https://angular.dev/license
*/

import {Component} from '@angular/core';
import {Component, signal} from '@angular/core';
import {TestBed} from '@angular/core/testing';
import {TestbedHarnessEnvironment} from '@angular/cdk/testing/testbed';
import {ComponentHarness} from '@angular/cdk/testing';
import {AccordionHarness, AccordionGroupHarness} from './accordion-harness';
import {AccordionGroup, AccordionPanel, AccordionTrigger} from '../index';
import {AccordionContent} from '../accordion-content';

/** Lightweight test harness to test querying inside the accordion body panel. */
class TestButtonHarness extends ComponentHarness {
Expand All @@ -27,7 +28,7 @@ describe('Accordion Harnesses', () => {
let loader: any;

@Component({
imports: [AccordionGroup, AccordionPanel, AccordionTrigger],
imports: [AccordionGroup, AccordionPanel, AccordionTrigger, AccordionContent],
template: `
<div ngAccordionGroup>
<div #panel1="ngAccordionPanel" ngAccordionPanel>
Expand All @@ -37,10 +38,19 @@ describe('Accordion Harnesses', () => {

<div #panel2="ngAccordionPanel" ngAccordionPanel>Content 2</div>
<button ngAccordionTrigger [panel]="panel2" disabled>Section 2</button>

<div #panel3="ngAccordionPanel" ngAccordionPanel [preserveContent]="preserveContent()">
<ng-template ngAccordionContent>
<button class="test-button">Inside Content 3</button>
</ng-template>
</div>
<button ngAccordionTrigger [panel]="panel3" id="custom-id-3">Section 3</button>
</div>
`,
})
class AccordionHarnessTestComponent {}
class AccordionHarnessTestComponent {
preserveContent = signal(false);
}

beforeEach(() => {
TestBed.configureTestingModule({
Expand All @@ -55,14 +65,14 @@ describe('Accordion Harnesses', () => {
const group = await loader.getHarness(AccordionGroupHarness);
const accordions = await group.getAccordions();

expect(accordions.length).toBe(2);
expect(accordions.length).toBe(3);
expect(await accordions[0].getTitle()).toBe('Section 1');
expect(await accordions[1].getTitle()).toBe('Section 2');
});

it('should find all individual accordions via standard root loader', async () => {
const accordions = await loader.getAllHarnesses(AccordionHarness);
expect(accordions.length).toBe(2);
expect(accordions.length).toBe(3);
});

it('should filter accordions by title', async () => {
Expand Down Expand Up @@ -147,4 +157,40 @@ describe('Accordion Harnesses', () => {
const button = await accordion.getHarness(TestButtonHarness);
expect(await button.getText()).toBe('Inside Content 1');
});

it('should filter accordions by id', async () => {
const accordions = await loader.getAllHarnesses(AccordionHarness.with({id: 'custom-id-3'}));
expect(accordions.length).toBe(1);
expect(await accordions[0].getTitle()).toBe('Section 3');
});

it('should handle deferred content when collapsed', async () => {
const accordion = await loader.getHarness(AccordionHarness.with({title: 'Section 3'}));

// Initially collapsed, content should not be available
const button = await accordion.getHarnessOrNull(TestButtonHarness);
expect(button).toBeNull();
});

it('should handle deferred content when expanded', async () => {
const accordion = await loader.getHarness(AccordionHarness.with({title: 'Section 3'}));
await accordion.expand();

// Now expanded, content should be available
const button = await accordion.getHarness(TestButtonHarness);
expect(await button.getText()).toBe('Inside Content 3');
});

it('should preserve content when collapsed if preserveContent is true', async () => {
fixture.componentInstance.preserveContent.set(true);
fixture.detectChanges();

const accordion = await loader.getHarness(AccordionHarness.with({title: 'Section 3'}));
await accordion.expand(); // Render it first
await accordion.collapse();

// Content should still be available even though collapsed
const button = await accordion.getHarness(TestButtonHarness);
expect(await button.getText()).toBe('Inside Content 3');
});
});
3 changes: 3 additions & 0 deletions src/aria/accordion/testing/accordion-harness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ export class AccordionHarness extends ContentContainerComponentHarness<Accordion
'disabled',
options.disabled,
async (harness, disabled) => (await harness.isDisabled()) === disabled,
)
.addOption('id', options.id, async (harness, id) =>
HarnessPredicate.stringMatches((await harness.host()).getAttribute('id'), id),
);
}

Expand Down
108 changes: 107 additions & 1 deletion src/aria/menu/menu.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {Component, DebugElement, ChangeDetectionStrategy} from '@angular/core';
import {Component, DebugElement, ChangeDetectionStrategy, signal} from '@angular/core';
import {ComponentFixture, TestBed} from '@angular/core/testing';
import {By} from '@angular/platform-browser';
import {provideFakeDirectionality} from '@angular/cdk/testing/private';
Expand Down Expand Up @@ -966,6 +966,70 @@ describe('Menu Bar Pattern', () => {
expect(document.activeElement).toBe(undo);
});
});

describe('with shuffled items', () => {
let loopFixture: ComponentFixture<MenuWithLoop>;
let loopComponent: MenuWithLoop;

beforeEach(() => {
TestBed.configureTestingModule({imports: [MenuWithLoop]});
loopFixture = TestBed.createComponent(MenuWithLoop);
loopComponent = loopFixture.componentInstance;
fixture = loopFixture as any; // Set global fixture
loopFixture.detectChanges();
});

it('should navigate correctly when items are reversed', () => {
const items = [...loopComponent.items()].reverse();
loopComponent.items.set(items);
loopFixture.detectChanges();

const menuItems = loopFixture.debugElement
.queryAll(By.directive(MenuItem))
.map(el => el.nativeElement as HTMLElement);

menuItems[0].focus();
expect(document.activeElement).toBe(menuItems[0]);

keydown(menuItems[0], 'ArrowDown');
expect(document.activeElement).toBe(menuItems[1]);
});
});

describe('using an if', () => {
let ifFixture: ComponentFixture<MenuWithIfs>;
let ifComponent: MenuWithIfs;

beforeEach(() => {
TestBed.configureTestingModule({imports: [MenuWithIfs]});
ifFixture = TestBed.createComponent(MenuWithIfs);
ifComponent = ifFixture.componentInstance;
ifFixture.detectChanges();
});

it('should skip removed item when navigating next', () => {
fixture = ifFixture as any; // Set global fixture

const menuItems = ifFixture.debugElement
.queryAll(By.directive(MenuItem))
.map(el => el.nativeElement as HTMLElement);

menuItems[0].focus();
expect(document.activeElement).toBe(menuItems[0]);

ifComponent.includeSecond.set(false);
ifFixture.detectChanges();

keydown(menuItems[0], 'ArrowDown');

const updatedItems = ifFixture.debugElement
.queryAll(By.directive(MenuItem))
.map(el => el.nativeElement as HTMLElement);

expect(document.activeElement).toBe(updatedItems[1]);
expect(updatedItems[1].textContent?.trim()).toBe('Cherry');
});
});
});

@Component({
Expand Down Expand Up @@ -1061,3 +1125,45 @@ class MenuTriggerExample {
changeDetection: ChangeDetectionStrategy.Eager,
})
class MenuBarExample {}

@Component({
template: `
<div ngMenu [expansionDelay]="0">
<ng-template ngMenuContent>
@for (item of items(); track item.value) {
<div ngMenuItem [value]="item.value" [searchTerm]="item.value">{{ item.value }}</div>
}
</ng-template>
</div>
`,
imports: [Menu, MenuItem, MenuContent],
changeDetection: ChangeDetectionStrategy.Eager,
})
class MenuWithLoop {
items = signal([{value: 'Apple'}, {value: 'Banana'}, {value: 'Cherry'}]);
}

@Component({
template: `
<div ngMenu [expansionDelay]="0">
<ng-template ngMenuContent>
@if (includeFirst()) {
<div ngMenuItem value="Apple" searchTerm="Apple">Apple</div>
}
@if (includeSecond()) {
<div ngMenuItem value="Banana" searchTerm="Banana">Banana</div>
}
@if (includeThird()) {
<div ngMenuItem value="Cherry" searchTerm="Cherry">Cherry</div>
}
</ng-template>
</div>
`,
imports: [Menu, MenuItem, MenuContent],
changeDetection: ChangeDetectionStrategy.Eager,
})
class MenuWithIfs {
includeFirst = signal(true);
includeSecond = signal(true);
includeThird = signal(true);
}
Loading
Loading