diff --git a/.claude/commands/create-eng-docs.md b/.claude/commands/create-eng-docs.md index 1de32d519..498187f22 100644 --- a/.claude/commands/create-eng-docs.md +++ b/.claude/commands/create-eng-docs.md @@ -215,12 +215,14 @@ Once confirmed, create the documentation file: > is good practice to add a **persistent**, **unique** id to the > component: - **Testing**: - - **Mandatory**: Start with this exact disclaimer: +- Testing examples are auto-generated from `.docs.spec.tsx` files + - **Mandatory**: Include this text before the injection token: > These examples demonstrate how to test your implementation when using > [Component] in your application. The component's internal functionality > is already tested by Nimbus - these patterns help you verify your > integration and application-specific logic. - - Provide realistic test examples (Rendering, Interaction, etc.) + - Add injection token: `{{docs-tests: {component-name}.docs.spec.tsx}}` + - Create companion `.docs.spec.tsx` file with test sections (see Step 6.1) - **Resources**: - Link to Storybook (use "link-tbd" placeholder) - **Internal component links must start with '/'** (e.g., @@ -237,6 +239,82 @@ Once confirmed, create the documentation file: 9. **Write the file** using the Write tool +### **Step 6.1: Create Documentation Test File** + +**IMPORTANT**: After creating the `.dev.mdx` file, create the companion `.docs.spec.tsx` test file. + +1. **Create test file**: `{component-name}.docs.spec.tsx` in same directory as component + +2. **File structure**: + ```typescript + import { describe, it, expect, vi } from 'vitest'; + import { render, screen } from '@testing-library/react'; + import userEvent from '@testing-library/user-event'; + import { ComponentName, NimbusProvider } from '@commercetools/nimbus'; + + /** + * @docs-section basic-rendering + * @docs-title Basic Rendering Tests + * @docs-description Verify the component renders with expected elements + * @docs-order 1 + */ + describe('ComponentName - Basic rendering', () => { + it('renders component', () => { + render( + + + + ); + + expect(screen.getByRole('...')).toBeInTheDocument(); + }); + }); + + /** + * @docs-section interactions + * @docs-title Interaction Tests + * @docs-description Test user interactions with the component + * @docs-order 2 + */ + describe('ComponentName - Interactions', () => { + it('handles user interaction', async () => { + const user = userEvent.setup(); + render( + + + + ); + + // Test interactions + }); + }); + ``` + +3. **JSDoc tags required**: + - `@docs-section` - Unique ID for section + - `@docs-title` - Display title + - `@docs-description` - Brief description + - `@docs-order` - Sort order (0 for setup, 1+ for tests) + +4. **Test patterns to include** (based on component features): + - Basic rendering (always) + - Interactions (if interactive) + - Controlled mode (if supports value/onChange) + - States (disabled, invalid, readonly, required - if component has these props) + - Component-specific features + +5. **Critical rules**: + - ✅ Every `render()` must wrap with `` + - ✅ Import NimbusProvider from `@commercetools/nimbus` + - ✅ Use `vi.fn()` for mocks (not `jest.fn()`) + - ✅ Use `userEvent.setup()` for interactions + - ✅ Base test names on component features, not generic patterns + +6. **Verify tests**: + ```bash + pnpm test:unit {component-name}.docs.spec.tsx + ``` + ### **Step 7: Validate Documentation** After generating the file, run validation checks: @@ -273,7 +351,9 @@ After generating the file, run validation checks: - [ ] Examples are realistic and functional - [ ] TypeScript types are correct - [ ] Accessibility requirements documented -- [ ] Testing examples provided +- [ ] Testing section has `{{docs-tests:}}` injection token +- [ ] Companion `.docs.spec.tsx` file created with test sections +- [ ] Tests pass when run with `pnpm test:unit` ### Structure Checklist @@ -286,12 +366,15 @@ After generating the file, run validation checks: ### Code Example Checklist - [ ] All interactive examples use `jsx-live-dev` -- [ ] All type/test examples use `tsx` +- [ ] All type examples use `tsx` - [ ] Examples follow `const App = () => { }` pattern - [ ] State declarations use prop type inference pattern - [ ] Controlled examples include state display with Text component -- [ ] Test examples use `userEvent.setup()` pattern -- [ ] Portal/popover tests use `waitFor` and document queries +- [ ] Test examples in `.docs.spec.tsx` file (not in MDX) +- [ ] Tests wrap every `render()` with `` +- [ ] Tests use `vi.fn()` for mocks (not `jest.fn()`) +- [ ] Tests use `userEvent.setup()` for interactions +- [ ] Portal/popover tests use `waitFor` with document queries ### Link Checklist @@ -312,12 +395,19 @@ Provide a final summary: ````markdown ## Documentation Created -**Component**: {ComponentName} **Type**: [Base Component / Field Pattern] -**File**: {full-path-to-file} **Size**: {file size in lines} +**Component**: {ComponentName} +**Type**: [Base Component / Field Pattern] +**Files Created**: +- `.dev.mdx`: {full-path-to-dev-mdx} +- `.docs.spec.tsx`: {full-path-to-docs-spec} ### Sections Included -- [List all sections included] +- [List all sections included in .dev.mdx] + +### Test Sections Created + +- [List all test sections in .docs.spec.tsx with @docs-section IDs] ### Key Features Documented @@ -325,20 +415,26 @@ Provide a final summary: ### Code Examples -- Total interactive examples: X -- Total test examples: Y +- Total interactive examples (jsx-live-dev): X +- Total test sections (in .docs.spec.tsx): Y +- Total test cases: Z ### Next Steps 1. Review the generated documentation -2. Test all interactive examples in the docs site -3. Update the Storybook link once available -4. Add any component-specific advanced patterns -5. Review with the team before publishing +2. Run tests: `pnpm test:unit {component-name}.docs.spec.tsx` +3. Build docs: `pnpm build:docs` +4. Test all interactive examples in the docs site +5. Update the Storybook link once available +6. Add any component-specific advanced patterns +7. Review with the team before publishing ### Useful Commands ```bash +# Run documentation tests +pnpm test:unit {component-name}.docs.spec.tsx + # Start docs site to preview pnpm start:docs diff --git a/docs/component-guidelines.md b/docs/component-guidelines.md index 896800c16..3a079b75a 100644 --- a/docs/component-guidelines.md +++ b/docs/component-guidelines.md @@ -36,7 +36,9 @@ detailed guidelines: ### Testing Files - **[Unit Tests ({utility}.spec.ts)](./file-type-guidelines/unit-testing.md)** - - Fast, isolated tests for utilities and hooks only (components use Storybook stories) + Fast, isolated tests for utilities, hooks, and documentation examples +- **[Documentation Tests ({component}.docs.spec.tsx)](../engineering-docs-validation.md)** - + Consumer-facing test examples automatically injected into `.dev.mdx` documentation ### Styling System Files (When Needed) @@ -81,9 +83,11 @@ detailed guidelines: stories with play functions for testing 8. **[Document](./file-type-guidelines/documentation.md)** - Create MDX documentation -9. **[Export](./file-type-guidelines/barrel-exports.md)** - Set up public API +9. **[Add Documentation Tests](../engineering-docs-validation.md)** - Create + `.docs.spec.tsx` with consumer test examples (optional but recommended) +10. **[Export](./file-type-guidelines/barrel-exports.md)** - Set up public API -**Note**: All component behavior is tested in Storybook stories with play functions. Unit tests are reserved for utilities and hooks only. +**Note**: All component behavior is tested in Storybook stories with play functions. Documentation tests (`.docs.spec.tsx`) provide consumer-facing examples. ### 🎨 Adding Styling to Components @@ -178,6 +182,8 @@ component-name/ ├── component-name.i18n.ts # i18n messages (if needed) ├── component-name.stories.tsx # Storybook stories (required) ├── component-name.mdx # Documentation (required) +├── component-name.dev.mdx # Engineering guide (optional) +├── component-name.docs.spec.tsx # Documentation tests (optional, recommended) ├── components/ # Compound parts (if compound) │ ├── component-name.root.tsx │ ├── component-name.part.tsx @@ -216,6 +222,8 @@ component-name/ | i18n | `{component-name}.i18n.ts` | `button.i18n.ts` | | Stories | `{component-name}.stories.tsx` | `button.stories.tsx` | | Documentation | `{component-name}.mdx` | `button.mdx` | +| Engineering Docs | `{component-name}.dev.mdx` | `button.dev.mdx` | +| Documentation Tests | `{component-name}.docs.spec.tsx` | `button.docs.spec.tsx` | ### Import Conventions diff --git a/docs/engineering-docs-validation.md b/docs/engineering-docs-validation.md new file mode 100644 index 000000000..97657af71 --- /dev/null +++ b/docs/engineering-docs-validation.md @@ -0,0 +1,380 @@ +# Engineering Documentation Test Integration + +This guide explains how test code examples in engineering documentation (`.dev.mdx` files) are automatically kept up-to-date using real, executable tests. + +## Overview + +Engineering documentation (`.dev.mdx` files) includes test code examples showing consumers how to test components. These examples are now **automatically generated from real test files**, ensuring they're always valid and up-to-date. + +## How It Works + +### The Inverted Strategy + +Instead of extracting tests from documentation, we **inject tests into documentation** at build time: + +1. **Tests live in `.docs.spec.tsx` files** - Real, executable test files colocated with components +2. **Tests run with normal test suite** - No special CLI workflow needed +3. **Build extracts test sections** - TypeScript AST parser finds tagged sections +4. **Documentation generated** - Test code injected into MDX at build time + +**Key Benefit:** Tests are the source of truth. Documentation is derived from working tests. + +## Writing Documentation Tests + +### 1. Create Test File + +Create a `.docs.spec.tsx` file alongside your component: + +``` +text-input/ +├── text-input.tsx +├── text-input.types.ts +├── text-input.dev.mdx # Developer guide +├── text-input.docs.spec.tsx # NEW: Tests for documentation +└── text-input.stories.tsx # Storybook tests +``` + +### 2. Tag Test Sections with JSDoc + +Use JSDoc tags to mark test sections for documentation: + +```typescript +import { describe, it, expect, vi } from 'vitest'; +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { TextInput, NimbusProvider } from '@commercetools/nimbus'; + +/** + * @docs-section basic-rendering + * @docs-title Basic Rendering Tests + * @docs-description Verify the component renders with expected elements + * @docs-order 1 + */ +describe('TextInput - Basic rendering', () => { + it('renders input element', () => { + render( + + + + ); + + expect(screen.getByRole('textbox')).toBeInTheDocument(); + }); + + it('renders with placeholder text', () => { + render( + + + + ); + + expect(screen.getByPlaceholderText('Email')).toBeInTheDocument(); + }); +}); + +/** + * @docs-section interactions + * @docs-title Interaction Tests + * @docs-description Test user interactions with the component + * @docs-order 2 + */ +describe('TextInput - Interactions', () => { + it('updates value when user types', async () => { + const user = userEvent.setup(); + render( + + + + ); + + const input = screen.getByRole('textbox'); + await user.type(input, 'Hello World'); + + expect(input).toHaveValue('Hello World'); + }); +}); +``` + +### 3. Add Injection Token to MDX + +In your `.dev.mdx` file, add a single token where tests should appear: + +```mdx +## Testing your implementation + +These examples demonstrate how to test your implementation when using ComponentName in your application. + +{{docs-tests: component-name.docs.spec.tsx}} + +## Additional testing considerations + +[Manual content about edge cases, patterns, etc.] +``` + +### 4. Build Documentation + +When you build docs, test sections are automatically extracted and injected: + +```bash +pnpm build:docs +``` + +The build process: +1. Finds `{{docs-tests:}}` tokens in MDX +2. Locates companion `.docs.spec.tsx` file +3. Parses TypeScript AST +4. Extracts sections with JSDoc tags +5. Generates markdown sections with code blocks +6. Cleans code (removes test infrastructure) +7. Injects into documentation + +## JSDoc Tag Reference + +### Required Tags + +| Tag | Purpose | Example | +|-----|---------|---------| +| `@docs-section` | Unique identifier for the section | `@docs-section basic-rendering` | +| `@docs-title` | Display title in documentation | `@docs-title Basic Rendering Tests` | +| `@docs-description` | Brief description of what tests demonstrate | `@docs-description Verify component renders correctly` | +| `@docs-order` | Sort order in documentation (lower = earlier) | `@docs-order 1` | + +### Example Usage + +```typescript +/** + * @docs-section controlled-mode + * @docs-title Testing Controlled Mode + * @docs-description Test controlled component behavior + * @docs-order 3 + */ +describe('Component - Controlled mode', () => { + // Tests here +}); +``` + +## Code Transparency + +The build process shows **full, unmodified test code** in documentation. What you write in `.docs.spec.tsx` files is exactly what consumers see. + +### What Consumers See + +All code is preserved as-is, including: + +✅ **Complete test setup:** +- `renderWithProvider()` helper function +- `NimbusProvider` imports +- `ReactNode` type imports +- All test infrastructure + +✅ **Full context:** +- Exact imports you use +- Helper functions defined +- Complete, copy-paste ready examples + +### Example + +**In test file (what you write):** + +```typescript +import { describe, it, expect } from 'vitest'; +import { render, screen } from '@testing-library/react'; +import { TextInput, NimbusProvider } from '@commercetools/nimbus'; + +describe('Tests', () => { + it('works', () => { + render( + + + + ); + + expect(screen.getByRole('textbox')).toBeInTheDocument(); + }); +}); +``` + +**In documentation (what consumers see):** + +```tsx +import { describe, it, expect } from 'vitest'; +import { render, screen } from '@testing-library/react'; +import { TextInput, NimbusProvider } from '@commercetools/nimbus'; + +describe('Tests', () => { + it('works', () => { + render( + + + + ); + + expect(screen.getByRole('textbox')).toBeInTheDocument(); + }); +}); +``` + +**Result:** Exactly the same! No transformations, no hidden setup. Complete transparency. + +## Best Practices + +### ✅ DO + +- **Keep examples simple and focused** - One concept per test section +- **Use realistic test patterns** - Show what consumers would actually write +- **Include common testing scenarios** - Basic rendering, interactions, states +- **Wrap every render with ``** - Shows consumers the required setup explicitly +- **Group related tests** - Use describe blocks with clear names +- **Order logically** - Use `@docs-order` to sequence from simple to complex +- **Be explicit about boilerplate** - Don't hide setup requirements in helpers + +### ❌ DON'T + +- **Include overly complex test setups** - Keep examples accessible +- **Use internal implementation details** - Focus on public API +- **Add test utilities not available to consumers** - Stick to standard libraries +- **Skip JSDoc tags** - All required tags must be present +- **Duplicate test logic** - One source of truth in `.docs.spec.tsx` + +## File Naming Convention + +| File Type | Purpose | Example | +|-----------|---------|---------| +| `.docs.spec.tsx` | Tests for documentation examples | `text-input.docs.spec.tsx` | +| `.spec.tsx` | Internal unit tests (not shown in docs) | `text-input.spec.tsx` | +| `.stories.tsx` | Storybook interaction tests | `text-input.stories.tsx` | + +**Note:** `.docs.spec.tsx` tests are discovered automatically by Vitest and run with the normal test suite. + +## Running Tests + +Documentation tests run normally with existing commands: + +```bash +# Run all tests (includes docs tests) +pnpm test + +# Run only unit tests (includes docs tests) +pnpm test:unit + +# Run specific docs test file +pnpm test:unit text-input.docs.spec.tsx + +# Run in watch mode +pnpm test:unit --watch +``` + +**No special CLI workflow needed!** + +## Verification Checklist + +When creating or updating documentation tests: + +- [ ] Test file named `{component}.docs.spec.tsx` +- [ ] File colocated with component +- [ ] All describe blocks have JSDoc tags (`@docs-section`, `@docs-title`, `@docs-description`, `@docs-order`) +- [ ] Every `render()` call wraps component with `` +- [ ] NimbusProvider imported from `@commercetools/nimbus` +- [ ] MDX file has `{{docs-tests: filename}}` token +- [ ] Tests pass when run with `pnpm test:unit` +- [ ] Documentation builds without errors +- [ ] Generated docs show full code including provider (no cleaning) + +## Troubleshooting + +### Tests Fail Locally + +**Issue:** Tests fail with "useContext returned undefined" errors + +**Solution:** Ensure every `render()` call wraps the component with ``: + +```typescript +// ✅ Correct - Provider wrapper included +describe('Tests', () => { + it('works', () => { + render( + + + + ); + }); +}); + +// ❌ Wrong - Missing provider +describe('Tests', () => { + it('works', () => { + render(); // Will fail with context error! + }); +}); +``` + +### Documentation Not Updating + +**Issue:** Changes to test file don't appear in docs + +**Solution:** +1. Rebuild nimbus-docs-build package: `pnpm --filter @commercetools/nimbus-docs-build build` +2. Rebuild documentation: `pnpm build:docs` +3. Restart dev server if running + +### Token Not Replaced + +**Issue:** `{{docs-tests:}}` token appears in documentation + +**Possible causes:** +- Test file not found (check filename matches exactly) +- No sections found (add `@docs-section` JSDoc tags) +- Syntax error in test file (run tests to verify) + +**Debug:** +```bash +# Check console output during docs build for warnings +pnpm build:docs + +# Look for messages like: +# "Test file not found: ..." +# "No test sections found in ..." +``` + +### Code Appears Malformed + +**Issue:** Generated code has syntax errors + +**Solution:** +- Verify test file has valid JSX syntax +- Ensure all imports are valid +- Run tests to confirm code is executable + +## Migration from Old System + +If migrating from the HTML comment tag system: + +1. **Extract test code** from `` tags in `.dev.mdx` +2. **Create `.docs.spec.tsx`** file with extracted tests +3. **Add JSDoc tags** to each describe block +4. **Wrap all render calls** with `` explicitly +5. **Add NimbusProvider import** to imports +6. **Replace test sections** in MDX with `{{docs-tests: filename}}` +7. **Verify** tests pass and docs build correctly + +## Example: Complete TextInput Implementation + +See `packages/nimbus/src/components/text-input/` for a complete reference: + +- `text-input.docs.spec.tsx` - Test file with 5 sections, 14 tests +- `text-input.dev.mdx` - Uses `{{docs-tests: text-input.docs.spec.tsx}}` +- Generated docs at `apps/docs/src/data/routes/components-inputs-textinput.json` + +## Benefits + +✅ **Always up-to-date** - Tests ARE the examples +✅ **Type-safe** - TypeScript catches API changes +✅ **IDE support** - Full autocomplete and type checking +✅ **No duplication** - Single source of truth +✅ **Normal workflow** - Tests run with `pnpm test` +✅ **Build-time validation** - Missing tests fail docs build +✅ **Clean examples** - Infrastructure removed automatically + +--- + +Last updated: January 2025 diff --git a/docs/file-type-guidelines/unit-testing.md b/docs/file-type-guidelines/unit-testing.md index 3cbd5c90f..f36fd5796 100644 --- a/docs/file-type-guidelines/unit-testing.md +++ b/docs/file-type-guidelines/unit-testing.md @@ -11,28 +11,44 @@ Unit test files (`{utility-name}.spec.ts` or `{hook-name}.spec.ts`) provide fast ## When to Use -Unit tests are exclusively for non-component code: +Unit tests are used for: - **Utility functions and helpers** - Pure functions, formatters, validators, data transformers - **React hooks** - Custom hooks tested in isolation with `renderHook` - **Business logic** - Calculations, algorithms, data processing - **Validation functions** - Input validators, schema validators - **Helper modules** - String manipulation, date formatting, number formatting +- **Documentation examples** - Consumer-facing test patterns (in `.docs.spec.tsx` files) -**For components**: Use Storybook stories with play functions to test all component behavior, interactions, visual states, and accessibility. +**For component behavior testing**: Use Storybook stories with play functions to test all component interactions, visual states, and accessibility. -### Unit Tests vs Storybook Tests +### Documentation Tests (`.docs.spec.tsx`) -| Aspect | Unit Tests | Storybook Tests | -|--------|------------|-----------------| -| **Environment** | JSDOM (fast, simulated) | Real browser (accurate) | -| **Purpose** | Utility/hook logic verification | Component behavior & interactions | -| **Speed** | Very fast (~ms per test) | Slower (~seconds per story) | -| **Focus** | Pure functions, hooks | UI, user flows, visual states, a11y | -| **Use For** | Utilities, hooks only | ALL components | -| **Required** | Only for utilities/hooks | For ALL interactive components | +A special category of unit tests used for engineering documentation: -**Testing Strategy**: Components are testable in JSDOM (for consumers using JSDOM), but Nimbus tests all component behavior exclusively in Storybook stories with play functions. +- **Purpose**: Provide real, working test examples for consumers +- **Location**: Colocated with components (e.g., `text-input.docs.spec.tsx`) +- **Integration**: Automatically injected into `.dev.mdx` documentation at build time +- **Workflow**: Write once, used in both test suite AND documentation + +See [Engineering Documentation Test Integration](../engineering-docs-validation.md) for complete details. + +### Unit Tests vs Storybook Tests vs Documentation Tests + +| Aspect | Unit Tests | Storybook Tests | Documentation Tests | +|--------|------------|-----------------|---------------------| +| **Environment** | JSDOM (fast, simulated) | Real browser (accurate) | JSDOM (consumer-friendly) | +| **Purpose** | Utility/hook logic verification | Component behavior & interactions | Consumer test examples | +| **Speed** | Very fast (~ms per test) | Slower (~seconds per story) | Very fast (~ms per test) | +| **Focus** | Pure functions, hooks | UI, user flows, visual states, a11y | Integration patterns, public API | +| **Use For** | Utilities, hooks | ALL components | Documentation examples | +| **File Pattern** | `*.spec.{ts,tsx}` | `*.stories.tsx` | `*.docs.spec.tsx` | +| **Audience** | Internal developers | Internal developers | External consumers | + +**Testing Strategy**: +- **Nimbus internal testing**: Storybook stories test all component behavior +- **Consumer documentation**: `.docs.spec.tsx` tests demonstrate integration patterns +- **Utility testing**: Standard `.spec.ts` files test helper functions and hooks ## Testing Infrastructure diff --git a/packages/nimbus-docs-build/package.json b/packages/nimbus-docs-build/package.json index a71d072c6..f502925d6 100644 --- a/packages/nimbus-docs-build/package.json +++ b/packages/nimbus-docs-build/package.json @@ -25,6 +25,8 @@ "typecheck": "tsc --noEmit" }, "dependencies": { + "@typescript-eslint/types": "^8.48.0", + "@typescript-eslint/typescript-estree": "^8.48.0", "gray-matter": "^4.0.3", "react-docgen-typescript": "^2.4.0", "remark": "^15.0.1", diff --git a/packages/nimbus-docs-build/src/parsers/index.ts b/packages/nimbus-docs-build/src/parsers/index.ts index 6397f7529..eda1729f6 100644 --- a/packages/nimbus-docs-build/src/parsers/index.ts +++ b/packages/nimbus-docs-build/src/parsers/index.ts @@ -5,3 +5,4 @@ export * from "./parse-mdx.js"; export * from "./parse-types.js"; export * from "./filter-props.js"; export * from "./process-types.js"; +export * from "./test-extractor.js"; diff --git a/packages/nimbus-docs-build/src/parsers/parse-mdx.ts b/packages/nimbus-docs-build/src/parsers/parse-mdx.ts index 1722deeaf..ecf0d76f2 100644 --- a/packages/nimbus-docs-build/src/parsers/parse-mdx.ts +++ b/packages/nimbus-docs-build/src/parsers/parse-mdx.ts @@ -16,6 +16,125 @@ import { mdxDocumentSchema } from "../schemas/mdx-document.js"; import type { MdxDocument, TocItem } from "../types/mdx.js"; import { menuToPath, getPathFromMonorepoRoot } from "../utils/index.js"; import { validateFilePath } from "../utils/validate-file-path.js"; +import { extractTestSections } from "./test-extractor.js"; +import type { TestSection } from "../types/test-section.js"; + +/** + * Resolve the path to a .docs.spec.ts file from an MDX file path + * @param mdxFilePath - Absolute path to the MDX file + * @param testFileName - Name of the test file from the {{docs-tests:}} token + * @returns Absolute path to the test file + */ +const resolveTestFilePath = ( + mdxFilePath: string, + testFileName: string +): string => { + const mdxDir = path.dirname(mdxFilePath); + return path.join(mdxDir, testFileName); +}; + +/** + * Generate markdown section from a test section + * @param section - Extracted test section + * @returns Markdown string with heading, description, and code block + */ +const generateMarkdownSection = (section: TestSection): string => { + const lines: string[] = []; + + // Add heading + lines.push(`### ${section.title}`); + lines.push(""); + + // Add description if present + if (section.description) { + lines.push(section.description); + lines.push(""); + } + + // Add code block with full, unmodified code + lines.push("```tsx"); + + // Add imports (deduplicated) + const uniqueImports = Array.from(new Set(section.imports)); + if (uniqueImports.length > 0) { + lines.push(uniqueImports.join("\n")); + lines.push(""); + } + + // Add test code (unmodified - show exactly what developers write) + lines.push(section.code); + lines.push("```"); + lines.push(""); + + return lines.join("\n"); +}; + +/** + * Inject test sections into MDX content by replacing {{docs-tests:}} tokens + * @param mdxContent - MDX content that may contain {{docs-tests:}} tokens + * @param mdxFilePath - Absolute path to the MDX file (for resolving test file paths) + * @returns MDX content with test sections injected + */ +const injectTestSections = async ( + mdxContent: string, + mdxFilePath: string +): Promise => { + // Pattern: {{docs-tests: filename.docs.spec.ts}} + const tokenPattern = /\{\{docs-tests:\s*([^}]+)\}\}/g; + + let result = mdxContent; + const matches = mdxContent.matchAll(tokenPattern); + + for (const match of matches) { + const [fullMatch, testFileName] = match; + + try { + // Resolve test file path + const testFilePath = resolveTestFilePath( + mdxFilePath, + testFileName.trim() + ); + + // Check if test file exists + try { + await fs.access(testFilePath); + } catch { + console.warn( + `Test file not found: ${testFilePath} (referenced in ${path.basename(mdxFilePath)})` + ); + // Leave token in place if file doesn't exist + continue; + } + + // Extract test sections + const sections = await extractTestSections(testFilePath); + + if (sections.length === 0) { + console.warn( + `No test sections found in ${testFileName} (add @docs-section JSDoc tags)` + ); + // Leave token in place if no sections found + continue; + } + + // Generate markdown for all sections + const sectionMarkdown = sections + .map((section) => generateMarkdownSection(section)) + .join("\n"); + + // Replace token with generated markdown + result = result.replace(fullMatch, sectionMarkdown); + } catch (error) { + console.error( + `Error processing test file ${testFileName}:`, + error instanceof Error ? error.message : error + ); + // Leave token in place on error + } + } + + return result; +}; /** * Generate table of contents from MDX content (without frontmatter) @@ -80,10 +199,18 @@ const parseSingleMdx = async ( } | null> => { try { const content = await fs.readFile(filePath, "utf8"); - const { data: frontmatter, content: mdx } = matter(content) as unknown as { + const { data: frontmatter, content: mdxContent } = matter( + content + ) as unknown as { data: Record; content: string; }; + + // Inject test sections if {{docs-tests:}} tokens are present + const mdx = mdxContent.includes("{{docs-tests:") + ? await injectTestSections(mdxContent, filePath) + : mdxContent; + const toc = await generateToc(mdx); return { mdx, toc, frontmatter }; } catch { @@ -117,11 +244,16 @@ export async function parseMdxFile( // Read main file content const content = await fs.readFile(filePath, "utf8"); - const { data: meta, content: mdx } = matter(content) as unknown as { + const { data: meta, content: mdxContent } = matter(content) as unknown as { data: Record; content: string; }; + // Inject test sections if {{docs-tests:}} tokens are present + const mdx = mdxContent.includes("{{docs-tests:") + ? await injectTestSections(mdxContent, filePath) + : mdxContent; + // Generate TOC for main file const toc = await generateToc(mdx); diff --git a/packages/nimbus-docs-build/src/parsers/test-extractor.ts b/packages/nimbus-docs-build/src/parsers/test-extractor.ts new file mode 100644 index 000000000..2d6648f85 --- /dev/null +++ b/packages/nimbus-docs-build/src/parsers/test-extractor.ts @@ -0,0 +1,217 @@ +import { readFile } from "node:fs/promises"; +import { parse } from "@typescript-eslint/typescript-estree"; +import type { TSESTree } from "@typescript-eslint/types"; +import type { + TestSection, + TestSectionMetadata, +} from "../types/test-section.js"; + +/** + * Extracts test sections from a .docs.spec.ts file for documentation injection + * + * Finds describe() blocks with JSDoc @docs-section tags and extracts: + * - Metadata from JSDoc tags + * - Full source code of the describe block + * - Required imports + * + * @param testFilePath - Absolute path to the .docs.spec.ts file + * @returns Array of test sections sorted by order + */ +export async function extractTestSections( + testFilePath: string +): Promise { + // Read the test file + const sourceCode = await readFile(testFilePath, "utf-8"); + const lines = sourceCode.split("\n"); + + // Parse TypeScript to AST (with JSX support) + const ast = parse(sourceCode, { + loc: true, + range: true, + comment: true, + tokens: false, + jsx: true, // Enable JSX parsing for .tsx files + }); + + // Extract all import statements + const imports = extractImports(ast, lines); + + // Find all describe() blocks with @docs-section tags + const sections: TestSection[] = []; + + if (ast.comments) { + // Visit all nodes in the AST + visit(ast, (node) => { + // Find describe() call expressions + if ( + node.type === "ExpressionStatement" && + node.expression.type === "CallExpression" && + node.expression.callee.type === "Identifier" && + node.expression.callee.name === "describe" + ) { + // Look for preceding JSDoc comment + const comment = findPrecedingComment(node, ast.comments || []); + + if (comment) { + const metadata = parseJSDocMetadata(comment.value); + + // Only include if has @docs-section tag + if (metadata.section) { + const { code, startLine, endLine } = extractCodeBlock(node, lines); + + sections.push({ + id: metadata.section, + title: metadata.title || metadata.section, + description: metadata.description || "", + order: metadata.order ?? 999, + code, + imports, + sourceFile: testFilePath, + startLine, + endLine, + }); + } + } + } + }); + } + + // Sort by order + return sections.sort((a, b) => a.order - b.order); +} + +/** + * Extract all import statements from the AST + */ +function extractImports(ast: TSESTree.Program, lines: string[]): string[] { + const imports: string[] = []; + + for (const node of ast.body) { + if (node.type === "ImportDeclaration" && node.loc) { + // Extract the full import statement from source + const importLines = lines.slice( + node.loc.start.line - 1, + node.loc.end.line + ); + imports.push(importLines.join("\n")); + } + } + + return imports; +} + +/** + * Find the JSDoc comment immediately preceding a node + */ +function findPrecedingComment( + node: TSESTree.Node, + comments: TSESTree.Comment[] +): TSESTree.Comment | null { + if (!node.loc) return null; + + // Find the last comment that ends before this node starts + let precedingComment: TSESTree.Comment | null = null; + + for (const comment of comments) { + if (!comment.loc) continue; + + // Comment must end before node starts + if (comment.loc.end.line < node.loc.start.line) { + // Must be a JSDoc comment (block comment starting with **) + if (comment.type === "Block" && comment.value.startsWith("*")) { + precedingComment = comment; + } + } + } + + return precedingComment; +} + +/** + * Parse JSDoc tags from a comment + */ +function parseJSDocMetadata(commentValue: string): TestSectionMetadata { + const metadata: TestSectionMetadata = {}; + + // Remove leading * from each line + const lines = commentValue + .split("\n") + .map((line) => line.trim().replace(/^\*\s?/, "")); + + for (const line of lines) { + // Extract @docs-section + const sectionMatch = line.match(/@docs-section\s+(\S+)/); + if (sectionMatch) { + metadata.section = sectionMatch[1]; + } + + // Extract @docs-title + const titleMatch = line.match(/@docs-title\s+(.+)/); + if (titleMatch) { + metadata.title = titleMatch[1].trim(); + } + + // Extract @docs-description + const descriptionMatch = line.match(/@docs-description\s+(.+)/); + if (descriptionMatch) { + metadata.description = descriptionMatch[1].trim(); + } + + // Extract @docs-order + const orderMatch = line.match(/@docs-order\s+(\d+)/); + if (orderMatch) { + metadata.order = parseInt(orderMatch[1], 10); + } + } + + return metadata; +} + +/** + * Extract the full source code of a describe() block + */ +function extractCodeBlock( + node: TSESTree.Node, + lines: string[] +): { code: string; startLine: number; endLine: number } { + if (!node.loc) { + return { code: "", startLine: 0, endLine: 0 }; + } + + const startLine = node.loc.start.line; + const endLine = node.loc.end.line; + + // Extract lines (loc is 1-indexed, array is 0-indexed) + const codeLines = lines.slice(startLine - 1, endLine); + const code = codeLines.join("\n"); + + return { code, startLine, endLine }; +} + +/** + * Simple AST visitor helper + */ +function visit( + node: TSESTree.Node | TSESTree.Program, + callback: (node: TSESTree.Node) => void +): void { + callback(node as TSESTree.Node); + + // Recursively visit all child nodes + for (const key of Object.keys(node)) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const value = (node as any)[key]; + + if (value && typeof value === "object") { + if (Array.isArray(value)) { + for (const item of value) { + if (item && typeof item === "object" && "type" in item) { + visit(item, callback); + } + } + } else if ("type" in value) { + visit(value, callback); + } + } + } +} diff --git a/packages/nimbus-docs-build/src/types/index.ts b/packages/nimbus-docs-build/src/types/index.ts index 49096b914..9145b5d2f 100644 --- a/packages/nimbus-docs-build/src/types/index.ts +++ b/packages/nimbus-docs-build/src/types/index.ts @@ -3,4 +3,5 @@ */ export * from "./mdx.js"; export * from "./config.js"; +export * from "./test-section.js"; export type { LifecycleState } from "../schemas/lifecycle-states.js"; diff --git a/packages/nimbus-docs-build/src/types/test-section.ts b/packages/nimbus-docs-build/src/types/test-section.ts new file mode 100644 index 000000000..efc818f08 --- /dev/null +++ b/packages/nimbus-docs-build/src/types/test-section.ts @@ -0,0 +1,48 @@ +/** + * Test section extracted from .docs.spec.ts files for documentation injection + */ +export interface TestSection { + /** Unique identifier for the section (from @docs-section tag) */ + id: string; + + /** Display title for the section (from @docs-title tag) */ + title: string; + + /** Description explaining what these tests demonstrate (from @docs-description tag) */ + description: string; + + /** Sort order for display in documentation (from @docs-order tag) */ + order: number; + + /** Full source code of the describe block including tests */ + code: string; + + /** Import statements required for this test section */ + imports: string[]; + + /** Source file path for debugging and error messages */ + sourceFile: string; + + /** Line number where this section starts in the source file */ + startLine: number; + + /** Line number where this section ends in the source file */ + endLine: number; +} + +/** + * Parsed JSDoc tags from test describe blocks + */ +export interface TestSectionMetadata { + /** @docs-section tag value */ + section?: string; + + /** @docs-title tag value */ + title?: string; + + /** @docs-description tag value */ + description?: string; + + /** @docs-order tag value (parsed to number) */ + order?: number; +} diff --git a/packages/nimbus/src/components/text-input/text-input.dev.mdx b/packages/nimbus/src/components/text-input/text-input.dev.mdx index 7a21e1789..90add0501 100644 --- a/packages/nimbus/src/components/text-input/text-input.dev.mdx +++ b/packages/nimbus/src/components/text-input/text-input.dev.mdx @@ -375,253 +375,7 @@ const App = () => { These examples demonstrate how to test your implementation when using TextInput in your application. The component's internal functionality is already tested by Nimbus - these patterns help you verify your integration and application-specific logic. -### Basic rendering tests - -Verify the component renders with expected elements: - -```tsx -import { render, screen } from '@testing-library/react'; -import { TextInput } from '@commercetools/nimbus'; - -describe('TextInput', () => { - it('renders input element', () => { - render(); - - // Verify input is present - expect(screen.getByRole('textbox')).toBeInTheDocument(); - }); - - it('renders with placeholder text', () => { - render(); - - expect(screen.getByPlaceholderText('Email address')).toBeInTheDocument(); - }); - - it('renders with aria-label', () => { - render(); - - expect(screen.getByRole('textbox', { name: /user email/i })).toBeInTheDocument(); - }); -}); -``` - -### Interaction tests - -Test user interactions with the component: - -```tsx -import { render, screen } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; -import { TextInput } from '@commercetools/nimbus'; - -describe('TextInput interactions', () => { - it('updates value when user types', async () => { - const user = userEvent.setup(); - render(); - - const input = screen.getByRole('textbox'); - await user.type(input, 'Hello World'); - - expect(input).toHaveValue('Hello World'); - }); - - it('calls onChange callback with string value', async () => { - const user = userEvent.setup(); - const handleChange = jest.fn(); - render(); - - const input = screen.getByRole('textbox'); - await user.type(input, 'test'); - - expect(handleChange).toHaveBeenCalled(); - - expect(typeof handleChange.mock.calls[0][0]).toBe('string'); - }); - - it('clears input when clear button is clicked', async () => { - const user = userEvent.setup(); - const TestComponent = () => { - const [value, setValue] = React.useState('initial'); - return ( - setValue(value)} - trailingElement={ - - } - /> - ); - }; - - render(); - - const input = screen.getByRole('textbox'); - expect(input).toHaveValue('initial'); - - await user.click(screen.getByText('Clear')); - - expect(input).toHaveValue(''); - }); -}); -``` - -### Testing controlled mode - -Test controlled component behavior: - -```tsx -import { render, screen } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; -import { TextInput } from '@commercetools/nimbus'; - -describe('TextInput controlled mode', () => { - it('displays controlled value', () => { - render( {}} />); - - const input = screen.getByRole('textbox'); - expect(input).toHaveValue('controlled value'); - }); - - it('updates when controlled value changes', () => { - const { rerender } = render( - {}} /> - ); - - expect(screen.getByRole('textbox')).toHaveValue('first value'); - - rerender( {}} />); - - expect(screen.getByRole('textbox')).toHaveValue('second value'); - }); - - it('validates input in controlled mode', async () => { - const user = userEvent.setup(); - const TestComponent = () => { - const [value, setValue] = React.useState(''); - const [isValid, setIsValid] = React.useState(true); - - const handleChange = (newValue: string) => { - setValue(newValue); - setIsValid(newValue.length >= 3); - }; - - return ( - <> - - {!isValid && Must be at least 3 characters} - - ); - }; - - render(); - - const input = screen.getByRole('textbox'); - await user.type(input, 'ab'); - - expect(screen.getByText('Must be at least 3 characters')).toBeInTheDocument(); - - await user.type(input, 'c'); - - expect(screen.queryByText('Must be at least 3 characters')).not.toBeInTheDocument(); - }); -}); -``` - -### Testing leading and trailing elements - -Test custom elements within the input: - -```tsx -import { render, screen } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; -import { TextInput } from '@commercetools/nimbus'; - -describe('TextInput with elements', () => { - it('renders leading element', () => { - render( - 🔍} - placeholder="Search" - /> - ); - - expect(screen.getByTestId('icon')).toBeInTheDocument(); - }); - - it('renders trailing element', () => { - render( - Clear - } - placeholder="Input" - /> - ); - - expect(screen.getByTestId('clear')).toBeInTheDocument(); - }); - - it('trailing button is interactive', async () => { - const user = userEvent.setup(); - const handleClick = jest.fn(); - - render( - Action - } - /> - ); - - await user.click(screen.getByText('Action')); - - expect(handleClick).toHaveBeenCalledTimes(1); - }); -}); -``` - -### Testing validation states - -Test different validation states: - -```tsx -import { render, screen } from '@testing-library/react'; -import { TextInput } from '@commercetools/nimbus'; - -describe('TextInput validation states', () => { - it('renders disabled state', () => { - render(); - - const input = screen.getByRole('textbox'); - expect(input).toBeDisabled(); - }); - - it('renders invalid state', () => { - render(); - - const input = screen.getByRole('textbox'); - expect(input).toHaveAttribute('aria-invalid', 'true'); - }); - - it('renders read-only state', () => { - render( {}} />); - - const input = screen.getByRole('textbox'); - expect(input).toHaveAttribute('readonly'); - }); - - it('renders required state', () => { - render(); - - const input = screen.getByRole('textbox'); - expect(input).toHaveAttribute('aria-required', 'true'); - }); -}); -``` +{{docs-tests: text-input.docs.spec.tsx}} ## Resources diff --git a/packages/nimbus/src/components/text-input/text-input.docs.spec.tsx b/packages/nimbus/src/components/text-input/text-input.docs.spec.tsx new file mode 100644 index 000000000..4c473fe67 --- /dev/null +++ b/packages/nimbus/src/components/text-input/text-input.docs.spec.tsx @@ -0,0 +1,223 @@ +import { describe, it, expect, vi } from "vitest"; +import { render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { TextInput, NimbusProvider } from "@commercetools/nimbus"; + +/** + * @docs-section basic-rendering + * @docs-title Basic Rendering Tests + * @docs-description Verify the component renders with expected elements + * @docs-order 1 + */ +describe("TextInput - Basic rendering", () => { + it("renders input element", () => { + render( + + + + ); + + // Verify input is present + expect(screen.getByRole("textbox")).toBeInTheDocument(); + }); + + it("renders with placeholder text", () => { + render( + + + + ); + + expect(screen.getByPlaceholderText("Email address")).toBeInTheDocument(); + }); + + it("renders with aria-label", () => { + render( + + + + ); + + expect( + screen.getByRole("textbox", { name: /user email/i }) + ).toBeInTheDocument(); + }); +}); + +/** + * @docs-section interactions + * @docs-title Interaction Tests + * @docs-description Test user interactions with the component + * @docs-order 2 + */ +describe("TextInput - Interactions", () => { + it("updates value when user types", async () => { + const user = userEvent.setup(); + render( + + + + ); + + const input = screen.getByRole("textbox"); + await user.type(input, "Hello World"); + + expect(input).toHaveValue("Hello World"); + }); + + it("calls onChange callback with string value", async () => { + const user = userEvent.setup(); + const handleChange = vi.fn(); + render( + + + + ); + + const input = screen.getByRole("textbox"); + await user.type(input, "test"); + + expect(handleChange).toHaveBeenCalled(); + expect(typeof handleChange.mock.calls[0][0]).toBe("string"); + }); +}); + +/** + * @docs-section controlled-mode + * @docs-title Testing Controlled Mode + * @docs-description Test controlled component behavior + * @docs-order 3 + */ +describe("TextInput - Controlled mode", () => { + it("displays controlled value", () => { + render( + + {}} /> + + ); + + const input = screen.getByRole("textbox"); + expect(input).toHaveValue("controlled value"); + }); + + it("updates when controlled value changes", () => { + const { rerender } = render( + + {}} /> + + ); + + expect(screen.getByRole("textbox")).toHaveValue("first value"); + + rerender( + + {}} /> + + ); + + expect(screen.getByRole("textbox")).toHaveValue("second value"); + }); +}); + +/** + * @docs-section leading-trailing-elements + * @docs-title Testing Leading and Trailing Elements + * @docs-description Test custom elements within the input + * @docs-order 4 + */ +describe("TextInput - Elements", () => { + it("renders leading element", () => { + render( + + 🔍} + placeholder="Search" + /> + + ); + + expect(screen.getByTestId("icon")).toBeInTheDocument(); + }); + + it("renders trailing element", () => { + render( + + Clear} + placeholder="Input" + /> + + ); + + expect(screen.getByTestId("clear")).toBeInTheDocument(); + }); + + it("trailing button is interactive", async () => { + const user = userEvent.setup(); + const handleClick = vi.fn(); + + render( + + Action} + /> + + ); + + await user.click(screen.getByText("Action")); + + expect(handleClick).toHaveBeenCalled(); + }); +}); + +/** + * @docs-section validation-states + * @docs-title Testing Validation States + * @docs-description Test different validation states + * @docs-order 5 + */ +describe("TextInput - Validation states", () => { + it("renders disabled state", () => { + render( + + + + ); + + const input = screen.getByRole("textbox"); + expect(input).toBeDisabled(); + }); + + it("renders invalid state", () => { + render( + + + + ); + + const input = screen.getByRole("textbox"); + expect(input).toHaveAttribute("aria-invalid", "true"); + }); + + it("renders read-only state", () => { + render( + + {}} /> + + ); + + const input = screen.getByRole("textbox"); + expect(input).toHaveAttribute("readonly"); + }); + + it("renders required state", () => { + render( + + + + ); + + const input = screen.getByRole("textbox"); + expect(input).toHaveAttribute("aria-required", "true"); + }); +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fc03a0e09..f43702d81 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -655,6 +655,12 @@ importers: packages/nimbus-docs-build: dependencies: + '@typescript-eslint/types': + specifier: ^8.48.0 + version: 8.48.0 + '@typescript-eslint/typescript-estree': + specifier: ^8.48.0 + version: 8.48.0(typescript@5.9.3) gray-matter: specifier: ^4.0.3 version: 4.0.3 @@ -2657,6 +2663,9 @@ packages: '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + '@types/lodash@4.17.20': + resolution: {integrity: sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA==} + '@types/lodash@4.17.21': resolution: {integrity: sha512-FOvQ0YPD5NOfPgMzJihoT+Za5pdkDJWcbpuj1DjaKZIr/gxodQjY/uWEFlTNqW2ugXHUiL8lRQgw63dzKHZdeQ==} @@ -2681,6 +2690,9 @@ packages: '@types/node@24.10.1': resolution: {integrity: sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==} + '@types/node@24.8.0': + resolution: {integrity: sha512-5x08bUtU8hfboMTrJ7mEO4CpepS9yBwAqcL52y86SWNmbPX8LVbNs3EP4cNrIZgdjk2NAlP2ahNihozpoZIxSg==} + '@types/parse-json@4.0.2': resolution: {integrity: sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==} @@ -2752,16 +2764,32 @@ packages: eslint: ^8.57.0 || ^9.0.0 typescript: ~5.9.3 + '@typescript-eslint/project-service@8.46.2': + resolution: {integrity: sha512-PULOLZ9iqwI7hXcmL4fVfIsBi6AN9YxRc0frbvmg8f+4hQAjQ5GYNKK0DIArNo+rOKmR/iBYwkpBmnIwin4wBg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: ~5.9.3 + '@typescript-eslint/project-service@8.48.0': resolution: {integrity: sha512-Ne4CTZyRh1BecBf84siv42wv5vQvVmgtk8AuiEffKTUo3DrBaGYZueJSxxBZ8fjk/N3DrgChH4TOdIOwOwiqqw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: ~5.9.3 + '@typescript-eslint/scope-manager@8.46.2': + resolution: {integrity: sha512-LF4b/NmGvdWEHD2H4MsHD8ny6JpiVNDzrSZr3CsckEgCbAGZbYM4Cqxvi9L+WqDMT+51Ozy7lt2M+d0JLEuBqA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/scope-manager@8.48.0': resolution: {integrity: sha512-uGSSsbrtJrLduti0Q1Q9+BF1/iFKaxGoQwjWOIVNJv0o6omrdyR8ct37m4xIl5Zzpkp69Kkmvom7QFTtue89YQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/tsconfig-utils@8.46.2': + resolution: {integrity: sha512-a7QH6fw4S57+F5y2FIxxSDyi5M4UfGF+Jl1bCGd7+L4KsaUY80GsiF/t0UoRFDHAguKlBaACWJRmdrc6Xfkkag==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: ~5.9.3 + '@typescript-eslint/tsconfig-utils@8.48.0': resolution: {integrity: sha512-WNebjBdFdyu10sR1M4OXTt2OkMd5KWIL+LLfeH9KhgP+jzfDV/LI3eXzwJ1s9+Yc0Kzo2fQCdY/OpdusCMmh6w==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -2775,16 +2803,33 @@ packages: eslint: ^8.57.0 || ^9.0.0 typescript: ~5.9.3 + '@typescript-eslint/types@8.46.2': + resolution: {integrity: sha512-lNCWCbq7rpg7qDsQrd3D6NyWYu+gkTENkG5IKYhUIcxSb59SQC/hEQ+MrG4sTgBVghTonNWq42bA/d4yYumldQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/types@8.48.0': resolution: {integrity: sha512-cQMcGQQH7kwKoVswD1xdOytxQR60MWKM1di26xSUtxehaDs/32Zpqsu5WJlXTtTTqyAVK8R7hvsUnIXRS+bjvA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/typescript-estree@8.46.2': + resolution: {integrity: sha512-f7rW7LJ2b7Uh2EiQ+7sza6RDZnajbNbemn54Ob6fRwQbgcIn+GWfyuHDHRYgRoZu1P4AayVScrRW+YfbTvPQoQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: ~5.9.3 + '@typescript-eslint/typescript-estree@8.48.0': resolution: {integrity: sha512-ljHab1CSO4rGrQIAyizUS6UGHHCiAYhbfcIZ1zVJr5nMryxlXMVWS3duFPSKvSUbFPwkXMFk1k0EMIjub4sRRQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: ~5.9.3 + '@typescript-eslint/utils@8.46.2': + resolution: {integrity: sha512-sExxzucx0Tud5tE0XqR0lT0psBQvEpnpiul9XbGUB1QwpWJJAps1O/Z7hJxLGiZLBKMCutjTzDgmd1muEhBnVg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: ~5.9.3 + '@typescript-eslint/utils@8.48.0': resolution: {integrity: sha512-yTJO1XuGxCsSfIVt1+1UrLHtue8xz16V8apzPYI06W0HbEbEWHxHXgZaAgavIkoh+GeV6hKKd5jm0sS6OYxWXQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -2792,6 +2837,10 @@ packages: eslint: ^8.57.0 || ^9.0.0 typescript: ~5.9.3 + '@typescript-eslint/visitor-keys@8.46.2': + resolution: {integrity: sha512-tUFMXI4gxzzMXt4xpGJEsBsTox0XbNQ1y94EwlD/CuZwFcQP79xfQqMhau9HsRc/J0cAPA/HZt1dZPtGn9V/7w==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/visitor-keys@8.48.0': resolution: {integrity: sha512-T0XJMaRPOH3+LBbAfzR2jalckP1MSG/L9eUtY0DEzUyVaXJ/t6zN0nR7co5kz0Jko/nkSYCBRkz1djvjajVTTg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -6116,6 +6165,9 @@ packages: undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + undici-types@7.14.0: + resolution: {integrity: sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA==} + undici-types@7.16.0: resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} @@ -6602,7 +6654,7 @@ snapshots: '@babel/helper-annotate-as-pure@7.27.3': dependencies: - '@babel/types': 7.28.5 + '@babel/types': 7.28.4 '@babel/helper-compilation-targets@7.27.2': dependencies: @@ -6661,7 +6713,7 @@ snapshots: '@babel/helper-optimise-call-expression@7.27.1': dependencies: - '@babel/types': 7.28.5 + '@babel/types': 7.28.4 '@babel/helper-plugin-utils@7.27.1': {} @@ -6676,8 +6728,8 @@ snapshots: '@babel/helper-skip-transparent-expression-wrappers@7.27.1': dependencies: - '@babel/traverse': 7.28.5 - '@babel/types': 7.28.5 + '@babel/traverse': 7.28.4 + '@babel/types': 7.28.4 transitivePeerDependencies: - supports-color @@ -6692,7 +6744,7 @@ snapshots: '@babel/helpers@7.28.4': dependencies: '@babel/template': 7.27.2 - '@babel/types': 7.28.5 + '@babel/types': 7.28.4 '@babel/parser@7.28.4': dependencies: @@ -6757,8 +6809,8 @@ snapshots: '@babel/template@7.27.2': dependencies: '@babel/code-frame': 7.27.1 - '@babel/parser': 7.28.5 - '@babel/types': 7.28.5 + '@babel/parser': 7.28.4 + '@babel/types': 7.28.4 '@babel/traverse@7.28.4': dependencies: @@ -8890,66 +8942,34 @@ snapshots: dependencies: '@babel/core': 7.28.4 - '@svgr/babel-plugin-add-jsx-attribute@8.0.0(@babel/core@7.28.5)': - dependencies: - '@babel/core': 7.28.5 - '@svgr/babel-plugin-remove-jsx-attribute@8.0.0(@babel/core@7.28.4)': dependencies: '@babel/core': 7.28.4 - '@svgr/babel-plugin-remove-jsx-attribute@8.0.0(@babel/core@7.28.5)': - dependencies: - '@babel/core': 7.28.5 - '@svgr/babel-plugin-remove-jsx-empty-expression@8.0.0(@babel/core@7.28.4)': dependencies: '@babel/core': 7.28.4 - '@svgr/babel-plugin-remove-jsx-empty-expression@8.0.0(@babel/core@7.28.5)': - dependencies: - '@babel/core': 7.28.5 - '@svgr/babel-plugin-replace-jsx-attribute-value@8.0.0(@babel/core@7.28.4)': dependencies: '@babel/core': 7.28.4 - '@svgr/babel-plugin-replace-jsx-attribute-value@8.0.0(@babel/core@7.28.5)': - dependencies: - '@babel/core': 7.28.5 - '@svgr/babel-plugin-svg-dynamic-title@8.0.0(@babel/core@7.28.4)': dependencies: '@babel/core': 7.28.4 - '@svgr/babel-plugin-svg-dynamic-title@8.0.0(@babel/core@7.28.5)': - dependencies: - '@babel/core': 7.28.5 - '@svgr/babel-plugin-svg-em-dimensions@8.0.0(@babel/core@7.28.4)': dependencies: '@babel/core': 7.28.4 - '@svgr/babel-plugin-svg-em-dimensions@8.0.0(@babel/core@7.28.5)': - dependencies: - '@babel/core': 7.28.5 - '@svgr/babel-plugin-transform-react-native-svg@8.1.0(@babel/core@7.28.4)': dependencies: '@babel/core': 7.28.4 - '@svgr/babel-plugin-transform-react-native-svg@8.1.0(@babel/core@7.28.5)': - dependencies: - '@babel/core': 7.28.5 - '@svgr/babel-plugin-transform-svg-component@8.0.0(@babel/core@7.28.4)': dependencies: '@babel/core': 7.28.4 - '@svgr/babel-plugin-transform-svg-component@8.0.0(@babel/core@7.28.5)': - dependencies: - '@babel/core': 7.28.5 - '@svgr/babel-preset@8.1.0(@babel/core@7.28.4)': dependencies: '@babel/core': 7.28.4 @@ -8962,18 +8982,6 @@ snapshots: '@svgr/babel-plugin-transform-react-native-svg': 8.1.0(@babel/core@7.28.4) '@svgr/babel-plugin-transform-svg-component': 8.0.0(@babel/core@7.28.4) - '@svgr/babel-preset@8.1.0(@babel/core@7.28.5)': - dependencies: - '@babel/core': 7.28.5 - '@svgr/babel-plugin-add-jsx-attribute': 8.0.0(@babel/core@7.28.5) - '@svgr/babel-plugin-remove-jsx-attribute': 8.0.0(@babel/core@7.28.5) - '@svgr/babel-plugin-remove-jsx-empty-expression': 8.0.0(@babel/core@7.28.5) - '@svgr/babel-plugin-replace-jsx-attribute-value': 8.0.0(@babel/core@7.28.5) - '@svgr/babel-plugin-svg-dynamic-title': 8.0.0(@babel/core@7.28.5) - '@svgr/babel-plugin-svg-em-dimensions': 8.0.0(@babel/core@7.28.5) - '@svgr/babel-plugin-transform-react-native-svg': 8.1.0(@babel/core@7.28.5) - '@svgr/babel-plugin-transform-svg-component': 8.0.0(@babel/core@7.28.5) - '@svgr/cli@8.1.0(typescript@5.9.3)': dependencies: '@svgr/core': 8.1.0(typescript@5.9.3) @@ -8992,8 +9000,8 @@ snapshots: '@svgr/core@8.1.0(typescript@5.9.3)': dependencies: - '@babel/core': 7.28.5 - '@svgr/babel-preset': 8.1.0(@babel/core@7.28.5) + '@babel/core': 7.28.4 + '@svgr/babel-preset': 8.1.0(@babel/core@7.28.4) camelcase: 6.3.0 cosmiconfig: 8.3.6(typescript@5.9.3) snake-case: 3.0.4 @@ -9142,29 +9150,29 @@ snapshots: '@types/babel__core@7.20.5': dependencies: - '@babel/parser': 7.28.5 - '@babel/types': 7.28.5 + '@babel/parser': 7.28.4 + '@babel/types': 7.28.4 '@types/babel__generator': 7.27.0 '@types/babel__template': 7.4.4 '@types/babel__traverse': 7.28.0 '@types/babel__generator@7.27.0': dependencies: - '@babel/types': 7.28.5 + '@babel/types': 7.28.4 '@types/babel__template@7.4.4': dependencies: - '@babel/parser': 7.28.5 - '@babel/types': 7.28.5 + '@babel/parser': 7.28.4 + '@babel/types': 7.28.4 '@types/babel__traverse@7.28.0': dependencies: - '@babel/types': 7.28.5 + '@babel/types': 7.28.4 '@types/body-parser@1.19.6': dependencies: '@types/connect': 3.4.38 - '@types/node': 24.10.1 + '@types/node': 24.8.0 '@types/chai@5.2.2': dependencies: @@ -9176,7 +9184,7 @@ snapshots: '@types/connect@3.4.38': dependencies: - '@types/node': 24.10.1 + '@types/node': 24.8.0 '@types/debug@4.1.12': dependencies: @@ -9198,7 +9206,7 @@ snapshots: '@types/express-serve-static-core@5.1.0': dependencies: - '@types/node': 24.10.1 + '@types/node': 24.8.0 '@types/qs': 6.14.0 '@types/range-parser': 1.2.7 '@types/send': 1.2.0 @@ -9232,12 +9240,14 @@ snapshots: '@types/jsdom@27.0.0': dependencies: - '@types/node': 24.10.1 + '@types/node': 24.8.0 '@types/tough-cookie': 4.0.5 parse5: 7.3.0 '@types/json-schema@7.0.15': {} + '@types/lodash@4.17.20': {} + '@types/lodash@4.17.21': {} '@types/mdast@4.0.4': @@ -9261,6 +9271,10 @@ snapshots: dependencies: undici-types: 7.16.0 + '@types/node@24.8.0': + dependencies: + undici-types: 7.14.0 + '@types/parse-json@4.0.2': {} '@types/prismjs@1.26.5': {} @@ -9283,23 +9297,23 @@ snapshots: '@types/resolve@1.17.1': dependencies: - '@types/node': 24.10.1 + '@types/node': 24.8.0 '@types/resolve@1.20.6': {} '@types/send@0.17.5': dependencies: '@types/mime': 1.3.5 - '@types/node': 24.10.1 + '@types/node': 24.8.0 '@types/send@1.2.0': dependencies: - '@types/node': 24.10.1 + '@types/node': 24.8.0 '@types/serve-static@1.15.9': dependencies: '@types/http-errors': 2.0.5 - '@types/node': 24.10.1 + '@types/node': 24.8.0 '@types/send': 0.17.5 '@types/slug@5.0.9': {} @@ -9345,6 +9359,15 @@ snapshots: transitivePeerDependencies: - supports-color + '@typescript-eslint/project-service@8.46.2(typescript@5.9.3)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.46.2(typescript@5.9.3) + '@typescript-eslint/types': 8.46.2 + debug: 4.4.3 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/project-service@8.48.0(typescript@5.9.3)': dependencies: '@typescript-eslint/tsconfig-utils': 8.48.0(typescript@5.9.3) @@ -9354,11 +9377,20 @@ snapshots: transitivePeerDependencies: - supports-color + '@typescript-eslint/scope-manager@8.46.2': + dependencies: + '@typescript-eslint/types': 8.46.2 + '@typescript-eslint/visitor-keys': 8.46.2 + '@typescript-eslint/scope-manager@8.48.0': dependencies: '@typescript-eslint/types': 8.48.0 '@typescript-eslint/visitor-keys': 8.48.0 + '@typescript-eslint/tsconfig-utils@8.46.2(typescript@5.9.3)': + dependencies: + typescript: 5.9.3 + '@typescript-eslint/tsconfig-utils@8.48.0(typescript@5.9.3)': dependencies: typescript: 5.9.3 @@ -9375,8 +9407,26 @@ snapshots: transitivePeerDependencies: - supports-color + '@typescript-eslint/types@8.46.2': {} + '@typescript-eslint/types@8.48.0': {} + '@typescript-eslint/typescript-estree@8.46.2(typescript@5.9.3)': + dependencies: + '@typescript-eslint/project-service': 8.46.2(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.46.2(typescript@5.9.3) + '@typescript-eslint/types': 8.46.2 + '@typescript-eslint/visitor-keys': 8.46.2 + debug: 4.4.3 + fast-glob: 3.3.3 + is-glob: 4.0.3 + minimatch: 9.0.5 + semver: 7.7.3 + ts-api-utils: 2.1.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/typescript-estree@8.48.0(typescript@5.9.3)': dependencies: '@typescript-eslint/project-service': 8.48.0(typescript@5.9.3) @@ -9392,6 +9442,17 @@ snapshots: transitivePeerDependencies: - supports-color + '@typescript-eslint/utils@8.46.2(eslint@9.39.1)(typescript@5.9.3)': + dependencies: + '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.1) + '@typescript-eslint/scope-manager': 8.46.2 + '@typescript-eslint/types': 8.46.2 + '@typescript-eslint/typescript-estree': 8.46.2(typescript@5.9.3) + eslint: 9.39.1 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/utils@8.48.0(eslint@9.39.1)(typescript@5.9.3)': dependencies: '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.1) @@ -9403,6 +9464,11 @@ snapshots: transitivePeerDependencies: - supports-color + '@typescript-eslint/visitor-keys@8.46.2': + dependencies: + '@typescript-eslint/types': 8.46.2 + eslint-visitor-keys: 4.2.1 + '@typescript-eslint/visitor-keys@8.48.0': dependencies: '@typescript-eslint/types': 8.48.0 @@ -9528,7 +9594,7 @@ snapshots: '@vue/compiler-core@3.5.22': dependencies: - '@babel/parser': 7.28.5 + '@babel/parser': 7.28.4 '@vue/shared': 3.5.22 entities: 4.5.0 estree-walker: 2.0.2 @@ -10784,7 +10850,7 @@ snapshots: eslint-plugin-storybook@9.1.13(eslint@9.39.1)(storybook@9.1.13(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.2.4(@types/node@24.10.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)))(typescript@5.9.3): dependencies: - '@typescript-eslint/utils': 8.48.0(eslint@9.39.1)(typescript@5.9.3) + '@typescript-eslint/utils': 8.46.2(eslint@9.39.1)(typescript@5.9.3) eslint: 9.39.1 storybook: 9.1.13(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@7.2.4(@types/node@24.10.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) transitivePeerDependencies: @@ -11582,7 +11648,7 @@ snapshots: jest-worker@26.6.2: dependencies: - '@types/node': 24.10.1 + '@types/node': 24.8.0 merge-stream: 2.0.0 supports-color: 7.2.0 @@ -11760,8 +11826,8 @@ snapshots: magicast@0.3.5: dependencies: - '@babel/parser': 7.28.5 - '@babel/types': 7.28.5 + '@babel/parser': 7.28.4 + '@babel/types': 7.28.4 source-map-js: 1.2.1 make-dir@4.0.0: @@ -12771,9 +12837,9 @@ snapshots: react-docgen@8.0.2: dependencies: - '@babel/core': 7.28.5 - '@babel/traverse': 7.28.5 - '@babel/types': 7.28.5 + '@babel/core': 7.28.4 + '@babel/traverse': 7.28.4 + '@babel/types': 7.28.4 '@types/babel__core': 7.20.5 '@types/babel__traverse': 7.28.0 '@types/doctrine': 0.0.9 @@ -13051,7 +13117,7 @@ snapshots: rollup-plugin-tree-shakeable@2.0.0: dependencies: - '@types/node': 24.10.1 + '@types/node': 24.8.0 estree-walker: 3.0.3 magic-string: 0.30.19 @@ -13235,7 +13301,7 @@ snapshots: slate-react@0.75.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(slate@0.75.0): dependencies: '@types/is-hotkey': 0.1.10 - '@types/lodash': 4.17.21 + '@types/lodash': 4.17.20 direction: 1.0.4 is-hotkey: 0.1.8 is-plain-object: 5.0.0 @@ -13601,6 +13667,8 @@ snapshots: undici-types@6.21.0: optional: true + undici-types@7.14.0: {} + undici-types@7.16.0: {} unicorn-magic@0.1.0: {}