Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@
"yjs": "^13.6.27"
},
"devDependencies": {
"@axe-core/playwright": "^4.11.0",
"@nextcloud/babel-config": "^1.3.0",
"@nextcloud/browserslist-config": "^3.1.2",
"@nextcloud/e2e-test-server": "^0.4.0",
Expand Down
199 changes: 199 additions & 0 deletions playwright/e2e/accessibility.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
/**
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

import { mergeTests } from '@playwright/test'
import { test as accessibilityTest, expect } from '../support/fixtures/accessibility'
import { test as editorTest } from '../support/fixtures/editor'
import { test as uploadFileTest } from '../support/fixtures/upload-file'
import {
formatViolations,
getAccessibilitySummary,
violationFingerprints,
} from '../support/utils/accessibility'

const test = mergeTests(accessibilityTest, editorTest, uploadFileTest)

test.describe('Editor Accessibility', () => {
test('should not have automatically detectable accessibility violations on the full page', async ({
page,
open,
makeAxeBuilder,
}, testInfo) => {
await open()

// Wait for the editor to be fully loaded
await page.waitForSelector('.text-editor', { state: 'visible' })

// Run the accessibility scan
const accessibilityScanResults = await makeAxeBuilder().analyze()

// Attach the full scan results for debugging
await testInfo.attach('accessibility-scan-results', {
body: JSON.stringify(accessibilityScanResults, null, 2),
contentType: 'application/json',
})

// Attach a summary for quick overview
const summary = getAccessibilitySummary(accessibilityScanResults)
await testInfo.attach('accessibility-summary', {
body: JSON.stringify(summary, null, 2),
contentType: 'application/json',
})

// Snapshot test for violations to track changes over time
expect(
violationFingerprints(accessibilityScanResults),
formatViolations(accessibilityScanResults),
).toMatchSnapshot('accessibility-violations.json')
})

test('should not have accessibility violations in the editor content area', async ({
page,
open,
makeAxeBuilder,
editor,
}, testInfo) => {
await open()
await editor.type('Test content')

// Wait for the editor to be ready
await page.waitForSelector('.text-editor', { state: 'visible' })

// Scan only the editor content area
const accessibilityScanResults = await makeAxeBuilder()
.include('.text-editor')
.analyze()

await testInfo.attach('editor-content-scan-results', {
body: JSON.stringify(accessibilityScanResults, null, 2),
contentType: 'application/json',
})

expect(
violationFingerprints(accessibilityScanResults),
formatViolations(accessibilityScanResults),
).toMatchSnapshot('editor-content-violations.json')
})

test('should not have accessibility violations in the menu bar', async ({
page,
open,
makeAxeBuilder,
}, testInfo) => {
await open()

// Wait for the menu bar to be visible
await page.waitForSelector('.text-menubar', { state: 'visible' })

// Scan only the menu bar
const accessibilityScanResults = await makeAxeBuilder()
.include('.text-menubar')
.analyze()

await testInfo.attach('menubar-scan-results', {
body: JSON.stringify(accessibilityScanResults, null, 2),
contentType: 'application/json',
})

expect(
violationFingerprints(accessibilityScanResults),
formatViolations(accessibilityScanResults),
).toMatchSnapshot('menubar-violations.json')
})

test('should have proper keyboard navigation support', async ({
page,
open,
makeAxeBuilder,
}, testInfo) => {
await open()

// Wait for the editor to be fully loaded
await page.waitForSelector('.text-editor', { state: 'visible' })

// Check for keyboard-specific accessibility issues
const accessibilityScanResults = await makeAxeBuilder()
// Focus on keyboard accessibility rules (WCAG only, not best-practice)
.withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa'])
.analyze()

await testInfo.attach('keyboard-navigation-scan-results', {
body: JSON.stringify(accessibilityScanResults, null, 2),
contentType: 'application/json',
})

// Check that interactive elements are keyboard accessible
expect(
violationFingerprints(accessibilityScanResults),
formatViolations(accessibilityScanResults),
).toMatchSnapshot('keyboard-navigation-violations.json')
})

test('should have proper ARIA labels on interactive elements', async ({
page,
open,
makeAxeBuilder,
editor,
}, testInfo) => {
await open()

// Open a menu to check its accessibility
const boldButton = editor.getMenu('Bold')
await expect(boldButton).toBeVisible()

// Wait for all menu items to be rendered
await page.waitForSelector('[role="button"], button', { state: 'attached' })

// Scan for ARIA-related issues
const accessibilityScanResults = await makeAxeBuilder()
.withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa'])
.analyze()

await testInfo.attach('aria-labels-scan-results', {
body: JSON.stringify(accessibilityScanResults, null, 2),
contentType: 'application/json',
})

expect(
violationFingerprints(accessibilityScanResults),
formatViolations(accessibilityScanResults),
).toMatchSnapshot('aria-labels-violations.json')
})

test('should maintain accessibility after text formatting', async ({
page,
open,
makeAxeBuilder,
editor,
}, testInfo) => {
await open()

// Type some text and format it
await editor.type('Format me')
const CtrlOrCmd = process.platform === 'darwin' ? 'Meta' : 'Control'
await editor.content.press(CtrlOrCmd + '+a')
await editor.getMenu('Bold').click()
await editor.getMenu('Italic').click()

// Wait for formatting to be applied
await page.waitForSelector('strong', { state: 'attached' })
await page.waitForSelector('em', { state: 'attached' })

// Scan after formatting operations
const accessibilityScanResults = await makeAxeBuilder()
.include('.text-editor')
.analyze()

await testInfo.attach('formatted-content-scan-results', {
body: JSON.stringify(accessibilityScanResults, null, 2),
contentType: 'application/json',
})

expect(
violationFingerprints(accessibilityScanResults),
formatViolations(accessibilityScanResults),
).toMatchSnapshot('formatted-content-violations.json')
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
[
{
"rule": "label",
"description": "Ensure every form element has a label",
"targets": [
[
"input[data-cy-upload-picker-input=\"\"]"
]
],
"impact": "critical"
}
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
[
{
"rule": "label",
"description": "Ensure every form element has a label",
"targets": [
[
"input[data-cy-upload-picker-input=\"\"]"
]
],
"impact": "critical"
}
]
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[]
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[]
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
[
{
"rule": "label",
"description": "Ensure every form element has a label",
"targets": [
[
"input[data-cy-upload-picker-input=\"\"]"
]
],
"impact": "critical"
}
]
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[]
28 changes: 28 additions & 0 deletions playwright/support/fixtures/accessibility.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/**
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

import { test as base } from '@playwright/test'
import AxeBuilder from '@axe-core/playwright'

Check warning on line 7 in playwright/support/fixtures/accessibility.ts

View workflow job for this annotation

GitHub Actions / NPM lint

Using exported name 'AxeBuilder' as identifier for default import

type AxeFixture = {
makeAxeBuilder: () => AxeBuilder
}

export const test = base.extend<AxeFixture>({
makeAxeBuilder: async ({ page }, use) => {
const makeAxeBuilder = () => new AxeBuilder({ page })
// Test against WCAG 2.0 and 2.1 Level A and AA
.withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa'])

// Use legacy mode for compatibility with custom Playwright fixtures
// See: https://github.com/dequelabs/axe-core-npm/blob/develop/packages/playwright/error-handling.md
.setLegacyMode()


await use(makeAxeBuilder)
},
})

export { expect } from '@playwright/test'
Loading
Loading