Testing Stack
Readme.so uses a comprehensive testing setup:
Jest - JavaScript testing framework
React Testing Library - React component testing utilities
@testing-library/user-event - User interaction simulation
@testing-library/jest-dom - Custom Jest matchers for DOM
Running Tests
The project includes several npm scripts for running tests:
Run All Tests
Watch Mode
With Coverage
Specific Test File
Test Scripts Explained
Command Description npm testRuns all tests once and exits npm test:watchRuns tests in watch mode, re-running on file changes npm test -- --coverageGenerates code coverage report npm test -- --verboseShows individual test results
Jest Configuration
The Jest configuration is defined in jest.config.js:
module . exports = {
resetModules: true ,
restoreMocks: true ,
setupFilesAfterEnv: [ '<rootDir>/jest.setup.js' ],
testPathIgnorePatterns: [
'<rootDir>/*.js' ,
'<rootDir>/.next/' ,
'<rootDir>/node_modules/'
],
collectCoverage: true ,
collectCoverageFrom: [
'<rootDir>/components/*' ,
'<rootDir>/pages/editor.js'
],
coveragePathIgnorePatterns: [ '<rootDir>/.next/*' ],
}
Key Settings:
resetModules - Resets module registry before each test
restoreMocks - Restores mocked functions after each test
setupFilesAfterEnv - Runs setup file before tests
collectCoverageFrom - Specifies which files to include in coverage
Jest Setup File
jest.setup.js configures the testing environment:
import '@testing-library/jest-dom'
This imports custom matchers like:
toBeInTheDocument()
toHaveClass()
toHaveTextContent()
toBeVisible()
Test Structure
Tests are located in the components/__tests__/ directory:
components/
├── __tests__/
│ ├── SectionsColumn.test.js
│ ├── EditorColumn.test.js
│ ├── PreviewColumn.test.js
│ ├── CustomSection.test.js
│ ├── Nav.test.js
│ ├── DownloadModal.test.js
│ ├── SortableItem.test.js
│ ├── Tabs.test.js
│ └── ...
├── SectionsColumn.js
├── EditorColumn.js
└── ...
Each component file has a corresponding test file with the same name plus .test.js
Writing Tests
Basic Test Structure
import { render , screen } from '@testing-library/react'
import { MyComponent } from '../MyComponent'
describe ( 'MyComponent' , () => {
it ( 'renders without crashing' , () => {
render ( < MyComponent /> )
expect ( screen . getByText ( 'Hello' )). toBeInTheDocument ()
})
})
Example: Testing a Simple Component
Let’s test a hypothetical SectionButton component:
import { render , screen , fireEvent } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import SectionButton from '../SectionButton'
describe ( 'SectionButton' , () => {
it ( 'renders the section name' , () => {
render (
< SectionButton
section = { { name: 'Installation' , slug: 'installation' } }
/>
)
expect ( screen . getByText ( 'Installation' )). toBeInTheDocument ()
})
it ( 'calls onClick when clicked' , () => {
const handleClick = jest . fn ()
render (
< SectionButton
section = { { name: 'Installation' , slug: 'installation' } }
onClick = { handleClick }
/>
)
fireEvent . click ( screen . getByText ( 'Installation' ))
expect ( handleClick ). toHaveBeenCalledTimes ( 1 )
})
it ( 'is disabled when disabled prop is true' , () => {
render (
< SectionButton
section = { { name: 'Installation' , slug: 'installation' } }
disabled = { true }
/>
)
const button = screen . getByRole ( 'button' )
expect ( button ). toBeDisabled ()
})
})
Testing User Interactions
Use @testing-library/user-event for realistic user interactions:
import userEvent from '@testing-library/user-event'
it ( 'updates input on typing' , async () => {
const user = userEvent . setup ()
render ( < SearchFilter /> )
const input = screen . getByRole ( 'textbox' )
await user . type ( input , 'installation' )
expect ( input ). toHaveValue ( 'installation' )
})
Testing with Props
Test components with different prop combinations:
describe ( 'PreviewColumn' , () => {
const mockGetTemplate = jest . fn (( slug ) => ({
slug ,
name: 'Test' ,
markdown: '## Test'
}))
it ( 'renders preview mode by default' , () => {
render (
< PreviewColumn
selectedSectionSlugs = { [ 'title' ] }
getTemplate = { mockGetTemplate }
selectedTab = "preview"
markdown = "# Test README"
/>
)
expect ( screen . getByText ( 'Test README' )). toBeInTheDocument ()
})
it ( 'renders raw markdown in raw mode' , () => {
render (
< PreviewColumn
selectedSectionSlugs = { [ 'title' ] }
getTemplate = { mockGetTemplate }
selectedTab = "raw"
markdown = "# Test README"
/>
)
const textarea = screen . getByRole ( 'textbox' )
expect ( textarea ). toHaveValue ( '# Test README' )
})
})
Common Testing Patterns
Mocking localStorage
Many components use localStorage. Mock it in your tests:
beforeEach (() => {
// Clear localStorage before each test
localStorage . clear ()
// Mock localStorage methods
Storage . prototype . getItem = jest . fn ()
Storage . prototype . setItem = jest . fn ()
Storage . prototype . removeItem = jest . fn ()
})
it ( 'saves data to localStorage' , () => {
render ( < SectionsColumn { ... props } /> )
// Perform actions...
expect ( localStorage . setItem ). toHaveBeenCalledWith (
'current-slug-list' ,
expect . any ( String )
)
})
Mocking Next.js Router
For components using Next.js router:
jest . mock ( 'next/router' , () => ({
useRouter : () => ({
push: jest . fn (),
pathname: '/editor' ,
query: {},
}),
}))
Mocking i18next Translations
Mock translation functions:
jest . mock ( 'next-i18next' , () => ({
useTranslation : () => ({
t : ( key ) => key ,
i18n: {
language: 'en' ,
changeLanguage: jest . fn (),
},
}),
}))
Testing Async Operations
Handle asynchronous operations with waitFor:
import { render , screen , waitFor } from '@testing-library/react'
it ( 'loads data asynchronously' , async () => {
render ( < DataComponent /> )
await waitFor (() => {
expect ( screen . getByText ( 'Loaded Data' )). toBeInTheDocument ()
})
})
Testing Hooks
Test custom hooks using renderHook:
import { renderHook , act } from '@testing-library/react'
import useLocalStorage from '../hooks/useLocalStorage'
describe ( 'useLocalStorage' , () => {
it ( 'saves backup with debounce' , async () => {
const { result } = renderHook (() => useLocalStorage ())
act (() => {
result . current . saveBackup ([{ slug: 'test' }])
})
// Wait for debounce
await new Promise ( resolve => setTimeout ( resolve , 1100 ))
expect ( localStorage . setItem ). toHaveBeenCalled ()
})
})
Testing Guidelines
What to Test
Focus on testing behavior, not implementation details.
Do Test:
✅ Component renders correctly with different props
✅ User interactions produce expected results
✅ State changes work as intended
✅ Error states are handled properly
✅ Accessibility requirements are met
✅ Edge cases and boundary conditions
Don’t Test:
❌ Implementation details (internal state, private methods)
❌ Third-party library internals
❌ Styles and CSS (unless critical to functionality)
Writing Testable Components
Good: Testable Component
export const SectionList = ({ sections , onSelect }) => {
return (
< ul role = "list" >
{ sections . map ( section => (
< li key = { section . slug } >
< button onClick = { () => onSelect ( section . slug ) } >
{ section . name }
</ button >
</ li >
)) }
</ ul >
)
}
Bad: Hard to Test
const SectionList = () => {
// Hard-coded data
const sections = getSections ()
// Side effects in render
useEffect (() => {
analytics . track ( 'view' )
}, [])
return (
< ul >
{ sections . map ( s => (
< li onClick = { () => {
// Complex logic
doManyThings ( s )
} } >
{ s . name }
</ li >
)) }
</ ul >
)
}
Test Coverage Goals
Aim for meaningful test coverage, not just high percentages. 100% coverage doesn’t guarantee bug-free code.
Current Coverage Targets:
Components: Covered in collectCoverageFrom
Editor page: Covered in collectCoverageFrom
Critical user paths: High priority
View Coverage Report:
npm test -- --coverage --coverageReporters=html
open coverage/index.html
Debugging Tests
Common Issues
Problem: Test times out
// Solution: Increase timeout for slow operations
jest . setTimeout ( 10000 ) // 10 seconds
it ( 'slow operation' , async () => {
// ...
}, 10000 )
Problem: Can’t find element
// Solution: Use screen.debug() to see rendered output
it ( 'finds element' , () => {
render ( < MyComponent /> )
screen . debug () // Prints DOM to console
// Or debug specific element
screen . debug ( screen . getByRole ( 'button' ))
})
Problem: State updates not reflected
// Solution: Wrap state updates in act()
import { act } from '@testing-library/react'
act (() => {
fireEvent . click ( button )
})
Running Tests in Debug Mode
# Run with Node debugger
node --inspect-brk node_modules/.bin/jest --runInBand
# In Chrome, navigate to:
chrome://inspect
Continuous Integration
Tests run automatically on GitHub Actions for every pull request:
Pre-merge Checks:
✅ All tests pass
✅ Linting passes
✅ Build succeeds
Pull requests must pass all automated checks before they can be merged.
Best Practices
1. Use Descriptive Test Names
it ( 'displays error message when email is invalid' , () => {})
it ( 'disables submit button while form is submitting' , () => {})
2. Follow AAA Pattern
it ( 'example test' , () => {
// Arrange: Set up test data and conditions
const user = { name: 'Alice' }
render ( < UserProfile user = { user } /> )
// Act: Perform the action being tested
fireEvent . click ( screen . getByText ( 'Edit' ))
// Assert: Verify the expected outcome
expect ( screen . getByText ( 'Editing Alice' )). toBeInTheDocument ()
})
3. Keep Tests Isolated
// Bad: Tests depend on each other
let sharedState = []
it ( 'adds item' , () => {
sharedState . push ( 'item' )
})
it ( 'removes item' , () => {
sharedState . pop () // Depends on previous test
})
// Good: Each test is independent
it ( 'adds item' , () => {
const state = []
state . push ( 'item' )
expect ( state ). toHaveLength ( 1 )
})
4. Use Data-Testid Sparingly
Prefer semantic queries:
// Best: Semantic queries
screen . getByRole ( 'button' , { name: 'Submit' })
screen . getByLabelText ( 'Email address' )
screen . getByText ( 'Welcome' )
// Acceptable: When no semantic option exists
screen . getByTestId ( 'custom-component' )
Next Steps
Local Setup Set up your development environment
Architecture Understand the codebase structure
Contributing Learn the contribution workflow
View Tests Browse existing tests on GitHub
Well-tested code is maintainable code. Thank you for writing tests!