@zohodesk/testinglibrary 0.0.74-n20-experimental → 0.0.82-n20-experimental

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.
Files changed (30) hide show
  1. package/README.md +21 -0
  2. package/build/common/data-generator/steps/DataGenerator.spec.js +5 -5
  3. package/build/common/data-generator/steps/DataGeneratorStepsHelper.js +2 -2
  4. package/build/core/dataGenerator/DataGenerator.js +16 -6
  5. package/build/core/dataGenerator/DataGeneratorError.js +12 -2
  6. package/build/core/dataGenerator/authenticator/Authenticator.js +37 -0
  7. package/build/core/dataGenerator/authenticator/CookieAuthenticator.js +48 -0
  8. package/build/core/dataGenerator/authenticator/JWTauthenticator.js +64 -0
  9. package/build/core/dataGenerator/authenticator/OAuthAuthenticator.js +26 -0
  10. package/build/core/dataGenerator/authenticator/OrgOAuthAuthenticator.js +26 -0
  11. package/build/core/dataGenerator/authenticator/cookieAuthConfig.js +11 -0
  12. package/build/core/playwright/builtInFixtures/actorContext.js +13 -10
  13. package/build/core/playwright/builtInFixtures/context.js +42 -3
  14. package/build/core/playwright/builtInFixtures/page.js +0 -1
  15. package/build/core/playwright/helpers/logCollector.js +167 -0
  16. package/build/core/playwright/readConfigFile.js +9 -1
  17. package/build/core/playwright/setup/ProjectConfiguration.js +3 -1
  18. package/build/core/playwright/setup/custom-reporter.js +87 -1
  19. package/build/core/playwright/types.js +1 -0
  20. package/build/setup-folder-structure/samples/uat-config-sample.js +2 -0
  21. package/build/test/core/dataGenerator/authenticator/__tests__/Authenticator.test.js +88 -0
  22. package/build/test/core/dataGenerator/authenticator/__tests__/JWTauthenticator.test.js +129 -0
  23. package/build/test/core/dataGenerator/authenticator/__tests__/OAuthAuthenticator.test.js +39 -0
  24. package/build/test/core/dataGenerator/authenticator/__tests__/OrgOAuthAuthenticator.test.js +39 -0
  25. package/build/test/core/playwright/__tests__/validateFeature.test.js +4 -1
  26. package/build/test/core/playwright/runner/__tests__/RunnerHelper.test.js +3 -0
  27. package/build/test/core/playwright/runner/__tests__/SpawnRunner.test.js +3 -0
  28. package/npm-shrinkwrap.json +1194 -1205
  29. package/package.json +4 -4
  30. package/unit_reports/unit-report.html +1 -1
package/README.md CHANGED
@@ -17,6 +17,27 @@
17
17
 
18
18
  - npm run report
19
19
 
20
+ ### v4.1.9/v3.3.5 - 03-07-2026
21
+
22
+ #### Feature
23
+ - PortalDomain option provided in the settings.json.
24
+ - This lets customer portal and related UAT flows resolve the portal URL from configuration instead of hardcoding environment-specific links.
25
+ - It simplifies switching between setups such as dev, QA, and UAT without changing test code.
26
+
27
+
28
+ ### v4.1.5/v3.3.3 - 19-06-2026
29
+
30
+ #### Refactor
31
+ - Authenticator module refactored to use static imports instead of dynamic imports
32
+ - Removed unnecessary `extends Authenticator` from concrete authenticator classes (`JWTauthenticator`, `OAuthAuthenticator`, `OrgOAuthAuthenticator`, `CookieAuthenticator`) to eliminate circular dependency
33
+ - Unit test cases updated to align with actual source behavior
34
+
35
+ ### v4.1.4/v3.3.3 - 18-06-2026
36
+
37
+ #### Bugfix
38
+ - Dependency project failure stats pushed into custom reporter
39
+ - `stepDefinitionsFolder` hardcoded changes removed
40
+
20
41
  ### v4.1.3/v3.3.2 - 08-06-2026
21
42
 
22
43
  #### Features
@@ -4,16 +4,16 @@ import { generateAndCacheTestData } from './DataGeneratorStepsHelper';
4
4
  const { Given } = createBdd();
5
5
 
