@azure/playwright 1.1.3 → 1.1.4

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 (125) hide show
  1. package/dist/browser/common/constants.d.ts +1 -4
  2. package/dist/browser/common/constants.d.ts.map +1 -1
  3. package/dist/browser/common/constants.js +1 -4
  4. package/dist/browser/common/constants.js.map +1 -1
  5. package/dist/browser/common/types.d.ts +0 -4
  6. package/dist/browser/common/types.d.ts.map +1 -1
  7. package/dist/browser/common/types.js.map +1 -1
  8. package/dist/browser/reporter/playwrightReporter.d.ts +0 -1
  9. package/dist/browser/reporter/playwrightReporter.d.ts.map +1 -1
  10. package/dist/browser/reporter/playwrightReporter.js +3 -17
  11. package/dist/browser/reporter/playwrightReporter.js.map +1 -1
  12. package/dist/browser/utils/PlaywrightServiceClient.d.ts +1 -2
  13. package/dist/browser/utils/PlaywrightServiceClient.d.ts.map +1 -1
  14. package/dist/browser/utils/PlaywrightServiceClient.js +1 -26
  15. package/dist/browser/utils/PlaywrightServiceClient.js.map +1 -1
  16. package/dist/browser/utils/utils.d.ts +2 -3
  17. package/dist/browser/utils/utils.d.ts.map +1 -1
  18. package/dist/browser/utils/utils.js +8 -21
  19. package/dist/browser/utils/utils.js.map +1 -1
  20. package/dist/commonjs/common/constants.d.ts +1 -4
  21. package/dist/commonjs/common/constants.d.ts.map +1 -1
  22. package/dist/commonjs/common/constants.js +2 -5
  23. package/dist/commonjs/common/constants.js.map +1 -1
  24. package/dist/commonjs/common/types.d.ts +0 -4
  25. package/dist/commonjs/common/types.d.ts.map +1 -1
  26. package/dist/commonjs/common/types.js.map +1 -1
  27. package/dist/commonjs/reporter/playwrightReporter.d.ts +0 -1
  28. package/dist/commonjs/reporter/playwrightReporter.d.ts.map +1 -1
  29. package/dist/commonjs/reporter/playwrightReporter.js +2 -16
  30. package/dist/commonjs/reporter/playwrightReporter.js.map +1 -1
  31. package/dist/commonjs/tsdoc-metadata.json +1 -1
  32. package/dist/commonjs/utils/PlaywrightServiceClient.d.ts +1 -2
  33. package/dist/commonjs/utils/PlaywrightServiceClient.d.ts.map +1 -1
  34. package/dist/commonjs/utils/PlaywrightServiceClient.js +0 -25
  35. package/dist/commonjs/utils/PlaywrightServiceClient.js.map +1 -1
  36. package/dist/commonjs/utils/utils.d.ts +2 -3
  37. package/dist/commonjs/utils/utils.d.ts.map +1 -1
  38. package/dist/commonjs/utils/utils.js +8 -22
  39. package/dist/commonjs/utils/utils.js.map +1 -1
  40. package/dist/esm/common/constants.d.ts +1 -4
  41. package/dist/esm/common/constants.d.ts.map +1 -1
  42. package/dist/esm/common/constants.js +92 -96
  43. package/dist/esm/common/constants.js.map +1 -7
  44. package/dist/esm/common/customerConfig.js +11 -11
  45. package/dist/esm/common/customerConfig.js.map +1 -7
  46. package/dist/esm/common/entraIdAccessToken.js +77 -85
  47. package/dist/esm/common/entraIdAccessToken.js.map +1 -7
  48. package/dist/esm/common/environmentVariables.js +19 -19
  49. package/dist/esm/common/environmentVariables.js.map +1 -7
  50. package/dist/esm/common/executor.js +58 -51
  51. package/dist/esm/common/executor.js.map +1 -7
  52. package/dist/esm/common/httpService.js +29 -34
  53. package/dist/esm/common/httpService.js.map +1 -7
  54. package/dist/esm/common/logger.js +4 -4
  55. package/dist/esm/common/logger.js.map +1 -7
  56. package/dist/esm/common/messages.js +166 -166
  57. package/dist/esm/common/messages.js.map +1 -7
  58. package/dist/esm/common/playwrightServiceConfig.js +91 -91
  59. package/dist/esm/common/playwrightServiceConfig.js.map +1 -7
  60. package/dist/esm/common/state.js +7 -7
  61. package/dist/esm/common/state.js.map +1 -7
  62. package/dist/esm/common/types.d.ts +0 -4
  63. package/dist/esm/common/types.d.ts.map +1 -1
  64. package/dist/esm/common/types.js +4 -0
  65. package/dist/esm/common/types.js.map +1 -7
  66. package/dist/esm/core/global/playwright-service-global-setup.js +17 -17
  67. package/dist/esm/core/global/playwright-service-global-setup.js.map +1 -7
  68. package/dist/esm/core/global/playwright-service-global-teardown.js +13 -16
  69. package/dist/esm/core/global/playwright-service-global-teardown.js.map +1 -7
  70. package/dist/esm/core/initializePlaywrightServiceTestRun.js +21 -13
  71. package/dist/esm/core/initializePlaywrightServiceTestRun.js.map +1 -7
  72. package/dist/esm/core/playwrightService.js +200 -149
  73. package/dist/esm/core/playwrightService.js.map +1 -7
  74. package/dist/esm/core/playwrightServiceEntra.js +42 -44
  75. package/dist/esm/core/playwrightServiceEntra.js.map +1 -7
  76. package/dist/esm/core/playwrightServiceUtils.js +8 -6
  77. package/dist/esm/core/playwrightServiceUtils.js.map +1 -7
  78. package/dist/esm/index.js +9 -7
  79. package/dist/esm/index.js.map +1 -7
  80. package/dist/esm/reporter/index.js +11 -4
  81. package/dist/esm/reporter/index.js.map +1 -7
  82. package/dist/esm/reporter/playwrightReporter.d.ts +0 -1
  83. package/dist/esm/reporter/playwrightReporter.d.ts.map +1 -1
  84. package/dist/esm/reporter/playwrightReporter.js +193 -202
  85. package/dist/esm/reporter/playwrightReporter.js.map +1 -7
  86. package/dist/esm/utils/PlaywrightServiceClient.d.ts +1 -2
  87. package/dist/esm/utils/PlaywrightServiceClient.d.ts.map +1 -1
  88. package/dist/esm/utils/PlaywrightServiceClient.js +61 -121
  89. package/dist/esm/utils/PlaywrightServiceClient.js.map +1 -7
  90. package/dist/esm/utils/cIInfoProvider.js +71 -58
  91. package/dist/esm/utils/cIInfoProvider.js.map +1 -7
  92. package/dist/esm/utils/getPackageVersion.js +17 -12
  93. package/dist/esm/utils/getPackageVersion.js.map +1 -7
  94. package/dist/esm/utils/getPlaywrightVersion.js +13 -15
  95. package/dist/esm/utils/getPlaywrightVersion.js.map +1 -7
  96. package/dist/esm/utils/packageManager.js +37 -37
  97. package/dist/esm/utils/packageManager.js.map +1 -7
  98. package/dist/esm/utils/parseJwt.js +14 -15
  99. package/dist/esm/utils/parseJwt.js.map +1 -7
  100. package/dist/esm/utils/playwrightReporterStorageManager.js +333 -358
  101. package/dist/esm/utils/playwrightReporterStorageManager.js.map +1 -7
  102. package/dist/esm/utils/utils.d.ts +2 -3
  103. package/dist/esm/utils/utils.d.ts.map +1 -1
  104. package/dist/esm/utils/utils.js +338 -380
  105. package/dist/esm/utils/utils.js.map +1 -7
  106. package/dist/react-native/common/constants.d.ts +1 -4
  107. package/dist/react-native/common/constants.d.ts.map +1 -1
  108. package/dist/react-native/common/constants.js +1 -4
  109. package/dist/react-native/common/constants.js.map +1 -1
  110. package/dist/react-native/common/types.d.ts +0 -4
  111. package/dist/react-native/common/types.d.ts.map +1 -1
  112. package/dist/react-native/common/types.js.map +1 -1
  113. package/dist/react-native/reporter/playwrightReporter.d.ts +0 -1
  114. package/dist/react-native/reporter/playwrightReporter.d.ts.map +1 -1
  115. package/dist/react-native/reporter/playwrightReporter.js +3 -17
  116. package/dist/react-native/reporter/playwrightReporter.js.map +1 -1
  117. package/dist/react-native/utils/PlaywrightServiceClient.d.ts +1 -2
  118. package/dist/react-native/utils/PlaywrightServiceClient.d.ts.map +1 -1
  119. package/dist/react-native/utils/PlaywrightServiceClient.js +1 -26
  120. package/dist/react-native/utils/PlaywrightServiceClient.js.map +1 -1
  121. package/dist/react-native/utils/utils.d.ts +2 -3
  122. package/dist/react-native/utils/utils.d.ts.map +1 -1
  123. package/dist/react-native/utils/utils.js +8 -21
  124. package/dist/react-native/utils/utils.js.map +1 -1
  125. package/package.json +6 -6
