Simplify and standardize testing structure
This commit is contained in:
@@ -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, component‑first 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/
|
||||
|
||||
@@ -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 end‑to‑end 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.
|
||||
- Smoke‑tests 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 **non‑trivial 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 1–3 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 cross‑browser 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.
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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)**
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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)]",
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
@@ -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})?$/);
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 },
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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 },
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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 },
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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 },
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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 },
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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 },
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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 },
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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 },
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
},
|
||||
};
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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)]");
|
||||
});
|
||||
});
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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)]",
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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"));
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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)]",
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -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
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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)]",
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -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)]");
|
||||
});
|
||||
});
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
@@ -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)}
|
||||
/>,
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user