Skip to content
Merged
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
37 changes: 37 additions & 0 deletions .drone.star
Original file line number Diff line number Diff line change
Expand Up @@ -571,6 +571,7 @@ def e2eTestsOnPlaywright(ctx):
environment = {
"BASE_URL_OCIS": "ocis:9200",
"PLAYWRIGHT_BROWSERS_PATH": ".playwright",
"TESTS_RUNNER": "playwright",
}

steps += restoreBuildArtifactCache(ctx, "pnpm", ".pnpm-store") + \
Expand All @@ -594,6 +595,9 @@ def e2eTestsOnPlaywright(ctx):
],
}]

if not "skip-a11y" in ctx.build.title.lower():
steps += uploadA11yResult(ctx)

pipelines.append({
"kind": "pipeline",
"type": "docker",
Expand Down Expand Up @@ -637,6 +641,7 @@ def e2eTests(ctx):
"federationServer": False,
"failOnUncaughtConsoleError": "false",
"extraServerEnvironment": {},
"skipA11y": "false",
}

e2e_trigger = {
Expand Down Expand Up @@ -671,6 +676,9 @@ def e2eTests(ctx):
if ("with-tracing" in ctx.build.title.lower()):
params["reportTracing"] = "true"

if "skip-a11y" in ctx.build.title.lower():
params["skipA11y"] = "true"

environment = {
"HEADLESS": "true",
"RETRY": "1",
Expand All @@ -680,6 +688,7 @@ def e2eTests(ctx):
"PLAYWRIGHT_BROWSERS_PATH": ".playwright",
"BROWSER": "chromium",
"FEDERATED_BASE_URL_OCIS": "federation-ocis:9200",
"SKIP_A11Y_TESTS": params["skipA11y"],
}

steps += restoreBuildArtifactCache(ctx, "pnpm", ".pnpm-store") + \
Expand Down Expand Up @@ -2009,3 +2018,31 @@ def restoreBrowsersCache():
],
},
]

def uploadA11yResult(ctx):
return [
{
"name": "upload-a11y-result",
"image": PLUGINS_S3_IMAGE,
"pull": "if-not-exists",
"settings": {
"bucket": S3_PUBLIC_CACHE_BUCKET,
"endpoint": S3_CACHE_SERVER,
"path_style": True,
"source": "%s/reports/e2e/a11y-report.json" % dir["web"],
"strip_prefix": "%s/reports/e2e/" % dir["web"],
"target": "/${DRONE_REPO}/${DRONE_BUILD_NUMBER}/a11y",
},
"environment": {
"AWS_ACCESS_KEY_ID": {
"from_secret": "cache_public_s3_access_key",
},
"AWS_SECRET_ACCESS_KEY": {
"from_secret": "cache_public_s3_secret_key",
},
},
"when": {
"status": ["failure", "success"],
},
},
]
48 changes: 48 additions & 0 deletions docs/testing/accesibility.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
---
title: 'Accessibility'
date: 2025-11-10T00:00:00+00:00
weight: 60
geekdocRepo: https://github.com/owncloud/web
geekdocEditPath: edit/master/docs/testing
geekdocFilePath: accessibility.md
---

{{< toc >}}

## Introduction

Accessibility is a crucial aspect of web development. It ensures that web applications are usable by everyone, including people with disabilities.

## What Tools We Use

### eslint-plugin-vuejs-accessibility

