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 docs/live-testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
22 changes: 22 additions & 0 deletions tests/browser/result-navigation.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
const { test, expect } = require('@playwright/test');

async function fixture(page, expanded, issue) {
await page.setContent(`<button aria-expanded="${expanded}" onclick="const open=this.getAttribute('aria-expanded')==='true';this.setAttribute('aria-expanded',!open);document.querySelector('section').hidden=open">Accessibility</button>
<section ${expanded ? '' : 'hidden'}>${issue ? '<p>Image without a text alternative</p>' : '<p>No accessibility issues</p>'}</section>
<button>Recheck draft</button>`);
}

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 });
});
3 changes: 2 additions & 1 deletion tests/live/diagnostics.mjs
Original file line number Diff line number Diff line change
@@ -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) {
Expand Down
80 changes: 47 additions & 33 deletions tests/live/prepublish.spec.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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. */ }
}
});
28 changes: 28 additions & 0 deletions tests/live/result-view.mjs
Original file line number Diff line number Diff line change
@@ -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(),
};
}
4 changes: 2 additions & 2 deletions tests/live/safe-reporter.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [];
Expand All @@ -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 });
Expand Down
5 changes: 3 additions & 2 deletions tests/unit/live.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -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' });
Expand All @@ -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 }); }
});

Expand Down
Loading