Skip to content

Commit 8f83269

Browse files
committed
test: [OCISDEV-417] add a11y playwright tests
Setup new Playwright a11y tests and add a11y reporter.
1 parent de71d61 commit 8f83269

File tree

18 files changed

+340
-9
lines changed

18 files changed

+340
-9
lines changed

.drone.star

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -571,6 +571,7 @@ def e2eTestsOnPlaywright(ctx):
571571
environment = {
572572
"BASE_URL_OCIS": "ocis:9200",
573573
"PLAYWRIGHT_BROWSERS_PATH": ".playwright",
574+
"TESTS_RUNNER": "playwright",
574575
}
575576

576577
steps += restoreBuildArtifactCache(ctx, "pnpm", ".pnpm-store") + \

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@
5757
"@vue/compiler-dom": "3.5.22",
5858
"@vue/compiler-sfc": "3.5.22",
5959
"@vue/test-utils": "2.4.6",
60+
"axe-core": "^4.11.0",
6061
"browserslist-to-esbuild": "^2.1.1",
6162
"browserslist-useragent-regexp": "^4.1.3",
6263
"commander": "14.0.2",

pnpm-lock.yaml

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tests/e2e-playwright/playwright.config.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
1-
import { defineConfig, devices } from '@playwright/test'
1+
import path from 'node:path'
2+
import { defineConfig, devices, ReporterDescription } from '@playwright/test'
3+
import { config } from '../e2e/config'
4+
5+
const __dirname = path.dirname(new URL(import.meta.url).pathname)
6+
const reportsDir = path.resolve(__dirname, '../../', config.reportDir)
27

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

2227
// Reporter to use
23-
reporter: 'html',
28+
reporter: [
29+
(process.env.CI && ['dot']) as ReporterDescription,
30+
(!process.env.CI && ['list']) as ReporterDescription,
31+
['./reporters/a11y.ts', { outputFile: path.join(reportsDir, 'a11y-report.json') }]
32+
].filter(Boolean) as ReporterDescription[],
33+
outputDir: reportsDir,
2434

