Add more unit tests
This commit is contained in:
@@ -0,0 +1 @@
|
|||||||
|
39731
|
||||||
@@ -0,0 +1,257 @@
|
|||||||
|
# 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
|
||||||
|
|
||||||
|
- **184 total tests** across the project
|
||||||
|
- **176 tests passing** (95.7% success rate)
|
||||||
|
- **8 tests failing** (all related to multiple element instances)
|
||||||
|
- **13 test files** covering all major components
|
||||||
|
|
||||||
|
## 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.
|
||||||
+44
-38
@@ -5,6 +5,35 @@ import Button from "./Button";
|
|||||||
import AvatarContainer from "./AvatarContainer";
|
import AvatarContainer from "./AvatarContainer";
|
||||||
import Avatar from "./Avatar";
|
import Avatar from "./Avatar";
|
||||||
|
|
||||||
|
// Configuration data for testing
|
||||||
|
export const navigationItems = [
|
||||||
|
{ href: "#", text: "Use cases", extraPadding: true },
|
||||||
|
{ href: "#", text: "Learn" },
|
||||||
|
{ href: "#", text: "About" },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const avatarImages = [
|
||||||
|
{ src: "assets/Avatar_1.png", alt: "Avatar 1" },
|
||||||
|
{ src: "assets/Avatar_2.png", alt: "Avatar 2" },
|
||||||
|
{ src: "assets/Avatar_3.png", alt: "Avatar 3" },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const logoConfig = [
|
||||||
|
{ breakpoint: "block sm:hidden", size: "header", showText: false },
|
||||||
|
{ breakpoint: "hidden sm:block md:hidden", size: "header", showText: true },
|
||||||
|
{
|
||||||
|
breakpoint: "hidden md:block lg:hidden",
|
||||||
|
size: "headerMd",
|
||||||
|
showText: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
breakpoint: "hidden lg:block xl:hidden",
|
||||||
|
size: "headerLg",
|
||||||
|
showText: true,
|
||||||
|
},
|
||||||
|
{ breakpoint: "hidden xl:block", size: "headerXl", showText: true },
|
||||||
|
];
|
||||||
|
|
||||||
export default function Header({ onToggle }) {
|
export default function Header({ onToggle }) {
|
||||||
// Schema markup for site navigation
|
// Schema markup for site navigation
|
||||||
const schemaData = {
|
const schemaData = {
|
||||||
@@ -18,33 +47,6 @@ export default function Header({ onToggle }) {
|
|||||||
"query-input": "required name=search_term_string",
|
"query-input": "required name=search_term_string",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
const navigationItems = [
|
|
||||||
{ href: "#", text: "Use cases", extraPadding: true },
|
|
||||||
{ href: "#", text: "Learn" },
|
|
||||||
{ href: "#", text: "About" },
|
|
||||||
];
|
|
||||||
|
|
||||||
const avatarImages = [
|
|
||||||
{ src: "assets/Avatar_1.png", alt: "Avatar 1" },
|
|
||||||
{ src: "assets/Avatar_2.png", alt: "Avatar 2" },
|
|
||||||
{ src: "assets/Avatar_3.png", alt: "Avatar 3" },
|
|
||||||
];
|
|
||||||
|
|
||||||
const logoConfig = [
|
|
||||||
{ breakpoint: "block sm:hidden", size: "header", showText: false },
|
|
||||||
{ breakpoint: "hidden sm:block md:hidden", size: "header", showText: true },
|
|
||||||
{
|
|
||||||
breakpoint: "hidden md:block lg:hidden",
|
|
||||||
size: "headerMd",
|
|
||||||
showText: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
breakpoint: "hidden lg:block xl:hidden",
|
|
||||||
size: "headerLg",
|
|
||||||
showText: true,
|
|
||||||
},
|
|
||||||
{ breakpoint: "hidden xl:block", size: "headerXl", showText: true },
|
|
||||||
];
|
|
||||||
|
|
||||||
const renderNavigationItems = (size) => {
|
const renderNavigationItems = (size) => {
|
||||||
return navigationItems.map((item, index) => (
|
return navigationItems.map((item, index) => (
|
||||||
@@ -118,7 +120,11 @@ export default function Header({ onToggle }) {
|
|||||||
{/* Logo - Consistent left positioning across all breakpoints */}
|
{/* Logo - Consistent left positioning across all breakpoints */}
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
{logoConfig.map((config, index) => (
|
{logoConfig.map((config, index) => (
|
||||||
<div key={index} className={config.breakpoint}>
|
<div
|
||||||
|
key={index}
|
||||||
|
className={config.breakpoint}
|
||||||
|
data-testid="logo-wrapper"
|
||||||
|
>
|
||||||
{renderLogo(config.size, config.showText)}
|
{renderLogo(config.size, config.showText)}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
@@ -127,29 +133,29 @@ export default function Header({ onToggle }) {
|
|||||||
{/* Navigation Links - Consistent center positioning */}
|
{/* Navigation Links - Consistent center positioning */}
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
{/* XSmall breakpoint - Navigation items moved to right section */}
|
{/* XSmall breakpoint - Navigation items moved to right section */}
|
||||||
<div className="block sm:hidden">
|
<div className="block sm:hidden" data-testid="nav-xs">
|
||||||
{/* Empty for XSmall - navigation moved to right */}
|
{/* Empty for XSmall - navigation moved to right */}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Small breakpoint - All items grouped together, centered */}
|
{/* Small breakpoint - All items grouped together, centered */}
|
||||||
<div className="hidden sm:block md:hidden">
|
<div className="hidden sm:block md:hidden" data-testid="nav-sm">
|
||||||
<MenuBar size="default">
|
<MenuBar size="default">
|
||||||
{renderNavigationItems("xsmall")}
|
{renderNavigationItems("xsmall")}
|
||||||
{renderLoginButton("xsmall")}
|
{renderLoginButton("xsmall")}
|
||||||
</MenuBar>
|
</MenuBar>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="hidden md:block lg:hidden">
|
<div className="hidden md:block lg:hidden" data-testid="nav-md">
|
||||||
<MenuBar size="default">
|
<MenuBar size="default">
|
||||||
{renderNavigationItems("xsmall")}
|
{renderNavigationItems("xsmall")}
|
||||||
</MenuBar>
|
</MenuBar>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="hidden lg:block xl:hidden">
|
<div className="hidden lg:block xl:hidden" data-testid="nav-lg">
|
||||||
<MenuBar size="large">{renderNavigationItems("large")}</MenuBar>
|
<MenuBar size="large">{renderNavigationItems("large")}</MenuBar>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="hidden xl:block">
|
<div className="hidden xl:block" data-testid="nav-xl">
|
||||||
<MenuBar size="large">{renderNavigationItems("xlarge")}</MenuBar>
|
<MenuBar size="large">{renderNavigationItems("xlarge")}</MenuBar>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -157,7 +163,7 @@ export default function Header({ onToggle }) {
|
|||||||
{/* Authentication Elements - Consistent right alignment across all breakpoints */}
|
{/* Authentication Elements - Consistent right alignment across all breakpoints */}
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
{/* XSmall breakpoint - All navigation items + Create Rule button */}
|
{/* XSmall breakpoint - All navigation items + Create Rule button */}
|
||||||
<div className="block sm:hidden">
|
<div className="block sm:hidden" data-testid="auth-xs">
|
||||||
<div className="flex items-center gap-[var(--spacing-scale-001)]">
|
<div className="flex items-center gap-[var(--spacing-scale-001)]">
|
||||||
{renderNavigationItems("xsmall")}
|
{renderNavigationItems("xsmall")}
|
||||||
{renderLoginButton("xsmall")}
|
{renderLoginButton("xsmall")}
|
||||||
@@ -166,14 +172,14 @@ export default function Header({ onToggle }) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Small breakpoint - Only Create Rule button */}
|
{/* Small breakpoint - Only Create Rule button */}
|
||||||
<div className="hidden sm:block md:hidden">
|
<div className="hidden sm:block md:hidden" data-testid="auth-sm">
|
||||||
<div className="flex items-center gap-[var(--spacing-scale-004)]">
|
<div className="flex items-center gap-[var(--spacing-scale-004)]">
|
||||||
{renderCreateRuleButton("xsmall", "small", "small")}
|
{renderCreateRuleButton("xsmall", "small", "small")}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Medium breakpoint */}
|
{/* Medium breakpoint */}
|
||||||
<div className="hidden md:block lg:hidden">
|
<div className="hidden md:block lg:hidden" data-testid="auth-md">
|
||||||
<div className="flex items-center gap-[var(--spacing-measures-spacing-010)]">
|
<div className="flex items-center gap-[var(--spacing-measures-spacing-010)]">
|
||||||
{renderLoginButton("xsmall")}
|
{renderLoginButton("xsmall")}
|
||||||
{renderCreateRuleButton("xsmall", "medium", "medium")}
|
{renderCreateRuleButton("xsmall", "medium", "medium")}
|
||||||
@@ -181,7 +187,7 @@ export default function Header({ onToggle }) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Large breakpoint */}
|
{/* Large breakpoint */}
|
||||||
<div className="hidden lg:block xl:hidden">
|
<div className="hidden lg:block xl:hidden" data-testid="auth-lg">
|
||||||
<div className="flex items-center gap-[var(--spacing-measures-spacing-004)]">
|
<div className="flex items-center gap-[var(--spacing-measures-spacing-004)]">
|
||||||
{renderLoginButton("large")}
|
{renderLoginButton("large")}
|
||||||
{renderCreateRuleButton("large", "xlarge", "xlarge")}
|
{renderCreateRuleButton("large", "xlarge", "xlarge")}
|
||||||
@@ -189,7 +195,7 @@ export default function Header({ onToggle }) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* XLarge breakpoint */}
|
{/* XLarge breakpoint */}
|
||||||
<div className="hidden xl:block">
|
<div className="hidden xl:block" data-testid="auth-xl">
|
||||||
<div className="flex items-center gap-[var(--spacing-measures-spacing-004)]">
|
<div className="flex items-center gap-[var(--spacing-measures-spacing-004)]">
|
||||||
{renderLoginButton("xlarge")}
|
{renderLoginButton("xlarge")}
|
||||||
{renderCreateRuleButton("xlarge", "xlarge", "xlarge")}
|
{renderCreateRuleButton("xlarge", "xlarge", "xlarge")}
|
||||||
|
|||||||
@@ -117,7 +117,6 @@ export default function Logo({ size = "default", showText = true }) {
|
|||||||
className={`flex items-center ${config.containerHeight} ${
|
className={`flex items-center ${config.containerHeight} ${
|
||||||
showText ? config.gap : ""
|
showText ? config.gap : ""
|
||||||
} transition-all duration-200 ease-in-out hover:scale-[1.02] cursor-pointer`}
|
} transition-all duration-200 ease-in-out hover:scale-[1.02] cursor-pointer`}
|
||||||
role="banner"
|
|
||||||
aria-label="CommunityRule Logo"
|
aria-label="CommunityRule Logo"
|
||||||
>
|
>
|
||||||
{/* Logo Text - only show if showText is true */}
|
{/* Logo Text - only show if showText is true */}
|
||||||
|
|||||||
@@ -0,0 +1,200 @@
|
|||||||
|
import Header from "../app/components/Header.js";
|
||||||
|
import { within, userEvent } from "@storybook/testing-library";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: "Components/Header/Responsive",
|
||||||
|
component: Header,
|
||||||
|
parameters: {
|
||||||
|
// Chromatic configuration for responsive testing
|
||||||
|
chromatic: {
|
||||||
|
viewports: [360, 640, 768, 1024, 1280],
|
||||||
|
// Capture screenshots at each breakpoint
|
||||||
|
delay: 100, // Small delay to ensure layout is stable
|
||||||
|
},
|
||||||
|
// Storybook viewport configuration
|
||||||
|
viewport: {
|
||||||
|
viewports: {
|
||||||
|
xs: {
|
||||||
|
name: "Extra Small (xs)",
|
||||||
|
styles: {
|
||||||
|
width: "360px",
|
||||||
|
height: "700px",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
sm: {
|
||||||
|
name: "Small (sm)",
|
||||||
|
styles: {
|
||||||
|
width: "640px",
|
||||||
|
height: "700px",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
md: {
|
||||||
|
name: "Medium (md)",
|
||||||
|
styles: {
|
||||||
|
width: "768px",
|
||||||
|
height: "700px",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
lg: {
|
||||||
|
name: "Large (lg)",
|
||||||
|
styles: {
|
||||||
|
width: "1024px",
|
||||||
|
height: "700px",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
xl: {
|
||||||
|
name: "Extra Large (xl)",
|
||||||
|
styles: {
|
||||||
|
width: "1280px",
|
||||||
|
height: "700px",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
argTypes: {
|
||||||
|
onToggle: { action: "toggled" },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Default story - will be captured at all viewports by Chromatic
|
||||||
|
export const Default = {
|
||||||
|
args: {
|
||||||
|
onToggle: () => console.log("Navigation toggled"),
|
||||||
|
},
|
||||||
|
parameters: {
|
||||||
|
docs: {
|
||||||
|
description: {
|
||||||
|
story:
|
||||||
|
"Header component at different breakpoints. Chromatic will capture screenshots at 360px, 640px, 768px, 1024px, and 1280px viewport widths to test responsive behavior.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Story for each breakpoint to make testing easier
|
||||||
|
export const ExtraSmall = {
|
||||||
|
args: {
|
||||||
|
onToggle: () => console.log("Navigation toggled"),
|
||||||
|
},
|
||||||
|
parameters: {
|
||||||
|
viewport: {
|
||||||
|
defaultViewport: "xs",
|
||||||
|
},
|
||||||
|
docs: {
|
||||||
|
description: {
|
||||||
|
story:
|
||||||
|
"Header at extra small breakpoint (360px). Navigation items are moved to the right section.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Small = {
|
||||||
|
args: {
|
||||||
|
onToggle: () => console.log("Navigation toggled"),
|
||||||
|
},
|
||||||
|
parameters: {
|
||||||
|
viewport: {
|
||||||
|
defaultViewport: "sm",
|
||||||
|
},
|
||||||
|
docs: {
|
||||||
|
description: {
|
||||||
|
story:
|
||||||
|
"Header at small breakpoint (640px). All navigation items are grouped together in the center.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Medium = {
|
||||||
|
args: {
|
||||||
|
onToggle: () => console.log("Navigation toggled"),
|
||||||
|
},
|
||||||
|
parameters: {
|
||||||
|
viewport: {
|
||||||
|
defaultViewport: "md",
|
||||||
|
},
|
||||||
|
docs: {
|
||||||
|
description: {
|
||||||
|
story:
|
||||||
|
"Header at medium breakpoint (768px). Navigation items are in the center, login and create rule buttons on the right.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Large = {
|
||||||
|
args: {
|
||||||
|
onToggle: () => console.log("Navigation toggled"),
|
||||||
|
},
|
||||||
|
parameters: {
|
||||||
|
viewport: {
|
||||||
|
defaultViewport: "lg",
|
||||||
|
},
|
||||||
|
docs: {
|
||||||
|
description: {
|
||||||
|
story:
|
||||||
|
"Header at large breakpoint (1024px). Full navigation layout with larger elements.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ExtraLarge = {
|
||||||
|
args: {
|
||||||
|
onToggle: () => console.log("Navigation toggled"),
|
||||||
|
},
|
||||||
|
parameters: {
|
||||||
|
viewport: {
|
||||||
|
defaultViewport: "xl",
|
||||||
|
},
|
||||||
|
docs: {
|
||||||
|
description: {
|
||||||
|
story:
|
||||||
|
"Header at extra large breakpoint (1280px). Maximum size layout with largest elements.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Interactive story for testing user interactions
|
||||||
|
export const Interactive = {
|
||||||
|
args: {
|
||||||
|
onToggle: () => console.log("Navigation toggled"),
|
||||||
|
},
|
||||||
|
parameters: {
|
||||||
|
docs: {
|
||||||
|
description: {
|
||||||
|
story:
|
||||||
|
"Interactive header for testing user interactions. Check the Actions panel to see triggered events.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
play: async ({ canvasElement, step }) => {
|
||||||
|
const canvas = within(canvasElement);
|
||||||
|
|
||||||
|
await step("Click navigation items", async () => {
|
||||||
|
const useCasesLink = canvas.getByRole("link", { name: /use cases/i });
|
||||||
|
await userEvent.click(useCasesLink);
|
||||||
|
|
||||||
|
const learnLink = canvas.getByRole("link", { name: /learn/i });
|
||||||
|
await userEvent.click(learnLink);
|
||||||
|
|
||||||
|
const aboutLink = canvas.getByRole("link", { name: /about/i });
|
||||||
|
await userEvent.click(aboutLink);
|
||||||
|
});
|
||||||
|
|
||||||
|
await step("Click authentication elements", async () => {
|
||||||
|
const loginLink = canvas.getByRole("link", {
|
||||||
|
name: /log in to your account/i,
|
||||||
|
});
|
||||||
|
await userEvent.click(loginLink);
|
||||||
|
|
||||||
|
const createRuleButton = canvas.getByRole("button", {
|
||||||
|
name: /create a new rule with avatar decoration/i,
|
||||||
|
});
|
||||||
|
await userEvent.click(createRuleButton);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,185 @@
|
|||||||
|
import { test, expect } from "@playwright/test";
|
||||||
|
|
||||||
|
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.describe(`Header responsive behavior at ${bp.name} breakpoint`, () => {
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
|
await page.setViewportSize({ width: bp.width, height: bp.height });
|
||||||
|
await page.goto("/");
|
||||||
|
});
|
||||||
|
|
||||||
|
test(`header layout at ${bp.name}`, async ({ page }) => {
|
||||||
|
const nav = page.getByRole("navigation", { name: /main navigation/i });
|
||||||
|
await expect(nav).toBeVisible();
|
||||||
|
|
||||||
|
// Check that header banner is visible
|
||||||
|
const header = page.getByRole("banner", {
|
||||||
|
name: /main navigation header/i,
|
||||||
|
});
|
||||||
|
await expect(header).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test(`navigation items visibility at ${bp.name}`, async ({ page }) => {
|
||||||
|
// All breakpoints should have navigation items
|
||||||
|
await expect(
|
||||||
|
page.getByRole("link", { name: /use cases/i })
|
||||||
|
).toBeVisible();
|
||||||
|
await expect(page.getByRole("link", { name: /learn/i })).toBeVisible();
|
||||||
|
await expect(page.getByRole("link", { name: /about/i })).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test(`authentication elements visibility at ${bp.name}`, async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
// All breakpoints should have login button
|
||||||
|
await expect(
|
||||||
|
page.getByRole("link", { 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(`logo visibility at ${bp.name}`, async ({ page }) => {
|
||||||
|
// Logo should be visible at all breakpoints
|
||||||
|
const logo = page.locator('[data-testid="logo-wrapper"]').first();
|
||||||
|
await expect(logo).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Breakpoint-specific tests
|
||||||
|
if (bp.name === "xs") {
|
||||||
|
test("xs breakpoint specific behavior", async ({ page }) => {
|
||||||
|
// At xs, navigation items should be in the right section
|
||||||
|
const authXs = page.locator('[data-testid="auth-xs"]');
|
||||||
|
await expect(authXs).toBeVisible();
|
||||||
|
|
||||||
|
// Navigation items should be in the auth section at xs
|
||||||
|
const useCasesLink = page.getByRole("link", { name: /use cases/i });
|
||||||
|
await expect(useCasesLink).toBeVisible();
|
||||||
|
|
||||||
|
// Login button should be in the auth section
|
||||||
|
const loginButton = page.getByRole("link", {
|
||||||
|
name: /log in to your account/i,
|
||||||
|
});
|
||||||
|
await expect(loginButton).toBeVisible();
|
||||||
|
|
||||||
|
// Create rule button should be visible
|
||||||
|
const createRuleButton = page.getByRole("button", {
|
||||||
|
name: /create a new rule with avatar decoration/i,
|
||||||
|
});
|
||||||
|
await expect(createRuleButton).toBeVisible();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bp.name === "sm") {
|
||||||
|
test("sm breakpoint specific behavior", async ({ page }) => {
|
||||||
|
// At sm, navigation should be in the center section
|
||||||
|
const navSm = page.locator('[data-testid="nav-sm"]');
|
||||||
|
await expect(navSm).toBeVisible();
|
||||||
|
|
||||||
|
// Auth section should only have create rule button
|
||||||
|
const authSm = page.locator('[data-testid="auth-sm"]');
|
||||||
|
await expect(authSm).toBeVisible();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bp.name === "md") {
|
||||||
|
test("md breakpoint specific behavior", async ({ page }) => {
|
||||||
|
// At md, navigation should be in the center section
|
||||||
|
const navMd = page.locator('[data-testid="nav-md"]');
|
||||||
|
await expect(navMd).toBeVisible();
|
||||||
|
|
||||||
|
// Auth section should have login and create rule button
|
||||||
|
const authMd = page.locator('[data-testid="auth-md"]');
|
||||||
|
await expect(authMd).toBeVisible();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bp.name === "lg") {
|
||||||
|
test("lg breakpoint specific behavior", async ({ page }) => {
|
||||||
|
// At lg, navigation should be in the center section
|
||||||
|
const navLg = page.locator('[data-testid="nav-lg"]');
|
||||||
|
await expect(navLg).toBeVisible();
|
||||||
|
|
||||||
|
// Auth section should have login and create rule button
|
||||||
|
const authLg = page.locator('[data-testid="auth-lg"]');
|
||||||
|
await expect(authLg).toBeVisible();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bp.name === "xl") {
|
||||||
|
test("xl breakpoint specific behavior", async ({ page }) => {
|
||||||
|
// At xl, navigation should be in the center section
|
||||||
|
const navXl = page.locator('[data-testid="nav-xl"]');
|
||||||
|
await expect(navXl).toBeVisible();
|
||||||
|
|
||||||
|
// Auth section should have login and create rule button
|
||||||
|
const authXl = page.locator('[data-testid="auth-xl"]');
|
||||||
|
await expect(authXl).toBeVisible();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Additional responsive behavior tests
|
||||||
|
test.describe("Header responsive behavior", () => {
|
||||||
|
test("header maintains proper layout across breakpoints", async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
// Test that header doesn't break at edge cases
|
||||||
|
const edgeCases = [
|
||||||
|
{ width: 320, height: 700 }, // Very small
|
||||||
|
{ width: 1920, height: 700 }, // Very large
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const viewport of edgeCases) {
|
||||||
|
await page.setViewportSize(viewport);
|
||||||
|
await page.goto("/");
|
||||||
|
|
||||||
|
const header = page.getByRole("banner", {
|
||||||
|
name: /main navigation header/i,
|
||||||
|
});
|
||||||
|
await expect(header).toBeVisible();
|
||||||
|
|
||||||
|
const nav = page.getByRole("navigation", { name: /main navigation/i });
|
||||||
|
await expect(nav).toBeVisible();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("header elements are properly accessible across breakpoints", async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
// Test accessibility at different breakpoints
|
||||||
|
for (const bp of breakpoints) {
|
||||||
|
await page.setViewportSize({ width: bp.width, height: bp.height });
|
||||||
|
await page.goto("/");
|
||||||
|
|
||||||
|
// Check that all interactive elements are accessible
|
||||||
|
const interactiveElements = [
|
||||||
|
page.getByRole("link", { name: /use cases/i }),
|
||||||
|
page.getByRole("link", { name: /learn/i }),
|
||||||
|
page.getByRole("link", { name: /about/i }),
|
||||||
|
page.getByRole("link", { name: /log in to your account/i }),
|
||||||
|
page.getByRole("button", {
|
||||||
|
name: /create a new rule with avatar decoration/i,
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const element of interactiveElements) {
|
||||||
|
await expect(element).toBeVisible();
|
||||||
|
await expect(element).toBeEnabled();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,253 @@
|
|||||||
|
import { describe, test, expect } from "vitest";
|
||||||
|
import { render, screen } from "@testing-library/react";
|
||||||
|
import userEvent from "@testing-library/user-event";
|
||||||
|
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 />);
|
||||||
|
|
||||||
|
const links = screen.getAllByRole("link");
|
||||||
|
|
||||||
|
links.forEach((link) => {
|
||||||
|
expect(link).toHaveClass("focus:outline-none");
|
||||||
|
expect(link).toHaveClass("focus:ring-2");
|
||||||
|
expect(link).toHaveClass("focus:ring-offset-2");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,344 @@
|
|||||||
|
import { describe, test, expect, vi, beforeEach } from "vitest";
|
||||||
|
import { render, screen } from "@testing-library/react";
|
||||||
|
import userEvent from "@testing-library/user-event";
|
||||||
|
import Header, {
|
||||||
|
navigationItems,
|
||||||
|
avatarImages,
|
||||||
|
logoConfig,
|
||||||
|
} from "../../app/components/Header.js";
|
||||||
|
|
||||||
|
describe("Header", () => {
|
||||||
|
const mockOnToggle = vi.fn();
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
mockOnToggle.mockClear();
|
||||||
|
// 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 onToggle={mockOnToggle} />);
|
||||||
|
|
||||||
|
// 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 onToggle={mockOnToggle} />);
|
||||||
|
|
||||||
|
// 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 onToggle={mockOnToggle} />);
|
||||||
|
|
||||||
|
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: "#",
|
||||||
|
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 onToggle={mockOnToggle} />);
|
||||||
|
|
||||||
|
const logoWrappers = screen.getAllByTestId("logo-wrapper");
|
||||||
|
expect(logoWrappers).toHaveLength(logoConfig.length);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("logo wrappers include expected breakpoint classes", () => {
|
||||||
|
render(<Header onToggle={mockOnToggle} />);
|
||||||
|
|
||||||
|
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 onToggle={mockOnToggle} />);
|
||||||
|
|
||||||
|
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 onToggle={mockOnToggle} />);
|
||||||
|
|
||||||
|
// 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 onToggle={mockOnToggle} />);
|
||||||
|
|
||||||
|
// 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 onToggle={mockOnToggle} />);
|
||||||
|
|
||||||
|
// 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 onToggle={mockOnToggle} />);
|
||||||
|
|
||||||
|
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 onToggle={mockOnToggle} />);
|
||||||
|
|
||||||
|
// 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 onToggle={mockOnToggle} />);
|
||||||
|
|
||||||
|
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 onToggle={mockOnToggle} />);
|
||||||
|
|
||||||
|
// 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 onToggle={mockOnToggle} />);
|
||||||
|
|
||||||
|
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 onToggle={mockOnToggle} />);
|
||||||
|
|
||||||
|
// 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 onToggle={mockOnToggle} />);
|
||||||
|
|
||||||
|
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("User Interactions", () => {
|
||||||
|
test("calls onToggle when navigation items are clicked", async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
render(<Header onToggle={mockOnToggle} />);
|
||||||
|
|
||||||
|
// Find and click a navigation item - use menuitem role since they're in a menubar
|
||||||
|
const useCasesLinks = screen.getAllByRole("menuitem", {
|
||||||
|
name: "Navigate to Use cases page",
|
||||||
|
});
|
||||||
|
expect(useCasesLinks.length).toBeGreaterThan(0);
|
||||||
|
const useCasesLink = useCasesLinks[0];
|
||||||
|
await user.click(useCasesLink);
|
||||||
|
|
||||||
|
expect(mockOnToggle).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("CSS Classes and Styling", () => {
|
||||||
|
test("has correct CSS classes for styling", () => {
|
||||||
|
const { container } = render(<Header onToggle={mockOnToggle} />);
|
||||||
|
|
||||||
|
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");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,183 @@
|
|||||||
|
import { describe, test, expect, vi } from "vitest";
|
||||||
|
import { render, screen } from "@testing-library/react";
|
||||||
|
import RootLayout from "../../app/layout";
|
||||||
|
|
||||||
|
// Mock the font imports since they're Next.js specific
|
||||||
|
vi.mock("next/font/google", () => ({
|
||||||
|
Inter: vi.fn(() => ({
|
||||||
|
variable: "--font-inter",
|
||||||
|
style: { fontFamily: "Inter" },
|
||||||
|
})),
|
||||||
|
Bricolage_Grotesque: vi.fn(() => ({
|
||||||
|
variable: "--font-bricolage-grotesque",
|
||||||
|
style: { fontFamily: "Bricolage Grotesque" },
|
||||||
|
})),
|
||||||
|
Space_Grotesk: vi.fn(() => ({
|
||||||
|
variable: "--font-space-grotesk",
|
||||||
|
style: { fontFamily: "Space Grotesk" },
|
||||||
|
})),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe("RootLayout", () => {
|
||||||
|
test("renders HTML structure with correct attributes", () => {
|
||||||
|
render(
|
||||||
|
<RootLayout>
|
||||||
|
<div>Test content</div>
|
||||||
|
</RootLayout>
|
||||||
|
);
|
||||||
|
|
||||||
|
const html = document.querySelector("html");
|
||||||
|
expect(html).toBeInTheDocument();
|
||||||
|
expect(html).toHaveAttribute("lang", "en");
|
||||||
|
expect(html).toHaveClass("font-sans");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("renders body with font variables", () => {
|
||||||
|
render(
|
||||||
|
<RootLayout>
|
||||||
|
<div>Test content</div>
|
||||||
|
</RootLayout>
|
||||||
|
);
|
||||||
|
|
||||||
|
const body = document.querySelector("body");
|
||||||
|
expect(body).toBeInTheDocument();
|
||||||
|
expect(body).toHaveClass("--font-inter");
|
||||||
|
expect(body).toHaveClass("--font-bricolage-grotesque");
|
||||||
|
expect(body).toHaveClass("--font-space-grotesk");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("renders main layout structure", () => {
|
||||||
|
render(
|
||||||
|
<RootLayout>
|
||||||
|
<div>Test content</div>
|
||||||
|
</RootLayout>
|
||||||
|
);
|
||||||
|
|
||||||
|
const mainContainer = document.querySelector(".min-h-screen.flex.flex-col");
|
||||||
|
expect(mainContainer).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("renders HomeHeader component", () => {
|
||||||
|
render(
|
||||||
|
<RootLayout>
|
||||||
|
<div>Test content</div>
|
||||||
|
</RootLayout>
|
||||||
|
);
|
||||||
|
|
||||||
|
// The HomeHeader component should be rendered
|
||||||
|
// We can check for its presence by looking for elements that would be in the header
|
||||||
|
const header = document.querySelector("header");
|
||||||
|
expect(header).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("renders main content area", () => {
|
||||||
|
render(
|
||||||
|
<RootLayout>
|
||||||
|
<div>Test content</div>
|
||||||
|
</RootLayout>
|
||||||
|
);
|
||||||
|
|
||||||
|
const main = document.querySelector("main");
|
||||||
|
expect(main).toBeInTheDocument();
|
||||||
|
expect(main).toHaveClass("flex-1");
|
||||||
|
expect(main).toHaveTextContent("Test content");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("renders Footer component", () => {
|
||||||
|
render(
|
||||||
|
<RootLayout>
|
||||||
|
<div>Test content</div>
|
||||||
|
</RootLayout>
|
||||||
|
);
|
||||||
|
|
||||||
|
// The Footer component should be rendered
|
||||||
|
const footer = document.querySelector("footer");
|
||||||
|
expect(footer).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("renders children content correctly", () => {
|
||||||
|
const testContent = "This is test content";
|
||||||
|
render(
|
||||||
|
<RootLayout>
|
||||||
|
<div>{testContent}</div>
|
||||||
|
</RootLayout>
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.getByText(testContent)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("has correct CSS classes for layout structure", () => {
|
||||||
|
render(
|
||||||
|
<RootLayout>
|
||||||
|
<div>Test content</div>
|
||||||
|
</RootLayout>
|
||||||
|
);
|
||||||
|
|
||||||
|
const mainContainer = document.querySelector(".min-h-screen.flex.flex-col");
|
||||||
|
expect(mainContainer).toBeInTheDocument();
|
||||||
|
expect(mainContainer).toHaveClass("min-h-screen");
|
||||||
|
expect(mainContainer).toHaveClass("flex");
|
||||||
|
expect(mainContainer).toHaveClass("flex-col");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("main element has correct flex properties", () => {
|
||||||
|
render(
|
||||||
|
<RootLayout>
|
||||||
|
<div>Test content</div>
|
||||||
|
</RootLayout>
|
||||||
|
);
|
||||||
|
|
||||||
|
const main = document.querySelector("main");
|
||||||
|
expect(main).toHaveClass("flex-1");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("renders complete page structure", () => {
|
||||||
|
render(
|
||||||
|
<RootLayout>
|
||||||
|
<div>Test content</div>
|
||||||
|
</RootLayout>
|
||||||
|
);
|
||||||
|
|
||||||
|
// Check for all major structural elements
|
||||||
|
expect(document.querySelector("html")).toBeInTheDocument();
|
||||||
|
expect(document.querySelector("body")).toBeInTheDocument();
|
||||||
|
expect(document.querySelector("header")).toBeInTheDocument();
|
||||||
|
expect(document.querySelector("main")).toBeInTheDocument();
|
||||||
|
expect(document.querySelector("footer")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("maintains proper document structure", () => {
|
||||||
|
render(
|
||||||
|
<RootLayout>
|
||||||
|
<div>Test content</div>
|
||||||
|
</RootLayout>
|
||||||
|
);
|
||||||
|
|
||||||
|
// Check that the document has proper structure
|
||||||
|
const html = document.querySelector("html");
|
||||||
|
const body = html.querySelector("body");
|
||||||
|
const header = body.querySelector("header");
|
||||||
|
const main = body.querySelector("main");
|
||||||
|
const footer = body.querySelector("footer");
|
||||||
|
|
||||||
|
expect(html).toBeInTheDocument();
|
||||||
|
expect(body).toBeInTheDocument();
|
||||||
|
expect(header).toBeInTheDocument();
|
||||||
|
expect(main).toBeInTheDocument();
|
||||||
|
expect(footer).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("renders multiple children correctly", () => {
|
||||||
|
render(
|
||||||
|
<RootLayout>
|
||||||
|
<div>First child</div>
|
||||||
|
<div>Second child</div>
|
||||||
|
<div>Third child</div>
|
||||||
|
</RootLayout>
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.getByText("First child")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("Second child")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("Third child")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,248 @@
|
|||||||
|
import { describe, test, expect } from "vitest";
|
||||||
|
import { render, screen } from "@testing-library/react";
|
||||||
|
import Page from "../../app/page";
|
||||||
|
|
||||||
|
describe("Page", () => {
|
||||||
|
test("renders all main sections", () => {
|
||||||
|
render(<Page />);
|
||||||
|
|
||||||
|
// Check that all main sections are rendered (using getAllByText since there are multiple instances)
|
||||||
|
expect(screen.getAllByText("Collaborate").length).toBeGreaterThan(0);
|
||||||
|
expect(screen.getAllByText("with clarity").length).toBeGreaterThan(0);
|
||||||
|
expect(
|
||||||
|
screen.getAllByText(
|
||||||
|
"Help your community make important decisions in a way that reflects its unique values."
|
||||||
|
).length
|
||||||
|
).toBeGreaterThan(0);
|
||||||
|
|
||||||
|
// Check numbered cards section (using getAllByText since there are multiple instances)
|
||||||
|
expect(
|
||||||
|
screen.getAllByText("How CommunityRule works").length
|
||||||
|
).toBeGreaterThan(0);
|
||||||
|
expect(
|
||||||
|
screen.getAllByText(
|
||||||
|
"Here's a quick overview of the process, from start to finish."
|
||||||
|
).length
|
||||||
|
).toBeGreaterThan(0);
|
||||||
|
|
||||||
|
// Check feature grid section (using getAllByText since there are multiple instances)
|
||||||
|
expect(
|
||||||
|
screen.getAllByText("We've got your back, every step of the way").length
|
||||||
|
).toBeGreaterThan(0);
|
||||||
|
expect(
|
||||||
|
screen.getAllByText(
|
||||||
|
"Use our toolkit to improve, document, and evolve your organization."
|
||||||
|
).length
|
||||||
|
).toBeGreaterThan(0);
|
||||||
|
|
||||||
|
// Check ask organizer section (using getAllByText since there are multiple instances)
|
||||||
|
expect(screen.getAllByText("Still have questions?").length).toBeGreaterThan(
|
||||||
|
0
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
screen.getAllByText("Get answers from an experienced organizer").length
|
||||||
|
).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("renders hero banner with correct data", () => {
|
||||||
|
render(<Page />);
|
||||||
|
|
||||||
|
// Check hero banner content (using getAllByText since there are multiple instances)
|
||||||
|
expect(screen.getAllByText("Collaborate").length).toBeGreaterThan(0);
|
||||||
|
expect(screen.getAllByText("with clarity").length).toBeGreaterThan(0);
|
||||||
|
expect(
|
||||||
|
screen.getAllByText(
|
||||||
|
"Help your community make important decisions in a way that reflects its unique values."
|
||||||
|
).length
|
||||||
|
).toBeGreaterThan(0);
|
||||||
|
expect(
|
||||||
|
screen.getAllByText("Learn how CommunityRule works").length
|
||||||
|
).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("renders numbered cards with correct data", () => {
|
||||||
|
render(<Page />);
|
||||||
|
|
||||||
|
// Check numbered cards content (using getAllByText since there are multiple instances)
|
||||||
|
expect(
|
||||||
|
screen.getAllByText("How CommunityRule works").length
|
||||||
|
).toBeGreaterThan(0);
|
||||||
|
expect(
|
||||||
|
screen.getAllByText(
|
||||||
|
"Here's a quick overview of the process, from start to finish."
|
||||||
|
).length
|
||||||
|
).toBeGreaterThan(0);
|
||||||
|
|
||||||
|
// Check individual card content (using getAllByText since there are multiple instances)
|
||||||
|
expect(
|
||||||
|
screen.getAllByText("Document how your community makes decisions").length
|
||||||
|
).toBeGreaterThan(0);
|
||||||
|
expect(
|
||||||
|
screen.getAllByText(
|
||||||
|
"Build an operating manual for a successful community"
|
||||||
|
).length
|
||||||
|
).toBeGreaterThan(0);
|
||||||
|
expect(
|
||||||
|
screen.getAllByText(
|
||||||
|
"Get a link to your manual for your group to review and evolve"
|
||||||
|
).length
|
||||||
|
).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("renders feature grid with correct data", () => {
|
||||||
|
render(<Page />);
|
||||||
|
|
||||||
|
// Check feature grid content (using getAllByText since there are multiple instances)
|
||||||
|
expect(
|
||||||
|
screen.getAllByText("We've got your back, every step of the way").length
|
||||||
|
).toBeGreaterThan(0);
|
||||||
|
expect(
|
||||||
|
screen.getAllByText(
|
||||||
|
"Use our toolkit to improve, document, and evolve your organization."
|
||||||
|
).length
|
||||||
|
).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("renders ask organizer section with correct data", () => {
|
||||||
|
render(<Page />);
|
||||||
|
|
||||||
|
// Check ask organizer content (using getAllByText since there are multiple instances)
|
||||||
|
expect(screen.getAllByText("Still have questions?").length).toBeGreaterThan(
|
||||||
|
0
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
screen.getAllByText("Get answers from an experienced organizer").length
|
||||||
|
).toBeGreaterThan(0);
|
||||||
|
expect(screen.getAllByText("Ask an organizer").length).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("renders all component sections", () => {
|
||||||
|
render(<Page />);
|
||||||
|
|
||||||
|
// Check that all major components are present by looking for their content
|
||||||
|
// HeroBanner
|
||||||
|
expect(screen.getAllByText("Collaborate").length).toBeGreaterThan(0);
|
||||||
|
|
||||||
|
// LogoWall - should be present (even if just the component structure)
|
||||||
|
// NumberedCards
|
||||||
|
expect(
|
||||||
|
screen.getAllByText("How CommunityRule works").length
|
||||||
|
).toBeGreaterThan(0);
|
||||||
|
|
||||||
|
// RuleStack - should be present
|
||||||
|
// FeatureGrid
|
||||||
|
expect(
|
||||||
|
screen.getAllByText("We've got your back, every step of the way").length
|
||||||
|
).toBeGreaterThan(0);
|
||||||
|
|
||||||
|
// QuoteBlock - should be present
|
||||||
|
// AskOrganizer
|
||||||
|
expect(screen.getAllByText("Still have questions?").length).toBeGreaterThan(
|
||||||
|
0
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("has correct page structure", () => {
|
||||||
|
render(<Page />);
|
||||||
|
|
||||||
|
const mainContainer = screen.getAllByText("Collaborate")[0].closest("div");
|
||||||
|
expect(mainContainer).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("renders call-to-action elements", () => {
|
||||||
|
render(<Page />);
|
||||||
|
|
||||||
|
// Check CTA button in hero banner
|
||||||
|
expect(
|
||||||
|
screen.getAllByText("Learn how CommunityRule works").length
|
||||||
|
).toBeGreaterThan(0);
|
||||||
|
|
||||||
|
// Check CTA button in ask organizer section
|
||||||
|
expect(screen.getAllByText("Ask an organizer").length).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("renders descriptive text content", () => {
|
||||||
|
render(<Page />);
|
||||||
|
|
||||||
|
// Check main description (using getAllByText since there are multiple instances)
|
||||||
|
expect(
|
||||||
|
screen.getAllByText(
|
||||||
|
"Help your community make important decisions in a way that reflects its unique values."
|
||||||
|
).length
|
||||||
|
).toBeGreaterThan(0);
|
||||||
|
|
||||||
|
// Check numbered cards description (using getAllByText since there are multiple instances)
|
||||||
|
expect(
|
||||||
|
screen.getAllByText(
|
||||||
|
"Here's a quick overview of the process, from start to finish."
|
||||||
|
).length
|
||||||
|
).toBeGreaterThan(0);
|
||||||
|
|
||||||
|
// Check feature grid description (using getAllByText since there are multiple instances)
|
||||||
|
expect(
|
||||||
|
screen.getAllByText(
|
||||||
|
"Use our toolkit to improve, document, and evolve your organization."
|
||||||
|
).length
|
||||||
|
).toBeGreaterThan(0);
|
||||||
|
|
||||||
|
// Check ask organizer description (using getAllByText since there are multiple instances)
|
||||||
|
expect(
|
||||||
|
screen.getAllByText("Get answers from an experienced organizer").length
|
||||||
|
).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("renders section titles correctly", () => {
|
||||||
|
render(<Page />);
|
||||||
|
|
||||||
|
// Check all section titles (using getAllByText since there are multiple instances)
|
||||||
|
expect(screen.getAllByText("Collaborate").length).toBeGreaterThan(0);
|
||||||
|
expect(
|
||||||
|
screen.getAllByText("How CommunityRule works").length
|
||||||
|
).toBeGreaterThan(0);
|
||||||
|
expect(
|
||||||
|
screen.getAllByText("We've got your back, every step of the way").length
|
||||||
|
).toBeGreaterThan(0);
|
||||||
|
expect(screen.getAllByText("Still have questions?").length).toBeGreaterThan(
|
||||||
|
0
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("renders numbered card items with correct content", () => {
|
||||||
|
render(<Page />);
|
||||||
|
|
||||||
|
// Check all three numbered card items (using getAllByText since there are multiple instances)
|
||||||
|
expect(
|
||||||
|
screen.getAllByText("Document how your community makes decisions").length
|
||||||
|
).toBeGreaterThan(0);
|
||||||
|
expect(
|
||||||
|
screen.getAllByText(
|
||||||
|
"Build an operating manual for a successful community"
|
||||||
|
).length
|
||||||
|
).toBeGreaterThan(0);
|
||||||
|
expect(
|
||||||
|
screen.getAllByText(
|
||||||
|
"Get a link to your manual for your group to review and evolve"
|
||||||
|
).length
|
||||||
|
).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("renders subtitle content correctly", () => {
|
||||||
|
render(<Page />);
|
||||||
|
|
||||||
|
// Check subtitles (using getAllByText since there are multiple instances)
|
||||||
|
expect(screen.getAllByText("with clarity").length).toBeGreaterThan(0);
|
||||||
|
expect(
|
||||||
|
screen.getAllByText(
|
||||||
|
"Here's a quick overview of the process, from start to finish."
|
||||||
|
).length
|
||||||
|
).toBeGreaterThan(0);
|
||||||
|
expect(
|
||||||
|
screen.getAllByText(
|
||||||
|
"Use our toolkit to improve, document, and evolve your organization."
|
||||||
|
).length
|
||||||
|
).toBeGreaterThan(0);
|
||||||
|
expect(
|
||||||
|
screen.getAllByText("Get answers from an experienced organizer").length
|
||||||
|
).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
import { describe, it, expect } from "vitest";
|
|
||||||
import React from "react";
|
|
||||||
|
|
||||||
function Thing() {
|
|
||||||
return <div>ok</div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
describe("jsx in .js", () => {
|
|
||||||
it("parses", () => {
|
|
||||||
expect(Thing).toBeTypeOf("function");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
import { describe, test, expect } from 'vitest';
|
|
||||||
|
|
||||||
describe('Simple Test', () => {
|
|
||||||
test('should work', () => {
|
|
||||||
expect(1 + 1).toBe(2);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
Reference in New Issue
Block a user