mirror of
https://github.com/owncloud/ocis.git
synced 2026-10-25 11:39:24 +03:00
- OcBreadcrumb: add `inert` to truncation placeholder <li> so its focusable <a> child is unreachable (fixes aria-hidden-focus) - OcTable: remove aria-hidden from header cell span and fall back to field.name so icon/avatar columns always have discernible text (fixes empty-table-header) - Plain layout: wrap router-view in <main> so login/public-link pages have a landmark (fixes region violations) - SidebarNav: change root element to <aside> with aria-label so the versions div is inside a landmark (fixes region violations) - web-app-ocm App: change root element to <main> (fixes region) - Add v-if="pageTitle" to sr-only <h1> in 6 components so empty headings are never rendered (fixes empty-heading) - Disable aria-allowed-role in search dropdown check — axe 4.11 incorrectly rejects <li role="group"> inside <ul role="listbox"> which is valid per ARIA 1.2 - Disable label rule in Application.open() check for pages that may transiently render Keycloak's OTP setup form - Remove stale post-navigation check in createFileFromTemplate sidebar branch (sidebar is unmounted before check runs)
194 lines
6.7 KiB
TypeScript
194 lines
6.7 KiB
TypeScript
import { Page } from '@playwright/test'
|
|
import util from 'util'
|
|
import { config } from '../../../config'
|
|
import { objects } from '../../index'
|
|
|
|
const appSwitcherButton = '#_appSwitcherButton'
|
|
const appSelector = `//ul[contains(@class, "applications-list")]//*[@data-test-id="%s"]`
|
|
const notificationsBell = `#oc-notifications-bell`
|
|
const notificationsDrop = `#oc-notifications-drop`
|
|
const notificationsLoading = `#oc-notifications-drop .oc-notifications-loading`
|
|
const markNotificationsAsReadButton = `#oc-notifications-drop .oc-notifications-mark-all`
|
|
const notificationItemsMessages = `#oc-notifications-drop .oc-notifications-item .oc-notifications-message`
|
|
const closeSidebarRootPanelBtn = `#app-sidebar .is-active-root-panel .header__close`
|
|
const closeSidebarSubPanelBtn = `#app-sidebar .is-active-sub-panel .header__close`
|
|
|
|
export class Application {
|
|
#page: Page
|
|
|
|
constructor({ page }: { page: Page }) {
|
|
this.#page = page
|
|
}
|
|
|
|
async reloadPage(): Promise<void> {
|
|
await this.#page.reload()
|
|
await objects.a11y.Accessibility.assertNoSevereA11yViolations(
|
|
this.#page,
|
|
['body'],
|
|
'body after page reload'
|
|
)
|
|
}
|
|
|
|
async open({ name }: { name: string }): Promise<void> {
|
|
await this.#page.waitForTimeout(1000)
|
|
await this.#page.locator(appSwitcherButton).click()
|
|
await objects.a11y.Accessibility.assertNoSevereA11yViolations(
|
|
this.#page,
|
|
['appSwitcherDropdown'],
|
|
'app switcher dropdown'
|
|
)
|
|
await this.#page.locator(util.format(appSelector, `app.${name}.menuItem`)).click()
|
|
await objects.a11y.Accessibility.assertNoSevereA11yViolations(
|
|
this.#page,
|
|
['body'],
|
|
`${name} app page`,
|
|
// the global search input can transiently keep aria-controls pointing at its
|
|
// (already unmounted) options dropdown for one render frame right after a route
|
|
// change - see editor.ts close() for the same, more thoroughly investigated case
|
|
//
|
|
// 'label' is disabled because on first admin login the OIDC redirect can land on
|
|
// Keycloak's own "Configure OTP" required-action page (#totp/#userLabel inputs),
|
|
// whose markup is Keycloak's default theme, not oCIS web's
|
|
['aria-valid-attr-value', 'label']
|
|
)
|
|
}
|
|
|
|
async getNotificationMessages(): Promise<string[]> {
|
|
// reload will fetch notifications immediately
|
|
// wait for the notifications to load
|
|
await Promise.all([
|
|
this.#page.waitForResponse(
|
|
(resp) =>
|
|
resp.url().endsWith('notifications') &&
|
|
resp.status() === 200 &&
|
|
resp.request().method() === 'GET'
|
|
),
|
|
this.#page.reload()
|
|
])
|
|
|
|
const dropIsOpen = await this.#page.locator(notificationsDrop).isVisible()
|
|
if (!dropIsOpen) {
|
|
await this.#page.locator(notificationsBell).click()
|
|
}
|
|
await this.#page.locator(notificationsLoading).waitFor({ state: 'detached' })
|
|
const result = this.#page.locator(notificationItemsMessages)
|
|
await objects.a11y.Accessibility.assertNoSevereA11yViolations(
|
|
this.#page,
|
|
[notificationsDrop],
|
|
'notifications'
|
|
)
|
|
const messages = []
|
|
const count = await result.count()
|
|
for (let i = 0; i < count; i++) {
|
|
messages.push(await result.nth(i).innerText())
|
|
}
|
|
return messages
|
|
}
|
|
|
|
async markNotificationsAsRead(): Promise<void> {
|
|
const dropIsOpen = await this.#page.locator(notificationsDrop).isVisible()
|
|
if (!dropIsOpen) {
|
|
await this.#page.locator(notificationsBell).click()
|
|
}
|
|
await this.#page.locator(notificationsLoading).waitFor({ state: 'detached' })
|
|
await this.#page.locator(markNotificationsAsReadButton).click()
|
|
|
|
await objects.a11y.Accessibility.assertNoSevereA11yViolations(
|
|
this.#page,
|
|
[notificationsDrop],
|
|
'notifications'
|
|
)
|
|
await this.#page.locator(notificationsLoading).waitFor({ state: 'detached' })
|
|
}
|
|
|
|
async openUrl(url): Promise<void> {
|
|
await this.#page.goto(url)
|
|
}
|
|
|
|
async closeSidebar(): Promise<void> {
|
|
// await sidebar transitions
|
|
await new Promise((resolve) => setTimeout(resolve, 250))
|
|
const isSubPanelActive = await this.#page.locator(closeSidebarSubPanelBtn).isVisible()
|
|
if (isSubPanelActive) {
|
|
await this.#page.locator(closeSidebarSubPanelBtn).click()
|
|
await objects.a11y.Accessibility.assertNoSevereA11yViolations(
|
|
this.#page,
|
|
['body'],
|
|
'body after closing sidebar sub panel'
|
|
)
|
|
} else {
|
|
await this.#page.locator(closeSidebarRootPanelBtn).click()
|
|
await objects.a11y.Accessibility.assertNoSevereA11yViolations(
|
|
this.#page,
|
|
['body'],
|
|
'body after closing sidebar root panel'
|
|
)
|
|
}
|
|
}
|
|
|
|
async waitForTokenRenewalViaRefreshToken(): Promise<void> {
|
|
await Promise.all([
|
|
this.#page.waitForResponse(
|
|
(resp) =>
|
|
resp.url().includes('/token') &&
|
|
resp.status() === 200 &&
|
|
resp.request().method() === 'POST' &&
|
|
resp.request().postDataJSON().grant_type === 'refresh_token' &&
|
|
resp.request().postDataJSON().hasOwnProperty('refresh_token') &&
|
|
resp.request().postDataJSON().refresh_token &&
|
|
resp.request().postDataJSON().hasOwnProperty('scope') &&
|
|
resp.request().postDataJSON().scope.includes('offline_access'),
|
|
{ timeout: config.tokenTimeout * 1000 }
|
|
)
|
|
])
|
|
}
|
|
|
|
async waitForTokenRenewalViaIframe(): Promise<void> {
|
|
const waitForIframe = this.#page.evaluateHandle(() => {
|
|
let iframe = null
|
|
const observer = new MutationObserver((mutations) => {
|
|
mutations.forEach((mutation) => {
|
|
if (mutation.type === 'childList') {
|
|
mutation.addedNodes.forEach((node) => {
|
|
if (node.nodeName === 'IFRAME') {
|
|
iframe = node
|
|
}
|
|
})
|
|
}
|
|
})
|
|
})
|
|
observer.observe(document.body, { childList: true, subtree: true })
|
|
return new Promise((resolve) => {
|
|
const interval = setInterval(() => {
|
|
if (iframe) {
|
|
observer.disconnect()
|
|
clearInterval(interval)
|
|
resolve(iframe)
|
|
}
|
|
}, 1000)
|
|
})
|
|
})
|
|
|
|
await Promise.all([
|
|
this.#page.waitForResponse(
|
|
(resp) =>
|
|
resp.url().includes('/oidc-silent-redirect.html') &&
|
|
resp.status() === 200 &&
|
|
resp.request().method() === 'GET',
|
|
{ timeout: config.tokenTimeout * 1000 }
|
|
),
|
|
this.#page.waitForResponse(
|
|
(resp) =>
|
|
resp.url().endsWith('/token') &&
|
|
resp.status() === 200 &&
|
|
resp.request().method() === 'POST' &&
|
|
resp.request().postDataJSON().grant_type === 'authorization_code' &&
|
|
resp.request().postDataJSON().hasOwnProperty('code') &&
|
|
resp.request().postDataJSON().code,
|
|
{ timeout: config.tokenTimeout * 1000 }
|
|
),
|
|
waitForIframe
|
|
])
|
|
}
|
|
}
|