Skip to content

Commit 4789b79

Browse files
committed
Add writing-tests-guidelines.md (#612)
1 parent a7d1e91 commit 4789b79

File tree

2 files changed

+294
-1
lines changed

2 files changed

+294
-1
lines changed

mkdocs.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,9 @@ nav:
142142
- Translations: 'docs/customize/translations.md'
143143
- Contribute:
144144
- General Guidelines: 'docs/contribute/general-guidelines.md'
145-
- Testing Guidelines: 'docs/contribute/testing-guidelines.md'
145+
- Testing Guidelines:
146+
- Testing: 'docs/contribute/testing-guidelines.md'
147+
- Writing Tests: 'docs/contribute/writing-tests-guidelines.md'
146148
- API Guidelines: 'docs/contribute/api.md'
147149
- Composition: 'docs/contribute/composition.md'
148150
- CSS Guidelines: 'docs/contribute/css.md'
Lines changed: 291 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,291 @@
1+
# Playwright Testing Structure
2+
3+
This document outlines the structure and guidelines for writing Playwright
4+
tests, ensuring consistency and maintainability throughout the codebase.
5+
6+
## Folder Structure
7+
8+
Playwright tests are organized into multiple files and folders, each
9+
serving a specific purpose.
10+
11+
The complete component structure should be as follows:
12+
13+
```text
14+
<ComponentName>/
15+
├── __tests__/
16+
│ ├── <ComponentName>.spec.tsx-snapshots/
17+
│ ├── _propTests/
18+
│ │ ├── <propTestName>.ts
19+
│ ├── <ComponentName>.spec.tsx
20+
│ ├── <ComponentName>.story.tsx
21+
├── ...rest component files
22+
```
23+
24+
- `<ComponentName>` - Root folder of the component, named after
25+
the component itself.
26+
- `<propTestName>.ts` - Defines local test property combinations used only
27+
within the context of the tested component. Global tests shared across
28+
the project are located in `tests/playwright/propTests`.
29+
- `<ComponentName>.spec.tsx` - Contains all tests, structured as
30+
described below.
31+
- `<ComponentName>.story.tsx` - Includes all component definitions used
32+
in tests. These components should be functional without requiring any
33+
properties to be passed from the tests.
34+
35+
## File Structure of `<ComponentName>.spec.tsx`
36+
37+
Playwright tests follow a structured format to ensure readability
38+
and scalability. Each displayed level represents a `test.describe(...)` block.
39+
The structure consists of:
40+
41+
```text
42+
<ComponentName>/
43+
├── base/
44+
│ ├── visual/
45+
│ │ ├── fullPage/
46+
│ ├── non-visual/
47+
│ ├── functionality/
48+
├── formLayout/
49+
│ ├── visual/
50+
│ ├── non-visual/
51+
│ ├── functionality/
52+
```
53+
54+
- `<ComponentName>` - Groups all tests for the tested component.
55+
- `base` - Contains component tests without any additional layout.
56+
- `visual` - Tests that compare the component state against snapshots.
57+
- `fullPage` - Subgroup of visual tests that must be performed
58+
on a full-scale page.
59+
- `non-visual` - Validates non-functional properties (e.g., `id` or `ref`).
60+
- `functionality` - Validates properties that affect the component's behavior
61+
(e.g., `onChange`).
62+
- `formLayout` - Contains tests for the component wrapped in `<FormLayout/>`.
63+
64+
Test block categories can be expanded or removed depending on the nature
65+
of the tested component and whether a predefined test block is applicable
66+
in a specific case.
67+
68+
## File Structure of `<ComponentName>.story.tsx`
69+
70+
The `<ComponentName>.story.tsx` file should include all component variants
71+
tested in `<ComponentName>.spec.tsx`. Components should be organized
72+
in the following order:
73+
74+
1. Component for normal tests (`<ComponentName>ForTest`)
75+
2. Component for `ref` attribute tests (`<ComponentName>ForRefTest`)
76+
3. Component for layout tests (`<ComponentName>ForLayoutTest`)
77+
4. Components for other type of tests that follow conventions above.
78+
79+
## Anatomy of Test Case
80+
81+
Each test case should follow the properties defined in the `PropTest` type.
82+
This type includes the properties `name`, `onBeforeTest`, `onBeforeSnapshot`,
83+
and `props`, which define the component setup for the actual test case.
84+
85+
- `name` - The name of the test case, following the naming conventions
86+
described in the next chapter.
87+
- `onBeforeTest` - A function called before the test and component render.
88+
It should perform any environment tweaks necessary for the defined test.
89+
- `onBeforeSnapshot` - A function called after the component is rendered
90+
and before its comparison against the snapshot.
91+
- `props` - The properties passed to the component in the defined
92+
test scenario.
93+
94+
## Formatting and Code Style
95+
96+
### Rules
97+
98+
- Test for the default component properties should always be placed first.
99+
This test is always represented by `propTests.defaultComponentPropTest`,
100+
defined in the global `propTests`.
101+
102+
- When possible, try to re-use globally defined `propTests`
103+
for visual tests, located in `tests/playwright/propTests`.
104+
105+
- It is essential to test all combinations of props that have a significant
106+
visual impact on the appearance of the component.
107+
108+
- For all possible combinations of multiple `propTests` should be used
109+
function `mixPropTests`.
110+
111+
### Format
112+
113+
- Prop test variants should be sorted alphabetically. If there are multiple
114+
prop tests like `feedbackColor`, `neutralColor`, etc., they should still be
115+
ordered alphabetically under the category of color.
116+
117+
```jsx
118+
test.describe('blockName', () => {
119+
[
120+
...propTests.aPropTest,
121+
...propTests.bPropTest,
122+
...propTests.cPropTest,
123+
].forEach(({
124+
name,
125+
onBeforeTest,
126+
onBeforeSnapshot,
127+
props,
128+
}) => {
129+
// Rest of test setup.
130+
});
131+
});
132+
133+
```
134+
135+
- Naming convention for propTests `name` property should follow this pattern:
136+
137+
```text
138+
someProp:string
139+
someProp:bool=true
140+
someProp:bool=false
141+
someProp:shape[flat]
142+
someProp:shape[nested]
143+
```
144+
145+
## Templates
146+
147+
### Template for `<ComponentName>.story.tsx`
148+
149+
```tsx
150+
import React from 'react';
151+
import type { HTMLAttributes } from 'react';
152+
import { ComponentName } from '..';
153+
154+
// Types for story component will be improved when we have full TypeScript support
155+
type ComponentForTestProps = HTMLAttributes<HTMLDivElement>;
156+
type ComponentForRefTestProps = ComponentForTestProps & {
157+
testRefAttrName: string;
158+
testRefAttrValue: string;
159+
};
160+
161+
export const ComponentForTest = ({
162+
...props
163+
} : ComponentForTestProps) => (
164+
<ComponentName
165+
requiredPropA="value-a"
166+
requiredPropB="value-b"
167+
{...props}
168+
/>
169+
);
170+
171+
// Story for `ref` prop, if applicable
172+
export const ComponentForRefTest = ({
173+
testRefAttrName,
174+
testRefAttrValue,
175+
...props
176+
} : ComponentForRefTestProps) => {
177+
const ref = useRef<HTMLDivElement>(undefined);
178+
179+
useEffect(() => {
180+
ref.current?.setAttribute(testRefAttrName, testRefAttrValue);
181+
}, [testRefAttrName, testRefAttrValue]);
182+
183+
return (
184+
<Component
185+
{...props}
186+
ref={ref}
187+
/>
188+
);
189+
};
190+
191+
```
192+
193+
### Template for `<ComponentName>.spec.tsx`
194+
195+
```tsx
196+
import React from 'react';
197+
import {
198+
expect,
199+
test,
200+
} from '@playwright/experimental-ct-react';
201+
import { propTests } from '../../../../tests/playwright';
202+
import { ComponentNameForTest } from './ComponentName.story';
203+
204+
test.describe('ComponentName', () => {
205+
test.describe('base', () => {
206+
test.describe('visual', () => {
207+
[
208+
...propTests.defaultComponentPropTest,
209+
// ...propTests.propTestA,
210+
// ...mixPropTests([
211+
// ...propTests.propTestX,
212+
// ...propTests.propTestY,
213+
// ]),
214+
].forEach(({
215+
name,
216+
onBeforeTest,
217+
onBeforeSnapshot,
218+
props,
219+
}) => {
220+
test(name, async ({
221+
mount,
222+
page,
223+
}) => {
224+
if (onBeforeTest) {
225+
await onBeforeTest(page);
226+
}
227+
228+
const component = await mount(
229+
<ComponentNameForTest
230+
{...props}
231+
/>,
232+
);
233+
234+
if (onBeforeSnapshot) {
235+
await onBeforeSnapshot(page, component);
236+
}
237+
238+
const screenshot = await component.screenshot();
239+
expect(screenshot).toMatchSnapshot();
240+
});
241+
});
242+
});
243+
244+
test.describe('non-visual', () => {
245+
// Test for `id` prop, if applicable
246+
test('id', async ({ mount }) => {
247+
const component = await mount(
248+
<ComponentForTest
249+
id="test-id"
250+
/>,
251+
);
252+
253+
await expect(component).toHaveAttribute('id', 'test-id');
254+
// Test the rest of internal IDs
255+
});
256+
257+
// Test for `ref` prop, if applicable
258+
test('ref', async ({ mount }) => {
259+
const component = await mount(
260+
<ComponentForRefTest
261+
testRefAttrName="test-ref"
262+
testRefAttrValue="test-ref-value"
263+
/>,
264+
);
265+
266+
await expect(component).toHaveAttribute('test-ref', 'test-ref-value');
267+
});
268+
269+
// Other non-visual tests
270+
});
271+
272+
test.describe('functionality', () => {
273+
// Functional tests
274+
});
275+
});
276+
277+
test.describe('formLayout', () => {
278+
test.describe('visual', () => {
279+
// Visual tests as in `base` block
280+
});
281+
282+
test.describe('non-visual', () => {
283+
// Non-visual tests as in `base` block
284+
});
285+
286+
test.describe('functionality', () => {
287+
// Functional tests as in `base` block
288+
});
289+
});
290+
});
291+
```

0 commit comments

Comments
 (0)