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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ Thumbs.db
*.plslide
*_files/
*.dzi
!frontend/e2e/fixtures/teaching-landmarks/landmarks_files/
!frontend/e2e/fixtures/teaching-landmarks/landmarks_files/**

# Runtime and packaging
/runtime/
Expand Down
72 changes: 72 additions & 0 deletions frontend/e2e/accessibility.pw.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import AxeBuilder from '@axe-core/playwright'
import { expect, test, type Page } from '@playwright/test'
import { captureBrowserErrors } from './browserErrors'
import path from 'node:path'
import { fileURLToPath } from 'node:url'

const here = path.dirname(fileURLToPath(import.meta.url))
const dzi = '<Image TileSize="256" Overlap="0" Format="png" xmlns="http://schemas.microsoft.com/deepzoom/2008"><Size Width="640" Height="320" /></Image>'
const slide = {
id: 'synthetic-responsive-slide', displayName: 'North-east synthetic landmarks.ome.tif', sourceBytes: 100,
format: 'OME_TIFF', status: 'READY', detail: 'Synthetic image ready', outputPath: '', sha256: 'synthetic',
selectedSeries: 0, width: 640, height: 320, downsample: 1, estimatedOutputBytes: 0,
projectedFileBytes: 0, projectedFileLowerBytes: 0, projectedFileUpperBytes: 0,
cropX: 0, cropY: 0, cropWidth: 640, cropHeight: 320, sourceFingerprint: 'synthetic-source',
configurationRevision: 'synthetic-configuration', currentArtifactRevision: '', approvedArtifactRevision: '',
}

async function installFixture(page: Page) {
await page.route('**/api/**', async (route) => {
const url = new URL(route.request().url())
if (url.pathname === '/api/session') return route.fulfill({ json: {}, headers: { 'X-Forge-CSRF': 'fixture' } })
if (url.pathname === '/api/datasets') return route.fulfill({ json: { datasets: [slide] }, headers: { ETag: 'synthetic-datasets' } })
if (url.pathname === '/api/capabilities') return route.fulfill({ json: {
conversionRuntime: 'Synthetic fixture', derivativeRuntime: 'Synthetic fixture', vsiConversion: false,
dziGeneration: false, downsamples: [1, 2, 4], usableBytes: 1_000_000, effectiveCapacityBytes: 2_000_000,
} })
if (url.pathname === '/api/features') return route.fulfill({ json: { features: [] } })
if (url.pathname === '/api/viewer/connection') return route.fulfill({ json: {
connected: false, viewerUrl: '', deviceName: '', scopes: [], conversionMode: 'PREPARED_DZI_V2',
} })
if (url.pathname.endsWith('/annotations')) return route.fulfill({ json: { annotations: [] } })
if (url.pathname.endsWith('/artifacts')) return route.fulfill({ json: { currentRevision: '', approvedRevision: '', revisions: [] } })
if (url.pathname === '/api/analysis/runs') return route.fulfill({ json: [] })
if (url.pathname.endsWith('/slide.dzi')) return route.fulfill({ status: 200, contentType: 'application/xml', body: dzi, headers: { 'x-pathlab-preview-mode': 'direct' } })
if (url.pathname.endsWith('/thumbnail')) return route.fulfill({ path: path.join(here, 'fixtures', 'teaching-landmarks', 'landmarks_files', '9', '0_0.png') })
const tile = url.pathname.match(/\/(\d+)\/(\d+)_(\d+)\.png$/)
if (tile) return route.fulfill({ path: path.join(here, 'fixtures', 'teaching-landmarks', 'landmarks_files', tile[1], `${tile[2]}_${tile[3]}.png`) })
return route.fulfill({ status: 404, json: { detail: `No synthetic fixture for ${url.pathname}` } })
})
}

for (const viewport of [{ width: 1440, height: 900, label: 'desktop' }, { width: 390, height: 844, label: 'phone' }]) {
test(`axe scans the populated Forge workspace at ${viewport.label} size`, async ({ page }, testInfo) => {
test.skip(testInfo.project.name !== 'chromium')
const expectNoBrowserErrors = captureBrowserErrors(page)
await installFixture(page)
await page.setViewportSize({ width: viewport.width, height: viewport.height })
await page.goto('/assets/')
await expect(page.locator('.forge-viewer-header').getByText(slide.displayName, { exact: true })).toBeVisible()
if (viewport.label === 'phone') {
const header = page.locator('.forge-viewer-header')
const headerBox = await header.boundingBox()
const buttons = header.getByRole('button')
for (const button of await buttons.all()) {
await expect(button).toHaveAccessibleName(/.+/)
const box = await button.boundingBox()
expect(box).not.toBeNull()
expect(box!.x + box!.width).toBeLessThanOrEqual(headerBox!.x + headerBox!.width + 1)
expect(box!.height).toBeGreaterThanOrEqual(44)
}
}
const results = await new AxeBuilder({ page }).withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa']).analyze()
const report = results.violations.map((violation) => ({
id: violation.id, impact: violation.impact, description: violation.description,
help: violation.help, nodes: violation.nodes.map((node) => ({ target: node.target, summary: node.failureSummary })),
}))
await testInfo.attach(`axe-${viewport.label}.json`, { body: JSON.stringify(report, null, 2), contentType: 'application/json' })
const severe = results.violations.filter((violation) => violation.impact === 'serious' || violation.impact === 'critical')
expect(severe, JSON.stringify(report, null, 2)).toEqual([])
expectNoBrowserErrors()
})
}
17 changes: 17 additions & 0 deletions frontend/e2e/browserErrors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { expect, type Page } from '@playwright/test'

export function captureBrowserErrors(page: Page, isExpectedFailedResponse: (status: number, url: string) => boolean = () => false) {
const errors: string[] = []
page.on('pageerror', (error) => errors.push(`uncaught: ${error.stack || error.message}`))
page.on('console', (message) => {
if (message.type() === 'error' && !message.text().startsWith('Failed to load resource:')) {
errors.push(`console.error: ${message.text()}`)
}
})
page.on('response', (response) => {
if (response.status() >= 400 && !isExpectedFailedResponse(response.status(), response.url())) {
errors.push(`HTTP ${response.status()}: ${response.url()}`)
}
})
return () => expect(errors, errors.join('\n')).toEqual([])
}
34 changes: 34 additions & 0 deletions frontend/e2e/fixtures/TeachingPreviewHarness.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { useState } from 'react'
import { createRoot } from 'react-dom/client'
import { TeachingSlidePreview } from '../../src/TeachingSlidePreview'
import type { TeachingAssociation } from '../../src/teachingAssociations'
import type OpenSeadragon from 'openseadragon'
import './teaching-preview.css'

const association: TeachingAssociation = {
referenceId: 'synthetic:landmarks',
viewerSlideId: '',
datasetId: 'synthetic-dataset',
artifactRevision: 'synthetic-revision',
packageSha256: 'a'.repeat(64),
outputWidth: 640,
outputHeight: 320,
}

declare global { interface Window { __teachingViewer?: OpenSeadragon.Viewer | null } }

function TeachingPreviewHarness() {
const [location, setLocation] = useState<{ x: number; y: number }>()
const [ready, setReady] = useState(false)
return <main>
<h1>Non-square teaching preview fixture</h1>
<output data-testid="teaching-ready">{ready ? 'Image ready' : 'Image loading'}</output>
<output data-testid="teaching-location">{location ? `${location.x.toFixed(5)},${location.y.toFixed(5)}` : 'No location selected'}</output>
<TeachingSlidePreview association={association} slideId={association.referenceId}
previewChecksum="synthetic-preview-checksum" onPixelsLoaded={(pixels) => setReady(Boolean(pixels))}
onLocation={(x, y) => setLocation({ x, y })} selectedLocation={location}
onViewerReady={(viewer) => { window.__teachingViewer = viewer }} />
</main>
}

createRoot(document.getElementById('fixture')!).render(<TeachingPreviewHarness />)
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions frontend/e2e/fixtures/teaching-preview.css

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

12 changes: 12 additions & 0 deletions frontend/e2e/fixtures/teaching-preview.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Synthetic teaching preview</title>
</head>
<body>
<div id="fixture"></div>
<script type="module" src="./TeachingPreviewHarness.tsx"></script>
</body>
</html>
Loading
Loading