Simplify and standardize testing structure

This commit is contained in:
adilallo
2026-01-28 14:04:04 -07:00
parent e7a31789e3
commit 7ea724a8d9
95 changed files with 1534 additions and 15485 deletions
+20 -22
View File
@@ -14,28 +14,22 @@ Open [http://localhost:3000](http://localhost:3000) with your browser to see the
## 🧪 Testing Framework
This project includes a comprehensive testing framework with multiple layers of testing:
This project uses a simplified, componentfirst testing model:
- **Component tests (Vitest + RTL)** live in `tests/components/` with a single file per component.
- **E2E tests (Playwright)** cover critical user journeys and visual regression.
### Quick Test Commands
```bash
# Unit tests with coverage
# All component tests with coverage
npm test
# E2E tests
npm run e2e
# Component tests only (new structure)
npm run test:component
# Performance tests
npm run lhci
# Storybook tests
npm run test:sb
# Performance monitoring
npm run test:performance # Comprehensive performance testing
npm run bundle:analyze # Bundle size analysis
npm run web-vitals:track # Web Vitals tracking
npm run monitor:all # All monitoring tools
# E2E tests only
npm run test:e2e
```
### Test Coverage
@@ -56,7 +50,7 @@ npm run monitor:all # All monitoring tools
- **Performance monitoring**
- **Code coverage reporting**
📖 **For detailed testing documentation, see [docs/README.md](docs/README.md)**
📖 **For detailed testing documentation, see `docs/TESTING_GUIDE.md` and [docs/README.md](docs/README.md)**
## ⚡ Performance Optimizations
@@ -144,10 +138,12 @@ The Storybook configuration automatically detects the environment:
### Testing
- `npm test` - Run unit tests with coverage
- `npm test` - Run all component tests with coverage
- `npm run test:component` - Run tests in `tests/components/` only
- `npm run test:watch` - Run tests in watch mode
- `npm run test:ui` - Run tests with UI
- `npm run e2e` - Run E2E tests
- `npm run test:e2e` - Run E2E tests only
- `npm run e2e` - Alias for Playwright E2E tests
- `npm run e2e:ui` - Run E2E tests with UI
- `npm run e2e:serve` - Start dev server and run E2E tests
- `npm run lhci` - Run performance tests
@@ -187,10 +183,12 @@ community-rule/
│ ├── status-runner.sh # Check runner status
│ └── stop-runner.sh # Stop Gitea runner
├── tests/ # Test files
│ ├── unit/ # Unit tests
│ ├── integration/ # Integration tests
│ ├── e2e/ # E2E tests
── accessibility/ # Accessibility tests
│ ├── components/ # Component tests (Vitest + RTL)
│ ├── pages/ # Page-level tests
│ ├── e2e/ # E2E tests (Playwright)
── utils/ # Test utilities (componentTestSuite, etc.)
│ ├── msw/ # MSW server setup
│ └── accessibility/ # E2E accessibility checks
├── .storybook/ # Storybook configuration
├── .gitea/ # Gitea Actions workflows
│ └── workflows/
+208
View File
@@ -0,0 +1,208 @@
## Testing Guide
### Philosophy
- **Test behaviour, not implementation**: Focus on what the user can see and do, not internal details.
- **Single source of truth per component**: Each component should have **one consolidated test file**.
- **Accessibility is mandatory**: Basic a11y checks run as part of every component suite.
- **E2E is sparse**: Only cover critical user journeys that span pages or systems.
### Test Structure
The test directory structure is organized as follows:
```text
tests/
components/ # All component-focused tests (Vitest + RTL)
Button.test.tsx
Input.test.tsx
Checkbox.test.tsx
Select.test.tsx
Switch.test.tsx
pages/ # Page-level tests (home, blog, etc.)
home.test.jsx
blog.test.jsx
e2e/ # True endtoend flows + visual regression (Playwright)
homepage.spec.ts
user-journeys.spec.ts
visual-regression.spec.ts
performance.spec.ts
utils/ # Shared test utilities
componentTestSuite.tsx
msw/ # MSW server setup for mocking
server.ts
accessibility/
e2e/ # E2E accessibility checks (WCAG compliance)
wcag-compliance.spec.ts
```
**Component tests** (`tests/components/`) use the standard `componentTestSuite` utility to ensure consistent baseline coverage for all UI components. **Page tests** (`tests/pages/`) cover page-level rendering and flows. **E2E tests** (`tests/e2e/`) focus on critical user journeys, visual regression, and performance. **Accessibility E2E** (`tests/accessibility/e2e/`) provides high-level WCAG compliance checks.
### Standard Component Test Suite
Use the shared suite in `tests/utils/componentTestSuite.tsx` to get a consistent baseline:
```ts
import Component from "../../app/components/Component";
import {
componentTestSuite,
type ComponentTestSuiteConfig,
} from "../utils/componentTestSuite";
type Props = React.ComponentProps<typeof Component>;
const config: ComponentTestSuiteConfig<Props> = {
component: Component,
name: "Component",
props: {
/* default props */
} as Props,
requiredProps: ["label"],
optionalProps: {
disabled: true,
},
queries: {
getPrimaryElement: (s) => s.getByRole("button"),
},
variants: {
disabled: {
props: { disabled: true },
assert: (el) => {
expect(el).toBeDisabled();
},
},
error: {
props: { error: true } as Partial<Props>,
assert: (el) => {
expect(el).toHaveClass("border-[var(--color-border-default-utility-negative)]");
},
},
},
testCases: {
renders: true,
accessibility: true,
keyboardNavigation: true,
disabledState: true,
errorState: true,
},
};
componentTestSuite<Props>(config);
```
#### What the Standard Suite Covers
- **Rendering**
- Component renders without throwing using the provided `props`.
- Required props are present and do not break rendering.
- Optional props can be applied without breaking.
- **Accessibility**
- Runs `axe` against the rendered output.
- Fails on common WCAG 2.1 issues (roles, labels, contrast, etc.).
- **Keyboard Navigation**
- Ensures the primary element can receive focus.
- Smoketests basic keyboard activation (`Enter`, `Space`) without runtime errors.
- **Disabled State**
- Uses `variants.disabled` to verify disabled behaviour (e.g., `aria-disabled`, `disabled` attribute, tab index).
- **Error State**
- Uses `variants.error` to verify error styling/attributes when applicable.
### When to Add Custom Tests
Use the standard suite for **baseline coverage**, then add custom `describe` blocks in the same file when:
- The component has **important variants** (different sizes, modes, label variants).
- There is **nontrivial interaction** (menus, dropdowns, complex keyboard behaviour).
- You need to exercise **stateful flows** (forms, validation, error messages).
Example (inside the same `*.test.tsx` file):
```ts
describe("Input behaviour specifics", () => {
it("calls onChange when user types", async () => {
// ...
});
});
```
### Test Commands
- **All component tests** (Vitest + RTL):
```bash
npm test
```
- **Component-only tests** (faster inner loop, focused on `tests/components/`):
```bash
npm run test:component
# filter by name:
npm run test:component -- --run tests/components/Button.test.tsx
```
- **E2E tests only** (Playwright):
```bash
npm run test:e2e
# or, equivalently:
npm run e2e
```
### What to Test vs. What Not to Test
- **Do test**
- Public behaviour: visible text, roles, labels, ARIA, keyboard paths.
- State transitions that users rely on (error -> success, disabled -> enabled).
- Critical component interactions (clicks, form submissions, dropdown selection).
- Accessibility invariants (no axe violations, basic keyboard support).
- **Avoid testing**
- Pure styling details that are likely to change frequently (exact shadow radius, minor spacing).
- Internal implementation details (private helpers, hook internals, memoisation specifics).
- Responsive visibility in JSDOM (use Playwright visual / responsive tests instead).
### Adding Tests for a New Component (≈5 minutes)
1. **Create the component file** in `app/components/`.
2. **Create a test file** in `tests/components/ComponentName.test.tsx`.
3. **Wire the standard suite** using `componentTestSuite`.
4. **Add 13 custom tests** for any unique behaviours.
5. Run:
```bash
npm run test:component -- --run tests/components/ComponentName.test.tsx
```
### E2E and Visual Regression
- Use **Playwright** for:
- Critical user journeys (e.g., create rule, navigate blog, key flows).
- Responsive behaviour and crossbrowser checks.
- Visual regression (`tests/e2e/visual-regression.spec.ts`).
```bash
npm run test:e2e
npm run visual:test
```
### Accessibility Testing
Accessibility is tested at two levels:
1. **Component-level accessibility** (`tests/components/*.test.tsx`):
- Automatically covered by `componentTestSuite` using `jest-axe`
- Tests roles, labels, ARIA attributes, keyboard navigation
- Runs as part of every component test suite
2. **Full-page accessibility** (`tests/accessibility/e2e/wcag-compliance.spec.ts`):
- E2E tests using Playwright and `@axe-core/playwright`
- Validates WCAG 2.1 AA compliance across entire pages
- Tests complete user journeys for accessibility barriers
This two-tier approach ensures both individual components and full page experiences meet accessibility standards.
-391
View File
@@ -1,391 +0,0 @@
# Performance Optimization Guide
## 📋 Table of Contents
- [Overview](#overview)
- [Performance Targets](#performance-targets)
- [Frontend Optimizations](#frontend-optimizations)
- [Performance Monitoring](#performance-monitoring)
- [Bundle Analysis](#bundle-analysis)
- [Web Vitals Tracking](#web-vitals-tracking)
- [Performance Testing](#performance-testing)
- [Troubleshooting](#troubleshooting)
- [Best Practices](#best-practices)
## 🎯 Overview
This guide covers the comprehensive performance optimization strategy implemented in Community Rule 3.0 to achieve sub-2-second load times across all platform features.
### Performance Philosophy
- **Measure First**: Comprehensive monitoring before optimization
- **Performance Budgets**: Enforce limits to prevent regression
- **Real User Monitoring**: Track actual user experience
- **Continuous Optimization**: Regular monitoring and improvement
## 🎯 Performance Targets
### Core Web Vitals
- **LCP (Largest Contentful Paint)**: < 2.5s (Good)
- **FID (First Input Delay)**: < 100ms (Good)
- **CLS (Cumulative Layout Shift)**: < 0.1 (Good)
- **FCP (First Contentful Paint)**: < 1.8s (Good)
- **TTFB (Time to First Byte)**: < 800ms (Good)
### Bundle Size Targets
- **Initial JavaScript Bundle**: < 250KB gzipped (currently 101KB)
- **Total Bundle Size**: < 2MB
- **Individual Component Bundles**: < 50KB
- **Image Assets**: Optimized with WebP/AVIF formats
### Lighthouse Scores
- **Performance**: > 90
- **Accessibility**: > 90
- **Best Practices**: > 90
- **SEO**: > 90
## ⚡ Frontend Optimizations
### 1. Code Splitting
Dynamic imports for non-critical components to reduce initial bundle size:
```javascript
// Dynamic imports for non-critical components
const NumberedCards = dynamic(() => import("./components/NumberedCards"), {
loading: () => <div className="loading-placeholder">Loading...</div>,
});
const LogoWall = dynamic(() => import("./components/LogoWall"), {
loading: () => <div className="loading-placeholder">Loading...</div>,
});
```
### 2. React.memo Optimization
Applied to all 30+ components to prevent unnecessary re-renders:
```javascript
import React, { memo } from "react";
const MyComponent = memo(({ prop1, prop2 }) => {
return <div>{/* Component content */}</div>;
});
MyComponent.displayName = "MyComponent";
export default MyComponent;
```
### 3. useMemo and useCallback
Optimized expensive computations and event handlers:
```javascript
import React, { memo, useMemo, useCallback } from "react";
const OptimizedComponent = memo(({ data, onAction }) => {
// Memoize expensive computations
const processedData = useMemo(() => {
return data.map((item) => expensiveOperation(item));
}, [data]);
// Memoize event handlers
const handleClick = useCallback(
(id) => {
onAction(id);
},
[onAction],
);
return <div onClick={handleClick}>{/* Component content */}</div>;
});
```
### 4. Image Optimization
Enhanced `next/image` with lazy loading and blur placeholders:
```javascript
import Image from "next/image";
<Image
src="/assets/image.jpg"
alt="Description"
width={300}
height={200}
sizes="(max-width: 768px) 100vw, 50vw"
loading="lazy"
placeholder="blur"
blurDataURL="data:image/jpeg;base64,..."
/>;
```
### 5. Font Optimization
Preloading and fallbacks for all fonts:
```javascript
import { Inter, Bricolage_Grotesque, Space_Grotesk } from "next/font/google";
const inter = Inter({
subsets: ["latin"],
preload: true,
fallback: ["system-ui", "arial"],
});
const bricolageGrotesque = Bricolage_Grotesque({
subsets: ["latin"],
preload: true,
fallback: ["system-ui", "arial"],
});
```
### 6. Error Boundaries
Comprehensive error handling to prevent cascade failures:
```javascript
import React, { Component } from "react";
class ErrorBoundary extends Component {
constructor(props) {
super(props);
this.state = { hasError: false, error: null };
}
static getDerivedStateFromError(error) {
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
console.error("ErrorBoundary caught an error:", error, errorInfo);
}
render() {
if (this.state.hasError) {
return <div>Something went wrong.</div>;
}
return this.props.children;
}
}
```
## 📊 Performance Monitoring
### Available Scripts
```bash
# Individual monitoring tools
npm run bundle:analyze # Analyze bundle sizes and budgets
npm run performance:monitor # Performance metrics and Lighthouse CI
npm run web-vitals:track # Core Web Vitals tracking
# Comprehensive testing
npm run test:performance # All performance tests
npm run monitor:all # All monitoring tools
```
### Performance Dashboard
Access the performance monitoring dashboard at `/monitor` to view:
- Real-time Web Vitals metrics
- Historical performance data
- Bundle analysis results
- Performance budget status
- Optimization recommendations
## 📦 Bundle Analysis
### Bundle Analyzer Script
The bundle analyzer provides comprehensive analysis of bundle sizes:
```bash
npm run bundle:analyze
```
**Features:**
- Analyzes static assets, chunks, and pages
- Checks against performance budgets
- Generates optimization recommendations
- Saves results in JSON and Markdown formats
**Output Files:**
- `.next/analyze/bundle-analysis.json` - Detailed analysis data
- `.next/analyze/bundle-report.md` - Human-readable report
### Performance Budgets
Defined in `performance-budgets.json`:
```json
{
"budgets": [
{
"name": "lcp",
"maxValue": 2500,
"description": "Largest Contentful Paint"
},
{
"name": "bundle-size",
"maxSizeKB": 250,
"description": "Initial JavaScript bundle size"
}
]
}
```
## 📈 Web Vitals Tracking
### Real-time Monitoring
The Web Vitals tracking system collects and reports Core Web Vitals:
```bash
npm run web-vitals:track
```
**Features:**
- Collects LCP, FID, CLS, FCP, TTFB metrics
- Stores historical data (last 100 entries per metric)
- Generates summary reports
- Provides optimization recommendations
**API Endpoint:**
- `POST /api/web-vitals` - Receives Web Vitals data
- `GET /api/web-vitals` - Returns aggregated metrics
### Web Vitals Dashboard
The dashboard component displays real-time and historical metrics:
```javascript
import WebVitalsDashboard from "./components/WebVitalsDashboard";
<WebVitalsDashboard />;
```
## 🧪 Performance Testing
### Comprehensive Testing
Run all performance tests with a single command:
```bash
npm run test:performance
```
**Test Coverage:**
- Bundle analysis with budget checking
- Performance monitoring with Lighthouse CI
- Web Vitals tracking setup
- Comprehensive reporting
### Individual Tests
```bash
# Bundle analysis only
npm run bundle:analyze
# Performance monitoring only
npm run performance:monitor
# Web Vitals tracking only
npm run web-vitals:track
# All monitoring tools
npm run monitor:all
```
## 🔧 Troubleshooting
### Common Issues
#### 1. Bundle Size Exceeds Budget
```bash
# Check bundle analysis
npm run bundle:analyze
# Review recommendations in .next/analyze/bundle-report.md
# Consider code splitting or removing unused dependencies
```
#### 2. Web Vitals Poor Performance
```bash
# Check Web Vitals data
npm run web-vitals:track
# Review dashboard at /monitor
# Optimize images, fonts, or JavaScript
```
#### 3. Performance Tests Failing
```bash
# Run comprehensive performance test
npm run test:performance
# Check individual components
npm run bundle:analyze
npm run performance:monitor
```
### Debug Commands
```bash
# Debug bundle analysis
npm run bundle:analyze --verbose
# Debug performance monitoring
npm run performance:monitor --debug
# Check Web Vitals data
curl http://localhost:3000/api/web-vitals
```
## 🎯 Best Practices
### Development
1. **Always use React.memo** for components that receive props
2. **Implement useMemo/useCallback** for expensive operations
3. **Use dynamic imports** for non-critical components
4. **Optimize images** with proper sizing and formats
5. **Preload critical fonts** and resources
### Monitoring
1. **Run bundle analysis** before major releases
2. **Monitor Web Vitals** in production
3. **Check performance budgets** in CI/CD
4. **Review optimization recommendations** regularly
### Performance Budgets
1. **Set realistic budgets** based on user needs
2. **Monitor budget violations** in CI/CD
3. **Optimize when budgets are exceeded**
4. **Update budgets** as requirements change
## 📚 Additional Resources
- **Next.js Performance**: https://nextjs.org/docs/advanced-features/measuring-performance
- **Web Vitals**: https://web.dev/vitals/
- **Lighthouse CI**: https://github.com/GoogleChrome/lighthouse-ci
- **React Performance**: https://react.dev/learn/render-and-commit
---
**Last Updated**: December 2024
**Maintained by**: CommunityRule Development Team
-810
View File
@@ -1,810 +0,0 @@
# Testing Framework Documentation
## 📋 Table of Contents
- [Overview](#overview)
- [Testing Architecture](#testing-architecture)
- [Quick Start](#quick-start)
- [Test Types & Coverage](#test-types--coverage)
- [Unit & Integration Testing](#unit--integration-testing)
- [E2E Testing](#e2e-testing)
- [Visual Regression Testing](#visual-regression-testing)
- [Accessibility Testing](#accessibility-testing)
- [Performance Testing](#performance-testing)
- [CI/CD Pipeline](#cicd-pipeline)
- [Development Workflow](#development-workflow)
- [Best Practices](#best-practices)
- [Troubleshooting](#troubleshooting)
## 🎯 Overview
The CommunityRule platform uses a comprehensive testing framework with multiple layers to ensure code quality, functionality, visual consistency, and accessibility across all browsers and devices.
### Testing Stack
- **Unit/Integration**: Vitest + JSDOM + React Testing Library
- **E2E**: Playwright (Chromium, Firefox, WebKit, Mobile)
- **Visual Regression**: Playwright Screenshots
- **Performance**: Lighthouse CI
- **Accessibility**: Axe-core + Playwright
- **CI/CD**: Gitea Actions
### Current Status
-**428 Unit Tests** (94.88% coverage - exceeds 85% target)
-**92 E2E Tests** across 4 browsers
-**23 Visual Regression Tests** per browser
-**Performance Budgets** with Lighthouse CI
-**WCAG 2.1 AA Compliance** with automated testing
-**Bundle Analysis** with automated monitoring
-**Web Vitals Tracking** with real-time metrics
-**Performance Optimization** with React.memo and code splitting
## 🏗 Testing Architecture
### Test Pyramid
- **Unit Tests**: Fast, focused, high coverage (94.88%)
- **Integration Tests**: Component interactions, data flow
- **E2E Tests**: Critical user journeys, cross-browser compatibility
### Testing Philosophy
**JSDOM Limitations**: Unit tests in JSDOM can't truly test responsive behavior since CSS media queries aren't evaluated. Therefore:
- **Unit/Integration Tests**: Test component structure, accessibility, and configuration
- **E2E Tests**: Test real responsive behavior at actual viewport widths
- **Visual Tests**: Capture visual consistency across breakpoints
## 🚀 Quick Start
### Prerequisites
```bash
# Install dependencies
npm install
# Install Playwright browsers
npx playwright install
```
### Essential Commands
```bash
# Unit tests with coverage
npm test
# E2E tests
npm run e2e
# Visual regression tests
npm run visual:test
# Performance tests
npm run lhci
# Storybook tests
npm run test:sb
```
## 🧪 Test Types & Coverage
### Test Structure
```
tests/
├── unit/ # Component unit tests
│ ├── Button.test.jsx # 12 tests
│ ├── Logo.test.jsx # 12 tests
│ ├── RuleCard.test.jsx # 18 tests
│ ├── SectionHeader.test.jsx # 17 tests
│ ├── NumberedCard.test.jsx # 18 tests
│ └── accessibility.test.jsx # 18 tests
├── integration/ # Component integration tests
│ ├── component-interactions.integration.test.jsx
│ ├── page-flow.integration.test.jsx
│ ├── user-journey.integration.test.jsx
│ ├── layout.integration.test.jsx
│ └── ContentLockup.integration.test.jsx
└── e2e/ # End-to-end tests
├── homepage.spec.ts # Homepage functionality
├── user-journeys.spec.ts # User workflows
├── header.responsive.spec.js # Responsive header
├── footer.responsive.spec.js # Responsive footer
├── visual-regression.spec.ts # Visual consistency
├── accessibility.spec.ts # Accessibility compliance
└── performance.spec.ts # Performance metrics
```
### Coverage Requirements
- **Statements**: >85% (Current: 94.88%) ✅
- **Branches**: >80% (Current: 86.93%) ✅
- **Functions**: >80% (Current: 88.67%) ✅
- **Lines**: >85% (Current: 94.88%) ✅
## 🧩 Unit & Integration Testing
### Framework
- **Vitest**: Fast unit test runner
- **JSDOM**: Browser environment simulation
- **React Testing Library**: Component testing utilities
- **MSW**: API mocking
### Configuration
```javascript
// vitest.config.js
export default defineConfig({
plugins: [react({ jsxRuntime: "automatic" })],
test: {
environment: "jsdom",
setupFiles: ["./vitest.setup.js"],
coverage: {
provider: "v8",
thresholds: { lines: 85, functions: 85, statements: 85, branches: 80 },
},
},
});
```
### Writing Unit Tests
```jsx
// tests/unit/Component.test.jsx
import { render, screen } from "@testing-library/react";
import { describe, test, expect, afterEach } from "vitest";
import { cleanup } from "@testing-library/react";
import Component from "../../app/components/Component";
describe("Component", () => {
afterEach(() => cleanup());
test("renders correctly", () => {
render(<Component />);
expect(screen.getByRole("button")).toBeInTheDocument();
});
test("handles user interactions", async () => {
const user = userEvent.setup();
render(<Component />);
const button = screen.getByRole("button");
await user.click(button);
expect(button).toHaveClass("clicked");
});
});
```
### Testing Library Queries (Priority Order)
1. **`getByRole`**: Most accessible, tests user experience
2. **`getByLabelText`**: For form inputs
3. **`getByText`**: For content
4. **`getByTestId`**: Last resort, avoid when possible
### Integration Testing
```jsx
test("components work together", () => {
render(
<div>
<Header />
<MainContent />
<Footer />
</div>,
);
// Test that components complement each other
expect(screen.getByRole("banner")).toBeInTheDocument();
expect(screen.getByRole("main")).toBeInTheDocument();
expect(screen.getByRole("contentinfo")).toBeInTheDocument();
});
```
### Available Scripts
```bash
npm test # Run all tests with coverage
npm run test:watch # Run tests in watch mode
npm run test:ui # Run tests with UI
```
## 🌐 E2E Testing
### Framework
- **Playwright**: Cross-browser E2E testing
- **Browsers**: Chromium, Firefox, WebKit, Mobile
- **Accessibility**: Axe-core integration
### Configuration
```typescript
// playwright.config.ts
export default defineConfig({
testDir: "./tests/e2e",
projects: [
{ name: "chromium", use: { ...devices["Desktop Chrome"] } },
{ name: "firefox", use: { ...devices["Desktop Firefox"] } },
{ name: "webkit", use: { ...devices["Desktop Safari"] } },
{ name: "mobile", use: { ...devices["iPhone 13"] } },
],
use: {
timezoneId: "UTC",
locale: "en-US",
headless: true,
},
});
```
### Test Categories
#### 1. Functional Tests
- Page loading and sections
- Component functionality
- Navigation and interactions
- User workflows
#### 2. Responsive Tests
- Layout changes between breakpoints
- Component visibility at different viewports
- Interactive behavior across screen sizes
#### 3. Accessibility Tests
- WCAG 2.1 AA compliance
- Screen reader compatibility
- Keyboard navigation
- Color contrast
### Writing E2E Tests
```typescript
// tests/e2e/example.spec.ts
import { test, expect } from "@playwright/test";
test.describe("Feature", () => {
test.beforeEach(async ({ page }) => {
await page.goto("/");
});
test("should work correctly", async ({ page }) => {
await expect(page).toHaveTitle(/CommunityRule/);
await expect(page.locator("h1")).toBeVisible();
});
test("responsive behavior", async ({ page }) => {
// Test mobile viewport
await page.setViewportSize({ width: 375, height: 667 });
await expect(page.getByTestId("mobile-nav")).toBeVisible();
// Test desktop viewport
await page.setViewportSize({ width: 1280, height: 800 });
await expect(page.getByTestId("desktop-nav")).toBeVisible();
});
});
```
### Available Scripts
```bash
npm run e2e # Run all E2E tests
npm run e2e:ui # Run E2E tests with UI
npm run e2e:serve # Start dev server and run tests
```
## 🎨 Visual Regression Testing
### Overview
Visual regression testing ensures UI consistency across browsers and prevents unintended visual changes by comparing screenshots against baseline images.
### Configuration
- **Snapshot Template**: `{testDir}/{testFileName}-snapshots/{arg}-{projectName}.png`
- **Deterministic Rendering**: Fixed timezone (UTC), locale (en-US), viewport
- **Tolerance**: 2% pixel difference or 500 pixels maximum
- **Animation Handling**: Disabled during capture
### Screenshots Generated
- **Full page screenshots** (mobile, tablet, desktop)
- **Component screenshots** (hero, logo wall, cards, etc.)
- **Interactive states** (hover, focus, loading, error)
- **Special modes** (dark mode, high contrast, reduced motion)
### Breakpoint Coverage
- **Mobile**: 375x667 (iPhone)
- **Tablet**: 768x1024 (iPad)
- **Desktop**: 1280x800 (Standard)
- **Large Desktop**: 1920x1080 (Full HD)
### Managing Visual Changes
```bash
# Update baselines after intentional changes
npm run visual:update
# Run visual regression tests
npm run visual:test
# Run with UI for debugging
npm run visual:ui
```
### Snapshot Management
```bash
# Update snapshots for all projects
PLAYWRIGHT_UPDATE_SNAPSHOTS=1 npx playwright test tests/e2e/visual-regression.spec.ts
# Update snapshots for specific project
PLAYWRIGHT_UPDATE_SNAPSHOTS=1 npx playwright test tests/e2e/visual-regression.spec.ts --project=chromium
# View test results
npx playwright show-report
```
## ♿ Accessibility Testing
### Framework
- **Unit Level**: jest-axe with Vitest (`tests/accessibility/unit/`)
- **E2E Level**: Playwright accessibility tests (`tests/accessibility/e2e/`)
- **Standards**: WCAG 2.1 AA compliance
### Test Organization
Accessibility tests are organized in a dedicated `tests/accessibility/` folder:
```
tests/accessibility/
├── unit/ # Unit-level accessibility tests
│ └── components.test.jsx # Component accessibility (jest-axe)
└── e2e/ # E2E accessibility tests
└── wcag-compliance.spec.ts # WCAG compliance (Playwright)
```
### Unit-Level Accessibility Testing
```jsx
// tests/accessibility/unit/components.test.jsx
import { axe, toHaveNoViolations } from "jest-axe";
test("component has no accessibility violations", async () => {
const { container } = render(<Component />);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
```
### E2E Accessibility Testing
```typescript
// tests/accessibility/e2e/wcag-compliance.spec.ts
import { test, expect } from "@playwright/test";
test("WCAG 2.1 AA compliance - homepage", async ({ page }) => {
await page.goto("/");
// Check for proper HTML structure
const html = page.locator("html");
const lang = await html.getAttribute("lang");
expect(lang).toBeTruthy();
// Check for main heading
const h1 = page.locator("h1").first();
await expect(h1).toBeVisible();
});
```
### Running Accessibility Tests
```bash
# Run all accessibility tests
npm test tests/accessibility/
# Run unit accessibility tests only
npm test tests/accessibility/unit/
# Run E2E accessibility tests only
npx playwright test tests/accessibility/e2e/
# Run specific accessibility test
npx playwright test tests/accessibility/e2e/wcag-compliance.spec.ts
```
### Manual Testing Checklist
- [ ] Screen reader compatibility
- [ ] Keyboard navigation
- [ ] Color contrast (WCAG AA)
- [ ] Focus management
- [ ] ARIA attributes
- [ ] Semantic HTML
### WCAG 2.1 AA Requirements
- **Perceivable**: Text alternatives, captions, adaptable content
- **Operable**: Keyboard accessible, timing adjustable, navigation
- **Understandable**: Readable, predictable, input assistance
- **Robust**: Compatible with assistive technologies
## ⚡ Performance Testing
### Framework
- **Lighthouse CI**: Automated performance testing
- **Bundle Analysis**: Real-time bundle size monitoring
- **Web Vitals Tracking**: Core Web Vitals collection and reporting
- **Performance Monitoring**: Comprehensive performance metrics
- **Performance Budgets**: Defined thresholds with automated enforcement
### Configuration
```json
// .lighthouserc.json
{
"ci": {
"collect": {
"url": ["http://localhost:3010"],
"chromeFlags": [
"--no-sandbox",
"--disable-dev-shm-usage",
"--disable-gpu",
"--headless"
]
},
"assert": {
"assertions": {
"categories:performance": ["warn", { "minScore": 0.8 }],
"categories:accessibility": ["error", { "minScore": 0.8 }]
}
}
}
}
```
### Performance Metrics
- **Core Web Vitals**: LCP < 2.5s, FID < 100ms, CLS < 0.1
- **Performance Score**: >80
- **Accessibility Score**: >80
- **Best Practices**: >90
- **Bundle Size**: <250KB gzipped (currently 101KB)
### Performance Budgets
- **First Contentful Paint**: <3000ms
- **Largest Contentful Paint**: <5000ms
- **First Input Delay**: <100ms
- **TTFB**: <700ms
- **Bundle Size**: <250KB gzipped
- **Total Bundle Size**: <2MB
### Performance Optimizations
- **✅ Code Splitting**: Dynamic imports for non-critical components
- **✅ React.memo**: Applied to all 30+ components
- **✅ Image Optimization**: Enhanced `next/image` with lazy loading
- **✅ Font Optimization**: Preloading and fallbacks
- **✅ Bundle Analysis**: Real-time monitoring with budgets
- **✅ Error Boundaries**: Comprehensive error handling
### Available Scripts
```bash
# Individual monitoring tools
npm run bundle:analyze # Analyze bundle sizes and budgets
npm run performance:monitor # Performance metrics and Lighthouse CI
npm run web-vitals:track # Core Web Vitals tracking
# Comprehensive testing
npm run test:performance # All performance tests
npm run monitor:all # All monitoring tools
# Traditional Lighthouse CI
npm run lhci # Run Lighthouse CI
npm run lhci:mobile # Run with mobile preset
npm run lhci:desktop # Run with desktop preset
```
### Performance Monitoring Dashboard
Access the performance monitoring dashboard at `/monitor` to view:
- Real-time Web Vitals metrics
- Historical performance data
- Bundle analysis results
- Performance budget status
- Optimization recommendations
## 🔄 CI/CD Pipeline
### Gitea Actions Workflow
Location: `.gitea/workflows/ci.yaml`
### Pipeline Jobs
#### 1. Unit Tests
- **Node.js versions**: 18, 20
- **Coverage reporting**: Codecov integration
- **Parallel execution**: Matrix strategy
#### 2. E2E Tests
- **Browsers**: Chromium, Firefox, WebKit
- **Parallel execution**: Matrix strategy
- **Artifact upload**: Test results and reports
#### 3. Visual Regression Tests
- **Screenshot comparison**: Baseline vs current
- **Cross-browser validation**: All 4 browser projects
#### 4. Performance Tests
- **Lighthouse CI**: Performance budgets
- **Core Web Vitals**: Monitoring
- **Accessibility compliance**
#### 5. Storybook Tests
- **Component testing**: Automated tests
- **Accessibility validation**: WCAG compliance
- **Build verification**: Storybook compilation
#### 6. Lint & Format
- **ESLint**: Code quality
- **Prettier**: Code formatting
#### 7. Build Verification
- **Next.js build**: Application compilation
- **Storybook build**: Documentation compilation
### Triggers
```yaml
on:
push:
branches: [main, develop]
pull_request:
branches: [main, develop]
```
## 🛠 Development Workflow
### 1. Feature Development
```bash
# Create feature branch
git checkout -b feature/new-component
# Write tests first (TDD)
npm run test:watch
# Implement feature
# Ensure tests pass
# Run E2E tests
npm run e2e
# Commit changes
git add .
git commit -m "feat: add new component with tests"
```
### 2. Pull Request Process
1. **Create PR** → CI pipeline starts automatically
2. **Review CI Results** → All 7 jobs must pass
3. **Check Coverage** → Ensure >85% coverage
4. **Review Visual Changes** → Check screenshot diffs
5. **Merge** → Only if all checks pass
### 3. Visual Changes
```bash
# Make visual changes
# Run visual regression tests
npm run visual:test
# If changes are intentional, update baselines
npm run visual:update
# Review and commit updated snapshots
git add tests/e2e/visual-regression.spec.ts-snapshots/
git commit -m "Update visual regression snapshots for [describe changes]"
```
### 4. Performance Monitoring
```bash
# Check performance before deploying
npm run lhci
# Review performance budgets
# Update .lighthouserc.json if needed
```
## 📋 Best Practices
### 1. Test-Driven Development
- Write tests before implementation
- Use descriptive test names
- Test edge cases and error scenarios
- Maintain high test coverage
### 2. Component Testing
```jsx
// ✅ Good: Test behavior, not implementation
test("shows error message when form is invalid", () => {
render(<Form />);
fireEvent.click(screen.getByRole("button"));
expect(screen.getByText("Please fill all fields")).toBeInTheDocument();
});
// ❌ Avoid: Testing implementation details
test("calls onSubmit with form data", () => {
const mockSubmit = vi.fn();
render(<Form onSubmit={mockSubmit} />);
// Implementation details...
});
```
### 3. E2E Testing
- Test user workflows, not technical details
- Use semantic selectors (role, text, label)
- Test accessibility features
- Include error scenarios
### 4. Visual Regression
- Update baselines only for intentional changes
- Review screenshot diffs carefully
- Test across multiple viewports
- Consider animation states
### 5. Performance Testing
- Set realistic performance budgets
- Monitor Core Web Vitals
- Test on different network conditions
- Regular performance audits
### 6. Responsive Testing
```javascript
// ✅ Good: Test real viewport sizes
await page.setViewportSize({ width: 640, height: 700 });
// ✅ Good: Test visibility at breakpoints
if (bp.name === "xs") {
await expect(page.getByTestId("auth-xs")).toBeVisible();
}
// ❌ Avoid: Testing responsive behavior in JSDOM
// JSDOM doesn't evaluate CSS media queries
```
## 🔧 Troubleshooting
### Common Issues
#### 1. Unit Tests Failing
```bash
# Run tests locally
npm test
# Check for:
# - Missing imports
# - Incorrect assertions
# - Component changes
# - Test environment issues
```
#### 2. E2E Tests Failing
```bash
# Run locally first
npm run e2e
# Common issues:
# - Selector changes
# - Component structure changes
# - Network issues
# - Browser compatibility
```
#### 3. Visual Regression Failing
```bash
# Check if changes are intentional
npm run visual:test
# Update baselines if needed
npm run visual:update
# Review screenshot diffs in CI artifacts
```
#### 4. Performance Tests Failing
```bash
# Run locally
npm run lhci
# Check performance budgets in .lighthouserc.json
# Optimize slow components
# Review bundle size
```
#### 5. CI Pipeline Issues
```bash
# Check Gitea Actions logs
# Verify workflow configuration
# Check for missing dependencies
# Review environment variables
```
### Debug Commands
```bash
# Debug unit tests
npm run test:ui
# Debug E2E tests
npm run e2e:ui
# Debug with browser dev tools
npx playwright test --debug
# Run specific test file
npx playwright test tests/e2e/homepage.spec.ts
# Run tests in headed mode
npx playwright test --headed
```
## 📚 Additional Resources
### Documentation
- [Vitest Documentation](https://vitest.dev/)
- [Playwright Documentation](https://playwright.dev/)
- [React Testing Library](https://testing-library.com/docs/react-testing-library/intro/)
- [Lighthouse CI](https://github.com/GoogleChrome/lighthouse-ci)
- [Storybook Testing](https://storybook.js.org/docs/writing-tests/introduction)
### Tools
- [Codecov](https://codecov.io/) - Coverage reporting
- [Axe-core](https://github.com/dequelabs/axe-core) - Accessibility testing
- [MSW](https://mswjs.io/) - API mocking
### Best Practices
- [Testing Best Practices](https://kentcdodds.com/blog/common-mistakes-with-react-testing-library)
- [E2E Testing Guide](https://playwright.dev/docs/best-practices)
- [Visual Regression Testing](https://storybook.js.org/docs/writing-tests/visual-testing)
---
**Last Updated**: December 2024
**Framework Version**: Next.js 15 + React 19 + Tailwind 4 + Storybook 9
**Maintained by**: CommunityRule Development Team
-357
View File
@@ -1,357 +0,0 @@
# Testing Quick Reference
## 🚀 Essential Commands
### Daily Development
```bash
# Run all tests with coverage
npm test
# Watch mode (during development)
npm run test:watch
# E2E tests
npm run e2e
# Visual regression tests
npm run visual:test
# Performance check
npm run lhci
# Performance monitoring
npm run test:performance # Comprehensive performance testing
npm run bundle:analyze # Bundle size analysis
npm run web-vitals:track # Web Vitals tracking
npm run monitor:all # All monitoring tools
# Storybook tests
npm run test:sb
```
### Test UI & Debugging
```bash
# Debug unit tests
npm run test:ui
# Debug E2E tests
npm run e2e:ui
# Debug with browser
npx playwright test --debug
# Run tests in headed mode
npx playwright test --headed
```
## 📊 Current Test Status
- **Unit Tests**: 94.88% ✅ (Target: >85%)
- **Integration Tests**: 5 comprehensive test suites ✅
- **E2E Tests**: 92 tests across 4 browsers ✅
- **Visual Regression**: 23 tests per browser ✅
- **Accessibility Tests**: WCAG 2.1 AA compliance ✅
- **Performance Tests**: Lighthouse CI with budgets ✅
- **Bundle Analysis**: Real-time monitoring with budgets ✅
- **Web Vitals Tracking**: Core Web Vitals collection ✅
- **Performance Optimization**: React.memo + code splitting ✅
## 🔧 Common Test Commands
### Unit Testing
```bash
# Run specific test file
npm test -- --run tests/unit/Component.test.jsx
# Run tests matching pattern
npm test -- --run Component
# Run with coverage report
npm test -- --coverage
# Run in watch mode
npm run test:watch
```
### E2E Testing
```bash
# Run specific test file
npm run e2e -- tests/e2e/homepage.spec.ts
# Run specific project (browser)
npm run e2e -- --project=chromium
# Run with headed browser
npm run e2e -- --headed
# Run with debug mode
npm run e2e -- --debug
```
### Visual Regression
```bash
# Update snapshots for all projects
npm run visual:update
# Update snapshots for specific project
PLAYWRIGHT_UPDATE_SNAPSHOTS=1 npx playwright test tests/e2e/visual-regression.spec.ts --project=chromium
# View test results
npx playwright show-report
```
### Performance Testing
```bash
# Run mobile performance test
npm run lhci:mobile
# Run desktop performance test
npm run lhci:desktop
# Run with custom budget
npm run performance:budget
```
### Accessibility Testing
```bash
# Run all accessibility tests
npm test tests/accessibility/
# Run unit accessibility tests only
npm test tests/accessibility/unit/
# Run E2E accessibility tests only
npx playwright test tests/accessibility/e2e/
# Run specific accessibility test
npx playwright test tests/accessibility/e2e/wcag-compliance.spec.ts
```
## 📱 Browser Support
| Browser | Project Name | Status |
| ----------- | ------------ | --------------- |
| **Chrome** | `chromium` | ✅ Full Support |
| **Firefox** | `firefox` | ✅ Full Support |
| **Safari** | `webkit` | ✅ Full Support |
| **Mobile** | `mobile` | ✅ Full Support |
## 🎯 Testing Best Practices
### 1. Test Structure (AAA Pattern)
```jsx
test("should do something", () => {
// Arrange: Set up test data
const data = { name: "Test" };
// Act: Perform the action
const result = processData(data);
// Assert: Verify the outcome
expect(result).toBe("Processed Test");
});
```
### 2. Query Priority
1. **`getByRole`** - Most accessible, tests user experience
2. **`getByLabelText`** - For form inputs
3. **`getByText`** - For content
4. **`getByTestId`** - Last resort, avoid when possible
### 3. Async Testing
```jsx
test("async operation", async () => {
const user = userEvent.setup();
render(<Component />);
const button = screen.getByRole("button");
await user.click(button);
await waitFor(() => {
expect(screen.getByText("Success")).toBeInTheDocument();
});
});
```
### 4. Responsive Testing
```javascript
// ✅ Good: Test real viewport sizes
await page.setViewportSize({ width: 640, height: 700 });
// ✅ Good: Test visibility at breakpoints
if (bp.name === "xs") {
await expect(page.getByTestId("auth-xs")).toBeVisible();
}
// ❌ Avoid: Testing responsive behavior in JSDOM
// JSDOM doesn't evaluate CSS media queries
```
## 🔍 Common Issues & Solutions
### Visual Regression Failures
```bash
# Regenerate snapshots
npm run visual:update
# Check for environment differences
# Ensure deterministic rendering in Playwright config
```
### E2E Test Failures
```bash
# Use waitFor instead of waitForTimeout
await page.waitForSelector("button", { state: "visible" });
# Use role-based selectors
await page.getByRole("button", { name: "Submit" }).click();
# Check for selector changes
# Verify component structure hasn't changed
```
### Performance Test Failures
```bash
# Check Chrome path on macOS
# Ensure arm64 Chrome for Apple Silicon
# Verify performance budgets in .lighthouserc.json
```
### Unit Test Failures
```bash
# Check for missing imports
# Verify component exports
# Ensure test environment setup
# Check for component changes
```
## 📈 Performance Budgets
### Lighthouse CI Targets
- **Performance Score**: >80
- **Accessibility Score**: >80
- **Best Practices**: >90
- **SEO Score**: >90
### Core Web Vitals
- **LCP**: <2.5s
- **FID**: <100ms
- **CLS**: <0.1
### Performance Budgets
- **First Contentful Paint**: <3000ms
- **Largest Contentful Paint**: <5000ms
- **First Input Delay**: <100ms
- **TTFB**: <700ms
## 🔄 CI/CD Pipeline Jobs
1. **Unit Tests** (Node 18, 20) - Coverage reporting
2. **E2E Tests** (Chromium, Firefox, WebKit) - Cross-browser testing
3. **Visual Regression Tests** - Screenshot comparison
4. **Performance Tests** - Lighthouse CI with budgets
5. **Storybook Tests** - Component testing & accessibility
6. **Lint & Format** - Code quality & formatting
7. **Build Verification** - Next.js & Storybook builds
## 📁 Test File Structure
```
tests/
├── unit/ # Component tests
│ ├── Button.test.jsx # 12 tests
│ ├── Logo.test.jsx # 12 tests
│ ├── RuleCard.test.jsx # 18 tests
│ ├── SectionHeader.test.jsx # 17 tests
│ ├── NumberedCard.test.jsx # 18 tests
│ └── ... # Other component tests
├── integration/ # Integration tests
│ ├── component-interactions.integration.test.jsx
│ ├── page-flow.integration.test.jsx
│ ├── user-journey.integration.test.jsx
│ ├── layout.integration.test.jsx
│ └── ContentLockup.integration.test.jsx
├── accessibility/ # Accessibility-focused tests
│ ├── unit/ # Unit-level accessibility (jest-axe)
│ │ └── components.test.jsx # Component accessibility tests
│ └── e2e/ # E2E accessibility (Playwright + axe-core)
│ └── wcag-compliance.spec.ts # WCAG compliance tests
└── e2e/ # General E2E tests
├── homepage.spec.ts # Homepage functionality
├── user-journeys.spec.ts # User workflows
├── header.responsive.spec.js # Responsive header
├── footer.responsive.spec.js # Responsive footer
├── visual-regression.spec.ts # Visual consistency
├── accessibility.spec.ts # General accessibility tests
└── performance.spec.ts # Performance metrics
```
## 🎨 Visual Regression Screenshots
### Generated Screenshots
- Full page (mobile, tablet, desktop)
- Component sections (hero, logo wall, cards)
- Interactive states (hover, focus, loading)
- Special modes (dark, high contrast, reduced motion)
### Managing Changes
```bash
# Intentional changes
npm run visual:update
# Review changes
git diff tests/e2e/visual-regression.spec.ts-snapshots/
# Commit updated snapshots
git add tests/e2e/visual-regression.spec.ts-snapshots/
git commit -m "Update visual regression snapshots for [describe changes]"
```
## 📈 Monitoring
### Test Metrics
- **Unit Tests**: 305 tests (94.88% coverage)
- **E2E Tests**: 92 tests (4 browsers)
- **Visual Screenshots**: 92 baselines per browser
- **Coverage**: >85% target (exceeded)
### CI Metrics
- **Pipeline Jobs**: 7 parallel jobs
- **Execution Time**: Monitor build performance
- **Success Rate**: Track pipeline stability
- **Artifacts**: Test results and screenshots
## 🔗 Useful Links
- **Full Testing Documentation**: [docs/guides/testing-framework.md](./testing-framework.md)
- **Vitest Docs**: https://vitest.dev/
- **Playwright Docs**: https://playwright.dev/
- **React Testing Library**: https://testing-library.com/docs/react-testing-library/intro/
- **Lighthouse CI**: https://github.com/GoogleChrome/lighthouse-ci
---
**Quick Reference Version**: December 2024
**For detailed guidelines, see [testing-framework.md](./testing-framework.md)**
-258
View File
@@ -1,258 +0,0 @@
# Testing Strategy for CommunityRule
## Overview
This document outlines our comprehensive testing strategy that properly separates unit testing from responsive behavior testing, following best practices for JSDOM limitations and real browser testing.
## Current Test Status
- **236 total tests** across the project
- **227 tests passing** (96.2% success rate)
- **9 tests failing** (performance and interaction tests)
- **15 test files** covering all major components
- **Performance Monitoring**: Comprehensive regression detection and budget enforcement
## Testing Philosophy
### The Problem with JSDOM and Responsive Testing
**Short take: Unit tests in JSDOM can't truly "switch breakpoints."** JSDOM doesn't evaluate CSS media queries, so Tailwind's `hidden sm:block …` won't change visibility when you "resize" the window.
### Solution: Proper Test Separation
- **Unit / component tests (Vitest + RTL):** assert **structure and classes**, not responsive visibility.
- **Responsive behavior:** verify with **browser-based tests** (Playwright) or **visual tests** (Chromatic/Storybook) at real viewport widths.
## Test Categories
### 1. Unit Tests (Vitest + React Testing Library)
**Purpose:** Test component structure, accessibility, and configuration data.
**What to test:**
- DOM roles/labels exist: `role="banner"`, nav landmark, menu items
- The right **Tailwind classes** are present on wrappers (`block sm:hidden`, `hidden md:block`, etc.)
- Data-driven bits produce the expected count/order (e.g., `navigationItems`, `avatarImages`, `logoConfig`)
- Component configuration and exported data structures
**Example:**
```javascript
// tests/unit/Header.structure.test.js
test("logo wrappers include breakpoint classes", () => {
render(<Header />);
const logoWrappers = screen.getAllByTestId("logo-wrapper");
// Check first logo variant (xs only)
expect(logoWrappers[0]).toHaveClass("block", "sm:hidden");
// Check second logo variant (sm only)
expect(logoWrappers[1]).toHaveClass("hidden", "sm:block", "md:hidden");
});
```
### 2. Browser-Based Tests (Playwright)
**Purpose:** Test real responsive behavior at actual viewport widths.
**What to test:**
- **Visibility** at real breakpoints
- **Layout changes** between breakpoints
- **Interactive behavior** at different screen sizes
- **Accessibility** across viewports
**Example:**
```javascript
// tests/e2e/header.responsive.spec.js
const breakpoints = [
{ name: "xs", width: 360, height: 700 },
{ name: "sm", width: 640, height: 700 },
{ name: "md", width: 768, height: 700 },
{ name: "lg", width: 1024, height: 700 },
{ name: "xl", width: 1280, height: 700 },
];
for (const bp of breakpoints) {
test(`header layout at ${bp.name}`, async ({ page }) => {
await page.setViewportSize({ width: bp.width, height: bp.height });
await page.goto("/");
const nav = page.getByRole("navigation", { name: /main navigation/i });
await expect(nav).toBeVisible();
});
}
```
### 3. Visual Tests (Storybook + Chromatic)
**Purpose:** Visual regression testing and design system validation.
**What to test:**
- **Visual diffs** per breakpoint
- **Design consistency** across viewports
- **Component variations** and states
**Example:**
```javascript
// stories/Header.responsive.stories.js
export default {
parameters: {
chromatic: {
viewports: [360, 640, 768, 1024, 1280],
delay: 100,
},
},
};
```
## Component Improvements
### Header Component Enhancements
1. **Added Test IDs** for easier testing:
```jsx
<div data-testid="logo-wrapper" className={config.breakpoint}>
{renderLogo(config.size, config.showText)}
</div>
```
2. **Exported Configuration** for testing:
```javascript
export const navigationItems = [...];
export const avatarImages = [...];
export const logoConfig = [...];
```
3. **Structured Breakpoint Containers**:
```jsx
<div data-testid="nav-xs" className="block sm:hidden">
<div data-testid="nav-sm" className="hidden sm:block md:hidden">
<div data-testid="nav-md" className="hidden md:block lg:hidden">
```
## Test File Structure
```
tests/
├── unit/ # Unit tests (Vitest + RTL)
│ ├── Header.test.jsx # CONSOLIDATED: Comprehensive Header tests
│ ├── Footer.test.jsx
│ ├── Layout.test.jsx
│ └── Page.test.jsx
├── integration/ # Integration tests
│ └── ContentLockup.integration.test.jsx
├── e2e/ # Browser tests (Playwright)
│ └── header.responsive.spec.js # NEW: Responsive behavior tests
└── stories/ # Storybook stories
└── Header.responsive.stories.js # NEW: Visual testing
```
## Best Practices
### Unit Testing (JSDOM)
1. **Test structure, not visibility**:
```javascript
// ✅ Good: Test classes exist
expect(element).toHaveClass("block", "sm:hidden");
// ❌ Bad: Test visibility (doesn't work in JSDOM)
expect(element).toBeVisible();
```
2. **Use test IDs for containers**:
```javascript
// ✅ Good: Test specific containers
const logoWrapper = screen.getByTestId("logo-wrapper");
// ❌ Bad: Query by complex class strings
const logoWrapper = document.querySelector(".block.sm\\:hidden");
```
3. **Test configuration data**:
```javascript
// ✅ Good: Test exported configuration
expect(navigationItems).toHaveLength(3);
expect(logoConfig).toHaveLength(5);
```
### Browser Testing (Playwright)
1. **Test real viewport sizes**:
```javascript
await page.setViewportSize({ width: 640, height: 700 });
```
2. **Test visibility at breakpoints**:
```javascript
if (bp.name === "xs") {
await expect(page.getByTestId("auth-xs")).toBeVisible();
}
```
3. **Test accessibility across viewports**:
```javascript
const interactiveElements = [
page.getByRole("link", { name: /use cases/i }),
page.getByRole("button", { name: /create rule/i }),
];
for (const element of interactiveElements) {
await expect(element).toBeVisible();
await expect(element).toBeEnabled();
}
```
## Running Tests
### Unit Tests
```bash
npm test # Run all unit tests
npm test tests/unit/ # Run only unit tests
npm test Header.structure # Run specific test file
```
### Browser Tests
```bash
npx playwright test # Run all browser tests
npx playwright test header.responsive.spec.js # Run specific test
```
### Visual Tests
```bash
npm run storybook # Start Storybook
npx chromatic --project-token=xxx # Run visual tests
```
## Future Improvements
1. **Add more Playwright tests** for other components
2. **Set up Chromatic** for visual regression testing
3. **Add performance tests** for responsive behavior
4. **Create component-specific test utilities**
5. **Add accessibility testing** with axe-core
## Key Takeaways
1. **JSDOM limitations** require separating structure tests from visibility tests
2. **Test IDs** make testing more reliable and maintainable
3. **Exported configuration** enables better data structure testing
4. **Real browser testing** is essential for responsive behavior
5. **Visual testing** catches design regressions across breakpoints
This strategy provides comprehensive coverage while respecting the limitations of different testing environments.
-391
View File
@@ -1,391 +0,0 @@
# Visual Regression Testing Guide
## Overview
Visual regression testing ensures UI consistency across browsers and prevents unintended visual changes by comparing screenshots against baseline images. This guide covers the complete workflow for managing visual regression tests.
## 🚀 Quick Start
### First-Time Setup
```bash
# 1. Generate baseline snapshots for all projects
npm run visual:update
# 2. Verify snapshots were created
ls tests/e2e/visual-regression.spec.ts-snapshots/
# 3. Commit the snapshots
git add tests/e2e/visual-regression.spec.ts-snapshots/
git commit -m "Add baseline visual regression snapshots"
# 4. Verify setup works
npm run visual:test
```
### Daily Workflow
```bash
# Run visual regression tests
npm run visual:test
# Run with UI for debugging
npm run visual:ui
# Update snapshots after UI changes
npm run visual:update
```
## 📝 Managing Visual Changes
### When UI Changes Are Intentional
1. **Make your UI changes** (design updates, component modifications, etc.)
2. **Update snapshots to reflect new design:**
```bash
npm run visual:update
```
3. **Review changes:**
```bash
git diff tests/e2e/visual-regression.spec.ts-snapshots/
```
4. **Commit updated snapshots:**
```bash
git add tests/e2e/visual-regression.spec.ts-snapshots/
git commit -m "Update snapshots for [describe changes]"
```
### When UI Changes Are Unintentional
1. **Investigate the failure** - Check what changed and why
2. **Fix the regression** - Revert or fix the unintended change
3. **Re-run tests** - Ensure they pass without updating snapshots
4. **Commit the fix** - Don't update snapshots for bug fixes
## ⚙️ Configuration
### Playwright Configuration
The visual regression tests use these key settings in `playwright.config.ts`:
```typescript
export default defineConfig({
expect: {
toHaveScreenshot: {
animations: "disabled",
maxDiffPixelRatio: 0.02, // 2% tolerance
maxDiffPixels: 500, // 500 pixel tolerance
},
},
use: {
timezoneId: "UTC", // Consistent timezone
locale: "en-US", // Consistent locale
headless: true, // Headless for CI
},
snapshotPathTemplate:
"{testDir}/{testFileName}-snapshots/{arg}-{projectName}.png",
});
```
### Deterministic Rendering
To ensure consistent screenshots across environments:
- **Fixed timezone**: UTC
- **Fixed locale**: en-US
- **Fixed viewport**: 1280x800 (configurable per test)
- **Disabled animations**: Prevents timing-related differences
- **Browser-specific snapshots**: Separate baselines per browser
## 📱 Breakpoint Coverage
### Standard Viewports
| Breakpoint | Width | Height | Description |
| ----------- | ------ | ------ | ---------------- |
| **Mobile** | 375px | 667px | iPhone portrait |
| **Tablet** | 768px | 1024px | iPad portrait |
| **Desktop** | 1280px | 800px | Standard desktop |
| **Large** | 1920px | 1080px | Full HD desktop |
### Custom Viewports
```typescript
test("mobile layout", async ({ page }) => {
await page.setViewportSize({ width: 375, height: 667 });
await expect(page).toHaveScreenshot("mobile-layout.png");
});
test("tablet layout", async ({ page }) => {
await page.setViewportSize({ width: 768, height: 1024 });
await expect(page).toHaveScreenshot("tablet-layout.png");
});
```
## 🎨 Screenshot Types
### Full Page Screenshots
```typescript
test("homepage full page", async ({ page }) => {
await expect(page).toHaveScreenshot("homepage-full.png", {
fullPage: true,
animations: "disabled",
scale: "css",
});
});
```
### Component Screenshots
```typescript
test("hero section", async ({ page }) => {
const hero = page.locator("[data-testid='hero-section']");
await expect(hero).toHaveScreenshot("hero-section.png");
});
```
### Interactive States
```typescript
test("button hover state", async ({ page }) => {
const button = page.getByRole("button", { name: "Submit" });
// Normal state
await expect(button).toHaveScreenshot("button-normal.png");
// Hover state
await button.hover();
await expect(button).toHaveScreenshot("button-hover.png");
});
```
### Special Modes
```typescript
test("dark mode", async ({ page }) => {
// Enable dark mode
await page.evaluate(() => {
document.documentElement.classList.add("dark");
});
await expect(page).toHaveScreenshot("dark-mode.png");
});
test("high contrast", async ({ page }) => {
// Enable high contrast
await page.evaluate(() => {
document.body.style.filter = "contrast(200%)";
});
await expect(page).toHaveScreenshot("high-contrast.png");
});
```
## 🔄 Snapshot Management
### Update Commands
```bash
# Update all snapshots for all projects
npm run visual:update
# Update snapshots for specific project
PLAYWRIGHT_UPDATE_SNAPSHOTS=1 npx playwright test tests/e2e/visual-regression.spec.ts --project=chromium
# Update snapshots for specific test
PLAYWRIGHT_UPDATE_SNAPSHOTS=1 npx playwright test tests/e2e/visual-regression.spec.ts --grep="homepage"
```
### Snapshot Naming Convention
Snapshots follow this pattern:
```
{testDir}/{testFileName}-snapshots/{arg}-{projectName}.png
```
Examples:
- `tests/e2e/visual-regression.spec.ts-snapshots/homepage-full-chromium.png`
- `tests/e2e/visual-regression.spec.ts-snapshots/hero-section-firefox.png`
- `tests/e2e/visual-regression.spec.ts-snapshots/button-hover-webkit.png`
- `tests/e2e/visual-regression.spec.ts-snapshots/mobile-layout-mobile.png`
### File Organization
```
tests/e2e/visual-regression.spec.ts-snapshots/
├── homepage-full-chromium.png
├── homepage-full-firefox.png
├── homepage-full-webkit.png
├── homepage-full-mobile.png
├── hero-section-chromium.png
├── hero-section-firefox.png
├── hero-section-webkit.png
├── hero-section-mobile.png
└── ... (92 total screenshots)
```
## 🐛 Troubleshooting
### Common Issues
#### 1. "Snapshot doesn't exist" errors
**Cause**: Baseline snapshots haven't been generated or are missing
**Solution**:
```bash
# Regenerate all snapshots
npm run visual:update
# Or regenerate for specific project
PLAYWRIGHT_UPDATE_SNAPSHOTS=1 npx playwright test tests/e2e/visual-regression.spec.ts --project=chromium
```
#### 2. Platform differences (macOS vs Linux)
**Cause**: Different font rendering between platforms
**Solution**:
- Use CI-generated snapshots for consistency
- Ensure deterministic rendering settings
- Check font availability across platforms
#### 3. Minor pixel differences
**Cause**: Font rendering, anti-aliasing, scaling differences
**Solution**:
- Check tolerance settings in `playwright.config.ts`
- Use `scale: "css"` for consistent scaling
- Ensure deterministic CSS properties
#### 4. Animation-related failures
**Cause**: Animations not fully disabled
**Solution**:
- Ensure `animations: "disabled"` is set in test configuration
- Wait for animations to complete before screenshots
- Use `waitForTimeout` if necessary
#### 5. Height differences (especially WebKit)
**Cause**: WebKit may render elements with slightly different heights
**Solution**:
- Increase tolerance for height-sensitive tests
- Use `maxDiffPixels: 1000` for specific tests
- Consider using `ignoreSize: false` (default)
### Debug Commands
```bash
# Run with UI for visual debugging
npm run visual:ui
# Run specific test with debugging
npx playwright test tests/e2e/visual-regression.spec.ts --grep="homepage" --debug
# Run with headed browser
npx playwright test tests/e2e/visual-regression.spec.ts --headed
# View test results
npx playwright show-report
```
### Environment Consistency
To ensure consistent results:
1. **Use same Node.js version** across environments
2. **Use same Playwright version** across environments
3. **Use same browser versions** when possible
4. **Set consistent environment variables** (timezone, locale)
5. **Use deterministic CSS** (avoid random values, timestamps)
## 📊 CI/CD Integration
### CI Workflow
Visual regression tests run automatically in the CI pipeline:
- **Main branch**: Tests run against existing snapshots
- **Feature branches**: Tests run against existing snapshots
- **Artifacts**: Test results and screenshots uploaded for review
### CI Best Practices
1. **Don't regenerate snapshots in CI** for feature branches
2. **Use CI-generated snapshots** as the source of truth
3. **Review screenshot diffs** in CI artifacts
4. **Fail fast** on visual regressions
### Artifact Management
```yaml
# Example CI artifact configuration
- name: Upload visual regression results
if: always()
uses: actions/upload-artifact@v3
with:
name: visual-regression-results
path: |
test-results/
tests/e2e/visual-regression.spec.ts-snapshots/
```
## 🎯 Best Practices
### 1. Snapshot Management
- **Update snapshots only for intentional changes**
- **Review all changes** before committing
- **Use descriptive names** for snapshot files
- **Keep snapshots in version control**
### 2. Test Design
- **Test critical UI components** first
- **Use consistent viewport sizes** across tests
- **Test responsive breakpoints** systematically
- **Include interactive states** when relevant
### 3. Performance
- **Limit snapshot count** to essential components
- **Use appropriate timeouts** for slow operations
- **Parallelize tests** when possible
- **Cache browser installations** in CI
### 4. Maintenance
- **Regular cleanup** of outdated snapshots
- **Update snapshots promptly** after UI changes
- **Monitor test execution time** and optimize
- **Review and update tolerance settings** as needed
## 📚 Additional Resources
- **Main Testing Documentation**: [testing-framework.md](./testing-framework.md) | [testing.md](./testing.md)
- **Playwright Visual Testing**: https://playwright.dev/docs/screenshots
- **Visual Regression Best Practices**: https://storybook.js.org/docs/writing-tests/visual-testing
- **CI/CD Integration**: [testing-quick-reference.md](./testing-quick-reference.md)
- **Performance Guide**: [performance.md](./performance.md)
---
**Last Updated**: December 2024
**Maintained by**: CommunityRule Development Team
+2
View File
@@ -17,8 +17,10 @@
"test": "vitest run --coverage",
"test:watch": "vitest",
"test:ui": "vitest --ui",
"test:component": "vitest run tests/components",
"test:sb": "storybook dev -p 6006 & wait-on http://localhost:6006 && test-storybook --url http://localhost:6006",
"e2e": "playwright test",
"test:e2e": "playwright test",
"e2e:ui": "playwright test --ui",
"e2e:performance": "playwright test tests/e2e/performance.spec.ts",
"lhci": "lhci autorun",
+53 -6
View File
@@ -1,11 +1,58 @@
import React from "react";
import Checkbox from "../app/components/Checkbox";
import {
DefaultInteraction,
CheckedInteraction,
StandardInteraction,
InverseInteraction,
} from "../tests/storybook/Checkbox.interactions.test";
import { within, userEvent } from "@storybook/test";
import { expect } from "@storybook/test";
// Interaction functions for Storybook play functions
const DefaultInteraction = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const checkbox = canvas.getByRole("checkbox");
expect(checkbox).toHaveAttribute("aria-checked", "false");
await userEvent.click(checkbox);
expect(checkbox).toHaveAttribute("aria-checked", "true");
await userEvent.click(checkbox);
expect(checkbox).toHaveAttribute("aria-checked", "false");
},
};
const CheckedInteraction = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const checkbox = canvas.getByRole("checkbox");
expect(checkbox).toHaveAttribute("aria-checked", "true");
await userEvent.click(checkbox);
expect(checkbox).toHaveAttribute("aria-checked", "false");
},
};
const StandardInteraction = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const checkboxes = canvas.getAllByRole("checkbox");
expect(checkboxes).toHaveLength(2);
expect(checkboxes[0]).toHaveAttribute("aria-checked", "false");
await userEvent.click(checkboxes[0]);
expect(checkboxes[0]).toHaveAttribute("aria-checked", "true");
expect(checkboxes[1]).toHaveAttribute("aria-checked", "true");
await userEvent.click(checkboxes[1]);
expect(checkboxes[1]).toHaveAttribute("aria-checked", "false");
},
};
const InverseInteraction = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const checkboxes = canvas.getAllByRole("checkbox");
expect(checkboxes).toHaveLength(2);
expect(checkboxes[0]).toHaveAttribute("aria-checked", "false");
await userEvent.click(checkboxes[0]);
expect(checkboxes[0]).toHaveAttribute("aria-checked", "true");
expect(checkboxes[1]).toHaveAttribute("aria-checked", "true");
await userEvent.click(checkboxes[1]);
expect(checkboxes[1]).toHaveAttribute("aria-checked", "false");
},
};
export default {
title: "Forms/Checkbox",
+49 -6
View File
@@ -1,11 +1,54 @@
import React from "react";
import RadioButton from "../app/components/RadioButton";
import {
DefaultInteraction,
CheckedInteraction,
StandardInteraction,
InverseInteraction,
} from "../tests/storybook/RadioButton.interactions.test";
import { expect } from "@storybook/test";
import { userEvent, within } from "@storybook/test";
// Interaction functions for Storybook play functions
const DefaultInteraction = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const radioButton = canvas.getByRole("radio");
await expect(radioButton).toHaveAttribute("aria-checked", "false");
await userEvent.click(radioButton);
await expect(radioButton).toHaveAttribute("aria-checked", "true");
await userEvent.click(radioButton);
await expect(radioButton).toHaveAttribute("aria-checked", "true");
},
};
const CheckedInteraction = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const radioButton = canvas.getByRole("radio");
await expect(radioButton).toHaveAttribute("aria-checked", "true");
await userEvent.click(radioButton);
await expect(radioButton).toHaveAttribute("aria-checked", "true");
},
};
const StandardInteraction = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const radioButtons = canvas.getAllByRole("radio");
await expect(radioButtons[0]).toHaveAttribute("aria-checked", "false");
await expect(radioButtons[1]).toHaveAttribute("aria-checked", "true");
await userEvent.click(radioButtons[0]);
await expect(radioButtons[0]).toHaveAttribute("aria-checked", "true");
await expect(radioButtons[1]).toHaveAttribute("aria-checked", "false");
},
};
const InverseInteraction = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const radioButtons = canvas.getAllByRole("radio");
await expect(radioButtons[0]).toHaveAttribute("aria-checked", "false");
await expect(radioButtons[1]).toHaveAttribute("aria-checked", "true");
await userEvent.click(radioButtons[0]);
await expect(radioButtons[0]).toHaveAttribute("aria-checked", "true");
await expect(radioButtons[1]).toHaveAttribute("aria-checked", "false");
},
};
const meta = {
title: "Forms/RadioButton",
+62 -6
View File
@@ -1,11 +1,67 @@
import React from "react";
import RadioGroup from "../app/components/RadioGroup";
import {
DefaultInteraction,
StandardInteraction,
InverseInteraction,
InteractiveInteraction,
} from "../tests/storybook/RadioGroup.interactions.test";
import { expect } from "@storybook/test";
import { userEvent, within } from "@storybook/test";
// Interaction functions for Storybook play functions
const DefaultInteraction = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const radioGroup = canvas.getByRole("radiogroup");
const radioButtons = canvas.getAllByRole("radio");
await expect(radioGroup).toBeInTheDocument();
await expect(radioButtons).toHaveLength(3);
await expect(radioButtons[0]).toHaveAttribute("aria-checked", "true");
await expect(radioButtons[1]).toHaveAttribute("aria-checked", "false");
await expect(radioButtons[2]).toHaveAttribute("aria-checked", "false");
},
};
const StandardInteraction = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const radioGroup = canvas.getByRole("radiogroup");
const radioButtons = canvas.getAllByRole("radio");
await expect(radioGroup).toBeInTheDocument();
await expect(radioButtons[0]).toHaveAttribute("aria-checked", "false");
await expect(radioButtons[1]).toHaveAttribute("aria-checked", "true");
await expect(radioButtons[2]).toHaveAttribute("aria-checked", "false");
await userEvent.click(radioButtons[0]);
await expect(radioButtons[0]).toHaveAttribute("aria-checked", "true");
await expect(radioButtons[1]).toHaveAttribute("aria-checked", "false");
await expect(radioButtons[2]).toHaveAttribute("aria-checked", "false");
},
};
const InverseInteraction = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const radioGroup = canvas.getByRole("radiogroup");
const radioButtons = canvas.getAllByRole("radio");
await expect(radioGroup).toBeInTheDocument();
await expect(radioButtons[0]).toHaveAttribute("aria-checked", "true");
await expect(radioButtons[1]).toHaveAttribute("aria-checked", "false");
await expect(radioButtons[2]).toHaveAttribute("aria-checked", "false");
await userEvent.click(radioButtons[1]);
await expect(radioButtons[0]).toHaveAttribute("aria-checked", "false");
await expect(radioButtons[1]).toHaveAttribute("aria-checked", "true");
await expect(radioButtons[2]).toHaveAttribute("aria-checked", "false");
},
};
const InteractiveInteraction = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const radioGroup = canvas.getByRole("radiogroup");
const radioButtons = canvas.getAllByRole("radio");
await expect(radioGroup).toBeInTheDocument();
await expect(canvas.getByText("Selected: option1")).toBeVisible();
await userEvent.click(radioButtons[1]);
await expect(canvas.getByText("Selected: option2")).toBeVisible();
await userEvent.click(radioButtons[2]);
await expect(canvas.getByText("Selected: option3")).toBeVisible();
},
};
const meta = {
title: "Forms/RadioGroup",
@@ -1,396 +0,0 @@
import React from "react";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { expect, describe, it, vi } from "vitest";
import { axe, toHaveNoViolations } from "jest-axe";
import ContextMenu from "../../app/components/ContextMenu";
import ContextMenuItem from "../../app/components/ContextMenuItem";
import ContextMenuSection from "../../app/components/ContextMenuSection";
import ContextMenuDivider from "../../app/components/ContextMenuDivider";
expect.extend(toHaveNoViolations);
describe("ContextMenu Components Accessibility", () => {
describe("ContextMenu Accessibility", () => {
it("has no accessibility violations", async () => {
const { container } = render(
<ContextMenu>
<ContextMenuItem onClick={vi.fn()}>Item 1</ContextMenuItem>
<ContextMenuItem onClick={vi.fn()}>Item 2</ContextMenuItem>
</ContextMenu>,
);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
it("has proper role and structure", () => {
render(
<ContextMenu>
<ContextMenuItem onClick={vi.fn()}>Item 1</ContextMenuItem>
<ContextMenuItem onClick={vi.fn()}>Item 2</ContextMenuItem>
</ContextMenu>,
);
const menu = screen.getByRole("menu");
expect(menu).toBeInTheDocument();
const items = screen.getAllByRole("menuitem");
expect(items).toHaveLength(2);
});
it("has proper focus management", async () => {
render(
<ContextMenu>
<ContextMenuItem onClick={vi.fn()}>Item 1</ContextMenuItem>
<ContextMenuItem onClick={vi.fn()}>Item 2</ContextMenuItem>
</ContextMenu>,
);
const firstItem = screen.getByRole("menuitem", { name: "Item 1" });
expect(firstItem).toHaveAttribute("tabIndex", "0");
expect(firstItem).toBeInTheDocument();
});
});
describe("ContextMenuItem Accessibility", () => {
it("has no accessibility violations", async () => {
const { container } = render(
<ContextMenu>
<ContextMenuItem onClick={vi.fn()}>Test Item</ContextMenuItem>
</ContextMenu>,
);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
it("has proper ARIA attributes", () => {
render(
<ContextMenu>
<ContextMenuItem onClick={vi.fn()}>Test Item</ContextMenuItem>
</ContextMenu>,
);
const item = screen.getByRole("menuitem");
expect(item).not.toHaveAttribute("aria-current");
});
it("updates aria-current when selected", () => {
render(
<ContextMenu>
<ContextMenuItem onClick={vi.fn()} selected={true}>
Test Item
</ContextMenuItem>
</ContextMenu>,
);
const item = screen.getByRole("menuitem");
expect(item).toHaveAttribute("aria-current", "true");
});
it("is keyboard accessible", async () => {
const user = userEvent.setup();
const onClick = vi.fn();
render(
<ContextMenu>
<ContextMenuItem onClick={onClick}>Test Item</ContextMenuItem>
</ContextMenu>,
);
const item = screen.getByRole("menuitem");
item.focus();
await user.keyboard("{Enter}");
expect(onClick).toHaveBeenCalled();
});
it("is accessible with Space key", async () => {
const user = userEvent.setup();
const onClick = vi.fn();
render(
<ContextMenu>
<ContextMenuItem onClick={onClick}>Test Item</ContextMenuItem>
</ContextMenu>,
);
const item = screen.getByRole("menuitem");
item.focus();
await user.keyboard(" ");
expect(onClick).toHaveBeenCalled();
});
it("has proper focus indicators", () => {
render(
<ContextMenu>
<ContextMenuItem onClick={vi.fn()}>Test Item</ContextMenuItem>
</ContextMenu>,
);
const item = screen.getByRole("menuitem");
expect(item).toHaveClass(
"hover:!bg-[var(--color-surface-default-secondary)]",
);
});
it("announces selection state to screen readers", () => {
render(
<ContextMenu>
<ContextMenuItem onClick={vi.fn()} selected={true}>
Test Item
</ContextMenuItem>
</ContextMenu>,
);
const item = screen.getByRole("menuitem");
expect(item).toHaveAttribute("aria-current", "true");
});
});
describe("ContextMenuSection Accessibility", () => {
it("has no accessibility violations", async () => {
const { container } = render(
<ContextMenu>
<ContextMenuSection title="Test Section">
<ContextMenuItem onClick={vi.fn()}>Item 1</ContextMenuItem>
</ContextMenuSection>
</ContextMenu>,
);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
it("has proper heading structure", () => {
render(
<ContextMenu>
<ContextMenuSection title="Test Section">
<ContextMenuItem onClick={vi.fn()}>Item 1</ContextMenuItem>
</ContextMenuSection>
</ContextMenu>,
);
const title = screen.getByText("Test Section");
expect(title).toBeInTheDocument();
});
it("has sufficient color contrast for section title", () => {
render(
<ContextMenu>
<ContextMenuSection title="Test Section">
<ContextMenuItem onClick={vi.fn()}>Item 1</ContextMenuItem>
</ContextMenuSection>
</ContextMenu>,
);
const title = screen.getByText("Test Section");
expect(title).toHaveClass("text-[var(--color-content-default-primary)]");
});
});
describe("ContextMenuDivider Accessibility", () => {
it("has no accessibility violations", async () => {
const { container } = render(<ContextMenuDivider />);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
it("has proper semantic structure", () => {
render(<ContextMenuDivider />);
const divider = screen.getByRole("separator");
expect(divider).toBeInTheDocument();
});
it("has sufficient visual contrast", () => {
render(<ContextMenuDivider />);
const divider = screen.getByRole("separator");
expect(divider).toHaveClass(
"border-[var(--color-border-default-tertiary)]",
);
});
});
describe("Integrated Menu Accessibility", () => {
const TestMenu = () => (
<ContextMenu>
<ContextMenuSection title="First Section">
<ContextMenuItem onClick={vi.fn()}>Item 1</ContextMenuItem>
<ContextMenuItem onClick={vi.fn()} selected={true}>
Item 2
</ContextMenuItem>
</ContextMenuSection>
<ContextMenuDivider />
<ContextMenuSection title="Second Section">
<ContextMenuItem onClick={vi.fn()} hasSubmenu={true}>
Item 3
</ContextMenuItem>
</ContextMenuSection>
</ContextMenu>
);
it("has no accessibility violations when integrated", async () => {
const { container } = render(<TestMenu />);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
it("has proper menu structure", () => {
render(<TestMenu />);
const menu = screen.getByRole("menu");
expect(menu).toBeInTheDocument();
const items = screen.getAllByRole("menuitem");
expect(items).toHaveLength(3);
expect(screen.getByText("First Section")).toBeInTheDocument();
expect(screen.getByText("Second Section")).toBeInTheDocument();
});
it("maintains proper focus order", async () => {
render(<TestMenu />);
const items = screen.getAllByRole("menuitem");
expect(items).toHaveLength(3);
// Check that all items are focusable
items.forEach((item) => {
expect(item).toHaveAttribute("tabIndex", "0");
});
});
it("handles keyboard navigation correctly", async () => {
const user = userEvent.setup();
const onClick = vi.fn();
render(
<ContextMenu>
<ContextMenuItem onClick={onClick}>Item 1</ContextMenuItem>
<ContextMenuItem onClick={vi.fn()}>Item 2</ContextMenuItem>
</ContextMenu>,
);
const items = screen.getAllByRole("menuitem");
items[0].focus();
await user.keyboard("{Enter}");
expect(onClick).toHaveBeenCalled();
});
});
describe("Color Contrast", () => {
it("has sufficient contrast for menu items", () => {
render(
<ContextMenu>
<ContextMenuItem onClick={vi.fn()}>Test Item</ContextMenuItem>
</ContextMenu>,
);
const item = screen.getByRole("menuitem");
expect(item).toHaveClass(
"text-[var(--color-content-default-brand-primary)]",
);
});
it("has sufficient contrast for section titles", () => {
render(
<ContextMenu>
<ContextMenuSection title="Test Section" />
</ContextMenu>,
);
const title = screen.getByText("Test Section");
expect(title).toHaveClass("text-[var(--color-content-default-primary)]");
});
it("has sufficient contrast for dividers", () => {
render(
<ContextMenu>
<ContextMenuDivider />
</ContextMenu>,
);
const divider = screen.getByRole("separator");
expect(divider).toHaveClass(
"border-[var(--color-border-default-tertiary)]",
);
});
});
describe("Screen Reader Support", () => {
it("announces menu structure correctly", () => {
render(
<ContextMenu>
<ContextMenuSection title="Test Section">
<ContextMenuItem onClick={vi.fn()}>Item 1</ContextMenuItem>
<ContextMenuItem onClick={vi.fn()} selected={true}>
Item 2
</ContextMenuItem>
</ContextMenuSection>
</ContextMenu>,
);
const menu = screen.getByRole("menu");
expect(menu).toBeInTheDocument();
const items = screen.getAllByRole("menuitem");
expect(items[0]).not.toHaveAttribute("aria-current");
expect(items[1]).toHaveAttribute("aria-current", "true");
});
it("announces selection state changes", async () => {
const { rerender } = render(
<ContextMenuItem onClick={vi.fn()} selected={false}>
Test Item
</ContextMenuItem>,
);
const item = screen.getByRole("menuitem");
expect(item).not.toHaveAttribute("aria-current");
rerender(
<ContextMenuItem onClick={vi.fn()} selected={true}>
Test Item
</ContextMenuItem>,
);
expect(item).toHaveAttribute("aria-current", "true");
});
});
describe("WCAG Compliance", () => {
it("meets WCAG 2.1 AA standards", async () => {
const { container } = render(
<ContextMenu>
<ContextMenuSection title="Test Section">
<ContextMenuItem onClick={vi.fn()}>Item 1</ContextMenuItem>
<ContextMenuItem onClick={vi.fn()} selected={true}>
Item 2
</ContextMenuItem>
</ContextMenuSection>
<ContextMenuDivider />
<ContextMenuItem onClick={vi.fn()}>Item 3</ContextMenuItem>
</ContextMenu>,
);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
it("meets WCAG standards in all states", async () => {
const { container } = render(
<ContextMenu>
<ContextMenuItem onClick={vi.fn()} selected={true}>
Selected Item
</ContextMenuItem>
<ContextMenuItem onClick={vi.fn()} hasSubmenu={true}>
Submenu Item
</ContextMenuItem>
<ContextMenuItem onClick={vi.fn()} disabled={true}>
Disabled Item
</ContextMenuItem>
</ContextMenu>,
);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
});
});
-286
View File
@@ -1,286 +0,0 @@
import React from "react";
import { render, screen, fireEvent } from "@testing-library/react";
import { expect, test, describe, vi } from "vitest";
import { axe, toHaveNoViolations } from "jest-axe";
import Input from "../../app/components/Input";
expect.extend(toHaveNoViolations);
describe("Input Component Accessibility", () => {
test("has no accessibility violations", async () => {
const { container } = render(<Input label="Test input" />);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
test("has no accessibility violations when disabled", async () => {
const { container } = render(<Input label="Test input" disabled={true} />);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
test("has no accessibility violations when in error state", async () => {
const { container } = render(<Input label="Test input" error={true} />);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
test("has no accessibility violations with horizontal label", async () => {
const { container } = render(
<Input label="Test input" labelVariant="horizontal" />,
);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
test("associates label with input correctly", () => {
render(<Input label="Test input" />);
const input = screen.getByLabelText("Test input");
expect(input).toBeInTheDocument();
expect(input).toHaveAttribute("type", "text");
});
test("maintains label association with custom ID", () => {
render(<Input id="custom-input" label="Test input" />);
const input = screen.getByLabelText("Test input");
expect(input).toHaveAttribute("id", "custom-input");
});
test("supports keyboard navigation", () => {
render(<Input label="Test input" />);
const input = screen.getByRole("textbox");
// Input should be focusable
input.focus();
expect(input).toHaveFocus();
});
test("supports keyboard activation", () => {
const handleChange = vi.fn();
render(<Input label="Test input" onChange={handleChange} />);
const input = screen.getByRole("textbox");
// Type in the input
fireEvent.change(input, { target: { value: "test" } });
expect(handleChange).toHaveBeenCalled();
});
test("supports Enter key activation", () => {
const handleChange = vi.fn();
render(<Input label="Test input" onChange={handleChange} />);
const input = screen.getByRole("textbox");
// Focus the input first
input.focus();
expect(input).toHaveFocus();
fireEvent.keyDown(input, { key: "Enter" });
// Input should still be focused and ready for typing
expect(input).toHaveFocus();
});
test("supports Space key activation", () => {
const handleChange = vi.fn();
render(<Input label="Test input" onChange={handleChange} />);
const input = screen.getByRole("textbox");
// Focus the input first
input.focus();
expect(input).toHaveFocus();
fireEvent.keyDown(input, { key: " " });
// Input should still be focused and ready for typing
expect(input).toHaveFocus();
});
test("supports Tab navigation", () => {
render(
<div>
<Input label="First input" />
<Input label="Second input" />
</div>,
);
const firstInput = screen.getByLabelText("First input");
const secondInput = screen.getByLabelText("Second input");
firstInput.focus();
expect(firstInput).toHaveFocus();
// Use userEvent for more realistic tab navigation
fireEvent.keyDown(firstInput, { key: "Tab", code: "Tab" });
// Note: In a real browser, Tab would move focus, but in tests we need to simulate it
secondInput.focus();
expect(secondInput).toHaveFocus();
});
test("supports Shift+Tab navigation", () => {
render(
<div>
<Input label="First input" />
<Input label="Second input" />
</div>,
);
const firstInput = screen.getByLabelText("First input");
const secondInput = screen.getByLabelText("Second input");
secondInput.focus();
expect(secondInput).toHaveFocus();
// Use userEvent for more realistic tab navigation
fireEvent.keyDown(secondInput, { key: "Tab", shiftKey: true, code: "Tab" });
// Note: In a real browser, Shift+Tab would move focus, but in tests we need to simulate it
firstInput.focus();
expect(firstInput).toHaveFocus();
});
test("handles disabled state accessibility", () => {
render(<Input label="Test input" disabled={true} />);
const input = screen.getByRole("textbox");
expect(input).toBeDisabled();
expect(input).toHaveAttribute("disabled");
});
test("handles error state accessibility", () => {
render(<Input label="Test input" error={true} />);
const input = screen.getByRole("textbox");
// Error state should still be accessible
expect(input).toBeInTheDocument();
expect(input).not.toBeDisabled();
});
test("supports different input types", () => {
const { rerender } = render(<Input type="email" label="Email" />);
let input = screen.getByRole("textbox");
expect(input).toHaveAttribute("type", "email");
rerender(<Input type="password" label="Password" />);
// Password inputs don't have textbox role, they have textbox role only for text inputs
input = screen.getByLabelText("Password");
expect(input).toHaveAttribute("type", "password");
rerender(<Input type="number" label="Number" />);
input = screen.getByRole("spinbutton");
expect(input).toHaveAttribute("type", "number");
});
test("supports placeholder accessibility", () => {
render(<Input placeholder="Enter your name" />);
const input = screen.getByPlaceholderText("Enter your name");
expect(input).toBeInTheDocument();
});
test("supports value accessibility", () => {
render(<Input value="test value" />);
const input = screen.getByDisplayValue("test value");
expect(input).toBeInTheDocument();
});
test("maintains focus management", () => {
const handleFocus = vi.fn();
const handleBlur = vi.fn();
render(
<Input label="Test input" onFocus={handleFocus} onBlur={handleBlur} />,
);
const input = screen.getByRole("textbox");
fireEvent.focus(input);
expect(handleFocus).toHaveBeenCalled();
// Focus the input to ensure it has focus
input.focus();
expect(input).toHaveFocus();
fireEvent.blur(input);
expect(handleBlur).toHaveBeenCalled();
// Manually blur the input to ensure it loses focus
input.blur();
expect(input).not.toHaveFocus();
});
test("supports form association", () => {
render(
<form>
<Input name="test-field" label="Test input" />
</form>,
);
const input = screen.getByRole("textbox");
expect(input).toHaveAttribute("name", "test-field");
});
test("supports ARIA attributes", () => {
render(
<Input
label="Test input"
aria-describedby="help-text"
aria-required="true"
/>,
);
const input = screen.getByRole("textbox");
expect(input).toHaveAttribute("aria-describedby", "help-text");
expect(input).toHaveAttribute("aria-required", "true");
});
test("supports custom ARIA labels", () => {
render(<Input aria-label="Custom input label" />);
const input = screen.getByLabelText("Custom input label");
expect(input).toBeInTheDocument();
});
test("handles multiple inputs without conflicts", () => {
render(
<div>
<Input label="First input" />
<Input label="Second input" />
<Input label="Third input" />
</div>,
);
const firstInput = screen.getByLabelText("First input");
const secondInput = screen.getByLabelText("Second input");
const thirdInput = screen.getByLabelText("Third input");
expect(firstInput).toBeInTheDocument();
expect(secondInput).toBeInTheDocument();
expect(thirdInput).toBeInTheDocument();
// Each should have unique IDs
expect(firstInput.id).not.toBe(secondInput.id);
expect(secondInput.id).not.toBe(thirdInput.id);
expect(firstInput.id).not.toBe(thirdInput.id);
});
test("supports screen reader navigation", () => {
render(<Input label="Test input" />);
const input = screen.getByRole("textbox");
const label = screen.getByText("Test input");
// Label should be associated with input
expect(label).toHaveAttribute("for", input.id);
});
test("handles dynamic label changes", () => {
const { rerender } = render(<Input label="Original label" />);
expect(screen.getByText("Original label")).toBeInTheDocument();
rerender(<Input label="Updated label" />);
expect(screen.getByText("Updated label")).toBeInTheDocument();
expect(screen.queryByText("Original label")).not.toBeInTheDocument();
});
test("supports controlled input behavior", () => {
const handleChange = vi.fn();
render(<Input value="controlled value" onChange={handleChange} />);
const input = screen.getByDisplayValue("controlled value");
fireEvent.change(input, { target: { value: "new value" } });
expect(handleChange).toHaveBeenCalled();
});
});
-306
View File
@@ -1,306 +0,0 @@
import React from "react";
import { render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { expect, describe, it, vi } from "vitest";
import { axe, toHaveNoViolations } from "jest-axe";
import Select from "../../app/components/Select";
expect.extend(toHaveNoViolations);
describe("Select Component Accessibility", () => {
const defaultProps = {
label: "Test Select",
placeholder: "Select an option",
options: [
{ value: "option1", label: "Option 1" },
{ value: "option2", label: "Option 2" },
{ value: "option3", label: "Option 3" },
],
};
describe("ARIA Attributes", () => {
it("has correct initial ARIA attributes", () => {
render(<Select {...defaultProps} />);
const selectButton = screen.getByRole("button");
expect(selectButton).toHaveAttribute("aria-expanded", "false");
expect(selectButton).toHaveAttribute("aria-haspopup", "listbox");
});
it("updates aria-expanded when dropdown opens", async () => {
const user = userEvent.setup();
render(<Select {...defaultProps} />);
const selectButton = screen.getByRole("button");
await user.click(selectButton);
await waitFor(() => {
expect(selectButton).toHaveAttribute("aria-expanded", "true");
});
});
it("has proper role for dropdown menu", async () => {
const user = userEvent.setup();
render(<Select {...defaultProps} />);
const selectButton = screen.getByRole("button");
await user.click(selectButton);
await waitFor(() => {
const listbox = screen.getByRole("listbox");
expect(listbox).toBeInTheDocument();
});
});
it("has proper role for menu items", async () => {
const user = userEvent.setup();
render(<Select {...defaultProps} />);
const selectButton = screen.getByRole("button");
await user.click(selectButton);
await waitFor(() => {
const options = screen.getAllByRole("option");
expect(options).toHaveLength(3);
expect(options[0]).toHaveTextContent("Option 1");
expect(options[1]).toHaveTextContent("Option 2");
expect(options[2]).toHaveTextContent("Option 3");
});
});
});
describe("Keyboard Navigation", () => {
it("opens dropdown with Enter key", async () => {
const user = userEvent.setup();
render(<Select {...defaultProps} />);
const selectButton = screen.getByRole("button");
selectButton.focus();
await user.keyboard("{Enter}");
await waitFor(() => {
expect(screen.getByRole("listbox")).toBeInTheDocument();
});
});
it("opens dropdown with Space key", async () => {
const user = userEvent.setup();
render(<Select {...defaultProps} />);
const selectButton = screen.getByRole("button");
selectButton.focus();
await user.keyboard(" ");
await waitFor(() => {
expect(screen.getByRole("listbox")).toBeInTheDocument();
});
});
it("closes dropdown with Escape key", async () => {
const user = userEvent.setup();
render(<Select {...defaultProps} />);
const selectButton = screen.getByRole("button");
await user.click(selectButton);
await waitFor(() => {
expect(screen.getByRole("listbox")).toBeInTheDocument();
});
await user.keyboard("{Escape}");
await waitFor(() => {
expect(screen.queryByRole("menu")).not.toBeInTheDocument();
});
});
it("selects option with click", async () => {
const user = userEvent.setup();
const onChange = vi.fn();
render(<Select {...defaultProps} onChange={onChange} />);
const selectButton = screen.getByRole("button");
await user.click(selectButton);
await waitFor(() => {
expect(screen.getByRole("listbox")).toBeInTheDocument();
});
await user.click(screen.getByText("Option 1"));
expect(onChange).toHaveBeenCalledWith({
target: { value: "option1", text: "Option 1" },
});
});
});
describe("Screen Reader Support", () => {
it("announces selected option", async () => {
render(<Select {...defaultProps} value="option2" />);
const selectButton = screen.getByRole("button");
expect(selectButton).toHaveTextContent("Option 2");
});
it("announces placeholder when no option selected", () => {
render(<Select {...defaultProps} />);
const selectButton = screen.getByRole("button");
expect(selectButton).toHaveTextContent("Select an option");
});
it("has accessible name from label", () => {
render(<Select {...defaultProps} />);
const selectButton = screen.getByRole("button");
expect(selectButton).toHaveAccessibleName("Test Select");
});
});
describe("Focus Management", () => {
it("maintains focus on select button when dropdown opens", async () => {
const user = userEvent.setup();
render(<Select {...defaultProps} />);
const selectButton = screen.getByRole("button");
await user.click(selectButton);
await waitFor(() => {
expect(selectButton).toHaveFocus();
});
});
it("returns focus to select button after selection", async () => {
const user = userEvent.setup();
render(<Select {...defaultProps} />);
const selectButton = screen.getByRole("button");
await user.click(selectButton);
await waitFor(() => {
expect(screen.getByRole("listbox")).toBeInTheDocument();
});
await user.click(screen.getByText("Option 1"));
await waitFor(() => {
expect(selectButton).toHaveFocus();
});
});
});
describe("Disabled State", () => {
it("is not focusable when disabled", async () => {
const user = userEvent.setup();
render(<Select {...defaultProps} disabled={true} />);
const selectButton = screen.getByRole("button");
expect(selectButton).toBeDisabled();
await user.tab();
expect(selectButton).not.toHaveFocus();
});
it("has correct ARIA attributes when disabled", () => {
render(<Select {...defaultProps} disabled={true} />);
const selectButton = screen.getByRole("button");
expect(selectButton).toBeDisabled();
});
});
describe("Error State", () => {
it("announces error state to screen readers", () => {
render(<Select {...defaultProps} error={true} />);
const selectButton = screen.getByRole("button");
expect(selectButton).toHaveClass(
"border-[var(--color-border-default-utility-negative)]",
);
});
});
describe("WCAG Compliance", () => {
it("meets WCAG 2.1 AA standards", async () => {
const { container } = render(<Select {...defaultProps} />);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
it("meets WCAG standards in disabled state", async () => {
const { container } = render(
<Select {...defaultProps} disabled={true} />,
);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
it("meets WCAG standards in error state", async () => {
const { container } = render(<Select {...defaultProps} error={true} />);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
it("meets WCAG standards when dropdown is open", async () => {
const user = userEvent.setup();
const { container } = render(<Select {...defaultProps} />);
const selectButton = screen.getByRole("button");
await user.click(selectButton);
await waitFor(() => {
expect(screen.getByRole("listbox")).toBeInTheDocument();
});
const results = await axe(container);
expect(results).toHaveNoViolations();
});
});
describe("Color Contrast", () => {
it("has sufficient color contrast for text", () => {
render(<Select {...defaultProps} />);
const selectButton = screen.getByRole("button");
expect(selectButton).toHaveClass(
"text-[var(--color-content-default-primary)]",
);
});
it("has sufficient color contrast for labels", () => {
render(<Select {...defaultProps} />);
const label = screen.getByText("Test Select");
expect(label).toHaveClass(
"text-[var(--color-content-default-secondary)]",
);
});
});
describe("Focus Indicators", () => {
it("has visible focus indicator", () => {
render(<Select {...defaultProps} />);
const selectButton = screen.getByRole("button");
expect(selectButton).toHaveClass(
"focus-visible:border-[var(--color-border-default-utility-info)]",
);
expect(selectButton).toHaveClass(
"focus-visible:shadow-[0_0_5px_3px_#3281F8]",
);
});
it("distinguishes between focus and hover states", () => {
render(<Select {...defaultProps} />);
const selectButton = screen.getByRole("button");
// Focus state should be different from hover state
expect(selectButton).toHaveClass(
"focus-visible:border-[var(--color-border-default-utility-info)]",
);
expect(selectButton).toHaveClass(
"hover:shadow-[0_0_0_2px_var(--color-border-default-tertiary)]",
);
});
});
});
-98
View File
@@ -1,98 +0,0 @@
import React from "react";
import { render, screen, fireEvent } from "@testing-library/react";
import { describe, it, expect, vi } from "vitest";
import { axe, toHaveNoViolations } from "jest-axe";
import Switch from "../../app/components/Switch";
expect.extend(toHaveNoViolations);
describe("Switch Accessibility", () => {
it("has proper ARIA attributes", () => {
render(<Switch checked={false} label="Test Switch" />);
const switchButton = screen.getByRole("switch");
expect(switchButton).toHaveAttribute("role", "switch");
expect(switchButton).toHaveAttribute("aria-checked", "false");
expect(switchButton).toHaveAttribute("aria-label", "Test Switch");
});
it("has proper ARIA attributes when checked", () => {
render(<Switch checked={true} label="Test Switch" />);
const switchButton = screen.getByRole("switch");
expect(switchButton).toHaveAttribute("aria-checked", "true");
});
it("has proper ARIA attributes when focused", () => {
render(<Switch state="focus" label="Test Switch" />);
const switchButton = screen.getByRole("switch");
expect(switchButton).toHaveAttribute("aria-checked", "false");
expect(switchButton).toHaveClass("shadow-[0_0_5px_3px_#3281F8]");
expect(switchButton).toHaveClass("rounded-full");
expect(switchButton).toHaveAttribute("aria-label", "Test Switch");
});
it("handles keyboard navigation", () => {
const handleChange = vi.fn();
render(<Switch onChange={handleChange} label="Test Switch" />);
const switchButton = screen.getByRole("switch");
// Test Enter key
fireEvent.keyDown(switchButton, { key: "Enter" });
expect(handleChange).toHaveBeenCalledTimes(1);
// Test Space key
fireEvent.keyDown(switchButton, { key: " " });
expect(handleChange).toHaveBeenCalledTimes(2);
});
it("handles focus state accessibility", () => {
const handleFocus = vi.fn();
render(<Switch onFocus={handleFocus} label="Test Switch" />);
const switchButton = screen.getByRole("switch");
fireEvent.focus(switchButton);
expect(handleFocus).toHaveBeenCalledTimes(1);
});
it("handles checked state accessibility", () => {
const { rerender } = render(<Switch checked={false} label="Test Switch" />);
let switchButton = screen.getByRole("switch");
expect(switchButton).toHaveAttribute("aria-checked", "false");
rerender(<Switch checked={true} label="Test Switch" />);
switchButton = screen.getByRole("switch");
expect(switchButton).toHaveAttribute("aria-checked", "true");
});
it("has no accessibility violations", async () => {
const { container } = render(<Switch label="Test Switch" />);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
it("has no accessibility violations when checked", async () => {
const { container } = render(<Switch checked={true} label="Test Switch" />);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
it("has no accessibility violations when focused", async () => {
const { container } = render(<Switch state="focus" label="Test Switch" />);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
it("has no accessibility violations with text", async () => {
const { container } = render(<Switch label="Enable notifications" />);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
it("has no accessibility violations without text", async () => {
const { container } = render(<Switch />);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
});
-121
View File
@@ -1,121 +0,0 @@
import { expect, test, describe, vi } from "vitest";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { axe, toHaveNoViolations } from "jest-axe";
import TextArea from "../../app/components/TextArea";
expect.extend(toHaveNoViolations);
describe("TextArea Accessibility", () => {
test("renders without accessibility violations", async () => {
const { container } = render(<TextArea label="Test TextArea" />);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
test("has proper label association", () => {
render(<TextArea label="Test Label" />);
const textarea = screen.getByRole("textbox");
const label = screen.getByText("Test Label");
expect(textarea).toHaveAttribute("id");
expect(label).toHaveAttribute("for", textarea.id);
});
test("has proper ARIA attributes", () => {
render(<TextArea label="Test Label" name="test-textarea" />);
const textarea = screen.getByRole("textbox");
expect(textarea).toHaveAttribute("id");
expect(textarea).toHaveAttribute("name", "test-textarea");
});
test("supports keyboard navigation", async () => {
const user = userEvent.setup();
render(<TextArea label="Test Label" />);
const textarea = screen.getByRole("textbox");
await user.tab();
expect(textarea).toHaveFocus();
});
test("announces changes to screen readers", async () => {
const user = userEvent.setup();
const handleChange = vi.fn();
render(<TextArea label="Test Label" onChange={handleChange} />);
const textarea = screen.getByRole("textbox");
await user.type(textarea, "test");
expect(textarea).toHaveValue("test");
});
test("handles disabled state accessibility", () => {
render(<TextArea label="Test Label" disabled />);
const textarea = screen.getByRole("textbox");
expect(textarea).toBeDisabled();
expect(textarea).toHaveAttribute("aria-disabled", "true");
});
test("handles error state accessibility", () => {
render(<TextArea label="Test Label" error />);
const textarea = screen.getByRole("textbox");
expect(textarea).toHaveAttribute("aria-invalid", "true");
});
test("maintains focus management", async () => {
const user = userEvent.setup();
render(<TextArea label="Test Label" />);
const textarea = screen.getByRole("textbox");
await user.click(textarea);
expect(textarea).toHaveFocus();
});
test("supports horizontal label layout", () => {
render(<TextArea labelVariant="horizontal" label="Horizontal Label" />);
const textarea = screen.getByRole("textbox");
const label = screen.getByText("Horizontal Label");
expect(textarea).toBeInTheDocument();
expect(label).toBeInTheDocument();
});
test("handles different sizes accessibility", () => {
const { rerender } = render(<TextArea size="small" label="Small" />);
let textarea = screen.getByRole("textbox");
expect(textarea).toBeInTheDocument();
rerender(<TextArea size="medium" label="Medium" />);
textarea = screen.getByRole("textbox");
expect(textarea).toBeInTheDocument();
rerender(<TextArea size="large" label="Large" />);
textarea = screen.getByRole("textbox");
expect(textarea).toBeInTheDocument();
});
test("maintains proper contrast ratios", () => {
render(<TextArea label="Test Label" />);
const textarea = screen.getByRole("textbox");
const label = screen.getByText("Test Label");
expect(textarea).toHaveClass("text-[var(--color-content-default-primary)]");
expect(label).toHaveClass("text-[var(--color-content-default-secondary)]");
});
test("supports screen reader announcements for state changes", async () => {
const user = userEvent.setup();
render(<TextArea label="Test Label" />);
const textarea = screen.getByRole("textbox");
await user.click(textarea);
await user.type(textarea, "Hello");
expect(textarea).toHaveValue("Hello");
});
});
-112
View File
@@ -1,112 +0,0 @@
import { expect, test, describe, vi } from "vitest";
import { render, screen, fireEvent } from "@testing-library/react";
import { axe, toHaveNoViolations } from "jest-axe";
import Toggle from "../../app/components/Toggle";
expect.extend(toHaveNoViolations);
describe("Toggle Accessibility", () => {
test("has proper ARIA attributes", () => {
render(<Toggle label="Test Toggle" />);
const toggle = screen.getByRole("switch");
expect(toggle).toHaveAttribute("aria-checked", "false");
expect(toggle).toHaveAttribute("type", "button");
});
test("has proper ARIA attributes when checked", () => {
render(<Toggle label="Test Toggle" checked={true} />);
const toggle = screen.getByRole("switch");
expect(toggle).toHaveAttribute("aria-checked", "true");
});
test("has proper ARIA attributes when disabled", () => {
render(<Toggle label="Test Toggle" disabled={true} />);
const toggle = screen.getByRole("switch");
expect(toggle).toHaveAttribute("disabled");
});
test("has proper label association", () => {
render(<Toggle label="Test Toggle" />);
const toggle = screen.getByRole("switch");
const label = screen.getByText("Test Toggle");
expect(toggle).toBeInTheDocument();
expect(label).toBeInTheDocument();
});
test("handles keyboard navigation", () => {
const handleChange = vi.fn();
render(<Toggle label="Test Toggle" onChange={handleChange} />);
const toggle = screen.getByRole("switch");
toggle.focus();
expect(toggle).toHaveFocus();
fireEvent.keyDown(toggle, { key: "Enter" });
expect(handleChange).toHaveBeenCalledTimes(1);
fireEvent.keyDown(toggle, { key: " " });
expect(handleChange).toHaveBeenCalledTimes(2);
});
test("handles disabled state accessibility", () => {
const handleChange = vi.fn();
render(
<Toggle label="Test Toggle" disabled={true} onChange={handleChange} />,
);
const toggle = screen.getByRole("switch");
expect(toggle).toHaveAttribute("disabled");
expect(toggle).toHaveClass("cursor-not-allowed");
fireEvent.click(toggle);
expect(handleChange).not.toHaveBeenCalled();
});
test("handles focus state accessibility", () => {
render(<Toggle label="Test Toggle" />);
const toggle = screen.getByRole("switch");
expect(toggle).toHaveClass("focus-visible:shadow-[0_0_5px_1px_#3281F8]");
});
test("has no accessibility violations", async () => {
const { container } = render(<Toggle label="Test Toggle" />);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
test("has no accessibility violations when checked", async () => {
const { container } = render(<Toggle label="Test Toggle" checked={true} />);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
test("has no accessibility violations when disabled", async () => {
const { container } = render(
<Toggle label="Test Toggle" disabled={true} />,
);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
test("has no accessibility violations with icon", async () => {
const { container } = render(
<Toggle label="Test Toggle" showIcon={true} icon="I" />,
);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
test("has no accessibility violations with text", async () => {
const { container } = render(
<Toggle label="Test Toggle" showText={true} text="Toggle" />,
);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
});
@@ -1,92 +0,0 @@
import React from "react";
import { render, screen, fireEvent } from "@testing-library/react";
import { describe, it, expect, vi } from "vitest";
import { axe, toHaveNoViolations } from "jest-axe";
import ToggleGroup from "../../app/components/ToggleGroup";
expect.extend(toHaveNoViolations);
describe("ToggleGroup Accessibility", () => {
it("has proper ARIA attributes", () => {
render(<ToggleGroup>Toggle Item</ToggleGroup>);
const toggleGroup = screen.getByRole("button");
expect(toggleGroup).toHaveAttribute("type", "button");
expect(toggleGroup).toHaveAttribute("role", "button");
});
it("has proper ARIA attributes when focused", () => {
render(<ToggleGroup state="focus">Focused Toggle</ToggleGroup>);
const toggleGroup = screen.getByRole("button");
expect(toggleGroup).toHaveAttribute("type", "button");
expect(toggleGroup).toHaveAttribute("role", "button");
});
it("handles keyboard navigation", () => {
const handleChange = vi.fn();
render(<ToggleGroup onChange={handleChange}>Keyboard Toggle</ToggleGroup>);
const toggleGroup = screen.getByRole("button");
// Test Enter key
fireEvent.keyDown(toggleGroup, { key: "Enter" });
expect(handleChange).toHaveBeenCalledTimes(1);
// Test Space key
fireEvent.keyDown(toggleGroup, { key: " " });
expect(handleChange).toHaveBeenCalledTimes(2);
});
it("handles focus state accessibility", () => {
render(<ToggleGroup state="focus">Focus Toggle</ToggleGroup>);
const toggleGroup = screen.getByRole("button");
expect(toggleGroup).toHaveClass("shadow-[0_0_5px_1px_#3281F8]");
});
it("handles selected state accessibility", () => {
render(<ToggleGroup state="selected">Selected Toggle</ToggleGroup>);
const toggleGroup = screen.getByRole("button");
expect(toggleGroup).toHaveClass("bg-[var(--color-magenta-magenta100)]");
expect(toggleGroup).toHaveClass(
"shadow-[inset_0_0_0_1px_var(--color-border-default-secondary)]",
);
});
it("has no accessibility violations", async () => {
const { container } = render(<ToggleGroup>Accessible Toggle</ToggleGroup>);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
it("has no accessibility violations when focused", async () => {
const { container } = render(
<ToggleGroup state="focus">Focused Toggle</ToggleGroup>,
);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
it("has no accessibility violations when selected", async () => {
const { container } = render(
<ToggleGroup state="selected">Selected Toggle</ToggleGroup>,
);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
it("has no accessibility violations with text", async () => {
const { container } = render(
<ToggleGroup showText={true}>Text Toggle</ToggleGroup>,
);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
it("has no accessibility violations without text", async () => {
const { container } = render(
<ToggleGroup showText={false} ariaLabel="Icon Toggle">
Icon Toggle
</ToggleGroup>,
);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
});
@@ -1,158 +0,0 @@
import React from "react";
import { render, screen } from "@testing-library/react";
import { expect, test, describe } from "vitest";
import { axe, toHaveNoViolations } from "jest-axe";
import Checkbox from "../../../app/components/Checkbox";
expect.extend(toHaveNoViolations);
describe("Checkbox Accessibility", () => {
test("should not have accessibility violations when unchecked", async () => {
const { container } = render(<Checkbox label="Test checkbox" />);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
test("should not have accessibility violations when checked", async () => {
const { container } = render(
<Checkbox label="Test checkbox" checked={true} />,
);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
test("should not have accessibility violations when disabled", async () => {
const { container } = render(
<Checkbox label="Test checkbox" disabled={true} />,
);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
test("should not have accessibility violations in inverse mode", async () => {
const { container } = render(
<Checkbox label="Test checkbox" mode="inverse" />,
);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
test("should have proper ARIA attributes", () => {
render(<Checkbox label="Test checkbox" checked={true} />);
const checkbox = screen.getByRole("checkbox");
expect(checkbox).toHaveAttribute("role", "checkbox");
expect(checkbox).toHaveAttribute("aria-checked", "true");
expect(checkbox).toHaveAttribute("tabIndex", "0");
});
test("should have proper ARIA attributes when disabled", () => {
render(<Checkbox label="Test checkbox" disabled={true} />);
const checkbox = screen.getByRole("checkbox");
expect(checkbox).toHaveAttribute("role", "checkbox");
expect(checkbox).toHaveAttribute("aria-checked", "false");
expect(checkbox).toHaveAttribute("aria-disabled", "true");
expect(checkbox).toHaveAttribute("tabIndex", "-1");
});
test("should have proper ARIA attributes when checked", () => {
render(<Checkbox label="Test checkbox" checked={true} />);
const checkbox = screen.getByRole("checkbox");
expect(checkbox).toHaveAttribute("role", "checkbox");
expect(checkbox).toHaveAttribute("aria-checked", "true");
expect(checkbox).toHaveAttribute("tabIndex", "0");
});
test("should have proper ARIA attributes when unchecked", () => {
render(<Checkbox label="Test checkbox" checked={false} />);
const checkbox = screen.getByRole("checkbox");
expect(checkbox).toHaveAttribute("role", "checkbox");
expect(checkbox).toHaveAttribute("aria-checked", "false");
expect(checkbox).toHaveAttribute("tabIndex", "0");
});
test("should have proper ARIA attributes with custom aria-label", () => {
render(<Checkbox ariaLabel="Custom accessibility label" />);
const checkbox = screen.getByRole("checkbox");
expect(checkbox).toHaveAttribute(
"aria-label",
"Custom accessibility label",
);
});
test("should have proper focus management", () => {
const { rerender } = render(<Checkbox label="Test checkbox" />);
const checkbox = screen.getByRole("checkbox");
// Should be focusable when not disabled
expect(checkbox).toHaveAttribute("tabIndex", "0");
// Should not be focusable when disabled
rerender(<Checkbox label="Test checkbox disabled" disabled={true} />);
const disabledCheckbox = screen.getByRole("checkbox");
expect(disabledCheckbox).toHaveAttribute("tabIndex", "-1");
});
test("should have proper keyboard navigation", () => {
render(<Checkbox label="Test checkbox" />);
const checkbox = screen.getByRole("checkbox");
// Should be focusable
expect(checkbox).toHaveAttribute("tabIndex", "0");
// Should support keyboard interaction
expect(checkbox).toHaveAttribute("role", "checkbox");
});
test("should have proper semantic structure", () => {
render(<Checkbox label="Test checkbox" />);
// Should have a label element
const label = screen.getByText("Test checkbox").closest("label");
expect(label).toBeInTheDocument();
// Should have a checkbox role
const checkbox = screen.getByRole("checkbox");
expect(checkbox).toBeInTheDocument();
// Should be associated with the label
expect(label).toContainElement(checkbox);
});
test("should have proper color contrast", async () => {
const { container } = render(<Checkbox label="Test checkbox" />);
const results = await axe(container);
// Check for color contrast violations
const contrastViolations = results.violations.filter(
(violation) => violation.id === "color-contrast",
);
expect(contrastViolations).toHaveLength(0);
});
test("should have proper focus indicators", async () => {
const { container } = render(<Checkbox label="Test checkbox" />);
const results = await axe(container);
// Check for focus indicator violations
const focusViolations = results.violations.filter(
(violation) => violation.id === "focus-order-semantics",
);
expect(focusViolations).toHaveLength(0);
});
test("should have proper form integration", () => {
render(<Checkbox name="test-checkbox" value="test-value" checked={true} />);
// Should have hidden input for form submission
const hiddenInput = screen.getByDisplayValue("test-value");
expect(hiddenInput).toBeInTheDocument();
expect(hiddenInput).toHaveAttribute("type", "checkbox");
expect(hiddenInput).toHaveAttribute("name", "test-checkbox");
expect(hiddenInput).toBeChecked();
});
});
@@ -1,234 +0,0 @@
import React from "react";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, it, expect, vi } from "vitest";
import RadioButton from "../../../app/components/RadioButton";
describe("RadioButton Accessibility", () => {
it("has proper ARIA attributes", () => {
render(<RadioButton label="Test Radio" />);
const radioButton = screen.getByRole("radio");
expect(radioButton).toHaveAttribute("role", "radio");
expect(radioButton).toHaveAttribute("aria-checked", "false");
expect(radioButton).toHaveAttribute("tabIndex", "0");
});
it("updates aria-checked when checked state changes", () => {
const { rerender } = render(
<RadioButton checked={false} label="Test Radio" />,
);
let radioButton = screen.getByRole("radio");
expect(radioButton).toHaveAttribute("aria-checked", "false");
rerender(<RadioButton checked={true} label="Test Radio" />);
radioButton = screen.getByRole("radio");
expect(radioButton).toHaveAttribute("aria-checked", "true");
});
it("associates label with radio button", () => {
render(<RadioButton label="Accessible Radio" />);
const radioButton = screen.getByRole("radio");
const labelId = radioButton.getAttribute("aria-labelledby");
expect(labelId).toBeTruthy();
const labelElement = document.getElementById(labelId);
expect(labelElement).toHaveTextContent("Accessible Radio");
});
it("uses aria-label when provided", () => {
render(<RadioButton ariaLabel="Custom Aria Label" />);
const radioButton = screen.getByRole("radio");
expect(radioButton).toHaveAttribute("aria-label", "Custom Aria Label");
expect(radioButton).not.toHaveAttribute("aria-labelledby");
});
it("prioritizes aria-label over aria-labelledby", () => {
render(<RadioButton label="Visible Label" ariaLabel="Hidden Aria Label" />);
const radioButton = screen.getByRole("radio");
expect(radioButton).toHaveAttribute("aria-label", "Hidden Aria Label");
expect(radioButton).not.toHaveAttribute("aria-labelledby");
});
it("is keyboard accessible", async () => {
const user = userEvent.setup();
const handleChange = vi.fn();
render(<RadioButton onChange={handleChange} label="Keyboard Radio" />);
const radioButton = screen.getByRole("radio");
radioButton.focus();
expect(radioButton).toHaveFocus();
await user.keyboard(" ");
expect(handleChange).toHaveBeenCalled();
});
it("handles Enter key activation", async () => {
const user = userEvent.setup();
const handleChange = vi.fn();
render(<RadioButton onChange={handleChange} label="Enter Radio" />);
const radioButton = screen.getByRole("radio");
await user.click(radioButton); // Focus the element first
await user.keyboard("Enter");
expect(handleChange).toHaveBeenCalled();
});
it("handles Space key activation", async () => {
const user = userEvent.setup();
const handleChange = vi.fn();
render(<RadioButton onChange={handleChange} label="Space Radio" />);
const radioButton = screen.getByRole("radio");
radioButton.focus();
await user.keyboard(" ");
expect(handleChange).toHaveBeenCalled();
});
it("ignores other keys", async () => {
const user = userEvent.setup();
const handleChange = vi.fn();
render(<RadioButton onChange={handleChange} label="Other Keys Radio" />);
const radioButton = screen.getByRole("radio");
radioButton.focus();
await user.keyboard("a");
await user.keyboard("Tab");
await user.keyboard("Escape");
expect(handleChange).not.toHaveBeenCalled();
});
it("has proper tab order", () => {
render(
<div>
<RadioButton label="First Radio" />
<RadioButton label="Second Radio" />
<RadioButton label="Third Radio" />
</div>,
);
const radioButtons = screen.getAllByRole("radio");
radioButtons.forEach((button) => {
expect(button).toHaveAttribute("tabIndex", "0");
});
});
it("generates unique IDs for accessibility", () => {
render(
<div>
<RadioButton label="Radio 1" />
<RadioButton label="Radio 2" />
<RadioButton label="Radio 3" />
</div>,
);
const radioButtons = screen.getAllByRole("radio");
const ids = radioButtons.map((button) => button.id);
const uniqueIds = new Set(ids);
expect(uniqueIds.size).toBe(3);
expect(ids.every((id) => id.startsWith("radio-"))).toBe(true);
});
it("uses provided ID for accessibility", () => {
render(<RadioButton id="custom-radio-id" label="Custom ID Radio" />);
const radioButton = screen.getByRole("radio");
expect(radioButton).toHaveAttribute("id", "custom-radio-id");
});
it("has accessible name from label", () => {
render(<RadioButton label="Accessible Name Radio" />);
const radioButton = screen.getByRole("radio");
const accessibleName = radioButton.getAttribute("aria-labelledby");
const labelElement = document.getElementById(accessibleName);
expect(labelElement).toHaveTextContent("Accessible Name Radio");
});
it("has accessible name from aria-label", () => {
render(<RadioButton ariaLabel="Aria Label Name" />);
const radioButton = screen.getByRole("radio");
expect(radioButton).toHaveAttribute("aria-label", "Aria Label Name");
});
it("maintains focus management", async () => {
const handleChange = vi.fn();
const { rerender } = render(
<RadioButton
checked={false}
onChange={handleChange}
label="Focus Radio"
/>,
);
const radioButton = screen.getByRole("radio");
radioButton.focus();
expect(radioButton).toHaveFocus();
// Change checked state
rerender(
<RadioButton
checked={true}
onChange={handleChange}
label="Focus Radio"
/>,
);
// Should still be focusable
expect(radioButton).toHaveAttribute("tabIndex", "0");
});
it("has proper role and state", () => {
render(<RadioButton checked={true} label="State Radio" />);
const radioButton = screen.getByRole("radio");
expect(radioButton).toHaveAttribute("role", "radio");
expect(radioButton).toHaveAttribute("aria-checked", "true");
});
it("supports screen reader navigation", () => {
render(
<div>
<RadioButton label="First Option" />
<RadioButton label="Second Option" />
<RadioButton label="Third Option" />
</div>,
);
const radioButtons = screen.getAllByRole("radio");
// All should be in tab order
radioButtons.forEach((button) => {
expect(button).toHaveAttribute("tabIndex", "0");
expect(button).toHaveAttribute("role", "radio");
});
});
it("has proper form association", () => {
render(
<RadioButton name="test-radio" value="test-value" label="Form Radio" />,
);
const hiddenInput = screen.getByDisplayValue("test-value");
expect(hiddenInput).toHaveAttribute("type", "radio");
expect(hiddenInput).toHaveAttribute("name", "test-radio");
expect(hiddenInput).toHaveAttribute("value", "test-value");
});
});
@@ -1,316 +0,0 @@
import React from "react";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, it, expect, vi } from "vitest";
import RadioGroup from "../../../app/components/RadioGroup";
describe("RadioGroup Accessibility", () => {
const defaultOptions = [
{ value: "option1", label: "Option 1" },
{ value: "option2", label: "Option 2" },
{ value: "option3", label: "Option 3" },
];
it("has proper radiogroup role", () => {
render(<RadioGroup options={defaultOptions} />);
const radioGroup = screen.getByRole("radiogroup");
expect(radioGroup).toBeInTheDocument();
});
it("has proper ARIA attributes on radiogroup", () => {
render(
<RadioGroup options={defaultOptions} aria-label="Test Radio Group" />,
);
const radioGroup = screen.getByRole("radiogroup");
expect(radioGroup).toHaveAttribute("aria-label", "Test Radio Group");
});
it("has proper radio button roles", () => {
render(<RadioGroup options={defaultOptions} />);
const radioButtons = screen.getAllByRole("radio");
expect(radioButtons).toHaveLength(3);
radioButtons.forEach((button) => {
expect(button).toHaveAttribute("role", "radio");
expect(button).toHaveAttribute("aria-checked");
});
});
it("shows correct selection state", () => {
render(<RadioGroup options={defaultOptions} value="option2" />);
const radioButtons = screen.getAllByRole("radio");
expect(radioButtons[0]).toHaveAttribute("aria-checked", "false");
expect(radioButtons[1]).toHaveAttribute("aria-checked", "true");
expect(radioButtons[2]).toHaveAttribute("aria-checked", "false");
});
it("updates selection state correctly", () => {
const { rerender } = render(
<RadioGroup options={defaultOptions} value="option1" />,
);
let radioButtons = screen.getAllByRole("radio");
expect(radioButtons[0]).toHaveAttribute("aria-checked", "true");
rerender(<RadioGroup options={defaultOptions} value="option3" />);
radioButtons = screen.getAllByRole("radio");
expect(radioButtons[0]).toHaveAttribute("aria-checked", "false");
expect(radioButtons[2]).toHaveAttribute("aria-checked", "true");
});
it("associates labels with radio buttons", () => {
render(<RadioGroup options={defaultOptions} />);
const radioButtons = screen.getAllByRole("radio");
radioButtons.forEach((button, index) => {
const labelId = button.getAttribute("aria-labelledby");
expect(labelId).toBeTruthy();
const labelElement = document.getElementById(labelId);
expect(labelElement).toHaveTextContent(`Option ${index + 1}`);
});
});
it("uses aria-label when provided in options", () => {
const optionsWithAria = [
{ value: "option1", label: "Option 1", ariaLabel: "First Option" },
{ value: "option2", label: "Option 2", ariaLabel: "Second Option" },
];
render(<RadioGroup options={optionsWithAria} />);
const radioButtons = screen.getAllByRole("radio");
expect(radioButtons[0]).toHaveAttribute("aria-label", "First Option");
expect(radioButtons[1]).toHaveAttribute("aria-label", "Second Option");
});
it("is keyboard accessible", async () => {
const user = userEvent.setup();
const handleChange = vi.fn();
render(
<RadioGroup
options={defaultOptions}
value="option1"
onChange={handleChange}
/>,
);
const radioButtons = screen.getAllByRole("radio");
// Focus first radio button
radioButtons[0].focus();
expect(radioButtons[0]).toHaveFocus();
// Navigate to second option
radioButtons[1].focus();
expect(radioButtons[1]).toHaveFocus();
// Activate with Space
await user.keyboard(" ");
expect(handleChange).toHaveBeenCalledWith({ value: "option2" });
});
it("handles Enter key activation", async () => {
const user = userEvent.setup();
const handleChange = vi.fn();
render(
<RadioGroup
options={defaultOptions}
value="option1"
onChange={handleChange}
/>,
);
const radioButtons = screen.getAllByRole("radio");
await user.click(radioButtons[2]); // Focus the element first
await user.keyboard("Enter");
expect(handleChange).toHaveBeenCalledWith({ value: "option3" });
});
it("handles Space key activation", async () => {
const user = userEvent.setup();
const handleChange = vi.fn();
render(
<RadioGroup
options={defaultOptions}
value="option1"
onChange={handleChange}
/>,
);
const radioButtons = screen.getAllByRole("radio");
radioButtons[1].focus();
await user.keyboard(" ");
expect(handleChange).toHaveBeenCalledWith({ value: "option2" });
});
it("ignores other keys", async () => {
const user = userEvent.setup();
const handleChange = vi.fn();
render(
<RadioGroup
options={defaultOptions}
value="option1"
onChange={handleChange}
/>,
);
const radioButtons = screen.getAllByRole("radio");
radioButtons[1].focus();
await user.keyboard("a");
await user.keyboard("Tab");
await user.keyboard("Escape");
expect(handleChange).not.toHaveBeenCalled();
});
it("has proper tab order", () => {
render(<RadioGroup options={defaultOptions} />);
const radioButtons = screen.getAllByRole("radio");
radioButtons.forEach((button) => {
expect(button).toHaveAttribute("tabIndex", "0");
});
});
it("generates unique IDs for accessibility", () => {
render(
<div>
<RadioGroup options={defaultOptions} />
<RadioGroup options={defaultOptions} />
</div>,
);
const radioButtons = screen.getAllByRole("radio");
const ids = radioButtons.map((button) => button.id);
const uniqueIds = new Set(ids);
// Should have unique IDs
expect(uniqueIds.size).toBe(6);
});
it("uses provided name for form association", () => {
render(<RadioGroup options={defaultOptions} name="test-group" />);
const hiddenInputs = screen.getAllByDisplayValue("option1");
hiddenInputs.forEach((input) => {
expect(input).toHaveAttribute("name", "test-group");
});
});
it("has proper form association", () => {
render(
<RadioGroup options={defaultOptions} name="test-group" value="option2" />,
);
const hiddenInputs = screen.getAllByDisplayValue("option1");
expect(hiddenInputs[0]).toHaveAttribute("name", "test-group");
expect(hiddenInputs[0]).toHaveAttribute("value", "option1");
expect(hiddenInputs[0]).not.toBeChecked();
const option2Inputs = screen.getAllByDisplayValue("option2");
expect(option2Inputs[0]).toHaveAttribute("name", "test-group");
expect(option2Inputs[0]).toHaveAttribute("value", "option2");
expect(option2Inputs[0]).toBeChecked();
});
it("maintains focus management", async () => {
const handleChange = vi.fn();
const { rerender } = render(
<RadioGroup
options={defaultOptions}
value="option1"
onChange={handleChange}
/>,
);
const radioButtons = screen.getAllByRole("radio");
radioButtons[1].focus();
expect(radioButtons[1]).toHaveFocus();
// Change selection
rerender(
<RadioGroup
options={defaultOptions}
value="option2"
onChange={handleChange}
/>,
);
// Should still be focusable
expect(radioButtons[1]).toHaveAttribute("tabIndex", "0");
});
it("supports screen reader navigation", () => {
render(<RadioGroup options={defaultOptions} />);
const radioGroup = screen.getByRole("radiogroup");
const radioButtons = screen.getAllByRole("radio");
// RadioGroup should be present
expect(radioGroup).toBeInTheDocument();
// All radio buttons should be in tab order
radioButtons.forEach((button) => {
expect(button).toHaveAttribute("tabIndex", "0");
expect(button).toHaveAttribute("role", "radio");
});
});
it("handles empty options gracefully", () => {
render(<RadioGroup options={[]} />);
const radioGroup = screen.getByRole("radiogroup");
expect(radioGroup).toBeInTheDocument();
const radioButtons = screen.queryAllByRole("radio");
expect(radioButtons).toHaveLength(0);
});
it("has proper accessible names", () => {
render(<RadioGroup options={defaultOptions} />);
const radioButtons = screen.getAllByRole("radio");
radioButtons.forEach((button, index) => {
const labelId = button.getAttribute("aria-labelledby");
const labelElement = document.getElementById(labelId);
expect(labelElement).toHaveTextContent(`Option ${index + 1}`);
});
});
it("maintains single selection behavior", async () => {
const user = userEvent.setup();
const handleChange = vi.fn();
render(
<RadioGroup
options={defaultOptions}
value="option1"
onChange={handleChange}
/>,
);
const radioButtons = screen.getAllByRole("radio");
// Click option 2 directly
await user.click(radioButtons[1]);
expect(handleChange).toHaveBeenCalledWith({ value: "option2" });
// Only one should be selected at a time
expect(handleChange).toHaveBeenCalledTimes(2);
});
});
@@ -1,217 +0,0 @@
import { describe, test, expect, beforeEach } from "vitest";
import { render, screen } from "@testing-library/react";
import { axe, toHaveNoViolations } from "jest-axe";
import Header from "../../../app/components/Header.js";
import Footer from "../../../app/components/Footer.js";
// Extend expect to include accessibility matchers
expect.extend(toHaveNoViolations);
describe("Accessibility - Component Level", () => {
beforeEach(() => {
document.body.innerHTML = "";
// Set up proper language attribute for accessibility testing
document.documentElement.setAttribute("lang", "en");
});
test("Header component has no accessibility violations", async () => {
const { container } = render(<Header />);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
test("Footer component has no accessibility violations", async () => {
const { container } = render(<Footer />);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
test("Header has proper semantic structure", () => {
render(<Header />);
// Check for banner landmark
const banner = screen.getByRole("banner");
expect(banner).toBeInTheDocument();
// Check for navigation landmark
const navigation = screen.getByRole("navigation");
expect(navigation).toBeInTheDocument();
// Check for proper heading structure (optional for header components)
try {
screen.getAllByRole("heading");
// Headings are not required in header components, so this is optional
} catch {
// No headings found, which is fine for a header component
}
});
test("Header navigation items are accessible", () => {
render(<Header />);
// Check that navigation items have proper roles
const navigationItems = screen.getAllByRole("menuitem");
expect(navigationItems.length).toBeGreaterThan(0);
// Check that each navigation item has accessible text or aria-label
navigationItems.forEach((item) => {
const hasAccessibleText =
item.textContent?.trim() || item.getAttribute("aria-label");
expect(hasAccessibleText).toBeTruthy();
});
});
test("Header buttons have accessible names", () => {
render(<Header />);
const buttons = screen.getAllByRole("button");
buttons.forEach((button) => {
// Check for aria-label, aria-labelledby, or text content
const hasAccessibleName =
button.getAttribute("aria-label") ||
button.getAttribute("aria-labelledby") ||
button.textContent?.trim();
expect(hasAccessibleName).toBeTruthy();
});
});
test("Header images have alt text", () => {
render(<Header />);
const images = screen.getAllByRole("img");
images.forEach((image) => {
const altText = image.getAttribute("alt");
// Alt text should exist (can be empty for decorative images)
expect(altText).not.toBeNull();
});
});
test("Footer has proper semantic structure", () => {
render(<Footer />);
// Check for contentinfo landmark
const contentinfo = screen.getByRole("contentinfo");
expect(contentinfo).toBeInTheDocument();
});
test("Footer links are accessible", () => {
render(<Footer />);
const links = screen.getAllByRole("link");
links.forEach((link) => {
// Check for accessible text or aria-label
const hasAccessibleText =
link.textContent?.trim() || link.getAttribute("aria-label");
expect(hasAccessibleText).toBeTruthy();
});
});
test("Focus management works correctly", () => {
render(<Header />);
// Test that focusable elements can receive focus
const buttons = screen.getAllByRole("button");
const links = screen.getAllByRole("link");
[...buttons, ...links].forEach((element) => {
try {
element.focus();
expect(element).toHaveFocus();
} catch {
// Some elements might not be focusable in test environment
// This is acceptable for accessibility testing
// Intentionally ignore focus failures in JSDOM
}
});
});
test("Color contrast meets WCAG standards", async () => {
const { container } = render(<Header />);
const results = await axe(container, {
rules: {
"color-contrast": { enabled: true },
},
});
expect(results).toHaveNoViolations();
});
test("Heading hierarchy is logical", () => {
render(<Header />);
// Try to get headings, but don't fail if none exist
let headings;
try {
headings = screen.getAllByRole("heading");
} catch {
// No headings found, which is fine for a header component
return;
}
// If there are no headings, that's fine for a header component
if (headings.length === 0) {
return;
}
const headingLevels = headings.map((heading) =>
parseInt(heading.tagName.charAt(1)),
);
// Check that heading levels are sequential (no skipping levels)
for (let i = 1; i < headingLevels.length; i++) {
const currentLevel = headingLevels[i];
const previousLevel = headingLevels[i - 1];
// Heading levels should not skip more than one level
expect(currentLevel - previousLevel).toBeLessThanOrEqual(1);
}
});
test("Interactive elements have proper ARIA attributes", () => {
render(<Header />);
// Get all interactive elements
const buttons = screen.getAllByRole("button");
const links = screen.getAllByRole("link");
const menuitems = screen.getAllByRole("menuitem");
const interactiveElements = [...buttons, ...links, ...menuitems];
interactiveElements.forEach((element) => {
// Check for proper ARIA attributes
const role = element.getAttribute("role");
if (role) {
// If role is specified, it should be valid
const validRoles = [
"button",
"link",
"menuitem",
"navigation",
"banner",
];
expect(validRoles).toContain(role);
}
});
});
test("No duplicate IDs exist", async () => {
const { container } = render(<Header />);
const results = await axe(container, {
rules: {
"duplicate-id": { enabled: true },
},
});
expect(results).toHaveNoViolations();
});
test("Proper language attributes", () => {
render(<Header />);
// Check that the document has proper language attributes
const html = document.documentElement;
const lang = html.getAttribute("lang");
expect(lang).toBeTruthy();
expect(lang).toMatch(/^[a-z]{2}(-[A-Z]{2})?$/);
});
});
+72
View File
@@ -0,0 +1,72 @@
import React from "react";
import { render, screen } from "@testing-library/react";
import { describe, it, expect } from "vitest";
import AskOrganizer from "../../app/components/AskOrganizer";
import {
componentTestSuite,
ComponentTestSuiteConfig,
} from "../utils/componentTestSuite";
type AskOrganizerProps = React.ComponentProps<typeof AskOrganizer>;
const baseProps: AskOrganizerProps = {
title: "Need help?",
};
const config: ComponentTestSuiteConfig<AskOrganizerProps> = {
component: AskOrganizer,
name: "AskOrganizer",
props: baseProps,
optionalProps: {
subtitle: "Subtitle",
description: "Description",
buttonText: "Button",
buttonHref: "/link",
className: "custom",
variant: "centered",
},
primaryRole: "region",
testCases: {
renders: true,
accessibility: true,
keyboardNavigation: false,
disabledState: false,
errorState: false,
},
};
componentTestSuite<AskOrganizerProps>(config);
describe("AskOrganizer (behavioral tests)", () => {
it("renders title", () => {
render(<AskOrganizer title="Test Title" />);
expect(
screen.getByRole("heading", { name: "Test Title" }),
).toBeInTheDocument();
});
it("renders subtitle when provided", () => {
render(<AskOrganizer title="Test" subtitle="Subtitle" />);
expect(screen.getByRole("heading", { name: "Subtitle" })).toBeInTheDocument();
});
it("renders button with default text", () => {
render(<AskOrganizer title="Test" />);
expect(
screen.getByRole("link", {
name: /ask an organizer/i,
}),
).toBeInTheDocument();
});
it("renders button with custom text", () => {
render(
<AskOrganizer title="Test" buttonText="Contact" buttonHref="/contact" />,
);
expect(
screen.getByRole("link", {
name: /contact/i,
}),
).toBeInTheDocument();
});
});
+66
View File
@@ -0,0 +1,66 @@
import React from "react";
import { describe, it, expect, vi } from "vitest";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import "@testing-library/jest-dom/vitest";
import Button from "../../app/components/Button";
import {
componentTestSuite,
ComponentTestSuiteConfig,
} from "../utils/componentTestSuite";
type ButtonProps = React.ComponentProps<typeof Button>;
const baseProps: ButtonProps = {
children: "Click me",
};
const config: ComponentTestSuiteConfig<ButtonProps> = {
component: Button,
name: "Button",
props: baseProps,
requiredProps: ["children"],
optionalProps: {
href: "/test",
ariaLabel: "Accessible button",
},
primaryRole: "button",
testCases: {
renders: true,
accessibility: true,
keyboardNavigation: true,
disabledState: true,
errorState: false,
},
states: {
disabledProps: { disabled: true },
},
};
componentTestSuite<ButtonProps>(config);
describe("Button (behavioral tests)", () => {
it("calls onClick when clicked", async () => {
const user = userEvent.setup();
const handleClick = vi.fn();
render(<Button onClick={handleClick}>Click me</Button>);
const button = screen.getByRole("button", { name: "Click me" });
await user.click(button);
expect(handleClick).toHaveBeenCalledTimes(1);
});
it("renders as a link when href is provided", () => {
render(
<Button href="/learn" variant="default">
Learn more
</Button>,
);
const link = screen.getByRole("link", { name: "Learn more" });
expect(link).toHaveAttribute("href", "/learn");
});
});
+29
View File
@@ -0,0 +1,29 @@
import React from "react";
import Checkbox from "../../app/components/Checkbox";
import { componentTestSuite } from "../utils/componentTestSuite";
type CheckboxProps = React.ComponentProps<typeof Checkbox>;
componentTestSuite<CheckboxProps>({
component: Checkbox,
name: "Checkbox",
props: {
label: "Test checkbox",
} as CheckboxProps,
requiredProps: ["label"],
optionalProps: {
value: "test",
},
primaryRole: "checkbox",
testCases: {
renders: true,
accessibility: true,
keyboardNavigation: true,
disabledState: true,
errorState: false,
},
states: {
disabledProps: { disabled: true },
},
});
+45
View File
@@ -0,0 +1,45 @@
import React from "react";
import { render, screen } from "@testing-library/react";
import { describe, it, expect, vi } from "vitest";
import ContentBanner from "../../app/components/ContentBanner";
import type { BlogPost } from "../../lib/content";
vi.mock("next/link", () => ({
default: ({ children, href, ...props }: any) => (
<a href={href} {...props}>
{children}
</a>
),
}));
vi.mock("../../lib/assetUtils", () => ({
getAssetPath: vi.fn((asset: string) => `/assets/${asset}`),
}));
const mockPost: BlogPost = {
slug: "test-article",
frontmatter: {
title: "Test Article",
description: "Test description",
author: "Test Author",
date: "2025-04-15",
},
};
describe("ContentBanner", () => {
it("renders without crashing", () => {
render(<ContentBanner post={mockPost} />);
});
it("renders article title", () => {
render(<ContentBanner post={mockPost} />);
expect(
screen.getByRole("heading", { name: "Test Article" }),
).toBeInTheDocument();
});
it("renders article description", () => {
render(<ContentBanner post={mockPost} />);
expect(screen.getByText("Test description")).toBeInTheDocument();
});
});
+28
View File
@@ -0,0 +1,28 @@
import React from "react";
import ContextMenu from "../../app/components/ContextMenu";
import ContextMenuItem from "../../app/components/ContextMenuItem";
import { componentTestSuite } from "../utils/componentTestSuite";
type ContextMenuProps = React.ComponentProps<typeof ContextMenu>;
componentTestSuite<ContextMenuProps>({
component: ContextMenu,
name: "ContextMenu",
props: {
children: (
<ContextMenuItem>
Item
</ContextMenuItem>
),
} as ContextMenuProps,
requiredProps: [],
primaryRole: "menu",
testCases: {
renders: true,
accessibility: true,
keyboardNavigation: false,
disabledState: false,
errorState: false,
},
});
+26
View File
@@ -0,0 +1,26 @@
import React from "react";
import ContextMenuItem from "../../app/components/ContextMenuItem";
import { componentTestSuite } from "../utils/componentTestSuite";
type ContextMenuItemProps = React.ComponentProps<typeof ContextMenuItem>;
componentTestSuite<ContextMenuItemProps>({
component: ContextMenuItem,
name: "ContextMenuItem",
props: {
children: "Item",
} as ContextMenuItemProps,
requiredProps: [],
primaryRole: "menuitem",
testCases: {
renders: true,
accessibility: false,
keyboardNavigation: true,
disabledState: true,
errorState: false,
},
states: {
disabledProps: { disabled: true },
},
});
+80
View File
@@ -0,0 +1,80 @@
import React from "react";
import { render, screen } from "@testing-library/react";
import { describe, it, expect } from "vitest";
import FeatureGrid from "../../app/components/FeatureGrid";
import {
componentTestSuite,
ComponentTestSuiteConfig,
} from "../utils/componentTestSuite";
type FeatureGridProps = React.ComponentProps<typeof FeatureGrid>;
const baseProps: FeatureGridProps = {
title: "Feature Tools",
subtitle: "Everything you need",
};
const config: ComponentTestSuiteConfig<FeatureGridProps> = {
component: FeatureGrid,
name: "FeatureGrid",
props: baseProps,
optionalProps: {
className: "custom-class",
title: undefined,
subtitle: undefined,
},
primaryRole: "region",
testCases: {
renders: true,
accessibility: true,
keyboardNavigation: false,
disabledState: false,
errorState: false,
},
};
componentTestSuite<FeatureGridProps>(config);
describe("FeatureGrid (behavioral tests)", () => {
it("renders title and subtitle", () => {
render(<FeatureGrid title="Test Title" subtitle="Test Subtitle" />);
expect(
screen.getByRole("heading", { name: "Test Title" }),
).toBeInTheDocument();
expect(
screen.getByRole("heading", { name: "Test Subtitle" }),
).toBeInTheDocument();
});
it("renders all four feature cards", () => {
render(<FeatureGrid title="Test" subtitle="Test" />);
expect(
screen.getByRole("link", { name: "Decision-making support tools" }),
).toBeInTheDocument();
expect(
screen.getByRole("link", { name: "Values alignment exercises" }),
).toBeInTheDocument();
expect(
screen.getByRole("link", { name: "Membership guidance resources" }),
).toBeInTheDocument();
expect(
screen.getByRole("link", { name: "Conflict resolution tools" }),
).toBeInTheDocument();
});
it("has proper accessibility attributes", () => {
render(<FeatureGrid title="Test" subtitle="Test" />);
const section = document.querySelector("section");
expect(section).toHaveAttribute("aria-labelledby", "feature-grid-headline");
expect(screen.getByRole("grid")).toHaveAttribute(
"aria-label",
"Feature tools and services",
);
});
it("handles missing props gracefully", () => {
render(<FeatureGrid />);
const section = document.querySelector("section");
expect(section).toBeInTheDocument();
});
});
+73
View File
@@ -0,0 +1,73 @@
import React from "react";
import { render, screen } from "@testing-library/react";
import { describe, it, expect } from "vitest";
import Footer from "../../app/components/Footer";
import {
componentTestSuite,
ComponentTestSuiteConfig,
} from "../utils/componentTestSuite";
type FooterProps = React.ComponentProps<typeof Footer>;
const baseProps: FooterProps = {};
const config: ComponentTestSuiteConfig<FooterProps> = {
component: Footer,
name: "Footer",
props: baseProps,
primaryRole: "contentinfo",
testCases: {
renders: true,
accessibility: true,
keyboardNavigation: false, // Footer is not primarily keyboard navigable
disabledState: false,
errorState: false,
},
};
componentTestSuite<FooterProps>(config);
describe("Footer (behavioral tests)", () => {
it("renders organization schema markup", () => {
render(<Footer />);
const script = document.querySelector('script[type="application/ld+json"]');
expect(script).toBeInTheDocument();
const schemaData = JSON.parse(script?.textContent || "{}");
expect(schemaData["@type"]).toBe("Organization");
expect(schemaData.name).toBe("Media Economies Design Lab");
});
it("renders organization name and contact", () => {
render(<Footer />);
expect(
screen.getAllByText("Media Economies Design Lab").length,
).toBeGreaterThan(0);
expect(
screen.getAllByRole("link", { name: "medlab@colorado.edu" }).length,
).toBeGreaterThan(0);
});
it("renders social media links", () => {
render(<Footer />);
expect(
screen.getAllByRole("link", { name: "Follow us on Bluesky" }).length,
).toBeGreaterThan(0);
expect(
screen.getAllByRole("link", { name: "Follow us on GitLab" }).length,
).toBeGreaterThan(0);
});
it("renders navigation links", () => {
render(<Footer />);
expect(screen.getAllByRole("link", { name: "Use cases" }).length).toBeGreaterThan(0);
expect(screen.getAllByRole("link", { name: "Learn" }).length).toBeGreaterThan(0);
expect(screen.getAllByRole("link", { name: "About" }).length).toBeGreaterThan(0);
});
it("renders legal links", () => {
render(<Footer />);
expect(screen.getAllByRole("link", { name: "Privacy Policy" }).length).toBeGreaterThan(0);
expect(screen.getAllByRole("link", { name: "Terms of Service" }).length).toBeGreaterThan(0);
});
});
+22
View File
@@ -0,0 +1,22 @@
import React from "react";
import Header from "../../app/components/Header";
import { componentTestSuite } from "../utils/componentTestSuite";
type HeaderProps = React.ComponentProps<typeof Header>;
componentTestSuite<HeaderProps>({
component: Header,
name: "Header",
// Header has no props; it reads from routing and config.
props: {} as HeaderProps,
requiredProps: [],
primaryRole: "banner",
testCases: {
renders: true,
accessibility: true,
keyboardNavigation: false,
disabledState: false,
errorState: false,
},
});
+66
View File
@@ -0,0 +1,66 @@
import React from "react";
import { render, screen } from "@testing-library/react";
import { describe, it, expect } from "vitest";
import HeroBanner from "../../app/components/HeroBanner";
import {
componentTestSuite,
ComponentTestSuiteConfig,
} from "../utils/componentTestSuite";
type HeroBannerProps = React.ComponentProps<typeof HeroBanner>;
const baseProps: HeroBannerProps = {
title: "Welcome",
};
const config: ComponentTestSuiteConfig<HeroBannerProps> = {
component: HeroBanner,
name: "HeroBanner",
props: baseProps,
optionalProps: {
subtitle: "Subtitle",
description: "Description",
ctaText: "CTA",
ctaHref: "/link",
},
primaryRole: "region",
testCases: {
renders: true,
accessibility: true,
keyboardNavigation: false,
disabledState: false,
errorState: false,
},
};
componentTestSuite<HeroBannerProps>(config);
describe("HeroBanner (behavioral tests)", () => {
it("renders title", () => {
render(<HeroBanner title="Test Title" />);
expect(
screen.getByRole("heading", { name: "Test Title" }),
).toBeInTheDocument();
});
it("renders subtitle when provided", () => {
render(<HeroBanner title="Test" subtitle="Subtitle" />);
expect(screen.getByRole("heading", { name: "Subtitle" })).toBeInTheDocument();
});
it("renders hero image", () => {
render(<HeroBanner title="Test" />);
expect(
screen.getByRole("img", { name: "Hero illustration" }),
).toBeInTheDocument();
});
it("renders CTA button when provided", () => {
render(
<HeroBanner title="Test" ctaText="Get Started" ctaHref="/start" />,
);
expect(
screen.getAllByRole("button", { name: "Get Started" }).length,
).toBeGreaterThan(0);
});
});
+30
View File
@@ -0,0 +1,30 @@
import React from "react";
import Input from "../../app/components/Input";
import { componentTestSuite } from "../utils/componentTestSuite";
type InputProps = React.ComponentProps<typeof Input>;
componentTestSuite<InputProps>({
component: Input,
name: "Input",
props: {
label: "Test input",
} as InputProps,
requiredProps: ["label"],
optionalProps: {
placeholder: "Enter value",
},
primaryRole: "textbox",
testCases: {
renders: true,
accessibility: true,
keyboardNavigation: true,
disabledState: true,
errorState: true,
},
states: {
disabledProps: { disabled: true },
errorProps: { error: true },
},
});
+68
View File
@@ -0,0 +1,68 @@
import React from "react";
import { render, screen } from "@testing-library/react";
import { describe, it, expect } from "vitest";
import Logo from "../../app/components/Logo";
import {
componentTestSuite,
ComponentTestSuiteConfig,
} from "../utils/componentTestSuite";
type LogoProps = React.ComponentProps<typeof Logo>;
const baseProps: LogoProps = {};
const config: ComponentTestSuiteConfig<LogoProps> = {
component: Logo,
name: "Logo",
props: baseProps,
primaryRole: "link",
testCases: {
renders: true,
accessibility: true,
keyboardNavigation: true,
disabledState: false,
errorState: false,
},
};
componentTestSuite<LogoProps>(config);
describe("Logo (behavioral tests)", () => {
it("renders as a link to home", () => {
render(<Logo />);
const logo = screen.getByRole("link", { name: /communityrule logo/i });
expect(logo).toHaveAttribute("href", "/");
expect(logo).toHaveAttribute("aria-label", "CommunityRule Logo");
});
it("renders logo icon", () => {
render(<Logo />);
expect(
screen.getByAltText("CommunityRule Logo Icon"),
).toBeInTheDocument();
});
it("renders text by default", () => {
render(<Logo />);
expect(screen.getByText("CommunityRule")).toBeInTheDocument();
});
it("hides text when showText is false", () => {
render(<Logo showText={false} />);
expect(screen.queryByText("CommunityRule")).not.toBeInTheDocument();
expect(
screen.getByAltText("CommunityRule Logo Icon"),
).toBeInTheDocument();
});
it("renders with different size variants", () => {
const { rerender } = render(<Logo size="header" />);
expect(screen.getByRole("link")).toBeInTheDocument();
rerender(<Logo size="footer" />);
expect(screen.getByRole("link")).toBeInTheDocument();
rerender(<Logo size="homeHeaderMd" />);
expect(screen.getByRole("link")).toBeInTheDocument();
});
});
+30
View File
@@ -0,0 +1,30 @@
import React from "react";
import RadioButton from "../../app/components/RadioButton";
import { componentTestSuite } from "../utils/componentTestSuite";
type RadioButtonProps = React.ComponentProps<typeof RadioButton>;
componentTestSuite<RadioButtonProps>({
component: RadioButton,
name: "RadioButton",
props: {
label: "Option A",
checked: false,
} as RadioButtonProps,
requiredProps: [],
optionalProps: {
mode: "inverse",
},
primaryRole: "radio",
testCases: {
renders: true,
accessibility: true,
keyboardNavigation: true,
disabledState: true,
errorState: false,
},
states: {
disabledProps: { disabled: true },
},
});
+28
View File
@@ -0,0 +1,28 @@
import React from "react";
import RadioGroup from "../../app/components/RadioGroup";
import { componentTestSuite } from "../utils/componentTestSuite";
type RadioGroupProps = React.ComponentProps<typeof RadioGroup>;
componentTestSuite<RadioGroupProps>({
component: RadioGroup,
name: "RadioGroup",
props: {
name: "example",
value: "a",
options: [
{ value: "a", label: "Option A" },
{ value: "b", label: "Option B" },
],
} as RadioGroupProps,
requiredProps: [],
primaryRole: "radiogroup",
testCases: {
renders: true,
accessibility: true,
keyboardNavigation: false,
disabledState: false,
errorState: false,
},
});
+81
View File
@@ -0,0 +1,81 @@
import React from "react";
import { render, screen } from "@testing-library/react";
import { describe, it, expect, vi } from "vitest";
import RelatedArticles from "../../app/components/RelatedArticles";
import type { BlogPost } from "../../lib/content";
vi.mock("next/link", () => ({
default: ({ children, href, ...props }: any) => (
<a href={href} {...props}>
{children}
</a>
),
}));
vi.mock("../../app/components/ContentThumbnailTemplate", () => ({
default: ({ post }: { post: BlogPost }) => (
<div data-testid={`thumbnail-${post.slug}`}>
<a href={`/blog/${post.slug}`}>
<h3>{post.frontmatter.title}</h3>
</a>
</div>
),
}));
vi.mock("../../app/hooks", () => ({
useIsMobile: () => false,
}));
const mockPosts: BlogPost[] = [
{
slug: "article-1",
frontmatter: {
title: "Article 1",
description: "Description 1",
author: "Author",
date: "2025-04-10",
},
},
{
slug: "article-2",
frontmatter: {
title: "Article 2",
description: "Description 2",
author: "Author",
date: "2025-04-11",
},
},
];
describe("RelatedArticles", () => {
it("renders without crashing", () => {
render(
<RelatedArticles
relatedPosts={mockPosts}
currentPostSlug="current"
/>,
);
});
it("renders related articles", () => {
render(
<RelatedArticles
relatedPosts={mockPosts}
currentPostSlug="current"
/>,
);
expect(screen.getByTestId("thumbnail-article-1")).toBeInTheDocument();
expect(screen.getByTestId("thumbnail-article-2")).toBeInTheDocument();
});
it("filters out current post", () => {
render(
<RelatedArticles
relatedPosts={mockPosts}
currentPostSlug="article-1"
/>,
);
expect(screen.queryByTestId("thumbnail-article-1")).not.toBeInTheDocument();
expect(screen.getByTestId("thumbnail-article-2")).toBeInTheDocument();
});
});
+24
View File
@@ -0,0 +1,24 @@
import React from "react";
import SectionHeader from "../../app/components/SectionHeader";
import { componentTestSuite } from "../utils/componentTestSuite";
type SectionHeaderProps = React.ComponentProps<typeof SectionHeader>;
componentTestSuite<SectionHeaderProps>({
component: SectionHeader,
name: "SectionHeader",
props: {
title: "Title",
subtitle: "Subtitle",
} as SectionHeaderProps,
requiredProps: ["title", "subtitle"],
primaryRole: "heading",
testCases: {
renders: true,
accessibility: true,
keyboardNavigation: false,
disabledState: false,
errorState: false,
},
});
+35
View File
@@ -0,0 +1,35 @@
import React from "react";
import Select from "../../app/components/Select";
import { componentTestSuite } from "../utils/componentTestSuite";
type SelectProps = React.ComponentProps<typeof Select>;
componentTestSuite<SelectProps>({
component: Select,
name: "Select",
props: {
label: "Test Select",
placeholder: "Select an option",
options: [
{ value: "option1", label: "Option 1" },
{ value: "option2", label: "Option 2" },
],
} as SelectProps,
requiredProps: ["options"],
optionalProps: {
size: "medium",
},
primaryRole: "button",
testCases: {
renders: true,
accessibility: true,
keyboardNavigation: true,
disabledState: true,
errorState: true,
},
states: {
disabledProps: { disabled: true },
errorProps: { error: true },
},
});
+29
View File
@@ -0,0 +1,29 @@
import React from "react";
import Switch from "../../app/components/Switch";
import { componentTestSuite } from "../utils/componentTestSuite";
type SwitchProps = React.ComponentProps<typeof Switch>;
componentTestSuite<SwitchProps>({
component: Switch,
name: "Switch",
props: {
label: "Test Switch",
} as SwitchProps,
requiredProps: [],
optionalProps: {
state: "focus",
},
primaryRole: "switch",
testCases: {
renders: true,
accessibility: true,
keyboardNavigation: true,
disabledState: true,
errorState: false,
},
states: {
disabledProps: { disabled: true },
},
});
+31
View File
@@ -0,0 +1,31 @@
import React from "react";
import TextArea from "../../app/components/TextArea";
import { componentTestSuite } from "../utils/componentTestSuite";
type TextAreaProps = React.ComponentProps<typeof TextArea>;
componentTestSuite<TextAreaProps>({
component: TextArea,
name: "TextArea",
props: {
label: "Description",
value: "",
} as TextAreaProps,
requiredProps: ["label"],
optionalProps: {
placeholder: "Enter description",
},
primaryRole: "textbox",
testCases: {
renders: true,
accessibility: true,
keyboardNavigation: true,
disabledState: true,
errorState: true,
},
states: {
disabledProps: { disabled: true },
errorProps: { error: true },
},
});
+27
View File
@@ -0,0 +1,27 @@
import React from "react";
import Toggle from "../../app/components/Toggle";
import { componentTestSuite } from "../utils/componentTestSuite";
type ToggleProps = React.ComponentProps<typeof Toggle>;
componentTestSuite<ToggleProps>({
component: Toggle,
name: "Toggle",
props: {
label: "Notifications",
checked: false,
} as ToggleProps,
requiredProps: [],
primaryRole: "switch",
testCases: {
renders: true,
accessibility: true,
keyboardNavigation: true,
disabledState: true,
errorState: false,
},
states: {
disabledProps: { disabled: true },
},
});
+23
View File
@@ -0,0 +1,23 @@
import React from "react";
import ToggleGroup from "../../app/components/ToggleGroup";
import { componentTestSuite } from "../utils/componentTestSuite";
type ToggleGroupProps = React.ComponentProps<typeof ToggleGroup>;
componentTestSuite<ToggleGroupProps>({
component: ToggleGroup,
name: "ToggleGroup",
props: {
children: "Option",
} as ToggleGroupProps,
requiredProps: [],
primaryRole: "button",
testCases: {
renders: true,
accessibility: true,
keyboardNavigation: true,
disabledState: false,
errorState: false,
},
});
-136
View File
@@ -1,136 +0,0 @@
import { test, expect } from "@playwright/test";
const breakpoints = [
{ name: "xs", width: 320, height: 568 },
{ name: "sm", width: 640, height: 720 },
{ name: "md", width: 768, height: 1024 },
{ name: "lg", width: 1024, height: 768 },
{ name: "xl", width: 1280, height: 800 },
{ name: "2xl", width: 1536, height: 864 },
{ name: "3xl", width: 1920, height: 1080 },
{ name: "4xl", width: 2560, height: 1440 },
{ name: "full", width: 3840, height: 2160 },
];
test.describe("Footer responsive behavior", () => {
for (const bp of breakpoints) {
test(`footer content visibility at ${bp.name}`, async ({ page }) => {
await page.setViewportSize({ width: bp.width, height: bp.height });
await page.goto("/");
// Scroll to footer
await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight));
await page.waitForTimeout(500);
// Test that footer is visible
const footer = page.getByRole("contentinfo");
await expect(footer).toBeVisible();
// Test navigation links
await expect(
page.getByRole("contentinfo").getByRole("link", { name: /use cases/i }),
).toBeVisible();
await expect(
page.getByRole("contentinfo").getByRole("link", { name: /learn/i }),
).toBeVisible();
await expect(
page.getByRole("contentinfo").getByRole("link", { name: /about/i }),
).toBeVisible();
// Test legal links
await expect(
page
.getByRole("contentinfo")
.getByRole("link", { name: /privacy policy/i }),
).toBeVisible();
await expect(
page
.getByRole("contentinfo")
.getByRole("link", { name: /terms of service/i }),
).toBeVisible();
await expect(
page
.getByRole("contentinfo")
.getByRole("link", { name: /cookies settings/i }),
).toBeVisible();
// Test social links
await expect(
page
.getByRole("contentinfo")
.getByRole("link", { name: /follow us on bluesky/i }),
).toBeVisible();
await expect(
page
.getByRole("contentinfo")
.getByRole("link", { name: /follow us on gitlab/i }),
).toBeVisible();
});
test(`footer layout consistency at ${bp.name}`, async ({ page }) => {
await page.setViewportSize({ width: bp.width, height: bp.height });
await page.goto("/");
// Scroll to footer
await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight));
await page.waitForTimeout(500);
// Test that footer has proper structure
const footer = page.getByRole("contentinfo");
await expect(footer).toBeVisible();
// Test that footer contains expected sections
// Note: Logo visibility can vary by breakpoint due to responsive design
// We'll skip this test to avoid flakiness
// await expect(footer.getByText("CommunityRule")).toBeVisible();
});
}
test.describe("Footer interaction states", () => {
test("hover states work correctly", async ({ page }) => {
await page.setViewportSize({ width: 1280, height: 800 });
await page.goto("/");
// Scroll to footer
await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight));
await page.waitForTimeout(500);
// Test hover on navigation items
const useCasesLink = page
.getByRole("contentinfo")
.getByRole("link", { name: /use cases/i });
await useCasesLink.hover();
await page.waitForTimeout(200);
// Test hover on social links
const blueskyLink = page.getByRole("contentinfo").getByRole("link", {
name: /follow us on bluesky/i,
});
await blueskyLink.hover();
await page.waitForTimeout(200);
});
test("focus states work correctly", async ({ page }) => {
await page.setViewportSize({ width: 1280, height: 800 });
await page.goto("/");
// Scroll to footer
await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight));
await page.waitForTimeout(500);
// Test focus on navigation items
const useCasesLink = page
.getByRole("contentinfo")
.getByRole("link", { name: /use cases/i });
await useCasesLink.focus();
await page.waitForTimeout(200);
// Test focus on social links
const blueskyLink = page.getByRole("contentinfo").getByRole("link", {
name: /follow us on bluesky/i,
});
await blueskyLink.focus();
await page.waitForTimeout(200);
});
});
});
-195
View File
@@ -1,195 +0,0 @@
import { test, expect } from "@playwright/test";
const breakpoints = [
{ name: "xs", width: 320, height: 568 },
{ name: "sm", width: 640, height: 720 },
{ name: "md", width: 768, height: 1024 },
{ name: "lg", width: 1024, height: 768 },
{ name: "xl", width: 1280, height: 800 },
{ name: "2xl", width: 1536, height: 864 },
{ name: "3xl", width: 1920, height: 1080 },
{ name: "4xl", width: 2560, height: 1440 },
{ name: "full", width: 3840, height: 2160 },
];
test.describe("Header responsive behavior", () => {
for (const bp of breakpoints) {
test(`navigation items visibility at ${bp.name}`, async ({ page }) => {
await page.setViewportSize({ width: bp.width, height: bp.height });
await page.goto("/");
// All breakpoints should have navigation items
await expect(
page.getByRole("menuitem", { name: /use cases/i }),
).toBeVisible();
await expect(
page.getByRole("menuitem", { name: /learn/i }),
).toBeVisible();
await expect(
page.getByRole("menuitem", { name: /about/i }),
).toBeVisible();
});
test(`login and create rule button visibility at ${bp.name}`, async ({
page,
}) => {
await page.setViewportSize({ width: bp.width, height: bp.height });
await page.goto("/");
// All breakpoints should have login button
await expect(
page.getByRole("menuitem", { name: /log in to your account/i }),
).toBeVisible();
// All breakpoints should have create rule button
await expect(
page.getByRole("button", {
name: /create a new rule with avatar decoration/i,
}),
).toBeVisible();
});
test(`header layout consistency at ${bp.name}`, async ({ page }) => {
await page.setViewportSize({ width: bp.width, height: bp.height });
await page.goto("/");
// Test that header is visible and has proper structure
const header = page.locator("header").first();
await expect(header).toBeVisible();
// Test that header contains navigation
await expect(header.getByRole("navigation")).toBeVisible();
// Test that header contains logo/brand
// Note: Logo visibility can vary by breakpoint due to responsive design
// We'll skip this test to avoid flakiness
// await expect(header.getByText("CommunityRule")).toBeVisible();
});
}
test.describe("Header interaction states", () => {
test("hover states work correctly", async ({ page }) => {
await page.setViewportSize({ width: 1280, height: 800 });
await page.goto("/");
// Test hover on navigation items
const useCasesLink = page.getByRole("menuitem", { name: /use cases/i });
await useCasesLink.hover();
await page.waitForTimeout(200);
// Test hover on create rule button
const createRuleButton = page.getByRole("button", {
name: /create a new rule with avatar decoration/i,
});
await createRuleButton.hover();
await page.waitForTimeout(200);
});
test("focus states work correctly", async ({ page }) => {
await page.setViewportSize({ width: 1280, height: 800 });
await page.goto("/");
// Test focus on navigation items
const useCasesLink = page.getByRole("menuitem", { name: /use cases/i });
await useCasesLink.focus();
await page.waitForTimeout(200);
// Test focus on create rule button
const createRuleButton = page.getByRole("button", {
name: /create a new rule with avatar decoration/i,
});
await createRuleButton.focus();
await page.waitForTimeout(200);
});
});
test.describe("Header sticky behavior", () => {
test("regular header is sticky on non-home pages", async ({ page }) => {
await page.setViewportSize({ width: 1280, height: 800 });
await page.goto("/learn");
const header = page.locator("header").first();
// Check that header has sticky positioning
const headerStyles = await header.evaluate((el) => {
const computed = window.getComputedStyle(el);
return {
position: computed.position,
top: computed.top,
zIndex: computed.zIndex,
};
});
expect(headerStyles.position).toBe("sticky");
expect(headerStyles.top).toBe("0px");
expect(headerStyles.zIndex).toBe("50");
});
test("home header is not sticky on home page", async ({ page }) => {
await page.setViewportSize({ width: 1280, height: 800 });
await page.goto("/");
const header = page.locator("header").first();
// Check that header does not have sticky positioning
const headerStyles = await header.evaluate((el) => {
const computed = window.getComputedStyle(el);
return {
position: computed.position,
top: computed.top,
zIndex: computed.zIndex,
};
});
expect(headerStyles.position).not.toBe("sticky");
});
});
test.describe("Active navigation state", () => {
test("learn page shows active state for learn navigation", async ({
page,
}) => {
await page.setViewportSize({ width: 1280, height: 800 });
await page.goto("/learn");
const learnLink = page.getByRole("menuitem", { name: /learn/i });
// Check that learn link has active styling
const linkStyles = await learnLink.evaluate((el) => {
const computed = window.getComputedStyle(el);
return {
outline: computed.outline,
outlineColor: computed.outlineColor,
color: computed.color,
};
});
// Should have outline and brand color
expect(linkStyles.outline).not.toBe("none");
expect(linkStyles.outlineColor).toContain("254, 252, 201"); // RGB value of #fefcc9
});
test("home page does not show active state for learn navigation", async ({
page,
}) => {
await page.setViewportSize({ width: 1280, height: 800 });
await page.goto("/");
const learnLink = page.getByRole("menuitem", { name: /learn/i });
// Check that learn link does not have active styling
const linkStyles = await learnLink.evaluate((el) => {
const computed = window.getComputedStyle(el);
return {
outline: computed.outline,
outlineColor: computed.outlineColor,
};
});
// Should not have active outline (may have default browser outline)
expect(linkStyles.outline).toMatch(
/^(none|0px|rgb\(0, 0, 0\) none 0px|rgb\(0, 0, 0\) 0px)$/,
);
});
});
});
@@ -1,171 +0,0 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { render, screen } from "@testing-library/react";
import RelatedArticles from "../../app/components/RelatedArticles";
// Mock ContentThumbnailTemplate with a simple implementation
vi.mock("../../app/components/ContentThumbnailTemplate", () => ({
default: ({ post, variant }) => (
<div data-testid={`thumbnail-${post.slug}`} data-variant={variant}>
<a href={`/blog/${post.slug}`}>
<h3>{post.frontmatter?.title || "Untitled"}</h3>
<p>{post.frontmatter?.description || "No description"}</p>
</a>
</div>
),
}));
// Mock blog post data
const mockRelatedPosts = [
{
slug: "resolving-active-conflicts",
frontmatter: {
title: "Resolving Active Conflicts",
description:
"Practical steps for resolving conflicts while maintaining trust",
author: "Test Author",
date: "2025-04-15",
},
},
{
slug: "operational-security-mutual-aid",
frontmatter: {
title: "Operational Security for Mutual Aid",
description:
"Tactics to protect members, secure communication, and prevent infiltration",
author: "Test Author",
date: "2025-04-14",
},
},
{
slug: "making-decisions-without-hierarchy",
frontmatter: {
title: "Making Decisions Without Hierarchy",
description:
"A brief guide to collaborative nonhierarchical decision making",
author: "Test Author",
date: "2025-04-13",
},
},
];
describe("Blog Core Integration", () => {
beforeEach(() => {
// Mock window.innerWidth for responsive tests
Object.defineProperty(window, "innerWidth", {
writable: true,
configurable: true,
value: 1024, // Desktop width
});
});
it("should render RelatedArticles component with correct structure", () => {
render(
<RelatedArticles
relatedPosts={mockRelatedPosts}
currentPostSlug="resolving-active-conflicts"
/>,
);
// Verify the section exists
expect(screen.getByRole("heading", { level: 2 })).toHaveTextContent(
"Related Articles",
);
// Verify thumbnails are rendered
expect(
screen.getByTestId("thumbnail-operational-security-mutual-aid"),
).toBeInTheDocument();
expect(
screen.getByTestId("thumbnail-making-decisions-without-hierarchy"),
).toBeInTheDocument();
// Current post should not be displayed
expect(
screen.queryByTestId("thumbnail-resolving-active-conflicts"),
).not.toBeInTheDocument();
});
it("should filter out current post from related articles", () => {
render(
<RelatedArticles
relatedPosts={mockRelatedPosts}
currentPostSlug="resolving-active-conflicts"
/>,
);
// Current post should not be displayed
expect(
screen.queryByTestId("thumbnail-resolving-active-conflicts"),
).not.toBeInTheDocument();
// Other posts should be displayed
expect(
screen.getByTestId("thumbnail-operational-security-mutual-aid"),
).toBeInTheDocument();
expect(
screen.getByTestId("thumbnail-making-decisions-without-hierarchy"),
).toBeInTheDocument();
});
it("should display all posts when no current post is specified", () => {
render(<RelatedArticles relatedPosts={mockRelatedPosts} />);
// All posts should be displayed
expect(
screen.getByTestId("thumbnail-resolving-active-conflicts"),
).toBeInTheDocument();
expect(
screen.getByTestId("thumbnail-operational-security-mutual-aid"),
).toBeInTheDocument();
expect(
screen.getByTestId("thumbnail-making-decisions-without-hierarchy"),
).toBeInTheDocument();
});
it("should handle empty related posts array", () => {
const { container } = render(
<RelatedArticles relatedPosts={[]} currentPostSlug="test-post" />,
);
expect(container.firstChild).toBeNull();
});
it("should create correct links for each thumbnail", () => {
render(
<RelatedArticles
relatedPosts={mockRelatedPosts}
currentPostSlug="resolving-active-conflicts"
/>,
);
// Verify links are created correctly
const operationalLink = screen
.getByTestId("thumbnail-operational-security-mutual-aid")
.querySelector("a");
const hierarchyLink = screen
.getByTestId("thumbnail-making-decisions-without-hierarchy")
.querySelector("a");
expect(operationalLink).toHaveAttribute(
"href",
"/blog/operational-security-mutual-aid",
);
expect(hierarchyLink).toHaveAttribute(
"href",
"/blog/making-decisions-without-hierarchy",
);
});
it("should display section heading", () => {
render(
<RelatedArticles
relatedPosts={mockRelatedPosts}
currentPostSlug="resolving-active-conflicts"
/>,
);
expect(screen.getByRole("heading", { level: 2 })).toHaveTextContent(
"Related Articles",
);
});
});
@@ -1,249 +0,0 @@
import React, { useState } from "react";
import { render, screen } from "@testing-library/react";
import { expect, test, describe, vi } from "vitest";
import userEvent from "@testing-library/user-event";
import Checkbox from "../../app/components/Checkbox";
// Test component that uses Checkbox in a form
function TestForm() {
const [formData, setFormData] = useState({
agree: false,
newsletter: false,
notifications: true,
});
const handleCheckboxChange =
(field) =>
({ checked }) => {
setFormData((prev) => ({ ...prev, [field]: checked }));
};
const handleSubmit = (e) => {
e.preventDefault();
// Form submission logic would go here
};
return (
<form onSubmit={handleSubmit} data-testid="test-form">
<Checkbox
label="I agree to the terms"
checked={formData.agree}
onChange={handleCheckboxChange("agree")}
name="agree"
data-testid="agree-checkbox"
/>
<Checkbox
label="Subscribe to newsletter"
checked={formData.newsletter}
onChange={handleCheckboxChange("newsletter")}
name="newsletter"
data-testid="newsletter-checkbox"
/>
<Checkbox
label="Enable notifications"
checked={formData.notifications}
onChange={handleCheckboxChange("notifications")}
name="notifications"
data-testid="notifications-checkbox"
/>
<button type="submit" data-testid="submit-button">
Submit
</button>
</form>
);
}
describe("Checkbox Integration Tests", () => {
test("handles multiple checkboxes in a form", async () => {
const user = userEvent.setup();
render(<TestForm />);
const agreeCheckbox = screen.getByTestId("agree-checkbox");
const newsletterCheckbox = screen.getByTestId("newsletter-checkbox");
const notificationsCheckbox = screen.getByTestId("notifications-checkbox");
// Initial state
expect(agreeCheckbox).toHaveAttribute("aria-checked", "false");
expect(newsletterCheckbox).toHaveAttribute("aria-checked", "false");
expect(notificationsCheckbox).toHaveAttribute("aria-checked", "true");
// Toggle checkboxes
await user.click(agreeCheckbox);
await user.click(newsletterCheckbox);
await user.click(notificationsCheckbox);
// Check final state
expect(agreeCheckbox).toHaveAttribute("aria-checked", "true");
expect(newsletterCheckbox).toHaveAttribute("aria-checked", "true");
expect(notificationsCheckbox).toHaveAttribute("aria-checked", "false");
});
test("handles keyboard navigation between checkboxes", async () => {
const user = userEvent.setup();
render(<TestForm />);
const agreeCheckbox = screen.getByTestId("agree-checkbox");
const newsletterCheckbox = screen.getByTestId("newsletter-checkbox");
const notificationsCheckbox = screen.getByTestId("notifications-checkbox");
// Focus first checkbox
await user.tab();
expect(agreeCheckbox).toHaveFocus();
// Navigate to next checkbox
await user.tab();
expect(newsletterCheckbox).toHaveFocus();
// Navigate to next checkbox
await user.tab();
expect(notificationsCheckbox).toHaveFocus();
});
test("handles keyboard activation", async () => {
const user = userEvent.setup();
render(<TestForm />);
const agreeCheckbox = screen.getByTestId("agree-checkbox");
// Focus and activate with Space
await user.tab();
expect(agreeCheckbox).toHaveFocus();
expect(agreeCheckbox).toHaveAttribute("aria-checked", "false");
await user.keyboard(" ");
expect(agreeCheckbox).toHaveAttribute("aria-checked", "true");
// Activate with Enter
await user.keyboard("Enter");
expect(agreeCheckbox).toHaveAttribute("aria-checked", "true");
});
test("handles mode switching", async () => {
function ModeSwitchForm() {
const [mode, setMode] = useState("standard");
const [checked, setChecked] = useState(false);
return (
<div>
<Checkbox
label="Switch to inverse mode"
checked={mode === "inverse"}
onChange={({ checked }) =>
setMode(checked ? "inverse" : "standard")
}
data-testid="mode-switch"
/>
<Checkbox
label="Test checkbox"
checked={checked}
onChange={({ checked }) => setChecked(checked)}
mode={mode}
data-testid="test-checkbox"
/>
</div>
);
}
const user = userEvent.setup();
render(<ModeSwitchForm />);
const modeSwitch = screen.getByTestId("mode-switch");
const testCheckbox = screen.getByTestId("test-checkbox");
// Initially standard mode
expect(testCheckbox).toBeInTheDocument();
// Switch to inverse mode
await user.click(modeSwitch);
expect(testCheckbox).toBeInTheDocument();
// Should still be functional
await user.click(testCheckbox);
expect(testCheckbox).toHaveAttribute("aria-checked", "true");
});
test("handles form submission with checkbox values", async () => {
const handleSubmit = vi.fn();
function FormWithSubmission() {
const [formData, setFormData] = useState({
agree: false,
newsletter: false,
});
const handleCheckboxChange =
(field) =>
({ checked }) => {
setFormData((prev) => ({ ...prev, [field]: checked }));
};
const onSubmit = (e) => {
e.preventDefault();
handleSubmit(formData);
};
return (
<form onSubmit={onSubmit} data-testid="form">
<Checkbox
label="I agree"
checked={formData.agree}
onChange={handleCheckboxChange("agree")}
name="agree"
value="yes"
data-testid="agree-checkbox"
/>
<Checkbox
label="Newsletter"
checked={formData.newsletter}
onChange={handleCheckboxChange("newsletter")}
name="newsletter"
value="yes"
data-testid="newsletter-checkbox"
/>
<button type="submit" data-testid="submit-button">
Submit
</button>
</form>
);
}
const user = userEvent.setup();
render(<FormWithSubmission />);
const agreeCheckbox = screen.getByTestId("agree-checkbox");
const newsletterCheckbox = screen.getByTestId("newsletter-checkbox");
const submitButton = screen.getByTestId("submit-button");
// Check some checkboxes
await user.click(agreeCheckbox);
await user.click(newsletterCheckbox);
// Submit form
await user.click(submitButton);
// Verify form data was captured
expect(handleSubmit).toHaveBeenCalledWith({
agree: true,
newsletter: true,
});
});
test("handles accessibility in form context", async () => {
render(<TestForm />);
const form = screen.getByTestId("test-form");
const checkboxes = screen.getAllByRole("checkbox");
// All checkboxes should be accessible
expect(checkboxes).toHaveLength(3);
checkboxes.forEach((checkbox) => {
expect(checkbox).toHaveAttribute("role", "checkbox");
expect(checkbox).toHaveAttribute("aria-checked");
expect(checkbox).toHaveAttribute("tabIndex");
});
// Form should be accessible
expect(form).toBeInTheDocument();
});
});
@@ -1,156 +0,0 @@
import { render, screen, cleanup } from "@testing-library/react";
import { describe, test, expect, afterEach } from "vitest";
import ContentLockup from "../../app/components/ContentLockup";
afterEach(() => {
cleanup();
});
describe("ContentLockup Integration", () => {
test("renders hero variant with all content", () => {
render(
<ContentLockup
variant="hero"
title="Welcome"
subtitle="Get Started"
description="This is a description"
ctaText="Get Started"
/>,
);
expect(
screen.getByRole("heading", { name: "Welcome" }),
).toBeInTheDocument();
expect(
screen.getByRole("heading", { name: "Get Started" }),
).toBeInTheDocument();
expect(screen.getByText("This is a description")).toBeInTheDocument();
expect(screen.getAllByRole("button", { name: "Get Started" })).toHaveLength(
3,
);
});
test("renders feature variant with link", () => {
render(
<ContentLockup
variant="feature"
title="Feature Title"
subtitle="Feature Subtitle"
description="Feature description"
linkText="Learn More"
linkHref="/learn"
/>,
);
expect(
screen.getByRole("heading", { name: "Feature Title" }),
).toBeInTheDocument();
expect(
screen.getByRole("link", { name: "Learn More" }),
).toBeInTheDocument();
expect(screen.getByRole("link", { name: "Learn More" })).toHaveAttribute(
"href",
"/learn",
);
});
test("renders ask variant with simplified structure", () => {
render(
<ContentLockup
variant="ask"
title="Ask Question"
subtitle="Ask subtitle"
/>,
);
expect(
screen.getByRole("heading", { name: "Ask Question" }),
).toBeInTheDocument();
expect(
screen.getByRole("heading", { name: "Ask subtitle" }),
).toBeInTheDocument();
// Ask variant should not have description or CTA
expect(screen.queryByRole("button")).not.toBeInTheDocument();
});
test("handles left alignment", () => {
render(
<ContentLockup
variant="ask"
title="Left Aligned"
subtitle="Subtitle"
alignment="left"
/>,
);
const container = screen
.getByRole("heading", { name: "Left Aligned" })
.closest("div");
expect(container).toHaveClass("justify-start");
});
test("renders responsive buttons correctly", () => {
render(
<ContentLockup variant="hero" title="Responsive" ctaText="Click Me" />,
);
// Should render all three button variants for different breakpoints
const buttons = screen.getAllByRole("button", { name: "Click Me" });
expect(buttons).toHaveLength(3);
});
test("applies custom button className", () => {
render(
<ContentLockup
variant="hero"
title="Custom Button"
ctaText="Custom"
buttonClassName="custom-button-class"
/>,
);
const buttons = screen.getAllByRole("button", { name: "Custom" });
// The large button (md breakpoint) should have the custom class
expect(buttons[1]).toHaveClass("custom-button-class");
});
test("handles missing optional props gracefully", () => {
render(<ContentLockup variant="hero" title="Minimal" />);
expect(
screen.getByRole("heading", { name: "Minimal" }),
).toBeInTheDocument();
// Should not crash without subtitle, description, or CTA
expect(screen.queryByRole("button")).not.toBeInTheDocument();
});
test("renders decorative shape for hero variant", () => {
render(<ContentLockup variant="hero" title="Hero with Shape" />);
const shape = screen.getByRole("presentation");
expect(shape).toBeInTheDocument();
expect(shape).toHaveAttribute("src", "/assets/Shapes_1.svg");
expect(shape).toHaveAttribute("alt", "");
});
test("does not render shape for non-hero variants", () => {
render(<ContentLockup variant="feature" title="Feature without Shape" />);
expect(screen.queryByRole("presentation")).not.toBeInTheDocument();
});
test("link has proper accessibility attributes", () => {
render(
<ContentLockup
variant="feature"
title="Accessible"
linkText="Accessible Link"
linkHref="/accessible"
/>,
);
const link = screen.getByRole("link", { name: "Accessible Link" });
expect(link).toHaveAttribute("href", "/accessible");
expect(link).toHaveClass("focus:outline-none", "focus:ring-2");
});
});
@@ -1,384 +0,0 @@
import React, { useState } from "react";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { expect, describe, it, vi } from "vitest";
import ContextMenu from "../../app/components/ContextMenu";
import ContextMenuItem from "../../app/components/ContextMenuItem";
import ContextMenuSection from "../../app/components/ContextMenuSection";
import ContextMenuDivider from "../../app/components/ContextMenuDivider";
describe("ContextMenu Components Integration", () => {
const TestMenu = ({ onItemClick, selectedValue }) => (
<ContextMenu>
<ContextMenuSection title="Actions">
<ContextMenuItem
onClick={() => onItemClick("action1")}
selected={selectedValue === "action1"}
>
Action 1
</ContextMenuItem>
<ContextMenuItem
onClick={() => onItemClick("action2")}
selected={selectedValue === "action2"}
>
Action 2
</ContextMenuItem>
</ContextMenuSection>
<ContextMenuDivider />
<ContextMenuSection title="Settings">
<ContextMenuItem
onClick={() => onItemClick("setting1")}
hasSubmenu={true}
>
Setting 1
</ContextMenuItem>
<ContextMenuItem
onClick={() => onItemClick("setting2")}
disabled={true}
>
Setting 2
</ContextMenuItem>
</ContextMenuSection>
</ContextMenu>
);
describe("Menu Interaction", () => {
it("handles item selection correctly", async () => {
const user = userEvent.setup();
const onItemClick = vi.fn();
render(<TestMenu onItemClick={onItemClick} selectedValue="" />);
const action1 = screen.getByText("Action 1");
await user.click(action1);
expect(onItemClick).toHaveBeenCalledWith("action1");
});
it("shows selected state correctly", () => {
render(<TestMenu onItemClick={vi.fn()} selectedValue="action1" />);
const action1 = screen.getByRole("menuitem", { name: "Action 1" });
expect(action1).toHaveClass(
"bg-[var(--color-surface-default-secondary)]",
);
});
it("handles disabled items correctly", async () => {
const user = userEvent.setup();
const onItemClick = vi.fn();
render(<TestMenu onItemClick={onItemClick} selectedValue="" />);
const setting2 = screen.getByText("Setting 2");
await user.click(setting2);
expect(onItemClick).not.toHaveBeenCalled();
});
it("shows submenu indicators correctly", () => {
render(<TestMenu onItemClick={vi.fn()} selectedValue="" />);
const arrow = screen
.getByRole("menuitem", { name: "Setting 1" })
.querySelector("svg");
expect(arrow).toBeInTheDocument();
});
});
describe("Keyboard Navigation", () => {
it("navigates through menu items with arrow keys", async () => {
render(<TestMenu onItemClick={vi.fn()} selectedValue="" />);
const items = screen.getAllByRole("menuitem");
expect(items).toHaveLength(4);
// Check that enabled items are focusable and disabled items are not
const enabledItems = items.filter(
(item) =>
!item.hasAttribute("aria-disabled") ||
item.getAttribute("aria-disabled") !== "true",
);
const disabledItems = items.filter(
(item) => item.getAttribute("aria-disabled") === "true",
);
enabledItems.forEach((item) => {
expect(item).toHaveAttribute("tabIndex", "0");
});
disabledItems.forEach((item) => {
expect(item).toHaveAttribute("tabIndex", "-1");
});
});
it("selects items with Enter key", async () => {
const user = userEvent.setup();
const onItemClick = vi.fn();
render(<TestMenu onItemClick={onItemClick} selectedValue="" />);
const items = screen.getAllByRole("menuitem");
items[0].focus();
await user.keyboard("{Enter}");
expect(onItemClick).toHaveBeenCalledWith("action1");
});
it("selects items with Space key", async () => {
const user = userEvent.setup();
const onItemClick = vi.fn();
render(<TestMenu onItemClick={onItemClick} selectedValue="" />);
const items = screen.getAllByRole("menuitem");
items[0].focus();
await user.keyboard(" ");
expect(onItemClick).toHaveBeenCalledWith("action1");
});
it("skips disabled items during navigation", async () => {
render(<TestMenu onItemClick={vi.fn()} selectedValue="" />);
const items = screen.getAllByRole("menuitem");
expect(items).toHaveLength(4);
// Check that disabled items have tabIndex="-1"
const disabledItem = screen.getByRole("menuitem", { name: "Setting 2" });
expect(disabledItem).toHaveAttribute("tabIndex", "-1");
expect(disabledItem).toHaveAttribute("aria-disabled", "true");
});
});
describe("Dynamic Menu Updates", () => {
const DynamicMenu = ({ items, selectedValue, onItemClick }) => (
<ContextMenu>
{items.map((item) => (
<ContextMenuItem
key={item.id}
onClick={() => onItemClick(item.id)}
selected={selectedValue === item.id}
disabled={item.disabled}
>
{item.label}
</ContextMenuItem>
))}
</ContextMenu>
);
it("handles dynamic item updates", async () => {
const user = userEvent.setup();
const onItemClick = vi.fn();
const { rerender } = render(
<DynamicMenu
items={[
{ id: "1", label: "Item 1" },
{ id: "2", label: "Item 2" },
]}
selectedValue=""
onItemClick={onItemClick}
/>,
);
const item1 = screen.getByText("Item 1");
await user.click(item1);
expect(onItemClick).toHaveBeenCalledWith("1");
// Update items
rerender(
<DynamicMenu
items={[
{ id: "1", label: "Item 1" },
{ id: "2", label: "Item 2" },
{ id: "3", label: "Item 3" },
]}
selectedValue="1"
onItemClick={onItemClick}
/>,
);
expect(screen.getByText("Item 3")).toBeInTheDocument();
expect(screen.getByRole("menuitem", { name: "Item 1" })).toHaveClass(
"bg-[var(--color-surface-default-secondary)]",
);
});
it("handles item removal", () => {
const { rerender } = render(
<DynamicMenu
items={[
{ id: "1", label: "Item 1" },
{ id: "2", label: "Item 2" },
{ id: "3", label: "Item 3" },
]}
selectedValue="2"
onItemClick={vi.fn()}
/>,
);
expect(screen.getByText("Item 2")).toBeInTheDocument();
rerender(
<DynamicMenu
items={[
{ id: "1", label: "Item 1" },
{ id: "3", label: "Item 3" },
]}
selectedValue=""
onItemClick={vi.fn()}
/>,
);
expect(screen.queryByText("Item 2")).not.toBeInTheDocument();
});
});
describe("Menu State Management", () => {
const StatefulMenu = () => {
const [selectedValue, setSelectedValue] = useState("");
const [isOpen, setIsOpen] = useState(false);
return (
<div>
<button onClick={() => setIsOpen(!isOpen)}>
{isOpen ? "Close Menu" : "Open Menu"}
</button>
{isOpen && (
<ContextMenu>
<ContextMenuItem
onClick={() => {
setSelectedValue("option1");
setIsOpen(false);
}}
selected={selectedValue === "option1"}
>
Option 1
</ContextMenuItem>
<ContextMenuItem
onClick={() => {
setSelectedValue("option2");
setIsOpen(false);
}}
selected={selectedValue === "option2"}
>
Option 2
</ContextMenuItem>
</ContextMenu>
)}
</div>
);
};
it("manages menu open/close state", async () => {
const user = userEvent.setup();
render(<StatefulMenu />);
const toggleButton = screen.getByRole("button", { name: "Open Menu" });
await user.click(toggleButton);
expect(screen.getByText("Option 1")).toBeInTheDocument();
expect(
screen.getByRole("button", { name: "Close Menu" }),
).toBeInTheDocument();
});
it("closes menu after selection", async () => {
const user = userEvent.setup();
render(<StatefulMenu />);
const toggleButton = screen.getByRole("button", { name: "Open Menu" });
await user.click(toggleButton);
const option1 = screen.getByText("Option 1");
await user.click(option1);
expect(screen.queryByText("Option 1")).not.toBeInTheDocument();
expect(
screen.getByRole("button", { name: "Open Menu" }),
).toBeInTheDocument();
});
});
describe("Performance", () => {
it("handles large menu lists efficiently", async () => {
const largeItems = Array.from({ length: 100 }, (_, i) => ({
id: `item${i}`,
label: `Item ${i}`,
}));
const LargeMenu = () => (
<ContextMenu>
{largeItems.map((item) => (
<ContextMenuItem key={item.id} onClick={vi.fn()}>
{item.label}
</ContextMenuItem>
))}
</ContextMenu>
);
render(<LargeMenu />);
const items = screen.getAllByRole("menuitem");
expect(items).toHaveLength(100);
// Test that all items are focusable
items.forEach((item) => {
expect(item).toHaveAttribute("tabIndex", "0");
});
});
it("handles rapid state changes", async () => {
const { rerender } = render(
<ContextMenu>
<ContextMenuItem onClick={vi.fn()} selected={false}>
Item 1
</ContextMenuItem>
<ContextMenuItem onClick={vi.fn()} selected={false}>
Item 2
</ContextMenuItem>
</ContextMenu>,
);
// Rapidly change selection state
for (let i = 0; i < 10; i++) {
rerender(
<ContextMenu>
<ContextMenuItem onClick={vi.fn()} selected={i % 2 === 0}>
Item 1
</ContextMenuItem>
<ContextMenuItem onClick={vi.fn()} selected={i % 2 === 1}>
Item 2
</ContextMenuItem>
</ContextMenu>,
);
}
// Should still be functional
const items = screen.getAllByRole("menuitem");
expect(items).toHaveLength(2);
});
});
describe("Error Handling", () => {
it("handles missing onClick gracefully", () => {
render(
<ContextMenu>
<ContextMenuItem>Item without onClick</ContextMenuItem>
</ContextMenu>,
);
const item = screen.getByText("Item without onClick");
expect(item).toBeInTheDocument();
});
it("handles invalid props gracefully", () => {
render(
<ContextMenu>
<ContextMenuItem onClick={vi.fn()} selected={null}>
Item with invalid selected
</ContextMenuItem>
</ContextMenu>,
);
const item = screen.getByText("Item with invalid selected");
expect(item).toBeInTheDocument();
});
});
});
@@ -1,426 +0,0 @@
import React, { useState } from "react";
import { render, screen, fireEvent } from "@testing-library/react";
import { expect, test, describe, vi } from "vitest";
import Input from "../../app/components/Input";
// Test component that uses Input with state management
const TestInputForm = ({ initialValue = "", onValueChange }) => {
const [value, setValue] = useState(initialValue);
const [focused, setFocused] = useState(false);
const handleChange = (e) => {
setValue(e.target.value);
onValueChange?.(e.target.value);
};
const handleFocus = () => setFocused(true);
const handleBlur = () => setFocused(false);
return (
<div>
<Input
label="Test Input"
value={value}
onChange={handleChange}
onFocus={handleFocus}
onBlur={handleBlur}
state={focused ? "focus" : "default"}
/>
<div data-testid="value-display">{value}</div>
<div data-testid="focus-status">{focused ? "focused" : "blurred"}</div>
</div>
);
};
// Test component with multiple inputs
const MultiInputForm = () => {
const [values, setValues] = useState({
firstName: "",
lastName: "",
email: "",
});
const handleChange = (field) => (e) => {
setValues((prev) => ({ ...prev, [field]: e.target.value }));
};
return (
<form>
<Input
label="First Name"
name="firstName"
value={values.firstName}
onChange={handleChange("firstName")}
/>
<Input
label="Last Name"
name="lastName"
value={values.lastName}
onChange={handleChange("lastName")}
/>
<Input
label="Email"
name="email"
type="email"
value={values.email}
onChange={handleChange("email")}
/>
</form>
);
};
// Test component with validation
const ValidatedInputForm = () => {
const [value, setValue] = useState("");
const [error, setError] = useState(false);
const handleChange = (e) => {
setValue(e.target.value);
setError(e.target.value.length < 3);
};
return (
<div>
<Input
label="Required Field"
value={value}
onChange={handleChange}
error={error}
/>
{error && (
<div data-testid="error-message">Minimum 3 characters required</div>
)}
</div>
);
};
describe("Input Component Integration", () => {
test("handles controlled input with state management", async () => {
const onValueChange = vi.fn();
render(<TestInputForm onValueChange={onValueChange} />);
const input = screen.getByLabelText("Test Input");
const valueDisplay = screen.getByTestId("value-display");
const focusStatus = screen.getByTestId("focus-status");
// Initial state
expect(valueDisplay).toHaveTextContent("");
expect(focusStatus).toHaveTextContent("blurred");
// Type in input
fireEvent.change(input, { target: { value: "test value" } });
expect(valueDisplay).toHaveTextContent("test value");
expect(onValueChange).toHaveBeenCalledWith("test value");
// Focus input
fireEvent.focus(input);
expect(focusStatus).toHaveTextContent("focused");
// Blur input
fireEvent.blur(input);
expect(focusStatus).toHaveTextContent("blurred");
});
test("handles multiple inputs independently", () => {
render(<MultiInputForm />);
const firstNameInput = screen.getByLabelText("First Name");
const lastNameInput = screen.getByLabelText("Last Name");
const emailInput = screen.getByLabelText("Email");
// Type in first input
fireEvent.change(firstNameInput, { target: { value: "John" } });
expect(firstNameInput).toHaveValue("John");
expect(lastNameInput).toHaveValue("");
expect(emailInput).toHaveValue("");
// Type in second input
fireEvent.change(lastNameInput, { target: { value: "Doe" } });
expect(firstNameInput).toHaveValue("John");
expect(lastNameInput).toHaveValue("Doe");
expect(emailInput).toHaveValue("");
// Type in third input
fireEvent.change(emailInput, { target: { value: "john@example.com" } });
expect(firstNameInput).toHaveValue("John");
expect(lastNameInput).toHaveValue("Doe");
expect(emailInput).toHaveValue("john@example.com");
});
test("handles form validation", () => {
render(<ValidatedInputForm />);
const input = screen.getByLabelText("Required Field");
const errorMessage = screen.queryByTestId("error-message");
// Initial state - no error
expect(errorMessage).not.toBeInTheDocument();
// Type short value - should show error
fireEvent.change(input, { target: { value: "ab" } });
expect(screen.getByTestId("error-message")).toBeInTheDocument();
expect(input).toHaveClass(
"border-[var(--color-border-default-utility-negative)]",
);
// Type longer value - should hide error
fireEvent.change(input, { target: { value: "abc" } });
expect(screen.queryByTestId("error-message")).not.toBeInTheDocument();
});
test("handles different input types", () => {
render(
<div>
<Input label="Text Input" type="text" />
<Input label="Email Input" type="email" />
<Input label="Password Input" type="password" />
<Input label="Number Input" type="number" />
</div>,
);
const textInput = screen.getByLabelText("Text Input");
const emailInput = screen.getByLabelText("Email Input");
const passwordInput = screen.getByLabelText("Password Input");
const numberInput = screen.getByLabelText("Number Input");
expect(textInput).toHaveAttribute("type", "text");
expect(emailInput).toHaveAttribute("type", "email");
expect(passwordInput).toHaveAttribute("type", "password");
expect(numberInput).toHaveAttribute("type", "number");
});
test("handles different sizes and label variants", () => {
render(
<div>
<Input label="Small Default" size="small" labelVariant="default" />
<Input
label="Small Horizontal"
size="small"
labelVariant="horizontal"
/>
<Input label="Medium Default" size="medium" labelVariant="default" />
<Input
label="Medium Horizontal"
size="medium"
labelVariant="horizontal"
/>
<Input label="Large Default" size="large" labelVariant="default" />
<Input
label="Large Horizontal"
size="large"
labelVariant="horizontal"
/>
</div>,
);
// All inputs should be present
expect(screen.getByLabelText("Small Default")).toBeInTheDocument();
expect(screen.getByLabelText("Small Horizontal")).toBeInTheDocument();
expect(screen.getByLabelText("Medium Default")).toBeInTheDocument();
expect(screen.getByLabelText("Medium Horizontal")).toBeInTheDocument();
expect(screen.getByLabelText("Large Default")).toBeInTheDocument();
expect(screen.getByLabelText("Large Horizontal")).toBeInTheDocument();
});
test("handles disabled state integration", () => {
const handleChange = vi.fn();
render(
<Input
label="Disabled Input"
disabled={true}
onChange={handleChange}
onFocus={vi.fn()}
onBlur={vi.fn()}
/>,
);
const input = screen.getByLabelText("Disabled Input");
// Should be disabled
expect(input).toBeDisabled();
// Should not call handlers
fireEvent.change(input, { target: { value: "test" } });
fireEvent.focus(input);
fireEvent.blur(input);
expect(handleChange).not.toHaveBeenCalled();
});
test("handles error state integration", () => {
render(<Input label="Error Input" error={true} />);
const input = screen.getByLabelText("Error Input");
expect(input).toHaveClass(
"border-[var(--color-border-default-utility-negative)]",
);
expect(input).not.toBeDisabled();
});
test("handles state transitions", async () => {
const TestStateTransitions = () => {
const [state, setState] = useState("default");
return (
<div>
<Input
label="State Test"
state={state}
onFocus={() => setState("focus")}
onBlur={() => setState("default")}
/>
<button onClick={() => setState("hover")}>Set Hover</button>
<button onClick={() => setState("active")}>Set Active</button>
</div>
);
};
render(<TestStateTransitions />);
const input = screen.getByLabelText("State Test");
const hoverButton = screen.getByText("Set Hover");
const activeButton = screen.getByText("Set Active");
// Initial state
expect(input).toHaveClass("border-[var(--color-border-default-tertiary)]");
// Set hover state
fireEvent.click(hoverButton);
expect(input).toHaveClass("border-[var(--color-border-default-tertiary)]");
expect(input).toHaveClass(
"shadow-[0_0_0_2px_var(--color-border-default-tertiary)]",
);
// Set active state
fireEvent.click(activeButton);
expect(input).toHaveClass("border-[var(--color-border-default-tertiary)]");
// Focus state
fireEvent.focus(input);
expect(input).toHaveClass(
"border-[var(--color-border-default-utility-info)]",
);
expect(input).toHaveClass("shadow-[0_0_5px_3px_#3281F8]");
});
test("handles keyboard navigation between inputs", () => {
render(
<div>
<Input label="First Input" />
<Input label="Second Input" />
<Input label="Third Input" />
</div>,
);
const firstInput = screen.getByLabelText("First Input");
const secondInput = screen.getByLabelText("Second Input");
const thirdInput = screen.getByLabelText("Third Input");
// Focus first input
firstInput.focus();
expect(firstInput).toHaveFocus();
// Tab to second input - simulate actual tab behavior
fireEvent.keyDown(firstInput, { key: "Tab" });
// Manually focus the second input since tab navigation doesn't work in jsdom
secondInput.focus();
expect(secondInput).toHaveFocus();
// Tab to third input
fireEvent.keyDown(secondInput, { key: "Tab" });
// Manually focus the third input
thirdInput.focus();
expect(thirdInput).toHaveFocus();
// Shift+Tab back to second input
fireEvent.keyDown(thirdInput, { key: "Tab", shiftKey: true });
// Manually focus the second input
secondInput.focus();
expect(secondInput).toHaveFocus();
});
test("handles form submission", () => {
const handleSubmit = vi.fn();
render(
<form onSubmit={handleSubmit}>
<Input label="Test Input" name="testField" />
<button type="submit">Submit</button>
</form>,
);
const input = screen.getByLabelText("Test Input");
const submitButton = screen.getByText("Submit");
// Type in input
fireEvent.change(input, { target: { value: "test value" } });
// Submit form
fireEvent.click(submitButton);
expect(handleSubmit).toHaveBeenCalled();
});
test("handles ref forwarding", () => {
const TestRefComponent = () => {
const inputRef = React.useRef();
const focusInput = () => {
inputRef.current?.focus();
};
return (
<div>
<Input ref={inputRef} label="Ref Test" />
<button onClick={focusInput}>Focus Input</button>
</div>
);
};
render(<TestRefComponent />);
const input = screen.getByLabelText("Ref Test");
const focusButton = screen.getByText("Focus Input");
// Click button to focus input via ref
fireEvent.click(focusButton);
expect(input).toHaveFocus();
});
test("handles dynamic prop changes", () => {
const TestDynamicProps = () => {
const [disabled, setDisabled] = useState(false);
const [error, setError] = useState(false);
return (
<div>
<Input label="Dynamic Input" disabled={disabled} error={error} />
<button onClick={() => setDisabled(!disabled)}>
Toggle Disabled
</button>
<button onClick={() => setError(!error)}>Toggle Error</button>
</div>
);
};
render(<TestDynamicProps />);
const input = screen.getByLabelText("Dynamic Input");
const toggleDisabledButton = screen.getByText("Toggle Disabled");
const toggleErrorButton = screen.getByText("Toggle Error");
// Initial state
expect(input).not.toBeDisabled();
expect(input).not.toHaveClass(
"border-[var(--color-border-default-utility-negative)]",
);
// Toggle disabled
fireEvent.click(toggleDisabledButton);
expect(input).toBeDisabled();
// Toggle error - but first disable the disabled state so error can be tested
fireEvent.click(toggleDisabledButton); // Turn off disabled
fireEvent.click(toggleErrorButton); // Turn on error
// The error state applies the border color through the stateStyles.input class
expect(input).toHaveClass(
"border-[var(--color-border-default-utility-negative)]",
);
});
});
@@ -1,365 +0,0 @@
import React, { useState } from "react";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, it, expect, vi } from "vitest";
import RadioButton from "../../app/components/RadioButton";
describe("RadioButton Integration", () => {
it("works in form context", async () => {
const user = userEvent.setup();
const handleSubmit = vi.fn();
function TestForm() {
const [value, setValue] = useState("option1");
return (
<form onSubmit={handleSubmit}>
<RadioButton
label="Option 1"
name="test-radio"
value="option1"
checked={value === "option1"}
onChange={({ checked }) => checked && setValue("option1")}
/>
<RadioButton
label="Option 2"
name="test-radio"
value="option2"
checked={value === "option2"}
onChange={({ checked }) => checked && setValue("option2")}
/>
<button type="submit">Submit</button>
</form>
);
}
render(<TestForm />);
const option2 = screen.getByText("Option 2").closest("label");
const submitButton = screen.getByRole("button");
// Initially option1 should be selected
expect(screen.getByDisplayValue("option1")).toBeChecked();
expect(screen.getByDisplayValue("option2")).not.toBeChecked();
// Click option2
await user.click(option2);
expect(screen.getByDisplayValue("option2")).toBeChecked();
expect(screen.getByDisplayValue("option1")).not.toBeChecked();
// Submit form
await user.click(submitButton);
expect(handleSubmit).toHaveBeenCalled();
});
it("handles keyboard navigation", async () => {
const user = userEvent.setup();
function KeyboardForm() {
const [value, setValue] = useState("option1");
return (
<div>
<RadioButton
label="Option 1"
name="keyboard-radio"
value="option1"
checked={value === "option1"}
onChange={({ checked }) => checked && setValue("option1")}
/>
<RadioButton
label="Option 2"
name="keyboard-radio"
value="option2"
checked={value === "option2"}
onChange={({ checked }) => checked && setValue("option2")}
/>
</div>
);
}
render(<KeyboardForm />);
const radioButtons = screen.getAllByRole("radio");
// Focus first radio button
radioButtons[0].focus();
expect(radioButtons[0]).toHaveFocus();
// Navigate to second radio button
await user.tab();
expect(radioButtons[1]).toHaveFocus();
// Activate with Space
await user.keyboard(" ");
expect(screen.getByDisplayValue("option2")).toBeChecked();
});
it("handles mode switching", async () => {
function ModeSwitchForm() {
const [mode, setMode] = useState("standard");
const [value, setValue] = useState("option1");
return (
<div>
<button
onClick={() =>
setMode(mode === "standard" ? "inverse" : "standard")
}
>
Toggle Mode
</button>
<RadioButton
label="Test Radio"
name="mode-radio"
value="option1"
checked={value === "option1"}
mode={mode}
onChange={({ checked }) => checked && setValue("option1")}
/>
</div>
);
}
const user = userEvent.setup();
render(<ModeSwitchForm />);
const toggleButton = screen.getByRole("button");
const radioButton = screen.getByRole("radio");
// Initially standard mode
expect(radioButton).toHaveClass(
"outline-[var(--color-border-default-tertiary)]",
);
// Switch to inverse mode
await user.click(toggleButton);
expect(radioButton).toHaveClass(
"outline-[var(--color-border-inverse-primary)]",
);
});
it("maintains state across re-renders", () => {
function StateForm() {
const [value, setValue] = useState("option1");
const [count, setCount] = useState(0);
return (
<div>
<button onClick={() => setCount(count + 1)}>
Re-render ({count})
</button>
<RadioButton
label="Test Radio"
name="state-radio"
value="option1"
checked={value === "option1"}
onChange={({ checked }) => checked && setValue("option1")}
/>
</div>
);
}
const user = userEvent.setup();
render(<StateForm />);
const radioButton = screen.getByRole("radio");
const reRenderButton = screen.getByRole("button");
// Should be checked initially
expect(radioButton).toHaveAttribute("aria-checked", "true");
// Re-render should maintain state
user.click(reRenderButton);
expect(radioButton).toHaveAttribute("aria-checked", "true");
});
it("works with multiple radio groups", async () => {
function MultiGroupForm() {
const [group1Value, setGroup1Value] = useState("option1");
const [group2Value, setGroup2Value] = useState("option1");
return (
<div>
<div>
<h3>Group 1</h3>
<RadioButton
label="Option A"
name="group1"
value="option1"
checked={group1Value === "option1"}
onChange={({ checked }) => checked && setGroup1Value("option1")}
/>
<RadioButton
label="Option B"
name="group1"
value="option2"
checked={group1Value === "option2"}
onChange={({ checked }) => checked && setGroup1Value("option2")}
/>
</div>
<div>
<h3>Group 2</h3>
<RadioButton
label="Option X"
name="group2"
value="option1"
checked={group2Value === "option1"}
onChange={({ checked }) => checked && setGroup2Value("option1")}
/>
<RadioButton
label="Option Y"
name="group2"
value="option2"
checked={group2Value === "option2"}
onChange={({ checked }) => checked && setGroup2Value("option2")}
/>
</div>
</div>
);
}
const user = userEvent.setup();
render(<MultiGroupForm />);
// Both groups should work independently
const group1OptionB = screen.getByText("Option B").closest("label");
const group2OptionY = screen.getByText("Option Y").closest("label");
await user.click(group1OptionB);
await user.click(group2OptionY);
const group1Inputs = screen
.getAllByDisplayValue("option2")
.filter((input) => input.getAttribute("name") === "group1");
const group2Inputs = screen
.getAllByDisplayValue("option2")
.filter((input) => input.getAttribute("name") === "group2");
expect(group1Inputs[0]).toBeChecked();
expect(group2Inputs[0]).toBeChecked();
});
it("handles controlled and uncontrolled scenarios", async () => {
function ControlledForm() {
const [controlledValue, setControlledValue] = useState("option1");
const [uncontrolledValue, setUncontrolledValue] = useState("option1");
return (
<div>
<div>
<h3>Controlled</h3>
<RadioButton
label="Controlled Option 1"
name="controlled"
value="option1"
checked={controlledValue === "option1"}
onChange={({ checked }) =>
checked && setControlledValue("option1")
}
/>
<RadioButton
label="Controlled Option 2"
name="controlled"
value="option2"
checked={controlledValue === "option2"}
onChange={({ checked }) =>
checked && setControlledValue("option2")
}
/>
</div>
<div>
<h3>Uncontrolled</h3>
<RadioButton
label="Uncontrolled Option 1"
name="uncontrolled"
value="option1"
checked={uncontrolledValue === "option1"}
onChange={({ checked }) =>
checked && setUncontrolledValue("option1")
}
/>
<RadioButton
label="Uncontrolled Option 2"
name="uncontrolled"
value="option2"
checked={uncontrolledValue === "option2"}
onChange={({ checked }) =>
checked && setUncontrolledValue("option2")
}
/>
</div>
</div>
);
}
const user = userEvent.setup();
render(<ControlledForm />);
// Both should work the same way
const controlledOption2 = screen
.getByText("Controlled Option 2")
.closest("label");
const uncontrolledOption2 = screen
.getByText("Uncontrolled Option 2")
.closest("label");
await user.click(controlledOption2);
await user.click(uncontrolledOption2);
const controlledInputs = screen
.getAllByDisplayValue("option2")
.filter((input) => input.getAttribute("name") === "controlled");
const uncontrolledInputs = screen
.getAllByDisplayValue("option2")
.filter((input) => input.getAttribute("name") === "uncontrolled");
expect(controlledInputs[0]).toBeChecked();
expect(uncontrolledInputs[0]).toBeChecked();
});
it("handles accessibility in complex forms", () => {
function AccessibleForm() {
const [value, setValue] = useState("option1");
return (
<form>
<fieldset>
<legend>Choose an option</legend>
<RadioButton
label="Option 1"
name="accessible-radio"
value="option1"
checked={value === "option1"}
onChange={({ checked }) => checked && setValue("option1")}
ariaLabel="First option"
/>
<RadioButton
label="Option 2"
name="accessible-radio"
value="option2"
checked={value === "option2"}
onChange={({ checked }) => checked && setValue("option2")}
ariaLabel="Second option"
/>
</fieldset>
</form>
);
}
render(<AccessibleForm />);
const radioButtons = screen.getAllByRole("radio");
// Should have proper accessibility attributes
radioButtons.forEach((button) => {
expect(button).toHaveAttribute("role", "radio");
expect(button).toHaveAttribute("aria-checked");
expect(button).toHaveAttribute("tabIndex", "0");
});
// Should have aria-labels
expect(radioButtons[0]).toHaveAttribute("aria-label", "First option");
expect(radioButtons[1]).toHaveAttribute("aria-label", "Second option");
});
});
@@ -1,430 +0,0 @@
import React, { useState } from "react";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, it, expect, vi } from "vitest";
import RadioGroup from "../../app/components/RadioGroup";
describe("RadioGroup Integration", () => {
const defaultOptions = [
{ value: "option1", label: "Option 1" },
{ value: "option2", label: "Option 2" },
{ value: "option3", label: "Option 3" },
];
it("works in form context", async () => {
const user = userEvent.setup();
const handleSubmit = vi.fn();
function TestForm() {
const [value, setValue] = useState("option1");
return (
<form onSubmit={handleSubmit}>
<RadioGroup
name="test-radio-group"
value={value}
options={defaultOptions}
onChange={({ value }) => setValue(value)}
/>
<button type="submit">Submit</button>
</form>
);
}
render(<TestForm />);
const option2 = screen.getByText("Option 2").closest("label");
const submitButton = screen.getByRole("button");
// Initially option1 should be selected
expect(screen.getByDisplayValue("option1")).toBeChecked();
expect(screen.getByDisplayValue("option2")).not.toBeChecked();
// Click option2
await user.click(option2);
expect(screen.getByDisplayValue("option2")).toBeChecked();
expect(screen.getByDisplayValue("option1")).not.toBeChecked();
// Submit form
await user.click(submitButton);
expect(handleSubmit).toHaveBeenCalled();
});
it("handles keyboard navigation", async () => {
const user = userEvent.setup();
function KeyboardForm() {
const [value, setValue] = useState("option1");
return (
<RadioGroup
name="keyboard-radio-group"
value={value}
options={defaultOptions}
onChange={({ value }) => setValue(value)}
/>
);
}
render(<KeyboardForm />);
const radioButtons = screen.getAllByRole("radio");
// Focus first radio button
radioButtons[0].focus();
expect(radioButtons[0]).toHaveFocus();
// Navigate to second radio button
await user.tab();
expect(radioButtons[1]).toHaveFocus();
// Activate with Space
await user.keyboard(" ");
expect(screen.getByDisplayValue("option2")).toBeChecked();
});
it("handles mode switching", async () => {
function ModeSwitchForm() {
const [mode, setMode] = useState("standard");
const [value, setValue] = useState("option1");
return (
<div>
<button
onClick={() =>
setMode(mode === "standard" ? "inverse" : "standard")
}
>
Toggle Mode
</button>
<RadioGroup
name="mode-radio-group"
value={value}
mode={mode}
options={defaultOptions}
onChange={({ value }) => setValue(value)}
/>
</div>
);
}
const user = userEvent.setup();
render(<ModeSwitchForm />);
const toggleButton = screen.getByRole("button");
const radioButtons = screen.getAllByRole("radio");
// Initially standard mode
radioButtons.forEach((button) => {
expect(button).toHaveClass(
"outline-[var(--color-border-default-tertiary)]",
);
});
// Switch to inverse mode
await user.click(toggleButton);
radioButtons.forEach((button) => {
expect(button).toHaveClass(
"outline-[var(--color-border-inverse-primary)]",
);
});
});
it("maintains state across re-renders", () => {
function StateForm() {
const [value, setValue] = useState("option1");
const [count, setCount] = useState(0);
return (
<div>
<button onClick={() => setCount(count + 1)}>
Re-render ({count})
</button>
<RadioGroup
name="state-radio-group"
value={value}
options={defaultOptions}
onChange={({ value }) => setValue(value)}
/>
</div>
);
}
const user = userEvent.setup();
render(<StateForm />);
const radioButtons = screen.getAllByRole("radio");
const reRenderButton = screen.getByRole("button");
// Should be checked initially
expect(radioButtons[0]).toHaveAttribute("aria-checked", "true");
// Re-render should maintain state
user.click(reRenderButton);
expect(radioButtons[0]).toHaveAttribute("aria-checked", "true");
});
it("works with multiple radio groups", async () => {
function MultiGroupForm() {
const [group1Value, setGroup1Value] = useState("option1");
const [group2Value, setGroup2Value] = useState("option1");
return (
<div>
<div>
<h3>Group 1</h3>
<RadioGroup
name="group1"
value={group1Value}
options={defaultOptions}
onChange={({ value }) => setGroup1Value(value)}
/>
</div>
<div>
<h3>Group 2</h3>
<RadioGroup
name="group2"
value={group2Value}
options={defaultOptions}
onChange={({ value }) => setGroup2Value(value)}
/>
</div>
</div>
);
}
const user = userEvent.setup();
render(<MultiGroupForm />);
// Both groups should work independently
// Find the Option 2 in group1 by filtering getAllByDisplayValue by name
const group1Option2Input = screen
.getAllByDisplayValue("option2")
.find((input) => input.getAttribute("name") === "group1");
const group1Option2 = group1Option2Input.closest("label");
// Find the Option 3 in group2 by filtering getAllByDisplayValue by name
const group2Option3Input = screen
.getAllByDisplayValue("option3")
.find((input) => input.getAttribute("name") === "group2");
const group2Option3 = group2Option3Input.closest("label");
await user.click(group1Option2);
await user.click(group2Option3);
const group1Inputs = screen
.getAllByDisplayValue("option2")
.filter((input) => input.getAttribute("name") === "group1");
const group2Inputs = screen
.getAllByDisplayValue("option3")
.filter((input) => input.getAttribute("name") === "group2");
expect(group1Inputs[0]).toBeChecked();
expect(group2Inputs[0]).toBeChecked();
});
it("handles controlled and uncontrolled scenarios", async () => {
function ControlledForm() {
const [controlledValue, setControlledValue] = useState("option1");
const [uncontrolledValue, setUncontrolledValue] = useState("option1");
return (
<div>
<div>
<h3>Controlled</h3>
<RadioGroup
name="controlled"
value={controlledValue}
options={defaultOptions}
onChange={({ value }) => setControlledValue(value)}
/>
</div>
<div>
<h3>Uncontrolled</h3>
<RadioGroup
name="uncontrolled"
value={uncontrolledValue}
options={defaultOptions}
onChange={({ value }) => setUncontrolledValue(value)}
/>
</div>
</div>
);
}
const user = userEvent.setup();
render(<ControlledForm />);
// Both should work the same way
// Find the Option 2 in controlled group by filtering getAllByDisplayValue by name
const controlledOption2Input = screen
.getAllByDisplayValue("option2")
.find((input) => input.getAttribute("name") === "controlled");
const controlledOption2 = controlledOption2Input.closest("label");
// Find the Option 2 in uncontrolled group by filtering getAllByDisplayValue by name
const uncontrolledOption2Input = screen
.getAllByDisplayValue("option2")
.find((input) => input.getAttribute("name") === "uncontrolled");
const uncontrolledOption2 = uncontrolledOption2Input.closest("label");
await user.click(controlledOption2);
await user.click(uncontrolledOption2);
const controlledInputs = screen
.getAllByDisplayValue("option2")
.filter((input) => input.getAttribute("name") === "controlled");
const uncontrolledInputs = screen
.getAllByDisplayValue("option2")
.filter((input) => input.getAttribute("name") === "uncontrolled");
expect(controlledInputs[0]).toBeChecked();
expect(uncontrolledInputs[0]).toBeChecked();
});
it("handles accessibility in complex forms", () => {
function AccessibleForm() {
const [value, setValue] = useState("option1");
const accessibleOptions = [
{ value: "option1", label: "Option 1", ariaLabel: "First option" },
{ value: "option2", label: "Option 2", ariaLabel: "Second option" },
{ value: "option3", label: "Option 3", ariaLabel: "Third option" },
];
return (
<form>
<fieldset>
<legend>Choose an option</legend>
<RadioGroup
name="accessible-radio-group"
value={value}
options={accessibleOptions}
onChange={({ value }) => setValue(value)}
aria-label="Accessible radio group"
/>
</fieldset>
</form>
);
}
render(<AccessibleForm />);
const radioGroup = screen.getByRole("radiogroup");
const radioButtons = screen.getAllByRole("radio");
// Should have proper accessibility attributes
expect(radioGroup).toHaveAttribute("aria-label", "Accessible radio group");
radioButtons.forEach((button) => {
expect(button).toHaveAttribute("role", "radio");
expect(button).toHaveAttribute("aria-checked");
expect(button).toHaveAttribute("tabIndex", "0");
});
// Should have aria-labels
expect(radioButtons[0]).toHaveAttribute("aria-label", "First option");
expect(radioButtons[1]).toHaveAttribute("aria-label", "Second option");
expect(radioButtons[2]).toHaveAttribute("aria-label", "Third option");
});
it("handles dynamic options", async () => {
function DynamicForm() {
const [value, setValue] = useState("option1");
const [options, setOptions] = useState(defaultOptions);
return (
<div>
<button
onClick={() =>
setOptions([...options, { value: "option4", label: "Option 4" }])
}
>
Add Option
</button>
<RadioGroup
name="dynamic-radio-group"
value={value}
options={options}
onChange={({ value }) => setValue(value)}
/>
</div>
);
}
const user = userEvent.setup();
render(<DynamicForm />);
const addButton = screen.getByRole("button");
// Initially 3 options
expect(screen.getAllByRole("radio")).toHaveLength(3);
// Add option
await user.click(addButton);
expect(screen.getAllByRole("radio")).toHaveLength(4);
expect(screen.getByText("Option 4")).toBeInTheDocument();
});
it("handles empty options gracefully", () => {
function EmptyForm() {
const [value, setValue] = useState("");
return (
<RadioGroup
name="empty-radio-group"
value={value}
options={[]}
onChange={({ value }) => setValue(value)}
/>
);
}
render(<EmptyForm />);
const radioGroup = screen.getByRole("radiogroup");
expect(radioGroup).toBeInTheDocument();
expect(screen.queryAllByRole("radio")).toHaveLength(0);
});
it("maintains single selection behavior", async () => {
const user = userEvent.setup();
const handleChange = vi.fn();
function SingleSelectionForm() {
const [value, setValue] = useState("option1");
return (
<RadioGroup
name="single-selection-radio-group"
value={value}
options={defaultOptions}
onChange={({ value }) => {
setValue(value);
handleChange(value);
}}
/>
);
}
render(<SingleSelectionForm />);
const radioButtons = screen.getAllByRole("radio");
// Initially option1 should be selected
expect(radioButtons[0]).toHaveAttribute("aria-checked", "true");
expect(radioButtons[1]).toHaveAttribute("aria-checked", "false");
expect(radioButtons[2]).toHaveAttribute("aria-checked", "false");
// Click option2
const option2 = screen.getByText("Option 2").closest("label");
await user.click(option2);
// Only option2 should be selected
expect(radioButtons[0]).toHaveAttribute("aria-checked", "false");
expect(radioButtons[1]).toHaveAttribute("aria-checked", "true");
expect(radioButtons[2]).toHaveAttribute("aria-checked", "false");
expect(handleChange).toHaveBeenCalledWith("option2");
});
});
@@ -1,214 +0,0 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { render, screen } from "@testing-library/react";
import RelatedArticles from "../../app/components/RelatedArticles";
// Mock ContentThumbnailTemplate
vi.mock("../../app/components/ContentThumbnailTemplate", () => ({
default: ({ post, variant }) => (
<div data-testid={`thumbnail-${post.slug}`} data-variant={variant}>
<a
href={`/blog/${post.slug}`}
data-testid={`thumbnail-link-${post.slug}`}
>
<h3>{post.frontmatter?.title || "Untitled"}</h3>
<p>{post.frontmatter?.description || "No description"}</p>
</a>
</div>
),
}));
// Mock blog post data
const mockRelatedPosts = [
{
slug: "resolving-active-conflicts",
frontmatter: {
title: "Resolving Active Conflicts",
description:
"Practical steps for resolving conflicts while maintaining trust",
author: "Test Author",
date: "2025-04-15",
},
},
{
slug: "operational-security-mutual-aid",
frontmatter: {
title: "Operational Security for Mutual Aid",
description:
"Tactics to protect members, secure communication, and prevent infiltration",
author: "Test Author",
date: "2025-04-14",
},
},
{
slug: "making-decisions-without-hierarchy",
frontmatter: {
title: "Making Decisions Without Hierarchy",
description:
"A brief guide to collaborative nonhierarchical decision making",
author: "Test Author",
date: "2025-04-13",
},
},
{
slug: "building-community-trust",
frontmatter: {
title: "Building Community Trust",
description: "Strategies for fostering trust in community organizations",
author: "Test Author",
date: "2025-04-12",
},
},
];
describe("Related Articles Integration", () => {
beforeEach(() => {
// Mock window.innerWidth for responsive tests
Object.defineProperty(window, "innerWidth", {
writable: true,
configurable: true,
value: 1024, // Desktop width
});
});
it("should filter out current post from related articles", () => {
render(
<RelatedArticles
relatedPosts={mockRelatedPosts}
currentPostSlug="resolving-active-conflicts"
/>,
);
// Current post should not be displayed
expect(
screen.queryByTestId("thumbnail-resolving-active-conflicts"),
).not.toBeInTheDocument();
// Other posts should be displayed
expect(
screen.getByTestId("thumbnail-operational-security-mutual-aid"),
).toBeInTheDocument();
expect(
screen.getByTestId("thumbnail-making-decisions-without-hierarchy"),
).toBeInTheDocument();
expect(
screen.getByTestId("thumbnail-building-community-trust"),
).toBeInTheDocument();
});
it("should display all posts when no current post is specified", () => {
render(<RelatedArticles relatedPosts={mockRelatedPosts} />);
// All posts should be displayed
expect(
screen.getByTestId("thumbnail-resolving-active-conflicts"),
).toBeInTheDocument();
expect(
screen.getByTestId("thumbnail-operational-security-mutual-aid"),
).toBeInTheDocument();
expect(
screen.getByTestId("thumbnail-making-decisions-without-hierarchy"),
).toBeInTheDocument();
expect(
screen.getByTestId("thumbnail-building-community-trust"),
).toBeInTheDocument();
});
it("should create correct links for each thumbnail", () => {
render(
<RelatedArticles
relatedPosts={mockRelatedPosts}
currentPostSlug="resolving-active-conflicts"
/>,
);
// Verify links are created correctly
expect(
screen.getByTestId("thumbnail-link-operational-security-mutual-aid"),
).toHaveAttribute("href", "/blog/operational-security-mutual-aid");
expect(
screen.getByTestId("thumbnail-link-making-decisions-without-hierarchy"),
).toHaveAttribute("href", "/blog/making-decisions-without-hierarchy");
expect(
screen.getByTestId("thumbnail-link-building-community-trust"),
).toHaveAttribute("href", "/blog/building-community-trust");
});
it("should handle empty related posts array", () => {
const { container } = render(
<RelatedArticles relatedPosts={[]} currentPostSlug="test-post" />,
);
expect(container.firstChild).toBeNull();
});
it("should handle single related post", () => {
const singlePost = [mockRelatedPosts[0]];
render(
<RelatedArticles
relatedPosts={singlePost}
currentPostSlug="different-post"
/>,
);
expect(
screen.getByTestId("thumbnail-resolving-active-conflicts"),
).toBeInTheDocument();
expect(
screen.queryByTestId("thumbnail-operational-security-mutual-aid"),
).not.toBeInTheDocument();
});
it("should handle all posts being filtered out", () => {
const currentPostOnly = [mockRelatedPosts[0]];
const { container } = render(
<RelatedArticles
relatedPosts={currentPostOnly}
currentPostSlug="resolving-active-conflicts"
/>,
);
expect(container.firstChild).toBeNull();
});
it("should display section heading", () => {
render(
<RelatedArticles
relatedPosts={mockRelatedPosts}
currentPostSlug="resolving-active-conflicts"
/>,
);
expect(screen.getByRole("heading", { level: 2 })).toHaveTextContent(
"Related Articles",
);
});
it("should maintain consistent structure across different current posts", () => {
const slugs = [
"resolving-active-conflicts",
"operational-security-mutual-aid",
"making-decisions-without-hierarchy",
];
slugs.forEach((slug) => {
const { unmount } = render(
<RelatedArticles
relatedPosts={mockRelatedPosts}
currentPostSlug={slug}
/>,
);
// Verify consistent structure
expect(screen.getByRole("heading", { level: 2 })).toHaveTextContent(
"Related Articles",
);
// Check that we have some thumbnails (the exact ones depend on the current post)
const thumbnails = screen.getAllByTestId(/thumbnail-/);
expect(thumbnails.length).toBeGreaterThan(0);
unmount();
});
});
});
@@ -1,407 +0,0 @@
import React, { useState } from "react";
import { render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { expect, describe, it } from "vitest";
import Select from "../../app/components/Select";
describe("Select Component Integration", () => {
const TestForm = ({ initialValue = "" }) => {
const [value, setValue] = useState(initialValue);
const [errors, setErrors] = useState({});
const handleChange = (newValue) => {
setValue(newValue);
if (errors.select) {
setErrors({ ...errors, select: null });
}
};
const handleSubmit = (e) => {
e.preventDefault();
if (!value) {
setErrors({ select: "Please select an option" });
}
};
return (
<form onSubmit={handleSubmit}>
<Select
label="Test Select"
placeholder="Select an option"
value={value}
onChange={handleChange}
error={!!errors.select}
options={[
{ value: "option1", label: "Option 1" },
{ value: "option2", label: "Option 2" },
{ value: "option3", label: "Option 3" },
]}
/>
{errors.select && <div data-testid="error">{errors.select}</div>}
<button type="submit">Submit</button>
</form>
);
};
describe("Form Integration", () => {
it("integrates with form submission", async () => {
const user = userEvent.setup();
render(<TestForm />);
const selectButton = screen.getByRole("button", { name: /Test Select/ });
await user.click(selectButton);
await waitFor(() => {
expect(screen.getByText("Option 1")).toBeInTheDocument();
});
await user.click(screen.getByText("Option 1"));
const submitButton = screen.getByRole("button", { name: "Submit" });
await user.click(submitButton);
expect(screen.queryByTestId("error")).not.toBeInTheDocument();
});
it("shows validation error when no option selected", async () => {
const user = userEvent.setup();
render(<TestForm />);
const submitButton = screen.getByRole("button", { name: "Submit" });
await user.click(submitButton);
expect(screen.getByTestId("error")).toHaveTextContent(
"Please select an option",
);
});
it("clears error when option is selected", async () => {
const user = userEvent.setup();
render(<TestForm />);
const submitButton = screen.getByRole("button", { name: "Submit" });
await user.click(submitButton);
expect(screen.getByTestId("error")).toBeInTheDocument();
const selectButton = screen.getByRole("button", { name: /Test Select/ });
await user.click(selectButton);
await waitFor(() => {
expect(screen.getByText("Option 1")).toBeInTheDocument();
});
await user.click(screen.getByText("Option 1"));
expect(screen.queryByTestId("error")).not.toBeInTheDocument();
});
});
describe("Multiple Select Components", () => {
const MultiSelectForm = () => {
const [values, setValues] = useState({ select1: "", select2: "" });
const handleChange = (field) => (newValue) => {
setValues({ ...values, [field]: newValue });
};
return (
<div>
<Select
label="First Select"
placeholder="Select first option"
value={values.select1}
onChange={handleChange("select1")}
options={[
{ value: "a1", label: "A1" },
{ value: "a2", label: "A2" },
]}
/>
<Select
label="Second Select"
placeholder="Select second option"
value={values.select2}
onChange={handleChange("select2")}
options={[
{ value: "b1", label: "B1" },
{ value: "b2", label: "B2" },
]}
/>
</div>
);
};
it("handles multiple select components independently", async () => {
const user = userEvent.setup();
render(<MultiSelectForm />);
const firstSelect = screen.getByRole("button", {
name: /First Select/,
});
const secondSelect = screen.getByRole("button", {
name: /Second Select/,
});
await user.click(firstSelect);
await waitFor(() => {
expect(screen.getByText("A1")).toBeInTheDocument();
});
await user.click(screen.getByText("A1"));
await user.click(secondSelect);
await waitFor(() => {
expect(screen.getByText("B1")).toBeInTheDocument();
});
await user.click(screen.getByText("B1"));
expect(firstSelect).toHaveTextContent("A1");
expect(secondSelect).toHaveTextContent("B1");
});
it("closes one dropdown when another is opened", async () => {
const user = userEvent.setup();
render(<MultiSelectForm />);
const firstSelect = screen.getByRole("button", {
name: /First Select/,
});
const secondSelect = screen.getByRole("button", {
name: /Second Select/,
});
await user.click(firstSelect);
await waitFor(() => {
expect(screen.getByText("A1")).toBeInTheDocument();
});
await user.click(secondSelect);
await waitFor(() => {
expect(screen.queryByText("A1")).not.toBeInTheDocument();
expect(screen.getByText("B1")).toBeInTheDocument();
});
});
});
describe("Keyboard Navigation Between Components", () => {
const KeyboardForm = () => {
const [values, setValues] = useState({ select1: "", select2: "" });
return (
<div>
<input placeholder="First input" />
<Select
label="First Select"
placeholder="Select first option"
value={values.select1}
onChange={(value) => setValues({ ...values, select1: value })}
options={[{ value: "a1", label: "A1" }]}
/>
<input placeholder="Second input" />
<Select
label="Second Select"
placeholder="Select second option"
value={values.select2}
onChange={(value) => setValues({ ...values, select2: value })}
options={[{ value: "b1", label: "B1" }]}
/>
</div>
);
};
it("handles keyboard navigation between inputs and selects", async () => {
const user = userEvent.setup();
render(<KeyboardForm />);
const firstInput = screen.getByPlaceholderText("First input");
const firstSelect = screen.getByRole("button", {
name: /First Select/,
});
const secondInput = screen.getByPlaceholderText("Second input");
const secondSelect = screen.getByRole("button", {
name: /Second Select/,
});
await user.tab();
expect(firstInput).toHaveFocus();
await user.tab();
expect(firstSelect).toHaveFocus();
await user.tab();
expect(secondInput).toHaveFocus();
await user.tab();
expect(secondSelect).toHaveFocus();
});
it("opens select with Enter key during tab navigation", async () => {
const user = userEvent.setup();
render(<KeyboardForm />);
const firstSelect = screen.getByRole("button", {
name: /First Select/,
});
await user.tab();
await user.tab();
expect(firstSelect).toHaveFocus();
await user.keyboard("{Enter}");
await waitFor(() => {
expect(screen.getByText("A1")).toBeInTheDocument();
});
});
});
describe("Dynamic Prop Changes", () => {
const DynamicSelect = ({ disabled, error, size }) => {
const [value, setValue] = useState("");
return (
<Select
label="Dynamic Select"
placeholder="Select an option"
value={value}
onChange={setValue}
disabled={disabled}
error={error}
size={size}
options={[
{ value: "option1", label: "Option 1" },
{ value: "option2", label: "Option 2" },
]}
/>
);
};
it("handles dynamic disabled state changes", async () => {
const { rerender } = render(<DynamicSelect disabled={false} />);
const selectButton = screen.getByRole("button", {
name: /Dynamic Select/,
});
expect(selectButton).not.toBeDisabled();
rerender(<DynamicSelect disabled={true} />);
expect(selectButton).toBeDisabled();
rerender(<DynamicSelect disabled={false} />);
expect(selectButton).not.toBeDisabled();
});
it("handles dynamic error state changes", async () => {
const { rerender } = render(<DynamicSelect error={false} />);
const selectButton = screen.getByRole("button", {
name: /Dynamic Select/,
});
expect(selectButton).not.toHaveClass(
"border-[var(--color-border-default-utility-negative)]",
);
rerender(<DynamicSelect error={true} />);
expect(selectButton).toHaveClass(
"border-[var(--color-border-default-utility-negative)]",
);
rerender(<DynamicSelect error={false} />);
expect(selectButton).not.toHaveClass(
"border-[var(--color-border-default-utility-negative)]",
);
});
it("handles dynamic size changes", async () => {
const { rerender } = render(<DynamicSelect size="small" />);
const selectButton = screen.getByRole("button", {
name: /Dynamic Select/,
});
expect(selectButton).toHaveClass("h-[32px]");
rerender(<DynamicSelect size="medium" />);
expect(selectButton).toHaveClass("h-[36px]");
rerender(<DynamicSelect size="large" />);
expect(selectButton).toHaveClass("h-[40px]");
});
});
describe("Focus State Behavior", () => {
it("enters focus state when tabbed to (not active state)", async () => {
const user = userEvent.setup();
render(<TestForm />);
const selectButton = screen.getByRole("button", { name: /Test Select/ });
await user.tab();
expect(selectButton).toHaveFocus();
// Should have focus state styling, not active state
expect(selectButton).toHaveClass(
"focus-visible:border-[var(--color-border-default-utility-info)]",
);
});
it("does not enter focus state when clicked", async () => {
const user = userEvent.setup();
render(<TestForm />);
const selectButton = screen.getByRole("button", { name: /Test Select/ });
await user.click(selectButton);
expect(selectButton).toHaveFocus();
// Click should not trigger focus-visible styles (class is always present but only active on keyboard focus)
// The focus-visible class is always in the component but only applies on keyboard focus
expect(selectButton).toHaveClass(
"focus-visible:border-[var(--color-border-default-utility-info)]",
);
});
});
describe("Performance", () => {
it("handles rapid state changes without issues", async () => {
const user = userEvent.setup();
const { rerender } = render(<TestForm />);
const selectButton = screen.getByRole("button", { name: /Test Select/ });
// Rapidly change props
for (let i = 0; i < 10; i++) {
rerender(<TestForm />);
await user.click(selectButton);
await user.keyboard("{Escape}");
}
// Should still be functional
await user.click(selectButton);
await waitFor(() => {
expect(screen.getByText("Option 1")).toBeInTheDocument();
});
});
it("handles large option lists efficiently", async () => {
const user = userEvent.setup();
const largeOptions = Array.from({ length: 100 }, (_, i) => ({
value: `option${i}`,
label: `Option ${i}`,
}));
render(
<Select
label="Large Select"
placeholder="Select an option"
options={largeOptions}
/>,
);
const selectButton = screen.getByRole("button", { name: /Large Select/ });
await user.click(selectButton);
await waitFor(() => {
expect(screen.getByText("Option 0")).toBeInTheDocument();
expect(screen.getByText("Option 99")).toBeInTheDocument();
});
});
});
});
@@ -1,265 +0,0 @@
import React from "react";
import { render, screen, waitFor } from "@testing-library/react";
import { describe, it, expect, vi } from "vitest";
import userEvent from "@testing-library/user-event";
import Switch from "../../app/components/Switch";
// Test form component
const TestForm = ({ onSubmit }) => {
const [switch1, setSwitch1] = React.useState(false);
const [switch2, setSwitch2] = React.useState(true);
const handleSubmit = (e) => {
e.preventDefault();
onSubmit({ switch1, switch2 });
};
return (
<form onSubmit={handleSubmit}>
<Switch
checked={switch1}
onChange={() => setSwitch1(!switch1)}
label="First Switch"
/>
<Switch
checked={switch2}
onChange={() => setSwitch2(!switch2)}
label="Second Switch"
/>
<button type="submit">Submit</button>
</form>
);
};
// Dynamic switch component
const DynamicSwitch = ({ initialState = false }) => {
const [checked, setChecked] = React.useState(initialState);
// Update state when initialState prop changes
React.useEffect(() => {
setChecked(initialState);
}, [initialState]);
return (
<div>
<Switch
checked={checked}
onChange={() => setChecked(!checked)}
label="Dynamic Switch"
/>
</div>
);
};
describe("Switch Integration", () => {
it("handles form submission", async () => {
const user = userEvent.setup();
const handleSubmit = vi.fn();
render(<TestForm onSubmit={handleSubmit} />);
const submitButton = screen.getByRole("button", { name: "Submit" });
await user.click(submitButton);
expect(handleSubmit).toHaveBeenCalledWith({
switch1: false,
switch2: true,
});
});
it("handles keyboard navigation between switches", async () => {
const user = userEvent.setup();
render(
<div>
<Switch label="First Switch" />
<Switch label="Second Switch" />
<Switch label="Third Switch" />
</div>,
);
const switches = screen.getAllByRole("switch");
expect(switches).toHaveLength(3);
// Focus first switch
await user.tab();
expect(switches[0]).toHaveFocus();
// Tab to second switch
await user.tab();
expect(switches[1]).toHaveFocus();
// Tab to third switch
await user.tab();
expect(switches[2]).toHaveFocus();
});
it("handles dynamic prop changes", () => {
const { rerender } = render(<DynamicSwitch initialState={false} />);
let switchButton = screen.getByRole("switch");
expect(switchButton).toHaveAttribute("aria-checked", "false");
// Change initial state - the DynamicSwitch component should handle this internally
rerender(<DynamicSwitch initialState={true} />);
switchButton = screen.getByRole("switch");
// The DynamicSwitch component manages its own state, so it should be checked
expect(switchButton).toHaveAttribute("aria-checked", "true");
});
it("handles multiple switches in form", async () => {
const user = userEvent.setup();
const handleSubmit = vi.fn();
const TestForm = () => {
const [switch1, setSwitch1] = React.useState(false);
const [switch2, setSwitch2] = React.useState(false);
const [switch3, setSwitch3] = React.useState(false);
return (
<form
onSubmit={(e) => {
e.preventDefault();
handleSubmit();
}}
>
<Switch
label="Switch 1"
checked={switch1}
onChange={() => setSwitch1(!switch1)}
/>
<Switch
label="Switch 2"
checked={switch2}
onChange={() => setSwitch2(!switch2)}
/>
<Switch
label="Switch 3"
checked={switch3}
onChange={() => setSwitch3(!switch3)}
/>
<button type="submit">Submit</button>
</form>
);
};
render(<TestForm />);
const switches = screen.getAllByRole("switch");
expect(switches).toHaveLength(3);
// Toggle first switch
await user.click(switches[0]);
expect(switches[0]).toHaveAttribute("aria-checked", "true");
// Toggle second switch
await user.click(switches[1]);
expect(switches[1]).toHaveAttribute("aria-checked", "true");
// Submit form
const submitButton = screen.getByRole("button", { name: "Submit" });
await user.click(submitButton);
expect(handleSubmit).toHaveBeenCalled();
});
it("handles state changes", async () => {
const user = userEvent.setup();
const TestComponent = () => {
const [checked, setChecked] = React.useState(false);
return (
<div>
<Switch
checked={checked}
onChange={() => setChecked(!checked)}
label="Test Switch"
/>
</div>
);
};
render(<TestComponent />);
const switchButton = screen.getByRole("switch");
// Initially unchecked
expect(switchButton).toHaveAttribute("aria-checked", "false");
// Toggle checked state
await user.click(switchButton);
expect(switchButton).toHaveAttribute("aria-checked", "true");
});
it("handles content changes", () => {
const { rerender } = render(<Switch label="Original Label" />);
expect(screen.getByText("Original Label")).toBeInTheDocument();
rerender(<Switch label="Updated Label" />);
expect(screen.getByText("Updated Label")).toBeInTheDocument();
expect(screen.queryByText("Original Label")).not.toBeInTheDocument();
});
it("handles performance with many switches", () => {
const switches = Array.from({ length: 100 }, (_, i) => (
<Switch key={i} label={`Switch ${i + 1}`} />
));
const startTime = performance.now();
render(<div>{switches}</div>);
const endTime = performance.now();
// Should render within reasonable time (less than 1 second)
expect(endTime - startTime).toBeLessThan(1000);
const renderedSwitches = screen.getAllByRole("switch");
expect(renderedSwitches).toHaveLength(100);
});
it("handles rapid state changes", async () => {
const user = userEvent.setup();
const TestComponent = () => {
const [checked, setChecked] = React.useState(false);
return (
<Switch
checked={checked}
onChange={() => setChecked(!checked)}
label="Rapid Toggle Switch"
/>
);
};
render(<TestComponent />);
const switchButton = screen.getByRole("switch");
// Rapidly toggle the switch
for (let i = 0; i < 10; i++) {
await user.click(switchButton);
await waitFor(() => {
expect(switchButton).toHaveAttribute(
"aria-checked",
i % 2 === 0 ? "true" : "false",
);
});
}
});
it("handles mixed content types", () => {
render(
<div>
<Switch label="Text Switch" />
<Switch label="Another Text Switch" />
<Switch />
<Switch label="Final Switch" />
</div>,
);
const switches = screen.getAllByRole("switch");
expect(switches).toHaveLength(4);
// Check that labels are rendered correctly
expect(screen.getByText("Text Switch")).toBeInTheDocument();
expect(screen.getByText("Another Text Switch")).toBeInTheDocument();
expect(screen.getByText("Final Switch")).toBeInTheDocument();
});
});
@@ -1,280 +0,0 @@
import React from "react";
import { expect, test, describe, vi } from "vitest";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import TextArea from "../../app/components/TextArea";
// Test form component for integration testing
const TestForm = () => {
const [formData, setFormData] = React.useState({
textarea1: "",
textarea2: "",
});
const handleSubmit = (e) => {
e.preventDefault();
};
return (
<form onSubmit={handleSubmit}>
<TextArea
label="First TextArea"
name="textarea1"
value={formData.textarea1}
onChange={(e) =>
setFormData((prev) => ({ ...prev, textarea1: e.target.value }))
}
placeholder="Enter first text..."
/>
<TextArea
label="Second TextArea"
name="textarea2"
value={formData.textarea2}
onChange={(e) =>
setFormData((prev) => ({ ...prev, textarea2: e.target.value }))
}
placeholder="Enter second text..."
/>
<button type="submit">Submit</button>
</form>
);
};
// Dynamic TextArea component for prop changes testing
const DynamicTextArea = ({ size, labelVariant, state, disabled, error }) => {
const [value, setValue] = React.useState("");
return (
<TextArea
label="Dynamic TextArea"
value={value}
onChange={(e) => setValue(e.target.value)}
size={size}
labelVariant={labelVariant}
state={state}
disabled={disabled}
error={error}
placeholder="Enter text..."
/>
);
};
describe("TextArea Integration Tests", () => {
test("handles form submission with multiple textareas", async () => {
const user = userEvent.setup();
render(<TestForm />);
const firstTextarea = screen.getByPlaceholderText("Enter first text...");
const secondTextarea = screen.getByPlaceholderText("Enter second text...");
const submitButton = screen.getByRole("button", { name: /Submit/ });
await user.type(firstTextarea, "First content");
await user.type(secondTextarea, "Second content");
expect(firstTextarea).toHaveValue("First content");
expect(secondTextarea).toHaveValue("Second content");
await user.click(submitButton);
// Form submission should not cause errors
});
test("handles keyboard navigation between textareas", async () => {
const user = userEvent.setup();
render(<TestForm />);
const firstTextarea = screen.getByPlaceholderText("Enter first text...");
const secondTextarea = screen.getByPlaceholderText("Enter second text...");
await user.click(firstTextarea);
expect(firstTextarea).toHaveFocus();
await user.tab();
expect(secondTextarea).toHaveFocus();
});
test("handles dynamic prop changes", () => {
const { rerender } = render(<DynamicTextArea size="small" />);
let textarea = screen.getByRole("textbox");
expect(textarea).toHaveClass("h-[60px]");
rerender(<DynamicTextArea size="medium" />);
textarea = screen.getByRole("textbox");
expect(textarea).toHaveClass("h-[100px]");
rerender(<DynamicTextArea size="large" />);
textarea = screen.getByRole("textbox");
expect(textarea).toHaveClass("h-[150px]");
});
test("handles state changes", () => {
const { rerender } = render(<DynamicTextArea state="default" />);
let textarea = screen.getByRole("textbox");
expect(textarea).toHaveClass(
"border-[var(--color-border-default-tertiary)]",
);
rerender(<DynamicTextArea state="hover" />);
textarea = screen.getByRole("textbox");
expect(textarea).toHaveClass(
"shadow-[0_0_0_2px_var(--color-border-default-tertiary)]",
);
rerender(<DynamicTextArea state="focus" />);
textarea = screen.getByRole("textbox");
expect(textarea).toHaveClass(
"border-[var(--color-border-default-utility-info)]",
"shadow-[0_0_5px_3px_#3281F8]",
);
});
test("handles disabled state changes", () => {
const { rerender } = render(<DynamicTextArea disabled={false} />);
let textarea = screen.getByRole("textbox");
expect(textarea).not.toBeDisabled();
rerender(<DynamicTextArea disabled={true} />);
textarea = screen.getByRole("textbox");
expect(textarea).toBeDisabled();
});
test("handles error state changes", () => {
const { rerender } = render(<DynamicTextArea error={false} />);
let textarea = screen.getByRole("textbox");
expect(textarea).toHaveClass(
"border-[var(--color-border-default-tertiary)]",
);
rerender(<DynamicTextArea error={true} />);
textarea = screen.getByRole("textbox");
expect(textarea).toHaveClass(
"border-[var(--color-border-default-utility-negative)]",
);
});
test("handles label variant changes", () => {
const { rerender } = render(<DynamicTextArea labelVariant="default" />);
let container = screen.getByRole("textbox").closest("div").parentElement;
expect(container).toHaveClass("flex", "flex-col");
rerender(<DynamicTextArea labelVariant="horizontal" />);
container = screen.getByRole("textbox").closest("div").parentElement;
expect(container).toHaveClass("flex", "items-center", "gap-[12px]");
});
test("handles text input and changes", async () => {
const user = userEvent.setup();
render(<DynamicTextArea />);
const textarea = screen.getByRole("textbox");
await user.type(textarea, "Hello World");
expect(textarea).toHaveValue("Hello World");
});
test("handles focus and blur events", async () => {
const user = userEvent.setup();
const handleFocus = vi.fn();
const handleBlur = vi.fn();
render(
<TextArea
label="Test TextArea"
onFocus={handleFocus}
onBlur={handleBlur}
/>,
);
const textarea = screen.getByRole("textbox");
await user.click(textarea);
expect(handleFocus).toHaveBeenCalled();
await user.tab();
expect(handleBlur).toHaveBeenCalled();
});
test("handles multiple textareas with different configurations", () => {
render(
<div>
<TextArea
size="small"
label="Small TextArea"
placeholder="Small placeholder"
/>
<TextArea
size="medium"
labelVariant="horizontal"
label="Medium Horizontal"
placeholder="Medium placeholder"
/>
<TextArea
size="large"
label="Large TextArea"
placeholder="Large placeholder"
/>
</div>,
);
expect(
screen.getByPlaceholderText("Small placeholder"),
).toBeInTheDocument();
expect(
screen.getByPlaceholderText("Medium placeholder"),
).toBeInTheDocument();
expect(
screen.getByPlaceholderText("Large placeholder"),
).toBeInTheDocument();
});
test("handles form validation with error states", () => {
render(
<div>
<TextArea label="Valid TextArea" placeholder="Valid input" />
<TextArea label="Invalid TextArea" placeholder="Invalid input" error />
<TextArea
label="Disabled TextArea"
placeholder="Disabled input"
disabled
/>
</div>,
);
const validTextarea = screen.getByPlaceholderText("Valid input");
const invalidTextarea = screen.getByPlaceholderText("Invalid input");
const disabledTextarea = screen.getByPlaceholderText("Disabled input");
expect(validTextarea).toHaveClass(
"border-[var(--color-border-default-tertiary)]",
);
expect(invalidTextarea).toHaveClass(
"border-[var(--color-border-default-utility-negative)]",
);
expect(disabledTextarea).toBeDisabled();
});
test("handles performance with multiple re-renders", () => {
const { rerender } = render(<DynamicTextArea />);
// Simulate multiple re-renders
for (let i = 0; i < 10; i++) {
rerender(<DynamicTextArea size={i % 2 === 0 ? "small" : "large"} />);
}
const textarea = screen.getByRole("textbox");
expect(textarea).toBeInTheDocument();
});
test("handles accessibility with screen readers", async () => {
const user = userEvent.setup();
render(<TextArea label="Accessible TextArea" />);
const textarea = screen.getByRole("textbox");
const label = screen.getByText("Accessible TextArea");
expect(textarea).toHaveAttribute("id");
expect(label).toHaveAttribute("for", textarea.id);
await user.click(textarea);
expect(textarea).toHaveFocus();
});
});
@@ -1,185 +0,0 @@
import React from "react";
import { expect, test, describe, vi } from "vitest";
import { render, screen, fireEvent } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import Toggle from "../../app/components/Toggle";
describe("Toggle Integration", () => {
test("handles form submission", () => {
const handleSubmit = vi.fn();
render(
<form onSubmit={handleSubmit}>
<Toggle label="Test Toggle" name="toggle" />
<button type="submit">Submit</button>
</form>,
);
const toggle = screen.getByRole("switch", { name: "Test Toggle" });
const submitButton = screen.getByRole("button", { name: "Submit" });
fireEvent.click(toggle);
fireEvent.click(submitButton);
expect(handleSubmit).toHaveBeenCalledTimes(1);
});
test("handles keyboard navigation between toggles", async () => {
const user = userEvent.setup();
render(
<div>
<Toggle label="First Toggle" />
<Toggle label="Second Toggle" />
<Toggle label="Third Toggle" />
</div>,
);
const firstToggle = screen.getByRole("switch", { name: "First Toggle" });
const secondToggle = screen.getByRole("switch", { name: "Second Toggle" });
const thirdToggle = screen.getByRole("switch", { name: "Third Toggle" });
await user.tab();
expect(firstToggle).toHaveFocus();
await user.tab();
expect(secondToggle).toHaveFocus();
await user.tab();
expect(thirdToggle).toHaveFocus();
});
test("handles dynamic prop changes", () => {
const { rerender } = render(<Toggle label="Test Toggle" checked={false} />);
let toggle = screen.getByRole("switch");
expect(toggle).toHaveAttribute("aria-checked", "false");
rerender(<Toggle label="Test Toggle" checked={true} />);
toggle = screen.getByRole("switch");
expect(toggle).toHaveAttribute("aria-checked", "true");
rerender(<Toggle label="Test Toggle" disabled={true} />);
toggle = screen.getByRole("switch");
expect(toggle).toHaveAttribute("disabled");
});
test("handles multiple toggles in form", () => {
const handleChange1 = vi.fn();
const handleChange2 = vi.fn();
render(
<div>
<Toggle label="First Toggle" onChange={handleChange1} />
<Toggle label="Second Toggle" onChange={handleChange2} />
</div>,
);
const firstToggle = screen.getByRole("switch", { name: "First Toggle" });
const secondToggle = screen.getByRole("switch", { name: "Second Toggle" });
fireEvent.click(firstToggle);
expect(handleChange1).toHaveBeenCalledTimes(1);
expect(handleChange2).not.toHaveBeenCalled();
fireEvent.click(secondToggle);
expect(handleChange2).toHaveBeenCalledTimes(1);
expect(handleChange1).toHaveBeenCalledTimes(1);
});
test("handles state changes", () => {
const { rerender } = render(<Toggle label="Test Toggle" state="default" />);
let toggle = screen.getByRole("switch");
expect(toggle).not.toHaveClass("shadow-[0_0_5px_1px_#3281F8]");
rerender(<Toggle label="Test Toggle" state="focus" />);
toggle = screen.getByRole("switch");
expect(toggle).toHaveClass("shadow-[0_0_5px_1px_#3281F8]");
});
test("handles content changes", () => {
const { rerender } = render(<Toggle label="Test Toggle" />);
let toggle = screen.getByRole("switch");
expect(toggle).not.toHaveTextContent("I");
expect(toggle).not.toHaveTextContent("Toggle");
rerender(<Toggle label="Test Toggle" showIcon={true} icon="I" />);
toggle = screen.getByRole("switch");
expect(toggle).toHaveTextContent("I");
rerender(<Toggle label="Test Toggle" showText={true} text="Toggle" />);
toggle = screen.getByRole("switch");
expect(toggle).toHaveTextContent("Toggle");
rerender(
<Toggle
label="Test Toggle"
showIcon={true}
showText={true}
icon="I"
text="Toggle"
/>,
);
toggle = screen.getByRole("switch");
expect(toggle).toHaveTextContent("I");
expect(toggle).toHaveTextContent("Toggle");
});
test("handles performance with many toggles", () => {
const toggles = Array.from({ length: 100 }, (_, i) => (
<Toggle key={i} label={`Toggle ${i}`} />
));
const startTime = performance.now();
render(<div>{toggles}</div>);
const endTime = performance.now();
expect(endTime - startTime).toBeLessThan(1000); // Should render in less than 1 second
expect(screen.getAllByRole("switch")).toHaveLength(100);
});
test("handles rapid state changes", () => {
const handleChange = vi.fn();
render(<Toggle label="Test Toggle" onChange={handleChange} />);
const toggle = screen.getByRole("switch");
// Rapid clicks
for (let i = 0; i < 10; i++) {
fireEvent.click(toggle);
}
expect(handleChange).toHaveBeenCalledTimes(10);
});
test("handles mixed content types", () => {
render(
<div>
<Toggle label="Icon Toggle" showIcon={true} icon="I" />
<Toggle label="Text Toggle" showText={true} text="Toggle" />
<Toggle
label="Both Toggle"
showIcon={true}
showText={true}
icon="I"
text="Toggle"
/>
<Toggle label="Empty Toggle" />
</div>,
);
const iconToggle = screen.getByRole("switch", { name: "Icon Toggle" });
const textToggle = screen.getByRole("switch", { name: "Text Toggle" });
const bothToggle = screen.getByRole("switch", { name: "Both Toggle" });
const emptyToggle = screen.getByRole("switch", { name: "Empty Toggle" });
expect(iconToggle).toHaveTextContent("I");
expect(textToggle).toHaveTextContent("Toggle");
expect(bothToggle).toHaveTextContent("I");
expect(bothToggle).toHaveTextContent("Toggle");
expect(emptyToggle).not.toHaveTextContent("I");
expect(emptyToggle).not.toHaveTextContent("Toggle");
});
});
@@ -1,219 +0,0 @@
import React, { useState } from "react";
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
import { describe, it, expect, vi } from "vitest";
import ToggleGroup from "../../app/components/ToggleGroup";
// Test component for form integration
const TestForm = () => {
const [selectedToggle, setSelectedToggle] = useState("left");
return (
<form>
<div className="flex">
<ToggleGroup
position="left"
state={selectedToggle === "left" ? "selected" : "default"}
onChange={() => setSelectedToggle("left")}
>
Left Option
</ToggleGroup>
<ToggleGroup
position="middle"
state={selectedToggle === "middle" ? "selected" : "default"}
onChange={() => setSelectedToggle("middle")}
>
Middle Option
</ToggleGroup>
<ToggleGroup
position="right"
state={selectedToggle === "right" ? "selected" : "default"}
onChange={() => setSelectedToggle("right")}
>
Right Option
</ToggleGroup>
</div>
</form>
);
};
// Dynamic component for prop changes
const DynamicToggleGroup = ({ position, state, showText }) => {
return (
<ToggleGroup position={position} state={state} showText={showText}>
Dynamic Content
</ToggleGroup>
);
};
describe("ToggleGroup Integration", () => {
it("handles form submission", async () => {
const handleSubmit = vi.fn();
render(
<form onSubmit={handleSubmit}>
<div className="flex">
<ToggleGroup position="left" onChange={() => {}}>
First Option
</ToggleGroup>
<ToggleGroup position="middle" onChange={() => {}}>
Second Option
</ToggleGroup>
<ToggleGroup position="right" onChange={() => {}}>
Third Option
</ToggleGroup>
</div>
<button type="submit">Submit</button>
</form>,
);
const submitButton = screen.getByRole("button", { name: "Submit" });
fireEvent.click(submitButton);
expect(handleSubmit).toHaveBeenCalledTimes(1);
});
it("handles keyboard navigation between toggle groups", () => {
render(<TestForm />);
const toggleGroups = screen.getAllByRole("button");
// Focus first toggle group
toggleGroups[0].focus();
expect(toggleGroups[0]).toHaveFocus();
// Test keyboard navigation
fireEvent.keyDown(toggleGroups[0], { key: "Tab" });
// Note: Tab navigation behavior depends on browser implementation
});
it("handles dynamic prop changes", () => {
const { rerender } = render(
<DynamicToggleGroup position="left" state="default" showText={true} />,
);
let toggleGroup = screen.getByRole("button");
expect(toggleGroup).toHaveClass(
"rounded-l-[var(--measures-radius-medium)]",
"rounded-r-none",
);
expect(toggleGroup).toHaveTextContent("Dynamic Content");
rerender(
<DynamicToggleGroup
position="middle"
state="selected"
showText={false}
/>,
);
toggleGroup = screen.getByRole("button");
expect(toggleGroup).toHaveClass("rounded-none");
expect(toggleGroup).toHaveClass("bg-[var(--color-magenta-magenta100)]");
expect(toggleGroup).toHaveTextContent("Dynamic Content");
});
it("handles multiple toggle groups in form", () => {
render(<TestForm />);
const toggleGroups = screen.getAllByRole("button");
expect(toggleGroups).toHaveLength(3);
// Test clicking different toggle groups
fireEvent.click(toggleGroups[0]);
fireEvent.click(toggleGroups[1]);
fireEvent.click(toggleGroups[2]);
});
it("handles state changes", async () => {
render(<TestForm />);
const toggleGroups = screen.getAllByRole("button");
// Initially, left should be selected
expect(toggleGroups[0]).toHaveClass("bg-[var(--color-magenta-magenta100)]");
// Click middle toggle
fireEvent.click(toggleGroups[1]);
await waitFor(() => {
expect(toggleGroups[1]).toHaveClass(
"bg-[var(--color-magenta-magenta100)]",
);
});
});
it("handles content changes", () => {
const { rerender } = render(
<ToggleGroup showText={true}>Initial Content</ToggleGroup>,
);
let toggleGroup = screen.getByRole("button");
expect(toggleGroup).toHaveTextContent("Initial Content");
rerender(<ToggleGroup showText={true}>Updated Content</ToggleGroup>);
toggleGroup = screen.getByRole("button");
expect(toggleGroup).toHaveTextContent("Updated Content");
rerender(<ToggleGroup showText={false}>Hidden Content</ToggleGroup>);
toggleGroup = screen.getByRole("button");
expect(toggleGroup).toHaveTextContent("Hidden Content");
});
it("handles performance with many toggle groups", () => {
const ManyToggleGroups = () => {
const [selected, setSelected] = useState(0);
return (
<div className="flex">
{Array.from({ length: 10 }, (_, i) => (
<ToggleGroup
key={i}
position={i === 0 ? "left" : i === 9 ? "right" : "middle"}
state={selected === i ? "selected" : "default"}
onChange={() => setSelected(i)}
>
Option {i + 1}
</ToggleGroup>
))}
</div>
);
};
render(<ManyToggleGroups />);
const toggleGroups = screen.getAllByRole("button");
expect(toggleGroups).toHaveLength(10);
// Test clicking different toggle groups
fireEvent.click(toggleGroups[5]);
expect(toggleGroups[5]).toHaveClass("bg-[var(--color-magenta-magenta100)]");
});
it("handles rapid state changes", async () => {
render(<TestForm />);
const toggleGroups = screen.getAllByRole("button");
// Rapidly change states
for (let i = 0; i < 5; i++) {
fireEvent.click(toggleGroups[i % 3]);
await waitFor(() => {
expect(toggleGroups[i % 3]).toHaveClass(
"bg-[var(--color-magenta-magenta100)]",
);
});
}
});
it("handles mixed content types", () => {
render(
<div className="flex">
<ToggleGroup position="left" showText={true}>
Text Only
</ToggleGroup>
<ToggleGroup position="middle" showText={false}>
Icon Only
</ToggleGroup>
<ToggleGroup position="right" showText={true}>
Text Only
</ToggleGroup>
</div>,
);
const toggleGroups = screen.getAllByRole("button");
expect(toggleGroups[0]).toHaveTextContent("Text Only");
expect(toggleGroups[1]).toHaveTextContent("Icon Only");
expect(toggleGroups[2]).toHaveTextContent("Text Only");
});
});
@@ -1,353 +0,0 @@
import { render, screen, cleanup } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, test, expect, afterEach } from "vitest";
import HeroBanner from "../../app/components/HeroBanner";
import NumberedCards from "../../app/components/NumberedCards";
import RuleStack from "../../app/components/RuleStack";
import FeatureGrid from "../../app/components/FeatureGrid";
import QuoteBlock from "../../app/components/QuoteBlock";
import AskOrganizer from "../../app/components/AskOrganizer";
afterEach(() => {
cleanup();
});
describe("Component Interactions Integration", () => {
test("hero banner and numbered cards work together to explain the process", () => {
const heroData = {
title: "Collaborate",
subtitle: "with clarity",
description:
"Help your community make important decisions in a way that reflects its unique values.",
ctaText: "Learn how CommunityRule works",
ctaHref: "#",
};
const numberedCardsData = {
title: "How CommunityRule works",
subtitle: "Here's a quick overview of the process, from start to finish.",
cards: [
{
text: "Document how your community makes decisions",
iconShape: "blob",
iconColor: "green",
},
{
text: "Build an operating manual for a successful community",
iconShape: "gear",
iconColor: "purple",
},
{
text: "Get a link to your manual for your group to review and evolve",
iconShape: "star",
iconColor: "orange",
},
],
};
render(
<div>
<HeroBanner {...heroData} />
<NumberedCards {...numberedCardsData} />
</div>,
);
// Hero introduces the concept
expect(
screen.getByText(/Help your community make important decisions/),
).toBeInTheDocument();
// Numbered cards explain the process
expect(screen.getByText("How CommunityRule works")).toBeInTheDocument();
expect(
screen.getByText("Document how your community makes decisions"),
).toBeInTheDocument();
expect(
screen.getByText("Build an operating manual for a successful community"),
).toBeInTheDocument();
expect(
screen.getByText(
"Get a link to your manual for your group to review and evolve",
),
).toBeInTheDocument();
});
test("rule stack and feature grid complement each other", () => {
const featureGridData = {
title: "We've got your back, every step of the way",
subtitle:
"Use our toolkit to improve, document, and evolve your organization.",
};
render(
<div>
<RuleStack />
<FeatureGrid {...featureGridData} />
</div>,
);
// Rule stack shows governance options
expect(screen.getByText("Consensus clusters")).toBeInTheDocument();
expect(screen.getByText("Elected Board")).toBeInTheDocument();
expect(screen.getByText("Consensus")).toBeInTheDocument();
expect(screen.getByText("Petition")).toBeInTheDocument();
// Feature grid provides support context
expect(
screen.getByText("We've got your back, every step of the way"),
).toBeInTheDocument();
expect(
screen.getByText(
"Use our toolkit to improve, document, and evolve your organization.",
),
).toBeInTheDocument();
});
test("quote block provides social proof for the entire application", () => {
render(<QuoteBlock />);
// Quote provides credibility and social proof
expect(
screen.getByText(/The rules of decision-making must be open/),
).toBeInTheDocument();
// Should have proper attribution
expect(screen.getByText("Jo Freeman")).toBeInTheDocument();
expect(
screen.getByText("The Tyranny of Structurelessness"),
).toBeInTheDocument();
});
test("ask organizer provides help context for all components", () => {
const askOrganizerData = {
title: "Still have questions?",
subtitle: "Get answers from an experienced organizer",
buttonText: "Ask an organizer",
buttonHref: "#contact",
};
render(<AskOrganizer {...askOrganizerData} />);
// Provides help for users who need assistance
expect(screen.getByText("Still have questions?")).toBeInTheDocument();
expect(
screen.getByText("Get answers from an experienced organizer"),
).toBeInTheDocument();
expect(
screen.getByRole("link", { name: /Ask an organizer/i }),
).toBeInTheDocument();
});
test("all components maintain consistent styling and branding", () => {
render(
<div>
<HeroBanner
title="Test"
subtitle="Test"
description="Test description"
ctaText="Test CTA"
/>
<NumberedCards
title="Test Cards"
subtitle="Test subtitle"
cards={[{ text: "Test card", iconShape: "blob", iconColor: "green" }]}
/>
<RuleStack />
<FeatureGrid title="Test Features" subtitle="Test subtitle" />
<QuoteBlock />
<AskOrganizer
title="Test Help"
subtitle="Test help subtitle"
buttonText="Test Help Button"
/>
</div>,
);
// All components should render without errors
expect(screen.getAllByText("Test").length).toBeGreaterThan(0);
expect(screen.getByText("Test Cards")).toBeInTheDocument();
expect(screen.getByText("Consensus clusters")).toBeInTheDocument();
expect(screen.getByText("Test Features")).toBeInTheDocument();
expect(
screen.getByText(/The rules of decision-making must be open/),
).toBeInTheDocument();
expect(screen.getByText("Test Help")).toBeInTheDocument();
});
test("components handle data flow and prop passing correctly", () => {
const testData = {
hero: {
title: "Test Hero",
subtitle: "Test Subtitle",
description: "Test description",
ctaText: "Test CTA",
ctaHref: "/test",
},
cards: {
title: "Test Cards",
subtitle: "Test subtitle",
cards: [
{ text: "Card 1", iconShape: "blob", iconColor: "green" },
{ text: "Card 2", iconShape: "gear", iconColor: "purple" },
],
},
features: {
title: "Test Features",
subtitle: "Test features subtitle",
},
help: {
title: "Test Help",
subtitle: "Test help subtitle",
buttonText: "Test Help Button",
buttonHref: "/help",
},
};
render(
<div>
<HeroBanner {...testData.hero} />
<NumberedCards {...testData.cards} />
<FeatureGrid {...testData.features} />
<AskOrganizer {...testData.help} />
</div>,
);
// Verify all data is passed correctly
expect(screen.getByText("Test Hero")).toBeInTheDocument();
expect(screen.getByText("Test Subtitle")).toBeInTheDocument();
expect(screen.getByText("Test description")).toBeInTheDocument();
expect(
screen.getAllByRole("button", { name: "Test CTA" }).length,
).toBeGreaterThan(0);
expect(screen.getByText("Test Cards")).toBeInTheDocument();
expect(screen.getByText("Card 1")).toBeInTheDocument();
expect(screen.getByText("Card 2")).toBeInTheDocument();
expect(screen.getByText("Test Features")).toBeInTheDocument();
expect(screen.getByText("Test Help")).toBeInTheDocument();
expect(
screen.getByRole("link", { name: /Test Help Button/i }),
).toBeInTheDocument();
});
test("components work together to create a cohesive user experience", async () => {
const user = userEvent.setup();
render(
<div>
<HeroBanner
title="Collaborate"
subtitle="with clarity"
description="Help your community make important decisions."
ctaText="Learn more"
ctaHref="#learn"
/>
<NumberedCards
title="How it works"
subtitle="Simple steps to get started"
cards={[
{ text: "Step 1", iconShape: "blob", iconColor: "green" },
{ text: "Step 2", iconShape: "gear", iconColor: "purple" },
]}
/>
<RuleStack />
<FeatureGrid title="Features" subtitle="Everything you need" />
<QuoteBlock />
<AskOrganizer
title="Need help?"
subtitle="We're here to help"
buttonText="Contact us"
buttonHref="#contact"
/>
</div>,
);
// Test interaction flow
const learnButtons = screen.getAllByRole("button", { name: "Learn more" });
await user.click(learnButtons[0]);
const createButtons = screen.getAllByRole("button", {
name: "Create CommunityRule",
});
if (createButtons.length > 0) {
await user.click(createButtons[0]);
}
const contactButton = screen.getByRole("link", { name: /Contact us/i });
await user.click(contactButton);
// All components should remain functional
expect(screen.getByText("Collaborate")).toBeInTheDocument();
expect(screen.getByText("How it works")).toBeInTheDocument();
expect(screen.getByText("Consensus clusters")).toBeInTheDocument();
expect(screen.getByText("Features")).toBeInTheDocument();
expect(
screen.getByText(/The rules of decision-making must be open/),
).toBeInTheDocument();
expect(screen.getByText("Need help?")).toBeInTheDocument();
});
test("components handle edge cases and missing data gracefully", () => {
// Test with minimal data
render(
<div>
<HeroBanner title="Minimal Hero" />
<NumberedCards title="Minimal Cards" cards={[]} />
<FeatureGrid title="Minimal Features" />
<AskOrganizer title="Minimal Help" />
</div>,
);
// Components should render without crashing
expect(screen.getByText("Minimal Hero")).toBeInTheDocument();
expect(screen.getByText("Minimal Cards")).toBeInTheDocument();
expect(screen.getByText("Minimal Features")).toBeInTheDocument();
expect(screen.getByText("Minimal Help")).toBeInTheDocument();
});
test("components maintain accessibility when used together", () => {
render(
<div>
<HeroBanner
title="Accessible Hero"
subtitle="Accessible Subtitle"
description="Accessible description"
ctaText="Accessible CTA"
/>
<NumberedCards
title="Accessible Cards"
subtitle="Accessible subtitle"
cards={[
{ text: "Accessible card", iconShape: "blob", iconColor: "green" },
]}
/>
<RuleStack />
<FeatureGrid
title="Accessible Features"
subtitle="Accessible features subtitle"
/>
<QuoteBlock />
<AskOrganizer
title="Accessible Help"
subtitle="Accessible help subtitle"
buttonText="Accessible Help Button"
/>
</div>,
);
// Check for proper heading hierarchy
const headings = screen.getAllByRole("heading");
expect(headings.length).toBeGreaterThan(0);
// Check for proper button roles
const buttons = screen.getAllByRole("button");
buttons.forEach((button) => {
expect(button).toBeInTheDocument();
});
// Check for proper link roles
const links = screen.getAllByRole("link");
links.forEach((link) => {
expect(link).toBeInTheDocument();
});
});
});
@@ -1,246 +0,0 @@
import { render, screen, cleanup } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, test, expect, afterEach } from "vitest";
import Header from "../../app/components/Header";
import Footer from "../../app/components/Footer";
afterEach(() => {
cleanup();
});
describe("Layout Integration", () => {
test("header and footer provide consistent branding", () => {
render(
<div>
<Header />
<Footer />
</div>,
);
// Check that CommunityRule branding appears in both header and footer
const headerLogos = screen.getAllByAltText("CommunityRule Logo Icon");
expect(headerLogos.length).toBeGreaterThan(0);
// Footer should have the organization name
expect(screen.getByText("Media Economies Design Lab")).toBeInTheDocument();
});
test("navigation is consistent between header and footer", () => {
render(
<div>
<Header />
<Footer />
</div>,
);
// Header navigation items
expect(
screen.getAllByRole("menuitem", { name: "Navigate to Use cases page" })
.length,
).toBeGreaterThan(0);
expect(
screen.getAllByRole("menuitem", { name: "Navigate to Learn page" })
.length,
).toBeGreaterThan(0);
expect(
screen.getAllByRole("menuitem", { name: "Navigate to About page" })
.length,
).toBeGreaterThan(0);
// Footer navigation items (should be present in footer as well)
// Footer has navigation links that match header
const footerUseCasesLinks = screen.getAllByRole("link", {
name: "Use cases",
});
const footerLearnLinks = screen.getAllByRole("link", { name: "Learn" });
const footerAboutLinks = screen.getAllByRole("link", { name: "About" });
// Check that footer has these links (they may be in header too, so getAllByRole will find both)
expect(footerUseCasesLinks.length).toBeGreaterThan(0);
expect(footerLearnLinks.length).toBeGreaterThan(0);
expect(footerAboutLinks.length).toBeGreaterThan(0);
});
test("header navigation is interactive", async () => {
const user = userEvent.setup();
render(<Header />);
// Test navigation links
const useCasesLinks = screen.getAllByRole("menuitem", {
name: "Navigate to Use cases page",
});
const learnLinks = screen.getAllByRole("menuitem", {
name: "Navigate to Learn page",
});
const aboutLinks = screen.getAllByRole("menuitem", {
name: "Navigate to About page",
});
const useCasesLink = useCasesLinks[0];
const learnLink = learnLinks[0];
const aboutLink = aboutLinks[0];
expect(useCasesLink).toHaveAttribute("href", "#");
expect(learnLink).toHaveAttribute("href", "/learn");
expect(aboutLink).toHaveAttribute("href", "#");
// Test button interactions
const createRuleButtons = screen.getAllByRole("button", {
name: "Create a new rule with avatar decoration",
});
await user.click(createRuleButtons[0]);
expect(createRuleButtons[0]).toBeInTheDocument();
});
test("footer provides contact and social information", () => {
render(<Footer />);
// Contact information
expect(screen.getByText("medlab@colorado.edu")).toBeInTheDocument();
expect(
screen.getByRole("link", { name: "medlab@colorado.edu" }),
).toHaveAttribute("href", "mailto:medlab@colorado.edu");
// Social media links
expect(
screen.getByRole("link", { name: "Follow us on Bluesky" }),
).toBeInTheDocument();
expect(
screen.getByRole("link", { name: "Follow us on GitLab" }),
).toBeInTheDocument();
// Legal links
expect(
screen.getByRole("link", { name: "Privacy Policy" }),
).toBeInTheDocument();
expect(
screen.getByRole("link", { name: "Terms of Service" }),
).toBeInTheDocument();
});
test("header provides proper authentication options", () => {
render(<Header />);
// Login button should be present
const loginButtons = screen.getAllByRole("menuitem", {
name: "Log in to your account",
});
const loginButton = loginButtons[0];
expect(loginButton).toBeInTheDocument();
// Create rule button should be present
const createRuleButtons = screen.getAllByRole("button", {
name: "Create a new rule with avatar decoration",
});
expect(createRuleButtons.length).toBeGreaterThan(0);
});
test("layout maintains proper semantic structure", () => {
render(
<div>
<Header />
<Footer />
</div>,
);
// Header should have banner role
const header = screen.getByRole("banner");
expect(header).toBeInTheDocument();
// Navigation should be present
const navigation = screen.getByRole("navigation");
expect(navigation).toBeInTheDocument();
// Footer should be present
const footer = screen.getByRole("contentinfo");
expect(footer).toBeInTheDocument();
});
test("responsive design elements are present", () => {
render(
<div>
<Header />
<Footer />
</div>,
);
// Header should have responsive navigation elements
const headerContainer = screen.getByRole("banner");
expect(headerContainer).toBeInTheDocument();
// Footer should have responsive layout
const footerContainer = screen.getByRole("contentinfo");
expect(footerContainer).toBeInTheDocument();
});
test("all interactive elements have proper focus management", () => {
render(
<div>
<Header />
<Footer />
</div>,
);
// Get all interactive elements
const buttons = screen.getAllByRole("button");
const links = screen.getAllByRole("link");
// All buttons should be focusable
buttons.forEach((button) => {
expect(button).not.toHaveAttribute("tabindex", "-1");
});
// All links should be focusable
links.forEach((link) => {
expect(link).not.toHaveAttribute("tabindex", "-1");
});
});
test("layout provides consistent user experience", () => {
render(
<div>
<Header />
<Footer />
</div>,
);
// Header provides main navigation
expect(screen.getByRole("navigation")).toBeInTheDocument();
// Footer provides additional navigation and contact info
expect(screen.getByText("Media Economies Design Lab")).toBeInTheDocument();
expect(screen.getByText("medlab@colorado.edu")).toBeInTheDocument();
// Both header and footer should have CommunityRule branding
const logos = screen.getAllByAltText("CommunityRule Logo Icon");
expect(logos.length).toBeGreaterThan(0);
});
test("header and footer work together for complete navigation", () => {
render(
<div>
<Header />
<Footer />
</div>,
);
// Main navigation in header
const headerNav = screen.getByRole("navigation");
expect(headerNav).toBeInTheDocument();
// Additional navigation in footer
const footerLinks = screen.getAllByRole("link");
const navigationLinks = footerLinks.filter(
(link) =>
link.textContent?.includes("Use cases") ||
link.textContent?.includes("Learn") ||
link.textContent?.includes("About"),
);
expect(navigationLinks.length).toBeGreaterThan(0);
// Contact information in footer
expect(
screen.getByRole("link", { name: "medlab@colorado.edu" }),
).toBeInTheDocument();
});
});
@@ -1,136 +0,0 @@
import { within, userEvent } from "@storybook/test";
import { expect } from "@storybook/test";
// Interaction test for Default story
export const DefaultInteraction = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const checkbox = canvas.getByRole("checkbox");
// Test initial state
expect(checkbox).toHaveAttribute("aria-checked", "false");
// Test click interaction
await userEvent.click(checkbox);
expect(checkbox).toHaveAttribute("aria-checked", "true");
// Test toggle back
await userEvent.click(checkbox);
expect(checkbox).toHaveAttribute("aria-checked", "false");
},
};
// Interaction test for Checked story
export const CheckedInteraction = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const checkbox = canvas.getByRole("checkbox");
// Test initial checked state
expect(checkbox).toHaveAttribute("aria-checked", "true");
// Test unchecking
await userEvent.click(checkbox);
expect(checkbox).toHaveAttribute("aria-checked", "false");
},
};
// Interaction test for Standard story
export const StandardInteraction = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const checkboxes = canvas.getAllByRole("checkbox");
// Test both checkboxes
expect(checkboxes).toHaveLength(2);
// Test first checkbox (unchecked)
expect(checkboxes[0]).toHaveAttribute("aria-checked", "false");
await userEvent.click(checkboxes[0]);
expect(checkboxes[0]).toHaveAttribute("aria-checked", "true");
// Test second checkbox (checked)
expect(checkboxes[1]).toHaveAttribute("aria-checked", "true");
await userEvent.click(checkboxes[1]);
expect(checkboxes[1]).toHaveAttribute("aria-checked", "false");
},
};
// Interaction test for Inverse story
export const InverseInteraction = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const checkboxes = canvas.getAllByRole("checkbox");
// Test both checkboxes in inverse mode
expect(checkboxes).toHaveLength(2);
// Test first checkbox (unchecked)
expect(checkboxes[0]).toHaveAttribute("aria-checked", "false");
await userEvent.click(checkboxes[0]);
expect(checkboxes[0]).toHaveAttribute("aria-checked", "true");
// Test second checkbox (checked)
expect(checkboxes[1]).toHaveAttribute("aria-checked", "true");
await userEvent.click(checkboxes[1]);
expect(checkboxes[1]).toHaveAttribute("aria-checked", "false");
},
};
// Keyboard interaction test
export const KeyboardInteraction = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const checkbox = canvas.getByRole("checkbox");
// Focus the checkbox
await userEvent.tab();
expect(checkbox).toHaveFocus();
// Test Space key
await userEvent.keyboard(" ");
expect(checkbox).toHaveAttribute("aria-checked", "true");
// Test Enter key
await userEvent.keyboard("Enter");
expect(checkbox).toHaveAttribute("aria-checked", "false");
},
};
// Accessibility interaction test
export const AccessibilityInteraction = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const checkbox = canvas.getByRole("checkbox");
// Test ARIA attributes
expect(checkbox).toHaveAttribute("role", "checkbox");
expect(checkbox).toHaveAttribute("aria-checked");
expect(checkbox).toHaveAttribute("tabIndex");
// Test keyboard navigation
await userEvent.tab();
expect(checkbox).toHaveFocus();
// Test activation
await userEvent.keyboard(" ");
expect(checkbox).toHaveAttribute("aria-checked", "true");
},
};
// Form integration test
export const FormIntegration = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const checkbox = canvas.getByRole("checkbox");
// Test form integration
const hiddenInput = canvas.getByRole("checkbox", { hidden: true });
expect(hiddenInput).toBeInTheDocument();
// Test checkbox interaction
await userEvent.click(checkbox);
expect(checkbox).toHaveAttribute("aria-checked", "true");
expect(hiddenInput).toBeChecked();
},
};
-234
View File
@@ -1,234 +0,0 @@
import { test, expect } from "@playwright/test";
test.describe("Checkbox Storybook Tests", () => {
test.beforeEach(async ({ page }) => {
await page.goto("http://localhost:6006");
});
test("should load Checkbox stories", async ({ page }) => {
// Navigate to Checkbox stories
await page.click('[data-testid="Forms"]');
await page.click('[data-testid="Checkbox"]');
// Check that the stories are loaded
await expect(page.locator('[data-testid="Default"]')).toBeVisible();
await expect(page.locator('[data-testid="Checked"]')).toBeVisible();
await expect(page.locator('[data-testid="Standard"]')).toBeVisible();
await expect(page.locator('[data-testid="Inverse"]')).toBeVisible();
});
test("Default story should render correctly", async ({ page }) => {
await page.click('[data-testid="Forms"]');
await page.click('[data-testid="Checkbox"]');
await page.click('[data-testid="Default"]');
// Check that the checkbox is rendered
const checkbox = page.locator('[role="checkbox"]').first();
await expect(checkbox).toBeVisible();
await expect(checkbox).toHaveAttribute("aria-checked", "false");
});
test("Checked story should render correctly", async ({ page }) => {
await page.click('[data-testid="Forms"]');
await page.click('[data-testid="Checkbox"]');
await page.click('[data-testid="Checked"]');
// Check that the checkbox is checked
const checkbox = page.locator('[role="checkbox"]').first();
await expect(checkbox).toBeVisible();
await expect(checkbox).toHaveAttribute("aria-checked", "true");
});
test("Standard story should show standard mode checkboxes", async ({
page,
}) => {
await page.click('[data-testid="Forms"]');
await page.click('[data-testid="Checkbox"]');
await page.click('[data-testid="Standard"]');
// Check that multiple checkboxes are rendered
const checkboxes = page.locator('[role="checkbox"]');
await expect(checkboxes).toHaveCount(2); // Unchecked and checked
// Check that they have proper styling (standard mode)
const firstCheckbox = checkboxes.first();
await expect(firstCheckbox).toBeVisible();
});
test("Inverse story should show inverse mode checkboxes", async ({
page,
}) => {
await page.click('[data-testid="Forms"]');
await page.click('[data-testid="Checkbox"]');
await page.click('[data-testid="Inverse"]');
// Check that multiple checkboxes are rendered
const checkboxes = page.locator('[role="checkbox"]');
await expect(checkboxes).toHaveCount(2); // Unchecked and checked
// Check that they have proper styling (inverse mode)
const firstCheckbox = checkboxes.first();
await expect(firstCheckbox).toBeVisible();
});
test("should have proper controls in Controls panel", async ({ page }) => {
await page.click('[data-testid="Forms"]');
await page.click('[data-testid="Checkbox"]');
await page.click('[data-testid="Default"]');
// Check that controls are available
await expect(page.locator('[data-testid="control-checked"]')).toBeVisible();
await expect(page.locator('[data-testid="control-mode"]')).toBeVisible();
await expect(page.locator('[data-testid="control-state"]')).toBeVisible();
await expect(
page.locator('[data-testid="control-disabled"]'),
).toBeVisible();
await expect(page.locator('[data-testid="control-label"]')).toBeVisible();
});
test("should update when controls are changed", async ({ page }) => {
await page.click('[data-testid="Forms"]');
await page.click('[data-testid="Checkbox"]');
await page.click('[data-testid="Default"]');
// Toggle checked control
await page.click('[data-testid="control-checked"]');
// Check that the checkbox is now checked
const checkbox = page.locator('[role="checkbox"]').first();
await expect(checkbox).toHaveAttribute("aria-checked", "true");
// Toggle back
await page.click('[data-testid="control-checked"]');
await expect(checkbox).toHaveAttribute("aria-checked", "false");
});
test("should change mode when mode control is changed", async ({ page }) => {
await page.click('[data-testid="Forms"]');
await page.click('[data-testid="Checkbox"]');
await page.click('[data-testid="Default"]');
// Change mode to inverse
await page.selectOption('[data-testid="control-mode"]', "inverse");
// Check that the checkbox styling has changed (inverse mode)
const checkbox = page.locator('[role="checkbox"]').first();
await expect(checkbox).toBeVisible();
// Change back to standard
await page.selectOption('[data-testid="control-mode"]', "standard");
await expect(checkbox).toBeVisible();
});
test("should show disabled state when disabled control is toggled", async ({
page,
}) => {
await page.click('[data-testid="Forms"]');
await page.click('[data-testid="Checkbox"]');
await page.click('[data-testid="Default"]');
// Toggle disabled control
await page.click('[data-testid="control-disabled"]');
// Check that the checkbox is now disabled
const checkbox = page.locator('[role="checkbox"]').first();
await expect(checkbox).toHaveAttribute("aria-disabled", "true");
await expect(checkbox).toHaveAttribute("tabIndex", "-1");
// Toggle back
await page.click('[data-testid="control-disabled"]');
await expect(checkbox).toHaveAttribute("aria-disabled", "false");
await expect(checkbox).toHaveAttribute("tabIndex", "0");
});
test("should update label when label control is changed", async ({
page,
}) => {
await page.click('[data-testid="Forms"]');
await page.click('[data-testid="Checkbox"]');
await page.click('[data-testid="Default"]');
// Change label
await page.fill('[data-testid="control-label"]', "Custom Label");
// Check that the label has updated
await expect(page.locator("text=Custom Label")).toBeVisible();
});
test("should have proper accessibility in Storybook", async ({ page }) => {
await page.click('[data-testid="Forms"]');
await page.click('[data-testid="Checkbox"]');
await page.click('[data-testid="Default"]');
// Check accessibility attributes
const checkbox = page.locator('[role="checkbox"]').first();
await expect(checkbox).toHaveAttribute("role", "checkbox");
await expect(checkbox).toHaveAttribute("aria-checked");
await expect(checkbox).toHaveAttribute("tabIndex");
});
test("should support keyboard navigation in Storybook", async ({ page }) => {
await page.click('[data-testid="Forms"]');
await page.click('[data-testid="Checkbox"]');
await page.click('[data-testid="Default"]');
const checkbox = page.locator('[role="checkbox"]').first();
// Focus the checkbox
await checkbox.focus();
await expect(checkbox).toBeFocused();
// Test keyboard activation
await checkbox.press(" ");
await expect(checkbox).toHaveAttribute("aria-checked", "true");
await checkbox.press(" ");
await expect(checkbox).toHaveAttribute("aria-checked", "false");
});
test("should show proper documentation", async ({ page }) => {
await page.click('[data-testid="Forms"]');
await page.click('[data-testid="Checkbox"]');
// Check that documentation is available
await expect(page.locator('[data-testid="docs-tab"]')).toBeVisible();
// Click on docs tab
await page.click('[data-testid="docs-tab"]');
// Check that documentation content is shown
await expect(page.locator("text=Checkbox")).toBeVisible();
await expect(page.locator("text=Props")).toBeVisible();
});
test("should have proper story navigation", async ({ page }) => {
await page.click('[data-testid="Forms"]');
await page.click('[data-testid="Checkbox"]');
// Test navigation between stories
const stories = ["Default", "Checked", "Standard", "Inverse"];
for (const story of stories) {
await page.click(`[data-testid="${story}"]`);
await expect(page.locator('[role="checkbox"]').first()).toBeVisible();
}
});
test("should maintain state between story switches", async ({ page }) => {
await page.click('[data-testid="Forms"]');
await page.click('[data-testid="Checkbox"]');
await page.click('[data-testid="Default"]');
// Interact with checkbox
const checkbox = page.locator('[role="checkbox"]').first();
await checkbox.click();
await expect(checkbox).toHaveAttribute("aria-checked", "true");
// Switch to another story and back
await page.click('[data-testid="Checked"]');
await page.click('[data-testid="Default"]');
// Check that the state is maintained
await expect(checkbox).toHaveAttribute("aria-checked", "true");
});
});
@@ -1,124 +0,0 @@
import { expect } from "@storybook/test";
import { userEvent, within } from "@storybook/test";
export const DefaultInteraction = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const radioButton = canvas.getByRole("radio");
// Should be unchecked initially
await expect(radioButton).toHaveAttribute("aria-checked", "false");
// Click to check
await userEvent.click(radioButton);
await expect(radioButton).toHaveAttribute("aria-checked", "true");
// Radio buttons can't be unchecked by clicking them again
// They stay checked until another radio button in the same group is selected
await userEvent.click(radioButton);
await expect(radioButton).toHaveAttribute("aria-checked", "true");
},
};
export const CheckedInteraction = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const radioButton = canvas.getByRole("radio");
// Should be checked initially
await expect(radioButton).toHaveAttribute("aria-checked", "true");
// Radio buttons can't be unchecked by clicking them again
// They stay checked until another radio button in the same group is selected
await userEvent.click(radioButton);
await expect(radioButton).toHaveAttribute("aria-checked", "true");
},
};
export const StandardInteraction = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const radioButtons = canvas.getAllByRole("radio");
// First should be unchecked
await expect(radioButtons[0]).toHaveAttribute("aria-checked", "false");
// Second should be checked
await expect(radioButtons[1]).toHaveAttribute("aria-checked", "true");
// Click first radio button
await userEvent.click(radioButtons[0]);
await expect(radioButtons[0]).toHaveAttribute("aria-checked", "true");
await expect(radioButtons[1]).toHaveAttribute("aria-checked", "false");
},
};
export const InverseInteraction = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const radioButtons = canvas.getAllByRole("radio");
// First should be unchecked
await expect(radioButtons[0]).toHaveAttribute("aria-checked", "false");
// Second should be checked
await expect(radioButtons[1]).toHaveAttribute("aria-checked", "true");
// Click first radio button
await userEvent.click(radioButtons[0]);
await expect(radioButtons[0]).toHaveAttribute("aria-checked", "true");
await expect(radioButtons[1]).toHaveAttribute("aria-checked", "false");
},
};
export const KeyboardInteraction = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const radioButton = canvas.getByRole("radio");
// Focus the radio button
await userEvent.click(radioButton);
await expect(radioButton).toHaveFocus();
// Test Space key
await userEvent.keyboard(" ");
await expect(radioButton).toHaveAttribute("aria-checked", "true");
// Test Enter key
await userEvent.keyboard("Enter");
await expect(radioButton).toHaveAttribute("aria-checked", "false");
},
};
export const AccessibilityInteraction = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const radioButton = canvas.getByRole("radio");
// Should have proper ARIA attributes
await expect(radioButton).toHaveAttribute("role", "radio");
await expect(radioButton).toHaveAttribute("aria-checked");
await expect(radioButton).toHaveAttribute("tabIndex", "0");
// Should be keyboard accessible
await userEvent.tab();
await expect(radioButton).toHaveFocus();
// Should have accessible name
const label = canvas.getByText("Default radio button");
await expect(label).toBeVisible();
},
};
export const FormIntegration = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const radioButton = canvas.getByRole("radio");
// Should have hidden input for form submission
const hiddenInput = canvas.getByRole("radio", { hidden: true });
await expect(hiddenInput).toBeInTheDocument();
// Should be included in form data
await userEvent.click(radioButton);
await expect(hiddenInput).toBeChecked();
},
};
@@ -1,177 +0,0 @@
import { test, expect } from "@playwright/test";
test.describe("RadioButton Storybook Tests", () => {
test.beforeEach(async ({ page }) => {
await page.goto(
"http://localhost:6006/iframe.html?id=forms-radiobutton--default",
);
});
test("renders default story", async ({ page }) => {
const radioButton = page.locator('[role="radio"]');
await expect(radioButton).toBeVisible();
await expect(radioButton).toHaveAttribute("aria-checked", "false");
});
test("renders checked story", async ({ page }) => {
await page.goto(
"http://localhost:6006/iframe.html?id=forms-radiobutton--checked",
);
const radioButton = page.locator('[role="radio"]');
await expect(radioButton).toBeVisible();
await expect(radioButton).toHaveAttribute("aria-checked", "true");
});
test("renders standard story", async ({ page }) => {
await page.goto(
"http://localhost:6006/iframe.html?id=forms-radiobutton--standard",
);
const radioButtons = page.locator('[role="radio"]');
await expect(radioButtons).toHaveCount(2);
// First should be unchecked
await expect(radioButtons.first()).toHaveAttribute("aria-checked", "false");
// Second should be checked
await expect(radioButtons.nth(1)).toHaveAttribute("aria-checked", "true");
});
test("renders inverse story", async ({ page }) => {
await page.goto(
"http://localhost:6006/iframe.html?id=forms-radiobutton--inverse",
);
const radioButtons = page.locator('[role="radio"]');
await expect(radioButtons).toHaveCount(2);
// First should be unchecked
await expect(radioButtons.first()).toHaveAttribute("aria-checked", "false");
// Second should be checked
await expect(radioButtons.nth(1)).toHaveAttribute("aria-checked", "true");
});
test("interacts with controls", async ({ page }) => {
// Test checked control
await page.check('[data-testid="checked-control"]');
const radioButton = page.locator('[role="radio"]');
await expect(radioButton).toHaveAttribute("aria-checked", "true");
await page.uncheck('[data-testid="checked-control"]');
await expect(radioButton).toHaveAttribute("aria-checked", "false");
});
test("interacts with mode control", async ({ page }) => {
// Test mode control
await page.selectOption('[data-testid="mode-control"]', "inverse");
const radioButton = page.locator('[role="radio"]');
await expect(radioButton).toHaveClass(
/outline-\[var\(--color-border-inverse-primary\)\]/,
);
await page.selectOption('[data-testid="mode-control"]', "standard");
await expect(radioButton).toHaveClass(
/outline-\[var\(--color-border-default-tertiary\)\]/,
);
});
test("interacts with state control", async ({ page }) => {
// Test state control
await page.selectOption('[data-testid="state-control"]', "focus");
const radioButton = page.locator('[role="radio"]');
await expect(radioButton).toHaveClass(/focus:outline/);
await page.selectOption('[data-testid="state-control"]', "hover");
await expect(radioButton).toHaveClass(/hover:outline/);
});
test("interacts with label control", async ({ page }) => {
// Test label control
await page.fill('[data-testid="label-control"]', "Custom Label");
await expect(page.locator('text="Custom Label"')).toBeVisible();
});
test("handles keyboard interaction", async ({ page }) => {
const radioButton = page.locator('[role="radio"]');
await radioButton.focus();
await expect(radioButton).toBeFocused();
// Test Space key
await page.keyboard.press("Space");
await expect(radioButton).toHaveAttribute("aria-checked", "true");
// Test Enter key
await page.keyboard.press("Enter");
await expect(radioButton).toHaveAttribute("aria-checked", "false");
});
test("has proper accessibility attributes", async ({ page }) => {
const radioButton = page.locator('[role="radio"]');
await expect(radioButton).toHaveAttribute("role", "radio");
await expect(radioButton).toHaveAttribute("aria-checked");
await expect(radioButton).toHaveAttribute("tabIndex", "0");
});
test("shows dot indicator when checked", async ({ page }) => {
await page.check('[data-testid="checked-control"]');
const radioButton = page.locator('[role="radio"]');
const dot = radioButton.locator("div").first();
await expect(dot).toHaveClass(/w-\[16px\]/, /h-\[16px\]/, /rounded-full/);
});
test("hides dot indicator when unchecked", async ({ page }) => {
await page.uncheck('[data-testid="checked-control"]');
const radioButton = page.locator('[role="radio"]');
const dot = radioButton.locator("div").first();
await expect(dot).toHaveCSS("background-color", "rgba(0, 0, 0, 0)");
});
test("maintains focus state", async ({ page }) => {
const radioButton = page.locator('[role="radio"]');
await radioButton.focus();
await expect(radioButton).toBeFocused();
// Should maintain focus after interaction
await page.keyboard.press("Space");
await expect(radioButton).toBeFocused();
});
test("handles mouse interaction", async ({ page }) => {
const radioButton = page.locator('[role="radio"]');
// Click to check
await radioButton.click();
await expect(radioButton).toHaveAttribute("aria-checked", "true");
// Click to uncheck
await radioButton.click();
await expect(radioButton).toHaveAttribute("aria-checked", "false");
});
test("shows proper styling for different modes", async ({ page }) => {
// Test standard mode
await page.selectOption('[data-testid="mode-control"]', "standard");
const radioButton = page.locator('[role="radio"]');
await expect(radioButton).toHaveClass(
/outline-\[var\(--color-border-default-tertiary\)\]/,
);
// Test inverse mode
await page.selectOption('[data-testid="mode-control"]', "inverse");
await expect(radioButton).toHaveClass(
/outline-\[var\(--color-border-inverse-primary\)\]/,
);
});
test("handles form submission", async ({ page }) => {
const hiddenInput = page.locator('input[type="radio"]');
await expect(hiddenInput).toBeVisible();
// Should be included in form data
await page.check('[data-testid="checked-control"]');
await expect(hiddenInput).toBeChecked();
});
});
@@ -1,183 +0,0 @@
import { expect } from "@storybook/test";
import { userEvent, within } from "@storybook/test";
export const DefaultInteraction = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const radioGroup = canvas.getByRole("radiogroup");
const radioButtons = canvas.getAllByRole("radio");
// Should have radiogroup role
await expect(radioGroup).toBeInTheDocument();
// Should have 3 radio buttons
await expect(radioButtons).toHaveLength(3);
// First should be selected initially
await expect(radioButtons[0]).toHaveAttribute("aria-checked", "true");
await expect(radioButtons[1]).toHaveAttribute("aria-checked", "false");
await expect(radioButtons[2]).toHaveAttribute("aria-checked", "false");
},
};
export const StandardInteraction = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const radioGroup = canvas.getByRole("radiogroup");
const radioButtons = canvas.getAllByRole("radio");
// Should have radiogroup role
await expect(radioGroup).toBeInTheDocument();
// Second should be selected initially
await expect(radioButtons[0]).toHaveAttribute("aria-checked", "false");
await expect(radioButtons[1]).toHaveAttribute("aria-checked", "true");
await expect(radioButtons[2]).toHaveAttribute("aria-checked", "false");
// Click first option
await userEvent.click(radioButtons[0]);
await expect(radioButtons[0]).toHaveAttribute("aria-checked", "true");
await expect(radioButtons[1]).toHaveAttribute("aria-checked", "false");
await expect(radioButtons[2]).toHaveAttribute("aria-checked", "false");
},
};
export const InverseInteraction = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const radioGroup = canvas.getByRole("radiogroup");
const radioButtons = canvas.getAllByRole("radio");
// Should have radiogroup role
await expect(radioGroup).toBeInTheDocument();
// First should be selected initially
await expect(radioButtons[0]).toHaveAttribute("aria-checked", "true");
await expect(radioButtons[1]).toHaveAttribute("aria-checked", "false");
await expect(radioButtons[2]).toHaveAttribute("aria-checked", "false");
// Click second option
await userEvent.click(radioButtons[1]);
await expect(radioButtons[0]).toHaveAttribute("aria-checked", "false");
await expect(radioButtons[1]).toHaveAttribute("aria-checked", "true");
await expect(radioButtons[2]).toHaveAttribute("aria-checked", "false");
},
};
export const InteractiveInteraction = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const radioGroup = canvas.getByRole("radiogroup");
const radioButtons = canvas.getAllByRole("radio");
// Should have radiogroup role
await expect(radioGroup).toBeInTheDocument();
// Should show initial state
await expect(canvas.getByText("Selected: option1")).toBeVisible();
// Click second option
await userEvent.click(radioButtons[1]);
await expect(canvas.getByText("Selected: option2")).toBeVisible();
// Click third option
await userEvent.click(radioButtons[2]);
await expect(canvas.getByText("Selected: option3")).toBeVisible();
},
};
export const KeyboardInteraction = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const radioButtons = canvas.getAllByRole("radio");
// Focus first radio button
await userEvent.click(radioButtons[0]);
await expect(radioButtons[0]).toHaveFocus();
// Navigate to second radio button
await userEvent.tab();
await expect(radioButtons[1]).toHaveFocus();
// Activate with Space
await userEvent.keyboard(" ");
await expect(radioButtons[1]).toHaveAttribute("aria-checked", "true");
await expect(radioButtons[0]).toHaveAttribute("aria-checked", "false");
// Navigate to third radio button
await userEvent.tab();
await expect(radioButtons[2]).toHaveFocus();
// Activate with Enter
await userEvent.keyboard("Enter");
await expect(radioButtons[2]).toHaveAttribute("aria-checked", "true");
await expect(radioButtons[1]).toHaveAttribute("aria-checked", "false");
},
};
export const AccessibilityInteraction = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const radioGroup = canvas.getByRole("radiogroup");
const radioButtons = canvas.getAllByRole("radio");
// Should have proper ARIA attributes
await expect(radioGroup).toHaveAttribute("role", "radiogroup");
radioButtons.forEach(async (button) => {
await expect(button).toHaveAttribute("role", "radio");
await expect(button).toHaveAttribute("aria-checked");
await expect(button).toHaveAttribute("tabIndex", "0");
});
// Should have accessible names
await expect(canvas.getByText("Option 1")).toBeVisible();
await expect(canvas.getByText("Option 2")).toBeVisible();
await expect(canvas.getByText("Option 3")).toBeVisible();
},
};
export const SingleSelectionInteraction = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const radioButtons = canvas.getAllByRole("radio");
// Initially first should be selected
await expect(radioButtons[0]).toHaveAttribute("aria-checked", "true");
await expect(radioButtons[1]).toHaveAttribute("aria-checked", "false");
await expect(radioButtons[2]).toHaveAttribute("aria-checked", "false");
// Click second option
await userEvent.click(radioButtons[1]);
await expect(radioButtons[0]).toHaveAttribute("aria-checked", "false");
await expect(radioButtons[1]).toHaveAttribute("aria-checked", "true");
await expect(radioButtons[2]).toHaveAttribute("aria-checked", "false");
// Click third option
await userEvent.click(radioButtons[2]);
await expect(radioButtons[0]).toHaveAttribute("aria-checked", "false");
await expect(radioButtons[1]).toHaveAttribute("aria-checked", "false");
await expect(radioButtons[2]).toHaveAttribute("aria-checked", "true");
},
};
export const FormIntegration = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const radioButtons = canvas.getAllByRole("radio");
// Should have hidden inputs for form submission
const hiddenInputs = canvas.getAllByRole("radio", { hidden: true });
await expect(hiddenInputs).toHaveLength(3);
// All should have the same name
const names = await Promise.all(
hiddenInputs.map((input) => input.getAttribute("name")),
);
expect(names.every((name) => name === names[0])).toBe(true);
// Should be included in form data
await userEvent.click(radioButtons[1]);
await expect(hiddenInputs[1]).toBeChecked();
},
};
@@ -1,252 +0,0 @@
import { test, expect } from "@playwright/test";
test.describe("RadioGroup Storybook Tests", () => {
test.beforeEach(async ({ page }) => {
await page.goto(
"http://localhost:6006/iframe.html?id=forms-radiogroup--default",
);
});
test("renders default story", async ({ page }) => {
const radioGroup = page.locator('[role="radiogroup"]');
await expect(radioGroup).toBeVisible();
const radioButtons = page.locator('[role="radio"]');
await expect(radioButtons).toHaveCount(3);
});
test("renders standard story", async ({ page }) => {
await page.goto(
"http://localhost:6006/iframe.html?id=forms-radiogroup--standard",
);
const radioGroup = page.locator('[role="radiogroup"]');
await expect(radioGroup).toBeVisible();
const radioButtons = page.locator('[role="radio"]');
await expect(radioButtons).toHaveCount(3);
// Second option should be selected
await expect(radioButtons.nth(1)).toHaveAttribute("aria-checked", "true");
});
test("renders inverse story", async ({ page }) => {
await page.goto(
"http://localhost:6006/iframe.html?id=forms-radiogroup--inverse",
);
const radioGroup = page.locator('[role="radiogroup"]');
await expect(radioGroup).toBeVisible();
const radioButtons = page.locator('[role="radio"]');
await expect(radioButtons).toHaveCount(3);
// First option should be selected
await expect(radioButtons.first()).toHaveAttribute("aria-checked", "true");
});
test("renders interactive story", async ({ page }) => {
await page.goto(
"http://localhost:6006/iframe.html?id=forms-radiogroup--interactive",
);
const radioGroup = page.locator('[role="radiogroup"]');
await expect(radioGroup).toBeVisible();
const radioButtons = page.locator('[role="radio"]');
await expect(radioButtons).toHaveCount(3);
// Should show selected value
await expect(page.locator('text="Selected: option1"')).toBeVisible();
});
test("interacts with controls", async ({ page }) => {
// Test mode control
await page.selectOption('[data-testid="mode-control"]', "inverse");
const radioButtons = page.locator('[role="radio"]');
// All radio buttons should have inverse styling
for (let i = 0; i < (await radioButtons.count()); i++) {
await expect(radioButtons.nth(i)).toHaveClass(
/outline-\[var\(--color-border-inverse-primary\)\]/,
);
}
await page.selectOption('[data-testid="mode-control"]', "standard");
for (let i = 0; i < (await radioButtons.count()); i++) {
await expect(radioButtons.nth(i)).toHaveClass(
/outline-\[var\(--color-border-default-tertiary\)\]/,
);
}
});
test("interacts with value control", async ({ page }) => {
// Test value control
await page.fill('[data-testid="value-control"]', "option2");
const radioButtons = page.locator('[role="radio"]');
await expect(radioButtons.nth(1)).toHaveAttribute("aria-checked", "true");
await expect(radioButtons.first()).toHaveAttribute("aria-checked", "false");
await expect(radioButtons.nth(2)).toHaveAttribute("aria-checked", "false");
});
test("handles keyboard navigation", async ({ page }) => {
const radioButtons = page.locator('[role="radio"]');
// Focus first radio button
await radioButtons.first().focus();
await expect(radioButtons.first()).toBeFocused();
// Navigate to second radio button
await page.keyboard.press("Tab");
await expect(radioButtons.nth(1)).toBeFocused();
// Navigate to third radio button
await page.keyboard.press("Tab");
await expect(radioButtons.nth(2)).toBeFocused();
});
test("handles keyboard activation", async ({ page }) => {
const radioButtons = page.locator('[role="radio"]');
// Focus second radio button
await radioButtons.nth(1).focus();
// Activate with Space
await page.keyboard.press("Space");
await expect(radioButtons.nth(1)).toHaveAttribute("aria-checked", "true");
await expect(radioButtons.first()).toHaveAttribute("aria-checked", "false");
// Activate third radio button with Enter
await radioButtons.nth(2).focus();
await page.keyboard.press("Enter");
await expect(radioButtons.nth(2)).toHaveAttribute("aria-checked", "true");
await expect(radioButtons.nth(1)).toHaveAttribute("aria-checked", "false");
});
test("handles mouse interaction", async ({ page }) => {
const radioButtons = page.locator('[role="radio"]');
// Click second option
await radioButtons.nth(1).click();
await expect(radioButtons.nth(1)).toHaveAttribute("aria-checked", "true");
await expect(radioButtons.first()).toHaveAttribute("aria-checked", "false");
// Click third option
await radioButtons.nth(2).click();
await expect(radioButtons.nth(2)).toHaveAttribute("aria-checked", "true");
await expect(radioButtons.nth(1)).toHaveAttribute("aria-checked", "false");
});
test("maintains single selection", async ({ page }) => {
const radioButtons = page.locator('[role="radio"]');
// Click first option
await radioButtons.first().click();
await expect(radioButtons.first()).toHaveAttribute("aria-checked", "true");
await expect(radioButtons.nth(1)).toHaveAttribute("aria-checked", "false");
await expect(radioButtons.nth(2)).toHaveAttribute("aria-checked", "false");
// Click second option
await radioButtons.nth(1).click();
await expect(radioButtons.first()).toHaveAttribute("aria-checked", "false");
await expect(radioButtons.nth(1)).toHaveAttribute("aria-checked", "true");
await expect(radioButtons.nth(2)).toHaveAttribute("aria-checked", "false");
});
test("has proper accessibility attributes", async ({ page }) => {
const radioGroup = page.locator('[role="radiogroup"]');
const radioButtons = page.locator('[role="radio"]');
await expect(radioGroup).toHaveAttribute("role", "radiogroup");
for (let i = 0; i < (await radioButtons.count()); i++) {
await expect(radioButtons.nth(i)).toHaveAttribute("role", "radio");
await expect(radioButtons.nth(i)).toHaveAttribute("aria-checked");
await expect(radioButtons.nth(i)).toHaveAttribute("tabIndex", "0");
}
});
test("shows proper labels", async ({ page }) => {
await expect(page.locator('text="Option 1"')).toBeVisible();
await expect(page.locator('text="Option 2"')).toBeVisible();
await expect(page.locator('text="Option 3"')).toBeVisible();
});
test("handles form submission", async ({ page }) => {
const hiddenInputs = page.locator('input[type="radio"]');
await expect(hiddenInputs).toHaveCount(3);
// All should have the same name
const names = await hiddenInputs.evaluateAll((inputs) =>
inputs.map((input) => input.getAttribute("name")),
);
expect(names.every((name) => name === names[0])).toBe(true);
});
test("shows dot indicators correctly", async ({ page }) => {
const radioButtons = page.locator('[role="radio"]');
// Initially first option should be selected
const firstDot = radioButtons.first().locator("div").first();
await expect(firstDot).toHaveClass(
/w-\[16px\]/,
/h-\[16px\]/,
/rounded-full/,
);
// Click second option
await radioButtons.nth(1).click();
// First dot should be hidden, second should be visible
const secondDot = radioButtons.nth(1).locator("div").first();
await expect(secondDot).toHaveClass(
/w-\[16px\]/,
/h-\[16px\]/,
/rounded-full/,
);
});
test("handles interactive story state changes", async ({ page }) => {
await page.goto(
"http://localhost:6006/iframe.html?id=forms-radiogroup--interactive",
);
// Should show initial state
await expect(page.locator('text="Selected: option1"')).toBeVisible();
// Click second option
const radioButtons = page.locator('[role="radio"]');
await radioButtons.nth(1).click();
// Should update displayed value
await expect(page.locator('text="Selected: option2"')).toBeVisible();
});
test("maintains focus state", async ({ page }) => {
const radioButtons = page.locator('[role="radio"]');
// Focus first radio button
await radioButtons.first().focus();
await expect(radioButtons.first()).toBeFocused();
// Should maintain focus after interaction
await page.keyboard.press("Space");
await expect(radioButtons.first()).toBeFocused();
});
test("handles different viewport sizes", async ({ page }) => {
// Test mobile viewport
await page.setViewportSize({ width: 375, height: 667 });
const radioGroup = page.locator('[role="radiogroup"]');
await expect(radioGroup).toBeVisible();
// Test tablet viewport
await page.setViewportSize({ width: 768, height: 1024 });
await expect(radioGroup).toBeVisible();
// Test desktop viewport
await page.setViewportSize({ width: 1920, height: 1080 });
await expect(radioGroup).toBeVisible();
});
});
-298
View File
@@ -1,298 +0,0 @@
import { render, screen, cleanup } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { vi, describe, test, expect, afterEach } from "vitest";
import AskOrganizer from "../../app/components/AskOrganizer";
afterEach(() => {
cleanup();
});
describe("AskOrganizer Component", () => {
test("renders with all props", () => {
render(
<AskOrganizer
title="Need help organizing?"
subtitle="Get expert guidance"
description="Our organizers can help you build better communities"
buttonText="Contact an organizer"
buttonHref="/contact"
/>,
);
expect(
screen.getByRole("heading", { name: "Need help organizing?" }),
).toBeInTheDocument();
expect(
screen.getByRole("heading", { name: "Get expert guidance" }),
).toBeInTheDocument();
// The description text might not be rendered or might be different
// Just verify the component renders without error
expect(
screen.getByRole("heading", { name: "Need help organizing?" }),
).toBeInTheDocument();
// Button renders as a link when href is provided
expect(
screen.getByRole("link", {
name: "Contact an organizer - Contact an organizer for help",
}),
).toBeInTheDocument();
});
test("renders with default button text", () => {
render(<AskOrganizer title="Test" subtitle="Test" description="Test" />);
// Button renders as a link when href is provided
expect(
screen.getByRole("link", {
name: "Ask an organizer - Contact an organizer for help",
}),
).toBeInTheDocument();
});
test("renders with custom className", () => {
render(
<AskOrganizer title="Test" subtitle="Test" className="custom-class" />,
);
const section = document.querySelector("section");
expect(section).toHaveClass("custom-class");
});
test("renders different variants", () => {
const { rerender } = render(
<AskOrganizer title="Test" subtitle="Test" variant="centered" />,
);
// Centered variant should have center alignment
const container = screen
.getByRole("region")
.querySelector('[class*="text-center"]');
expect(container).toBeInTheDocument();
rerender(
<AskOrganizer title="Test" subtitle="Test" variant="left-aligned" />,
);
// Left-aligned variant should have left alignment
const leftContainer = screen
.getByRole("region")
.querySelector('[class*="text-left"]');
expect(leftContainer).toBeInTheDocument();
});
test("renders ContentLockup with ask variant", () => {
render(
<AskOrganizer
title="Ask Title"
subtitle="Ask Subtitle"
description="Ask Description"
/>,
);
expect(
screen.getByRole("heading", { name: "Ask Title" }),
).toBeInTheDocument();
expect(
screen.getByRole("heading", { name: "Ask Subtitle" }),
).toBeInTheDocument();
// Description might not be rendered if not provided to ContentLockup
// Just verify the component renders without error
expect(
screen.getByRole("heading", { name: "Ask Title" }),
).toBeInTheDocument();
});
test("renders button with correct props", () => {
render(
<AskOrganizer
title="Test"
subtitle="Test"
buttonText="Custom Button"
buttonHref="/custom"
/>,
);
const button = screen.getByRole("link", {
name: "Custom Button - Contact an organizer for help",
});
expect(button).toHaveAttribute("href", "/custom");
expect(button).toHaveClass("xl:!px-[var(--spacing-scale-020)]");
});
test("handles button click events", async () => {
const user = userEvent.setup();
const onContactClick = vi.fn();
render(
<AskOrganizer
title="Test"
subtitle="Test"
onContactClick={onContactClick}
/>,
);
const button = screen.getByRole("link", {
name: "Ask an organizer - Contact an organizer for help",
});
await user.click(button);
expect(onContactClick).toHaveBeenCalledWith({
event: "contact_button_click",
component: "AskOrganizer",
variant: "centered",
buttonText: "Ask an organizer",
buttonHref: "#",
timestamp: expect.any(String),
});
});
test("applies analytics tracking", async () => {
const user = userEvent.setup();
const gtagSpy = vi.fn();
// Mock window.gtag
Object.defineProperty(window, "gtag", {
value: gtagSpy,
writable: true,
});
render(<AskOrganizer title="Test" subtitle="Test" />);
const button = screen.getByRole("link", {
name: "Ask an organizer - Contact an organizer for help",
});
await user.click(button);
// Verify gtag was called with the expected event
expect(gtagSpy).toHaveBeenCalledWith(
"event",
"contact_button_click",
expect.objectContaining({
event_category: "engagement",
event_label: "ask_organizer",
value: 1,
}),
);
});
test("renders with proper accessibility attributes", () => {
render(
<AskOrganizer title="Test" subtitle="Test" buttonText="Custom Button" />,
);
const section = document.querySelector("section");
expect(section).toHaveAttribute(
"aria-labelledby",
"ask-organizer-headline",
);
expect(section).toHaveAttribute("tabIndex", "-1");
const button = screen.getByRole("link", {
name: "Custom Button - Contact an organizer for help",
});
expect(button).toHaveAttribute(
"aria-label",
"Custom Button - Contact an organizer for help",
);
});
test("renders with design tokens", () => {
render(<AskOrganizer title="Test" subtitle="Test" />);
const section = document.querySelector("section");
expect(section).toHaveClass(
"py-[var(--spacing-scale-032)]",
"px-[var(--spacing-scale-032)]",
);
});
test("applies responsive spacing", () => {
render(<AskOrganizer title="Test" subtitle="Test" />);
const section = document.querySelector("section");
expect(section).toHaveClass(
"md:py-[var(--spacing-scale-096)]",
"md:px-[var(--spacing-scale-064)]",
);
});
test("renders with proper semantic structure", () => {
render(<AskOrganizer title="Test" subtitle="Test" />);
const section = document.querySelector("section");
expect(section).toBeInTheDocument();
// Check for proper heading structure
const headings = screen.getAllByRole("heading");
expect(headings).toHaveLength(2); // title and subtitle
});
test("applies variant-specific styling", () => {
const { rerender } = render(
<AskOrganizer title="Test" subtitle="Test" variant="compact" />,
);
// Compact variant should have different padding
const section = screen.getByRole("region");
expect(section).toHaveClass(
"py-[var(--spacing-scale-016)]",
"px-[var(--spacing-scale-016)]",
);
rerender(
<AskOrganizer title="Test" subtitle="Test" variant="left-aligned" />,
);
// Left-aligned variant should have left alignment
const container = section.querySelector('[class*="text-left"]');
expect(container).toBeInTheDocument();
});
test("renders button with custom styling", () => {
render(<AskOrganizer title="Test" subtitle="Test" />);
const button = screen.getByRole("link", {
name: "Ask an organizer - Contact an organizer for help",
});
expect(button).toHaveClass(
"xl:!px-[var(--spacing-scale-020)]",
"xl:!py-[var(--spacing-scale-012)]",
);
});
test("handles missing optional props gracefully", () => {
render(<AskOrganizer title="Test" />);
// Should still render the structure
const section = document.querySelector("section");
expect(section).toBeInTheDocument();
// Should render default button (as link when href is provided)
expect(
screen.getByRole("link", {
name: "Ask an organizer - Contact an organizer for help",
}),
).toBeInTheDocument();
});
test("applies responsive button container alignment", () => {
render(<AskOrganizer title="Test" subtitle="Test" variant="centered" />);
// Button renders as a link when href is provided
const buttonContainer = screen
.getByRole("link", {
name: "Ask an organizer - Contact an organizer for help",
})
.closest("div");
expect(buttonContainer).toHaveClass("flex", "justify-center");
});
test("renders with proper content gap", () => {
render(<AskOrganizer title="Test" subtitle="Test" variant="compact" />);
const container = screen
.getByRole("region")
.querySelector('[class*="flex flex-col"]');
expect(container).toHaveClass("gap-[var(--spacing-scale-020)]");
});
});
-160
View File
@@ -1,160 +0,0 @@
import { render, screen, fireEvent } from "@testing-library/react";
import { describe, it, expect, vi } from "vitest";
import Button from "../../app/components/Button";
describe("Button Component", () => {
it("renders button with default props", () => {
render(<Button>Click me</Button>);
const button = screen.getByRole("button", { name: /click me/i });
expect(button).toBeInTheDocument();
expect(button).toHaveClass("bg-[var(--color-surface-inverse-primary)]");
expect(button).toHaveAttribute("type", "button");
});
it("renders with custom className", () => {
const customClass = "custom-button-class";
render(<Button className={customClass}>Custom Button</Button>);
const button = screen.getByRole("button");
expect(button).toHaveClass(customClass);
});
it("applies variant classes correctly", () => {
const { rerender } = render(<Button variant="secondary">Secondary</Button>);
let button = screen.getByRole("button");
expect(button).toHaveClass("bg-transparent");
rerender(<Button variant="primary">Primary</Button>);
button = screen.getByRole("button");
expect(button).toHaveClass("bg-[var(--color-surface-default-primary)]");
rerender(<Button variant="outlined">Outlined</Button>);
button = screen.getByRole("button");
expect(button).toHaveClass("bg-transparent", "border-[1.5px]");
});
it("applies size classes correctly", () => {
const { rerender } = render(<Button size="small">Small</Button>);
let button = screen.getByRole("button");
expect(button).toHaveClass("px-[var(--spacing-measures-spacing-008)]");
rerender(<Button size="large">Large</Button>);
button = screen.getByRole("button");
expect(button).toHaveClass("px-[var(--spacing-scale-012)]");
rerender(<Button size="xlarge">XLarge</Button>);
button = screen.getByRole("button");
expect(button).toHaveClass("px-[var(--spacing-scale-020)]");
});
it("renders as link when href is provided", () => {
const href = "/test-page";
render(<Button href={href}>Link Button</Button>);
const link = screen.getByRole("link", { name: /link button/i });
expect(link).toBeInTheDocument();
expect(link).toHaveAttribute("href", href);
});
it("renders as button when href is not provided", () => {
render(<Button>Regular Button</Button>);
expect(screen.queryByRole("link")).not.toBeInTheDocument();
expect(screen.getByRole("button")).toBeInTheDocument();
});
it("handles click events", () => {
const handleClick = vi.fn();
render(<Button onClick={handleClick}>Clickable</Button>);
const button = screen.getByRole("button");
fireEvent.click(button);
expect(handleClick).toHaveBeenCalledTimes(1);
});
it("applies disabled state correctly", () => {
render(<Button disabled>Disabled Button</Button>);
const button = screen.getByRole("button");
expect(button).toBeDisabled();
expect(button).toHaveClass(
"disabled:opacity-50",
"disabled:cursor-not-allowed",
);
expect(button).toHaveAttribute("aria-disabled", "true");
expect(button).toHaveAttribute("tabIndex", "-1");
});
it("applies proper accessibility attributes", () => {
render(<Button ariaLabel="Custom label">Button</Button>);
const button = screen.getByRole("button", { name: /custom label/i });
expect(button).toHaveAttribute("aria-label", "Custom label");
});
it("applies hover effects correctly", () => {
render(<Button>Hover Button</Button>);
const button = screen.getByRole("button");
expect(button).toHaveClass("hover:scale-[1.02]", "transition-all");
});
it("applies focus styles correctly", () => {
render(<Button>Focus Button</Button>);
const button = screen.getByRole("button");
expect(button).toHaveClass("focus:outline-none", "focus:ring-1");
});
it("applies active styles correctly", () => {
render(<Button>Active Button</Button>);
const button = screen.getByRole("button");
expect(button).toHaveClass("active:scale-[0.98]");
});
it("handles target and rel props for links", () => {
render(
<Button href="/test" target="_blank" rel="noopener">
External Link
</Button>,
);
const link = screen.getByRole("link");
expect(link).toHaveAttribute("target", "_blank");
expect(link).toHaveAttribute("rel", "noopener");
});
it("forwards additional props", () => {
render(<Button data-testid="test-button">Test Button</Button>);
const button = screen.getByTestId("test-button");
expect(button).toBeInTheDocument();
});
it("applies proper font styles for different sizes", () => {
const { rerender } = render(<Button size="xsmall">XSmall</Button>);
let button = screen.getByRole("button");
expect(button).toHaveClass("text-[10px]", "leading-[12px]");
rerender(<Button size="xlarge">XLarge</Button>);
button = screen.getByRole("button");
expect(button).toHaveClass("text-[24px]", "leading-[28px]");
});
it("applies proper border radius", () => {
render(<Button>Rounded Button</Button>);
const button = screen.getByRole("button");
expect(button).toHaveClass("rounded-[var(--radius-measures-radius-full)]");
});
it("maintains proper tab index when not disabled", () => {
render(<Button>Tab Button</Button>);
const button = screen.getByRole("button");
expect(button).toHaveAttribute("tabIndex", "0");
});
});
-166
View File
@@ -1,166 +0,0 @@
import React from "react";
import { render, screen, fireEvent } from "@testing-library/react";
import { expect, test, describe, vi } from "vitest";
import Checkbox from "../../app/components/Checkbox";
describe("Checkbox Component", () => {
test("renders with default props", () => {
render(<Checkbox />);
const checkbox = screen.getByRole("checkbox");
expect(checkbox).toBeInTheDocument();
expect(checkbox).toHaveAttribute("aria-checked", "false");
});
test("renders with label", () => {
render(<Checkbox label="Test checkbox" />);
expect(screen.getByText("Test checkbox")).toBeInTheDocument();
});
test("renders as checked when checked prop is true", () => {
render(<Checkbox checked={true} />);
const checkbox = screen.getByRole("checkbox");
expect(checkbox).toHaveAttribute("aria-checked", "true");
});
test("renders as unchecked when checked prop is false", () => {
render(<Checkbox checked={false} />);
const checkbox = screen.getByRole("checkbox");
expect(checkbox).toHaveAttribute("aria-checked", "false");
});
test("calls onChange when clicked", () => {
const handleChange = vi.fn();
render(<Checkbox onChange={handleChange} />);
const checkbox = screen.getByRole("checkbox");
fireEvent.click(checkbox);
expect(handleChange).toHaveBeenCalledWith({
checked: true,
value: undefined,
event: expect.any(Object),
});
});
test("calls onChange when toggled from checked to unchecked", () => {
const handleChange = vi.fn();
render(<Checkbox checked={true} onChange={handleChange} />);
const checkbox = screen.getByRole("checkbox");
fireEvent.click(checkbox);
expect(handleChange).toHaveBeenCalledWith({
checked: false,
value: undefined,
event: expect.any(Object),
});
});
test("handles keyboard navigation", () => {
const handleChange = vi.fn();
render(<Checkbox onChange={handleChange} />);
const checkbox = screen.getByRole("checkbox");
// Test Space key
fireEvent.keyDown(checkbox, { key: " " });
expect(handleChange).toHaveBeenCalledWith({
checked: true,
value: undefined,
event: expect.any(Object),
});
// Test Enter key
fireEvent.keyDown(checkbox, { key: "Enter" });
expect(handleChange).toHaveBeenCalledTimes(2);
});
test("does not call onChange when disabled", () => {
const handleChange = vi.fn();
render(<Checkbox disabled={true} onChange={handleChange} />);
const checkbox = screen.getByRole("checkbox");
fireEvent.click(checkbox);
expect(handleChange).not.toHaveBeenCalled();
});
test("applies disabled attributes when disabled", () => {
render(<Checkbox disabled={true} />);
const checkbox = screen.getByRole("checkbox");
expect(checkbox).toHaveAttribute("aria-disabled", "true");
expect(checkbox).toHaveAttribute("tabIndex", "-1");
});
test("applies correct tabIndex when not disabled", () => {
render(<Checkbox />);
const checkbox = screen.getByRole("checkbox");
expect(checkbox).toHaveAttribute("tabIndex", "0");
});
test("renders with standard mode by default", () => {
render(<Checkbox />);
const checkbox = screen.getByRole("checkbox");
expect(checkbox).toBeInTheDocument();
});
test("renders with inverse mode", () => {
render(<Checkbox mode="inverse" />);
const checkbox = screen.getByRole("checkbox");
expect(checkbox).toBeInTheDocument();
});
test("applies custom className", () => {
render(<Checkbox className="custom-class" />);
const label = screen.getByRole("checkbox").closest("label");
expect(label).toHaveClass("custom-class");
});
test("passes through additional props", () => {
render(<Checkbox id="test-checkbox" name="test" value="test-value" />);
const checkbox = screen.getByRole("checkbox");
expect(checkbox).toHaveAttribute("id", "test-checkbox");
});
test("renders hidden native input for form compatibility", () => {
render(<Checkbox name="test" value="test-value" checked={true} />);
const hiddenInput = screen.getByDisplayValue("test-value");
expect(hiddenInput).toBeInTheDocument();
expect(hiddenInput).toHaveAttribute("type", "checkbox");
expect(hiddenInput).toHaveAttribute("name", "test");
expect(hiddenInput).toBeChecked();
});
test("applies aria-label when provided", () => {
render(<Checkbox ariaLabel="Custom label" />);
const checkbox = screen.getByRole("checkbox");
expect(checkbox).toHaveAttribute("aria-label", "Custom label");
});
test("prevents default on mouse down", () => {
render(<Checkbox />);
const label = screen.getByRole("checkbox").closest("label");
const mouseDownEvent = new MouseEvent("mousedown", { bubbles: true });
const preventDefaultSpy = vi.spyOn(mouseDownEvent, "preventDefault");
fireEvent(label, mouseDownEvent);
expect(preventDefaultSpy).toHaveBeenCalled();
});
test("renders checkmark SVG when checked", () => {
render(<Checkbox checked={true} />);
const svg = screen.getByRole("checkbox").querySelector("svg");
expect(svg).toBeInTheDocument();
expect(svg).toHaveAttribute("aria-hidden", "true");
expect(svg).toHaveAttribute("focusable", "false");
});
test("does not render checkmark SVG when unchecked", () => {
render(<Checkbox checked={false} />);
const svg = screen.getByRole("checkbox").querySelector("svg");
expect(svg).toBeInTheDocument();
// SVG should be present but checkmark should be transparent
const path = svg.querySelector("polyline");
expect(path).toHaveAttribute("stroke", "transparent");
});
});
-287
View File
@@ -1,287 +0,0 @@
import { describe, it, expect, vi } from "vitest";
import { render, screen } from "@testing-library/react";
import ContentBanner from "../../app/components/ContentBanner";
// Mock Next.js components
vi.mock("next/link", () => {
return {
default: ({ children, href, ...props }) => (
<a href={href} {...props}>
{children}
</a>
),
};
});
// Mock asset utils
vi.mock("../../lib/assetUtils", () => ({
getAssetPath: vi.fn((asset) => `/assets/${asset}`),
ASSETS: {
CONTENT_BANNER_1: "Content_Banner_1.svg",
CONTENT_BANNER_2: "Content_Banner_2.svg",
},
}));
// Mock blog post data
const mockPost = {
slug: "test-article",
frontmatter: {
title: "Test Article Title",
description: "This is a test article description",
author: "Test Author",
date: "2025-04-15",
thumbnail: {
horizontal: "test-article-horizontal.svg",
},
banner: {
horizontal: "test-article-banner.svg",
},
},
};
describe("ContentBanner", () => {
it("renders the banner with correct structure", () => {
render(<ContentBanner post={mockPost} />);
// Check that the banner container exists - it's the first div with the specific classes
const banner = document.querySelector(
"div[class*='pt-[var(--measures-spacing-016)]']",
);
expect(banner).toBeInTheDocument();
expect(banner).toHaveClass(
"pt-[var(--measures-spacing-016)]",
"md:pt-[var(--measures-spacing-008)]",
"lg:pt-[50px]",
"xl:pt-[112px]",
"h-[275px]",
"sm:h-[326px]",
"md:h-[224px]",
"lg:h-[358.4px]",
"xl:h-[504px]",
"relative",
"w-full",
"sm:overflow-hidden",
);
});
it("displays the background image correctly", () => {
render(<ContentBanner post={mockPost} />);
// Check for background div with correct styling
const backgroundDiv = document.querySelector(
"div[style*='background-image']",
);
expect(backgroundDiv).toBeInTheDocument();
expect(backgroundDiv).toHaveClass(
"absolute",
"inset-0",
"w-full",
"h-full",
"bg-cover",
"bg-no-repeat",
"aspect-[320/225.5]",
);
});
it("shows banner image at md breakpoint and above", () => {
render(<ContentBanner post={mockPost} />);
// Check for the md+ background div with banner image
const mdBackgroundDiv = document.querySelector(
"div[style*='test-article-banner.svg']",
);
expect(mdBackgroundDiv).toBeInTheDocument();
expect(mdBackgroundDiv).toHaveClass("hidden", "md:block");
});
it("displays the article title", () => {
render(<ContentBanner post={mockPost} />);
expect(screen.getByText("Test Article Title")).toBeInTheDocument();
});
it("displays the article description", () => {
render(<ContentBanner post={mockPost} />);
expect(
screen.getByText("This is a test article description"),
).toBeInTheDocument();
});
it("displays the author and date metadata", () => {
render(<ContentBanner post={mockPost} />);
expect(screen.getByText("Test Author")).toBeInTheDocument();
expect(screen.getByText("April 2025")).toBeInTheDocument();
});
it("applies correct styling classes", () => {
render(<ContentBanner post={mockPost} />);
// Check the content container div
const contentContainer = document.querySelector(
"div[class*='relative z-10']",
);
expect(contentContainer).toBeInTheDocument();
expect(contentContainer).toHaveClass(
"relative",
"z-10",
"h-full",
"flex",
"flex-col",
);
});
it("applies correct text styling", () => {
render(<ContentBanner post={mockPost} />);
const title = screen.getByText("Test Article Title");
expect(title).toHaveClass(
"font-bricolage",
"font-medium",
"text-[18px]",
"leading-[120%]",
"text-[var(--color-content-inverse-brand-royal)]",
);
const description = screen.getByText("This is a test article description");
expect(description).toHaveClass(
"font-inter",
"font-normal",
"text-[12px]",
"leading-[16px]",
"text-[var(--color-content-inverse-brand-royal)]",
);
});
it("applies correct metadata styling", () => {
render(<ContentBanner post={mockPost} />);
const author = screen.getByText("Test Author");
expect(author).toHaveClass(
"font-inter",
"font-normal",
"text-[10px]",
"leading-[14px]",
"text-[var(--color-content-inverse-brand-royal)]",
);
const date = screen.getByText("April 2025");
expect(date).toHaveClass(
"font-inter",
"font-normal",
"text-[10px]",
"leading-[14px]",
"text-[var(--color-content-inverse-brand-royal)]",
);
});
it("has proper spacing between elements", () => {
render(<ContentBanner post={mockPost} />);
// Check the ContentContainer spacing
const contentContainer = document.querySelector(
"div[class*='relative z-20']",
);
expect(contentContainer).toHaveClass("gap-[var(--measures-spacing-012)]");
});
it("has proper outer container padding", () => {
render(<ContentBanner post={mockPost} />);
const outerContainer = document.querySelector(
"div[class*='pt-[var(--measures-spacing-016)]']",
);
expect(outerContainer).toHaveClass(
"pt-[var(--measures-spacing-016)]",
"md:pt-[var(--measures-spacing-008)]",
"lg:pt-[50px]",
"xl:pt-[112px]",
);
});
it("handles missing post data gracefully", () => {
const incompletePost = {
slug: "incomplete",
frontmatter: {
title: "Incomplete Post",
// Missing other fields
},
};
render(<ContentBanner post={incompletePost} />);
expect(screen.getByText("Incomplete Post")).toBeInTheDocument();
});
it("falls back to thumbnail.horizontal when banner.horizontal is missing", () => {
const postWithoutBanner = {
...mockPost,
frontmatter: {
...mockPost.frontmatter,
banner: undefined,
},
};
render(<ContentBanner post={postWithoutBanner} />);
// Should use thumbnail.horizontal for md+ breakpoint
const mdBackgroundDiv = document.querySelector(
"div[style*='test-article-horizontal.svg'][class*='md:block']",
);
expect(mdBackgroundDiv).toBeInTheDocument();
expect(mdBackgroundDiv).toHaveClass("hidden", "md:block");
});
it("falls back to default banner when no images are provided", () => {
const postWithoutImages = {
...mockPost,
frontmatter: {
...mockPost.frontmatter,
thumbnail: undefined,
banner: undefined,
},
};
render(<ContentBanner post={postWithoutImages} />);
// Should use default banner for md+ breakpoint
const mdBackgroundDiv = document.querySelector(
"div[style*='Content_Banner_2.svg']",
);
expect(mdBackgroundDiv).toBeInTheDocument();
expect(mdBackgroundDiv).toHaveClass("hidden", "md:block");
});
it("applies responsive text sizing", () => {
render(<ContentBanner post={mockPost} />);
const title = screen.getByText("Test Article Title");
expect(title).toHaveClass(
"sm:text-[24px]",
"md:text-[32px]",
"lg:text-[44px]",
"xl:text-[64px]",
);
const description = screen.getByText("This is a test article description");
expect(description).toHaveClass(
"sm:text-[14px]",
"md:text-[14px]",
"lg:text-[18px]",
"xl:text-[24px]",
);
});
it("has proper accessibility attributes", () => {
render(<ContentBanner post={mockPost} />);
// Check that the component renders without accessibility errors
const banner = document.querySelector("div");
expect(banner).toBeInTheDocument();
// Check that the icon has proper alt text
const icon = screen.getByAltText("Icon for Test Article Title");
expect(icon).toBeInTheDocument();
});
});
-321
View File
@@ -1,321 +0,0 @@
import React from "react";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { expect, describe, it, vi, beforeEach } from "vitest";
import { axe, toHaveNoViolations } from "jest-axe";
import ContextMenu from "../../app/components/ContextMenu";
import ContextMenuItem from "../../app/components/ContextMenuItem";
import ContextMenuSection from "../../app/components/ContextMenuSection";
import ContextMenuDivider from "../../app/components/ContextMenuDivider";
expect.extend(toHaveNoViolations);
describe("ContextMenu Component", () => {
const defaultProps = {
children: "Context Menu Content",
};
describe("Rendering", () => {
it("renders with default props", () => {
render(<ContextMenu {...defaultProps} />);
expect(screen.getByText("Context Menu Content")).toBeInTheDocument();
});
it("renders with custom className", () => {
render(<ContextMenu {...defaultProps} className="custom-class" />);
const menu = screen.getByText("Context Menu Content").closest("div");
expect(menu).toHaveClass("custom-class");
});
it("applies correct base styles", () => {
render(<ContextMenu {...defaultProps} />);
const menu = screen.getByText("Context Menu Content").closest("div");
expect(menu).toHaveClass(
"bg-black",
"border",
"rounded-[var(--measures-radius-medium)]",
"shadow-lg",
"p-[4px]",
);
});
it("has solid black background", () => {
render(<ContextMenu {...defaultProps} />);
const menu = screen.getByText("Context Menu Content").closest("div");
expect(menu).toHaveStyle({ backgroundColor: "#000000" });
});
});
describe("Accessibility", () => {
it("has no accessibility violations", async () => {
const { container } = render(
<ContextMenu {...defaultProps}>
<ContextMenuItem onClick={vi.fn()}>Menu Item</ContextMenuItem>
</ContextMenu>,
);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
it("has proper role", () => {
render(<ContextMenu {...defaultProps} />);
const menu = screen.getByText("Context Menu Content").closest("div");
expect(menu).toHaveAttribute("role", "menu");
});
});
});
describe("ContextMenuItem Component", () => {
const defaultProps = {
children: "Menu Item",
onClick: vi.fn(),
};
beforeEach(() => {
vi.clearAllMocks();
});
describe("Rendering", () => {
it("renders with default props", () => {
render(<ContextMenuItem {...defaultProps} />);
expect(screen.getByText("Menu Item")).toBeInTheDocument();
});
it("renders as selected when selected prop is true", () => {
render(<ContextMenuItem {...defaultProps} selected={true} />);
const item = screen.getByRole("menuitem");
expect(item).toHaveClass(
"bg-[var(--color-surface-default-secondary)]",
"rounded-[var(--measures-radius-small)]",
);
});
it("renders with submenu arrow when hasSubmenu prop is true", () => {
render(<ContextMenuItem {...defaultProps} hasSubmenu={true} />);
// Check for the right-pointing chevron SVG
const item = screen.getByRole("menuitem");
const svg = item.querySelector("svg:last-child");
expect(svg).toBeInTheDocument();
});
it("renders with checkmark when selected prop is true", () => {
render(<ContextMenuItem {...defaultProps} selected={true} />);
// Check for the checkmark SVG
const item = screen.getByRole("menuitem");
const svg = item.querySelector("svg:first-child");
expect(svg).toBeInTheDocument();
});
it("applies correct size styles", () => {
render(<ContextMenuItem {...defaultProps} size="small" />);
const item = screen.getByRole("menuitem");
expect(item).toHaveClass("text-[10px]", "leading-[14px]");
});
it("applies medium size styles", () => {
render(<ContextMenuItem {...defaultProps} size="medium" />);
const item = screen.getByRole("menuitem");
expect(item).toHaveClass("text-[14px]", "leading-[20px]");
});
it("applies large size styles", () => {
render(<ContextMenuItem {...defaultProps} size="large" />);
const item = screen.getByRole("menuitem");
expect(item).toHaveClass("text-[16px]", "leading-[24px]");
});
});
describe("Interaction", () => {
it("calls onClick when clicked", async () => {
const user = userEvent.setup();
render(<ContextMenuItem {...defaultProps} />);
const item = screen.getByText("Menu Item");
await user.click(item);
expect(defaultProps.onClick).toHaveBeenCalledTimes(1);
});
it("does not call onClick when disabled", async () => {
const user = userEvent.setup();
render(<ContextMenuItem {...defaultProps} disabled={true} />);
const item = screen.getByText("Menu Item");
await user.click(item);
expect(defaultProps.onClick).not.toHaveBeenCalled();
});
it("has hover effects", () => {
render(<ContextMenuItem {...defaultProps} />);
const item = screen.getByRole("menuitem");
expect(item).toHaveClass(
"hover:!bg-[var(--color-surface-default-secondary)]",
);
});
});
describe("Accessibility", () => {
it("has no accessibility violations", async () => {
const { container } = render(
<ContextMenu>
<ContextMenuItem {...defaultProps} />
</ContextMenu>,
);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
it("has proper role", () => {
render(<ContextMenuItem {...defaultProps} />);
const item = screen.getByRole("menuitem");
expect(item).toBeInTheDocument();
});
});
describe("Styling", () => {
it("applies correct text color", () => {
render(<ContextMenuItem {...defaultProps} />);
const item = screen.getByRole("menuitem");
expect(item).toHaveClass(
"text-[var(--color-content-default-brand-primary)]",
);
});
it("applies correct padding", () => {
render(<ContextMenuItem {...defaultProps} />);
const item = screen.getByRole("menuitem");
expect(item).toHaveClass("px-[8px]", "py-[4px]");
});
it("applies correct gap between checkmark and text", () => {
render(<ContextMenuItem {...defaultProps} selected={true} />);
const item = screen.getByText("Menu Item").closest("div");
expect(item).toHaveClass("gap-[8px]");
});
});
});
describe("ContextMenuSection Component", () => {
const defaultProps = {
title: "Section Title",
children: "Section Content",
};
describe("Rendering", () => {
it("renders with title and children", () => {
render(<ContextMenuSection {...defaultProps} />);
expect(screen.getByText("Section Title")).toBeInTheDocument();
expect(screen.getByText("Section Content")).toBeInTheDocument();
});
it("renders without title when not provided", () => {
render(<ContextMenuSection>Section Content</ContextMenuSection>);
expect(screen.getByText("Section Content")).toBeInTheDocument();
expect(screen.queryByText("Section Title")).not.toBeInTheDocument();
});
it("applies correct title styling", () => {
render(<ContextMenuSection {...defaultProps} />);
const title = screen.getByText("Section Title");
expect(title).toHaveClass(
"text-[var(--color-content-default-primary)]",
"font-medium",
);
});
});
describe("Accessibility", () => {
it("has no accessibility violations", async () => {
const { container } = render(<ContextMenuSection {...defaultProps} />);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
});
});
describe("ContextMenuDivider Component", () => {
describe("Rendering", () => {
it("renders divider", () => {
render(<ContextMenuDivider />);
const divider = screen.getByRole("separator");
expect(divider).toBeInTheDocument();
});
it("applies correct styling", () => {
render(<ContextMenuDivider />);
const divider = screen.getByRole("separator");
expect(divider).toHaveClass(
"border-t",
"border-[var(--color-border-default-tertiary)]",
"my-1",
);
});
});
describe("Accessibility", () => {
it("has no accessibility violations", async () => {
const { container } = render(<ContextMenuDivider />);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
});
});
describe("ContextMenu Components Integration", () => {
const TestMenu = () => (
<ContextMenu>
<ContextMenuSection title="First Section">
<ContextMenuItem onClick={vi.fn()}>Item 1</ContextMenuItem>
<ContextMenuItem onClick={vi.fn()} selected={true}>
Item 2
</ContextMenuItem>
</ContextMenuSection>
<ContextMenuDivider />
<ContextMenuSection title="Second Section">
<ContextMenuItem onClick={vi.fn()} hasSubmenu={true}>
Item 3
</ContextMenuItem>
</ContextMenuSection>
</ContextMenu>
);
it("renders all components together", () => {
render(<TestMenu />);
expect(screen.getByText("First Section")).toBeInTheDocument();
expect(screen.getByText("Item 1")).toBeInTheDocument();
expect(screen.getByText("Item 2")).toBeInTheDocument();
expect(screen.getByText("Second Section")).toBeInTheDocument();
expect(screen.getByText("Item 3")).toBeInTheDocument();
expect(screen.getByRole("separator")).toBeInTheDocument();
});
it("has no accessibility violations when integrated", async () => {
const { container } = render(<TestMenu />);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
});
-144
View File
@@ -1,144 +0,0 @@
import { render, screen, cleanup } from "@testing-library/react";
import { describe, test, expect, afterEach } from "vitest";
import FeatureGrid from "../../app/components/FeatureGrid";
afterEach(() => {
cleanup();
});
describe("FeatureGrid Component", () => {
test("renders with title and subtitle", () => {
render(
<FeatureGrid
title="Feature Tools"
subtitle="Everything you need to build better communities"
/>,
);
expect(
screen.getByRole("heading", { name: "Feature Tools" }),
).toBeInTheDocument();
expect(
screen.getByRole("heading", {
name: "Everything you need to build better communities",
}),
).toBeInTheDocument();
});
test("renders with custom className", () => {
render(
<FeatureGrid title="Test" subtitle="Test" className="custom-class" />,
);
const section = document.querySelector("section");
expect(section).toHaveClass("custom-class");
});
test("renders all four MiniCard components", () => {
render(<FeatureGrid title="Test" subtitle="Test" />);
// Check for all four MiniCard components
expect(
screen.getByRole("link", { name: "Decision-making support tools" }),
).toBeInTheDocument();
expect(
screen.getByRole("link", { name: "Values alignment exercises" }),
).toBeInTheDocument();
expect(
screen.getByRole("link", { name: "Membership guidance resources" }),
).toBeInTheDocument();
expect(
screen.getByRole("link", { name: "Conflict resolution tools" }),
).toBeInTheDocument();
});
test("renders ContentLockup with feature variant", () => {
render(<FeatureGrid title="Feature Title" subtitle="Feature Subtitle" />);
expect(
screen.getByRole("heading", { name: "Feature Title" }),
).toBeInTheDocument();
expect(
screen.getByRole("heading", { name: "Feature Subtitle" }),
).toBeInTheDocument();
expect(
screen.getByRole("link", { name: "Learn more" }),
).toBeInTheDocument();
});
test("has proper accessibility attributes", () => {
render(<FeatureGrid title="Test" subtitle="Test" />);
const section = document.querySelector("section");
expect(section).toHaveAttribute("aria-labelledby", "feature-grid-headline");
expect(section).toHaveAttribute("tabIndex", "-1");
const grid = screen.getByRole("grid");
expect(grid).toHaveAttribute("aria-label", "Feature tools and services");
});
test("renders with design tokens", () => {
render(<FeatureGrid title="Test" subtitle="Test" />);
const section = document.querySelector("section");
expect(section).toHaveClass("p-0", "lg:p-[var(--spacing-scale-064)]");
const container = section.querySelector('[class*="bg-[#171717]"]');
expect(container).toBeInTheDocument();
});
test("applies responsive grid layout", () => {
render(<FeatureGrid title="Test" subtitle="Test" />);
const grid = screen.getByRole("grid");
expect(grid).toHaveClass("grid", "grid-cols-2", "md:grid-cols-4");
});
test("renders MiniCard with correct props", () => {
render(<FeatureGrid title="Test" subtitle="Test" />);
// Check first MiniCard (Decision-making support)
const firstCard = screen.getByRole("link", {
name: "Decision-making support tools",
});
expect(firstCard).toHaveAttribute("href", "#decision-making");
// Check second MiniCard (Values alignment)
const secondCard = screen.getByRole("link", {
name: "Values alignment exercises",
});
expect(secondCard).toHaveAttribute("href", "#values-alignment");
});
test("renders with proper semantic structure", () => {
render(<FeatureGrid title="Test" subtitle="Test" />);
const section = document.querySelector("section");
expect(section).toBeInTheDocument();
const grid = screen.getByRole("grid");
expect(grid).toBeInTheDocument();
});
test("handles missing optional props gracefully", () => {
render(<FeatureGrid />);
// Should still render the structure
const section = document.querySelector("section");
expect(section).toBeInTheDocument();
// Should render default MiniCards
expect(
screen.getByRole("link", { name: "Decision-making support tools" }),
).toBeInTheDocument();
});
test("applies focus-within styles", () => {
render(<FeatureGrid title="Test" subtitle="Test" />);
const container = document
.querySelector("section")
.querySelector('[class*="focus-within:ring-2"]');
expect(container).toBeInTheDocument();
});
});
-285
View File
@@ -1,285 +0,0 @@
import { describe, test, expect } from "vitest";
import { render, screen } from "@testing-library/react";
import Footer from "../../app/components/Footer";
describe("Footer", () => {
test("renders footer with correct structure", () => {
render(<Footer />);
const footers = screen.getAllByRole("contentinfo");
expect(footers.length).toBeGreaterThan(0);
const footer = footers[0];
expect(footer).toBeInTheDocument();
expect(footer).toHaveClass("bg-[var(--color-surface-default-primary)]");
expect(footer).toHaveClass("w-full");
});
test("renders schema markup for organization information", () => {
render(<Footer />);
const script = document.querySelector('script[type="application/ld+json"]');
expect(script).toBeInTheDocument();
const schemaData = JSON.parse(script.textContent);
expect(schemaData["@type"]).toBe("Organization");
expect(schemaData.name).toBe("Media Economies Design Lab");
expect(schemaData.email).toBe("medlab@colorado.edu");
expect(schemaData.url).toBe("https://communityrule.com");
expect(schemaData.sameAs).toContain(
"https://bsky.app/profile/medlabboulder",
);
expect(schemaData.sameAs).toContain("https://gitlab.com/medlabboulder");
});
test("renders organization name and contact information", () => {
render(<Footer />);
expect(
screen.getAllByText("Media Economies Design Lab").length,
).toBeGreaterThan(0);
const emailLinks = screen.getAllByRole("link", {
name: "medlab@colorado.edu",
});
expect(emailLinks.length).toBeGreaterThan(0);
const emailLink = emailLinks[0];
expect(emailLink).toBeInTheDocument();
expect(emailLink).toHaveAttribute("href", "mailto:medlab@colorado.edu");
});
test("renders social media links with correct accessibility", () => {
render(<Footer />);
// Check Bluesky link
const blueskyLinks = screen.getAllByRole("link", {
name: "Follow us on Bluesky",
});
expect(blueskyLinks.length).toBeGreaterThan(0);
const blueskyLink = blueskyLinks[0];
expect(blueskyLink).toBeInTheDocument();
expect(screen.getAllByText("medlabboulder").length).toBeGreaterThan(0);
// Check GitLab link
const gitlabLinks = screen.getAllByRole("link", {
name: "Follow us on GitLab",
});
expect(gitlabLinks.length).toBeGreaterThan(0);
const gitlabLink = gitlabLinks[0];
expect(gitlabLink).toBeInTheDocument();
// Check social media images
const blueskyImages = screen.getAllByAltText("Bluesky");
expect(blueskyImages.length).toBeGreaterThan(0);
const blueskyImage = blueskyImages[0];
expect(blueskyImage).toBeInTheDocument();
expect(blueskyImage).toHaveAttribute("src", "/assets/Bluesky_Logo.svg");
const gitlabImages = screen.getAllByAltText("GitLab");
expect(gitlabImages.length).toBeGreaterThan(0);
const gitlabImage = gitlabImages[0];
expect(gitlabImage).toBeInTheDocument();
expect(gitlabImage).toHaveAttribute("src", "/assets/GitLab_Icon.png");
});
test("renders navigation links", () => {
render(<Footer />);
expect(
screen.getAllByRole("link", { name: "Use cases" }).length,
).toBeGreaterThan(0);
expect(
screen.getAllByRole("link", { name: "Learn" }).length,
).toBeGreaterThan(0);
expect(
screen.getAllByRole("link", { name: "About" }).length,
).toBeGreaterThan(0);
});
test("renders legal links", () => {
render(<Footer />);
expect(
screen.getAllByRole("link", { name: "Privacy Policy" }).length,
).toBeGreaterThan(0);
expect(
screen.getAllByRole("link", { name: "Terms of Service" }).length,
).toBeGreaterThan(0);
expect(
screen.getAllByRole("link", { name: "Cookies Settings" }).length,
).toBeGreaterThan(0);
});
test("renders copyright information", () => {
render(<Footer />);
expect(screen.getAllByText("© All right reserved").length).toBeGreaterThan(
0,
);
});
test("renders responsive logo configurations", () => {
render(<Footer />);
// Check that logo containers exist for different breakpoints
const logoContainers = document.querySelectorAll(
'[class*="block sm:hidden"], [class*="hidden sm:block lg:hidden"], [class*="hidden lg:block"]',
);
expect(logoContainers.length).toBeGreaterThan(0);
});
test("has correct CSS classes for responsive design", () => {
render(<Footer />);
const footers = screen.getAllByRole("contentinfo");
expect(footers.length).toBeGreaterThan(0);
const footer = footers[0];
const mainContainer = footer.querySelector("div");
expect(mainContainer).toHaveClass("flex");
expect(mainContainer).toHaveClass("flex-col");
expect(mainContainer).toHaveClass("items-start");
expect(mainContainer).toHaveClass("mx-auto");
});
test("renders separator component", () => {
render(<Footer />);
// The Separator component should be rendered (it uses a div with border, not hr)
const separator = document.querySelector(
".bg-\\[var\\(--border-color-default-secondary\\)\\]",
);
expect(separator).toBeInTheDocument();
});
test("social media links have hover and focus states", () => {
render(<Footer />);
const blueskyLinks = screen.getAllByRole("link", {
name: "Follow us on Bluesky",
});
expect(blueskyLinks.length).toBeGreaterThan(0);
expect(blueskyLinks[0]).toHaveClass("hover:opacity-80");
expect(blueskyLinks[0]).toHaveClass("active:opacity-60");
expect(blueskyLinks[0]).toHaveClass("focus:opacity-80");
expect(blueskyLinks[0]).toHaveClass("transition-opacity");
const gitlabLinks = screen.getAllByRole("link", {
name: "Follow us on GitLab",
});
expect(gitlabLinks.length).toBeGreaterThan(0);
expect(gitlabLinks[0]).toHaveClass("hover:opacity-80");
expect(gitlabLinks[0]).toHaveClass("active:opacity-60");
expect(gitlabLinks[0]).toHaveClass("focus:opacity-80");
expect(gitlabLinks[0]).toHaveClass("transition-opacity");
});
test("navigation links have hover and focus states", () => {
render(<Footer />);
const useCasesLinks = screen.getAllByRole("link", { name: "Use cases" });
expect(useCasesLinks.length).toBeGreaterThan(0);
expect(useCasesLinks[0]).toHaveClass("hover:opacity-80");
expect(useCasesLinks[0]).toHaveClass("active:opacity-60");
expect(useCasesLinks[0]).toHaveClass("focus:opacity-80");
expect(useCasesLinks[0]).toHaveClass("transition-opacity");
});
test("legal links have hover and focus states", () => {
render(<Footer />);
const privacyLinks = screen.getAllByRole("link", {
name: "Privacy Policy",
});
expect(privacyLinks.length).toBeGreaterThan(0);
expect(privacyLinks[0]).toHaveClass("hover:opacity-80");
expect(privacyLinks[0]).toHaveClass("active:opacity-60");
expect(privacyLinks[0]).toHaveClass("focus:opacity-80");
expect(privacyLinks[0]).toHaveClass("transition-opacity");
});
test("email link has hover and focus states", () => {
render(<Footer />);
const emailLinks = screen.getAllByRole("link", {
name: "medlab@colorado.edu",
});
expect(emailLinks.length).toBeGreaterThan(0);
expect(emailLinks[0]).toHaveClass("hover:opacity-80");
expect(emailLinks[0]).toHaveClass("active:opacity-60");
expect(emailLinks[0]).toHaveClass("focus:opacity-80");
expect(emailLinks[0]).toHaveClass("transition-opacity");
});
test("social media images have hover effects", () => {
render(<Footer />);
const blueskyImages = screen.getAllByAltText("Bluesky");
expect(blueskyImages.length).toBeGreaterThan(0);
expect(blueskyImages[0]).toHaveClass("group-hover:scale-110");
expect(blueskyImages[0]).toHaveClass("transition-transform");
const gitlabImages = screen.getAllByAltText("GitLab");
expect(gitlabImages.length).toBeGreaterThan(0);
expect(gitlabImages[0]).toHaveClass("group-hover:scale-110");
expect(gitlabImages[0]).toHaveClass("transition-transform");
expect(gitlabImages[0]).toHaveClass("grayscale");
});
test("renders multiple instances of navigation links for responsive design", () => {
render(<Footer />);
// Should have navigation links in the footer
const useCasesLinks = screen.getAllByText("Use cases");
const learnLinks = screen.getAllByText("Learn");
const aboutLinks = screen.getAllByText("About");
expect(useCasesLinks.length).toBeGreaterThan(0);
expect(learnLinks.length).toBeGreaterThan(0);
expect(aboutLinks.length).toBeGreaterThan(0);
});
test("has proper focus management for accessibility", () => {
render(<Footer />);
// Get specific links that should have focus management
const emailLinks = screen.getAllByRole("link", {
name: "medlab@colorado.edu",
});
const blueskyLinks = screen.getAllByRole("link", {
name: "Follow us on Bluesky",
});
const gitlabLinks = screen.getAllByRole("link", {
name: "Follow us on GitLab",
});
// Use the first instance of each social media link
const blueskyLink = blueskyLinks[0];
const gitlabLink = gitlabLinks[0];
// Check email links (multiple due to responsive design)
emailLinks.forEach((emailLink) => {
expect(emailLink).toHaveClass("focus:outline-none");
expect(emailLink).toHaveClass("focus:ring-2");
expect(emailLink).toHaveClass("focus:ring-offset-2");
expect(emailLink).toHaveClass(
"focus:ring-[var(--color-content-default-primary)]",
);
expect(emailLink).toHaveClass(
"focus:ring-offset-[var(--color-surface-default-primary)]",
);
});
// Check social media links
[blueskyLink, gitlabLink].forEach((link) => {
expect(link).toHaveClass("focus:outline-none");
expect(link).toHaveClass("focus:ring-2");
expect(link).toHaveClass("focus:ring-offset-2");
expect(link).toHaveClass(
"focus:ring-[var(--color-content-default-primary)]",
);
expect(link).toHaveClass(
"focus:ring-offset-[var(--color-surface-default-primary)]",
);
});
});
});
-334
View File
@@ -1,334 +0,0 @@
import { describe, test, expect, beforeEach } from "vitest";
import { render, screen } from "@testing-library/react";
import Header, {
navigationItems,
avatarImages,
logoConfig,
} from "../../app/components/Header.js";
describe("Header", () => {
beforeEach(() => {
// Clear any existing rendered content
document.body.innerHTML = "";
});
describe("Accessibility and Landmarks", () => {
test("renders header with correct structure and accessibility attributes", () => {
const { container } = render(<Header />);
// Check main header structure - use container to scope the search
const header = container.querySelector(
'[role="banner"][aria-label="Main navigation header"]',
);
expect(header).toBeInTheDocument();
expect(header).toHaveAttribute("aria-label", "Main navigation header");
// Check navigation - use container to scope the search
const nav = container.querySelector(
'[role="navigation"][aria-label="Main navigation"]',
);
expect(nav).toBeInTheDocument();
expect(nav).toHaveAttribute("aria-label", "Main navigation");
});
test("renders all navigation items with proper accessibility", () => {
render(<Header />);
// Check all navigation items have proper aria-labels - use menuitem role since they're in a menubar
expect(
screen.getAllByRole("menuitem", { name: "Navigate to Use cases page" })
.length,
).toBeGreaterThan(0);
expect(
screen.getAllByRole("menuitem", { name: "Navigate to Learn page" })
.length,
).toBeGreaterThan(0);
expect(
screen.getAllByRole("menuitem", { name: "Navigate to About page" })
.length,
).toBeGreaterThan(0);
});
});
describe("Schema Markup", () => {
test("renders schema markup for site navigation", () => {
render(<Header />);
const script = document.querySelector(
'script[type="application/ld+json"]',
);
expect(script).toBeInTheDocument();
const schemaData = JSON.parse(script.textContent);
expect(schemaData["@type"]).toBe("WebSite");
expect(schemaData.name).toBe("CommunityRule");
expect(schemaData.url).toBe("https://communityrule.com");
expect(schemaData.potentialAction["@type"]).toBe("SearchAction");
});
});
describe("Configuration Data", () => {
test("navigationItems has correct structure and count", () => {
expect(navigationItems).toHaveLength(3);
expect(navigationItems[0]).toEqual({
href: "#",
text: "Use cases",
extraPadding: true,
});
expect(navigationItems[1]).toEqual({
href: "/learn",
text: "Learn",
});
expect(navigationItems[2]).toEqual({
href: "#",
text: "About",
});
});
test("avatarImages has correct structure and count", () => {
expect(avatarImages).toHaveLength(3);
expect(avatarImages[0]).toEqual({
src: "/assets/Avatar_1.png",
alt: "Avatar 1",
});
expect(avatarImages[1]).toEqual({
src: "/assets/Avatar_2.png",
alt: "Avatar 2",
});
expect(avatarImages[2]).toEqual({
src: "/assets/Avatar_3.png",
alt: "Avatar 3",
});
});
test("logoConfig has correct structure and count", () => {
expect(logoConfig).toHaveLength(5);
// Check first config (xs)
expect(logoConfig[0]).toEqual({
breakpoint: "block sm:hidden",
size: "header",
showText: false,
});
// Check last config (xl+)
expect(logoConfig[4]).toEqual({
breakpoint: "hidden xl:block",
size: "headerXl",
showText: true,
});
});
});
describe("Logo Configuration", () => {
test("renders correct number of logo variants", () => {
render(<Header />);
const logoWrappers = screen.getAllByTestId("logo-wrapper");
expect(logoWrappers).toHaveLength(logoConfig.length);
});
test("logo wrappers include expected breakpoint classes", () => {
render(<Header />);
const logoWrappers = screen.getAllByTestId("logo-wrapper");
// Check first logo variant (xs only)
expect(logoWrappers[0]).toHaveClass("block", "sm:hidden");
// Check second logo variant (sm only)
expect(logoWrappers[1]).toHaveClass("hidden", "sm:block", "md:hidden");
// Check third logo variant (md only)
expect(logoWrappers[2]).toHaveClass("hidden", "md:block", "lg:hidden");
// Check fourth logo variant (lg only)
expect(logoWrappers[3]).toHaveClass("hidden", "lg:block", "xl:hidden");
// Check fifth logo variant (xl+)
expect(logoWrappers[4]).toHaveClass("hidden", "xl:block");
});
});
describe("Navigation Structure", () => {
test("renders all breakpoint-specific navigation containers", () => {
render(<Header />);
expect(screen.getByTestId("nav-xs")).toBeInTheDocument();
expect(screen.getByTestId("nav-sm")).toBeInTheDocument();
expect(screen.getByTestId("nav-md")).toBeInTheDocument();
expect(screen.getByTestId("nav-lg")).toBeInTheDocument();
expect(screen.getByTestId("nav-xl")).toBeInTheDocument();
});
test("navigation containers use expected breakpoint classes", () => {
render(<Header />);
// XSmall navigation
const navXs = screen.getByTestId("nav-xs");
expect(navXs).toHaveClass("block", "sm:hidden");
// Small navigation
const navSm = screen.getByTestId("nav-sm");
expect(navSm).toHaveClass("hidden", "sm:block", "md:hidden");
// Medium navigation
const navMd = screen.getByTestId("nav-md");
expect(navMd).toHaveClass("hidden", "md:block", "lg:hidden");
// Large navigation
const navLg = screen.getByTestId("nav-lg");
expect(navLg).toHaveClass("hidden", "lg:block", "xl:hidden");
// XLarge navigation
const navXl = screen.getByTestId("nav-xl");
expect(navXl).toHaveClass("hidden", "xl:block");
});
test("renders navigation items with correct text and links", () => {
render(<Header />);
// Check navigation items
expect(screen.getAllByText("Use cases").length).toBeGreaterThan(0);
expect(screen.getAllByText("Learn").length).toBeGreaterThan(0);
expect(screen.getAllByText("About").length).toBeGreaterThan(0);
});
test("renders multiple instances of navigation items for responsive design", () => {
render(<Header />);
// Should have multiple instances of navigation items for different breakpoints
const useCasesLinks = screen.getAllByText("Use cases");
const learnLinks = screen.getAllByText("Learn");
const aboutLinks = screen.getAllByText("About");
expect(useCasesLinks.length).toBeGreaterThan(1);
expect(learnLinks.length).toBeGreaterThan(1);
expect(aboutLinks.length).toBeGreaterThan(1);
});
});
describe("Authentication Structure", () => {
test("renders all breakpoint-specific auth containers", () => {
render(<Header />);
expect(screen.getByTestId("auth-xs")).toBeInTheDocument();
expect(screen.getByTestId("auth-sm")).toBeInTheDocument();
expect(screen.getByTestId("auth-md")).toBeInTheDocument();
expect(screen.getByTestId("auth-lg")).toBeInTheDocument();
expect(screen.getByTestId("auth-xl")).toBeInTheDocument();
});
test("auth containers use expected breakpoint classes", () => {
render(<Header />);
// XSmall auth
const authXs = screen.getByTestId("auth-xs");
expect(authXs).toHaveClass("block", "sm:hidden");
// Small auth
const authSm = screen.getByTestId("auth-sm");
expect(authSm).toHaveClass("hidden", "sm:block", "md:hidden");
// Medium auth
const authMd = screen.getByTestId("auth-md");
expect(authMd).toHaveClass("hidden", "md:block", "lg:hidden");
// Large auth
const authLg = screen.getByTestId("auth-lg");
expect(authLg).toHaveClass("hidden", "lg:block", "xl:hidden");
// XLarge auth
const authXl = screen.getByTestId("auth-xl");
expect(authXl).toHaveClass("hidden", "xl:block");
});
test("renders login button with correct accessibility", () => {
render(<Header />);
const loginLinks = screen.getAllByRole("menuitem", {
name: "Log in to your account",
});
expect(loginLinks.length).toBeGreaterThan(0);
expect(screen.getAllByText("Log in").length).toBeGreaterThan(0);
});
test("renders multiple login buttons for responsive design", () => {
render(<Header />);
// Should have multiple login buttons for different breakpoints
const loginButtons = screen.getAllByText("Log in");
expect(loginButtons.length).toBeGreaterThan(1);
});
test("renders create rule button with avatar decoration", () => {
render(<Header />);
const createRuleButtons = screen.getAllByRole("button", {
name: "Create a new rule with avatar decoration",
});
expect(createRuleButtons.length).toBeGreaterThan(0);
expect(screen.getAllByText("Create rule").length).toBeGreaterThan(0);
});
test("renders multiple create rule buttons for responsive design", () => {
render(<Header />);
// Should have multiple create rule buttons for different breakpoints
const createRuleButtons = screen.getAllByText("Create rule");
expect(createRuleButtons.length).toBeGreaterThan(1);
});
});
describe("Avatar Images", () => {
test("renders avatar images with correct attributes", () => {
render(<Header />);
const avatars = screen.getAllByRole("img");
expect(avatars.length).toBeGreaterThan(0);
// Check for avatar images
const avatarImages = avatars.filter(
(img) =>
img.alt === "Avatar 1" ||
img.alt === "Avatar 2" ||
img.alt === "Avatar 3",
);
expect(avatarImages.length).toBeGreaterThan(0);
});
});
describe("Sticky Header Behavior", () => {
test("applies sticky positioning classes", () => {
const { container } = render(<Header />);
const header = container.querySelector(
'[role="banner"][aria-label="Main navigation header"]',
);
expect(header).toHaveClass("sticky", "top-0", "z-50");
});
});
describe("CSS Classes and Styling", () => {
test("has correct CSS classes for styling", () => {
const { container } = render(<Header />);
const header = container.querySelector(
'[role="banner"][aria-label="Main navigation header"]',
);
expect(header).toHaveClass("bg-[var(--color-surface-default-primary)]");
expect(header).toHaveClass("w-full");
expect(header).toHaveClass("border-b");
expect(header).toHaveClass(
"border-[var(--border-color-default-tertiary)]",
);
const nav = container.querySelector(
'[role="navigation"][aria-label="Main navigation"]',
);
expect(nav).toHaveClass("flex");
expect(nav).toHaveClass("items-center");
expect(nav).toHaveClass("justify-between");
});
});
});
-141
View File
@@ -1,141 +0,0 @@
import { render, screen, cleanup } from "@testing-library/react";
import { describe, test, expect, afterEach } from "vitest";
import HeroBanner from "../../app/components/HeroBanner";
afterEach(() => {
cleanup();
});
describe("HeroBanner Component", () => {
test("renders with all props", () => {
render(
<HeroBanner
title="Welcome to CommunityRule"
subtitle="Build better communities"
description="Create and manage community rules with ease"
ctaText="Get Started"
ctaHref="/signup"
/>,
);
expect(
screen.getByRole("heading", { name: "Welcome to CommunityRule" }),
).toBeInTheDocument();
expect(
screen.getByRole("heading", { name: "Build better communities" }),
).toBeInTheDocument();
expect(
screen.getByText("Create and manage community rules with ease"),
).toBeInTheDocument();
// Button component renders multiple versions for different screen sizes
// Use getAllByRole to handle multiple buttons with same text
const buttons = screen.getAllByRole("button", { name: "Get Started" });
expect(buttons.length).toBeGreaterThan(0);
});
test("renders with minimal props", () => {
render(<HeroBanner title="Minimal" />);
expect(
screen.getByRole("heading", { name: "Minimal" }),
).toBeInTheDocument();
expect(
screen.getByRole("img", { name: "Hero illustration" }),
).toBeInTheDocument();
});
test("renders hero image", () => {
render(<HeroBanner title="Test" />);
const heroImage = screen.getByRole("img", { name: "Hero illustration" });
expect(heroImage).toBeInTheDocument();
expect(heroImage).toHaveAttribute("src", "/assets/HeroImage.png");
});
test("applies correct CSS classes", () => {
render(<HeroBanner title="Test" />);
const section = document.querySelector("section");
expect(section).toHaveClass("bg-transparent");
// Find the div with md:flex-1 class
const contentLockup = document.querySelector('[class*="md:flex-1"]');
expect(contentLockup).toBeInTheDocument();
});
test("renders ContentLockup with correct props", () => {
render(
<HeroBanner
title="Test Title"
subtitle="Test Subtitle"
description="Test Description"
ctaText="Test CTA"
ctaHref="/test"
/>,
);
// Check that ContentLockup receives the props correctly
expect(
screen.getByRole("heading", { name: "Test Title" }),
).toBeInTheDocument();
expect(
screen.getByRole("heading", { name: "Test Subtitle" }),
).toBeInTheDocument();
expect(screen.getByText("Test Description")).toBeInTheDocument();
// Button component renders multiple versions for different screen sizes
const buttons = screen.getAllByRole("button", { name: "Test CTA" });
expect(buttons.length).toBeGreaterThan(0);
});
test("renders HeroDecor component", () => {
render(<HeroBanner title="Test" />);
// HeroDecor should be present (it's a decorative component)
const heroDecor = document.querySelector(
'[class*="pointer-events-none absolute z-0"]',
);
expect(heroDecor).toBeInTheDocument();
});
test("has proper semantic structure", () => {
render(<HeroBanner title="Test" />);
const section = document.querySelector("section");
expect(section).toBeInTheDocument();
// Should have proper heading structure
const heading = screen.getByRole("heading", { name: "Test" });
expect(heading).toBeInTheDocument();
});
test("handles empty title gracefully", () => {
render(<HeroBanner title="" />);
// Should still render the structure even with empty title
const section = document.querySelector("section");
expect(section).toBeInTheDocument();
});
test("applies responsive design classes", () => {
render(<HeroBanner title="Test" />);
const section = document.querySelector("section");
expect(section).toHaveClass(
"px-[var(--spacing-scale-008)]",
"sm:px-[var(--spacing-scale-010)]",
);
});
test("renders with design tokens", () => {
render(<HeroBanner title="Test" />);
const section = document.querySelector("section");
expect(section).toHaveClass("bg-transparent");
// Check for design token usage in the component structure
const container = section.querySelector(
'[class*="bg-[var(--color-surface-inverse-brand-primary)]"]',
);
expect(container).toBeInTheDocument();
});
});
-271
View File
@@ -1,271 +0,0 @@
import React from "react";
import { render, screen, fireEvent } from "@testing-library/react";
import { expect, test, describe, vi } from "vitest";
import Input from "../../app/components/Input";
describe("Input Component", () => {
test("renders with default props", () => {
render(<Input />);
const input = screen.getByRole("textbox");
expect(input).toBeInTheDocument();
expect(input).toHaveAttribute("type", "text");
});
test("renders with label", () => {
render(<Input label="Test input" />);
expect(screen.getByText("Test input")).toBeInTheDocument();
expect(screen.getByLabelText("Test input")).toBeInTheDocument();
});
test("renders with placeholder", () => {
render(<Input placeholder="Enter text..." />);
const input = screen.getByPlaceholderText("Enter text...");
expect(input).toBeInTheDocument();
});
test("renders with value", () => {
render(<Input value="test value" />);
const input = screen.getByDisplayValue("test value");
expect(input).toBeInTheDocument();
});
test("calls onChange when text is entered", () => {
const handleChange = vi.fn();
render(<Input onChange={handleChange} />);
const input = screen.getByRole("textbox");
fireEvent.change(input, { target: { value: "new text" } });
expect(handleChange).toHaveBeenCalledWith(expect.any(Object));
});
test("calls onFocus when focused", () => {
const handleFocus = vi.fn();
render(<Input onFocus={handleFocus} />);
const input = screen.getByRole("textbox");
fireEvent.focus(input);
expect(handleFocus).toHaveBeenCalledWith(expect.any(Object));
});
test("calls onBlur when blurred", () => {
const handleBlur = vi.fn();
render(<Input onBlur={handleBlur} />);
const input = screen.getByRole("textbox");
fireEvent.blur(input);
expect(handleBlur).toHaveBeenCalledWith(expect.any(Object));
});
test("does not call onChange when disabled", () => {
const handleChange = vi.fn();
render(<Input disabled={true} onChange={handleChange} />);
const input = screen.getByRole("textbox");
fireEvent.change(input, { target: { value: "new text" } });
expect(handleChange).not.toHaveBeenCalled();
});
test("does not call onFocus when disabled", () => {
const handleFocus = vi.fn();
render(<Input disabled={true} onFocus={handleFocus} />);
const input = screen.getByRole("textbox");
fireEvent.focus(input);
expect(handleFocus).not.toHaveBeenCalled();
});
test("does not call onBlur when disabled", () => {
const handleBlur = vi.fn();
render(<Input disabled={true} onBlur={handleBlur} />);
const input = screen.getByRole("textbox");
fireEvent.blur(input);
expect(handleBlur).not.toHaveBeenCalled();
});
test("applies disabled attributes when disabled", () => {
render(<Input disabled={true} />);
const input = screen.getByRole("textbox");
expect(input).toBeDisabled();
});
test("applies correct size classes", () => {
const { rerender } = render(<Input size="small" />);
let input = screen.getByRole("textbox");
expect(input).toHaveClass("h-[32px]");
rerender(<Input size="medium" />);
input = screen.getByRole("textbox");
expect(input).toHaveClass("h-[36px]");
rerender(<Input size="large" />);
input = screen.getByRole("textbox");
expect(input).toHaveClass("h-[40px]");
});
test("applies correct label variant classes", () => {
const { rerender } = render(<Input label="Test" labelVariant="default" />);
let container = screen.getByRole("textbox").closest("div").parentElement;
expect(container).toHaveClass("flex-col");
rerender(<Input label="Test" labelVariant="horizontal" />);
container = screen.getByRole("textbox").closest("div").parentElement;
expect(container).toHaveClass("flex", "items-center");
});
test("applies error state classes", () => {
render(<Input error={true} />);
const input = screen.getByRole("textbox");
expect(input).toHaveClass(
"border-[var(--color-border-default-utility-negative)]",
);
});
test("applies disabled state classes", () => {
render(<Input disabled={true} />);
const input = screen.getByRole("textbox");
expect(input).toHaveClass("cursor-not-allowed");
expect(input).toHaveClass("bg-[var(--color-content-default-secondary)]");
});
test("applies focus state classes", () => {
render(<Input state="focus" />);
const input = screen.getByRole("textbox");
expect(input).toHaveClass(
"border-[var(--color-border-default-utility-info)]",
);
expect(input).toHaveClass("shadow-[0_0_5px_3px_#3281F8]");
});
test("applies hover state classes", () => {
render(<Input state="hover" />);
const input = screen.getByRole("textbox");
expect(input).toHaveClass("border-[var(--color-border-default-tertiary)]");
expect(input).toHaveClass(
"shadow-[0_0_0_2px_var(--color-border-default-tertiary)]",
);
});
test("applies active state classes", () => {
render(<Input state="active" />);
const input = screen.getByRole("textbox");
expect(input).toHaveClass("border-[var(--color-border-default-tertiary)]");
});
test("applies default state classes", () => {
render(<Input state="default" />);
const input = screen.getByRole("textbox");
expect(input).toHaveClass("border-[var(--color-border-default-tertiary)]");
expect(input).toHaveClass(
"hover:shadow-[0_0_0_2px_var(--color-border-default-tertiary)]",
);
});
test("applies custom className", () => {
render(<Input className="custom-class" />);
const input = screen.getByRole("textbox");
expect(input).toHaveClass("custom-class");
});
test("passes through additional props", () => {
render(<Input id="test-input" name="test" type="email" />);
const input = screen.getByRole("textbox");
expect(input).toHaveAttribute("id", "test-input");
expect(input).toHaveAttribute("name", "test");
expect(input).toHaveAttribute("type", "email");
});
test("generates unique ID when not provided", () => {
render(<Input label="Test" />);
const input = screen.getByRole("textbox");
const label = screen.getByText("Test");
expect(input).toHaveAttribute("id");
expect(label).toHaveAttribute("for", input.id);
});
test("uses provided ID when given", () => {
render(<Input id="custom-id" label="Test" />);
const input = screen.getByRole("textbox");
const label = screen.getByText("Test");
expect(input).toHaveAttribute("id", "custom-id");
expect(label).toHaveAttribute("for", "custom-id");
});
test("applies correct border radius style", () => {
const { rerender } = render(<Input size="small" />);
let input = screen.getByRole("textbox");
expect(input).toHaveStyle("border-radius: var(--measures-radius-small)");
rerender(<Input size="medium" />);
input = screen.getByRole("textbox");
expect(input).toHaveStyle("border-radius: var(--measures-radius-medium)");
rerender(<Input size="large" />);
input = screen.getByRole("textbox");
expect(input).toHaveStyle("border-radius: var(--measures-radius-large)");
});
test("applies opacity wrapper when disabled", () => {
render(<Input disabled={true} />);
const wrapper = screen.getByRole("textbox").closest("div");
expect(wrapper).toHaveClass("opacity-40");
});
test("does not apply opacity wrapper when not disabled", () => {
render(<Input disabled={false} />);
const wrapper = screen.getByRole("textbox").closest("div");
expect(wrapper).not.toHaveClass("opacity-40");
});
test("applies correct label styling", () => {
render(<Input label="Test label" size="small" />);
const label = screen.getByText("Test label");
expect(label).toHaveClass("text-[12px]");
expect(label).toHaveClass("leading-[14px]");
expect(label).toHaveClass("font-medium");
expect(label).toHaveClass("text-[var(--color-content-default-secondary)]");
});
test("applies correct input text styling for different sizes", () => {
const { rerender } = render(<Input size="small" />);
let input = screen.getByRole("textbox");
expect(input).toHaveClass("text-[10px]");
rerender(<Input size="medium" />);
input = screen.getByRole("textbox");
expect(input).toHaveClass("text-[14px]");
expect(input).toHaveClass("leading-[20px]");
rerender(<Input size="large" />);
input = screen.getByRole("textbox");
expect(input).toHaveClass("text-[16px]");
expect(input).toHaveClass("leading-[24px]");
});
test("handles keyboard navigation", () => {
const handleFocus = vi.fn();
render(<Input onFocus={handleFocus} />);
const input = screen.getByRole("textbox");
fireEvent.keyDown(input, { key: "Tab" });
fireEvent.focus(input);
expect(handleFocus).toHaveBeenCalled();
});
test("forwards ref correctly", () => {
const ref = React.createRef();
render(<Input ref={ref} />);
expect(ref.current).toBeInstanceOf(HTMLInputElement);
});
test("is memoized", () => {
expect(Input.$$typeof).toBe(Symbol.for("react.memo"));
});
});
-128
View File
@@ -1,128 +0,0 @@
import { render, screen } from "@testing-library/react";
import { describe, it, expect } from "vitest";
import Logo from "../../app/components/Logo";
describe("Logo Component", () => {
it("renders the logo with default props", () => {
render(<Logo />);
const logo = screen.getByRole("link", { name: /communityrule logo/i });
expect(logo).toBeInTheDocument();
expect(screen.getByText("CommunityRule")).toBeInTheDocument();
expect(screen.getByAltText("CommunityRule Logo Icon")).toBeInTheDocument();
});
it("renders with custom size variant", () => {
const { rerender } = render(<Logo size="header" />);
let logoDiv = screen.getByRole("link").querySelector("div");
expect(logoDiv).toHaveClass("h-[20.85px]");
rerender(<Logo size="headerLg" />);
logoDiv = screen.getByRole("link").querySelector("div");
expect(logoDiv).toHaveClass("h-[28px]");
rerender(<Logo size="footer" />);
logoDiv = screen.getByRole("link").querySelector("div");
expect(logoDiv).toHaveClass("h-[calc(40px*1.37)]");
});
it("renders without text when showText is false", () => {
render(<Logo showText={false} />);
expect(screen.queryByText("CommunityRule")).not.toBeInTheDocument();
expect(screen.getByAltText("CommunityRule Logo Icon")).toBeInTheDocument();
});
it("applies proper hover effects", () => {
render(<Logo />);
const logoDiv = screen.getByRole("link").querySelector("div");
expect(logoDiv).toHaveClass("hover:scale-[1.02]", "transition-all");
});
it("applies proper accessibility attributes", () => {
render(<Logo />);
const logo = screen.getByRole("link");
expect(logo).toHaveAttribute("aria-label", "CommunityRule Logo");
expect(logo).toHaveAttribute("href", "/");
});
it("applies proper text styling for different sizes", () => {
const { rerender } = render(<Logo size="homeHeaderMd" />);
let textElement = screen.getByText("CommunityRule");
expect(textElement).toHaveClass(
"text-[var(--color-content-inverse-primary)]",
);
rerender(<Logo size="header" />);
textElement = screen.getByText("CommunityRule");
expect(textElement).toHaveClass(
"text-[var(--color-content-default-primary)]",
);
});
it("applies proper icon sizing for different variants", () => {
const { rerender } = render(<Logo size="homeHeaderSm" />);
let icon = screen.getByAltText("CommunityRule Logo Icon");
expect(icon).toHaveClass("w-[14.39px]", "h-[14.39px]");
rerender(<Logo size="headerXl" />);
icon = screen.getByAltText("CommunityRule Logo Icon");
expect(icon).toHaveClass("w-[33.81px]", "h-[33.81px]");
});
it("applies brightness filter for home header variants", () => {
render(<Logo size="homeHeaderMd" />);
const icon = screen.getByAltText("CommunityRule Logo Icon");
expect(icon).toHaveClass("filter", "brightness-0");
});
it("maintains proper spacing when text is hidden", () => {
render(<Logo showText={false} />);
const logo = screen.getByRole("link");
// Should not have gap class when text is hidden
expect(logo.className).not.toContain("gap-[8.28px]");
});
it("applies proper font classes to text", () => {
render(<Logo />);
const textElement = screen.getByText("CommunityRule");
expect(textElement).toHaveClass("font-bricolage-grotesque", "font-normal");
});
it("applies proper icon attributes", () => {
render(<Logo />);
const icon = screen.getByAltText("CommunityRule Logo Icon");
expect(icon).toHaveAttribute("src", "/assets/Logo.svg");
expect(icon).toHaveAttribute("aria-hidden", "true");
});
it("handles all size variants correctly", () => {
const sizes = [
"default",
"homeHeaderXsmall",
"homeHeaderSm",
"homeHeaderMd",
"homeHeaderLg",
"homeHeaderXl",
"header",
"headerMd",
"headerLg",
"headerXl",
"footer",
"footerLg",
];
sizes.forEach((size) => {
const { unmount } = render(<Logo size={size} />);
const logo = screen.getByRole("link");
expect(logo).toBeInTheDocument();
unmount();
});
});
});
-248
View File
@@ -1,248 +0,0 @@
import React from "react";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, it, expect, vi } from "vitest";
import RadioButton from "../../app/components/RadioButton";
describe("RadioButton", () => {
it("renders with default props", () => {
render(<RadioButton />);
const radioButton = screen.getByRole("radio");
expect(radioButton).toBeInTheDocument();
expect(radioButton).toHaveAttribute("aria-checked", "false");
});
it("renders with label", () => {
render(<RadioButton label="Test Radio" />);
expect(screen.getByText("Test Radio")).toBeInTheDocument();
});
it("shows checked state", () => {
render(<RadioButton checked={true} label="Checked Radio" />);
const radioButton = screen.getByRole("radio");
expect(radioButton).toHaveAttribute("aria-checked", "true");
});
it("calls onChange when clicked", async () => {
const user = userEvent.setup();
const handleChange = vi.fn();
render(
<RadioButton
checked={false}
onChange={handleChange}
label="Test Radio"
/>,
);
const radioButton = screen.getByRole("radio");
await user.click(radioButton);
expect(handleChange).toHaveBeenCalledWith({
checked: true,
value: undefined,
});
});
it("calls onChange with value when clicked", async () => {
const user = userEvent.setup();
const handleChange = vi.fn();
render(
<RadioButton
checked={false}
value="test-value"
onChange={handleChange}
label="Test Radio"
/>,
);
const radioButton = screen.getByRole("radio");
await user.click(radioButton);
expect(handleChange).toHaveBeenCalledWith({
checked: true,
value: "test-value",
});
});
it("does not call onChange when clicking already checked radio button", async () => {
const user = userEvent.setup();
const handleChange = vi.fn();
render(
<RadioButton checked={true} onChange={handleChange} label="Test Radio" />,
);
const radioButton = screen.getByRole("radio");
await user.click(radioButton);
// Radio buttons should not be unchecked by clicking them again
expect(handleChange).not.toHaveBeenCalled();
});
it("handles keyboard activation", async () => {
const user = userEvent.setup();
const handleChange = vi.fn();
render(
<RadioButton
checked={false}
onChange={handleChange}
label="Test Radio"
/>,
);
const radioButton = screen.getByRole("radio");
radioButton.focus();
await user.keyboard(" ");
expect(handleChange).toHaveBeenCalledWith({
checked: true,
value: undefined,
});
});
it("handles Enter key activation", async () => {
const user = userEvent.setup();
const handleChange = vi.fn();
render(
<RadioButton
checked={false}
onChange={handleChange}
label="Test Radio"
/>,
);
const radioButton = screen.getByRole("radio");
await user.click(radioButton); // Focus the element first
await user.keyboard("{Enter}");
expect(handleChange).toHaveBeenCalledWith({
checked: true,
value: undefined,
});
});
it("applies standard mode classes", () => {
render(<RadioButton mode="standard" label="Standard Radio" />);
const radioButton = screen.getByRole("radio");
expect(radioButton).toHaveClass(
"outline-[var(--color-border-default-tertiary)]",
);
});
it("applies inverse mode classes", () => {
render(<RadioButton mode="inverse" label="Inverse Radio" />);
const radioButton = screen.getByRole("radio");
expect(radioButton).toHaveClass(
"outline-[var(--color-border-inverse-primary)]",
);
});
it("applies focus state classes", () => {
render(<RadioButton state="focus" label="Focus Radio" />);
const radioButton = screen.getByRole("radio");
expect(radioButton).toHaveClass("focus:outline");
});
it("applies hover state classes", () => {
render(<RadioButton state="hover" label="Hover Radio" />);
const radioButton = screen.getByRole("radio");
expect(radioButton).toHaveClass("hover:outline");
});
it("renders hidden input for form submission", () => {
render(
<RadioButton
name="test-radio"
value="test-value"
checked={true}
label="Test Radio"
/>,
);
const hiddenInput = screen.getByDisplayValue("test-value");
expect(hiddenInput).toBeInTheDocument();
expect(hiddenInput).toHaveAttribute("type", "radio");
expect(hiddenInput).toHaveAttribute("name", "test-radio");
expect(hiddenInput).toBeChecked();
});
it("applies custom className", () => {
render(<RadioButton className="custom-class" label="Custom Radio" />);
const label = screen.getByText("Custom Radio").closest("label");
expect(label).toHaveClass("custom-class");
});
it("generates unique ID when not provided", () => {
render(<RadioButton label="Radio 1" />);
render(<RadioButton label="Radio 2" />);
const radioButtons = screen.getAllByRole("radio");
expect(radioButtons[0]).toHaveAttribute("id");
expect(radioButtons[1]).toHaveAttribute("id");
expect(radioButtons[0].id).not.toBe(radioButtons[1].id);
});
it("uses provided ID", () => {
render(<RadioButton id="custom-id" label="Custom ID Radio" />);
const radioButton = screen.getByRole("radio");
expect(radioButton).toHaveAttribute("id", "custom-id");
});
it("associates label with radio button for accessibility", () => {
render(<RadioButton label="Accessible Radio" />);
const radioButton = screen.getByRole("radio");
const labelId = radioButton.getAttribute("aria-labelledby");
expect(labelId).toBeTruthy();
const labelElement = document.getElementById(labelId);
expect(labelElement).toHaveTextContent("Accessible Radio");
});
it("uses aria-label when provided", () => {
render(<RadioButton ariaLabel="Custom Aria Label" />);
const radioButton = screen.getByRole("radio");
expect(radioButton).toHaveAttribute("aria-label", "Custom Aria Label");
});
it("shows dot indicator when checked", () => {
render(
<RadioButton checked={true} mode="standard" label="Checked Radio" />,
);
const dot = screen.getByRole("radio").querySelector("div");
expect(dot).toHaveClass("w-[16px]", "h-[16px]", "rounded-full");
});
it("hides dot indicator when unchecked", () => {
render(
<RadioButton checked={false} mode="standard" label="Unchecked Radio" />,
);
const dot = screen.getByRole("radio").querySelector("div");
// Check if the dot has transparent background or no background color set
const computedStyle = window.getComputedStyle(dot);
const backgroundColor = computedStyle.backgroundColor;
// The dot should either be transparent or have no background color
expect(
backgroundColor === "transparent" ||
backgroundColor === "rgba(0, 0, 0, 0)" ||
backgroundColor === "",
).toBe(true);
});
});
-240
View File
@@ -1,240 +0,0 @@
import React from "react";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, it, expect, vi } from "vitest";
import RadioGroup from "../../app/components/RadioGroup";
describe("RadioGroup", () => {
const defaultOptions = [
{ value: "option1", label: "Option 1" },
{ value: "option2", label: "Option 2" },
{ value: "option3", label: "Option 3" },
];
it("renders with default props", () => {
render(<RadioGroup options={defaultOptions} />);
const radioGroup = screen.getByRole("radiogroup");
expect(radioGroup).toBeInTheDocument();
const radioButtons = screen.getAllByRole("radio");
expect(radioButtons).toHaveLength(3);
});
it("renders all options", () => {
render(<RadioGroup options={defaultOptions} />);
expect(screen.getByText("Option 1")).toBeInTheDocument();
expect(screen.getByText("Option 2")).toBeInTheDocument();
expect(screen.getByText("Option 3")).toBeInTheDocument();
});
it("shows selected option", () => {
render(<RadioGroup options={defaultOptions} value="option2" />);
const radioButtons = screen.getAllByRole("radio");
expect(radioButtons[0]).toHaveAttribute("aria-checked", "false");
expect(radioButtons[1]).toHaveAttribute("aria-checked", "true");
expect(radioButtons[2]).toHaveAttribute("aria-checked", "false");
});
it("calls onChange when option is selected", async () => {
const user = userEvent.setup();
const handleChange = vi.fn();
render(
<RadioGroup
options={defaultOptions}
value="option1"
onChange={handleChange}
/>,
);
const option2 = screen.getByText("Option 2").closest("label");
await user.click(option2);
expect(handleChange).toHaveBeenCalledWith({ value: "option2" });
});
it("updates selection when different option is clicked", async () => {
const user = userEvent.setup();
const handleChange = vi.fn();
render(
<RadioGroup
options={defaultOptions}
value="option1"
onChange={handleChange}
/>,
);
// Click option 3
const option3 = screen.getByText("Option 3").closest("label");
await user.click(option3);
expect(handleChange).toHaveBeenCalledWith({ value: "option3" });
});
it("handles keyboard navigation", async () => {
const user = userEvent.setup();
const handleChange = vi.fn();
render(
<RadioGroup
options={defaultOptions}
value="option1"
onChange={handleChange}
/>,
);
const radioButtons = screen.getAllByRole("radio");
radioButtons[1].focus();
await user.keyboard(" ");
expect(handleChange).toHaveBeenCalledWith({ value: "option2" });
});
it("handles Enter key activation", async () => {
const user = userEvent.setup();
const handleChange = vi.fn();
render(
<RadioGroup
options={defaultOptions}
value="option1"
onChange={handleChange}
/>,
);
const radioButtons = screen.getAllByRole("radio");
await user.click(radioButtons[2]);
await user.keyboard("{Enter}");
expect(handleChange).toHaveBeenCalledWith({ value: "option3" });
});
it("applies standard mode to all radio buttons", () => {
render(<RadioGroup options={defaultOptions} mode="standard" />);
const radioButtons = screen.getAllByRole("radio");
radioButtons.forEach((button) => {
expect(button).toHaveClass(
"outline-[var(--color-border-default-tertiary)]",
);
});
});
it("applies inverse mode to all radio buttons", () => {
render(<RadioGroup options={defaultOptions} mode="inverse" />);
const radioButtons = screen.getAllByRole("radio");
radioButtons.forEach((button) => {
expect(button).toHaveClass(
"outline-[var(--color-border-inverse-primary)]",
);
});
});
it("applies state to all radio buttons", () => {
render(<RadioGroup options={defaultOptions} state="focus" />);
const radioButtons = screen.getAllByRole("radio");
radioButtons.forEach((button) => {
expect(button).toHaveClass("focus:outline");
});
});
it("generates unique group name when not provided", () => {
render(<RadioGroup options={defaultOptions} />);
render(<RadioGroup options={defaultOptions} />);
const hiddenInputs = screen.getAllByRole("radio", { hidden: true });
const names = hiddenInputs.map((input) => input.getAttribute("name"));
// Should have unique names
const uniqueNames = new Set(names);
expect(uniqueNames.size).toBeGreaterThan(1);
});
it("uses provided name for all radio buttons", () => {
render(<RadioGroup options={defaultOptions} name="test-group" />);
const hiddenInputs = screen.getAllByDisplayValue("option1");
hiddenInputs.forEach((input) => {
expect(input).toHaveAttribute("name", "test-group");
});
});
it("applies custom className to container", () => {
render(<RadioGroup options={defaultOptions} className="custom-group" />);
const radioGroup = screen.getByRole("radiogroup");
expect(radioGroup).toHaveClass("custom-group");
});
it("passes aria-label to radiogroup", () => {
render(
<RadioGroup options={defaultOptions} aria-label="Test Radio Group" />,
);
const radioGroup = screen.getByRole("radiogroup");
expect(radioGroup).toHaveAttribute("aria-label", "Test Radio Group");
});
it("handles empty options array", () => {
render(<RadioGroup options={[]} />);
const radioGroup = screen.getByRole("radiogroup");
expect(radioGroup).toBeInTheDocument();
const radioButtons = screen.queryAllByRole("radio");
expect(radioButtons).toHaveLength(0);
});
it("handles options with ariaLabel", () => {
const optionsWithAria = [
{ value: "option1", label: "Option 1", ariaLabel: "First Option" },
{ value: "option2", label: "Option 2", ariaLabel: "Second Option" },
];
render(<RadioGroup options={optionsWithAria} />);
const radioButtons = screen.getAllByRole("radio");
expect(radioButtons[0]).toHaveAttribute("aria-label", "First Option");
expect(radioButtons[1]).toHaveAttribute("aria-label", "Second Option");
});
it("maintains selection state correctly", () => {
const { rerender } = render(
<RadioGroup options={defaultOptions} value="option1" />,
);
let radioButtons = screen.getAllByRole("radio");
expect(radioButtons[0]).toHaveAttribute("aria-checked", "true");
rerender(<RadioGroup options={defaultOptions} value="option3" />);
radioButtons = screen.getAllByRole("radio");
expect(radioButtons[0]).toHaveAttribute("aria-checked", "false");
expect(radioButtons[2]).toHaveAttribute("aria-checked", "true");
});
it("does not call onChange when clicking already selected option", async () => {
const user = userEvent.setup();
const handleChange = vi.fn();
render(
<RadioGroup
options={defaultOptions}
value="option2"
onChange={handleChange}
/>,
);
const option2 = screen.getByText("Option 2").closest("label");
await user.click(option2);
// Should not call onChange since it's already selected
expect(handleChange).not.toHaveBeenCalled();
});
});
-395
View File
@@ -1,395 +0,0 @@
import { describe, expect, vi, beforeEach, it } from "vitest";
import { render, screen } from "@testing-library/react";
import RelatedArticles from "../../app/components/RelatedArticles";
// Mock Next.js components
vi.mock("next/link", () => {
return {
default: ({ children, href, ...props }) => (
<a href={href} {...props}>
{children}
</a>
),
};
});
// Mock ContentThumbnailTemplate
vi.mock("../../app/components/ContentThumbnailTemplate", () => {
return {
default: ({ post }) => (
<div data-testid={`thumbnail-${post.slug}`}>
<a href={`/blog/${post.slug}`}>
<h3>{post.frontmatter.title}</h3>
<p>{post.frontmatter.description}</p>
</a>
</div>
),
};
});
// Mock blog post data
const mockRelatedPosts = [
{
slug: "related-article-1",
frontmatter: {
title: "Related Article 1",
description: "This is the first related article",
author: "Test Author",
date: "2025-04-10",
},
},
{
slug: "related-article-2",
frontmatter: {
title: "Related Article 2",
description: "This is the second related article",
author: "Test Author",
date: "2025-04-12",
},
},
{
slug: "related-article-3",
frontmatter: {
title: "Related Article 3",
description: "This is the third related article",
author: "Test Author",
date: "2025-04-14",
},
},
];
describe("RelatedArticles", () => {
beforeEach(() => {
// Mock window.innerWidth for responsive tests
Object.defineProperty(window, "innerWidth", {
writable: true,
configurable: true,
value: 1024, // Desktop width
});
});
it("renders the section with correct structure", () => {
render(
<RelatedArticles
relatedPosts={mockRelatedPosts}
currentPostSlug="current-article"
/>,
);
const section = document.querySelector("section");
expect(section).toBeInTheDocument();
expect(section).toHaveClass(
"py-[var(--spacing-scale-032)]",
"lg:py-[var(--spacing-scale-064)]",
);
});
it("displays the section heading", () => {
render(
<RelatedArticles
relatedPosts={mockRelatedPosts}
currentPostSlug="current-article"
/>,
);
const heading = screen.getByRole("heading", { level: 2 });
expect(heading).toBeInTheDocument();
expect(heading).toHaveTextContent("Related Articles");
expect(heading).toHaveClass(
"text-[32px]",
"lg:text-[44px]",
"leading-[110%]",
"font-medium",
"text-[var(--color-content-inverse-primary)]",
"text-center",
);
});
it("renders all related articles", () => {
render(
<RelatedArticles
relatedPosts={mockRelatedPosts}
currentPostSlug="current-article"
/>,
);
expect(
screen.getByTestId("thumbnail-related-article-1"),
).toBeInTheDocument();
expect(
screen.getByTestId("thumbnail-related-article-2"),
).toBeInTheDocument();
expect(
screen.getByTestId("thumbnail-related-article-3"),
).toBeInTheDocument();
});
it("filters out the current post from related articles", () => {
const postsWithCurrent = [
...mockRelatedPosts,
{
slug: "current-article",
frontmatter: {
title: "Current Article",
description: "This is the current article",
author: "Test Author",
date: "2025-04-15",
},
},
];
render(
<RelatedArticles
relatedPosts={postsWithCurrent}
currentPostSlug="current-article"
/>,
);
// Should not render the current article
expect(
screen.queryByTestId("thumbnail-current-article"),
).not.toBeInTheDocument();
// Should still render the other related articles
expect(
screen.getByTestId("thumbnail-related-article-1"),
).toBeInTheDocument();
expect(
screen.getByTestId("thumbnail-related-article-2"),
).toBeInTheDocument();
expect(
screen.getByTestId("thumbnail-related-article-3"),
).toBeInTheDocument();
});
it("renders nothing when no related posts", () => {
const { container } = render(
<RelatedArticles relatedPosts={[]} currentPostSlug="current-article" />,
);
expect(container.firstChild).toBeNull();
});
it("renders nothing when all posts are filtered out", () => {
const currentPostOnly = [
{
slug: "current-article",
frontmatter: {
title: "Current Article",
description: "This is the current article",
author: "Test Author",
date: "2025-04-15",
},
},
];
const { container } = render(
<RelatedArticles
relatedPosts={currentPostOnly}
currentPostSlug="current-article"
/>,
);
expect(container.firstChild).toBeNull();
});
it("has correct container styling", () => {
render(
<RelatedArticles
relatedPosts={mockRelatedPosts}
currentPostSlug="current-article"
/>,
);
const container = document.querySelector("section > div");
expect(container).toHaveClass(
"flex",
"flex-col",
"gap-[var(--spacing-scale-032)]",
"lg:gap-[51px]",
);
});
it("has correct articles container styling", () => {
render(
<RelatedArticles
relatedPosts={mockRelatedPosts}
currentPostSlug="current-article"
/>,
);
const articlesContainer = document.querySelector("section > div > div");
expect(articlesContainer).toHaveClass(
"flex",
"justify-center",
"overflow-hidden",
);
});
it("applies correct responsive behavior for desktop", () => {
// Set desktop width (must be > 1024px to be desktop, since lg breakpoint is 1024px)
Object.defineProperty(window, "innerWidth", {
writable: true,
configurable: true,
value: 1200,
});
render(
<RelatedArticles
relatedPosts={mockRelatedPosts}
currentPostSlug="current-article"
/>,
);
const carouselContainer = document.querySelector(
"section > div > div > div",
);
expect(carouselContainer).toHaveClass(
"overflow-x-auto",
"scrollbar-hide",
"cursor-grab",
"active:cursor-grabbing",
);
});
it("applies correct responsive behavior for mobile", () => {
// Set mobile width
Object.defineProperty(window, "innerWidth", {
writable: true,
configurable: true,
value: 768,
});
render(
<RelatedArticles
relatedPosts={mockRelatedPosts}
currentPostSlug="current-article"
/>,
);
const carouselContainer = document.querySelector(
"section > div > div > div",
);
expect(carouselContainer).toHaveClass(
"transition-transform",
"duration-500",
"ease-in-out",
);
});
it("handles single related article", () => {
const singlePost = [mockRelatedPosts[0]];
render(
<RelatedArticles
relatedPosts={singlePost}
currentPostSlug="current-article"
/>,
);
expect(
screen.getByTestId("thumbnail-related-article-1"),
).toBeInTheDocument();
expect(
screen.queryByTestId("thumbnail-related-article-2"),
).not.toBeInTheDocument();
expect(
screen.queryByTestId("thumbnail-related-article-3"),
).not.toBeInTheDocument();
});
it("handles two related articles", () => {
const twoPosts = mockRelatedPosts.slice(0, 2);
render(
<RelatedArticles
relatedPosts={twoPosts}
currentPostSlug="current-article"
/>,
);
expect(
screen.getByTestId("thumbnail-related-article-1"),
).toBeInTheDocument();
expect(
screen.getByTestId("thumbnail-related-article-2"),
).toBeInTheDocument();
expect(
screen.queryByTestId("thumbnail-related-article-3"),
).not.toBeInTheDocument();
});
it("has proper accessibility attributes", () => {
render(
<RelatedArticles
relatedPosts={mockRelatedPosts}
currentPostSlug="current-article"
/>,
);
const section = document.querySelector("section");
expect(section).toBeInTheDocument();
});
it("applies correct gap between articles", () => {
render(
<RelatedArticles
relatedPosts={mockRelatedPosts}
currentPostSlug="current-article"
/>,
);
const carouselContainer = document.querySelector(
"section > div > div > div",
);
expect(carouselContainer).toHaveClass("gap-0");
});
it("handles missing currentPostSlug gracefully", () => {
render(<RelatedArticles relatedPosts={mockRelatedPosts} />);
// Should still render all articles
expect(
screen.getByTestId("thumbnail-related-article-1"),
).toBeInTheDocument();
expect(
screen.getByTestId("thumbnail-related-article-2"),
).toBeInTheDocument();
expect(
screen.getByTestId("thumbnail-related-article-3"),
).toBeInTheDocument();
});
it("handles malformed post data gracefully", () => {
const malformedPosts = [
{
slug: "malformed-1",
frontmatter: {
title: "Malformed Post 1",
description: "Test description",
author: "Test Author",
date: "2025-04-15",
},
},
{
slug: "malformed-2",
frontmatter: {
title: "Malformed Post 2",
description: "Test description",
author: "Test Author",
date: "2025-04-15",
},
},
];
render(
<RelatedArticles
relatedPosts={malformedPosts}
currentPostSlug="current-article"
/>,
);
expect(screen.getByTestId("thumbnail-malformed-1")).toBeInTheDocument();
expect(screen.getByTestId("thumbnail-malformed-2")).toBeInTheDocument();
});
});
-198
View File
@@ -1,198 +0,0 @@
import { render, screen } from "@testing-library/react";
import { describe, it, expect } from "vitest";
import SectionHeader from "../../app/components/SectionHeader";
describe("SectionHeader Component", () => {
it("renders section header with title", () => {
render(<SectionHeader title="Test Section" />);
expect(screen.getByRole("heading", { level: 2 })).toBeInTheDocument();
// Check for both mobile and desktop versions of the title
expect(screen.getAllByText("Test Section")).toHaveLength(2);
});
it("renders with subtitle when provided", () => {
const subtitle = "This is a test subtitle";
render(<SectionHeader title="Test Section" subtitle={subtitle} />);
expect(screen.getByText(subtitle)).toBeInTheDocument();
});
it("renders with titleLg when provided", () => {
const titleLg = "Large Title for Desktop";
render(<SectionHeader title="Test Section" titleLg={titleLg} />);
// Check for mobile title and desktop titleLg
expect(screen.getByText("Test Section")).toBeInTheDocument();
expect(screen.getByText(titleLg)).toBeInTheDocument();
});
it("applies variant classes correctly", () => {
const { rerender } = render(
<SectionHeader title="Default Header" variant="default" />,
);
let titleContainer = screen
.getByRole("heading", { level: 2 })
.closest("div");
expect(titleContainer).toHaveClass(
"lg:w-[369px]",
"lg:h-[var(--spacing-scale-120)]",
);
rerender(<SectionHeader title="Multi-line Header" variant="multi-line" />);
titleContainer = screen.getByRole("heading", { level: 2 }).closest("div");
expect(titleContainer).toHaveClass(
"lg:w-[50%]",
"lg:h-[var(--spacing-scale-120)]",
);
});
it("renders responsive title spans", () => {
render(<SectionHeader title="Test Section" />);
const mobileTitle = screen.getByText("Test Section", {
selector: "span.block.lg\\:hidden",
});
const desktopTitle = screen.getByText("Test Section", {
selector: "span.hidden.lg\\:block",
});
expect(mobileTitle).toBeInTheDocument();
expect(desktopTitle).toBeInTheDocument();
});
it("uses titleLg for desktop when provided", () => {
const titleLg = "Desktop Title";
render(<SectionHeader title="Mobile Title" titleLg={titleLg} />);
const mobileTitle = screen.getByText("Mobile Title", {
selector: "span.block.lg\\:hidden",
});
const desktopTitle = screen.getByText("Desktop Title", {
selector: "span.hidden.lg\\:block",
});
expect(mobileTitle).toBeInTheDocument();
expect(desktopTitle).toBeInTheDocument();
});
it("falls back to title for desktop when titleLg not provided", () => {
render(<SectionHeader title="Test Section" />);
const mobileTitle = screen.getByText("Test Section", {
selector: "span.block.lg\\:hidden",
});
const desktopTitle = screen.getByText("Test Section", {
selector: "span.hidden.lg\\:block",
});
expect(mobileTitle).toBeInTheDocument();
expect(desktopTitle).toBeInTheDocument();
});
it("applies proper responsive layout classes", () => {
render(<SectionHeader title="Test Section" />);
const container = screen
.getByRole("heading", { level: 2 })
.closest("div").parentElement;
expect(container).toHaveClass(
"flex",
"flex-col",
"lg:flex-row",
"lg:justify-between",
);
});
it("handles empty subtitle gracefully", () => {
render(<SectionHeader title="Test Section" subtitle="" />);
expect(screen.getByRole("heading", { level: 2 })).toBeInTheDocument();
// Empty subtitle should not cause issues - check that the paragraph element exists
const subtitleContainer = screen
.getByRole("heading", { level: 2 })
.closest("div")
.parentElement.querySelector("p");
expect(subtitleContainer).toBeInTheDocument();
});
it("maintains proper heading structure", () => {
render(<SectionHeader title="Test Section" />);
const heading = screen.getByRole("heading", { level: 2 });
expect(heading).toHaveTextContent("Test Section");
expect(heading.tagName).toBe("H2");
});
it("applies proper font classes", () => {
render(<SectionHeader title="Test Section" />);
const heading = screen.getByRole("heading", { level: 2 });
expect(heading).toHaveClass("font-bricolage-grotesque", "font-bold");
});
it("applies proper text sizing", () => {
render(<SectionHeader title="Test Section" />);
const heading = screen.getByRole("heading", { level: 2 });
expect(heading).toHaveClass(
"text-[28px]",
"sm:text-[32px]",
"lg:text-[32px]",
"xl:text-[40px]",
);
});
it("applies proper line heights", () => {
render(<SectionHeader title="Test Section" />);
const heading = screen.getByRole("heading", { level: 2 });
expect(heading).toHaveClass(
"leading-[36px]",
"sm:leading-[40px]",
"lg:leading-[40px]",
"xl:leading-[52px]",
);
});
it("applies proper text colors", () => {
render(<SectionHeader title="Test Section" />);
const heading = screen.getByRole("heading", { level: 2 });
expect(heading).toHaveClass("text-[var(--color-content-default-primary)]");
});
it("applies proper subtitle styling", () => {
const subtitle = "Test Subtitle";
render(<SectionHeader title="Test Section" subtitle={subtitle} />);
const subtitleElement = screen.getByText(subtitle);
expect(subtitleElement).toHaveClass("font-inter", "font-normal");
});
it("applies proper subtitle text sizing", () => {
const subtitle = "Test Subtitle";
render(<SectionHeader title="Test Section" subtitle={subtitle} />);
const subtitleElement = screen.getByText(subtitle);
expect(subtitleElement).toHaveClass(
"text-[18px]",
"sm:text-[18px]",
"lg:text-[24px]",
"xl:text-[32px]",
);
});
it("applies proper subtitle colors", () => {
const subtitle = "Test Subtitle";
render(<SectionHeader title="Test Section" subtitle={subtitle} />);
const subtitleElement = screen.getByText(subtitle);
expect(subtitleElement).toHaveClass(
"text-[#484848]",
"sm:text-[var(--color-content-default-tertiary)]",
"lg:text-[var(--color-content-default-tertiary)]",
"xl:text-[var(--color-content-default-tertiary)]",
);
});
});
-401
View File
@@ -1,401 +0,0 @@
import { render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { expect, describe, it, vi } from "vitest";
import { axe, toHaveNoViolations } from "jest-axe";
import Select from "../../app/components/Select";
expect.extend(toHaveNoViolations);
describe("Select Component", () => {
const defaultProps = {
label: "Test Select",
placeholder: "Select an option",
options: [
{ value: "option1", label: "Option 1" },
{ value: "option2", label: "Option 2" },
{ value: "option3", label: "Option 3" },
],
};
describe("Rendering", () => {
it("renders with default props", () => {
render(<Select {...defaultProps} />);
expect(screen.getByText("Test Select")).toBeInTheDocument();
expect(screen.getByText("Select an option")).toBeInTheDocument();
});
it("renders without label when not provided", () => {
render(
<Select
placeholder="Select an option"
options={defaultProps.options}
/>,
);
expect(screen.queryByText("Test Select")).not.toBeInTheDocument();
expect(screen.getByText("Select an option")).toBeInTheDocument();
});
it("renders with horizontal label variant", () => {
render(<Select {...defaultProps} labelVariant="horizontal" />);
const container = screen.getByText("Test Select").closest("div");
expect(container).toHaveClass("flex", "items-center");
});
it("renders with default label variant", () => {
render(<Select {...defaultProps} labelVariant="default" />);
const container = screen.getByText("Test Select").closest("div");
expect(container).toHaveClass("flex", "flex-col");
});
});
describe("Size Variants", () => {
it("renders small size correctly", () => {
render(<Select {...defaultProps} size="small" />);
const selectButton = screen.getByRole("button");
expect(selectButton).toHaveClass("h-[32px]");
});
it("renders medium size correctly", () => {
render(<Select {...defaultProps} size="medium" />);
const selectButton = screen.getByRole("button");
expect(selectButton).toHaveClass("h-[36px]");
});
it("renders large size correctly", () => {
render(<Select {...defaultProps} size="large" />);
const selectButton = screen.getByRole("button");
expect(selectButton).toHaveClass("h-[40px]");
});
it("applies correct height for small horizontal label", () => {
render(
<Select {...defaultProps} size="small" labelVariant="horizontal" />,
);
const selectButton = screen.getByRole("button");
expect(selectButton).toHaveClass("h-[30px]");
});
it("applies correct height for small default label", () => {
render(<Select {...defaultProps} size="small" labelVariant="default" />);
const selectButton = screen.getByRole("button");
expect(selectButton).toHaveClass("h-[32px]");
});
});
describe("State Variants", () => {
it("renders default state", () => {
render(<Select {...defaultProps} />);
const selectButton = screen.getByRole("button");
expect(selectButton).toHaveClass(
"border-[var(--color-border-default-tertiary)]",
);
});
it("renders hover state", () => {
render(<Select {...defaultProps} state="hover" />);
const selectButton = screen.getByRole("button");
expect(selectButton).toHaveClass(
"shadow-[0_0_0_2px_var(--color-border-default-tertiary)]",
);
});
it("renders focus state", () => {
render(<Select {...defaultProps} state="focus" />);
const selectButton = screen.getByRole("button");
expect(selectButton).toHaveClass(
"border-[var(--color-border-default-utility-info)]",
);
expect(selectButton).toHaveClass("shadow-[0_0_5px_3px_#3281F8]");
});
it("renders error state", () => {
render(<Select {...defaultProps} error={true} />);
const selectButton = screen.getByRole("button");
expect(selectButton).toHaveClass(
"border-[var(--color-border-default-utility-negative)]",
);
});
it("renders disabled state", () => {
render(<Select {...defaultProps} disabled={true} />);
const selectButton = screen.getByRole("button");
expect(selectButton).toHaveClass("cursor-not-allowed");
expect(selectButton).toHaveClass("opacity-40");
});
});
describe("Interaction", () => {
it("opens dropdown when clicked", async () => {
const user = userEvent.setup();
render(<Select {...defaultProps} />);
const selectButton = screen.getByRole("button");
await user.click(selectButton);
await waitFor(() => {
expect(screen.getByText("Option 1")).toBeInTheDocument();
expect(screen.getByText("Option 2")).toBeInTheDocument();
expect(screen.getByText("Option 3")).toBeInTheDocument();
});
});
it("closes dropdown when clicked again", async () => {
const user = userEvent.setup();
render(<Select {...defaultProps} />);
const selectButton = screen.getByRole("button");
await user.click(selectButton);
await waitFor(() => {
expect(screen.getByText("Option 1")).toBeInTheDocument();
});
await user.click(selectButton);
await waitFor(() => {
expect(screen.queryByText("Option 1")).not.toBeInTheDocument();
});
});
it("selects an option when clicked", async () => {
const user = userEvent.setup();
const onChange = vi.fn();
render(<Select {...defaultProps} onChange={onChange} />);
const selectButton = screen.getByRole("button");
await user.click(selectButton);
await waitFor(() => {
expect(screen.getByText("Option 1")).toBeInTheDocument();
});
await user.click(screen.getByText("Option 1"));
expect(onChange).toHaveBeenCalledWith({
target: {
value: "option1",
text: "Option 1",
},
});
expect(screen.getByText("Option 1")).toBeInTheDocument();
});
it("closes dropdown when option is selected", async () => {
const user = userEvent.setup();
render(<Select {...defaultProps} />);
const selectButton = screen.getByRole("button");
await user.click(selectButton);
await waitFor(() => {
expect(screen.getByText("Option 1")).toBeInTheDocument();
});
await user.click(screen.getByText("Option 1"));
await waitFor(() => {
expect(screen.queryByText("Option 2")).not.toBeInTheDocument();
});
});
it("does not open when disabled", async () => {
const user = userEvent.setup();
render(<Select {...defaultProps} disabled={true} />);
const selectButton = screen.getByRole("button");
await user.click(selectButton);
expect(screen.queryByText("Option 1")).not.toBeInTheDocument();
});
});
describe("Keyboard Navigation", () => {
it("opens dropdown with Enter key", async () => {
const user = userEvent.setup();
render(<Select {...defaultProps} />);
const selectButton = screen.getByRole("button");
selectButton.focus();
await user.keyboard("{Enter}");
await waitFor(() => {
expect(screen.getByText("Option 1")).toBeInTheDocument();
});
});
it("opens dropdown with Space key", async () => {
const user = userEvent.setup();
render(<Select {...defaultProps} />);
const selectButton = screen.getByRole("button");
selectButton.focus();
await user.keyboard(" ");
await waitFor(() => {
expect(screen.getByText("Option 1")).toBeInTheDocument();
});
});
it("closes dropdown with Escape key", async () => {
const user = userEvent.setup();
render(<Select {...defaultProps} />);
const selectButton = screen.getByRole("button");
await user.click(selectButton);
await waitFor(() => {
expect(screen.getByText("Option 1")).toBeInTheDocument();
});
await user.keyboard("{Escape}");
await waitFor(() => {
expect(screen.queryByText("Option 1")).not.toBeInTheDocument();
});
});
it("does not respond to keyboard when disabled", async () => {
const user = userEvent.setup();
render(<Select {...defaultProps} disabled={true} />);
const selectButton = screen.getByRole("button");
selectButton.focus();
await user.keyboard("{Enter}");
expect(screen.queryByText("Option 1")).not.toBeInTheDocument();
});
});
describe("Click Outside", () => {
it("closes dropdown when clicking outside", async () => {
const user = userEvent.setup();
render(
<div>
<Select {...defaultProps} />
<div data-testid="outside">Outside element</div>
</div>,
);
const selectButton = screen.getByRole("button");
await user.click(selectButton);
await waitFor(() => {
expect(screen.getByText("Option 1")).toBeInTheDocument();
});
await user.click(screen.getByTestId("outside"));
await waitFor(() => {
expect(screen.queryByText("Option 1")).not.toBeInTheDocument();
});
});
});
describe("Value Display", () => {
it("shows placeholder when no value selected", () => {
render(<Select {...defaultProps} />);
expect(screen.getByText("Select an option")).toBeInTheDocument();
});
it("shows selected value when option is selected", async () => {
const user = userEvent.setup();
render(<Select {...defaultProps} />);
const selectButton = screen.getByRole("button");
await user.click(selectButton);
await waitFor(() => {
expect(screen.getByText("Option 1")).toBeInTheDocument();
});
await user.click(screen.getByText("Option 1"));
expect(screen.getByText("Option 1")).toBeInTheDocument();
expect(screen.queryByText("Select an option")).not.toBeInTheDocument();
});
it("shows selected value when value prop is provided", () => {
render(<Select {...defaultProps} value="option2" />);
expect(screen.getByText("Option 2")).toBeInTheDocument();
});
});
describe("Accessibility", () => {
it("has no accessibility violations", async () => {
const { container } = render(<Select {...defaultProps} />);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
it("has proper ARIA attributes", () => {
render(<Select {...defaultProps} />);
const selectButton = screen.getByRole("button");
expect(selectButton).toHaveAttribute("aria-expanded", "false");
expect(selectButton).toHaveAttribute("aria-haspopup", "listbox");
});
it("updates aria-expanded when dropdown opens", async () => {
const user = userEvent.setup();
render(<Select {...defaultProps} />);
const selectButton = screen.getByRole("button");
await user.click(selectButton);
await waitFor(() => {
expect(selectButton).toHaveAttribute("aria-expanded", "true");
});
});
it("associates label with select button", () => {
render(<Select {...defaultProps} />);
const label = screen.getByText("Test Select");
const selectButton = screen.getByRole("button");
expect(label).toHaveAttribute("for", selectButton.id);
});
});
describe("Focus Behavior", () => {
it("enters focus state when tabbed to", async () => {
const user = userEvent.setup();
render(<Select {...defaultProps} />);
const selectButton = screen.getByRole("button");
await user.tab();
expect(selectButton).toHaveFocus();
expect(selectButton).toHaveClass(
"focus-visible:border-[var(--color-border-default-utility-info)]",
);
});
it("does not enter focus state when clicked", async () => {
const user = userEvent.setup();
render(<Select {...defaultProps} />);
const selectButton = screen.getByRole("button");
await user.click(selectButton);
expect(selectButton).toHaveFocus();
// Focus state should not be applied on click, only on keyboard navigation
});
});
});
-184
View File
@@ -1,184 +0,0 @@
import React from "react";
import { render, screen, fireEvent } from "@testing-library/react";
import { describe, it, expect, vi } from "vitest";
import Switch from "../../app/components/Switch";
describe("Switch Component", () => {
it("renders with default props", () => {
render(<Switch />);
const switchButton = screen.getByRole("switch");
expect(switchButton).toBeInTheDocument();
expect(switchButton).toHaveAttribute("aria-checked", "false");
});
it("renders with custom props", () => {
const handleChange = vi.fn();
render(
<Switch
checked={true}
onChange={handleChange}
label="Test Switch"
state="focus"
/>,
);
const switchButton = screen.getByRole("switch");
expect(switchButton).toHaveAttribute("aria-checked", "true");
expect(screen.getByText("Test Switch")).toBeInTheDocument();
});
it("handles checked prop correctly", () => {
const { rerender } = render(<Switch checked={false} />);
let switchButton = screen.getByRole("switch");
expect(switchButton).toHaveAttribute("aria-checked", "false");
rerender(<Switch checked={true} />);
switchButton = screen.getByRole("switch");
expect(switchButton).toHaveAttribute("aria-checked", "true");
});
it("handles state prop correctly", () => {
const { rerender } = render(<Switch state="default" />);
let switchButton = screen.getByRole("switch");
expect(switchButton).not.toHaveClass("shadow-[0_0_5px_1px_#3281F8]");
rerender(<Switch state="focus" />);
switchButton = screen.getByRole("switch");
expect(switchButton).toHaveClass("shadow-[0_0_5px_3px_#3281F8]");
});
it("calls onChange when clicked", () => {
const handleChange = vi.fn();
render(<Switch onChange={handleChange} />);
const switchButton = screen.getByRole("switch");
fireEvent.click(switchButton);
expect(handleChange).toHaveBeenCalledTimes(1);
});
it("calls onFocus when focused", () => {
const handleFocus = vi.fn();
render(<Switch onFocus={handleFocus} />);
const switchButton = screen.getByRole("switch");
fireEvent.focus(switchButton);
expect(handleFocus).toHaveBeenCalledTimes(1);
});
it("calls onBlur when blurred", () => {
const handleBlur = vi.fn();
render(<Switch onBlur={handleBlur} />);
const switchButton = screen.getByRole("switch");
fireEvent.blur(switchButton);
expect(handleBlur).toHaveBeenCalledTimes(1);
});
it("handles keyboard events correctly", () => {
const handleChange = vi.fn();
render(<Switch onChange={handleChange} />);
const switchButton = screen.getByRole("switch");
// Test Enter key
fireEvent.keyDown(switchButton, { key: "Enter" });
expect(handleChange).toHaveBeenCalledTimes(1);
// Test Space key
fireEvent.keyDown(switchButton, { key: " " });
expect(handleChange).toHaveBeenCalledTimes(2);
// Test other key (should not trigger)
fireEvent.keyDown(switchButton, { key: "Tab" });
expect(handleChange).toHaveBeenCalledTimes(2);
});
it("applies correct classes for different states", () => {
const { rerender } = render(<Switch checked={false} />);
let switchButton = screen.getByRole("switch");
expect(switchButton).toHaveClass("cursor-pointer");
rerender(<Switch checked={true} />);
switchButton = screen.getByRole("switch");
expect(switchButton).toHaveClass("cursor-pointer");
});
it("applies correct track styles based on checked state", () => {
const { rerender } = render(<Switch checked={false} />);
let switchButton = screen.getByRole("switch");
let track = switchButton.querySelector("div");
expect(track).toHaveClass("bg-[var(--color-surface-default-tertiary)]");
rerender(<Switch checked={true} />);
switchButton = screen.getByRole("switch");
track = switchButton.querySelector("div");
expect(track).toHaveClass("bg-[var(--color-surface-inverse-tertiary)]");
switchButton = screen.getByRole("switch");
track = switchButton.querySelector("div");
expect(track).toHaveClass("bg-[var(--color-surface-inverse-tertiary)]");
});
it("applies correct focus styles", () => {
const { rerender } = render(<Switch state="default" />);
let switchButton = screen.getByRole("switch");
expect(switchButton).not.toHaveClass("shadow-[0_0_5px_1px_#3281F8]");
rerender(<Switch state="focus" />);
switchButton = screen.getByRole("switch");
expect(switchButton).toHaveClass("shadow-[0_0_5px_3px_#3281F8]");
});
it("applies correct base classes", () => {
render(<Switch />);
const switchButton = screen.getByRole("switch");
expect(switchButton).toHaveClass(
"relative",
"inline-flex",
"items-center",
"cursor-pointer",
"transition-all",
"duration-200",
"focus:outline-none",
"focus-visible:shadow-[0_0_5px_3px_#3281F8]",
);
});
it("forwards ref correctly", () => {
const ref = React.createRef();
render(<Switch ref={ref} />);
expect(ref.current).toBeInstanceOf(HTMLButtonElement);
});
it("applies custom className", () => {
render(<Switch className="custom-class" />);
const switchButton = screen.getByRole("switch");
expect(switchButton).toHaveClass("custom-class");
});
it("renders label when provided", () => {
render(<Switch label="Test Label" />);
expect(screen.getByText("Test Label")).toBeInTheDocument();
});
it("does not render label when not provided", () => {
render(<Switch />);
expect(screen.queryByText("Switch label")).not.toBeInTheDocument();
// Should have aria-label for accessibility
const switchButton = screen.getByRole("switch");
expect(switchButton).toHaveAttribute("aria-label", "Toggle switch");
});
it("applies correct label styles", () => {
render(<Switch label="Test Label" />);
const label = screen.getByText("Test Label");
expect(label).toHaveClass(
"ml-[var(--measures-spacing-008)]",
"font-inter",
"font-normal",
"text-[14px]",
"leading-[20px]",
"text-[var(--color-content-default-primary)]",
);
});
});
-203
View File
@@ -1,203 +0,0 @@
import { expect, test, describe, vi } from "vitest";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import TextArea from "../../app/components/TextArea";
describe("TextArea", () => {
test("renders with default props", () => {
render(<TextArea />);
const textarea = screen.getByRole("textbox");
expect(textarea).toBeInTheDocument();
});
test("renders with label", () => {
render(<TextArea label="Test Label" />);
expect(screen.getByText("Test Label")).toBeInTheDocument();
expect(screen.getByLabelText("Test Label")).toBeInTheDocument();
});
test("renders with placeholder", () => {
render(<TextArea placeholder="Enter text..." />);
expect(screen.getByPlaceholderText("Enter text...")).toBeInTheDocument();
});
test("renders with value", () => {
render(<TextArea value="Test value" />);
const textarea = screen.getByRole("textbox");
expect(textarea).toHaveValue("Test value");
});
test("renders with different sizes", () => {
const { rerender } = render(<TextArea size="small" label="Small" />);
let textarea = screen.getByRole("textbox");
expect(textarea).toHaveClass("h-[60px]");
rerender(<TextArea size="medium" label="Medium" />);
textarea = screen.getByRole("textbox");
expect(textarea).toHaveClass("h-[100px]");
rerender(<TextArea size="large" label="Large" />);
textarea = screen.getByRole("textbox");
expect(textarea).toHaveClass("h-[150px]");
});
test("renders with horizontal label variant", () => {
render(<TextArea labelVariant="horizontal" label="Horizontal Label" />);
const container = screen.getByRole("textbox").closest("div").parentElement;
expect(container).toHaveClass("flex", "items-center", "gap-[12px]");
});
test("renders with default label variant", () => {
render(<TextArea labelVariant="default" label="Default Label" />);
const container = screen.getByRole("textbox").closest("div").parentElement;
expect(container).toHaveClass("flex", "flex-col");
});
test("applies disabled state", () => {
render(<TextArea disabled />);
const textarea = screen.getByRole("textbox");
expect(textarea).toBeDisabled();
});
test("applies error state", () => {
render(<TextArea error />);
const textarea = screen.getByRole("textbox");
expect(textarea).toHaveClass(
"border-[var(--color-border-default-utility-negative)]",
);
});
test("applies different states", () => {
const { rerender } = render(<TextArea state="active" />);
let textarea = screen.getByRole("textbox");
expect(textarea).toHaveClass(
"border-[var(--color-border-default-tertiary)]",
);
rerender(<TextArea state="hover" />);
textarea = screen.getByRole("textbox");
expect(textarea).toHaveClass(
"shadow-[0_0_0_2px_var(--color-border-default-tertiary)]",
);
rerender(<TextArea state="focus" />);
textarea = screen.getByRole("textbox");
expect(textarea).toHaveClass(
"border-[var(--color-border-default-utility-info)]",
"shadow-[0_0_5px_3px_#3281F8]",
);
});
test("calls onChange when text changes", async () => {
const user = userEvent.setup();
const handleChange = vi.fn();
render(<TextArea onChange={handleChange} />);
const textarea = screen.getByRole("textbox");
await user.type(textarea, "test");
expect(handleChange).toHaveBeenCalledTimes(4);
});
test("calls onFocus when focused", async () => {
const user = userEvent.setup();
const handleFocus = vi.fn();
render(<TextArea onFocus={handleFocus} />);
const textarea = screen.getByRole("textbox");
await user.click(textarea);
expect(handleFocus).toHaveBeenCalled();
});
test("calls onBlur when blurred", async () => {
const user = userEvent.setup();
const handleBlur = vi.fn();
render(<TextArea onBlur={handleBlur} />);
const textarea = screen.getByRole("textbox");
await user.click(textarea);
await user.tab();
expect(handleBlur).toHaveBeenCalled();
});
test("does not call onChange when disabled", async () => {
const user = userEvent.setup();
const handleChange = vi.fn();
render(<TextArea disabled onChange={handleChange} />);
const textarea = screen.getByRole("textbox");
await user.type(textarea, "test");
expect(handleChange).not.toHaveBeenCalled();
});
test("applies custom className", () => {
render(<TextArea className="custom-class" />);
const textarea = screen.getByRole("textbox");
expect(textarea).toHaveClass("custom-class");
});
test("forwards ref", () => {
const ref = vi.fn();
render(<TextArea ref={ref} />);
expect(ref).toHaveBeenCalled();
});
test("applies correct height for small horizontal label", () => {
render(
<TextArea
size="small"
labelVariant="horizontal"
label="Small Horizontal"
/>,
);
const textarea = screen.getByRole("textbox");
expect(textarea).toHaveClass("h-[60px]");
});
test("applies correct height for medium horizontal label", () => {
render(
<TextArea
size="medium"
labelVariant="horizontal"
label="Medium Horizontal"
/>,
);
const textarea = screen.getByRole("textbox");
expect(textarea).toHaveClass("h-[110px]");
});
test("applies correct border radius for different sizes", () => {
const { rerender } = render(<TextArea size="small" />);
let textarea = screen.getByRole("textbox");
expect(textarea).toHaveStyle({
borderRadius: "var(--measures-radius-xsmall)",
});
rerender(<TextArea size="medium" />);
textarea = screen.getByRole("textbox");
expect(textarea).toHaveStyle({
borderRadius: "var(--measures-radius-xsmall)",
});
rerender(<TextArea size="large" />);
textarea = screen.getByRole("textbox");
expect(textarea).toHaveStyle({
borderRadius: "var(--measures-radius-small)",
});
});
test("applies correct text color", () => {
render(<TextArea />);
const textarea = screen.getByRole("textbox");
expect(textarea).toHaveClass("text-[var(--color-content-default-primary)]");
});
test("applies correct label color", () => {
render(<TextArea label="Test Label" />);
const label = screen.getByText("Test Label");
expect(label).toHaveClass("text-[var(--color-content-default-secondary)]");
});
});
-195
View File
@@ -1,195 +0,0 @@
import { expect, test, describe, vi } from "vitest";
import { render, screen, fireEvent } from "@testing-library/react";
import Toggle from "../../app/components/Toggle";
describe("Toggle Component", () => {
test("renders with default props", () => {
render(<Toggle label="Test Toggle" />);
const toggle = screen.getByRole("switch");
const label = screen.getByText("Test Toggle");
expect(toggle).toBeInTheDocument();
expect(label).toBeInTheDocument();
expect(toggle).toHaveAttribute("type", "button");
});
test("renders with custom props", () => {
render(
<Toggle
label="Custom Toggle"
checked={true}
disabled={true}
className="custom-class"
/>,
);
const toggle = screen.getByRole("switch");
expect(toggle).toBeInTheDocument();
expect(toggle).toHaveAttribute("aria-checked", "true");
expect(toggle).toHaveAttribute("disabled");
});
test("handles checked state", () => {
const { rerender } = render(<Toggle label="Test Toggle" checked={false} />);
let toggle = screen.getByRole("switch");
expect(toggle).toHaveAttribute("aria-checked", "false");
rerender(<Toggle label="Test Toggle" checked={true} />);
toggle = screen.getByRole("switch");
expect(toggle).toHaveAttribute("aria-checked", "true");
});
test("handles disabled state", () => {
render(<Toggle label="Test Toggle" disabled={true} />);
const toggle = screen.getByRole("switch");
expect(toggle).toHaveAttribute("disabled");
expect(toggle).toHaveClass("cursor-not-allowed");
});
test("handles state prop", () => {
const { rerender } = render(<Toggle label="Test Toggle" state="focus" />);
let toggle = screen.getByRole("switch");
expect(toggle).toHaveClass("shadow-[0_0_5px_1px_#3281F8]");
rerender(<Toggle label="Test Toggle" state="default" />);
toggle = screen.getByRole("switch");
expect(toggle).not.toHaveClass("shadow-[0_0_5px_1px_#3281F8]");
});
test("handles showIcon and icon props", () => {
render(<Toggle label="Test Toggle" showIcon={true} icon="I" />);
const toggle = screen.getByRole("switch");
expect(toggle).toHaveTextContent("I");
});
test("handles showText and text props", () => {
render(<Toggle label="Test Toggle" showText={true} text="Toggle" />);
const toggle = screen.getByRole("switch");
expect(toggle).toHaveTextContent("Toggle");
});
test("handles both icon and text", () => {
render(
<Toggle
label="Test Toggle"
showIcon={true}
showText={true}
icon="I"
text="Toggle"
/>,
);
const toggle = screen.getByRole("switch");
expect(toggle).toHaveTextContent("I");
expect(toggle).toHaveTextContent("Toggle");
});
test("calls onChange when clicked", () => {
const handleChange = vi.fn();
render(<Toggle label="Test Toggle" onChange={handleChange} />);
const toggle = screen.getByRole("switch");
fireEvent.click(toggle);
expect(handleChange).toHaveBeenCalledTimes(1);
});
test("does not call onChange when disabled", () => {
const handleChange = vi.fn();
render(
<Toggle label="Test Toggle" disabled={true} onChange={handleChange} />,
);
const toggle = screen.getByRole("switch");
fireEvent.click(toggle);
expect(handleChange).not.toHaveBeenCalled();
});
test("applies correct classes for different states", () => {
const { rerender } = render(<Toggle label="Test Toggle" checked={false} />);
let toggle = screen.getByRole("switch");
expect(toggle).toHaveClass("bg-[var(--color-surface-default-primary)]");
rerender(<Toggle label="Test Toggle" checked={true} />);
toggle = screen.getByRole("switch");
expect(toggle).toHaveClass("bg-[var(--color-magenta-magenta100)]");
rerender(<Toggle label="Test Toggle" disabled={true} />);
toggle = screen.getByRole("switch");
expect(toggle).toHaveClass("bg-[var(--color-surface-default-tertiary)]");
});
test("applies hover classes when not checked", () => {
render(<Toggle label="Test Toggle" checked={false} />);
const toggle = screen.getByRole("switch");
expect(toggle).toHaveClass(
"hover:!bg-[var(--color-surface-default-secondary)]",
);
});
test("does not apply hover classes when checked", () => {
render(<Toggle label="Test Toggle" checked={true} />);
const toggle = screen.getByRole("switch");
expect(toggle).not.toHaveClass(
"hover:!bg-[var(--color-surface-default-secondary)]",
);
});
test("applies focus-visible classes", () => {
render(<Toggle label="Test Toggle" />);
const toggle = screen.getByRole("switch");
expect(toggle).toHaveClass("focus-visible:shadow-[0_0_5px_1px_#3281F8]");
});
test("applies correct size classes", () => {
render(<Toggle label="Test Toggle" />);
const toggle = screen.getByRole("switch");
expect(toggle).toHaveClass("h-[var(--measures-sizing-032)]");
expect(toggle).toHaveClass("px-[16px]");
expect(toggle).toHaveClass("py-[8px]");
expect(toggle).toHaveClass("gap-[4px]");
});
test("applies correct text classes", () => {
render(<Toggle label="Test Toggle" />);
const toggle = screen.getByRole("switch");
expect(toggle).toHaveClass("text-[12px]");
expect(toggle).toHaveClass("leading-[16px]");
});
test("applies correct label classes", () => {
render(<Toggle label="Test Toggle" />);
const label = screen.getByText("Test Toggle");
expect(label).toHaveClass("text-[12px]");
expect(label).toHaveClass("leading-[16px]");
expect(label).toHaveClass("text-[var(--color-content-default-secondary)]");
});
test("forwards ref correctly", () => {
const ref = vi.fn();
render(<Toggle label="Test Toggle" ref={ref} />);
expect(ref).toHaveBeenCalled();
});
test("applies custom className", () => {
render(<Toggle label="Test Toggle" className="custom-class" />);
const toggle = screen.getByRole("switch");
expect(toggle).toHaveClass("custom-class");
});
});
-213
View File
@@ -1,213 +0,0 @@
import React from "react";
import { render, screen, fireEvent } from "@testing-library/react";
import { describe, it, expect, vi } from "vitest";
import ToggleGroup from "../../app/components/ToggleGroup";
describe("ToggleGroup Component", () => {
it("renders with default props", () => {
render(<ToggleGroup>Test Content</ToggleGroup>);
const toggleGroup = screen.getByRole("button");
expect(toggleGroup).toBeInTheDocument();
expect(toggleGroup).toHaveTextContent("Test Content");
});
it("renders with custom props", () => {
render(
<ToggleGroup position="middle" state="selected" showText={true}>
Custom Content
</ToggleGroup>,
);
const toggleGroup = screen.getByRole("button");
expect(toggleGroup).toBeInTheDocument();
expect(toggleGroup).toHaveTextContent("Custom Content");
});
it("handles position prop correctly", () => {
const { rerender } = render(
<ToggleGroup position="left">Left</ToggleGroup>,
);
let toggleGroup = screen.getByRole("button");
expect(toggleGroup).toHaveClass(
"rounded-l-[var(--measures-radius-medium)]",
"rounded-r-none",
);
rerender(<ToggleGroup position="middle">Middle</ToggleGroup>);
toggleGroup = screen.getByRole("button");
expect(toggleGroup).toHaveClass("rounded-none");
rerender(<ToggleGroup position="right">Right</ToggleGroup>);
toggleGroup = screen.getByRole("button");
expect(toggleGroup).toHaveClass(
"rounded-r-[var(--measures-radius-medium)]",
"rounded-l-none",
);
});
it("handles state prop correctly", () => {
const { rerender } = render(
<ToggleGroup state="default">Default</ToggleGroup>,
);
let toggleGroup = screen.getByRole("button");
expect(toggleGroup).toHaveClass(
"bg-[var(--color-surface-default-primary)]",
);
rerender(<ToggleGroup state="hover">Hover</ToggleGroup>);
toggleGroup = screen.getByRole("button");
expect(toggleGroup).toHaveClass("bg-[var(--color-magenta-magenta100)]");
rerender(<ToggleGroup state="focus">Focus</ToggleGroup>);
toggleGroup = screen.getByRole("button");
expect(toggleGroup).toHaveClass(
"bg-[var(--color-surface-default-primary)]",
"shadow-[0_0_5px_1px_#3281F8]",
);
rerender(<ToggleGroup state="selected">Selected</ToggleGroup>);
toggleGroup = screen.getByRole("button");
expect(toggleGroup).toHaveClass(
"bg-[var(--color-magenta-magenta100)]",
"shadow-[inset_0_0_0_1px_var(--color-border-default-secondary)]",
);
});
it("handles showText prop correctly", () => {
const { rerender } = render(
<ToggleGroup showText={true}>Visible Text</ToggleGroup>,
);
let toggleGroup = screen.getByRole("button");
expect(toggleGroup).toHaveTextContent("Visible Text");
rerender(<ToggleGroup showText={false}></ToggleGroup>);
toggleGroup = screen.getByRole("button");
expect(toggleGroup).toHaveTextContent("☰");
});
it("calls onChange when clicked", () => {
const handleChange = vi.fn();
render(<ToggleGroup onChange={handleChange}>Clickable</ToggleGroup>);
const toggleGroup = screen.getByRole("button");
fireEvent.click(toggleGroup);
expect(handleChange).toHaveBeenCalledTimes(1);
});
it("calls onFocus when focused", () => {
const handleFocus = vi.fn();
render(<ToggleGroup onFocus={handleFocus}>Focusable</ToggleGroup>);
const toggleGroup = screen.getByRole("button");
fireEvent.focus(toggleGroup);
expect(handleFocus).toHaveBeenCalledTimes(1);
});
it("calls onBlur when blurred", () => {
const handleBlur = vi.fn();
render(<ToggleGroup onBlur={handleBlur}>Blurable</ToggleGroup>);
const toggleGroup = screen.getByRole("button");
fireEvent.blur(toggleGroup);
expect(handleBlur).toHaveBeenCalledTimes(1);
});
it("handles keyboard events correctly", () => {
const handleChange = vi.fn();
render(<ToggleGroup onChange={handleChange}>Keyboard</ToggleGroup>);
const toggleGroup = screen.getByRole("button");
// Test Enter key
fireEvent.keyDown(toggleGroup, { key: "Enter" });
expect(handleChange).toHaveBeenCalledTimes(1);
// Test Space key
fireEvent.keyDown(toggleGroup, { key: " " });
expect(handleChange).toHaveBeenCalledTimes(2);
// Test other key (should not trigger)
fireEvent.keyDown(toggleGroup, { key: "Escape" });
expect(handleChange).toHaveBeenCalledTimes(2);
});
it("applies correct classes for different states", () => {
const { rerender } = render(
<ToggleGroup state="default">Default</ToggleGroup>,
);
let toggleGroup = screen.getByRole("button");
expect(toggleGroup).toHaveClass(
"bg-[var(--color-surface-default-primary)]",
);
rerender(<ToggleGroup state="hover">Hover</ToggleGroup>);
toggleGroup = screen.getByRole("button");
expect(toggleGroup).toHaveClass("bg-[var(--color-magenta-magenta100)]");
rerender(<ToggleGroup state="focus">Focus</ToggleGroup>);
toggleGroup = screen.getByRole("button");
expect(toggleGroup).toHaveClass("shadow-[0_0_5px_1px_#3281F8]");
rerender(<ToggleGroup state="selected">Selected</ToggleGroup>);
toggleGroup = screen.getByRole("button");
expect(toggleGroup).toHaveClass(
"bg-[var(--color-magenta-magenta100)]",
"shadow-[inset_0_0_0_1px_var(--color-border-default-secondary)]",
);
});
it("applies correct position classes", () => {
const { rerender } = render(
<ToggleGroup position="left">Left</ToggleGroup>,
);
let toggleGroup = screen.getByRole("button");
expect(toggleGroup).toHaveClass(
"rounded-l-[var(--measures-radius-medium)]",
"rounded-r-none",
);
rerender(<ToggleGroup position="middle">Middle</ToggleGroup>);
toggleGroup = screen.getByRole("button");
expect(toggleGroup).toHaveClass("rounded-none");
rerender(<ToggleGroup position="right">Right</ToggleGroup>);
toggleGroup = screen.getByRole("button");
expect(toggleGroup).toHaveClass(
"rounded-r-[var(--measures-radius-medium)]",
"rounded-l-none",
);
});
it("applies correct base classes", () => {
render(<ToggleGroup>Base</ToggleGroup>);
const toggleGroup = screen.getByRole("button");
expect(toggleGroup).toHaveClass(
"py-[var(--measures-spacing-008)]",
"px-[var(--measures-spacing-008)]",
"gap-[var(--measures-spacing-008)]",
"font-inter",
"font-medium",
"text-[12px]",
"leading-[12px]",
"cursor-pointer",
"transition-all",
"duration-200",
"focus:outline-none",
"focus-visible:shadow-[0_0_5px_1px_#3281F8]",
"hover:bg-[var(--color-magenta-magenta100)]",
"flex",
"items-center",
"justify-center",
);
});
it("forwards ref correctly", () => {
const ref = React.createRef();
render(<ToggleGroup ref={ref}>Ref Test</ToggleGroup>);
expect(ref.current).toBeInstanceOf(HTMLButtonElement);
});
it("applies custom className", () => {
render(<ToggleGroup className="custom-class">Custom</ToggleGroup>);
const toggleGroup = screen.getByRole("button");
expect(toggleGroup).toHaveClass("custom-class");
});
});
+221
View File
@@ -0,0 +1,221 @@
import React from "react";
import { describe, it, expect } from "vitest";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { axe, toHaveNoViolations } from "jest-axe";
expect.extend(toHaveNoViolations);
type TestCases = {
renders?: boolean;
accessibility?: boolean;
keyboardNavigation?: boolean;
disabledState?: boolean;
errorState?: boolean;
};
type StateConfig<TProps> = {
disabledProps?: Partial<TProps>;
errorProps?: Partial<TProps>;
};
export interface ComponentTestSuiteConfig<TProps> {
/**
* React component under test.
*/
component: React.ComponentType<TProps>;
/**
* Human-readable name for the suite (usually the component name).
*/
name: string;
/**
* Default props used for baseline rendering.
*/
props: TProps;
/**
* Props that are considered required for the component to behave correctly.
* Used for simple sanity checks (e.g., does label text render).
*/
requiredProps?: (keyof TProps)[];
/**
* Optional props that should not cause the component to break when omitted.
*/
optionalProps?: Partial<TProps>;
/**
* Primary ARIA role for the main interactive element.
* Used for generic keyboardNavigation and accessibility checks.
*
* Examples: "button", "textbox", "checkbox", "radio", "combobox".
*/
primaryRole?: string;
/**
* Which standard tests to run for this component.
*/
testCases?: TestCases;
/**
* State-specific props for disabled/error tests.
*/
states?: StateConfig<TProps>;
}
/**
* Standardized component test suite.
*
* Usage:
* componentTestSuite({
* component: Button,
* name: "Button",
* props: { children: "Click me" },
* requiredProps: ["children"],
* primaryRole: "button",
* testCases: {
* renders: true,
* accessibility: true,
* keyboardNavigation: true,
* disabledState: true,
* },
* states: {
* disabledProps: { disabled: true },
* },
* });
*/
export function componentTestSuite<TProps>(
config: ComponentTestSuiteConfig<TProps>,
) {
const {
component: Component,
name,
props,
requiredProps = [],
optionalProps,
primaryRole = "button",
testCases = {
renders: true,
accessibility: true,
keyboardNavigation: true,
disabledState: true,
errorState: false,
},
states = {},
} = config;
describe(`${name} (standard suite)`, () => {
if (testCases.renders) {
it("renders without crashing", () => {
render(<Component {...props} />);
});
}
if (requiredProps.length > 0) {
it("honors required props", () => {
render(<Component {...props} />);
for (const key of requiredProps) {
const value = (props as Record<string, unknown>)[key as string];
expect(
value,
`Expected required prop "${String(key)}" to be defined`,
).toBeDefined();
}
});
}
if (optionalProps) {
it("handles optional props gracefully when omitted", () => {
// Render with all props
render(<Component {...props} />);
// Render again with optional props omitted to ensure no runtime error
const { unmount } = render(
<Component {...({ ...props, ...Object.fromEntries(
Object.keys(optionalProps).map((k) => [k, undefined]),
) } as TProps)} />,
);
// Basic sanity check: component is mounted
// (we don't assert specific DOM for optional props generically)
expect(unmount).toBeDefined();
});
}
if (testCases.accessibility) {
it("has no obvious accessibility violations (axe)", async () => {
const { container } = render(<Component {...props} />);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
}
if (testCases.keyboardNavigation) {
it("supports basic keyboard navigation (Tab + Enter/Space)", async () => {
const user = userEvent.setup();
render(<Component {...props} />);
// Focus the primary interactive element by role
const interactive =
screen.queryByRole(primaryRole as never) ??
// Fallback: first button if specified role is not found
screen.getByRole("button");
interactive.focus();
expect(interactive).toHaveFocus();
// Trigger activation via keyboard
await user.keyboard("{Enter}");
await user.keyboard(" ");
// Still in the document after interaction
expect(interactive).toBeInTheDocument();
});
}
if (testCases.disabledState && states.disabledProps) {
it("handles disabled state correctly", async () => {
const user = userEvent.setup();
render(
<Component
{...({
...props,
...states.disabledProps,
} as TProps)}
/>,
);
const interactive =
screen.queryByRole(primaryRole as never) ??
screen.getByRole("button");
// If the component exposes disabled via attribute, assert it
if ("disabled" in interactive) {
expect(interactive).toHaveAttribute("disabled");
}
// Attempt interaction; should not throw or cause obvious change
await user.click(interactive);
expect(interactive).toBeInTheDocument();
});
}
if (testCases.errorState && states.errorProps) {
it("handles error state without crashing", () => {
// Render with error props applied; no additional assertions to keep this generic
render(
<Component
{...({
...props,
...states.errorProps,
} as TProps)}
/>,
);
});
}
});
}
-83
View File
@@ -1,83 +0,0 @@
import { test, expect } from "@playwright/test";
test.describe("Checkbox Visual Regression Tests", () => {
test("Standard mode - unchecked", async ({ page }) => {
await page.goto("/forms");
await expect(
page.locator('[data-testid="standard-unchecked"]'),
).toBeVisible();
await expect(page).toHaveScreenshot("checkbox-standard-unchecked.png");
});
test("Standard mode - checked", async ({ page }) => {
await page.goto("/forms");
await expect(
page.locator('[data-testid="standard-checked"]'),
).toBeVisible();
await expect(page).toHaveScreenshot("checkbox-standard-checked.png");
});
test("Inverse mode - unchecked", async ({ page }) => {
await page.goto("/forms");
await expect(
page.locator('[data-testid="inverse-unchecked"]'),
).toBeVisible();
await expect(page).toHaveScreenshot("checkbox-inverse-unchecked.png");
});
test("Inverse mode - checked", async ({ page }) => {
await page.goto("/forms");
await expect(page.locator('[data-testid="inverse-checked"]')).toBeVisible();
await expect(page).toHaveScreenshot("checkbox-inverse-checked.png");
});
test("Standard mode - hover state", async ({ page }) => {
await page.goto("/forms");
const checkbox = page.locator('[data-testid="standard-unchecked"]');
await checkbox.hover();
await expect(page).toHaveScreenshot("checkbox-standard-hover.png");
});
test("Standard mode - focus state", async ({ page }) => {
await page.goto("/forms");
const checkbox = page.locator('[data-testid="standard-unchecked"]');
await checkbox.focus();
await expect(page).toHaveScreenshot("checkbox-standard-focus.png");
});
test("Inverse mode - hover state", async ({ page }) => {
await page.goto("/forms");
const checkbox = page.locator('[data-testid="inverse-unchecked"]');
await checkbox.hover();
await expect(page).toHaveScreenshot("checkbox-inverse-hover.png");
});
test("Inverse mode - focus state", async ({ page }) => {
await page.goto("/forms");
const checkbox = page.locator('[data-testid="inverse-unchecked"]');
await checkbox.focus();
await expect(page).toHaveScreenshot("checkbox-inverse-focus.png");
});
test("Disabled state - standard", async ({ page }) => {
await page.goto("/forms");
await expect(
page.locator('[data-testid="standard-disabled"]'),
).toBeVisible();
await expect(page).toHaveScreenshot("checkbox-standard-disabled.png");
});
test("Disabled state - inverse", async ({ page }) => {
await page.goto("/forms");
await expect(
page.locator('[data-testid="inverse-disabled"]'),
).toBeVisible();
await expect(page).toHaveScreenshot("checkbox-inverse-disabled.png");
});
test("All variations grid", async ({ page }) => {
await page.goto("/forms");
await expect(page.locator('[data-testid="checkbox-grid"]')).toBeVisible();
await expect(page).toHaveScreenshot("checkbox-all-variations.png");
});
});
-215
View File
@@ -1,215 +0,0 @@
/**
* Visual Regression Testing Configuration
*
* This file defines the configuration for visual regression testing across
* different breakpoints, components, and scenarios.
*/
// Breakpoint definitions for responsive testing
export const breakpoints = {
// Mobile breakpoints
xs: { width: 320, height: 700, name: "Extra Small" },
sm: { width: 360, height: 700, name: "Small" },
md: { width: 480, height: 700, name: "Medium" },
// Tablet breakpoints
lg: { width: 640, height: 700, name: "Large" },
xl: { width: 768, height: 700, name: "Extra Large" },
// Desktop breakpoints
"2xl": { width: 1024, height: 700, name: "2XL" },
"3xl": { width: 1280, height: 700, name: "3XL" },
"4xl": { width: 1440, height: 700, name: "4XL" },
full: { width: 1920, height: 700, name: "Full HD" },
};
// Key breakpoints for focused testing
export const keyBreakpoints = [
breakpoints.xs, // Mobile
breakpoints.md, // Tablet
breakpoints.xl, // Desktop
];
// Visual testing scenarios
export const visualScenarios = {
// Component states
states: {
default: "Default state",
hover: "Hover state",
focus: "Focus state",
active: "Active/pressed state",
disabled: "Disabled state",
},
// Interactive states
interactions: {
hover: "Element hovered",
focus: "Element focused",
click: "Element clicked",
loading: "Loading state",
error: "Error state",
},
// Content variations
content: {
short: "Short content",
long: "Long content",
empty: "Empty state",
loading: "Loading content",
error: "Error content",
},
// Layout scenarios
layout: {
compact: "Compact layout",
spacious: "Spacious layout",
stacked: "Stacked layout",
grid: "Grid layout",
list: "List layout",
},
};
// Chromatic configuration
export const chromaticConfig = {
// Viewports for Chromatic screenshots
viewports: Object.values(breakpoints).map((bp) => bp.width),
// Delay for layout stabilization
delay: 200,
// Modes for different themes
modes: {
light: {},
dark: {
colorScheme: "dark",
},
},
// Storybook viewport configuration
storybookViewports: Object.entries(breakpoints).reduce((acc, [key, bp]) => {
acc[key] = {
name: bp.name,
styles: {
width: `${bp.width}px`,
height: `${bp.height}px`,
},
};
return acc;
}, {}),
};
// Playwright visual testing configuration
export const playwrightVisualConfig = {
// Screenshot options
screenshot: {
fullPage: false,
type: "png",
quality: 90,
},
// Visual comparison options
visualComparison: {
threshold: 0.1, // 10% difference threshold
maxDiffPixels: 100,
maxDiffPixelRatio: 0.1,
},
// Test timeouts
timeouts: {
navigation: 30000,
action: 5000,
assertion: 10000,
},
};
// Component-specific visual testing configurations
export const componentConfigs = {
Header: {
breakpoints: [breakpoints.xs, breakpoints.md, breakpoints.xl],
states: ["default", "hover", "focus"],
scenarios: ["navigation", "authentication", "responsive"],
},
Footer: {
breakpoints: [breakpoints.xs, breakpoints.md, breakpoints.xl],
states: ["default", "hover", "focus"],
scenarios: ["navigation", "social", "legal"],
},
Button: {
breakpoints: [breakpoints.sm, breakpoints.md, breakpoints.lg],
states: ["default", "hover", "focus", "active", "disabled"],
variants: ["default", "home"],
sizes: ["xsmall", "small", "medium", "large", "xlarge"],
},
Logo: {
breakpoints: [breakpoints.xs, breakpoints.md, breakpoints.xl],
states: ["default", "hover"],
variants: ["with-text", "icon-only"],
},
MenuBar: {
breakpoints: [breakpoints.xs, breakpoints.md, breakpoints.xl],
states: ["default", "hover", "focus"],
scenarios: ["navigation", "dropdown"],
},
};
// Visual regression test patterns
export const testPatterns = {
// Basic component testing
basic: {
description: "Basic component rendering",
steps: [
"Navigate to component",
"Wait for layout stabilization",
"Take screenshot",
],
},
// Interactive state testing
interactive: {
description: "Interactive state testing",
steps: [
"Navigate to component",
"Interact with element (hover/focus/click)",
"Wait for state change",
"Take screenshot",
],
},
// Responsive testing
responsive: {
description: "Responsive behavior testing",
steps: [
"Set viewport size",
"Navigate to component",
"Wait for layout stabilization",
"Take screenshot",
"Repeat for all breakpoints",
],
},
// Content variation testing
contentVariation: {
description: "Content variation testing",
steps: [
"Navigate to component with different content",
"Wait for layout stabilization",
"Take screenshot",
"Compare with baseline",
],
},
};
// Export all configurations
export default {
breakpoints,
keyBreakpoints,
visualScenarios,
chromaticConfig,
playwrightVisualConfig,
componentConfigs,
testPatterns,
};
+6 -5
View File
@@ -25,9 +25,10 @@ export default defineConfig({
environment: "jsdom",
setupFiles: ["./vitest.setup.ts"],
include: [
"tests/unit/**/*.test.{js,jsx,ts,tsx}",
"tests/integration/**/*.test.{js,jsx,ts,tsx}",
"tests/accessibility/**/*.test.{js,jsx,ts,tsx}",
"tests/components/**/*.test.{js,jsx,ts,tsx}",
"tests/pages/**/*.test.{js,jsx,ts,tsx}",
"tests/utils/**/*.test.{js,jsx,ts,tsx}",
"tests/unit/**/*.test.{js,jsx,ts,tsx}", // Legacy - remaining non-component tests
"tests/e2e/**/*.e2e.test.{js,jsx,ts,tsx}",
],
exclude: [
@@ -58,8 +59,8 @@ export default defineConfig({
"**/dist/**",
"**/build/**",
],
thresholds: { lines: 50, functions: 50, statements: 50, branches: 50 },
// Disable coverage collection in CI to prevent test failures
// Global thresholds intentionally removed to prioritize simplicity
// over strict coverage gating. Use reports for guidance instead.
enabled: !(typeof process !== "undefined" && process.env.CI),
},
pool: "threads", // Use threads for better performance