|
| 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