Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions .eslintrc.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,6 @@ rules:
- src/config.ts
- src/createLocator.ts
- src/getModulesGraph.ts
- src/globby.ts
- src/index.ts
missingExports: true
unusedExports: true
Expand Down Expand Up @@ -193,7 +192,7 @@ rules:
'@typescript-eslint/no-loop-func': error
'@typescript-eslint/no-magic-numbers':
- error
- ignore: [-2, -1, 0, 1, 2, 1024]
- ignore: [-2, -1, 0, 1, 2, 1000, 1024]
ignoreArrayIndexes: true
ignoreDefaultValues: true
ignoreEnums: true
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ jobs:
- run: npm run build
- run: npm run test:local
- uses: actions/upload-artifact@v4
if: ${{ always() }}
with:
name: reports
path: build/autotests/reports
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ After the run, a detailed HTML report and a summary lite report in JSON format a

## Adding e2ed to a project

Prerequisites: [node](https://nodejs.org/en/) >=20,
Prerequisites: [node](https://nodejs.org/en/) >=22,
[TypeScript](https://www.typescriptlang.org/) >=5.

All commands below are run from the root directory of the project.
Expand Down Expand Up @@ -365,7 +365,7 @@ You can define the `SkipTests` type and `skipTests` processing rules in the hook
at the time of the test error, for display in the HTML report.

`testFileGlobs: readonly string[]`: an array of globs with pack test (task) files.
https://www.npmjs.com/package/globby is used for matching globs.
`fs.glob` from `nodejs` is used for matching globs.

`testIdleTimeout: number`: timeout (in milliseconds) for each individual test step.
If the test step (interval between two `log` function calls) takes longer than this timeout,
Expand Down
1 change: 1 addition & 0 deletions autotests/entities/device.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export const createDevice = async ({
cookies,
input: 7,
model,
title: model,
version,
});

Expand Down
3 changes: 2 additions & 1 deletion autotests/entities/product.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,12 @@ import type {ClientFunction} from 'e2ed/types';
*/
export const addProduct: ClientFunction<[Product], Promise<ApiProduct>> = createClientFunction(
(product: Product) =>
fetch(`https://reqres.in/api/product/${product.id}?size=${product.size}`, {
fetch(`https://dummyjson.com/products/add?id=${product.id}&size=${product.size}`, {
body: JSON.stringify({
cookies: [],
input: product.input,
model: product.model,
title: product.model,
version: product.version,
}),
headers: {'Content-Type': 'application/json; charset=UTF-8'},
Expand Down
42 changes: 32 additions & 10 deletions autotests/entities/worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,30 +4,52 @@ import {log} from 'e2ed/utils';
import type {UserWorker} from 'autotests/types';
import type {ClientFunction} from 'e2ed/types';

const clientGetUsers = createClientFunction(
(delay: number) =>
fetch(`https://reqres.in/api/users?delay=${delay}`, {method: 'GET'}).then((res) => res.json()),
{name: 'getUsers', timeout: 6_000},
);
type GetUsersOptions = Readonly<{delay?: number; retries?: number}> | undefined;

let clientGetUsers: ClientFunction<[number], unknown> | undefined;
let clientGetUsersRetries: number | undefined;

/**
* Adds user-worker.
*/
export const addUser: ClientFunction<[UserWorker, number?], Promise<object>> = createClientFunction(
(user: UserWorker, delay?: number) =>
fetch(`https://reqres.in/api/users${delay !== undefined ? `?delay=${delay}` : ''}`, {
export const addUser: ClientFunction<
[Readonly<{delay?: number; user: UserWorker}>],
Promise<object>
> = createClientFunction(
({delay, user}) =>
fetch(`https://dummyjson.com/users/add${delay !== undefined ? `?delay=${delay}` : ''}`, {
body: JSON.stringify(user),
headers: {'Content-Type': 'application/json; charset=UTF-8'},
method: 'POST',
}),
})
.then((res) => res.json())
.then((result: UserWorker) => {
// eslint-disable-next-line no-console
console.log('addUser return', result);

return result;
}),

{name: 'addUser', timeout: 3_000},
);

/**
* Get list of user-workers.
*/
export const getUsers = (delay: number): Promise<unknown> => {
export const getUsers = ({delay = 0, retries = 0}: GetUsersOptions = {}): Promise<unknown> => {
log(`Send API request with delay = ${delay}s`);

if (clientGetUsers === undefined || clientGetUsersRetries !== retries) {
clientGetUsersRetries = retries;

clientGetUsers = createClientFunction(
(clientDelay: number) =>
fetch(`https://dummyjson.com/users?delay=${clientDelay}`, {method: 'GET'}).then(
(res) => res.json() as unknown,
),
{name: 'getUsers', retries, timeout: 6_000},
);
}

return clientGetUsers(delay);
};
1 change: 0 additions & 1 deletion autotests/fixtures/fullMocks/mr-iHTD7Lp.json

This file was deleted.

1 change: 1 addition & 0 deletions autotests/fixtures/fullMocks/nAsmmzYTv6.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"/products/add":[{"completionTimeInMs":1747533319483,"duration":"3ms","request":{"method":"POST","query":{"id":"135865","size":"13"},"requestBody":{"cookies":[],"input":17,"model":"samsung","title":"samsung","version":"12"},"requestHeaders":{"accept":"*/*","accept-language":"en-US","content-type":"application/json; charset=UTF-8","referer":"https://joomcode.github.io/","user-agent":"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36","sec-ch-ua":"\"Chromium\";v=\"134\", \"Not:A-Brand\";v=\"24\", \"HeadlessChrome\";v=\"134\"","sec-ch-ua-mobile":"?0","sec-ch-ua-platform":"\"Linux\""},"url":"https://dummyjson.com/products/add?id=135865&size=13","utcTimeInMs":1747533319480},"responseBody":{"id":135865,"method":"POST","output":"17","payload":{"id":"135865","cookies":[],"input":17,"model":"samsung","title":"samsung","version":"12"},"query":{"id":"135865","size":"13"},"url":"https://dummyjson.com/products/add?id=135865&size=13"},"responseHeaders":{"content-type":"application/json; charset=UTF-8","content-length":"241"},"statusCode":200},{"completionTimeInMs":1747533319780,"duration":"3ms","request":{"method":"POST","query":{"id":"135865","size":"13"},"requestBody":{"cookies":[],"input":17,"model":"samsung","title":"samsung","version":"12"},"requestHeaders":{"accept":"*/*","accept-language":"en-US","content-type":"application/json; charset=UTF-8","referer":"https://joomcode.github.io/","user-agent":"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36","sec-ch-ua":"\"Chromium\";v=\"134\", \"Not:A-Brand\";v=\"24\", \"HeadlessChrome\";v=\"134\"","sec-ch-ua-mobile":"?0","sec-ch-ua-platform":"\"Linux\""},"url":"https://dummyjson.com/products/add?id=135865&size=13","utcTimeInMs":1747533319777},"responseBody":{"id":195,"title":"samsung"},"responseHeaders":{"etag":"W/\"1c-hP2nNXEenyGq06xPgJ9a0RJCH9E\"","x-content-type-options":"nosniff","date":"Sun, 18 May 2025 01:55:19 GMT","content-type":"application/json; charset=utf-8","vary":"Accept-Encoding","x-frame-options":"SAMEORIGIN","strict-transport-security":"max-age=15552000; includeSubDomains","x-dns-prefetch-control":"off","x-ratelimit-reset":"1747533329","x-download-options":"noopen","x-ratelimit-remaining":"99","access-control-allow-origin":"*","content-length":"28","x-xss-protection":"1; mode=block","x-ratelimit-limit":"100","x-railway-request-id":"cHuLc3G1SFSKT7u4m3z_FQ","x-powered-by":"Cats on Keyboards","x-railway-edge":"railway/europe-west4-drams3a","server":"railway-edge"},"statusCode":201}]}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {
} from 'autotests/actions';
import {setPageCookies, setPageRequestHeaders} from 'autotests/context';
import {E2edReportExample as E2edReportExampleRoute} from 'autotests/routes/pageRoutes';
import {createSelector, locator} from 'autotests/selectors';
import {locator} from 'autotests/selectors';
import {Page} from 'e2ed';
import {setReadonlyProperty} from 'e2ed/utils';

Expand All @@ -23,7 +23,7 @@ export class E2edReportExample extends Page<CustomPageParams> {
/**
* Page header.
*/
readonly header: Selector = createSelector('.header');
readonly header: Selector = locator('header');

/**
* Navigation bar with test retries.
Expand Down
2 changes: 2 additions & 0 deletions autotests/pageObjects/pages/Main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,9 @@ export class Main extends Page<CustomPageParams> {
await waitForAllRequestsComplete(
({url}) => {
if (
url.startsWith('https://browser.events.data.msn.com/') ||
url.startsWith('https://img-s-msn-com.akamaized.net/') ||
url.startsWith('https://rewards.bing.com/widget/') ||
url.startsWith('https://www.bing.com/th?id=')
) {
return false;
Expand Down
4 changes: 2 additions & 2 deletions autotests/routes/apiRoutes/AddUser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import type {Url} from 'e2ed/types';

type Params = Readonly<{delay?: number}>;

const pathStart = '/api/users';
const pathStart = '/users/add';

/**
* Client API route for adding user-worker.
Expand Down Expand Up @@ -37,7 +37,7 @@ export class AddUser extends ApiRoute<Params, ApiAddUserRequest, ApiAddUserRespo
}

override getOrigin(): Url {
return 'https://reqres.in' as Url;
return 'https://dummyjson.com' as Url;
}

getPath(): string {
Expand Down
6 changes: 3 additions & 3 deletions autotests/routes/apiRoutes/CreateProduct.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import type {Url} from 'e2ed/types';

type Params = Readonly<{id: ProductId; size: number}>;

const pathStart = '/api/product/';
const pathStart = '/products/add';

/**
* Test API route for creating a product.
Expand All @@ -27,7 +27,7 @@ export class CreateProduct extends ApiRoute<
{urlObject},
);

const id = Number(urlObject.pathname.slice(pathStart.length)) as ProductId;
const id = Number(urlObject.searchParams.get('id')) as ProductId;
const size = Number(urlObject.searchParams.get('size'));

assertValueIsTrue(Number.isInteger(id), 'url has correct id', {id, size, urlObject});
Expand All @@ -43,6 +43,6 @@ export class CreateProduct extends ApiRoute<
getPath(): string {
const {id, size} = this.routeParams;

return `${pathStart}${id}?size=${size}`;
return `${pathStart}?id=${id}&size=${size}`;
}
}
33 changes: 30 additions & 3 deletions autotests/routes/apiRoutes/GetUsers.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,48 @@
import {ApiRoute} from 'autotests/routes';
import {assertValueIsTrue} from 'e2ed/utils';

import type {ApiGetUsersRequest, ApiGetUsersResponse} from 'autotests/types';
import type {Url} from 'e2ed/types';

type Params = Readonly<{delay?: number}> | undefined;

const pathStart = '/users';

/**
* Client API route for getting users list.
*/
export class GetUsers extends ApiRoute<undefined, ApiGetUsersRequest, ApiGetUsersResponse> {
export class GetUsers extends ApiRoute<Params, ApiGetUsersRequest, ApiGetUsersResponse> {
static override getParamsFromUrlOrThrow(url: Url): Params {
const urlObject = new URL(url);

assertValueIsTrue(
urlObject.pathname.startsWith(pathStart),
'url pathname starts with correct path',
{urlObject},
);

const delay = Number(urlObject.searchParams.get('delay'));

if (delay >= 0) {
assertValueIsTrue(Number.isInteger(delay), 'url has correct delay', {delay, urlObject});

return {delay};
}

return {};
}

getMethod(): 'GET' {
return 'GET';
}

override getOrigin(): Url {
return 'https://reqres.in' as Url;
return 'https://dummyjson.com' as Url;
}

getPath(): string {
return '/api/users?delay=3';
const {delay} = this.routeParams ?? {};

return delay !== undefined ? `${pathStart}?delay=${delay}` : pathStart;
}
}
10 changes: 7 additions & 3 deletions autotests/tests/e2edReportExample/fullMocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ test('full mocks works correctly', {enableCsp: false, meta: {testId: '18'}}, asy

const mockedProduct = await addProduct(product);

const fetchUrl = `https://reqres.in/api/product/${productId}?size=${product.size}` as Url;
const fetchUrl = `https://dummyjson.com/products/add?id=${productId}&size=${product.size}` as Url;

await expect(mockedProduct, 'mocked API returns correct result').eql({
id: productId,
Expand All @@ -46,15 +46,19 @@ test('full mocks works correctly', {enableCsp: false, meta: {testId: '18'}}, asy
id: String(productId) as DeviceId,
input: product.input,
model: product.model,
title: product.model,
version: product.version,
},
query: {size: product.size},
query: {id: String(productId), size: product.size},
url: fetchUrl,
});

await unmockApiRoute(CreateProductRoute);

const newMockedProduct = await addProduct(product);

await expect('createdAt' in newMockedProduct, 'API mock on CreateProductRoute was umocked').ok();
await expect(
'title' in newMockedProduct && newMockedProduct.title === product.model,
'API mock on CreateProductRoute was umocked',
).ok();
});
12 changes: 6 additions & 6 deletions autotests/tests/internalTypeTests/waitForEvents.skip.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,15 +93,15 @@ void waitForRequestToRoute(AddUser, () => {}, {skipLogs: true});
// ok
void waitForRequestToRoute(AddUser, {
predicate: ({delay}, {requestBody, url}) => {
if (delay !== undefined && delay > 0 && requestBody.job !== 'foo') {
if (delay !== undefined && delay > 0 && requestBody.firstName !== 'foo') {
return url.startsWith('https');
}

return false;
},
}).then(
({request, routeParams}) =>
request.requestBody.job === 'foo' && 'delay' in routeParams && routeParams.delay > 0,
request.requestBody.lastName === 'foo' && 'delay' in routeParams && routeParams.delay > 0,
);

// @ts-expect-error: waitForRequestToRoute does not accept routes without `getParamsFromUrlOrThrow` method
Expand All @@ -122,8 +122,8 @@ void waitForResponseToRoute(AddUser, {
if (
delay !== undefined &&
delay > 0 &&
requestBody.job !== 'foo' &&
responseBody.job !== 'bar'
requestBody.firstName !== 'foo' &&
responseBody.firstName !== 'bar'
) {
return url.startsWith('https');
}
Expand All @@ -132,8 +132,8 @@ void waitForResponseToRoute(AddUser, {
},
}).then(
({response, routeParams}) =>
response.request.requestBody.job === 'foo' &&
response.responseBody.name === 'bar' &&
response.request.requestBody.firstName === 'foo' &&
response.responseBody.lastName === 'bar' &&
'delay' in routeParams &&
routeParams.delay > 0,
);
Expand Down
8 changes: 5 additions & 3 deletions autotests/tests/mockApiRoute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ test(

const mockedProduct = await addProduct(product);

const fetchUrl = `https://reqres.in/api/product/${productId}?size=${product.size}` as Url;
const fetchUrl =
`https://dummyjson.com/products/add?id=${productId}&size=${product.size}` as Url;

const productRouteParams = CreateProductRoute.getParamsFromUrlOrThrow(fetchUrl);

Expand All @@ -54,9 +55,10 @@ test(
id: String(productRouteFromUrl.routeParams.id) as DeviceId,
input: product.input,
model: product.model,
title: product.model,
version: product.version,
},
query: {size: product.size},
query: {id: String(productId), size: product.size},
url: fetchUrl,
});

Expand All @@ -65,7 +67,7 @@ test(
const newMockedProduct = await addProduct(product);

await expect(
'createdAt' in newMockedProduct,
'title' in newMockedProduct && newMockedProduct.title === product.model,
'API mock on CreateProductRoute was umocked',
).ok();
},
Expand Down
10 changes: 6 additions & 4 deletions autotests/tests/request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,15 @@ test(
{meta: {testId: '7'}, testIdleTimeout: 6_000},
async () => {
const {
responseBody: {data},
} = await request(GetUsers);
responseBody: {users},
} = await request(GetUsers, {
routeParams: {delay: 3_000},
});

await expect(data.length, 'request returns some users').gt(0);
await expect(users.length, 'request returns some users').gt(0);

await assertFunctionThrows(async () => {
await request(GetUsers, {maxRetriesCount: 1, timeout: 2_000});
await request(GetUsers, {maxRetriesCount: 1, routeParams: {delay: 3_000}, timeout: 2_000});
}, 'request function throws an error on timeout');
},
);
Loading