@gravity-ui/playwright-tools 1.0.0 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  A library of additional utilities for writing tests using Playwright Test.
4
4
 
5
5
  ```
6
- npm i -D playwright-tools
6
+ npm i -D @gravity-ui/playwright-tools
7
7
  ```
8
8
 
9
9
  The package contains several subdirectories with utilities for different purposes. You should import from these subdirectories, for example:
@@ -1,8 +1,8 @@
1
1
  import type { MockNetworkFixtureBuilderParams } from './types';
2
- export type HarPatcherParams = Pick<MockNetworkFixtureBuilderParams, 'headersToRemove' | 'setCookieToRemove' | 'onHarEntryWillRead' | 'onHarEntryWillWrite' | 'onTransformHarLookupParams' | 'onTransformHarLookupResult'> & {
2
+ export type HarPatcherParams = Pick<MockNetworkFixtureBuilderParams, 'headersToRemove' | 'setCookieToRemove' | 'onHarEntryWillRead' | 'onHarEntryWillWrite' | 'onTransformHarLookupParams' | 'onTransformHarLookupResult' | 'shouldMarkIdenticalRequests'> & {
3
3
  /**
4
4
  * Base url of the test
5
5
  */
6
6
  baseURL: string;
7
7
  };
8
- export declare function harPatcher({ baseURL, headersToRemove: additionalHeadersToRemove, setCookieToRemove: additionalSetCookieToRemove, onHarEntryWillWrite, onHarEntryWillRead, onTransformHarLookupParams, onTransformHarLookupResult, }: HarPatcherParams): void;
8
+ export declare function harPatcher({ baseURL, headersToRemove: additionalHeadersToRemove, setCookieToRemove: additionalSetCookieToRemove, onHarEntryWillWrite, onHarEntryWillRead, onTransformHarLookupParams, onTransformHarLookupResult, shouldMarkIdenticalRequests, }: HarPatcherParams): void;
@@ -2,6 +2,8 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.harPatcher = harPatcher;
4
4
  const har_1 = require("../../har");
5
+ const createDuplicateIdTransform_1 = require("../../utils/createDuplicateIdTransform");
6
+ const markIdenticalRequests_1 = require("../../utils/markIdenticalRequests");
5
7
  const DEFAULT_REMOVE_HEADERS = new Set([
6
8
  'cookie',
7
9
  'x-csrf-token',
@@ -10,7 +12,7 @@ const DEFAULT_REMOVE_HEADERS = new Set([
10
12
  ]);
11
13
  const DEFAULT_REMOVE_SET_COOKIE_FOR = new Set(['CSRF-TOKEN']);
12
14
  const baseUrlPLaceholder = 'https://base.url.placeholder';
13
- function harPatcher({ baseURL, headersToRemove: additionalHeadersToRemove = [], setCookieToRemove: additionalSetCookieToRemove = [], onHarEntryWillWrite, onHarEntryWillRead, onTransformHarLookupParams, onTransformHarLookupResult, }) {
15
+ function harPatcher({ baseURL, headersToRemove: additionalHeadersToRemove = [], setCookieToRemove: additionalSetCookieToRemove = [], onHarEntryWillWrite, onHarEntryWillRead, onTransformHarLookupParams, onTransformHarLookupResult, shouldMarkIdenticalRequests = false, }) {
14
16
  const headersToRemove = new Set([...DEFAULT_REMOVE_HEADERS, ...additionalHeadersToRemove]);
15
17
  const setCookieToRemove = new Set([
16
18
  ...DEFAULT_REMOVE_SET_COOKIE_FOR,
@@ -27,16 +29,39 @@ function harPatcher({ baseURL, headersToRemove: additionalHeadersToRemove = [],
27
29
  removeSetCookieFor: setCookieToRemove,
28
30
  });
29
31
  (0, har_1.replaceBaseUrlInEntry)(entry, baseURL, baseUrlPLaceholder);
30
- onHarEntryWillWrite?.(entry);
32
+ onHarEntryWillWrite?.(entry, baseURL);
31
33
  });
32
34
  (0, har_1.addHarOpenTransform)((harFile) => {
33
35
  const entries = harFile.log.entries;
34
36
  for (const entry of entries) {
35
37
  (0, har_1.replaceBaseUrlInEntry)(entry, baseUrlPLaceholder, baseURL);
36
- onHarEntryWillRead?.(entry);
38
+ onHarEntryWillRead?.(entry, baseURL);
37
39
  }
38
40
  });
39
- (0, har_1.addHarLookupTransform)(onTransformHarLookupParams, onTransformHarLookupResult);
40
- // Filter out canceled requests before writing to har file
41
- (0, har_1.addFlushTransform)((entries) => entries.filter((entry) => entry.time !== -1));
41
+ // Create duplicate ID transformer once to preserve state between calls
42
+ const duplicateIdTransform = shouldMarkIdenticalRequests ? (0, createDuplicateIdTransform_1.createDuplicateIdTransform)() : null;
43
+ const onTransformHarLookupParamsFinal = (params) => {
44
+ // Apply duplicate ID transform first (if enabled)
45
+ const modifiedParams = duplicateIdTransform ? duplicateIdTransform(params) : params;
46
+ // Then apply custom transformer (if provided)
47
+ return onTransformHarLookupParams
48
+ ? onTransformHarLookupParams(modifiedParams, baseURL)
49
+ : modifiedParams;
50
+ };
51
+ const onTransformHarLookupResultFinal = (result, params) => {
52
+ return onTransformHarLookupResult
53
+ ? onTransformHarLookupResult(result, params, baseURL)
54
+ : result;
55
+ };
56
+ (0, har_1.addHarLookupTransform)(onTransformHarLookupParamsFinal, onTransformHarLookupResultFinal);
57
+ // Transform requests before writing to har file
58
+ (0, har_1.addFlushTransform)((entries) => {
59
+ // Before writing to har file, filter out canceled requests
60
+ const filteredEntries = entries.filter((entry) => entry.time !== -1);
61
+ // Add x-tests-duplicate-id header for identical requests (if enabled)
62
+ if (shouldMarkIdenticalRequests) {
63
+ return (0, markIdenticalRequests_1.markIdenticalRequests)(filteredEntries);
64
+ }
65
+ return filteredEntries;
66
+ });
42
67
  }
@@ -1,6 +1,6 @@
1
1
  import type { PlaywrightTestArgs, PlaywrightTestOptions, TestFixture } from '@playwright/test';
2
2
  import type { MockNetworkFixtureBuilderParams } from './types';
3
- export declare function mockNetworkFixtureBuilder({ shouldUpdate, forceUpdateIfHarMissing, updateTimeout, zip, url: urlMatcherBuilder, dumpsFilePath, ...harPatcherParams }: MockNetworkFixtureBuilderParams): [TestFixture<boolean, PlaywrightTestArgs & PlaywrightTestOptions>, {
3
+ export declare function mockNetworkFixtureBuilder<TestArgs extends PlaywrightTestArgs & PlaywrightTestOptions = PlaywrightTestArgs & PlaywrightTestOptions>({ shouldUpdate, forceUpdateIfHarMissing, updateTimeout, zip, url, dumpsFilePath, optionallyEnabled, ...harPatcherParams }: MockNetworkFixtureBuilderParams): [TestFixture<boolean, TestArgs>, {
4
4
  auto: boolean;
5
5
  scope: "test";
6
6
  }];
@@ -3,34 +3,59 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.mockNetworkFixtureBuilder = mockNetworkFixtureBuilder;
4
4
  const har_1 = require("../../har");
5
5
  const har_patcher_1 = require("./har-patcher");
6
- function mockNetworkFixtureBuilder({ shouldUpdate, forceUpdateIfHarMissing, updateTimeout, zip = true, url: urlMatcherBuilder, dumpsFilePath, ...harPatcherParams }) {
7
- const mockNetworkFixture = async ({ baseURL: rawBaseURL, page }, use, testInfo) => {
8
- if (!rawBaseURL) {
9
- throw new Error('baseURL should be specified in playwright config');
10
- }
11
- const baseURL = rawBaseURL.replace(/\/+$/, '');
12
- (0, har_patcher_1.harPatcher)({
13
- baseURL,
14
- ...harPatcherParams,
15
- });
16
- const update = Boolean(shouldUpdate);
17
- const url = urlMatcherBuilder(baseURL);
18
- await (0, har_1.initDumps)(page, testInfo, {
19
- dumpsFilePath,
6
+ const fixtureFunction = async ({ baseURL: rawBaseURL, page, }, { shouldUpdate, forceUpdateIfHarMissing, updateTimeout, zip = true, url: urlMatcherBuilder, dumpsFilePath, ...harPatcherParams }, use, testInfo) => {
7
+ if (!rawBaseURL) {
8
+ throw new Error('baseURL should be specified in playwright config');
9
+ }
10
+ const baseURL = rawBaseURL.replace(/\/+$/, '');
11
+ (0, har_patcher_1.harPatcher)({
12
+ baseURL,
13
+ ...harPatcherParams,
14
+ });
15
+ const update = Boolean(shouldUpdate);
16
+ const url = urlMatcherBuilder(baseURL);
17
+ await (0, har_1.initDumps)(page, testInfo, {
18
+ dumpsFilePath,
19
+ forceUpdateIfHarMissing,
20
+ updateTimeout,
21
+ update,
22
+ url,
23
+ zip,
24
+ });
25
+ await use(!update);
26
+ };
27
+ function mockNetworkFixtureBuilder({ shouldUpdate, forceUpdateIfHarMissing, updateTimeout, zip = true, url, dumpsFilePath, optionallyEnabled, ...harPatcherParams }) {
28
+ const mockNetworkFixture = async ({ baseURL, page }, use, testInfo) => {
29
+ return fixtureFunction({ baseURL, page }, {
30
+ shouldUpdate,
20
31
  forceUpdateIfHarMissing,
21
32
  updateTimeout,
22
- update,
33
+ zip,
23
34
  url,
35
+ dumpsFilePath,
36
+ ...harPatcherParams,
37
+ }, use, testInfo);
38
+ };
39
+ const mockNetworkFixtureWithOptionalParam = async ({ baseURL, page, enableNetworkMocking }, use, testInfo) => {
40
+ if (!enableNetworkMocking) {
41
+ return use(false);
42
+ }
43
+ return fixtureFunction({ baseURL, page }, {
44
+ shouldUpdate,
45
+ forceUpdateIfHarMissing,
46
+ updateTimeout,
24
47
  zip,
25
- });
26
- await use(!update);
48
+ url,
49
+ dumpsFilePath,
50
+ ...harPatcherParams,
51
+ }, use, testInfo);
27
52
  };
28
53
  const fixtureOptions = {
29
54
  auto: true,
30
55
  scope: 'test',
31
56
  };
32
57
  const mockNetwork = [
33
- mockNetworkFixture,
58
+ optionallyEnabled ? mockNetworkFixtureWithOptionalParam : mockNetworkFixture,
34
59
  fixtureOptions,
35
60
  ];
36
61
  return mockNetwork;
@@ -53,21 +53,43 @@ export type MockNetworkFixtureBuilderParams = {
53
53
  * Callback for processing requests and responses by saving to .har. Useful for various post-processing of requests: cleaning, changing format, etc.
54
54
  * By default, sensitive headers are removed + the base url of the request is changed to a stub
55
55
  * @param entry The entry in .har that will be written
56
+ * @param baseURL The base URL of the test
56
57
  */
57
- onHarEntryWillWrite?: (entry: Entry) => void;
58
+ onHarEntryWillWrite?: (entry: Entry, baseURL: string) => void;
58
59
  /**
59
60
  * Callback to process requests and responses written in .har before they are used
60
61
  * Useful for reverting changes made in onHarEntryWillWrite
61
62
  * By default, the base url templates are replaced with the actual baseUrl of the test
62
63
  * @param entry The entry in .har that will be used
64
+ * @param baseURL The base URL of the test
63
65
  */
64
- onHarEntryWillRead?: (entry: Entry) => void;
66
+ onHarEntryWillRead?: (entry: Entry, baseURL: string) => void;
65
67
  /**
66
68
  * Callback for changing search parameters of queries in .har
69
+ * @param params The lookup parameters
70
+ * @param baseURL The base URL of the test
67
71
  */
68
- onTransformHarLookupParams?: HarLookupParamsTransformFunction;
72
+ onTransformHarLookupParams?: (params: Parameters<HarLookupParamsTransformFunction>[0], baseURL: string) => ReturnType<HarLookupParamsTransformFunction>;
69
73
  /**
70
74
  * Callback for transforming the search query result into .har
75
+ * @param result The lookup result
76
+ * @param params The lookup parameters
77
+ * @param baseURL The base URL of the test
71
78
  */
72
- onTransformHarLookupResult?: HarLookupResultTransformFunction;
79
+ onTransformHarLookupResult?: (result: Parameters<HarLookupResultTransformFunction>[0], params: Parameters<HarLookupResultTransformFunction>[1], baseURL: string) => ReturnType<HarLookupResultTransformFunction>;
80
+ /**
81
+ * Allow optionally enable fixture.
82
+ * Set "enableNetworkMocking" fixture to turn on/off network mocking
83
+ * @defaultValue false `
84
+ */
85
+ optionallyEnabled?: boolean;
86
+ /**
87
+ * Flag to enable or disable adding the x-tests-duplicate-id header for identical requests
88
+ * By default, the header is not added
89
+ * @defaultValue `false`
90
+ */
91
+ shouldMarkIdenticalRequests?: boolean;
92
+ };
93
+ export type OptionallyEnabledTestArgs = {
94
+ enableNetworkMocking?: boolean;
73
95
  };
package/har/index.d.ts CHANGED
@@ -10,5 +10,5 @@ export { clearHeaders } from './clearHeaders';
10
10
  export { initDumps } from './initDumps';
11
11
  export { replaceBaseUrlInEntry } from './replaceBaseUrlInEntry';
12
12
  export { setExtraHash } from './setExtraHash';
13
- export type { HARFile, Entry } from './types';
13
+ export type { HARFile, Entry, Header, LocalUtilsHarLookupParams, LocalUtilsHarLookupResult, QueryParameter, } from './types';
14
14
  export { defaultDumpsFilePathBuilder, dumpsPathBuldeWithSlugBuilder } from './dumpsFilePathBulders';
@@ -2,7 +2,7 @@ import type { Entry } from './types';
2
2
  /**
3
3
  * Replaces the base URL in the HAR file request entry
4
4
  *
5
- * @param entry ЗRecording from HAR file
5
+ * @param entry Recording from HAR file
6
6
  * @param fromUrl URL to replace
7
7
  * @param toUrl URL to replace with
8
8
  */
@@ -8,7 +8,7 @@ const escape_string_regexp_1 = __importDefault(require("escape-string-regexp"));
8
8
  /**
9
9
  * Replaces the base URL in the HAR file request entry
10
10
  *
11
- * @param entry ЗRecording from HAR file
11
+ * @param entry Recording from HAR file
12
12
  * @param fromUrl URL to replace
13
13
  * @param toUrl URL to replace with
14
14
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gravity-ui/playwright-tools",
3
- "version": "1.0.0",
3
+ "version": "1.1.1",
4
4
  "description": "Tools for Playwright Test",
5
5
  "keywords": [
6
6
  "playwright",
@@ -0,0 +1,11 @@
1
+ import type { LocalUtilsHarLookupParams } from '../har';
2
+ /**
3
+ * Creates a transformer for automatically adding the x-tests-duplicate-id header
4
+ * to requests when searching in a HAR file.
5
+ *
6
+ * This allows distinguishing identical requests without changing test code.
7
+ *
8
+ * The header is NOT added to the first request (for backward compatibility with old HARs),
9
+ * and is added starting from the second: 2, 3, 4...
10
+ */
11
+ export declare function createDuplicateIdTransform(): (params: LocalUtilsHarLookupParams) => LocalUtilsHarLookupParams;
@@ -0,0 +1,32 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createDuplicateIdTransform = createDuplicateIdTransform;
4
+ const requestKeyService_1 = require("./requestKeyService");
5
+ /**
6
+ * Creates a transformer for automatically adding the x-tests-duplicate-id header
7
+ * to requests when searching in a HAR file.
8
+ *
9
+ * This allows distinguishing identical requests without changing test code.
10
+ *
11
+ * The header is NOT added to the first request (for backward compatibility with old HARs),
12
+ * and is added starting from the second: 2, 3, 4...
13
+ */
14
+ function createDuplicateIdTransform() {
15
+ const requestCounts = new Map();
16
+ return (params) => {
17
+ const key = (0, requestKeyService_1.createRequestKeyFromLookupParams)(params);
18
+ // Increment the counter for this request
19
+ const count = (requestCounts.get(key) || 0) + 1;
20
+ requestCounts.set(key, count);
21
+ // Add header only starting from the second call
22
+ // This ensures backward compatibility with old HAR files,
23
+ // where requests did not have the x-tests-duplicate-id header
24
+ if (count > 1) {
25
+ params.headers.push({
26
+ name: 'x-tests-duplicate-id',
27
+ value: String(count),
28
+ });
29
+ }
30
+ return params;
31
+ };
32
+ }
@@ -0,0 +1,9 @@
1
+ import type { Entry } from '../har';
2
+ /**
3
+ * Adds the x-tests-duplicate-id header only for duplicate requests.
4
+ *
5
+ * The first request remains without a header for backward compatibility with old HAR files.
6
+ * Starting from the second, requests receive headers: 2, 3, 4...
7
+ *
8
+ */
9
+ export declare const markIdenticalRequests: (entries: Entry[]) => Entry[];
@@ -0,0 +1,28 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.markIdenticalRequests = void 0;
4
+ const requestKeyService_1 = require("./requestKeyService");
5
+ /**
6
+ * Adds the x-tests-duplicate-id header only for duplicate requests.
7
+ *
8
+ * The first request remains without a header for backward compatibility with old HAR files.
9
+ * Starting from the second, requests receive headers: 2, 3, 4...
10
+ *
11
+ */
12
+ const markIdenticalRequests = (entries) => {
13
+ const currentCounts = new Map();
14
+ entries.forEach((entry) => {
15
+ const key = (0, requestKeyService_1.createRequestKeyFromEntry)(entry);
16
+ const currentCount = (currentCounts.get(key) || 0) + 1;
17
+ currentCounts.set(key, currentCount);
18
+ // Add header only if this is not the first instance (currentCount > 1)
19
+ if (currentCount > 1) {
20
+ entry.request.headers.push({
21
+ name: 'x-tests-duplicate-id',
22
+ value: String(currentCount),
23
+ });
24
+ }
25
+ });
26
+ return entries;
27
+ };
28
+ exports.markIdenticalRequests = markIdenticalRequests;
@@ -0,0 +1,30 @@
1
+ import type { Entry, Header, LocalUtilsHarLookupParams, QueryParameter } from '../har';
2
+ /**
3
+ * Creates a unique request key based on its parameters.
4
+ * Used to identify identical requests when working with HAR files.
5
+ *
6
+ * The key is formed based on:
7
+ * - HTTP method
8
+ * - URL (without query parameters)
9
+ * - Query parameters (sorted)
10
+ * - Headers (sorted, excluding service headers)
11
+ */
12
+ export declare function createRequestKey(params: RequestKeyParams): string;
13
+ /**
14
+ * Parameters for creating a request key
15
+ */
16
+ type RequestKeyParams = {
17
+ method: string;
18
+ url: string;
19
+ headers: Header[];
20
+ queryString?: QueryParameter[];
21
+ };
22
+ /**
23
+ * Creates a key from Entry (when writing HAR)
24
+ */
25
+ export declare function createRequestKeyFromEntry(entry: Entry): string;
26
+ /**
27
+ * Creates a key from LocalUtilsHarLookupParams (when replaying)
28
+ */
29
+ export declare function createRequestKeyFromLookupParams(params: LocalUtilsHarLookupParams): string;
30
+ export {};
@@ -0,0 +1,79 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createRequestKey = createRequestKey;
4
+ exports.createRequestKeyFromEntry = createRequestKeyFromEntry;
5
+ exports.createRequestKeyFromLookupParams = createRequestKeyFromLookupParams;
6
+ /**
7
+ * Creates a unique request key based on its parameters.
8
+ * Used to identify identical requests when working with HAR files.
9
+ *
10
+ * The key is formed based on:
11
+ * - HTTP method
12
+ * - URL (without query parameters)
13
+ * - Query parameters (sorted)
14
+ * - Headers (sorted, excluding service headers)
15
+ */
16
+ function createRequestKey(params) {
17
+ const method = params.method;
18
+ const url = params.url;
19
+ // Parse URL to extract the base part and query parameters
20
+ const urlObj = new URL(url);
21
+ // Get sorted query parameters
22
+ const sortedQueryString = params.queryString
23
+ ? sortQueryStringFromArray(params.queryString)
24
+ : sortQueryStringFromUrl(urlObj);
25
+ // Get sorted headers (excluding service headers)
26
+ const sortedHeaders = sortHeaders(params.headers);
27
+ // Form the key: method + URL without query + query parameters + headers
28
+ return `${method}:${urlObj.origin}${urlObj.pathname}:${sortedQueryString}:${sortedHeaders}`;
29
+ }
30
+ /**
31
+ * Creates a key from Entry (when writing HAR)
32
+ */
33
+ function createRequestKeyFromEntry(entry) {
34
+ return createRequestKey({
35
+ method: entry.request.method,
36
+ url: entry.request.url,
37
+ headers: entry.request.headers,
38
+ queryString: entry.request.queryString,
39
+ });
40
+ }
41
+ /**
42
+ * Creates a key from LocalUtilsHarLookupParams (when replaying)
43
+ */
44
+ function createRequestKeyFromLookupParams(params) {
45
+ return createRequestKey({
46
+ method: params.method,
47
+ url: params.url,
48
+ headers: params.headers,
49
+ // queryString is automatically extracted from URL
50
+ });
51
+ }
52
+ /**
53
+ * Sorts query parameters from QueryParameter array
54
+ */
55
+ function sortQueryStringFromArray(queryString) {
56
+ return [...queryString]
57
+ .sort((a, b) => a.name.localeCompare(b.name))
58
+ .map((queryParameter) => `${queryParameter.name}=${queryParameter.value}`)
59
+ .join('&');
60
+ }
61
+ /**
62
+ * Sorts query parameters from URL
63
+ */
64
+ function sortQueryStringFromUrl(urlObj) {
65
+ return Array.from(urlObj.searchParams.entries())
66
+ .sort((a, b) => a[0].localeCompare(b[0]))
67
+ .map(([key, value]) => `${key}=${value}`)
68
+ .join('&');
69
+ }
70
+ /**
71
+ * Sorts headers, excluding service headers
72
+ */
73
+ function sortHeaders(headers) {
74
+ return [...headers]
75
+ .filter((header) => header.name.toLowerCase() !== 'x-tests-duplicate-id')
76
+ .sort((a, b) => a.name.localeCompare(b.name))
77
+ .map((header) => `${header.name.toLowerCase()}:${header.value}`)
78
+ .join('|');
79
+ }