Skip to content

Commit 001c5ce

Browse files
committed
PRO-16178 fix: support expect methods in grouping steps
1 parent beca126 commit 001c5ce

7 files changed

Lines changed: 161 additions & 90 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,9 @@
1212
- [fix: add `expect` method `toBeInViewport` instead of separate asserts](https://github.com/joomcode/e2ed/commit/0c50fe8f6b7c3adc20c21c66a334a15dd00c054f) ([uid11](https://github.com/uid11))
1313
- [Merge pull request #125 from joomcode/feat/support-steps-in-actions](https://github.com/joomcode/e2ed/commit/72b7b394289085dc955f200621aa19f9e83f1e9e) ([uid11](https://github.com/uid11))
1414

15-
feat: support steps in actions (for groupping steps)
15+
feat: support steps in actions (for grouping steps)
1616

17-
- [PRO-16178 feat: support steps in actions (for groupping steps)](https://github.com/joomcode/e2ed/commit/ba860e4ed6f2e778374d8c0ddd4d758836a98859) ([uid11](https://github.com/uid11))
17+
- [PRO-16178 feat: support steps in actions (for grouping steps)](https://github.com/joomcode/e2ed/commit/ba860e4ed6f2e778374d8c0ddd4d758836a98859) ([uid11](https://github.com/uid11))
1818
- [Merge pull request #124 from joomcode/feat/add-logs-with-children](https://github.com/joomcode/e2ed/commit/e8e72d924150feefcc382dfdcf7c86ca51127657) ([uid11](https://github.com/uid11))
1919

2020
feat: add functionality of `regroupSteps` in HTML reports

autotests/configurator/regroupSteps.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import {setReadonlyProperty} from 'e2ed/utils';
44
import type {LogEvent, Mutable} from 'e2ed/types';
55

66
/**
7-
* Regroup log events (for groupping of `TestRun` steps).
7+
* Regroup log events (for grouping of `TestRun` steps).
88
* This base client function should not use scope variables (except other base functions).
99
* @internal
1010
*/

autotests/packs/allTests.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ const browserFlags = [
3333
const filterTestsIntoPack: FilterTestsIntoPack = ({options}) => options.meta.testId !== '13';
3434

3535
const userAgent =
36-
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36';
36+
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36';
3737

3838
const msInMinute = 60_000;
3939
const packTimeoutInMinutes = 5;

src/actions/navigateToUrl.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {LogEventType} from '../constants/internal';
1+
import {ADDITIONAL_STEP_TIMEOUT, LogEventType} from '../constants/internal';
22
import {step} from '../step';
33
import {getPlaywrightPage} from '../useContext';
44

@@ -11,7 +11,7 @@ export const navigateToUrl = async (
1111
url: Url,
1212
options: NavigateToUrlOptions = {},
1313
): Promise<NavigationReturn> => {
14-
const {skipLogs = false} = options;
14+
const {skipLogs = false, timeout} = options;
1515
let statusCode: StatusCode | undefined;
1616

1717
await step(
@@ -27,7 +27,12 @@ export const navigateToUrl = async (
2727

2828
return {statusCode};
2929
},
30-
{payload: options, skipLogs, type: LogEventType.InternalAction},
30+
{
31+
payload: options,
32+
skipLogs,
33+
...(timeout !== undefined ? {timeout: timeout + ADDITIONAL_STEP_TIMEOUT} : undefined),
34+
type: LogEventType.InternalAction,
35+
},
3136
);
3237

3338
return {statusCode};

src/actions/setHeadersAndNavigateToUrl.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import {AsyncLocalStorage} from 'node:async_hooks';
22

3-
import {LogEventType} from '../constants/internal';
3+
import {ADDITIONAL_STEP_TIMEOUT, LogEventType} from '../constants/internal';
44
import {step} from '../step';
55
import {getPlaywrightPage} from '../useContext';
66
import {assertValueIsDefined} from '../utils/asserts';
@@ -26,6 +26,7 @@ export const setHeadersAndNavigateToUrl = async (
2626
navigateToUrlOptions?: NavigateToUrlOptions,
2727
): Promise<NavigationReturn> => {
2828
let navigationReturn: NavigationReturn | undefined;
29+
const timeout = navigateToUrlOptions?.timeout ?? getFullPackConfig().navigationTimeout;
2930

3031
await step(
3132
`Navigate to ${url} and map headers`,
@@ -42,8 +43,6 @@ export const setHeadersAndNavigateToUrl = async (
4243
return route.fallback();
4344
}
4445

45-
const timeout = navigateToUrlOptions?.timeout ?? getFullPackConfig().navigationTimeout;
46-
4746
const response = await route.fetch({timeout});
4847
const headers = response.headers();
4948

@@ -76,7 +75,11 @@ export const setHeadersAndNavigateToUrl = async (
7675

7776
return {requestHeaders, responseHeaders};
7877
},
79-
{skipLogs, type: LogEventType.InternalAction},
78+
{
79+
skipLogs,
80+
timeout: timeout + ADDITIONAL_STEP_TIMEOUT,
81+
type: LogEventType.InternalAction,
82+
},
8083
);
8184

8285
assertValueIsDefined(navigationReturn, 'navigationReturn is defined', {

src/utils/expect/createExpectMethod.ts

Lines changed: 52 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,23 @@
1-
import {LogEventStatus, LogEventType, RESOLVED_PROMISE, RETRY_KEY} from '../../constants/internal';
1+
import {LogEventStatus, LogEventType, RETRY_KEY} from '../../constants/internal';
22

3+
import {assertValueIsDefined} from '../asserts';
34
import {getFullPackConfig} from '../config';
45
import {E2edError} from '../error';
56
import {getDurationWithUnits} from '../getDurationWithUnits';
6-
import {log} from '../log';
7+
import {logAndGetLogEvent} from '../log';
78
import {setReadonlyProperty} from '../object';
89
import {addTimeoutToPromise} from '../promise';
910
import {Selector} from '../selectors';
1011
import {isThenable} from '../typeGuards';
1112
import {removeStyleFromString, valueToString, wrapStringForLogs} from '../valueToString';
1213

13-
import {additionalMatchers} from './additionalMatchers';
14-
import {applyAdditionalMatcher} from './applyAdditionalMatcher';
14+
import {getAssertionPromise} from './getAssertionPromise';
1515

16-
import type {Fn, SelectorPropertyRetryData} from '../../types/internal';
16+
import type {SelectorPropertyRetryData, UtcTimeInMs} from '../../types/internal';
1717

18-
import type {Expect} from './Expect';
18+
import type {additionalMatchers} from './additionalMatchers';
1919
import type {AssertionFunction, ExpectMethod} from './types';
2020

21-
import {expect as playwrightExpect} from '@playwright/test';
22-
2321
const additionalAssertionTimeoutInMs = 1_000;
2422

2523
/**
@@ -40,67 +38,14 @@ export const createExpectMethod = (
4038
const selectorPropertyRetryData = (
4139
this.actualValue as {[RETRY_KEY]?: SelectorPropertyRetryData}
4240
)?.[RETRY_KEY];
43-
const timeoutWithUnits = getDurationWithUnits(timeout);
44-
const timeoutError = new E2edError(
45-
`"${key}" assertion promise rejected after ${timeoutWithUnits} timeout`,
46-
);
4741

48-
const runAssertion = (value: unknown): Promise<Expect> => {
49-
const additionalMatcher = additionalMatchers[key as keyof typeof additionalMatchers];
50-
const ctx: Expect = {actualValue: value, description: this.description};
51-
52-
if (additionalMatcher !== undefined) {
53-
return addTimeoutToPromise(
54-
applyAdditionalMatcher(
55-
additionalMatcher as Fn<unknown[], Promise<unknown>>,
56-
ctx,
57-
args,
58-
selectorPropertyRetryData,
59-
),
60-
timeout,
61-
timeoutError,
62-
).catch((assertError: Error) => {
63-
setReadonlyProperty(ctx, 'error', assertError);
64-
65-
return ctx;
66-
});
67-
}
68-
69-
const assertion = playwrightExpect(value, ctx.description) as unknown as Record<
70-
string,
71-
Fn<unknown[], Promise<unknown>>
72-
>;
73-
74-
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
75-
return addTimeoutToPromise(assertion[key]!(...args), timeout, timeoutError).then(
76-
() => ctx,
77-
(assertError: Error) => {
78-
setReadonlyProperty(ctx, 'error', assertError);
79-
80-
return ctx;
81-
},
82-
);
83-
};
84-
85-
const assertionPromise: Promise<Expect> = RESOLVED_PROMISE.then(() => {
86-
if (isThenable(this.actualValue)) {
87-
return addTimeoutToPromise(
88-
this.actualValue as Promise<unknown>,
89-
timeout,
90-
timeoutError,
91-
).then(runAssertion);
92-
}
93-
94-
return runAssertion(this.actualValue);
95-
});
96-
97-
return assertionPromise.then(({actualValue, additionalLogFields, error}) => {
98-
const logMessage = `Assert: ${this.description}`;
99-
const logPayload = {
42+
const printedValue = isThenable(this.actualValue) ? '<Thenable>' : this.actualValue;
43+
const logEvent = logAndGetLogEvent(
44+
`Assert: ${this.description}`,
45+
{
46+
actualValue: printedValue,
47+
assertion: wrapStringForLogs(`value ${valueToString(printedValue)} ${message}`),
10048
assertionArguments: args,
101-
...additionalLogFields,
102-
error: error?.message === undefined ? undefined : removeStyleFromString(error.message),
103-
logEventStatus: error ? LogEventStatus.Failed : LogEventStatus.Passed,
10449
selector:
10550
selectorPropertyRetryData?.selector.description ??
10651
(this.actualValue instanceof Selector ? this.actualValue.description : undefined),
@@ -110,22 +55,50 @@ export const createExpectMethod = (
11055
selectorPropertyArgs: selectorPropertyRetryData.args,
11156
}
11257
: undefined),
113-
};
58+
},
59+
LogEventType.InternalAssert,
60+
);
61+
62+
assertValueIsDefined(logEvent, 'logEvent is defined', {args, message, ...this});
63+
64+
const {payload} = logEvent;
65+
66+
assertValueIsDefined(payload, 'payload is defined', {args, message, ...this});
67+
68+
const timeoutError = new E2edError(
69+
`"${key}" assertion promise rejected after ${getDurationWithUnits(timeout)} timeout`,
70+
);
71+
72+
const assertionPromise = getAssertionPromise({
73+
args,
74+
context: this,
75+
key: key as keyof typeof additionalMatchers,
76+
selectorPropertyRetryData,
77+
timeout,
78+
timeoutError,
79+
});
80+
81+
return assertionPromise.then(({actualValue, additionalLogFields, error}) => {
82+
Object.assign(payload, {
83+
...additionalLogFields,
84+
error: error?.message === undefined ? undefined : removeStyleFromString(error.message),
85+
logEventStatus: error ? LogEventStatus.Failed : LogEventStatus.Passed,
86+
});
11487

11588
return addTimeoutToPromise(Promise.resolve(actualValue), timeout, timeoutError)
11689
.then(
117-
(value) =>
118-
log(
119-
logMessage,
120-
{
121-
actualValue: value,
122-
assertion: wrapStringForLogs(`value ${valueToString(value)} ${message}`),
123-
...logPayload,
124-
},
125-
LogEventType.InternalAssert,
126-
),
90+
(value) => {
91+
Object.assign(payload, {
92+
actualValue: value,
93+
assertion: wrapStringForLogs(`value ${valueToString(value)} ${message}`),
94+
});
95+
96+
setReadonlyProperty(logEvent, 'endTime', Date.now() as UtcTimeInMs);
97+
},
12798
(actualValueResolveError: Error) => {
128-
log(logMessage, {actualValueResolveError, ...logPayload}, LogEventType.InternalAssert);
99+
Object.assign(payload, {actualValueResolveError});
100+
101+
setReadonlyProperty(logEvent, 'endTime', Date.now() as UtcTimeInMs);
129102
},
130103
)
131104
.then(() => {
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import {RESOLVED_PROMISE} from '../../constants/internal';
2+
3+
import {setReadonlyProperty} from '../object';
4+
import {addTimeoutToPromise} from '../promise';
5+
import {isThenable} from '../typeGuards';
6+
7+
import {additionalMatchers} from './additionalMatchers';
8+
import {applyAdditionalMatcher} from './applyAdditionalMatcher';
9+
10+
import type {Fn, SelectorPropertyRetryData} from '../../types/internal';
11+
12+
import type {E2edError} from '../error';
13+
14+
import type {Expect} from './Expect';
15+
import type {ExpectMethod} from './types';
16+
17+
import {expect as playwrightExpect} from '@playwright/test';
18+
19+
type Options = Readonly<{
20+
args: Parameters<ExpectMethod>;
21+
context: Expect;
22+
key: keyof typeof additionalMatchers;
23+
selectorPropertyRetryData: SelectorPropertyRetryData | undefined;
24+
timeout: number;
25+
timeoutError: E2edError;
26+
}>;
27+
28+
/**
29+
* Get internal assertion promise by assertion options.
30+
* @internal
31+
*/
32+
export const getAssertionPromise = ({
33+
args,
34+
context,
35+
key,
36+
selectorPropertyRetryData,
37+
timeout,
38+
timeoutError,
39+
}: Options): Promise<Expect> => {
40+
const runAssertion = (value: unknown): Promise<Expect> => {
41+
const additionalMatcher = additionalMatchers[key];
42+
const ctx: Expect = {actualValue: value, description: context.description};
43+
44+
if (additionalMatcher !== undefined) {
45+
return addTimeoutToPromise(
46+
applyAdditionalMatcher(
47+
additionalMatcher as Fn<unknown[], Promise<unknown>>,
48+
ctx,
49+
args,
50+
selectorPropertyRetryData,
51+
),
52+
timeout,
53+
timeoutError,
54+
).catch((assertError: Error) => {
55+
setReadonlyProperty(ctx, 'error', assertError);
56+
57+
return ctx;
58+
});
59+
}
60+
61+
const assertion = playwrightExpect(value, ctx.description) as unknown as Record<
62+
string,
63+
Fn<unknown[], Promise<unknown>>
64+
>;
65+
66+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
67+
return addTimeoutToPromise(assertion[key]!(...args), timeout, timeoutError).then(
68+
() => ctx,
69+
(assertError: Error) => {
70+
setReadonlyProperty(ctx, 'error', assertError);
71+
72+
return ctx;
73+
},
74+
);
75+
};
76+
77+
const assertionPromise: Promise<Expect> = RESOLVED_PROMISE.then(() => {
78+
if (isThenable(context.actualValue)) {
79+
return addTimeoutToPromise(
80+
context.actualValue as Promise<unknown>,
81+
timeout,
82+
timeoutError,
83+
).then(runAssertion);
84+
}
85+
86+
return runAssertion(context.actualValue);
87+
});
88+
89+
return assertionPromise;
90+
};

0 commit comments

Comments
 (0)