diff --git a/src/aria/accordion/accordion.spec.ts b/src/aria/accordion/accordion.spec.ts index 6d63b17582c3..12d0515b8ad9 100644 --- a/src/aria/accordion/accordion.spec.ts +++ b/src/aria/accordion/accordion.spec.ts @@ -17,32 +17,32 @@ describe('AccordionGroup', () => { let triggerElements: HTMLElement[]; let panelElements: HTMLElement[]; - const click = (target: HTMLElement) => { + const click = async (target: HTMLElement) => { target.dispatchEvent(new PointerEvent('click', {bubbles: true})); - fixture.detectChanges(); + await fixture.whenStable(); }; - const keydown = (key: string, target = groupElement) => { + const keydown = async (key: string, target = groupElement) => { target.dispatchEvent(new KeyboardEvent('keydown', {bubbles: true, key})); - fixture.detectChanges(); + await fixture.whenStable(); }; - const spaceKey = (target: HTMLElement) => keydown(' ', target); - const enterKey = (target: HTMLElement) => keydown('Enter', target); - const downArrowKey = () => keydown('ArrowDown'); - const upArrowKey = () => keydown('ArrowUp'); - const homeKey = () => keydown('Home'); - const endKey = () => keydown('End'); + const spaceKey = async (target: HTMLElement) => await keydown(' ', target); + const enterKey = async (target: HTMLElement) => await keydown('Enter', target); + const downArrowKey = async () => await keydown('ArrowDown'); + const upArrowKey = async () => await keydown('ArrowUp'); + const homeKey = async () => await keydown('Home'); + const endKey = async () => await keydown('End'); - function setupAccordionGroup() { + async function setupAccordionGroup() { testComponent = fixture.componentInstance as AccordionGroupWithLoop; groupElement = fixture.nativeElement.querySelector('[ngAccordionGroup]') as HTMLElement; - setupTriggerAndPanels(); + await setupTriggerAndPanels(); } - function setupTriggerAndPanels() { - fixture.detectChanges(); + async function setupTriggerAndPanels() { + await fixture.whenStable(); const triggerDebugElements = fixture.debugElement.queryAll(By.directive(AccordionTrigger)); const panelDebugElements = fixture.debugElement.queryAll(By.directive(AccordionPanel)); @@ -73,10 +73,10 @@ describe('AccordionGroup', () => { }); describe('using a loop', () => { - beforeEach(() => { + beforeEach(async () => { fixture = TestBed.createComponent(AccordionGroupWithLoop); - setupAccordionGroup(); + await setupAccordionGroup(); }); describe('ARIA attributes and roles', () => { @@ -111,9 +111,9 @@ describe('AccordionGroup', () => { expect(getTriggerAttribute(2, 'aria-disabled')).toBe('false'); }); - it('should set aria-disabled="true" if trigger is disabled', () => { + it('should set aria-disabled="true" if trigger is disabled', async () => { testComponent.disableItem('item-1', true); - fixture.detectChanges(); + await fixture.whenStable(); expect(getTriggerAttribute(0, 'aria-disabled')).toBe('true'); expect(getTriggerAttribute(1, 'aria-disabled')).toBe('false'); @@ -144,33 +144,33 @@ describe('AccordionGroup', () => { describe('Expansion behavior', () => { describe('single expansion mode (multiExpandable=false)', () => { - it('should expand panel on trigger click and update expanded panels', () => { - click(triggerElements[0]); + it('should expand panel on trigger click and update expanded panels', async () => { + await click(triggerElements[0]); expect(isTriggerExpanded(0)).toBeTrue(); expect(panelElements[0].getAttribute('inert')).toBe(null); }); - it('should collapes panel on trigger click and update expanded panels', () => { - click(triggerElements[0]); - click(triggerElements[0]); // Collapse + it('should collapes panel on trigger click and update expanded panels', async () => { + await click(triggerElements[0]); + await click(triggerElements[0]); // Collapse expect(isTriggerExpanded(0)).toBeFalse(); expect(panelElements[0].getAttribute('inert')).toBe('true'); }); - it('should expand one and collapse others', () => { - click(triggerElements[0]); + it('should expand one and collapse others', async () => { + await click(triggerElements[0]); expect(isTriggerExpanded(0)).toBeTrue(); - click(triggerElements[1]); + await click(triggerElements[1]); expect(isTriggerExpanded(0)).toBeFalse(); expect(panelElements[0].getAttribute('inert')).toBe('true'); expect(isTriggerExpanded(1)).toBeTrue(); expect(panelElements[1].getAttribute('inert')).toBe(null); }); - it('should allow setting initial value', () => { + it('should allow setting initial value', async () => { testComponent.expandItem('item-2', true); - fixture.detectChanges(); + await fixture.whenStable(); expect(isTriggerExpanded(0)).toBeFalse(); expect(isTriggerExpanded(1)).toBeTrue(); @@ -179,35 +179,35 @@ describe('AccordionGroup', () => { }); describe('multiple expansion mode (multiExpandable=true)', () => { - beforeEach(() => { + beforeEach(async () => { testComponent.multiExpandable.set(true); - fixture.detectChanges(); + await fixture.whenStable(); }); - it('should expand multiple panels', () => { - click(triggerElements[0]); + it('should expand multiple panels', async () => { + await click(triggerElements[0]); expect(isTriggerExpanded(0)).toBeTrue(); - click(triggerElements[1]); + await click(triggerElements[1]); expect(isTriggerExpanded(0)).toBeTrue(); expect(isTriggerExpanded(1)).toBeTrue(); }); - it('should collapse an item without affecting others', () => { - click(triggerElements[0]); - click(triggerElements[1]); + it('should collapse an item without affecting others', async () => { + await click(triggerElements[0]); + await click(triggerElements[1]); expect(isTriggerExpanded(0)).toBeTrue(); expect(isTriggerExpanded(1)).toBeTrue(); - click(triggerElements[0]); + await click(triggerElements[0]); expect(isTriggerExpanded(0)).toBeFalse(); expect(isTriggerExpanded(1)).toBeTrue(); }); - it('should allow setting initial multiple values', () => { + it('should allow setting initial multiple values', async () => { testComponent.expandItem('item-1', true); testComponent.expandItem('item-3', true); - fixture.detectChanges(); + await fixture.whenStable(); expect(isTriggerExpanded(0)).toBeTrue(); expect(isTriggerExpanded(1)).toBeFalse(); @@ -216,85 +216,85 @@ describe('AccordionGroup', () => { }); describe('disabled items and group', () => { - it('should not expand a disabled trigger', () => { + it('should not expand a disabled trigger', async () => { testComponent.disableItem('item-1', true); - fixture.detectChanges(); + await fixture.whenStable(); - click(triggerElements[0]); + await click(triggerElements[0]); expect(isTriggerExpanded(0)).toBeFalse(); expect(triggerElements[0].getAttribute('aria-disabled')).toBe('true'); }); - it('should not expand any trigger if group is disabled', () => { + it('should not expand any trigger if group is disabled', async () => { testComponent.disabledGroup.set(true); - fixture.detectChanges(); + await fixture.whenStable(); - click(triggerElements[0]); + await click(triggerElements[0]); expect(isTriggerExpanded(0)).toBeFalse(); - click(triggerElements[1]); + await click(triggerElements[1]); expect(isTriggerExpanded(1)).toBeFalse(); }); }); }); describe('Keyboard navigation and interaction', () => { - beforeEach(() => { + beforeEach(async () => { // Focus on the first trigger as initial state. triggerElements[0].focus(); - fixture.detectChanges(); + await fixture.whenStable(); expect(isTriggerActive(0)).toBeTrue(); }); - it('should focus next trigger with ArrowDown', () => { - downArrowKey(); + it('should focus next trigger with ArrowDown', async () => { + await downArrowKey(); expect(isTriggerActive(0)).toBeFalse(); expect(isTriggerActive(1)).toBeTrue(); }); - it('should focus previous trigger with ArrowUp', () => { - downArrowKey(); + it('should focus previous trigger with ArrowUp', async () => { + await downArrowKey(); expect(isTriggerActive(1)).toBeTrue(); - upArrowKey(); + await upArrowKey(); expect(isTriggerActive(1)).toBeFalse(); expect(isTriggerActive(0)).toBeTrue(); }); - it('should focus first trigger with Home when another item is focused', () => { - downArrowKey(); - downArrowKey(); + it('should focus first trigger with Home when another item is focused', async () => { + await downArrowKey(); + await downArrowKey(); expect(isTriggerActive(2)).toBeTrue(); - homeKey(); + await homeKey(); expect(isTriggerActive(0)).toBeTrue(); }); - it('should focus last trigger with End', () => { - endKey(); + it('should focus last trigger with End', async () => { + await endKey(); expect(isTriggerActive(2)).toBeTrue(); }); - it('should toggle expansion of focused trigger with Enter', () => { + it('should toggle expansion of focused trigger with Enter', async () => { expect(isTriggerExpanded(0)).toBeFalse(); - enterKey(triggerElements[0]); + await enterKey(triggerElements[0]); expect(isTriggerExpanded(0)).toBeTrue(); - enterKey(triggerElements[0]); + await enterKey(triggerElements[0]); expect(isTriggerExpanded(0)).toBeFalse(); }); - it('should toggle expansion of focused trigger with Space', () => { + it('should toggle expansion of focused trigger with Space', async () => { expect(isTriggerExpanded(0)).toBeFalse(); - spaceKey(triggerElements[0]); + await spaceKey(triggerElements[0]); expect(isTriggerExpanded(0)).toBeTrue(); - spaceKey(triggerElements[0]); + await spaceKey(triggerElements[0]); expect(isTriggerExpanded(0)).toBeFalse(); }); @@ -307,11 +307,11 @@ describe('AccordionGroup', () => { // Shuffle (reverse) data const items = testComponent.items().reverse(); testComponent.items.set([...items]); - fixture.detectChanges(); + await fixture.whenStable(); await waitForMicrotasks(); // Re-query elements to check new DOM order - setupTriggerAndPanels(); + await setupTriggerAndPanels(); expect(triggerElements.length).toBe(3); expect(triggerElements[0].textContent?.trim()).toBe('Item 3 Header'); @@ -319,88 +319,88 @@ describe('AccordionGroup', () => { }); describe('wrap behavior', () => { - it('should wrap to first on ArrowDown from last if wrap=true', () => { + it('should wrap to first on ArrowDown from last if wrap=true', async () => { testComponent.wrap.set(true); - fixture.detectChanges(); + await fixture.whenStable(); - endKey(); + await endKey(); expect(isTriggerActive(2)).toBeTrue(); - downArrowKey(); + await downArrowKey(); expect(isTriggerActive(0)).toBeTrue(); }); - it('should not wrap on ArrowDown from last if wrap=false', () => { + it('should not wrap on ArrowDown from last if wrap=false', async () => { testComponent.wrap.set(false); - fixture.detectChanges(); + await fixture.whenStable(); - endKey(); + await endKey(); expect(isTriggerActive(2)).toBeTrue(); - downArrowKey(); + await downArrowKey(); expect(isTriggerActive(2)).toBeTrue(); }); - it('should wrap to last on ArrowUp from first if wrap=true', () => { + it('should wrap to last on ArrowUp from first if wrap=true', async () => { testComponent.wrap.set(true); - fixture.detectChanges(); + await fixture.whenStable(); expect(isTriggerActive(0)).toBeTrue(); - upArrowKey(); + await upArrowKey(); expect(isTriggerActive(2)).toBeTrue(); }); - it('should not wrap on ArrowUp from first if wrap=false', () => { + it('should not wrap on ArrowUp from first if wrap=false', async () => { testComponent.wrap.set(false); - fixture.detectChanges(); + await fixture.whenStable(); expect(isTriggerActive(0)).toBeTrue(); - upArrowKey(); + await upArrowKey(); expect(isTriggerActive(0)).toBeTrue(); }); }); describe('softDisabled behavior', () => { - it('should skip disabled items if softDisabled=false', () => { + it('should skip disabled items if softDisabled=false', async () => { testComponent.softDisabled.set(false); testComponent.disableItem('item-2'); - fixture.detectChanges(); + await fixture.whenStable(); expect(isTriggerActive(0)).toBeTrue(); - downArrowKey(); + await downArrowKey(); expect(isTriggerActive(2)).toBeTrue(); }); - it('should focus disabled items if softDisabled=true', () => { + it('should focus disabled items if softDisabled=true', async () => { testComponent.softDisabled.set(true); testComponent.disableItem('item-2'); expect(isTriggerActive(0)).toBeTrue(); - downArrowKey(); + await downArrowKey(); expect(isTriggerActive(1)).toBeTrue(); - enterKey(triggerElements[1]); + await enterKey(triggerElements[1]); expect(isTriggerExpanded(1)).toBeFalse(); }); }); - it('should not allow keyboard navigation if group is disabled', () => { + it('should not allow keyboard navigation if group is disabled', async () => { testComponent.disabledGroup.set(true); - fixture.detectChanges(); + await fixture.whenStable(); - downArrowKey(); + await downArrowKey(); expect(isTriggerActive(1)).toBeFalse(); }); - it('should not allow expansion if group is disabled', () => { + it('should not allow expansion if group is disabled', async () => { testComponent.disabledGroup.set(true); - fixture.detectChanges(); + await fixture.whenStable(); - enterKey(triggerElements[0]); + await enterKey(triggerElements[0]); expect(isTriggerExpanded(0)).toBeFalse(); }); }); @@ -409,78 +409,78 @@ describe('AccordionGroup', () => { describe('using an if', () => { let testComponent: AccordionGroupWithIfs; - beforeEach(() => { + beforeEach(async () => { fixture = TestBed.createComponent(AccordionGroupWithIfs); testComponent = fixture.componentInstance as AccordionGroupWithIfs; groupElement = fixture.nativeElement.querySelector('[ngAccordionGroup]') as HTMLElement; - setupTriggerAndPanels(); + await setupTriggerAndPanels(); }); describe('Keyboard navigation and interaction', () => { - beforeEach(() => { + beforeEach(async () => { // Focus on the first trigger as initial state. triggerElements[0].focus(); - fixture.detectChanges(); + await fixture.whenStable(); expect(isTriggerActive(0)).toBeTrue(); }); - it('should change focus between first and last triggers when second removed', () => { + it('should change focus between first and last triggers when second removed', async () => { testComponent.includeSecond.set(false); - fixture.detectChanges(); + await fixture.whenStable(); - downArrowKey(); + await downArrowKey(); expect(isTriggerActive(0)).toBeFalse(); expect(isTriggerActive(2)).toBeTrue(); - upArrowKey(); + await upArrowKey(); expect(isTriggerActive(2)).toBeFalse(); expect(isTriggerActive(0)).toBeTrue(); }); - it('should focus second trigger with Home when first is removed', () => { + it('should focus second trigger with Home when first is removed', async () => { triggerElements[2].focus(); testComponent.includeFirst.set(false); - fixture.detectChanges(); + await fixture.whenStable(); - homeKey(); + await homeKey(); expect(isTriggerActive(0)).toBeTrue(); }); - it('should focus second trigger with End when last is removed', () => { + it('should focus second trigger with End when last is removed', async () => { triggerElements[0].focus(); testComponent.includeThird.set(false); - fixture.detectChanges(); + await fixture.whenStable(); - endKey(); + await endKey(); expect(isTriggerActive(1)).toBeTrue(); }); - it('should iterate focus through all 3 in order when replaced ', () => { + it('should iterate focus through all 3 in order when replaced ', async () => { testComponent.includeFirst.set(false); testComponent.includeSecond.set(false); testComponent.includeThird.set(false); - fixture.detectChanges(); + await fixture.whenStable(); testComponent.includeThird.set(true); testComponent.includeFirst.set(true); testComponent.includeSecond.set(true); - setupTriggerAndPanels(); + await setupTriggerAndPanels(); triggerElements[0].focus(); - fixture.detectChanges(); + await fixture.whenStable(); expect(isTriggerActive(0)).toBeTrue(); - downArrowKey(); + await downArrowKey(); expect(isTriggerActive(0)).toBeFalse(); expect(isTriggerActive(1)).toBeTrue(); - downArrowKey(); + await downArrowKey(); expect(isTriggerActive(1)).toBeFalse(); expect(isTriggerActive(2)).toBeTrue(); - downArrowKey(); + await downArrowKey(); expect(isTriggerActive(2)).toBeFalse(); expect(isTriggerActive(0)).toBeTrue(); }); @@ -494,14 +494,14 @@ describe('AccordionGroup', () => { consoleSpy = spyOn(console, 'warn'); }); - afterEach(() => { + afterEach(async () => { TestBed.resetTestingModule(); TestBed.configureTestingModule({ imports: [AccordionGroupWithLoop], providers: [provideFakeDirectionality('ltr'), _IdGenerator], }); fixture = TestBed.createComponent(AccordionGroupWithLoop); - setupAccordionGroup(); + await setupAccordionGroup(); }); it('should warn when multiple triggers control the same panel', () => { diff --git a/src/aria/combobox/combobox.spec.ts b/src/aria/combobox/combobox.spec.ts index 08b587b2181e..d6ac1f9a793a 100644 --- a/src/aria/combobox/combobox.spec.ts +++ b/src/aria/combobox/combobox.spec.ts @@ -24,8 +24,8 @@ describe('Combobox', () => { let fixture: ComponentFixture; let inputElement: HTMLInputElement; - const keydown = (key: string, modifierKeys: {} = {}) => { - focus(); + const keydown = async (key: string, modifierKeys: {} = {}) => { + await focus(); inputElement.dispatchEvent( new KeyboardEvent('keydown', { key, @@ -33,38 +33,38 @@ describe('Combobox', () => { ...modifierKeys, }), ); - fixture.detectChanges(); + await fixture.whenStable(); }; - const input = (value: string) => { - focus(); + const input = async (value: string) => { + await focus(); inputElement.value = value; inputElement.dispatchEvent(new Event('input', {bubbles: true})); - fixture.detectChanges(); + await fixture.whenStable(); }; - const click = (element: HTMLElement, eventInit?: PointerEventInit) => { - focus(); + const click = async (element: HTMLElement, eventInit?: PointerEventInit) => { + await focus(); element.dispatchEvent(new PointerEvent('click', {bubbles: true, ...eventInit})); - fixture.detectChanges(); + await fixture.whenStable(); }; - const focus = () => { + const focus = async () => { inputElement.dispatchEvent(new FocusEvent('focusin', {bubbles: true})); - fixture.detectChanges(); + await fixture.whenStable(); }; - const blur = (relatedTarget?: EventTarget) => { + const blur = async (relatedTarget?: EventTarget) => { inputElement.dispatchEvent(new FocusEvent('focusout', {bubbles: true, relatedTarget})); - fixture.detectChanges(); + await fixture.whenStable(); }; - const up = (modifierKeys?: {}) => keydown('ArrowUp', modifierKeys); - const down = (modifierKeys?: {}) => keydown('ArrowDown', modifierKeys); - const enter = (modifierKeys?: {}) => keydown('Enter', modifierKeys); - const escape = (modifierKeys?: {}) => keydown('Escape', modifierKeys); + const up = async (modifierKeys?: {}) => await keydown('ArrowUp', modifierKeys); + const down = async (modifierKeys?: {}) => await keydown('ArrowDown', modifierKeys); + const enter = async (modifierKeys?: {}) => await keydown('Enter', modifierKeys); + const escape = async (modifierKeys?: {}) => await keydown('Escape', modifierKeys); - function setupCombobox( + async function setupCombobox( componentType: any = ComboboxListboxExample, opts: {readonly?: boolean} = {}, ) { @@ -75,7 +75,7 @@ describe('Combobox', () => { testComponent.readonly.set(true); } - fixture.detectChanges(); + await fixture.whenStable(); defineTestVariables(); } @@ -96,35 +96,35 @@ describe('Combobox', () => { afterEach(async () => await runAccessibilityChecks(fixture.nativeElement)); describe('ARIA attributes and roles', () => { - beforeEach(() => setupCombobox()); + beforeEach(async () => await setupCombobox()); it('should have the combobox role on the input', () => { expect(inputElement.getAttribute('role')).toBe('combobox'); }); - it('should have aria-haspopup set to listbox', () => { - focus(); + it('should have aria-haspopup set to listbox', async () => { + await focus(); expect(inputElement.getAttribute('aria-haspopup')).toBe('listbox'); }); - it('should set aria-controls to the listbox id', () => { - down(); // Focus on Alabama + it('should set aria-controls to the listbox id', async () => { + await down(); // Focus on Alabama const listbox = fixture.debugElement.query(By.directive(Listbox)).nativeElement; expect(inputElement.getAttribute('aria-controls')).toBe(listbox.id); }); - it('should set aria-multiselectable to false on the listbox', () => { - down(); // Focus on Alabama + it('should set aria-multiselectable to false on the listbox', async () => { + await down(); // Focus on Alabama const listbox = fixture.debugElement.query(By.directive(Listbox)).nativeElement; expect(listbox.getAttribute('aria-multiselectable')).toBe('false'); }); it('should set aria-selected on the selected option', async () => { - down(); // Focus on Alabama + await down(); // Focus on Alabama expect(getOption('Alabama')!.getAttribute('aria-selected')).toBe('false'); - enter(); // Select Alabama + await enter(); // Select Alabama - down(); // Reopen popup and focus on Alabama + await down(); // Reopen popup and focus on Alabama expect(getOption('Alabama')!.getAttribute('aria-selected')).toBe('true'); }); @@ -133,10 +133,10 @@ describe('Combobox', () => { expect(inputElement.getAttribute('aria-expanded')).toBe('false'); }); - it('should toggle aria-expanded when opening and closing', () => { - down(); + it('should toggle aria-expanded when opening and closing', async () => { + await down(); expect(inputElement.getAttribute('aria-expanded')).toBe('true'); - escape(); + await escape(); expect(inputElement.getAttribute('aria-expanded')).toBe('false'); }); @@ -145,25 +145,25 @@ describe('Combobox', () => { }); it('should set aria-activedescendant to the active option id', async () => { - down(); + await down(); const option = getOption('Alabama')!; expect(inputElement.getAttribute('aria-activedescendant')).toBe(option.id); }); }); describe('Navigation', () => { - beforeEach(() => setupCombobox()); + beforeEach(async () => await setupCombobox()); it('should navigate to the first item on ArrowDown', async () => { - down(); + await down(); const options = getOptions(); expect(inputElement.getAttribute('aria-activedescendant')).toBe(options[0].id); }); it('should navigate to the last item on ArrowUp', async () => { - down(); // Opens the focus on Alabama - up(); + await down(); // Opens the focus on Alabama + await up(); const options = getOptions(); expect(inputElement.getAttribute('aria-activedescendant')).toBe( options[options.length - 1].id, @@ -171,31 +171,31 @@ describe('Combobox', () => { }); it('should navigate to the next item on ArrowDown when open', async () => { - down(); // Open popup - down(); // Move to next item + await down(); // Open popup + await down(); // Move to next item const options = getOptions(); expect(inputElement.getAttribute('aria-activedescendant')).toBe(options[1].id); }); it('should navigate to the previous item on ArrowUp when open', async () => { - down(); // Open - down(); // Move to next item - up(); // Move back to first item + await down(); // Open + await down(); // Move to next item + await up(); // Move back to first item const options = getOptions(); expect(inputElement.getAttribute('aria-activedescendant')).toBe(options[0].id); }); it('should navigate to the first item on Home when open', async () => { - down(); // Open - down(); // Move to next item - keydown('Home'); + await down(); // Open + await down(); // Move to next item + await keydown('Home'); const options = getOptions(); expect(inputElement.getAttribute('aria-activedescendant')).toBe(options[0].id); }); it('should navigate to the last item on End when open', async () => { - down(); // Open - keydown('End'); + await down(); // Open + await keydown('End'); const options = getOptions(); expect(inputElement.getAttribute('aria-activedescendant')).toBe( options[options.length - 1].id, @@ -204,97 +204,97 @@ describe('Combobox', () => { }); describe('Expansion', () => { - beforeEach(() => setupCombobox()); + beforeEach(async () => await setupCombobox()); - it('should open on ArrowDown', () => { - focus(); - keydown('ArrowDown'); + it('should open on ArrowDown', async () => { + await focus(); + await keydown('ArrowDown'); expect(inputElement.getAttribute('aria-expanded')).toBe('true'); }); - it('should close on Escape', () => { - down(); - escape(); + it('should close on Escape', async () => { + await down(); + await escape(); expect(inputElement.getAttribute('aria-expanded')).toBe('false'); }); - it('should close on focusout', () => { - focus(); - blur(); + it('should close on focusout', async () => { + await focus(); + await blur(); expect(inputElement.getAttribute('aria-expanded')).toBe('false'); }); it('should close on escape and maintain the current input value', async () => { - setupCombobox(ComboboxListboxHighlightExample); + await setupCombobox(ComboboxListboxHighlightExample); - down(); // Use down() instead of focus() - input('Ala'); + await down(); // Use await down() instead of await focus() + await input('Ala'); expect(inputElement.value).toBe('Alabama'); expect(inputElement.getAttribute('aria-expanded')).toBe('true'); - escape(); + await escape(); expect(inputElement.value).toBe('Alabama'); expect(inputElement.selectionEnd).toBe(7); expect(inputElement.selectionStart).toBe(3); expect(inputElement.getAttribute('aria-expanded')).toBe('false'); }); - it('should close on enter', () => { - down(); - enter(); + it('should close on enter', async () => { + await down(); + await enter(); expect(inputElement.getAttribute('aria-expanded')).toBe('false'); }); - it('should close on click to select an item', () => { - down(); + it('should close on click to select an item', async () => { + await down(); const fruitItem = getOption('Alabama')!; - click(fruitItem); + await click(fruitItem); expect(inputElement.getAttribute('aria-expanded')).toBe('false'); }); }); describe('Selection', () => { describe('with manual filtering', () => { - beforeEach(() => setupCombobox(ComboboxListboxExample)); + beforeEach(async () => await setupCombobox(ComboboxListboxExample)); it('should select and commit on click', async () => { - down(); // Use down() to open + await down(); // Use await down() to open const options = getOptions(); - click(options[0]); + await click(options[0]); expect(fixture.componentInstance.value()).toEqual(['Alabama']); expect(inputElement.value).toBe('Alabama'); }); it('should select and commit to input on Enter', async () => { - focus(); - down(); + await focus(); + await down(); - enter(); + await enter(); expect(fixture.componentInstance.value()).toEqual(['Alabama']); expect(inputElement.value).toBe('Alabama'); }); - it('should not select on navigation', () => { - down(); - down(); + it('should not select on navigation', async () => { + await down(); + await down(); expect(fixture.componentInstance.value()).toEqual([]); }); - it('should select on focusout if the input text exactly matches an item', () => { - focus(); - input('Alabama'); - blur(); + it('should select on focusout if the input text exactly matches an item', async () => { + await focus(); + await input('Alabama'); + await blur(); expect(fixture.componentInstance.value()).toEqual(['Alabama']); }); - it('should not select on focusout if the input text does not match an item', () => { - focus(); - input('Appl'); - blur(); + it('should not select on focusout if the input text does not match an item', async () => { + await focus(); + await input('Appl'); + await blur(); expect(fixture.componentInstance.value()).toEqual([]); expect(inputElement.value).toBe('Appl'); @@ -302,51 +302,51 @@ describe('Combobox', () => { }); describe('with auto-select behavior', () => { - beforeEach(() => setupCombobox(ComboboxListboxAutoSelectExample)); + beforeEach(async () => await setupCombobox(ComboboxListboxAutoSelectExample)); it('should select and commit on click', async () => { - down(); // Use down() to open + await down(); // Use await down() to open const options = getOptions(); - click(options[1]); + await click(options[1]); expect(fixture.componentInstance.value()).toEqual(['Alaska']); expect(inputElement.value).toBe('Alaska'); }); - it('should select and commit on Enter', () => { - down(); - down(); - enter(); + it('should select and commit on Enter', async () => { + await down(); + await down(); + await enter(); expect(fixture.componentInstance.value()).toEqual(['Alaska']); expect(inputElement.value).toBe('Alaska'); }); it('should select on navigation in auto-select', async () => { - down(); + await down(); expect(fixture.componentInstance.value()).toEqual(['Alabama']); - down(); + await down(); expect(fixture.componentInstance.value()).toEqual(['Alaska']); - down(); + await down(); expect(fixture.componentInstance.value()).toEqual(['Arizona']); }); - it('should select the first option on input', () => { - focus(); - input('W'); + it('should select the first option on input', async () => { + await focus(); + await input('W'); expect(fixture.componentInstance.value()).toEqual(['Washington']); }); - it('should commit the selected option on focusout', () => { - focus(); - input('G'); - blur(); + it('should commit the selected option on focusout', async () => { + await focus(); + await input('G'); + await blur(); expect(inputElement.value).toBe('Georgia'); expect(fixture.componentInstance.value()).toEqual(['Georgia']); @@ -354,63 +354,63 @@ describe('Combobox', () => { }); describe('with highlight behavior', () => { - beforeEach(() => setupCombobox(ComboboxListboxHighlightExample)); + beforeEach(async () => await setupCombobox(ComboboxListboxHighlightExample)); it('should select and commit on click', async () => { - down(); // Use down() to open + await down(); // Use await down() to open const options = getOptions(); - click(options[2]); + await click(options[2]); expect(fixture.componentInstance.value()).toEqual(['Arizona']); expect(inputElement.value).toBe('Arizona'); }); it('should select and commit on Enter', async () => { - down(); + await down(); - down(); - down(); - enter(); + await down(); + await down(); + await enter(); expect(fixture.componentInstance.value()).toEqual(['Arizona']); expect(inputElement.value).toBe('Arizona'); }); it('should select on navigation', async () => { - down(); + await down(); // Should auto-select the first option on open expect(fixture.componentInstance.value()).toEqual(['Alabama']); - down(); + await down(); // Should update selection on navigation expect(fixture.componentInstance.value()).toEqual(['Alaska']); }); it('should update input value on navigation', async () => { - down(); + await down(); expect(inputElement.value).toBe('Alabama'); - down(); + await down(); expect(inputElement.value).toBe('Alaska'); }); it('should select the first option on input', async () => { - down(); // Use down() instead of focus() + await down(); // Use await down() instead of await focus() - input('Cali'); + await input('Cali'); expect(fixture.componentInstance.value()).toEqual(['California']); }); it('should insert a highlighted completion string on input', async () => { - down(); // Use down() instead of focus() + await down(); // Use await down() instead of await focus() - input('A'); + await input('A'); expect(inputElement.value).toBe('Alabama'); expect(inputElement.selectionStart).toBe(1); @@ -418,9 +418,9 @@ describe('Combobox', () => { }); it('should not insert a completion string on backspace', async () => { - down(); // Use down() instead of focus() + await down(); // Use await down() instead of await focus() - input('New'); + await input('New'); expect(inputElement.value).toBe('New Hampshire'); expect(inputElement.selectionStart).toBe(3); @@ -428,13 +428,13 @@ describe('Combobox', () => { }); it('should insert a completion string even if the items are not changed', async () => { - down(); // Use down() instead of focus() + await down(); // Use await down() instead of await focus() - input('New'); + await input('New'); + await fixture.whenStable(); await fixture.whenStable(); - fixture.detectChanges(); - input('New '); + await input('New '); expect(inputElement.value).toBe('New Hampshire'); expect(inputElement.selectionStart).toBe(4); @@ -442,21 +442,21 @@ describe('Combobox', () => { }); it('should commit the selected option on focusout', async () => { - down(); // Use down() instead of focus() + await down(); // Use await down() instead of await focus() - input('Cali'); + await input('Cali'); - blur(); + await blur(); expect(inputElement.value).toBe('California'); expect(fixture.componentInstance.value()).toEqual(['California']); }); it('should resume inserting completion strings on navigation after a backspace deletion', async () => { - down(); // Open popup + await down(); // Open popup // 1. Type 'A', completion should pop up 'Alabama' - input('A'); + await input('A'); expect(inputElement.value).toBe('Alabama'); // 2. Simulate Backspace deletion (dispatch InputEvent with deleteContentBackward) @@ -467,13 +467,13 @@ describe('Combobox', () => { inputType: 'deleteContentBackward', }), ); - fixture.detectChanges(); + await fixture.whenStable(); // Confirm no completion gets inserted during deletion expect(inputElement.value).toBe(''); // 3. Press ArrowDown key to navigate to the next option (Alaska) - down(); + await down(); // Active descendant navigation resets `isDeleting`, so highlight/completion should successfully populate the current active match! const options = getOptions(); @@ -485,18 +485,18 @@ describe('Combobox', () => { describe('Filtering', () => { it('should lazily render options', async () => { - setupCombobox(); + await setupCombobox(); expect(getOptions().length).toBe(0); - down(); + await down(); expect(getOptions().length).toBe(50); }); - it('should filter the options based on the input value', () => { - setupCombobox(); - focus(); - input('New'); + it('should filter the options based on the input value', async () => { + await setupCombobox(); + await focus(); + await input('New'); const options = getOptions(); expect(options.length).toBe(4); @@ -506,128 +506,128 @@ describe('Combobox', () => { expect(options[3].textContent?.trim()).toBe('New York'); }); - it('should show no options if nothing matches', () => { - setupCombobox(); - focus(); - input('xyz'); + it('should show no options if nothing matches', async () => { + await setupCombobox(); + await focus(); + await input('xyz'); const options = getOptions(); expect(options.length).toBe(0); }); - it('should show all options when the input is cleared', () => { - setupCombobox(); - focus(); - input('Alabama'); + it('should show all options when the input is cleared', async () => { + await setupCombobox(); + await focus(); + await input('Alabama'); expect(getOptions().length).toBe(1); - input(''); + await input(''); expect(getOptions().length).toBe(50); }); }); describe('Readonly', () => { - beforeEach(() => setupCombobox(ComboboxListboxExample, {readonly: true})); + beforeEach(async () => await setupCombobox(ComboboxListboxExample, {readonly: true})); - it('should close on selection', () => { - focus(); - down(); - click(getOption('Alabama')!); + it('should close on selection', async () => { + await focus(); + await down(); + await click(getOption('Alabama')!); expect(inputElement.value).toBe('Alabama'); expect(inputElement.getAttribute('aria-expanded')).toBe('false'); }); - it('should close on escape', () => { - focus(); - down(); + it('should close on escape', async () => { + await focus(); + await down(); expect(inputElement.getAttribute('aria-expanded')).toBe('true'); - escape(); + await escape(); expect(inputElement.getAttribute('aria-expanded')).toBe('false'); }); }); describe('Always Expanded', () => { - beforeEach(() => setupCombobox()); + beforeEach(async () => await setupCombobox()); - it('should not close on escape when alwaysExpanded is true', () => { + it('should not close on escape when alwaysExpanded is true', async () => { fixture.componentInstance.alwaysExpanded.set(true); - fixture.detectChanges(); + await fixture.whenStable(); expect(inputElement.getAttribute('aria-expanded')).toBe('true'); - escape(); + await escape(); expect(inputElement.getAttribute('aria-expanded')).toBe('true'); }); - it('should automatically report as expanded when alwaysExpanded is true', () => { + it('should automatically report as expanded when alwaysExpanded is true', async () => { expect(inputElement.getAttribute('aria-expanded')).toBe('false'); fixture.componentInstance.alwaysExpanded.set(true); - fixture.detectChanges(); + await fixture.whenStable(); expect(inputElement.getAttribute('aria-expanded')).toBe('true'); }); }); describe('Disabled', () => { - beforeEach(() => setupCombobox()); + beforeEach(async () => await setupCombobox()); - it('should keep the input focusable by default when disabled', () => { + it('should keep the input focusable by default when disabled', async () => { fixture.componentInstance.disabled.set(true); - fixture.detectChanges(); + await fixture.whenStable(); expect(inputElement.disabled).toBe(false); expect(inputElement.getAttribute('disabled')).toBeNull(); expect(inputElement.getAttribute('aria-disabled')).toBe('true'); }); - it('should make the input read-only when disabled and softDisabled is true', () => { + it('should make the input read-only when disabled and softDisabled is true', async () => { fixture.componentInstance.disabled.set(true); - fixture.detectChanges(); + await fixture.whenStable(); expect(inputElement.getAttribute('readonly')).toBe(''); }); - it('should block interactions when disabled', () => { + it('should block interactions when disabled', async () => { fixture.componentInstance.disabled.set(true); - fixture.detectChanges(); + await fixture.whenStable(); - focus(); - keydown('ArrowDown'); + await focus(); + await keydown('ArrowDown'); expect(inputElement.getAttribute('aria-expanded')).toBe('false'); }); - it('should make the input unfocusable when softDisabled is false', () => { + it('should make the input unfocusable when softDisabled is false', async () => { fixture.componentInstance.disabled.set(true); fixture.componentInstance.softDisabled.set(false); - fixture.detectChanges(); + await fixture.whenStable(); expect(inputElement.disabled).toBe(true); expect(inputElement.getAttribute('disabled')).toBe(''); expect(inputElement.getAttribute('aria-disabled')).toBe('true'); }); - it('should respect user-defined tabindex when softDisabled is true', () => { + it('should respect user-defined tabindex when softDisabled is true', async () => { fixture.componentInstance.disabled.set(true); fixture.componentInstance.tabIndex.set(0); - fixture.detectChanges(); + await fixture.whenStable(); expect(inputElement.getAttribute('tabindex')).toBe('0'); }); - it('should respect user-defined tabindex when not disabled', () => { + it('should respect user-defined tabindex when not disabled', async () => { fixture.componentInstance.tabIndex.set(0); - fixture.detectChanges(); + await fixture.whenStable(); expect(inputElement.getAttribute('tabindex')).toBe('0'); }); - it('should default to tabindex 0 when not disabled', () => { - fixture.detectChanges(); + it('should default to tabindex 0 when not disabled', async () => { + await fixture.whenStable(); expect(inputElement.getAttribute('tabindex')).toBe('0'); }); - it('should force tabindex to -1 when hard-disabled, ignoring user-defined tabindex', () => { + it('should force tabindex to -1 when hard-disabled, ignoring user-defined tabindex', async () => { fixture.componentInstance.disabled.set(true); fixture.componentInstance.softDisabled.set(false); fixture.componentInstance.tabIndex.set(0); - fixture.detectChanges(); + await fixture.whenStable(); expect(inputElement.getAttribute('tabindex')).toBe('-1'); }); @@ -638,8 +638,8 @@ describe('Combobox', () => { let fixture: ComponentFixture; let inputElement: HTMLInputElement; - const keydown = (key: string, modifierKeys: {} = {}) => { - focus(); + const keydown = async (key: string, modifierKeys: {} = {}) => { + await focus(); inputElement.dispatchEvent( new KeyboardEvent('keydown', { key, @@ -647,40 +647,40 @@ describe('Combobox', () => { ...modifierKeys, }), ); - fixture.detectChanges(); + await fixture.whenStable(); }; - const input = (value: string) => { - focus(); + const input = async (value: string) => { + await focus(); inputElement.value = value; inputElement.dispatchEvent(new Event('input', {bubbles: true})); - fixture.detectChanges(); + await fixture.whenStable(); }; - const click = (element: HTMLElement, eventInit?: PointerEventInit) => { - focus(); + const click = async (element: HTMLElement, eventInit?: PointerEventInit) => { + await focus(); element.dispatchEvent(new PointerEvent('click', {bubbles: true, ...eventInit})); - fixture.detectChanges(); + await fixture.whenStable(); }; - const focus = () => { + const focus = async () => { inputElement.dispatchEvent(new FocusEvent('focusin', {bubbles: true})); - fixture.detectChanges(); + await fixture.whenStable(); }; - const blur = (relatedTarget?: EventTarget) => { + const blur = async (relatedTarget?: EventTarget) => { inputElement.dispatchEvent(new FocusEvent('focusout', {bubbles: true, relatedTarget})); - fixture.detectChanges(); + await fixture.whenStable(); }; - const up = (modifierKeys?: {}) => keydown('ArrowUp', modifierKeys); - const down = (modifierKeys?: {}) => keydown('ArrowDown', modifierKeys); - const left = (modifierKeys?: {}) => keydown('ArrowLeft', modifierKeys); - const right = (modifierKeys?: {}) => keydown('ArrowRight', modifierKeys); - const enter = (modifierKeys?: {}) => keydown('Enter', modifierKeys); - const escape = (modifierKeys?: {}) => keydown('Escape', modifierKeys); + const up = async (modifierKeys?: {}) => await keydown('ArrowUp', modifierKeys); + const down = async (modifierKeys?: {}) => await keydown('ArrowDown', modifierKeys); + const left = async (modifierKeys?: {}) => await keydown('ArrowLeft', modifierKeys); + const right = async (modifierKeys?: {}) => await keydown('ArrowRight', modifierKeys); + const enter = async (modifierKeys?: {}) => await keydown('Enter', modifierKeys); + const escape = async (modifierKeys?: {}) => await keydown('Escape', modifierKeys); - function setupCombobox(opts: {readonly?: boolean} = {}) { + async function setupCombobox(opts: {readonly?: boolean} = {}) { fixture = TestBed.createComponent(ComboboxTreeExample); const testComponent = fixture.componentInstance; @@ -688,7 +688,7 @@ describe('Combobox', () => { testComponent.readonly.set(true); } - fixture.detectChanges(); + await fixture.whenStable(); defineTestVariables(); } @@ -727,84 +727,84 @@ describe('Combobox', () => { }); describe('ARIA attributes and roles', () => { - beforeEach(() => setupCombobox()); + beforeEach(async () => await setupCombobox()); - it('should have aria-haspopup set to tree', () => { - focus(); + it('should have aria-haspopup set to tree', async () => { + await focus(); expect(inputElement.getAttribute('aria-haspopup')).toBe('tree'); }); - it('should set aria-controls to the tree id', () => { - down(); + it('should set aria-controls to the tree id', async () => { + await down(); const tree = fixture.debugElement.query(By.directive(Tree)).nativeElement; expect(inputElement.getAttribute('aria-controls')).toBe(tree.id); }); it('should set aria-selected on the selected tree item', async () => { - down(); + await down(); const item = getTreeItem('Winter')!; - enter(); + await enter(); expect(item.getAttribute('aria-selected')).toBe('true'); }); it('should toggle aria-expanded on parent nodes', async () => { - down(); + await down(); const item = getTreeItem('Winter')!; expect(item.getAttribute('aria-expanded')).toBe('false'); - right(); // Opens Winter + await right(); // Opens Winter expect(item.getAttribute('aria-expanded')).toBe('true'); - left(); // Closes Winter + await left(); // Closes Winter expect(item.getAttribute('aria-expanded')).toBe('false'); }); }); describe('Navigation', () => { - beforeEach(() => setupCombobox()); + beforeEach(async () => await setupCombobox()); it('should navigate to the first focusable item on ArrowDown', async () => { - down(); // Winter + await down(); // Winter const item = getTreeItem('Winter')!; expect(inputElement.getAttribute('aria-activedescendant')).toBe(item.id); }); it('should navigate to the last focusable item on ArrowUp', async () => { - down(); // Winter - up(); // Fall + await down(); // Winter + await up(); // Fall const item = getTreeItem('Fall')!; expect(inputElement.getAttribute('aria-activedescendant')).toBe(item.id); }); it('should navigate to the next focusable item on ArrowDown when open', async () => { - down(); // Winter - down(); // Spring + await down(); // Winter + await down(); // Spring const item = getTreeItem('Spring')!; expect(inputElement.getAttribute('aria-activedescendant')).toBe(item.id); }); it('should navigate to the previous item on ArrowUp when open', async () => { - down(); // Winter - down(); // Spring - down(); // Summer - down(); // Fall - up(); // Summer + await down(); // Winter + await down(); // Spring + await down(); // Summer + await down(); // Fall + await up(); // Summer const item = getTreeItem('Summer')!; expect(inputElement.getAttribute('aria-activedescendant')).toBe(item.id); }); it('should expand a closed node on ArrowRight', async () => { - down(); // Winter + await down(); // Winter expect(getVisibleTreeItems().length).toBe(4); - right(); // Expand Winter + await right(); // Expand Winter expect(getVisibleTreeItems().length).toBe(7); expect(getTreeItem('January')).not.toBeNull(); }); it('should navigate to the next item on ArrowRight when already expanded', async () => { - down(); // Winter - right(); // Expand Winter - right(); // December + await down(); // Winter + await right(); // Expand Winter + await right(); // December const item = getTreeItem('December')!; @@ -812,10 +812,10 @@ describe('Combobox', () => { }); it('should collapse an open node on ArrowLeft', async () => { - down(); // Winter - right(); // Winter Expanded + await down(); // Winter + await right(); // Winter Expanded expect(getVisibleTreeItems().length).toBe(7); - left(); // Winter Collapsed + await left(); // Winter Collapsed expect(getVisibleTreeItems().length).toBe(4); const item = getTreeItem('Winter')!; @@ -823,32 +823,32 @@ describe('Combobox', () => { }); it('should navigate to the parent node on ArrowLeft when in a child node', async () => { - down(); // Winter - right(); // Expand Winter - right(); // December + await down(); // Winter + await right(); // Expand Winter + await right(); // December const item1 = getTreeItem('December')!; expect(inputElement.getAttribute('aria-activedescendant')).toBe(item1.id); - left(); + await left(); const item2 = getTreeItem('Winter')!; expect(inputElement.getAttribute('aria-activedescendant')).toBe(item2.id); }); it('should navigate to the first focusable item on Home when open', async () => { - down(); - down(); - keydown('Home'); + await down(); + await down(); + await keydown('Home'); const item = getTreeItem('Winter')!; expect(inputElement.getAttribute('aria-activedescendant')).toBe(item.id); }); it('should navigate to the last focusable item on End when open', async () => { - down(); - down(); - keydown('End'); + await down(); + await down(); + await keydown('End'); const grainsItem = getTreeItem('Fall')!; expect(inputElement.getAttribute('aria-activedescendant')).toBe(grainsItem.id); @@ -856,93 +856,93 @@ describe('Combobox', () => { }); describe('Expansion', () => { - beforeEach(() => setupCombobox()); + beforeEach(async () => await setupCombobox()); - it('should open on ArrowDown', () => { - focus(); - keydown('ArrowDown'); + it('should open on ArrowDown', async () => { + await focus(); + await keydown('ArrowDown'); expect(inputElement.getAttribute('aria-expanded')).toBe('true'); }); - it('should close on Escape', () => { - down(); - escape(); + it('should close on Escape', async () => { + await down(); + await escape(); expect(inputElement.getAttribute('aria-expanded')).toBe('false'); }); - it('should close on focusout', () => { - focus(); - blur(); + it('should close on focusout', async () => { + await focus(); + await blur(); expect(inputElement.getAttribute('aria-expanded')).toBe('false'); }); - it('should close on escape', () => { - focus(); - input('Mar'); + it('should close on escape', async () => { + await focus(); + await input('Mar'); expect(inputElement.getAttribute('aria-expanded')).toBe('true'); - escape(); + await escape(); expect(inputElement.getAttribute('aria-expanded')).toBe('false'); }); - it('should close on enter', () => { - down(); - enter(); + it('should close on enter', async () => { + await down(); + await enter(); expect(inputElement.getAttribute('aria-expanded')).toBe('false'); }); - it('should close on click to select an item', () => { - down(); - click(getTreeItem('Spring')!); + it('should close on click to select an item', async () => { + await down(); + await click(getTreeItem('Spring')!); expect(inputElement.getAttribute('aria-expanded')).toBe('false'); }); }); describe('Selection', () => { describe('with manual filtering', () => { - beforeEach(() => setupCombobox()); + beforeEach(async () => await setupCombobox()); - it('should select and commit on click', () => { - click(inputElement); + it('should select and commit on click', async () => { + await click(inputElement); // Iterate to the parent node and expand it so the child is visible - down(); // Winter - down(); // Spring - right(); // Expand Spring + await down(); // Winter + await down(); // Spring + await right(); // Expand Spring const item = getTreeItem('April')!; - click(item); + await click(item); expect(fixture.componentInstance.value()).toEqual(['April']); expect(inputElement.value).toBe('April'); }); - it('should select and commit to input on Enter', () => { - down(); - enter(); + it('should select and commit to input on Enter', async () => { + await down(); + await enter(); expect(fixture.componentInstance.value()).toEqual(['Winter']); expect(inputElement.value).toBe('Winter'); }); - it('should select on focusout if the input text exactly matches an item', () => { - focus(); - input('November'); - blur(); + it('should select on focusout if the input text exactly matches an item', async () => { + await focus(); + await input('November'); + await blur(); expect(fixture.componentInstance.value()).toEqual(['November']); }); - it('should not select on navigation', () => { - down(); - down(); + it('should not select on navigation', async () => { + await down(); + await down(); expect(fixture.componentInstance.value()).toEqual([]); }); - it('should not select on focusout if the input text does not match an item', () => { - focus(); - input('Appl'); - blur(); + it('should not select on focusout if the input text does not match an item', async () => { + await focus(); + await input('Appl'); + await blur(); expect(fixture.componentInstance.value()).toEqual([]); expect(inputElement.value).toBe('Appl'); @@ -951,35 +951,35 @@ describe('Combobox', () => { }); describe('Filtering', () => { - beforeEach(() => setupCombobox()); + beforeEach(async () => await setupCombobox()); it('should lazily render options', async () => { expect(getTreeItems().length).toBe(0); - focus(); - down(); + await focus(); + await down(); // Mutate dataSource to expand all fixture.componentInstance.dataSource().forEach(node => (node.expanded = true)); // Force computed signal to re-evaluate by updating dataSource reference fixture.componentInstance.dataSource.set([...fixture.componentInstance.dataSource()]); - fixture.detectChanges(); + await fixture.whenStable(); expect(getTreeItems().length).toBe(16); }); - it('should filter the options based on the input value', () => { - focus(); - input('Summer'); + it('should filter the options based on the input value', async () => { + await focus(); + await input('Summer'); let items = getVisibleTreeItems(); expect(items.length).toBe(1); expect(items[0].textContent?.trim()).toBe('Summer'); }); - it('should render parents if a child matches', () => { - focus(); - input('January'); + it('should render parents if a child matches', async () => { + await focus(); + await input('January'); let items = getVisibleTreeItems(); expect(items.length).toBe(2); @@ -987,28 +987,28 @@ describe('Combobox', () => { expect(items[1].textContent?.trim()).toBe('January'); }); - it('should show no options if nothing matches', () => { - focus(); - input('xyz'); + it('should show no options if nothing matches', async () => { + await focus(); + await input('xyz'); expect(getVisibleTreeItems().length).toBe(0); }); - it('should show all options when the input is cleared', () => { - focus(); - input('Winter'); + it('should show all options when the input is cleared', async () => { + await focus(); + await input('Winter'); expect(getVisibleTreeItems().length).toBe(1); - input(''); + await input(''); expect(getVisibleTreeItems().length).toBe(4); }); - it('should expand all nodes when filtering', () => { - focus(); - down(); + it('should expand all nodes when filtering', async () => { + await focus(); + await down(); expect(getVisibleTreeItems().length).toBe(4); - input('J'); + await input('J'); expect(getTreeItem('Winter')!.getAttribute('aria-expanded')).toBe('true'); expect(getTreeItem('Summer')!.getAttribute('aria-expanded')).toBe('true'); @@ -1020,8 +1020,8 @@ describe('Combobox', () => { let fixture: ComponentFixture; let inputElement: HTMLInputElement; - const keydown = (key: string, modifierKeys: {} = {}) => { - focus(); + const keydown = async (key: string, modifierKeys: {} = {}) => { + await focus(); inputElement.dispatchEvent( new KeyboardEvent('keydown', { key, @@ -1029,202 +1029,202 @@ describe('Combobox', () => { ...modifierKeys, }), ); - fixture.detectChanges(); + await fixture.whenStable(); }; - const focus = () => { + const focus = async () => { inputElement.dispatchEvent(new FocusEvent('focusin', {bubbles: true})); - fixture.detectChanges(); + await fixture.whenStable(); }; - const blur = (relatedTarget?: EventTarget) => { + const blur = async (relatedTarget?: EventTarget) => { inputElement.dispatchEvent(new FocusEvent('focusout', {bubbles: true, relatedTarget})); - fixture.detectChanges(); + await fixture.whenStable(); }; - const up = (modifierKeys?: {}) => keydown('ArrowUp', modifierKeys); - const down = (modifierKeys?: {}) => keydown('ArrowDown', modifierKeys); - const left = (modifierKeys?: {}) => keydown('ArrowLeft', modifierKeys); - const right = (modifierKeys?: {}) => keydown('ArrowRight', modifierKeys); - const enter = (modifierKeys?: {}) => keydown('Enter', modifierKeys); - const escape = (modifierKeys?: {}) => keydown('Escape', modifierKeys); - const home = (modifierKeys?: {}) => keydown('Home', modifierKeys); - const end = (modifierKeys?: {}) => keydown('End', modifierKeys); + const up = async (modifierKeys?: {}) => await keydown('ArrowUp', modifierKeys); + const down = async (modifierKeys?: {}) => await keydown('ArrowDown', modifierKeys); + const left = async (modifierKeys?: {}) => await keydown('ArrowLeft', modifierKeys); + const right = async (modifierKeys?: {}) => await keydown('ArrowRight', modifierKeys); + const enter = async (modifierKeys?: {}) => await keydown('Enter', modifierKeys); + const escape = async (modifierKeys?: {}) => await keydown('Escape', modifierKeys); + const home = async (modifierKeys?: {}) => await keydown('Home', modifierKeys); + const end = async (modifierKeys?: {}) => await keydown('End', modifierKeys); - function setupCombobox() { + async function setupCombobox() { fixture = TestBed.createComponent(ComboboxGridExample); - fixture.detectChanges(); + await fixture.whenStable(); const inputDebugElement = fixture.debugElement.query(By.directive(Combobox)); inputElement = inputDebugElement.nativeElement as HTMLInputElement; } - beforeEach(() => setupCombobox()); + beforeEach(async () => await setupCombobox()); describe('ARIA attributes and roles', () => { - beforeEach(() => setupCombobox()); + beforeEach(async () => await setupCombobox()); it('should have the combobox role on the input', () => { expect(inputElement.getAttribute('role')).toBe('combobox'); }); - it('should have aria-haspopup set to grid', () => { - focus(); + it('should have aria-haspopup set to grid', async () => { + await focus(); expect(inputElement.getAttribute('aria-haspopup')).toBe('grid'); }); - it('should set aria-controls to the grid id', () => { - down(); + it('should set aria-controls to the grid id', async () => { + await down(); const grid = fixture.debugElement.query(By.directive(Grid)).nativeElement; expect(inputElement.getAttribute('aria-controls')).toBe(grid.id); }); - it('should toggle aria-expanded when opening and closing', () => { - down(); + it('should toggle aria-expanded when opening and closing', async () => { + await down(); expect(inputElement.getAttribute('aria-expanded')).toBe('true'); - escape(); + await escape(); expect(inputElement.getAttribute('aria-expanded')).toBe('false'); }); it('should set aria-activedescendant to the active grid cell id', async () => { - focus(); - down(); // Open popup + await focus(); + await down(); // Open popup expect(inputElement.getAttribute('aria-activedescendant')).toBe('Antelope-label'); }); }); it('should navigate up and down with grid navigation', async () => { - focus(); - down(); // Open popup + await focus(); + await down(); // Open popup - down(); // Navigate down to 'Bird-label' + await down(); // Navigate down to 'Bird-label' expect(inputElement.getAttribute('aria-activedescendant')).toBe('Bird-label'); - up(); // Navigate back up to 'Antelope-label' + await up(); // Navigate back up to 'Antelope-label' expect(inputElement.getAttribute('aria-activedescendant')).toBe('Antelope-label'); }); it('should navigate left and right with grid navigation', async () => { - focus(); - down(); // Open popup + await focus(); + await down(); // Open popup - right(); // Move right to 'Antelope-delete' + await right(); // Move right to 'Antelope-delete' expect(inputElement.getAttribute('aria-activedescendant')).toBe('Antelope-delete'); - left(); // Move back left to 'Antelope-label' + await left(); // Move back left to 'Antelope-label' expect(inputElement.getAttribute('aria-activedescendant')).toBe('Antelope-label'); }); it('should navigate to the start of the row on Home', async () => { - focus(); - down(); // Open popup + await focus(); + await down(); // Open popup - right(); // Move right to 'Antelope-delete' + await right(); // Move right to 'Antelope-delete' expect(inputElement.getAttribute('aria-activedescendant')).toBe('Antelope-delete'); - home(); // Move back to 'Antelope-label' + await home(); // Move back to 'Antelope-label' expect(inputElement.getAttribute('aria-activedescendant')).toBe('Antelope-label'); }); it('should navigate to the end of the row on End', async () => { - focus(); - down(); // Open popup + await focus(); + await down(); // Open popup - end(); // Move to end of row ('Antelope-delete') + await end(); // Move to end of row ('Antelope-delete') expect(inputElement.getAttribute('aria-activedescendant')).toBe('Antelope-delete'); }); it('should update aria-activedescendant with grid navigation', async () => { - focus(); - down(); // Open popup + await focus(); + await down(); // Open popup - down(); // Navigate down + await down(); // Navigate down // The active item is 'Bird' because we navigated down once more expect(inputElement.getAttribute('aria-activedescendant')).toBe('Bird-label'); - right(); // Move right to delete button + await right(); // Move right to delete button expect(inputElement.getAttribute('aria-activedescendant')).toBe('Bird-delete'); - down(); // Move down to next row + await down(); // Move down to next row expect(inputElement.getAttribute('aria-activedescendant')).toBe('Cat-delete'); }); it('should remove an item when delete is pressed in the delete cell', async () => { - down(); // On Antelope - right(); // Move right to delete button - enter(); // Click delete button + await down(); // On Antelope + await right(); // Move right to delete button + await enter(); // Click delete button expect(fixture.componentInstance.items()).not.toContain('Antelope'); }); it('should filter items and maintain selection', async () => { - down(); // Antelope - enter(); // Select active item + await down(); // Antelope + await enter(); // Select active item expect(fixture.componentInstance.searchString()).toBe('Antelope'); inputElement.value = ''; inputElement.dispatchEvent(new Event('input', {bubbles: true})); - fixture.detectChanges(); + await fixture.whenStable(); expect(fixture.componentInstance.searchString()).toBe(''); - down(); // Go to BirdLabel + await down(); // Go to BirdLabel expect(inputElement.getAttribute('aria-activedescendant')).toBe('Bird-label'); }); describe('Expansion', () => { - beforeEach(() => setupCombobox()); + beforeEach(async () => await setupCombobox()); - it('should close on Escape', () => { - down(); - escape(); + it('should close on Escape', async () => { + await down(); + await escape(); expect(inputElement.getAttribute('aria-expanded')).toBe('false'); }); - it('should close on focusout', () => { - focus(); - blur(); + it('should close on focusout', async () => { + await focus(); + await blur(); expect(inputElement.getAttribute('aria-expanded')).toBe('false'); }); - it('should close on enter', () => { - down(); - enter(); + it('should close on enter', async () => { + await down(); + await enter(); expect(inputElement.getAttribute('aria-expanded')).toBe('false'); }); }); describe('Selection', () => { - beforeEach(() => setupCombobox()); + beforeEach(async () => await setupCombobox()); it('should select and commit on click', async () => { - focus(); - down(); // Open popup + await focus(); + await down(); // Open popup const gridCells = fixture.nativeElement.querySelectorAll('[ngGridCellWidget]'); gridCells[0].dispatchEvent(new PointerEvent('click', {bubbles: true})); - fixture.detectChanges(); + await fixture.whenStable(); expect(fixture.componentInstance.selectedItem()).toBe('Antelope'); expect(inputElement.value).toBe('Antelope'); }); it('should not select on navigation', async () => { - focus(); - down(); // Open popup + await focus(); + await down(); // Open popup - down(); // Move row down + await down(); // Move row down expect(fixture.componentInstance.selectedItem()).toBeNull(); }); diff --git a/src/aria/grid/grid.spec.ts b/src/aria/grid/grid.spec.ts index b62fea85aca5..3e799a4670eb 100644 --- a/src/aria/grid/grid.spec.ts +++ b/src/aria/grid/grid.spec.ts @@ -53,30 +53,30 @@ describe('Grid directives', () => { let gridElement: HTMLElement; let gridInstance: Grid; - const keydown = (key: string, modifierKeys: ModifierKeys = {}) => { + const keydown = async (key: string, modifierKeys: ModifierKeys = {}) => { const event = new KeyboardEvent('keydown', {key, bubbles: true, ...modifierKeys}); gridElement.dispatchEvent(event); - fixture.detectChanges(); + await fixture.whenStable(); }; - const pointerDown = (target: HTMLElement, eventInit: PointerEventInit = {}) => { + const pointerDown = async (target: HTMLElement, eventInit: PointerEventInit = {}) => { target.dispatchEvent(new PointerEvent('click', {bubbles: true, ...eventInit})); - fixture.detectChanges(); + await fixture.whenStable(); }; - const up = (modifierKeys?: ModifierKeys) => keydown('ArrowUp', modifierKeys); - const down = (modifierKeys?: ModifierKeys) => keydown('ArrowDown', modifierKeys); - const left = (modifierKeys?: ModifierKeys) => keydown('ArrowLeft', modifierKeys); - const right = (modifierKeys?: ModifierKeys) => keydown('ArrowRight', modifierKeys); - const home = (modifierKeys?: ModifierKeys) => keydown('Home', modifierKeys); - const end = (modifierKeys?: ModifierKeys) => keydown('End', modifierKeys); - const enter = (modifierKeys?: ModifierKeys) => keydown('Enter', modifierKeys); - const space = (modifierKeys?: ModifierKeys) => keydown(' ', modifierKeys); + const up = async (modifierKeys?: ModifierKeys) => await keydown('ArrowUp', modifierKeys); + const down = async (modifierKeys?: ModifierKeys) => await keydown('ArrowDown', modifierKeys); + const left = async (modifierKeys?: ModifierKeys) => await keydown('ArrowLeft', modifierKeys); + const right = async (modifierKeys?: ModifierKeys) => await keydown('ArrowRight', modifierKeys); + const home = async (modifierKeys?: ModifierKeys) => await keydown('Home', modifierKeys); + const end = async (modifierKeys?: ModifierKeys) => await keydown('End', modifierKeys); + const enter = async (modifierKeys?: ModifierKeys) => await keydown('Enter', modifierKeys); + const space = async (modifierKeys?: ModifierKeys) => await keydown(' ', modifierKeys); - const tabIntoGrid = () => { + const tabIntoGrid = async () => { const focusableElement = gridElement.querySelector('[tabindex="0"]') as HTMLElement; focusableElement?.focus(); - fixture.detectChanges(); + await fixture.whenStable(); }; function getActiveCellId(): string | null { @@ -87,7 +87,7 @@ describe('Grid directives', () => { return gridElement.getAttribute('aria-activedescendant'); } - function setupGrid(opts?: { + async function setupGrid(opts?: { disabled?: boolean; multi?: boolean; rowWrap?: 'continuous' | 'loop' | 'nowrap'; @@ -121,7 +121,7 @@ describe('Grid directives', () => { testComponent.gridData.set(createGridData()); } - fixture.detectChanges(); + await fixture.whenStable(); gridDebugElement = fixture.debugElement.query(By.directive(Grid)); gridElement = gridDebugElement.nativeElement; gridInstance = gridDebugElement.injector.get(Grid); @@ -129,62 +129,62 @@ describe('Grid directives', () => { describe('Grid', () => { describe('ARIA attributes and roles', () => { - it('should set role="grid" on the host element', () => { - setupGrid(); + it('should set role="grid" on the host element', async () => { + await setupGrid(); expect(gridElement.getAttribute('role')).toBe('grid'); }); - it('should set aria-disabled="true" when the disabled input is true', () => { - setupGrid({disabled: true}); + it('should set aria-disabled="true" when the disabled input is true', async () => { + await setupGrid({disabled: true}); expect(gridElement.getAttribute('aria-disabled')).toBe('true'); }); - it('should set aria-disabled="false" when the disabled input is false', () => { - setupGrid({disabled: false}); + it('should set aria-disabled="false" when the disabled input is false', async () => { + await setupGrid({disabled: false}); expect(gridElement.getAttribute('aria-disabled')).toBe('false'); }); - it('should set aria-multiselectable="true" when the multi input is true', () => { - setupGrid({enableSelection: true, multi: true}); + it('should set aria-multiselectable="true" when the multi input is true', async () => { + await setupGrid({enableSelection: true, multi: true}); expect(gridElement.getAttribute('aria-multiselectable')).toBe('true'); }); - it('should set aria-multiselectable="false" when the multi input is false', () => { - setupGrid({enableSelection: true, multi: false}); + it('should set aria-multiselectable="false" when the multi input is false', async () => { + await setupGrid({enableSelection: true, multi: false}); expect(gridElement.getAttribute('aria-multiselectable')).toBe('false'); }); - it('should set aria-activedescendant to the active cell id', () => { - setupGrid({focusMode: 'activedescendant'}); + it('should set aria-activedescendant to the active cell id', async () => { + await setupGrid({focusMode: 'activedescendant'}); // Simulate gaining focus so active Descendant gets populated gridElement.dispatchEvent(new Event('focusin')); - fixture.detectChanges(); + await fixture.whenStable(); expect(gridElement.getAttribute('aria-activedescendant')).toBe('c0-0'); }); }); describe('focus management', () => { it('should set tabindex based on the pattern tabIndex', async () => { - setupGrid({focusMode: 'roving'}); + await setupGrid({focusMode: 'roving'}); gridInstance._pattern.setDefaultStateEffect(); - fixture.detectChanges(); + await fixture.whenStable(); await fixture.whenStable(); expect(gridElement.getAttribute('tabindex')).toBe('-1'); // roving defaults to -1 on host - setupGrid({focusMode: 'activedescendant'}); + await setupGrid({focusMode: 'activedescendant'}); gridInstance._pattern.setDefaultStateEffect(); - fixture.detectChanges(); + await fixture.whenStable(); await fixture.whenStable(); expect(gridElement.getAttribute('tabindex')).toBe('0'); // activedescendant defaults to 0 on host }); - it('should be able to override tabindex', () => { - setupGrid({focusMode: 'activedescendant', tabIndex: -1}); + it('should be able to override tabindex', async () => { + await setupGrid({focusMode: 'activedescendant', tabIndex: -1}); expect(gridElement.getAttribute('tabindex')).toBe('-1'); }); - it('should activate the cell when the grid receives focusin', () => { - setupGrid(); + it('should activate the cell when the grid receives focusin', async () => { + await setupGrid(); // Let effect run to set default state which sets initial active cell gridInstance._pattern.setDefaultStateEffect(); @@ -193,21 +193,21 @@ describe('Grid directives', () => { // Dispatch focusin to the cell cell1.dispatchEvent(new FocusEvent('focusin', {bubbles: true})); - fixture.detectChanges(); + await fixture.whenStable(); expect(gridInstance._pattern.activeCell()?.element()).toBe(cell1); expect(gridInstance._pattern.isFocused()).toBeTrue(); }); - it('should deactivate the grid when focusout moves outside the grid', () => { - setupGrid(); + it('should deactivate the grid when focusout moves outside the grid', async () => { + await setupGrid(); const cell1 = fixture.debugElement.query(By.directive(GridCell)).nativeElement; // Focus first gridInstance._pattern.setDefaultStateEffect(); cell1.dispatchEvent(new FocusEvent('focusin', {bubbles: true})); - fixture.detectChanges(); + await fixture.whenStable(); expect(gridInstance._pattern.isFocused()).toBeTrue(); // Focusout (blur) @@ -217,7 +217,7 @@ describe('Grid directives', () => { relatedTarget: document.body, }); cell1.dispatchEvent(focusOutEvent); - fixture.detectChanges(); + await fixture.whenStable(); expect(gridInstance._pattern.isFocused()).toBeFalse(); }); @@ -225,158 +225,158 @@ describe('Grid directives', () => { describe('keyboard interactions', () => { describe('navigation keys', () => { - beforeEach(() => { - setupGrid(); + beforeEach(async () => { + await setupGrid(); // Let effect run to set default state which sets initial active cell gridInstance._pattern.setDefaultStateEffect(); - fixture.detectChanges(); + await fixture.whenStable(); // Start interactions from the middle cell (c1-1) const centerCell = gridElement.querySelector('#c1-1') as HTMLElement; centerCell.dispatchEvent(new FocusEvent('focusin', {bubbles: true})); - fixture.detectChanges(); + await fixture.whenStable(); }); - it('should move focus up to the previous row on ArrowUp', () => { - up(); + it('should move focus up to the previous row on ArrowUp', async () => { + await up(); expect(getActiveCellId()).toBe('c0-1'); }); - it('should move focus down to the next row on ArrowDown', () => { - down(); + it('should move focus down to the next row on ArrowDown', async () => { + await down(); expect(getActiveCellId()).toBe('c2-1'); }); - it('should move focus left to the previous column on ArrowLeft', () => { - left(); + it('should move focus left to the previous column on ArrowLeft', async () => { + await left(); expect(getActiveCellId()).toBe('c1-0'); }); - it('should move focus right to the next column on ArrowRight', () => { - right(); + it('should move focus right to the next column on ArrowRight', async () => { + await right(); expect(getActiveCellId()).toBe('c1-2'); }); - it('should move focus to the first cell in the row on Home', () => { - home(); + it('should move focus to the first cell in the row on Home', async () => { + await home(); expect(getActiveCellId()).toBe('c1-0'); }); - it('should move focus to the last cell in the row on End', () => { - end(); + it('should move focus to the last cell in the row on End', async () => { + await end(); expect(getActiveCellId()).toBe('c1-2'); }); describe('colWrap configuration', () => { - it('should wrap to next row when moving right from last column of a row', () => { - setupGrid({colWrap: 'continuous'}); + it('should wrap to next row when moving right from last column of a row', async () => { + await setupGrid({colWrap: 'continuous'}); gridInstance._pattern.setDefaultStateEffect(); - fixture.detectChanges(); + await fixture.whenStable(); const cell = gridElement.querySelector('#c1-2') as HTMLElement; cell.dispatchEvent(new FocusEvent('focusin', {bubbles: true})); - fixture.detectChanges(); + await fixture.whenStable(); - right(); + await right(); expect(getActiveCellId()).toBe('c2-0'); }); - it('should wrap to previous row when moving left from first column of a row', () => { - setupGrid({colWrap: 'continuous'}); + it('should wrap to previous row when moving left from first column of a row', async () => { + await setupGrid({colWrap: 'continuous'}); gridInstance._pattern.setDefaultStateEffect(); - fixture.detectChanges(); + await fixture.whenStable(); const cell = gridElement.querySelector('#c1-0') as HTMLElement; cell.dispatchEvent(new FocusEvent('focusin', {bubbles: true})); - fixture.detectChanges(); + await fixture.whenStable(); - left(); + await left(); expect(getActiveCellId()).toBe('c0-2'); }); - it('should not wrap to next row when passing row boundaries if colWrap is nowrap', () => { - setupGrid({colWrap: 'nowrap'}); + it('should not wrap to next row when passing row boundaries if colWrap is nowrap', async () => { + await setupGrid({colWrap: 'nowrap'}); gridInstance._pattern.setDefaultStateEffect(); - fixture.detectChanges(); + await fixture.whenStable(); const cell = gridElement.querySelector('#c1-2') as HTMLElement; cell.dispatchEvent(new FocusEvent('focusin', {bubbles: true})); - fixture.detectChanges(); + await fixture.whenStable(); - right(); + await right(); expect(getActiveCellId()).toBe('c1-2'); }); - it('should wrap around the same row when passing row boundaries if colWrap is loop', () => { - setupGrid({colWrap: 'loop'}); + it('should wrap around the same row when passing row boundaries if colWrap is loop', async () => { + await setupGrid({colWrap: 'loop'}); gridInstance._pattern.setDefaultStateEffect(); - fixture.detectChanges(); + await fixture.whenStable(); const cell = gridElement.querySelector('#c1-2') as HTMLElement; cell.dispatchEvent(new FocusEvent('focusin', {bubbles: true})); - fixture.detectChanges(); + await fixture.whenStable(); - right(); + await right(); expect(getActiveCellId()).toBe('c1-0'); }); }); describe('rowWrap configuration', () => { - it('should wrap to next column when moving down from last row of a column', () => { - setupGrid({rowWrap: 'continuous'}); + it('should wrap to next column when moving down from last row of a column', async () => { + await setupGrid({rowWrap: 'continuous'}); gridInstance._pattern.setDefaultStateEffect(); - fixture.detectChanges(); + await fixture.whenStable(); const cell = gridElement.querySelector('#c2-1') as HTMLElement; cell.dispatchEvent(new FocusEvent('focusin', {bubbles: true})); - fixture.detectChanges(); + await fixture.whenStable(); - down(); + await down(); expect(getActiveCellId()).toBe('c0-2'); }); - it('should wrap to previous column when moving up from first row of a column', () => { - setupGrid({rowWrap: 'continuous'}); + it('should wrap to previous column when moving up from first row of a column', async () => { + await setupGrid({rowWrap: 'continuous'}); gridInstance._pattern.setDefaultStateEffect(); - fixture.detectChanges(); + await fixture.whenStable(); const cell = gridElement.querySelector('#c0-1') as HTMLElement; cell.dispatchEvent(new FocusEvent('focusin', {bubbles: true})); - fixture.detectChanges(); + await fixture.whenStable(); - up(); + await up(); expect(getActiveCellId()).toBe('c2-0'); }); - it('should not wrap to next column when passing column boundaries if rowWrap is nowrap', () => { - setupGrid({rowWrap: 'nowrap'}); + it('should not wrap to next column when passing column boundaries if rowWrap is nowrap', async () => { + await setupGrid({rowWrap: 'nowrap'}); gridInstance._pattern.setDefaultStateEffect(); - fixture.detectChanges(); + await fixture.whenStable(); const cell = gridElement.querySelector('#c2-1') as HTMLElement; cell.dispatchEvent(new FocusEvent('focusin', {bubbles: true})); - fixture.detectChanges(); + await fixture.whenStable(); - down(); + await down(); expect(getActiveCellId()).toBe('c2-1'); }); - it('should wrap around the same column when passing column boundaries if rowWrap is loop', () => { - setupGrid({rowWrap: 'loop'}); + it('should wrap around the same column when passing column boundaries if rowWrap is loop', async () => { + await setupGrid({rowWrap: 'loop'}); gridInstance._pattern.setDefaultStateEffect(); - fixture.detectChanges(); + await fixture.whenStable(); const cell = gridElement.querySelector('#c2-1') as HTMLElement; cell.dispatchEvent(new FocusEvent('focusin', {bubbles: true})); - fixture.detectChanges(); + await fixture.whenStable(); - down(); + await down(); expect(getActiveCellId()).toBe('c0-1'); }); }); @@ -384,58 +384,58 @@ describe('Grid directives', () => { describe('selection keys', () => { describe('selectionMode="explicit"', () => { - beforeEach(() => { - setupGrid({ + beforeEach(async () => { + await setupGrid({ enableSelection: true, selectionMode: 'explicit', multi: true, }); gridInstance._pattern.setDefaultStateEffect(); - fixture.detectChanges(); + await fixture.whenStable(); // Start interactions from the middle cell const centerCell = gridElement.querySelector('#c1-1') as HTMLElement; centerCell.dispatchEvent(new FocusEvent('focusin', {bubbles: true})); - fixture.detectChanges(); + await fixture.whenStable(); }); - it('should toggle selection of the active cell on Space', () => { + it('should toggle selection of the active cell on Space', async () => { const centerCell = gridElement.querySelector('#c1-1') as HTMLElement; expect(centerCell.getAttribute('aria-selected')).toBe('false'); - space(); + await space(); expect(centerCell.getAttribute('aria-selected')).toBe('true'); - space(); + await space(); expect(centerCell.getAttribute('aria-selected')).toBe('false'); }); - it('should trigger default action of the active cell on Enter', () => { + it('should trigger default action of the active cell on Enter', async () => { const centerCell = gridElement.querySelector('#c1-1') as HTMLElement; expect(centerCell.getAttribute('aria-selected')).toBe('false'); - enter(); + await enter(); expect(centerCell.getAttribute('aria-selected')).toBe('true'); - enter(); + await enter(); expect(centerCell.getAttribute('aria-selected')).toBe('false'); }); - it('should select all selectable cells on Ctrl+A', () => { - keydown('a', {ctrlKey: true}); + it('should select all selectable cells on Ctrl+A', async () => { + await keydown('a', {ctrlKey: true}); const cells = gridElement.querySelectorAll('[ngGridCell]'); for (let i = 0; i < cells.length; i++) { expect(cells[i].getAttribute('aria-selected')).toBe('true'); } - keydown('a', {ctrlKey: true}); + await keydown('a', {ctrlKey: true}); for (let i = 0; i < cells.length; i++) { expect(cells[i].getAttribute('aria-selected')).toBe('false'); @@ -444,22 +444,22 @@ describe('Grid directives', () => { }); describe('selectionMode="follow"', () => { - beforeEach(() => { - setupGrid({ + beforeEach(async () => { + await setupGrid({ enableSelection: true, selectionMode: 'follow', multi: false, }); gridInstance._pattern.setDefaultStateEffect(); - fixture.detectChanges(); + await fixture.whenStable(); // Start interactions from the middle cell const centerCell = gridElement.querySelector('#c1-1') as HTMLElement; centerCell.dispatchEvent(new FocusEvent('focusin', {bubbles: true})); - fixture.detectChanges(); + await fixture.whenStable(); }); - it('should select an item when navigated to with Arrow keys and deselect others', () => { + it('should select an item when navigated to with Arrow keys and deselect others', async () => { const centerCell = gridElement.querySelector('#c1-1') as HTMLElement; const topCell = gridElement.querySelector('#c0-1') as HTMLElement; @@ -467,13 +467,13 @@ describe('Grid directives', () => { expect(centerCell.getAttribute('aria-selected')).toBe('false'); expect(topCell.getAttribute('aria-selected')).toBe('false'); - up(); + await up(); // Arrow keys select the target expect(centerCell.getAttribute('aria-selected')).toBe('false'); expect(topCell.getAttribute('aria-selected')).toBe('true'); - down(); + await down(); expect(topCell.getAttribute('aria-selected')).toBe('false'); expect(centerCell.getAttribute('aria-selected')).toBe('true'); @@ -483,21 +483,21 @@ describe('Grid directives', () => { }); describe('click interactions', () => { - beforeEach(() => { - setupGrid({ + beforeEach(async () => { + await setupGrid({ enableSelection: true, selectionMode: 'explicit', multi: true, }); gridInstance._pattern.setDefaultStateEffect(); - fixture.detectChanges(); + await fixture.whenStable(); }); - it('should focus and select the clicked cell on click', () => { + it('should focus and select the clicked cell on click', async () => { const cell = gridElement.querySelector('#c1-1') as HTMLElement; expect(cell.getAttribute('aria-selected')).toBe('false'); - pointerDown(cell); + await pointerDown(cell); expect(cell.getAttribute('aria-selected')).toBe('true'); expect(getActiveCellId()).toBe('c1-1'); @@ -505,8 +505,8 @@ describe('Grid directives', () => { }); describe('configuration', () => { - it('should prevent multiple selections if selectionMode is single (multi false)', () => { - setupGrid({ + it('should prevent multiple selections if selectionMode is single (multi false)', async () => { + await setupGrid({ enableSelection: true, selectionMode: 'explicit', multi: false, @@ -515,65 +515,65 @@ describe('Grid directives', () => { const cell1 = gridElement.querySelector('#c0-0') as HTMLElement; const cell2 = gridElement.querySelector('#c1-1') as HTMLElement; - pointerDown(cell1); + await pointerDown(cell1); expect(cell1.getAttribute('aria-selected')).toBe('true'); - pointerDown(cell2); + await pointerDown(cell2); expect(cell1.getAttribute('aria-selected')).toBe('false'); expect(cell2.getAttribute('aria-selected')).toBe('true'); }); - it('should allow interaction but indicate disabled state if softDisabled is true', () => { + it('should allow interaction but indicate disabled state if softDisabled is true', async () => { const gridData = createGridData(); gridData[1].cells[1].disabled = true; - setupGrid({ + await setupGrid({ enableSelection: true, selectionMode: 'explicit', softDisabled: true, gridData, }); gridInstance._pattern.setDefaultStateEffect(); - fixture.detectChanges(); + await fixture.whenStable(); const disabledCell = gridElement.querySelector('#c1-1') as HTMLElement; expect(disabledCell.getAttribute('aria-disabled')).toBe('true'); // Can still interact visually, e.g. focus disabledCell.dispatchEvent(new FocusEvent('focusin', {bubbles: true})); - fixture.detectChanges(); + await fixture.whenStable(); expect(getActiveCellId()).toBe('c1-1'); // But space should not select it? Wait, interaction is allowed, but actions might be blocked. - space(); + await space(); expect(disabledCell.getAttribute('aria-selected')).toBe('false'); }); - it('should skip disabled cells if softDisabled is false', () => { + it('should skip disabled cells if softDisabled is false', async () => { const gridData = createGridData(); gridData[1].cells[1].disabled = true; - setupGrid({ + await setupGrid({ softDisabled: false, gridData, }); gridInstance._pattern.setDefaultStateEffect(); - fixture.detectChanges(); + await fixture.whenStable(); const startCell = gridElement.querySelector('#c1-0') as HTMLElement; startCell.dispatchEvent(new FocusEvent('focusin', {bubbles: true})); - fixture.detectChanges(); + await fixture.whenStable(); // Navigating right from c1-0 should skip c1-1 and go to c1-2 - right(); + await right(); expect(getActiveCellId()).toBe('c1-2'); }); }); describe('dynamic updates', () => { it('should update row order correctly after rows are shuffled', async () => { - setupGrid(); + await setupGrid(); gridInstance._pattern.setDefaultStateEffect(); - fixture.detectChanges(); + await fixture.whenStable(); const rowPatternsBefore = gridInstance._pattern.inputs.rows(); expect(rowPatternsBefore.length).toBe(3); @@ -583,7 +583,7 @@ describe('Grid directives', () => { const firstRow = gridData.shift()!; gridData.push(firstRow); fixture.componentInstance.gridData.set([...gridData]); - fixture.detectChanges(); + await fixture.whenStable(); await waitForMicrotasks(); const rowPatternsAfter = gridInstance._pattern.inputs.rows(); @@ -595,16 +595,16 @@ describe('Grid directives', () => { describe('GridRow', () => { describe('ARIA attributes and roles', () => { - it('should set role="row" on the host element', () => { - setupGrid(); + it('should set role="row" on the host element', async () => { + await setupGrid(); const row = gridElement.querySelector('tr') as HTMLElement; expect(row.getAttribute('role')).toBe('row'); }); - it('should set aria-rowindex based on the rowIndex input', () => { + it('should set aria-rowindex based on the rowIndex input', async () => { const gridData = createGridData(); gridData[0].rowIndex = 5; - setupGrid({gridData}); + await setupGrid({gridData}); const row = gridElement.querySelector('tr') as HTMLElement; expect(row.getAttribute('aria-rowindex')).toBe('5'); }); @@ -612,9 +612,9 @@ describe('Grid directives', () => { describe('dynamic updates', () => { it('should update cell order correctly after cells are shuffled', async () => { - setupGrid(); + await setupGrid(); gridInstance._pattern.setDefaultStateEffect(); - fixture.detectChanges(); + await fixture.whenStable(); const firstRow = gridDebugElement.query(By.directive(GridRow)).injector.get(GridRow); const cellPatternsBefore = firstRow._pattern.inputs.cells(); @@ -626,7 +626,7 @@ describe('Grid directives', () => { const firstCell = firstRowCells.shift()!; firstRowCells.push(firstCell); fixture.componentInstance.gridData.set([...gridData]); - fixture.detectChanges(); + await fixture.whenStable(); await waitForMicrotasks(); const cellPatternsAfter = firstRow._pattern.inputs.cells(); @@ -638,106 +638,106 @@ describe('Grid directives', () => { describe('GridCell', () => { describe('ARIA attributes and roles', () => { - it('should set the role based on the role input', () => { + it('should set the role based on the role input', async () => { const gridData = createGridData(); gridData[1].cells[1].role = 'columnheader'; - setupGrid({gridData}); + await setupGrid({gridData}); const cell = gridElement.querySelector('#c1-1') as HTMLElement; expect(cell.getAttribute('role')).toBe('columnheader'); }); - it('should set aria-rowindex to the provided row index', () => { + it('should set aria-rowindex to the provided row index', async () => { const gridData = createGridData(); gridData[1].cells[1].rowIndex = 4; - setupGrid({gridData}); + await setupGrid({gridData}); const cell = gridElement.querySelector('#c1-1') as HTMLElement; expect(cell.getAttribute('aria-rowindex')).toBe('4'); }); - it('should set aria-colindex to the provided column index', () => { + it('should set aria-colindex to the provided column index', async () => { const gridData = createGridData(); gridData[1].cells[1].colIndex = 3; - setupGrid({gridData}); + await setupGrid({gridData}); const cell = gridElement.querySelector('#c1-1') as HTMLElement; expect(cell.getAttribute('aria-colindex')).toBe('3'); }); - it('should set aria-selected="true" when the cell is selected', () => { - setupGrid({enableSelection: true, selectionMode: 'explicit'}); + it('should set aria-selected="true" when the cell is selected', async () => { + await setupGrid({enableSelection: true, selectionMode: 'explicit'}); gridInstance._pattern.setDefaultStateEffect(); - fixture.detectChanges(); + await fixture.whenStable(); const cell = gridElement.querySelector('#c1-1') as HTMLElement; expect(cell.getAttribute('aria-selected')).toBe('false'); - pointerDown(cell); + await pointerDown(cell); expect(cell.getAttribute('aria-selected')).toBe('true'); }); - it('should set aria-selected="false" when the cell is unselected', () => { - setupGrid({enableSelection: true, selectionMode: 'explicit'}); + it('should set aria-selected="false" when the cell is unselected', async () => { + await setupGrid({enableSelection: true, selectionMode: 'explicit'}); gridInstance._pattern.setDefaultStateEffect(); - fixture.detectChanges(); + await fixture.whenStable(); const cell = gridElement.querySelector('#c1-1') as HTMLElement; - pointerDown(cell); + await pointerDown(cell); expect(cell.getAttribute('aria-selected')).toBe('true'); - pointerDown(cell); + await pointerDown(cell); expect(cell.getAttribute('aria-selected')).toBe('false'); }); - it('should set aria-rowspan to the provided rowspan configuration', () => { + it('should set aria-rowspan to the provided rowspan configuration', async () => { const gridData = createGridData(); gridData[1].cells[1].rowSpan = 2; - setupGrid({gridData}); + await setupGrid({gridData}); const cell = gridElement.querySelector('#c1-1') as HTMLElement; expect(cell.getAttribute('rowspan')).toBe('2'); expect(cell.getAttribute('aria-rowspan')).toBe('2'); }); - it('should set aria-colspan to the provided colspan configuration', () => { + it('should set aria-colspan to the provided colspan configuration', async () => { const gridData = createGridData(); gridData[1].cells[1].colSpan = 3; - setupGrid({gridData}); + await setupGrid({gridData}); const cell = gridElement.querySelector('#c1-1') as HTMLElement; expect(cell.getAttribute('colspan')).toBe('3'); expect(cell.getAttribute('aria-colspan')).toBe('3'); }); - it('should set id from the id input if provided', () => { + it('should set id from the id input if provided', async () => { const gridData = createGridData(); gridData[1].cells[1].id = 'custom-id'; - setupGrid({gridData}); + await setupGrid({gridData}); const cell = gridElement.querySelector('#custom-id') as HTMLElement; expect(cell.getAttribute('id')).toBe('custom-id'); }); }); describe('focus management', () => { - it('should set tabindex="0" on the active cell', () => { - setupGrid({focusMode: 'roving'}); + it('should set tabindex="0" on the active cell', async () => { + await setupGrid({focusMode: 'roving'}); gridInstance._pattern.setDefaultStateEffect(); - fixture.detectChanges(); + await fixture.whenStable(); const cell = gridElement.querySelector('#c0-0') as HTMLElement; expect(cell.getAttribute('tabindex')).toBe('0'); }); - it('should set tabindex="-1" on inactive cells', () => { - setupGrid({focusMode: 'roving'}); + it('should set tabindex="-1" on inactive cells', async () => { + await setupGrid({focusMode: 'roving'}); gridInstance._pattern.setDefaultStateEffect(); - fixture.detectChanges(); + await fixture.whenStable(); const cell = gridElement.querySelector('#c1-1') as HTMLElement; expect(cell.getAttribute('tabindex')).toBe('-1'); }); - it('should set data-active="true" when the cell becomes active', () => { - setupGrid(); + it('should set data-active="true" when the cell becomes active', async () => { + await setupGrid(); gridInstance._pattern.setDefaultStateEffect(); - fixture.detectChanges(); + await fixture.whenStable(); const cell = gridElement.querySelector('#c0-0') as HTMLElement; expect(cell.getAttribute('data-active')).toBe('true'); @@ -745,64 +745,64 @@ describe('Grid directives', () => { }); describe('configuration', () => { - it('should prevent selection when disabled input is true', () => { + it('should prevent selection when disabled input is true', async () => { const gridData = createGridData(); gridData[1].cells[1].disabled = true; - setupGrid({enableSelection: true, selectionMode: 'explicit', gridData}); + await setupGrid({enableSelection: true, selectionMode: 'explicit', gridData}); gridInstance._pattern.setDefaultStateEffect(); - fixture.detectChanges(); + await fixture.whenStable(); const cell = gridElement.querySelector('#c1-1') as HTMLElement; - pointerDown(cell); + await pointerDown(cell); expect(cell.getAttribute('aria-selected')).toBe('false'); }); - it('should prevent interaction when disabled input is true', () => { + it('should prevent interaction when disabled input is true', async () => { const gridData = createGridData(); gridData[1].cells[1].disabled = true; - setupGrid({ + await setupGrid({ enableSelection: true, selectionMode: 'explicit', softDisabled: false, gridData, }); gridInstance._pattern.setDefaultStateEffect(); - fixture.detectChanges(); + await fixture.whenStable(); const startCell = gridElement.querySelector('#c1-0') as HTMLElement; startCell.dispatchEvent(new FocusEvent('focusin', {bubbles: true})); - fixture.detectChanges(); + await fixture.whenStable(); // Navigating right from c1-0 should skip c1-1 and go to c1-2 because it's completely disabled - right(); + await right(); expect(getActiveCellId()).toBe('c1-2'); }); - it('should prevent selection when selectable input is false', () => { + it('should prevent selection when selectable input is false', async () => { const gridData = createGridData(); gridData[1].cells[1].selectable = false; - setupGrid({enableSelection: true, selectionMode: 'explicit', gridData}); + await setupGrid({enableSelection: true, selectionMode: 'explicit', gridData}); gridInstance._pattern.setDefaultStateEffect(); - fixture.detectChanges(); + await fixture.whenStable(); const cell = gridElement.querySelector('#c1-1') as HTMLElement; - pointerDown(cell); + await pointerDown(cell); expect(cell.hasAttribute('aria-selected')).toBeFalse(); }); - it('should update the selected property when bound value changes', () => { + it('should update the selected property when bound value changes', async () => { // Needs a two-way binding test, but we can just test selection updates since it is model() - setupGrid({enableSelection: true, selectionMode: 'explicit'}); + await setupGrid({enableSelection: true, selectionMode: 'explicit'}); gridInstance._pattern.setDefaultStateEffect(); - fixture.detectChanges(); + await fixture.whenStable(); const cellDirective = fixture.debugElement.query(By.css('#c1-1')).injector.get(GridCell); expect(cellDirective.selected()).toBeFalse(); const cell = gridElement.querySelector('#c1-1') as HTMLElement; - pointerDown(cell); + await pointerDown(cell); expect(cellDirective.selected()).toBeTrue(); }); @@ -811,65 +811,65 @@ describe('Grid directives', () => { describe('GridCellWidget', () => { describe('ARIA and Host attributes', () => { - it('should set data-active="true" when the widget is active', () => { + it('should set data-active="true" when the widget is active', async () => { const gridData = createGridData(); gridData[1].cells[1].widgets = [{id: 'w1', type: 'simple'}]; - setupGrid({gridData}); + await setupGrid({gridData}); gridInstance._pattern.setDefaultStateEffect(); - fixture.detectChanges(); + await fixture.whenStable(); const cell = gridElement.querySelector('#c1-1') as HTMLElement; const widget = gridElement.querySelector('#w1') as HTMLElement; expect(widget.getAttribute('data-active')).toBe('false'); - pointerDown(cell); + await pointerDown(cell); expect(widget.getAttribute('data-active')).toBe('true'); }); - it('should set data-active-control="widget" when the widget is activated', () => { + it('should set data-active-control="widget" when the widget is activated', async () => { const gridData = createGridData(); gridData[1].cells[1].widgets = [{id: 'w1', type: 'editable'}]; - setupGrid({gridData}); + await setupGrid({gridData}); gridInstance._pattern.setDefaultStateEffect(); - fixture.detectChanges(); + await fixture.whenStable(); const cell = gridElement.querySelector('#c1-1') as HTMLElement; const widget = gridElement.querySelector('#w1') as HTMLElement; - pointerDown(cell); + await pointerDown(cell); expect(widget.getAttribute('data-active-control')).toBe('cell'); - keydown('Enter'); + await keydown('Enter'); expect(widget.getAttribute('data-active-control')).toBe('widget'); }); - it('should set tabindex="0" on the active widget', () => { + it('should set tabindex="0" on the active widget', async () => { const gridData = createGridData(); gridData[1].cells[1].widgets = [{id: 'w1', type: 'simple'}]; - setupGrid({gridData}); + await setupGrid({gridData}); gridInstance._pattern.setDefaultStateEffect(); - fixture.detectChanges(); + await fixture.whenStable(); const cell = gridElement.querySelector('#c1-1') as HTMLElement; const widget = gridElement.querySelector('#w1') as HTMLElement; - pointerDown(cell); + await pointerDown(cell); expect(widget.getAttribute('tabindex')).toBe('0'); }); - it('should set tabindex="-1" on inactive widgets', () => { + it('should set tabindex="-1" on inactive widgets', async () => { const gridData = createGridData(); gridData[1].cells[1].widgets = [{id: 'w1', type: 'simple'}]; gridData[1].cells[2].widgets = [{id: 'w2', type: 'simple'}]; - setupGrid({gridData}); + await setupGrid({gridData}); gridInstance._pattern.setDefaultStateEffect(); - fixture.detectChanges(); + await fixture.whenStable(); const widget1 = gridElement.querySelector('#w1') as HTMLElement; const widget2 = gridElement.querySelector('#w2') as HTMLElement; const cell = gridElement.querySelector('#c1-1') as HTMLElement; - pointerDown(cell); + await pointerDown(cell); expect(widget1.getAttribute('tabindex')).toBe('0'); expect(widget2.getAttribute('tabindex')).toBe('-1'); @@ -877,103 +877,103 @@ describe('Grid directives', () => { }); describe('widget activation', () => { - it('should immediately delegate focus when widgetType is simple', () => { + it('should immediately delegate focus when widgetType is simple', async () => { const gridData = createGridData(); gridData[0].cells[0].widgets = [{id: 'w1', type: 'simple'}]; - setupGrid({gridData}); + await setupGrid({gridData}); gridInstance._pattern.setDefaultStateEffect(); - fixture.detectChanges(); + await fixture.whenStable(); const widget = gridElement.querySelector('#w1') as HTMLElement; - tabIntoGrid(); + await tabIntoGrid(); expect(document.activeElement).toBe(widget); }); - it('should wait for enter key to delegate focus when widgetType is complex', () => { + it('should wait for enter key to delegate focus when widgetType is complex', async () => { const gridData = createGridData(); gridData[0].cells[0].widgets = [{id: 'w1', type: 'complex'}]; - setupGrid({gridData}); + await setupGrid({gridData}); gridInstance._pattern.setDefaultStateEffect(); - fixture.detectChanges(); + await fixture.whenStable(); const widgetDirective = fixture.debugElement .query(By.css('#w1')) .injector.get(GridCellWidget); - tabIntoGrid(); + await tabIntoGrid(); expect(widgetDirective.isActivated()).toBeFalse(); - keydown('Enter'); + await keydown('Enter'); expect(widgetDirective.isActivated()).toBeTrue(); }); - it('should enter edit mode when widgetType is editable and enter is pressed', () => { + it('should enter edit mode when widgetType is editable and enter is pressed', async () => { const gridData = createGridData(); gridData[0].cells[0].widgets = [{id: 'w1', type: 'editable'}]; - setupGrid({gridData}); + await setupGrid({gridData}); gridInstance._pattern.setDefaultStateEffect(); - fixture.detectChanges(); + await fixture.whenStable(); const widgetDirective = fixture.debugElement .query(By.css('#w1')) .injector.get(GridCellWidget); - tabIntoGrid(); + await tabIntoGrid(); - keydown('Enter'); + await keydown('Enter'); expect(widgetDirective.isActivated()).toBeTrue(); }); - it('should give widget tabindex -1 when focusTarget is present', () => { + it('should give widget tabindex -1 when focusTarget is present', async () => { const gridData = createGridData(); gridData[0].cells[0].widgets = [{id: 'w1', type: 'complex', hasTarget: true}]; - setupGrid({gridData}); + await setupGrid({gridData}); gridInstance._pattern.setDefaultStateEffect(); - fixture.detectChanges(); + await fixture.whenStable(); const widgetElement = gridElement.querySelector('#w1') as HTMLElement; expect(widgetElement.getAttribute('tabindex')).toBe('-1'); }); - it('should emit the activated output on Enter for simple widget', () => { + it('should emit the activated output on Enter for simple widget', async () => { const gridData = createGridData(); gridData[0].cells[0].widgets = [{id: 'w1', type: 'simple'}]; - setupGrid({gridData}); + await setupGrid({gridData}); gridInstance._pattern.setDefaultStateEffect(); - fixture.detectChanges(); + await fixture.whenStable(); - tabIntoGrid(); + await tabIntoGrid(); expect(fixture.componentInstance.onActivated).not.toHaveBeenCalled(); - keydown('Enter'); + await keydown('Enter'); expect(fixture.componentInstance.onActivated).toHaveBeenCalled(); }); - it('should emit the activated output on Space for simple widget', () => { + it('should emit the activated output on Space for simple widget', async () => { const gridData = createGridData(); gridData[0].cells[0].widgets = [{id: 'w1', type: 'simple'}]; - setupGrid({gridData}); + await setupGrid({gridData}); gridInstance._pattern.setDefaultStateEffect(); - fixture.detectChanges(); + await fixture.whenStable(); - tabIntoGrid(); + await tabIntoGrid(); expect(fixture.componentInstance.onActivated).not.toHaveBeenCalled(); - keydown(' '); + await keydown(' '); expect(fixture.componentInstance.onActivated).toHaveBeenCalled(); }); - it('should emit the activated output in activedescendant mode when event is dispatched directly to grid', () => { + it('should emit the activated output in activedescendant mode when event is dispatched directly to grid', async () => { const gridData = createGridData(); gridData[0].cells[0].widgets = [{id: 'w1', type: 'simple'}]; - setupGrid({gridData, focusMode: 'activedescendant'}); + await setupGrid({gridData, focusMode: 'activedescendant'}); gridInstance._pattern.setDefaultStateEffect(); - fixture.detectChanges(); + await fixture.whenStable(); expect(fixture.componentInstance.onActivated).not.toHaveBeenCalled(); @@ -983,78 +983,78 @@ describe('Grid directives', () => { bubbles: true, }); gridElement.dispatchEvent(event); - fixture.detectChanges(); + await fixture.whenStable(); expect(fixture.componentInstance.onActivated).toHaveBeenCalled(); }); - it('should emit the activated output when the widget becomes active', () => { + it('should emit the activated output when the widget becomes active', async () => { const gridData = createGridData(); gridData[0].cells[0].widgets = [{id: 'w1', type: 'complex'}]; - setupGrid({gridData}); + await setupGrid({gridData}); gridInstance._pattern.setDefaultStateEffect(); - fixture.detectChanges(); + await fixture.whenStable(); - tabIntoGrid(); + await tabIntoGrid(); expect(fixture.componentInstance.onActivated).not.toHaveBeenCalled(); - keydown('Enter'); + await keydown('Enter'); expect(fixture.componentInstance.onActivated).toHaveBeenCalled(); }); - it('should emit the deactivated output when the widget loses active state', () => { + it('should emit the deactivated output when the widget loses active state', async () => { const gridData = createGridData(); gridData[0].cells[0].widgets = [{id: 'w1', type: 'complex'}]; - setupGrid({gridData}); + await setupGrid({gridData}); gridInstance._pattern.setDefaultStateEffect(); - fixture.detectChanges(); + await fixture.whenStable(); - tabIntoGrid(); + await tabIntoGrid(); - keydown('Enter'); + await keydown('Enter'); expect(fixture.componentInstance.onDeactivated).not.toHaveBeenCalled(); - keydown('Escape'); + await keydown('Escape'); expect(fixture.componentInstance.onDeactivated).toHaveBeenCalled(); }); - it('should become active when activate() is called programmatically', () => { + it('should become active when activate() is called programmatically', async () => { const gridData = createGridData(); gridData[0].cells[0].widgets = [{id: 'w1', type: 'complex'}]; - setupGrid({gridData}); + await setupGrid({gridData}); gridInstance._pattern.setDefaultStateEffect(); - fixture.detectChanges(); + await fixture.whenStable(); const widgetDirective = fixture.debugElement .query(By.css('#w1')) .injector.get(GridCellWidget); widgetDirective.activate(); - fixture.detectChanges(); + await fixture.whenStable(); expect(widgetDirective.isActivated()).toBeTrue(); expect(fixture.componentInstance.onActivated).toHaveBeenCalled(); }); - it('should lose active state when deactivate() is called programmatically', () => { + it('should lose active state when deactivate() is called programmatically', async () => { const gridData = createGridData(); gridData[0].cells[0].widgets = [{id: 'w1', type: 'complex'}]; - setupGrid({gridData}); + await setupGrid({gridData}); gridInstance._pattern.setDefaultStateEffect(); - fixture.detectChanges(); + await fixture.whenStable(); const widgetDirective = fixture.debugElement .query(By.css('#w1')) .injector.get(GridCellWidget); widgetDirective.activate(); - fixture.detectChanges(); + await fixture.whenStable(); expect(widgetDirective.isActivated()).toBeTrue(); widgetDirective.deactivate(); - fixture.detectChanges(); + await fixture.whenStable(); expect(widgetDirective.isActivated()).toBeFalse(); }); @@ -1068,9 +1068,9 @@ describe('Grid directives', () => { consoleSpy = spyOn(console, 'warn'); }); - afterEach(() => { + afterEach(async () => { TestBed.resetTestingModule(); - setupGrid(); + await setupGrid(); }); it('should warn when ngGridRow contains no cells', () => { diff --git a/src/aria/listbox/listbox.spec.ts b/src/aria/listbox/listbox.spec.ts index 84cb05254921..b6065d252e02 100644 --- a/src/aria/listbox/listbox.spec.ts +++ b/src/aria/listbox/listbox.spec.ts @@ -22,7 +22,7 @@ describe('Listbox', () => { let listboxElement: HTMLElement; let optionElements: HTMLElement[]; - const keydown = (key: string, modifierKeys: ModifierKeys = {}) => { + const keydown = async (key: string, modifierKeys: ModifierKeys = {}) => { listboxElement.dispatchEvent( new KeyboardEvent('keydown', { key, @@ -30,30 +30,30 @@ describe('Listbox', () => { ...modifierKeys, }), ); - fixture.detectChanges(); + await fixture.whenStable(); }; - const click = (index: number, eventInit?: PointerEventInit, targets?: HTMLElement[]) => { + const click = async (index: number, eventInit?: PointerEventInit, targets?: HTMLElement[]) => { (targets || optionElements)[index].dispatchEvent( new PointerEvent('click', { bubbles: true, ...eventInit, }), ); - fixture.detectChanges(); + await fixture.whenStable(); }; - const space = (modifierKeys?: ModifierKeys) => keydown(' ', modifierKeys); - const enter = (modifierKeys?: ModifierKeys) => keydown('Enter', modifierKeys); - const up = (modifierKeys?: ModifierKeys) => keydown('ArrowUp', modifierKeys); - const down = (modifierKeys?: ModifierKeys) => keydown('ArrowDown', modifierKeys); - const left = (modifierKeys?: ModifierKeys) => keydown('ArrowLeft', modifierKeys); - const right = (modifierKeys?: ModifierKeys) => keydown('ArrowRight', modifierKeys); - const home = (modifierKeys?: ModifierKeys) => keydown('Home', modifierKeys); - const end = (modifierKeys?: ModifierKeys) => keydown('End', modifierKeys); - const type = (char: string) => keydown(char); - - function setupListbox(opts?: { + const space = async (modifierKeys?: ModifierKeys) => await keydown(' ', modifierKeys); + const enter = async (modifierKeys?: ModifierKeys) => await keydown('Enter', modifierKeys); + const up = async (modifierKeys?: ModifierKeys) => await keydown('ArrowUp', modifierKeys); + const down = async (modifierKeys?: ModifierKeys) => await keydown('ArrowDown', modifierKeys); + const left = async (modifierKeys?: ModifierKeys) => await keydown('ArrowLeft', modifierKeys); + const right = async (modifierKeys?: ModifierKeys) => await keydown('ArrowRight', modifierKeys); + const home = async (modifierKeys?: ModifierKeys) => await keydown('Home', modifierKeys); + const end = async (modifierKeys?: ModifierKeys) => await keydown('End', modifierKeys); + const type = async (char: string) => await keydown(char); + + async function setupListbox(opts?: { orientation?: 'horizontal' | 'vertical'; disabled?: boolean; readonly?: boolean; @@ -97,11 +97,11 @@ describe('Listbox', () => { testComponent.options.set([...currentOptions]); } - fixture.detectChanges(); + await fixture.whenStable(); defineTestVariables(fixture); } - function setupDefaultListbox() { + async function setupDefaultListbox() { TestBed.configureTestingModule({ providers: [provideFakeDirectionality('ltr')], }); @@ -123,7 +123,7 @@ describe('Listbox', () => { describe('ARIA attributes and roles', () => { describe('default configuration', () => { - beforeEach(() => setupDefaultListbox()); + beforeEach(async () => await setupDefaultListbox()); it('should correctly set the role attribute to "listbox"', () => { expect(listboxElement.getAttribute('role')).toBe('listbox'); @@ -159,33 +159,33 @@ describe('Listbox', () => { }); describe('custom configuration', () => { - it('should be able to set aria-orientation to "horizontal"', () => { - setupListbox({orientation: 'horizontal'}); + it('should be able to set aria-orientation to "horizontal"', async () => { + await setupListbox({orientation: 'horizontal'}); expect(listboxElement.getAttribute('aria-orientation')).toBe('horizontal'); }); - it('should be able to set aria-disabled to "true"', () => { - setupListbox({disabled: true}); + it('should be able to set aria-disabled to "true"', async () => { + await setupListbox({disabled: true}); expect(listboxElement.getAttribute('aria-disabled')).toBe('true'); }); - it('should be able to set aria-readonly to "true"', () => { - setupListbox({readonly: true}); + it('should be able to set aria-readonly to "true"', async () => { + await setupListbox({readonly: true}); expect(listboxElement.getAttribute('aria-readonly')).toBe('true'); }); - it('should be able to set aria-multiselectable to "true"', () => { - setupListbox({multi: true}); + it('should be able to set aria-multiselectable to "true"', async () => { + await setupListbox({multi: true}); expect(listboxElement.getAttribute('aria-multiselectable')).toBe('true'); }); - it('should be able to override tabindex', () => { - setupListbox({tabIndex: -1}); + it('should be able to override tabindex', async () => { + await setupListbox({tabIndex: -1}); expect(listboxElement.getAttribute('tabindex')).toBe('-1'); }); - it('should set aria-selected to "true" for selected options', () => { - setupListbox({multi: true, value: [1, 3]}); + it('should set aria-selected to "true" for selected options', async () => { + await setupListbox({multi: true, value: [1, 3]}); expect(optionElements[0].getAttribute('aria-selected')).toBe('false'); expect(optionElements[1].getAttribute('aria-selected')).toBe('true'); expect(optionElements[2].getAttribute('aria-selected')).toBe('false'); @@ -193,8 +193,8 @@ describe('Listbox', () => { expect(optionElements[4].getAttribute('aria-selected')).toBe('false'); }); - it('should set aria-disabled to "true" for disabled options', () => { - setupListbox({disabledOptions: [1]}); + it('should set aria-disabled to "true" for disabled options', async () => { + await setupListbox({disabledOptions: [1]}); expect(optionElements[0].getAttribute('aria-disabled')).toBe('false'); expect(optionElements[1].getAttribute('aria-disabled')).toBe('true'); expect(optionElements[2].getAttribute('aria-disabled')).toBe('false'); @@ -202,18 +202,18 @@ describe('Listbox', () => { }); describe('roving focus mode', () => { - it('should have tabindex="-1" for the listbox when focusMode is "roving"', () => { - setupListbox({focusMode: 'roving'}); + it('should have tabindex="-1" for the listbox when focusMode is "roving"', async () => { + await setupListbox({focusMode: 'roving'}); expect(listboxElement.getAttribute('tabindex')).toBe('-1'); }); - it('should set tabindex="0" for the listbox when disabled and focusMode is "roving"', () => { - setupListbox({disabled: true, focusMode: 'roving'}); + it('should set tabindex="0" for the listbox when disabled and focusMode is "roving"', async () => { + await setupListbox({disabled: true, focusMode: 'roving'}); expect(listboxElement.getAttribute('tabindex')).toBe('0'); }); - it('should set initial focus (tabindex="0") on the first non-disabled option if no values are set', () => { - setupListbox({focusMode: 'roving'}); + it('should set initial focus (tabindex="0") on the first non-disabled option if no values are set', async () => { + await setupListbox({focusMode: 'roving'}); expect(optionElements[0].getAttribute('tabindex')).toBe('0'); expect(optionElements[1].getAttribute('tabindex')).toBe('-1'); expect(optionElements[2].getAttribute('tabindex')).toBe('-1'); @@ -221,8 +221,8 @@ describe('Listbox', () => { expect(optionElements[4].getAttribute('tabindex')).toBe('-1'); }); - it('should set initial focus (tabindex="0") on the first selected option', () => { - setupListbox({focusMode: 'roving', value: [2]}); + it('should set initial focus (tabindex="0") on the first selected option', async () => { + await setupListbox({focusMode: 'roving', value: [2]}); expect(optionElements[0].getAttribute('tabindex')).toBe('-1'); expect(optionElements[1].getAttribute('tabindex')).toBe('-1'); expect(optionElements[2].getAttribute('tabindex')).toBe('0'); @@ -230,8 +230,8 @@ describe('Listbox', () => { expect(optionElements[4].getAttribute('tabindex')).toBe('-1'); }); - it('should set initial focus (tabindex="0") on the first non-disabled option if selected option is disabled when softDisabled is false', () => { - setupListbox({ + it('should set initial focus (tabindex="0") on the first non-disabled option if selected option is disabled when softDisabled is false', async () => { + await setupListbox({ focusMode: 'roving', value: [1], disabledOptions: [0], @@ -241,8 +241,8 @@ describe('Listbox', () => { expect(optionElements[1].getAttribute('tabindex')).toBe('0'); }); - it('should set initial focus (tabindex="0") on the first option if selected option is disabled', () => { - setupListbox({ + it('should set initial focus (tabindex="0") on the first option if selected option is disabled', async () => { + await setupListbox({ focusMode: 'roving', value: [0], disabledOptions: [0], @@ -251,35 +251,35 @@ describe('Listbox', () => { expect(optionElements[1].getAttribute('tabindex')).toBe('-1'); }); - it('should not have aria-activedescendant when focusMode is "roving"', () => { - setupListbox({focusMode: 'roving'}); + it('should not have aria-activedescendant when focusMode is "roving"', async () => { + await setupListbox({focusMode: 'roving'}); expect(listboxElement.hasAttribute('aria-activedescendant')).toBe(false); }); }); describe('activedescendant focus mode', () => { - it('should have tabindex="0" for the listbox', () => { - setupListbox({focusMode: 'activedescendant'}); + it('should have tabindex="0" for the listbox', async () => { + await setupListbox({focusMode: 'activedescendant'}); expect(listboxElement.getAttribute('tabindex')).toBe('0'); }); - it('should set aria-activedescendant to the ID of the first non-disabled option if no value is set', () => { - setupListbox({focusMode: 'activedescendant'}); + it('should set aria-activedescendant to the ID of the first non-disabled option if no value is set', async () => { + await setupListbox({focusMode: 'activedescendant'}); expect(listboxElement.getAttribute('aria-activedescendant')).toBe(optionElements[0].id); }); - it('should set aria-activedescendant to the ID of the first selected option', () => { - setupListbox({focusMode: 'activedescendant', value: [2]}); + it('should set aria-activedescendant to the ID of the first selected option', async () => { + await setupListbox({focusMode: 'activedescendant', value: [2]}); expect(listboxElement.getAttribute('aria-activedescendant')).toBe(optionElements[2].id); }); - it('should set aria-activedescendant to the ID of the first non-disabled option if selected option is disabled', () => { - setupListbox({focusMode: 'activedescendant', value: [0], disabledOptions: [0]}); + it('should set aria-activedescendant to the ID of the first non-disabled option if selected option is disabled', async () => { + await setupListbox({focusMode: 'activedescendant', value: [0], disabledOptions: [0]}); expect(listboxElement.getAttribute('aria-activedescendant')).toBe(optionElements[0].id); }); - it('should set aria-activedescendant to the ID of the first non-disabled option if selected option is disabled when softDisabled is false', () => { - setupListbox({ + it('should set aria-activedescendant to the ID of the first non-disabled option if selected option is disabled when softDisabled is false', async () => { + await setupListbox({ focusMode: 'activedescendant', value: [1], disabledOptions: [0], @@ -288,8 +288,8 @@ describe('Listbox', () => { expect(listboxElement.getAttribute('aria-activedescendant')).toBe(optionElements[1].id); }); - it('should set tabindex="-1" for all options', () => { - setupListbox({focusMode: 'activedescendant'}); + it('should set tabindex="-1" for all options', async () => { + await setupListbox({focusMode: 'activedescendant'}); expect(optionElements[0].getAttribute('tabindex')).toBe('-1'); expect(optionElements[1].getAttribute('tabindex')).toBe('-1'); expect(optionElements[2].getAttribute('tabindex')).toBe('-1'); @@ -300,43 +300,43 @@ describe('Listbox', () => { }); describe('value and selection', () => { - it('should select the options corresponding to the value input', () => { - setupListbox({multi: true, value: [1, 3]}); + it('should select the options corresponding to the value input', async () => { + await setupListbox({multi: true, value: [1, 3]}); expect(optionElements[1].getAttribute('aria-selected')).toBe('true'); expect(optionElements[3].getAttribute('aria-selected')).toBe('true'); expect(listboxInstance.value()).toEqual([1, 3]); }); - it('should update the value model when an option is selected via UI (single select)', () => { - setupListbox({multi: false}); - click(1); + it('should update the value model when an option is selected via UI (single select)', async () => { + await setupListbox({multi: false}); + await click(1); expect(listboxInstance.value()).toEqual([1]); - click(2); + await click(2); expect(listboxInstance.value()).toEqual([2]); }); - it('should update the value model when options are selected via UI (multi select)', () => { - setupListbox({multi: true}); - click(1); + it('should update the value model when options are selected via UI (multi select)', async () => { + await setupListbox({multi: true}); + await click(1); expect(listboxInstance.value()).toEqual([1]); - click(3); + await click(3); expect(listboxInstance.value()).toEqual([1, 3]); - click(1); + await click(1); expect(listboxInstance.value()).toEqual([3]); }); describe('pointer interactions', () => { describe('single select', () => { - it('should select an option on click', () => { - setupListbox({multi: false}); - click(1); + it('should select an option on click', async () => { + await setupListbox({multi: false}); + await click(1); expect(listboxInstance.value()).toEqual([1]); expect(optionElements[1].getAttribute('aria-selected')).toBe('true'); }); - it('should select a new option and deselect the old one on click', () => { - setupListbox({multi: false, value: [0]}); - click(1); + it('should select a new option and deselect the old one on click', async () => { + await setupListbox({multi: false, value: [0]}); + await click(1); expect(listboxInstance.value()).toEqual([1]); expect(optionElements[0].getAttribute('aria-selected')).toBe('false'); expect(optionElements[1].getAttribute('aria-selected')).toBe('true'); @@ -345,79 +345,79 @@ describe('Listbox', () => { describe('multi select', () => { describe('selection follows focus', () => { - it('should select only the clicked option with a simple click', () => { - setupListbox({multi: true, selectionMode: 'follow', value: [0]}); - click(1); + it('should select only the clicked option with a simple click', async () => { + await setupListbox({multi: true, selectionMode: 'follow', value: [0]}); + await click(1); expect(listboxInstance.value()).toEqual([1]); expect(optionElements[0].getAttribute('aria-selected')).toBe('false'); expect(optionElements[1].getAttribute('aria-selected')).toBe('true'); }); - it('should toggle the selected state of an option with ctrl + click', () => { - setupListbox({multi: true, selectionMode: 'follow', value: [0]}); - click(1, {ctrlKey: true}); + it('should toggle the selected state of an option with ctrl + click', async () => { + await setupListbox({multi: true, selectionMode: 'follow', value: [0]}); + await click(1, {ctrlKey: true}); expect(listboxInstance.value().sort()).toEqual([0, 1]); expect(optionElements[0].getAttribute('aria-selected')).toBe('true'); expect(optionElements[1].getAttribute('aria-selected')).toBe('true'); - click(0, {ctrlKey: true}); + await click(0, {ctrlKey: true}); expect(listboxInstance.value()).toEqual([1]); expect(optionElements[0].getAttribute('aria-selected')).toBe('false'); expect(optionElements[1].getAttribute('aria-selected')).toBe('true'); }); - it('should select a range starting from the first option on shift + click', () => { - setupListbox({multi: true, selectionMode: 'follow', value: [0]}); - click(2, {shiftKey: true}); + it('should select a range starting from the first option on shift + click', async () => { + await setupListbox({multi: true, selectionMode: 'follow', value: [0]}); + await click(2, {shiftKey: true}); expect(listboxInstance.value().sort()).toEqual([0, 1, 2]); expect(optionElements[0].getAttribute('aria-selected')).toBe('true'); expect(optionElements[1].getAttribute('aria-selected')).toBe('true'); expect(optionElements[2].getAttribute('aria-selected')).toBe('true'); }); - it('should select a range starting from the current active option on shift + click', () => { - setupListbox({multi: true, selectionMode: 'follow'}); - click(1); - click(3, {shiftKey: true}); + it('should select a range starting from the current active option on shift + click', async () => { + await setupListbox({multi: true, selectionMode: 'follow'}); + await click(1); + await click(3, {shiftKey: true}); expect(listboxInstance.value().sort()).toEqual([1, 2, 3]); }); - it('should not select disabled options on shift + click', () => { - setupListbox({multi: true, selectionMode: 'follow', disabledOptions: [1]}); - click(2, {shiftKey: true}); + it('should not select disabled options on shift + click', async () => { + await setupListbox({multi: true, selectionMode: 'follow', disabledOptions: [1]}); + await click(2, {shiftKey: true}); expect(listboxInstance.value()).toEqual([0, 2]); }); }); describe('explicit selection', () => { - it('should toggle selection of the clicked option with a simple click', () => { - setupListbox({multi: true, selectionMode: 'explicit', value: [0]}); - click(1); + it('should toggle selection of the clicked option with a simple click', async () => { + await setupListbox({multi: true, selectionMode: 'explicit', value: [0]}); + await click(1); expect(listboxInstance.value().sort()).toEqual([0, 1]); expect(optionElements[0].getAttribute('aria-selected')).toBe('true'); expect(optionElements[1].getAttribute('aria-selected')).toBe('true'); - click(0); + await click(0); expect(listboxInstance.value()).toEqual([1]); expect(optionElements[0].getAttribute('aria-selected')).toBe('false'); }); - it('should select a range starting from the first option on shift + click', () => { - setupListbox({multi: true, selectionMode: 'explicit', value: [0]}); - click(2, {shiftKey: true}); + it('should select a range starting from the first option on shift + click', async () => { + await setupListbox({multi: true, selectionMode: 'explicit', value: [0]}); + await click(2, {shiftKey: true}); expect(listboxInstance.value().sort()).toEqual([0, 1, 2]); }); - it('should select a range starting from the current active option on shift + click', () => { - setupListbox({multi: true, selectionMode: 'explicit'}); - click(1); - click(3, {shiftKey: true}); + it('should select a range starting from the current active option on shift + click', async () => { + await setupListbox({multi: true, selectionMode: 'explicit'}); + await click(1); + await click(3, {shiftKey: true}); expect(listboxInstance.value().sort()).toEqual([1, 2, 3]); }); - it('should not select disabled options on shift + click', () => { - setupListbox({multi: true, selectionMode: 'follow', disabledOptions: [1]}); - click(2, {shiftKey: true}); + it('should not select disabled options on shift + click', async () => { + await setupListbox({multi: true, selectionMode: 'follow', disabledOptions: [1]}); + await click(2, {shiftKey: true}); expect(listboxInstance.value()).toEqual([0, 2]); }); }); @@ -426,7 +426,7 @@ describe('Listbox', () => { describe('with shuffled items', () => { it('should update collection order when items are shuffled', async () => { - setupListbox({ + await setupListbox({ options: [ {value: 1, label: 'Item 1', disabled: false}, {value: 2, label: 'Item 2', disabled: false}, @@ -442,7 +442,7 @@ describe('Listbox', () => { const testComponent = fixture.componentInstance as ListboxExample; const items = testComponent.options().reverse(); testComponent.options.set([...items]); - fixture.detectChanges(); + await fixture.whenStable(); await waitForMicrotasks(); // Re-query elements to check new DOM order @@ -461,9 +461,9 @@ describe('Listbox', () => { consoleSpy = spyOn(console, 'warn'); }); - afterEach(() => { + afterEach(async () => { TestBed.resetTestingModule(); - setupListbox(); + await setupListbox(); }); it('should warn when duplicate option values are detected inside ngListbox', () => { @@ -509,65 +509,65 @@ describe('Listbox', () => { describe('keyboard interactions', () => { describe('single select', () => { describe('selection follows focus', () => { - it('should select the next option on ArrowDown', () => { - setupListbox({multi: false, selectionMode: 'follow'}); - down(); + it('should select the next option on ArrowDown', async () => { + await setupListbox({multi: false, selectionMode: 'follow'}); + await down(); expect(listboxInstance.value()).toEqual([1]); expect(optionElements[1].getAttribute('aria-selected')).toBe('true'); - down(); + await down(); expect(listboxInstance.value()).toEqual([2]); expect(optionElements[2].getAttribute('aria-selected')).toBe('true'); }); - it('should select the previous option on ArrowUp', () => { - setupListbox({multi: false, selectionMode: 'follow', value: [2]}); - up(); + it('should select the previous option on ArrowUp', async () => { + await setupListbox({multi: false, selectionMode: 'follow', value: [2]}); + await up(); expect(listboxInstance.value()).toEqual([1]); expect(optionElements[1].getAttribute('aria-selected')).toBe('true'); }); - it('should select the first option on Home', () => { - setupListbox({multi: false, selectionMode: 'follow', value: [2]}); - home(); + it('should select the first option on Home', async () => { + await setupListbox({multi: false, selectionMode: 'follow', value: [2]}); + await home(); expect(listboxInstance.value()).toEqual([0]); }); - it('should select the last option on End', () => { - setupListbox({multi: false, selectionMode: 'follow', value: [2]}); - end(); + it('should select the last option on End', async () => { + await setupListbox({multi: false, selectionMode: 'follow', value: [2]}); + await end(); expect(listboxInstance.value()).toEqual([4]); }); }); describe('explicit selection', () => { - it('should move focus but not select on navigation', () => { - setupListbox({multi: false, selectionMode: 'explicit'}); - down(); - up(); - home(); - end(); + it('should move focus but not select on navigation', async () => { + await setupListbox({multi: false, selectionMode: 'explicit'}); + await down(); + await up(); + await home(); + await end(); expect(listboxInstance.value()).toEqual([]); expect(optionElements[1].getAttribute('aria-selected')).toBe('false'); }); - it('should select the focused option on Space', () => { - setupListbox({multi: false, selectionMode: 'explicit'}); - down(); - space(); + it('should select the focused option on Space', async () => { + await setupListbox({multi: false, selectionMode: 'explicit'}); + await down(); + await space(); expect(listboxInstance.value()).toEqual([1]); expect(optionElements[1].getAttribute('aria-selected')).toBe('true'); - down(); - down(); - space(); + await down(); + await down(); + await space(); expect(listboxInstance.value()).toEqual([3]); expect(optionElements[1].getAttribute('aria-selected')).toBe('false'); expect(optionElements[3].getAttribute('aria-selected')).toBe('true'); }); - it('should select the focused option on Enter', () => { - setupListbox({multi: false, selectionMode: 'explicit'}); - down(); - enter(); + it('should select the focused option on Enter', async () => { + await setupListbox({multi: false, selectionMode: 'explicit'}); + await down(); + await enter(); expect(listboxInstance.value()).toEqual([1]); expect(optionElements[1].getAttribute('aria-selected')).toBe('true'); }); @@ -576,101 +576,101 @@ describe('Listbox', () => { describe('multi select', () => { describe('selection follows focus', () => { - it('should select only the focused option on ArrowDown (no modifier)', () => { - setupListbox({multi: true, selectionMode: 'follow', value: [0]}); - down(); + it('should select only the focused option on ArrowDown (no modifier)', async () => { + await setupListbox({multi: true, selectionMode: 'follow', value: [0]}); + await down(); expect(listboxInstance.value()).toEqual([1]); expect(optionElements[0].getAttribute('aria-selected')).toBe('false'); expect(optionElements[1].getAttribute('aria-selected')).toBe('true'); }); - it('should move focus but not change selection on ctrl + ArrowDown, then toggle with ctrl + Space', () => { - setupListbox({multi: true, selectionMode: 'follow', value: [0]}); - down({ctrlKey: true}); + it('should move focus but not change selection on ctrl + ArrowDown, then toggle with ctrl + Space', async () => { + await setupListbox({multi: true, selectionMode: 'follow', value: [0]}); + await down({ctrlKey: true}); expect(listboxInstance.value()).toEqual([0]); - space({ctrlKey: true}); + await space({ctrlKey: true}); expect(listboxInstance.value().sort()).toEqual([0, 1]); }); - it('should toggle selection of the focused item on ctrl + Space', () => { - setupListbox({multi: true, selectionMode: 'follow', value: [0]}); - space({ctrlKey: true}); + it('should toggle selection of the focused item on ctrl + Space', async () => { + await setupListbox({multi: true, selectionMode: 'follow', value: [0]}); + await space({ctrlKey: true}); expect(listboxInstance.value()).toEqual([]); - down(); + await down(); expect(listboxInstance.value()).toEqual([1]); - space({ctrlKey: true}); + await space({ctrlKey: true}); expect(listboxInstance.value()).toEqual([]); }); - it('should extend selection on shift + ArrowDown', () => { - setupListbox({multi: true, selectionMode: 'follow', value: [0]}); - down({shiftKey: true}); + it('should extend selection on shift + ArrowDown', async () => { + await setupListbox({multi: true, selectionMode: 'follow', value: [0]}); + await down({shiftKey: true}); expect(listboxInstance.value().sort()).toEqual([0, 1]); - down({shiftKey: true}); + await down({shiftKey: true}); expect(listboxInstance.value().sort()).toEqual([0, 1, 2]); }); - it('should select all on Ctrl+A, then select active on second Ctrl+A', () => { - setupListbox({multi: true, selectionMode: 'follow', value: [0]}); - keydown('A', {ctrlKey: true}); + it('should select all on Ctrl+A, then select active on second Ctrl+A', async () => { + await setupListbox({multi: true, selectionMode: 'follow', value: [0]}); + await keydown('A', {ctrlKey: true}); expect(listboxInstance.value().sort()).toEqual([0, 1, 2, 3, 4]); - keydown('A', {ctrlKey: true}); + await keydown('A', {ctrlKey: true}); expect(listboxInstance.value()).toEqual([0]); }); }); describe('explicit selection', () => { - it('should move focus but not select on ArrowDown', () => { - setupListbox({multi: true, selectionMode: 'explicit'}); - down(); + it('should move focus but not select on ArrowDown', async () => { + await setupListbox({multi: true, selectionMode: 'explicit'}); + await down(); expect(listboxInstance.value()).toEqual([]); }); - it('should toggle selection of the focused item on Space', () => { - setupListbox({multi: true, selectionMode: 'explicit'}); - down(); - space(); + it('should toggle selection of the focused item on Space', async () => { + await setupListbox({multi: true, selectionMode: 'explicit'}); + await down(); + await space(); expect(listboxInstance.value()).toEqual([1]); - down(); - space(); + await down(); + await space(); expect(listboxInstance.value().sort()).toEqual([1, 2]); - space(); + await space(); expect(listboxInstance.value()).toEqual([1]); }); - it('should toggle selection of the focused item on Enter', () => { - setupListbox({multi: true, selectionMode: 'explicit'}); - down(); - enter(); + it('should toggle selection of the focused item on Enter', async () => { + await setupListbox({multi: true, selectionMode: 'explicit'}); + await down(); + await enter(); expect(listboxInstance.value()).toEqual([1]); }); - it('should extend selection on Shift+ArrowDown', () => { - setupListbox({multi: true, selectionMode: 'explicit'}); - down({shiftKey: true}); + it('should extend selection on Shift+ArrowDown', async () => { + await setupListbox({multi: true, selectionMode: 'explicit'}); + await down({shiftKey: true}); expect(listboxInstance.value().sort()).toEqual([0, 1]); - down({shiftKey: true}); + await down({shiftKey: true}); expect(listboxInstance.value().sort()).toEqual([0, 1, 2]); }); - it('should move selection anchor along with focus during normal non-shift navigation', () => { - setupListbox({multi: true, selectionMode: 'explicit'}); - down({shiftKey: true}); + it('should move selection anchor along with focus during normal non-shift navigation', async () => { + await setupListbox({multi: true, selectionMode: 'explicit'}); + await down({shiftKey: true}); expect(listboxInstance.value().sort()).toEqual([0, 1]); - down(); - down(); - down(); - up({shiftKey: true}); + await down(); + await down(); + await down(); + await up({shiftKey: true}); expect(listboxInstance.value().sort()).toEqual([0, 1, 3, 4]); }); - it('should toggle selection of all options on Ctrl+A', () => { - setupListbox({multi: true, selectionMode: 'explicit', value: [0]}); - keydown('A', {ctrlKey: true}); + it('should toggle selection of all options on Ctrl+A', async () => { + await setupListbox({multi: true, selectionMode: 'explicit', value: [0]}); + await keydown('A', {ctrlKey: true}); expect(listboxInstance.value().sort()).toEqual([0, 1, 2, 3, 4]); - keydown('A', {ctrlKey: true}); + await keydown('A', {ctrlKey: true}); expect(listboxInstance.value()).toEqual([]); }); }); @@ -683,92 +683,92 @@ describe('Listbox', () => { isFocused: (index: number) => boolean, ) { describe(`keyboard navigation (focusMode="${focusMode}")`, () => { - it('should move focus to the last focusable option on End', () => { - setupListbox({focusMode, disabledOptions: [4]}); - end(); + it('should move focus to the last focusable option on End', async () => { + await setupListbox({focusMode, disabledOptions: [4]}); + await end(); expect(isFocused(4)).toBe(true); }); - it('should move focus to the first focusable option on Home', () => { - setupListbox({focusMode, disabledOptions: [0]}); - end(); - home(); + it('should move focus to the first focusable option on Home', async () => { + await setupListbox({focusMode, disabledOptions: [0]}); + await end(); + await home(); expect(isFocused(0)).toBe(true); }); - it('should allow keyboard navigation if the group is readonly', () => { - setupListbox({focusMode, orientation: 'horizontal', readonly: true}); - right(); + it('should allow keyboard navigation if the group is readonly', async () => { + await setupListbox({focusMode, orientation: 'horizontal', readonly: true}); + await right(); expect(isFocused(1)).toBe(true); }); - it('should wrap focus from last to first with ArrowDown when wrap is true (vertical)', () => { - setupListbox({focusMode, orientation: 'vertical', wrap: true}); - for (let i = 0; i < optionElements.length - 1; i++) down(); - down(); + it('should wrap focus from last to first with ArrowDown when wrap is true (vertical)', async () => { + await setupListbox({focusMode, orientation: 'vertical', wrap: true}); + for (let i = 0; i < optionElements.length - 1; i++) await down(); + await down(); expect(isFocused(0)).toBe(true); }); - it('should not wrap focus from last to first with ArrowDown when wrap is false (vertical)', () => { - setupListbox({focusMode, orientation: 'vertical', wrap: false}); - for (let i = 0; i < optionElements.length - 1; i++) down(); - down(); + it('should not wrap focus from last to first with ArrowDown when wrap is false (vertical)', async () => { + await setupListbox({focusMode, orientation: 'vertical', wrap: false}); + for (let i = 0; i < optionElements.length - 1; i++) await down(); + await down(); expect(isFocused(optionElements.length - 1)).toBe(true); }); describe('vertical orientation', () => { - it('should move focus to the next option on ArrowDown', () => { - setupListbox({focusMode, orientation: 'vertical'}); - down(); + it('should move focus to the next option on ArrowDown', async () => { + await setupListbox({focusMode, orientation: 'vertical'}); + await down(); expect(isFocused(1)).toBe(true); }); - it('should skip disabled options with ArrowDown (softDisabled="false")', () => { - setupListbox({ + it('should skip disabled options with ArrowDown (softDisabled="false")', async () => { + await setupListbox({ focusMode, orientation: 'vertical', softDisabled: false, disabledOptions: [1, 2], }); - down(); + await down(); expect(isFocused(3)).toBe(true); }); - it('should not skip disabled options with ArrowDown (softDisabled="true")', () => { - setupListbox({ + it('should not skip disabled options with ArrowDown (softDisabled="true")', async () => { + await setupListbox({ focusMode, orientation: 'vertical', softDisabled: true, disabledOptions: [1, 2], }); - down(); + await down(); expect(isFocused(1)).toBe(true); }); - it('should not be focusable ArrowDown when completely disabled', () => { - setupListbox({ + it('should not be focusable ArrowDown when completely disabled', async () => { + await setupListbox({ focusMode, orientation: 'vertical', softDisabled: true, disabled: true, }); - down(); + await down(); expect(isFocused(0)).toBe(false); }); }); describe('horizontal orientation', () => { - it('should move focus to the next option on ArrowRight', () => { - setupListbox({focusMode, orientation: 'horizontal'}); - right(); + it('should move focus to the next option on ArrowRight', async () => { + await setupListbox({focusMode, orientation: 'horizontal'}); + await right(); expect(isFocused(1)).toBe(true); }); describe('text direction rtl', () => { - it('should move focus to the next option on ArrowLeft (rtl)', () => { - setupListbox({focusMode, textDirection: 'rtl', orientation: 'horizontal'}); - left(); + it('should move focus to the next option on ArrowLeft (rtl)', async () => { + await setupListbox({focusMode, textDirection: 'rtl', orientation: 'horizontal'}); + await left(); expect(isFocused(1)).toBe(true); }); }); @@ -776,21 +776,21 @@ describe('Listbox', () => { }); describe(`pointer navigation (focusMode="${focusMode}")`, () => { - it('should move focus to the clicked option', () => { - setupListbox({focusMode}); - click(3); + it('should move focus to the clicked option', async () => { + await setupListbox({focusMode}); + await click(3); expect(isFocused(3)).toBe(true); }); - it('should move focus to the clicked disabled option', () => { - setupListbox({focusMode, disabledOptions: [2], softDisabled: true}); - click(2); + it('should move focus to the clicked disabled option', async () => { + await setupListbox({focusMode, disabledOptions: [2], softDisabled: true}); + await click(2); expect(isFocused(2)).toBe(true); }); - it('should move focus if listbox is readonly', () => { - setupListbox({focusMode, readonly: true}); - click(3); + it('should move focus if listbox is readonly', async () => { + await setupListbox({focusMode, readonly: true}); + await click(3); expect(isFocused(3)).toBe(true); }); }); @@ -804,50 +804,60 @@ describe('Listbox', () => { {value: 4, label: 'Orange', disabled: false}, ]; - it('should focus the first matching option when typing characters', () => { - setupListbox({options: getOptions(), focusMode}); - type('B'); + it('should focus the first matching option when typing characters', async () => { + await setupListbox({options: getOptions(), focusMode}); + await type('B'); expect(isFocused(2)).toBe(true); - type('l'); + await type('l'); expect(isFocused(3)).toBe(true); }); - it('should select the focused option if selectionMode is "follow"', () => { - setupListbox({options: getOptions(), focusMode, selectionMode: 'follow'}); - type('O'); + it('should select the focused option if selectionMode is "follow"', async () => { + await setupListbox({options: getOptions(), focusMode, selectionMode: 'follow'}); + await type('O'); expect(isFocused(4)).toBe(true); expect(listboxInstance.value()).toEqual([4]); expect(optionElements[4].getAttribute('aria-selected')).toBe('true'); }); - it('should not select the focused option if selectionMode is "explicit"', () => { - setupListbox({options: getOptions(), focusMode, selectionMode: 'explicit'}); - type('O'); + it('should not select the focused option if selectionMode is "explicit"', async () => { + await setupListbox({options: getOptions(), focusMode, selectionMode: 'explicit'}); + await type('O'); expect(isFocused(4)).toBe(true); expect(listboxInstance.value()).toEqual([]); expect(optionElements[4].getAttribute('aria-selected')).toBe('false'); }); it('should reset search term after typeaheadDelay', async () => { - setupListbox({options: getOptions(), focusMode, typeaheadDelay: 100}); + await setupListbox({options: getOptions(), focusMode, typeaheadDelay: 100}); - type('A'); + await type('A'); expect(isFocused(1)).toBe(true); await new Promise(resolve => setTimeout(resolve, 100)); - type('A'); + await type('A'); expect(isFocused(0)).toBe(true); }); - it('should skip disabled options with typeahead (softDisabled=false)', () => { - setupListbox({options: getOptions(), focusMode, disabledOptions: [2], softDisabled: false}); - type('B'); + it('should skip disabled options with typeahead (softDisabled=false)', async () => { + await setupListbox({ + options: getOptions(), + focusMode, + disabledOptions: [2], + softDisabled: false, + }); + await type('B'); expect(isFocused(3)).toBe(true); }); - it('should focus disabled options with typeahead if softDisabled=true', () => { - setupListbox({options: getOptions(), focusMode, disabledOptions: [2], softDisabled: true}); - type('B'); + it('should focus disabled options with typeahead if softDisabled=true', async () => { + await setupListbox({ + options: getOptions(), + focusMode, + disabledOptions: [2], + softDisabled: true, + }); + await type('B'); expect(isFocused(2)).toBe(true); }); }); @@ -866,25 +876,25 @@ describe('Listbox', () => { }); describe('failure cases', () => { - it('should handle an empty set of options gracefully', () => { - setupListbox({options: []}); + it('should handle an empty set of options gracefully', async () => { + await setupListbox({options: []}); expect(optionElements.length).toBe(0); - expect(() => down()).not.toThrow(); - expect(() => space()).not.toThrow(); + await down(); + await space(); expect(listboxInstance.value()).toEqual([]); }); }); describe('item mutations and focus stability', () => { it('should recover focus by shifting to the default state if the active option is removed', async () => { - setupListbox({focusMode: 'activedescendant'}); - click(2); + await setupListbox({focusMode: 'activedescendant'}); + await click(2); expect(listboxElement.getAttribute('aria-activedescendant')).toBe(optionElements[2].id); const testComponent = fixture.componentInstance as ListboxExample; const updatedOptions = testComponent.options().filter(o => o.value !== 2); testComponent.options.set(updatedOptions); - fixture.detectChanges(); + await fixture.whenStable(); await waitForMicrotasks(); defineTestVariables(fixture); diff --git a/src/aria/menu/menu.spec.ts b/src/aria/menu/menu.spec.ts index 9ba7d23d562c..c95273b1ce00 100644 --- a/src/aria/menu/menu.spec.ts +++ b/src/aria/menu/menu.spec.ts @@ -22,7 +22,7 @@ import {waitForMicrotasks} from '../private/testing/test-helpers'; describe('Standalone Menu Pattern', () => { let fixture: ComponentFixture; - const keydown = (element: Element, key: string, modifierKeys: {} = {}) => { + const keydown = async (element: Element, key: string, modifierKeys: {} = {}) => { element.dispatchEvent( new KeyboardEvent('keydown', { key, @@ -30,36 +30,36 @@ describe('Standalone Menu Pattern', () => { ...modifierKeys, }), ); - fixture.detectChanges(); + await fixture.whenStable(); }; const mouseover = async (element: Element) => { element.dispatchEvent(new MouseEvent('mouseover', {bubbles: true})); await new Promise(resolve => setTimeout(resolve, 0)); - fixture.detectChanges(); + await fixture.whenStable(); }; - const mouseout = (element: Element) => { + const mouseout = async (element: Element) => { element.dispatchEvent(new MouseEvent('mouseout', {bubbles: true})); - fixture.detectChanges(); + await fixture.whenStable(); }; - const click = (element: Element, eventInit?: PointerEventInit) => { + const click = async (element: Element, eventInit?: PointerEventInit) => { element.dispatchEvent(new PointerEvent('click', {bubbles: true, ...eventInit})); - fixture.detectChanges(); + await fixture.whenStable(); }; - const focusout = (element: Element, relatedTarget?: EventTarget) => { + const focusout = async (element: Element, relatedTarget?: EventTarget) => { element.dispatchEvent(new FocusEvent('focusout', {bubbles: true, relatedTarget})); - fixture.detectChanges(); + await fixture.whenStable(); }; - function setupMenu(opts?: {textDirection: 'ltr' | 'rtl'}) { + async function setupMenu(opts?: {textDirection: 'ltr' | 'rtl'}) { TestBed.configureTestingModule({ providers: [provideFakeDirectionality(opts?.textDirection ?? 'ltr')], }); fixture = TestBed.createComponent(StandaloneMenuExample); - fixture.detectChanges(); + await fixture.whenStable(); getItem('Apple')?.focus(); } @@ -102,141 +102,141 @@ describe('Standalone Menu Pattern', () => { }); describe('Navigation', () => { - beforeEach(() => setupMenu()); + beforeEach(async () => await setupMenu()); it('should focus the first item by default', () => { expect(getItem('Apple')?.tabIndex).toBe(0); }); - it('should move the active item with the up and down arrows', () => { + it('should move the active item with the up and down arrows', async () => { const apple = getItem('Apple'); const banana = getItem('Banana'); - keydown(apple!, 'ArrowDown'); + await keydown(apple!, 'ArrowDown'); expect(document.activeElement).toBe(banana); - keydown(banana!, 'ArrowUp'); + await keydown(banana!, 'ArrowUp'); expect(document.activeElement).toBe(apple); }); - it('should wrap navigation when reaching the top or bottom', () => { + it('should wrap navigation when reaching the top or bottom', async () => { const apple = getItem('Apple'); const cherry = getItem('Cherry'); - keydown(apple!, 'ArrowUp'); + await keydown(apple!, 'ArrowUp'); expect(document.activeElement).toBe(cherry); - keydown(cherry!, 'ArrowDown'); + await keydown(cherry!, 'ArrowDown'); expect(document.activeElement).toBe(apple); }); - it('should move focus to the first and last items with home and end', () => { + it('should move focus to the first and last items with home and end', async () => { const apple = getItem('Apple'); const cherry = getItem('Cherry'); - keydown(apple!, 'End'); + await keydown(apple!, 'End'); expect(document.activeElement).toBe(cherry); - keydown(cherry!, 'Home'); + await keydown(cherry!, 'Home'); expect(document.activeElement).toBe(apple); }); - it('should move focus on mouse over a menu item', () => { + it('should move focus on mouse over a menu item', async () => { const banana = getItem('Banana'); - mouseover(banana!); + await mouseover(banana!); expect(document.activeElement).toBe(banana); }); describe('Typeahead', () => { - it('should move the active item to the next item that starts with the typed character', () => { + it('should move the active item to the next item that starts with the typed character', async () => { const apple = getItem('Apple'); const banana = getItem('Banana'); - keydown(apple!, 'b'); + await keydown(apple!, 'b'); expect(document.activeElement).toBe(banana); }); - it('should support multi-character typeahead', () => { + it('should support multi-character typeahead', async () => { const apple = getItem('Apple'); const banana = getItem('Banana'); const berries = getItem('Berries'); - keydown(apple!, 'b'); + await keydown(apple!, 'b'); expect(document.activeElement).toBe(banana); - keydown(document.activeElement!, 'e'); + await keydown(document.activeElement!, 'e'); expect(document.activeElement).toBe(berries); }); - it('should wrap when reaching the end of the list during typeahead', () => { + it('should wrap when reaching the end of the list during typeahead', async () => { const apple = getItem('Apple'); const cherry = getItem('Cherry'); // Start at cherry by pressing End - keydown(apple!, 'End'); + await keydown(apple!, 'End'); expect(document.activeElement).toBe(cherry); // Type 'a', which should wrap to 'Apple' - keydown(document.activeElement!, 'a'); + await keydown(document.activeElement!, 'a'); expect(document.activeElement).toBe(apple); }); - it('should not move the active item if no item matches the typed character', () => { + it('should not move the active item if no item matches the typed character', async () => { const apple = getItem('Apple'); - keydown(apple!, 'z'); + await keydown(apple!, 'z'); expect(document.activeElement).toBe(apple); }); }); }); describe('Selection', () => { - beforeEach(() => setupMenu()); + beforeEach(async () => await setupMenu()); - it('should select an item on click', () => { + it('should select an item on click', async () => { const banana = getItem('Banana'); spyOn(fixture.componentInstance, 'itemSelected'); - click(banana!); + await click(banana!); expect(fixture.componentInstance.itemSelected).toHaveBeenCalledWith('Banana'); }); - it('should select an item on enter', () => { + it('should select an item on enter', async () => { const banana = getItem('Banana'); spyOn(fixture.componentInstance, 'itemSelected'); - keydown(document.activeElement!, 'ArrowDown'); // Move focus to Banana + await keydown(document.activeElement!, 'ArrowDown'); // Move focus to Banana expect(document.activeElement).toBe(banana); - keydown(banana!, 'Enter'); + await keydown(banana!, 'Enter'); expect(fixture.componentInstance.itemSelected).toHaveBeenCalledWith('Banana'); }); - it('should select an item on space', () => { + it('should select an item on space', async () => { const banana = getItem('Banana'); spyOn(fixture.componentInstance, 'itemSelected'); - keydown(document.activeElement!, 'ArrowDown'); // Move focus to Banana + await keydown(document.activeElement!, 'ArrowDown'); // Move focus to Banana expect(document.activeElement).toBe(banana); - keydown(banana!, ' '); + await keydown(banana!, ' '); expect(fixture.componentInstance.itemSelected).toHaveBeenCalledWith('Banana'); }); - it('should not select a disabled item', () => { + it('should not select a disabled item', async () => { const cherry = getItem('Cherry'); spyOn(fixture.componentInstance, 'itemSelected'); - click(cherry!); + await click(cherry!); expect(fixture.componentInstance.itemSelected).not.toHaveBeenCalled(); - keydown(document.activeElement!, 'End'); + await keydown(document.activeElement!, 'End'); expect(document.activeElement).toBe(cherry); - keydown(cherry!, 'Enter'); + await keydown(cherry!, 'Enter'); expect(fixture.componentInstance.itemSelected).not.toHaveBeenCalled(); - keydown(cherry!, ' '); + await keydown(cherry!, ' '); expect(fixture.componentInstance.itemSelected).not.toHaveBeenCalled(); }); }); @@ -252,97 +252,97 @@ describe('Standalone Menu Pattern', () => { return berries?.getAttribute('aria-expanded') === 'true'; } - beforeEach(() => setupMenu()); + beforeEach(async () => await setupMenu()); it('should be expanded by default', () => { expect(getMenu()).not.toBeNull(); expect(isSubmenuExpanded()).toBe(false); }); - it('should expand submenu on click', () => { + it('should expand submenu on click', async () => { const berries = getItem('Berries'); - click(berries!); + await click(berries!); expect(isSubmenuExpanded()).toBe(true); }); - it('should open submenu on arrow right', () => { + it('should open submenu on arrow right', async () => { const apple = getItem('Apple'); const banana = getItem('Banana'); const berries = getItem('Berries'); - keydown(apple!, 'ArrowDown'); - keydown(banana!, 'ArrowDown'); - keydown(berries!, 'ArrowRight'); + await keydown(apple!, 'ArrowDown'); + await keydown(banana!, 'ArrowDown'); + await keydown(berries!, 'ArrowRight'); expect(isSubmenuExpanded()).toBe(true); expect(document.activeElement).toBe(getItem('Blueberry')); }); - it('should close submenu on arrow left', () => { + it('should close submenu on arrow left', async () => { const apple = getItem('Apple'); const banana = getItem('Banana'); const berries = getItem('Berries'); - keydown(apple!, 'ArrowDown'); - keydown(banana!, 'ArrowDown'); - keydown(berries!, 'ArrowRight'); + await keydown(apple!, 'ArrowDown'); + await keydown(banana!, 'ArrowDown'); + await keydown(berries!, 'ArrowRight'); const blueberry = getItem('Blueberry'); - keydown(blueberry!, 'ArrowLeft'); + await keydown(blueberry!, 'ArrowLeft'); expect(isSubmenuExpanded()).toBe(false); expect(document.activeElement).toBe(berries); }); - it('should close submenu on click outside', () => { + it('should close submenu on click outside', async () => { const berries = getItem('Berries'); - click(berries!); + await click(berries!); expect(isSubmenuExpanded()).toBe(true); - focusout(getItem('Blueberry')!, document.body); + await focusout(getItem('Blueberry')!, document.body); expect(isSubmenuExpanded()).toBe(false); }); - it('should expand submenu on enter', () => { + it('should expand submenu on enter', async () => { const apple = getItem('Apple'); const banana = getItem('Banana'); const berries = getItem('Berries'); - keydown(apple!, 'ArrowDown'); - keydown(banana!, 'ArrowDown'); - keydown(berries!, 'Enter'); + await keydown(apple!, 'ArrowDown'); + await keydown(banana!, 'ArrowDown'); + await keydown(berries!, 'Enter'); expect(isSubmenuExpanded()).toBe(true); expect(document.activeElement).toBe(getItem('Blueberry')); }); - it('should expand submenu on space', () => { + it('should expand submenu on space', async () => { const apple = getItem('Apple'); const banana = getItem('Banana'); const berries = getItem('Berries'); - keydown(apple!, 'ArrowDown'); - keydown(banana!, 'ArrowDown'); - keydown(berries!, ' '); + await keydown(apple!, 'ArrowDown'); + await keydown(banana!, 'ArrowDown'); + await keydown(berries!, ' '); expect(isSubmenuExpanded()).toBe(true); expect(document.activeElement).toBe(getItem('Blueberry')); }); - it('should close submenu on escape', () => { + it('should close submenu on escape', async () => { const apple = getItem('Apple'); const banana = getItem('Banana'); const berries = getItem('Berries'); - keydown(apple!, 'ArrowDown'); - keydown(banana!, 'ArrowDown'); - keydown(berries!, 'ArrowRight'); + await keydown(apple!, 'ArrowDown'); + await keydown(banana!, 'ArrowDown'); + await keydown(berries!, 'ArrowRight'); expect(isSubmenuExpanded()).toBe(true); const blueberry = getItem('Blueberry'); expect(document.activeElement).toBe(blueberry); - keydown(blueberry!, 'Escape'); + await keydown(blueberry!, 'Escape'); expect(isSubmenuExpanded()).toBe(false); expect(document.activeElement).toBe(berries); @@ -354,63 +354,63 @@ describe('Standalone Menu Pattern', () => { expect(isSubmenuExpanded()).toBe(true); }); - it('should close on selecting an item on click', () => { + it('should close on selecting an item on click', async () => { spyOn(fixture.componentInstance, 'itemSelected'); - click(getItem('Berries')!); // open submenu + await click(getItem('Berries')!); // open submenu expect(isSubmenuExpanded()).toBe(true); - click(getItem('Blueberry')!); + await click(getItem('Blueberry')!); expect(fixture.componentInstance.itemSelected).toHaveBeenCalledWith('Blueberry'); expect(isSubmenuExpanded()).toBe(false); }); - it('should close on selecting an item on enter', () => { + it('should close on selecting an item on enter', async () => { spyOn(fixture.componentInstance, 'itemSelected'); const apple = getItem('Apple'); const banana = getItem('Banana'); const berries = getItem('Berries'); - keydown(apple!, 'ArrowDown'); - keydown(banana!, 'ArrowDown'); - keydown(berries!, 'Enter'); // open submenu + await keydown(apple!, 'ArrowDown'); + await keydown(banana!, 'ArrowDown'); + await keydown(berries!, 'Enter'); // open submenu const blueberry = getItem('Blueberry'); expect(document.activeElement).toBe(blueberry); - keydown(blueberry!, 'Enter'); + await keydown(blueberry!, 'Enter'); expect(fixture.componentInstance.itemSelected).toHaveBeenCalledWith('Blueberry'); expect(isSubmenuExpanded()).toBe(false); }); - it('should close on selecting an item on space', () => { + it('should close on selecting an item on space', async () => { spyOn(fixture.componentInstance, 'itemSelected'); const apple = getItem('Apple'); const banana = getItem('Banana'); const berries = getItem('Berries'); - keydown(apple!, 'ArrowDown'); - keydown(banana!, 'ArrowDown'); - keydown(berries!, ' '); // open submenu + await keydown(apple!, 'ArrowDown'); + await keydown(banana!, 'ArrowDown'); + await keydown(berries!, ' '); // open submenu const blueberry = getItem('Blueberry'); expect(document.activeElement).toBe(blueberry); - keydown(blueberry!, ' '); + await keydown(blueberry!, ' '); expect(fixture.componentInstance.itemSelected).toHaveBeenCalledWith('Blueberry'); expect(isSubmenuExpanded()).toBe(false); }); - it('should close a submenu on focus out', () => { + it('should close a submenu on focus out', async () => { const apple = getItem('Apple'); const banana = getItem('Banana'); const berries = getItem('Berries'); - keydown(apple!, 'ArrowDown'); - keydown(banana!, 'ArrowDown'); - keydown(berries!, 'ArrowRight'); + await keydown(apple!, 'ArrowDown'); + await keydown(banana!, 'ArrowDown'); + await keydown(berries!, 'ArrowRight'); expect(isSubmenuExpanded()).toBe(true); const blueberry = getItem('Blueberry'); expect(document.activeElement).toBe(blueberry); @@ -418,7 +418,7 @@ describe('Standalone Menu Pattern', () => { const externalElement = document.createElement('button'); document.body.appendChild(externalElement); - focusout(blueberry!, externalElement); + await focusout(blueberry!, externalElement); expect(isSubmenuExpanded()).toBe(false); externalElement.remove(); @@ -431,8 +431,8 @@ describe('Standalone Menu Pattern', () => { await mouseover(berries!); expect(isSubmenuExpanded()).toBe(true); - mouseout(berries!); - mouseout(submenu!); + await mouseout(berries!); + await mouseout(submenu!); expect(isSubmenuExpanded()).toBe(false); }); @@ -444,8 +444,8 @@ describe('Standalone Menu Pattern', () => { await mouseover(berries!); expect(isSubmenuExpanded()).toBe(true); - mouseout(berries!); - mouseover(submenu!); + await mouseout(berries!); + await mouseover(submenu!); expect(isSubmenuExpanded()).toBe(true); }); }); @@ -456,31 +456,31 @@ describe('Standalone Menu Pattern', () => { return berries?.getAttribute('aria-expanded') === 'true'; } - beforeEach(() => setupMenu({textDirection: 'rtl'})); + beforeEach(async () => await setupMenu({textDirection: 'rtl'})); - it('should open submenu on arrow left', () => { + it('should open submenu on arrow left', async () => { const apple = getItem('Apple'); const banana = getItem('Banana'); const berries = getItem('Berries'); - keydown(apple!, 'ArrowDown'); - keydown(banana!, 'ArrowDown'); - keydown(berries!, 'ArrowLeft'); + await keydown(apple!, 'ArrowDown'); + await keydown(banana!, 'ArrowDown'); + await keydown(berries!, 'ArrowLeft'); expect(isSubmenuExpanded()).toBe(true); expect(document.activeElement).toBe(getItem('Blueberry')); }); - it('should close submenu on arrow right', () => { + it('should close submenu on arrow right', async () => { const apple = getItem('Apple'); const banana = getItem('Banana'); const berries = getItem('Berries'); - keydown(apple!, 'ArrowDown'); - keydown(banana!, 'ArrowDown'); - keydown(berries!, 'ArrowLeft'); + await keydown(apple!, 'ArrowDown'); + await keydown(banana!, 'ArrowDown'); + await keydown(berries!, 'ArrowLeft'); const blueberry = getItem('Blueberry'); - keydown(blueberry!, 'ArrowRight'); + await keydown(blueberry!, 'ArrowRight'); expect(isSubmenuExpanded()).toBe(false); expect(document.activeElement).toBe(berries); @@ -489,7 +489,7 @@ describe('Standalone Menu Pattern', () => { it('should not reset default state on hover triggers expansion', async () => { fixture = TestBed.createComponent(StandaloneMenuExample); - fixture.detectChanges(); + await fixture.whenStable(); const berries = getItem('Berries'); await mouseover(berries!); @@ -498,38 +498,38 @@ describe('Standalone Menu Pattern', () => { it('should be able to set an aria-label on a menu item', async () => { fixture = TestBed.createComponent(StandaloneMenuExample); - fixture.detectChanges(); + await fixture.whenStable(); const item = getItem('Apple'); expect(item?.getAttribute('aria-label')).toBeFalsy(); fixture.componentInstance.firstItemAriaLabel.set('Apple item label'); - fixture.detectChanges(); + await fixture.whenStable(); expect(item?.getAttribute('aria-label')).toBe('Apple item label'); }); describe('softDisabled', () => { - it('should skip disabled items during navigation when softDisabled is false', () => { - setupMenu(); + it('should skip disabled items during navigation when softDisabled is false', async () => { + await setupMenu(); fixture.componentInstance.softDisabled.set(false); - fixture.detectChanges(); + await fixture.whenStable(); const apple = getItem('Apple')!; const berries = getItem('Berries'); - keydown(apple, 'ArrowUp'); + await keydown(apple, 'ArrowUp'); expect(document.activeElement).toBe(berries); }); - it('should focus disabled items during navigation when softDisabled is true', () => { - setupMenu(); + it('should focus disabled items during navigation when softDisabled is true', async () => { + await setupMenu(); fixture.componentInstance.softDisabled.set(true); - fixture.detectChanges(); + await fixture.whenStable(); const apple = getItem('Apple')!; const cherry = getItem('Cherry'); - keydown(apple!, 'ArrowUp'); + await keydown(apple!, 'ArrowUp'); expect(document.activeElement).toBe(cherry); }); }); @@ -563,9 +563,9 @@ describe('Standalone Menu Pattern', () => { consoleSpy = spyOn(console, 'warn'); }); - afterEach(() => { + afterEach(async () => { TestBed.resetTestingModule(); - setupMenu(); + await setupMenu(); }); it('should warn when duplicate values are detected inside ngMenu', () => { @@ -597,13 +597,13 @@ describe('Standalone Menu Pattern', () => { describe('Menu Trigger Pattern', () => { let fixture: ComponentFixture; - const focusin = (element: Element) => { + const focusin = async (element: Element) => { element.dispatchEvent(new FocusEvent('focusin', {bubbles: true})); - fixture.detectChanges(); + await fixture.whenStable(); }; - const keydown = (element: Element, key: string, modifierKeys: {} = {}) => { - focusin(element); + const keydown = async (element: Element, key: string, modifierKeys: {} = {}) => { + await focusin(element); element.dispatchEvent( new KeyboardEvent('keydown', { key, @@ -611,23 +611,23 @@ describe('Menu Trigger Pattern', () => { ...modifierKeys, }), ); - fixture.detectChanges(); + await fixture.whenStable(); }; - const click = (element: Element, eventInit?: PointerEventInit) => { - focusin(element); + const click = async (element: Element, eventInit?: PointerEventInit) => { + await focusin(element); element.dispatchEvent(new PointerEvent('click', {bubbles: true, ...eventInit})); - fixture.detectChanges(); + await fixture.whenStable(); }; - const focusout = (element: Element, relatedTarget?: EventTarget) => { + const focusout = async (element: Element, relatedTarget?: EventTarget) => { element.dispatchEvent(new FocusEvent('focusout', {bubbles: true, relatedTarget})); - fixture.detectChanges(); + await fixture.whenStable(); }; - function setupMenu() { + async function setupMenu() { fixture = TestBed.createComponent(MenuTriggerExample); - fixture.detectChanges(); + await fixture.whenStable(); } function getTrigger(): HTMLElement { @@ -651,123 +651,123 @@ describe('Menu Trigger Pattern', () => { } describe('Navigation', () => { - beforeEach(() => setupMenu()); + beforeEach(async () => await setupMenu()); - it('should navigate to the first item on click', () => { - click(getTrigger()); + it('should navigate to the first item on click', async () => { + await click(getTrigger()); expect(document.activeElement).toBe(getItem('Apple')); }); - it('should navigate to the first item on arrow down', () => { + it('should navigate to the first item on arrow down', async () => { const trigger = getTrigger(); - keydown(trigger, 'ArrowDown'); + await keydown(trigger, 'ArrowDown'); expect(document.activeElement).toBe(getItem('Apple')); }); - it('should navigate to the last item on arrow up', () => { + it('should navigate to the last item on arrow up', async () => { const trigger = getTrigger(); - keydown(trigger, 'ArrowUp'); + await keydown(trigger, 'ArrowUp'); expect(document.activeElement).toBe(getItem('Cherry')); }); - it('should navigate to the first item on enter', () => { + it('should navigate to the first item on enter', async () => { const trigger = getTrigger(); - keydown(trigger, 'Enter'); + await keydown(trigger, 'Enter'); expect(document.activeElement).toBe(getItem('Apple')); }); - it('should navigate to the first item on space', () => { + it('should navigate to the first item on space', async () => { const trigger = getTrigger(); - keydown(trigger, ' '); + await keydown(trigger, ' '); expect(document.activeElement).toBe(getItem('Apple')); }); }); describe('Expansion', () => { - beforeEach(() => setupMenu()); + beforeEach(async () => await setupMenu()); it('should be closed by default', () => { expect(isExpanded()).toBe(false); }); - it('should open on click', () => { - click(getTrigger()); + it('should open on click', async () => { + await click(getTrigger()); expect(isExpanded()).toBe(true); }); - it('should close on second click', () => { + it('should close on second click', async () => { const trigger = getTrigger(); - click(trigger); + await click(trigger); expect(isExpanded()).toBe(true); - click(trigger); + await click(trigger); expect(isExpanded()).toBe(false); }); - it('should open on arrow down', () => { - keydown(getTrigger(), 'ArrowDown'); + it('should open on arrow down', async () => { + await keydown(getTrigger(), 'ArrowDown'); expect(isExpanded()).toBe(true); }); - it('should open on arrow up', () => { - keydown(getTrigger(), 'ArrowUp'); + it('should open on arrow up', async () => { + await keydown(getTrigger(), 'ArrowUp'); expect(isExpanded()).toBe(true); }); - it('should open on space', () => { - keydown(getTrigger(), ' '); + it('should open on space', async () => { + await keydown(getTrigger(), ' '); expect(isExpanded()).toBe(true); }); - it('should open on enter', () => { - keydown(getTrigger(), 'Enter'); + it('should open on enter', async () => { + await keydown(getTrigger(), 'Enter'); expect(isExpanded()).toBe(true); }); - it('should close on escape', () => { + it('should close on escape', async () => { const trigger = getTrigger(); - click(trigger); + await click(trigger); expect(isExpanded()).toBe(true); - keydown(getMenu()!, 'Escape'); + await keydown(getMenu()!, 'Escape'); expect(isExpanded()).toBe(false); expect(document.activeElement).toBe(trigger); }); - it('should close on selecting an item on click', () => { - click(getTrigger()); - click(getItem('Apple')!); + it('should close on selecting an item on click', async () => { + await click(getTrigger()); + await click(getItem('Apple')!); expect(isExpanded()).toBe(false); }); - it('should close on selecting an item on enter', () => { - click(getTrigger()); - keydown(getItem('Apple')!, 'Enter'); + it('should close on selecting an item on enter', async () => { + await click(getTrigger()); + await keydown(getItem('Apple')!, 'Enter'); expect(isExpanded()).toBe(false); }); it('should close on selecting an item on space', async () => { - click(getTrigger()); - keydown(getItem('Apple')!, ' '); + await click(getTrigger()); + await keydown(getItem('Apple')!, ' '); expect(isExpanded()).toBe(false); }); - it('should close the trigger on focus out from the menu', () => { - click(getTrigger()); + it('should close the trigger on focus out from the menu', async () => { + await click(getTrigger()); expect(isExpanded()).toBe(true); - focusout(getMenu()!, document.body); + await focusout(getMenu()!, document.body); expect(isExpanded()).toBe(false); }); }); describe('Selection', () => { - beforeEach(() => setupMenu()); + beforeEach(async () => await setupMenu()); - it('should select an item on click', () => { + it('should select an item on click', async () => { spyOn(fixture.componentInstance, 'itemSelected'); - click(getTrigger()); - click(getItem('Apple')!); + await click(getTrigger()); + await click(getItem('Apple')!); expect(fixture.componentInstance.itemSelected).toHaveBeenCalledWith('Apple'); }); }); @@ -776,13 +776,13 @@ describe('Menu Trigger Pattern', () => { describe('CDK Overlay Menu Pattern', () => { let fixture: ComponentFixture; - const focusin = (element: Element) => { + const focusin = async (element: Element) => { element.dispatchEvent(new FocusEvent('focusin', {bubbles: true})); - fixture.detectChanges(); + await fixture.whenStable(); }; const keydown = async (element: Element, key: string, modifierKeys: {} = {}) => { - focusin(element); + await focusin(element); element.dispatchEvent( new KeyboardEvent('keydown', { key, @@ -790,22 +790,22 @@ describe('CDK Overlay Menu Pattern', () => { ...modifierKeys, }), ); - fixture.detectChanges(); + await fixture.whenStable(); await waitForMicrotasks(); - fixture.detectChanges(); + await fixture.whenStable(); }; const click = async (element: Element, eventInit?: PointerEventInit) => { - focusin(element); + await focusin(element); element.dispatchEvent(new PointerEvent('click', {bubbles: true, ...eventInit})); - fixture.detectChanges(); + await fixture.whenStable(); await waitForMicrotasks(); - fixture.detectChanges(); + await fixture.whenStable(); }; - function setupMenu() { + async function setupMenu() { fixture = TestBed.createComponent(CdkOverlayMenuExample); - fixture.detectChanges(); + await fixture.whenStable(); } function getTrigger(): HTMLElement { @@ -819,7 +819,7 @@ describe('CDK Overlay Menu Pattern', () => { return items.find(item => item.textContent?.trim() === text) || null; } - beforeEach(() => setupMenu()); + beforeEach(async () => await setupMenu()); it('should focus the first item when opened via arrow down', async () => { await keydown(getTrigger(), 'ArrowDown'); @@ -855,7 +855,7 @@ describe('CDK Overlay Menu Pattern', () => { // Explicitly clear cached menu before second open fixture.componentInstance.clearMenu(); - fixture.detectChanges(); + await fixture.whenStable(); // Second open await keydown(trigger, 'Enter'); @@ -870,7 +870,7 @@ describe('CDK Overlay Menu Pattern', () => { describe('Menu Bar Pattern', () => { let fixture: ComponentFixture; - const keydown = (element: Element, key: string, modifierKeys: {} = {}) => { + const keydown = async (element: Element, key: string, modifierKeys: {} = {}) => { element.dispatchEvent( new KeyboardEvent('keydown', { key, @@ -878,30 +878,30 @@ describe('Menu Bar Pattern', () => { ...modifierKeys, }), ); - fixture.detectChanges(); + await fixture.whenStable(); }; - const mouseover = (element: Element) => { + const mouseover = async (element: Element) => { element.dispatchEvent(new MouseEvent('mouseover', {bubbles: true})); - fixture.detectChanges(); + await fixture.whenStable(); }; - const click = (element: Element, eventInit?: PointerEventInit) => { + const click = async (element: Element, eventInit?: PointerEventInit) => { element.dispatchEvent(new PointerEvent('click', {bubbles: true, ...eventInit})); - fixture.detectChanges(); + await fixture.whenStable(); }; - const focusout = (element: Element, relatedTarget?: EventTarget) => { + const focusout = async (element: Element, relatedTarget?: EventTarget) => { element.dispatchEvent(new FocusEvent('focusout', {bubbles: true, relatedTarget})); - fixture.detectChanges(); + await fixture.whenStable(); }; - function setupMenu(opts?: {textDirection: 'ltr' | 'rtl'}) { + async function setupMenu(opts?: {textDirection: 'ltr' | 'rtl'}) { TestBed.configureTestingModule({ providers: [provideFakeDirectionality(opts?.textDirection ?? 'ltr')], }); fixture = TestBed.createComponent(MenuBarExample); - fixture.detectChanges(); + await fixture.whenStable(); getMenuBarItem('File')?.focus(); } @@ -962,266 +962,266 @@ describe('Menu Bar Pattern', () => { }); describe('Navigation', () => { - beforeEach(() => setupMenu()); + beforeEach(async () => await setupMenu()); - it('should navigate to the first item on arrow down', () => { + it('should navigate to the first item on arrow down', async () => { const file = getMenuBarItem('File'); const edit = getMenuBarItem('Edit'); const view = getMenuBarItem('View'); - keydown(file!, 'ArrowRight'); - keydown(edit!, 'ArrowRight'); - keydown(view!, 'ArrowDown'); + await keydown(file!, 'ArrowRight'); + await keydown(edit!, 'ArrowRight'); + await keydown(view!, 'ArrowDown'); expect(document.activeElement).toBe(getMenuItem('Zoom In')); }); - it('should navigate to the last item on arrow up', () => { + it('should navigate to the last item on arrow up', async () => { const file = getMenuBarItem('File'); const edit = getMenuBarItem('Edit'); const view = getMenuBarItem('View'); - keydown(file!, 'ArrowRight'); - keydown(edit!, 'ArrowRight'); - keydown(view!, 'ArrowUp'); + await keydown(file!, 'ArrowRight'); + await keydown(edit!, 'ArrowRight'); + await keydown(view!, 'ArrowUp'); expect(document.activeElement).toBe(getMenuItem('Full Screen')); }); - it('should navigate to the first item on enter', () => { + it('should navigate to the first item on enter', async () => { const file = getMenuBarItem('File'); const edit = getMenuBarItem('Edit'); const view = getMenuBarItem('View'); - keydown(file!, 'ArrowRight'); - keydown(edit!, 'ArrowRight'); - keydown(view!, 'Enter'); + await keydown(file!, 'ArrowRight'); + await keydown(edit!, 'ArrowRight'); + await keydown(view!, 'Enter'); expect(document.activeElement).toBe(getMenuItem('Zoom In')); }); - it('should navigate to the first item on space', () => { + it('should navigate to the first item on space', async () => { const file = getMenuBarItem('File'); const edit = getMenuBarItem('Edit'); const view = getMenuBarItem('View'); - keydown(file!, 'ArrowRight'); - keydown(edit!, 'ArrowRight'); - keydown(view!, ' '); + await keydown(file!, 'ArrowRight'); + await keydown(edit!, 'ArrowRight'); + await keydown(view!, ' '); expect(document.activeElement).toBe(getMenuItem('Zoom In')); }); - it('should navigate to a menubar item on mouse over', () => { + it('should navigate to a menubar item on mouse over', async () => { const edit = getMenuBarItem('Edit'); - mouseover(edit!); + await mouseover(edit!); expect(document.activeElement).toBe(edit); }); - it('should focus the first item of the next menubar item on arrow right', () => { + it('should focus the first item of the next menubar item on arrow right', async () => { const edit = getMenuBarItem('Edit'); const file = getMenuBarItem('File'); const view = getMenuBarItem('View'); const documentation = getMenuBarItem('Documentation'); const zoomIn = getMenuItem('Zoom In'); - keydown(file!, 'ArrowRight'); - keydown(edit!, 'ArrowRight'); - keydown(view!, 'ArrowDown'); + await keydown(file!, 'ArrowRight'); + await keydown(edit!, 'ArrowRight'); + await keydown(view!, 'ArrowDown'); - keydown(zoomIn!, 'ArrowRight'); + await keydown(zoomIn!, 'ArrowRight'); expect(document.activeElement).toBe(documentation); }); - it('should focus the first item of the previous menubar item on arrow left', () => { + it('should focus the first item of the previous menubar item on arrow left', async () => { const edit = getMenuBarItem('Edit'); const file = getMenuBarItem('File'); const view = getMenuBarItem('View'); const undo = getMenuItem('Undo'); const zoomIn = getMenuItem('Zoom In'); - keydown(file!, 'ArrowRight'); - keydown(edit!, 'ArrowRight'); - keydown(view!, 'ArrowDown'); + await keydown(file!, 'ArrowRight'); + await keydown(edit!, 'ArrowRight'); + await keydown(view!, 'ArrowDown'); - keydown(zoomIn!, 'ArrowLeft'); + await keydown(zoomIn!, 'ArrowLeft'); expect(document.activeElement).toBe(undo); }); }); describe('Expansion', () => { - beforeEach(() => setupMenu()); + beforeEach(async () => await setupMenu()); it('should be collapsed by default', () => { expect(isExpanded('View')).toBe(false); }); - it('should expand on click', () => { - click(getMenuBarItem('View')!); + it('should expand on click', async () => { + await click(getMenuBarItem('View')!); expect(isExpanded('View')).toBe(true); }); - it('should collapse on second click', () => { + it('should collapse on second click', async () => { const view = getMenuBarItem('View'); - click(view!); + await click(view!); expect(isExpanded('View')).toBe(true); - click(view!); + await click(view!); expect(isExpanded('View')).toBe(false); }); - it('should expand on arrow down', () => { + it('should expand on arrow down', async () => { const file = getMenuBarItem('File'); const edit = getMenuBarItem('Edit'); const view = getMenuBarItem('View'); - keydown(file!, 'ArrowRight'); - keydown(edit!, 'ArrowRight'); - keydown(view!, 'ArrowDown'); + await keydown(file!, 'ArrowRight'); + await keydown(edit!, 'ArrowRight'); + await keydown(view!, 'ArrowDown'); expect(isExpanded('View')).toBe(true); }); - it('should expand on arrow up', () => { + it('should expand on arrow up', async () => { const file = getMenuBarItem('File'); const edit = getMenuBarItem('Edit'); const view = getMenuBarItem('View'); - keydown(file!, 'ArrowRight'); - keydown(edit!, 'ArrowRight'); - keydown(view!, 'ArrowUp'); + await keydown(file!, 'ArrowRight'); + await keydown(edit!, 'ArrowRight'); + await keydown(view!, 'ArrowUp'); expect(isExpanded('View')).toBe(true); }); - it('should expand on space', () => { + it('should expand on space', async () => { const file = getMenuBarItem('File'); const edit = getMenuBarItem('Edit'); const view = getMenuBarItem('View'); - keydown(file!, 'ArrowRight'); - keydown(edit!, 'ArrowRight'); - keydown(view!, ' '); + await keydown(file!, 'ArrowRight'); + await keydown(edit!, 'ArrowRight'); + await keydown(view!, ' '); expect(isExpanded('View')).toBe(true); }); - it('should expand on enter', () => { + it('should expand on enter', async () => { const file = getMenuBarItem('File'); const edit = getMenuBarItem('Edit'); const view = getMenuBarItem('View'); - keydown(file!, 'ArrowRight'); - keydown(edit!, 'ArrowRight'); - keydown(view!, 'Enter'); + await keydown(file!, 'ArrowRight'); + await keydown(edit!, 'ArrowRight'); + await keydown(view!, 'Enter'); expect(isExpanded('View')).toBe(true); }); - it('should close on escape', () => { + it('should close on escape', async () => { const view = getMenuBarItem('View'); - click(view!); + await click(view!); expect(isExpanded('View')).toBe(true); - keydown(getMenu()!, 'Escape'); + await keydown(getMenu()!, 'Escape'); expect(isExpanded('View')).toBe(false); expect(document.activeElement).toBe(view); }); - it('should close on selecting an item on click', () => { - click(getMenuBarItem('View')!); - click(getMenuItem('Zoom In')!); + it('should close on selecting an item on click', async () => { + await click(getMenuBarItem('View')!); + await click(getMenuItem('Zoom In')!); expect(isExpanded('View')).toBe(false); }); - it('should close on selecting an item on enter', () => { + it('should close on selecting an item on enter', async () => { const view = getMenuBarItem('View'); - click(view!); - keydown(view!, 'ArrowDown'); - keydown(getMenuItem('Zoom In')!, 'Enter'); + await click(view!); + await keydown(view!, 'ArrowDown'); + await keydown(getMenuItem('Zoom In')!, 'Enter'); expect(isExpanded('View')).toBe(false); }); - it('should close on selecting an item on space', () => { + it('should close on selecting an item on space', async () => { const view = getMenuBarItem('View'); - click(view!); - keydown(view!, 'ArrowDown'); - keydown(getMenuItem('Zoom In')!, ' '); + await click(view!); + await keydown(view!, 'ArrowDown'); + await keydown(getMenuItem('Zoom In')!, ' '); expect(isExpanded('View')).toBe(false); }); - it('should close on focus out from the menu', () => { - click(getMenuBarItem('View')!); + it('should close on focus out from the menu', async () => { + await click(getMenuBarItem('View')!); expect(isExpanded('View')).toBe(true); - focusout(getMenu()!, document.body); + await focusout(getMenu()!, document.body); expect(isExpanded('View')).toBe(false); }); - it('should close on arrow right on a leaf menu item', () => { + it('should close on arrow right on a leaf menu item', async () => { const view = getMenuBarItem('View'); - click(view!); + await click(view!); expect(isExpanded('View')).toBe(true); - keydown(getMenuItem('Zoom In')!, 'ArrowRight'); + await keydown(getMenuItem('Zoom In')!, 'ArrowRight'); expect(isExpanded('View')).toBe(false); }); - it('should close on arrow left on a root menu item', () => { + it('should close on arrow left on a root menu item', async () => { const view = getMenuBarItem('View'); - click(view!); - keydown(view!, 'ArrowDown'); - keydown(getMenuItem('Zoom In')!, 'ArrowLeft'); + await click(view!); + await keydown(view!, 'ArrowDown'); + await keydown(getMenuItem('Zoom In')!, 'ArrowLeft'); expect(isExpanded('View')).toBe(false); }); - it('should expand the next menu bar item on arrow right on a leaf menu item', () => { + it('should expand the next menu bar item on arrow right on a leaf menu item', async () => { const view = getMenuBarItem('View'); const zoomIn = getMenuItem('Zoom In'); - click(view!); + await click(view!); expect(isExpanded('View')).toBe(true); expect(document.activeElement).toBe(view); - keydown(view!, 'ArrowDown'); + await keydown(view!, 'ArrowDown'); expect(document.activeElement).toBe(zoomIn); - keydown(zoomIn!, 'ArrowRight'); + await keydown(zoomIn!, 'ArrowRight'); expect(isExpanded('View')).toBe(false); expect(isExpanded('Help')).toBe(true); }); - it('should expand the previous menu bar item on arrow left on a root menu item', () => { + it('should expand the previous menu bar item on arrow left on a root menu item', async () => { const view = getMenuBarItem('View'); const zoomIn = getMenuItem('Zoom In'); - click(view!); + await click(view!); expect(isExpanded('View')).toBe(true); expect(document.activeElement).toBe(view); - keydown(view!, 'ArrowDown'); + await keydown(view!, 'ArrowDown'); expect(document.activeElement).toBe(zoomIn); - keydown(zoomIn!, 'ArrowLeft'); + await keydown(zoomIn!, 'ArrowLeft'); expect(isExpanded('View')).toBe(false); expect(isExpanded('Edit')).toBe(true); }); }); describe('RTL', () => { - beforeEach(() => setupMenu({textDirection: 'rtl'})); + beforeEach(async () => await setupMenu({textDirection: 'rtl'})); - it('should focus the first item of the next menubar item on arrow left', () => { + it('should focus the first item of the next menubar item on arrow left', async () => { const edit = getMenuBarItem('Edit'); const file = getMenuBarItem('File'); const view = getMenuBarItem('View'); const documentation = getMenuBarItem('Documentation'); const zoomIn = getMenuItem('Zoom In'); - keydown(file!, 'ArrowLeft'); - keydown(edit!, 'ArrowLeft'); - keydown(view!, 'ArrowDown'); + await keydown(file!, 'ArrowLeft'); + await keydown(edit!, 'ArrowLeft'); + await keydown(view!, 'ArrowDown'); - keydown(zoomIn!, 'ArrowLeft'); + await keydown(zoomIn!, 'ArrowLeft'); expect(document.activeElement).toBe(documentation); }); - it('should focus the first item of the previous menubar item on arrow right', () => { + it('should focus the first item of the previous menubar item on arrow right', async () => { const edit = getMenuBarItem('Edit'); const file = getMenuBarItem('File'); const view = getMenuBarItem('View'); const undo = getMenuItem('Undo'); const zoomIn = getMenuItem('Zoom In'); - keydown(file!, 'ArrowLeft'); - keydown(edit!, 'ArrowLeft'); - keydown(view!, 'ArrowDown'); + await keydown(file!, 'ArrowLeft'); + await keydown(edit!, 'ArrowLeft'); + await keydown(view!, 'ArrowDown'); - keydown(zoomIn!, 'ArrowRight'); + await keydown(zoomIn!, 'ArrowRight'); expect(document.activeElement).toBe(undo); }); }); diff --git a/src/aria/tabs/tabs.spec.ts b/src/aria/tabs/tabs.spec.ts index 2459a852187b..065677e040d1 100644 --- a/src/aria/tabs/tabs.spec.ts +++ b/src/aria/tabs/tabs.spec.ts @@ -36,7 +36,7 @@ describe('Tabs', () => { let tabElements: HTMLElement[]; let tabPanelElements: HTMLElement[]; - const keydown = (key: string, modifierKeys: ModifierKeys = {}) => { + const keydown = async (key: string, modifierKeys: ModifierKeys = {}) => { tabListElement.dispatchEvent( new KeyboardEvent('keydown', { key, @@ -44,31 +44,31 @@ describe('Tabs', () => { ...modifierKeys, }), ); - fixture.detectChanges(); + await fixture.whenStable(); defineTestVariables(); }; - const click = (target: HTMLElement, eventInit?: PointerEventInit) => { + const click = async (target: HTMLElement, eventInit?: PointerEventInit) => { target.dispatchEvent( new PointerEvent('click', { bubbles: true, ...eventInit, }), ); - fixture.detectChanges(); + await fixture.whenStable(); defineTestVariables(); }; - const space = (modifierKeys?: ModifierKeys) => keydown(' ', modifierKeys); - const enter = (modifierKeys?: ModifierKeys) => keydown('Enter', modifierKeys); - const up = (modifierKeys?: ModifierKeys) => keydown('ArrowUp', modifierKeys); - const down = (modifierKeys?: ModifierKeys) => keydown('ArrowDown', modifierKeys); - const left = (modifierKeys?: ModifierKeys) => keydown('ArrowLeft', modifierKeys); - const right = (modifierKeys?: ModifierKeys) => keydown('ArrowRight', modifierKeys); - const home = (modifierKeys?: ModifierKeys) => keydown('Home', modifierKeys); - const end = (modifierKeys?: ModifierKeys) => keydown('End', modifierKeys); + const space = async (modifierKeys?: ModifierKeys) => await keydown(' ', modifierKeys); + const enter = async (modifierKeys?: ModifierKeys) => await keydown('Enter', modifierKeys); + const up = async (modifierKeys?: ModifierKeys) => await keydown('ArrowUp', modifierKeys); + const down = async (modifierKeys?: ModifierKeys) => await keydown('ArrowDown', modifierKeys); + const left = async (modifierKeys?: ModifierKeys) => await keydown('ArrowLeft', modifierKeys); + const right = async (modifierKeys?: ModifierKeys) => await keydown('ArrowRight', modifierKeys); + const home = async (modifierKeys?: ModifierKeys) => await keydown('Home', modifierKeys); + const end = async (modifierKeys?: ModifierKeys) => await keydown('End', modifierKeys); - function setupTestTabs(options: {textDirection?: Direction} = {}) { + async function setupTestTabs(options: {textDirection?: Direction} = {}) { TestBed.configureTestingModule({ providers: [provideFakeDirectionality(options.textDirection ?? 'ltr')], }); @@ -76,11 +76,11 @@ describe('Tabs', () => { fixture = TestBed.createComponent(TestTabsComponent); testComponent = fixture.componentInstance; - fixture.detectChanges(); + await fixture.whenStable(); defineTestVariables(); } - function updateTabs( + async function updateTabs( options: { initialTabs?: TestTabDefinition[]; selectedTab?: string | undefined; @@ -101,7 +101,7 @@ describe('Tabs', () => { if (options.focusMode !== undefined) testComponent.focusMode.set(options.focusMode); if (options.selectionMode !== undefined) testComponent.selectionMode.set(options.selectionMode); - fixture.detectChanges(); + await fixture.whenStable(); defineTestVariables(); } @@ -130,9 +130,9 @@ describe('Tabs', () => { }); describe('ARIA attributes and roles', () => { - beforeEach(() => { - setupTestTabs(); - updateTabs({ + beforeEach(async () => { + await setupTestTabs(); + await updateTabs({ initialTabs: [ {value: 'tab1', label: 'Tab 1', content: 'Content 1'}, {value: 'tab2', label: 'Tab 2', content: 'Content 2', disabled: true}, @@ -146,33 +146,33 @@ describe('Tabs', () => { expect(tabListElement.getAttribute('role')).toBe('tablist'); }); - it('should set aria-orientation based on input', () => { + it('should set aria-orientation based on input', async () => { expect(tabListElement.getAttribute('aria-orientation')).toBe('horizontal'); - updateTabs({orientation: 'vertical'}); + await updateTabs({orientation: 'vertical'}); expect(tabListElement.getAttribute('aria-orientation')).toBe('vertical'); }); - it('should set aria-disabled based on input', () => { + it('should set aria-disabled based on input', async () => { expect(tabListElement.getAttribute('aria-disabled')).toBe('false'); - updateTabs({disabled: true}); + await updateTabs({disabled: true}); expect(tabListElement.getAttribute('aria-disabled')).toBe('true'); }); - it('should have tabindex set by focusMode', () => { - updateTabs({focusMode: 'roving'}); + it('should have tabindex set by focusMode', async () => { + await updateTabs({focusMode: 'roving'}); expect(tabListElement.getAttribute('tabindex')).toBe('-1'); - updateTabs({focusMode: 'activedescendant'}); + await updateTabs({focusMode: 'activedescendant'}); expect(tabListElement.getAttribute('tabindex')).toBe('0'); }); - it('should set aria-activedescendant in activedescendant mode', () => { - updateTabs({focusMode: 'activedescendant', selectedTab: 'tab1'}); + it('should set aria-activedescendant in activedescendant mode', async () => { + await updateTabs({focusMode: 'activedescendant', selectedTab: 'tab1'}); expect(tabListElement.getAttribute('aria-activedescendant')).toBe(tabElements[0].id); }); - it('should not set aria-activedescendant in roving mode', () => { - updateTabs({selectedTab: 'tab1'}); + it('should not set aria-activedescendant in roving mode', async () => { + await updateTabs({selectedTab: 'tab1'}); expect(tabListElement.hasAttribute('aria-activedescendant')).toBe(false); }); }); @@ -184,13 +184,13 @@ describe('Tabs', () => { }); }); - it('should have aria-selected based on selection state', () => { - updateTabs({selectedTab: 'tab1'}); + it('should have aria-selected based on selection state', async () => { + await updateTabs({selectedTab: 'tab1'}); expect(tabElements[0].getAttribute('aria-selected')).toBe('true'); expect(tabElements[1].getAttribute('aria-selected')).toBe('false'); expect(tabElements[2].getAttribute('aria-selected')).toBe('false'); - updateTabs({selectedTab: 'tab3'}); + await updateTabs({selectedTab: 'tab3'}); expect(tabElements[0].getAttribute('aria-selected')).toBe('false'); expect(tabElements[1].getAttribute('aria-selected')).toBe('false'); expect(tabElements[2].getAttribute('aria-selected')).toBe('true'); @@ -208,13 +208,13 @@ describe('Tabs', () => { expect(tabElements[2].getAttribute('aria-disabled')).toBe('false'); }); - it('should have tabindex set by focusMode and active state', () => { - updateTabs({focusMode: 'roving', selectedTab: 'tab1'}); + it('should have tabindex set by focusMode and active state', async () => { + await updateTabs({focusMode: 'roving', selectedTab: 'tab1'}); expect(tabElements[0].getAttribute('tabindex')).toBe('0'); expect(tabElements[1].getAttribute('tabindex')).toBe('-1'); expect(tabElements[2].getAttribute('tabindex')).toBe('-1'); - updateTabs({focusMode: 'activedescendant'}); + await updateTabs({focusMode: 'activedescendant'}); tabElements.forEach(tabElement => { expect(tabElement.getAttribute('tabindex')).toBe('-1'); }); @@ -228,13 +228,13 @@ describe('Tabs', () => { }); }); - it('should have tabindex="0" when visible.', () => { - updateTabs({selectedTab: 'tab1'}); + it('should have tabindex="0" when visible.', async () => { + await updateTabs({selectedTab: 'tab1'}); expect(tabPanelElements[0].getAttribute('tabindex')).toBe('0'); }); - it('should have tabindex="-1" when hidden.', () => { - updateTabs({selectedTab: 'tab1'}); + it('should have tabindex="-1" when hidden.', async () => { + await updateTabs({selectedTab: 'tab1'}); expect(tabPanelElements[1].getAttribute('tabindex')).toBe('-1'); expect(tabPanelElements[2].getAttribute('tabindex')).toBe('-1'); }); @@ -245,13 +245,13 @@ describe('Tabs', () => { expect(tabPanelElements[2].getAttribute('aria-labelledby')).toBe(tabElements[2].id); }); - it('should have inert attribute when hidden and not when visible', () => { - updateTabs({selectedTab: 'tab1'}); + it('should have inert attribute when hidden and not when visible', async () => { + await updateTabs({selectedTab: 'tab1'}); expect(tabPanelElements[0].hasAttribute('inert')).toBe(false); expect(tabPanelElements[1].hasAttribute('inert')).toBe(true); expect(tabPanelElements[2].hasAttribute('inert')).toBe(true); - updateTabs({selectedTab: 'tab3'}); + await updateTabs({selectedTab: 'tab3'}); expect(tabPanelElements[0].hasAttribute('inert')).toBe(true); expect(tabPanelElements[1].hasAttribute('inert')).toBe(true); expect(tabPanelElements[2].hasAttribute('inert')).toBe(false); @@ -263,9 +263,9 @@ describe('Tabs', () => { for (const focusMode of ['roving', 'activedescendant'] as const) { describe(`focusMode="${focusMode}"`, () => { describe('LTR', () => { - beforeEach(() => { - setupTestTabs({textDirection: 'ltr'}); - updateTabs({ + beforeEach(async () => { + await setupTestTabs({textDirection: 'ltr'}); + await updateTabs({ initialTabs: [ {value: 'tab1', label: 'Tab 1', content: 'Content 1'}, {value: 'tab2', label: 'Tab 2', content: 'Content 2', disabled: true}, @@ -276,87 +276,87 @@ describe('Tabs', () => { }); }); - it('should move focus with ArrowRight', () => { + it('should move focus with ArrowRight', async () => { expect(isTabFocused(0)).toBe(true); - right(); + await right(); expect(isTabFocused(1)).toBe(true); }); - it('should move focus with ArrowLeft', () => { - right(); + it('should move focus with ArrowLeft', async () => { + await right(); expect(isTabFocused(1)).toBe(true); - left(); + await left(); expect(isTabFocused(0)).toBe(true); }); - it('should wrap focus with ArrowRight if wrap is true', () => { - updateTabs({wrap: true}); - right(); + it('should wrap focus with ArrowRight if wrap is true', async () => { + await updateTabs({wrap: true}); + await right(); expect(isTabFocused(1)).toBe(true); - right(); + await right(); expect(isTabFocused(2)).toBe(true); - right(); + await right(); expect(isTabFocused(0)).toBe(true); }); - it('should not wrap focus with ArrowRight if wrap is false', () => { - updateTabs({wrap: false}); - right(); + it('should not wrap focus with ArrowRight if wrap is false', async () => { + await updateTabs({wrap: false}); + await right(); expect(isTabFocused(1)).toBe(true); - right(); + await right(); expect(isTabFocused(2)).toBe(true); - right(); + await right(); expect(isTabFocused(2)).toBe(true); }); - it('should wrap focus with ArrowLeft if wrap is true', () => { - updateTabs({wrap: true}); + it('should wrap focus with ArrowLeft if wrap is true', async () => { + await updateTabs({wrap: true}); expect(isTabFocused(0)).toBe(true); - left(); + await left(); expect(isTabFocused(2)).toBe(true); }); - it('should not wrap focus with ArrowLeft if wrap is false', () => { - updateTabs({wrap: false}); + it('should not wrap focus with ArrowLeft if wrap is false', async () => { + await updateTabs({wrap: false}); expect(isTabFocused(0)).toBe(true); - left(); + await left(); expect(isTabFocused(0)).toBe(true); }); - it('should move focus to first tab with Home', () => { - left(); + it('should move focus to first tab with Home', async () => { + await left(); expect(isTabFocused(2)).toBe(true); - home(); + await home(); expect(isTabFocused(0)).toBe(true); }); - it('should move focus to last tab with End', () => { + it('should move focus to last tab with End', async () => { expect(isTabFocused(0)).toBe(true); - end(); + await end(); expect(isTabFocused(2)).toBe(true); }); - it('should skip disabled tabs if softDisabled is false', () => { - updateTabs({softDisabled: false}); + it('should skip disabled tabs if softDisabled is false', async () => { + await updateTabs({softDisabled: false}); expect(isTabFocused(0)).toBe(true); - right(); + await right(); expect(isTabFocused(2)).toBe(true); }); - it('should not skip disabled tabs if softDisabled is true', () => { - updateTabs({softDisabled: true}); + it('should not skip disabled tabs if softDisabled is true', async () => { + await updateTabs({softDisabled: true}); tabListElement.focus(); - fixture.detectChanges(); + await fixture.whenStable(); expect(isTabFocused(0)).toBe(true); - right(); + await right(); expect(isTabFocused(1)).toBe(true); }); }); describe('RTL', () => { - beforeEach(() => { - setupTestTabs({textDirection: 'rtl'}); - updateTabs({ + beforeEach(async () => { + await setupTestTabs({textDirection: 'rtl'}); + await updateTabs({ initialTabs: [ {value: 'tab1', label: 'Tab 1', content: 'Content 1'}, {value: 'tab2', label: 'Tab 2', content: 'Content 2', disabled: true}, @@ -366,41 +366,41 @@ describe('Tabs', () => { selectedTab: 'tab1', }); }); - it('should move focus with ArrowLeft (effectively next)', () => { + it('should move focus with ArrowLeft (effectively next)', async () => { expect(isTabFocused(0)).toBe(true); - left(); + await left(); expect(isTabFocused(1)).toBe(true); }); - it('should move focus with ArrowRight (effectively previous)', () => { - left(); + it('should move focus with ArrowRight (effectively previous)', async () => { + await left(); expect(isTabFocused(1)).toBe(true); - right(); + await right(); expect(isTabFocused(0)).toBe(true); }); - it('should wrap focus with ArrowLeft if wrap is true', () => { - updateTabs({wrap: true}); - left(); + it('should wrap focus with ArrowLeft if wrap is true', async () => { + await updateTabs({wrap: true}); + await left(); expect(isTabFocused(1)).toBe(true); - left(); + await left(); expect(isTabFocused(2)).toBe(true); - left(); + await left(); expect(isTabFocused(0)).toBe(true); }); - it('should not wrap focus with ArrowLeft if wrap is false', () => { - updateTabs({wrap: false}); - left(); - left(); + it('should not wrap focus with ArrowLeft if wrap is false', async () => { + await updateTabs({wrap: false}); + await left(); + await left(); expect(isTabFocused(2)).toBe(true); }); }); describe('orientation="vertical"', () => { - beforeEach(() => { - setupTestTabs({textDirection: 'ltr'}); - updateTabs({ + beforeEach(async () => { + await setupTestTabs({textDirection: 'ltr'}); + await updateTabs({ orientation: 'vertical', initialTabs: [ {value: 'tab1', label: 'Tab 1', content: 'Content 1'}, @@ -412,71 +412,71 @@ describe('Tabs', () => { }); }); - it('should move focus with ArrowDown', () => { + it('should move focus with ArrowDown', async () => { expect(isTabFocused(0)).toBe(true); - down(); + await down(); expect(isTabFocused(1)).toBe(true); }); - it('should move focus with ArrowUp', () => { - down(); + it('should move focus with ArrowUp', async () => { + await down(); expect(isTabFocused(1)).toBe(true); - up(); + await up(); expect(isTabFocused(0)).toBe(true); }); - it('should wrap focus with ArrowDown if wrap is true', () => { - updateTabs({wrap: true}); - down(); + it('should wrap focus with ArrowDown if wrap is true', async () => { + await updateTabs({wrap: true}); + await down(); expect(isTabFocused(1)).toBe(true); - down(); + await down(); expect(isTabFocused(2)).toBe(true); - down(); + await down(); expect(isTabFocused(0)).toBe(true); }); - it('should not wrap focus with ArrowDown if wrap is false', () => { - updateTabs({wrap: false}); - down(); + it('should not wrap focus with ArrowDown if wrap is false', async () => { + await updateTabs({wrap: false}); + await down(); expect(isTabFocused(1)).toBe(true); - down(); + await down(); expect(isTabFocused(2)).toBe(true); - down(); + await down(); expect(isTabFocused(2)).toBe(true); }); - it('should wrap focus with ArrowUp if wrap is true', () => { - updateTabs({wrap: true}); + it('should wrap focus with ArrowUp if wrap is true', async () => { + await updateTabs({wrap: true}); expect(isTabFocused(0)).toBe(true); - up(); + await up(); expect(isTabFocused(2)).toBe(true); }); - it('should not wrap focus with ArrowUp if wrap is false', () => { - updateTabs({wrap: false}); + it('should not wrap focus with ArrowUp if wrap is false', async () => { + await updateTabs({wrap: false}); expect(isTabFocused(0)).toBe(true); - up(); + await up(); expect(isTabFocused(0)).toBe(true); }); - it('should move focus to first tab with Home', () => { - end(); + it('should move focus to first tab with Home', async () => { + await end(); expect(isTabFocused(2)).toBe(true); - home(); + await home(); expect(isTabFocused(0)).toBe(true); }); - it('should move focus to last tab with End', () => { + it('should move focus to last tab with End', async () => { expect(isTabFocused(0)).toBe(true); - end(); + await end(); expect(isTabFocused(2)).toBe(true); }); - it('should not move focus with ArrowLeft/ArrowRight', () => { + it('should not move focus with ArrowLeft/ArrowRight', async () => { expect(isTabFocused(0)).toBe(true); - left(); + await left(); expect(isTabFocused(0)).toBe(true); - right(); + await right(); expect(isTabFocused(0)).toBe(true); }); }); @@ -486,8 +486,8 @@ describe('Tabs', () => { describe('with shuffled items', () => { it('should update collection order when items are shuffled', async () => { - setupTestTabs(); - updateTabs({ + await setupTestTabs(); + await updateTabs({ initialTabs: [ {value: 'tab1', label: 'Tab 1', content: 'Content 1'}, {value: 'tab2', label: 'Tab 2', content: 'Content 2'}, @@ -503,7 +503,7 @@ describe('Tabs', () => { // Shuffle (reverse) data const items = testComponent.tabsData().reverse(); testComponent.tabsData.set([...items]); - fixture.detectChanges(); + await fixture.whenStable(); await waitForMicrotasks(); // Re-query elements to check new DOM order @@ -516,9 +516,9 @@ describe('Tabs', () => { }); describe('Tab selection', () => { - beforeEach(() => { - setupTestTabs(); - updateTabs({ + beforeEach(async () => { + await setupTestTabs(); + await updateTabs({ initialTabs: [ {value: 'tab1', label: 'Tab 1', content: 'Content 1'}, {value: 'tab2', label: 'Tab 2', content: 'Content 2'}, @@ -529,147 +529,147 @@ describe('Tabs', () => { }); describe('selectionMode="follow"', () => { - beforeEach(() => { - updateTabs({selectionMode: 'follow'}); + beforeEach(async () => { + await updateTabs({selectionMode: 'follow'}); }); - it('should select tab on focus via ArrowKeys', () => { - updateTabs({selectedTab: 'tab1'}); + it('should select tab on focus via ArrowKeys', async () => { + await updateTabs({selectedTab: 'tab1'}); expect(testComponent.selectedTab()).toBe('tab1'); expect(tabElements[0].getAttribute('aria-selected')).toBe('true'); - right(); + await right(); expect(testComponent.selectedTab()).toBe('tab2'); expect(tabElements[1].getAttribute('aria-selected')).toBe('true'); expect(tabElements[0].getAttribute('aria-selected')).toBe('false'); - left(); + await left(); expect(testComponent.selectedTab()).toBe('tab1'); expect(tabElements[0].getAttribute('aria-selected')).toBe('true'); expect(tabElements[1].getAttribute('aria-selected')).toBe('false'); }); - it('should select tab on focus via Home/End', () => { - updateTabs({selectedTab: 'tab2'}); + it('should select tab on focus via Home/End', async () => { + await updateTabs({selectedTab: 'tab2'}); expect(testComponent.selectedTab()).toBe('tab2'); - home(); + await home(); expect(testComponent.selectedTab()).toBe('tab1'); expect(tabElements[0].getAttribute('aria-selected')).toBe('true'); - end(); + await end(); expect(testComponent.selectedTab()).toBe('tab3'); expect(tabElements[2].getAttribute('aria-selected')).toBe('true'); }); - it('should select tab on click', () => { - updateTabs({selectedTab: 'tab1'}); + it('should select tab on click', async () => { + await updateTabs({selectedTab: 'tab1'}); expect(testComponent.selectedTab()).toBe('tab1'); - click(tabElements[1]); + await click(tabElements[1]); expect(testComponent.selectedTab()).toBe('tab2'); expect(tabElements[1].getAttribute('aria-selected')).toBe('true'); }); - it('should not change selection with Space/Enter on already selected tab', () => { - updateTabs({selectedTab: 'tab1'}); + it('should not change selection with Space/Enter on already selected tab', async () => { + await updateTabs({selectedTab: 'tab1'}); expect(testComponent.selectedTab()).toBe('tab1'); - space(); + await space(); expect(testComponent.selectedTab()).toBe('tab1'); - enter(); + await enter(); expect(testComponent.selectedTab()).toBe('tab1'); }); }); describe('selectionMode="explicit"', () => { - beforeEach(() => { - updateTabs({selectionMode: 'explicit'}); + beforeEach(async () => { + await updateTabs({selectionMode: 'explicit'}); }); - it('should not select tab on focus via ArrowKeys', () => { - updateTabs({selectedTab: 'tab1'}); + it('should not select tab on focus via ArrowKeys', async () => { + await updateTabs({selectedTab: 'tab1'}); expect(testComponent.selectedTab()).toBe('tab1'); expect(tabElements[0].getAttribute('aria-selected')).toBe('true'); - right(); + await right(); expect(testComponent.selectedTab()).toBe('tab1'); expect(tabElements[0].getAttribute('aria-selected')).toBe('true'); expect(tabElements[1].getAttribute('aria-selected')).toBe('false'); expect(isTabFocused(1)).toBe(true); - left(); + await left(); expect(testComponent.selectedTab()).toBe('tab1'); expect(tabElements[0].getAttribute('aria-selected')).toBe('true'); expect(tabElements[1].getAttribute('aria-selected')).toBe('false'); expect(isTabFocused(0)).toBe(true); }); - it('should select focused tab on Space', () => { - updateTabs({selectedTab: 'tab1'}); + it('should select focused tab on Space', async () => { + await updateTabs({selectedTab: 'tab1'}); expect(testComponent.selectedTab()).toBe('tab1'); - right(); + await right(); expect(isTabFocused(1)).toBe(true); expect(testComponent.selectedTab()).toBe('tab1'); - space(); + await space(); expect(testComponent.selectedTab()).toBe('tab2'); expect(tabElements[1].getAttribute('aria-selected')).toBe('true'); }); - it('should select focused tab on Enter', () => { - updateTabs({selectedTab: 'tab1'}); + it('should select focused tab on Enter', async () => { + await updateTabs({selectedTab: 'tab1'}); expect(testComponent.selectedTab()).toBe('tab1'); - right(); + await right(); expect(isTabFocused(1)).toBe(true); expect(testComponent.selectedTab()).toBe('tab1'); - enter(); + await enter(); expect(testComponent.selectedTab()).toBe('tab2'); expect(tabElements[1].getAttribute('aria-selected')).toBe('true'); }); - it('should select tab on click', () => { - updateTabs({selectedTab: 'tab1'}); + it('should select tab on click', async () => { + await updateTabs({selectedTab: 'tab1'}); expect(testComponent.selectedTab()).toBe('tab1'); - click(tabElements[1]); + await click(tabElements[1]); expect(testComponent.selectedTab()).toBe('tab2'); expect(tabElements[1].getAttribute('aria-selected')).toBe('true'); }); }); - it('should update selectedTab model on selection change', () => { - updateTabs({selectedTab: 'tab1', selectionMode: 'follow'}); + it('should update selectedTab model on selection change', async () => { + await updateTabs({selectedTab: 'tab1', selectionMode: 'follow'}); expect(testComponent.selectedTab()).toBe('tab1'); - right(); + await right(); expect(testComponent.selectedTab()).toBe('tab2'); - updateTabs({selectionMode: 'explicit'}); - right(); + await updateTabs({selectionMode: 'explicit'}); + await right(); expect(testComponent.selectedTab()).toBe('tab2'); - enter(); + await enter(); expect(testComponent.selectedTab()).toBe('tab3'); - click(tabElements[0]); + await click(tabElements[0]); expect(testComponent.selectedTab()).toBe('tab1'); }); - it('should update selection when selectedTab model changes', () => { - updateTabs({selectedTab: 'tab1'}); + it('should update selection when selectedTab model changes', async () => { + await updateTabs({selectedTab: 'tab1'}); expect(tabElements[0].getAttribute('aria-selected')).toBe('true'); - updateTabs({selectedTab: 'tab2'}); + await updateTabs({selectedTab: 'tab2'}); expect(tabElements[1].getAttribute('aria-selected')).toBe('true'); expect(tabElements[0].getAttribute('aria-selected')).toBe('false'); - updateTabs({selectedTab: 'tab3'}); + await updateTabs({selectedTab: 'tab3'}); expect(tabElements[2].getAttribute('aria-selected')).toBe('true'); expect(tabElements[1].getAttribute('aria-selected')).toBe('false'); }); - it('should not select a disabled tab via click', () => { - updateTabs({ + it('should not select a disabled tab via click', async () => { + await updateTabs({ initialTabs: [ {value: 'tab1', label: 'Tab 1', content: 'Content 1'}, {value: 'tab2', label: 'Tab 2', content: 'Content 2', disabled: true}, @@ -678,13 +678,13 @@ describe('Tabs', () => { selectedTab: 'tab1', }); expect(testComponent.selectedTab()).toBe('tab1'); - click(tabElements[1]); + await click(tabElements[1]); expect(testComponent.selectedTab()).toBe('tab1'); expect(tabElements[1].getAttribute('aria-selected')).toBe('false'); }); - it('should not select a disabled tab via keyboard', () => { - updateTabs({ + it('should not select a disabled tab via keyboard', async () => { + await updateTabs({ initialTabs: [ {value: 'tab1', label: 'Tab 1', content: 'Content 1'}, {value: 'tab2', label: 'Tab 2', content: 'Content 2', disabled: true}, @@ -695,31 +695,31 @@ describe('Tabs', () => { softDisabled: true, }); expect(testComponent.selectedTab()).toBe('tab1'); - right(); + await right(); expect(isTabFocused(1)).toBe(true); - enter(); + await enter(); expect(testComponent.selectedTab()).toBe('tab1'); expect(tabElements[1].getAttribute('aria-selected')).toBe('false'); }); - it('should not change selection if tablist is disabled', () => { - updateTabs({selectedTab: 'tab1', disabled: true}); + it('should not change selection if tablist is disabled', async () => { + await updateTabs({selectedTab: 'tab1', disabled: true}); expect(testComponent.selectedTab()).toBe('tab1'); - click(tabElements[1]); + await click(tabElements[1]); expect(testComponent.selectedTab()).toBe('tab1'); - right(); + await right(); expect(testComponent.selectedTab()).toBe('tab1'); }); - it('should handle initial selection via input', () => { - updateTabs({selectedTab: 'tab2'}); + it('should handle initial selection via input', async () => { + await updateTabs({selectedTab: 'tab2'}); expect(testComponent.selectedTab()).toBe('tab2'); expect(tabElements[1].getAttribute('aria-selected')).toBe('true'); expect(tabElements[0].getAttribute('aria-selected')).toBe('false'); }); - it('should allow programmatic selection even if disabled', () => { - updateTabs({disabled: true}); + it('should allow programmatic selection even if disabled', async () => { + await updateTabs({disabled: true}); expect(tabElements[0].getAttribute('aria-selected')).toBe('true'); expect(tabElements[1].getAttribute('aria-selected')).toBe('false'); @@ -729,7 +729,7 @@ describe('Tabs', () => { expect(tabPanelElements[1].hasAttribute('inert')).toBe(true); expect(tabPanelElements[2].hasAttribute('inert')).toBe(true); - updateTabs({selectedTab: 'tab2'}); + await updateTabs({selectedTab: 'tab2'}); expect(tabElements[0].getAttribute('aria-selected')).toBe('false'); expect(tabElements[1].getAttribute('aria-selected')).toBe('true'); @@ -742,9 +742,9 @@ describe('Tabs', () => { }); describe('Dynamic tabs', () => { - beforeEach(() => { - setupTestTabs(); - updateTabs({ + beforeEach(async () => { + await setupTestTabs(); + await updateTabs({ initialTabs: [ {value: 'tab1', label: 'Tab 1', content: 'Content 1'}, {value: 'tab2', label: 'Tab 2', content: 'Content 2'}, @@ -754,20 +754,20 @@ describe('Tabs', () => { }); }); - it('should update selection when active tab is removed', () => { + it('should update selection when active tab is removed', async () => { expect(testComponent.selectedTab()).toBe('tab2'); testComponent.tabsData.set([ {value: 'tab1', label: 'Tab 1', content: 'Content 1'}, {value: 'tab3', label: 'Tab 3', content: 'Content 3'}, ]); - fixture.detectChanges(); + await fixture.whenStable(); defineTestVariables(); expect(testComponent.selectedTab()).toBeUndefined(); }); - it('should maintain selection when a new tab is added', () => { + it('should maintain selection when a new tab is added', async () => { expect(testComponent.selectedTab()).toBe('tab2'); testComponent.tabsData.set([ @@ -776,7 +776,7 @@ describe('Tabs', () => { {value: 'tab3', label: 'Tab 3', content: 'Content 3'}, {value: 'tab4', label: 'Tab 4', content: 'Content 4'}, ]); - fixture.detectChanges(); + await fixture.whenStable(); defineTestVariables(); expect(testComponent.selectedTab()).toBe('tab2'); @@ -814,9 +814,9 @@ describe('Tabs', () => { consoleSpy = spyOn(console, 'warn'); }); - afterEach(() => { + afterEach(async () => { TestBed.resetTestingModule(); - setupTestTabs(); + await setupTestTabs(); }); it('should warn when ngTab is missing its corresponding ngTabPanel', () => { diff --git a/src/aria/toolbar/toolbar.spec.ts b/src/aria/toolbar/toolbar.spec.ts index 8b7060ec3e1e..5ffecc259df2 100644 --- a/src/aria/toolbar/toolbar.spec.ts +++ b/src/aria/toolbar/toolbar.spec.ts @@ -18,7 +18,7 @@ describe('Toolbar', () => { let fixture: ComponentFixture; let toolbarElement: HTMLElement; - const keydown = (key: string, target?: HTMLElement, modifierKeys: {} = {}) => { + const keydown = async (key: string, target?: HTMLElement, modifierKeys: {} = {}) => { const eventTarget = target || toolbarElement; eventTarget.dispatchEvent( new KeyboardEvent('keydown', { @@ -27,28 +27,31 @@ describe('Toolbar', () => { ...modifierKeys, }), ); - fixture.detectChanges(); + await fixture.whenStable(); }; - const click = (element: HTMLElement, eventInit?: PointerEventInit) => { + const click = async (element: HTMLElement, eventInit?: PointerEventInit) => { element.dispatchEvent( // Include pointerType to better simulate a real mouse click v.s. enter keyboard event. new PointerEvent('click', {bubbles: true, pointerType: 'mouse', ...eventInit}), ); - fixture.detectChanges(); + await fixture.whenStable(); }; - const right = (target?: HTMLElement, modifierKeys?: {}) => - keydown('ArrowRight', target, modifierKeys); - const left = (target?: HTMLElement, modifierKeys?: {}) => - keydown('ArrowLeft', target, modifierKeys); - const up = (target?: HTMLElement, modifierKeys?: {}) => keydown('ArrowUp', target, modifierKeys); - const down = (target?: HTMLElement, modifierKeys?: {}) => - keydown('ArrowDown', target, modifierKeys); - const home = (target?: HTMLElement, modifierKeys?: {}) => keydown('Home', target, modifierKeys); - const end = (target?: HTMLElement, modifierKeys?: {}) => keydown('End', target, modifierKeys); - - function setupToolbar( + const right = async (target?: HTMLElement, modifierKeys?: {}) => + await keydown('ArrowRight', target, modifierKeys); + const left = async (target?: HTMLElement, modifierKeys?: {}) => + await keydown('ArrowLeft', target, modifierKeys); + const up = async (target?: HTMLElement, modifierKeys?: {}) => + await keydown('ArrowUp', target, modifierKeys); + const down = async (target?: HTMLElement, modifierKeys?: {}) => + await keydown('ArrowDown', target, modifierKeys); + const home = async (target?: HTMLElement, modifierKeys?: {}) => + await keydown('Home', target, modifierKeys); + const end = async (target?: HTMLElement, modifierKeys?: {}) => + await keydown('End', target, modifierKeys); + + async function setupToolbar( opts: { orientation?: 'vertical' | 'horizontal'; softDisabled?: boolean; @@ -77,7 +80,7 @@ describe('Toolbar', () => { testComponent.wrap.set(opts.wrap); } - fixture.detectChanges(); + await fixture.whenStable(); defineTestVariables(); } @@ -105,7 +108,7 @@ describe('Toolbar', () => { fixture = TestBed.createComponent( ShuffledToolbarExample, ) as unknown as ComponentFixture; - fixture.detectChanges(); + await fixture.whenStable(); const shuffledToolbarDebugEl = fixture.debugElement.query(By.directive(Toolbar)); const shuffledToolbarInstance = shuffledToolbarDebugEl.injector.get(Toolbar); @@ -117,7 +120,7 @@ describe('Toolbar', () => { const firstItem = items.shift()!; items.push(firstItem); (fixture.componentInstance as unknown as ShuffledToolbarExample).items.set([...items]); - fixture.detectChanges(); + await fixture.whenStable(); await waitForMicrotasks(); const widgetsAfter = shuffledToolbarInstance._itemPatterns(); @@ -128,292 +131,292 @@ describe('Toolbar', () => { describe('Navigation', () => { describe('with horizontal orientation', () => { - it('should navigate on click (horizontal)', () => { - setupToolbar(); + it('should navigate on click (horizontal)', async () => { + await setupToolbar(); const item3 = getWidgetEl('item 3')!; - click(item3); + await click(item3); expect(document.activeElement).toBe(item3); }); describe('with ltr text direction', () => { - beforeEach(() => setupToolbar()); + beforeEach(async () => await setupToolbar()); - it('should navigate next on ArrowRight', () => { + it('should navigate next on ArrowRight', async () => { const item0 = getWidgetEl('item 0')!; - click(item0); - right(); + await click(item0); + await right(); expect(document.activeElement).toBe(getWidgetEl('item 1')); }); - it('should navigate prev on ArrowLeft', () => { + it('should navigate prev on ArrowLeft', async () => { const item1 = getWidgetEl('item 1')!; - click(item1); - left(); + await click(item1); + await left(); expect(document.activeElement).toBe(getWidgetEl('item 0')); }); - it('should not navigate next on ArrowDown when not in a widget group (horizontal, ltr)', () => { + it('should not navigate next on ArrowDown when not in a widget group (horizontal, ltr)', async () => { const item0 = getWidgetEl('item 0')!; - click(item0); - down(); + await click(item0); + await down(); expect(document.activeElement).toBe(item0); }); - it('should not navigate prev on ArrowUp when not in a widget group (horizontal, ltr)', () => { + it('should not navigate prev on ArrowUp when not in a widget group (horizontal, ltr)', async () => { const item0 = getWidgetEl('item 0')!; - click(item0); - up(); + await click(item0); + await up(); expect(document.activeElement).toBe(item0); }); - it('should navigate next in a widget group on ArrowDown (horizontal, ltr)', () => { + it('should navigate next in a widget group on ArrowDown (horizontal, ltr)', async () => { const item2 = getWidgetEl('item 2')!; - click(item2); - down(); + await click(item2); + await down(); expect(document.activeElement).toBe(getWidgetEl('item 3')); }); - it('should navigate prev in a widget group on ArrowUp (horizontal, ltr)', () => { + it('should navigate prev in a widget group on ArrowUp (horizontal, ltr)', async () => { const item3 = getWidgetEl('item 3')!; - click(item3); - up(); + await click(item3); + await up(); expect(document.activeElement).toBe(getWidgetEl('item 2')); }); - it('should navigate last to first in a widget group on ArrowDown (horizontal, ltr)', () => { + it('should navigate last to first in a widget group on ArrowDown (horizontal, ltr)', async () => { const item4 = getWidgetEl('item 4')!; - click(item4); - down(); + await click(item4); + await down(); expect(document.activeElement).toBe(getWidgetEl('item 2')); }); - it('should navigate first to last in a widget group on ArrowUp (horizontal, ltr)', () => { + it('should navigate first to last in a widget group on ArrowUp (horizontal, ltr)', async () => { const item2 = getWidgetEl('item 2')!; - click(item2); - up(); + await click(item2); + await up(); expect(document.activeElement).toBe(getWidgetEl('item 4')); }); describe('with wrap false', () => { - beforeEach(() => { + beforeEach(async () => { fixture.componentInstance.wrap.set(false); - fixture.detectChanges(); + await fixture.whenStable(); }); - it('should not wrap from last to first', () => { + it('should not wrap from last to first', async () => { const item5 = getWidgetEl('item 5')!; - click(item5); - right(); + await click(item5); + await right(); expect(document.activeElement).toBe(item5); }); - it('should not wrap from first to last', () => { + it('should not wrap from first to last', async () => { const item0 = getWidgetEl('item 0')!; - click(item0); - left(); + await click(item0); + await left(); expect(document.activeElement).toBe(item0); }); }); describe('with softDisabled true', () => { - beforeEach(() => { + beforeEach(async () => { fixture.componentInstance.softDisabled.set(true); - fixture.detectChanges(); + await fixture.whenStable(); }); - it('should not skip disabled items when navigating next', () => { + it('should not skip disabled items when navigating next', async () => { fixture.componentInstance.widgets[1].disabled.set(true); - fixture.detectChanges(); - click(getWidgetEl('item 0')!); - right(); + await fixture.whenStable(); + await click(getWidgetEl('item 0')!); + await right(); expect(document.activeElement).toBe(getWidgetEl('item 1')); }); - it('should not skip disabled items when navigating prev', () => { + it('should not skip disabled items when navigating prev', async () => { fixture.componentInstance.widgets[1].disabled.set(true); - fixture.detectChanges(); + await fixture.whenStable(); const item2 = getWidgetEl('item 2')!; - click(item2); - left(); + await click(item2); + await left(); expect(document.activeElement).toBe(getWidgetEl('item 1')); }); - it('should not skip disabled groups when navigating next', () => { + it('should not skip disabled groups when navigating next', async () => { fixture.componentInstance.groups[0].disabled.set(true); - fixture.detectChanges(); + await fixture.whenStable(); const item1 = getWidgetEl('item 1')!; - click(item1); - right(); + await click(item1); + await right(); expect(document.activeElement).toBe(getWidgetEl('item 2')); }); - it('should not skip disabled groups when navigating prev', () => { + it('should not skip disabled groups when navigating prev', async () => { fixture.componentInstance.groups[0].disabled.set(true); - fixture.detectChanges(); + await fixture.whenStable(); const item5 = getWidgetEl('item 5')!; - click(item5); - left(); + await click(item5); + await left(); expect(document.activeElement).toBe(getWidgetEl('item 4')); }); - it('should navigate to the last item on End', () => { + it('should navigate to the last item on End', async () => { const item0 = getWidgetEl('item 0')!; - click(item0); - end(); + await click(item0); + await end(); expect(document.activeElement).toBe(getWidgetEl('item 5')); }); - it('should navigate to the first item on Home', () => { + it('should navigate to the first item on Home', async () => { const item5 = getWidgetEl('item 5')!; - click(item5); - home(); + await click(item5); + await home(); expect(document.activeElement).toBe(getWidgetEl('item 0')); }); describe('with wrap true', () => { - beforeEach(() => { + beforeEach(async () => { fixture.componentInstance.wrap.set(true); - fixture.detectChanges(); + await fixture.whenStable(); }); - it('should wrap from last to first', () => { + it('should wrap from last to first', async () => { const item5 = getWidgetEl('item 5')!; - click(item5); - right(); + await click(item5); + await right(); expect(document.activeElement).toBe(getWidgetEl('item 0')); }); - it('should wrap from first to last', () => { + it('should wrap from first to last', async () => { const item0 = getWidgetEl('item 0')!; - click(item0); - left(); + await click(item0); + await left(); expect(document.activeElement).toBe(getWidgetEl('item 5')); }); }); }); describe('with softDisabled false', () => { - beforeEach(() => { + beforeEach(async () => { fixture.componentInstance.softDisabled.set(false); - fixture.detectChanges(); + await fixture.whenStable(); }); - it('should not navigate to disabled items on click', () => { + it('should not navigate to disabled items on click', async () => { fixture.componentInstance.widgets[1].disabled.set(true); - fixture.detectChanges(); + await fixture.whenStable(); const item1 = getWidgetEl('item 1')!; - click(item1); + await click(item1); expect(document.activeElement).not.toBe(item1); }); - it('should skip disabled items when navigating next', () => { + it('should skip disabled items when navigating next', async () => { fixture.componentInstance.widgets[1].disabled.set(true); - fixture.detectChanges(); + await fixture.whenStable(); const item0 = getWidgetEl('item 0')!; - click(item0); - right(); + await click(item0); + await right(); expect(document.activeElement).toBe(getWidgetEl('item 2')); }); - it('should skip disabled items when navigating prev', () => { + it('should skip disabled items when navigating prev', async () => { fixture.componentInstance.widgets[1].disabled.set(true); - fixture.detectChanges(); + await fixture.whenStable(); const item2 = getWidgetEl('item 2')!; - click(item2); - left(); + await click(item2); + await left(); expect(document.activeElement).toBe(getWidgetEl('item 0')); }); - it('should not navigate to items in disabled groups on click', () => { + it('should not navigate to items in disabled groups on click', async () => { fixture.componentInstance.groups[0].disabled.set(true); - fixture.detectChanges(); + await fixture.whenStable(); const item3 = getWidgetEl('item 3')!; - click(item3); + await click(item3); expect(document.activeElement).not.toBe(item3); }); - it('should skip disabled groups when navigating next', () => { + it('should skip disabled groups when navigating next', async () => { fixture.componentInstance.groups[0].disabled.set(true); - fixture.detectChanges(); + await fixture.whenStable(); const item1 = getWidgetEl('item 1')!; - click(item1); - right(); + await click(item1); + await right(); expect(document.activeElement).toBe(getWidgetEl('item 5')); }); - it('should skip disabled groups when navigating prev', () => { + it('should skip disabled groups when navigating prev', async () => { fixture.componentInstance.groups[0].disabled.set(true); - fixture.detectChanges(); + await fixture.whenStable(); const item5 = getWidgetEl('item 5')!; - click(item5); - left(); + await click(item5); + await left(); expect(document.activeElement).toBe(getWidgetEl('item 1')); }); - it('should navigate to the last focusable item on End', () => { + it('should navigate to the last focusable item on End', async () => { fixture.componentInstance.widgets[5].disabled.set(true); - fixture.detectChanges(); + await fixture.whenStable(); const item0 = getWidgetEl('item 0')!; - click(item0); - end(); + await click(item0); + await end(); expect(document.activeElement).toBe(getWidgetEl('item 4')); }); - it('should navigate to the first focusable item on Home', () => { + it('should navigate to the first focusable item on Home', async () => { fixture.componentInstance.widgets[0].disabled.set(true); - fixture.detectChanges(); + await fixture.whenStable(); const item5 = getWidgetEl('item 5')!; - click(item5); - home(); + await click(item5); + await home(); expect(document.activeElement).toBe(getWidgetEl('item 1')); }); describe('with wrap true', () => { - beforeEach(() => { + beforeEach(async () => { fixture.componentInstance.wrap.set(true); - fixture.detectChanges(); + await fixture.whenStable(); }); - it('should wrap from last to first focusable item', () => { + it('should wrap from last to first focusable item', async () => { fixture.componentInstance.widgets[0].disabled.set(true); - fixture.detectChanges(); + await fixture.whenStable(); const item5 = getWidgetEl('item 5')!; - click(item5); - right(); + await click(item5); + await right(); expect(document.activeElement).toBe(getWidgetEl('item 1')); }); - it('should wrap from first to last focusable item', () => { + it('should wrap from first to last focusable item', async () => { fixture.componentInstance.widgets[5].disabled.set(true); - fixture.detectChanges(); + await fixture.whenStable(); const item0 = getWidgetEl('item 0')!; - click(item0); - left(); + await click(item0); + await left(); expect(document.activeElement).toBe(getWidgetEl('item 4')); }); }); describe('with wrap false', () => { - beforeEach(() => { + beforeEach(async () => { fixture.componentInstance.wrap.set(false); - fixture.detectChanges(); + await fixture.whenStable(); }); - it('should not wrap from last to first focusable item', () => { + it('should not wrap from last to first focusable item', async () => { fixture.componentInstance.widgets[0].disabled.set(true); - fixture.detectChanges(); + await fixture.whenStable(); const item5 = getWidgetEl('item 5')!; - click(item5); - right(); + await click(item5); + await right(); expect(document.activeElement).toBe(item5); }); - it('should not wrap from first to last focusable item', () => { + it('should not wrap from first to last focusable item', async () => { fixture.componentInstance.widgets[5].disabled.set(true); - fixture.detectChanges(); + await fixture.whenStable(); const item0 = getWidgetEl('item 0')!; - click(item0); - left(); + await click(item0); + await left(); expect(document.activeElement).toBe(item0); }); }); @@ -421,282 +424,282 @@ describe('Toolbar', () => { }); describe('with rtl text direction', () => { - beforeEach(() => setupToolbar({textDirection: 'rtl'})); + beforeEach(async () => await setupToolbar({textDirection: 'rtl'})); - it('should navigate on click (horizontal, rtl)', () => { + it('should navigate on click (horizontal, rtl)', async () => { const item3 = getWidgetEl('item 3')!; - click(item3); + await click(item3); expect(document.activeElement).toBe(item3); }); - it('should navigate next on ArrowLeft', () => { + it('should navigate next on ArrowLeft', async () => { const item0 = getWidgetEl('item 0')!; - click(item0); - left(); + await click(item0); + await left(); expect(document.activeElement).toBe(getWidgetEl('item 1')); }); - it('should navigate prev on ArrowRight', () => { - click(getWidgetEl('item 1')!); - right(); + it('should navigate prev on ArrowRight', async () => { + await click(getWidgetEl('item 1')!); + await right(); expect(document.activeElement).toBe(getWidgetEl('item 0')); }); - it('should not navigate next on ArrowDown when not in a widget group (horizontal, rtl)', () => { + it('should not navigate next on ArrowDown when not in a widget group (horizontal, rtl)', async () => { const item0 = getWidgetEl('item 0')!; - click(item0); - down(); + await click(item0); + await down(); expect(document.activeElement).toBe(item0); }); - it('should not navigate prev on ArrowUp when not in a widget group (horizontal, rtl)', () => { + it('should not navigate prev on ArrowUp when not in a widget group (horizontal, rtl)', async () => { const item0 = getWidgetEl('item 0')!; - click(item0); - up(); + await click(item0); + await up(); expect(document.activeElement).toBe(item0); }); - it('should navigate next in a widget group on ArrowDown (horizontal, rtl)', () => { + it('should navigate next in a widget group on ArrowDown (horizontal, rtl)', async () => { const item2 = getWidgetEl('item 2')!; - click(item2); - down(); + await click(item2); + await down(); expect(document.activeElement).toBe(getWidgetEl('item 3')); }); - it('should navigate prev in a widget group on ArrowUp (horizontal, rtl)', () => { + it('should navigate prev in a widget group on ArrowUp (horizontal, rtl)', async () => { const item3 = getWidgetEl('item 3')!; - click(item3); - up(); + await click(item3); + await up(); expect(document.activeElement).toBe(getWidgetEl('item 2')); }); - it('should navigate first to last in a widget group on ArrowUp (horizontal, rtl)', () => { + it('should navigate first to last in a widget group on ArrowUp (horizontal, rtl)', async () => { const item2 = getWidgetEl('item 2')!; - click(item2); - up(); + await click(item2); + await up(); expect(document.activeElement).toBe(getWidgetEl('item 4')); }); - it('should navigate last to first in a widget group on ArrowDown (horizontal, rtl)', () => { + it('should navigate last to first in a widget group on ArrowDown (horizontal, rtl)', async () => { const item4 = getWidgetEl('item 4')!; - click(item4); - down(); + await click(item4); + await down(); expect(document.activeElement).toBe(getWidgetEl('item 2')); }); }); }); describe('with vertical orientation', () => { - beforeEach(() => setupToolbar({orientation: 'vertical'})); + beforeEach(async () => await setupToolbar({orientation: 'vertical'})); - it('should navigate next on ArrowDown', () => { + it('should navigate next on ArrowDown', async () => { const item0 = getWidgetEl('item 0')!; - click(item0); - down(); + await click(item0); + await down(); expect(document.activeElement).toBe(getWidgetEl('item 1')); }); - it('should navigate prev on ArrowUp', () => { + it('should navigate prev on ArrowUp', async () => { const item1 = getWidgetEl('item 1')!; - click(item1); - up(); + await click(item1); + await up(); expect(document.activeElement).toBe(getWidgetEl('item 0')); }); - it('should not navigate next on ArrowRight when not in a widget group', () => { + it('should not navigate next on ArrowRight when not in a widget group', async () => { const item0 = getWidgetEl('item 0')!; - click(item0); - right(); + await click(item0); + await right(); expect(document.activeElement).toBe(item0); }); - it('should not navigate prev on ArrowLeft when not in a widget group', () => { + it('should not navigate prev on ArrowLeft when not in a widget group', async () => { const item0 = getWidgetEl('item 0')!; - click(item0); - left(); + await click(item0); + await left(); expect(document.activeElement).toBe(item0); }); - it('should navigate next in a widget group on ArrowRight', () => { + it('should navigate next in a widget group on ArrowRight', async () => { const item2 = getWidgetEl('item 2')!; - click(item2); - right(); + await click(item2); + await right(); expect(document.activeElement).toBe(getWidgetEl('item 3')); }); - it('should navigate prev in a widget group on ArrowLeft', () => { + it('should navigate prev in a widget group on ArrowLeft', async () => { const item3 = getWidgetEl('item 3')!; - click(item3); - left(); + await click(item3); + await left(); expect(document.activeElement).toBe(getWidgetEl('item 2')); }); - it('should navigate last to first in a widget group on ArrowRight', () => { + it('should navigate last to first in a widget group on ArrowRight', async () => { const item4 = getWidgetEl('item 4')!; - click(item4); - right(); + await click(item4); + await right(); expect(document.activeElement).toBe(getWidgetEl('item 2')); }); - it('should navigate first to last in a widget group on ArrowLeft', () => { + it('should navigate first to last in a widget group on ArrowLeft', async () => { const item2 = getWidgetEl('item 2')!; - click(item2); - left(); + await click(item2); + await left(); expect(document.activeElement).toBe(getWidgetEl('item 4')); }); }); describe('with disabled toolbar', () => { - it('should not navigate on any key press', () => { - setupToolbar({disabled: true}); + it('should not navigate on any key press', async () => { + await setupToolbar({disabled: true}); const item0 = getWidgetEl('item 0')!; const initialActiveElement = document.activeElement; - click(item0); + await click(item0); expect(document.activeElement).toBe(initialActiveElement); - right(); + await right(); expect(document.activeElement).toBe(initialActiveElement); - left(); + await left(); expect(document.activeElement).toBe(initialActiveElement); - down(); + await down(); expect(document.activeElement).toBe(initialActiveElement); - up(); + await up(); expect(document.activeElement).toBe(initialActiveElement); - home(); + await home(); expect(document.activeElement).toBe(initialActiveElement); - end(); + await end(); expect(document.activeElement).toBe(initialActiveElement); }); }); describe('with wrapped toolbar widgets', () => { - beforeEach(() => { + beforeEach(async () => { TestBed.configureTestingModule({imports: [WrappedToolbarExample]}); fixture = TestBed.createComponent(WrappedToolbarExample) as any; - fixture.detectChanges(); + await fixture.whenStable(); }); - it('should navigate on click (wrapped)', () => { + it('should navigate on click (wrapped)', async () => { const widgets = fixture.debugElement .queryAll(By.css('[toolbar-button]')) .map((debugEl: DebugElement) => debugEl.nativeElement as HTMLElement); - click(widgets[0]); + await click(widgets[0]); expect(document.activeElement).toBe(widgets[0]); }); }); }); describe('Selection', () => { - beforeEach(() => setupToolbar()); + beforeEach(async () => await setupToolbar()); - it('should toggle the active item on Enter', () => { + it('should toggle the active item on Enter', async () => { const item0 = getWidgetEl('item 0')!; - click(item0); - keydown('Enter'); + await click(item0); + await keydown('Enter'); expect(item0.getAttribute('aria-pressed')).toBe('false'); - keydown('Enter'); + await keydown('Enter'); expect(item0.getAttribute('aria-pressed')).toBe('true'); }); - it('should toggle the active item on Space', () => { + it('should toggle the active item on Space', async () => { const item0 = getWidgetEl('item 0')!; - click(item0); - keydown(' '); + await click(item0); + await keydown(' '); expect(item0.getAttribute('aria-pressed')).toBe('false'); - keydown(' '); + await keydown(' '); expect(item0.getAttribute('aria-pressed')).toBe('true'); }); - it('should toggle the active item on click', () => { + it('should toggle the active item on click', async () => { const item0 = getWidgetEl('item 0')!; - click(item0); + await click(item0); expect(item0.getAttribute('aria-pressed')).toBe('true'); - click(item0); + await click(item0); expect(item0.getAttribute('aria-pressed')).toBe('false'); }); - it('should be able to select multiple items in the toolbar', () => { + it('should be able to select multiple items in the toolbar', async () => { const item0 = getWidgetEl('item 0')!; const item1 = getWidgetEl('item 1')!; - click(item0); - click(item1); + await click(item0); + await click(item1); expect(item0.getAttribute('aria-pressed')).toBe('true'); expect(item1.getAttribute('aria-pressed')).toBe('true'); }); - it('should not be able to select multiple items in a group', () => { + it('should not be able to select multiple items in a group', async () => { const item2 = getWidgetEl('item 2')!; const item3 = getWidgetEl('item 3')!; - click(item2); - click(item3); + await click(item2); + await click(item3); expect(item2.getAttribute('aria-pressed')).toBe('false'); expect(item3.getAttribute('aria-pressed')).toBe('true'); }); - it('should not select disabled items', () => { + it('should not select disabled items', async () => { fixture.componentInstance.widgets[1].disabled.set(true); - fixture.detectChanges(); + await fixture.whenStable(); const item1 = getWidgetEl('item 1')!; - click(item1); + await click(item1); expect(item1.getAttribute('aria-pressed')).toBe('false'); }); - it('should not select items in a disabled group', () => { + it('should not select items in a disabled group', async () => { fixture.componentInstance.groups[0].disabled.set(true); - fixture.detectChanges(); + await fixture.whenStable(); const item2 = getWidgetEl('item 2')!; - click(item2); + await click(item2); expect(item2.getAttribute('aria-pressed')).toBe('false'); }); }); describe('ARIA attributes and roles', () => { - beforeEach(() => setupToolbar()); + beforeEach(async () => await setupToolbar()); it('should have role="toolbar"', () => { expect(toolbarElement.getAttribute('role')).toBe('toolbar'); }); - it('should set aria-orientation based on input', () => { + it('should set aria-orientation based on input', async () => { expect(toolbarElement.getAttribute('aria-orientation')).toBe('horizontal'); fixture.componentInstance.orientation.set('vertical'); - fixture.detectChanges(); + await fixture.whenStable(); expect(toolbarElement.getAttribute('aria-orientation')).toBe('vertical'); }); - it('should set aria-disabled based on input', () => { + it('should set aria-disabled based on input', async () => { expect(toolbarElement.getAttribute('aria-disabled')).toBe('false'); fixture.componentInstance.disabled.set(true); - fixture.detectChanges(); + await fixture.whenStable(); expect(toolbarElement.getAttribute('aria-disabled')).toBe('true'); }); }); describe('Focus management', () => { - beforeEach(() => setupToolbar()); + beforeEach(async () => await setupToolbar()); - it('should have tabindex on widgets set by active state', () => { + it('should have tabindex on widgets set by active state', async () => { const widgets = getWidgetEls(); expect(widgets[0].getAttribute('tabindex')).toBe('0'); expect(widgets[1].getAttribute('tabindex')).toBe('-1'); - click(widgets[1]); + await click(widgets[1]); expect(widgets[0].getAttribute('tabindex')).toBe('-1'); expect(widgets[1].getAttribute('tabindex')).toBe('0'); }); }); describe('Hard disabled state attributes', () => { - beforeEach(() => setupToolbar({softDisabled: false})); + beforeEach(async () => await setupToolbar({softDisabled: false})); - it('should set inert and disabled attributes on hard-disabled widgets', () => { + it('should set inert and disabled attributes on hard-disabled widgets', async () => { fixture.componentInstance.widgets[0].disabled.set(true); - fixture.detectChanges(); + await fixture.whenStable(); const widgets = getWidgetEls(); expect(widgets[0].hasAttribute('inert')).toBe(true); @@ -711,9 +714,9 @@ describe('Toolbar', () => { consoleSpy = spyOn(console, 'warn'); }); - afterEach(() => { + afterEach(async () => { TestBed.resetTestingModule(); - setupToolbar(); + await setupToolbar(); }); it('should warn when duplicate values are detected inside ngToolbar', () => { diff --git a/src/aria/tree/tree.spec.ts b/src/aria/tree/tree.spec.ts index e54c2b8c09da..28f2c17a6795 100644 --- a/src/aria/tree/tree.spec.ts +++ b/src/aria/tree/tree.spec.ts @@ -23,42 +23,42 @@ describe('Tree', () => { let treeInstance: Tree; let treeItemElements: HTMLElement[]; - const keydown = (key: string, modifierKeys: ModifierKeys = {}) => { + const keydown = async (key: string, modifierKeys: ModifierKeys = {}) => { const event = new KeyboardEvent('keydown', {key, bubbles: true, ...modifierKeys}); treeElement.dispatchEvent(event); - fixture.detectChanges(); + await fixture.whenStable(); defineTestVariables(); }; - const up = (modifierKeys?: ModifierKeys) => keydown('ArrowUp', modifierKeys); - const down = (modifierKeys?: ModifierKeys) => keydown('ArrowDown', modifierKeys); - const left = (modifierKeys?: ModifierKeys) => keydown('ArrowLeft', modifierKeys); - const right = (modifierKeys?: ModifierKeys) => keydown('ArrowRight', modifierKeys); - const home = (modifierKeys?: ModifierKeys) => keydown('Home', modifierKeys); - const end = (modifierKeys?: ModifierKeys) => keydown('End', modifierKeys); - const enter = (modifierKeys?: ModifierKeys) => keydown('Enter', modifierKeys); - const space = (modifierKeys?: ModifierKeys) => keydown(' ', modifierKeys); - const shift = () => keydown('Shift'); - const type = (chars: string) => { + const up = async (modifierKeys?: ModifierKeys) => await keydown('ArrowUp', modifierKeys); + const down = async (modifierKeys?: ModifierKeys) => await keydown('ArrowDown', modifierKeys); + const left = async (modifierKeys?: ModifierKeys) => await keydown('ArrowLeft', modifierKeys); + const right = async (modifierKeys?: ModifierKeys) => await keydown('ArrowRight', modifierKeys); + const home = async (modifierKeys?: ModifierKeys) => await keydown('Home', modifierKeys); + const end = async (modifierKeys?: ModifierKeys) => await keydown('End', modifierKeys); + const enter = async (modifierKeys?: ModifierKeys) => await keydown('Enter', modifierKeys); + const space = async (modifierKeys?: ModifierKeys) => await keydown(' ', modifierKeys); + const shift = async () => await keydown('Shift'); + const type = async (chars: string) => { for (const char of chars) { - keydown(char); + await keydown(char); } }; - const clickHelper = (target: HTMLElement, eventInit: PointerEventInit = {}) => { + const clickHelper = async (target: HTMLElement, eventInit: PointerEventInit = {}) => { target.dispatchEvent( new PointerEvent('click', { bubbles: true, ...eventInit, }), ); - fixture.detectChanges(); + await fixture.whenStable(); defineTestVariables(); }; - const click = (target: HTMLElement) => clickHelper(target); - const shiftClick = (target: HTMLElement) => clickHelper(target, {shiftKey: true}); - const ctrlClick = (target: HTMLElement) => clickHelper(target, {ctrlKey: true}); + const click = async (target: HTMLElement) => await clickHelper(target); + const shiftClick = async (target: HTMLElement) => await clickHelper(target, {shiftKey: true}); + const ctrlClick = async (target: HTMLElement) => await clickHelper(target, {ctrlKey: true}); - function setupTestTree(textDirection: Direction = 'ltr') { + async function setupTestTree(textDirection: Direction = 'ltr') { TestBed.configureTestingModule({ providers: [provideFakeDirectionality(textDirection)], }); @@ -66,7 +66,7 @@ describe('Tree', () => { fixture = TestBed.createComponent(TestTreeComponent); testComponent = fixture.componentInstance; - fixture.detectChanges(); + await fixture.whenStable(); defineTestVariables(); } @@ -79,7 +79,7 @@ describe('Tree', () => { treeItemElements = treeItemDebugElements.map(debugEl => debugEl.nativeElement); } - function updateTree( + async function updateTree( config: { nodes?: TestTreeNode[]; value?: string[]; @@ -108,11 +108,11 @@ describe('Tree', () => { if (config.currentType !== undefined) testComponent.currentType.set(config.currentType); if (config.tabIndex !== undefined) testComponent.tabIndex.set(config.tabIndex); - fixture.detectChanges(); + await fixture.whenStable(); defineTestVariables(); } - function updateTreeItemByValue(value: string, config: Partial>) { + async function updateTreeItemByValue(value: string, config: Partial>) { const newNodes = JSON.parse(JSON.stringify(testComponent.nodes())); const childrenList = [newNodes]; while (childrenList.length > 0) { @@ -124,7 +124,7 @@ describe('Tree', () => { if (config.children !== undefined) node.children = config.children; if (config.disabled !== undefined) node.disabled = config.disabled; if (config.selectable !== undefined) node.selectable = config.selectable; - updateTree({nodes: newNodes}); + await updateTree({nodes: newNodes}); return; } if (node.children) { @@ -151,26 +151,26 @@ describe('Tree', () => { return item?.getAttribute('data-value') ?? undefined; } - function expandAll() { + async function expandAll() { const fruitsEl = getTreeItemElementByValue('fruits')!; - click(fruitsEl); + await click(fruitsEl); const berriesEl = getTreeItemElementByValue('berries')!; - click(berriesEl); + await click(berriesEl); const vegetablesEl = getTreeItemElementByValue('vegetables')!; - click(vegetablesEl); - updateTree({value: []}); + await click(vegetablesEl); + await updateTree({value: []}); } afterEach(async () => { - fixture.detectChanges(); + await fixture.whenStable(); await runAccessibilityChecks(fixture.nativeElement); }); describe('dynamic updates', () => { it('should update item order correctly after items are shuffled', async () => { - setupTestTree(); - expandAll(); - fixture.detectChanges(); + await setupTestTree(); + await expandAll(); + await fixture.whenStable(); const treeDirective = fixture.debugElement.query(By.directive(Tree)).injector.get(Tree); const itemsBefore = treeDirective._pattern.inputs.items(); @@ -182,7 +182,7 @@ describe('Tree', () => { const firstNode = nodes.shift()!; nodes.push(firstNode); testComponent.nodes.set([...nodes]); - fixture.detectChanges(); + await fixture.whenStable(); await waitForMicrotasks(); const itemsAfter = treeDirective._pattern.inputs.items(); @@ -198,9 +198,9 @@ describe('Tree', () => { consoleSpy = spyOn(console, 'warn'); }); - afterEach(() => { + afterEach(async () => { TestBed.resetTestingModule(); - setupTestTree(); + await setupTestTree(); }); it('should warn when duplicate values are detected inside ngTree', () => { @@ -232,16 +232,16 @@ describe('Tree', () => { describe('ARIA attributes and roles', () => { describe('default configuration', () => { - beforeEach(() => { - setupTestTree(); + beforeEach(async () => { + await setupTestTree(); }); it('should correctly set the role attribute to "tree" for Tree', () => { expect(treeElement.getAttribute('role')).toBe('tree'); }); - it('should correctly set the role attribute to "treeitem" for TreeItem', () => { - expandAll(); + it('should correctly set the role attribute to "treeitem" for TreeItem', async () => { + await expandAll(); expect(getTreeItemElementByValue('fruits')!.getAttribute('role')).toBe('treeitem'); expect(getTreeItemElementByValue('vegetables')!.getAttribute('role')).toBe('treeitem'); @@ -280,8 +280,8 @@ describe('Tree', () => { expect(fruitsItem.getAttribute('aria-expanded')).toBe('false'); }); - it('should set aria-level, aria-setsize, and aria-posinset correctly', () => { - expandAll(); + it('should set aria-level, aria-setsize, and aria-posinset correctly', async () => { + await expandAll(); const fruits = getTreeItemElementByValue('fruits')!; expect(fruits.getAttribute('aria-level')).toBe('1'); @@ -324,43 +324,43 @@ describe('Tree', () => { }); describe('custom configuration', () => { - beforeEach(() => { - setupTestTree(); + beforeEach(async () => { + await setupTestTree(); }); - it('should set aria-orientation to "horizontal"', () => { - updateTree({orientation: 'horizontal'}); + it('should set aria-orientation to "horizontal"', async () => { + await updateTree({orientation: 'horizontal'}); expect(treeElement.getAttribute('aria-orientation')).toBe('horizontal'); }); - it('should set aria-multiselectable to "true"', () => { - updateTree({multi: true}); + it('should set aria-multiselectable to "true"', async () => { + await updateTree({multi: true}); expect(treeElement.getAttribute('aria-multiselectable')).toBe('true'); }); - it('should set aria-disabled to "true" for the tree', () => { - updateTree({disabled: true}); + it('should set aria-disabled to "true" for the tree', async () => { + await updateTree({disabled: true}); expect(treeElement.getAttribute('aria-disabled')).toBe('true'); }); - it('should be able to override tabindex', () => { - updateTree({tabIndex: -1}); + it('should be able to override tabindex', async () => { + await updateTree({tabIndex: -1}); expect(treeElement.getAttribute('tabindex')).toBe('-1'); }); - it('should set aria-disabled to "true" for disabled items', () => { - updateTreeItemByValue('fruits', {disabled: true}); + it('should set aria-disabled to "true" for disabled items', async () => { + await updateTreeItemByValue('fruits', {disabled: true}); const fruitsItem = getTreeItemElementByValue('fruits')!; expect(fruitsItem.getAttribute('aria-disabled')).toBe('true'); }); - it('should set aria-selected to "true" for selected items', () => { - expandAll(); - updateTree({value: ['apple']}); + it('should set aria-selected to "true" for selected items', async () => { + await expandAll(); + await updateTree({value: ['apple']}); const appleItem = getTreeItemElementByValue('apple')!; expect(appleItem.getAttribute('aria-selected')).toBe('true'); @@ -368,76 +368,76 @@ describe('Tree', () => { expect(fruitsItem.getAttribute('aria-selected')).toBe('false'); }); - it('should set aria-expanded to "true" for expanded items', () => { - right(); + it('should set aria-expanded to "true" for expanded items', async () => { + await right(); const fruitsItem = getTreeItemElementByValue('fruits')!; expect(fruitsItem.getAttribute('aria-expanded')).toBe('true'); }); - it('should set aria-current to specific current type when nav="true"', () => { - expandAll(); - updateTree({nav: true, value: ['apple']}); + it('should set aria-current to specific current type when nav="true"', async () => { + await expandAll(); + await updateTree({nav: true, value: ['apple']}); const appleItem = getTreeItemElementByValue('apple')!; const bananaItem = getTreeItemElementByValue('banana')!; expect(appleItem.getAttribute('aria-current')).toBe('page'); expect(bananaItem.hasAttribute('aria-current')).toBe(false); - updateTree({currentType: 'location'}); + await updateTree({currentType: 'location'}); expect(appleItem.getAttribute('aria-current')).toBe('location'); }); - it('should not set aria-current when not selectable', () => { - expandAll(); - updateTree({nav: true, value: ['apple']}); + it('should not set aria-current when not selectable', async () => { + await expandAll(); + await updateTree({nav: true, value: ['apple']}); const appleItem = getTreeItemElementByValue('apple')!; expect(appleItem.getAttribute('aria-current')).toBe('page'); - updateTreeItemByValue('apple', {selectable: false}); + await updateTreeItemByValue('apple', {selectable: false}); expect(appleItem.hasAttribute('aria-current')).toBe(false); }); - it('should not set aria-selected when nav="true"', () => { - expandAll(); + it('should not set aria-selected when nav="true"', async () => { + await expandAll(); - updateTree({value: ['apple'], nav: true}); + await updateTree({value: ['apple'], nav: true}); const appleItem = getTreeItemElementByValue('apple')!; expect(appleItem.hasAttribute('aria-selected')).toBe(false); - updateTree({nav: false}); + await updateTree({nav: false}); expect(appleItem.getAttribute('aria-selected')).toBe('true'); }); - it('should not set aria-selected when not selectable', () => { - expandAll(); - updateTree({value: ['apple']}); + it('should not set aria-selected when not selectable', async () => { + await expandAll(); + await updateTree({value: ['apple']}); const appleItem = getTreeItemElementByValue('apple')!; expect(appleItem.getAttribute('aria-selected')).toBe('true'); - updateTreeItemByValue('apple', {selectable: false}); + await updateTreeItemByValue('apple', {selectable: false}); expect(appleItem.hasAttribute('aria-selected')).toBe(false); }); }); describe('roving focus mode (focusMode="roving")', () => { - beforeEach(() => { - setupTestTree(); - updateTree({focusMode: 'roving'}); + beforeEach(async () => { + await setupTestTree(); + await updateTree({focusMode: 'roving'}); }); it('should set tabindex="-1" for the tree', () => { expect(treeElement.getAttribute('tabindex')).toBe('-1'); }); - it('should set tabindex="0" for the tree when disabled when softDisabled is false', () => { - updateTree({disabled: true, focusMode: 'roving', softDisabled: false}); + it('should set tabindex="0" for the tree when disabled when softDisabled is false', async () => { + await updateTree({disabled: true, focusMode: 'roving', softDisabled: false}); expect(treeElement.getAttribute('tabindex')).toBe('0'); }); - it('should set tabindex="0" for the tree when disabled when softDisabled is true', () => { - updateTree({disabled: true, focusMode: 'roving'}); + it('should set tabindex="0" for the tree when disabled when softDisabled is true', async () => { + await updateTree({disabled: true, focusMode: 'roving'}); expect(treeElement.getAttribute('tabindex')).toBe('0'); }); @@ -454,8 +454,8 @@ describe('Tree', () => { expect(dairyItem.getAttribute('tabindex')).toBe('-1'); }); - it('should set initial focus (tabindex="0") on the first selected item', () => { - updateTree({value: ['vegetables', 'dairy'], focusMode: 'roving'}); + it('should set initial focus (tabindex="0") on the first selected item', async () => { + await updateTree({value: ['vegetables', 'dairy'], focusMode: 'roving'}); const fruitsItem = getTreeItemElementByValue('fruits')!; const vegetablesItem = getTreeItemElementByValue('vegetables')!; @@ -474,9 +474,9 @@ describe('Tree', () => { }); describe('activedescendant focus mode (focusMode="activedescendant")', () => { - beforeEach(() => { - setupTestTree(); - updateTree({focusMode: 'activedescendant'}); + beforeEach(async () => { + await setupTestTree(); + await updateTree({focusMode: 'activedescendant'}); }); it('should set tabindex="0" for the tree', () => { @@ -488,15 +488,15 @@ describe('Tree', () => { expect(treeElement.getAttribute('aria-activedescendant')).toBe(fruitsItem.id); }); - it('should set aria-activedescendant to the ID of the first selected item', () => { - updateTree({value: ['vegetables', 'dairy'], focusMode: 'activedescendant'}); + it('should set aria-activedescendant to the ID of the first selected item', async () => { + await updateTree({value: ['vegetables', 'dairy'], focusMode: 'activedescendant'}); const vegetablesItem = getTreeItemElementByValue('vegetables')!; expect(treeElement.getAttribute('aria-activedescendant')).toBe(vegetablesItem.id); }); - it('should set tabindex="-1" for all items', () => { - expandAll(); + it('should set tabindex="-1" for all items', async () => { + await expandAll(); expect(getTreeItemElementByValue('fruits')!.getAttribute('tabindex')).toBe('-1'); expect(getTreeItemElementByValue('apple')!.getAttribute('tabindex')).toBe('-1'); @@ -514,10 +514,10 @@ describe('Tree', () => { }); describe('value and selection', () => { - it('should select items based on the initial value input', () => { - setupTestTree(); - expandAll(); - updateTree({value: ['apple', 'strawberry', 'carrot']}); + it('should select items based on the initial value input', async () => { + await setupTestTree(); + await expandAll(); + await updateTree({value: ['apple', 'strawberry', 'carrot']}); expect(getTreeItemElementByValue('apple')!.getAttribute('aria-selected')).toBe('true'); expect(getTreeItemElementByValue('strawberry')!.getAttribute('aria-selected')).toBe('true'); @@ -527,22 +527,22 @@ describe('Tree', () => { describe('pointer interactions', () => { describe('single select (multi=false, selectionMode="explicit")', () => { - beforeEach(() => { - setupTestTree(); - updateTree({multi: false, selectionMode: 'explicit'}); + beforeEach(async () => { + await setupTestTree(); + await updateTree({multi: false, selectionMode: 'explicit'}); }); - it('should select an item on click and deselect others', () => { - right(); + it('should select an item on click and deselect others', async () => { + await right(); const appleEl = getTreeItemElementByValue('apple')!; const bananaEl = getTreeItemElementByValue('banana')!; - click(appleEl); + await click(appleEl); expect(treeInstance.value()).toEqual(['apple']); expect(appleEl.getAttribute('aria-selected')).toBe('true'); expect(bananaEl.getAttribute('aria-selected')).toBe('false'); - click(bananaEl); + await click(bananaEl); expect(treeInstance.value()).toEqual(['banana']); expect(appleEl.getAttribute('aria-selected')).toBe('false'); expect(bananaEl.getAttribute('aria-selected')).toBe('true'); @@ -550,27 +550,27 @@ describe('Tree', () => { }); describe('multi select (multi=true)', () => { - beforeEach(() => { - setupTestTree(); - updateTree({multi: true}); + beforeEach(async () => { + await setupTestTree(); + await updateTree({multi: true}); // Expands vegetables and fruits - down(); - right(); - up(); - right(); + await down(); + await right(); + await up(); + await right(); }); describe('selectionMode="explicit"', () => { - beforeEach(() => { - updateTree({selectionMode: 'explicit'}); + beforeEach(async () => { + await updateTree({selectionMode: 'explicit'}); }); - it('should select a range with shift+click', () => { + it('should select a range with shift+click', async () => { const appleEl = getTreeItemElementByValue('apple')!; const carrotEl = getTreeItemElementByValue('carrot')!; - click(appleEl); + await click(appleEl); expect(treeInstance.value()).toEqual(['apple']); expect(appleEl.getAttribute('aria-selected')).toBe('true'); @@ -584,27 +584,27 @@ describe('Tree', () => { ]); }); - it('should toggle selection of an item on simple click, leaving other selections intact', () => { + it('should toggle selection of an item on simple click, leaving other selections intact', async () => { const appleEl = getTreeItemElementByValue('apple')!; const bananaEl = getTreeItemElementByValue('banana')!; - click(appleEl); + await click(appleEl); expect(treeInstance.value()).toEqual(['apple']); - click(bananaEl); + await click(bananaEl); expect(treeInstance.value()).toEqual(['apple', 'banana']); - click(appleEl); + await click(appleEl); expect(treeInstance.value()).toEqual(['banana']); }); describe('selectable=false', () => { - it('should not select an item on click', () => { - updateTree({value: ['banana']}); - updateTreeItemByValue('apple', {selectable: false}); + it('should not select an item on click', async () => { + await updateTree({value: ['banana']}); + await updateTreeItemByValue('apple', {selectable: false}); const appleEl = getTreeItemElementByValue('apple')!; - click(appleEl); + await click(appleEl); expect(treeInstance.value()).not.toContain('apple'); expect(treeInstance.value()).toContain('banana'); }); @@ -612,11 +612,11 @@ describe('Tree', () => { }); describe('selectionMode="follow"', () => { - beforeEach(() => { - updateTree({selectionMode: 'follow'}); + beforeEach(async () => { + await updateTree({selectionMode: 'follow'}); }); - it('should select only the clicked item with a simple click (like single select), deselecting others', () => { + it('should select only the clicked item with a simple click (like single select), deselecting others', async () => { const appleEl = getTreeItemElementByValue('apple')!; const bananaEl = getTreeItemElementByValue('banana')!; const carrotEl = getTreeItemElementByValue('carrot')!; @@ -625,18 +625,18 @@ describe('Tree', () => { ctrlClick(bananaEl); expect(treeInstance.value()).toEqual(['apple', 'banana']); - click(carrotEl); + await click(carrotEl); expect(treeInstance.value()).toEqual(['carrot']); - click(appleEl); + await click(appleEl); expect(treeInstance.value()).toEqual(['apple']); }); - it('should add to selection with ctrl+click and toggle individual items', () => { + it('should add to selection with ctrl+click and toggle individual items', async () => { const appleEl = getTreeItemElementByValue('apple')!; const berriesEl = getTreeItemElementByValue('berries')!; - click(appleEl); + await click(appleEl); expect(treeInstance.value()).toEqual(['apple']); ctrlClick(berriesEl); @@ -646,13 +646,13 @@ describe('Tree', () => { expect(treeInstance.value()).toEqual(['berries']); }); - it('should select a range with shift+click, anchoring from last selected/focused', () => { + it('should select a range with shift+click, anchoring from last selected/focused', async () => { const appleEl = getTreeItemElementByValue('apple')!; const berriesEl = getTreeItemElementByValue('berries')!; const carrotEl = getTreeItemElementByValue('carrot')!; const broccoliEl = getTreeItemElementByValue('broccoli')!; - click(appleEl); + await click(appleEl); expect(treeInstance.value()).toEqual(['apple']); shiftClick(carrotEl); @@ -664,7 +664,7 @@ describe('Tree', () => { 'carrot', ]); - click(berriesEl); + await click(berriesEl); expect(treeInstance.value()).toEqual(['berries']); shiftClick(broccoliEl); @@ -679,12 +679,12 @@ describe('Tree', () => { }); describe('selectable=false', () => { - it('should not select a range with shift+click if an item is not selectable', () => { - updateTreeItemByValue('banana', {selectable: false}); + it('should not select a range with shift+click if an item is not selectable', async () => { + await updateTreeItemByValue('banana', {selectable: false}); const appleEl = getTreeItemElementByValue('apple')!; const berriesEl = getTreeItemElementByValue('berries')!; - click(appleEl); + await click(appleEl); shiftClick(berriesEl); expect(treeInstance.value()).not.toContain('banana'); @@ -692,17 +692,17 @@ describe('Tree', () => { expect(treeInstance.value()).toContain('berries'); }); - it('should not toggle selection of an item on simple click', () => { - updateTreeItemByValue('apple', {selectable: false}); + it('should not toggle selection of an item on simple click', async () => { + await updateTreeItemByValue('apple', {selectable: false}); const appleEl = getTreeItemElementByValue('apple')!; - click(appleEl); + await click(appleEl); expect(treeInstance.value()).not.toContain('apple'); }); - it('should not add to selection with ctrl+click', () => { - updateTree({value: ['banana']}); - updateTreeItemByValue('apple', {selectable: false}); + it('should not add to selection with ctrl+click', async () => { + await updateTree({value: ['banana']}); + await updateTreeItemByValue('apple', {selectable: false}); const appleEl = getTreeItemElementByValue('apple')!; ctrlClick(appleEl); @@ -716,167 +716,167 @@ describe('Tree', () => { describe('keyboard interactions', () => { describe('single select (multi=false)', () => { - beforeEach(() => { - setupTestTree(); - updateTree({multi: false}); + beforeEach(async () => { + await setupTestTree(); + await updateTree({multi: false}); }); describe('selectionMode="explicit"', () => { - beforeEach(() => { - updateTree({selectionMode: 'explicit'}); + beforeEach(async () => { + await updateTree({selectionMode: 'explicit'}); }); - it('should select the focused item with Enter and deselect others', () => { - enter(); + it('should select the focused item with Enter and deselect others', async () => { + await enter(); expect(treeInstance.value()).toEqual(['fruits']); - down(); - enter(); + await down(); + await enter(); expect(treeInstance.value()).toEqual(['vegetables']); }); - it('should select the focused item with Space and deselect others', () => { - space(); + it('should select the focused item with Space and deselect others', async () => { + await space(); expect(treeInstance.value()).toEqual(['fruits']); - down(); - space(); + await down(); + await space(); expect(treeInstance.value()).toEqual(['vegetables']); }); - it('should move focus with arrows without changing selection until Enter/Space', () => { - enter(); + it('should move focus with arrows without changing selection until Enter/Space', async () => { + await enter(); expect(treeInstance.value()).toEqual(['fruits']); - down(); + await down(); expect(treeInstance.value()).toEqual(['fruits']); - down(); + await down(); expect(treeInstance.value()).toEqual(['fruits']); - enter(); + await enter(); expect(treeInstance.value()).toEqual(['grains']); }); describe('selectable=false', () => { - it('should not select the focused item with Enter', () => { - updateTreeItemByValue('fruits', {selectable: false}); - enter(); + it('should not select the focused item with Enter', async () => { + await updateTreeItemByValue('fruits', {selectable: false}); + await enter(); expect(treeInstance.value()).toEqual([]); }); - it('should not select the focused item with Space', () => { - updateTreeItemByValue('fruits', {selectable: false}); - space(); + it('should not select the focused item with Space', async () => { + await updateTreeItemByValue('fruits', {selectable: false}); + await space(); expect(treeInstance.value()).toEqual([]); }); }); }); describe('selectionMode="follow"', () => { - beforeEach(() => { - updateTree({selectionMode: 'follow'}); + beforeEach(async () => { + await updateTree({selectionMode: 'follow'}); }); - it('should select an item when it becomes focused with ArrowDown and deselect others', () => { - updateTree({value: ['fruits']}); + it('should select an item when it becomes focused with ArrowDown and deselect others', async () => { + await updateTree({value: ['fruits']}); expect(treeInstance.value()).toEqual(['fruits']); - down(); + await down(); expect(treeInstance.value()).toEqual(['vegetables']); - down(); + await down(); expect(treeInstance.value()).toEqual(['grains']); }); - it('should select an item when it becomes focused with ArrowUp and deselect others', () => { - updateTree({value: ['grains']}); + it('should select an item when it becomes focused with ArrowUp and deselect others', async () => { + await updateTree({value: ['grains']}); - up(); + await up(); expect(treeInstance.value()).toEqual(['vegetables']); }); - it('should select the first item with Home and deselect others', () => { - updateTree({value: ['grains']}); + it('should select the first item with Home and deselect others', async () => { + await updateTree({value: ['grains']}); expect(treeInstance.value()).toEqual(['grains']); - home(); + await home(); expect(treeInstance.value()).toEqual(['fruits']); }); - it('should select the last visible item with End and deselect others', () => { - updateTree({value: ['fruits']}); + it('should select the last visible item with End and deselect others', async () => { + await updateTree({value: ['fruits']}); expect(treeInstance.value()).toEqual(['fruits']); - end(); + await end(); expect(treeInstance.value()).toEqual(['dairy']); }); - it('should select an item via typeahead and deselect others', () => { - updateTree({value: ['fruits']}); + it('should select an item via typeahead and deselect others', async () => { + await updateTree({value: ['fruits']}); expect(treeInstance.value()).toEqual(['fruits']); - type('V'); + await type('V'); expect(treeInstance.value()).toEqual(['vegetables']); }); }); }); describe('multi select (multi=true)', () => { - beforeEach(() => { - setupTestTree(); - updateTree({multi: true}); + beforeEach(async () => { + await setupTestTree(); + await updateTree({multi: true}); }); describe('selectionMode="explicit"', () => { - beforeEach(() => { - updateTree({selectionMode: 'explicit'}); + beforeEach(async () => { + await updateTree({selectionMode: 'explicit'}); }); - it('should toggle selection of the focused item with Space, leaving other selections intact', () => { - space(); + it('should toggle selection of the focused item with Space, leaving other selections intact', async () => { + await space(); expect(treeInstance.value()).toEqual(['fruits']); - down(); - space(); + await down(); + await space(); expect(treeInstance.value().sort()).toEqual(['fruits', 'vegetables']); }); - it('should move focus with arrows without changing selection', () => { - space(); + it('should move focus with arrows without changing selection', async () => { + await space(); expect(treeInstance.value()).toEqual(['fruits']); - down(); + await down(); expect(treeInstance.value()).toEqual(['fruits']); }); - it('should extend selection downwards with Shift+ArrowDown', () => { - shift(); - down({shiftKey: true}); + it('should extend selection downwards with Shift+ArrowDown', async () => { + await shift(); + await down({shiftKey: true}); expect(treeInstance.value().sort()).toEqual(['fruits', 'vegetables']); - down({shiftKey: true}); + await down({shiftKey: true}); expect(treeInstance.value().sort()).toEqual(['fruits', 'grains', 'vegetables']); }); - it('should extend selection upwards with Shift+ArrowUp', () => { - end(); - shift(); - up({shiftKey: true}); + it('should extend selection upwards with Shift+ArrowUp', async () => { + await end(); + await shift(); + await up({shiftKey: true}); expect(treeInstance.value().sort()).toEqual(['dairy', 'grains']); }); - it('Ctrl+A should select all enabled visible items, then deselect all', () => { + it('Ctrl+A should select all enabled visible items, then deselect all', async () => { // Expands vegetables and fruits - down(); - right(); - up(); - right(); + await down(); + await right(); + await up(); + await right(); - updateTreeItemByValue('carrot', {disabled: true}); - updateTreeItemByValue('broccoli', {disabled: true}); + await updateTreeItemByValue('carrot', {disabled: true}); + await updateTreeItemByValue('broccoli', {disabled: true}); - keydown('A', {ctrlKey: true}); + await keydown('A', {ctrlKey: true}); expect(treeInstance.value().sort()).toEqual([ 'apple', 'banana', @@ -887,42 +887,42 @@ describe('Tree', () => { 'vegetables', ]); - keydown('A', {ctrlKey: true}); + await keydown('A', {ctrlKey: true}); expect(treeInstance.value()).toEqual([]); }); - it('Ctrl+ArrowKey should move focus without changing selection', () => { - space(); + it('Ctrl+ArrowKey should move focus without changing selection', async () => { + await space(); expect(treeInstance.value()).toEqual(['fruits']); - down({ctrlKey: true}); + await down({ctrlKey: true}); expect(treeInstance.value()).toEqual(['fruits']); - up({ctrlKey: true}); + await up({ctrlKey: true}); expect(treeInstance.value()).toEqual(['fruits']); }); describe('selectable=false', () => { - it('should not toggle selection of the focused item with Space', () => { - updateTreeItemByValue('fruits', {selectable: false}); - space(); + it('should not toggle selection of the focused item with Space', async () => { + await updateTreeItemByValue('fruits', {selectable: false}); + await space(); expect(treeInstance.value()).toEqual([]); }); - it('should not extend selection with Shift+ArrowDown', () => { - updateTreeItemByValue('vegetables', {selectable: false}); - shift(); - down({shiftKey: true}); - down({shiftKey: true}); + it('should not extend selection with Shift+ArrowDown', async () => { + await updateTreeItemByValue('vegetables', {selectable: false}); + await shift(); + await down({shiftKey: true}); + await down({shiftKey: true}); expect(treeInstance.value()).not.toContain('vegetables'); expect(treeInstance.value().sort()).toEqual(['fruits', 'grains']); }); - it('Ctrl+A should not select non-selectable items', () => { - expandAll(); - updateTreeItemByValue('apple', {selectable: false}); - updateTreeItemByValue('carrot', {selectable: false}); - keydown('A', {ctrlKey: true}); + it('Ctrl+A should not select non-selectable items', async () => { + await expandAll(); + await updateTreeItemByValue('apple', {selectable: false}); + await updateTreeItemByValue('carrot', {selectable: false}); + await keydown('A', {ctrlKey: true}); const value = treeInstance.value(); expect(value).not.toContain('apple'); expect(value).not.toContain('carrot'); @@ -933,110 +933,110 @@ describe('Tree', () => { }); describe('selectionMode="follow"', () => { - beforeEach(() => { - updateTree({selectionMode: 'follow'}); + beforeEach(async () => { + await updateTree({selectionMode: 'follow'}); }); - it('should select the focused item and deselect others on ArrowDown', () => { - updateTree({value: ['fruits']}); + it('should select the focused item and deselect others on ArrowDown', async () => { + await updateTree({value: ['fruits']}); expect(treeInstance.value()).toEqual(['fruits']); - down(); + await down(); expect(treeInstance.value()).toEqual(['vegetables']); }); - it('should select the focused item and deselect others on ArrowUp', () => { - updateTree({value: ['vegetables']}); + it('should select the focused item and deselect others on ArrowUp', async () => { + await updateTree({value: ['vegetables']}); expect(treeInstance.value()).toEqual(['vegetables']); - up(); + await up(); expect(treeInstance.value()).toEqual(['fruits']); }); - it('should move focus without changing selection on Ctrl+ArrowDown', () => { - updateTree({value: ['fruits']}); + it('should move focus without changing selection on Ctrl+ArrowDown', async () => { + await updateTree({value: ['fruits']}); expect(getFocusedTreeItemValue()).toBe('fruits'); - down({ctrlKey: true}); + await down({ctrlKey: true}); expect(treeInstance.value()).toEqual(['fruits']); expect(getFocusedTreeItemValue()).toBe('vegetables'); }); - it('should move focus without changing selection on Ctrl+ArrowUp', () => { - updateTree({value: ['fruits']}); + it('should move focus without changing selection on Ctrl+ArrowUp', async () => { + await updateTree({value: ['fruits']}); - down({ctrlKey: true}); + await down({ctrlKey: true}); expect(getFocusedTreeItemValue()).toBe('vegetables'); - up({ctrlKey: true}); + await up({ctrlKey: true}); expect(treeInstance.value()).toEqual(['fruits']); expect(getFocusedTreeItemValue()).toBe('fruits'); }); - it('should toggle selection of the focused item on Ctrl+Space, adding to existing selection', () => { - updateTree({value: ['fruits']}); - down({ctrlKey: true}); + it('should toggle selection of the focused item on Ctrl+Space, adding to existing selection', async () => { + await updateTree({value: ['fruits']}); + await down({ctrlKey: true}); expect(getFocusedTreeItemValue()).toBe('vegetables'); - space({ctrlKey: true}); + await space({ctrlKey: true}); expect(treeInstance.value().sort()).toEqual(['fruits', 'vegetables']); - space({ctrlKey: true}); + await space({ctrlKey: true}); expect(treeInstance.value()).toEqual(['fruits']); }); - it('should toggle selection of the focused item on Ctrl+Enter, adding to existing selection', () => { - updateTree({value: ['fruits']}); - down({ctrlKey: true}); + it('should toggle selection of the focused item on Ctrl+Enter, adding to existing selection', async () => { + await updateTree({value: ['fruits']}); + await down({ctrlKey: true}); expect(getFocusedTreeItemValue()).toBe('vegetables'); - enter({ctrlKey: true}); + await enter({ctrlKey: true}); expect(treeInstance.value().sort()).toEqual(['fruits', 'vegetables']); - enter({ctrlKey: true}); + await enter({ctrlKey: true}); expect(treeInstance.value()).toEqual(['fruits']); }); - it('should extend selection downwards with Shift+ArrowDown', () => { - right(); // Expands fruits - updateTree({value: ['fruits']}); + it('should extend selection downwards with Shift+ArrowDown', async () => { + await right(); // Expands fruits + await updateTree({value: ['fruits']}); - shift(); - down({shiftKey: true}); + await shift(); + await down({shiftKey: true}); expect(treeInstance.value().sort()).toEqual(['apple', 'fruits']); - down({shiftKey: true}); + await down({shiftKey: true}); expect(treeInstance.value().sort()).toEqual(['apple', 'banana', 'fruits']); }); - it('should extend selection upwards with Shift+ArrowUp', () => { - updateTree({value: ['grains']}); + it('should extend selection upwards with Shift+ArrowUp', async () => { + await updateTree({value: ['grains']}); - shift(); - up({shiftKey: true}); + await shift(); + await up({shiftKey: true}); expect(treeInstance.value().sort()).toEqual(['grains', 'vegetables']); - up({shiftKey: true}); + await up({shiftKey: true}); expect(treeInstance.value().sort()).toEqual(['fruits', 'grains', 'vegetables']); }); - it('should select a range with Shift+Space, anchoring from last selected/focused item', () => { - right(); // Expands fruits - updateTree({value: ['fruits']}); + it('should select a range with Shift+Space, anchoring from last selected/focused item', async () => { + await right(); // Expands fruits + await updateTree({value: ['fruits']}); - down({ctrlKey: true}); - down({ctrlKey: true}); + await down({ctrlKey: true}); + await down({ctrlKey: true}); expect(getFocusedTreeItemValue()).toBe('banana'); - space({shiftKey: true}); + await space({shiftKey: true}); expect(treeInstance.value().sort()).toEqual(['apple', 'banana', 'fruits']); }); - it('Ctrl+A: select all enabled visible items; second Ctrl+A deselects all except focused', () => { - right(); // Expands fruits - updateTreeItemByValue('vegetables', {disabled: true}); + it('Ctrl+A: select all enabled visible items; second Ctrl+A deselects all except focused', async () => { + await right(); // Expands fruits + await updateTreeItemByValue('vegetables', {disabled: true}); - keydown('A', {ctrlKey: true}); + await keydown('A', {ctrlKey: true}); expect(treeInstance.value().sort()).toEqual([ 'apple', 'banana', @@ -1046,68 +1046,68 @@ describe('Tree', () => { 'grains', ]); - keydown('A', {ctrlKey: true}); + await keydown('A', {ctrlKey: true}); expect(treeInstance.value()).toEqual(['fruits']); }); - it('typeahead should select the focused item and deselect others', () => { - updateTree({value: ['fruits']}); - type('V'); + it('typeahead should select the focused item and deselect others', async () => { + await updateTree({value: ['fruits']}); + await type('V'); expect(treeInstance.value()).toEqual(['vegetables']); expect(getFocusedTreeItemValue()).toBe('vegetables'); }); describe('selectable=false', () => { - it('should not select an item on ArrowDown', () => { - updateTreeItemByValue('vegetables', {selectable: false}); - down(); + it('should not select an item on ArrowDown', async () => { + await updateTreeItemByValue('vegetables', {selectable: false}); + await down(); expect(treeInstance.value()).not.toContain('vegetables'); expect(treeInstance.value()).toEqual([]); }); - it('should not toggle selection of the focused item on Ctrl+Space', () => { - updateTreeItemByValue('fruits', {selectable: false}); - space({ctrlKey: true}); + it('should not toggle selection of the focused item on Ctrl+Space', async () => { + await updateTreeItemByValue('fruits', {selectable: false}); + await space({ctrlKey: true}); expect(treeInstance.value()).toEqual([]); }); - it('should not extend selection with Shift+ArrowDown', () => { - updateTreeItemByValue('vegetables', {selectable: false}); - shift(); - down({shiftKey: true}); - down({shiftKey: true}); + it('should not extend selection with Shift+ArrowDown', async () => { + await updateTreeItemByValue('vegetables', {selectable: false}); + await shift(); + await down({shiftKey: true}); + await down({shiftKey: true}); expect(treeInstance.value()).not.toContain('vegetables'); expect(treeInstance.value().sort()).toEqual(['fruits', 'grains']); }); - it('typeahead should not select the focused item', () => { - updateTreeItemByValue('vegetables', {selectable: false}); - type('v'); + it('typeahead should not select the focused item', async () => { + await updateTreeItemByValue('vegetables', {selectable: false}); + await type('v'); expect(getFocusedTreeItemValue()).toBe('vegetables'); expect(treeInstance.value()).not.toContain('vegetables'); }); }); - it('should not select disabled items during Shift+ArrowKey navigation even if softDisabled is true', () => { - right(); // Expands fruits - updateTreeItemByValue('banana', {disabled: true}); - updateTree({value: ['apple'], softDisabled: true}); - down(); // Focus moves to apple + it('should not select disabled items during Shift+ArrowKey navigation even if softDisabled is true', async () => { + await right(); // Expands fruits + await updateTreeItemByValue('banana', {disabled: true}); + await updateTree({value: ['apple'], softDisabled: true}); + await down(); // Focus moves to apple expect(getFocusedTreeItemValue()).toBe('apple'); - keydown('Shift'); - down({shiftKey: true}); + await keydown('Shift'); + await down({shiftKey: true}); expect(getFocusedTreeItemValue()).toBe('banana'); expect(treeInstance.value().sort()).toEqual(['apple']); - down({shiftKey: true}); // Focus 'berries' + await down({shiftKey: true}); // Focus 'berries' expect(getFocusedTreeItemValue()).toBe('berries'); expect(treeInstance.value().sort()).toEqual(['apple', 'berries']); }); - it('should not change selection if tree is disabled', () => { - updateTree({value: ['fruits'], disabled: true}); - down(); + it('should not change selection if tree is disabled', async () => { + await updateTree({value: ['fruits'], disabled: true}); + await down(); expect(treeInstance.value()).toEqual(['fruits']); }); }); @@ -1116,9 +1116,9 @@ describe('Tree', () => { }); describe('expansion and collapse', () => { - it('should expand items by setting expanded input', () => { - setupTestTree(); - updateTree({ + it('should expand items by setting expanded input', async () => { + await setupTestTree(); + await updateTree({ nodes: [ { value: 'fruits', @@ -1146,9 +1146,9 @@ describe('Tree', () => { expect(berriesEl.getAttribute('aria-expanded')).toBe('true'); }); - it('should not affect selected item when collapse', () => { - setupTestTree(); - updateTree({ + it('should not affect selected item when collapse', async () => { + await setupTestTree(); + await updateTree({ nodes: [ { value: 'fruits', @@ -1174,105 +1174,105 @@ describe('Tree', () => { const berriesEl = getTreeItemElementByValue('berries')!; const fruits = getTreeItemElementByValue('fruits')!; - click(blueberryEl); + await click(blueberryEl); expect(treeInstance.value()).toEqual(['blueberry']); - left(); - left(); // collapse berries + await left(); + await left(); // collapse berries expect(berriesEl.getAttribute('aria-expanded')).toBe('false'); expect(treeInstance.value()).toEqual(['blueberry']); - left(); - left(); // collapse fruits + await left(); + await left(); // collapse fruits expect(fruits.getAttribute('aria-expanded')).toBe('false'); expect(treeInstance.value()).toEqual(['blueberry']); }); describe('LTR', () => { - beforeEach(() => { - setupTestTree(); + beforeEach(async () => { + await setupTestTree(); }); describe('orientation="vertical"', () => { - beforeEach(() => { - updateTree({orientation: 'vertical'}); + beforeEach(async () => { + await updateTree({orientation: 'vertical'}); }); - it('should expand a collapsed item with ArrowRight', () => { + it('should expand a collapsed item with ArrowRight', async () => { const fruitsEl = getTreeItemElementByValue('fruits')!; expect(fruitsEl.getAttribute('aria-expanded')).toBe('false'); - right(); + await right(); expect(fruitsEl.getAttribute('aria-expanded')).toBe('true'); }); - it('should move focus to first child if ArrowRight on an expanded item', () => { - right(); // Expands fruits + it('should move focus to first child if ArrowRight on an expanded item', async () => { + await right(); // Expands fruits expect(getFocusedTreeItemValue()).toBe('fruits'); - right(); + await right(); expect(getFocusedTreeItemValue()).toBe('apple'); }); - it('should collapse an expanded item with ArrowLeft', () => { - right(); // Expands fruits + it('should collapse an expanded item with ArrowLeft', async () => { + await right(); // Expands fruits const fruitsEl = getTreeItemElementByValue('fruits')!; expect(fruitsEl.getAttribute('aria-expanded')).toBe('true'); - left(); + await left(); expect(fruitsEl.getAttribute('aria-expanded')).toBe('false'); }); - it('should move focus to parent if ArrowLeft on a collapsed non-root item', () => { - right(); // Expands fruits - right(); // Focus apple (child of fruits) + it('should move focus to parent if ArrowLeft on a collapsed non-root item', async () => { + await right(); // Expands fruits + await right(); // Focus apple (child of fruits) expect(getFocusedTreeItemValue()).toBe('apple'); - left(); + await left(); expect(getFocusedTreeItemValue()).toBe('fruits'); }); }); describe('orientation="horizontal"', () => { - beforeEach(() => { - updateTree({orientation: 'horizontal'}); + beforeEach(async () => { + await updateTree({orientation: 'horizontal'}); }); - it('should expand a collapsed item with ArrowDown', () => { - updateTree({orientation: 'horizontal'}); + it('should expand a collapsed item with ArrowDown', async () => { + await updateTree({orientation: 'horizontal'}); const fruitsEl = getTreeItemElementByValue('fruits')!; expect(fruitsEl.getAttribute('aria-expanded')).toBe('false'); - down(); + await down(); expect(fruitsEl.getAttribute('aria-expanded')).toBe('true'); }); - it('should move focus to first child if ArrowDown on an expanded item', () => { - updateTree({orientation: 'horizontal'}); + it('should move focus to first child if ArrowDown on an expanded item', async () => { + await updateTree({orientation: 'horizontal'}); expect(getFocusedTreeItemValue()).toBe('fruits'); - down(); - down(); + await down(); + await down(); expect(getFocusedTreeItemValue()).toBe('apple'); }); - it('should collapse an expanded item with ArrowUp', () => { - updateTree({orientation: 'horizontal'}); - down(); // Expands fruits + it('should collapse an expanded item with ArrowUp', async () => { + await updateTree({orientation: 'horizontal'}); + await down(); // Expands fruits const fruitsEl = getTreeItemElementByValue('fruits')!; expect(fruitsEl.getAttribute('aria-expanded')).toBe('true'); - up(); + await up(); expect(fruitsEl.getAttribute('aria-expanded')).toBe('false'); }); - it('should move focus to parent if ArrowUp on a collapsed non-root item', () => { - updateTree({orientation: 'horizontal'}); - down(); // Expands fruits - down(); + it('should move focus to parent if ArrowUp on a collapsed non-root item', async () => { + await updateTree({orientation: 'horizontal'}); + await down(); // Expands fruits + await down(); expect(getFocusedTreeItemValue()).toBe('apple'); - up(); + await up(); expect(getFocusedTreeItemValue()).toBe('fruits'); }); }); - it('should expand all sibling items with Shift + *', () => { + it('should expand all sibling items with Shift + *', async () => { const fruitsEl = getTreeItemElementByValue('fruits')!; const vegetablesEl = getTreeItemElementByValue('vegetables')!; const grainsEl = getTreeItemElementByValue('grains')!; @@ -1281,7 +1281,7 @@ describe('Tree', () => { expect(fruitsEl.getAttribute('aria-expanded')).toBe('false'); expect(vegetablesEl.getAttribute('aria-expanded')).toBe('false'); - keydown('*', {shiftKey: true}); + await keydown('*', {shiftKey: true}); expect(fruitsEl.getAttribute('aria-expanded')).toBe('true'); expect(vegetablesEl.getAttribute('aria-expanded')).toBe('true'); @@ -1289,90 +1289,90 @@ describe('Tree', () => { expect(dairyEl.hasAttribute('aria-expanded')).toBe(false); }); - it('should toggle expansion on pointerdown (click) for an expandable item', () => { + it('should toggle expansion on pointerdown (click) for an expandable item', async () => { const fruitsEl = getTreeItemElementByValue('fruits')!; expect(fruitsEl.getAttribute('aria-expanded')).toBe('false'); - click(fruitsEl); + await click(fruitsEl); expect(fruitsEl.getAttribute('aria-expanded')).toBe('true'); expect(getFocusedTreeItemValue()).toBe('fruits'); - click(fruitsEl); + await click(fruitsEl); expect(fruitsEl.getAttribute('aria-expanded')).toBe('false'); }); - it('should not expand a non-expandable item on click', () => { + it('should not expand a non-expandable item on click', async () => { const grainsEl = getTreeItemElementByValue('grains')!; expect(grainsEl.hasAttribute('aria-expanded')).toBe(false); - click(grainsEl); + await click(grainsEl); expect(grainsEl.hasAttribute('aria-expanded')).toBe(false); expect(getFocusedTreeItemValue()).toBe('grains'); }); - it('should not expand a non-expandable item with expand key', () => { + it('should not expand a non-expandable item with expand key', async () => { const grainsEl = getTreeItemElementByValue('grains')!; - down(); - down(); + await down(); + await down(); expect(getFocusedTreeItemValue()).toBe('grains'); - right(); + await right(); expect(grainsEl.hasAttribute('aria-expanded')).toBe(false); expect(getFocusedTreeItemValue()).toBe('grains'); }); - it('should not expand/collapse if item is disabled', () => { - updateTreeItemByValue('fruits', {disabled: true}); + it('should not expand/collapse if item is disabled', async () => { + await updateTreeItemByValue('fruits', {disabled: true}); const fruitsEl = getTreeItemElementByValue('fruits')!; - click(fruitsEl); + await click(fruitsEl); expect(fruitsEl.getAttribute('aria-expanded')).toBe('false'); - right(); + await right(); expect(fruitsEl.getAttribute('aria-expanded')).toBe('false'); }); - it('should not expand/collapse if tree is disabled', () => { - updateTree({disabled: true}); + it('should not expand/collapse if tree is disabled', async () => { + await updateTree({disabled: true}); const fruitsEl = getTreeItemElementByValue('fruits')!; - click(fruitsEl); + await click(fruitsEl); expect(fruitsEl.getAttribute('aria-expanded')).toBe('false'); - right(); + await right(); expect(fruitsEl.getAttribute('aria-expanded')).toBe('false'); }); - it('should do nothing on collapseKey if item is collapsed and is a root item', () => { + it('should do nothing on collapseKey if item is collapsed and is a root item', async () => { const fruitsEl = getTreeItemElementByValue('fruits')!; expect(fruitsEl.getAttribute('aria-expanded')).toBe('false'); expect(getFocusedTreeItemValue()).toBe('fruits'); - left(); + await left(); expect(fruitsEl.getAttribute('aria-expanded')).toBe('false'); expect(getFocusedTreeItemValue()).toBe('fruits'); }); }); describe('RTL', () => { - beforeEach(() => { - setupTestTree('rtl'); + beforeEach(async () => { + await setupTestTree('rtl'); }); describe('orientation="vertical"', () => { - it('should expand a collapsed item with ArrowLeft', () => { + it('should expand a collapsed item with ArrowLeft', async () => { const fruitsEl = getTreeItemElementByValue('fruits')!; expect(fruitsEl.getAttribute('aria-expanded')).toBe('false'); - left(); + await left(); expect(fruitsEl.getAttribute('aria-expanded')).toBe('true'); }); - it('should collapse an expanded item with ArrowRight', () => { - left(); + it('should collapse an expanded item with ArrowRight', async () => { + await left(); const fruitsEl = getTreeItemElementByValue('fruits')!; expect(fruitsEl.getAttribute('aria-expanded')).toBe('true'); - right(); + await right(); expect(fruitsEl.getAttribute('aria-expanded')).toBe('false'); }); }); @@ -1385,210 +1385,210 @@ describe('Tree', () => { describe(`focusMode="${focusMode}"`, () => { describe('LTR', () => { - beforeEach(() => { - setupTestTree('ltr'); - updateTree({focusMode}); + beforeEach(async () => { + await setupTestTree('ltr'); + await updateTree({focusMode}); }); describe('vertical orientation', () => { - beforeEach(() => { - updateTree({orientation: 'vertical'}); + beforeEach(async () => { + await updateTree({orientation: 'vertical'}); }); - it('should move focus to the next visible item on ArrowDown', () => { + it('should move focus to the next visible item on ArrowDown', async () => { expect(isFocused('fruits')).toBe(true); - right(); // Expands fruits - down(); + await right(); // Expands fruits + await down(); expect(isFocused('apple')).toBe(true); - down(); + await down(); expect(isFocused('banana')).toBe(true); }); - it('should move focus to the previous visible item on ArrowUp', () => { + it('should move focus to the previous visible item on ArrowUp', async () => { expect(isFocused('fruits')).toBe(true); - right(); // Expands fruits - down(); - down(); + await right(); // Expands fruits + await down(); + await down(); expect(isFocused('banana')).toBe(true); - up(); + await up(); expect(isFocused('apple')).toBe(true); - up(); + await up(); expect(isFocused('fruits')).toBe(true); }); - it('should skip disabled items with ArrowDown if softDisabled=false', () => { - right(); // Expands fruits - updateTreeItemByValue('apple', {disabled: true}); - updateTree({softDisabled: false}); + it('should skip disabled items with ArrowDown if softDisabled=false', async () => { + await right(); // Expands fruits + await updateTreeItemByValue('apple', {disabled: true}); + await updateTree({softDisabled: false}); expect(isFocused('fruits')).toBe(true); - down(); + await down(); expect(isFocused('banana')).toBe(true); }); - it('should not skip disabled items with ArrowDown if softDisabled=true', () => { - right(); // Expands fruits - updateTreeItemByValue('apple', {disabled: true}); - updateTree({softDisabled: true}); + it('should not skip disabled items with ArrowDown if softDisabled=true', async () => { + await right(); // Expands fruits + await updateTreeItemByValue('apple', {disabled: true}); + await updateTree({softDisabled: true}); expect(isFocused('fruits')).toBe(true); - down(); + await down(); expect(isFocused('apple')).toBe(true); }); - it('should wrap focus from last to first with ArrowDown when wrap is true', () => { - updateTree({wrap: true}); - end(); + it('should wrap focus from last to first with ArrowDown when wrap is true', async () => { + await updateTree({wrap: true}); + await end(); expect(isFocused('dairy')).toBe(true); - down(); + await down(); expect(isFocused('fruits')).toBe(true); }); - it('should not wrap focus from last to first with ArrowDown when wrap is false', () => { - updateTree({wrap: false}); - end(); + it('should not wrap focus from last to first with ArrowDown when wrap is false', async () => { + await updateTree({wrap: false}); + await end(); expect(isFocused('dairy')).toBe(true); - down(); + await down(); expect(isFocused('dairy')).toBe(true); }); }); describe('horizontal orientation', () => { - beforeEach(() => { - updateTree({orientation: 'horizontal'}); + beforeEach(async () => { + await updateTree({orientation: 'horizontal'}); }); - it('should move focus to the next visible item on ArrowRight', () => { + it('should move focus to the next visible item on ArrowRight', async () => { expect(isFocused('fruits')).toBe(true); - right(); + await right(); expect(isFocused('vegetables')).toBe(true); }); - it('should move focus to the previous visible item on ArrowLeft', () => { - right(); + it('should move focus to the previous visible item on ArrowLeft', async () => { + await right(); expect(isFocused('vegetables')).toBe(true); - left(); + await left(); expect(isFocused('fruits')).toBe(true); }); }); - it('should move focus to the last enabled visible item on End (softDisabled="false")', () => { - updateTree({softDisabled: false}); - right(); // Expands fruits - updateTreeItemByValue('dairy', {disabled: true}); - updateTreeItemByValue('grains', {disabled: true}); - updateTreeItemByValue('vegetables', {disabled: true}); - end(); + it('should move focus to the last enabled visible item on End (softDisabled="false")', async () => { + await updateTree({softDisabled: false}); + await right(); // Expands fruits + await updateTreeItemByValue('dairy', {disabled: true}); + await updateTreeItemByValue('grains', {disabled: true}); + await updateTreeItemByValue('vegetables', {disabled: true}); + await end(); expect(isFocused('berries')).toBe(true); }); - it('should move focus to the first enabled visible item on Home (softDisabled="false")', () => { - updateTree({softDisabled: false}); - end(); - updateTreeItemByValue('fruits', {disabled: true}); - home(); + it('should move focus to the first enabled visible item on Home (softDisabled="false")', async () => { + await updateTree({softDisabled: false}); + await end(); + await updateTreeItemByValue('fruits', {disabled: true}); + await home(); expect(isFocused('vegetables')).toBe(true); }); }); describe('RTL', () => { - beforeEach(() => { - setupTestTree('rtl'); - updateTree({focusMode}); + beforeEach(async () => { + await setupTestTree('rtl'); + await updateTree({focusMode}); }); describe('vertical orientation', () => { - beforeEach(() => { - updateTree({orientation: 'vertical'}); + beforeEach(async () => { + await updateTree({orientation: 'vertical'}); }); - it('should move focus to the next visible item on ArrowDown', () => { + it('should move focus to the next visible item on ArrowDown', async () => { expect(isFocused('fruits')).toBe(true); - down(); + await down(); expect(isFocused('vegetables')).toBe(true); }); - it('should move focus to the previous visible item on ArrowUp', () => { - down(); + it('should move focus to the previous visible item on ArrowUp', async () => { + await down(); expect(isFocused('vegetables')).toBe(true); - up(); + await up(); expect(isFocused('fruits')).toBe(true); }); }); describe('horizontal orientation', () => { - beforeEach(() => { - updateTree({orientation: 'horizontal'}); + beforeEach(async () => { + await updateTree({orientation: 'horizontal'}); }); - it('should move focus to the next visible item on ArrowLeft', () => { + it('should move focus to the next visible item on ArrowLeft', async () => { expect(isFocused('fruits')).toBe(true); - left(); + await left(); expect(isFocused('vegetables')).toBe(true); }); - it('should move focus to the previous visible item on ArrowRight', () => { - left(); + it('should move focus to the previous visible item on ArrowRight', async () => { + await left(); expect(isFocused('vegetables')).toBe(true); - right(); + await right(); expect(isFocused('fruits')).toBe(true); }); }); }); describe('pointer navigation', () => { - beforeEach(() => setupTestTree()); + beforeEach(async () => await setupTestTree()); - it('should move focus to the clicked item', () => { + it('should move focus to the clicked item', async () => { const vegetablesEl = getTreeItemElementByValue('vegetables')!; - click(vegetablesEl); + await click(vegetablesEl); expect(isFocused('vegetables')).toBe(true); }); - it('should move focus to the clicked disabled item if softDisabled=true', () => { - updateTreeItemByValue('vegetables', {disabled: true}); - updateTree({softDisabled: true}); + it('should move focus to the clicked disabled item if softDisabled=true', async () => { + await updateTreeItemByValue('vegetables', {disabled: true}); + await updateTree({softDisabled: true}); const vegetablesEl = getTreeItemElementByValue('vegetables')!; - click(vegetablesEl); + await click(vegetablesEl); expect(isFocused('vegetables')).toBe(true); }); }); describe('typeahead functionality', () => { - beforeEach(() => setupTestTree()); // LTR by default + beforeEach(async () => await setupTestTree()); // LTR by default - it('should focus the first matching visible item when typing characters', () => { - right(); // Expands fruits - type('Ba'); + it('should focus the first matching visible item when typing characters', async () => { + await right(); // Expands fruits + await type('Ba'); expect(isFocused('banana')).toBe(true); }); - it('should select the focused item if selectionMode is "follow"', () => { - updateTree({selectionMode: 'follow'}); - type('Gr'); + it('should select the focused item if selectionMode is "follow"', async () => { + await updateTree({selectionMode: 'follow'}); + await type('Gr'); expect(isFocused('grains')).toBe(true); expect(treeInstance.value()).toEqual(['grains']); }); - it('should not select the focused item if selectionMode is "explicit"', () => { - updateTree({selectionMode: 'explicit'}); - type('Gr'); + it('should not select the focused item if selectionMode is "explicit"', async () => { + await updateTree({selectionMode: 'explicit'}); + await type('Gr'); expect(isFocused('grains')).toBe(true); expect(treeInstance.value()).toEqual([]); }); - it('should skip disabled items with typeahead if softDisabled=false', () => { - right(); // Expands fruits - updateTreeItemByValue('banana', {disabled: true}); - updateTree({softDisabled: false}); - type('B'); + it('should skip disabled items with typeahead if softDisabled=false', async () => { + await right(); // Expands fruits + await updateTreeItemByValue('banana', {disabled: true}); + await updateTree({softDisabled: false}); + await type('B'); expect(isFocused('berries')).toBe(true); }); - it('should focus disabled items with typeahead if softDisabled=true', () => { - updateTreeItemByValue('vegetables', {disabled: true}); - updateTree({softDisabled: true}); - type('V'); + it('should focus disabled items with typeahead if softDisabled=true', async () => { + await updateTreeItemByValue('vegetables', {disabled: true}); + await updateTree({softDisabled: true}); + await type('V'); expect(isFocused('vegetables')).toBe(true); }); }); @@ -1598,16 +1598,16 @@ describe('Tree', () => { describe('item mutations and focus stability', () => { it('should recover focus by shifting to the default state if the active item is removed', async () => { - setupTestTree(); - updateTree({focusMode: 'activedescendant'}); + await setupTestTree(); + await updateTree({focusMode: 'activedescendant'}); const vegetablesEl = getTreeItemElementByValue('vegetables')!; - click(vegetablesEl); + await click(vegetablesEl); expect(getFocusedTreeItemValue()).toBe('vegetables'); const updatedNodes = testComponent.nodes().filter(n => n.value !== 'vegetables'); testComponent.nodes.set(updatedNodes); - fixture.detectChanges(); + await fixture.whenStable(); await waitForMicrotasks(); defineTestVariables();