From 9cb2b6a6fb10106197bf91ef1e01fd100072d4e7 Mon Sep 17 00:00:00 2001 From: Cyril Date: Thu, 7 Aug 2025 09:55:28 +0200 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8(frontend)=20improve=20accessibility?= =?UTF-8?q?=20of=20cdoc=20content=20with=20correct=20aria=20tags?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit added appropriate aria attributes and semantic tags to enhance accessibility Signed-off-by: Cyril --- CHANGELOG.md | 1 + .../__tests__/app-impress/doc-create.spec.ts | 8 +- .../__tests__/app-impress/doc-export.spec.ts | 74 +++++++++---------- .../__tests__/app-impress/doc-header.spec.ts | 21 ++++-- .../__tests__/app-impress/doc-routing.spec.ts | 49 ++++++------ .../__tests__/app-impress/doc-tree.spec.ts | 4 +- .../e2e/__tests__/app-impress/utils-common.ts | 12 +-- .../doc-export/components/ModalExport.tsx | 2 +- .../docs/doc-header/components/DocTitle.tsx | 5 +- .../docs/doc-header/components/DocToolBox.tsx | 10 ++- .../components/SimpleDocItem.tsx | 2 +- .../doc-share/components/DocRoleDropdown.tsx | 2 +- .../apps/impress/src/i18n/translations.json | 12 +++ 13 files changed, 115 insertions(+), 87 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8776690e..47a628b9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,6 +38,7 @@ and this project adheres to - ♿(frontend) improve accessibility for decorative images in editor #1282 - #1338 - #1281 + - #1271 - ♻️(backend) fallback to email identifier when no name #1298 - 🐛(backend) allow ASCII characters in user sub field #1295 - ⚡️(frontend) improve fallback width calculation #1333 diff --git a/src/frontend/apps/e2e/__tests__/app-impress/doc-create.spec.ts b/src/frontend/apps/e2e/__tests__/app-impress/doc-create.spec.ts index 43a855cf..f2b43299 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/doc-create.spec.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/doc-create.spec.ts @@ -45,8 +45,8 @@ test.describe('Doc Create', () => { }) .click(); - const input = page.getByRole('textbox', { name: 'doc title input' }); - await expect(input).toHaveText(''); + const input = page.getByRole('textbox', { name: 'Document title' }); + await expect(input).toHaveText('', { timeout: 10000 }); await expect( page.locator('.c__tree-view--row-content').getByText('Untitled document'), ).toBeVisible(); @@ -67,8 +67,8 @@ test.describe('Doc Create', () => { .getByText('New sub-doc') .click(); - const input = page.getByRole('textbox', { name: 'doc title input' }); - await expect(input).toHaveText(''); + const input = page.getByRole('textbox', { name: 'Document title' }); + await expect(input).toHaveText('', { timeout: 10000 }); await expect( page.locator('.c__tree-view--row-content').getByText('Untitled document'), ).toBeVisible(); diff --git a/src/frontend/apps/e2e/__tests__/app-impress/doc-export.spec.ts b/src/frontend/apps/e2e/__tests__/app-impress/doc-export.spec.ts index 5d8feab0..6450da39 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/doc-export.spec.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/doc-export.spec.ts @@ -40,7 +40,7 @@ test.describe('Doc Export', () => { await expect( page.getByRole('button', { name: 'Close the modal' }), ).toBeVisible(); - await expect(page.getByTestId('modal-download-button')).toBeVisible(); + await expect(page.getByTestId('doc-export-download-button')).toBeVisible(); }); test('it exports the doc with pdf line break', async ({ @@ -81,12 +81,7 @@ test.describe('Doc Export', () => { return download.suggestedFilename().includes(`${randomDoc}.pdf`); }); - void page - .getByRole('button', { - name: 'Download', - exact: true, - }) - .click(); + void page.getByTestId('doc-export-download-button').click(); const download = await downloadPromise; expect(download.suggestedFilename()).toBe(`${randomDoc}.pdf`); @@ -131,13 +126,13 @@ test.describe('Doc Export', () => { await page.getByRole('combobox', { name: 'Format' }).click(); await page.getByRole('option', { name: 'Docx' }).click(); - await expect(page.getByTestId('modal-download-button')).toBeVisible(); + await expect(page.getByTestId('doc-export-download-button')).toBeVisible(); const downloadPromise = page.waitForEvent('download', (download) => { return download.suggestedFilename().includes(`${randomDoc}.docx`); }); - void page.getByTestId('modal-download-button').click(); + void page.getByTestId('doc-export-download-button').click(); const download = await downloadPromise; expect(download.suggestedFilename()).toBe(`${randomDoc}.docx`); @@ -203,7 +198,7 @@ test.describe('Doc Export', () => { await new Promise((resolve) => setTimeout(resolve, 1000)); - await expect(page.getByTestId('modal-download-button')).toBeVisible(); + await expect(page.getByTestId('doc-export-download-button')).toBeVisible(); const responseCorsPromise = page.waitForResponse( (response) => @@ -214,7 +209,7 @@ test.describe('Doc Export', () => { return download.suggestedFilename().includes(`${randomDoc}.pdf`); }); - void page.getByTestId('modal-download-button').click(); + void page.getByTestId('doc-export-download-button').click(); const responseCors = await responseCorsPromise; expect(responseCors.ok()).toBe(true); @@ -256,13 +251,13 @@ test.describe('Doc Export', () => { }) .click(); - await expect(page.getByTestId('modal-download-button')).toBeVisible(); + await expect(page.getByTestId('doc-export-download-button')).toBeVisible(); const downloadPromise = page.waitForEvent('download', (download) => { return download.suggestedFilename().includes(`${randomDoc}.pdf`); }); - void page.getByTestId('modal-download-button').click(); + void page.getByTestId('doc-export-download-button').click(); const download = await downloadPromise; expect(download.suggestedFilename()).toBe(`${randomDoc}.pdf`); @@ -298,13 +293,15 @@ test.describe('Doc Export', () => { }) .click(); - await expect(page.getByTestId('modal-download-button')).toBeVisible(); + await expect( + page.getByTestId('doc-open-modal-download-button'), + ).toBeVisible(); const downloadPromise = page.waitForEvent('download', (download) => { return download.suggestedFilename().includes(`${randomDoc}.pdf`); }); - void page.getByTestId('modal-download-button').click(); + void page.getByTestId('doc-export-download-button').click(); const download = await downloadPromise; expect(download.suggestedFilename()).toBe(`${randomDoc}.pdf`); @@ -350,13 +347,15 @@ test.describe('Doc Export', () => { }) .click(); - await expect(page.getByTestId('modal-download-button')).toBeVisible(); + await expect( + page.getByTestId('doc-open-modal-download-button'), + ).toBeVisible(); const downloadPromise = page.waitForEvent('download', (download) => { return download.suggestedFilename().includes(`${randomDoc}.pdf`); }); - void page.getByTestId('modal-download-button').click(); + void page.getByTestId('doc-export-download-button').click(); const download = await downloadPromise; expect(download.suggestedFilename()).toBe(`${randomDoc}.pdf`); @@ -392,14 +391,9 @@ test.describe('Doc Export', () => { }) .click(); - await page.waitForURL('**/docs/**', { - timeout: 10000, - waitUntil: 'domcontentloaded', - }); - - const input = page.getByLabel('doc title input'); + const input = page.locator('.--docs--doc-title-input[role="textbox"]'); await expect(input).toBeVisible(); - await expect(input).toHaveText(''); + await expect(input).toHaveText('', { timeout: 10000 }); await input.click(); await input.fill(randomDocFrench); await input.blur(); @@ -418,7 +412,7 @@ test.describe('Doc Export', () => { return download.suggestedFilename().includes(`${randomDocFrench}.pdf`); }); - void page.getByTestId('modal-download-button').click(); + void page.getByTestId('doc-export-download-button').click(); const download = await downloadPromise; expect(download.suggestedFilename()).toBe(`${randomDocFrench}.pdf`); @@ -453,19 +447,23 @@ test.describe('Doc Export', () => { await page.locator('.bn-block-outer').last().fill('/'); await page.getByText('Link a doc').first().click(); - await page - .locator( - "span[data-inline-content-type='interlinkingSearchInline'] input", - ) - .fill('interlink-child'); + const input = page.locator( + "span[data-inline-content-type='interlinkingSearchInline'] input", + ); + const searchContainer = page.locator('.quick-search-container'); - await page - .locator('.quick-search-container') - .getByText('interlink-child') - .click(); + await input.fill('export-interlink'); - const interlink = page.getByRole('link', { - name: 'interlink-child', + await expect(searchContainer).toBeVisible(); + await expect(searchContainer.getByText(randomDoc)).toBeVisible(); + + // We are in docChild, we want to create a link to randomDoc (parent) + await searchContainer.getByText(randomDoc).click(); + + // Search the interlinking link in the editor (not in the document tree) + const editor = page.locator('.ProseMirror.bn-editor'); + const interlink = editor.getByRole('link', { + name: randomDoc, }); await expect(interlink).toBeVisible(); @@ -480,7 +478,7 @@ test.describe('Doc Export', () => { }) .click(); - void page.getByTestId('modal-download-button').click(); + void page.getByTestId('doc-export-download-button').click(); const download = await downloadPromise; expect(download.suggestedFilename()).toBe(`${docChild}.pdf`); @@ -488,6 +486,6 @@ test.describe('Doc Export', () => { const pdfBuffer = await cs.toBuffer(await download.createReadStream()); const pdfData = await pdf(pdfBuffer); - expect(pdfData.text).toContain('interlink-child'); // This is the pdf text + expect(pdfData.text).toContain(randomDoc); }); }); 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 b76f718e..c707163e 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 @@ -25,7 +25,7 @@ test.describe('Doc Header', () => { 'It is the card information about the document.', ); - const docTitle = card.getByRole('textbox', { name: 'doc title input' }); + const docTitle = card.getByRole('textbox', { name: 'Document title' }); await expect(docTitle).toBeVisible(); await page.getByRole('button', { name: 'Share' }).click(); @@ -54,7 +54,7 @@ test.describe('Doc Header', () => { test('it updates the title doc', async ({ page, browserName }) => { await createDoc(page, 'doc-update', browserName, 1); - const docTitle = page.getByRole('textbox', { name: 'doc title input' }); + const docTitle = page.getByRole('textbox', { name: 'Document title' }); await expect(docTitle).toBeVisible(); await docTitle.fill('Hello World'); await docTitle.blur(); @@ -66,7 +66,7 @@ test.describe('Doc Header', () => { browserName, }) => { await createDoc(page, 'doc-update', browserName, 1); - const docTitle = page.getByRole('textbox', { name: 'doc title input' }); + const docTitle = page.getByRole('textbox', { name: 'Document title' }); await expect(docTitle).toBeVisible(); await docTitle.fill('👍 Hello Emoji World'); await docTitle.blur(); @@ -228,23 +228,27 @@ test.describe('Doc Header', () => { await page.getByRole('button', { name: 'Share' }).click(); - const shareModal = page.getByLabel('Share modal'); + const shareModal = page.getByRole('dialog', { + name: 'Share modal content', + }); + await expect(shareModal).toBeVisible(); await expect(page.getByText('Share the document')).toBeVisible(); await expect(page.getByPlaceholder('Type a name or email')).toBeHidden(); const invitationCard = shareModal.getByLabel('List invitation card'); + await expect(invitationCard).toBeVisible(); await expect( invitationCard.getByText('test@invitation.test').first(), ).toBeVisible(); - await expect(invitationCard.getByLabel('doc-role-text')).toBeVisible(); + await expect(invitationCard.getByLabel('Document role text')).toBeVisible(); await expect( invitationCard.getByRole('button', { name: 'more_horiz' }), ).toBeHidden(); const memberCard = shareModal.getByLabel('List members card'); await expect(memberCard.getByText('test@accesses.test')).toBeVisible(); - await expect(memberCard.getByLabel('doc-role-text')).toBeVisible(); + await expect(memberCard.getByLabel('Document role text')).toBeVisible(); await expect( memberCard.getByRole('button', { name: 'more_horiz' }), ).toBeHidden(); @@ -296,17 +300,18 @@ test.describe('Doc Header', () => { await expect(page.getByPlaceholder('Type a name or email')).toBeHidden(); const invitationCard = shareModal.getByLabel('List invitation card'); + await expect(invitationCard).toBeVisible(); await expect( invitationCard.getByText('test@invitation.test').first(), ).toBeVisible(); - await expect(invitationCard.getByLabel('doc-role-text')).toBeVisible(); + await expect(invitationCard.getByLabel('Document role text')).toBeVisible(); await expect( invitationCard.getByRole('button', { name: 'more_horiz' }), ).toBeHidden(); const memberCard = shareModal.getByLabel('List members card'); await expect(memberCard.getByText('test@accesses.test')).toBeVisible(); - await expect(memberCard.getByLabel('doc-role-text')).toBeVisible(); + await expect(memberCard.getByLabel('Document role text')).toBeVisible(); await expect( memberCard.getByRole('button', { name: 'more_horiz' }), ).toBeHidden(); diff --git a/src/frontend/apps/e2e/__tests__/app-impress/doc-routing.spec.ts b/src/frontend/apps/e2e/__tests__/app-impress/doc-routing.spec.ts index 1d64435f..2c084eec 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/doc-routing.spec.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/doc-routing.spec.ts @@ -60,32 +60,37 @@ test.describe('Doc Routing', () => { await page.locator('.ProseMirror.bn-editor').fill('Hello World'); - const responsePromise = page.route( - /.*\/documents\/.*\/$|users\/me\/$/, - async (route) => { - const request = route.request(); + // Wait for the doc link (via its dynamic title) to be visible + const docLink = page.getByRole('link', { name: docTitle }); + await expect(docLink).toBeVisible(); - if ( - request.method().includes('PATCH') || - request.method().includes('GET') - ) { - await route.fulfill({ - status: 401, - json: { - detail: 'Log in to access the document', - }, - }); - } else { - await route.continue(); - } - }, + // Intercept GET/PATCH requests to return 401 + await page.route(/.*\/documents\/.*\/$|users\/me\/$/, async (route) => { + const request = route.request(); + if ( + request.method().includes('PATCH') || + request.method().includes('GET') + ) { + await route.fulfill({ + status: 401, + json: { detail: 'Log in to access the document' }, + }); + } else { + await route.continue(); + } + }); + + // Explicitly wait for a 401 response after clicking + const wait401 = page.waitForResponse( + (resp) => + resp.status() === 401 && + /\/(documents\/[^/]+\/|users\/me\/)$/.test(resp.url()), ); - await page.getByRole('link', { name: '401-doc-parent' }).click(); + await docLink.click(); + await wait401; - await responsePromise; - - await expect(page.getByText('Log in to access the document')).toBeVisible({ + await expect(page.getByText('Log in to access the document.')).toBeVisible({ timeout: 10000, }); }); 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 a22c38ba..a0a155ff 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 @@ -50,7 +50,7 @@ test.describe('Doc Tree', () => { await expect(subPageItem).toBeVisible(); await subPageItem.click(); await verifyDocName(page, ''); - const input = page.getByRole('textbox', { name: 'doc title input' }); + const input = page.getByRole('textbox', { name: 'Document title' }); await input.click(); const [randomDocName] = randomName('doc-tree-test', browserName, 1); await input.fill(randomDocName); @@ -196,7 +196,7 @@ test.describe('Doc Tree', () => { await page.getByText('Move to my docs').click(); await expect( - page.getByRole('textbox', { name: 'doc title input' }), + page.getByRole('textbox', { name: 'Document title' }), ).not.toHaveText(docChild); const header = page.locator('header').first(); 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 dbab861d..504f0b4e 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/utils-common.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/utils-common.ts @@ -101,10 +101,9 @@ export const createDoc = async ( waitUntil: 'networkidle', }); - const input = page.getByLabel('doc title input'); + const input = page.getByLabel('Document title'); await expect(input).toBeVisible(); await expect(input).toHaveText(''); - await input.click(); await input.fill(randomDocs[i]); await input.blur(); @@ -120,10 +119,11 @@ export const verifyDocName = async (page: Page, docName: string) => { timeout: 10000, }); + /*replace toHaveText with toContainText to handle cases where emojis or other characters might be added*/ try { await expect( - page.getByRole('textbox', { name: 'doc title input' }), - ).toHaveText(docName); + page.getByRole('textbox', { name: 'Document title' }), + ).toContainText(docName); } catch { await expect(page.getByRole('heading', { name: docName })).toBeVisible(); } @@ -182,9 +182,9 @@ export const goToGridDoc = async ( }; export const updateDocTitle = async (page: Page, title: string) => { - const input = page.getByLabel('doc title input'); - await expect(input).toBeVisible(); + const input = page.getByRole('textbox', { name: 'Document title' }); await expect(input).toHaveText(''); + await expect(input).toBeVisible(); await input.click(); await input.fill(title); await input.click(); diff --git a/src/frontend/apps/impress/src/features/docs/doc-export/components/ModalExport.tsx b/src/frontend/apps/impress/src/features/docs/doc-export/components/ModalExport.tsx index 39df90be..b23a94eb 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-export/components/ModalExport.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-export/components/ModalExport.tsx @@ -145,12 +145,12 @@ export const ModalExport = ({ onClose, doc }: ModalExportProps) => { {t('Cancel')} diff --git a/src/frontend/apps/impress/src/features/docs/doc-header/components/DocTitle.tsx b/src/frontend/apps/impress/src/features/docs/doc-header/components/DocTitle.tsx index f8b404f6..c1e9fb24 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-header/components/DocTitle.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-header/components/DocTitle.tsx @@ -105,7 +105,7 @@ const DocTitleInput = ({ doc }: DocTitleProps) => { }, [doc]); return ( - + { defaultValue={titleDisplay || undefined} onKeyDownCapture={handleKeyDown} suppressContentEditableWarning={true} - aria-label="doc title input" + aria-label={`${t('Document title')}`} + aria-multiline={false} onBlurCapture={(event) => handleTitleSubmit(event.target.textContent || '') } diff --git a/src/frontend/apps/impress/src/features/docs/doc-header/components/DocToolBox.tsx b/src/frontend/apps/impress/src/features/docs/doc-header/components/DocToolBox.tsx index f0c11928..778a99b2 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-header/components/DocToolBox.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-header/components/DocToolBox.tsx @@ -215,7 +215,7 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => { >