From fbbadd3644869d24440fe9e17e391379e1ea1425 Mon Sep 17 00:00:00 2001 From: Mohammer5 Date: Mon, 28 Oct 2024 15:02:37 +0800 Subject: [PATCH] test(select a11y): cover keyboard interactions + disabled with tests --- collections/forms/i18n/en.pot | 4 +- .../keyboard-interactions.test.e2e.js | 419 +++++++++++++++--- .../single-select-a11y.e2e.stories.js | 9 +- .../single-select-a11y.test.js | 289 ++++++++++++ 4 files changed, 658 insertions(+), 63 deletions(-) diff --git a/collections/forms/i18n/en.pot b/collections/forms/i18n/en.pot index 2b1b5f1f1..375e2a093 100644 --- a/collections/forms/i18n/en.pot +++ b/collections/forms/i18n/en.pot @@ -5,8 +5,8 @@ msgstr "" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1)\n" -"POT-Creation-Date: 2024-10-28T03:18:56.736Z\n" -"PO-Revision-Date: 2024-10-28T03:18:56.736Z\n" +"POT-Creation-Date: 2024-10-28T03:31:24.818Z\n" +"PO-Revision-Date: 2024-10-28T03:31:24.818Z\n" msgid "Upload file" msgstr "Upload file" diff --git a/components/select/src/single-select-a11y/features/keyboard-interactions.test.e2e.js b/components/select/src/single-select-a11y/features/keyboard-interactions.test.e2e.js index 993dd3731..bfc6b396b 100644 --- a/components/select/src/single-select-a11y/features/keyboard-interactions.test.e2e.js +++ b/components/select/src/single-select-a11y/features/keyboard-interactions.test.e2e.js @@ -1,16 +1,18 @@ describe('', () => { it('should highlight the first option on the currently displayed page', () => { - // Given a select with 100 options is displayed + cy.log('**Given a select with 100 options is displayed**') cy.visitStory('Single Select A11y', 'Hundret Options') - // And the menu is visible + cy.log('**And the menu is visible**') cy.findByRole('combobox').click() cy.findByRole('option', { selected: true }).should('be.visible') - // And the 74th option is being highlighted + cy.log('**And the 74th option is being highlighted**') cy.findByRole('combobox').focus().type(`Select option 73`) - // And the 70th option is the first option on the current page + cy.log( + '**And the 70th option is the first option on the current page**' + ) let optionOffset cy.findAllByRole('option') .eq(70) @@ -23,64 +25,152 @@ describe('', () => { .invoke('parent') // listbox .invoke('parent') // scrollable div .then((listBoxParent) => { - console.log('> listBoxParent', listBoxParent.get(0)) - console.log('> optionOffset', optionOffset) listBoxParent.get(0).scrollTop = optionOffset }) cy.findAllByRole('option').eq(70).should('be.visible') - // When the PageUp key is pressed + cy.log('**When the PageUp key is pressed**') cy.findByRole('combobox').trigger('keydown', { key: 'PageUp', force: true, }) - // Then the first option on the currently displayed page is highlighted + cy.log( + '**Then the first option on the currently displayed page is highlighted**' + ) cy.findByRole('option', { selected: true }) .invoke('attr', 'aria-label') .should('equal', 'Select option 70') }) + it('should highlight the first enabled option before the first (disabled) option on the currently displayed page', () => { + cy.log( + '**Given a select with 100 options is displayed and option #17 is disabled**' + ) + cy.visitStory('Single Select A11y', 'Hundret Options With Disabled') + + cy.log('**And the menu is visible**') + cy.findByRole('combobox').click() + cy.findByRole('option', { selected: true }).should('be.visible') + + cy.log('**And the 26th option is being highlighted**') + cy.findByRole('combobox').focus().type(`Select option 25`) + + cy.log( + '**And the 17th option is the first option on the current page**' + ) + let optionOffset + cy.findAllByRole('option') + .eq(17) + .then((option) => { + const { offsetTop } = option.get(0) + optionOffset = offsetTop + }) + + cy.findByRole('option', { selected: true }) + .invoke('parent') // listbox + .invoke('parent') // scrollable div + .then((listBoxParent) => { + listBoxParent.get(0).scrollTop = optionOffset + }) + + cy.findAllByRole('option').eq(17).should('be.visible') + + cy.log('**When the PageUp key is pressed**') + cy.findByRole('combobox').trigger('keydown', { + key: 'PageUp', + force: true, + }) + + cy.log( + '**Then the first option on the currently displayed page is highlighted**' + ) + cy.findByRole('option', { selected: true }) + .invoke('attr', 'aria-label') + .should('equal', 'Select option 16') + }) + it('should highlight the first option on the previous page', () => { - // Given a select with 100 options is displayed + cy.log('**Given a select with 100 options is displayed**') cy.visitStory('Single Select A11y', 'Hundret Options') - // And the menu is visible + cy.log('**And the menu is visible**') cy.findByRole('combobox').click() cy.findByRole('option', { selected: true }).should('be.visible') - // And the 70th option is being highlighted + cy.log('**And the 70th option is being highlighted**') // Will automatically scroll there and make it the first option on the page cy.findByRole('combobox').focus().type(`Select option 70`) - // When the PageUp key is pressed + cy.log('**When the PageUp key is pressed**') cy.findByRole('combobox').trigger('keydown', { key: 'PageUp', force: true, }) - // Then the first option on the previous page is highlighted + cy.log('**Then the first option on the previous page is highlighted**') cy.findByRole('option', { selected: true }) .invoke('attr', 'aria-label') .should('equal', 'Select option 61') - // And the previously highlighted option is not visible + // That option is truly the first option + cy.get('[role="option"]:visible') + .first() + .invoke('attr', 'aria-label') + .should('equal', 'Select option 61') + + cy.log('**And the previously highlighted option is not visible**') cy.findAllByRole('option').eq(70).should('not.be.visible') }) + it('should highlight the first enabled option before the first (disabled) option on the previous page', () => { + cy.log( + '**Given a select with 100 options is displayed and option #17 is disabled**' + ) + cy.visitStory('Single Select A11y', 'Hundret Options With Disabled') + + cy.log('**And the menu is visible**') + cy.findByRole('combobox').click() + cy.findByRole('option', { selected: true }).should('be.visible') + + cy.log('**And the 27th option is being highlighted**') + // Will automatically scroll there and make it the first option on the page + cy.findByRole('combobox').focus().type(`Select option 27`) + + cy.log('**When the PageUp key is pressed**') + cy.findByRole('combobox').trigger('keydown', { + key: 'PageUp', + force: true, + }) + + cy.log('**Then the first option on the previous page is highlighted**') + cy.findByRole('option', { selected: true }) + .invoke('attr', 'aria-label') + .should('equal', 'Select option 16') + + cy.log('**And that option is truly the first option**') + cy.get('[role="option"]:visible') + .first() + .invoke('attr', 'aria-label') + .should('equal', 'Select option 16') + + cy.log('**And the previously highlighted option is not visible**') + cy.findAllByRole('option').eq(27).should('not.be.visible') + }) + it('should highlight the first option', () => { - // Given a select with 100 options is displayed + cy.log('**Given a select with 100 options is displayed**') cy.visitStory('Single Select A11y', 'Hundret Options') - // And the menu is visible + cy.log('**And the menu is visible**') cy.findByRole('combobox').click() cy.findByRole('option', { selected: true }).should('be.visible') - // And the 2nd option is being highlighted + cy.log('**And the 2nd option is being highlighted**') cy.findByRole('combobox').focus().type(`Select option 1`) - // And the 2nd option is the first option on the current page + cy.log('**And the 2nd option is the first option on the current page**') let optionOffset cy.findAllByRole('option') .eq(1) @@ -93,20 +183,62 @@ describe('', () => { .invoke('parent') // listbox .invoke('parent') // scrollable div .then((listBoxParent) => { - console.log('> listBoxParent', listBoxParent.get(0)) - console.log('> optionOffset', optionOffset) listBoxParent.get(0).scrollTop = optionOffset }) - // When the PageUp key is pressed + cy.log('**When the PageUp key is pressed**') cy.findByRole('combobox').trigger('keydown', { key: 'PageUp', force: true, }) - // Then the first option is being highlighted + cy.log('**Then the first option is being highlighted**') cy.all( - () => cy.findAllByRole('option').first().invoke('get', 0), + () => cy.findAllByRole('option').invoke('get', 0), + () => cy.findByRole('option', { selected: true }).invoke('get', 0) + ).then(([firstOption, highlightedOption]) => { + expect(highlightedOption).to.equal(firstOption) + }) + }) + + it('should highlight the second option when the first option is disabled', () => { + cy.log( + '**Given a select with 100 options is displayed and option #2 is disabled**' + ) + cy.visitStory('Single Select A11y', 'Hundret Options With Disabled') + + cy.log('**And the menu is visible**') + cy.findByRole('combobox').click() + cy.findByRole('option', { selected: true }).should('be.visible') + + cy.log('**And the 3rd option is being highlighted**') + cy.findByRole('combobox').focus().type(`Select option 2`) + + cy.log('**And the 3rd option is the first option on the current page**') + let optionOffset + cy.findAllByRole('option') + .eq(2) + .then((option) => { + const { offsetTop } = option.get(0) + optionOffset = offsetTop + }) + + cy.findByRole('option', { selected: true }) + .invoke('parent') // listbox + .invoke('parent') // scrollable div + .then((listBoxParent) => { + listBoxParent.get(0).scrollTop = optionOffset + }) + + cy.log('**When the PageUp key is pressed**') + cy.findByRole('combobox').trigger('keydown', { + key: 'PageUp', + force: true, + }) + + cy.log('**Then the second option is being highlighted**') + cy.all( + () => cy.findAllByRole('option').invoke('get', 1), () => cy.findByRole('option', { selected: true }).invoke('get', 0) ).then(([firstOption, highlightedOption]) => { expect(highlightedOption).to.equal(firstOption) @@ -114,21 +246,23 @@ describe('', () => { }) it('should highlight the last option on the currently displayed page', () => { - // Given a select with 100 options is displayed + cy.log('**Given a select with 100 options is displayed**') cy.visitStory('Single Select A11y', 'Hundret Options') - // And the menu is visible + cy.log('**And the menu is visible**') // first option will be highlighted automatically cy.findByRole('combobox').click() cy.findByRole('option', { selected: true }).should('be.visible') - // When the PageDown key is pressed + cy.log('**When the PageDown key is pressed**') cy.findByRole('combobox').trigger('keydown', { key: 'PageDown', force: true, }) - // Then the last option on the currently displayed page is highlighted + cy.log( + '**Then the last option on the currently displayed page is highlighted**' + ) cy.all( () => cy.get('[role="option"]:visible').last().invoke('get', 0), () => cy.findByRole('option', { selected: true }).invoke('get', 0) @@ -137,49 +271,162 @@ describe('', () => { }) }) + it('should highlight the first enabled option after the last (disabled) option on the currently displayed page', () => { + cy.log( + '**Given a select with 100 options is displayed and option #17 is disabled**' + ) + cy.visitStory('Single Select A11y', 'Hundret Options With Disabled') + + cy.log( + '**And the 9th option is being selected (will cause it to be the first option on the page when opening the menu)**' + ) + cy.findByRole('combobox').focus().type(`Select option 9`) + + cy.log('**And the menu is visible**') + // first option will be highlighted automatically + cy.findByRole('combobox').click() + cy.findByRole('option', { selected: true }).should('be.visible') + + // That option is truly the first option + cy.get('[role="option"]:visible') + .first() + .invoke('attr', 'aria-label') + .should('equal', 'Select option 9') + + cy.log('**When the PageDown key is pressed**') + cy.findByRole('combobox').trigger('keydown', { + key: 'PageDown', + force: true, + }) + + cy.log( + '**Then the first enabled option after last option on the currently displayed page is highlighted**' + ) + cy.all( + () => cy.findAllByRole('option').invoke('get', 19), + () => cy.findByRole('option', { selected: true }).invoke('get', 0) + ).then(([eighteensOptions, highlightedOption]) => { + expect(highlightedOption).to.equal(eighteensOptions) + }) + + // For some reason, without the timeout, + // cypress will still get the previously visible page + // when using the `:visible` pseudo-selector + cy.wait(0) + + cy.log( + '**And the first enabled option after last option on the currently displayed page is the first visible option**' + ) + cy.all( + () => cy.get('[role="option"]:visible').invoke('get', 0), + () => cy.findByRole('option', { selected: true }).invoke('get', 0) + ).then(([lastVisibleOption, highlightedOption]) => { + expect(highlightedOption).to.equal(lastVisibleOption) + }) + }) + it( 'should highlight the last option on the next page', // We don't want the options to scroll when we check whether they're // visible or not (as that'd make them visible) { scrollBehavior: false }, () => { - // Given a select with 100 options is displayed + const LAST_VISIBLE_OPTION_INDEX = 8 + + cy.log('**Given a select with 100 options is displayed**') cy.visitStory('Single Select A11y', 'Hundret Options') - // And the menu is visible + cy.log('**And the last option on the first page is selected**') + cy.findByRole('combobox').focus().type(`Select option 8`) + + cy.log('**And the menu is visible**') cy.findByRole('combobox').click() cy.findByRole('option', { selected: true }).should('be.visible') - // And the option last visible option is being highlighted - cy.get('[role="option"]:visible').then(($visibleOptions) => { - const visibleOptionsAmount = $visibleOptions.length + cy.get('[role="option"]') + .eq(LAST_VISIBLE_OPTION_INDEX) + .invoke('attr', 'aria-selected') + .should('equal', 'true') + + cy.log('**When the PageDown key is pressed**') + cy.findByRole('combobox').trigger('keydown', { + key: 'PageDown', + force: true, + }) + + cy.log('**Then the next page is shown**') + cy.get('[role="option"]') + .eq(LAST_VISIBLE_OPTION_INDEX) + .should('not.be.visible') + cy.get('[role="option"]') + .eq(LAST_VISIBLE_OPTION_INDEX + 1) + .should('be.visible') + + cy.log('**And the last option on the next page is highlighted**') + cy.get('[role="option"]:visible') + .last() + .invoke('attr', 'aria-selected') + .should('equal', 'true') + + cy.log('**And the previously highlighted option is not visible**') + cy.get('[role="option"]') + .eq(LAST_VISIBLE_OPTION_INDEX + 1) + .invoke('attr', 'aria-selected') + .should('equal', 'false') + } + ) + + it( + 'should highlight the first enabled option after the last (disabled) option on the next page', + // We don't want the options to scroll when we check whether they're + // visible or not (as that'd make them visible) + { scrollBehavior: false }, + () => { + cy.log( + '**Given a select with 100 options is displayed and option #17 is disabled**' + ) + cy.visitStory('Single Select A11y', 'Hundret Options With Disabled') + + cy.log('**And the 2nd option is being selected**') + cy.findByRole('combobox').focus().type(`Select option 9`) - for (let i = 0; i < visibleOptionsAmount - 1; ++i) { - cy.findByRole('combobox').trigger('keydown', { - key: 'ArrowDown', - force: true, - }) + cy.log('**And the menu is visible**') + cy.findByRole('combobox').click() + cy.findByRole('option', { selected: true }).should('be.visible') - if (i === visibleOptionsAmount - 2) { - cy.wrap(i).as('lastVisibleOptionIndex') - } - } + cy.log( + '**And the 2nd option is the first option on the current page**' + ) + cy.all( + () => cy.findAllByRole('option').eq(1), + // scrollable div + () => + cy + .findByRole('option', { selected: true }) + .invoke('parent') + .invoke('parent') + ).then(([nextTopOption, listBoxParent]) => { + const { offsetTop } = nextTopOption.get(0) + listBoxParent.get(0).scrollTop = offsetTop }) + cy.wrap(9).as('lastVisibleOptionIndex') + cy.get('@lastVisibleOptionIndex').then((lastVisibleOptionIndex) => { cy.get('[role="option"]') - .eq(lastVisibleOptionIndex + 1) // 1-based + .eq(lastVisibleOptionIndex) .invoke('attr', 'aria-selected') .should('equal', 'true') }) - // When the PageDown key is pressed + cy.log('**When the PageDown key is pressed**') cy.findByRole('combobox').trigger('keydown', { key: 'PageDown', force: true, }) - // Then the next page is shown + cy.log('**Then the next page is shown,**') + // but one option is skipped due to the disabled option at the end of the next page cy.get('@lastVisibleOptionIndex').then((lastVisibleOptionIndex) => { cy.get('[role="option"]') .eq(lastVisibleOptionIndex + 1) @@ -189,19 +436,16 @@ describe('', () => { .should('be.visible') }) - // And the last option on the next page is highlighted + cy.log('**And the last option on the next page is highlighted**') cy.get('[role="option"]:visible') .last() .invoke('attr', 'aria-selected') .should('equal', 'true') - // And the previously highlighted option is not visible - cy.get('@lastVisibleOptionIndex').then((lastVisibleOptionIndex) => { - cy.get('[role="option"]') - .eq(lastVisibleOptionIndex + 1) - .invoke('attr', 'aria-selected') - .should('equal', 'false') - }) + cy.get('[role="option"]:visible') + .last() + .invoke('attr', 'aria-label') + .should('equal', 'Select option 19') } ) @@ -211,14 +455,16 @@ describe('', () => { // visible or not (as that'd make them visible) { scrollBehavior: false }, () => { - // Given a select with 100 options is displayed + cy.log('**Given a select with 100 options is displayed**') cy.visitStory('Single Select A11y', 'Hundret Options') - // And the menu is visible + cy.log('**And the menu is visible**') cy.findByRole('combobox').click() cy.findByRole('option', { selected: true }).should('be.visible') - // And the 2nd-last option is being highlighted and visible + cy.log( + '**And the 2nd-last option is being highlighted and visible**' + ) for ( let i = 0; i < 11; // This will bring us to the second last option exactly @@ -231,16 +477,16 @@ describe('', () => { } cy.findAllByRole('option').eq(98).should('be.visible') - // And the last option is not visible + cy.log('**And the last option is not visible**') cy.findAllByRole('option').last().should('not.be.visible') - // When the PageDown key is pressed + cy.log('**When the PageDown key is pressed**') cy.findByRole('combobox').trigger('keydown', { key: 'PageDown', force: true, }) - // Then the last option is highlighted + cy.log('**Then the last option is highlighted**') cy.all( () => cy.findAllByRole('option').last().invoke('get', 0), () => @@ -249,8 +495,63 @@ describe('', () => { expect(highlightedOption).to.equal(lastOption) }) - // And the last option is visible + cy.log('**And the last option is visible**') cy.findAllByRole('option').last().should('be.visible') } ) + + it( + 'should highlight the last enabled option', + // We don't want the options to scroll when we check whether they're + // visible or not (as that'd make them visible) + { scrollBehavior: false }, + () => { + cy.log( + '**Given a select with 100 options is displayed and option #99 is disabled**' + ) + cy.visitStory('Single Select A11y', 'Hundret Options With Disabled') + + cy.log('**And the 3rd-last option is being selected**') + cy.findByRole('combobox').focus().type(`Select option 97`) + + cy.log('**And the menu is visible**') + cy.findByRole('combobox').click() + cy.findByRole('option', { selected: true }).should('be.visible') + + cy.log( + '**And the 3rd-last option is the last option on the current page**' + ) + cy.all( + () => cy.findAllByRole('option').eq(89), + () => cy.findByRole('listbox').invoke('parent') // scrollable div + ).then(([nextTopOption, listBoxParent]) => { + const { offsetTop } = nextTopOption.get(0) + listBoxParent.get(0).scrollTop = offsetTop + }) + + cy.findAllByRole('option').eq(97).should('be.visible') + cy.findAllByRole('option').eq(98).should('not.be.visible') + cy.findAllByRole('option').last().should('not.be.visible') + + cy.log('**When the PageDown key is pressed**') + cy.findByRole('combobox').trigger('keydown', { + key: 'PageDown', + force: true, + }) + + cy.log('**Then the last option is highlighted**') + cy.all( + () => cy.findAllByRole('option').invoke('get', 98), + () => + cy.findByRole('option', { selected: true }).invoke('get', 0) + ).then(([secondLastOption, highlightedOption]) => { + expect(highlightedOption).to.equal(secondLastOption) + }) + + cy.log('**And the second last option is visible**') + cy.findAllByRole('option').eq(98).should('be.visible') + cy.log('**And the last option is not visible**') + cy.findAllByRole('option').eq(99).should('not.be.visible') + } + ) }) diff --git a/components/select/src/single-select-a11y/single-select-a11y.e2e.stories.js b/components/select/src/single-select-a11y/single-select-a11y.e2e.stories.js index b9f622e9a..318e68f27 100644 --- a/components/select/src/single-select-a11y/single-select-a11y.e2e.stories.js +++ b/components/select/src/single-select-a11y/single-select-a11y.e2e.stories.js @@ -181,7 +181,12 @@ export const HundretOptionsWithDisabled = () => { Array.apply(null, Array(100)).map((x, i) => ({ value: `${i}`, label: `Select option ${i}`, - disabled: i === 1 || i === 17 || i === 18, + disabled: [ + 0, // first otpion + 17, + 18, + 99, // last otpion + ].includes(i), })) ) @@ -201,7 +206,7 @@ export const NativeSelect = () => { Array.apply(null, Array(100)).map((x, i) => ({ value: `${i}`, label: `Select option ${i}`, - disabled: i > 19, + disabled: i > 19 && i < 30, })) ) diff --git a/components/select/src/single-select-a11y/single-select-a11y.test.js b/components/select/src/single-select-a11y/single-select-a11y.test.js index 15b12209d..86b3fe68b 100644 --- a/components/select/src/single-select-a11y/single-select-a11y.test.js +++ b/components/select/src/single-select-a11y/single-select-a11y.test.js @@ -460,6 +460,35 @@ describe('', () => { }) }) + describe.each([ + { key: ' ' }, + { key: 'Enter' }, + { key: 'ArrowDown', altKey: true }, + { key: 'ArrowUp', altKey: true }, + ])('disabled & $key ($altKey)', (keyDownOptions) => { + test('not open the menu', () => { + const onChange = jest.fn() + + render( + + ) + + fireEvent.keyDown(screen.getByRole('combobox'), keyDownOptions) + expect(screen.queryByRole('listbox')).toBeNull() + expect(onChange).not.toHaveBeenCalled() + }) + }) + describe.each([ { key: ' ' }, { key: 'Enter' }, @@ -553,6 +582,56 @@ describe('', () => { expect(onChange).toHaveBeenCalledWith('foo') }) + it('should not select the next option when closed, disabled and user presses ArrowDown', () => { + const onChange = jest.fn() + + render( + + ) + + expect(screen.queryByRole('listbox')).toBeNull() + + // highlighting the next option + fireEvent.keyDown(screen.getByRole('combobox'), { key: 'ArrowDown' }) + expect(screen.queryByRole('listbox')).toBeNull() + expect(onChange).toHaveBeenCalledTimes(0) + }) + + it('should select the second-next option when closed, next option is disabled and user presses ArrowDown', () => { + const onChange = jest.fn() + + render( + + ) + + expect(screen.queryByRole('listbox')).toBeNull() + + // highlighting the next option + fireEvent.keyDown(screen.getByRole('combobox'), { key: 'ArrowDown' }) + expect(screen.queryByRole('listbox')).toBeNull() + expect(onChange).toHaveBeenCalledTimes(1) + expect(onChange).toHaveBeenCalledWith('bar') + }) + it('should select the previous option when closed and user presses ArrowUp', () => { const onChange = jest.fn() @@ -578,6 +657,56 @@ describe('', () => { expect(onChange).toHaveBeenCalledWith('foo') }) + it('should not select the previous option when closed, disabled and user presses ArrowUp', () => { + const onChange = jest.fn() + + render( + + ) + + expect(screen.queryByRole('listbox')).toBeNull() + + // highlighting the next option + fireEvent.keyDown(screen.getByRole('combobox'), { key: 'ArrowUp' }) + expect(screen.queryByRole('listbox')).toBeNull() + expect(onChange).toHaveBeenCalledTimes(0) + }) + + it('should select the second-previous option when closed, previous option disabled and user presses ArrowUp', () => { + const onChange = jest.fn() + + render( + + ) + + expect(screen.queryByRole('listbox')).toBeNull() + + // highlighting the next option + fireEvent.keyDown(screen.getByRole('combobox'), { key: 'ArrowUp' }) + expect(screen.queryByRole('listbox')).toBeNull() + expect(onChange).toHaveBeenCalledTimes(1) + expect(onChange).toHaveBeenCalledWith('') + }) + it('should highlight the next option', () => { const onChange = jest.fn() @@ -618,6 +747,46 @@ describe('', () => { ).toBe('Foo') }) + it('should highlight the second next option when the next option is disabled', () => { + const onChange = jest.fn() + + render( + + ) + + // open the menu + expect(screen.queryByRole('listbox')).toBeNull() + const comboBox = screen.getByRole('combobox') + fireEvent.click(comboBox) + expect(screen.queryByRole('listbox')).not.toBeNull() + + // the first option should be highlighted + const highlightedOptionBefore = screen.getByRole('option', { + selected: true, + }) + expect( + highlightedOptionBefore.attributes.getNamedItem('aria-label').value + ).toBe('None') + + // The second option should be highlighted + fireEvent.keyDown(comboBox, { key: 'ArrowDown' }) + const highlightedOptionAfter = screen.getByRole('option', { + selected: true, + }) + expect( + highlightedOptionAfter.attributes.getNamedItem('aria-label').value + ).toBe('Bar') + }) + it('should highlight the previous option', () => { const onChange = jest.fn() @@ -658,6 +827,46 @@ describe('', () => { ).toBe('Foo') }) + it('should highlight the second previous option when previous option is disabled', () => { + const onChange = jest.fn() + + render( + + ) + + // open the menu + expect(screen.queryByRole('listbox')).toBeNull() + const comboBox = screen.getByRole('combobox') + fireEvent.click(comboBox) + expect(screen.queryByRole('listbox')).not.toBeNull() + + // the last option should be highlighted + const highlightedOptionBefore = screen.getByRole('option', { + selected: true, + }) + expect( + highlightedOptionBefore.attributes.getNamedItem('aria-label').value + ).toBe('Bar') + + // The second option should be highlighted + fireEvent.keyDown(comboBox, { key: 'ArrowUp' }) + const highlightedOptionAfter = screen.getByRole('option', { + selected: true, + }) + expect( + highlightedOptionAfter.attributes.getNamedItem('aria-label').value + ).toBe('None') + }) + it('should highlight the first option', () => { const onChange = jest.fn() @@ -698,6 +907,46 @@ describe('', () => { ).toBe('None') }) + it('should highlight the first enabled option', () => { + const onChange = jest.fn() + + render( + + ) + + // open the menu + expect(screen.queryByRole('listbox')).toBeNull() + const comboBox = screen.getByRole('combobox') + fireEvent.click(comboBox) + expect(screen.queryByRole('listbox')).not.toBeNull() + + // the last option should be highlighted + const highlightedOptionBefore = screen.getByRole('option', { + selected: true, + }) + expect( + highlightedOptionBefore.attributes.getNamedItem('aria-label').value + ).toBe('Bar') + + // The first option should be highlighted + fireEvent.keyDown(comboBox, { key: 'Home' }) + const highlightedOptionAfter = screen.getByRole('option', { + selected: true, + }) + expect( + highlightedOptionAfter.attributes.getNamedItem('aria-label').value + ).toBe('Foo') + }) + it('should highlight the last option', () => { const onChange = jest.fn() @@ -737,4 +986,44 @@ describe('', () => { highlightedOptionAfter.attributes.getNamedItem('aria-label').value ).toBe('Bar') }) + + it('should highlight the last enabled option', () => { + const onChange = jest.fn() + + render( + + ) + + // open the menu + expect(screen.queryByRole('listbox')).toBeNull() + const comboBox = screen.getByRole('combobox') + fireEvent.click(comboBox) + expect(screen.queryByRole('listbox')).not.toBeNull() + + // the first option should be highlighted + const highlightedOptionBefore = screen.getByRole('option', { + selected: true, + }) + expect( + highlightedOptionBefore.attributes.getNamedItem('aria-label').value + ).toBe('None') + + // The last option should be highlighted + fireEvent.keyDown(comboBox, { key: 'End' }) + const highlightedOptionAfter = screen.getByRole('option', { + selected: true, + }) + expect( + highlightedOptionAfter.attributes.getNamedItem('aria-label').value + ).toBe('Foo') + }) })