diff --git a/.cursorrules b/.cursorrules new file mode 100644 index 00000000..b71e1e3c --- /dev/null +++ b/.cursorrules @@ -0,0 +1,120 @@ +# Riteway - Unit Testing Library + +## Project Overview +Riteway is a JavaScript unit testing library that forces developers to write **R**eadable, **I**solated, **T**horough, and **E**xplicit tests. Built on top of tape, it provides a simple API that ensures every test answers the 5 essential questions: + +1. What is the unit under test? +2. What should it do? +3. What was the actual output? +4. What was the expected output? +5. How do you reproduce the failure? + +## Architecture & Key Components + +### Core API +- `describe(unit, testFunction)` - Main test function +- `assert({given, should, actual, expected})` - Assertion function with required prose descriptions +- `Try(fn, ...args)` - Safe function execution with error handling +- `match(text)` - Text/pattern matching utility for component testing +- `render(component)` - React component rendering utility +- `countKeys(obj)` - Object key counting utility + +### Module Structure +- `/source/` - Main source code (CommonJS) +- `/esm/` - ES module versions (copied from source) +- `/bin/riteway` - CLI test runner +- Type definitions in root (`.d.ts` files) + +## Coding Standards & Patterns + +### Test Writing Patterns +```javascript +describe('functionName()', async assert => { + assert({ + given: 'a clear description of input/context', + should: 'describe expected behavior', + actual: functionCall(input), + expected: expectedOutput + }); +}); +``` + +### Error Handling +- Use `Try()` for testing functions that may throw +- Always handle async operations properly +- Prefer async/await over raw promises + +### Code Style +- Use ES6+ features (arrow functions, destructuring, etc.) +- Prefer const over let when possible +- Use descriptive variable names +- Follow existing formatting patterns (2 spaces, semicolons) + +### React Component Testing +```javascript +const $ = render(); +const contains = match($('.selector').html()); +assert({ + given: 'component with props', + should: 'render expected content', + actual: contains('expected text'), + expected: 'expected text' +}); +``` + +## Development Workflow + +### Common Commands +- `npm test` - Run all tests +- `npm run lint` - Run ESLint +- `npm run typecheck` - TypeScript checking +- `npm run esm` - Update ESM modules +- `npm run watch` - Watch mode for development + +### Build Process +1. Source files are in `/source/` +2. ESM versions are generated via copy to `/esm/` +3. TypeScript definitions are manually maintained +4. Babel is used for transpilation in tests + +### Testing Guidelines +- Every test must have `given`, `should`, `actual`, `expected` +- Use prose descriptions that read like sentences +- Test one thing per assertion +- Prefer isolated unit tests +- Use `describe.only()` for focused testing +- Use `describe.skip()` to temporarily disable tests + +## Dependencies & Tools +- **tape** - Underlying test runner +- **cheerio** - DOM manipulation for component testing +- **react/react-dom** - For component rendering +- **babel** - Transpilation +- **eslint** - Linting +- **typescript** - Type checking (dev only) + +## File Naming Conventions +- `*-test.js` - Test files +- `*.d.ts` - TypeScript definitions +- Main modules: `riteway.js`, `match.js`, `render-component.js`, etc. + +## Key Design Principles +1. **Explicitness over brevity** - Every test should be completely clear +2. **Readability first** - Tests should read like documentation +3. **Fail fast and clear** - Test failures should immediately show the problem +4. **Minimal API surface** - Keep the API small and focused +5. **TAP compatibility** - Output should work with TAP ecosystem tools + +## Common Issues & Solutions +- Missing babel config causes syntax errors - ensure `.babelrc` is properly configured +- ESM modules need manual updating via `npm run esm` +- TypeScript checking is separate from runtime - run `npm run typecheck` +- For React testing, ensure proper imports and babel presets + +## When Contributing +- Follow existing test patterns exactly +- Ensure all tests pass before submitting +- Run linting and type checking +- Update ESM modules if source changes +- Maintain TypeScript definitions for API changes +- Keep test output clear and readable \ No newline at end of file diff --git a/AI_SETUP.md b/AI_SETUP.md new file mode 100644 index 00000000..bd3dc86e --- /dev/null +++ b/AI_SETUP.md @@ -0,0 +1,38 @@ +# AI Development Assistant Setup + +This repository includes configuration files to help AI assistants (like GitHub Copilot, Cursor, and others) understand the Riteway testing library and provide better development assistance. + +## Files Added + +### `.cursorrules` +Project-wide configuration that provides AI assistants with: +- Complete project overview and architecture +- Coding standards and patterns used in Riteway +- Test writing conventions and best practices +- Development workflow and common commands +- Key design principles and guidelines + +### `/ai` Directory +Collection of specialized prompts and documentation for common development scenarios: + +- **`README.md`** - Overview of available AI prompts +- **`test-writing.md`** - Comprehensive patterns for writing Riteway tests +- **`component-testing.md`** - React component testing with Riteway utilities +- **`api-development.md`** - Guidelines for extending the Riteway API +- **`debugging.md`** - Common issues and debugging strategies +- **`refactoring.md`** - Safe refactoring patterns and techniques + +## Benefits + +These configurations help AI assistants: +- Understand Riteway's unique testing philosophy (Readable, Isolated, Thorough, Explicit) +- Generate proper test code following the required `{given, should, actual, expected}` pattern +- Suggest appropriate debugging approaches for common test failures +- Maintain code quality and consistency with project standards +- Provide relevant examples and patterns for specific development tasks + +## Usage + +The configuration automatically activates when using compatible AI tools in this repository. No additional setup is required - AI assistants will automatically reference these files to provide contextually appropriate suggestions and help. + +For developers using Cursor, VS Code with Copilot, or other AI-enhanced editors, you should notice more accurate and helpful suggestions when working with Riteway code and tests. \ No newline at end of file diff --git a/ai/README.md b/ai/README.md new file mode 100644 index 00000000..97732084 --- /dev/null +++ b/ai/README.md @@ -0,0 +1,15 @@ +# AI Development Prompts for Riteway + +This directory contains prompts and templates to help AI assistants understand and work with the Riteway testing library. + +## Available Prompts + +- `test-writing.md` - Templates and patterns for writing Riteway tests +- `component-testing.md` - React component testing patterns +- `api-development.md` - Guidelines for extending the Riteway API +- `debugging.md` - Common debugging scenarios and solutions +- `refactoring.md` - Patterns for safely refactoring code and tests + +## Usage + +These prompts are designed to provide context and examples for AI assistants when working on Riteway development tasks. Reference them when asking for help with specific development scenarios. \ No newline at end of file diff --git a/ai/api-development.md b/ai/api-development.md new file mode 100644 index 00000000..ab4506f3 --- /dev/null +++ b/ai/api-development.md @@ -0,0 +1,348 @@ +# API Development Guidelines for Riteway + +## Core Design Principles + +When extending or modifying the Riteway API, follow these principles: + +1. **Simplicity**: Keep the API surface minimal and focused +2. **Explicitness**: Force users to be explicit about what they're testing +3. **Readability**: API calls should read like natural language +4. **TAP Compatibility**: Maintain compatibility with the TAP ecosystem +5. **Error Clarity**: Failures should provide clear, actionable feedback + +## Current API Structure + +### Core Functions + +```javascript +// Main test runner +describe(unit: String, cb: TestFunction) => Void +describe.only(unit: String, cb: TestFunction) => Void +describe.skip(unit: String, cb: TestFunction) => Void + +// Assertion function +assert({ + given: String, + should: String, + actual: Any, + expected: Any +}) => Void + +// Utility functions +Try(fn: Function, ...args) => Any +createStream() => Stream +countKeys(obj: Object) => Number +``` + +### Testing Utilities + +```javascript +// Text/pattern matching +match(text: String) => (pattern: String|RegExp) => String + +// React component rendering +render(component: ReactElement) => CheerioStatic +``` + +## Adding New API Functions + +### Function Design Pattern + +All API functions should follow these patterns: + +```javascript +// Pure functions - no side effects +const newUtility = (input) => { + // Validate inputs + if (!input) { + throw new Error('Input is required'); + } + + // Transform and return + return transformedOutput; +}; + +// Test runner extensions +const newTestType = (description, testFn) => { + // Wrap with existing patterns + return describe(description, testFn); +}; +``` + +### Input Validation + +Always validate inputs and provide clear error messages: + +```javascript +const validateAssertArgs = (args = {}) => { + const requiredKeys = ['given', 'should', 'actual', 'expected']; + const missing = requiredKeys.filter( + k => !Object.keys(args).includes(k) + ); + + if (missing.length) { + throw new Error( + `The following parameters are required: ${missing.join(', ')}` + ); + } +}; +``` + +### TypeScript Definitions + +Always provide TypeScript definitions for new API functions: + +```typescript +// In appropriate .d.ts file +export declare function newFunction( + param1: string, + param2?: number +): ReturnType; + +export interface NewOptions { + option1: string; + option2?: boolean; +} +``` + +## Extending the Assert Function + +The `assert` function is the core of Riteway. Any extensions should maintain its design: + +### Current Implementation Pattern +```javascript +const assert = (args = {}) => { + // Validate required fields + validateAssertArgs(args); + + const { given, should, actual, expected } = args; + + // Use tape's assertion + test.same( + actual, + expected, + `Given ${given}: should ${should}` + ); +}; +``` + +### Adding New Assertion Types + +If adding specialized assertions, follow this pattern: + +```javascript +const assertContains = (args = {}) => { + const { given, should, actual, expected } = args; + + // Custom validation logic + const contains = actual.includes(expected); + + test.ok( + contains, + `Given ${given}: should ${should}` + ); +}; +``` + +## Testing New API Functions + +All new API functions must be thoroughly tested: + +```javascript +describe('newFunction()', async assert => { + assert({ + given: 'valid input', + should: 'return expected output', + actual: newFunction('input'), + expected: 'expectedOutput' + }); + + assert({ + given: 'invalid input', + should: 'throw descriptive error', + actual: Try(newFunction, null), + expected: new Error('Input is required') + }); +}); +``` + +## Utility Function Guidelines + +### Pure Functions Only +```javascript +// Good - pure function +const formatMessage = (message) => `[TEST] ${message}`; + +// Bad - side effects +const logMessage = (message) => { + console.log(message); // Side effect + return message; +}; +``` + +### Composable Design +```javascript +// Design functions to work together +const createMatcher = (text) => (pattern) => match(text)(pattern); +const createRenderer = (component) => render(component); +``` + +### Error Handling +```javascript +const safeOperation = (input) => { + try { + return riskyOperation(input); + } catch (error) { + // Return error object, don't throw + return error; + } +}; +``` + +## Module Organization + +### Source Structure +``` +source/ +├── riteway.js # Main API +├── match.js # Text matching utilities +├── render-component.js # React testing utilities +├── vitest.js # Vitest integration +└── test.js # API tests +``` + +### Export Patterns +```javascript +// Named exports for utilities +export { match, render }; + +// Default export for main API +export default describe; + +// Combined exports +export { describe, Try, createStream, countKeys }; +``` + +## Integration with Test Runners + +### Tape Integration +Riteway is built on tape and should maintain compatibility: + +```javascript +const withTape = tapeFn => (unit = '', TestFunction = noop) => + tapeFn(unit, withRiteway(TestFunction)); + +const describe = Object.assign(withTape(tape), { + only: withTape(tape.only), + skip: tape.skip +}); +``` + +### Other Test Runner Support +When adding support for other test runners: + +```javascript +// Maintain the same API +const withNewRunner = runnerFn => (unit, TestFunction) => { + // Adapt to new runner's API + return runnerFn(unit, adaptedTestFunction); +}; +``` + +## Documentation Requirements + +### JSDoc Comments +```javascript +/** + * Creates a text matcher function for testing content. + * + * @param {string} text - The text to search within + * @returns {function} A function that matches patterns in the text + * @example + * const contains = match('
Hello World
'); + * contains('Hello'); // Returns 'Hello' + */ +const match = (text) => (pattern) => { + // Implementation +}; +``` + +### README Updates +When adding new API functions, update: +1. API section with new function signatures +2. Usage examples +3. TypeScript definitions reference + +## Backwards Compatibility + +### Deprecation Process +1. Mark old API as deprecated in JSDoc +2. Add console warning in development +3. Maintain functionality for at least one major version +4. Provide migration guide + +### Version Strategy +- Patch: Bug fixes, no API changes +- Minor: New features, backwards compatible +- Major: Breaking changes, API modifications + +## Common Patterns to Follow + +### Function Factories +```javascript +// Create specialized versions of generic functions +const createAsyncTester = (timeout = 5000) => async (fn) => { + // Implementation with timeout +}; +``` + +### Currying for Composability +```javascript +// Allow partial application +const matcher = text => pattern => match(text)(pattern); +const htmlMatcher = matcher(htmlString); +``` + +### Error Object Structure +```javascript +// Consistent error format +const createError = (message, code, details = {}) => { + const error = new Error(message); + error.code = code; + error.details = details; + return error; +}; +``` + +## Testing API Changes + +### Integration Tests +```javascript +// Test how new features work with existing API +describe('API integration', async assert => { + const result = newFunction(existingFunction(input)); + + assert({ + given: 'new function with existing API', + should: 'work seamlessly together', + actual: result, + expected: expectedResult + }); +}); +``` + +### Performance Considerations +- Keep API functions lightweight +- Avoid expensive operations in hot paths +- Consider memoization for expensive computations +- Test performance with large inputs + +## Release Process + +1. Update version in package.json +2. Run full test suite +3. Update TypeScript definitions +4. Update ESM modules (`npm run esm`) +5. Update documentation +6. Tag release in git +7. Publish to npm \ No newline at end of file diff --git a/ai/component-testing.md b/ai/component-testing.md new file mode 100644 index 00000000..7cf44430 --- /dev/null +++ b/ai/component-testing.md @@ -0,0 +1,327 @@ +# React Component Testing with Riteway + +## Setup and Imports + +```javascript +import React from 'react'; +import { describe } from 'riteway'; +import render from 'riteway/render-component'; +import match from 'riteway/match'; +``` + +## Basic Component Testing Pattern + +```javascript +// Component to test +const Greeting = ({ name }) =>

