diff --git a/.claude/skills/code-review/SKILL.md b/.claude/skills/code-review/SKILL.md new file mode 100644 index 000000000..309234e57 --- /dev/null +++ b/.claude/skills/code-review/SKILL.md @@ -0,0 +1,538 @@ +--- +name: code-review-excellence +description: Master effective code review practices to provide constructive feedback, catch bugs early, and foster knowledge sharing while maintaining team morale. Use when reviewing pull requests, establishing review standards, or mentoring developers. +--- + +# Code Review Excellence + +Transform code reviews from gatekeeping to knowledge sharing through constructive feedback, systematic analysis, and collaborative improvement. + +## When to Use This Skill + +- Reviewing pull requests and code changes +- Establishing code review standards for teams +- Mentoring junior developers through reviews +- Conducting architecture reviews +- Creating review checklists and guidelines +- Improving team collaboration +- Reducing code review cycle time +- Maintaining code quality standards + +## Core Principles + +### 1. The Review Mindset + +**Goals of Code Review:** + +- Catch bugs and edge cases +- Ensure code maintainability +- Share knowledge across team +- Enforce coding standards +- Improve design and architecture +- Build team culture + +**Not the Goals:** + +- Show off knowledge +- Nitpick formatting (use linters) +- Block progress unnecessarily +- Rewrite to your preference + +### 2. Effective Feedback + +**Good Feedback is:** + +- Specific and actionable +- Educational, not judgmental +- Focused on the code, not the person +- Balanced (praise good work too) +- Prioritized (critical vs nice-to-have) + +```markdown +❌ Bad: "This is wrong." +✅ Good: "This could cause a race condition when multiple users +access simultaneously. Consider using a mutex here." + +❌ Bad: "Why didn't you use X pattern?" +✅ Good: "Have you considered the Repository pattern? It would +make this easier to test. Here's an example: [link]" + +❌ Bad: "Rename this variable." +✅ Good: "[nit] Consider `userCount` instead of `uc` for +clarity. Not blocking if you prefer to keep it." +``` + +### 3. Review Scope + +**What to Review:** + +- Logic correctness and edge cases +- Security vulnerabilities +- Performance implications +- Test coverage and quality +- Error handling +- Documentation and comments +- API design and naming +- Architectural fit + +**What Not to Review Manually:** + +- Code formatting (use Prettier, Black, etc.) +- Import organization +- Linting violations +- Simple typos + +## Review Process + +### Phase 1: Context Gathering (2-3 minutes) + +```markdown +Before diving into code, understand: + +1. Read PR description and linked issue +2. Check PR size (>400 lines? Ask to split) +3. Review CI/CD status (tests passing?) +4. Understand the business requirement +5. Note any relevant architectural decisions +``` + +### Phase 2: High-Level Review (5-10 minutes) + +```markdown +1. **Architecture & Design** + - Does the solution fit the problem? + - Are there simpler approaches? + - Is it consistent with existing patterns? + - Will it scale? + +2. **File Organization** + - Are new files in the right places? + - Is code grouped logically? + - Are there duplicate files? + +3. **Testing Strategy** + - Are there tests? + - Do tests cover edge cases? + - Are tests readable? +``` + +### Phase 3: Line-by-Line Review (10-20 minutes) + +```markdown +For each file: + +1. **Logic & Correctness** + - Edge cases handled? + - Off-by-one errors? + - Null/undefined checks? + - Race conditions? + +2. **Security** + - Input validation? + - SQL injection risks? + - XSS vulnerabilities? + - Sensitive data exposure? + +3. **Performance** + - N+1 queries? + - Unnecessary loops? + - Memory leaks? + - Blocking operations? + +4. **Maintainability** + - Clear variable names? + - Functions doing one thing? + - Complex code commented? + - Magic numbers extracted? +``` + +### Phase 4: Summary & Decision (2-3 minutes) + +```markdown +1. Summarize key concerns +2. Highlight what you liked +3. Make clear decision: + - ✅ Approve + - 💬 Comment (minor suggestions) + - 🔄 Request Changes (must address) +4. Offer to pair if complex +``` + +## Review Techniques + +### Technique 1: The Checklist Method + +```markdown +## Security Checklist + +- [ ] User input validated and sanitized +- [ ] SQL queries use parameterization +- [ ] Authentication/authorization checked +- [ ] Secrets not hardcoded +- [ ] Error messages don't leak info + +## Performance Checklist + +- [ ] No N+1 queries +- [ ] Database queries indexed +- [ ] Large lists paginated +- [ ] Expensive operations cached +- [ ] No blocking I/O in hot paths + +## Testing Checklist + +- [ ] Happy path tested +- [ ] Edge cases covered +- [ ] Error cases tested +- [ ] Test names are descriptive +- [ ] Tests are deterministic +``` + +### Technique 2: The Question Approach + +Instead of stating problems, ask questions to encourage thinking: + +```markdown +❌ "This will fail if the list is empty." +✅ "What happens if `items` is an empty array?" + +❌ "You need error handling here." +✅ "How should this behave if the API call fails?" + +❌ "This is inefficient." +✅ "I see this loops through all users. Have we considered +the performance impact with 100k users?" +``` + +### Technique 3: Suggest, Don't Command + +```markdown +## Use Collaborative Language + +❌ "You must change this to use async/await" +✅ "Suggestion: async/await might make this more readable: +`typescript + async function fetchUser(id: string) { + const user = await db.query('SELECT * FROM users WHERE id = ?', id); + return user; + } + ` +What do you think?" + +❌ "Extract this into a function" +✅ "This logic appears in 3 places. Would it make sense to +extract it into a shared utility function?" +``` + +### Technique 4: Differentiate Severity + +```markdown +Use labels to indicate priority: + +🔴 [blocking] - Must fix before merge +🟡 [important] - Should fix, discuss if disagree +🟢 [nit] - Nice to have, not blocking +💡 [suggestion] - Alternative approach to consider +📚 [learning] - Educational comment, no action needed +🎉 [praise] - Good work, keep it up! + +Example: +"🔴 [blocking] This SQL query is vulnerable to injection. +Please use parameterized queries." + +"🟢 [nit] Consider renaming `data` to `userData` for clarity." + +"🎉 [praise] Excellent test coverage! This will catch edge cases." +``` + +## Language-Specific Patterns + +### Python Code Review + +```python +# Check for Python-specific issues + +# ❌ Mutable default arguments +def add_item(item, items=[]): # Bug! Shared across calls + items.append(item) + return items + +# ✅ Use None as default +def add_item(item, items=None): + if items is None: + items = [] + items.append(item) + return items + +# ❌ Catching too broad +try: + result = risky_operation() +except: # Catches everything, even KeyboardInterrupt! + pass + +# ✅ Catch specific exceptions +try: + result = risky_operation() +except ValueError as e: + logger.error(f"Invalid value: {e}") + raise + +# ❌ Using mutable class attributes +class User: + permissions = [] # Shared across all instances! + +# ✅ Initialize in __init__ +class User: + def __init__(self): + self.permissions = [] +``` + +### TypeScript/JavaScript Code Review + +```typescript +// Check for TypeScript-specific issues + +// ❌ Using any defeats type safety +function processData(data: any) { // Avoid any + return data.value; +} + +// ✅ Use proper types +interface DataPayload { + value: string; +} +function processData(data: DataPayload) { + return data.value; +} + +// ❌ Not handling async errors +async function fetchUser(id: string) { + const response = await fetch(`/api/users/${id}`); + return response.json(); // What if network fails? +} + +// ✅ Handle errors properly +async function fetchUser(id: string): Promise { + try { + const response = await fetch(`/api/users/${id}`); + if (!response.ok) { + throw new Error(`HTTP ${response.status}`); + } + return await response.json(); + } catch (error) { + console.error('Failed to fetch user:', error); + throw error; + } +} + +// ❌ Mutation of props +function UserProfile({ user }: Props) { + user.lastViewed = new Date(); // Mutating prop! + return
{user.name}
; +} + +// ✅ Don't mutate props +function UserProfile({ user, onView }: Props) { + useEffect(() => { + onView(user.id); // Notify parent to update + }, [user.id]); + return
{user.name}
; +} +``` + +## Advanced Review Patterns + +### Pattern 1: Architectural Review + +```markdown +When reviewing significant changes: + +1. **Design Document First** + - For large features, request design doc before code + - Review design with team before implementation + - Agree on approach to avoid rework + +2. **Review in Stages** + - First PR: Core abstractions and interfaces + - Second PR: Implementation + - Third PR: Integration and tests + - Easier to review, faster to iterate + +3. **Consider Alternatives** + - "Have we considered using [pattern/library]?" + - "What's the tradeoff vs. the simpler approach?" + - "How will this evolve as requirements change?" +``` + +### Pattern 2: Test Quality Review + +```typescript +// ❌ Poor test: Implementation detail testing +test('increments counter variable', () => { + const component = render(); + const button = component.getByRole('button'); + fireEvent.click(button); + expect(component.state.counter).toBe(1); // Testing internal state +}); + +// ✅ Good test: Behavior testing +test('displays incremented count when clicked', () => { + render(); + const button = screen.getByRole('button', { name: /increment/i }); + fireEvent.click(button); + expect(screen.getByText('Count: 1')).toBeInTheDocument(); +}); + +// Review questions for tests: +// - Do tests describe behavior, not implementation? +// - Are test names clear and descriptive? +// - Do tests cover edge cases? +// - Are tests independent (no shared state)? +// - Can tests run in any order? +``` + +### Pattern 3: Security Review + +```markdown +## Security Review Checklist + +### Authentication & Authorization + +- [ ] Is authentication required where needed? +- [ ] Are authorization checks before every action? +- [ ] Is JWT validation proper (signature, expiry)? +- [ ] Are API keys/secrets properly secured? + +### Input Validation + +- [ ] All user inputs validated? +- [ ] File uploads restricted (size, type)? +- [ ] SQL queries parameterized? +- [ ] XSS protection (escape output)? + +### Data Protection + +- [ ] Passwords hashed (bcrypt/argon2)? +- [ ] Sensitive data encrypted at rest? +- [ ] HTTPS enforced for sensitive data? +- [ ] PII handled according to regulations? + +### Common Vulnerabilities + +- [ ] No eval() or similar dynamic execution? +- [ ] No hardcoded secrets? +- [ ] CSRF protection for state-changing operations? +- [ ] Rate limiting on public endpoints? +``` + +## Giving Difficult Feedback + +### Pattern: The Sandwich Method (Modified) + +```markdown +Traditional: Praise + Criticism + Praise (feels fake) + +Better: Context + Specific Issue + Helpful Solution + +Example: +"I noticed the payment processing logic is inline in the +controller. This makes it harder to test and reuse. + +[Specific Issue] +The calculateTotal() function mixes tax calculation, +discount logic, and database queries, making it difficult +to unit test and reason about. + +[Helpful Solution] +Could we extract this into a PaymentService class? That +would make it testable and reusable. I can pair with you +on this if helpful." +``` + +### Handling Disagreements + +```markdown +When author disagrees with your feedback: + +1. **Seek to Understand** + "Help me understand your approach. What led you to + choose this pattern?" + +2. **Acknowledge Valid Points** + "That's a good point about X. I hadn't considered that." + +3. **Provide Data** + "I'm concerned about performance. Can we add a benchmark + to validate the approach?" + +4. **Escalate if Needed** + "Let's get [architect/senior dev] to weigh in on this." + +5. **Know When to Let Go** + If it's working and not a critical issue, approve it. + Perfection is the enemy of progress. +``` + +## Best Practices + +1. **Review Promptly**: Within 24 hours, ideally same day +2. **Limit PR Size**: 200-400 lines max for effective review +3. **Review in Time Blocks**: 60 minutes max, take breaks +4. **Use Review Tools**: GitHub, GitLab, or dedicated tools +5. **Automate What You Can**: Linters, formatters, security scans +6. **Build Rapport**: Emoji, praise, and empathy matter +7. **Be Available**: Offer to pair on complex issues +8. **Learn from Others**: Review others' review comments + +## Common Pitfalls + +- **Perfectionism**: Blocking PRs for minor style preferences +- **Scope Creep**: "While you're at it, can you also..." +- **Inconsistency**: Different standards for different people +- **Delayed Reviews**: Letting PRs sit for days +- **Ghosting**: Requesting changes then disappearing +- **Rubber Stamping**: Approving without actually reviewing +- **Bike Shedding**: Debating trivial details extensively + +## Templates + +### PR Review Comment Template + +```markdown +## Summary + +[Brief overview of what was reviewed] + +## Strengths + +- [What was done well] +- [Good patterns or approaches] + +## Required Changes + +🔴 [Blocking issue 1] +🔴 [Blocking issue 2] + +## Suggestions + +💡 [Improvement 1] +💡 [Improvement 2] + +## Questions + +❓ [Clarification needed on X] +❓ [Alternative approach consideration] + +## Verdict + +✅ Approve after addressing required changes +``` + +## Resources + +- **references/code-review-best-practices.md**: Comprehensive review guidelines +- **references/common-bugs-checklist.md**: Language-specific bugs to watch for +- **references/security-review-guide.md**: Security-focused review checklist +- **assets/pr-review-template.md**: Standard review comment template +- **assets/review-checklist.md**: Quick reference checklist +- **scripts/pr-analyzer.py**: Analyze PR complexity and suggest reviewers diff --git a/.claude/skills/doc-coauthoring/SKILL.md b/.claude/skills/doc-coauthoring/SKILL.md new file mode 100644 index 000000000..3d21e277b --- /dev/null +++ b/.claude/skills/doc-coauthoring/SKILL.md @@ -0,0 +1,394 @@ +--- +name: doc-coauthoring +description: Guide users through a structured workflow for co-authoring documentation. Use when user wants to write documentation, proposals, technical specs, decision docs, or similar structured content. This workflow helps users efficiently transfer context, refine content through iteration, and verify the doc works for readers. Trigger when user mentions writing docs, creating proposals, drafting specs, or similar documentation tasks. +--- + +# Doc Co-Authoring Workflow + +This skill provides a structured workflow for guiding users through collaborative document creation. Act as an active guide, walking users through three stages: Context Gathering, Refinement & Structure, and Reader Testing. + +## When to Offer This Workflow + +**Trigger conditions:** + +- User mentions writing documentation: "write a doc", "draft a proposal", "create a spec", "write up" +- User mentions specific doc types: "PRD", "design doc", "decision doc", "RFC" +- User seems to be starting a substantial writing task + +**Initial offer:** +Offer the user a structured workflow for co-authoring the document. Explain the three stages: + +1. **Context Gathering**: User provides all relevant context while Claude asks clarifying questions +2. **Refinement & Structure**: Iteratively build each section through brainstorming and editing +3. **Reader Testing**: Test the doc with a fresh Claude (no context) to catch blind spots before others read it + +Explain that this approach helps ensure the doc works well when others read it (including when they paste it into Claude). Ask if they want to try this workflow or prefer to work freeform. + +If user declines, work freeform. If user accepts, proceed to Stage 1. + +## Stage 1: Context Gathering + +**Goal:** Close the gap between what the user knows and what Claude knows, enabling smart guidance later. + +### Initial Questions + +Start by asking the user for meta-context about the document: + +1. What type of document is this? (e.g., technical spec, decision doc, proposal) +2. Who's the primary audience? +3. What's the desired impact when someone reads this? +4. Is there a template or specific format to follow? +5. Any other constraints or context to know? + +Inform them they can answer in shorthand or dump information however works best for them. + +**If user provides a template or mentions a doc type:** + +- Ask if they have a template document to share +- If they provide a link to a shared document, use the appropriate integration to fetch it +- If they provide a file, read it + +**If user mentions editing an existing shared document:** + +- Use the appropriate integration to read the current state +- Check for images without alt-text +- If images exist without alt-text, explain that when others use Claude to understand the doc, Claude won't be able to see them. Ask if they want alt-text generated. If so, request they paste each image into chat for descriptive alt-text generation. + +### Info Dumping + +Once initial questions are answered, encourage the user to dump all the context they have. Request information such as: + +- Background on the project/problem +- Related team discussions or shared documents +- Why alternative solutions aren't being used +- Organizational context (team dynamics, past incidents, politics) +- Timeline pressures or constraints +- Technical architecture or dependencies +- Stakeholder concerns + +Advise them not to worry about organizing it - just get it all out. Offer multiple ways to provide context: + +- Info dump stream-of-consciousness +- Point to team channels or threads to read +- Link to shared documents + +**If integrations are available** (e.g., Slack, Teams, Google Drive, SharePoint, or other MCP servers), mention that these can be used to pull in context directly. + +**If no integrations are detected and in Claude.ai or Claude app:** Suggest they can enable connectors in their Claude settings to allow pulling context from messaging apps and document storage directly. + +Inform them clarifying questions will be asked once they've done their initial dump. + +**During context gathering:** + +- If user mentions team channels or shared documents: + - If integrations available: Inform them the content will be read now, then use the appropriate integration + - If integrations not available: Explain lack of access. Suggest they enable connectors in Claude settings, or paste the relevant content directly. + +- If user mentions entities/projects that are unknown: + - Ask if connected tools should be searched to learn more + - Wait for user confirmation before searching + +- As user provides context, track what's being learned and what's still unclear + +**Asking clarifying questions:** + +When user signals they've done their initial dump (or after substantial context provided), ask clarifying questions to ensure understanding: + +Generate 5-10 numbered questions based on gaps in the context. + +Inform them they can use shorthand to answer (e.g., "1: yes, 2: see #channel, 3: no because backwards compat"), link to more docs, point to channels to read, or just keep info-dumping. Whatever's most efficient for them. + +**Exit condition:** +Sufficient context has been gathered when questions show understanding - when edge cases and trade-offs can be asked about without needing basics explained. + +**Transition:** +Ask if there's any more context they want to provide at this stage, or if it's time to move on to drafting the document. + +If user wants to add more, let them. When ready, proceed to Stage 2. + +## Stage 2: Refinement & Structure + +**Goal:** Build the document section by section through brainstorming, curation, and iterative refinement. + +**Instructions to user:** +Explain that the document will be built section by section. For each section: + +1. Clarifying questions will be asked about what to include +2. 5-20 options will be brainstormed +3. User will indicate what to keep/remove/combine +4. The section will be drafted +5. It will be refined through surgical edits + +Start with whichever section has the most unknowns (usually the core decision/proposal), then work through the rest. + +**Section ordering:** + +If the document structure is clear: +Ask which section they'd like to start with. + +Suggest starting with whichever section has the most unknowns. For decision docs, that's usually the core proposal. For specs, it's typically the technical approach. Summary sections are best left for last. + +If user doesn't know what sections they need: +Based on the type of document and template, suggest 3-5 sections appropriate for the doc type. + +Ask if this structure works, or if they want to adjust it. + +**Once structure is agreed:** + +Create the initial document structure with placeholder text for all sections. + +**If access to artifacts is available:** +Use `create_file` to create an artifact. This gives both Claude and the user a scaffold to work from. + +Inform them that the initial structure with placeholders for all sections will be created. + +Create artifact with all section headers and brief placeholder text like "[To be written]" or "[Content here]". + +Provide the scaffold link and indicate it's time to fill in each section. + +**If no access to artifacts:** +Create a markdown file in the working directory. Name it appropriately (e.g., `decision-doc.md`, `technical-spec.md`). + +Inform them that the initial structure with placeholders for all sections will be created. + +Create file with all section headers and placeholder text. + +Confirm the filename has been created and indicate it's time to fill in each section. + +**For each section:** + +### Step 1: Clarifying Questions + +Announce work will begin on the [SECTION NAME] section. Ask 5-10 clarifying questions about what should be included: + +Generate 5-10 specific questions based on context and section purpose. + +Inform them they can answer in shorthand or just indicate what's important to cover. + +### Step 2: Brainstorming + +For the [SECTION NAME] section, brainstorm [5-20] things that might be included, depending on the section's complexity. Look for: + +- Context shared that might have been forgotten +- Angles or considerations not yet mentioned + +Generate 5-20 numbered options based on section complexity. At the end, offer to brainstorm more if they want additional options. + +### Step 3: Curation + +Ask which points should be kept, removed, or combined. Request brief justifications to help learn priorities for the next sections. + +Provide examples: + +- "Keep 1,4,7,9" +- "Remove 3 (duplicates 1)" +- "Remove 6 (audience already knows this)" +- "Combine 11 and 12" + +**If user gives freeform feedback** (e.g., "looks good" or "I like most of it but...") instead of numbered selections, extract their preferences and proceed. Parse what they want kept/removed/changed and apply it. + +### Step 4: Gap Check + +Based on what they've selected, ask if there's anything important missing for the [SECTION NAME] section. + +### Step 5: Drafting + +Use `str_replace` to replace the placeholder text for this section with the actual drafted content. + +Announce the [SECTION NAME] section will be drafted now based on what they've selected. + +**If using artifacts:** +After drafting, provide a link to the artifact. + +Ask them to read through it and indicate what to change. Note that being specific helps learning for the next sections. + +**If using a file (no artifacts):** +After drafting, confirm completion. + +Inform them the [SECTION NAME] section has been drafted in [filename]. Ask them to read through it and indicate what to change. Note that being specific helps learning for the next sections. + +**Key instruction for user (include when drafting the first section):** +Provide a note: Instead of editing the doc directly, ask them to indicate what to change. This helps learning of their style for future sections. For example: "Remove the X bullet - already covered by Y" or "Make the third paragraph more concise". + +### Step 6: Iterative Refinement + +As user provides feedback: + +- Use `str_replace` to make edits (never reprint the whole doc) +- **If using artifacts:** Provide link to artifact after each edit +- **If using files:** Just confirm edits are complete +- If user edits doc directly and asks to read it: mentally note the changes they made and keep them in mind for future sections (this shows their preferences) + +**Continue iterating** until user is satisfied with the section. + +### Quality Checking + +After 3 consecutive iterations with no substantial changes, ask if anything can be removed without losing important information. + +When section is done, confirm [SECTION NAME] is complete. Ask if ready to move to the next section. + +**Repeat for all sections.** + +### Near Completion + +As approaching completion (80%+ of sections done), announce intention to re-read the entire document and check for: + +- Flow and consistency across sections +- Redundancy or contradictions +- Anything that feels like "slop" or generic filler +- Whether every sentence carries weight + +Read entire document and provide feedback. + +**When all sections are drafted and refined:** +Announce all sections are drafted. Indicate intention to review the complete document one more time. + +Review for overall coherence, flow, completeness. + +Provide any final suggestions. + +Ask if ready to move to Reader Testing, or if they want to refine anything else. + +## Stage 3: Reader Testing + +**Goal:** Test the document with a fresh Claude (no context bleed) to verify it works for readers. + +**Instructions to user:** +Explain that testing will now occur to see if the document actually works for readers. This catches blind spots - things that make sense to the authors but might confuse others. + +### Testing Approach + +**If access to sub-agents is available (e.g., in Claude Code):** + +Perform the testing directly without user involvement. + +### Step 1: Predict Reader Questions + +Announce intention to predict what questions readers might ask when trying to discover this document. + +Generate 5-10 questions that readers would realistically ask. + +### Step 2: Test with Sub-Agent + +Announce that these questions will be tested with a fresh Claude instance (no context from this conversation). + +For each question, invoke a sub-agent with just the document content and the question. + +Summarize what Reader Claude got right/wrong for each question. + +### Step 3: Run Additional Checks + +Announce additional checks will be performed. + +Invoke sub-agent to check for ambiguity, false assumptions, contradictions. + +Summarize any issues found. + +### Step 4: Report and Fix + +If issues found: +Report that Reader Claude struggled with specific issues. + +List the specific issues. + +Indicate intention to fix these gaps. + +Loop back to refinement for problematic sections. + +--- + +**If no access to sub-agents (e.g., claude.ai web interface):** + +The user will need to do the testing manually. + +### Step 1: Predict Reader Questions + +Ask what questions people might ask when trying to discover this document. What would they type into Claude.ai? + +Generate 5-10 questions that readers would realistically ask. + +### Step 2: Setup Testing + +Provide testing instructions: + +1. Open a fresh Claude conversation: https://claude.ai +2. Paste or share the document content (if using a shared doc platform with connectors enabled, provide the link) +3. Ask Reader Claude the generated questions + +For each question, instruct Reader Claude to provide: + +- The answer +- Whether anything was ambiguous or unclear +- What knowledge/context the doc assumes is already known + +Check if Reader Claude gives correct answers or misinterprets anything. + +### Step 3: Additional Checks + +Also ask Reader Claude: + +- "What in this doc might be ambiguous or unclear to readers?" +- "What knowledge or context does this doc assume readers already have?" +- "Are there any internal contradictions or inconsistencies?" + +### Step 4: Iterate Based on Results + +Ask what Reader Claude got wrong or struggled with. Indicate intention to fix those gaps. + +Loop back to refinement for any problematic sections. + +--- + +### Exit Condition (Both Approaches) + +When Reader Claude consistently answers questions correctly and doesn't surface new gaps or ambiguities, the doc is ready. + +## Final Review + +When Reader Testing passes: +Announce the doc has passed Reader Claude testing. Before completion: + +1. Recommend they do a final read-through themselves - they own this document and are responsible for its quality +2. Suggest double-checking any facts, links, or technical details +3. Ask them to verify it achieves the impact they wanted + +Ask if they want one more review, or if the work is done. + +**If user wants final review, provide it. Otherwise:** +Announce document completion. Provide a few final tips: + +- Consider linking this conversation in an appendix so readers can see how the doc was developed +- Use appendices to provide depth without bloating the main doc +- Update the doc as feedback is received from real readers + +## Tips for Effective Guidance + +**Tone:** + +- Be direct and procedural +- Explain rationale briefly when it affects user behavior +- Don't try to "sell" the approach - just execute it + +**Handling Deviations:** + +- If user wants to skip a stage: Ask if they want to skip this and write freeform +- If user seems frustrated: Acknowledge this is taking longer than expected. Suggest ways to move faster +- Always give user agency to adjust the process + +**Context Management:** + +- Throughout, if context is missing on something mentioned, proactively ask +- Don't let gaps accumulate - address them as they come up + +**Artifact Management:** + +- Use `create_file` for drafting full sections +- Use `str_replace` for all edits +- Provide artifact link after every change +- Never use artifacts for brainstorming lists - that's just conversation + +**Quality over Speed:** + +- Don't rush through stages +- Each iteration should make meaningful improvements +- The goal is a document that actually works for readers diff --git a/AGENTS.md b/AGENTS.md index eb9c00d30..6c3e21729 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -53,3 +53,44 @@ The project uses `yarn` for dependency management and script execution. - `src/index.ts`: Main entry point, re-exports `pure` and adds side effects (auto-cleanup). - `examples/`: Example React Native applications using the library. - `website/`: Documentation website. + + + +## Available Skills + + + +When users ask you to perform tasks, check if any of the available skills below can help complete the task more effectively. Skills provide specialized capabilities and domain knowledge. + +How to use skills: + +- Invoke: Bash("openskills read ") +- The skill content will load with detailed instructions on how to complete the task +- Base directory provided in output for resolving bundled resources (references/, scripts/, assets/) + +Usage notes: + +- Only use skills listed in below +- Do not invoke a skill that is already loaded in your context +- Each skill invocation is stateless + + + + + +code-review +Master effective code review practices to provide constructive feedback, catch bugs early, and foster knowledge sharing while maintaining team morale. Use when reviewing pull requests, establishing review standards, or mentoring developers. +project + + + +doc-coauthoring +Guide users through a structured workflow for co-authoring documentation. Use when user wants to write documentation, proposals, technical specs, decision docs, or similar structured content. This workflow helps users efficiently transfer context, refine content through iteration, and verify the doc works for readers. Trigger when user mentions writing docs, creating proposals, drafting specs, or similar documentation tasks. +project + + + + + + + diff --git a/README.md b/README.md index 26fd0dc15..cb4c3e15d 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,15 @@ yarn add --dev @testing-library/react-native npm install --save-dev @testing-library/react-native ``` -This library has a `peerDependencies` listing for `test-renderer`. Make sure that your `test-renderer` version matches exactly the `react` version, avoid using `^` in version number. +This library has a `peerDependencies` listing for [Test Renderer](https://github.com/mdjastrzebski/test-renderer). Make sure to install it as a dev dependency: + +```sh +# Yarn install: +yarn add --dev test-renderer + +# NPM install +npm install --save-dev test-renderer +``` ### Additional Jest matchers @@ -57,7 +65,7 @@ test('form submits two answers', async () => { const onSubmit = jest.fn(); const user = userEvent.setup(); - render(); + await render(); const answerInputs = screen.getAllByLabelText('answer input'); @@ -85,9 +93,9 @@ React Native Testing Library consists of following APIs: - [`screen` object](https://callstack.github.io/react-native-testing-library/docs/api/screen) - access rendered UI: - [Queries](https://callstack.github.io/react-native-testing-library/docs/api/queries) - find rendered components by various predicates: role, text, test ids, etc - Lifecycle methods: [`rerender`](https://callstack.github.io/react-native-testing-library/docs/api/screen#rerender), [`unmount`](https://callstack.github.io/react-native-testing-library/docs/api/screen#unmount) - - Helpers: [`debug`](https://callstack.github.io/react-native-testing-library/docs/api/screen#debug), [`toJSON`](https://callstack.github.io/react-native-testing-library/docs/api/screen#tojson), [`root`](https://callstack.github.io/react-native-testing-library/docs/api/screen#root) + - Helpers: [`debug`](https://callstack.github.io/react-native-testing-library/docs/api/screen#debug), [`toJSON`](https://callstack.github.io/react-native-testing-library/docs/api/screen#tojson), [`root`](https://callstack.github.io/react-native-testing-library/docs/api/screen#root), [`container`](https://callstack.github.io/react-native-testing-library/docs/api/screen#container) - [Jest matchers](https://callstack.github.io/react-native-testing-library/docs/api/jest-matchers) - validate assumptions about your UI -- [User Event](https://callstack.github.io/react-native-testing-library/docs/api/events/user-event) - simulate common user interactions like [`press`](https://callstack.github.io/react-native-testing-library/docs/api/events/user-event#press) or [`type`](https://callstack.github.io/react-native-testing-library/docs/user-event#type) in a realistic way +- [User Event](https://callstack.github.io/react-native-testing-library/docs/api/events/user-event) - simulate common user interactions like [`press`](https://callstack.github.io/react-native-testing-library/docs/api/events/user-event#press) or [`type`](https://callstack.github.io/react-native-testing-library/docs/api/events/user-event#type) in a realistic way - [Fire Event](https://callstack.github.io/react-native-testing-library/docs/api/events/fire-event) - simulate any component event in a simplified way - [`renderHook` function](https://callstack.github.io/react-native-testing-library/docs/api/misc/render-hook) - render hooks for testing purposes - Miscellaneous APIs: @@ -98,6 +106,7 @@ React Native Testing Library consists of following APIs: ## Migration Guides +- **[Migration to 14.0](https://callstack.github.io/react-native-testing-library/docs/migration/v14)** - Drops React 18, async APIs by default - [Migration to 13.0](https://callstack.github.io/react-native-testing-library/docs/migration/v13) - [Migration to built-in Jest Matchers](https://callstack.github.io/react-native-testing-library/docs/migration/jest-matchers) diff --git a/codemods/v14-update-deps/README.md b/codemods/v14-update-deps/README.md index 96fe4d821..5d1deb098 100644 --- a/codemods/v14-update-deps/README.md +++ b/codemods/v14-update-deps/README.md @@ -7,7 +7,7 @@ This codemod automatically updates your `package.json` to prepare for React Nati - Removes `@types/react-test-renderer` and `react-test-renderer` (no longer needed) - Moves `@testing-library/react-native` to `devDependencies` if it's in `dependencies` - Updates `@testing-library/react-native` to `^14.0.0-alpha.5` -- Adds `test-renderer@0.12.0` to `devDependencies` +- Adds `test-renderer@0.14.0` to `devDependencies` ## Usage diff --git a/package.json b/package.json index 46cb19fea..d70bf51b2 100644 --- a/package.json +++ b/package.json @@ -59,7 +59,7 @@ "jest": ">=29.0.0", "react": ">=19.0.0", "react-native": ">=0.78", - "test-renderer": "~0.13.2" + "test-renderer": "^0.14.0" }, "peerDependenciesMeta": { "jest": { @@ -91,7 +91,7 @@ "react-native": "0.83.1", "react-native-gesture-handler": "^2.29.1", "release-it": "^19.0.6", - "test-renderer": "0.13.2", + "test-renderer": "0.14.0", "typescript": "^5.9.3", "typescript-eslint": "^8.47.0" }, diff --git a/src/render.tsx b/src/render.tsx index a98d519a0..bea7b2093 100644 --- a/src/render.tsx +++ b/src/render.tsx @@ -30,7 +30,7 @@ export interface RenderOptions { export type RenderResult = Awaited>; /** - * Renders test component deeply using React Test Renderer and exposes helpers + * Renders test component deeply using Test Renderer and exposes helpers * to assert on the output. */ export async function render(element: React.ReactElement, options: RenderOptions = {}) { diff --git a/website/docs/14.x/cookbook/advanced/network-requests.md b/website/docs/14.x/cookbook/advanced/network-requests.md index 31a31d6c4..1808d3efa 100644 --- a/website/docs/14.x/cookbook/advanced/network-requests.md +++ b/website/docs/14.x/cookbook/advanced/network-requests.md @@ -246,7 +246,7 @@ afterAll(() => server.close()); describe('PhoneBook', () => { it('fetches all contacts and favorites successfully and renders lists in sections correctly', async () => { - render(); + await render(); await waitForElementToBeRemoved(() => screen.getByText(/users data not quite there yet/i)); expect(await screen.findByText('Name: Mrs Ida Kristensen')).toBeOnTheScreen(); @@ -325,7 +325,7 @@ describe('PhoneBook', () => { ... it('fails to fetch all contacts and renders error message', async () => { mockServerFailureForGetAllContacts(); - render(); + await render(); await waitForElementToBeRemoved(() => screen.getByText(/users data not quite there yet/i)); expect( diff --git a/website/docs/14.x/cookbook/basics/_meta.json b/website/docs/14.x/cookbook/basics/_meta.json index 591daedc8..105f33cd9 100644 --- a/website/docs/14.x/cookbook/basics/_meta.json +++ b/website/docs/14.x/cookbook/basics/_meta.json @@ -1 +1 @@ -["async-tests", "custom-render"] +["async-events", "custom-render"] diff --git a/website/docs/14.x/cookbook/basics/async-tests.md b/website/docs/14.x/cookbook/basics/async-events.md similarity index 84% rename from website/docs/14.x/cookbook/basics/async-tests.md rename to website/docs/14.x/cookbook/basics/async-events.md index 8c9cd4c43..3f0b14675 100644 --- a/website/docs/14.x/cookbook/basics/async-tests.md +++ b/website/docs/14.x/cookbook/basics/async-events.md @@ -1,17 +1,18 @@ -# Async tests +# Async Events ## Summary -Typically, you would write synchronous tests, as they are simple and get the work done. However, there are cases when using asynchronous (async) tests might be necessary or beneficial. The two most common cases are: +In RNTL v14, all tests are async since `render()`, `fireEvent()`, and other core APIs return Promises. Beyond the basic async APIs, there are additional async utilities for handling events that complete over time: -1. **Testing Code with asynchronous operations**: When your code relies on asynchronous operations, such as network calls or database queries, async tests are essential. Even though you should mock these network calls, the mock should act similarly to the actual behavior and hence by async. -2. **UserEvent API:** Using the [User Event API](docs/api/events/user-event) in your tests creates more realistic event handling. These interactions introduce delays (even though these are typically event-loop ticks with 0 ms delays), requiring async tests to handle the timing correctly. +1. **Waiting for elements to appear**: Use `findBy*` queries when elements appear after some delay (e.g., after data fetching). +2. **Waiting for conditions**: Use `waitFor()` to wait for arbitrary conditions to be met. +3. **Waiting for elements to disappear**: Use `waitForElementToBeRemoved()` when elements should be removed after some action. -Using async tests when needed ensures your tests are reliable and simulate real-world conditions accurately. +These utilities help you write reliable tests that properly handle timing in your application. ### Example -Consider a basic asynchronous test for a user signing in with correct credentials: +Consider a test for a user signing in with correct credentials: ```javascript test('User can sign in with correct credentials', async () => { diff --git a/website/docs/14.x/cookbook/basics/custom-render.md b/website/docs/14.x/cookbook/basics/custom-render.md index 830d3005c..aa3a645f7 100644 --- a/website/docs/14.x/cookbook/basics/custom-render.md +++ b/website/docs/14.x/cookbook/basics/custom-render.md @@ -51,8 +51,8 @@ Example [full source code](https://github.com/callstack/react-native-testing-lib A custom render function might accept additional parameters to allow for setting up different start conditions for a test, e.g., the initial state for global state management. ```tsx title=SomeScreen.test.tsx -test('renders SomeScreen for logged in user', () => { - renderScreen(, { state: loggedInState }); +test('renders SomeScreen for logged in user', async () => { + await renderScreen(, { state: loggedInState }); // ... }); ``` @@ -66,13 +66,18 @@ function renderNavigator(ui, options); function renderScreen(ui, options); ``` -#### Async function +#### Async setup -Make it async if you want to put some async setup in your custom render function. +Since `render` is async, your custom render function should be marked as `async` and use `await render()`. This pattern also makes it easy to add additional async setup if needed: ```tsx title=SomeScreen.test.tsx +async function renderWithData(ui: React.ReactElement) { + const data = await fetchTestData(); + return await render({ui}); +} + test('renders SomeScreen', async () => { - await renderWithAsync(); + await renderWithData(); // ... }); ``` diff --git a/website/docs/14.x/cookbook/state-management/jotai.md b/website/docs/14.x/cookbook/state-management/jotai.md index 0e0c72e88..e599ea362 100644 --- a/website/docs/14.x/cookbook/state-management/jotai.md +++ b/website/docs/14.x/cookbook/state-management/jotai.md @@ -65,18 +65,18 @@ We can test our `TaskList` component using React Native Testing Library's (RNTL) function. Although it is sufficient to test the empty state of the `TaskList` component, it is not enough to test the component with initial tasks present in the list. -```tsx title=status-management/jotai/__tests__/TaskList.test.tsx +```tsx title=state-management/jotai/__tests__/TaskList.test.tsx import * as React from 'react'; import { render, screen, userEvent } from '@testing-library/react-native'; import { renderWithAtoms } from './test-utils'; -import { TaskList } from './TaskList'; -import { newTaskTitleAtom, tasksAtom } from './state'; -import { Task } from './types'; +import { TaskList } from '../TaskList'; +import { newTaskTitleAtom, tasksAtom } from '../state'; +import { Task } from '../types'; jest.useFakeTimers(); -test('renders an empty task list', () => { - render(); +test('renders an empty task list', async () => { + await render(); expect(screen.getByText(/no tasks, start by adding one/i)).toBeOnTheScreen(); }); ``` @@ -88,7 +88,7 @@ initial values. We can create a custom render function that uses Jotai's `useHyd hydrate the atoms with initial values. This function will accept the initial atoms and their corresponding values as an argument. -```tsx title=status-management/jotai/test-utils.tsx +```tsx title=state-management/jotai/__tests__/test-utils.tsx import * as React from 'react'; import { render } from '@testing-library/react-native'; import { useHydrateAtoms } from 'jotai/utils'; @@ -108,14 +108,14 @@ export interface RenderWithAtomsOptions { * @param options - The render options including the initial atom values. * @returns The render result from `@testing-library/react-native`. */ -export const renderWithAtoms = ( +export async function renderWithAtoms( component: React.ReactElement, options: RenderWithAtomsOptions -) => { - return render( +) { + return await render( {component} ); -}; +} export type HydrateAtomsWrapperProps = React.PropsWithChildren<{ initialValues: AtomInitialValueTuple[]; @@ -144,12 +144,11 @@ We can now use the `renderWithAtoms` function to render the `TaskList` component In our test, we populated only one atom and its initial value, but you can add other Jotai atoms and their corresponding values to the initialValues array as needed. ::: -```tsx title=status-management/jotai/__tests__/TaskList.test.tsx -======= +```tsx title=state-management/jotai/__tests__/TaskList.test.tsx const INITIAL_TASKS: Task[] = [{ id: '1', title: 'Buy bread' }]; test('renders a to do list with 1 items initially, and adds a new item', async () => { - renderWithAtoms(, { + await renderWithAtoms(, { initialValues: [ [tasksAtom, INITIAL_TASKS], [newTaskTitleAtom, ''], @@ -202,7 +201,7 @@ No special setup is required to test these functions, as `store.set` is availabl Jotai. ```tsx title=state-management/jotai/__tests__/TaskList.test.tsx -import { addTask, getAllTasks, store, tasksAtom } from './state'; +import { addTask, getAllTasks, store, tasksAtom } from '../state'; //... diff --git a/website/docs/14.x/docs/advanced/testing-env.mdx b/website/docs/14.x/docs/advanced/testing-env.mdx index 6fd797c13..db6a3fd15 100644 --- a/website/docs/14.x/docs/advanced/testing-env.mdx +++ b/website/docs/14.x/docs/advanced/testing-env.mdx @@ -41,7 +41,7 @@ It's worth noting that the React Testing Library (web one) works a bit different ## Element tree -Calling the `render()` function creates an element tree. This is done internally by invoking the renderer's `create()` method from Test Renderer. The output tree represents your React Native component tree, and each node of that tree is an "instance" of some React component (to be more precise, each node represents a React fiber, and only class components have instances, while function components store the hook state using fibers). +Calling the `render()` function creates an element tree. This is done internally by invoking the `createRoot()` function from Test Renderer. The output tree represents your React Native component tree, containing only host elements. Each node of that tree corresponds to a host component that would have a counterpart in the native view hierarchy. These tree elements are represented by `HostElement` type from Test Renderer: @@ -60,17 +60,17 @@ For more details, see the [Test Renderer documentation](https://github.com/mdjas ## Host and composite components -One of the most important aspects of the element tree is that it is composed of both host and composite components: +To understand RNTL's element tree, it's important to know the difference between host and composite components in React Native: -- [Host components](https://reactnative.dev/architecture/glossary#react-host-components-or-host-components) will have direct counterparts in the native view tree. Typical examples are ``, `` , ``, and `` from React Native. You can think of these as an analog of `
`, `` etc on the Web. You can also create custom host views as native modules or import them from 3rd party libraries, like React Navigation or React Native Gesture Handler. +- [Host components](https://reactnative.dev/architecture/glossary#react-host-components-or-host-components) have direct counterparts in the native view tree. Typical examples are ``, ``, ``, and `` from React Native. You can think of these as an analog of `
`, `` etc on the Web. You can also create custom host views as native modules or import them from 3rd party libraries, like React Navigation or React Native Gesture Handler. - [Composite components](https://reactnative.dev/architecture/glossary#react-composite-components) are React code organization units that exist only on the JavaScript side of your app. Typical examples are components you create (function and class components), components imported from React Native (`View`, `Text`, etc.), or 3rd party packages. That might initially sound confusing since we put React Native's `View` in both categories. There are two `View` components: composite and host. The relation between them is as follows: -- composite `View` is the type imported from the `react-native` package. It is a JavaScript component that renders the host `View` as its only child in the element tree. -- host `View`, which you do not render directly. React Native takes the props you pass to the composite `View`, does some processing on them and passes them to the host `View`. +- Composite `View` is the type imported from the `react-native` package. It is a JavaScript component that renders the host `View` as its only child. +- Host `View`, which you do not render directly. React Native takes the props you pass to the composite `View`, does some processing on them and passes them to the host `View`. -The part of the tree looks as follows: +In a full React tree, this would look like: ```jsx * (composite) @@ -78,13 +78,7 @@ The part of the tree looks as follows: * children prop passed in JSX ``` -A similar relation exists between other composite and host pairs: e.g. `Text` , `TextInput`, and `Image` components: - -```jsx -* (composite) - * (host) - * string (or mixed) content -``` +A similar relation exists between other composite and host pairs: e.g. `Text`, `TextInput`, and `Image` components. Not all React Native components are organized this way, e.g., when you use `Pressable` (or `TouchableOpacity`), there is no host `Pressable`, but composite `Pressable` is rendering a host `View` with specific props being set: @@ -94,28 +88,19 @@ Not all React Native components are organized this way, e.g., when you use `Pres * children prop passed in JSX ``` -### Differentiating between host and composite elements - -Any easy way to differentiate between host and composite elements is the `type` prop of `HostElement`: - -- for host components, it's always a string value representing a component name, e.g., `"View"` -- for composite components, it's a function or class corresponding to the component +### Host-only element tree -You can use the following code to check if a given element is a host one: +In RNTL v14, [Test Renderer](https://github.com/mdjastrzebski/test-renderer) only exposes host elements in the element tree. Composite components are not visible in the tree - you only see their host element output. This is an intentional design choice that aligns with Testing Library's philosophy: tests should focus on what users can see and interact with (host elements), not on implementation details (composite components). -```jsx -function isHostElement(element: HostElement) { - return typeof element.type === 'string'; -} -``` +For a `HostElement`, the `type` prop is always a string value representing the host component name, e.g., `"View"`, `"Text"`, `"TextInput"`. ## Tree nodes -We encourage you to only assert values on host views in your tests because they represent the user interface view and controls which the user can see and interact with. Users cannot see or interact with composite views as they exist purely in the JavaScript domain and do not generate any visible UI. +RNTL v14 queries and the element tree only expose host elements. This aligns with Testing Library's philosophy: tests should assert on what users can see and interact with. Host elements represent the actual UI controls that users interact with, while composite components exist purely in the JavaScript domain. -### Asserting props +### Understanding props -For example, suppose you assert a `style` prop of a composite element. In that case, there is no guarantee that the style will be visible to the user, as the component author can forget to pass this prop to some underlying `View` or other host component. Similarly `onPress` event handler on a composite prop can be unreachable by the user. +When asserting props on host elements, you're verifying what actually reaches the native view. This is important because composite components may process, transform, or even forget to pass props to their host children. ```jsx function ForgotToPassPropsButton({ title, onPress, style }) { @@ -127,7 +112,7 @@ function ForgotToPassPropsButton({ title, onPress, style }) { } ``` -In the above example, user-defined components accept both `onPress` and `style` props but do not pass them (through `Pressable`) to host views, so they will not affect the user interface. Additionally, React Native and other libraries might pass some of the props under different names or transform their values between composite and host components. +In the above example, the component accepts `onPress` and `style` props but doesn't pass them to host views, so they won't affect the user interface. By testing host elements, RNTL helps you catch these issues: if a prop doesn't reach a host element, users won't see or interact with it. ## Tree navigation @@ -135,12 +120,8 @@ In the above example, user-defined components accept both `onPress` and `style` You should avoid navigating over the element tree, as this makes your testing code fragile and may result in false positives. This section is more relevant for people who want to contribute to our codebase. ::: -You will encounter host and composite elements when navigating a tree of react elements using `parent` or `children` props of a `HostElement` element. You should be careful when navigating the element tree, as the tree structure for third-party components can change independently from your code and cause unexpected test failures. - -Inside RNTL, we have various tree navigation helpers: `getHostParent`, `getHostChildren`, etc. These are intentionally not exported, as using them is not recommended. +You can navigate the tree of host elements using `parent` or `children` props of a `HostElement`. Be careful when doing this, as the tree structure for third-party components can change independently from your code and cause unexpected test failures. ## Queries -All recommended Testing Library queries return host components to encourage the best practices described above. - -Only `UNSAFE_*ByType` and `UNSAFE_*ByProps` queries can return both host and composite components depending on used predicates. They are marked as unsafe precisely because testing composite components makes your test more fragile. +All Testing Library queries return host components to encourage the best practices described above. Since v14, RNTL uses [Test Renderer](https://github.com/mdjastrzebski/test-renderer), which only renders host elements, making it impossible to query composite components directly. diff --git a/website/docs/14.x/docs/advanced/understanding-act.mdx b/website/docs/14.x/docs/advanced/understanding-act.mdx index c1ae6f422..d67cfdfc4 100644 --- a/website/docs/14.x/docs/advanced/understanding-act.mdx +++ b/website/docs/14.x/docs/advanced/understanding-act.mdx @@ -1,13 +1,13 @@ # Understanding `act` function -When writing RNTL tests one of the things that confuses developers the most are cryptic [`act()`](https://reactjs.org/docs/testing-recipes.html#act) function errors logged into console. In this article I will try to build an understanding of the purpose and behaviour of `act()` so you can build your tests with more confidence. +When writing RNTL tests one of the things that confuses developers the most are cryptic [`act()`](https://react.dev/link/wrap-tests-with-act) function errors logged into console. In this article I will try to build an understanding of the purpose and behaviour of `act()` so you can build your tests with more confidence. -## `act` warnings +## `act` warning -Let’s start with typical `act()` warnings logged to console. There are two kinds of these issues, let’s call the first one the "sync `act()`" warning: +Let's start with a typical `act()` warning logged to console: ``` -Warning: An update to Component inside a test was not wrapped in act(...). +An update to Root inside a test was not wrapped in act(...). When testing, code that causes React state updates should be wrapped into act(...): @@ -15,17 +15,12 @@ act(() => { /* fire events that update state */ }); /* assert on the output */ -``` - -The second one relates to async usage of `act` so let’s call it the "async `act`" error: -``` -Warning: You called act(async () => ...) without await. This could lead to unexpected -testing behaviour, interleaving multiple act calls and mixing their scopes. You should -- await act(async () => ...); +This ensures that you're testing the behavior the user would see in the browser. +Learn more at https://react.dev/link/wrap-tests-with-act ``` -## Synchronous `act` +## Understanding `act` ### Responsibility @@ -46,66 +41,75 @@ function TestComponent() { } ``` -In the following tests we will directly use `ReactTestRenderer` instead of RNTL `render` function to render our component for tests. In order to expose familiar queries like `getByText` we will use `within` function from RNTL. +In the following tests we will directly use [Test Renderer](https://github.com/mdjastrzebski/test-renderer) instead of RNTL `render` function to render our component for tests. In order to expose familiar queries like `getByText` we will use `within` function from RNTL. ```jsx +import { createRoot } from 'test-renderer'; +import { within } from '@testing-library/react-native'; + test('render without act', () => { - const renderer = TestRenderer.create(); + const renderer = createRoot(); + renderer.render(); // Bind RNTL queries for root element. - const view = within(renderer.root); - expect(view.getByText('Count 0')).toBeOnTheScreen(); + const view = within(renderer.container); + expect(view.getByText('Count 0')).toBeTruthy(); }); ``` -When testing without `act` call wrapping rendering call, we see that the assertion runs just after the rendering but before `useEffect`hooks effects are applied. Which is not what we expected in our tests. +When testing without `act` call wrapping rendering call, we see that the assertion runs just after the rendering but before `useEffect` hooks effects are applied. Which is not what we expected in our tests. ```jsx +import { createRoot } from 'test-renderer'; +import { act, within } from '@testing-library/react-native'; + test('render with act', async () => { - let renderer: ReactTestRenderer; - await act(async () => { - renderer = TestRenderer.create(); + const renderer = createRoot(); + await act(() => { + renderer.render(); }); // Bind RNTL queries for root element. - const view = within(renderer!.root); - expect(view.getByText('Count 1')).toBeOnTheScreen(); + const view = within(renderer.container); + expect(view.getByText('Count 1')).toBeTruthy(); }); ``` -**Note**: In v14, `act` is now async by default and always returns a Promise. Even if your callback is synchronous, you should use `await act(async () => ...)`. +**Note**: In v14, `act` is now async by default and always returns a Promise. You should always use `await act(...)`. When wrapping rendering call with `act` we see that the changes caused by `useEffect` hook have been applied as we would expect. ### When to use act -The name `act` comes from [Arrange-Act-Assert](http://wiki.c2.com/?ArrangeActAssert) unit testing pattern. Which means it’s related to part of the test when we execute some actions on the component tree. +The name `act` comes from [Arrange-Act-Assert](http://wiki.c2.com/?ArrangeActAssert) unit testing pattern. Which means it's related to part of the test when we execute some actions on the component tree. So far we learned that `act` function allows tests to wait for all pending React interactions to be applied before we make our assertions. When using `act` we get guarantee that any state updates will be executed as well as any enqueued effects will be executed. Therefore, we should use `act` whenever there is some action that causes element tree to render, particularly: -- initial render call - `ReactTestRenderer.create` call -- re-rendering of component -`renderer.rerender` call +- initial render call - `renderer.render` call +- re-rendering of component - `renderer.render` call with updated element - triggering any event handlers that cause component tree render -Thankfully, for these basic cases RNTL has got you covered as our `render`, `rerender` and `fireEvent` methods already wrap their calls in `act` so that you do not have to do it explicitly. +Thankfully, for these basic cases RNTL has got you covered as our `render`, `rerender` and `fireEvent` methods already wrap their calls in `act` so that you do not have to do it explicitly. In v14, these functions are all async and should be awaited. -Note that `act` calls can be safely nested and internally form a stack of calls. However, overlapping `act` calls, which can be achieved using async version of `act`, [are not supported](https://github.com/facebook/react/blob/main/packages/react/src/ReactAct.js#L161). +Note that `act` calls can be safely nested and internally form a stack of calls. ### Implementation -As of React version of 18.1.0, the `act` implementation is defined in the [ReactAct.js source file](https://github.com/facebook/react/blob/main/packages/react/src/ReactAct.js) inside React repository. This implementation has been fairly stable since React 17.0. +The `act` implementation is defined in the [ReactAct.js source file](https://github.com/facebook/react/blob/main/packages/react/src/ReactAct.js) inside React repository. RNTL v14 requires React 19+, which provides the `act` function directly via `React.act`. + +RNTL exports `act` for convenience of the users as defined in the [act.ts source file](https://github.com/callstack/react-native-testing-library/blob/main/src/act.ts). In v14, `act` is now async by default and always returns a Promise, making it compatible with async React features like `Suspense` boundary or `use()` hook. The underlying implementation wraps React's `act` function to ensure consistent async behavior. -RNTL exports `act` for convenience of the users as defined in the [act.ts source file](https://github.com/callstack/react-native-testing-library/blob/main/src/act.ts). In v14, `act` is now async by default and always returns a Promise, making it compatible with React 19, React Suspense, and `React.use()`. The underlying implementation still uses React's `act` function, but wraps it to ensure consistent async behavior. +**Important**: You should always use `act` exported from `@testing-library/react-native` rather than the one from `react`. The RNTL version automatically ensures async behavior, whereas using `React.act` directly could still trigger synchronous act behavior if used improperly, leading to subtle test issues. -## Asynchronous `act` +## Asynchronous code -So far we have seen synchronous version of `act` which runs its callback immediately. This can deal with things like synchronous effects or mocks using already resolved promises. However, not all component code is synchronous. Frequently our components or mocks contain some asynchronous behaviours like `setTimeout` calls or network calls. Starting from React 16.9, `act` can also be called in asynchronous mode. In such case `act` implementation checks that the passed callback returns [object resembling promise](https://github.com/facebook/react/blob/ce13860281f833de8a3296b7a3dad9caced102e9/packages/react/src/ReactAct.js#L60). +In v14, `act` is always async and returns a Promise. While the callback you pass to `act` can be synchronous (dealing with things like synchronous effects or mocks using already resolved promises), the `act` function itself should always be awaited. However, not all component code is synchronous. Frequently our components or mocks contain some asynchronous behaviours like `setTimeout` calls or network calls. -### Asynchronous code +### Handling asynchronous operations -Asynchronous version of `act` also is executed immediately, but the callback is not yet completed because of some asynchronous operations inside. +When the callback passed to `act` contains asynchronous operations, the Promise returned by `act` will resolve only after those operations complete. Lets look at a simple example with component using `setTimeout` call to simulate asynchronous behaviour: @@ -125,26 +129,13 @@ function TestAsyncComponent() { ```jsx import { render, screen } from '@testing-library/react-native'; -test('render async natively', () => { - render(); +test('render async natively', async () => { + await render(); expect(screen.getByText('Count 0')).toBeOnTheScreen(); }); ``` -If we test our component in a native way without handling its asynchronous behaviour we will end up with sync act warning: - -``` -Warning: An update to TestAsyncComponent inside a test was not wrapped in act(...). - -When testing, code that causes React state updates should be wrapped into act(...): - -act(() => { - /* fire events that update state */ -}); -/* assert on the output */ -``` - -Note that this is not yet the infamous async act warning. It only asks us to wrap our event code with `act` calls. However, this time our immediate state change does not originate from externally triggered events but rather forms an internal part of the component. So how can we apply `act` in such scenario? +If we test our component in a native way without handling its asynchronous behaviour we will end up with an act warning. This is because the `setTimeout` callback will trigger a state update after the test has finished. ### Solution with fake timers @@ -153,27 +144,27 @@ First solution is to use Jest's fake timers inside out tests: ```jsx test('render with fake timers', async () => { jest.useFakeTimers(); - render(); + await render(); - await act(async () => { + await act(() => { jest.runAllTimers(); }); expect(screen.getByText('Count 1')).toBeOnTheScreen(); }); ``` -**Note**: In v14, `act` is now async by default, so you should await it even when using fake timers. +**Note**: In v14, both `render` and `act` are async by default, so you should await them. -That way we can wrap `jest.runAllTimers()` call which triggers the `setTimeout` updates inside an `act` call, hence resolving the act warning. Note that this whole code is synchronous thanks to usage of Jest fake timers. +That way we can wrap `jest.runAllTimers()` call which triggers the `setTimeout` updates inside an `act` call, hence resolving the act warning. ### Solution with real timers -If we wanted to stick with real timers then things get a bit more complex. Let’s start by applying a crude solution of opening async `act()` call for the expected duration of components updates: +If we wanted to stick with real timers then things get a bit more complex. Let's start by applying a crude solution of opening async `act()` call for the expected duration of components updates: ```jsx test('render with real timers - sleep', async () => { - render(); - await act(async () => { + await render(); + await act(() => { await sleep(100); // Wait a bit longer than setTimeout in `TestAsyncComponent` }); @@ -183,11 +174,11 @@ test('render with real timers - sleep', async () => { This works correctly as we use an explicit async `act()` call that resolves the console error. However, it relies on our knowledge of exact implementation details which is a bad practice. -Let’s try more elegant solution using `waitFor` that will wait for our desired state: +Let's try more elegant solution using `waitFor` that will wait for our desired state: ```jsx test('render with real timers - waitFor', async () => { - render(); + await render(); await waitFor(() => screen.getByText('Count 1')); expect(screen.getByText('Count 1')).toBeOnTheScreen(); @@ -200,7 +191,7 @@ The above code can be simplified using `findBy` query: ```jsx test('render with real timers - findBy', async () => { - render(); + await render(); expect(await screen.findByText('Count 1')).toBeOnTheScreen(); }); @@ -210,21 +201,7 @@ This also works since `findByText` internally calls `waitFor` which uses async ` Note that all of the above examples are async tests using & awaiting async `act()` function call. -### Async act warning - -If we modify any of the above async tests and remove `await` keyword, then we will trigger the notorious async `act()`warning: - -```jsx -Warning: You called act(async () => ...) without await. This could lead to unexpected -testing behaviour, interleaving multiple act calls and mixing their scopes. You should -- await act(async () => ...); -``` - -React decides to show this error whenever it detects that async `act()`call [has not been awaited](https://github.com/facebook/react/blob/ce13860281f833de8a3296b7a3dad9caced102e9/packages/react/src/ReactAct.js#L93). - -The exact reasons why you might see async `act()` warnings vary, but finally it means that `act()` has been called with callback that returns `Promise`-like object, but it has not been waited on. - ## References - [React `act` implementation source](https://github.com/facebook/react/blob/main/packages/react/src/ReactAct.js) -- [React testing recipes: `act()`](https://reactjs.org/docs/testing-recipes.html#act) +- [React testing documentation](https://react.dev/link/wrap-tests-with-act) diff --git a/website/docs/14.x/docs/api/events/fire-event.mdx b/website/docs/14.x/docs/api/events/fire-event.mdx index 287b99cd0..00a194cce 100644 --- a/website/docs/14.x/docs/api/events/fire-event.mdx +++ b/website/docs/14.x/docs/api/events/fire-event.mdx @@ -24,7 +24,7 @@ import { render, screen, fireEvent } from '@testing-library/react-native'; test('fire changeText event', async () => { const onEventMock = jest.fn(); - render( + await render( // MyComponent renders TextInput which has a placeholder 'Enter details' // and with `onChangeText` bound to handleChangeText @@ -43,11 +43,11 @@ An example using `fireEvent` with native events that aren't already aliased by t ```jsx import { TextInput, View } from 'react-native'; -import { fireEvent, render } from '@testing-library/react-native'; +import { fireEvent, render, screen } from '@testing-library/react-native'; const onBlurMock = jest.fn(); -render( +await render( @@ -86,7 +86,7 @@ const eventData = { }, }; -render( +await render( Press me @@ -120,7 +120,7 @@ import { render, screen, fireEvent } from '@testing-library/react-native'; const onChangeTextMock = jest.fn(); const CHANGE_TEXT = 'content'; -render( +await render( @@ -159,11 +159,11 @@ const eventData = { }, }; -render( - - XD +await render( + + Content ); -await fireEvent.scroll(screen.getByText('scroll-view'), eventData); +await fireEvent.scroll(screen.getByTestId('scroll-view'), eventData); ``` diff --git a/website/docs/14.x/docs/api/events/user-event.mdx b/website/docs/14.x/docs/api/events/user-event.mdx index ebc071059..0baa3d643 100644 --- a/website/docs/14.x/docs/api/events/user-event.mdx +++ b/website/docs/14.x/docs/api/events/user-event.mdx @@ -12,8 +12,8 @@ If User Event supports a given interaction, you should always prefer it over the ```ts userEvent.setup(options?: { - delay: number; - advanceTimers: (delay: number) => Promise | void; + delay?: number; + advanceTimers?: (delay: number) => Promise | void; }) ``` @@ -54,7 +54,7 @@ This event will take a minimum of 130 ms to run due to the internal React Native ```ts longPress( element: HostElement, - options: { duration: number } = { duration: 500 } + options?: { duration?: number } ): Promise ``` @@ -84,6 +84,7 @@ type( skipBlur?: boolean; submitEditing?: boolean; } +): Promise ``` Example @@ -143,7 +144,7 @@ The `endEditing` and `blur` events can be skipped by passing the `skipBlur: true ```ts clear( element: HostElement, -) +): Promise ``` Example @@ -187,7 +188,7 @@ Events will not be emitted if the `editable` prop is set to `false`. paste( element: HostElement, text: string, -) +): Promise ``` Example @@ -218,6 +219,7 @@ Events will not be emitted if the `editable` prop is set to `false`. - `change` - `changeText` - `selectionChange` +- `contentSizeChange` (only multiline) **Leaving the element**: @@ -230,16 +232,17 @@ Events will not be emitted if the `editable` prop is set to `false`. scrollTo( element: HostElement, options: { - y: number, - momentumY?: number, - contentSize?: { width: number, height: number }, - layoutMeasurement?: { width: number, height: number }, + y: number; + momentumY?: number; + contentSize?: { width: number; height: number }; + layoutMeasurement?: { width: number; height: number }; } | { - x: number, - momentumX?: number, - contentSize?: { width: number, height: number }, - layoutMeasurement?: { width: number, height: number }, + x: number; + momentumX?: number; + contentSize?: { width: number; height: number }; + layoutMeasurement?: { width: number; height: number }; } +): Promise ``` Example diff --git a/website/docs/14.x/docs/api/jest-matchers.mdx b/website/docs/14.x/docs/api/jest-matchers.mdx index 13a662fb9..75c17b988 100644 --- a/website/docs/14.x/docs/api/jest-matchers.mdx +++ b/website/docs/14.x/docs/api/jest-matchers.mdx @@ -133,7 +133,7 @@ expect(element).toBeExpanded(); expect(element).toBeCollapsed(); ``` -These allows you to assert whether the given element is expanded or collapsed from the user's perspective. It relies on the accessibility disabled state as set by `aria-expanded` or `accessibilityState.expanded` props. +These allow you to assert whether the given element is expanded or collapsed from the user's perspective. It relies on the accessibility expanded state as set by `aria-expanded` or `accessibilityState.expanded` props. :::note These matchers are the negation of each other for expandable elements (elements with explicit `aria-expanded` or `accessibilityState.expanded` props). However, both won't pass for non-expandable elements (ones without explicit `aria-expanded` or `accessibilityState.expanded` props). @@ -145,7 +145,7 @@ These matchers are the negation of each other for expandable elements (elements expect(element).toBeBusy(); ``` -This allows you to assert whether the given element is busy from the user's perspective. It relies on the accessibility selected state as set by `aria-busy` or `accessibilityState.busy` props. +This allows you to assert whether the given element is busy from the user's perspective. It relies on the accessibility busy state as set by `aria-busy` or `accessibilityState.busy` props. ## Checking element style @@ -185,7 +185,7 @@ expect(element).toHaveAccessibleName( This allows you to assert whether the given element has a specified accessible name. It accepts either `string` or `RegExp` matchers, as well as [text match options](docs/api/queries#text-match-options) of `exact` and `normalizer`. -The accessible name will be computed based on `aria-labelledby`, `accessibilityLabelledBy`, `aria-label`, and `accessibilityLabel` props, in the absence of these props, the element text content will be used. +The accessible name will be computed based on `aria-labelledby`, `accessibilityLabelledBy`, `aria-label`, and `accessibilityLabel` props. For `Image` elements, the `alt` prop will also be considered. In the absence of these props, the element text content will be used. When the `name` parameter is `undefined` it will only check if the element has any accessible name. diff --git a/website/docs/14.x/docs/api/misc/accessibility.mdx b/website/docs/14.x/docs/api/misc/accessibility.mdx index 916830364..dee4ad36f 100644 --- a/website/docs/14.x/docs/api/misc/accessibility.mdx +++ b/website/docs/14.x/docs/api/misc/accessibility.mdx @@ -13,7 +13,7 @@ Checks if given element is hidden from assistive technology, e.g. screen readers :::note Like [`isInaccessible`](https://testing-library.com/docs/dom-testing-library/api-accessibility/#isinaccessible) function from DOM Testing Library this function considers both accessibility elements and presentational elements (regular `View`s) to be accessible, unless they are hidden in terms of host platform. -This covers only part of [ARIA notion of Accessiblity Tree](https://www.w3.org/TR/wai-aria-1.2/#tree_exclusion), as ARIA excludes both hidden and presentational elements from the Accessibility Tree. +This covers only part of [ARIA notion of Accessibility Tree](https://www.w3.org/TR/wai-aria-1.2/#tree_exclusion), as ARIA excludes both hidden and presentational elements from the Accessibility Tree. ::: For the scope of this function, element is inaccessible when it, or any of its ancestors, meets any of the following conditions: @@ -24,4 +24,4 @@ For the scope of this function, element is inaccessible when it, or any of its a - it has [`importantForAccessibility`](https://reactnative.dev/docs/accessibility#importantforaccessibility-android) prop set to `no-hide-descendants` - it has sibling host element with either [`aria-modal`](https://reactnative.dev/docs/accessibility#aria-modal-ios) or [`accessibilityViewIsModal`](https://reactnative.dev/docs/accessibility#accessibilityviewismodal-ios) prop set to `true` -Specifying `accessible={false}`, `accessiblityRole="none"`, or `importantForAccessibility="no"` props does not cause the element to become inaccessible. +Specifying `accessible={false}`, `role="none"`, `accessibilityRole="none"`, or `importantForAccessibility="no"` props does not cause the element to become inaccessible. diff --git a/website/docs/14.x/docs/api/misc/async.mdx b/website/docs/14.x/docs/api/misc/async.mdx index 4638ece49..e2721a18b 100644 --- a/website/docs/14.x/docs/api/misc/async.mdx +++ b/website/docs/14.x/docs/api/misc/async.mdx @@ -9,7 +9,11 @@ The `findBy*` queries are used to find elements that are not instantly available ```tsx function waitFor( expectation: () => T, - options?: { timeout: number; interval: number } + options?: { + timeout?: number; + interval?: number; + onTimeout?: (error: Error) => Error; + } ): Promise; ``` @@ -40,9 +44,9 @@ You can enforce awaiting `waitFor` by using the [await-async-utils](https://gith Since `waitFor` is likely to run `expectation` callback multiple times, it is highly recommended for it [not to perform any side effects](https://kentcdodds.com/blog/common-mistakes-with-react-testing-library#performing-side-effects-in-waitfor) in `waitFor`. ```jsx -await waitFor(() => { +await waitFor(async () => { // ❌ button will be pressed on each waitFor iteration - fireEvent.press(screen.getByText('press me')); + await fireEvent.press(screen.getByText('press me')); expect(mockOnPress).toHaveBeenCalled(); }); ``` @@ -85,21 +89,34 @@ setTimeout(() => { // in test jest.useFakeTimers(); -await waitFor(() => { - expect(someFunction).toHaveBeenCalledWith(); -}, 10000); +await waitFor( + () => { + expect(someFunction).toHaveBeenCalledWith(); + }, + { timeout: 10000 } +); ``` :::note -If you receive warnings related to `act()` function consult our [Understanding Act](docs/advanced/understanding-act.md) function document. +If you receive warnings related to `act()` function consult our [Understanding Act](docs/advanced/understanding-act) function document. ::: +### Options + +- `timeout`: How long to wait for, in ms. Defaults to 1000 ms (configured by `asyncUtilTimeout` option). +- `interval`: How often to check, in ms. Defaults to 50 ms. +- `onTimeout`: Callback to transform the error before it's thrown. Useful for debugging, e.g., `onTimeout: () => { screen.debug(); }`. + ## `waitForElementToBeRemoved` ```ts function waitForElementToBeRemoved( expectation: () => T, - options?: { timeout: number; interval: number } + options?: { + timeout?: number; + interval?: number; + onTimeout?: (error: Error) => Error; + } ): Promise; ``` @@ -109,7 +126,7 @@ Waits for non-deterministic periods of time until queried element is removed or import { render, screen, waitForElementToBeRemoved } from '@testing-library/react-native'; test('waiting for an Banana to be removed', async () => { - render(); + await render(); await waitForElementToBeRemoved(() => screen.getByText('Banana ready')); }); @@ -120,5 +137,5 @@ This method expects that the element is initially present in the render tree and You can use any of `getBy`, `getAllBy`, `queryBy` and `queryAllBy` queries for `expectation` parameter. :::note -If you receive warnings related to `act()` function consult our [Understanding Act](docs/advanced/understanding-act.md) function document. +If you receive warnings related to `act()` function consult our [Understanding Act](docs/advanced/understanding-act) function document. ::: diff --git a/website/docs/14.x/docs/api/misc/config.mdx b/website/docs/14.x/docs/api/misc/config.mdx index b8566c66f..d7cc763cd 100644 --- a/website/docs/14.x/docs/api/misc/config.mdx +++ b/website/docs/14.x/docs/api/misc/config.mdx @@ -4,13 +4,22 @@ ```ts type Config = { + /** Default timeout, in ms, for `waitFor` and `findBy*` queries. */ asyncUtilTimeout: number; + + /** Default value for `includeHiddenElements` query option. */ + defaultIncludeHiddenElements: boolean; + + /** Default options for `debug` helper. */ + defaultDebugOptions?: Partial; +}; + +type ConfigAliasOptions = { + /** RTL-compatibility alias for `defaultIncludeHiddenElements`. */ defaultHidden: boolean; - defaultDebugOptions: Partial; - concurrentRoot: boolean; }; -function configure(options: Partial) {} +function configure(options: Partial) {} ``` ### `asyncUtilTimeout` option @@ -27,11 +36,6 @@ This option is also available as `defaultHidden` alias for compatibility with [R Default [debug options](#debug) to be used when calling `debug()`. These default options will be overridden by the ones you specify directly when calling `debug()`. -### `concurrentRoot` option {#concurrent-root} - -Set to `false` to disable concurrent rendering. -Otherwise, `render` will default to using concurrent rendering used in the React Native New Architecture. - ## `resetToDefaults()` ```ts diff --git a/website/docs/14.x/docs/api/misc/other.mdx b/website/docs/14.x/docs/api/misc/other.mdx index b16dd1ff0..c6316a3e2 100644 --- a/website/docs/14.x/docs/api/misc/other.mdx +++ b/website/docs/14.x/docs/api/misc/other.mdx @@ -13,11 +13,11 @@ Please note that additional `render` specific operations like `rerender`, `unmou ::: ```jsx -const detailsScreen = within(screen.getByA11yHint('Details Screen')); +const detailsScreen = within(screen.getByHintText('Details Screen')); expect(detailsScreen.getByText('Some Text')).toBeOnTheScreen(); expect(detailsScreen.getByDisplayValue('Some Value')).toBeOnTheScreen(); expect(detailsScreen.queryByLabelText('Some Label')).toBeOnTheScreen(); -await expect(detailsScreen.findByA11yHint('Some Label')).resolves.toBeOnTheScreen(); +await expect(detailsScreen.findByHintText('Some Label')).resolves.toBeOnTheScreen(); ``` Use cases for scoped queries include: @@ -31,29 +31,29 @@ Use cases for scoped queries include: function act(callback: () => T | Promise): Promise; ``` -Useful function to help testing components that use hooks API. By default any `render`, `rerender`, `fireEvent`, and `waitFor` calls are wrapped by this function, so there is no need to wrap it manually. +Wraps code that causes React state updates to ensure all updates are processed before assertions. By default any `render`, `rerender`, `fireEvent`, and `waitFor` calls are wrapped by this function, so there is no need to wrap it manually. -**In v14, `act` is now async by default and always returns a Promise**, making it compatible with React 19, React Suspense, and `React.use()`. This ensures all pending React updates are executed before the Promise resolves. +**In v14, `act` is now async by default and always returns a Promise**, making it compatible with async React features like `Suspense` boundary or `use()` hook. This ensures all pending React updates are executed before the Promise resolves. ```ts import { act } from '@testing-library/react-native'; it('should update state', async () => { - await act(async () => { + await act(() => { setState('new value'); }); expect(state).toBe('new value'); }); ``` -**Note**: Even if your callback is synchronous, you should still use `await act(async () => ...)` as `act` now always returns a Promise. +**Note**: Even if your callback is synchronous, you should still use `await act(...)` as `act` now always returns a Promise. -Consult our [Understanding Act function](docs/advanced/understanding-act.md) document for more understanding of its intricacies. +Consult our [Understanding Act function](docs/advanced/understanding-act) document for more understanding of its intricacies. ## `cleanup` ```ts -const cleanup: () => void; +function cleanup(): Promise; ``` Unmounts React trees that were mounted with `render` and clears `screen` variable that holds latest `render` output. @@ -68,10 +68,12 @@ For example, if you're using the `jest` testing framework, then you would need t import { cleanup, render } from '@testing-library/react-native/pure'; import { View } from 'react-native'; -afterEach(cleanup); +afterEach(async () => { + await cleanup(); +}); -it('renders a view', () => { - render(); +it('renders a view', async () => { + await render(); // ... }); ``` @@ -80,10 +82,12 @@ The `afterEach(cleanup)` call also works in `describe` blocks: ```jsx describe('when logged in', () => { - afterEach(cleanup); + afterEach(async () => { + await cleanup(); + }); - it('renders the user', () => { - render(); + it('renders the user', async () => { + await render(); // ... }); }); diff --git a/website/docs/14.x/docs/api/misc/render-hook.mdx b/website/docs/14.x/docs/api/misc/render-hook.mdx index 1afde0486..5a292b54a 100644 --- a/website/docs/14.x/docs/api/misc/render-hook.mdx +++ b/website/docs/14.x/docs/api/misc/render-hook.mdx @@ -4,18 +4,18 @@ ```ts async function renderHook( - hookFn: (props?: Props) => Result, + hookFn: (props: Props) => Result, options?: RenderHookOptions ): Promise>; ``` Renders a test component that will call the provided `callback`, including any hooks it calls, every time it renders. Returns a Promise that resolves to a [`RenderHookResult`](#renderhookresult) object, which you can interact with. -**This is the recommended default API** for testing hooks. It uses async `act` internally to ensure all pending React updates are executed during rendering, making it compatible with React 19, React Suspense, and `React.use()`. +**This is the recommended default API** for testing hooks. It uses async `act` internally to ensure all pending React updates are executed during rendering, making it compatible with async React features like `Suspense` boundary or `use()` hook. - **Returns a Promise**: Should be awaited - **Async methods**: Both `rerender` and `unmount` return Promises and should be awaited -- **Suspense support**: Compatible with React Suspense boundaries and `React.use()` +- **Suspense support**: Compatible with `Suspense` boundaries and `use()` hook ```ts import { renderHook, act } from '@testing-library/react-native'; @@ -25,7 +25,7 @@ it('should increment count', async () => { const { result } = await renderHook(() => useCount()); expect(result.current.count).toBe(0); - await act(async () => { + await act(() => { // Note that you should wrap the calls to functions your hook returns with `act` if they trigger an update of your hook's state to ensure pending useEffects are run before your next assertion. result.current.increment(); }); @@ -35,6 +35,8 @@ it('should increment count', async () => { ```ts // useCount.js +import { useState } from 'react'; + export const useCount = () => { const [count, setCount] = useState(0); const increment = () => setCount((previousCount) => previousCount + 1); @@ -77,6 +79,8 @@ The `renderHook` function returns a Promise that resolves to an object with the The `current` value of the `result` will reflect the latest of whatever is returned from the `callback` passed to `renderHook`. The `Result` type is determined by the type passed to or inferred by the `renderHook` call. +**Note:** When using React Suspense, `result.current` will be `null` while the hook is suspended. + #### `rerender` An async function to rerender the test component, causing any hooks to be recalculated. If `newProps` are passed, they will replace the `callback` function's `initialProps` for subsequent rerenders. The `Props` type is determined by the type passed to or inferred by the `renderHook` call. @@ -96,6 +100,9 @@ Here we present some extra examples of using `renderHook` API. #### With `initialProps` ```ts +import { useState, useEffect } from 'react'; +import { renderHook, act } from '@testing-library/react-native'; + const useCount = (initialCount: number) => { const [count, setCount] = useState(initialCount); const increment = () => setCount((previousCount) => previousCount + 1); @@ -114,7 +121,7 @@ it('should increment count', async () => { expect(result.current.count).toBe(1); - await act(async () => { + await act(() => { result.current.increment(); }); @@ -163,7 +170,7 @@ it('handles hook with suspense', async () => { // Initially suspended, result should not be available expect(result.current).toBeNull(); - await act(async () => resolvePromise('resolved')); + await act(() => resolvePromise('resolved')); expect(result.current).toBe('resolved'); }); ``` diff --git a/website/docs/14.x/docs/api/queries.mdx b/website/docs/14.x/docs/api/queries.mdx index cb607a8c3..3aa2798e2 100644 --- a/website/docs/14.x/docs/api/queries.mdx +++ b/website/docs/14.x/docs/api/queries.mdx @@ -11,8 +11,8 @@ All queries described below are accessible in two main ways: through the `screen ```tsx import { render, screen } from '@testing-library/react-native'; -test('accessing queries using "screen" object', () => { - render(...); +test('accessing queries using "screen" object', async () => { + await render(...); screen.getByRole("button", { name: "Start" }); }) @@ -25,8 +25,8 @@ The modern and recommended way of accessing queries is to use the `screen` objec ```tsx import { render } from '@testing-library/react-native'; -test('accessing queries using "render" result', () => { - const { getByRole } = render(...); +test('accessing queries using "render" result', async () => { + const { getByRole } = await render(...); getByRole("button", { name: "Start" }); }) ``` @@ -130,11 +130,11 @@ In cases when your `findBy*` and `findAllBy*` queries throw when unable to find ## Query predicates -_Note: most methods like this one return a [`HostElement`](https://reactjs.org/docs/test-renderer.html#testinstance) with following properties that you may be interested in:_ +_Note: most methods like this one return a [`HostElement`](https://github.com/mdjastrzebski/test-renderer#hostelement) with following properties that you may be interested in:_ ```typescript type HostElement = { - type: string | Function; + type: string; props: { [propName: string]: any }; parent: HostElement | null; children: Array; @@ -180,7 +180,7 @@ In order for `*ByRole` queries to match an element it needs to be considered an ```jsx import { render, screen } from '@testing-library/react-native'; -render( +await render( Hello @@ -237,11 +237,12 @@ Returns a `HostElement` with matching label: - either by matching [`aria-label`](https://reactnative.dev/docs/accessibility#aria-label)/[`accessibilityLabel`](https://reactnative.dev/docs/accessibility#accessibilitylabel) prop - or by matching text content of view referenced by [`aria-labelledby`](https://reactnative.dev/docs/accessibility#aria-labelledby-android)/[`accessibilityLabelledBy`](https://reactnative.dev/docs/accessibility#accessibilitylabelledby-android) prop +- or by matching the [`alt`](https://reactnative.dev/docs/image#alt) prop on `Image` elements ```jsx import { render, screen } from '@testing-library/react-native'; -render(); +await render(); const element = screen.getByLabelText('my-label'); ``` @@ -265,7 +266,7 @@ Returns a `HostElement` for a `TextInput` with a matching placeholder – may be ```jsx import { render, screen } from '@testing-library/react-native'; -render(); +await render(); const element = screen.getByPlaceholderText('username'); ``` @@ -289,7 +290,7 @@ Returns a `HostElement` for a `TextInput` with a matching display value – may ```jsx import { render, screen } from '@testing-library/react-native'; -render(); +await render(); const element = screen.getByDisplayValue('username'); ``` @@ -315,7 +316,7 @@ This method will join `` siblings to find matches, similarly to [how React ```jsx import { render, screen } from '@testing-library/react-native'; -render(); +await render(); const element = screen.getByText('banana'); ``` @@ -341,7 +342,7 @@ Returns a `HostElement` with matching `accessibilityHint` prop. ```jsx import { render, screen } from '@testing-library/react-native'; -render(); +await render(); const element = screen.getByHintText('Plays a song'); ``` @@ -369,7 +370,7 @@ Returns a `HostElement` with matching `testID` prop. `testID` – may be a strin ```jsx import { render, screen } from '@testing-library/react-native'; -render(); +await render(); const element = screen.getByTestId('unique-id'); ``` @@ -392,7 +393,7 @@ This option is also available as `hidden` alias for compatibility with [React Te **Examples** ```tsx -render(Hidden from accessibility); +await render(Hidden from accessibility); // Exclude hidden elements expect( @@ -421,7 +422,7 @@ Most of the query APIs take a `TextMatch` as an argument, which means the argume Given the following render: ```jsx -render(Hello World); +await render(Hello World); ``` Will **find a match**: @@ -491,7 +492,7 @@ Specifying a value for `normalizer` replaces the built-in normalization, but you To perform a match against text without trimming: ```typescript -screen.getByText(node, 'text', { +screen.getByText('text', { normalizer: getDefaultNormalizer({ trim: false }), }); ``` @@ -499,33 +500,7 @@ screen.getByText(node, 'text', { To override normalization to remove some Unicode characters whilst keeping some (but not all) of the built-in normalization behavior: ```typescript -screen.getByText(node, 'text', { +screen.getByText('text', { normalizer: (str) => getDefaultNormalizer({ trim: false })(str).replace(/[\u200E-\u200F]*/g, ''), }); ``` - -## Legacy unit testing helpers - -`render` from `@testing-library/react-native` exposes additional queries that **should not be used in integration or component testing**, but some users (like component library creators) interested in unit testing some components may find helpful. - -The interface is the same as for other queries, but we won't provide full names so that they're harder to find by search engines. - -### `UNSAFE_ByType` - -> UNSAFE_getByType, UNSAFE_getAllByType, UNSAFE_queryByType, UNSAFE_queryAllByType - -Returns a `HostElement` with matching a React component type. - -:::caution -This query has been marked unsafe, since it requires knowledge about implementation details of the component. Use responsibly. -::: - -### `UNSAFE_ByProps` - -> UNSAFE_getByProps, UNSAFE_getAllByProps, UNSAFE_queryByProps, UNSAFE_queryAllByProps - -Returns a `HostElement` with matching props object. - -:::caution -This query has been marked unsafe, since it requires knowledge about implementation details of the component. Use responsibly. -::: diff --git a/website/docs/14.x/docs/api/render.mdx b/website/docs/14.x/docs/api/render.mdx index 9f7fa232e..cc6861db3 100644 --- a/website/docs/14.x/docs/api/render.mdx +++ b/website/docs/14.x/docs/api/render.mdx @@ -2,14 +2,14 @@ ## `render` function {#render} -```jsx -async function render( - component: React.Element, +```ts +async function render( + element: React.ReactElement, options?: RenderOptions -): Promise +): Promise; ``` -The `render` function is the entry point for writing React Native Testing Library tests. It deeply renders the given React element and returns helpers to query the output components' structure. This function is async by default and uses async `act` internally to ensure all pending React updates are executed during rendering, making it compatible with React 19, React Suspense, and `React.use()`. +The `render` function is the entry point for writing React Native Testing Library tests. It deeply renders the given React element and returns helpers to query the output components' structure. This function is async by default and uses async `act` internally to ensure all pending React updates are executed during rendering, making it compatible with async React features like `Suspense` boundary or `use()` hook. ```jsx import { render, screen } from '@testing-library/react-native'; @@ -34,15 +34,10 @@ wrapper?: React.ComponentType, This option allows you to wrap the tested component, passed as the first option to the `render()` function, in an additional wrapper component. This is useful for creating reusable custom render functions for common React Context providers. -#### `concurrentRoot` {#concurrent-root} - -Set to `false` to disable concurrent rendering. -Otherwise, `render` will default to using concurrent rendering used in the React Native New Architecture. - #### `createNodeMock` {#create-node-mock} ```ts -createNodeMock?: (element: React.Element) => unknown, +createNodeMock?: (element: React.ReactElement) => object, ``` This option allows you to pass `createNodeMock` option to the renderer's `create()` method in order to allow for custom mock refs. This option is passed through to [Test Renderer](https://github.com/mdjastrzebski/test-renderer). @@ -61,8 +56,8 @@ The `render` function returns a promise that resolves to the same queries and ut See [this article](https://kentcdodds.com/blog/common-mistakes-with-react-testing-library#not-using-screen) from Kent C. Dodds for more details. -:::warning Async lifecycle methods +:::note Type information -When using `render`, the lifecycle methods `rerender` and `unmount` are async and must be awaited. +Query results and element references use the `HostElement` type from [Test Renderer](https://github.com/mdjastrzebski/test-renderer). If you need to type element variables, you can import this type directly from `test-renderer`. ::: diff --git a/website/docs/14.x/docs/api/screen.mdx b/website/docs/14.x/docs/api/screen.mdx index 5a419f715..ffbdbcea0 100644 --- a/website/docs/14.x/docs/api/screen.mdx +++ b/website/docs/14.x/docs/api/screen.mdx @@ -8,7 +8,7 @@ let screen: { debug(options?: DebugOptions): void toJSON(): JsonElement | null; container: HostElement; - root: HostElement; + root: HostElement | null; }; ``` @@ -41,7 +41,7 @@ function rerender(element: React.Element): Promise; Re-render the in-memory tree with a new root element. This simulates a React update render at the root. If the new element has the same type (and `key`) as the previous element, the tree will be updated; otherwise, it will re-mount a new tree, in both cases triggering the appropriate lifecycle events. -This method is async and uses async `act` function internally to ensure all pending React updates are executed during updating, making it compatible with React 19, React Suspense, and `React.use()`. +This method is async and uses async `act` function internally to ensure all pending React updates are executed during updating, making it compatible with async React features like `Suspense` boundary or `use()` hook. ```jsx import { render, screen } from '@testing-library/react-native'; @@ -62,7 +62,7 @@ function unmount(): Promise; Unmount the in-memory tree, triggering the appropriate lifecycle events. -This method is async and uses async `act` function internally to ensure all pending React updates are executed during unmounting, making it compatible with React 19, React Suspense, and `React.use()`. +This method is async and uses async `act` function internally to ensure all pending React updates are executed during unmounting, making it compatible with async React features like `Suspense` boundary or `use()` hook. :::note @@ -164,13 +164,19 @@ test('example', async () => { ### `root` ```ts -const root: HostElement; +const root: HostElement | null; ``` -Returns the rendered root [host element](docs/advanced/testing-env#host-and-composite-components). This is the first child of the `container`, which represents the actual root element you rendered. +Returns the rendered root [host element](docs/advanced/testing-env#host-and-composite-components), or `null` if nothing was rendered. This is the first child of the `container`, which represents the actual root element you rendered. This API is primarily useful for component tests, as it allows you to access root host view without using `*ByTestId` queries or similar methods. +:::note + +In rare cases where your root element is a `React.Fragment` with multiple children, the `container` will have more than one child, and `root` will return only the first one. In such cases, use `container.children` to access all rendered elements. + +::: + ```jsx import { render, screen } from '@testing-library/react-native'; diff --git a/website/docs/14.x/docs/guides/_meta.json b/website/docs/14.x/docs/guides/_meta.json index 4b5f8b683..5ae12d7e2 100644 --- a/website/docs/14.x/docs/guides/_meta.json +++ b/website/docs/14.x/docs/guides/_meta.json @@ -1 +1 @@ -["how-to-query", "react-19", "troubleshooting", "faq", "community-resources"] +["how-to-query", "troubleshooting", "faq", "community-resources"] diff --git a/website/docs/14.x/docs/guides/how-to-query.mdx b/website/docs/14.x/docs/guides/how-to-query.mdx index e3519dd72..ff03b6e8a 100644 --- a/website/docs/14.x/docs/guides/how-to-query.mdx +++ b/website/docs/14.x/docs/guides/how-to-query.mdx @@ -46,15 +46,15 @@ Avoid using `queryAllBy*` in regular tests, as it provides no assertions on the The query predicate describes how you decide whether to match the given element. -| Predicate | Supported elements | Inspected props | -| ------------------------------------------------------------ | ------------------ | ------------------------------------------------------------------------------------------- | -| [`*ByRole`](docs/api/queries#by-role) | all host elements | `role`, `accessibilityRole`,
optional: accessible name, accessibility state and value | -| [`*ByLabelText`](docs/api/queries#by-label-text) | all host elements | `aria-label`, `aria-labelledby`,
`accessibilityLabel`, `accessibilityLabelledBy` | -| [`*ByDisplayValue`](docs/api/queries#by-display-value) | `TextInput` | `value`, `defaultValue` | -| [`*ByPlaceholderText`](docs/api/queries#by-placeholder-text) | `TextInput` | `placeholder` | -| [`*ByText`](docs/api/queries#by-text) | `Text` | `children` (text content) | -| [`*ByHintText`](docs/api/queries#by-hint-text) | all host elements | `accessibilityHint` | -| [`*ByTestId`](docs/api/queries#by-test-id) | all host elements | `testID` | +| Predicate | Supported elements | Inspected props | +| ------------------------------------------------------------ | ------------------ | ----------------------------------------------------------------------------------------------------------------- | +| [`*ByRole`](docs/api/queries#by-role) | all host elements | `role`, `accessibilityRole`,
optional: accessible name, accessibility state and value | +| [`*ByLabelText`](docs/api/queries#by-label-text) | all host elements | `aria-label`, `aria-labelledby`,
`accessibilityLabel`, `accessibilityLabelledBy`,
`alt` (for `Image`) | +| [`*ByDisplayValue`](docs/api/queries#by-display-value) | `TextInput` | `value`, `defaultValue` | +| [`*ByPlaceholderText`](docs/api/queries#by-placeholder-text) | `TextInput` | `placeholder` | +| [`*ByText`](docs/api/queries#by-text) | `Text` | `children` (text content) | +| [`*ByHintText`](docs/api/queries#by-hint-text) | all host elements | `accessibilityHint` | +| [`*ByTestId`](docs/api/queries#by-test-id) | all host elements | `testID` | ### Idiomatic query predicates diff --git a/website/docs/14.x/docs/guides/react-19.mdx b/website/docs/14.x/docs/guides/react-19.mdx deleted file mode 100644 index c899b6cda..000000000 --- a/website/docs/14.x/docs/guides/react-19.mdx +++ /dev/null @@ -1,70 +0,0 @@ -# React 19 & Suspense Support - -React 19 introduced full support for React Suspense, [`React.use()`](https://react.dev/reference/react/use), and other async rendering features to React Native [0.78.0](https://github.com/facebook/react-native/releases/tag/v0.78.0). - -When testing components that use these features, React requires the [`async act`](https://react.dev/reference/react/act) helper to handle async state updates. This means React Native Testing Library needs new async versions of its core APIs. These async APIs work with both React 18 and React 19. - -## New async APIs - -RNTL 14 introduces async versions of the core testing APIs to handle React 19's async rendering: - -**Rendering APIs:** - -- **[`render`](docs/api/render#render)** - now async by default (was `renderAsync` in v13) -- **[`screen.rerender`](docs/api/screen#rerender)** - now async (returns `Promise`) -- **[`screen.unmount`](docs/api/screen#unmount)** - now async (returns `Promise`) - -**Event APIs:** - -- **[`fireEvent`](docs/api/events/fire-event)** - updated to be async by default. - -## APIs that remain unchanged - -Many existing APIs continue to work without modification: - -- **[Query methods](docs/api/queries)**: `screen.getBy*`, `screen.queryBy*`, `screen.findBy*` - all work the same -- **[User Event API](docs/api/events/user-event)** - already async, so no API changes needed -- **[Jest matchers](docs/api/jest-matchers)** - work with already-rendered output, so no changes required - -## What changes in your tests - -### Making tests async - -The main change is using [`render`](docs/api/render#render) which is now async by default, which requires: - -1. Making your test function `async` -2. Adding `await` before `render` - -```tsx -// Async approach (React 19 ready, default in v14) -test('my component', async () => { - await render(); - expect(screen.getByText('Hello')).toBeOnTheScreen(); -}); -``` - -### When to use async APIs - -Use the async APIs when your components: - -- Use React Suspense for data fetching or code splitting -- Call `React.use()` for reading promises or context -- Have async state updates that need proper `act()` handling - -## Migration strategy - -### New projects - -Use the async-ready APIs (`render`, User Event, Jest Matchers, etc.) from the start. They work with both React 18 and React 19. - -### Existing projects - -You can migrate gradually: - -- **Existing tests** should be migrated to use async `render` (see migration guide) -- **New tests** should use async `render` (default in v14) -- **Tests with Suspense/`React.use()`** must use async `render` - -### Migration - -See the [v14 migration guide](../migration/v14#render-async-default) for detailed migration steps. diff --git a/website/docs/14.x/docs/guides/troubleshooting.mdx b/website/docs/14.x/docs/guides/troubleshooting.mdx index fbf9b1357..42e19f609 100644 --- a/website/docs/14.x/docs/guides/troubleshooting.mdx +++ b/website/docs/14.x/docs/guides/troubleshooting.mdx @@ -2,25 +2,6 @@ This guide describes common issues found by users when integrating React Native Test Library to their projects: -## Matching React Native, React & Test Renderer versions - -Check that you have matching versions of core dependencies: - -- React Native -- React -- Test Renderer (automatically installed with RNTL) - -React Native uses different versioning scheme from React, you can use [React Native Upgrade Helper](https://react-native-community.github.io/upgrade-helper/) to find the correct matching between React Native & React versions. In case you use Expo, run `npx expo install --fix` in your project to validate and install compatible versions of these dependencies. - -Test Renderer is automatically managed as a dependency of React Native Testing Library and is compatible with React 18 and React 19. - -Related issues: [#1061](https://github.com/callstack/react-native-testing-library/issues/1061), [#938](https://github.com/callstack/react-native-testing-library/issues/938), [#920](https://github.com/callstack/react-native-testing-library/issues/920) - -Errors that might indicate that you are facing this issue: - -- `TypeError: Cannot read property 'current' of undefined` when calling `render()` -- `TypeError: Cannot read property 'isBatchingLegacy' of undefined` when calling `render()` - ## Example repository We maintain an [example repository](https://github.com/callstack/react-native-testing-library/tree/main/examples/basic) that showcases a modern React Native Testing Library setup with TypeScript, etc. diff --git a/website/docs/14.x/docs/migration/_meta.json b/website/docs/14.x/docs/migration/_meta.json index 64bd10c03..2a769db78 100644 --- a/website/docs/14.x/docs/migration/_meta.json +++ b/website/docs/14.x/docs/migration/_meta.json @@ -1,6 +1 @@ -[ - "v14", - "v13", - "jest-matchers", - { "type": "dir", "name": "previous", "label": "Previous versions", "collapsed": true } -] +["v14", "jest-matchers", "v13", "v12", "v11", "v9", "v7", "v2"] diff --git a/website/docs/14.x/docs/migration/jest-matchers.mdx b/website/docs/14.x/docs/migration/jest-matchers.mdx index cb4939574..fbdca61e0 100644 --- a/website/docs/14.x/docs/migration/jest-matchers.mdx +++ b/website/docs/14.x/docs/migration/jest-matchers.mdx @@ -66,5 +66,5 @@ New [`toHaveAccessibleName()`](docs/api/jest-matchers#tohaveaccessiblename) has You should be aware of the following details: - [`toBeEnabled()` / `toBeDisabled()`](docs/api/jest-matchers#tobeenabled) matchers also check the disabled state for the element's ancestors and not only the element itself. This is the same as in legacy Jest Native matchers of the same name but differs from the removed `toHaveAccessibilityState()` matcher. -- [`toBeChecked()`](docs/api/jest-matchers#tobechecked) matcher supports only elements with a `checkbox`, `radio` and 'switch' role +- [`toBeChecked()`](docs/api/jest-matchers#tobechecked) matcher works only on `Switch` host elements or accessibility elements with `checkbox`, `radio` or `switch` role - [`toBePartiallyChecked()`](docs/api/jest-matchers#tobechecked) matcher supports only elements with `checkbox` role diff --git a/website/docs/14.x/docs/migration/previous/_meta.json b/website/docs/14.x/docs/migration/previous/_meta.json deleted file mode 100644 index c3540052d..000000000 --- a/website/docs/14.x/docs/migration/previous/_meta.json +++ /dev/null @@ -1 +0,0 @@ -["v12", "v11", "v9", "v7", "v2"] diff --git a/website/docs/14.x/docs/migration/previous/v11.mdx b/website/docs/14.x/docs/migration/v11.mdx similarity index 100% rename from website/docs/14.x/docs/migration/previous/v11.mdx rename to website/docs/14.x/docs/migration/v11.mdx diff --git a/website/docs/14.x/docs/migration/previous/v12.mdx b/website/docs/14.x/docs/migration/v12.mdx similarity index 100% rename from website/docs/14.x/docs/migration/previous/v12.mdx rename to website/docs/14.x/docs/migration/v12.mdx diff --git a/website/docs/14.x/docs/migration/v14.mdx b/website/docs/14.x/docs/migration/v14.mdx index 470da3b2a..7088f6b9e 100644 --- a/website/docs/14.x/docs/migration/v14.mdx +++ b/website/docs/14.x/docs/migration/v14.mdx @@ -1,42 +1,105 @@ +import { PackageManagerTabs } from 'rspress/theme'; + # Migration to 14.x This guide describes the migration to React Native Testing Library version 14 from version 13.x. -## Breaking changes +## Overview + +RNTL v14 is a major release that **drops support for React 18** and fully embraces React 19's async-first paradigm. Key changes include: + +- **React 19+ required**: Minimum supported versions are React 19.0.0 and React Native 0.78+ +- **Async APIs by default**: `render`, `renderHook`, `fireEvent`, and `act` are now async +- **New renderer**: Switched from deprecated [React Test Renderer](https://reactjs.org/docs/test-renderer.html) to [Test Renderer](https://github.com/mdjastrzebski/test-renderer) +- **API cleanup**: Removed deprecated APIs (`update`, `getQueriesForElement`, `UNSAFE_root`, `concurrentRoot` option) +- **Safe `container` API**: Reintroduced `container` which is now safe to use + +:::info React 18 Users + +If you need to support React 18, please continue using RNTL v13.x. + +::: + +## Quick Migration + +We provide codemods to automate most of the migration: + +**Step 1: Update dependencies** + + + +**Step 2: Update test code to async** + + + +After running the codemods, review the changes and run your tests. + +## Breaking Changes + +### Supported React and React Native versions + +**This version requires React 19+ and React Native 0.78+.** If you need to support React 18, please use the latest v13.x version. + +| RNTL Version | React Version | React Native Version | +| ------------ | ------------- | -------------------- | +| v14.x | >= 19.0.0 | >= 0.78 | +| v13.x | >= 18.0.0 | >= 0.71 | ### Test Renderer replaces React Test Renderer -In v14, React Native Testing Library now uses [Test Renderer](https://github.com/mdjastrzebski/test-renderer) instead of the deprecated `react-test-renderer` package. Test Renderer is a modern, actively maintained alternative that provides better compatibility with React 19 and improved type safety. +In v14, React Native Testing Library now uses [Test Renderer](https://github.com/mdjastrzebski/test-renderer) instead of the deprecated [React Test Renderer](https://reactjs.org/docs/test-renderer.html). Test Renderer is a modern, actively maintained alternative that provides better compatibility with React 19 and improved type safety. **What changed:** -- The underlying rendering engine has been switched from `react-test-renderer` to `test-renderer` +- The underlying renderer has been switched from React Test Renderer to Test Renderer - This change is mostly internal and should not require code changes in most cases -- Type definitions have been updated to use `HostElement` from Test Renderer instead of `ReactTestInstance` +- Type definitions have been updated to use [`HostElement`](https://github.com/mdjastrzebski/test-renderer#hostelement) from Test Renderer instead of `ReactTestInstance` **Migration:** #### 1. Update dependencies -Remove `react-test-renderer` and its type definitions from your dev dependencies, and add `test-renderer`: +Run codemod for updating dependencies: -```bash -# Using npm -npm uninstall react-test-renderer @types/react-test-renderer -npm install -D test-renderer@^0.10.0 + -# Using yarn -yarn remove react-test-renderer @types/react-test-renderer -yarn add -D test-renderer@^0.10.0 +##### Manual changes -# Using pnpm -pnpm remove react-test-renderer @types/react-test-renderer -pnpm add -D test-renderer@^0.10.0 -``` +Remove React Test Renderer and its type definitions from your dev dependencies, and add Test Renderer: + + #### 2. Update type imports (if needed) -If you were directly importing types from `react-test-renderer`, you may need to update your imports: +If you were directly importing types from React Test Renderer, you may need to update your imports: ```ts // Before (v13) @@ -50,26 +113,34 @@ import type { HostElement } from 'test-renderer'; For more details, see the [Test Renderer documentation](https://github.com/mdjastrzebski/test-renderer). -### `container` API reintroduced +### Async APIs by Default -In v14, the `container` API has been reintroduced and is now safe to use. Previously, `container` was renamed to `UNSAFE_root` in v12 due to behavioral differences from React Testing Library's `container`. In v14, `container` now returns a pseudo-element container whose children are the elements you asked to render, making it safe and consistent with React Testing Library's behavior. +With React 18 support dropped, RNTL v14 fully embraces React 19's async rendering model. The following functions are now async by default: -**What changed:** +- `render()` → returns `Promise` +- `rerender()` and `unmount()` → return `Promise` +- `renderHook()` → returns `Promise` +- `fireEvent()` and helpers (`press`, `changeText`, `scroll`) → return `Promise` +- `act()` → always returns `Promise` -- `screen.container` is now available and safe to use -- `container` returns a pseudo-element container from Test Renderer -- The container's children are the elements you rendered -- `UNSAFE_root` has been removed +:::tip Already using async APIs? + +If you adopted the async APIs introduced in RNTL v13.3 (`renderAsync`, `fireEventAsync`, `renderHookAsync`), simply rename them to their non-async counterparts (`render`, `fireEvent`, `renderHook`). The async versions have been removed as the standard APIs are now async by default. + +::: + +#### `render` is now async {#render-async-default} + +In v14, `render` is now async by default and returns a Promise. This change enables proper support for async React features like `Suspense` boundary or `use()` hook. **Before (v13):** ```ts import { render, screen } from '@testing-library/react-native'; -it('should access root', () => { +it('should render component', () => { render(); - // UNSAFE_root was the only way to access the container - const root = screen.UNSAFE_root; + expect(screen.getByText('Hello')).toBeOnTheScreen(); }); ``` @@ -78,205 +149,148 @@ it('should access root', () => { ```ts import { render, screen } from '@testing-library/react-native'; -it('should access container', async () => { +it('should render component', async () => { await render(); - // container is now safe and available - const container = screen.container; - // root is the first child of container - const root = screen.root; + expect(screen.getByText('Hello')).toBeOnTheScreen(); }); ``` -**Migration:** - -If you were using `UNSAFE_root`, replace it with `container`: - -```ts -// Before -const root = screen.UNSAFE_root; - -// After -const container = screen.container; -// Or use screen.root if you need the actual root element -const root = screen.root; -``` - -For more details, see the [`screen` API documentation](/docs/api/screen#container). +For more details, see the [`render` API documentation](/docs/api/render). -### `render` is now async by default {#render-async-default} +#### `renderHook` is now async -In v14, `render` is now async by default and returns a Promise. This change makes it compatible with React 19, React Suspense, and `React.use()`. +In v14, `renderHook` is now async by default and returns a Promise. **Before (v13):** ```ts -import { render, screen } from '@testing-library/react-native'; +import { renderHook } from '@testing-library/react-native'; -it('should render component', () => { - render(); - expect(screen.getByText('Hello')).toBeOnTheScreen(); +it('should test hook', () => { + const { result, rerender } = renderHook(() => useMyHook()); + + rerender(newProps); + unmount(); }); ``` **After (v14):** ```ts -import { render, screen } from '@testing-library/react-native'; +import { renderHook } from '@testing-library/react-native'; -it('should render component', async () => { - await render(); - expect(screen.getByText('Hello')).toBeOnTheScreen(); +it('should test hook', async () => { + const { result, rerender } = await renderHook(() => useMyHook()); + + await rerender(newProps); + await unmount(); }); ``` -#### Step-by-step migration guide - -To migrate from v13 `render` to v14 `render`: - -##### 1. Add `async` to your test function +For more details, see the [`renderHook` API documentation](/docs/api/misc/render-hook). -```ts -// Before (v13) -it('should render component', () => { +#### `fireEvent` is now async -// After (v14) -it('should render component', async () => { -``` +In v14, `fireEvent` and its helpers (`press`, `changeText`, `scroll`) are now async by default and return a Promise. -##### 2. Await `render` +**Before (v13):** ```ts -// Before -render(); +import { fireEvent, screen } from '@testing-library/react-native'; -// After -await render(); +it('should press button', () => { + render(); + fireEvent.press(screen.getByText('Press me')); + expect(onPress).toHaveBeenCalled(); +}); ``` -##### 3. Update `rerender` calls to await +**After (v14):** ```ts -// Before -screen.rerender(); +import { fireEvent, screen } from '@testing-library/react-native'; -// After -await screen.rerender(); +it('should press button', async () => { + await render(); + await fireEvent.press(screen.getByText('Press me')); + expect(onPress).toHaveBeenCalled(); +}); ``` -##### 4. Replace `update` alias with `rerender` - -The `update` alias for `rerender` has been removed in v14. If you were using `screen.update()` or `renderer.update()`, replace it with `rerender()`: +#### `act` is now async -```ts -// Before (v13) -screen.update(); -// or -const { update } = render(); -update(); +In v14, `act` is now async by default and always returns a Promise. You should always `await` the result of `act()`. -// After (v14) -await screen.rerender(); -// or -const { rerender } = await render(); -await rerender(); -``` +**What changed:** -##### 5. Update `unmount` calls to await +- `act` now always returns a `Promise` instead of `T | Thenable` +- `act` should always be awaited +- The API is more consistent and predictable -```ts -// Before -screen.unmount(); +:::note -// After -await screen.unmount(); -``` +The transition to async `act` may prevent testing very short transient states, as awaiting `act` will flush all pending updates before returning. -#### Complete example +::: **Before (v13):** ```ts -import { render, screen } from '@testing-library/react-native'; - -it('should update component', () => { - render(); - expect(screen.getByText('Count: 0')).toBeOnTheScreen(); - - screen.rerender(); - expect(screen.getByText('Count: 5')).toBeOnTheScreen(); +import { act } from '@testing-library/react-native'; - screen.unmount(); +it('should update state', () => { + act(() => { + setState('new value'); + }); + expect(state).toBe('new value'); }); ``` **After (v14):** ```ts -import { render, screen } from '@testing-library/react-native'; - -it('should update component', async () => { - await render(); - expect(screen.getByText('Count: 0')).toBeOnTheScreen(); - - await screen.rerender(); - expect(screen.getByText('Count: 5')).toBeOnTheScreen(); +import { act } from '@testing-library/react-native'; - await screen.unmount(); +it('should update state', async () => { + await act(() => { + setState('new value'); + }); + expect(state).toBe('new value'); }); ``` -#### Benefits of async `render` - -- **React 19 compatibility**: Works seamlessly with React 19's async features -- **Suspense support**: Properly handles React Suspense boundaries and `React.use()` -- **Better timing**: Ensures all pending React updates are executed before assertions -- **Future-proof**: Aligns with React's direction toward async rendering - -For more details, see the [`render` API documentation](/docs/api/render). +**Note**: Even if your callback is synchronous, you should still use `await act(...)` as `act` now always returns a Promise. -### `update` alias removed +For more details, see the [`act` API documentation](/docs/api/misc/other#act). -In v14, the `update` alias for `rerender` has been removed. You must use `rerender` instead. +#### Benefits of Async APIs -**What changed:** +- **Suspense support**: Properly handles `Suspense` boundaries and `use()` hook +- **Better timing**: Ensures all pending React updates are executed before assertions +- **Simpler mental model**: All rendering operations are consistently async +- **Future-proof**: Aligns with React's direction toward async rendering -- `screen.update()` has been removed -- `renderer.update()` has been removed -- Only `rerender` is now available +### Removed APIs -**Migration:** +#### `update` alias removed -Replace all `update` calls with `rerender`: +The `update` alias for `rerender` has been removed. Use `rerender` instead: ```ts // Before (v13) screen.update(); -// or const { update } = render(); -update(); +update(); // After (v14) await screen.rerender(); -// or const { rerender } = await render(); -await rerender(); +await rerender(); ``` -**Note:** This change is included in the step-by-step migration guide for async `render` above. - -### `getQueriesForElement` export removed - -In v14, the `getQueriesForElement` export alias for `within` has been removed. You must use `within` instead. - -**What changed:** +#### `getQueriesForElement` export removed -- `getQueriesForElement` is no longer exported from the main package -- `within` is the only exported function for scoped queries -- `getQueriesForElement` remains available internally but is not part of the public API - -**Migration:** - -Replace all `getQueriesForElement` imports and usage with `within`: +The `getQueriesForElement` export alias for `within` has been removed. Use `within` instead: ```ts // Before (v13) @@ -292,178 +306,91 @@ const queries = within(element); **Note:** `getQueriesForElement` was just an alias for `within`, so the functionality is identical - only the import needs to change. -### `renderHook` is now async by default +#### `UNSAFE_root` removed -In v14, `renderHook` is now async by default and returns a Promise. This change makes it compatible with React 19, React Suspense, and `React.use()`. - -**Before (v13):** - -```ts -import { renderHook } from '@testing-library/react-native'; - -it('should test hook', () => { - const { result, rerender } = renderHook(() => useMyHook()); - - rerender(newProps); - unmount(); -}); -``` - -**After (v14):** - -```ts -import { renderHook } from '@testing-library/react-native'; - -it('should test hook', async () => { - const { result, rerender } = await renderHook(() => useMyHook()); - - await rerender(newProps); - await unmount(); -}); -``` - -#### Step-by-step migration guide - -To migrate from v13 `renderHook` to v14 `renderHook`: - -##### 1. Add `async` to your test function - -```ts -// Before -it('should test hook', () => { - -// After -it('should test hook', async () => { -``` - -##### 2. Await `renderHook` +`UNSAFE_root` has been removed. Use `container` to access the pseudo-element container, or `root` to access the first rendered host element: ```ts // Before (v13) -const { result } = renderHook(() => useMyHook()); +const unsafeRoot = screen.UNSAFE_root; // After (v14) -const { result } = await renderHook(() => useMyHook()); +const container = screen.container; // pseudo-element container +const root = screen.root; // first rendered host element ``` -##### 3. Update `rerender` calls to await +#### Legacy `UNSAFE_*` queries removed -```ts -// Before -rerender(newProps); - -// After -await rerender(newProps); -``` - -##### 4. Update `unmount` calls to await - -```ts -// Before -unmount(); - -// After -await unmount(); -``` +The legacy `UNSAFE_getAllByType`, `UNSAFE_getByType`, `UNSAFE_getAllByProps`, and `UNSAFE_getByProps` queries have been removed. These queries could return composite (user-defined) components, which is no longer supported with [Test Renderer](https://github.com/mdjastrzebski/test-renderer) as it only renders host elements. -##### 6. Update `act` calls to use async `act` +If you were using these legacy queries, you should refactor your tests to use the standard queries (`getByRole`, `getByText`, `getByTestId`, etc.) which target host elements. ```ts -// Before -act(() => { - result.current.doSomething(); -}); +// Before (v13) +const buttons = screen.UNSAFE_getAllByType(Button); +const input = screen.UNSAFE_getByProps({ placeholder: 'Enter text' }); -// After -await act(async () => { - result.current.doSomething(); -}); +// After (v14) +const buttons = screen.getAllByRole('button'); +const input = screen.getByPlaceholderText('Enter text'); ``` -#### Complete example +#### `concurrentRoot` option removed -**Before (v13):** +The `concurrentRoot` option has been removed from both `render` options and `configure` function. In v14, concurrent rendering is always enabled by default, as it is the standard rendering mode for React 19 and React Native's New Architecture. ```ts -import { renderHook, act } from '@testing-library/react-native'; - -it('should increment count', () => { - const { result, rerender } = renderHook((initialCount: number) => useCount(initialCount), { - initialProps: 1, - }); - - expect(result.current.count).toBe(1); - - act(() => { - result.current.increment(); - }); - - expect(result.current.count).toBe(2); - rerender(5); - expect(result.current.count).toBe(5); -}); -``` - -**After (v14):** - -```ts -import { renderHook, act } from '@testing-library/react-native'; - -it('should increment count', async () => { - const { result, rerender } = await renderHook((initialCount: number) => useCount(initialCount), { - initialProps: 1, - }); - - expect(result.current.count).toBe(1); - - await act(async () => { - result.current.increment(); - }); +// Before (v13) +render(, { concurrentRoot: true }); // Enable concurrent mode +render(, { concurrentRoot: false }); // Disable concurrent mode +configure({ concurrentRoot: false }); // Disable globally - expect(result.current.count).toBe(2); - await rerender(5); - expect(result.current.count).toBe(5); -}); +// After (v14) +await render(); // Always uses concurrent rendering ``` -#### Benefits of async `renderHook` +**Migration:** Simply remove any `concurrentRoot` options from your `render` calls and `configure` function. If you were explicitly setting `concurrentRoot: true`, no changes are needed beyond removing the option. If you were setting `concurrentRoot: false` to disable concurrent rendering, this is no longer supported in v14. -- **React 19 compatibility**: Works seamlessly with React 19's async features -- **Suspense support**: Properly handles React Suspense boundaries and `React.use()` -- **Better timing**: Ensures all pending React updates are executed before assertions -- **Future-proof**: Aligns with React's direction toward async rendering +### `container` API reintroduced -For more details, see the [`renderHook` API documentation](/docs/api/misc/render-hook). +In v14, the `container` API has been reintroduced and is now safe to use. Previously, `container` was renamed to `UNSAFE_root` in v12 due to behavioral differences from React Testing Library's `container`. In v14, `container` now returns a pseudo-element container whose children are the elements you asked to render, making it safe and consistent with React Testing Library's behavior. -### `fireEvent` is now async by default +**What changed:** -In v14, `fireEvent` and its helpers (`press`, `changeText`, `scroll`) are now async by default and return a Promise. This change makes it compatible with React 19, React Suspense, and `React.use()`. +- `screen.container` is now available and safe to use +- `container` returns a pseudo-element container from Test Renderer +- The container's children are the elements you rendered +- `UNSAFE_root` has been removed **Before (v13):** ```ts -import { fireEvent, screen } from '@testing-library/react-native'; +import { render, screen } from '@testing-library/react-native'; -it('should press button', () => { +it('should access root', () => { render(); - fireEvent.press(screen.getByText('Press me')); - expect(onPress).toHaveBeenCalled(); + // UNSAFE_root was the only way to access the container + const root = screen.UNSAFE_root; }); ``` **After (v14):** ```ts -import { fireEvent, screen } from '@testing-library/react-native'; +import { render, screen } from '@testing-library/react-native'; -it('should press button', async () => { - render(); - await fireEvent.press(screen.getByText('Press me')); - expect(onPress).toHaveBeenCalled(); +it('should access container', async () => { + await render(); + // container is now safe and available + const container = screen.container; + // root is the first child of container + const root = screen.root; }); ``` -### Text string validation now enforced by default +For more details, see the [`screen` API documentation](/docs/api/screen#container). + +### Text string validation enforced by default In v14, Test Renderer automatically enforces React Native's requirement that text strings must be rendered within a `` component. This means the `unstable_validateStringsRenderedWithinText` option has been removed from `RenderOptions`, as this validation is now always enabled. @@ -479,7 +406,7 @@ If you were using `unstable_validateStringsRenderedWithinText: true` in your ren ```ts // Before (v13) -await render(, { +render(, { unstable_validateStringsRenderedWithinText: true, }); @@ -490,135 +417,64 @@ await render(); If you were relying on the previous behavior where strings could be rendered outside of `` components, you'll need to fix your components to wrap strings in `` components, as this matches React Native's actual runtime behavior. -### `act` is now async by default - -In v14, `act` is now async by default and always returns a Promise. This change makes it compatible with React 19, React Suspense, and `React.use()`. - -**What changed:** - -- `act` now always returns a `Promise` instead of `T | Thenable` -- `act` is now a named export (no default export) +## Codemods -**Before (v13):** +We provide two codemods to automate the migration. Both codemods are **safe to run multiple times** - they will only transform code that hasn't been migrated yet. -```ts -import act from '@testing-library/react-native'; +### `rntl-v14-update-deps` -it('should update state', () => { - act(() => { - // Synchronous callback - setState('new value'); - }); - // act could return synchronously or a thenable - expect(state).toBe('new value'); -}); -``` +Updates your `package.json`: -**After (v14):** +- Removes React Test Renderer (`react-test-renderer` and `@types/react-test-renderer`) +- Adds Test Renderer (`test-renderer`) +- Updates `@testing-library/react-native` to `^14.0.0` -```ts -import { act } from '@testing-library/react-native'; + -it('should update state', async () => { - await act(async () => { - // Callback can be sync or async - setState('new value'); - }); - // act always returns a Promise - expect(state).toBe('new value'); -}); -``` +### `rntl-v14-async-functions` -#### Step-by-step migration guide +Transforms test files: -To migrate from v13 `act` to v14 `act`: +- Adds `await` to `render()`, `act()`, `renderHook()`, `fireEvent()` calls +- Makes test functions async when needed +- Handles `screen.rerender()`, `screen.unmount()`, and renderer methods -##### 1. Update import statement + -```ts -// Before -import act from '@testing-library/react-native'; +#### Custom render functions -// After -import { act } from '@testing-library/react-native'; -``` +If you have custom render helpers (like `renderWithProviders`), you can specify them using the `customRenderFunctions` parameter. The codemod will then also transform calls to these functions: -##### 2. Add `async` to your test function + -```ts -// Before -it('should update state', () => { +This will add `await` to your custom render calls and make the containing test functions async, just like it does for the standard `render` function. -// After -it('should update state', async () => { -``` +#### Limitations -##### 3. Await `act` calls +- Helper functions defined in test files are not transformed by default +- Namespace imports (`import * as RNTL`) are not handled -```ts -// Before -act(() => { - setState('new value'); -}); +## Full Changelog -// After -await act(async () => { - setState('new value'); -}); -``` - -**Note**: Even if your callback is synchronous, you should still use `await act(async () => ...)` as `act` now always returns a Promise. - -#### Complete example - -**Before (v13):** - -```ts -import act from '@testing-library/react-native'; - -it('should update state synchronously', () => { - act(() => { - setState('new value'); - }); - expect(state).toBe('new value'); -}); - -it('should update state asynchronously', () => { - act(async () => { - await Promise.resolve(); - setState('new value'); - }).then(() => { - expect(state).toBe('new value'); - }); -}); -``` - -**After (v14):** - -```ts -import { act } from '@testing-library/react-native'; - -it('should update state synchronously', async () => { - await act(async () => { - setState('new value'); - }); - expect(state).toBe('new value'); -}); - -it('should update state asynchronously', async () => { - await act(async () => { - await Promise.resolve(); - setState('new value'); - }); - expect(state).toBe('new value'); -}); -``` - -#### Benefits of async `act` - -- **React 19 compatibility**: Works seamlessly with React 19's async features -- **Suspense support**: Properly handles React Suspense boundaries and `React.use()` -- **Consistent API**: Always returns a Promise, making the API more predictable -- **Future-proof**: Aligns with React's direction toward async rendering - -For more details, see the [`act` API documentation](/docs/api/misc/other#act). +https://github.com/callstack/react-native-testing-library/compare/v13.3.3...v14.0.0 diff --git a/website/docs/14.x/docs/migration/previous/v2.mdx b/website/docs/14.x/docs/migration/v2.mdx similarity index 100% rename from website/docs/14.x/docs/migration/previous/v2.mdx rename to website/docs/14.x/docs/migration/v2.mdx diff --git a/website/docs/14.x/docs/migration/previous/v7.mdx b/website/docs/14.x/docs/migration/v7.mdx similarity index 100% rename from website/docs/14.x/docs/migration/previous/v7.mdx rename to website/docs/14.x/docs/migration/v7.mdx diff --git a/website/docs/14.x/docs/migration/previous/v9.mdx b/website/docs/14.x/docs/migration/v9.mdx similarity index 100% rename from website/docs/14.x/docs/migration/previous/v9.mdx rename to website/docs/14.x/docs/migration/v9.mdx diff --git a/website/docs/14.x/docs/start/_meta.json b/website/docs/14.x/docs/start/_meta.json index ac138301d..06cb91e9b 100644 --- a/website/docs/14.x/docs/start/_meta.json +++ b/website/docs/14.x/docs/start/_meta.json @@ -1 +1,9 @@ -["intro", "quick-start"] +[ + "intro", + "quick-start", + { + "type": "link", + "label": "v14 Migration", + "link": "/docs/migration/v14" + } +] diff --git a/website/docs/14.x/docs/start/intro.md b/website/docs/14.x/docs/start/intro.md index 391e6098e..edf0924b1 100644 --- a/website/docs/14.x/docs/start/intro.md +++ b/website/docs/14.x/docs/start/intro.md @@ -6,7 +6,7 @@ You want to write maintainable tests for your React Native components. As a part ## This solution -The React Native Testing Library (RNTL) is a lightweight solution for testing React Native components. It provides light utility functions on top of [Test Renderer](https://github.com/mdjastrzebski/test-renderer), in a way that encourages better testing practices. Its primary guiding principle is: +The React Native Testing Library (RNTL) is a comprehensive solution for testing React Native components. It provides React Native runtime simulation on top of [Test Renderer](https://github.com/mdjastrzebski/test-renderer), in a way that encourages better testing practices. Its primary guiding principle is: > The more your tests resemble how your software is used, the more confidence they can give you. @@ -23,7 +23,7 @@ test('form submits two answers', async () => { const onSubmit = jest.fn(); const user = userEvent.setup(); - render(); + await render(); const answerInputs = screen.getAllByLabelText('answer input'); await user.type(answerInputs[0], 'a1'); diff --git a/website/docs/14.x/docs/start/quick-start.mdx b/website/docs/14.x/docs/start/quick-start.mdx index 116c50d80..95ebcd22c 100644 --- a/website/docs/14.x/docs/start/quick-start.mdx +++ b/website/docs/14.x/docs/start/quick-start.mdx @@ -8,16 +8,29 @@ Open a Terminal in your project's folder and run: + +This library has a peer dependency on [Test Renderer](https://github.com/mdjastrzebski/test-renderer). Make sure to install it: + + -This library uses [Test Renderer](https://github.com/mdjastrzebski/test-renderer) as its underlying rendering engine. Test Renderer is automatically installed as a dependency and provides better compatibility with React 19 and improved type safety compared to the deprecated `react-test-renderer` package. +Test Renderer provides better compatibility with React 19 and improved type safety compared to the deprecated [React Test Renderer](https://reactjs.org/docs/test-renderer.html). ### Jest matchers -RNTL v13 automatically extends Jest with React Native-specific matchers. The only thing you need to do is to import anything from `@testing-library/react-native` which you already need to do to access the `render` function. +RNTL automatically extends Jest with React Native-specific matchers. The only thing you need to do is to import anything from `@testing-library/react-native` which you already need to do to access the `render` function. ### ESLint plugin @@ -27,8 +40,10 @@ Install the plugin (assuming you already have `eslint` installed & configured): diff --git a/yarn.lock b/yarn.lock index 83b3db2e1..053f7e3c9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3038,14 +3038,14 @@ __metadata: react-native-gesture-handler: "npm:^2.29.1" redent: "npm:^3.0.0" release-it: "npm:^19.0.6" - test-renderer: "npm:0.13.2" + test-renderer: "npm:0.14.0" typescript: "npm:^5.9.3" typescript-eslint: "npm:^8.47.0" peerDependencies: jest: ">=29.0.0" react: ">=19.0.0" react-native: ">=0.78" - test-renderer: ~0.13.2 + test-renderer: ^0.14.0 peerDependenciesMeta: jest: optional: true @@ -3213,12 +3213,12 @@ __metadata: languageName: node linkType: hard -"@types/react-reconciler@npm:~0.32.0": - version: 0.32.3 - resolution: "@types/react-reconciler@npm:0.32.3" +"@types/react-reconciler@npm:~0.31.0": + version: 0.31.0 + resolution: "@types/react-reconciler@npm:0.31.0" peerDependencies: "@types/react": "*" - checksum: 10c0/8d6485c6da3aa6d84b5c320c4cf9737e67511cd6c7ebfb7252ec032fd1cfb0515d3a2c06054491603c6ab040202694ff5ad125cd840e1da9e76d7447cef53dff + checksum: 10c0/9d8fd6334760d51e94dbf22b9783199c8937a2b76d1f682ef6f7f46d0ced578ccc8a9e285475931c9d410df1cae4b0fc17c0b3bb55dd00cc4e9a70a5707b3b09 languageName: node linkType: hard @@ -9380,14 +9380,14 @@ __metadata: languageName: node linkType: hard -"react-reconciler@npm:~0.32.0": - version: 0.32.0 - resolution: "react-reconciler@npm:0.32.0" +"react-reconciler@npm:~0.31.0": + version: 0.31.0 + resolution: "react-reconciler@npm:0.31.0" dependencies: - scheduler: "npm:^0.26.0" + scheduler: "npm:^0.25.0" peerDependencies: - react: ^19.1.0 - checksum: 10c0/ace0562d2aa99685416ac62741354706dec6df334aa64acc7ad455bd8a6f6af0068b276ad2e5412c3875388022ab13807f0b7d688fda7b2835301c110247146b + react: ^19.0.0 + checksum: 10c0/97920e1866c7206e200c3920c133c2e85f62a3c54fd9bc4b83c10c558d83d98eb378caab4fe37498e0cc1b1b2665d898627f2ae2537b29c8ab295ec8abc0c580 languageName: node linkType: hard @@ -9778,10 +9778,10 @@ __metadata: languageName: node linkType: hard -"scheduler@npm:^0.26.0": - version: 0.26.0 - resolution: "scheduler@npm:0.26.0" - checksum: 10c0/5b8d5bfddaae3513410eda54f2268e98a376a429931921a81b5c3a2873aab7ca4d775a8caac5498f8cbc7d0daeab947cf923dbd8e215d61671f9f4e392d34356 +"scheduler@npm:^0.25.0": + version: 0.25.0 + resolution: "scheduler@npm:0.25.0" + checksum: 10c0/a4bb1da406b613ce72c1299db43759526058fdcc413999c3c3e0db8956df7633acf395cb20eb2303b6a65d658d66b6585d344460abaee8080b4aa931f10eaafe languageName: node linkType: hard @@ -10440,15 +10440,15 @@ __metadata: languageName: node linkType: hard -"test-renderer@npm:0.13.2": - version: 0.13.2 - resolution: "test-renderer@npm:0.13.2" +"test-renderer@npm:0.14.0": + version: 0.14.0 + resolution: "test-renderer@npm:0.14.0" dependencies: - "@types/react-reconciler": "npm:~0.32.0" - react-reconciler: "npm:~0.32.0" + "@types/react-reconciler": "npm:~0.31.0" + react-reconciler: "npm:~0.31.0" peerDependencies: - react: ^19.1.0 - checksum: 10c0/ab55078e2287e77145eafc61a7762bdb51b3c9ee1d65fef0e9efb3a20d876bc5e0cbec22c42db01e342bc2d237ff11fe60b9e2f2bf6d9e70dba8c0841826aa7f + react: ^19.0.0 + checksum: 10c0/d259f5d146f57bb6689cb828de73cd95a559bc3203f1a21824b027a6b43143447bbf60eed5d2f8ea18b806ee15002aa6ba1d023e01669fa9945ca8278391667d languageName: node linkType: hard