Skip to content

Commit 8c22c04

Browse files
wd055denis-vlasov
andauthored
add playwright support (#19)
* add playwright support Co-authored-by: denis-vlasov <denis-vlasov@yandex-team.ru>
1 parent 6d29d80 commit 8c22c04

9 files changed

Lines changed: 271 additions & 70 deletions

File tree

README.md

Lines changed: 65 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,32 @@ trees:
159159

160160
## Автоматическое определение признака automationState
161161

162-
Вместе с информацией о функциональных требованиях можно выгружать информацию о том, что их проверка автоматизирована. Для некоторых видов автотестов реализовано автоматическое вычисление признака `automationState`. На текущий момент поддерживаются `jest` и `storybook`.
162+
Вместе с информацией о функциональных требованиях можно выгружать информацию о том, что их проверка автоматизирована. Для некоторых видов автотестов реализовано автоматическое вычисление признака `automationState` на основе json отчета, полученного из инструмента тестирования. На текущий момент поддерживаются `storybook`, `jest`, `testplane` и `playwright`. Для каждого автоматизированного теста и теста из YML конфига формируется идентификатор для их сопастовления.
163+
164+
#### Идентификаторы тестов
165+
166+
Для каждого ФТ, описанного в yml файлах проекта, будет сформирован уникальный идентификатор на основе поля `"keys"`. Если в отчете есть тест, полное имя которого совпадает с идентификатором ФТ, то считается, что проверка этого ФТ — автоматизирована.
167+
168+
Например, если в проекте есть yml файл с содержимым, [указанным выше](#формат-yml) и поле `"keys"` в разделе `"storybook"`, `"jest"`, `"testplane"` или `"playwright"` конфигурационного файла имеет значение `["featureTitle", "groupTitle", "assertionTitle"]`, то указанная ниже история будет сопоставлена с ФТ `"Отображается количество и общая стоимость товаров в корзине"`:
169+
170+
```js
171+
describe('Главная страница', () => {
172+
describe('Блок корзины', () => {
173+
it('Отображается количество и общая стоимость товаров в корзине', async ({ browser }) => {
174+
// ...
175+
});
176+
});
177+
});
178+
```
179+
В качестве частей идентификатора допустимо указывать следующие значения:
180+
- `"featureTitle"` — название фичи
181+
- `"featureCode"` — уникальный код фичи
182+
- `"groupTitle"` — название группы ФТ
183+
- `"assertionTitle"` — название ФТ
184+
- `"filePath"` — путь к yml файлу, относительно корня проекта
185+
- `"fileName"` — название yml файла без расширения (от начала до первого символа `.`)
186+
- `@<attribute>` — значение указанного атрибута (идентификатор значения)
187+
- `$<attribute>` — значение указанного атрибута (человеко-понятное название)
163188

164189
### Jest
165190

@@ -183,30 +208,41 @@ jest --json --outputFile=jest-report.json
183208
}
184209
```
185210

186-
Для каждого ФТ, описанного в yml файлах проекта, будет сформирован уникальный идентификатор на основе поля `"keys"`. Если в отчете jest есть тест, полное имя которого совпадает с идентификатором ФТ, то считается, что проверка этого ФТ — автоматизирована.
211+
### Testplane
187212

188-
Например, если в проекте есть yml файл с содержимым, [указанным выше](#формат-yml) и поле `"keys"` в разделе `"jest"` конфигурационного файла имеет значение `["featureTitle", "groupTitle", "assertionTitle"]`, то указанный ниже тест будет сопоставлен с ФТ `"Отображается количество и общая стоимость товаров в корзине"`:
213+
Отчет о выполнении тестов можно сформировать, запустив testplane с плагином [json-reporter](https://www.npmjs.com/package/json-reporter)
214+
215+
Чтобы добавить в выгрузку ФТ информацию из отчета testplane, добавьте в корень [конфигурационного файла](#формат-конфига) секцию `"testplane"`:
189216

190217
```js
191-
describe('Главная страница', () => {
192-
describe('Блок корзины', () => {
193-
test('Отображается количество и общая стоимость товаров в корзине', () => {
194-
// ...
195-
});
196-
});
197-
});
218+
{
219+
// ...
220+
"testplane": {
221+
// путь к файлу с отчетом о выполнении тестов
222+
"reportPath": "testplane-report.json",
223+
224+
// сегменты идентификатора для сопоставления автотестов с ФТ
225+
"keys": ["featureTitle", "groupTitle", "assertionTitle"]
226+
}
198227
```
199228

200-
В качестве частей идентификатора допустимо указывать следующие значения:
229+
### Playwright
201230

202-
- `"featureTitle"` — название фичи
203-
- `"featureCode"` — уникальный код фичи
204-
- `"groupTitle"` — название группы ФТ
205-
- `"assertionTitle"` — название ФТ
206-
- `"filePath"` — путь к yml файлу, относительно корня проекта
207-
- `"fileName"` — название yml файла без расширения (от начала до первого символа `.`)
208-
- `@<attribute>` — значение указанного атрибута (идентификатор значения)
209-
- `$<attribute>` — значение указанного атрибута (человеко-понятное название)
231+
Отчет о выполнении тестов можно сформировать, запустив playwright с [json отчетом](https://playwright.dev/docs/test-reporters#json-reporter)
232+
233+
Чтобы добавить в выгрузку ФТ информацию из отчета playwright, добавьте в корень [конфигурационного файла](#формат-конфига) секцию `"playwright"`:
234+
235+
```js
236+
{
237+
// ...
238+
"playwright": {
239+
// путь к файлу с отчетом о выполнении тестов
240+
"reportPath": "playwright-report.json",
241+
242+
// сегменты идентификатора для сопоставления автотестов с ФТ
243+
"keys": ["featureTitle", "groupTitle", "assertionTitle"]
244+
}
245+
```
210246

211247
### Storybook
212248

@@ -226,10 +262,6 @@ describe('Главная страница', () => {
226262
}
227263
```
228264

229-
Поле `"keys"` работает таким же образом, как и для [конфигурации jest](#jest). Если в `index.json` есть стори, путь до которой в дереве сторей совпадает с идентификатором ФТ, то считается, что проверка этого ФТ — автоматизирована.
230-
231-
Например, если в проекте есть yml файл с содержимым, [указанным выше](#формат-yml) и поле `"keys"` в разделе `"storybook"` конфигурационного файла имеет значение `["featureTitle", "groupTitle", "assertionTitle"]`, то указанная ниже история будет сопоставлена с ФТ `"Отображается количество и общая стоимость товаров в корзине"`:
232-
233265
```js
234266
import type { Meta, StoryObj } from '@storybook/react';
235267
import { Cart } from './Cart';
@@ -247,49 +279,6 @@ export const Default: StoryObj<typeof Cart> = {
247279

248280
Если вы укажете в настройках поле `publicUrl`, то для каждого ФТ, для которого в сторибуке написана история, будет прикреплена ссылка на эту историю.
249281

250-
### Testplane
251-
252-
Отчет о выполнении тестов можно сформировать, запустив testplane с плагином [json-reporter](https://www.npmjs.com/package/json-reporter)
253-
254-
Чтобы добавить в выгрузку ФТ информацию из отчета testplane, добавьте в корень [конфигурационного файла](#формат-конфига) секцию `"testplane"`:
255-
256-
```js
257-
{
258-
// ...
259-
"testplane": {
260-
// путь к файлу с отчетом о выполнении тестов
261-
"reportPath": "testplane-report.json",
262-
263-
// сегменты идентификатора для сопоставления автотестов с ФТ
264-
"keys": ["featureTitle", "groupTitle", "assertionTitle"]
265-
}
266-
```
267-
268-
Для каждого ФТ, описанного в yml файлах проекта, будет сформирован уникальный идентификатор на основе поля `"keys"`. Если в отчете testplane есть тест, полное имя которого совпадает с идентификатором ФТ, то считается, что проверка этого ФТ — автоматизирована.
269-
270-
Например, если в проекте есть yml файл с содержимым, [указанным выше](#формат-yml) и поле `"keys"` в разделе `"testplane"` конфигурационного файла имеет значение `["featureTitle", "groupTitle", "assertionTitle"]`, то указанный ниже тест будет сопоставлен с ФТ `"Отображается количество и общая стоимость товаров в корзине"`:
271-
272-
```js
273-
describe('Главная страница', () => {
274-
describe('Блок корзины', () => {
275-
it('Отображается количество и общая стоимость товаров в корзине', async ({ browser }) => {
276-
// ...
277-
});
278-
});
279-
});
280-
```
281-
282-
В качестве частей идентификатора допустимо указывать следующие значения:
283-
284-
- `"featureTitle"` — название фичи
285-
- `"featureCode"` — уникальный код фичи
286-
- `"groupTitle"` — название группы ФТ
287-
- `"assertionTitle"` — название ФТ
288-
- `"filePath"` — путь к yml файлу, относительно корня проекта
289-
- `"fileName"` — название yml файла без расширения (от начала до первого символа `.`)
290-
- `@<attribute>` — значение указанного атрибута (идентификатор значения)
291-
- `$<attribute>` — значение указанного атрибута (человеко-понятное название)
292-
293282
## Формат конфига
294283

295284
Ниже указаны все возможные параметры конфигурационного файла:
@@ -351,5 +340,15 @@ describe('Главная страница', () => {
351340
"assertionTitle"
352341
]
353342
}
343+
// настройки для сопоставления ФТ с отчетами playwright
344+
"playwright": {
345+
"reportPath": "playwright-report.json", // путь к файлу с отчетом о выполнении тестов
346+
"keys": [ // сегменты идентификатора для сопоставления автотестов с ФТ
347+
"$component"
348+
"featureTitle",
349+
"groupTitle",
350+
"assertionTitle"
351+
]
352+
}
354353
}
355354
```

src/commands/sync.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { CommonOptions } from '../lib/utils';
1010
import { Validator } from '../lib/validators';
1111
import { loadStorybookIndex, applyStorybookIndex } from '../lib/storybook';
1212
import { applyTestplaneReport, loadTestplaneReport } from '../lib/testplane';
13+
import { applyPlaywrightReport, loadPlaywrightReport } from '../lib/playwright';
1314

1415
export interface SyncOptions extends CommonOptions {
1516
message?: string;
@@ -19,7 +20,16 @@ export const cmdSync: CommandModule<{}, SyncOptions> = {
1920
command: 'sync',
2021
handler: async (args) => {
2122
console.log('SYNC');
22-
const { yml, api, jest, storybook, testplane, validation = {}, projectPath } = await loadConfig(args.config);
23+
const {
24+
yml,
25+
api,
26+
jest,
27+
storybook,
28+
testplane,
29+
playwright,
30+
validation = {},
31+
projectPath,
32+
} = await loadConfig(args.config);
2333
const validationContext = new Validator(validation);
2434

2535
const meta = await loadMeta(validationContext, yml.metaPath, projectPath);
@@ -50,6 +60,12 @@ export const cmdSync: CommandModule<{}, SyncOptions> = {
5060
applyTestplaneReport(validationContext, projectData, testplaneReport, testplane.keys);
5161
}
5262

63+
if (playwright) {
64+
const playwrightReport = await loadPlaywrightReport(playwright.reportPath, projectPath);
65+
66+
applyPlaywrightReport(validationContext, projectData, playwrightReport, playwright.keys);
67+
}
68+
5369
validationContext.printReport();
5470
if (validationContext.hasCriticalErrors) {
5571
throw Error('Выгрузка невозможна из-за наличия критических ошибок');

src/commands/validate-only.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,14 @@ import { CommonOptions } from '../lib/utils';
99
import { Validator } from '../lib/validators';
1010
import { applyStorybookIndex, loadStorybookIndex } from '../lib/storybook';
1111
import { applyTestplaneReport, loadTestplaneReport } from '../lib/testplane';
12+
import { applyPlaywrightReport, loadPlaywrightReport } from '../lib/playwright';
1213

1314
export const cmdValidateOnly: CommandModule<{}, CommonOptions> = {
1415
command: 'validate',
1516
handler: async (args) => {
1617
console.log('VALIDATION');
1718

18-
const { yml, jest, storybook, testplane, validation = {}, projectPath } = await loadConfig(args.config);
19+
const { yml, jest, storybook, testplane, playwright, validation = {}, projectPath } = await loadConfig(args.config);
1920
const validationContext = new Validator(validation);
2021
const meta = await loadMeta(validationContext, yml.metaPath, projectPath);
2122

@@ -47,6 +48,12 @@ export const cmdValidateOnly: CommandModule<{}, CommonOptions> = {
4748
applyTestplaneReport(validationContext, projectData, testplaneReport, testplane.keys);
4849
}
4950

51+
if (playwright) {
52+
const playwrightReport = await loadPlaywrightReport(playwright.reportPath, projectPath);
53+
54+
applyPlaywrightReport(validationContext, projectData, playwrightReport, playwright.keys);
55+
}
56+
5057
validationContext.printReport();
5158
if (validationContext.hasCriticalErrors) {
5259
throw Error('При валидации были обнаружены критические ошибки');

src/lib/config/models.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,11 @@ export const testplaneConfigDecoder = d.struct({
9292
keys: d.array(d.union(literalKeyPartDecoder, attributeKeyPartDecoder)),
9393
});
9494

95+
export const playwrightConfigDecoder = d.struct({
96+
reportPath: d.string,
97+
keys: d.array(d.union(literalKeyPartDecoder, attributeKeyPartDecoder)),
98+
});
99+
95100
export const configDecoder = d.intersect(
96101
d.struct({
97102
api: apiConfigDecoder,
@@ -104,6 +109,7 @@ export const configDecoder = d.intersect(
104109
jest: jestConfigDecoder,
105110
storybook: storybookConfigDecoder,
106111
testplane: testplaneConfigDecoder,
112+
playwright: playwrightConfigDecoder,
107113
}),
108114
);
109115

@@ -113,6 +119,7 @@ export type YmlConfig = d.TypeOf<typeof ymlConfigDecoder>;
113119
export type JestConfig = d.TypeOf<typeof jestConfigDecoder>;
114120
export type StorybookConfig = d.TypeOf<typeof storybookConfigDecoder>;
115121
export type TestplaneConfig = d.TypeOf<typeof testplaneConfigDecoder>;
122+
export type PlaywrightConfig = d.TypeOf<typeof playwrightConfigDecoder>;
116123
export type ValidationConfig = d.TypeOf<typeof validationConfigDecoder>;
117124
export type ValidationSeverity = d.TypeOf<typeof validationSeverityDecoder>;
118125

src/lib/playwright/index.ts

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import { ProjectData, getAssertionContext, getAttributesContext, getKey } from '../domain';
2+
import { AutomationState } from '../domain/models';
3+
import { parseObject, readTextFile } from '../utils';
4+
import { Validator } from '../validators';
5+
import { PlaywrightReport, playwrightReportDecoder } from './models';
6+
7+
export const getFullName = (...parts: string[]) => parts.join(' / ');
8+
9+
export const applyPlaywrightReport = (
10+
validationContext: Validator,
11+
{ features, attributes }: ProjectData,
12+
report: PlaywrightReport,
13+
keyParts: string[],
14+
) => {
15+
const names = new Map<string, string[]>();
16+
17+
const state = new Map<string, AutomationState>();
18+
19+
const calcReverse = (prevSuites: PlaywrightReport['suites'], paths: string[]) => {
20+
for (let { file: path, suites, specs, title } of prevSuites) {
21+
if (suites) {
22+
calcReverse(suites, [...paths, title]);
23+
}
24+
25+
if (!specs.length) {
26+
continue;
27+
}
28+
29+
// На первом уровне playwrite-report в title название файла,
30+
// которого не должно быть в названии теста.
31+
// Поэтому име генерируем со второго элемента
32+
const parts = paths.concat(title).slice(1);
33+
for (let { tests, title: testTitle } of specs) {
34+
const name = getFullName(...parts, testTitle);
35+
36+
// Добавление в мапу для ошибки тестов без описания
37+
const pathes = names.get(name) || [];
38+
pathes.push(path);
39+
names.set(name, pathes);
40+
41+
const automated = tests.find(({ expectedStatus }) => expectedStatus == 'passed');
42+
43+
if (automated) {
44+
state.set(name, 'Automated');
45+
} else {
46+
state.set(name, 'Problem');
47+
}
48+
}
49+
}
50+
};
51+
52+
calcReverse(report.suites, []);
53+
54+
const attributesCtx = getAttributesContext(attributes);
55+
56+
// заполняем поле isAutomated
57+
for (let feature of features) {
58+
for (let group of feature.groups || []) {
59+
for (let assertion of group.assertions || []) {
60+
const assertionCtx = getAssertionContext(feature, group, assertion);
61+
62+
const parts = getKey(keyParts, assertionCtx, attributesCtx);
63+
const fullName = getFullName(...parts);
64+
65+
const automationState = state.get(fullName);
66+
if (automationState) {
67+
assertion.automationState = automationState;
68+
}
69+
70+
names.delete(fullName);
71+
}
72+
}
73+
}
74+
Array.from(names.keys()).forEach((name) => {
75+
const pathes = names.get(name);
76+
pathes?.forEach((path) => validationContext.registerPlaywrightUnusedTests(name, path));
77+
});
78+
};
79+
80+
export const loadPlaywrightReport = async (path: string, basePath?: string) => {
81+
const json = await readTextFile(path, basePath);
82+
const data: unknown = JSON.parse(json);
83+
84+
const entity = parseObject(data, playwrightReportDecoder);
85+
86+
return entity;
87+
};

0 commit comments

Comments
 (0)