docs: add contribution guidelines from wiki to Developer Portal (#36523)

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Evan Rusackas
2025-12-15 16:54:29 -08:00
committed by GitHub
parent 5f431ee1ec
commit 6f8052b828
9 changed files with 609 additions and 816 deletions

View File

@@ -1,6 +1,6 @@
---
title: Component Style Guidelines and Best Practices
sidebar_position: 1
sidebar_position: 2
---
<!--
@@ -35,7 +35,7 @@ This guide is intended primarily for reusable components. Whenever possible, all
- All components should be made to be reusable whenever possible
- All components should follow the structure and best practices as detailed below
## Directory and component structure
### Directory and component structure
```
superset-frontend/src/components
@@ -51,208 +51,142 @@ superset-frontend/src/components
**Component directory name:** Use `PascalCase` for the component directory name
**Storybook:** Components should come with a storybook file whenever applicable, with the following naming convention `{ComponentName}.stories.tsx`. More details about Storybook below
**Storybook:** Components should come with a storybook file whenever applicable, with the following naming convention `\{ComponentName\}.stories.tsx`. More details about Storybook below
**Unit and end-to-end tests:** All components should come with unit tests using Jest and React Testing Library. The file name should follow this naming convention `{ComponentName}.test.tsx.` Read the [Testing Guidelines and Best Practices](./testing-guidelines) for more details about tests
**Unit and end-to-end tests:** All components should come with unit tests using Jest and React Testing Library. The file name should follow this naming convention `\{ComponentName\}.test.tsx`. Read the [Testing Guidelines and Best Practices](../../testing/testing-guidelines) for more details
## Component Development Best Practices
**Reference naming:** Use `PascalCase` for React components and `camelCase` for component instances
### Use TypeScript
All new components should be written in TypeScript. This helps catch errors early and provides better development experience with IDE support.
```tsx
interface ComponentProps {
title: string;
isVisible?: boolean;
onClose?: () => void;
}
export const MyComponent: React.FC<ComponentProps> = ({
title,
isVisible = true,
onClose
}) => {
// Component implementation
};
**BAD:**
```jsx
import mainNav from './MainNav';
```
### Prefer Functional Components
**GOOD:**
```jsx
import MainNav from './MainNav';
```
Use functional components with hooks instead of class components:
**BAD:**
```jsx
const NavItem = <MainNav />;
```
**GOOD:**
```jsx
const navItem = <MainNav />;
```
**Component naming:** Use the file name as the component name
**BAD:**
```jsx
import MainNav from './MainNav/index';
```
**GOOD:**
```jsx
import MainNav from './MainNav';
```
**Props naming:** Do not use DOM related props for different purposes
**BAD:**
```jsx
<MainNav style="big" />
```
**GOOD:**
```jsx
<MainNav variant="big" />
```
**Importing dependencies:** Only import what you need
**BAD:**
```jsx
import * as React from "react";
```
**GOOD:**
```jsx
import React, { useState } from "react";
```
**Default VS named exports:** As recommended by [TypeScript](https://www.typescriptlang.org/docs/handbook/modules.html), "If a module's primary purpose is to house one specific export, then you should consider exporting it as a default export. This makes both importing and actually using the import a little easier". If you're exporting multiple objects, use named exports instead.
_As a default export_
```jsx
import MainNav from './MainNav';
```
_As a named export_
```jsx
import { MainNav, SecondaryNav } from './Navbars';
```
**ARIA roles:** Always make sure you are writing accessible components by using the official [ARIA roles](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Techniques)
## Use TypeScript
All components should be written in TypeScript and their extensions should be `.ts` or `.tsx`
### type vs interface
Validate all props with the correct types. This replaces the need for a run-time validation as provided by the prop-types library.
```tsx
// ✅ Good - Functional component with hooks
export const MyComponent: React.FC<Props> = ({ data }) => {
const [loading, setLoading] = useState(false);
type HeadingProps = {
param: string;
}
useEffect(() => {
// Effect logic
}, []);
return <div>{/* Component JSX */}</div>;
};
// ❌ Avoid - Class component
class MyComponent extends React.Component {
// Class implementation
export default function Heading({ children }: HeadingProps) {
return <h2>{children}</h2>
}
```
### Follow Ant Design Patterns
Use `type` for your component props and state. Use `interface` when you want to enable _declaration merging_.
Extend Ant Design components rather than building from scratch:
### Define default values for non-required props
In order to improve the readability of your code and reduce assumptions, always add default values for non required props, when applicable, for example:
```tsx
import { Button } from 'antd';
import styled from '@emotion/styled';
const StyledButton = styled(Button)`
// Custom styling using emotion
`;
const applyDiscount = (price: number, discount = 0.05) => price * (1 - discount);
```
### Reusability and Props Design
## Functional components and Hooks
Design components with reusability in mind:
We prefer functional components and the usage of hooks over class components.
### useState
Always explicitly declare the type unless the type can easily be assumed by the declaration.
```tsx
interface ButtonProps {
variant?: 'primary' | 'secondary' | 'tertiary';
size?: 'small' | 'medium' | 'large';
loading?: boolean;
disabled?: boolean;
children: React.ReactNode;
onClick?: () => void;
}
export const CustomButton: React.FC<ButtonProps> = ({
variant = 'primary',
size = 'medium',
...props
}) => {
// Implementation
};
const [customer, setCustomer] = useState<ICustomer | null>(null);
```
## Testing Components
### useReducer
Every component should include comprehensive tests:
Always prefer `useReducer` over `useState` when your state has complex logics.
```tsx
// MyComponent.test.tsx
import { render, screen, fireEvent } from '@testing-library/react';
import { MyComponent } from './MyComponent';
### useMemo and useCallback
test('renders component with title', () => {
render(<MyComponent title="Test Title" />);
expect(screen.getByText('Test Title')).toBeInTheDocument();
});
Always memoize when your components take functions or complex objects as props to avoid unnecessary rerenders.
test('calls onClose when close button is clicked', () => {
const mockOnClose = jest.fn();
render(<MyComponent title="Test" onClose={mockOnClose} />);
### Custom hooks
fireEvent.click(screen.getByRole('button', { name: /close/i }));
expect(mockOnClose).toHaveBeenCalledTimes(1);
});
```
All custom hooks should be located in the directory `/src/hooks`. Before creating a new custom hook, make sure that is not available in the existing custom hooks.
## Storybook Stories
## Storybook
Create stories for visual testing and documentation:
Each component should come with its dedicated storybook file.
```tsx
// MyComponent.stories.tsx
import type { Meta, StoryObj } from '@storybook/react';
import { MyComponent } from './MyComponent';
**One component per story:** Each storybook file should only contain one component unless substantially different variants are required
const meta: Meta<typeof MyComponent> = {
title: 'Components/MyComponent',
component: MyComponent,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
};
**Component variants:** If the component behavior is substantially different when certain props are used, it is best to separate the story into different types. See the `superset-frontend/src/components/Select/Select.stories.tsx` as an example.
export default meta;
type Story = StoryObj<typeof meta>;
**Isolated state:** The storybook should show how the component works in an isolated state and with as few dependencies as possible
export const Default: Story = {
args: {
title: 'Default Component',
isVisible: true,
},
};
export const Hidden: Story = {
args: {
title: 'Hidden Component',
isVisible: false,
},
};
```
## Performance Considerations
### Use React.memo for Expensive Components
```tsx
import React, { memo } from 'react';
export const ExpensiveComponent = memo<Props>(({ data }) => {
// Expensive rendering logic
return <div>{/* Component content */}</div>;
});
```
### Optimize Re-renders
Use `useCallback` and `useMemo` appropriately:
```tsx
export const OptimizedComponent: React.FC<Props> = ({ items, onSelect }) => {
const expensiveValue = useMemo(() => {
return items.reduce((acc, item) => acc + item.value, 0);
}, [items]);
const handleSelect = useCallback((id: string) => {
onSelect(id);
}, [onSelect]);
return <div>{/* Component content */}</div>;
};
```
## Accessibility
Ensure components are accessible:
```tsx
export const AccessibleButton: React.FC<Props> = ({ children, onClick }) => {
return (
<button
type="button"
aria-label="Descriptive label"
onClick={onClick}
>
{children}
</button>
);
};
```
## Error Boundaries
For components that might fail, consider error boundaries:
```tsx
export const SafeComponent: React.FC<Props> = ({ children }) => {
return (
<ErrorBoundary fallback={<div>Something went wrong</div>}>
{children}
</ErrorBoundary>
);
};
```
**Use args:** It should be possible to test the component with every variant of the available props. We recommend using [args](https://storybook.js.org/docs/react/writing-stories/args)

View File

@@ -1,6 +1,6 @@
---
title: Emotion Styling Guidelines and Best Practices
sidebar_position: 2
sidebar_position: 3
---
<!--
@@ -33,314 +33,245 @@ under the License.
- **DO** use `css` when you want to amend/merge sets of styles compositionally
- **DO** use `css` when you're making a small, or single-use set of styles for a component
- **DO** move your style definitions from direct usage in the `css` prop to an external variable when they get long
- **DO** prefer tagged template literals (`css={css\`...\`}`) over style objects wherever possible for maximum style portability/consistency
- **DO** use `useTheme` to get theme variables
- **DO** prefer tagged template literals (`css={css`...`}`) over style objects wherever possible for maximum style portability/consistency (note: typescript support may be diminished, but IDE plugins like [this](https://marketplace.visualstudio.com/items?itemName=jpoissonnier.vscode-styled-components) make life easy)
- **DO** use `useTheme` to get theme variables. `withTheme` should be only used for wrapping legacy Class-based components.
### DON'T do these things:
- **DON'T** use `styled` for small, single-use style tweaks that would be easier to read/review if they were inline
- **DON'T** export incomplete Ant Design components
- **DON'T** export incomplete AntD components (make sure all their compound components are exported)
## Emotion Tips and Strategies
The first thing to consider when adding styles to an element is how much you think a style might be reusable in other areas of Superset. Always err on the side of reusability here. Nobody wants to chase styling inconsistencies, or try to debug little endless overrides scattered around the codebase. The more we can consolidate, the less will have to be figured out by those who follow. Reduce, reuse, recycle.
## When to use `css` or `styled`
### Use `css` for:
In short, either works for just about any use case! And you'll see them used somewhat interchangeably in the existing codebase. But we need a way to weigh it when we encounter the choice, so here's one way to think about it:
1. **Small, single-use styles**
```tsx
import { css } from '@emotion/react';
A good use of `styled` syntax if you want to re-use a styled component. In other words, if you wanted to export flavors of a component for use, like so:
const MyComponent = () => (
<div
css={css`
margin: 8px;
padding: 16px;
`}
>
Content
</div>
);
```
2. **Composing styles**
```tsx
const baseStyles = css`
padding: 16px;
border-radius: 4px;
```jsx
const StatusThing = styled.div`
padding: 10px;
border-radius: 10px;
`;
const primaryStyles = css`
${baseStyles}
background-color: blue;
color: white;
export const InfoThing = styled(StatusThing)`
background: blue;
&::before {
content: "";
}
`;
const secondaryStyles = css`
${baseStyles}
background-color: gray;
color: black;
export const WarningThing = styled(StatusThing)`
background: orange;
&::before {
content: "⚠️";
}
`;
```
3. **Conditional styling**
```tsx
const MyComponent = ({ isActive }: { isActive: boolean }) => (
<div
css={[
baseStyles,
isActive && activeStyles,
]}
>
Content
</div>
);
```
### Use `styled` for:
1. **Reusable components**
```tsx
import styled from '@emotion/styled';
const StyledButton = styled.button`
padding: 12px 24px;
border: none;
border-radius: 4px;
background-color: ${({ theme }) => theme.colors.primary};
color: white;
&:hover {
background-color: ${({ theme }) => theme.colors.primaryDark};
export const TerribleThing = styled(StatusThing)`
background: red;
&::before {
content: "🔥";
}
`;
```
2. **Components with complex nested selectors**
```tsx
const StyledCard = styled.div`
padding: 16px;
border: 1px solid ${({ theme }) => theme.colors.border};
You can also use `styled` when you're building a bigger component, and just want to have some custom bits for internal use in your JSX. For example:
.card-header {
font-weight: bold;
margin-bottom: 8px;
}
.card-content {
color: ${({ theme }) => theme.colors.text};
p {
margin-bottom: 12px;
}
}
```jsx
const SeparatorOnlyUsedInThisComponent = styled.hr`
height: 12px;
border: 0;
box-shadow: inset 0 12px 12px -12px rgba(0, 0, 0, 0.5);
`;
function SuperComplicatedComponent(props) {
return (
<>
Daily standup for {user.name}!
<SeparatorOnlyUsedInThisComponent />
<h2>Yesterday:</h2>
// spit out a list of accomplishments
<SeparatorOnlyUsedInThisComponent />
<h2>Today:</h2>
// spit out a list of plans
<SeparatorOnlyUsedInThisComponent />
<h2>Tomorrow:</h2>
// spit out a list of goals
</>
);
}
```
3. **Extending Ant Design components**
```tsx
import { Button } from 'antd';
import styled from '@emotion/styled';
The `css` prop, in reality, shares all the same styling capabilities as `styled` but it does have some particular use cases that jump out as sensible. For example, if you just want to style one element in your component, you could add the styles inline like so:
const CustomButton = styled(Button)`
border-radius: 8px;
font-weight: 600;
&.ant-btn-primary {
background: linear-gradient(45deg, #1890ff, #722ed1);
}
`;
```jsx
function SomeFanciness(props) {
return (
<>
Here's an awesome report card for {user.name}!
<div
css={css`
box-shadow: 5px 5px 10px #ccc;
border-radius: 10px;
`}
>
<h2>Yesterday:</h2>
// ...some stuff
<h2>Today:</h2>
// ...some stuff
<h2>Tomorrow:</h2>
// ...some stuff
</div>
</>
);
}
```
## Using Theme Variables
You can also define the styles as a variable, external to your JSX. This is handy if the styles get long and you just want it out of the way. This is also handy if you want to apply the same styles to disparate element types, kind of like you might use a CSS class on varied elements. Here's a trumped up example:
Always use theme variables for consistent styling:
```tsx
import { useTheme } from '@emotion/react';
const MyComponent = () => {
const theme = useTheme();
```jsx
function FakeGlobalNav(props) {
const menuItemStyles = css`
display: block;
border-bottom: 1px solid cadetblue;
font-family: "Comic Sans", cursive;
`;
return (
<div
css={css`
background-color: ${theme.colors.grayscale.light5};
color: ${theme.colors.grayscale.dark2};
padding: ${theme.gridUnit * 4}px;
border-radius: ${theme.borderRadius}px;
`}
>
Content
<Nav>
<a css={menuItemStyles} href="#">One link</a>
<Link css={menuItemStyles} to={url}>Another link</Link>
<div css={menuItemStyles} onClick={() => alert('clicked')}>Another link</div>
</Nav>
);
}
```
## CSS tips and tricks
### `css` lets you write actual CSS
By default the `css` prop uses the object syntax with JS style definitions, like so:
```jsx
<div css={{
borderRadius: 10,
marginTop: 10,
backgroundColor: '#00FF00'
}}>Howdy</div>
```
But you can use the `css` interpolator as well to get away from icky JS styling syntax. Doesn't this look cleaner?
```jsx
<div css={css`
border-radius: 10px;
margin-top: 10px;
background-color: #00FF00;
`}>Howdy</div>
```
You might say "whatever… I can read and write JS syntax just fine." Well, that's great. But… let's say you're migrating in some of our legacy LESS styles… now it's copy/paste! Or if you want to migrate to or from `styled` syntax… also copy/paste!
### You can combine `css` definitions with array syntax
You can use multiple groupings of styles with the `css` interpolator, and combine/override them in array syntax, like so:
```jsx
function AnotherSillyExample(props) {
const shadowedCard = css`
box-shadow: 2px 2px 4px #999;
padding: 4px;
`;
const infoCard = css`
background-color: #33f;
border-radius: 4px;
`;
const overrideInfoCard = css`
background-color: #f33;
`;
return (
<div className="App">
Combining two classes:
<div css={[shadowedCard, infoCard]}>Hello</div>
Combining again, but now with overrides:
<div css={[shadowedCard, infoCard, overrideInfoCard]}>Hello</div>
</div>
);
};
```
## Common Patterns
### Responsive Design
```tsx
const ResponsiveContainer = styled.div`
display: flex;
flex-direction: column;
${({ theme }) => theme.breakpoints.up('md')} {
flex-direction: row;
}
`;
```
### Animation
```tsx
const FadeInComponent = styled.div`
opacity: 0;
transition: opacity 0.3s ease-in-out;
&.visible {
opacity: 1;
}
`;
```
### Conditional Props
```tsx
interface StyledDivProps {
isHighlighted?: boolean;
size?: 'small' | 'medium' | 'large';
}
```
const StyledDiv = styled.div<StyledDivProps>`
padding: 16px;
background-color: ${({ isHighlighted, theme }) =>
isHighlighted ? theme.colors.primary : theme.colors.grayscale.light5};
### Style variations with props
${({ size }) => {
switch (size) {
case 'small':
return css`font-size: 12px;`;
case 'large':
return css`font-size: 18px;`;
default:
return css`font-size: 14px;`;
}
}}
You can give any component a custom prop, and reference that prop in your component styles, effectively using the prop to turn on a "flavor" of that component.
For example, let's make a styled component that acts as a card. Of course, this could be done with any AntD component, or any component at all. But we'll do this with a humble `div` to illustrate the point:
```jsx
const SuperCard = styled.div`
${({ theme, cutout }) => `
padding: ${theme.gridUnit * 2}px;
border-radius: ${theme.borderRadius}px;
box-shadow: 10px 5px 10px #ccc ${cutout && 'inset'};
border: 1px solid ${cutout ? 'transparent' : theme.colors.secondary.light3};
`}
`;
```
## Best Practices
Then just use the component as `<SuperCard>Some content</SuperCard>` or with the (potentially dynamic) prop: `<SuperCard cutout>Some content</SuperCard>`
### 1. Use Semantic CSS Properties
```tsx
// ✅ Good - semantic property names
const Header = styled.h1`
font-size: ${({ theme }) => theme.typography.sizes.xl};
margin-bottom: ${({ theme }) => theme.gridUnit * 4}px;
`;
## Styled component tips
// ❌ Avoid - magic numbers
const Header = styled.h1`
font-size: 24px;
margin-bottom: 16px;
`;
```
### No need to use `theme` the hard way
### 2. Group Related Styles
```tsx
// ✅ Good - grouped styles
const Card = styled.div`
/* Layout */
display: flex;
flex-direction: column;
padding: ${({ theme }) => theme.gridUnit * 4}px;
It's very tempting (and commonly done) to use the `theme` prop inline in the template literal like so:
/* Appearance */
background-color: ${({ theme }) => theme.colors.grayscale.light5};
border: 1px solid ${({ theme }) => theme.colors.grayscale.light2};
```jsx
const SomeStyledThing = styled.div`
padding: ${({ theme }) => theme.gridUnit * 2}px;
border-radius: ${({ theme }) => theme.borderRadius}px;
/* Typography */
font-family: ${({ theme }) => theme.typography.families.sansSerif};
color: ${({ theme }) => theme.colors.grayscale.dark2};
border: 1px solid ${({ theme }) => theme.colors.secondary.light3};
`;
```
### 3. Extract Complex Styles
```tsx
// ✅ Good - extract complex styles to variables
const complexGradient = css`
background: linear-gradient(
135deg,
${({ theme }) => theme.colors.primary} 0%,
${({ theme }) => theme.colors.secondary} 100%
);
`;
Instead, you can make things a little easier to read/type by writing it like so:
const GradientButton = styled.button`
${complexGradient}
padding: 12px 24px;
border: none;
color: white;
```jsx
const SomeStyledThing = styled.div`
${({ theme }) => `
padding: ${theme.gridUnit * 2}px;
border-radius: ${theme.borderRadius}px;
border: 1px solid ${theme.colors.secondary.light3};
`}
`;
```
### 4. Avoid Deep Nesting
```tsx
// ✅ Good - shallow nesting
const Navigation = styled.nav`
.nav-item {
padding: 8px 16px;
}
## Extend an AntD component with custom styling
.nav-link {
color: ${({ theme }) => theme.colors.text};
text-decoration: none;
}
As mentioned, you want to keep your styling as close to the root of your component system as possible, to minimize repetitive styling/overrides, and err on the side of reusability. In some cases, that means you'll want to globally tweak one of our core components to match our design system. In Superset, that's Ant Design (AntD).
AntD uses a cool trick called compound components. For example, the `Menu` component also lets you use `Menu.Item`, `Menu.SubMenu`, `Menu.ItemGroup`, and `Menu.Divider`.
### The `Object.assign` trick
Let's say you want to override an AntD component called `Foo`, and have `Foo.Bar` display some custom CSS for the `Bar` compound component. You can do it effectively like so:
```jsx
import {
Foo as AntdFoo,
} from 'antd';
export const StyledBar = styled(AntdFoo.Bar)`
border-radius: ${({ theme }) => theme.borderRadius}px;
`;
// ❌ Avoid - deep nesting
const Navigation = styled.nav`
ul {
li {
a {
span {
color: blue; /* Too nested */
}
}
}
}
`;
```
## Performance Tips
### 1. Avoid Inline Functions in CSS
```tsx
// ✅ Good - external function
const getBackgroundColor = (isActive: boolean, theme: any) =>
isActive ? theme.colors.primary : theme.colors.secondary;
const Button = styled.button<{ isActive: boolean }>`
background-color: ${({ isActive, theme }) => getBackgroundColor(isActive, theme)};
`;
// ❌ Avoid - inline function (creates new function on each render)
const Button = styled.button<{ isActive: boolean }>`
background-color: ${({ isActive, theme }) =>
isActive ? theme.colors.primary : theme.colors.secondary};
`;
```
### 2. Use CSS Objects for Dynamic Styles
```tsx
// For highly dynamic styles, consider CSS objects
const dynamicStyles = (props: Props) => ({
backgroundColor: props.color,
fontSize: `${props.size}px`,
// ... other dynamic properties
export const Foo = Object.assign(AntdFoo, {
Bar: StyledBar,
});
const DynamicComponent = (props: Props) => (
<div css={dynamicStyles(props)}>
Content
</div>
);
```
You can then import this customized `Foo` and use `Foo.Bar` as expected. You should probably save your creation in `src/components` for maximum reusability, and add a Storybook entry so future engineers can view your creation, and designers can better understand how it fits the Superset Design System.

View File

@@ -1,297 +0,0 @@
---
title: Testing Guidelines and Best Practices
sidebar_position: 3
---
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->
# Testing Guidelines and Best Practices
We feel that tests are an important part of a feature and not an additional or optional effort. That's why we colocate test files with functionality and sometimes write tests upfront to help validate requirements and shape the API of our components. Every new component or file added should have an associated test file with the `.test` extension.
We use Jest, React Testing Library (RTL), and Cypress to write our unit, integration, and end-to-end tests. For each type, we have a set of best practices/tips described below:
## Jest
### Write simple, standalone tests
The importance of simplicity is often overlooked in test cases. Clear, dumb code should always be preferred over complex ones. The test cases should be pretty much standalone and should not involve any external logic if not absolutely necessary. That's because you want the corpus of the tests to be easy to read and understandable at first sight.
### Avoid nesting when you're testing
Avoid the use of `describe` blocks in favor of inlined tests. If your tests start to grow and you feel the need to group tests, prefer to break them into multiple test files. Check this awesome [article](https://kentcdodds.com/blog/avoid-nesting-when-youre-testing) written by [Kent C. Dodds](https://kentcdodds.com/) about this topic.
### No warnings!
Your tests shouldn't trigger warnings. This is really common when testing async functionality. It's really difficult to read test results when we have a bunch of warnings.
## React Testing Library (RTL)
### Keep accessibility in mind when writing your tests
One of the most important points of RTL is accessibility and this is also a very important point for us. We should try our best to follow the RTL [Priority](https://testing-library.com/docs/queries/about/#priority) when querying for elements in our tests. `getByTestId` is not viewable by the user and should only be used when the element isn't accessible in any other way.
### Don't use `act` unnecessarily
`render` and `fireEvent` are already wrapped in `act`, so wrapping them in `act` again is a common mistake. Some solutions to the warnings related to async testing can be found in the RTL docs.
## Example Test Structure
```tsx
// MyComponent.test.tsx
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { MyComponent } from './MyComponent';
// ✅ Good - Simple, standalone test
test('renders loading state initially', () => {
render(<MyComponent />);
expect(screen.getByText('Loading...')).toBeInTheDocument();
});
// ✅ Good - Tests user interaction
test('calls onSubmit when form is submitted', async () => {
const user = userEvent.setup();
const mockOnSubmit = jest.fn();
render(<MyComponent onSubmit={mockOnSubmit} />);
await user.type(screen.getByLabelText('Username'), 'testuser');
await user.click(screen.getByRole('button', { name: 'Submit' }));
expect(mockOnSubmit).toHaveBeenCalledWith({ username: 'testuser' });
});
// ✅ Good - Tests async behavior
test('displays error message when API call fails', async () => {
const mockFetch = jest.fn().mockRejectedValue(new Error('API Error'));
global.fetch = mockFetch;
render(<MyComponent />);
await waitFor(() => {
expect(screen.getByText('Error: API Error')).toBeInTheDocument();
});
});
```
## Testing Best Practices
### Use appropriate queries in priority order
1. **Accessible to everyone**: `getByRole`, `getByLabelText`, `getByPlaceholderText`, `getByText`
2. **Semantic queries**: `getByAltText`, `getByTitle`
3. **Test IDs**: `getByTestId` (last resort)
```tsx
// ✅ Good - using accessible queries
test('user can submit form', () => {
render(<LoginForm />);
const usernameInput = screen.getByLabelText('Username');
const passwordInput = screen.getByLabelText('Password');
const submitButton = screen.getByRole('button', { name: 'Log in' });
// Test implementation
});
// ❌ Avoid - using test IDs when better options exist
test('user can submit form', () => {
render(<LoginForm />);
const usernameInput = screen.getByTestId('username-input');
const passwordInput = screen.getByTestId('password-input');
const submitButton = screen.getByTestId('submit-button');
// Test implementation
});
```
### Test behavior, not implementation details
```tsx
// ✅ Good - tests what the user experiences
test('shows success message after successful form submission', async () => {
render(<ContactForm />);
await userEvent.type(screen.getByLabelText('Email'), 'test@example.com');
await userEvent.click(screen.getByRole('button', { name: 'Submit' }));
await waitFor(() => {
expect(screen.getByText('Message sent successfully!')).toBeInTheDocument();
});
});
// ❌ Avoid - testing implementation details
test('calls setState when form is submitted', () => {
const component = shallow(<ContactForm />);
const instance = component.instance();
const spy = jest.spyOn(instance, 'setState');
instance.handleSubmit();
expect(spy).toHaveBeenCalled();
});
```
### Mock external dependencies appropriately
```tsx
// Mock API calls
jest.mock('../api/userService', () => ({
getUser: jest.fn(),
createUser: jest.fn(),
}));
// Mock components that aren't relevant to the test
jest.mock('../Chart/Chart', () => {
return function MockChart() {
return <div data-testid="mock-chart">Chart Component</div>;
};
});
```
## Async Testing Patterns
### Testing async operations
```tsx
test('loads and displays user data', async () => {
const mockUser = { id: 1, name: 'John Doe' };
const mockGetUser = jest.fn().mockResolvedValue(mockUser);
render(<UserProfile getUserData={mockGetUser} />);
// Wait for the async operation to complete
await waitFor(() => {
expect(screen.getByText('John Doe')).toBeInTheDocument();
});
expect(mockGetUser).toHaveBeenCalledTimes(1);
});
```
### Testing loading states
```tsx
test('shows loading spinner while fetching data', async () => {
const mockGetUser = jest.fn().mockImplementation(
() => new Promise(resolve => setTimeout(resolve, 100))
);
render(<UserProfile getUserData={mockGetUser} />);
expect(screen.getByText('Loading...')).toBeInTheDocument();
await waitFor(() => {
expect(screen.queryByText('Loading...')).not.toBeInTheDocument();
});
});
```
## Component-Specific Testing
### Testing form components
```tsx
test('validates required fields', async () => {
render(<RegistrationForm />);
const submitButton = screen.getByRole('button', { name: 'Register' });
await userEvent.click(submitButton);
expect(screen.getByText('Username is required')).toBeInTheDocument();
expect(screen.getByText('Email is required')).toBeInTheDocument();
});
```
### Testing modals and overlays
```tsx
test('opens and closes modal', async () => {
render(<ModalContainer />);
const openButton = screen.getByRole('button', { name: 'Open Modal' });
await userEvent.click(openButton);
expect(screen.getByRole('dialog')).toBeInTheDocument();
const closeButton = screen.getByRole('button', { name: 'Close' });
await userEvent.click(closeButton);
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
});
```
### Testing with context providers
```tsx
const renderWithTheme = (component: React.ReactElement) => {
return render(
<ThemeProvider theme={mockTheme}>
{component}
</ThemeProvider>
);
};
test('applies theme colors correctly', () => {
renderWithTheme(<ThemedButton />);
const button = screen.getByRole('button');
expect(button).toHaveStyle({
backgroundColor: mockTheme.colors.primary,
});
});
```
## Performance Testing
### Testing with large datasets
```tsx
test('handles large lists efficiently', () => {
const largeDataset = Array.from({ length: 10000 }, (_, i) => ({
id: i,
name: `Item ${i}`,
}));
const { container } = render(<VirtualizedList items={largeDataset} />);
// Should only render visible items
const renderedItems = container.querySelectorAll('[data-testid="list-item"]');
expect(renderedItems.length).toBeLessThan(100);
});
```
## Testing Accessibility
```tsx
test('is accessible to screen readers', () => {
render(<AccessibleForm />);
const form = screen.getByRole('form');
const inputs = screen.getAllByRole('textbox');
inputs.forEach(input => {
expect(input).toHaveAttribute('aria-label');
});
expect(form).toHaveAttribute('aria-describedby');
});
```