+ );
+}
+```
+
+---
+
+## Theming Coexistence
+
+### Token Bridge
+
+Map WaysNX tokens to your existing system:
+
+```css
+:root {
+ /* Your tokens */
+ --app-primary: #007bff;
+ --app-spacing: 16px;
+
+ /* Bridge WaysNX to your tokens */
+ --wx-color-primary: var(--app-primary);
+ --wx-spacing-md: var(--app-spacing);
+}
+```
+
+**Benefit:** WaysNX components adopt your brand automatically.
+
+### Two-Theme Setup
+
+Explicit scoping for each design system:
+
+```css
+/* Scope legacy tokens */
+.legacy-header, .legacy-main {
+ --color-primary: #007bff;
+}
+
+/* Scope WaysNX tokens */
+.waysnx-scope {
+ --wx-color-primary: #f19924;
+}
+```
+
+---
+
+## Common Integration Pitfalls
+
+### ❌ Global CSS Reset Conflicts
+
+**Problem:** WaysNX and your app both apply normalize/reset stylesheets, causing unpredictable rendering.
+
+**Solution:** Import only one global reset. If both are needed, audit for conflicts and merge manually.
+
+```css
+/* Pick one: */
+@import 'your-reset.css'; /* OR */
+@import '@waysnx/ui-core/dist/reset.css';
+```
+
+### ❌ Navigation Replacement Breaks Layout
+
+**Problem:** Replacing your existing `` with `` changes layout structure, breaking page flow.
+
+**Solution:** Keep your header initially; add Sidebar alongside it. Refactor layout in a dedicated sprint.
+
+```tsx
+// Phase 1: Add sidebar, keep header
+
+
+
+ {/* New */}
+ {children}
+
+
+
+// Phase 2: Remove header after layout refactor
+
+ {/* Header content moved here */}
+ {children}
+
+```
+
+### ❌ TypeScript Errors from Duplicate Dependencies
+
+**Problem:** Your app and WaysNX both depend on `react` 19, but npm/yarn installs two copies, causing type mismatches.
+
+**Solution:** Use peer dependencies and ensure single React installation:
+
+```bash
+# Check for duplicate React
+npm ls react
+
+# Deduplicate
+npm dedupe
+# or
+pnpm dedupe
+```
+
+### ❌ Dark Mode Fighting
+
+**Problem:** Your app and WaysNX both implement dark mode via `prefers-color-scheme`, but with different token values.
+
+**Solution:** Disable one dark mode implementation. Either:
+
+1. Use WaysNX dark mode exclusively (override your tokens with WaysNX values in dark mode)
+2. Use your dark mode exclusively (override `--wx-*` tokens with your dark values)
+
+```css
+/* Option 1: Use WaysNX dark mode */
+@media (prefers-color-scheme: dark) {
+ :root {
+ --app-bg: var(--wx-color-surface); /* Inherit from WaysNX */
+ }
+}
+
+/* Option 2: Override WaysNX tokens */
+@media (prefers-color-scheme: dark) {
+ :root {
+ --wx-color-surface: var(--app-bg); /* Force WaysNX to use your tokens */
+ }
+}
+```
+
+### ❌ Z-Index Conflicts
+
+**Problem:** Modals, dropdowns, and tooltips from both systems compete for stacking order.
+
+**Solution:** Establish a z-index scale and map both systems to it:
+
+```css
+:root {
+ /* Your z-index scale */
+ --z-dropdown: 1000;
+ --z-modal: 2000;
+ --z-tooltip: 3000;
+
+ /* Map WaysNX components */
+ --wx-z-dropdown: var(--z-dropdown);
+ --wx-z-modal: var(--z-modal);
+ --wx-z-tooltip: var(--z-tooltip);
+}
+```
+
+---
+
+## Testing During Migration
+
+### Visual Regression Testing
+
+Capture before/after screenshots to detect unintended style changes:
+
+```bash
+# Baseline: before WaysNX
+npm run test:visual:baseline
+
+# Install WaysNX
+pnpm add @waysnx/ui-kit
+
+# Compare
+npm run test:visual:compare
+```
+
+Tools: Percy, Chromatic, BackstopJS, or Playwright screenshots.
+
+### Isolated Component Testing
+
+Test WaysNX components in isolation before integrating:
+
+```tsx
+// storybook/WaysNXButton.stories.tsx
+import { Button } from '@waysnx/ui-core';
+
+export default {
+ title: 'Migration/WaysNX/Button',
+ component: Button,
+};
+
+export const Primary = () => ;
+```
+
+Validate:
+- Matches design mockups
+- Works with your theme tokens
+- Keyboard navigation
+- Screen reader labels
+
+### Integration Smoke Tests
+
+After adding WaysNX to a route, verify:
+
+1. **Layout integrity** — page structure unchanged
+2. **No console errors** — check browser DevTools
+3. **Click paths work** — critical user flows still function
+4. **Responsive behavior** — mobile/tablet viewports render correctly
+
+---
+
+## Performance Considerations
+
+### Bundle Size Impact
+
+WaysNX UI Kit is modular. Import only what you need:
+
+```tsx
+// ❌ Imports entire aggregate (large bundle)
+import { Button, Sidebar, Menu } from '@waysnx/ui-kit';
+
+// ✅ Import specific libraries (smaller bundle)
+import { Button } from '@waysnx/ui-core';
+import { Sidebar, Menu } from '@waysnx/ui-navigation';
+```
+
+**Tip:** Use Webpack Bundle Analyzer or Vite's `rollup-plugin-visualizer` to audit bundle size before/after.
+
+### CSS Loading Strategy
+
+Avoid loading all WaysNX CSS upfront. Use route-based code splitting:
+
+```tsx
+// Lazy-load WaysNX CSS when user navigates to admin panel
+const AdminPanel = lazy(async () => {
+ await import('@waysnx/ui-navigation/dist/styles.css');
+ return import('./AdminPanel');
+});
+```
+
+---
+
+## Rollback Strategy
+
+Plan for rollback if integration causes critical issues:
+
+1. **Feature flag** — gate WaysNX components behind a flag:
+ ```tsx
+ {featureFlags.useWaysNX ? : }
+ ```
+
+2. **Git branch strategy** — keep WaysNX work in a feature branch until validated in staging.
+
+3. **Canary deployment** — roll out to 5% of users first, monitor errors, expand gradually.
+
+---
+
+## When to Avoid WaysNX UI Kit
+
+**Do not integrate** if:
+
+- Your app ships in 2 weeks and design is locked
+- You have zero tolerance for visual regressions
+- Your team lacks capacity for testing and iteration
+- Your existing design system is mature, well-tested, and meets all needs
+
+**Do integrate** if:
+
+- You're starting a greenfield project
+- Your current UI library is unmaintained
+- You need components your system lacks (e.g., `Sidebar`, `Breadcrumb`, `Chart`)
+- You're consolidating multiple apps under a shared design system
+
+---
+
+## Getting Help
+
+- **Documentation:** https://uikit.waysnx.tech
+- **Examples:** See `examples/waysnx-admin-demo/` in the repository
+- **Issues:** Open issues on the WaysNX UI Kit GitHub repository with:
+ - Your integration approach (CSS layers, namespacing, etc.)
+ - Minimal reproduction of the conflict
+ - Screenshots or video of unexpected behavior
+
+---
+
+**Last Updated:** October 2026
+**Applies to:** WaysNX UI Kit v1.0.0+
diff --git a/docs/theming/TOKENS.md b/docs/theming/TOKENS.md
new file mode 100644
index 0000000..5614e62
--- /dev/null
+++ b/docs/theming/TOKENS.md
@@ -0,0 +1,506 @@
+# Design Tokens Reference
+
+WaysNX UI Kit uses CSS custom properties (CSS variables) as design tokens to provide consistent theming across all components.
+
+All tokens are prefixed with `--wx-` to avoid naming collisions with existing application styles.
+
+---
+
+## Color Tokens (20 total)
+
+### Brand Colors (4 tokens)
+
+Primary color used for buttons, links, and interactive elements:
+
+| Token | Value | Usage |
+|-------|-------|-------|
+| `--wx-color-primary` | `#f19924` | Primary brand color |
+| `--wx-color-primary-hover` | `#e08916` | Hover state for primary elements |
+| `--wx-color-primary-contrast` | `#ffffff` | Text on primary backgrounds |
+| `--wx-color-primary-light` | `#fff5e6` | Subtle primary backgrounds |
+
+**Example:**
+```css
+.my-button {
+ background-color: var(--wx-color-primary);
+ color: var(--wx-color-primary-contrast);
+}
+
+.my-button:hover {
+ background-color: var(--wx-color-primary-hover);
+}
+```
+
+### Text Colors (3 tokens)
+
+| Token | Value | Usage |
+|-------|-------|-------|
+| `--wx-color-text` | `#1e293b` | Primary body text |
+| `--wx-color-text-muted` | `#64748b` | Secondary text, labels |
+| `--wx-color-text-light` | `#94a3b8` | Tertiary text, placeholders |
+
+**Example:**
+```css
+.my-label {
+ color: var(--wx-color-text-muted);
+}
+
+.my-placeholder {
+ color: var(--wx-color-text-light);
+}
+```
+
+### Surface Colors (3 tokens)
+
+Background colors for cards, panels, and surfaces:
+
+| Token | Value | Usage |
+|-------|-------|-------|
+| `--wx-color-surface` | `#ffffff` | Card backgrounds, panels |
+| `--wx-color-surface-alt` | `#f8fafc` | Alternate surface (zebra stripes, disabled inputs) |
+| `--wx-color-surface-hover` | `#f1f5f9` | Hover state for interactive surfaces |
+
+**Example:**
+```css
+.my-card {
+ background-color: var(--wx-color-surface);
+ border: 1px solid var(--wx-color-border);
+}
+
+.my-card:hover {
+ background-color: var(--wx-color-surface-hover);
+}
+```
+
+### Border Colors (1 token)
+
+| Token | Value | Usage |
+|-------|-------|-------|
+| `--wx-color-border` | `#e2e8f0` | Default border color |
+
+### Status Colors (9 tokens)
+
+Colors for feedback states (error, success, warning, info):
+
+| Token | Value | Usage |
+|-------|-------|-------|
+| `--wx-color-error` | `#ef4444` | Error states, destructive actions |
+| `--wx-color-error-hover` | `#b91c1c` | Error hover state |
+| `--wx-color-error-light` | `#fef2f2` | Error background (alerts, badges) |
+| `--wx-color-success` | `#22c55e` | Success states, confirmations |
+| `--wx-color-success-light` | `#f0fdf4` | Success background |
+| `--wx-color-warning` | `#f59e0b` | Warning states |
+| `--wx-color-info` | `#3b82f6` | Informational states |
+| `--wx-color-destructive` | `#dc2626` | Alias for error (used in buttons) |
+| `--wx-color-destructive-hover` | `#b91c1c` | Destructive hover state |
+
+**Example:**
+```css
+.alert--error {
+ background-color: var(--wx-color-error-light);
+ color: var(--wx-color-error);
+ border-left: 4px solid var(--wx-color-error);
+}
+
+.alert--success {
+ background-color: var(--wx-color-success-light);
+ color: var(--wx-color-success);
+}
+```
+
+---
+
+## Spacing Tokens (5 total)
+
+Consistent spacing scale for padding, margins, and gaps:
+
+| Token | Value | Usage |
+|-------|-------|-------|
+| `--wx-spacing-xs` | `4px` | Tight spacing (icon padding, inline gaps) |
+| `--wx-spacing-sm` | `8px` | Small spacing (button padding, compact layouts) |
+| `--wx-spacing-md` | `16px` | Medium spacing (default padding, content gaps) |
+| `--wx-spacing-lg` | `24px` | Large spacing (section padding) |
+| `--wx-spacing-xl` | `32px` | Extra-large spacing (page margins) |
+
+**Example:**
+```css
+.my-container {
+ padding: var(--wx-spacing-md);
+ gap: var(--wx-spacing-sm);
+}
+
+.my-section {
+ margin-bottom: var(--wx-spacing-lg);
+}
+```
+
+---
+
+## Typography Tokens (8 total)
+
+### Font Family (1 token)
+
+| Token | Value | Usage |
+|-------|-------|-------|
+| `--wx-font-family` | `inherit` | Inherits from application base font |
+
+**Override example:**
+```css
+:root {
+ --wx-font-family: 'Inter', system-ui, sans-serif;
+}
+```
+
+### Font Sizes (7 tokens)
+
+All font sizes support accessibility font scaling:
+
+| Token | Base Size | Actual Definition | Usage |
+|-------|-----------|-------------------|-------|
+| `--wx-font-size-xs` | `12px` | `calc(12px * var(--wx-accessibility-font-scale, 1))` | Small labels, captions |
+| `--wx-font-size-sm` | `14px` | `calc(14px * var(--wx-accessibility-font-scale, 1))` | Secondary text, form labels |
+| `--wx-font-size-md` | `15px` | `calc(15px * var(--wx-accessibility-font-scale, 1))` | Body text (default) |
+| `--wx-font-size-lg` | `18px` | `calc(18px * var(--wx-accessibility-font-scale, 1))` | Subheadings |
+| `--wx-font-size-xl` | `20px` | `calc(20px * var(--wx-accessibility-font-scale, 1))` | Headings (H4) |
+| `--wx-font-size-2xl` | `24px` | `calc(24px * var(--wx-accessibility-font-scale, 1))` | Headings (H3) |
+| `--wx-font-size-3xl` | `32px` | `calc(32px * var(--wx-accessibility-font-scale, 1))` | Headings (H1, H2) |
+
+**Note:** `--wx-accessibility-font-scale` is a **multiplier variable** (not a declared token). It is referenced in font-size token definitions via `calc()` but is not provided a default value in `tokens.css`.
+
+**Source code:** Each font-size token uses this pattern:
+```css
+--wx-font-size-md: calc(15px * var(--wx-accessibility-font-scale, 1));
+```
+
+This means:
+- **Default behavior:** If you don't define `--wx-accessibility-font-scale`, the fallback value `1` is used (no scaling)
+- **Scaling enabled:** Define `--wx-accessibility-font-scale` at `:root` or on a parent element to scale all WaysNX font sizes proportionally
+
+**Accessibility scaling:**
+```css
+/* User prefers larger text */
+:root {
+ --wx-accessibility-font-scale: 1.25; /* All font sizes × 1.25 */
+}
+```
+
+**Result:** `--wx-font-size-md` becomes `calc(15px * 1.25)` = `18.75px`
+
+**Important:** This is a **multiplication factor only**. WaysNX does not automatically detect user font preferences or apply scaling. Your application must implement the detection and set the variable accordingly (e.g., via media queries or JavaScript).
+
+**Example:**
+```css
+.my-heading {
+ font-size: var(--wx-font-size-2xl);
+}
+
+.my-body-text {
+ font-size: var(--wx-font-size-md);
+}
+```
+
+---
+
+## Border Radius Tokens (4 total)
+
+Consistent rounding for corners:
+
+| Token | Value | Usage |
+|-------|-------|-------|
+| `--wx-radius-sm` | `4px` | Buttons, badges, small elements |
+| `--wx-radius-md` | `8px` | Cards, inputs, default rounding |
+| `--wx-radius-lg` | `12px` | Panels, modals |
+| `--wx-radius-xl` | `16px` | Large surfaces |
+
+**Example:**
+```css
+.my-input {
+ border-radius: var(--wx-radius-sm);
+}
+
+.my-card {
+ border-radius: var(--wx-radius-md);
+}
+```
+
+---
+
+## Shadow Tokens (5 total)
+
+Elevation shadows for depth:
+
+| Token | Value | Usage |
+|-------|-------|-------|
+| `--wx-shadow-sm` | `0 1px 2px rgba(0,0,0,0.05)` | Subtle lift (buttons, inputs) |
+| `--wx-shadow-md` | `0 4px 6px -1px rgba(0,0,0,0.07)` | Cards, dropdowns |
+| `--wx-shadow-lg` | `0 10px 15px -3px rgba(0,0,0,0.08)` | Modals, drawers |
+| `--wx-shadow-xl` | `0 20px 25px -5px rgba(0,0,0,0.08)` | Maximum elevation (overlays) |
+| `--wx-shadow-focus` | `0 0 0 3px rgba(241, 153, 36, 0.1)` | Focus ring |
+
+**Example:**
+```css
+.my-modal {
+ box-shadow: var(--wx-shadow-lg);
+}
+
+.my-button:focus-visible {
+ box-shadow: var(--wx-shadow-focus);
+}
+```
+
+---
+
+## Component-Specific Tokens (2 total)
+
+### Input (1 token)
+
+| Token | Value | Usage |
+|-------|-------|-------|
+| `--wx-input-height` | `40px` | Default input height |
+
+### Overlay (1 token)
+
+| Token | Value | Usage |
+|-------|-------|-------|
+| `--wx-overlay-bg` | `rgba(0, 0, 0, 0.4)` | Modal/drawer backdrop |
+
+**Usage:**
+```css
+.my-backdrop {
+ background-color: var(--wx-overlay-bg);
+}
+```
+
+---
+
+## Navigation-Specific Variables
+
+The `@waysnx/ui-navigation` package uses unprefixed CSS variables for component theming (these are **not** core WaysNX tokens):
+
+### Menu Component Variables
+
+| Variable | Default | Usage |
+|----------|---------|-------|
+| `--nav-menu-bg` | `var(--wx-color-surface)` | Menu background |
+| `--nav-menu-text` | `var(--wx-color-text)` | Menu text color |
+| `--nav-menu-item-height` | `2.5rem` | Menu item height |
+| `--nav-menu-item-padding` | `0.5rem 1rem` | Menu item padding |
+| `--nav-menu-gap` | `0.5rem` | Gap between menu items (horizontal) |
+| `--nav-menu-submenu-indent` | `1.5rem` | Submenu indentation |
+
+**Example:**
+```css
+/* Override Menu styling */
+.compact-menu {
+ --nav-menu-item-height: 2rem;
+ --nav-menu-item-padding: 0.25rem 0.75rem;
+}
+```
+
+**Note:** These `--nav-menu-*` variables are component-scoped customization hooks, not core design tokens. The Sidebar component uses dynamic inline styles for width rather than design tokens.
+
+---
+
+## Application-Level Token Examples
+
+The WaysNX UI Kit does **not** provide z-index tokens (`--wx-z-*`). Z-index layering is application-specific and should be managed by your application's CSS architecture.
+
+**Example application-level z-index system:**
+```css
+/* your-app/styles/tokens.css */
+:root {
+ --app-z-dropdown: 1000;
+ --app-z-modal: 2000;
+ --app-z-toast: 3000;
+}
+```
+
+Similarly, some layout concerns (viewport sizing, application-specific spacing multipliers) are best handled at the application level rather than through the design system.
+
+---
+
+## Customizing Tokens
+
+### Global Override
+
+Override tokens at the `:root` level to rebrand the entire UI Kit:
+
+```css
+/* your-theme.css */
+:root {
+ --wx-color-primary: #007bff; /* Change brand color to blue */
+ --wx-spacing-md: 20px; /* Increase default spacing */
+ --wx-radius-md: 12px; /* More rounded corners */
+}
+```
+
+Import your theme **after** WaysNX styles:
+
+```tsx
+import '@waysnx/ui-kit/dist/styles.css'; // Base tokens
+import './your-theme.css'; // Override tokens
+```
+
+### Scoped Override
+
+Override tokens for specific sections of your app:
+
+```css
+/* Admin panel uses different brand color */
+.admin-panel {
+ --wx-color-primary: #8b5cf6; /* Purple */
+}
+```
+
+```tsx
+
+ {/* Purple button */}
+
+```
+
+**Token Inheritance:** CSS custom properties inherit down the DOM tree. **Verified in sampled components:** Button and Sidebar components use `var(--wx-*)` with fallback values, which respect token overrides from parent elements:
+
+```tsx
+
+
+ {/* Also uses green primary color */}
+
+```
+
+**How it works:** WaysNX components define styles like `background: var(--wx-color-primary, #f19924)`. When a parent element sets `--wx-color-primary: #10b981`, CSS inheritance makes that value available to all descendant elements, so the component uses `#10b981` instead of the fallback `#f19924`.
+
+**Verified in source:**
+- `packages/ui-core/src/components/Button/Button.css` uses `var(--wx-color-primary, #f19924)`
+- `packages/ui-navigation/src/components/Sidebar/Sidebar.css` uses `var(--wx-color-surface, #ffffff)`, `var(--wx-color-text, #1e293b)`, etc.
+
+The sampled components (Button, Sidebar) follow the `var(--wx-*, fallback)` pattern. This pattern is expected to be consistent across the UI Kit, ensuring token inheritance works for components that follow this convention.
+
+### Component-Level Override
+
+Override tokens for individual components:
+
+```css
+.my-custom-button {
+ --wx-color-primary: #10b981; /* Green button */
+}
+```
+
+```tsx
+
+```
+
+---
+
+## Dark Mode Support
+
+WaysNX UI Kit supports dark mode via `prefers-color-scheme`. To enable:
+
+```css
+/* Override tokens in dark mode */
+@media (prefers-color-scheme: dark) {
+ :root {
+ --wx-color-surface: #0f172a;
+ --wx-color-surface-alt: #1e293b;
+ --wx-color-text: #e2e8f0;
+ --wx-color-text-muted: #94a3b8;
+ --wx-color-border: #334155;
+ }
+}
+```
+
+Some components (e.g., `Sidebar`) support explicit dark mode via `colorScheme` prop:
+
+```tsx
+ {/* Force dark mode */}
+ {children}
+
+```
+
+---
+
+## Token Naming Convention
+
+All WaysNX tokens follow this pattern:
+
+```
+--wx--[-]
+```
+
+**Examples:**
+- `--wx-color-primary` → category: `color`, property: `primary`
+- `--wx-spacing-md` → category: `spacing`, property: `md`
+- `--wx-color-error-light` → category: `color`, property: `error`, variant: `light`
+
+**Why `--wx-` prefix?**
+- Avoids collisions with existing CSS variables
+- Easy to search/grep in codebase
+- Clear ownership (WaysNX UI Kit tokens)
+
+---
+
+## Browser Support
+
+CSS custom properties (CSS variables) are supported in:
+
+- Chrome 49+
+- Firefox 31+
+- Safari 9.1+
+- Edge 15+
+
+For older browsers, consider a fallback:
+
+```css
+.my-element {
+ color: #f19924; /* Fallback */
+ color: var(--wx-color-primary); /* Modern browsers */
+}
+```
+
+---
+
+## Complete Token Reference
+
+### All Tokens at a Glance
+
+**Colors (20 tokens):**
+- Brand: primary, primary-hover, primary-contrast, primary-light (4)
+- Text: text, text-muted, text-light (3)
+- Surface: surface, surface-alt, surface-hover (3)
+- Border: border (1)
+- Status: error, error-hover, error-light, success, success-light, warning, info, destructive, destructive-hover (9)
+
+**Spacing (5 tokens):**
+- xs, sm, md, lg, xl
+
+**Typography (8 tokens):**
+- font-family
+- font-size-xs, sm, md, lg, xl, 2xl, 3xl
+
+**Border Radius (4 tokens):**
+- radius-sm, md, lg, xl
+
+**Shadows (5 tokens):**
+- shadow-sm, md, lg, xl, focus
+
+**Component (2 tokens):**
+- input-height
+- overlay-bg
+
+**Total: 44 CSS custom properties** (declared in `packages/ui-core/src/styles/tokens.css`)
+
+**Note:** `--wx-accessibility-font-scale` is referenced as a fallback multiplier in font-size tokens but is not declared with a default value. Applications can optionally define it to scale all font sizes proportionally.
+
+---
+
+## Getting Help
+
+- **Full token list:** `packages/ui-core/src/styles/tokens.css`
+- **Documentation:** https://uikit.waysnx.tech
+- **Examples:** See `examples/waysnx-admin-demo/` for token usage patterns
+
+---
+
+**Last Updated:** October 2026
+**Applies to:** WaysNX UI Kit v1.0.0+
From 9a09ad89653c1a667e0c51d735631ac0f6325c2b Mon Sep 17 00:00:00 2001
From: Kranti Ghatpande <119097691+krantighatpande@users.noreply.github.com>
Date: Fri, 23 Oct 2026 15:32:26 +0530
Subject: [PATCH 2/2] test: add vitest infrastructure for ui-core and
ui-navigation
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Establish comprehensive unit test infrastructure for WaysNX UI Kit component packages.
Phase 2: Test Infrastructure Setup (Steps 1 & 2)
Packages Configured:
- @waysnx/ui-core: Added vitest setup with Button test suite (9 tests)
- @waysnx/ui-navigation: Added vitest setup with Sidebar, Menu, and integration tests (68 tests)
Test Coverage:
- Total: 77 tests across 2 packages, 100% passing
- ui-core: Button component (variants, interactions, disabled state, styling)
- ui-navigation: Sidebar (collapse, persistence, SSR, accessibility)
- ui-navigation: Menu (expansion, interaction, permissions, accessibility)
- ui-navigation: Integration tests (Sidebar + Menu coordination)
Phase 3 Items Documented (11 tests):
Tests document current behavior and mark limitations for Phase 3 implementation:
- Keyboard navigation (ArrowDown, ArrowUp, Home, End, Escape) - 6 tests
- Controlled isCollapsed prop - 2 tests
- Inline style prop forwarding - 2 tests
- SSR localStorage safety - 1 test
Configuration:
- Vitest v3.2.4 (ui-core) / v3.2.7 (ui-navigation)
- @testing-library/react v16.0.0 for component testing
- Non-globals mode with explicit imports
- jsdom environment for React components
Test Quality:
- Menu expansion: Verified aria-expanded changes
- Permission filtering: Explicit negative assertions
- SSR test: Finally block for guaranteed cleanup
- Controlled state: Documented uncontrolled-only behavior
- Keyboard nav: Documented current limitations with Phase 3 markers
Files Changed:
- packages/ui-core/vitest.config.ts (new)
- packages/ui-core/tests/Button.test.tsx (new)
- packages/ui-core/package.json (added test deps + scripts)
- packages/ui-navigation/vitest.config.ts (new)
- packages/ui-navigation/tests/Sidebar.test.tsx (new)
- packages/ui-navigation/tests/Menu.test.tsx (new)
- packages/ui-navigation/tests/integration.test.tsx (new)
- packages/ui-navigation/package.json (added test deps + scripts)
- pnpm-lock.yaml (test dependency tree)
- PHASE2_STEP1_FINAL_REPORT.md (new)
- PHASE2_STEP2_FINAL_COMPLETION_REPORT.md (new)
Verification:
✓ All 77 tests passing (ui-core: 9/9, ui-navigation: 68/68)
✓ Builds successful for both packages
✓ No production code changes
✓ No regressions introduced
Part of Phase 2: Test Infrastructure Setup
---
packages/ui-core/package.json | 12 +-
packages/ui-core/tests/Button.test.tsx | 85 +++
packages/ui-core/vitest.config.ts | 16 +
packages/ui-navigation/package.json | 12 +-
packages/ui-navigation/tests/Menu.test.tsx | 506 ++++++++++++++++++
packages/ui-navigation/tests/Sidebar.test.tsx | 334 ++++++++++++
.../ui-navigation/tests/integration.test.tsx | 350 ++++++++++++
packages/ui-navigation/vitest.config.ts | 16 +
pnpm-lock.yaml | 407 +++++++++++++-
9 files changed, 1732 insertions(+), 6 deletions(-)
create mode 100644 packages/ui-core/tests/Button.test.tsx
create mode 100644 packages/ui-core/vitest.config.ts
create mode 100644 packages/ui-navigation/tests/Menu.test.tsx
create mode 100644 packages/ui-navigation/tests/Sidebar.test.tsx
create mode 100644 packages/ui-navigation/tests/integration.test.tsx
create mode 100644 packages/ui-navigation/vitest.config.ts
diff --git a/packages/ui-core/package.json b/packages/ui-core/package.json
index 84eabde..2f22bcb 100644
--- a/packages/ui-core/package.json
+++ b/packages/ui-core/package.json
@@ -37,19 +37,27 @@
"react-number-format": "^5.4.4"
},
"devDependencies": {
+ "@testing-library/jest-dom": "^6.5.0",
+ "@testing-library/react": "^16.0.0",
+ "@testing-library/user-event": "^14.5.0",
"@types/dompurify": "^3.0.5",
"@types/node": "^25.0.3",
"@types/react": "^19.2.7",
"@types/react-datepicker": "^7.0.0",
"@types/react-dom": "^19.2.3",
+ "@vitejs/plugin-react": "^4.3.0",
"copyfiles": "^2.4.1",
+ "jsdom": "^25.0.0",
"tsup": "^8.5.1",
- "typescript": "^5.9.3"
+ "typescript": "^5.9.3",
+ "vitest": "^3.2.4"
},
"scripts": {
"build": "tsup && npm run copy-css",
"copy-css": "copyfiles -u 1 \"src/**/*.css\" dist",
- "clean": "rm -rf dist"
+ "clean": "rm -rf dist",
+ "test": "vitest run",
+ "test:watch": "vitest"
},
"exports": {
"./dist/index.css": "./dist/index.css",
diff --git a/packages/ui-core/tests/Button.test.tsx b/packages/ui-core/tests/Button.test.tsx
new file mode 100644
index 0000000..665f731
--- /dev/null
+++ b/packages/ui-core/tests/Button.test.tsx
@@ -0,0 +1,85 @@
+import { describe, it, expect, vi, afterEach } from "vitest";
+import { render, screen, cleanup } from "@testing-library/react";
+import { userEvent } from "@testing-library/user-event";
+import { Button } from "../src/components/Button";
+import * as matchers from "@testing-library/jest-dom/matchers";
+
+// Extend vitest's expect with jest-dom matchers
+expect.extend(matchers);
+
+describe("Button", () => {
+ // Clean up DOM after each test
+ afterEach(() => {
+ cleanup();
+ });
+ it("renders with children text", () => {
+ render();
+ expect(screen.getByText("Click me")).toBeInTheDocument();
+ });
+
+ it("renders with primary variant by default", () => {
+ render();
+ const button = screen.getByRole("button", { name: "Primary" });
+ expect(button).toHaveClass("wx-button");
+ expect(button).toHaveClass("wx-button--primary");
+ });
+
+ it("renders with specified variant", () => {
+ render();
+ const button = screen.getByRole("button", { name: "Secondary" });
+ expect(button).toHaveClass("wx-button--secondary");
+ });
+
+ it("invokes click handler when clicked", async () => {
+ const user = userEvent.setup();
+ const handleClick = vi.fn();
+
+ render();
+ const button = screen.getByRole("button", { name: "Click me" });
+
+ await user.click(button);
+
+ expect(handleClick).toHaveBeenCalledTimes(1);
+ });
+
+ it("does not invoke click handler when disabled", async () => {
+ const user = userEvent.setup();
+ const handleClick = vi.fn();
+
+ render();
+ const button = screen.getByRole("button", { name: "Disabled" });
+
+ expect(button).toBeDisabled();
+
+ await user.click(button);
+
+ expect(handleClick).not.toHaveBeenCalled();
+ });
+
+ it("applies custom className alongside default classes", () => {
+ render();
+ const button = screen.getByRole("button", { name: "Custom" });
+
+ expect(button).toHaveClass("wx-button");
+ expect(button).toHaveClass("wx-button--primary");
+ expect(button).toHaveClass("custom-class");
+ });
+
+ it("renders destructive variant correctly", () => {
+ render();
+ const button = screen.getByRole("button", { name: "Delete" });
+ expect(button).toHaveClass("wx-button--destructive");
+ });
+
+ it("renders outline variant correctly", () => {
+ render();
+ const button = screen.getByRole("button", { name: "Outline" });
+ expect(button).toHaveClass("wx-button--outline");
+ });
+
+ it("renders ghost variant correctly", () => {
+ render();
+ const button = screen.getByRole("button", { name: "Ghost" });
+ expect(button).toHaveClass("wx-button--ghost");
+ });
+});
diff --git a/packages/ui-core/vitest.config.ts b/packages/ui-core/vitest.config.ts
new file mode 100644
index 0000000..0bdecb6
--- /dev/null
+++ b/packages/ui-core/vitest.config.ts
@@ -0,0 +1,16 @@
+import { defineConfig } from "vitest/config";
+import react from "@vitejs/plugin-react";
+
+export default defineConfig({
+ plugins: [react()],
+ test: {
+ globals: false,
+ environment: "jsdom",
+ include: ["tests/**/*.test.{ts,tsx}"],
+ css: {
+ modules: {
+ classNameStrategy: "non-scoped",
+ },
+ },
+ },
+});
diff --git a/packages/ui-navigation/package.json b/packages/ui-navigation/package.json
index 26c4845..5800ab3 100644
--- a/packages/ui-navigation/package.json
+++ b/packages/ui-navigation/package.json
@@ -19,17 +19,25 @@
"@waysnx/ui-i18n": "workspace:*"
},
"devDependencies": {
+ "@testing-library/jest-dom": "^6.5.0",
+ "@testing-library/react": "^16.0.0",
+ "@testing-library/user-event": "^14.5.0",
"@types/node": "^25.0.3",
"@types/react": "^19.2.7",
"@types/react-dom": "^19.2.3",
+ "@vitejs/plugin-react": "^4.3.0",
"copyfiles": "^2.4.1",
+ "jsdom": "^25.0.0",
"tsup": "^8.5.1",
- "typescript": "^5.9.3"
+ "typescript": "^5.9.3",
+ "vitest": "^3.2.4"
},
"scripts": {
"build": "tsup && npm run copy-css",
"copy-css": "copyfiles -u 1 \"src/**/*.css\" dist",
- "clean": "rm -rf dist"
+ "clean": "rm -rf dist",
+ "test": "vitest run",
+ "test:watch": "vitest"
},
"exports": {
"./dist/index.css": "./dist/index.css",
diff --git a/packages/ui-navigation/tests/Menu.test.tsx b/packages/ui-navigation/tests/Menu.test.tsx
new file mode 100644
index 0000000..ba4e8a1
--- /dev/null
+++ b/packages/ui-navigation/tests/Menu.test.tsx
@@ -0,0 +1,506 @@
+import { describe, it, expect, vi, afterEach } from "vitest";
+import { render, screen, cleanup } from "@testing-library/react";
+import { userEvent } from "@testing-library/user-event";
+import { Menu } from "../src/components/Menu";
+import type { NavigationItem, SecurityContext } from "../src/types";
+import * as matchers from "@testing-library/jest-dom/matchers";
+
+// Extend vitest's expect with jest-dom matchers
+expect.extend(matchers);
+
+describe("Menu", () => {
+ // Mock navigation items
+ const mockItems: NavigationItem[] = [
+ { id: "1", label: "Home", href: "/" },
+ { id: "2", label: "About", href: "/about" },
+ { id: "3", label: "Contact", href: "/contact" },
+ ];
+
+ const mockItemsWithChildren: NavigationItem[] = [
+ { id: "1", label: "Dashboard", href: "/dashboard" },
+ {
+ id: "2",
+ label: "Products",
+ href: "/products",
+ children: [
+ { id: "2-1", label: "Electronics", href: "/products/electronics" },
+ { id: "2-2", label: "Clothing", href: "/products/clothing" },
+ ],
+ },
+ { id: "3", label: "Support", href: "/support" },
+ ];
+
+ // Clean up DOM after each test
+ afterEach(() => {
+ cleanup();
+ });
+
+ describe("Rendering", () => {
+ it("renders all menu items", () => {
+ render();
+
+ expect(screen.getByText("Home")).toBeInTheDocument();
+ expect(screen.getByText("About")).toBeInTheDocument();
+ expect(screen.getByText("Contact")).toBeInTheDocument();
+ });
+
+ it("renders with vertical orientation by default", () => {
+ const { container } = render();
+ const menu = container.querySelector(".nav-menu");
+
+ expect(menu).toHaveClass("nav-menu--vertical");
+ });
+
+ it("renders with horizontal orientation when specified", () => {
+ const { container } = render(
+
+ );
+ const menu = container.querySelector(".nav-menu");
+
+ expect(menu).toHaveClass("nav-menu--horizontal");
+ });
+
+ it("renders parent items with expandable indicators", () => {
+ const { container } = render();
+
+ expect(screen.getByText("Dashboard")).toBeInTheDocument();
+ expect(screen.getByText("Products")).toBeInTheDocument();
+ expect(screen.getByText("Support")).toBeInTheDocument();
+
+ // Parent with children should have expandable class
+ const productsItem = screen.getByText("Products").closest("li");
+ expect(productsItem).toHaveClass("nav-menu-item--expandable");
+
+ // Children items are not rendered until expanded (collapsed by default)
+ });
+
+ it("applies density class", () => {
+ const { container } = render(
+
+ );
+ const menu = container.querySelector(".nav-menu");
+
+ expect(menu).toHaveClass("nav-menu--compact");
+ });
+
+ it("applies variant class", () => {
+ const { container } = render(
+
+ );
+ const menu = container.querySelector(".nav-menu");
+
+ expect(menu).toHaveClass("nav-menu--minimal");
+ });
+
+ it("applies custom className", () => {
+ const { container } = render(
+
+ );
+ const menu = container.querySelector(".nav-menu");
+
+ expect(menu).toHaveClass("nav-menu");
+ expect(menu).toHaveClass("custom-menu");
+ });
+ });
+
+ describe("Item Expansion", () => {
+ it("marks items with children as expandable and toggles aria-expanded", async () => {
+ const user = userEvent.setup();
+ const { container } = render();
+
+ const productsItem = screen.getByText("Products").closest("li");
+ const productsLink = screen.getByText("Products").closest("a");
+
+ // Should have expandable class and aria-expanded=false initially
+ expect(productsItem).toHaveClass("nav-menu-item--expandable");
+ expect(productsLink).toHaveAttribute("aria-expanded", "false");
+
+ // Click to expand
+ await user.click(screen.getByText("Products"));
+
+ // Verify aria-expanded changes to true after click
+ expect(productsLink).toHaveAttribute("aria-expanded", "true");
+
+ // Note: Child visibility depends on MenuItem implementation
+ // The expandable state and aria-expanded are the contract
+ });
+
+ it("nested children become visible when parent is expanded", async () => {
+ const user = userEvent.setup();
+ render();
+
+ const productsLink = screen.getByText("Products").closest("a");
+
+ // Initially collapsed
+ expect(productsLink).toHaveAttribute("aria-expanded", "false");
+
+ // Children should not be visible initially (or should verify actual behavior)
+ // If implementation renders children collapsed, verify they exist but are hidden
+ // If implementation doesn't render until expanded, verify they're absent
+
+ // Click to expand
+ await user.click(screen.getByText("Products"));
+
+ // Verify expanded state
+ expect(productsLink).toHaveAttribute("aria-expanded", "true");
+
+ // Verify children are now visible
+ expect(screen.getByText("Electronics")).toBeVisible();
+ expect(screen.getByText("Clothing")).toBeVisible();
+ });
+ });
+
+ describe("Item Interaction", () => {
+ it("invokes onItemClick when item is clicked", async () => {
+ const user = userEvent.setup();
+ const handleItemClick = vi.fn();
+
+ render();
+
+ await user.click(screen.getByText("Home"));
+
+ expect(handleItemClick).toHaveBeenCalledTimes(1);
+ expect(handleItemClick).toHaveBeenCalledWith(
+ expect.objectContaining({ id: "1", label: "Home" })
+ );
+ });
+
+ it("invokes onActiveChange when item is clicked", async () => {
+ const user = userEvent.setup();
+ const handleActiveChange = vi.fn();
+
+ render();
+
+ await user.click(screen.getByText("About"));
+
+ expect(handleActiveChange).toHaveBeenCalledTimes(1);
+ expect(handleActiveChange).toHaveBeenCalledWith(
+ expect.objectContaining({ id: "2", label: "About" })
+ );
+ });
+
+ it("highlights active item", () => {
+ const activeItem = mockItems[1]; // "About"
+ const { container } = render(
+
+ );
+
+ const aboutItem = screen.getByText("About").closest("li");
+ expect(aboutItem).toHaveClass("nav-menu-item--active");
+ });
+ });
+
+ describe("Keyboard Navigation", () => {
+ it("documents keyboard nav limitation - ArrowDown (Phase 3)", async () => {
+ // KNOWN LIMITATION - Phase 3: Menu does not implement custom keyboard navigation
+ // Expected: Arrow keys should move focus between items
+ // Current: Focus does not move (standard browser behavior only)
+
+ const user = userEvent.setup();
+ render();
+
+ const firstLink = screen.getByText("Home").closest("a");
+ const secondLink = screen.getByText("About").closest("a");
+
+ // Focus first item
+ firstLink?.focus();
+ expect(document.activeElement).toBe(firstLink);
+
+ // Press ArrowDown
+ await user.keyboard("{ArrowDown}");
+
+ // CURRENT BEHAVIOR: Focus does not move
+ expect(document.activeElement).toBe(firstLink);
+
+ // TODO Phase 3: Implement arrow key navigation
+ // Expected: expect(document.activeElement).toBe(secondLink);
+ });
+
+ it("documents keyboard nav limitation - ArrowUp (Phase 3)", async () => {
+ // KNOWN LIMITATION - Phase 3: Arrow key navigation not implemented
+
+ const user = userEvent.setup();
+ render();
+
+ const firstLink = screen.getByText("Home").closest("a");
+ const secondLink = screen.getByText("About").closest("a");
+
+ // Focus second item
+ secondLink?.focus();
+ expect(document.activeElement).toBe(secondLink);
+
+ // Press ArrowUp
+ await user.keyboard("{ArrowUp}");
+
+ // CURRENT BEHAVIOR: Focus does not move
+ expect(document.activeElement).toBe(secondLink);
+
+ // TODO Phase 3: expect(document.activeElement).toBe(firstLink);
+ });
+
+ it("Enter key triggers native link behavior", async () => {
+ const user = userEvent.setup();
+ const handleItemClick = vi.fn();
+ render(
+
+ );
+
+ const firstLink = screen.getByText("Home").closest("a");
+
+ // Focus first item
+ firstLink?.focus();
+ expect(document.activeElement).toBe(firstLink);
+
+ // Press Enter
+ await user.keyboard("{Enter}");
+
+ // Enter triggers click via native link behavior
+ expect(handleItemClick).toHaveBeenCalledTimes(1);
+ expect(handleItemClick).toHaveBeenCalledWith(
+ expect.objectContaining({ id: "1", label: "Home" })
+ );
+ });
+
+ it("documents keyboard nav limitation - Home key (Phase 3)", async () => {
+ // KNOWN LIMITATION - Phase 3: Home/End keys not implemented
+
+ const user = userEvent.setup();
+ render();
+
+ const lastLink = screen.getByText("Contact").closest("a");
+ const firstLink = screen.getByText("Home").closest("a");
+
+ // Focus last item
+ lastLink?.focus();
+ expect(document.activeElement).toBe(lastLink);
+
+ // Press Home
+ await user.keyboard("{Home}");
+
+ // CURRENT BEHAVIOR: Focus does not move
+ expect(document.activeElement).toBe(lastLink);
+
+ // TODO Phase 3: expect(document.activeElement).toBe(firstLink);
+ });
+
+ it("documents keyboard nav limitation - End key (Phase 3)", async () => {
+ // KNOWN LIMITATION - Phase 3: Home/End keys not implemented
+
+ const user = userEvent.setup();
+ render();
+
+ const firstLink = screen.getByText("Home").closest("a");
+ const lastLink = screen.getByText("Contact").closest("a");
+
+ // Focus first item
+ firstLink?.focus();
+ expect(document.activeElement).toBe(firstLink);
+
+ // Press End
+ await user.keyboard("{End}");
+
+ // CURRENT BEHAVIOR: Focus does not move
+ expect(document.activeElement).toBe(firstLink);
+
+ // TODO Phase 3: expect(document.activeElement).toBe(lastLink);
+ });
+
+ it("documents keyboard nav limitation - Escape key (Phase 3)", async () => {
+ // KNOWN LIMITATION - Phase 3: Escape key collapse not implemented
+
+ const user = userEvent.setup();
+ render();
+
+ const productsLink = screen.getByText("Products").closest("a");
+
+ // Click to expand
+ await user.click(screen.getByText("Products"));
+
+ // Verify expanded
+ expect(productsLink).toHaveAttribute("aria-expanded", "true");
+
+ // Focus the item
+ productsLink?.focus();
+
+ // Press Escape
+ await user.keyboard("{Escape}");
+
+ // CURRENT BEHAVIOR: Does not collapse
+ expect(productsLink).toHaveAttribute("aria-expanded", "true");
+
+ // TODO Phase 3: Implement Escape key handler
+ // Expected: expect(productsLink).toHaveAttribute("aria-expanded", "false");
+ });
+
+ it("keyboard navigation prop is available but not fully implemented", async () => {
+ const user = userEvent.setup();
+ render();
+
+ const firstLink = screen.getByText("Home").closest("a");
+ const secondLink = screen.getByText("About").closest("a");
+
+ // Focus first item
+ firstLink?.focus();
+ expect(document.activeElement).toBe(firstLink);
+
+ // Press ArrowDown
+ await user.keyboard("{ArrowDown}");
+
+ // With keyboardNav false, custom nav should not work (currently not implemented either way)
+ expect(firstLink).toBeInTheDocument();
+ expect(secondLink).toBeInTheDocument();
+ });
+ });
+
+ describe("Permission-Based Filtering", () => {
+ const itemsWithPermissions: NavigationItem[] = [
+ { id: "1", label: "Public Page", href: "/public" },
+ { id: "2", label: "Admin Page", href: "/admin", permissions: ["admin"] },
+ { id: "3", label: "User Page", href: "/user", roles: ["user"] },
+ ];
+
+ it("shows all items when no security context provided", () => {
+ render();
+
+ // All items should be visible without security filtering
+ expect(screen.getByText("Public Page")).toBeInTheDocument();
+ expect(screen.getByText("Admin Page")).toBeInTheDocument();
+ expect(screen.getByText("User Page")).toBeInTheDocument();
+ });
+
+ it("filters items based on security context permissions", () => {
+ const securityContext: SecurityContext = {
+ permissions: ["admin"],
+ roles: [],
+ };
+
+ render(
+
+ );
+
+ // Should show public and admin items
+ expect(screen.getByText("Public Page")).toBeInTheDocument();
+ expect(screen.getByText("Admin Page")).toBeInTheDocument();
+
+ // Should NOT show user page (requires 'user' role which is not present)
+ expect(screen.queryByText("User Page")).not.toBeInTheDocument();
+ });
+
+ it("filters items based on security context roles", () => {
+ const securityContext: SecurityContext = {
+ permissions: [],
+ roles: ["user"],
+ };
+
+ render(
+
+ );
+
+ // Should show public and user items
+ expect(screen.getByText("Public Page")).toBeInTheDocument();
+ expect(screen.getByText("User Page")).toBeInTheDocument();
+
+ // Should NOT show admin page (requires 'admin' permission which is not present)
+ expect(screen.queryByText("Admin Page")).not.toBeInTheDocument();
+ });
+
+ it("filters multiple items when permissions are missing", () => {
+ const securityContext: SecurityContext = {
+ permissions: [],
+ roles: [],
+ };
+
+ render(
+
+ );
+
+ // Should only show public item
+ expect(screen.getByText("Public Page")).toBeInTheDocument();
+
+ // Should NOT show restricted items
+ expect(screen.queryByText("Admin Page")).not.toBeInTheDocument();
+ expect(screen.queryByText("User Page")).not.toBeInTheDocument();
+ });
+ });
+
+ describe("Accessibility", () => {
+ it("has menu role by default", () => {
+ render();
+
+ const menu = screen.getByRole("menu");
+ expect(menu).toBeInTheDocument();
+ });
+
+ it("has navigation role when specified", () => {
+ render();
+
+ const nav = screen.getByRole("navigation");
+ expect(nav).toBeInTheDocument();
+ });
+
+ it("has accessible label", () => {
+ render();
+
+ expect(screen.getByLabelText("Main menu")).toBeInTheDocument();
+ });
+
+ it("uses default label when ariaLabel not provided", () => {
+ render();
+
+ expect(screen.getByLabelText("Menu")).toBeInTheDocument();
+ });
+ });
+
+ describe("Icons and Badges", () => {
+ const itemsWithExtras: NavigationItem[] = [
+ {
+ id: "1",
+ label: "Dashboard",
+ href: "/dashboard",
+ icon: 📊,
+ },
+ {
+ id: "2",
+ label: "Messages",
+ href: "/messages",
+ badge: 5,
+ },
+ ];
+
+ it("shows icons when showIcons is true", () => {
+ render();
+
+ expect(screen.getByTestId("icon-dashboard")).toBeInTheDocument();
+ });
+
+ it("shows badges when showBadges is true", () => {
+ render();
+
+ expect(screen.getByText("5")).toBeInTheDocument();
+ });
+ });
+
+ describe("Custom Styling", () => {
+ it("documents inline styles limitation (Phase 3)", () => {
+ // KNOWN LIMITATION - Phase 3: Menu does not apply inline styles from style prop
+ // Expected: Inline styles should be applied to menu element
+ // Current: style prop is accepted but not applied
+
+ const { container } = render(
+
+ );
+ const menu = container.querySelector(".nav-menu");
+
+ // Menu element exists
+ expect(menu).toBeInTheDocument();
+
+ // CURRENT BEHAVIOR: Inline styles are not applied
+ // Style prop is accepted but ignored by component implementation
+
+ // TODO Phase 3: Implement style prop forwarding
+ // Expected after fix:
+ // expect(menu).toHaveStyle({ backgroundColor: "blue" });
+ // expect(menu).toHaveStyle({ padding: "20px" });
+ });
+ });
+});
diff --git a/packages/ui-navigation/tests/Sidebar.test.tsx b/packages/ui-navigation/tests/Sidebar.test.tsx
new file mode 100644
index 0000000..ec4ff9b
--- /dev/null
+++ b/packages/ui-navigation/tests/Sidebar.test.tsx
@@ -0,0 +1,334 @@
+import { describe, it, expect, vi, afterEach, beforeEach } from "vitest";
+import { render, screen, cleanup } from "@testing-library/react";
+import { userEvent } from "@testing-library/user-event";
+import { Sidebar } from "../src/components/Sidebar";
+import type { NavigationItem } from "../src/types";
+import * as matchers from "@testing-library/jest-dom/matchers";
+
+// Extend vitest's expect with jest-dom matchers
+expect.extend(matchers);
+
+describe("Sidebar", () => {
+ // Mock navigation items
+ const mockItems: NavigationItem[] = [
+ { id: "1", label: "Dashboard", href: "/dashboard" },
+ { id: "2", label: "Projects", href: "/projects" },
+ {
+ id: "3",
+ label: "Settings",
+ href: "/settings",
+ children: [
+ { id: "3-1", label: "Profile", href: "/settings/profile" },
+ { id: "3-2", label: "Security", href: "/settings/security" },
+ ],
+ },
+ ];
+
+ // Clean up DOM and localStorage after each test
+ afterEach(() => {
+ cleanup();
+ localStorage.clear();
+ });
+
+ describe("Rendering", () => {
+ it("renders with navigation items", () => {
+ render();
+
+ expect(screen.getByText("Dashboard")).toBeInTheDocument();
+ expect(screen.getByText("Projects")).toBeInTheDocument();
+ expect(screen.getByText("Settings")).toBeInTheDocument();
+ });
+
+ it("renders with logo", () => {
+ const logo =