import { AxeBuilder } from '@axe-core/playwright' import { expect, test } from '@playwright/test' import type { Page } from '@playwright/test' /** * Visual regression tests for OsButton component * * These tests capture screenshots of Storybook stories and compare them * against baseline images to detect unintended visual changes. * Each test also runs accessibility checks using axe-core. */ const STORY_URL = '/iframe.html?id=components-osbutton' const STORY_ROOT = '#storybook-root' /** * Wait for all fonts to be loaded before taking screenshots */ async function waitForFonts(page: Page) { await page.evaluate(async () => document.fonts.ready) } /** * Helper to run accessibility check on the current page */ async function checkA11y(page: Page) { const results = await new AxeBuilder({ page }).include(STORY_ROOT).analyze() expect(results.violations).toEqual([]) } test.describe('OsButton keyboard accessibility', () => { test('all variants show visible focus indicator', async ({ page }) => { await page.goto(`${STORY_URL}--all-appearances&viewMode=story`) const root = page.locator(STORY_ROOT) await root.waitFor() const buttons = root.locator('button:not([disabled])') const count = await buttons.count() expect(count).toBeGreaterThan(0) for (let i = 0; i < count; i++) { const button = buttons.nth(i) await button.focus() const outline = await button.evaluate((el) => getComputedStyle(el).outlineStyle) const label = (await button.textContent()) ?? '' expect(outline, `Button "${label}" must have visible focus outline`).not.toBe('none') } }) }) test.describe('OsButton visual regression', () => { test('all variants', async ({ page }) => { await page.goto(`${STORY_URL}--all-variants&viewMode=story`) const root = page.locator(STORY_ROOT) await root.waitFor() await waitForFonts(page) await expect(root.locator('.flex')).toHaveScreenshot('all-variants.png') await checkA11y(page) }) test('all sizes', async ({ page }) => { await page.goto(`${STORY_URL}--all-sizes&viewMode=story`) const root = page.locator(STORY_ROOT) await root.waitFor() await waitForFonts(page) await expect(root.locator('.flex-col').first()).toHaveScreenshot('all-sizes.png') await checkA11y(page) }) test('appearance filled', async ({ page }) => { await page.goto(`${STORY_URL}--appearance-filled&viewMode=story`) const root = page.locator(STORY_ROOT) await root.waitFor() await waitForFonts(page) await expect(root.locator('.flex')).toHaveScreenshot('appearance-filled.png') await checkA11y(page) }) test('appearance outline', async ({ page }) => { await page.goto(`${STORY_URL}--appearance-outline&viewMode=story`) const root = page.locator(STORY_ROOT) await root.waitFor() await waitForFonts(page) await expect(root.locator('.flex')).toHaveScreenshot('appearance-outline.png') await checkA11y(page) }) test('appearance ghost', async ({ page }) => { await page.goto(`${STORY_URL}--appearance-ghost&viewMode=story`) const root = page.locator(STORY_ROOT) await root.waitFor() await waitForFonts(page) await expect(root.locator('.flex')).toHaveScreenshot('appearance-ghost.png') await checkA11y(page) }) test('all appearances', async ({ page }) => { await page.goto(`${STORY_URL}--all-appearances&viewMode=story`) const root = page.locator(STORY_ROOT) await root.waitFor() await waitForFonts(page) await expect(root.locator('.flex-col').first()).toHaveScreenshot('all-appearances.png') await checkA11y(page) }) test('disabled state', async ({ page }) => { await page.goto(`${STORY_URL}--disabled&viewMode=story`) const root = page.locator(STORY_ROOT) await root.waitFor() await waitForFonts(page) await expect(root.locator('.flex-col').first()).toHaveScreenshot('disabled.png') await checkA11y(page) }) test('full width', async ({ page }) => { await page.goto(`${STORY_URL}--full-width&viewMode=story`) const root = page.locator(STORY_ROOT) await root.waitFor() await waitForFonts(page) await expect(root.locator('.flex-col').first()).toHaveScreenshot('full-width.png') await checkA11y(page) }) test('icon', async ({ page }) => { await page.goto(`${STORY_URL}--icon&viewMode=story`) const root = page.locator(STORY_ROOT) await root.waitFor() await waitForFonts(page) await expect(root.locator('.flex')).toHaveScreenshot('icon.png') await checkA11y(page) }) test('icon only', async ({ page }) => { await page.goto(`${STORY_URL}--icon-only&viewMode=story`) const root = page.locator(STORY_ROOT) await root.waitFor() await waitForFonts(page) await expect(root.locator('.flex')).toHaveScreenshot('icon-only.png') await checkA11y(page) }) test('icon sizes', async ({ page }) => { await page.goto(`${STORY_URL}--icon-sizes&viewMode=story`) const root = page.locator(STORY_ROOT) await root.waitFor() await waitForFonts(page) await expect(root.locator('.flex')).toHaveScreenshot('icon-sizes.png') await checkA11y(page) }) test('icon appearances', async ({ page }) => { await page.goto(`${STORY_URL}--icon-appearances&viewMode=story`) const root = page.locator(STORY_ROOT) await root.waitFor() await waitForFonts(page) await expect(root.locator('.flex-col').first()).toHaveScreenshot('icon-appearances.png') await checkA11y(page) }) test('circle', async ({ page }) => { await page.goto(`${STORY_URL}--circle&viewMode=story`) const root = page.locator(STORY_ROOT) await root.waitFor() await waitForFonts(page) await expect(root.locator('.flex')).toHaveScreenshot('circle.png') await checkA11y(page) }) test('circle sizes', async ({ page }) => { await page.goto(`${STORY_URL}--circle-sizes&viewMode=story`) const root = page.locator(STORY_ROOT) await root.waitFor() await waitForFonts(page) await expect(root.locator('.flex-col').first()).toHaveScreenshot('circle-sizes.png') await checkA11y(page) }) test('circle appearances', async ({ page }) => { await page.goto(`${STORY_URL}--circle-appearances&viewMode=story`) const root = page.locator(STORY_ROOT) await root.waitFor() await waitForFonts(page) await expect(root.locator('.flex-col').first()).toHaveScreenshot('circle-appearances.png') await checkA11y(page) }) test('polymorphic', async ({ page }) => { await page.goto(`${STORY_URL}--polymorphic&viewMode=story`) const root = page.locator(STORY_ROOT) await root.waitFor() await waitForFonts(page) await expect(root.locator('.flex-col').first()).toHaveScreenshot('polymorphic.png') await checkA11y(page) }) test('suffix', async ({ page }) => { await page.goto(`${STORY_URL}--suffix&viewMode=story`) const root = page.locator(STORY_ROOT) await root.waitFor() await waitForFonts(page) // Pause animations for deterministic screenshots await page.evaluate(() => { document.querySelectorAll('.os-button__spinner').forEach((el) => { ;(el as HTMLElement).style.animationPlayState = 'paused' }) document.querySelectorAll('.os-button__spinner circle').forEach((el) => { ;(el as HTMLElement).style.animationPlayState = 'paused' }) }) await expect(root.locator('.flex-col').first()).toHaveScreenshot('suffix.png') await checkA11y(page) }) test('loading', async ({ page }) => { await page.goto(`${STORY_URL}--loading&viewMode=story`) const root = page.locator(STORY_ROOT) await root.waitFor() await waitForFonts(page) // Pause animations for deterministic screenshots await page.evaluate(() => { document.querySelectorAll('.os-button__spinner').forEach((el) => { ;(el as HTMLElement).style.animationPlayState = 'paused' }) document.querySelectorAll('.os-button__spinner circle').forEach((el) => { ;(el as HTMLElement).style.animationPlayState = 'paused' }) }) await expect(root.locator('.flex-col').first()).toHaveScreenshot('loading.png') await checkA11y(page) }) })