2535
use: {
2636
ignoreHTTPSErrors: true,
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
import type {
2+
Reporter,
3+
FullConfig,
4+
Suite,
5+
TestCase,
6+
TestResult,
7+
FullResult
8+
} from '@playwright/test/reporter'
9+
import { AxeResults } from 'axe-core'
10+
import * as fs from 'fs/promises'
11+
import * as path from 'path'
12+
13+
interface A11yTestResult {
14+
test: string
15+
file: string
16+
line: number
17+
status: string
18+
duration: number
19+
url?: string
20+
violations: AxeResults['violations']
21+
violationCount: number
22+
passCount: number
23+
incompleteCount: number
24+
}
25+
26+
interface A11yReport {
27+
summary: {
28+
totalTests: number
29+
totalViolations: number
30+
totalPasses: number
31+
totalIncomplete: number
32+
timestamp: string
33+
duration: number
34+
}
35+
tests: A11yTestResult[]
36+
}
37+
38+
class A11yReporter implements Reporter {
39+
private results: A11yTestResult[] = []
40+
private outputFile: string
41+
private startTime: number = 0
42+
43+
constructor(options: { outputFile?: string } = {}) {
44+
this.outputFile = options.outputFile || 'a11y-report.json'
45+
}
46+
47+
onBegin(_config: FullConfig, _suite: Suite): void {
48+
this.startTime = Date.now()
49+
}
50+
51+
onTestEnd(test: TestCase, result: TestResult): void {
52+
result.attachments.forEach((attachment) => {
53+
if (attachment.name !== 'accessibility-scan') {
54+
return
55+
}
56+
57+
try {
58+
let axeResults: AxeResults
59+
60+
if (attachment.body) {
61+
axeResults = JSON.parse(attachment.body.toString('utf-8'))
62+
} else if (attachment.path) {
63+
const content = require('fs').readFileSync(attachment.path, 'utf-8')
64+
axeResults = JSON.parse(content)
65+
} else {
66+
throw new Error('No accessibility scan attachment found')
67+
}
68+
69+
this.results.push({
70+
test: test.title,
71+
file: path.relative(process.cwd(), test.location.file),
72+
line: test.location.line,
73+
status: result.status,
74+
duration: result.duration,
75+
url: axeResults.url,
76+
violations: axeResults.violations || [],
77+
violationCount: axeResults.violations?.length || 0,
78+
passCount: axeResults.passes?.length || 0,
79+
incompleteCount: axeResults.incomplete?.length || 0
80+
})
81+
} catch (error) {
82+
console.error(`Error parsing accessibility results for test "${test.title}":`, error)
83+
}
84+
})
85+
}
86+
87+
async onEnd(_result: FullResult): Promise<void> {
88+
const duration = Date.now() - this.startTime
89+
const totalViolations = this.results.reduce((sum, test) => sum + test.violationCount, 0)
90+
91+
const report: A11yReport = {
92+
summary: {
93+
totalTests: this.results.length,
94+
totalViolations,
95+
totalPasses: this.results.reduce((sum, test) => sum + test.passCount, 0),
96+
totalIncomplete: this.results.reduce((sum, test) => sum + test.incompleteCount, 0),
97+
timestamp: new Date().toISOString(),
98+
duration
99+
},
100+
tests: this.results
101+
}
102+
103+
try {
104+
const outputDir = path.dirname(this.outputFile)
105+
await fs.mkdir(outputDir, { recursive: true })
106+
107+
await fs.writeFile(this.outputFile, JSON.stringify(report, null, 2), 'utf-8')
108+
109+
console.info(`\n📊 Accessibility Report Generated:`)
110+
console.info(` File: ${this.outputFile}`)
111+
console.info(` Tests: ${report.summary.totalTests}`)
112+
console.warn(` Violations: ${report.summary.totalViolations}`)
113+
114+
if (totalViolations > 0) {
115+
console.warn(`\n⚠️ Found ${totalViolations} accessibility violations`)
116+
} else {
117+
console.info(`\n✅ No accessibility violations found`)
118+
}
119+
} catch (error) {
120+
console.error('Error writing accessibility report:', error)
121+
}
122+
}
123+
}
124+
125+
export default A11yReporter

tests/e2e-playwright/steps/api/api.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,20 @@ export async function userHasBeenCreated({
2020
}
2121
}
2222

23+
export async function deleteUser({
24+
usersEnvironment,
25+
stepUser,
26+
targetUser
27+
}: {
28+
usersEnvironment: UsersEnvironment
29+
stepUser: string
30+
targetUser: string
31+
}): Promise<void> {
32+
const admin = usersEnvironment.getUser({ key: stepUser })
33+
const user = usersEnvironment.getUser({ key: targetUser })
34+
await api.provision.deleteUser({ user, admin })
35+
}
36+
2337
export async function userHasCreatedFolder({
2438
usersEnvironment,
2539
stepUser,
@@ -33,6 +47,23 @@ export async function userHasCreatedFolder({
3347
await api.dav.createFolderInsidePersonalSpace({ user, folder: folderName })
3448
}
3549

50+
export async function userHasCreatedFile({
51+
usersEnvironment,
52+
stepUser,
53+
filename
54+
}: {
55+
usersEnvironment: UsersEnvironment
56+
stepUser: string
57+
filename: string
58+
}): Promise<void> {
59+
const user = usersEnvironment.getUser({ key: stepUser })
60+
await api.dav.uploadFileInPersonalSpace({
61+
user,
62+
pathToFile: filename,
63+
content: 'This is a test file'
64+
})
65+
}
66+
3667
export async function userHasSharedResource({
3768
usersEnvironment,
3869
stepUser,

tests/e2e-playwright/steps/ui/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@ export * from './public'
22
export * from './shares'
33
export * from './resources'
44
export * from './session'
5+
export * from './spaces'

tests/e2e-playwright/steps/ui/public.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { objects } from '../../../e2e/support'
22
import { ActorsEnvironment, LinksEnvironment } from '../../../e2e/support/environment'
3+
import { substitute } from '../../../e2e/support/utils'
34

45
export async function openPublicLink({
56
actorsEnvironment,
@@ -21,3 +22,19 @@ export async function openPublicLink({
2122
const pageObject = new objects.applicationFiles.page.Public({ page })
2223
await pageObject.open({ url })
2324
}
25+
26+
export async function createPublicLink({
27+
actorsEnvironment,
28+
stepUser,
29+
resource,
30+
password
31+
}: {
32+
actorsEnvironment: ActorsEnvironment
33+
stepUser: string
34+
resource: string
35+
password: string
36+
}): Promise<void> {
37+
const { page } = actorsEnvironment.getActor({ key: stepUser })
38+
const publicObject = new objects.applicationFiles.Link({ page })
39+
await publicObject.create({ resource, password: substitute(password) })
40+
}

tests/e2e-playwright/steps/ui/resources.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ export async function uploadResource({
2121
const { page } = actorsEnvironment.getActor({ key: stepUser })
2222
const resourceObject = new objects.applicationFiles.Resource({ page })
2323
await resourceObject.upload({
24+
returnToOriginalPage: false,
2425
to: to,
2526
resources: [filesEnvironment.getFile({ name: resource })],
2627
option: option,
@@ -42,3 +43,20 @@ export async function isAbleToEditFileOrFolder({
4243
const userCanEdit = await resourceObject.canManageResource({ resource })
4344
return userCanEdit
4445
}
46+
47+
export async function createDir({
48+
actorsEnvironment,
49+
stepUser,
50+
directoryName
51+
}: {
52+
actorsEnvironment: ActorsEnvironment
53+
stepUser: string
54+
directoryName: string
55+
}): Promise<void> {
56+
const { page } = actorsEnvironment.getActor({ key: stepUser })
57+
const resourceObject = new objects.applicationFiles.Resource({ page })
58+
await resourceObject.create({
59+
name: directoryName,
60+
type: 'folder'
61+
})
62+
}

tests/e2e-playwright/steps/ui/shares.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,3 +112,35 @@ export async function updateShareeRole({
112112
})
113113
}
114114
}
115+
116+
export async function shareResource({
117+
actorsEnvironment,
118+
usersEnvironment,
119+
stepUser,
120+
resource,
121+
resourceType,
122+
recipient
123+
}: {
124+
actorsEnvironment: ActorsEnvironment
125+
usersEnvironment: UsersEnvironment
126+
stepUser: string
127+
resource: string
128+
resourceType: ResourceType
129+
recipient: string
130+
}): Promise<void> {
131+
const { page } = actorsEnvironment.getActor({ key: stepUser })
132+
const shareObject = new objects.applicationFiles.Share({ page })
133+
const roleId = await getDynamicRoleIdByName(
134+
usersEnvironment.getUser({ key: stepUser }),
135+
'Can view',
136+
resourceType
137+
)
138+
139+
await shareObject.create({
140+
resource,
141+
recipients: [
142+
{ collaborator: usersEnvironment.getUser({ key: recipient }), role: roleId, type: 'user' }
143+
],
144+
via: 'QUICK_ACTION'
145+
})
146+
}

0 commit comments

Comments
 (0)