Skip to content

Commit 1103aba

Browse files
sonic16xrocketraccoon
andauthored
feat(new-ui): squash repeated steps (#689)
* feat(new-ui): squash repeated steps * feat(new-ui): squash repeated steps to groups * feat(new-ui): squash repeated steps to groups #1 * feat(new-ui): squash repeated steps to groups #2 * feat(new-ui): squash repeated steps to groups #3 * feat(new-ui): squash repeated steps to groups #4 * feat(new-ui): squash repeated steps to groups #5 * feat(new-ui): squash repeated steps to groups #6 * feat(new-ui): squash repeated steps to groups #7 * feat(new-ui): squash repeated steps to groups #7 * feat(new-ui): squash repeated steps to groups #8 --------- Co-authored-by: rocketraccoon <rocketraccoon@yandex-team.ru>
1 parent b6bb191 commit 1103aba

File tree

19 files changed

+428
-101
lines changed

19 files changed

+428
-101
lines changed

lib/adapters/test-collection/testplane.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,23 @@
11
import {TestplaneTestAdapter} from '../test/testplane';
22
import type {TestCollectionAdapter} from './';
3-
import type {TestCollection} from 'testplane';
3+
import type {Config, TestCollection} from 'testplane';
44

55
export class TestplaneTestCollectionAdapter implements TestCollectionAdapter {
66
private _testCollection: TestCollection;
77
private _testAdapters: TestplaneTestAdapter[];
88

99
static create<T>(
10-
this: new (testCollection: TestCollection) => T,
11-
testCollection: TestCollection
10+
this: new (testCollection: TestCollection, saveHistoryMode?: Config['saveHistoryMode']) => T,
11+
testCollection: TestCollection,
12+
saveHistoryMode?: Config['saveHistoryMode']
1213
): T {
13-
return new this(testCollection);
14+
return new this(testCollection, saveHistoryMode);
1415
}
1516

16-
constructor(testCollection: TestCollection) {
17+
constructor(testCollection: TestCollection, saveHistoryMode?: Config['saveHistoryMode']) {
1718
this._testCollection = testCollection;
18-
this._testAdapters = this._testCollection.mapTests(test => TestplaneTestAdapter.create(test));
19+
20+
this._testAdapters = this._testCollection.mapTests(test => TestplaneTestAdapter.create(test, saveHistoryMode));
1921
}
2022

2123
get original(): TestCollection {
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
import {TestStepKey, TestStepCompressed, TestplaneSuite, TestplaneTestResult} from '../../../types';
2+
import type {Config} from 'testplane';
3+
4+
export const getSkipComment = (suite: TestplaneTestResult | TestplaneSuite): string | null | undefined => (
5+
suite.skipReason || suite.parent && getSkipComment(suite.parent)
6+
);
7+
8+
export const wrapSkipComment = (skipComment: string | null | undefined): string => (
9+
skipComment ?? 'Unknown reason'
10+
);
11+
12+
const testItemsAreTheSame = (a: TestStepCompressed, b: TestStepCompressed): boolean => (
13+
a &&
14+
(!a[TestStepKey.Children] || !b[TestStepKey.Children]) &&
15+
(!a[TestStepKey.IsFailed] || !b[TestStepKey.IsFailed]) &&
16+
a[TestStepKey.Name] === b[TestStepKey.Name] &&
17+
a[TestStepKey.Args].join() === b[TestStepKey.Args].join()
18+
);
19+
20+
const arraysEqual = <I>(a: I[], b: I[], checkFunc: (a: I, b: I) => boolean): boolean => {
21+
if (a.length !== b.length) {
22+
return false;
23+
}
24+
25+
return a.every((val, idx) => checkFunc(val, b[idx]));
26+
};
27+
28+
export const getTotalTime = (items: TestStepCompressed[], start: number, size: number): number => {
29+
let total = 0;
30+
31+
for (let i = start; i < (start + size); i++) {
32+
if (items[i] && items[i][TestStepKey.Duration]) {
33+
total += items[i][TestStepKey.Duration];
34+
}
35+
}
36+
37+
return total;
38+
};
39+
40+
export const getItemAverageTime = (
41+
items: TestStepCompressed[],
42+
start: number,
43+
repeat: number,
44+
index: number,
45+
groupLen: number
46+
): number => {
47+
let total = 0;
48+
49+
for (let i = 0; i < (repeat - 1); i++) {
50+
total += items[start + (i * groupLen) + index][TestStepKey.Duration];
51+
}
52+
53+
return parseFloat((total / (repeat - 1)).toFixed(2));
54+
};
55+
56+
export const MIN_REPEATS = 3; // Min count of repeats elements of group elements for squash
57+
58+
export const collapseRepeatingGroups = (
59+
arr: TestStepCompressed[],
60+
minRepeats: number = MIN_REPEATS
61+
): TestStepCompressed[] => {
62+
const result: TestStepCompressed[] = [];
63+
let i = 0;
64+
65+
while (i < arr.length) {
66+
let foundGroup = false;
67+
68+
// max len of group can't be more that totalLen / minRepeats
69+
for (let groupLen = 1; groupLen <= Math.floor((arr.length - i) / minRepeats); groupLen++) {
70+
const group = arr.slice(i, i + groupLen);
71+
72+
let allGroupsMatch = true;
73+
74+
// check that group is repeated required count of times
75+
for (let repeat = 1; repeat < minRepeats; repeat++) {
76+
const nextGroupStart = i + repeat * groupLen;
77+
const nextGroupEnd = nextGroupStart + groupLen;
78+
79+
if (nextGroupEnd > arr.length) {
80+
allGroupsMatch = false;
81+
break;
82+
}
83+
84+
const nextGroup = arr.slice(nextGroupStart, nextGroupEnd);
85+
86+
if (!arraysEqual(group, nextGroup, testItemsAreTheSame)) {
87+
allGroupsMatch = false;
88+
break;
89+
}
90+
}
91+
92+
if (allGroupsMatch) {
93+
foundGroup = true;
94+
let repeatCount = minRepeats;
95+
96+
// finding another repeats of group
97+
while (
98+
i + groupLen * repeatCount <= arr.length &&
99+
arraysEqual(
100+
group,
101+
arr.slice(i + groupLen * repeatCount, i + groupLen * (repeatCount + 1)),
102+
testItemsAreTheSame
103+
)
104+
) {
105+
repeatCount++;
106+
}
107+
108+
const groupsTotalLen = groupLen * repeatCount;
109+
110+
if (groupLen === 1) {
111+
result.push({
112+
...group[0],
113+
[TestStepKey.Duration]: getTotalTime(arr, i, groupsTotalLen),
114+
[TestStepKey.Repeat]: groupsTotalLen
115+
});
116+
} else {
117+
result.push({
118+
[TestStepKey.Name]: 'Repeated group',
119+
[TestStepKey.Args]: [`${group.length} items`],
120+
[TestStepKey.Duration]: getTotalTime(arr, i, groupsTotalLen),
121+
[TestStepKey.TimeStart]: group[0][TestStepKey.TimeStart],
122+
[TestStepKey.IsFailed]: false,
123+
[TestStepKey.IsGroup]: true,
124+
[TestStepKey.Children]: group.map((item, index) => ({
125+
...item,
126+
[TestStepKey.Repeat]: -1, // -1 need to detect in ui that this is child of group for show ~ in duration
127+
[TestStepKey.Duration]: getItemAverageTime(arr, i, repeatCount, index, groupLen)
128+
})),
129+
[TestStepKey.Repeat]: repeatCount
130+
});
131+
}
132+
133+
i += groupsTotalLen;
134+
break;
135+
}
136+
}
137+
138+
if (!foundGroup) {
139+
result.push(arr[i]);
140+
i++;
141+
}
142+
}
143+
144+
return result;
145+
};
146+
147+
export const getHistory = (
148+
history: TestplaneTestResult['history'] | undefined,
149+
saveHistoryMode: Config['saveHistoryMode'] = 'all'
150+
): TestStepCompressed[] => (
151+
collapseRepeatingGroups(
152+
history?.map((step) => {
153+
const result: TestStepCompressed = {
154+
[TestStepKey.Name]: step[TestStepKey.Name],
155+
[TestStepKey.Args]: step[TestStepKey.Args],
156+
[TestStepKey.Duration]: step[TestStepKey.Duration],
157+
[TestStepKey.TimeStart]: step[TestStepKey.TimeStart],
158+
[TestStepKey.IsFailed]: step[TestStepKey.IsFailed],
159+
[TestStepKey.IsGroup]: step[TestStepKey.IsGroup]
160+
};
161+
162+
if (
163+
step[TestStepKey.Children] && (
164+
(step[TestStepKey.IsGroup] && saveHistoryMode === 'all') ||
165+
(step[TestStepKey.IsFailed] && (saveHistoryMode === 'all' || saveHistoryMode === 'onlyFailed'))
166+
)
167+
) {
168+
result[TestStepKey.Children] = getHistory(step[TestStepKey.Children], saveHistoryMode);
169+
}
170+
171+
return result;
172+
}) ?? [],
173+
MIN_REPEATS
174+
)
175+
);

lib/adapters/test-result/testplane.ts renamed to lib/adapters/test-result/testplane/index.ts

Lines changed: 28 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,10 @@
1-
import path from 'path';
2-
import _ from 'lodash';
1+
import {ERROR, FAIL, SUCCESS, TestStatus, UNKNOWN_SESSION_ID, UPDATED} from '../../../constants';
2+
import {ReporterTestResult} from '../index';
33
import type Testplane from 'testplane';
4-
import type {Test as TestplaneTest} from 'testplane';
5-
import {ValueOf} from 'type-fest';
6-
7-
import {ERROR, FAIL, SUCCESS, TestStatus, UNKNOWN_SESSION_ID, UPDATED} from '../../constants';
8-
import {
9-
getError,
10-
hasUnrelatedToScreenshotsErrors,
11-
isImageDiffError,
12-
isInvalidRefImageError,
13-
isNoRefImageError
14-
} from '../../common-utils';
4+
import type {Test as TestplaneTest, Config} from 'testplane';
155
import {
6+
Attachment,
167
ErrorDetails,
17-
TestplaneSuite,
18-
TestplaneTestResult,
198
ImageBase64,
209
ImageFile,
2110
ImageInfoDiff,
@@ -25,11 +14,24 @@ import {
2514
ImageInfoPageSuccess,
2615
ImageInfoSuccess,
2716
ImageInfoUpdated,
28-
TestError, TestStepCompressed, TestStepKey, Attachment
29-
} from '../../types';
30-
import {ReporterTestResult} from './index';
31-
import {getSuitePath} from '../../plugin-utils';
32-
import {extractErrorDetails} from './utils';
17+
TestError,
18+
TestplaneTestResult,
19+
TestStepCompressed
20+
} from '../../../types';
21+
import _ from 'lodash';
22+
import {
23+
getError,
24+
hasUnrelatedToScreenshotsErrors,
25+
isImageDiffError,
26+
isInvalidRefImageError,
27+
isNoRefImageError
28+
} from '../../../common-utils';
29+
import {getSuitePath} from '../../../plugin-utils';
30+
import {extractErrorDetails} from '../utils';
31+
import path from 'path';
32+
import {ValueOf} from 'type-fest';
33+
34+
import {getHistory, wrapSkipComment, getSkipComment} from './history';
3335

3436
export const getStatus = (eventName: ValueOf<Testplane['events']>, events: Testplane['events'], testResult: TestplaneTestResult): TestStatus => {
3537
if (eventName === events.TEST_PASS) {
@@ -44,37 +46,11 @@ export const getStatus = (eventName: ValueOf<Testplane['events']>, events: Testp
4446
return TestStatus.IDLE;
4547
};
4648

47-
const getSkipComment = (suite: TestplaneTestResult | TestplaneSuite): string | null | undefined => {
48-
return suite.skipReason || suite.parent && getSkipComment(suite.parent);
49-
};
50-
51-
const wrapSkipComment = (skipComment: string | null | undefined): string => {
52-
return skipComment ?? 'Unknown reason';
53-
};
54-
55-
const getHistory = (history?: TestplaneTestResult['history']): TestStepCompressed[] => {
56-
return history?.map(h => {
57-
const result: TestStepCompressed = {
58-
[TestStepKey.Name]: h[TestStepKey.Name],
59-
[TestStepKey.Args]: h[TestStepKey.Args],
60-
[TestStepKey.Duration]: h[TestStepKey.Duration],
61-
[TestStepKey.TimeStart]: h[TestStepKey.TimeStart],
62-
[TestStepKey.IsFailed]: h[TestStepKey.IsFailed],
63-
[TestStepKey.IsGroup]: h[TestStepKey.IsGroup]
64-
};
65-
66-
if (h[TestStepKey.Children] && h[TestStepKey.IsFailed]) {
67-
result[TestStepKey.Children] = getHistory(h[TestStepKey.Children]);
68-
}
69-
70-
return result;
71-
}) ?? [];
72-
};
73-
7449
export interface TestplaneTestResultAdapterOptions {
7550
attempt: number;
7651
status: TestStatus;
7752
duration: number;
53+
saveHistoryMode?: Config['saveHistoryMode'];
7854
}
7955

8056
export class TestplaneTestResultAdapter implements ReporterTestResult {
@@ -84,6 +60,7 @@ export class TestplaneTestResultAdapter implements ReporterTestResult {
8460
private _attempt: number;
8561
private _status: TestStatus;
8662
private _duration: number;
63+
private _saveHistoryMode?: Config['saveHistoryMode'];
8764

8865
static create(
8966
this: new (testResult: TestplaneTest | TestplaneTestResult, options: TestplaneTestResultAdapterOptions) => TestplaneTestResultAdapter,
@@ -93,7 +70,7 @@ export class TestplaneTestResultAdapter implements ReporterTestResult {
9370
return new this(testResult, options);
9471
}
9572

96-
constructor(testResult: TestplaneTest | TestplaneTestResult, {attempt, status, duration}: TestplaneTestResultAdapterOptions) {
73+
constructor(testResult: TestplaneTest | TestplaneTestResult, {attempt, status, duration, saveHistoryMode}: TestplaneTestResultAdapterOptions) {
9774
this._testResult = testResult;
9875
this._errorDetails = null;
9976
this._timestamp = (this._testResult as TestplaneTestResult).startTime
@@ -107,6 +84,8 @@ export class TestplaneTestResultAdapter implements ReporterTestResult {
10784

10885
this._attempt = attempt;
10986
this._duration = duration;
87+
88+
this._saveHistoryMode = saveHistoryMode;
11089
}
11190

11291
get fullName(): string {
@@ -200,7 +179,7 @@ export class TestplaneTestResultAdapter implements ReporterTestResult {
200179
}
201180

202181
get history(): TestStepCompressed[] {
203-
return getHistory((this._testResult as TestplaneTestResult).history);
182+
return getHistory((this._testResult as TestplaneTestResult).history, this._saveHistoryMode);
204183
}
205184

206185
get error(): undefined | TestError {

lib/adapters/test/testplane.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,20 @@ import {TestplaneTestResultAdapter} from '../test-result/testplane';
22
import {DEFAULT_TITLE_DELIMITER, UNKNOWN_ATTEMPT} from '../../constants';
33

44
import type {TestAdapter, CreateTestResultOpts} from './';
5-
import type {Test, Suite} from 'testplane';
5+
import type {Test, Suite, Config} from 'testplane';
66
import type {ReporterTestResult} from '../test-result';
77

88
export class TestplaneTestAdapter implements TestAdapter {
99
private _test: Test;
10+
private _saveHistoryMode?: Config['saveHistoryMode'];
1011

11-
static create<T extends TestplaneTestAdapter>(this: new (test: Test) => T, test: Test): T {
12-
return new this(test);
12+
static create<T extends TestplaneTestAdapter>(this: new (test: Test, saveHistoryMode?: Config['saveHistoryMode']) => T, test: Test, saveHistoryMode?: Config['saveHistoryMode']): T {
13+
return new this(test, saveHistoryMode);
1314
}
1415

15-
constructor(test: Test) {
16+
constructor(test: Test, saveHistoryMode?: Config['saveHistoryMode']) {
1617
this._test = test;
18+
this._saveHistoryMode = saveHistoryMode;
1719
}
1820

1921
get original(): Test {
@@ -68,7 +70,7 @@ export class TestplaneTestAdapter implements TestAdapter {
6870
}
6971
});
7072

71-
return TestplaneTestResultAdapter.create(test, {attempt, status, duration});
73+
return TestplaneTestResultAdapter.create(test, {attempt, status, duration, saveHistoryMode: this._saveHistoryMode});
7274
}
7375
}
7476

lib/adapters/tool/testplane/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ export class TestplaneToolAdapter implements ToolAdapter {
136136

137137
const testCollection = await this._tool.readTests(paths, {grep, sets, browsers, replMode});
138138

139-
return TestplaneTestCollectionAdapter.create(testCollection);
139+
return TestplaneTestCollectionAdapter.create(testCollection, this._tool.config.saveHistoryMode);
140140
}
141141

142142
async run(testCollectionAdapter: TestplaneTestCollectionAdapter, tests: TestSpec[] = [], cliTool: CommanderStatic): Promise<boolean> {

0 commit comments

Comments
 (0)