mirror of
https://github.com/apache/superset.git
synced 2026-04-19 08:04:53 +00:00
feat(docs): Populate Developer Portal with comprehensive documentation framework (#35217)
Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,258 @@
|
||||
---
|
||||
title: Component Style Guidelines and Best Practices
|
||||
sidebar_position: 1
|
||||
---
|
||||
|
||||
<!--
|
||||
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.
|
||||
-->
|
||||
|
||||
# Component Style Guidelines and Best Practices
|
||||
|
||||
This documentation illustrates how we approach component development in Superset and provides examples to help you in writing new components or updating existing ones by following our community-approved standards.
|
||||
|
||||
This guide is intended primarily for reusable components. Whenever possible, all new components should be designed with reusability in mind.
|
||||
|
||||
## General Guidelines
|
||||
|
||||
- We use [Ant Design](https://ant.design/) as our component library. Do not build a new component if Ant Design provides one but rather instead extend or customize what the library provides
|
||||
- Always style your component using Emotion and always prefer the theme variables whenever applicable. See: [Emotion Styling Guidelines and Best Practices](./emotion-styling-guidelines)
|
||||
- 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
|
||||
|
||||
```
|
||||
superset-frontend/src/components
|
||||
{ComponentName}/
|
||||
index.tsx
|
||||
{ComponentName}.test.tsx
|
||||
{ComponentName}.stories.tsx
|
||||
```
|
||||
|
||||
**Components root directory:** Components that are meant to be re-used across different parts of the application should go in the `superset-frontend/src/components` directory. Components that are meant to be specific for a single part of the application should be located in the nearest directory where the component is used, for example, `superset-frontend/src/Explore/components`
|
||||
|
||||
**Exporting the component:** All components within the `superset-frontend/src/components` directory should be exported from `superset-frontend/src/components/index.ts` to facilitate their imports by other 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
|
||||
|
||||
**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
|
||||
|
||||
## Component Development Best Practices
|
||||
|
||||
### 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
|
||||
};
|
||||
```
|
||||
|
||||
### Prefer Functional Components
|
||||
|
||||
Use functional components with hooks instead of class components:
|
||||
|
||||
```tsx
|
||||
// ✅ Good - Functional component with hooks
|
||||
export const MyComponent: React.FC<Props> = ({ data }) => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
// Effect logic
|
||||
}, []);
|
||||
|
||||
return <div>{/* Component JSX */}</div>;
|
||||
};
|
||||
|
||||
// ❌ Avoid - Class component
|
||||
class MyComponent extends React.Component {
|
||||
// Class implementation
|
||||
}
|
||||
```
|
||||
|
||||
### Follow Ant Design Patterns
|
||||
|
||||
Extend Ant Design components rather than building from scratch:
|
||||
|
||||
```tsx
|
||||
import { Button } from 'antd';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
const StyledButton = styled(Button)`
|
||||
// Custom styling using emotion
|
||||
`;
|
||||
```
|
||||
|
||||
### Reusability and Props Design
|
||||
|
||||
Design components with reusability in mind:
|
||||
|
||||
```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
|
||||
};
|
||||
```
|
||||
|
||||
## Testing Components
|
||||
|
||||
Every component should include comprehensive tests:
|
||||
|
||||
```tsx
|
||||
// MyComponent.test.tsx
|
||||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
import { MyComponent } from './MyComponent';
|
||||
|
||||
test('renders component with title', () => {
|
||||
render(<MyComponent title="Test Title" />);
|
||||
expect(screen.getByText('Test Title')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('calls onClose when close button is clicked', () => {
|
||||
const mockOnClose = jest.fn();
|
||||
render(<MyComponent title="Test" onClose={mockOnClose} />);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /close/i }));
|
||||
expect(mockOnClose).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
```
|
||||
|
||||
## Storybook Stories
|
||||
|
||||
Create stories for visual testing and documentation:
|
||||
|
||||
```tsx
|
||||
// MyComponent.stories.tsx
|
||||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { MyComponent } from './MyComponent';
|
||||
|
||||
const meta: Meta<typeof MyComponent> = {
|
||||
title: 'Components/MyComponent',
|
||||
component: MyComponent,
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
},
|
||||
tags: ['autodocs'],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
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>
|
||||
);
|
||||
};
|
||||
```
|
||||
@@ -0,0 +1,346 @@
|
||||
---
|
||||
title: Emotion Styling Guidelines and Best Practices
|
||||
sidebar_position: 2
|
||||
---
|
||||
|
||||
<!--
|
||||
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.
|
||||
-->
|
||||
|
||||
# Emotion Styling Guidelines and Best Practices
|
||||
|
||||
## Emotion Styling Guidelines
|
||||
|
||||
### DO these things:
|
||||
|
||||
- **DO** use `styled` when you want to include additional (nested) class selectors in your styles
|
||||
- **DO** use `styled` components when you intend to export a styled component for re-use elsewhere
|
||||
- **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
|
||||
|
||||
### 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
|
||||
|
||||
## When to use `css` or `styled`
|
||||
|
||||
### Use `css` for:
|
||||
|
||||
1. **Small, single-use styles**
|
||||
```tsx
|
||||
import { css } from '@emotion/react';
|
||||
|
||||
const MyComponent = () => (
|
||||
<div
|
||||
css={css`
|
||||
margin: 8px;
|
||||
padding: 16px;
|
||||
`}
|
||||
>
|
||||
Content
|
||||
</div>
|
||||
);
|
||||
```
|
||||
|
||||
2. **Composing styles**
|
||||
```tsx
|
||||
const baseStyles = css`
|
||||
padding: 16px;
|
||||
border-radius: 4px;
|
||||
`;
|
||||
|
||||
const primaryStyles = css`
|
||||
${baseStyles}
|
||||
background-color: blue;
|
||||
color: white;
|
||||
`;
|
||||
|
||||
const secondaryStyles = css`
|
||||
${baseStyles}
|
||||
background-color: gray;
|
||||
color: black;
|
||||
`;
|
||||
```
|
||||
|
||||
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};
|
||||
}
|
||||
`;
|
||||
```
|
||||
|
||||
2. **Components with complex nested selectors**
|
||||
```tsx
|
||||
const StyledCard = styled.div`
|
||||
padding: 16px;
|
||||
border: 1px solid ${({ theme }) => theme.colors.border};
|
||||
|
||||
.card-header {
|
||||
font-weight: bold;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.card-content {
|
||||
color: ${({ theme }) => theme.colors.text};
|
||||
|
||||
p {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
}
|
||||
`;
|
||||
```
|
||||
|
||||
3. **Extending Ant Design components**
|
||||
```tsx
|
||||
import { Button } from 'antd';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
const CustomButton = styled(Button)`
|
||||
border-radius: 8px;
|
||||
font-weight: 600;
|
||||
|
||||
&.ant-btn-primary {
|
||||
background: linear-gradient(45deg, #1890ff, #722ed1);
|
||||
}
|
||||
`;
|
||||
```
|
||||
|
||||
## Using Theme Variables
|
||||
|
||||
Always use theme variables for consistent styling:
|
||||
|
||||
```tsx
|
||||
import { useTheme } from '@emotion/react';
|
||||
|
||||
const MyComponent = () => {
|
||||
const theme = useTheme();
|
||||
|
||||
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
|
||||
</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};
|
||||
|
||||
${({ size }) => {
|
||||
switch (size) {
|
||||
case 'small':
|
||||
return css`font-size: 12px;`;
|
||||
case 'large':
|
||||
return css`font-size: 18px;`;
|
||||
default:
|
||||
return css`font-size: 14px;`;
|
||||
}
|
||||
}}
|
||||
`;
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 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;
|
||||
`;
|
||||
|
||||
// ❌ Avoid - magic numbers
|
||||
const Header = styled.h1`
|
||||
font-size: 24px;
|
||||
margin-bottom: 16px;
|
||||
`;
|
||||
```
|
||||
|
||||
### 2. Group Related Styles
|
||||
```tsx
|
||||
// ✅ Good - grouped styles
|
||||
const Card = styled.div`
|
||||
/* Layout */
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: ${({ theme }) => theme.gridUnit * 4}px;
|
||||
|
||||
/* Appearance */
|
||||
background-color: ${({ theme }) => theme.colors.grayscale.light5};
|
||||
border: 1px solid ${({ theme }) => theme.colors.grayscale.light2};
|
||||
border-radius: ${({ theme }) => theme.borderRadius}px;
|
||||
|
||||
/* Typography */
|
||||
font-family: ${({ theme }) => theme.typography.families.sansSerif};
|
||||
color: ${({ theme }) => theme.colors.grayscale.dark2};
|
||||
`;
|
||||
```
|
||||
|
||||
### 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%
|
||||
);
|
||||
`;
|
||||
|
||||
const GradientButton = styled.button`
|
||||
${complexGradient}
|
||||
padding: 12px 24px;
|
||||
border: none;
|
||||
color: white;
|
||||
`;
|
||||
```
|
||||
|
||||
### 4. Avoid Deep Nesting
|
||||
```tsx
|
||||
// ✅ Good - shallow nesting
|
||||
const Navigation = styled.nav`
|
||||
.nav-item {
|
||||
padding: 8px 16px;
|
||||
}
|
||||
|
||||
.nav-link {
|
||||
color: ${({ theme }) => theme.colors.text};
|
||||
text-decoration: none;
|
||||
}
|
||||
`;
|
||||
|
||||
// ❌ 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
|
||||
});
|
||||
|
||||
const DynamicComponent = (props: Props) => (
|
||||
<div css={dynamicStyles(props)}>
|
||||
Content
|
||||
</div>
|
||||
);
|
||||
```
|
||||
297
docs/developer_portal/guidelines/frontend/testing-guidelines.md
Normal file
297
docs/developer_portal/guidelines/frontend/testing-guidelines.md
Normal file
@@ -0,0 +1,297 @@
|
||||
---
|
||||
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');
|
||||
});
|
||||
```
|
||||
Reference in New Issue
Block a user