6
6
  Given('generate a {string} entity {string} with generator {string}', async ({ page, context, i18N, cacheLayer, executionContext}, module, entityName, generatorName, dataTable) => {
7
- await generateAndCacheTestData(executionContext, "template", generatorName, dataTable, cacheLayer, entityName);
7
+ await generateAndCacheTestData({ executionContext, type: "template", identifier: generatorName, dataTable, cacheLayer, entityName, page });
8
8
  });
9
9
 
10
10
  Given('generate a {string} entity {string} with API {string}', async ({ page, context, i18N, cacheLayer, executionContext}, module, entityName, operationId, dataTable) => {
11
- await generateAndCacheTestData(executionContext, "API", operationId, dataTable, cacheLayer, entityName);
11
+ await generateAndCacheTestData({ executionContext, type: "API", identifier: operationId, dataTable, cacheLayer, entityName, page });
12
12
  });
13
13
  Given('generate a {string} entity {string} with generator {string} using {string} profile', async ({ page, context, i18N, cacheLayer, executionContext}, module, entityName, generatorName, profile, dataTable) => {
14
- await generateAndCacheTestData(executionContext, "template", generatorName, dataTable, cacheLayer, entityName, profile);
14
+ await generateAndCacheTestData({ executionContext, type: "template", identifier: generatorName, dataTable, cacheLayer, entityName, profile, page });
15
15
  });
16
16
 
17
17
  Given('generate a {string} entity {string} with API {string} using {string} profile', async ({ page, context, i18N, cacheLayer, executionContext}, module, entityName, operationId, profile, dataTable) => {
18
- await generateAndCacheTestData(executionContext, "API", operationId, dataTable, cacheLayer, entityName, profile);
19
- });
18
+ await generateAndCacheTestData({ executionContext, type: "API", identifier: operationId, dataTable, cacheLayer, entityName, profile, page });
19
+ });
@@ -4,7 +4,7 @@ import DataGenerator from '@zohodesk/testinglibrary/DataGenerator';
4
4
 
5
5
  const dataGenerator = new DataGenerator();
6
6
 