Hello, {name}!

; + +describe('Greeting component', async assert => { + const $ = render(); + + assert({ + given: 'a name prop', + should: 'render greeting with the name', + actual: $('h1').text(), + expected: 'Hello, World!' + }); +}); +``` + +## Using match() for Content Verification + +The `match()` function is particularly useful for testing rendered content: + +```javascript +const BlogPost = ({ title, content }) => ( +
+

{title}

+
{content}
+
+); + +describe('BlogPost component', async assert => { + const title = 'My Blog Post'; + const content = 'This is the blog content.'; + const $ = render(); + + // Using match to find specific text + const contains = match($('.content').html()); + + assert({ + given: 'title and content props', + should: 'render the content', + actual: contains(content), + expected: content + }); +}); +``` + +## Testing Component State and Props + +```javascript +const Counter = ({ initialCount = 0 }) => { + const [count, setCount] = React.useState(initialCount); + + return ( +
+ {count} + +
+ ); +}; + +describe('Counter component', async assert => { + { + const $ = render(); + + assert({ + given: 'no initial count', + should: 'display zero as default', + actual: $('.count').text(), + expected: '0' + }); + } + + { + const $ = render(); + + assert({ + given: 'an initial count of 5', + should: 'display the initial count', + actual: $('.count').text(), + expected: '5' + }); + } +}); +``` + +## Testing Component Structure and CSS Classes + +```javascript +const Card = ({ title, children, variant = 'default' }) => ( +
+

{title}

+
{children}
+
+); + +describe('Card component', async assert => { + const $ = render( + +

Card content

+
+ ); + + assert({ + given: 'a variant prop', + should: 'apply the correct CSS class', + actual: $('.card').hasClass('card--primary'), + expected: true + }); + + assert({ + given: 'a title prop', + should: 'render title in correct element', + actual: $('.card__title').text(), + expected: 'Test Card' + }); +}); +``` + +## Testing Lists and Repeated Elements + +```javascript +const TodoList = ({ todos }) => ( +
    + {todos.map(todo => ( +
  • + {todo.text} +
  • + ))} +
+); + +describe('TodoList component', async assert => { + const todos = [ + { id: 1, text: 'Buy milk', completed: false }, + { id: 2, text: 'Walk dog', completed: true } + ]; + + const $ = render(); + + assert({ + given: 'an array of todos', + should: 'render correct number of items', + actual: $('.todo').length, + expected: 2 + }); + + assert({ + given: 'a completed todo', + should: 'apply completed class', + actual: $('.todo').eq(1).hasClass('completed'), + expected: true + }); +}); +``` + +## Testing Component with Event Handlers + +```javascript +const Button = ({ onClick, children, disabled = false }) => ( + +); + +describe('Button component', async assert => { + const $ = render(); + + assert({ + given: 'disabled prop is true', + should: 'render button as disabled', + actual: $('button').prop('disabled'), + expected: true + }); + + assert({ + given: 'children content', + should: 'render button text', + actual: $('button').text(), + expected: 'Click me' + }); +}); +``` + +## Testing Conditional Rendering + +```javascript +const Message = ({ type, text }) => { + if (!text) return null; + + return ( +
+ {type === 'error' && ⚠️} + {text} +
+ ); +}; + +describe('Message component', async assert => { + { + const $ = render(); + + assert({ + given: 'error type and text', + should: 'render error icon', + actual: $('.icon').text(), + expected: '⚠️' + }); + } + + { + const $ = render(); + + assert({ + given: 'empty text', + should: 'render nothing', + actual: $('.message').length, + expected: 0 + }); + } +}); +``` + +## Testing Components with Complex Markup + +```javascript +const ProductCard = ({ product }) => ( +
+ {product.name} +

{product.name}

+

${product.price}

+

{product.description}

+
+); + +describe('ProductCard component', async assert => { + const product = { + name: 'Widget', + price: 29.99, + description: 'A useful widget', + image: '/widget.jpg' + }; + + const $ = render(); + const contains = match($('.product-card').html()); + + assert({ + given: 'a product object', + should: 'render product name', + actual: contains(product.name), + expected: product.name + }); + + assert({ + given: 'a product with price', + should: 'format price correctly', + actual: $('.price').text(), + expected: '$29.99' + }); +}); +``` + +## Common Patterns for Component Testing + +### Testing Accessibility Attributes +```javascript +assert({ + given: 'component with accessibility requirements', + should: 'have correct aria-label', + actual: $('button').attr('aria-label'), + expected: 'Close dialog' +}); +``` + +### Testing Data Attributes +```javascript +assert({ + given: 'component with data attributes', + should: 'set correct data attribute', + actual: $('.item').attr('data-testid'), + expected: 'product-123' +}); +``` + +### Testing Component Composition +```javascript +const Layout = ({ children }) => ( +
+
Header
+
{children}
+
+); + +const $ = render( + +

Content

+
+); + +assert({ + given: 'children content', + should: 'render children in main section', + actual: $('main p').text(), + expected: 'Content' +}); +``` + +## Best Practices + +1. **Test the rendered output, not implementation details** +2. **Use `match()` for text content verification** +3. **Test component behavior at different prop values** +4. **Focus on what users see and interact with** +5. **Keep component tests isolated and independent** +6. **Use descriptive selectors based on CSS classes or structure** +7. **Test both positive and negative cases (what should and shouldn't render)** \ No newline at end of file diff --git a/ai/debugging.md b/ai/debugging.md new file mode 100644 index 00000000..64ad34d5 --- /dev/null +++ b/ai/debugging.md @@ -0,0 +1,392 @@ +# Debugging Common Issues in Riteway + +## Test Failures and Debugging + +### Understanding Test Output + +Riteway uses TAP format. A typical failure looks like: + +``` +not ok 5 Given invalid input: should throw an error + --- + operator: deepEqual + expected: Error: Invalid input + actual: undefined + ... +``` + +Key debugging information: +- **Test number**: `5` - helps locate the failing test +- **Description**: `Given invalid input: should throw an error` - from your `given` and `should` fields +- **Expected vs Actual**: Shows what you expected vs what you got + +### Common Test Failure Patterns + +#### 1. Missing Error Handling +```javascript +// Problem: Function doesn't throw when it should +describe('validateEmail()', async assert => { + assert({ + given: 'invalid email', + should: 'throw validation error', + actual: Try(validateEmail, 'invalid'), // Returns undefined instead of Error + expected: new Error('Invalid email') + }); +}); + +// Solution: Check your function implementation +const validateEmail = (email) => { + if (!email.includes('@')) { + throw new Error('Invalid email'); // Make sure this actually throws + } + return email; +}; +``` + +#### 2. Async/Await Issues +```javascript +// Problem: Not awaiting async operations +describe('fetchUser()', async assert => { + assert({ + given: 'user ID', + should: 'return user data', + actual: fetchUser(123), // Missing await - returns Promise object + expected: { name: 'John' } + }); +}); + +// Solution: Await the async operation +describe('fetchUser()', async assert => { + const user = await fetchUser(123); + + assert({ + given: 'user ID', + should: 'return user data', + actual: user, + expected: { name: 'John' } + }); +}); +``` + +#### 3. Object Comparison Issues +```javascript +// Problem: Comparing objects that aren't deeply equal +assert({ + given: 'user data', + should: 'create user object', + actual: createUser('John'), // Returns { id: 'abc123', name: 'John' } + expected: { name: 'John' } // Missing id field +}); + +// Solution: Match the exact structure or test specific properties +assert({ + given: 'user data', + should: 'create user with correct name', + actual: createUser('John').name, + expected: 'John' +}); +``` + +## Component Testing Debug Patterns + +### React Component Not Rendering +```javascript +// Problem: Component returns null or undefined +const $ = render(); + +assert({ + given: 'show prop is false', + should: 'render nothing', + actual: $('.component').length, // Might be looking for wrong selector + expected: 0 +}); + +// Debug: Check what's actually rendered +console.log($.html()); // See the full HTML output +``` + +### CSS Selector Issues +```javascript +// Problem: Wrong CSS selector +const $ = render(