We use [eslint-plugin-vuejs-accessibility](https://github.com/vue-a11y/eslint-plugin-vuejs-accessibility) to quickly catch accessibility issues in the codebase. This plugin is used in the [@ownclouders/eslint-config](https://www.npmjs.com/package/@ownclouders/eslint-config).

### @axe-core/playwright

We use [@axe-core/playwright](https://github.com/dequelabs/axe-core-npm) to automatically test the accessibility of the ownCloud Web client. All tests are run automatically on every PR and on commits to the `master` branch. We are not running dedicated accessibility tests and instead make them part of the E2E tests. This way we do not have to maintain duplicate tests and we can granularly add accessibility tests to the specific steps of the tests. The tests are considered failed if any `serious` or `critical` accessibility violations are found.

#### Running Accessibility Tests

To run the accessibility tests, you can simply run our existing E2E tests using the following command:

```bash
pnpm test:e2e:cucumber
```

#### Skipping Accessibility Tests Locally

If you want to skip the accessibility tests, you can add the `SKIP_A11Y_TESTS` environment variable to your command.

```bash
SKIP_A11Y_TESTS=true pnpm test:e2e:cucumber
```

#### Skipping Accessibility Tests in CI

If you want to skip the accessibility tests in CI, you can add the `[skip-a11y]` flag into the title of the PR.

#### Accessibility Report

After the tests are run, a JSON accessibility report is generated in the `reports/e2e/a11y-report.json` file. This report contains detailed information about the accessibility violations found in the tests.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
"@vue/compiler-dom": "3.5.24",
"@vue/compiler-sfc": "3.5.24",
"@vue/test-utils": "2.4.6",
"axe-core": "^4.11.0",
"browserslist-to-esbuild": "^2.1.1",
"browserslist-useragent-regexp": "^4.1.3",
"commander": "14.0.2",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import OcLoader from './OcLoader.vue'
import { mount } from '@ownclouders/web-test-helpers'

const selectors = {
label: '[data-testid="oc-loader-label"]'
}

describe('OcLoader', () => {
function getWrapper(props = {}) {
return mount(OcLoader, {
Expand All @@ -9,7 +13,7 @@ describe('OcLoader', () => {
}
it('should set provided aria-label', () => {
const wrapper = getWrapper({ ariaLabel: 'test' })
expect(wrapper.attributes('aria-label')).toBe('test')
expect(wrapper.find(selectors.label).text()).toBe('test')
})
describe('when prop flat is enabled', () => {
it('should set loader flat class to the wrapper', () => {
Expand Down
4 changes: 3 additions & 1 deletion packages/design-system/src/components/OcLoader/OcLoader.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
<template>
<div :class="['oc-loader', { 'oc-loader-flat': flat }]" :aria-label="ariaLabel" />
<div :class="['oc-loader', { 'oc-loader-flat': flat }]">
<span class="oc-invisible-sr" data-testid="oc-loader-label" v-text="ariaLabel" />
</div>
</template>

<script lang="ts" setup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,12 +53,7 @@
:title="$gettext('External user')"
/>
</div>
<div
v-if="isExternalShare"
class="oc-text-small"
data-testid="external-share-domain"
:aria-label="`External Share Domain: ${externalShareDomainName}`"
>
<div v-if="isExternalShare" class="oc-text-small" data-testid="external-share-domain">
{{ externalShareDomainName }}
</div>
<div>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,6 @@
<template>
<div class="oc-flex oc-flex-center expiration-date-indicator">
<oc-icon
v-oc-tooltip="expirationDateTooltip"
:aria-label="expirationDateTooltip"
name="calendar-event"
fill-type="line"
/>
<oc-icon v-oc-tooltip="expirationDateTooltip" name="calendar-event" fill-type="line" />
<span class="oc-invisible-sr" v-text="screenreaderShareExpiration" />
</div>
</template>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
class="oc-files-file-link-has-password oc-mr-xs"
fill-type="line"
:aria-label="$gettext('This link is password-protected')"
role="img"
/>
</div>
<expiration-date-indicator
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@
:type="componentType"
v-bind="componentProps"
:class="[action.class, 'action-menu-item', 'oc-py-s', 'oc-px-m', 'oc-width-1-1']"
:aria-label="componentProps.disabled ? action.disabledTooltip?.(actionOptions) : ''"
:aria-label="componentProps.disabled ? action.disabledTooltip?.(actionOptions) : null"
data-testid="action-handler"
:size="size"
justify-content="left"
:title="action.label(actionOptions)"
v-on="componentListeners"
>
<oc-img
Expand Down
6 changes: 6 additions & 0 deletions packages/web-pkg/src/components/CreateLinkModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,12 @@
appearance="filled"
variation="primary"
:disabled="confirmButtonDisabled"
:title="
$pgettext(
'Create link modal confirmation dropdown button title',
'Additional copy options'
)
"
>
<oc-icon size="small" name="arrow-down-s" />
</oc-button>
Expand Down
3 changes: 3 additions & 0 deletions pnpm-lock.yaml

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

14 changes: 12 additions & 2 deletions tests/e2e-playwright/playwright.config.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import { defineConfig, devices } from '@playwright/test'
import path from 'node:path'
import { defineConfig, devices, ReporterDescription } from '@playwright/test'
import { config } from '../e2e/config'

const __dirname = path.dirname(new URL(import.meta.url).pathname)
const reportsDir = path.resolve(__dirname, '../../', config.reportDir)

/**
* See https://playwright.dev/docs/test-configuration.
Expand All @@ -20,7 +25,12 @@ export default defineConfig({
workers: process.env.CI ? 1 : undefined,

// Reporter to use
reporter: 'html',
reporter: [
(process.env.CI && ['dot']) as ReporterDescription,
(!process.env.CI && ['list']) as ReporterDescription,
['./reporters/a11y.ts', { outputFile: path.join(reportsDir, 'a11y-report.json') }]
].filter(Boolean) as ReporterDescription[],
outputDir: reportsDir,

use: {
ignoreHTTPSErrors: true,
Expand Down
125 changes: 125 additions & 0 deletions tests/e2e-playwright/reporters/a11y.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import type {
Reporter,
FullConfig,
Suite,
TestCase,
TestResult,
FullResult
} from '@playwright/test/reporter'
import { AxeResults } from 'axe-core'
import * as fs from 'fs/promises'
import * as path from 'path'

interface A11yTestResult {
test: string
file: string
line: number
status: string
duration: number
url?: string
violations: AxeResults['violations']
violationCount: number
passCount: number
incompleteCount: number
}

interface A11yReport {
summary: {
totalTests: number
totalViolations: number
totalPasses: number
totalIncomplete: number
timestamp: string
duration: number
}
tests: A11yTestResult[]
}

class A11yReporter implements Reporter {
private results: A11yTestResult[] = []
private outputFile: string
private startTime: number = 0

constructor(options: { outputFile?: string } = {}) {
this.outputFile = options.outputFile || 'a11y-report.json'
}

onBegin(_config: FullConfig, _suite: Suite): void {
this.startTime = Date.now()
}

onTestEnd(test: TestCase, result: TestResult): void {
result.attachments.forEach((attachment) => {
if (attachment.name !== 'accessibility-scan') {
return
}

try {
let axeResults: AxeResults

if (attachment.body) {
axeResults = JSON.parse(attachment.body.toString('utf-8'))
} else if (attachment.path) {
const content = require('fs').readFileSync(attachment.path, 'utf-8')
axeResults = JSON.parse(content)
} else {
throw new Error('No accessibility scan attachment found')
}

this.results.push({
test: test.title,
file: path.relative(process.cwd(), test.location.file),
line: test.location.line,
status: result.status,
duration: result.duration,
url: axeResults.url,
violations: axeResults.violations || [],
violationCount: axeResults.violations?.length || 0,
passCount: axeResults.passes?.length || 0,
incompleteCount: axeResults.incomplete?.length || 0
})
} catch (error) {
console.error(`Error parsing accessibility results for test "${test.title}":`, error)
}
})
}

async onEnd(_result: FullResult): Promise<void> {
const duration = Date.now() - this.startTime
const totalViolations = this.results.reduce((sum, test) => sum + test.violationCount, 0)

const report: A11yReport = {
summary: {
totalTests: this.results.length,
totalViolations,
totalPasses: this.results.reduce((sum, test) => sum + test.passCount, 0),
totalIncomplete: this.results.reduce((sum, test) => sum + test.incompleteCount, 0),
timestamp: new Date().toISOString(),
duration
},
tests: this.results
}

try {
const outputDir = path.dirname(this.outputFile)
await fs.mkdir(outputDir, { recursive: true })

await fs.writeFile(this.outputFile, JSON.stringify(report, null, 2), 'utf-8')

console.info(`\n📊 Accessibility Report Generated:`)
console.info(` File: ${this.outputFile}`)
console.info(` Tests: ${report.summary.totalTests}`)
console.warn(` Violations: ${report.summary.totalViolations}`)

if (totalViolations > 0) {
console.warn(`\n⚠️ Found ${totalViolations} accessibility violations`)
} else {
console.info(`\n✅ No accessibility violations found`)
}
} catch (error) {
console.error('Error writing accessibility report:', error)
}
}
}

export default A11yReporter
Loading