7
- export async function generateAndCacheTestData(executionContext, type, identifier, dataTable, cacheLayer, entityName, profile = null) {
7
+ export async function generateAndCacheTestData({ executionContext, type, identifier, dataTable, cacheLayer, entityName, profile = null, page = null }) {
8
8
  let actorInfo;
9
9
  const testInfo = test.info();
10
10
  const scenarioName = testInfo.title.split('/').pop() || 'Unknown Scenario';
@@ -16,6 +16,6 @@ export async function generateAndCacheTestData(executionContext, type, identifie
16
16
  actorInfo = executionContext.actorInfo;
17
17
  }
18
18
 
19
- const generatedData = await dataGenerator.generate(testInfo, actorInfo, type, identifier, scenarioName, dataTable ? dataTable.hashes() : []);
19
+ const generatedData = await dataGenerator.generate(testInfo, actorInfo, type, identifier, scenarioName, dataTable ? dataTable.hashes() : [], page);
20
20
  await cacheLayer.set(entityName, generatedData.data);
21
21
  }
@@ -9,6 +9,7 @@ var _path = _interopRequireDefault(require("path"));
9
9
  var _fs = _interopRequireDefault(require("fs"));
10
10
  var _logger = require("../../utils/logger");
11
11
  var _DataGeneratorHelper = require("./DataGeneratorHelper");
12
+ var _Authenticator = _interopRequireDefault(require("./authenticator/Authenticator.js"));
12
13
  var _helpers = require("@zohodesk/testinglibrary/helpers");
13
14
  var _DataGeneratorError = require("./DataGeneratorError");
14
15
  function _classPrivateMethodInitSpec(e, a) { _checkPrivateRedeclaration(e, a), a.add(e); }
@@ -19,16 +20,21 @@ class DataGenerator {
19
20
  constructor() {
20
21
  _classPrivateMethodInitSpec(this, _DataGenerator_brand);
21
22
  }
22
- async generate(testInfo, actorInfo, generatorType, generatorName, scenarioName, dataTable) {
23
+ async generate(testInfo, actorInfo, generatorType, generatorName, scenarioName, dataTable, page = null) {
23
24
  try {
24
25
  let generators;
26
+ let domainUrl;
25
27
  if (generatorType === 'API') {
26
28
  generators = await _assertClassBrand(_DataGenerator_brand, this, _generateAPIGenerator).call(this, generatorName);
27
29
  } else {
28
30
  generators = await _assertClassBrand(_DataGenerator_brand, this, _getGenerator).call(this, testInfo, generatorName);
29
31
  }
32
+ const generatorOperationIds = generators.map(({
33
+ generatorOperationId
34
+ }) => generatorOperationId).filter(Boolean);
35
+ domainUrl = generatorOperationIds.some(operationId => operationId.startsWith('portal.')) ? process.env.portalDomain : process.env.domain;
30
36
  const processedGenerators = await (0, _DataGeneratorHelper.processGenerator)(generators, dataTable);
31
- const apiPayload = await _assertClassBrand(_DataGenerator_brand, this, _constructApiPayload).call(this, scenarioName, processedGenerators, actorInfo);
37
+ const apiPayload = await _assertClassBrand(_DataGenerator_brand, this, _constructApiPayload).call(this, scenarioName, processedGenerators, actorInfo, domainUrl, page);
32
38
  const response = await (0, _DataGeneratorHelper.makeRequest)(process.env.DG_SERVICE_DOMAIN + process.env.DG_SERVICE_API_PATH, apiPayload);
33
39
  _logger.Logger.log(_logger.Logger.INFO_TYPE, `Generated response for the generator: ${generatorName} for scenario: ${scenarioName}, Response: ${JSON.stringify(response)}`);
34
40
  return response;
@@ -72,12 +78,12 @@ async function _generateAPIGenerator(operationId) {
72
78
  name: operationId
73
79
  }];
74
80
  }
75
- async function _constructApiPayload(scenarioName, processedGenerators, actorInfo) {
81
+ async function _constructApiPayload(scenarioName, processedGenerators, actorInfo, domainUrl, page = null) {
76
82
  const dataGeneratorObj = actorInfo['data-generator'];
77
83
  if (!dataGeneratorObj) {
78
84
  throw new _DataGeneratorError.DataGeneratorConfigurationError(`Data Generator configuration is missing for the profile: ${actorInfo['profile']}`);
79
85
  }
80
- const apiPayload = {
86
+ let apiPayload = {
81
87
  scenario_name: scenarioName,
82
88
  data_generation_templates: processedGenerators,
83
89
  ...dataGeneratorObj
@@ -90,10 +96,14 @@ async function _constructApiPayload(scenarioName, processedGenerators, actorInfo
90
96
  const environmentDetails = apiPayload.environmentDetails || {};
91
97
  if (environmentDetails) {
92
98
  environmentDetails.iam_url = process.env.DG_IAM_DOMAIN;
93
- const domainUrl = new URL(process.env.domain);
94
- environmentDetails.host = domainUrl.origin;
99
+ const selectedDomain = new URL(domainUrl || process.env.domain);
100
+ environmentDetails.host = selectedDomain.origin;
95
101
  }
96
102
  apiPayload.environmentDetails = environmentDetails;
103
+
104
+ // Only perform auth augmentation via Authenticator
105
+ apiPayload = await new _Authenticator.default().constructPayload(apiPayload, actorInfo, page);
106
+ _logger.Logger.log(_logger.Logger.INFO_TYPE, `Final API Payload after authentication augmentation: ${JSON.stringify(apiPayload)}`);
97
107
  return apiPayload;
98
108
  }
99
109
  var _default = exports.default = DataGenerator;
@@ -3,7 +3,7 @@
3
3
  Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
- exports.GeneratorError = exports.DataGeneratorError = exports.DataGeneratorConfigurationError = void 0;
6
+ exports.GeneratorError = exports.DataGeneratorError = exports.DataGeneratorConfigurationError = exports.AuthenticationError = void 0;
7
7
  class DataGeneratorError extends Error {
8
8
  constructor(message) {
9
9
  super(message);
@@ -47,4 +47,14 @@ class GeneratorError extends DataGeneratorError {
47
47
  }
48
48
  }
49
49
  }
50
- exports.GeneratorError = GeneratorError;
50
+ exports.GeneratorError = GeneratorError;
51
+ class AuthenticationError extends DataGeneratorError {
52
+ constructor(message) {
53
+ super(message);
54
+ this.name = 'AuthenticationError';
55
+ if (Error.captureStackTrace) {
56
+ Error.captureStackTrace(this, AuthenticationError);
57
+ }
58
+ }
59
+ }
60
+ exports.AuthenticationError = AuthenticationError;
@@ -0,0 +1,37 @@
1
+ "use strict";
2
+
3
+ var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
4
+ Object.defineProperty(exports, "__esModule", {
5
+ value: true
6
+ });
7
+ exports.default = void 0;
8
+ var _DataGeneratorError = require("../DataGeneratorError.js");
9
+ var _JWTauthenticator = _interopRequireDefault(require("./JWTauthenticator.js"));
10
+ var _OAuthAuthenticator = _interopRequireDefault(require("./OAuthAuthenticator.js"));
11
+ var _OrgOAuthAuthenticator = _interopRequireDefault(require("./OrgOAuthAuthenticator.js"));
12
+ var _CookieAuthenticator = _interopRequireDefault(require("./CookieAuthenticator.js"));
13
+ class Authenticator {
14
+ // Only route authentication augmentations; do not modify environment or base payload here.
15
+ async constructPayload(apiPayload, actorInfo, page = null) {
16
+ var _actorInfo$dataGener;
17
+ const authenticationLoaders = {
18
+ 'jwt-auth': () => new _JWTauthenticator.default(),
19
+ 'oauth2': () => new _OAuthAuthenticator.default(),
20
+ 'org-oauth': () => new _OrgOAuthAuthenticator.default(),
21
+ 'cookie-auth': () => new _CookieAuthenticator.default()
22
+ };
23
+ const authentications = (actorInfo === null || actorInfo === void 0 || (_actorInfo$dataGener = actorInfo['data-generator']) === null || _actorInfo$dataGener === void 0 || (_actorInfo$dataGener = _actorInfo$dataGener.account) === null || _actorInfo$dataGener === void 0 ? void 0 : _actorInfo$dataGener.authentications) || [];
24
+ const authenticationTypes = authentications.map(auth => String(auth).toLowerCase()).filter(Boolean);
25
+ for (const authenticationType of authenticationTypes) {
26
+ const authenticatorLoader = authenticationLoaders[authenticationType];
27
+ if (!authenticatorLoader) {
28
+ throw new _DataGeneratorError.AuthenticationError(`Unknown authentication type: "${authenticationType}". Supported types are: ${Object.keys(authenticationLoaders).join(', ')}`);
29
+ }
30
+ const authenticatorInstance = await authenticatorLoader();
31
+ apiPayload = await authenticatorInstance.constructPayload(apiPayload, actorInfo, page);
32
+ break;
33
+ }
34
+ return apiPayload;
35
+ }
36
+ }
37
+ exports.default = Authenticator;
@@ -0,0 +1,48 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.default = void 0;
7
+ var _cookieAuthConfig = require("./cookieAuthConfig.js");
8
+ var _DataGeneratorError = require("../DataGeneratorError.js");
9
+ /**
10
+ * CookieAuthenticator prepares the payload for cookie-based authentication.
11
+ * It extracts cookies from the browser page context and attaches them
12
+ * as cookieAuthHeaders in the account block for the data generator service.
13
+ */
14
+ class CookieAuthenticator {
15
+ async constructPayload(apiPayload, actorInfo, page = null) {
16
+ if (!page) {
17
+ throw new _DataGeneratorError.AuthenticationError('Cookie authentication failed: "page" is required but was not provided');
18
+ }
19
+ const {
20
+ csrfHeaderName
21
+ } = _cookieAuthConfig.COOKIE_AUTH_CONFIG;
22
+ const {
23
+ csrfToken,
24
+ cookieString
25
+ } = await _getCookies.call(CookieAuthenticator, page);
26
+ apiPayload.account.authHeaders = {
27
+ Cookie: cookieString,
28
+ [csrfHeaderName]: csrfToken
29
+ };
30
+ return apiPayload;
31
+ }
32
+ }
33
+ async function _getCookies(page) {
34
+ const {
35
+ csrfCookieName,
36
+ csrfParamName
37
+ } = _cookieAuthConfig.COOKIE_AUTH_CONFIG;
38
+ const context = page.context();
39
+ const pageUrl = new URL(page.url());
40
+ const currentDomain = pageUrl.hostname;
41
+ const cookies = await context.cookies([page.url()]);
42
+ const csrfCookies = cookies.filter(cookie => cookie.domain.includes(currentDomain) && cookie.name === csrfCookieName);
43
+ return {
44
+ csrfToken: csrfCookies.map(c => `${csrfParamName}=${c.value}`).join('; '),
45
+ cookieString: cookies.map(c => `${c.name}=${c.value}`).join('; ')
46
+ };
47
+ }
48
+ var _default = exports.default = CookieAuthenticator;
@@ -0,0 +1,64 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.default = void 0;
7
+ var _DataGeneratorError = require("../DataGeneratorError.js");
8
+ /**
9
+ * JWTauthenticator constructs payload by fetching a JWT access token
10
+ * from the authenticator app and attaching it to the account.jwt config.
11
+ */
12
+ class JWTauthenticator {
13
+ async constructPayload(apiPayload, actorInfo) {
14
+ const account = actorInfo['data-generator'].account;
15
+ if (!account) {
16
+ throw new _DataGeneratorError.AuthenticationError('JWT authentication failed: "account" is missing in actor configuration');
17
+ }
18
+ const jwtConf = account['jwt-oauth'];
19
+ if (!jwtConf) {
20
+ throw new _DataGeneratorError.AuthenticationError('JWT authentication failed: "jwt-oauth" is missing in actor configuration');
21
+ }
22
+
23
+ // Build payload for authenticator service
24
+ const userToken = encodeURIComponent((actorInfo === null || actorInfo === void 0 ? void 0 : actorInfo.email) || '');
25
+ const jwtAuthUrl = process.env.DG_JWT_AUTH_URL;
26
+ const responseType = jwtConf.response_type;
27
+ const clientId = jwtConf.client_id;
28
+ const tokenUrl = jwtConf.token_url;
29
+ const domainUrl = process.env.DG_DOMAIN_URL;
30
+ const authPayload = {
31
+ user_token: userToken,
32
+ jwt_secret: jwtConf.jwt_secret,
33
+ jwt_auth_url: jwtAuthUrl,
34
+ response_type: responseType,
35
+ client_id: clientId,
36
+ token_url: tokenUrl,
37
+ domain_url: domainUrl
38
+ };
39
+ const authenticatorApi = process.env.DG_JWT_AUTHENTICATOR_API;
40
+ const resp = await fetch(authenticatorApi, {
41
+ method: 'POST',
42
+ headers: {
43
+ 'Content-Type': 'application/json'
44
+ },
45
+ body: JSON.stringify(authPayload)
46
+ });
47
+ if (!resp.ok) {
48
+ const errorBody = await resp.text();
49
+ throw new Error(`JWT authenticator error! status: ${resp.status}, body: ${errorBody}`);
50
+ }
51
+ const json = await resp.json();
52
+ const accessToken = json === null || json === void 0 ? void 0 : json.access_token;
53
+ if (!accessToken) {
54
+ throw new Error('JWT authenticator did not return access_token');
55
+ }
56
+
57
+ // Attach access token to authHeaders so downstream API calls can use it directly
58
+ apiPayload.account = apiPayload.account || {};
59
+ apiPayload.account.authHeaders = apiPayload.account.authHeaders || {};
60
+ apiPayload.account.authHeaders.Authorization = `Zoho-oauthtoken ${accessToken}`;
61
+ return apiPayload;
62
+ }
63
+ }
64
+ var _default = exports.default = JWTauthenticator;
@@ -0,0 +1,26 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.default = void 0;
7
+ var _DataGeneratorError = require("../DataGeneratorError.js");
8
+ /**
9
+ * OAuthAuthenticator prepares the payload for OAuth2-based authentication.
10
+ * It ensures oauth2 config is present in the account block so the
11
+ * data generator service can use it.
12
+ */
13
+ class OAuthAuthenticator {
14
+ async constructPayload(apiPayload, actorInfo) {
15
+ const account = actorInfo['data-generator'].account;
16
+ if (!account) {
17
+ throw new _DataGeneratorError.AuthenticationError('OAuth2 authentication failed: "account" is missing in actor configuration');
18
+ }
19
+ if (!account.oauth2) {
20
+ throw new _DataGeneratorError.AuthenticationError('OAuth2 authentication failed: "oauth2" is missing in actor configuration');
21
+ }
22
+ apiPayload.account.oauth2 = account.oauth2;
23
+ return apiPayload;
24
+ }
25
+ }
26
+ var _default = exports.default = OAuthAuthenticator;
@@ -0,0 +1,26 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.default = void 0;
7
+ var _DataGeneratorError = require("../DataGeneratorError.js");
8
+ /**
9
+ * OrgOAuthAuthenticator prepares the payload for org_oauth authentication.
10
+ * It ensures org_oauth config is present in the account block so the
11
+ * data generator service can use it.
12
+ */
13
+ class OrgOAuthAuthenticator {
14
+ async constructPayload(apiPayload, actorInfo) {
15
+ const account = actorInfo['data-generator'].account;
16
+ if (!account) {
17
+ throw new _DataGeneratorError.AuthenticationError('OrgOAuth authentication failed: "account" is missing in actor configuration');
18
+ }
19
+ if (!account.org_oauth) {
20
+ throw new _DataGeneratorError.AuthenticationError('OrgOAuth authentication failed: "org_oauth" is missing in actor configuration');
21
+ }
22
+ apiPayload.account.org_oauth = account.org_oauth;
23
+ return apiPayload;
24
+ }
25
+ }
26
+ var _default = exports.default = OrgOAuthAuthenticator;
@@ -0,0 +1,11 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.COOKIE_AUTH_CONFIG = void 0;
7
+ const COOKIE_AUTH_CONFIG = exports.COOKIE_AUTH_CONFIG = {
8
+ csrfHeaderName: 'X-ZCSRF-TOKEN',
9
+ csrfCookieName: 'crmcsr',
10
+ csrfParamName: 'crmcsrfparam'
11
+ };
@@ -25,20 +25,20 @@ class ActorContext {
25
25
  };
26
26
  const additionalActors = (0, _additionalProfiles.additionProfiles)($tags);
27
27
  await Promise.all(Object.entries(additionalActors).map(async ([role, actorInfo]) => {
28
- let context = await browser.newContext();
29
- let page = await context.newPage();
30
- let ctxTestDetails = {
31
- page,
28
+ const additionalContext = await browser.newContext();
29
+ const additionalPage = await additionalContext.newPage();
30
+ const ctxTestDetails = {
31
+ page: additionalPage,
32
32
  $tags,
33
- context,
33
+ context: additionalContext,
34
34
  ...actorInfo
35
35
  };
36
- await (0, _loginDefaultStepsHelper.executeDefaultLoginSteps)(context, testInfo, ctxTestDetails, actorInfo);
36
+ await (0, _loginDefaultStepsHelper.executeDefaultLoginSteps)(additionalContext, testInfo, ctxTestDetails, actorInfo);
37
37
  this.actorsObj[role] = {
38
38
  role,
39
39
  browser,
40
- context,
41
- page,
40
+ context: additionalContext,
41
+ page: additionalPage,
42
42
  executionContext: {
43
43
  actorInfo
44
44
  }
@@ -69,7 +69,10 @@ var _default = exports.default = {
69
69
  }, use, testInfo) => {
70
70
  const ctxObject = new ActorContext();
71
71
  await ctxObject.setup(browser, $tags, testInfo, context, page, executionContext);
72
- await use(ctxObject);
73
- await ctxObject.cleanup();
72
+ try {
73
+ await use(ctxObject);
74
+ } finally {
75
+ await ctxObject.cleanup();
76
+ }
74
77
  }
75
78
  };
@@ -1,13 +1,28 @@
1
1
  "use strict";
2
2
 
3
+ var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
3
4
  Object.defineProperty(exports, "__esModule", {
4
5
  value: true
5
6
  });
6
7
  exports.default = void 0;
8
+ var _path = _interopRequireDefault(require("path"));
7
9
  var _readConfigFile = require("../readConfigFile");
10
+ var _logCollector = require("../helpers/logCollector");
8
11
  const {
9
- testSetup
12
+ testSetup,
13
+ uatDirectory,
14
+ browserLogs = {}
10
15
  } = (0, _readConfigFile.generateConfigFromFile)();
16
+
17
+ // Keep runtime logs OUTSIDE reportPath (HTML reporter wipes it on onEnd) and
18
+ // outside test-results (Playwright outputDir is cleaned each run).
19
+ const LOGS_DIR = _path.default.join(uatDirectory, 'runtime-logs');
20
+ const MAX_FILENAME_LENGTH = 120;
21
+ function sanitizeForFilename(name) {
22
+ if (!name) return 'test';
23
+ const cleaned = name.replace(/[^A-Za-z0-9._-]+/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '').slice(0, MAX_FILENAME_LENGTH);
24
+ return cleaned || 'test';
25
+ }
11
26
  async function performDefaultContextSteps({
12
27
  context
13
28
  }) {
@@ -20,13 +35,37 @@ async function performDefaultContextSteps({
20
35
  var _default = exports.default = {
21
36
  context: async ({
22
37
  context
23
- }, use) => {
38
+ }, use, testInfo) => {
24
39
  await context.addInitScript(() =>
25
40
  // eslint-disable-next-line no-undef
26
41
  window.localStorage.setItem('isDnBannerHide', true));
27
42
  await performDefaultContextSteps({
28
43
  context
29
44
  });
30
- await use(context);
45
+ const collector = new _logCollector.LogCollector(browserLogs.limits);
46
+ collector.attachToContext(context);
47
+ testInfo.runtimeLogs = collector;
48
+ try {
49
+ await use(context);
50
+ } finally {
51
+ collector.detach();
52
+ const failureOnly = browserLogs.mode === 'failure-only';
53
+ const isFailure = testInfo.status && testInfo.status !== testInfo.expectedStatus;
54
+ const shouldFlush = !failureOnly || isFailure;
55
+ const summary = collector.toSummary();
56
+ let filePath = null;
57
+ if (shouldFlush) {
58
+ const base = `w${testInfo.workerIndex}-${sanitizeForFilename(testInfo.title)}-${testInfo.retry}`;
59
+ filePath = collector.flushToFile(LOGS_DIR, base);
60
+ }
61
+ await testInfo.attach('runtime-log', {
62
+ contentType: 'application/json',
63
+ body: Buffer.from(JSON.stringify({
64
+ schemaVersion: 1,
65
+ summary,
66
+ file: filePath
67
+ }))
68
+ });
69
+ }
31
70
  }
32
71
  };
@@ -32,7 +32,6 @@ var _default = exports.default = {
32
32
  } finally {
33
33
  await (0, _loginDefaultStepsHelper.performDefaultPageSteps)(testDetails);
34
34
  await use(page);
35
- await context.close();
36
35
  }
37
36
  }
38
37
  };
@@ -0,0 +1,167 @@
1
+ "use strict";
2
+
3
+ var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
4
+ Object.defineProperty(exports, "__esModule", {
5
+ value: true
6
+ });
7
+ exports.LogCollector = void 0;
8
+ var _path = _interopRequireDefault(require("path"));
9
+ var _fileUtils = require("../../../utils/fileUtils");
10
+ // Per-context Playwright runtime event collector — network failures,
11
+ // console errors, page errors and crashes. Buffered in-memory and flushed
12
+ // once per test via writeFileContents.
13
+
14
+ const DEFAULT_CONFIG = {
15
+ maxEvents: 2000,
16
+ maxBodyBytes: 0,
17
+ captureHeaders: false,
18
+ ignoreMessagePatterns: [/n\.enableDarkTheme is not a function/],
19
+ ignoreUrlPatterns: [/\.(png|jpe?g|gif|svg|webp|woff2?|ttf|css|map)(\?|$)/i, /google-analytics\.com|googletagmanager\.com|sentry\.io/i]
20
+ };
21
+ class LogCollector {
22
+ constructor(config = {}) {
23
+ this.config = {
24
+ ...DEFAULT_CONFIG,
25
+ ...config
26
+ };
27
+ this.events = [];
28
+ this.truncated = false;
29
+ this.counts = {
30
+ console: 0,
31
+ consoleError: 0,
32
+ pageError: 0,
33
+ request: 0,
34
+ response: 0,
35
+ failed: 0,
36
+ crash: 0
37
+ };
38
+ this._bound = [];
39
+ }
40
+ _push(evt) {
41
+ if (this.events.length >= this.config.maxEvents) {
42
+ this.truncated = true;
43
+ this.events.shift();
44
+ }
45
+ this.events.push({
46
+ ts: Date.now(),
47
+ ...evt
48
+ });
49
+ }
50
+ _shouldIgnore(url) {
51
+ return this.config.ignoreUrlPatterns.some(re => re.test(url));
52
+ }
53
+ _shouldIgnoreMessage(message) {
54
+ if (!message) {
55
+ return false;
56
+ }
57
+ return this.config.ignoreMessagePatterns.some(re => re.test(message));
58
+ }
59
+ _bind(target, event, handler) {
60
+ target.on(event, handler);
61
+ this._bound.push([target, event, handler]);
62
+ }
63
+ attachToContext(context) {
64
+ context.pages().forEach(p => this._attachPage(p));
65
+ this._bind(context, 'page', p => this._attachPage(p));
66
+ this._bind(context, 'request', req => {
67
+ if (this._shouldIgnore(req.url())) {
68
+ return;
69
+ }
70
+ this.counts.request++;
71
+ });
72
+ this._bind(context, 'response', res => {
73
+ const url = res.url();
74
+ if (this._shouldIgnore(url)) {
75
+ return;
76
+ }
77
+ this.counts.response++;
78
+ if (res.status() >= 400) {
79
+ this._push({
80
+ kind: 'response',
81
+ status: res.status(),
82
+ method: res.request().method(),
83
+ url,
84
+ fromCache: res.fromServiceWorker()
85
+ });
86
+ }
87
+ });
88
+ this._bind(context, 'requestfailed', req => {
89
+ var _req$failure;
90
+ this.counts.failed++;
91
+ this._push({
92
+ kind: 'requestfailed',
93
+ method: req.method(),
94
+ url: req.url(),
95
+ failure: (_req$failure = req.failure()) === null || _req$failure === void 0 ? void 0 : _req$failure.errorText,
96
+ resourceType: req.resourceType()
97
+ });
98
+ });
99
+ }
100
+ _attachPage(page) {
101
+ this._bind(page, 'console', msg => {
102
+ const type = msg.type();
103
+ this.counts.console++;
104
+ if (type !== 'error') {
105
+ return;
106
+ }
107
+ const text = msg.text();
108
+ if (this._shouldIgnoreMessage(text)) {
109
+ return;
110
+ }
111
+ this.counts.consoleError++;
112
+ this._push({
113
+ kind: 'console',
114
+ level: type,
115
+ text,
116
+ location: msg.location()
117
+ });
118
+ });
119
+ this._bind(page, 'pageerror', err => {
120
+ if (this._shouldIgnoreMessage(err.message)) {
121
+ return;
122
+ }
123
+ this.counts.pageError++;
124
+ this._push({
125
+ kind: 'pageerror',
126
+ name: err.name,
127
+ message: err.message,
128
+ stack: err.stack
129
+ });
130
+ });
131
+ this._bind(page, 'crash', () => {
132
+ this.counts.crash++;
133
+ this._push({
134
+ kind: 'crash',
135
+ url: page.url()
136
+ });
137
+ });
138
+ }
139
+ detach() {
140
+ for (const [target, event, handler] of this._bound) {
141
+ try {
142
+ target.removeListener(event, handler);
143
+ } catch {
144
+ // context may already be closed
145
+ }
146
+ }
147
+ this._bound.length = 0;
148
+ }
149
+ toSummary() {
150
+ return {
151
+ ...this.counts,
152
+ total: this.events.length,
153
+ truncated: this.truncated
154
+ };
155
+ }
156
+ flushToFile(dir, baseName) {
157
+ if (this.events.length === 0) {
158
+ return null;
159
+ }
160
+ const file = _path.default.join(dir, `${baseName}.jsonl`);
161
+ const body = this.events.map(e => JSON.stringify(e)).join('\n') + '\n';
162
+ (0, _fileUtils.writeFileContents)(file, body);
163
+ this.events.length = 0;
164
+ return file;
165
+ }
166
+ }
167
+ exports.LogCollector = LogCollector;