John

); + +assert({ + given: 'user component', + should: 'render user name', + actual: $('.user-name').text(), // Wrong class name + expected: 'John' +}); + +// Debug: Check available elements +console.log($.html()); // See actual HTML structure +console.log($('h1').text()); // Try different selectors +``` + +### Text Content Matching Issues +```javascript +// Problem: Extra whitespace or different content +const $ = render(

Hello World

); + +assert({ + given: 'text content', + should: 'match exactly', + actual: $('p').text(), // " Hello World " (with spaces) + expected: 'Hello World' // Without spaces +}); + +// Solution: Trim whitespace or use match() +import match from 'riteway/match'; + +const contains = match($('p').html()); +assert({ + given: 'text content with spacing', + should: 'contain the expected text', + actual: contains('Hello World'), + expected: 'Hello World' +}); +``` + +## Build and Environment Issues + +### Babel Configuration Problems +```javascript +// Error: SyntaxError: Unexpected token '<' +// Problem: JSX not being transpiled + +// Solution: Check .babelrc +{ + "presets": [ + "@babel/preset-env", + "@babel/preset-react" // Make sure this is included + ] +} +``` + +### Module Import Issues +```javascript +// Error: Cannot find module 'riteway/match' +// Problem: Wrong import path + +// Wrong +import match from 'riteway/match'; + +// Correct +import match from 'riteway/source/match'; +// OR if using ESM +import match from 'riteway/esm/match'; +``` + +### TypeScript Definition Issues +```javascript +// Error: Property 'given' does not exist on type... +// Problem: TypeScript definitions out of sync + +// Check if using correct import +import { describe } from 'riteway'; // Should have proper types + +// If types are missing, check index.d.ts file exists and is correct +``` + +## Debugging Strategies + +### 1. Isolate the Problem +```javascript +// Start with simplest possible test +describe('debug test', async assert => { + assert({ + given: 'simple input', + should: 'return simple output', + actual: 1 + 1, + expected: 2 + }); +}); +``` + +### 2. Log Intermediate Values +```javascript +describe('complex function', async assert => { + const input = { name: 'John', age: 30 }; + const result = processUser(input); + + console.log('Input:', input); + console.log('Result:', result); + console.log('Result type:', typeof result); + + assert({ + given: 'user input', + should: 'process correctly', + actual: result, + expected: expectedOutput + }); +}); +``` + +### 3. Test Individual Properties +```javascript +// Instead of comparing entire objects +assert({ + given: 'complex object', + should: 'have correct name property', + actual: result.name, + expected: 'John' +}); + +assert({ + given: 'complex object', + should: 'have correct age property', + actual: result.age, + expected: 30 +}); +``` + +### 4. Use describe.only for Focus Testing +```javascript +// Only run this specific test +describe.only('focused debug test', async assert => { + // Your debugging test here +}); +``` + +## Common Error Messages and Solutions + +### "Test exited without ending" +```javascript +// Problem: Async test not properly handled +describe('async test', (assert, end) => { + setTimeout(() => { + assert({...}); + // Missing end() call + }, 100); +}); + +// Solution: Use async/await OR call end() +describe('async test', async assert => { + await delay(100); + assert({...}); +}); +``` + +### "The following parameters are required by assert" +```javascript +// Problem: Missing required assert fields +assert({ + given: 'input', + // Missing 'should', 'actual', 'expected' +}); + +// Solution: Include all required fields +assert({ + given: 'input', + should: 'produce output', + actual: myFunction(input), + expected: expectedOutput +}); +``` + +### "Cannot read property of undefined" +```javascript +// Problem: Function returns undefined +const result = myFunction(); + +assert({ + given: 'some input', + should: 'return object with property', + actual: result.property, // Error if result is undefined + expected: 'value' +}); + +// Solution: Check function return value first +const result = myFunction(); +console.log('Function returned:', result); + +assert({ + given: 'some input', + should: 'return defined result', + actual: result !== undefined, + expected: true +}); +``` + +## Performance Debugging + +### Slow Tests +```javascript +// Add timing to identify slow operations +describe('performance test', async assert => { + const start = Date.now(); + const result = expensiveOperation(); + const end = Date.now(); + + console.log(`Operation took ${end - start}ms`); + + assert({ + given: 'expensive operation', + should: 'complete in reasonable time', + actual: (end - start) < 1000, + expected: true + }); +}); +``` + +### Memory Issues +```javascript +// Check for memory leaks in component tests +describe('component memory test', async assert => { + // Render many components + for (let i = 0; i < 1000; i++) { + const $ = render(); + // Make sure components are properly cleaned up + } + + assert({ + given: 'many component renders', + should: 'not leak memory', + actual: process.memoryUsage().heapUsed < threshold, + expected: true + }); +}); +``` + +## Tools for Debugging + +### Using Node.js Debugger +```bash +# Run tests with debugger +node --inspect-brk -r @babel/register source/test.js + +# Then open Chrome DevTools +# Navigate to chrome://inspect +``` + +### TAP Formatters for Better Output +```bash +# Install tap-nirvana for better test output +npm install --save-dev tap-nirvana + +# Use in package.json +"test": "riteway test/**/*-test.js | tap-nirvana" +``` + +### ESLint for Code Issues +```bash +# Run linting to catch common issues +npm run lint + +# Auto-fix some issues +npm run lint-fix +``` + +## Debugging Checklist + +When a test fails: + +1. ✅ **Read the error message carefully** - it usually tells you exactly what's wrong +2. ✅ **Check that all assert fields are provided** - given, should, actual, expected +3. ✅ **Verify async operations are awaited** - use `await` for promises +4. ✅ **Test the function in isolation** - make sure it works outside the test +5. ✅ **Log intermediate values** - use console.log to see what's happening +6. ✅ **Check object structure** - make sure expected and actual have same shape +7. ✅ **Use describe.only** - focus on just the failing test +8. ✅ **Simplify the test** - start with the simplest case that works +9. ✅ **Check environment setup** - babel, imports, dependencies +10. ✅ **Review similar working tests** - see what's different \ No newline at end of file diff --git a/ai/refactoring.md b/ai/refactoring.md new file mode 100644 index 00000000..e09c10db --- /dev/null +++ b/ai/refactoring.md @@ -0,0 +1,553 @@ +# Refactoring Guidelines for Riteway + +## Safe Refactoring Patterns + +When refactoring code that uses Riteway, follow these patterns to maintain test reliability and readability. + +## Refactoring Test Code + +### Extracting Common Test Setup + +#### Before: Repeated Setup +```javascript +describe('UserService', async assert => { + const user = { id: 1, name: 'John', email: 'john@example.com' }; + const userService = new UserService(); + + assert({ + given: 'valid user data', + should: 'create user successfully', + actual: userService.create(user).name, + expected: 'John' + }); +}); + +describe('UserService validation', async assert => { + const user = { id: 1, name: 'John', email: 'john@example.com' }; + const userService = new UserService(); + + assert({ + given: 'user with valid email', + should: 'pass validation', + actual: userService.validate(user), + expected: true + }); +}); +``` + +#### After: Extracted Setup +```javascript +// Helper function for common setup +const createTestUser = () => ({ + id: 1, + name: 'John', + email: 'john@example.com' +}); + +const createUserService = () => new UserService(); + +describe('UserService', async assert => { + const user = createTestUser(); + const userService = createUserService(); + + assert({ + given: 'valid user data', + should: 'create user successfully', + actual: userService.create(user).name, + expected: 'John' + }); +}); + +describe('UserService validation', async assert => { + const user = createTestUser(); + const userService = createUserService(); + + assert({ + given: 'user with valid email', + should: 'pass validation', + actual: userService.validate(user), + expected: true + }); +}); +``` + +### Grouping Related Assertions + +#### Before: Scattered Tests +```javascript +describe('Calculator add', async assert => { + assert({ + given: 'positive numbers', + should: 'return sum', + actual: calculator.add(2, 3), + expected: 5 + }); +}); + +describe('Calculator subtract', async assert => { + assert({ + given: 'positive numbers', + should: 'return difference', + actual: calculator.subtract(5, 3), + expected: 2 + }); +}); +``` + +#### After: Grouped Tests +```javascript +describe('Calculator operations', async assert => { + // Addition tests + assert({ + given: 'positive numbers for addition', + should: 'return correct sum', + actual: calculator.add(2, 3), + expected: 5 + }); + + // Subtraction tests + assert({ + given: 'positive numbers for subtraction', + should: 'return correct difference', + actual: calculator.subtract(5, 3), + expected: 2 + }); +}); +``` + +### Improving Test Descriptions + +#### Before: Vague Descriptions +```javascript +describe('function test', async assert => { + assert({ + given: 'input', + should: 'work', + actual: myFunction(input), + expected: output + }); +}); +``` + +#### After: Clear Descriptions +```javascript +describe('formatCurrency()', async assert => { + assert({ + given: 'a number with decimal places', + should: 'format as currency with dollar sign and two decimal places', + actual: formatCurrency(123.456), + expected: '$123.46' + }); +}); +``` + +## Refactoring Production Code with Tests + +### Step-by-Step Refactoring Process + +1. **Ensure comprehensive test coverage** +2. **Make one small change at a time** +3. **Run tests after each change** +4. **Keep the API unchanged until refactoring is complete** + +#### Example: Refactoring a Complex Function + +##### Before: Monolithic Function +```javascript +const processOrder = (order) => { + // Validation + if (!order.items || order.items.length === 0) { + throw new Error('Order must have items'); + } + + // Calculate total + let total = 0; + for (const item of order.items) { + total += item.price * item.quantity; + } + + // Apply discount + if (order.discount) { + total = total * (1 - order.discount / 100); + } + + // Add tax + const tax = total * 0.08; + total += tax; + + return { + orderId: generateId(), + total: Math.round(total * 100) / 100, + tax, + status: 'processed' + }; +}; +``` + +##### Step 1: Add Comprehensive Tests +```javascript +describe('processOrder()', async assert => { + const basicOrder = { + items: [ + { price: 10.00, quantity: 2 }, + { price: 5.00, quantity: 1 } + ] + }; + + assert({ + given: 'order with basic items', + should: 'calculate correct total with tax', + actual: processOrder(basicOrder).total, + expected: 27.00 // (20 + 5) * 1.08 + }); + + assert({ + given: 'order with discount', + should: 'apply discount before tax', + actual: processOrder({ + ...basicOrder, + discount: 10 + }).total, + expected: 24.30 // (25 * 0.9) * 1.08 + }); + + assert({ + given: 'empty order', + should: 'throw validation error', + actual: Try(processOrder, { items: [] }), + expected: new Error('Order must have items') + }); +}); +``` + +##### Step 2: Extract Validation +```javascript +const validateOrder = (order) => { + if (!order.items || order.items.length === 0) { + throw new Error('Order must have items'); + } +}; + +const processOrder = (order) => { + validateOrder(order); + + // Rest of function unchanged... +}; + +// Run tests - should still pass +``` + +##### Step 3: Extract Calculation Logic +```javascript +const calculateSubtotal = (items) => { + return items.reduce((total, item) => { + return total + (item.price * item.quantity); + }, 0); +}; + +const applyDiscount = (amount, discountPercent) => { + if (!discountPercent) return amount; + return amount * (1 - discountPercent / 100); +}; + +const calculateTax = (amount, taxRate = 0.08) => { + return amount * taxRate; +}; + +const processOrder = (order) => { + validateOrder(order); + + const subtotal = calculateSubtotal(order.items); + const discountedAmount = applyDiscount(subtotal, order.discount); + const tax = calculateTax(discountedAmount); + const total = discountedAmount + tax; + + return { + orderId: generateId(), + total: Math.round(total * 100) / 100, + tax: Math.round(tax * 100) / 100, + status: 'processed' + }; +}; + +// Run tests - should still pass +``` + +##### Step 4: Add Tests for New Functions +```javascript +describe('calculateSubtotal()', async assert => { + assert({ + given: 'array of items with price and quantity', + should: 'return sum of price * quantity', + actual: calculateSubtotal([ + { price: 10, quantity: 2 }, + { price: 5, quantity: 1 } + ]), + expected: 25 + }); +}); + +describe('applyDiscount()', async assert => { + assert({ + given: 'amount and discount percentage', + should: 'return discounted amount', + actual: applyDiscount(100, 10), + expected: 90 + }); + + assert({ + given: 'amount with no discount', + should: 'return original amount', + actual: applyDiscount(100), + expected: 100 + }); +}); +``` + +## Refactoring Component Tests + +### Extracting Component Factories + +#### Before: Repeated Component Creation +```javascript +describe('UserCard component', async assert => { + const $ = render( + + ); + + assert({ + given: 'user with email', + should: 'display user name', + actual: $('.user-name').text(), + expected: 'John' + }); +}); + +describe('UserCard with hidden email', async assert => { + const $ = render( + + ); + + assert({ + given: 'showEmail is false', + should: 'hide email address', + actual: $('.user-email').length, + expected: 0 + }); +}); +``` + +#### After: Component Factory +```javascript +const createUserCard = (props = {}) => { + const defaultProps = { + user: { name: 'John', email: 'john@example.com' }, + showEmail: true + }; + + return render(); +}; + +describe('UserCard component', async assert => { + const $ = createUserCard(); + + assert({ + given: 'user with email', + should: 'display user name', + actual: $('.user-name').text(), + expected: 'John' + }); +}); + +describe('UserCard with hidden email', async assert => { + const $ = createUserCard({ showEmail: false }); + + assert({ + given: 'showEmail is false', + should: 'hide email address', + actual: $('.user-email').length, + expected: 0 + }); +}); +``` + +### Extracting Common Assertions + +#### Before: Repeated Assertions +```javascript +describe('Button variants', async assert => { + const primaryButton = render(); + + assert({ + given: 'primary variant', + should: 'have primary class', + actual: primaryButton('.btn').hasClass('btn--primary'), + expected: true + }); + + const secondaryButton = render(); + + assert({ + given: 'secondary variant', + should: 'have secondary class', + actual: secondaryButton('.btn').hasClass('btn--secondary'), + expected: true + }); +}); +``` + +#### After: Assertion Helper +```javascript +const assertButtonVariant = (assert, variant) => { + const $ = render(); + + assert({ + given: `${variant} variant`, + should: `have ${variant} class`, + actual: $('.btn').hasClass(`btn--${variant}`), + expected: true + }); +}; + +describe('Button variants', async assert => { + assertButtonVariant(assert, 'primary'); + assertButtonVariant(assert, 'secondary'); +}); +``` + +## Legacy Code Refactoring + +### Adding Tests to Untested Code + +#### Step 1: Characterization Tests +```javascript +// First, test the current behavior (even if it's wrong) +describe('legacyFunction() characterization', async assert => { + assert({ + given: 'current implementation', + should: 'maintain existing behavior', + actual: legacyFunction('input'), + expected: currentOutput // Whatever it currently returns + }); +}); +``` + +#### Step 2: Incremental Improvement +```javascript +// Add tests for desired behavior +describe('legacyFunction() desired behavior', async assert => { + assert({ + given: 'proper input handling', + should: 'return sanitized output', + actual: legacyFunction('input'), + expected: desiredOutput + }); +}); + +// Then modify the function to pass new tests +``` + +### Dealing with Dependencies + +#### Before: Hard Dependencies +```javascript +const sendEmail = (user) => { + const emailService = new EmailService(); // Hard dependency + return emailService.send(user.email, 'Welcome!'); +}; + +// Hard to test +describe('sendEmail()', async assert => { + // This will actually send emails! + assert({ + given: 'user with email', + should: 'send welcome email', + actual: sendEmail({ email: 'test@example.com' }), + expected: true + }); +}); +``` + +#### After: Dependency Injection +```javascript +const sendEmail = (user, emailService = new EmailService()) => { + return emailService.send(user.email, 'Welcome!'); +}; + +// Easy to test with mock +const mockEmailService = { + send: (email, message) => `Sent "${message}" to ${email}` +}; + +describe('sendEmail()', async assert => { + assert({ + given: 'user with email and mock service', + should: 'call email service with correct parameters', + actual: sendEmail({ email: 'test@example.com' }, mockEmailService), + expected: 'Sent "Welcome!" to test@example.com' + }); +}); +``` + +## Refactoring Anti-Patterns to Avoid + +### Don't Change Tests and Code Simultaneously +```javascript +// BAD: Changing test and implementation together +describe('newFunction()', async assert => { + assert({ + given: 'new input format', // Changed test + should: 'return new format', // Changed test + actual: newFunction(newInput), // Changed implementation + expected: newOutput // Changed test + }); +}); + +// GOOD: Change tests first, then implementation +// 1. First, update tests for new desired behavior +// 2. Run tests (they should fail) +// 3. Update implementation to make tests pass +``` + +### Don't Remove Tests During Refactoring +```javascript +// BAD: Removing tests that became inconvenient +// describe('oldBehavior()', async assert => { +// // Commented out because it's hard to maintain +// }); + +// GOOD: Update tests to reflect new behavior +describe('refactoredBehavior()', async assert => { + assert({ + given: 'same input as before', + should: 'provide improved output', + actual: refactoredFunction(input), + expected: improvedOutput + }); +}); +``` + +## Refactoring Checklist + +Before refactoring: +- ✅ **Ensure comprehensive test coverage** +- ✅ **All tests are passing** +- ✅ **Understand the current behavior fully** + +During refactoring: +- ✅ **Make small, incremental changes** +- ✅ **Run tests after each change** +- ✅ **Keep the public API stable** +- ✅ **Add tests for new internal functions** + +After refactoring: +- ✅ **All tests still pass** +- ✅ **Code is more readable and maintainable** +- ✅ **Performance hasn't degraded** +- ✅ **Documentation is updated if needed** \ No newline at end of file diff --git a/ai/test-writing.md b/ai/test-writing.md new file mode 100644 index 00000000..d0b7610a --- /dev/null +++ b/ai/test-writing.md @@ -0,0 +1,165 @@ +# Test Writing Patterns for Riteway + +## Basic Test Structure + +Every Riteway test must follow this exact pattern: + +```javascript +import { describe } from 'riteway'; + +describe('functionName()', async assert => { + assert({ + given: 'a clear description of the input or context', + should: 'describe what the function should do', + actual: functionName(input), + expected: expectedOutput + }); +}); +``` + +## Key Principles + +1. **All four fields are required**: `given`, `should`, `actual`, `expected` +2. **Use async functions**: Always use `async assert =>` for the test function +3. **Test one thing per assertion**: Keep assertions focused and isolated +4. **Use descriptive prose**: The `given` and `should` fields should read like clear English + +## Common Patterns + +### Testing Pure Functions +```javascript +describe('sum()', async assert => { + assert({ + given: 'two positive numbers', + should: 'return their sum', + actual: sum(2, 3), + expected: 5 + }); + + assert({ + given: 'negative numbers', + should: 'handle negative values correctly', + actual: sum(-1, -2), + expected: -3 + }); +}); +``` + +### Testing Functions That Throw +```javascript +import { Try } from 'riteway'; + +describe('validateEmail()', async assert => { + assert({ + given: 'an invalid email', + should: 'throw a validation error', + actual: Try(validateEmail, 'invalid-email'), + expected: new Error('Invalid email format') + }); +}); +``` + +### Testing Async Functions +```javascript +describe('fetchUser()', async assert => { + const user = await fetchUser(123); + + assert({ + given: 'a valid user ID', + should: 'return user data', + actual: user.name, + expected: 'John Doe' + }); +}); +``` + +### Testing Object Properties +```javascript +describe('createUser()', async assert => { + const user = createUser('John', 'john@example.com'); + + assert({ + given: 'name and email', + should: 'create user with correct name', + actual: user.name, + expected: 'John' + }); + + assert({ + given: 'name and email', + should: 'create user with correct email', + actual: user.email, + expected: 'john@example.com' + }); +}); +``` + +### Testing Array Operations +```javascript +describe('filterAdults()', async assert => { + const people = [ + { name: 'Alice', age: 25 }, + { name: 'Bob', age: 17 }, + { name: 'Charlie', age: 30 } + ]; + + assert({ + given: 'array of people with mixed ages', + should: 'return only adults', + actual: filterAdults(people).length, + expected: 2 + }); +}); +``` + +## Test Organization + +### Grouping Related Tests +```javascript +describe('Calculator operations', async assert => { + // Addition tests + assert({ + given: 'two numbers for addition', + should: 'return correct sum', + actual: calculator.add(2, 3), + expected: 5 + }); + + // Subtraction tests + assert({ + given: 'two numbers for subtraction', + should: 'return correct difference', + actual: calculator.subtract(5, 3), + expected: 2 + }); +}); +``` + +### Using describe.only and describe.skip +```javascript +// Run only this test +describe.only('focused test', async assert => { + // test implementation +}); + +// Skip this test +describe.skip('broken test', async assert => { + // test implementation +}); +``` + +## Common Mistakes to Avoid + +1. **Missing required fields**: All four fields (given, should, actual, expected) are mandatory +2. **Not using async**: Always use `async assert =>` +3. **Testing multiple things in one assertion**: Keep each assertion focused +4. **Unclear prose descriptions**: Make `given` and `should` read like clear English +5. **Not handling errors properly**: Use `Try()` for functions that may throw + +## Best Practices + +1. **Start with the simplest case**: Test the happy path first +2. **Test edge cases**: Consider null, undefined, empty arrays, etc. +3. **Use meaningful test data**: Choose inputs that make the test intention clear +4. **Keep tests independent**: Each test should work in isolation +5. **Use descriptive names**: The `describe` parameter should clearly identify what's being tested \ No newline at end of file