import AxeBuilder from '@axe-core/playwright'; import { expect, test } from '@playwright/test'; import { contextForUser } from './helpers'; /** * VS-NfD marking pack (issue #244): in mode `marked`, catalog-listed * controls carry an accessible deviation marking that follows the CURRENT * (unsaved) value; in mode `off` nothing is marked anywhere. The api's * mode is deploy-level, so each half runs only against the matching stack: * CI runs the marked half (VS_NFD_MODE=marked on the e2e api), local full * runs against a default stack cover the off half. */ const BASE_URL = process.env.E2E_BASE_URL ?? 'http://localhost:5173'; const MODE = process.env.E2E_VS_NFD_MODE ?? 'off'; test('mode marked: card, checkbox marking, and point-of-choice marking', async ({ browser }) => { test.skip(MODE !== 'marked', 'needs an api started with VS_NFD_MODE=marked'); const context = await contextForUser(browser, BASE_URL, 'fixture-admin'); const page = await context.newPage(); await page.goto('/admin'); await page.waitForLoadState('networkidle'); // The profile card shows mode and violation count (issue #243). await expect(page.locator('.vs-nfd-summary')).toBeVisible(); // feeds.enabled defaults to true = violating: its toggle row is marked, // and the marking is part of the control's accessible description. const feedsInput = page.locator('input[aria-describedby="vs-nfd-mark-feeds.enabled"]'); await expect(feedsInput).toBeVisible(); await expect(page.locator('#vs-nfd-mark-feeds\\.enabled')).toBeVisible(); // Point of choice: the registration-mode marking follows the UNSAVED // select value — compliant choice clears it, violating choice brings it // back, no save in between. const regField = page.locator('label.field', { has: page.locator('select[name="auth.registrationMode"]'), }); const regSelect = regField.locator('select'); await regSelect.selectOption('open'); await expect(regField.locator('.vs-nfd-mark')).toBeVisible(); await regSelect.selectOption('closed'); await expect(regField.locator('.vs-nfd-mark')).toHaveCount(0); await regSelect.selectOption('open'); await expect(regField.locator('.vs-nfd-mark')).toBeVisible(); await context.close(); }); test('mode hidden: rows disappear, notes mark the hiding, a11y clean', async ({ browser }) => { test.skip(MODE !== 'hidden', 'needs an api started with VS_NFD_MODE=hidden'); const context = await contextForUser(browser, BASE_URL, 'fixture-admin'); const page = await context.newPage(); await page.goto('/admin'); await page.waitForLoadState('networkidle'); // feeds.enabled defaults to true = ALREADY violating: never silently // hidden — the row stays visible with its marking. await expect(page.locator('#vs-nfd-mark-feeds\\.enabled')).toBeVisible(); // Master switches: compliant (false) rows disappear, violating (true) // rows stay — derive the expectation from the actual instance settings, // the seed state differs between local and CI databases. const settings = (await ( await context.request.get(`${BASE_URL}/api/v1/admin/settings`) ).json()) as Record; const masterKeys = ['api.enabled', 'mcp.enabled', 'feeds.enabled', 'plugins.enabled'] as const; const visible = masterKeys.filter((key) => settings[key] === true).length; expect(visible).toBeLessThan(masterKeys.length); // at least one row is hidden await expect(page.locator('.api-opt-in__label')).toHaveCount(visible); await expect(page.locator('.vs-nfd-hidden-note').first()).toBeVisible(); // Value-listed control: the compliant registration mode keeps only its // compliant choice (seed leaves it open = violating? then all options). const regSelect = page.locator('select[name="auth.registrationMode"]'); const regField = page.locator('label.field', { has: regSelect }); const optionCount = await regSelect.locator('option').count(); const marked = await regField.locator('.vs-nfd-mark').count(); // Exactly one of the two treatments applies, never a silent third state. expect(optionCount === 1 || marked === 1).toBe(true); // The hidden state and the policy note pass the axe WCAG A/AA scan. const results = await new AxeBuilder({ page }).withTags(['wcag2a', 'wcag2aa']).analyze(); expect(results.violations).toEqual([]); await context.close(); }); test('mode off: no card, no markings', async ({ browser }) => { test.skip(MODE !== 'off', 'covers the default stack only'); const context = await contextForUser(browser, BASE_URL, 'fixture-admin'); const page = await context.newPage(); await page.goto('/admin'); await page.waitForLoadState('networkidle'); await expect(page.locator('.vs-nfd-summary')).toHaveCount(0); await expect(page.locator('.vs-nfd-mark')).toHaveCount(0); await context.close(); });