diff --git a/docs/live-testing.md b/docs/live-testing.md index 6b0b5a3..2d68191 100644 --- a/docs/live-testing.md +++ b/docs/live-testing.md @@ -56,3 +56,5 @@ The earlier smoke baseline followed the [WordPress live runner](https://github.c After login, the test waits for authenticated report data for the mapped URL before opening the report panel, matching the WordPress flow. Diagnostic fields are independently filtered by the reporter; they contain no raw response fields, account text or URLs. Report identity is checked against the exact `url` parameter on the SDK polling request. Like WordPress, the response must be authenticated, error-free, include a nonnegative numeric issue count and a nonempty `mainUrl`; that response field is not assumed to equal the crawled page URL. + +The test opens the Accessibility results section before asserting the documented image-alternative rule. Safe result diagnostics report only whether the category, target issue, alert, running/recheck controls or nested frames are present/visible. Setup and result diagnostics are merged through the same allowlist; no result text or URLs are retained. diff --git a/tests/browser/result-navigation.spec.js b/tests/browser/result-navigation.spec.js new file mode 100644 index 0000000..0d63feb --- /dev/null +++ b/tests/browser/result-navigation.spec.js @@ -0,0 +1,22 @@ +const { test, expect } = require('@playwright/test'); + +async function fixture(page, expanded, issue) { + await page.setContent(` +
${issue ? '

Image without a text alternative

' : '

No accessibility issues

'}
+ `); +} + +test('opens collapsed accessibility results to expose the documented issue', async ({ page }) => { + const { openAccessibilityResults, resultViewState } = await import('../live/result-view.mjs'); + await fixture(page, false, true); + await openAccessibilityResults(page); + expect(await resultViewState(page)).toMatchObject({ imageIssuePresent: true, imageIssueVisible: true, resultRecheckVisible: true }); +}); + +test('keeps expanded corrected results open and does not manufacture an issue', async ({ page }) => { + const { openAccessibilityResults, resultViewState } = await import('../live/result-view.mjs'); + await fixture(page, true, false); + await openAccessibilityResults(page); + await expect(page.locator('section')).toBeVisible(); + expect(await resultViewState(page)).toMatchObject({ imageIssuePresent: false, imageIssueVisible: false, resultAlertVisible: false }); +}); diff --git a/tests/live/diagnostics.mjs b/tests/live/diagnostics.mjs index 300cfb4..a9fff32 100644 --- a/tests/live/diagnostics.mjs +++ b/tests/live/diagnostics.mjs @@ -1,6 +1,7 @@ const flags = ['entitlementReady', 'pollSeen', 'pollOk', 'pollAuthenticated', 'pollUrlMatches', 'pollMainUrlPresent', 'pollIssueCountValid', 'pollErrorNone', 'panelFramePresent', 'panelVisible', 'launcherVisible', - 'blockedExternalRequest']; + 'blockedExternalRequest', 'accessibilityCategoryVisible', 'imageIssuePresent', 'imageIssueVisible', + 'resultAlertVisible', 'resultHasNestedFrame', 'resultRunning', 'resultRecheckVisible']; // Account text and arbitrary response fields must never enter public artifacts. export function safeDiagnostics(value) { diff --git a/tests/live/prepublish.spec.mjs b/tests/live/prepublish.spec.mjs index 0634e0d..136aaaa 100644 --- a/tests/live/prepublish.spec.mjs +++ b/tests/live/prepublish.spec.mjs @@ -2,6 +2,7 @@ import { test, expect } from '@playwright/test'; import { openLiveEditor } from './editor.mjs'; import { settings } from './settings.mjs'; import { observeDraft } from './prepublish.mjs'; +import { openAccessibilityResults, resultViewState } from './result-view.mjs'; import { imageAlternativeRule } from './accessibility-rule.mjs'; async function scan(page, evidence, marker) { @@ -32,39 +33,52 @@ test('prepublish detects WCAG 1.1.1 image alternative issue and clears it after expect(Boolean(marker && fixedMarker && marker !== fixedMarker)).toBe(true); const evidence = await observeDraft(context, config.cmsOrigin, [marker, fixedMarker]); await openLiveEditor(page, context); - const preview = page.frameLocator('iframe[name="sitePreview"]'); - await expect(preview.locator('#live-test-marker')).toHaveText(marker); - await expect(preview.locator('img')).toHaveCount(1); - expect(await preview.locator('#live-test-image').getAttribute('alt')).toBeNull(); - const publishedPath = new URL(config.crawledUrl).pathname; - const published = await page.request.get(publishedPath); - expect(published.ok()).toBe(true); - expect(await published.text()).not.toContain(marker); - await test.step('fresh draft is handed to the SDK and leaves the running state', async () => { - await scan(page, evidence, marker); - await test.step('live: WCAG 1.1.1 issue detected', async () => { - const overlay = page.frameLocator('iframe.si-iframe-element'); - await expect(overlay.getByText(imageAlternativeRule.label, { exact: true })).toBeVisible(); + try { + const preview = page.frameLocator('iframe[name="sitePreview"]'); + await expect(preview.locator('#live-test-marker')).toHaveText(marker); + await expect(preview.locator('img')).toHaveCount(1); + expect(await preview.locator('#live-test-image').getAttribute('alt')).toBeNull(); + const publishedPath = new URL(config.crawledUrl).pathname; + const published = await page.request.get(publishedPath); + expect(published.ok()).toBe(true); + expect(await published.text()).not.toContain(marker); + await test.step('fresh draft is handed to the SDK and leaves the running state', async () => { + await scan(page, evidence, marker); + await test.step('live: accessibility results', async () => { + await openAccessibilityResults(page.frameLocator('iframe.si-iframe-element')); + }); + await test.step('live: WCAG 1.1.1 issue detected', async () => { + const overlay = page.frameLocator('iframe.si-iframe-element'); + await expect(overlay.getByText(imageAlternativeRule.label, { exact: true })).toBeVisible(); + }); }); - }); - await test.step('saved draft correction is handed to the SDK and leaves the running state', async () => { - const response = await page.request.post('/test/live-draft/fix', { headers: { 'X-Cms-Test': 'prepublish' } }); - expect(response.ok()).toBe(true); - const { contentId } = await response.json(); - await page.goto(`/episerver/cms/#context=epi.cms.contentdata:///${contentId}`); - await expect(preview.locator('#live-test-marker')).toHaveText(fixedMarker); - await expect(preview.locator('#live-test-image')).toHaveAttribute('alt', 'Blue square for the prepublish test'); - const panel = page.locator('iframe.si-iframe-element'); - if (!await panel.isVisible()) await page.locator('.si-smallbox button.si-button').click(); - await expect(panel).toBeVisible(); - await scan(page, evidence, fixedMarker); - await test.step('live: WCAG 1.1.1 issue cleared', async () => { - const overlay = page.frameLocator('iframe.si-iframe-element'); - // Require the results section to remain visible and reject an error alert. - await expect(overlay.getByText('Accessibility', { exact: true }).first()).toBeVisible(); - await expect(overlay.getByRole('alert')).toHaveCount(0); - await expect(overlay.getByText(imageAlternativeRule.label, { exact: true })).toHaveCount(0); + await test.step('saved draft correction is handed to the SDK and leaves the running state', async () => { + const response = await page.request.post('/test/live-draft/fix', { headers: { 'X-Cms-Test': 'prepublish' } }); + expect(response.ok()).toBe(true); + const { contentId } = await response.json(); + await page.goto(`/episerver/cms/#context=epi.cms.contentdata:///${contentId}`); + await expect(preview.locator('#live-test-marker')).toHaveText(fixedMarker); + await expect(preview.locator('#live-test-image')).toHaveAttribute('alt', 'Blue square for the prepublish test'); + const panel = page.locator('iframe.si-iframe-element'); + if (!await panel.isVisible()) await page.locator('.si-smallbox button.si-button').click(); + await expect(panel).toBeVisible(); + await scan(page, evidence, fixedMarker); + await test.step('live: accessibility results', async () => { + await openAccessibilityResults(page.frameLocator('iframe.si-iframe-element')); + }); + await test.step('live: WCAG 1.1.1 issue cleared', async () => { + const overlay = page.frameLocator('iframe.si-iframe-element'); + // Require the results section to remain visible and reject an error alert. + await expect(overlay.getByText('Accessibility', { exact: true }).first()).toBeVisible(); + await expect(overlay.getByRole('alert').filter({ visible: true })).toHaveCount(0); + await expect(overlay.getByText(imageAlternativeRule.label, { exact: true })).toHaveCount(0); + }); + expect(await (await page.request.get(publishedPath)).text()).not.toContain(fixedMarker); }); - expect(await (await page.request.get(publishedPath)).text()).not.toContain(fixedMarker); - }); + } finally { + try { + const state = await resultViewState(page.frameLocator('iframe.si-iframe-element')); + test.info().annotations.push({ type: 'live-diagnostics', description: JSON.stringify(state) }); + } catch { /* Diagnostic collection must not hide the original result. */ } + } }); diff --git a/tests/live/result-view.mjs b/tests/live/result-view.mjs new file mode 100644 index 0000000..1e588ac --- /dev/null +++ b/tests/live/result-view.mjs @@ -0,0 +1,28 @@ +import { expect } from '@playwright/test'; +import { imageAlternativeRule } from './accessibility-rule.mjs'; + +const category = overlay => overlay.getByRole('button', { name: /^Accessibility\b/i }) + .or(overlay.getByRole('tab', { name: /^Accessibility\b/i })) + .or(overlay.getByText('Accessibility', { exact: true })).filter({ visible: true }).first(); + +export async function openAccessibilityResults(overlay) { + const issue = overlay.getByText(imageAlternativeRule.label, { exact: true }).filter({ visible: true }); + if (await issue.count()) return; + const control = category(overlay); + await expect(control).toBeVisible(); + if (await control.getAttribute('aria-expanded') !== 'true' + && await control.getAttribute('aria-selected') !== 'true') await control.click(); +} + +export async function resultViewState(overlay) { + const issue = overlay.getByText(imageAlternativeRule.label, { exact: true }); + return { + accessibilityCategoryVisible: await category(overlay).isVisible(), + imageIssuePresent: await issue.count() > 0, + imageIssueVisible: await issue.filter({ visible: true }).count() > 0, + resultAlertVisible: await overlay.getByRole('alert').filter({ visible: true }).count() > 0, + resultHasNestedFrame: await overlay.locator('iframe').count() > 0, + resultRunning: await overlay.getByRole('button', { name: /Cancel content check/i }).isVisible(), + resultRecheckVisible: await overlay.getByRole('button', { name: /^Recheck draft$/i }).isVisible(), + }; +} diff --git a/tests/live/safe-reporter.mjs b/tests/live/safe-reporter.mjs index 433dd94..4dcc2a4 100644 --- a/tests/live/safe-reporter.mjs +++ b/tests/live/safe-reporter.mjs @@ -7,7 +7,7 @@ const stages = new Set(['live: entitlement', 'live: CMS login', 'live: public UR 'live: draft preview', 'live: open login popup', 'live: identity username', 'live: identity password', 'live: submit login', 'live: report panel', 'live: mapped report data', 'live: start prepublish', 'live: draft handoff', 'live: loading-state exit', - 'live: WCAG 1.1.1 issue detected', 'live: WCAG 1.1.1 issue cleared']); + 'live: accessibility results', 'live: WCAG 1.1.1 issue detected', 'live: WCAG 1.1.1 issue cleared']); export default class SafeReporter { results = []; @@ -20,7 +20,7 @@ export default class SafeReporter { let diagnostics = {}; for (const annotation of result.annotations ?? []) { if (annotation.type !== 'live-diagnostics') continue; - try { diagnostics = safeDiagnostics(JSON.parse(annotation.description)); } catch {} + try { Object.assign(diagnostics, safeDiagnostics(JSON.parse(annotation.description))); } catch {} } this.results.push({ test: test.title, status: result.status, durationMs: result.duration, lastStage: this.stages.get(result) ?? null, diagnostics }); diff --git a/tests/unit/live.test.cjs b/tests/unit/live.test.cjs index d57b1ea..ff3050e 100644 --- a/tests/unit/live.test.cjs +++ b/tests/unit/live.test.cjs @@ -63,7 +63,8 @@ test('live reporting discards raw failures and attachments', async () => { reporter.onError(new Error('private-value')); const result = { status: 'failed', duration: 1, annotations: [{ type: 'live-diagnostics', description: JSON.stringify({ entitlementStatus: 429, - panelVisible: false, pollStatus: 'private-value', url: 'private-value', token: 'private-value' }) }], + panelVisible: false, pollStatus: 'private-value', url: 'private-value', token: 'private-value' }) }, + { type: 'live-diagnostics', description: JSON.stringify({ imageIssuePresent: true, imageIssueVisible: false, privateText: 'private-value' }) }], error: { message: 'private-value' }, attachments: [{ body: 'private-value' }] }; reporter.onStepBegin({}, result, { category: 'test.step', title: 'live: CMS login' }); reporter.onStepBegin({}, result, { category: 'test.step', title: 'private-value' }); @@ -74,7 +75,7 @@ test('live reporting discards raw failures and attachments', async () => { assert.equal(output.includes('private-value'), false); assert.equal(JSON.parse(output).tests[0].status, 'failed'); assert.equal(JSON.parse(output).tests[0].lastStage, 'live: CMS login'); - assert.deepEqual(JSON.parse(output).tests[0].diagnostics, { panelVisible: false, entitlementStatus: 429 }); + assert.deepEqual(JSON.parse(output).tests[0].diagnostics, { panelVisible: false, entitlementStatus: 429, imageIssuePresent: true, imageIssueVisible: false }); } finally { process.chdir(cwd); fs.rmSync(temp, { recursive: true, force: true }); } });