diff --git a/CHANGELOG.md b/CHANGELOG.md index d3287593..1ce4455b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ and this project adheres to - ♿️(frontend) fix share modal heading hierarchy #2007 - ♿️(frontend) fix Copy link toast accessibility for screen readers #2029 - ♿️(frontend) fix modal aria-label and name #2014 +- ♿️(frontend) fix language dropdown ARIA for screen readers #2020 ## [v4.8.1] - 2026-03-17 diff --git a/src/frontend/apps/e2e/__tests__/app-impress/doc-ai.spec.ts b/src/frontend/apps/e2e/__tests__/app-impress/doc-ai.spec.ts index 99f82923..0a3885fd 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/doc-ai.spec.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/doc-ai.spec.ts @@ -3,6 +3,7 @@ import { expect, test } from '@playwright/test'; import { createDoc, + getMenuItem, mockedDocument, overrideConfig, verifyDocName, @@ -178,32 +179,18 @@ test.describe('Doc AI feature', () => { await page.getByRole('button', { name: 'AI', exact: true }).click(); - await expect( - page.getByRole('menuitem', { name: 'Use as prompt' }), - ).toBeVisible(); - await expect( - page.getByRole('menuitem', { name: 'Rephrase' }), - ).toBeVisible(); - await expect( - page.getByRole('menuitem', { name: 'Summarize' }), - ).toBeVisible(); - await expect(page.getByRole('menuitem', { name: 'Correct' })).toBeVisible(); - await expect( - page.getByRole('menuitem', { name: 'Language' }), - ).toBeVisible(); + await expect(getMenuItem(page, 'Use as prompt')).toBeVisible(); + await expect(getMenuItem(page, 'Rephrase')).toBeVisible(); + await expect(getMenuItem(page, 'Summarize')).toBeVisible(); + await expect(getMenuItem(page, 'Correct')).toBeVisible(); + await expect(getMenuItem(page, 'Language')).toBeVisible(); - await page.getByRole('menuitem', { name: 'Language' }).hover(); - await expect( - page.getByRole('menuitem', { name: 'English', exact: true }), - ).toBeVisible(); - await expect( - page.getByRole('menuitem', { name: 'French', exact: true }), - ).toBeVisible(); - await expect( - page.getByRole('menuitem', { name: 'German', exact: true }), - ).toBeVisible(); + await getMenuItem(page, 'Language').hover(); + await expect(getMenuItem(page, 'English', { exact: true })).toBeVisible(); + await expect(getMenuItem(page, 'French', { exact: true })).toBeVisible(); + await expect(getMenuItem(page, 'German', { exact: true })).toBeVisible(); - await page.getByRole('menuitem', { name: 'German', exact: true }).click(); + await getMenuItem(page, 'German', { exact: true }).click(); await expect(editor.getByText('Hallo Welt')).toBeVisible(); }); @@ -269,23 +256,15 @@ test.describe('Doc AI feature', () => { await page.getByRole('button', { name: 'AI', exact: true }).click(); if (ai_transform) { - await expect( - page.getByRole('menuitem', { name: 'Use as prompt' }), - ).toBeVisible(); + await expect(getMenuItem(page, 'Use as prompt')).toBeVisible(); } else { - await expect( - page.getByRole('menuitem', { name: 'Use as prompt' }), - ).toBeHidden(); + await expect(getMenuItem(page, 'Use as prompt')).toBeHidden(); } if (ai_translate) { - await expect( - page.getByRole('menuitem', { name: 'Language' }), - ).toBeVisible(); + await expect(getMenuItem(page, 'Language')).toBeVisible(); } else { - await expect( - page.getByRole('menuitem', { name: 'Language' }), - ).toBeHidden(); + await expect(getMenuItem(page, 'Language')).toBeHidden(); } }); }); diff --git a/src/frontend/apps/e2e/__tests__/app-impress/doc-comments.spec.ts b/src/frontend/apps/e2e/__tests__/app-impress/doc-comments.spec.ts index 6c79b9ea..218c63b2 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/doc-comments.spec.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/doc-comments.spec.ts @@ -3,6 +3,7 @@ import { expect, test } from '@playwright/test'; import { closeHeaderMenu, createDoc, + getMenuItem, getOtherBrowserName, verifyDocName, } from './utils-common'; @@ -150,7 +151,7 @@ test.describe('Doc Comments', () => { // Edit Comment await thread.getByText('This is a comment').first().hover(); await thread.locator('[data-test="moreactions"]').first().click(); - await thread.getByRole('menuitem', { name: 'Edit comment' }).click(); + await getMenuItem(thread, 'Edit comment').click(); const commentEditor = thread.getByText('This is a comment').first(); await commentEditor.fill('This is an edited comment'); const saveBtn = thread.locator('button[data-test="save"]').first(); @@ -175,7 +176,7 @@ test.describe('Doc Comments', () => { // Delete second comment await thread.getByText('This is a second comment').first().hover(); await thread.locator('[data-test="moreactions"]').first().click(); - await thread.getByRole('menuitem', { name: 'Delete comment' }).click(); + await getMenuItem(thread, 'Delete comment').click(); await expect( thread.getByText('This is a second comment').first(), ).toBeHidden(); @@ -205,7 +206,7 @@ test.describe('Doc Comments', () => { await thread.getByText('This is a new comment').first().hover(); await thread.locator('[data-test="moreactions"]').first().click(); - await thread.getByRole('menuitem', { name: 'Delete comment' }).click(); + await getMenuItem(thread, 'Delete comment').click(); await expect(editor.getByText('Hello')).toHaveCSS( 'background-color', diff --git a/src/frontend/apps/e2e/__tests__/app-impress/doc-editor.spec.ts b/src/frontend/apps/e2e/__tests__/app-impress/doc-editor.spec.ts index 51583cd4..8f8d9694 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/doc-editor.spec.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/doc-editor.spec.ts @@ -5,6 +5,7 @@ import cs from 'convert-stream'; import { createDoc, + getMenuItem, goToGridDoc, overrideConfig, verifyDocName, @@ -147,7 +148,7 @@ test.describe('Doc Editor', () => { const wsClosePromise = webSocket.waitForEvent('close'); await selectVisibility.click(); - await page.getByRole('menuitem', { name: 'Connected' }).click(); + await getMenuItem(page, 'Connected').click(); // Assert that the doc reconnects to the ws const wsClose = await wsClosePromise; @@ -608,7 +609,7 @@ test.describe('Doc Editor', () => { await page.getByRole('button', { name: 'Share' }).click(); await page.getByTestId('doc-access-mode').click(); - await page.getByRole('menuitem', { name: 'Reading' }).click(); + await getMenuItem(page, 'Reading').click(); // Close the modal await page.getByRole('button', { name: 'close' }).first().click(); diff --git a/src/frontend/apps/e2e/__tests__/app-impress/doc-grid-move.spec.ts b/src/frontend/apps/e2e/__tests__/app-impress/doc-grid-move.spec.ts index 09d157ec..63d7f91b 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/doc-grid-move.spec.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/doc-grid-move.spec.ts @@ -3,6 +3,7 @@ import { expect, test } from '@playwright/test'; import { createDoc, getGridRow, + getMenuItem, getOtherBrowserName, mockedListDocs, toggleHeaderMenu, @@ -206,7 +207,7 @@ test.describe('Doc grid move', () => { const row = await getGridRow(page, titleDoc1); await row.getByText(`more_horiz`).click(); - await page.getByRole('menuitem', { name: 'Move into a doc' }).click(); + await getMenuItem(page, 'Move into a doc').click(); await expect( page.getByRole('dialog').getByRole('heading', { name: 'Move' }), @@ -294,7 +295,7 @@ test.describe('Doc grid move', () => { const row = await getGridRow(page, titleDoc1); await row.getByText(`more_horiz`).click(); - await page.getByRole('menuitem', { name: 'Move into a doc' }).click(); + await getMenuItem(page, 'Move into a doc').click(); await expect( page.getByRole('dialog').getByRole('heading', { name: 'Move' }), @@ -341,7 +342,7 @@ test.describe('Doc grid move', () => { `doc-share-access-request-row-${emailRequest}`, ); await container.getByTestId('doc-role-dropdown').click(); - await otherPage.getByRole('menuitem', { name: 'Administrator' }).click(); + await getMenuItem(otherPage, 'Administrator').click(); await container.getByRole('button', { name: 'Approve' }).click(); await expect(otherPage.getByText('Access Requests')).toBeHidden(); @@ -352,7 +353,7 @@ test.describe('Doc grid move', () => { await page.reload(); await row.getByText(`more_horiz`).click(); - await page.getByRole('menuitem', { name: 'Move into a doc' }).click(); + await getMenuItem(page, 'Move into a doc').click(); await expect( page.getByRole('dialog').getByRole('heading', { name: 'Move' }), diff --git a/src/frontend/apps/e2e/__tests__/app-impress/doc-grid.spec.ts b/src/frontend/apps/e2e/__tests__/app-impress/doc-grid.spec.ts index 95a4258e..eb13f07f 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/doc-grid.spec.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/doc-grid.spec.ts @@ -1,6 +1,11 @@ import { expect, test } from '@playwright/test'; -import { createDoc, getGridRow, verifyDocName } from './utils-common'; +import { + createDoc, + getGridRow, + getMenuItem, + verifyDocName, +} from './utils-common'; import { addNewMember, connectOtherUserToDoc } from './utils-share'; type SmallDoc = { @@ -99,7 +104,7 @@ test.describe('Document grid item options', () => { const row = await getGridRow(page, docTitle); await row.getByText(`more_horiz`).click(); - await page.getByRole('menuitem', { name: 'Share' }).click(); + await getMenuItem(page, 'Share').click(); await expect( page.getByRole('dialog').getByText('Share the document'), @@ -115,7 +120,7 @@ test.describe('Document grid item options', () => { // Pin await row.getByText(`more_horiz`).click(); - await page.getByRole('menuitem', { name: 'Pin' }).click(); + await getMenuItem(page, 'Pin').click(); // Check is pinned await expect(row.getByTestId('doc-pinned-icon')).toBeVisible(); @@ -142,7 +147,7 @@ test.describe('Document grid item options', () => { const row = await getGridRow(page, docTitle); await row.getByText(`more_horiz`).click(); - await page.getByRole('menuitem', { name: 'Delete' }).click(); + await getMenuItem(page, 'Delete').click(); await expect( page.getByRole('heading', { name: 'Delete a doc' }), diff --git a/src/frontend/apps/e2e/__tests__/app-impress/doc-header.spec.ts b/src/frontend/apps/e2e/__tests__/app-impress/doc-header.spec.ts index be74a2c5..98ed2731 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/doc-header.spec.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/doc-header.spec.ts @@ -3,6 +3,7 @@ import { expect, test } from '@playwright/test'; import { createDoc, getGridRow, + getMenuItem, goToGridDoc, mockedDocument, verifyDocName, @@ -78,11 +79,7 @@ test.describe('Doc Header', () => { await page.getByTestId('doc-visibility').click(); - await page - .getByRole('menuitem', { - name: 'Public', - }) - .click(); + await getMenuItem(page, 'Public').click(); await page.getByRole('button', { name: 'close' }).first().click(); @@ -156,10 +153,8 @@ test.describe('Doc Header', () => { const emojiPicker = page.locator('.--docs--doc-title').getByRole('button'); const optionMenu = page.getByLabel('Open the document options'); - const addEmojiMenuItem = page.getByRole('menuitem', { name: 'Add emoji' }); - const removeEmojiMenuItem = page.getByRole('menuitem', { - name: 'Remove emoji', - }); + const addEmojiMenuItem = getMenuItem(page, 'Add emoji'); + const removeEmojiMenuItem = getMenuItem(page, 'Remove emoji'); // Top parent should not have emoji picker await expect(emojiPicker).toBeHidden(); @@ -213,7 +208,7 @@ test.describe('Doc Header', () => { const [randomDoc] = await createDoc(page, 'doc-delete', browserName, 1); await page.getByLabel('Open the document options').click(); - await page.getByRole('menuitem', { name: 'Delete document' }).click(); + await getMenuItem(page, 'Delete document').click(); await expect( page.getByRole('heading', { name: 'Delete a doc' }), @@ -275,9 +270,7 @@ test.describe('Doc Header', () => { await page.getByLabel('Open the document options').click(); - await expect( - page.getByRole('menuitem', { name: 'Delete document' }), - ).toBeDisabled(); + await expect(getMenuItem(page, 'Delete document')).toBeDisabled(); // Click somewhere else to close the options await page.click('body', { position: { x: 0, y: 0 } }); @@ -300,7 +293,7 @@ test.describe('Doc Header', () => { await invitationRole.click(); - await page.getByRole('menuitem', { name: 'Remove access' }).click(); + await getMenuItem(page, 'Remove access').click(); await expect(invitationCard).toBeHidden(); const memberCard = shareModal.getByLabel('List members card'); @@ -312,9 +305,7 @@ test.describe('Doc Header', () => { await expect(roles).toBeVisible(); await roles.click(); - await expect( - page.getByRole('menuitem', { name: 'Remove access' }), - ).toBeEnabled(); + await expect(getMenuItem(page, 'Remove access')).toBeEnabled(); }); test('it checks the options available if editor', async ({ page }) => { @@ -354,9 +345,7 @@ test.describe('Doc Header', () => { ).toBeVisible(); await page.getByLabel('Open the document options').click(); - await expect( - page.getByRole('menuitem', { name: 'Delete document' }), - ).toBeDisabled(); + await expect(getMenuItem(page, 'Delete document')).toBeDisabled(); // Click somewhere else to close the options await page.click('body', { position: { x: 0, y: 0 } }); @@ -426,9 +415,7 @@ test.describe('Doc Header', () => { ).toBeVisible(); await page.getByLabel('Open the document options').click(); - await expect( - page.getByRole('menuitem', { name: 'Delete document' }), - ).toBeDisabled(); + await expect(getMenuItem(page, 'Delete document')).toBeDisabled(); // Click somewhere else to close the options await page.click('body', { position: { x: 0, y: 0 } }); @@ -486,7 +473,7 @@ test.describe('Doc Header', () => { // Copy content to clipboard await page.getByLabel('Open the document options').click(); - await page.getByRole('menuitem', { name: 'Copy as Markdown' }).click(); + await getMenuItem(page, 'Copy as Markdown').click(); await expect(page.getByText('Copied to clipboard')).toBeVisible(); // Test that clipboard is in Markdown format @@ -548,7 +535,7 @@ test.describe('Doc Header', () => { .click(); // Pin - await page.getByRole('menuitem', { name: 'Pin' }).click(); + await getMenuItem(page, 'Pin').click(); await page .getByRole('button', { name: 'Open the document options' }) .click(); @@ -569,11 +556,11 @@ test.describe('Doc Header', () => { .click(); // Unpin - await page.getByRole('menuitem', { name: 'Unpin' }).click(); + await getMenuItem(page, 'Unpin').click(); await page .getByRole('button', { name: 'Open the document options' }) .click(); - await expect(page.getByRole('menuitem', { name: 'Pin' })).toBeVisible(); + await expect(getMenuItem(page, 'Pin')).toBeVisible(); await page.goto('/'); @@ -591,7 +578,7 @@ test.describe('Doc Header', () => { await page.getByLabel('Open the document options').click(); - await page.getByRole('menuitem', { name: 'Duplicate' }).click(); + await getMenuItem(page, 'Duplicate').click(); await expect( page.getByText('Document duplicated successfully!'), ).toBeVisible(); @@ -606,7 +593,7 @@ test.describe('Doc Header', () => { await expect(row.getByText(duplicateTitle)).toBeVisible(); await row.getByText(`more_horiz`).click(); - await page.getByRole('menuitem', { name: 'Duplicate' }).click(); + await getMenuItem(page, 'Duplicate').click(); const duplicateDuplicateTitle = 'Copy of ' + duplicateTitle; await page.getByText(duplicateDuplicateTitle).click(); await expect(page.getByText('Hello Duplicated World')).toBeVisible(); @@ -639,7 +626,7 @@ test.describe('Doc Header', () => { const currentUrl = page.url(); - await page.getByRole('menuitem', { name: 'Duplicate' }).click(); + await getMenuItem(page, 'Duplicate').click(); await expect(page).not.toHaveURL(new RegExp(currentUrl)); @@ -678,10 +665,8 @@ test.describe('Documents Header mobile', () => { await expect(page.getByRole('button', { name: 'Copy link' })).toBeHidden(); await page.getByLabel('Open the document options').click(); - await expect( - page.getByRole('menuitem', { name: 'Copy link' }), - ).toBeVisible(); - await page.getByRole('menuitem', { name: 'Share' }).click(); + await expect(getMenuItem(page, 'Copy link')).toBeVisible(); + await getMenuItem(page, 'Share').click(); await expect(page.getByRole('button', { name: 'Copy link' })).toBeVisible(); }); @@ -704,7 +689,7 @@ test.describe('Documents Header mobile', () => { await goToGridDoc(page); await page.getByLabel('Open the document options').click(); - await page.getByRole('menuitem', { name: 'Share' }).click(); + await getMenuItem(page, 'Share').click(); const shareModal = page.getByRole('dialog', { name: 'Share the document', diff --git a/src/frontend/apps/e2e/__tests__/app-impress/doc-inherited-share.spec.ts b/src/frontend/apps/e2e/__tests__/app-impress/doc-inherited-share.spec.ts index 7ad02d31..452cead2 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/doc-inherited-share.spec.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/doc-inherited-share.spec.ts @@ -1,6 +1,6 @@ import { expect, test } from '@playwright/test'; -import { createDoc, verifyDocName } from './utils-common'; +import { createDoc, getMenuItem, verifyDocName } from './utils-common'; import { updateShareLink } from './utils-share'; import { createRootSubPage } from './utils-sub-pages'; @@ -53,19 +53,17 @@ test.describe('Inherited share accesses', () => { await expect(docVisibilityCard.getByText('Reading')).toBeVisible(); await docVisibilityCard.getByText('Reading').click(); - await page.getByRole('menuitem', { name: 'Editing' }).click(); + await getMenuItem(page, 'Editing').click(); await expect(docVisibilityCard.getByText('Reading')).toBeHidden(); await expect(docVisibilityCard.getByText('Editing')).toBeVisible(); // Verify inherited link await docVisibilityCard.getByText('Connected').click(); - await expect( - page.getByRole('menuitem', { name: 'Private' }), - ).toBeDisabled(); + await expect(getMenuItem(page, 'Private')).toBeDisabled(); // Update child link - await page.getByRole('menuitem', { name: 'Public' }).click(); + await getMenuItem(page, 'Public').click(); await expect(docVisibilityCard.getByText('Connected')).toBeHidden(); await expect( diff --git a/src/frontend/apps/e2e/__tests__/app-impress/doc-member-create.spec.ts b/src/frontend/apps/e2e/__tests__/app-impress/doc-member-create.spec.ts index 1b9867dd..a95b78aa 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/doc-member-create.spec.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/doc-member-create.spec.ts @@ -3,6 +3,7 @@ import { expect, test } from '@playwright/test'; import { BROWSERS, createDoc, + getMenuItem, keyCloakSignIn, randomName, verifyDocName, @@ -75,15 +76,13 @@ test.describe('Document create member', () => { // Check roles are displayed await list.getByTestId('doc-role-dropdown').click(); - await expect(page.getByRole('menuitem', { name: 'Reader' })).toBeVisible(); - await expect(page.getByRole('menuitem', { name: 'Editor' })).toBeVisible(); - await expect(page.getByRole('menuitem', { name: 'Owner' })).toBeVisible(); - await expect( - page.getByRole('menuitem', { name: 'Administrator' }), - ).toBeVisible(); + await expect(getMenuItem(page, 'Reader')).toBeVisible(); + await expect(getMenuItem(page, 'Editor')).toBeVisible(); + await expect(getMenuItem(page, 'Owner')).toBeVisible(); + await expect(getMenuItem(page, 'Administrator')).toBeVisible(); // Validate - await page.getByRole('menuitem', { name: 'Administrator' }).click(); + await getMenuItem(page, 'Administrator').click(); await page.getByTestId('doc-share-invite-button').click(); // Check invitation added @@ -129,7 +128,7 @@ test.describe('Document create member', () => { // Choose a role const container = page.getByTestId('doc-share-add-member-list'); await container.getByTestId('doc-role-dropdown').click(); - await page.getByRole('menuitem', { name: 'Owner' }).click(); + await getMenuItem(page, 'Owner').click(); const responsePromiseCreateInvitation = page.waitForResponse( (response) => @@ -147,7 +146,7 @@ test.describe('Document create member', () => { // Choose a role await container.getByTestId('doc-role-dropdown').click(); - await page.getByRole('menuitem', { name: 'Owner' }).click(); + await getMenuItem(page, 'Owner').click(); const responsePromiseCreateInvitationFail = page.waitForResponse( (response) => @@ -184,7 +183,7 @@ test.describe('Document create member', () => { // Choose a role const container = page.getByTestId('doc-share-add-member-list'); await container.getByTestId('doc-role-dropdown').click(); - await page.getByRole('menuitem', { name: 'Administrator' }).click(); + await getMenuItem(page, 'Administrator').click(); const responsePromiseCreateInvitation = page.waitForResponse( (response) => @@ -211,13 +210,13 @@ test.describe('Document create member', () => { ); await userInvitation.getByTestId('doc-role-dropdown').click(); - await page.getByRole('menuitem', { name: 'Reader' }).click(); + await getMenuItem(page, 'Reader').click(); const responsePatchInvitation = await responsePromisePatchInvitation; expect(responsePatchInvitation.ok()).toBeTruthy(); await userInvitation.getByTestId('doc-role-dropdown').click(); - await page.getByRole('menuitem', { name: 'Remove access' }).click(); + await getMenuItem(page, 'Remove access').click(); await expect(userInvitation).toBeHidden(); }); @@ -269,7 +268,7 @@ test.describe('Document create member', () => { `doc-share-access-request-row-${emailRequest}`, ); await container.getByTestId('doc-role-dropdown').click(); - await page.getByRole('menuitem', { name: 'Administrator' }).click(); + await getMenuItem(page, 'Administrator').click(); await container.getByRole('button', { name: 'Approve' }).click(); await expect(page.getByText('Access Requests')).toBeHidden(); diff --git a/src/frontend/apps/e2e/__tests__/app-impress/doc-member-list.spec.ts b/src/frontend/apps/e2e/__tests__/app-impress/doc-member-list.spec.ts index e87fc7df..62298109 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/doc-member-list.spec.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/doc-member-list.spec.ts @@ -1,6 +1,6 @@ import { expect, test } from '@playwright/test'; -import { createDoc, verifyDocName } from './utils-common'; +import { createDoc, getMenuItem, verifyDocName } from './utils-common'; import { addNewMember } from './utils-share'; test.beforeEach(async ({ page }) => { @@ -160,9 +160,7 @@ test.describe('Document list members', () => { `You are the sole owner of this group, make another member the group owner before you can change your own role or be removed from your document.`, ); await expect(soloOwner).toBeVisible(); - await expect( - page.getByRole('menuitem', { name: 'Administrator' }), - ).toBeDisabled(); + await expect(getMenuItem(page, 'Administrator')).toBeDisabled(); await list.click({ force: true, // Force click to close the dropdown @@ -185,18 +183,18 @@ test.describe('Document list members', () => { }); await currentUserRole.click(); - await page.getByRole('menuitem', { name: 'Administrator' }).click(); + await getMenuItem(page, 'Administrator').click(); await list.click(); await expect(currentUserRole).toBeVisible(); await newUserRoles.click(); - await expect(page.getByRole('menuitem', { name: 'Owner' })).toBeDisabled(); + await expect(getMenuItem(page, 'Owner')).toBeDisabled(); await list.click({ force: true, // Force click to close the dropdown }); await currentUserRole.click(); - await page.getByRole('menuitem', { name: 'Reader' }).click(); + await getMenuItem(page, 'Reader').click(); await list.click({ force: true, // Force click to close the dropdown }); @@ -236,11 +234,11 @@ test.describe('Document list members', () => { await expect(userReader).toBeVisible(); await userReaderRole.click(); - await page.getByRole('menuitem', { name: 'Remove access' }).click(); + await getMenuItem(page, 'Remove access').click(); await expect(userReader).toBeHidden(); await mySelfRole.click(); - await page.getByRole('menuitem', { name: 'Remove access' }).click(); + await getMenuItem(page, 'Remove access').click(); await expect( page.getByText('Insufficient access rights to view the document.'), ).toBeVisible(); diff --git a/src/frontend/apps/e2e/__tests__/app-impress/doc-search.spec.ts b/src/frontend/apps/e2e/__tests__/app-impress/doc-search.spec.ts index c9df5094..ecfbf517 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/doc-search.spec.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/doc-search.spec.ts @@ -1,6 +1,6 @@ import { expect, test } from '@playwright/test'; -import { createDoc, verifyDocName } from './utils-common'; +import { createDoc, getMenuItem, verifyDocName } from './utils-common'; import { createRootSubPage } from './utils-sub-pages'; test.beforeEach(async ({ page }) => { @@ -136,13 +136,9 @@ test.describe('Document search', () => { await filters.click(); await filters.getByRole('button', { name: 'Current doc' }).click(); - await expect( - page.getByRole('menuitem', { name: 'All docs' }), - ).toBeVisible(); - await expect( - page.getByRole('menuitem', { name: 'Current doc' }), - ).toBeVisible(); - await page.getByRole('menuitem', { name: 'All docs' }).click(); + await expect(getMenuItem(page, 'All docs')).toBeVisible(); + await expect(getMenuItem(page, 'Current doc')).toBeVisible(); + await getMenuItem(page, 'All docs').click(); await expect(page.getByRole('button', { name: 'Reset' })).toBeVisible(); }); diff --git a/src/frontend/apps/e2e/__tests__/app-impress/doc-tree.spec.ts b/src/frontend/apps/e2e/__tests__/app-impress/doc-tree.spec.ts index 7f5fbfa0..f3d0477c 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/doc-tree.spec.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/doc-tree.spec.ts @@ -3,6 +3,7 @@ import { expect, test } from '@playwright/test'; import { createDoc, expectLoginPage, + getMenuItem, keyCloakSignIn, updateDocTitle, verifyDocName, @@ -162,7 +163,7 @@ test.describe('Doc Tree', () => { ); const currentUserRole = currentUser.getByTestId('doc-role-dropdown'); await currentUserRole.click(); - await page.getByRole('menuitem', { name: 'Administrator' }).click(); + await getMenuItem(page, 'Administrator').click(); await list.click(); await page.getByRole('button', { name: 'Ok' }).click(); @@ -192,9 +193,10 @@ test.describe('Doc Tree', () => { const menu = child.getByText(`more_horiz`); await menu.click(); - await expect( - page.getByRole('menuitem', { name: 'Move to my docs' }), - ).toHaveAttribute('aria-disabled', 'true'); + await expect(getMenuItem(page, 'Move to my docs')).toHaveAttribute( + 'aria-disabled', + 'true', + ); }); test('keyboard navigation with Enter key opens documents', async ({ @@ -338,9 +340,7 @@ test.describe('Doc Tree', () => { await row.hover(); const menu = row.getByText(`more_horiz`); await menu.click(); - await expect( - page.getByRole('menuitem', { name: 'Remove emoji' }), - ).toBeHidden(); + await expect(getMenuItem(page, 'Remove emoji')).toBeHidden(); // Close the menu await page.keyboard.press('Escape'); @@ -360,7 +360,7 @@ test.describe('Doc Tree', () => { // Now remove the emoji using the new action await row.hover(); await menu.click(); - await page.getByRole('menuitem', { name: 'Remove emoji' }).click(); + await getMenuItem(page, 'Remove emoji').click(); await expect(row.getByText('😀')).toBeHidden(); await expect(titleEmojiPicker).toBeHidden(); @@ -390,11 +390,7 @@ test.describe('Doc Tree: Inheritance', () => { const selectVisibility = page.getByTestId('doc-visibility'); await selectVisibility.click(); - await page - .getByRole('menuitem', { - name: 'Public', - }) - .click(); + await getMenuItem(page, 'Public').click(); await expect( page.getByText('The document visibility has been updated.'), diff --git a/src/frontend/apps/e2e/__tests__/app-impress/doc-version.spec.ts b/src/frontend/apps/e2e/__tests__/app-impress/doc-version.spec.ts index 799f0b73..a7fddb39 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/doc-version.spec.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/doc-version.spec.ts @@ -2,6 +2,7 @@ import { expect, test } from '@playwright/test'; import { createDoc, + getMenuItem, goToGridDoc, mockedDocument, verifyDocName, @@ -20,7 +21,7 @@ test.describe('Doc Version', () => { // Initially, there is no version await page.getByLabel('Open the document options').click(); - await page.getByRole('menuitem', { name: 'Version history' }).click(); + await getMenuItem(page, 'Version history').click(); await expect(page.getByText('History', { exact: true })).toBeVisible(); const modal = page.getByRole('dialog', { name: 'Version history' }); @@ -74,7 +75,7 @@ test.describe('Doc Version', () => { ).toBeVisible(); await page.getByLabel('Open the document options').click(); - await page.getByRole('menuitem', { name: 'Version history' }).click(); + await getMenuItem(page, 'Version history').click(); await expect(panel).toBeVisible(); await expect(page.getByText('History', { exact: true })).toBeVisible(); @@ -124,9 +125,7 @@ test.describe('Doc Version', () => { await verifyDocName(page, 'Mocked document'); await page.getByLabel('Open the document options').click(); - await expect( - page.getByRole('menuitem', { name: 'Version history' }), - ).toBeDisabled(); + await expect(getMenuItem(page, 'Version history')).toBeDisabled(); }); test('it restores the doc version', async ({ page, browserName }) => { @@ -153,7 +152,7 @@ test.describe('Doc Version', () => { await expect(page.getByText('World')).toBeVisible(); await page.getByLabel('Open the document options').click(); - await page.getByRole('menuitem', { name: 'Version history' }).click(); + await getMenuItem(page, 'Version history').click(); const modal = page.getByRole('dialog', { name: 'Version history' }); const panel = modal.getByLabel('version list'); diff --git a/src/frontend/apps/e2e/__tests__/app-impress/doc-visibility.spec.ts b/src/frontend/apps/e2e/__tests__/app-impress/doc-visibility.spec.ts index d240c7bf..4da4fbbd 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/doc-visibility.spec.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/doc-visibility.spec.ts @@ -4,6 +4,7 @@ import { BROWSERS, createDoc, expectLoginPage, + getMenuItem, keyCloakSignIn, verifyDocName, } from './utils-common'; @@ -46,21 +47,17 @@ test.describe('Doc Visibility', () => { await expect(selectVisibility.getByText('Private')).toBeVisible(); - await expect( - page.getByRole('menuitem', { name: 'Read only' }), - ).toBeHidden(); - await expect( - page.getByRole('menuitem', { name: 'Can read and edit' }), - ).toBeHidden(); + await expect(getMenuItem(page, 'Read only')).toBeHidden(); + await expect(getMenuItem(page, 'Can read and edit')).toBeHidden(); await selectVisibility.click(); - await page.getByRole('menuitem', { name: 'Connected' }).click(); + await getMenuItem(page, 'Connected').click(); await expect(page.getByTestId('doc-access-mode')).toBeVisible(); await selectVisibility.click(); - await page.getByRole('menuitem', { name: 'Public' }).click(); + await getMenuItem(page, 'Public').click(); await expect(page.getByTestId('doc-access-mode')).toBeVisible(); }); @@ -205,11 +202,7 @@ test.describe('Doc Visibility: Public', () => { const selectVisibility = page.getByTestId('doc-visibility'); await selectVisibility.click(); - await page - .getByRole('menuitem', { - name: 'Public', - }) - .click(); + await getMenuItem(page, 'Public').click(); await expect( page.getByText('The document visibility has been updated.'), @@ -217,11 +210,7 @@ test.describe('Doc Visibility: Public', () => { await expect(page.getByTestId('doc-access-mode')).toBeVisible(); await page.getByTestId('doc-access-mode').click(); - await page - .getByRole('menuitem', { - name: 'Reading', - }) - .click(); + await getMenuItem(page, 'Reading').click(); await expect( page.getByText('The document visibility has been updated.').first(), @@ -307,18 +296,14 @@ test.describe('Doc Visibility: Public', () => { const selectVisibility = page.getByTestId('doc-visibility'); await selectVisibility.click(); - await page - .getByRole('menuitem', { - name: 'Public', - }) - .click(); + await getMenuItem(page, 'Public').click(); await expect( page.getByText('The document visibility has been updated.'), ).toBeVisible(); await page.getByTestId('doc-access-mode').click(); - await page.getByRole('menuitem', { name: 'Editing' }).click(); + await getMenuItem(page, 'Editing').click(); await expect( page.getByText('The document visibility has been updated.').first(), @@ -402,11 +387,7 @@ test.describe('Doc Visibility: Authenticated', () => { await page.getByRole('button', { name: 'Share' }).click(); const selectVisibility = page.getByTestId('doc-visibility'); await selectVisibility.click(); - await page - .getByRole('menuitem', { - name: 'Connected', - }) - .click(); + await getMenuItem(page, 'Connected').click(); await expect( page.getByText('The document visibility has been updated.'), @@ -454,11 +435,7 @@ test.describe('Doc Visibility: Authenticated', () => { await page.getByRole('button', { name: 'Share' }).click(); const selectVisibility = page.getByTestId('doc-visibility'); await selectVisibility.click(); - await page - .getByRole('menuitem', { - name: 'Connected', - }) - .click(); + await getMenuItem(page, 'Connected').click(); await expect( page.getByText('The document visibility has been updated.'), @@ -556,11 +533,7 @@ test.describe('Doc Visibility: Authenticated', () => { await page.getByRole('button', { name: 'Share' }).click(); const selectVisibility = page.getByTestId('doc-visibility'); await selectVisibility.click(); - await page - .getByRole('menuitem', { - name: 'Connected', - }) - .click(); + await getMenuItem(page, 'Connected').click(); await expect( page.getByText('The document visibility has been updated.'), @@ -568,7 +541,7 @@ test.describe('Doc Visibility: Authenticated', () => { const urlDoc = page.url(); await page.getByTestId('doc-access-mode').click(); - await page.getByRole('menuitem', { name: 'Editing' }).click(); + await getMenuItem(page, 'Editing').click(); await expect( page.getByText('The document visibility has been updated.').first(), diff --git a/src/frontend/apps/e2e/__tests__/app-impress/footer.spec.ts b/src/frontend/apps/e2e/__tests__/app-impress/footer.spec.ts index c7e03038..7fb446f7 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/footer.spec.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/footer.spec.ts @@ -1,6 +1,6 @@ import { expect, test } from '@playwright/test'; -import { overrideConfig } from './utils-common'; +import { getMenuItem, overrideConfig } from './utils-common'; test.describe('Footer', () => { test.use({ storageState: { cookies: [], origins: [] } }); @@ -47,7 +47,7 @@ test.describe('Footer', () => { // Check the translation const header = page.locator('header').first(); await header.getByRole('button').getByText('English').click(); - await page.getByRole('menuitem', { name: 'Français' }).click(); + await getMenuItem(page, 'Français').click(); await expect( page.locator('footer').getByText('Mentions légales'), @@ -131,7 +131,7 @@ test.describe('Footer', () => { // Check the translation const header = page.locator('header').first(); await header.getByRole('button').getByText('English').click(); - await page.getByRole('menuitem', { name: 'Français' }).click(); + await getMenuItem(page, 'Français').click(); await expect( page diff --git a/src/frontend/apps/e2e/__tests__/app-impress/help.spec.ts b/src/frontend/apps/e2e/__tests__/app-impress/help.spec.ts index fcb90775..707b6e14 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/help.spec.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/help.spec.ts @@ -2,6 +2,7 @@ import { expect, test } from '@playwright/test'; import { TestLanguage, + getMenuItem, overrideConfig, waitForLanguageSwitch, } from './utils-common'; @@ -44,7 +45,7 @@ test.describe('Help feature', () => { await page.getByRole('button', { name: 'Open onboarding menu' }).click(); - await page.getByRole('menuitem', { name: 'Onboarding' }).click(); + await getMenuItem(page, 'Onboarding').click(); const modal = page.getByTestId('onboarding-modal'); await expect(modal).toBeVisible(); @@ -87,7 +88,7 @@ test.describe('Help feature', () => { test('closes modal with Skip button', async ({ page }) => { await page.getByRole('button', { name: 'Open onboarding menu' }).click(); - await page.getByRole('menuitem', { name: 'Onboarding' }).click(); + await getMenuItem(page, 'Onboarding').click(); const modal = page.getByTestId('onboarding-modal'); await expect(modal).toBeVisible(); @@ -110,7 +111,7 @@ test.describe('Help feature', () => { .getByRole('button', { name: "Ouvrir le menu d'embarquement" }) .click(); - await page.getByRole('menuitem', { name: 'Premiers pas' }).click(); + await getMenuItem(page, 'Premiers pas').click(); const modal = page.getByLabel('Apprenez les principes fondamentaux'); diff --git a/src/frontend/apps/e2e/__tests__/app-impress/language.spec.ts b/src/frontend/apps/e2e/__tests__/app-impress/language.spec.ts index 8538239e..f6e80f5b 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/language.spec.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/language.spec.ts @@ -75,7 +75,7 @@ test.describe('Language', () => { await expect(page.locator('[role="menu"]')).toBeVisible(); - const menuItems = page.getByRole('menuitem'); + const menuItems = page.locator('[role="menuitem"], [role="menuitemradio"]'); await expect(menuItems.first()).toBeVisible(); await menuItems.first().click(); diff --git a/src/frontend/apps/e2e/__tests__/app-impress/utils-common.ts b/src/frontend/apps/e2e/__tests__/app-impress/utils-common.ts index 920497d2..b6481b3c 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/utils-common.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/utils-common.ts @@ -3,6 +3,16 @@ import path from 'path'; import { Locator, Page, TestInfo, expect } from '@playwright/test'; +/** Returns a locator for a menu item (handles both menuitem and menuitemradio roles) */ +export const getMenuItem = ( + context: Page | Locator, + name: string, + options?: { exact?: boolean }, +): Locator => + context + .getByRole('menuitem', { name, exact: options?.exact }) + .or(context.getByRole('menuitemradio', { name, exact: options?.exact })); + import theme_customization from '../../../../../backend/impress/configuration/theme/default.json'; export type BrowserName = 'chromium' | 'firefox' | 'webkit'; @@ -382,12 +392,12 @@ export async function waitForLanguageSwitch( await languagePicker.click(); - await page.getByRole('menuitem', { name: lang.label }).click(); + await getMenuItem(page, lang.label).click(); } export const clickInEditorMenu = async (page: Page, textButton: string) => { await page.getByRole('button', { name: 'Open the document options' }).click(); - await page.getByRole('menuitem', { name: textButton }).click(); + await getMenuItem(page, textButton).click(); }; export const clickInGridMenu = async ( @@ -398,7 +408,7 @@ export const clickInGridMenu = async ( await row .getByRole('button', { name: /Open the menu of actions for the document/ }) .click(); - await page.getByRole('menuitem', { name: textButton }).click(); + await getMenuItem(page, textButton).click(); }; export const writeReport = async ( diff --git a/src/frontend/apps/e2e/__tests__/app-impress/utils-share.ts b/src/frontend/apps/e2e/__tests__/app-impress/utils-share.ts index 4260aafd..5f9a4cd4 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/utils-share.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/utils-share.ts @@ -2,6 +2,7 @@ import { Page, chromium, expect } from '@playwright/test'; import { BrowserName, + getMenuItem, getOtherBrowserName, keyCloakSignIn, verifyDocName, @@ -39,7 +40,7 @@ export const addNewMember = async ( // Choose a role await page.getByTestId('doc-role-dropdown').click(); - await page.getByRole('menuitem', { name: role }).click(); + await getMenuItem(page, role).click(); await page.getByTestId('doc-share-invite-button').click(); return users[index].email; @@ -51,7 +52,7 @@ export const updateShareLink = async ( linkRole?: LinkRole | null, ) => { await page.getByTestId('doc-visibility').click(); - await page.getByRole('menuitem', { name: linkReach }).click(); + await getMenuItem(page, linkReach).click(); const visibilityUpdatedText = page .getByText('The document visibility has been updated') @@ -61,7 +62,7 @@ export const updateShareLink = async ( if (linkRole) { await page.getByTestId('doc-access-mode').click(); - await page.getByRole('menuitem', { name: linkRole }).click(); + await getMenuItem(page, linkRole).click(); await expect(visibilityUpdatedText).toBeVisible(); } }; @@ -76,7 +77,7 @@ export const updateRoleUser = async ( const currentUser = list.getByTestId(`doc-share-member-row-${email}`); const currentUserRole = currentUser.getByTestId('doc-role-dropdown'); await currentUserRole.click(); - await page.getByRole('menuitem', { name: role }).click(); + await getMenuItem(page, role).click(); await list.click(); }; diff --git a/src/frontend/apps/impress/src/components/DropButton.tsx b/src/frontend/apps/impress/src/components/DropButton.tsx index ba9a2712..141eb9d8 100644 --- a/src/frontend/apps/impress/src/components/DropButton.tsx +++ b/src/frontend/apps/impress/src/components/DropButton.tsx @@ -93,6 +93,8 @@ export const DropButton = ({ onOpenChangeHandler(true); }} aria-label={label} + aria-haspopup="true" + aria-expanded={isLocalOpen} data-testid={testId} $css={css` font-family: ${font}; diff --git a/src/frontend/apps/impress/src/components/dropdown-menu/DropdownMenu.tsx b/src/frontend/apps/impress/src/components/dropdown-menu/DropdownMenu.tsx index 05cba790..9d0c5d74 100644 --- a/src/frontend/apps/impress/src/components/dropdown-menu/DropdownMenu.tsx +++ b/src/frontend/apps/impress/src/components/dropdown-menu/DropdownMenu.tsx @@ -110,6 +110,10 @@ export const DropdownMenu = ({ [onOpenChange], ); + const hasSelectable = + selectedValues !== undefined || + options.some((option) => option.isSelected !== undefined); + if (disabled) { return children; } @@ -172,6 +176,11 @@ export const DropdownMenu = ({ } const isDisabled = option.disabled !== undefined && option.disabled; const isFocused = index === focusedIndex; + const ariaChecked = hasSelectable + ? option.isSelected || + selectedValues?.includes(option.value ?? '') || + false + : undefined; return ( @@ -179,7 +188,8 @@ export const DropdownMenu = ({ ref={(el) => { menuItemRefs.current[index] = el; }} - role="menuitem" + role={hasSelectable ? 'menuitemradio' : 'menuitem'} + aria-checked={ariaChecked} data-testid={option.testId} $direction="row" disabled={isDisabled} diff --git a/src/frontend/apps/impress/src/components/dropdown-menu/__tests__/DropdownMenu.spec.tsx b/src/frontend/apps/impress/src/components/dropdown-menu/__tests__/DropdownMenu.spec.tsx new file mode 100644 index 00000000..6210c9fc --- /dev/null +++ b/src/frontend/apps/impress/src/components/dropdown-menu/__tests__/DropdownMenu.spec.tsx @@ -0,0 +1,119 @@ +import { render, screen, waitFor, within } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import React from 'react'; +import { describe, expect, test, vi } from 'vitest'; + +import { AppWrapper } from '@/tests/utils'; + +import { DropdownMenu, DropdownMenuOption } from '../DropdownMenu'; + +const mockAddLastFocus = vi.fn(); +vi.mock('@/stores', () => ({ + useFocusStore: (selector: any) => + selector({ addLastFocus: mockAddLastFocus }), + useResponsiveStore: () => ({ isDesktop: true }), +})); + +const baseOptions: DropdownMenuOption[] = [ + { label: 'English', callback: vi.fn() }, + { label: 'Français', callback: vi.fn() }, + { label: 'Deutsch', callback: vi.fn() }, +]; + +const selectableOptions: DropdownMenuOption[] = [ + { label: 'English', isSelected: false, callback: vi.fn() }, + { label: 'Français', isSelected: true, callback: vi.fn() }, + { label: 'Deutsch', isSelected: false, callback: vi.fn() }, +]; + +describe('', () => { + test('renders menuitem role when options have no selection', async () => { + render( + + Open menu + , + { wrapper: AppWrapper }, + ); + + const items = screen.getAllByRole('menuitem'); + expect(items).toHaveLength(3); + items.forEach((item) => { + expect(item).not.toHaveAttribute('aria-checked'); + }); + }); + + test('renders menuitemradio role with aria-checked when options have isSelected', async () => { + render( + + Français + , + { wrapper: AppWrapper }, + ); + + const radios = screen.getAllByRole('menuitemradio'); + expect(radios).toHaveLength(3); + + expect(radios[0]).toHaveAttribute('aria-checked', 'false'); + expect(radios[1]).toHaveAttribute('aria-checked', 'true'); + expect(radios[2]).toHaveAttribute('aria-checked', 'false'); + }); + + test('renders menuitemradio role with aria-checked when selectedValues is provided', async () => { + const optionsWithValues: DropdownMenuOption[] = [ + { label: 'English', value: 'en', callback: vi.fn() }, + { label: 'Français', value: 'fr', callback: vi.fn() }, + { label: 'Deutsch', value: 'de', callback: vi.fn() }, + ]; + + render( + + Français + , + { wrapper: AppWrapper }, + ); + + const radios = screen.getAllByRole('menuitemradio'); + expect(radios).toHaveLength(3); + + expect(radios[0]).toHaveAttribute('aria-checked', 'false'); + expect(radios[1]).toHaveAttribute('aria-checked', 'true'); + expect(radios[2]).toHaveAttribute('aria-checked', 'false'); + }); + + test('trigger button has aria-haspopup and aria-expanded', async () => { + render( + + Français + , + { wrapper: AppWrapper }, + ); + + const trigger = screen.getByRole('button', { name: 'Select language' }); + expect(trigger).toHaveAttribute('aria-haspopup', 'true'); + expect(trigger).toHaveAttribute('aria-expanded', 'false'); + + await userEvent.click(trigger); + await waitFor(() => { + expect(trigger).toHaveAttribute('aria-expanded', 'true'); + }); + }); + + test('displays check icon for selected item', async () => { + render( + + Français + , + { wrapper: AppWrapper }, + ); + + const menu = screen.getByRole('menu'); + const checkedItem = within(menu).getAllByRole('menuitemradio')[1]; + expect(checkedItem).toHaveAttribute('aria-checked', 'true'); + expect(within(checkedItem).getByText('Français')).toBeInTheDocument(); + }); +});