@@ -1,125 +1,112 @@
1
+ // Copyright (c) Microsoft Corporation.
2
+ // Licensed under the MIT License.
1
3
  import { BlobServiceClient } from "@azure/storage-blob";
2
4
  import { coreLogger } from "../common/logger.js";
3
5
  import { readFileSync, writeFileSync, existsSync, createReadStream } from "fs";
4
6
  import { join } from "path";
5
7
  import { UploadConstants } from "../common/constants.js";
6
8
  import { ServiceErrorMessageConstants } from "../common/messages.js";
7
- import {
8
- populateValuesFromServiceUrl,
9
- calculateOptimalConcurrency,
10
- collectAllFiles,
11
- getStorageAccountNameFromUri
12
- } from "./utils.js";
9
+ import { populateValuesFromServiceUrl, calculateOptimalConcurrency, collectAllFiles, getStorageAccountNameFromUri, } from "./utils.js";
13
10
  import { PlaywrightServiceConfig } from "../common/playwrightServiceConfig.js";
14
- class PlaywrightReporterStorageManager {
15
- // Uploads the HTML report folder to Azure Storage
16
- async uploadHtmlReportFolder(credential, runId, outputFolder, workspaceDetails) {
17
- coreLogger.info(
18
- `Starting HTML report upload for runId: ${runId}, outputFolder: ${outputFolder}`
19
- );
20
- const storageAccountName = getStorageAccountNameFromUri(workspaceDetails?.storageUri || "") || "unknown";
21
- try {
22
- if (!workspaceDetails.storageUri) {
23
- coreLogger.error("Storage URI not found in workspace details");
24
- return {
25
- success: false,
26
- errorMessage: ServiceErrorMessageConstants.STORAGE_URI_NOT_FOUND.message
27
- };
28
- }
29
- const blobServiceClient = new BlobServiceClient(workspaceDetails?.storageUri, credential);
30
- coreLogger.info("blobServiceClient created successfully.");
31
- const serviceUrlInfo = populateValuesFromServiceUrl();
32
- if (!serviceUrlInfo?.accountId) {
33
- coreLogger.error("Unable to extract workspace ID from service URL");
34
- return {
35
- success: false,
36
- errorMessage: ServiceErrorMessageConstants.UNABLE_TO_EXTRACT_WORKSPACE_ID.message
37
- };
38
- }
39
- const containerName = serviceUrlInfo.accountId.toLowerCase().replace(/[^a-z0-9-]/g, "-");
40
- const containerClient = blobServiceClient.getContainerClient(containerName);
41
- const containerExists = await containerClient.exists();
42
- if (!containerExists) {
43
- coreLogger.info(`Container ${containerName} does not exist. Creating new container.`);
44
- await containerClient.create();
45
- } else {
46
- coreLogger.info(`Container ${containerName} already exists.`);
47
- }
48
- const folderName = runId;
49
- console.log(
50
- ServiceErrorMessageConstants.UPLOADING_ARTIFACTS.formatWithDetails(
51
- storageAccountName,
52
- containerName,
53
- folderName
54
- )
55
- );
56
- await this.modifyTraceIndexHtml(outputFolder);
57
- const uploadResults = await this.uploadFolderInParallel(
58
- containerClient,
59
- outputFolder,
60
- outputFolder,
61
- folderName
62
- );
63
- if (uploadResults.totalFiles === 0) {
64
- return { success: false, errorMessage: "No files found to upload" };
65
- }
66
- const failedFiles = uploadResults.totalFiles - uploadResults.uploadedFiles.length;
67
- if (failedFiles > 0) {
68
- if (uploadResults.failedFileDetails) {
69
- const hasAuthorizationError = uploadResults.failedFileDetails.some(
70
- (fileDetail) => fileDetail.error.includes("not authorized to perform this operation") || fileDetail.error.includes("AuthorizationFailure")
71
- );
72
- if (hasAuthorizationError) {
73
- return {
74
- success: false,
75
- errorMessage: ServiceErrorMessageConstants.STORAGE_AUTHORIZATION_FAILED.formatWithStorageAccount(
76
- storageAccountName
77
- )
78
- };
79
- }
11
+ export class PlaywrightReporterStorageManager {
12
+ // Uploads the HTML report folder to Azure Storage
13
+ async uploadHtmlReportFolder(credential, runId, outputFolder, workspaceDetails) {
14
+ coreLogger.info(`Starting HTML report upload for runId: ${runId}, outputFolder: ${outputFolder}`);
15
+ const storageAccountName = getStorageAccountNameFromUri(workspaceDetails?.storageUri || "") || "unknown";
16
+ try {
17
+ if (!workspaceDetails.storageUri) {
18
+ coreLogger.error("Storage URI not found in workspace details");
19
+ return {
20
+ success: false,
21
+ errorMessage: ServiceErrorMessageConstants.STORAGE_URI_NOT_FOUND.message,
22
+ };
23
+ }
24
+ const blobServiceClient = new BlobServiceClient(workspaceDetails?.storageUri, credential);
25
+ coreLogger.info("blobServiceClient created successfully.");
26
+ const serviceUrlInfo = populateValuesFromServiceUrl();
27
+ if (!serviceUrlInfo?.accountId) {
28
+ coreLogger.error("Unable to extract workspace ID from service URL");
29
+ return {
30
+ success: false,
31
+ errorMessage: ServiceErrorMessageConstants.UNABLE_TO_EXTRACT_WORKSPACE_ID.message,
32
+ };
33
+ }
34
+ const containerName = serviceUrlInfo.accountId.toLowerCase().replace(/[^a-z0-9-]/g, "-");
35
+ const containerClient = blobServiceClient.getContainerClient(containerName);
36
+ const containerExists = await containerClient.exists();
37
+ if (!containerExists) {
38
+ coreLogger.info(`Container ${containerName} does not exist. Creating new container.`);
39
+ await containerClient.create();
40
+ }
41
+ else {
42
+ coreLogger.info(`Container ${containerName} already exists.`);
43
+ }
44
+ const folderName = runId;
45
+ console.log(ServiceErrorMessageConstants.UPLOADING_ARTIFACTS.formatWithDetails(storageAccountName, containerName, folderName));
46
+ await this.modifyTraceIndexHtml(outputFolder);
47
+ const uploadResults = await this.uploadFolderInParallel(containerClient, outputFolder, outputFolder, folderName);
48
+ if (uploadResults.totalFiles === 0) {
49
+ return { success: false, errorMessage: "No files found to upload" };
50
+ }
51
+ const failedFiles = uploadResults.totalFiles - uploadResults.uploadedFiles.length;
52
+ if (failedFiles > 0) {
53
+ if (uploadResults.failedFileDetails) {
54
+ const hasAuthorizationError = uploadResults.failedFileDetails.some((fileDetail) => fileDetail.error.includes("not authorized to perform this operation") ||
55
+ fileDetail.error.includes("AuthorizationFailure"));
56
+ if (hasAuthorizationError) {
57
+ return {
58
+ success: false,
59
+ errorMessage: ServiceErrorMessageConstants.STORAGE_AUTHORIZATION_FAILED.formatWithStorageAccount(storageAccountName),
60
+ };
61
+ }
62
+ }
63
+ // Get list of failed file names by comparing total files with uploaded files
64
+ const uploadedSet = new Set(uploadResults.uploadedFiles);
65
+ const allFiles = collectAllFiles(outputFolder, outputFolder, folderName);
66
+ const failedFileNames = allFiles
67
+ .filter((file) => !uploadedSet.has(file.relativePath))
68
+ .map((file) => file.relativePath);
69
+ return {
70
+ success: false,
71
+ partialSuccess: true,
72
+ failedFileCount: failedFiles,
73
+ totalFiles: uploadResults.totalFiles,
74
+ failedFiles: failedFileNames,
75
+ failedFileDetails: uploadResults.failedFileDetails,
76
+ };
77
+ }
78
+ return { success: true };
79
+ }
80
+ catch (error) {
81
+ const errorMessage = error instanceof Error ? error.message : String(error);
82
+ const hasStorageAccountDeletedError = errorMessage.includes("ENOTFOUND") ||
83
+ errorMessage.includes("getaddrinfo") ||
84
+ errorMessage.includes("not found") ||
85
+ errorMessage.includes("404");
86
+ if (hasStorageAccountDeletedError) {
87
+ return {
88
+ success: false,
89
+ errorMessage: ServiceErrorMessageConstants.STORAGE_ACCOUNT_DELETED.formatWithStorageAccount(storageAccountName),
90
+ };
91
+ }
92
+ coreLogger.error(`Failed to upload HTML report: ${error}`);
93
+ return { success: false, errorMessage };
80
94
  }
81
- const uploadedSet = new Set(uploadResults.uploadedFiles);
82
- const allFiles = collectAllFiles(outputFolder, outputFolder, folderName);
83
- const failedFileNames = allFiles.filter((file) => !uploadedSet.has(file.relativePath)).map((file) => file.relativePath);
84
- return {
85
- success: false,
86
- partialSuccess: true,
87
- failedFileCount: failedFiles,
88
- totalFiles: uploadResults.totalFiles,
89
- failedFiles: failedFileNames,
90
- failedFileDetails: uploadResults.failedFileDetails
91
- };
92
- }
93
- return { success: true };
94
- } catch (error) {
95
- const errorMessage = error instanceof Error ? error.message : String(error);
96
- const hasStorageAccountDeletedError = errorMessage.includes("ENOTFOUND") || errorMessage.includes("getaddrinfo") || errorMessage.includes("not found") || errorMessage.includes("404");
97
- if (hasStorageAccountDeletedError) {
98
- return {
99
- success: false,
100
- errorMessage: ServiceErrorMessageConstants.STORAGE_ACCOUNT_DELETED.formatWithStorageAccount(
101
- storageAccountName
102
- )
103
- };
104
- }
105
- coreLogger.error(`Failed to upload HTML report: ${error}`);
106
- return { success: false, errorMessage };
107
- }
108
- }
109
- async modifyTraceIndexHtml(outputFolder) {
110
- coreLogger.info(`Starting trace modification for folder: ${outputFolder}`);
111
- const indexPath = join(outputFolder, "trace/index.html");
112
- const localIndexPath = join(outputFolder, "trace/index.local.html");
113
- if (!existsSync(indexPath)) {
114
- coreLogger.error(`trace/index.html not found at path: ${indexPath}`);
115
- return;
116
95
  }
117
- coreLogger.info(`Found trace/index.html at: ${indexPath}`);
118
- try {
119
- const originalHtml = readFileSync(indexPath, "utf-8");
120
- writeFileSync(localIndexPath, originalHtml, "utf-8");
121
- coreLogger.info(`Backed up original trace viewer to: ${localIndexPath}`);
122
- const redirectTraceviewerScript = `<!DOCTYPE html>
96
+ async modifyTraceIndexHtml(outputFolder) {
97
+ coreLogger.info(`Starting trace modification for folder: ${outputFolder}`);
98
+ const indexPath = join(outputFolder, "trace/index.html");
99
+ const localIndexPath = join(outputFolder, "trace/index.local.html");
100
+ if (!existsSync(indexPath)) {
101
+ coreLogger.error(`trace/index.html not found at path: ${indexPath}`);
102
+ return;
103
+ }
104
+ coreLogger.info(`Found trace/index.html at: ${indexPath}`);
105
+ try {
106
+ const originalHtml = readFileSync(indexPath, "utf-8");
107
+ writeFileSync(localIndexPath, originalHtml, "utf-8");
108
+ coreLogger.info(`Backed up original trace viewer to: ${localIndexPath}`);
109
+ const redirectTraceviewerScript = `<!DOCTYPE html>
123
110
  <html>
124
111
  <head>
125
112
  <meta charset="utf-8">
@@ -213,265 +200,253 @@ class PlaywrightReporterStorageManager {
213
200
  location.replace(localViewerUrl.toString());
214
201
  }
215
202
  })();
216
- <\/script>
203
+ </script>
217
204
  </body>
218
205
  </html>
219
206
  `;
220
- writeFileSync(indexPath, redirectTraceviewerScript, "utf-8");
221
- coreLogger.info("Successfully updated TraceViewer index file");
222
- } catch (error) {
223
- coreLogger.error(
224
- `Error modifying trace/index.html: ${error instanceof Error ? error.message : String(error)}`
225
- );
226
- return;
227
- }
228
- }
229
- // Uploads the entire Playwright HTML report folder after tests complete.
230
- async uploadPlaywrightHtmlReportAfterTests(outputFolderName, workspaceMetadata) {
231
- try {
232
- coreLogger.info(
233
- `Starting post-test HTML report upload, folder name: ${outputFolderName || "default (playwright-report)"}`
234
- );
235
- const cred = PlaywrightServiceConfig.instance.credential;
236
- if (!cred) {
237
- coreLogger.error("No credential found for authentication");
238
- return {
239
- success: false,
240
- errorMessage: ServiceErrorMessageConstants.NO_CRED_ENTRA_AUTH_ERROR.message
241
- };
242
- }
243
- coreLogger.info("Credential found for authentication");
244
- const folderName = outputFolderName || "playwright-report";
245
- const outputFolderPath = join(process.cwd(), folderName);
246
- if (!existsSync(outputFolderPath)) {
247
- coreLogger.error(`HTML report folder not found: ${outputFolderPath}`);
248
- return {
249
- success: false,
250
- errorMessage: ServiceErrorMessageConstants.PLAYWRIGHT_TEST_REPORT_NOT_FOUND.formatWithFolder(
251
- folderName
252
- )
253
- };
254
- }
255
- coreLogger.info(`HTML report folder found: ${outputFolderPath}`);
256
- const testRunId = PlaywrightServiceConfig.instance.runId;
257
- coreLogger.info(`Starting upload for test run ID: ${testRunId}`);
258
- const result = await this.uploadHtmlReportFolder(
259
- cred,
260
- testRunId,
261
- outputFolderPath,
262
- workspaceMetadata
263
- );
264
- coreLogger.info(`Completed upload for test run ID: ${testRunId}`);
265
- return result;
266
- } catch (error) {
267
- const errorMessage = error instanceof Error ? error.message : String(error);
268
- coreLogger.error(`Upload failed: ${errorMessage}`);
269
- return { success: false, errorMessage };
207
+ writeFileSync(indexPath, redirectTraceviewerScript, "utf-8");
208
+ coreLogger.info("Successfully updated TraceViewer index file");
209
+ }
210
+ catch (error) {
211
+ coreLogger.error(`Error modifying trace/index.html: ${error instanceof Error ? error.message : String(error)}`);
212
+ return;
213
+ }
270
214
  }
271
- }
272
- // Parallel Upload Engine - Core upload orchestration with performance optimization
273
- async uploadFolderInParallel(containerClient, folderPath, basePath, runIdFolderPrefix) {
274
- const filesToUpload = collectAllFiles(folderPath, basePath, runIdFolderPrefix).sort(
275
- (a, b) => b.size - a.size
276
- );
277
- if (filesToUpload.length === 0) {
278
- return { uploadedFiles: [], failedFiles: [], totalFiles: 0, totalSize: 0, uploadTime: 0 };
215
+ // Uploads the entire Playwright HTML report folder after tests complete.
216
+ async uploadPlaywrightHtmlReportAfterTests(outputFolderName, workspaceMetadata) {
217
+ try {
218
+ coreLogger.info(`Starting post-test HTML report upload, folder name: ${outputFolderName || "default (playwright-report)"}`);
219
+ const cred = PlaywrightServiceConfig.instance.credential;
220
+ if (!cred) {
221
+ coreLogger.error("No credential found for authentication");
222
+ return {
223
+ success: false,
224
+ errorMessage: ServiceErrorMessageConstants.NO_CRED_ENTRA_AUTH_ERROR.message,
225
+ };
226
+ }
227
+ coreLogger.info("Credential found for authentication");
228
+ const folderName = outputFolderName || "playwright-report";
229
+ const outputFolderPath = join(process.cwd(), folderName);
230
+ if (!existsSync(outputFolderPath)) {
231
+ coreLogger.error(`HTML report folder not found: ${outputFolderPath}`);
232
+ return {
233
+ success: false,
234
+ errorMessage: ServiceErrorMessageConstants.PLAYWRIGHT_TEST_REPORT_NOT_FOUND.formatWithFolder(folderName),
235
+ };
236
+ }
237
+ coreLogger.info(`HTML report folder found: ${outputFolderPath}`);
238
+ const testRunId = PlaywrightServiceConfig.instance.runId;
239
+ coreLogger.info(`Starting upload for test run ID: ${testRunId}`);
240
+ const result = await this.uploadHtmlReportFolder(cred, testRunId, outputFolderPath, workspaceMetadata);
241
+ coreLogger.info(`Completed upload for test run ID: ${testRunId}`);
242
+ return result;
243
+ }
244
+ catch (error) {
245
+ const errorMessage = error instanceof Error ? error.message : String(error);
246
+ coreLogger.error(`Upload failed: ${errorMessage}`);
247
+ return { success: false, errorMessage };
248
+ }
279
249
  }
280
- const totalSize = filesToUpload.reduce((sum, file) => sum + file.size, 0);
281
- const concurrency = calculateOptimalConcurrency(filesToUpload);
282
- coreLogger.info(
283
- `Calculated optimal concurrency: ${concurrency} for ${filesToUpload.length} files (total size: ${(totalSize / 1024 / 1024).toFixed(2)} MB)`
284
- );
285
- const uploadStartTime = Date.now();
286
- coreLogger.info(`Starting parallel upload with ${concurrency} concurrent operations`);
287
- const results = await this.uploadWithConcurrencyControl(
288
- containerClient,
289
- filesToUpload,
290
- concurrency
291
- );
292
- const uploadEndTime = Date.now();
293
- const uploadTime = uploadEndTime - uploadStartTime;
294
- coreLogger.info(`Upload completed in ${uploadTime}ms (${(uploadTime / 1e3).toFixed(2)}s)`);
295
- const failed = results.filter((r) => r.status === "rejected").length;
296
- const successful = results.filter((r) => r.status === "fulfilled").length;
297
- coreLogger.info(
298
- `Upload results: ${successful} successful, ${failed} failed out of ${results.length} total files`
299
- );
300
- if (failed > 0) {
301
- const errors = results.filter((r) => r.status === "rejected").map((r) => r.reason.message).slice(0, 5);
302
- errors.forEach((error, index) => {
303
- coreLogger.error(` ${index + 1}. ${error}`);
304
- });
305
- const uploadedSet = new Set(
306
- results.filter((r) => r.status === "fulfilled").map((r) => r.value)
307
- );
308
- const failedFileNames = filesToUpload.filter((file) => !uploadedSet.has(file.relativePath)).map((file) => file.relativePath);
309
- const failedFileDetails = [];
310
- results.forEach((result, index) => {
311
- if (result.status === "rejected") {
312
- const fileName = filesToUpload[index]?.relativePath || `File at index ${index}`;
313
- const errorMessage = result.reason instanceof Error ? result.reason.message : String(result.reason);
314
- failedFileDetails.push({ fileName, error: errorMessage });
250
+ // Parallel Upload Engine - Core upload orchestration with performance optimization
251
+ async uploadFolderInParallel(containerClient, folderPath, basePath, runIdFolderPrefix) {
252
+ // Sort by size descending - upload large files first for better parallelization
253
+ const filesToUpload = collectAllFiles(folderPath, basePath, runIdFolderPrefix).sort((a, b) => b.size - a.size);
254
+ if (filesToUpload.length === 0) {
255
+ return { uploadedFiles: [], failedFiles: [], totalFiles: 0, totalSize: 0, uploadTime: 0 };
256
+ }
257
+ const totalSize = filesToUpload.reduce((sum, file) => sum + file.size, 0);
258
+ const concurrency = calculateOptimalConcurrency(filesToUpload);
259
+ coreLogger.info(`Calculated optimal concurrency: ${concurrency} for ${filesToUpload.length} files (total size: ${(totalSize / 1024 / 1024).toFixed(2)} MB)`);
260
+ const uploadStartTime = Date.now();
261
+ coreLogger.info(`Starting parallel upload with ${concurrency} concurrent operations`);
262
+ const results = await this.uploadWithConcurrencyControl(containerClient, filesToUpload, concurrency);
263
+ const uploadEndTime = Date.now();
264
+ const uploadTime = uploadEndTime - uploadStartTime;
265
+ coreLogger.info(`Upload completed in ${uploadTime}ms (${(uploadTime / 1000).toFixed(2)}s)`);
266
+ const failed = results.filter((r) => r.status === "rejected").length;
267
+ const successful = results.filter((r) => r.status === "fulfilled").length;
268
+ coreLogger.info(`Upload results: ${successful} successful, ${failed} failed out of ${results.length} total files`);
269
+ if (failed > 0) {
270
+ const errors = results
271
+ .filter((r) => r.status === "rejected")
272
+ .map((r) => r.reason.message)
273
+ .slice(0, 5); // Show first 5 errors
274
+ errors.forEach((error, index) => {
275
+ coreLogger.error(` ${index + 1}. ${error}`);
276
+ });
277
+ // Get failed file names and their error messages
278
+ const uploadedSet = new Set(results
279
+ .filter((r) => r.status === "fulfilled")
280
+ .map((r) => r.value));
281
+ const failedFileNames = filesToUpload
282
+ .filter((file) => !uploadedSet.has(file.relativePath))
283
+ .map((file) => file.relativePath);
284
+ // Create detailed error mapping
285
+ const failedFileDetails = [];
286
+ results.forEach((result, index) => {
287
+ if (result.status === "rejected") {
288
+ const fileName = filesToUpload[index]?.relativePath || `File at index ${index}`;
289
+ const errorMessage = result.reason instanceof Error ? result.reason.message : String(result.reason);
290
+ failedFileDetails.push({ fileName, error: errorMessage });
291
+ }
292
+ });
293
+ // Log error but don't throw to prevent breaking HTML reporter
294
+ coreLogger.error(`Upload failed: ${failed} files could not be uploaded. Sample errors: ${errors.join(", ")}`);
295
+ return {
296
+ uploadedFiles: results
297
+ .filter((r) => r.status === "fulfilled")
298
+ .map((r) => r.value),
299
+ failedFiles: failedFileNames,
300
+ failedFileDetails,
301
+ totalFiles: filesToUpload.length,
302
+ totalSize,
303
+ uploadTime,
304
+ };
315
305
  }
316
- });
317
- coreLogger.error(
318
- `Upload failed: ${failed} files could not be uploaded. Sample errors: ${errors.join(", ")}`
319
- );
320
- return {
321
- uploadedFiles: results.filter((r) => r.status === "fulfilled").map((r) => r.value),
322
- failedFiles: failedFileNames,
323
- failedFileDetails,
324
- totalFiles: filesToUpload.length,
325
- totalSize,
326
- uploadTime
327
- };
306
+ const uploadedFiles = results
307
+ .filter((r) => r.status === "fulfilled")
308
+ .map((r) => r.value);
309
+ return {
310
+ uploadedFiles,
311
+ failedFiles: [],
312
+ totalFiles: filesToUpload.length,
313
+ totalSize,
314
+ uploadTime,
315
+ };
328
316
  }
329
- const uploadedFiles = results.filter((r) => r.status === "fulfilled").map((r) => r.value);
330
- return {
331
- uploadedFiles,
332
- failedFiles: [],
333
- totalFiles: filesToUpload.length,
334
- totalSize,
335
- uploadTime
336
- };
337
- }
338
- // Concurrency Control System - Manages parallel execution with controlled resource usage
339
- async uploadWithConcurrencyControl(containerClient, files, concurrency) {
340
- const uploadTasks = files.map((fileInfo) => async () => {
341
- try {
342
- await this.uploadSingleFileOptimized(containerClient, fileInfo);
343
- return fileInfo.relativePath;
344
- } catch (error) {
345
- coreLogger.error(
346
- `Failed to upload file: ${fileInfo.relativePath} - ${error instanceof Error ? error.message : "Unknown error"}`
347
- );
348
- throw error;
349
- }
350
- });
351
- return this.executeWithOptimizedBatching(uploadTasks, concurrency);
352
- }
353
- // Optimized Batch Execution Engine - High-performance task processing system
354
- async executeWithOptimizedBatching(tasks, concurrency) {
355
- const results = new Array(tasks.length);
356
- let completedTasks = 0;
357
- const batchSize = Math.min(UploadConstants.BATCH_SIZE, concurrency * 2);
358
- const batches = [];
359
- for (let i = 0; i < tasks.length; i += batchSize) {
360
- batches.push(tasks.slice(i, i + batchSize));
317
+ // Concurrency Control System - Manages parallel execution with controlled resource usage
318
+ async uploadWithConcurrencyControl(containerClient, files, concurrency) {
319
+ const uploadTasks = files.map((fileInfo) => async () => {
320
+ try {
321
+ await this.uploadSingleFileOptimized(containerClient, fileInfo);
322
+ return fileInfo.relativePath;
323
+ }
324
+ catch (error) {
325
+ coreLogger.error(`Failed to upload file: ${fileInfo.relativePath} - ${error instanceof Error ? error.message : "Unknown error"}`);
326
+ throw error;
327
+ }
328
+ });
329
+ return this.executeWithOptimizedBatching(uploadTasks, concurrency);
361
330
  }
362
- coreLogger.info(`Processing ${batches.length} batches with ${batchSize} tasks per batch`);
363
- for (let batchIndex = 0; batchIndex < batches.length; batchIndex++) {
364
- const batch = batches[batchIndex];
365
- const batchStartIndex = batchIndex * batchSize;
366
- coreLogger.info(
367
- `Starting batch ${batchIndex + 1}/${batches.length} with ${batch.length} tasks`
368
- );
369
- const batchPromises = batch.map(async (task, taskIndex) => {
370
- const globalIndex = batchStartIndex + taskIndex;
371
- try {
372
- const result = await task();
373
- results[globalIndex] = { status: "fulfilled", value: result };
374
- return result;
375
- } catch (error) {
376
- results[globalIndex] = { status: "rejected", reason: error };
377
- coreLogger.error(
378
- `Task failed at index ${globalIndex}: ${error instanceof Error ? error.message : "Unknown error"}`
379
- );
380
- return;
331
+ // Optimized Batch Execution Engine - High-performance task processing system
332
+ async executeWithOptimizedBatching(tasks, concurrency) {
333
+ const results = new Array(tasks.length);
334
+ let completedTasks = 0;
335
+ // Splits tasks into optimal batches to balance memory usage vs. throughput
336
+ const batchSize = Math.min(UploadConstants.BATCH_SIZE, concurrency * 2);
337
+ const batches = [];
338
+ // Each batch will be processed with full concurrency before moving to next batch
339
+ for (let i = 0; i < tasks.length; i += batchSize) {
340
+ batches.push(tasks.slice(i, i + batchSize));
381
341
  }
382
- });
383
- const executing = [];
384
- for (const promise of batchPromises) {
385
- if (executing.length >= concurrency) {
386
- await Promise.race(executing);
342
+ // Process each batch sequentially to maintain memory efficiency
343
+ coreLogger.info(`Processing ${batches.length} batches with ${batchSize} tasks per batch`);
344
+ for (let batchIndex = 0; batchIndex < batches.length; batchIndex++) {
345
+ const batch = batches[batchIndex];
346
+ const batchStartIndex = batchIndex * batchSize;
347
+ coreLogger.info(`Starting batch ${batchIndex + 1}/${batches.length} with ${batch.length} tasks`);
348
+ // Transform each task into a promise that captures results at correct index
349
+ const batchPromises = batch.map(async (task, taskIndex) => {
350
+ const globalIndex = batchStartIndex + taskIndex;
351
+ try {
352
+ const result = await task();
353
+ results[globalIndex] = { status: "fulfilled", value: result };
354
+ return result;
355
+ }
356
+ catch (error) {
357
+ results[globalIndex] = { status: "rejected", reason: error };
358
+ coreLogger.error(`Task failed at index ${globalIndex}: ${error instanceof Error ? error.message : "Unknown error"}`);
359
+ return;
360
+ }
361
+ });
362
+ const executing = [];
363
+ for (const promise of batchPromises) {
364
+ if (executing.length >= concurrency) {
365
+ await Promise.race(executing);
366
+ }
367
+ const wrappedPromise = promise
368
+ .then((result) => {
369
+ completedTasks++;
370
+ return result;
371
+ })
372
+ .catch(() => {
373
+ completedTasks++;
374
+ })
375
+ .finally(() => {
376
+ // Cleanup: remove completed promise from executing array
377
+ const index = executing.indexOf(wrappedPromise);
378
+ if (index > -1)
379
+ executing.splice(index, 1);
380
+ });
381
+ executing.push(wrappedPromise);
382
+ }
383
+ // Wait for all promises in current batch to complete before moving to next batch
384
+ await Promise.allSettled(executing);
385
+ coreLogger.info(`Completed batch ${batchIndex + 1}/${batches.length}`);
387
386
  }
388
- const wrappedPromise = promise.then((result) => {
389
- completedTasks++;
390
- return result;
391
- }).catch(() => {
392
- completedTasks++;
393
- }).finally(() => {
394
- const index = executing.indexOf(wrappedPromise);
395
- if (index > -1) executing.splice(index, 1);
396
- });
397
- executing.push(wrappedPromise);
398
- }
399
- await Promise.allSettled(executing);
400
- coreLogger.info(`Completed batch ${batchIndex + 1}/${batches.length}`);
387
+ return results;
401
388
  }
402
- return results;
403
- }
404
- // Individual File Upload with Retry Logic
405
- async uploadSingleFileOptimized(containerClient, fileInfo) {
406
- coreLogger.info(
407
- `Uploading file: ${fileInfo.relativePath} (${(fileInfo.size / 1024).toFixed(2)} KB, ${fileInfo.contentType})`
408
- );
409
- const blockBlobClient = containerClient.getBlockBlobClient(fileInfo.relativePath);
410
- const maxRetries = UploadConstants.MAX_RETRY_ATTEMPTS;
411
- const baseDelay = UploadConstants.RETRY_BASE_DELAY;
412
- for (let attempt = 1; attempt <= maxRetries; attempt++) {
413
- try {
414
- await this.performOptimizedUpload(blockBlobClient, fileInfo);
415
- return;
416
- } catch (error) {
417
- const isLastAttempt = attempt === maxRetries;
418
- const errorMessage = error instanceof Error ? error.message : "Unknown error";
419
- coreLogger.info(
420
- `Upload attempt ${attempt} failed for ${fileInfo.relativePath}: ${errorMessage}`
421
- );
422
- if (isLastAttempt) {
423
- coreLogger.error(
424
- `All retry attempts exhausted for ${fileInfo.relativePath}: ${errorMessage}`
425
- );
426
- if (error instanceof Error) {
427
- throw error;
428
- }
429
- throw new Error(
430
- `Upload failed for ${fileInfo.relativePath} after ${maxRetries} attempts: ${errorMessage}`
431
- );
389
+ // Individual File Upload with Retry Logic
390
+ async uploadSingleFileOptimized(containerClient, fileInfo) {
391
+ coreLogger.info(`Uploading file: ${fileInfo.relativePath} (${(fileInfo.size / 1024).toFixed(2)} KB, ${fileInfo.contentType})`);
392
+ const blockBlobClient = containerClient.getBlockBlobClient(fileInfo.relativePath);
393
+ const maxRetries = UploadConstants.MAX_RETRY_ATTEMPTS;
394
+ const baseDelay = UploadConstants.RETRY_BASE_DELAY;
395
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
396
+ try {
397
+ await this.performOptimizedUpload(blockBlobClient, fileInfo);
398
+ return;
399
+ }
400
+ catch (error) {
401
+ const isLastAttempt = attempt === maxRetries;
402
+ const errorMessage = error instanceof Error ? error.message : "Unknown error";
403
+ coreLogger.info(`Upload attempt ${attempt} failed for ${fileInfo.relativePath}: ${errorMessage}`);
404
+ if (isLastAttempt) {
405
+ coreLogger.error(`All retry attempts exhausted for ${fileInfo.relativePath}: ${errorMessage}`);
406
+ if (error instanceof Error) {
407
+ throw error;
408
+ }
409
+ throw new Error(`Upload failed for ${fileInfo.relativePath} after ${maxRetries} attempts: ${errorMessage}`);
410
+ }
411
+ // Exponential backoff with jitter (Azure SDK pattern)
412
+ const delay = baseDelay * Math.pow(2, attempt - 1) + Math.random() * 500;
413
+ coreLogger.info(`Retrying upload for ${fileInfo.relativePath} in ${delay.toFixed(0)}ms (attempt ${attempt + 1}/${maxRetries})`);
414
+ await new Promise((_resolve) => setTimeout(_resolve, delay));
415
+ }
432
416
  }
433
- const delay = baseDelay * Math.pow(2, attempt - 1) + Math.random() * 500;
434
- coreLogger.info(
435
- `Retrying upload for ${fileInfo.relativePath} in ${delay.toFixed(0)}ms (attempt ${attempt + 1}/${maxRetries})`
436
- );
437
- await new Promise((_resolve) => setTimeout(_resolve, delay));
438
- }
439
417
  }
440
- }
441
- // Multi-Strategy Upload Engine - Optimized upload based on file characteristics
442
- async performOptimizedUpload(blockBlobClient, fileInfo) {
443
- if (fileInfo.size <= UploadConstants.SMALL_FILE_THRESHOLD) {
444
- const fileContent = readFileSync(fileInfo.fullPath);
445
- await blockBlobClient.upload(fileContent, fileContent.length, {
446
- blobHTTPHeaders: {
447
- blobContentType: fileInfo.contentType
418
+ // Multi-Strategy Upload Engine - Optimized upload based on file characteristics
419
+ async performOptimizedUpload(blockBlobClient, fileInfo) {
420
+ if (fileInfo.size <= UploadConstants.SMALL_FILE_THRESHOLD) {
421
+ // DIRECT UPLOAD: Optimal for small files (≤1MB)
422
+ const fileContent = readFileSync(fileInfo.fullPath);
423
+ await blockBlobClient.upload(fileContent, fileContent.length, {
424
+ blobHTTPHeaders: {
425
+ blobContentType: fileInfo.contentType,
426
+ },
427
+ });
448
428
  }
449
- });
450
- } else if (fileInfo.size <= UploadConstants.LARGE_FILE_THRESHOLD) {
451
- const fileContent = readFileSync(fileInfo.fullPath);
452
- await blockBlobClient.uploadData(fileContent, {
453
- blobHTTPHeaders: {
454
- blobContentType: fileInfo.contentType
455
- },
456
- blockSize: UploadConstants.OPTIMIZED_BLOCK_SIZE,
457
- concurrency: UploadConstants.PER_FILE_CONCURRENCY
458
- });
459
- } else {
460
- const stream = createReadStream(fileInfo.fullPath);
461
- await blockBlobClient.uploadStream(
462
- stream,
463
- UploadConstants.STREAM_BUFFER_SIZE,
464
- UploadConstants.LARGE_FILE_CONCURRENCY,
465
- {
466
- blobHTTPHeaders: {
467
- blobContentType: fileInfo.contentType
468
- }
429
+ else if (fileInfo.size <= UploadConstants.LARGE_FILE_THRESHOLD) {
430
+ // BLOCK UPLOAD: Optimal for medium files (1MB - 100MB)
431
+ const fileContent = readFileSync(fileInfo.fullPath);
432
+ await blockBlobClient.uploadData(fileContent, {
433
+ blobHTTPHeaders: {
434
+ blobContentType: fileInfo.contentType,
435
+ },
436
+ blockSize: UploadConstants.OPTIMIZED_BLOCK_SIZE,
437
+ concurrency: UploadConstants.PER_FILE_CONCURRENCY,
438
+ });
439
+ }
440
+ else {
441
+ // STREAMING UPLOAD: Optimal for large files (>100MB)
442
+ const stream = createReadStream(fileInfo.fullPath);
443
+ await blockBlobClient.uploadStream(stream, UploadConstants.STREAM_BUFFER_SIZE, UploadConstants.LARGE_FILE_CONCURRENCY, {
444
+ blobHTTPHeaders: {
445
+ blobContentType: fileInfo.contentType,
446
+ },
447
+ });
469
448
  }
470
- );
449
+ coreLogger.info(`Successfully uploaded: ${fileInfo.relativePath}`);
471
450
  }
472
- coreLogger.info(`Successfully uploaded: ${fileInfo.relativePath}`);
473
- }
474
451
  }
475
- export {
476
- PlaywrightReporterStorageManager
477
- };
452
+ //# sourceMappingURL=playwrightReporterStorageManager.js.map