Slight adjustment to tab styles + added spinner component
All checks were successful
Build, Test, and Publish (to Private NPM Registry) UI Components Library / publish (push) Successful in 1m2s

This commit is contained in:
Alan Bridgeman 2026-05-19 21:36:00 -05:00
parent d85419a0d8
commit d95bdda9ec
11 changed files with 407 additions and 14 deletions

View file

@ -28,6 +28,10 @@ app.get('/test/tabs', (req: Request, res: Response) => {
res.render('tabs', { toggleUnderline, toggleFilled });
});
app.get('/test/spinner', (req: Request, res: Response) => {
res.render('spinner');
});
app.listen(3080, () => {
console.log('Test server running on http://localhost:3080');
});

View file

@ -0,0 +1,62 @@
import { test, expect } from '@playwright/test';
import { AxeBuilder } from '@axe-core/playwright';
test.describe('Accessible Spinner Component', () => {
// Before each test, navigate to the specific EJS view serving the spinner
test.beforeEach(async ({ page }) => {
// Assuming your test harness routes this to /test/spinner
await page.goto('/test/spinner');
});
test('should pass AAA accessibility audits', async ({ page }) => {
// Wait for the component to be fully hydrated
await page.waitForLoadState('networkidle');
// Run the Axe-core engine against the page
const accessibilityScanResults = await new AxeBuilder({ page })
.withTags(['wcag2a', 'wcag2aa', 'wcag2aaa', 'best-practice'])
.analyze();
// If there are violations, the test fails and prints them in the console
expect(accessibilityScanResults.violations).toEqual([]);
});
test('should render correct ARIA attributes and variant classes based on props', async ({ page }) => {
// Test Default Spinner
const defaultSpinner = page.locator('#example-spinner');
await expect(defaultSpinner).toBeVisible();
await expect(defaultSpinner).toHaveAttribute('aria-busy', 'true');
// Ensure no extra variant classes leaked into the default spinner
await expect(defaultSpinner).not.toHaveClass(/inline/);
await expect(defaultSpinner).not.toHaveClass(/fast|slow/);
// Test Inline Spinner
const inlineSpinner = page.locator('#inline-spinner');
await expect(inlineSpinner).toHaveClass(/inline/);
await expect(inlineSpinner).toHaveAttribute('aria-busy', 'true');
// Test Speed Variants
const slowSpinner = page.locator('#slow-spinner');
await expect(slowSpinner).toHaveClass(/slow/);
const fastSpinner = page.locator('#fast-spinner');
await expect(fastSpinner).toHaveClass(/fast/);
// Test Combo (Slow + Inline)
const comboSpinner = page.locator('#slow-inline-spinner');
await expect(comboSpinner).toHaveClass(/inline/);
await expect(comboSpinner).toHaveClass(/slow/);
});
test('visual regression: component variants render correctly without layout shifts', async ({ page }) => {
// We capture the entire <main> block to ensure all variants render correctly relative to standard text.
const mainContent = page.locator('main');
// CRITICAL: Because spinners use CSS @keyframes, the rotation angle will be slightly different
// every time Playwright takes a screenshot, causing the test to flake and fail.
// `animations: 'disabled'` tells Playwright to pause all CSS animations and transitions instantly.
await expect(mainContent).toHaveScreenshot('spinner-all-variants.png', {
animations: 'disabled'
});
});
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.2 KiB

After

Width:  |  Height:  |  Size: 9.2 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Before After
Before After

View file

@ -0,0 +1,36 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Spinner Test</title>
<link rel="stylesheet" type="text/css" href="<%= componentsStyleHref %>">
<script src="<%= componentsScriptSrc %>"></script>
</head>
<body>
<main style="padding: 20px;">
<h1>Spinner Test</h1>
<p>The following is an example of a spinner component. It indicates a loading or processing state.</p>
<section>
<h2>Default Spinner</h2>
<%- useComponent('spinner', { id: 'example-spinner', content: 'Loading...' }) %>
</section>
<section>
<h2>Inline Spinner</h2>
<%- useComponent('spinner', { id: 'inline-spinner', content: 'Loading...', inline: true }) %>
</section>
<section>
<h2>Slow Spinner</h2>
<%- useComponent('spinner', { id: 'slow-spinner', content: 'Loading...', speed: 'slow' }) %>
</section>
<section>
<h2>Fast Spinner</h2>
<%- useComponent('spinner', { id: 'fast-spinner', content: 'Loading...', speed: 'fast' }) %>
</section>
<section>
<h2>Slow Inline Spinner</h2>
<%- useComponent('spinner', { id: 'slow-inline-spinner', content: 'Loading...', inline: true, speed: 'slow' }) %>
</section>
</main>
</body>
</html>