️(frontend) fix language dropdown ARIA for screen readers

Add aria-haspopup, aria-expanded and menuitemradio pattern for SR.
This commit is contained in:
Cyril 2026-03-16 16:26:28 +01:00
parent 4e54a53072
commit 0e5c9ed834
No known key found for this signature in database
GPG key ID: D5E8474B0AB0064A
22 changed files with 269 additions and 194 deletions

View file

@ -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

View file

@ -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();
}
});
});

View file

@ -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',

View file

@ -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();

View file

@ -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' }),

View file

@ -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' }),

View file

@ -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',

View file

@ -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(

View file

@ -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();

View file

@ -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();

View file

@ -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();
});

View file

@ -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.'),

View file

@ -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');

View file

@ -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(),

View file

@ -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

View file

@ -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');

View file

@ -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();

View file

@ -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 (

View file

@ -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();
};

View file

@ -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};

View file

@ -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 (
<Fragment key={option.label}>
@ -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}

View file

@ -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('<DropdownMenu />', () => {
test('renders menuitem role when options have no selection', async () => {
render(
<DropdownMenu options={baseOptions} label="Options" opened>
Open menu
</DropdownMenu>,
{ 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(
<DropdownMenu options={selectableOptions} label="Select language" opened>
Français
</DropdownMenu>,
{ 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(
<DropdownMenu
options={optionsWithValues}
selectedValues={['fr']}
label="Select language"
opened
>
Français
</DropdownMenu>,
{ 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(
<DropdownMenu options={baseOptions} label="Select language">
Français
</DropdownMenu>,
{ 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(
<DropdownMenu options={selectableOptions} label="Select language" opened>
Français
</DropdownMenu>,
{ 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();
});
});