@azure/playwright 1.1.2 → 1.1.3-alpha.20260310.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/dist/browser/common/types.d.ts +1 -1
- package/dist/browser/common/types.d.ts.map +1 -1
- package/dist/browser/common/types.js.map +1 -1
- package/dist/browser/index.d.ts +1 -1
- package/dist/browser/index.d.ts.map +1 -1
- package/dist/browser/index.js.map +1 -1
- package/dist/browser/utils/PlaywrightServiceClient.d.ts +1 -1
- package/dist/browser/utils/PlaywrightServiceClient.d.ts.map +1 -1
- package/dist/browser/utils/PlaywrightServiceClient.js.map +1 -1
- package/dist/browser/utils/playwrightReporterStorageManager.d.ts.map +1 -1
- package/dist/browser/utils/playwrightReporterStorageManager.js.map +1 -1
- package/dist/browser/utils/utils.d.ts +2 -2
- package/dist/browser/utils/utils.d.ts.map +1 -1
- package/dist/browser/utils/utils.js.map +1 -1
- package/dist/commonjs/common/constants.js +127 -95
- package/dist/commonjs/common/constants.js.map +7 -1
- package/dist/commonjs/common/customerConfig.js +31 -14
- package/dist/commonjs/common/customerConfig.js.map +7 -1
- package/dist/commonjs/common/entraIdAccessToken.js +106 -83
- package/dist/commonjs/common/entraIdAccessToken.js.map +7 -1
- package/dist/commonjs/common/environmentVariables.js +43 -24
- package/dist/commonjs/common/environmentVariables.js.map +7 -1
- package/dist/commonjs/common/executor.js +88 -67
- package/dist/commonjs/common/executor.js.map +7 -1
- package/dist/commonjs/common/httpService.js +54 -35
- package/dist/commonjs/common/httpService.js.map +7 -1
- package/dist/commonjs/common/logger.js +28 -8
- package/dist/commonjs/common/logger.js.map +7 -1
- package/dist/commonjs/common/messages.js +189 -169
- package/dist/commonjs/common/messages.js.map +7 -1
- package/dist/commonjs/common/playwrightServiceConfig.js +113 -98
- package/dist/commonjs/common/playwrightServiceConfig.js.map +7 -1
- package/dist/commonjs/common/state.js +30 -10
- package/dist/commonjs/common/state.js.map +7 -1
- package/dist/commonjs/common/types.d.ts +1 -1
- package/dist/commonjs/common/types.d.ts.map +1 -1
- package/dist/commonjs/common/types.js +15 -5
- package/dist/commonjs/common/types.js.map +7 -1
- package/dist/commonjs/core/global/playwright-service-global-setup.js +52 -26
- package/dist/commonjs/core/global/playwright-service-global-setup.js.map +7 -1
- package/dist/commonjs/core/global/playwright-service-global-teardown.js +49 -20
- package/dist/commonjs/core/global/playwright-service-global-teardown.js.map +7 -1
- package/dist/commonjs/core/initializePlaywrightServiceTestRun.js +39 -27
- package/dist/commonjs/core/initializePlaywrightServiceTestRun.js.map +7 -1
- package/dist/commonjs/core/playwrightService.js +166 -205
- package/dist/commonjs/core/playwrightService.js.map +7 -1
- package/dist/commonjs/core/playwrightServiceEntra.js +67 -48
- package/dist/commonjs/core/playwrightServiceEntra.js.map +7 -1
- package/dist/commonjs/core/playwrightServiceUtils.d.ts +1 -1
- package/dist/commonjs/core/playwrightServiceUtils.d.ts.map +1 -0
- package/dist/commonjs/core/playwrightServiceUtils.js +44 -11
- package/dist/commonjs/core/playwrightServiceUtils.js.map +7 -0
- package/dist/commonjs/index.d.ts +1 -1
- package/dist/commonjs/index.d.ts.map +1 -1
- package/dist/commonjs/index.js +36 -18
- package/dist/commonjs/index.js.map +7 -1
- package/dist/commonjs/reporter/index.js +34 -15
- package/dist/commonjs/reporter/index.js.map +7 -1
- package/dist/commonjs/reporter/playwrightReporter.js +222 -216
- package/dist/commonjs/reporter/playwrightReporter.js.map +7 -1
- package/dist/commonjs/tsdoc-metadata.json +1 -1
- package/dist/commonjs/utils/PlaywrightServiceClient.d.ts +1 -1
- package/dist/commonjs/utils/PlaywrightServiceClient.d.ts.map +1 -1
- package/dist/commonjs/utils/PlaywrightServiceClient.js +140 -91
- package/dist/commonjs/utils/PlaywrightServiceClient.js.map +7 -1
- package/dist/commonjs/utils/cIInfoProvider.js +81 -74
- package/dist/commonjs/utils/cIInfoProvider.js.map +7 -1
- package/dist/commonjs/utils/getPackageVersion.d.ts +1 -1
- package/dist/commonjs/utils/getPackageVersion.d.ts.map +1 -0
- package/dist/commonjs/utils/getPackageVersion.js +50 -19
- package/dist/commonjs/utils/getPackageVersion.js.map +7 -0
- package/dist/commonjs/utils/getPlaywrightVersion.js +42 -21
- package/dist/commonjs/utils/getPlaywrightVersion.js.map +7 -1
- package/dist/commonjs/utils/packageManager.js +59 -40
- package/dist/commonjs/utils/packageManager.js.map +7 -1
- package/dist/commonjs/utils/parseJwt.js +37 -17
- package/dist/commonjs/utils/parseJwt.js.map +7 -1
- package/dist/commonjs/utils/playwrightReporterStorageManager.d.ts.map +1 -1
- package/dist/commonjs/utils/playwrightReporterStorageManager.js +381 -342
- package/dist/commonjs/utils/playwrightReporterStorageManager.js.map +7 -1
- package/dist/commonjs/utils/utils.d.ts +2 -2
- package/dist/commonjs/utils/utils.d.ts.map +1 -1
- package/dist/commonjs/utils/utils.js +417 -369
- package/dist/commonjs/utils/utils.js.map +7 -1
- package/dist/esm/common/constants.js +92 -92
- package/dist/esm/common/constants.js.map +7 -1
- package/dist/esm/common/customerConfig.js +11 -11
- package/dist/esm/common/customerConfig.js.map +7 -1
- package/dist/esm/common/entraIdAccessToken.js +85 -77
- package/dist/esm/common/entraIdAccessToken.js.map +7 -1
- package/dist/esm/common/environmentVariables.js +19 -19
- package/dist/esm/common/environmentVariables.js.map +7 -1
- package/dist/esm/common/executor.js +51 -58
- package/dist/esm/common/executor.js.map +7 -1
- package/dist/esm/common/httpService.js +34 -29
- package/dist/esm/common/httpService.js.map +7 -1
- package/dist/esm/common/logger.js +4 -4
- package/dist/esm/common/logger.js.map +7 -1
- package/dist/esm/common/messages.js +166 -166
- package/dist/esm/common/messages.js.map +7 -1
- package/dist/esm/common/playwrightServiceConfig.js +91 -91
- package/dist/esm/common/playwrightServiceConfig.js.map +7 -1
- package/dist/esm/common/state.js +7 -7
- package/dist/esm/common/state.js.map +7 -1
- package/dist/esm/common/types.d.ts +1 -1
- package/dist/esm/common/types.d.ts.map +1 -1
- package/dist/esm/common/types.js +0 -4
- package/dist/esm/common/types.js.map +7 -1
- package/dist/esm/core/global/playwright-service-global-setup.js +17 -17
- package/dist/esm/core/global/playwright-service-global-setup.js.map +7 -1
- package/dist/esm/core/global/playwright-service-global-teardown.js +16 -13
- package/dist/esm/core/global/playwright-service-global-teardown.js.map +7 -1
- package/dist/esm/core/initializePlaywrightServiceTestRun.js +13 -21
- package/dist/esm/core/initializePlaywrightServiceTestRun.js.map +7 -1
- package/dist/esm/core/playwrightService.js +144 -195
- package/dist/esm/core/playwrightService.js.map +7 -1
- package/dist/esm/core/playwrightServiceEntra.js +44 -42
- package/dist/esm/core/playwrightServiceEntra.js.map +7 -1
- package/dist/esm/core/playwrightServiceUtils.js +6 -8
- package/dist/esm/core/playwrightServiceUtils.js.map +7 -1
- package/dist/esm/index.d.ts +1 -1
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js +7 -9
- package/dist/esm/index.js.map +7 -1
- package/dist/esm/reporter/index.js +4 -11
- package/dist/esm/reporter/index.js.map +7 -1
- package/dist/esm/reporter/playwrightReporter.js +202 -207
- package/dist/esm/reporter/playwrightReporter.js.map +7 -1
- package/dist/esm/utils/PlaywrightServiceClient.d.ts +1 -1
- package/dist/esm/utils/PlaywrightServiceClient.d.ts.map +1 -1
- package/dist/esm/utils/PlaywrightServiceClient.js +120 -85
- package/dist/esm/utils/PlaywrightServiceClient.js.map +7 -1
- package/dist/esm/utils/cIInfoProvider.js +58 -71
- package/dist/esm/utils/cIInfoProvider.js.map +7 -1
- package/dist/esm/utils/getPackageVersion.js +12 -17
- package/dist/esm/utils/getPackageVersion.js.map +7 -1
- package/dist/esm/utils/getPlaywrightVersion.js +15 -13
- package/dist/esm/utils/getPlaywrightVersion.js.map +7 -1
- package/dist/esm/utils/packageManager.js +37 -37
- package/dist/esm/utils/packageManager.js.map +7 -1
- package/dist/esm/utils/parseJwt.js +15 -14
- package/dist/esm/utils/parseJwt.js.map +7 -1
- package/dist/esm/utils/playwrightReporterStorageManager.d.ts.map +1 -1
- package/dist/esm/utils/playwrightReporterStorageManager.js +358 -333
- package/dist/esm/utils/playwrightReporterStorageManager.js.map +7 -1
- package/dist/esm/utils/utils.d.ts +2 -2
- package/dist/esm/utils/utils.d.ts.map +1 -1
- package/dist/esm/utils/utils.js +377 -350
- package/dist/esm/utils/utils.js.map +7 -1
- package/dist/react-native/common/types.d.ts +1 -1
- package/dist/react-native/common/types.d.ts.map +1 -1
- package/dist/react-native/common/types.js.map +1 -1
- package/dist/react-native/index.d.ts +1 -1
- package/dist/react-native/index.d.ts.map +1 -1
- package/dist/react-native/index.js.map +1 -1
- package/dist/react-native/utils/PlaywrightServiceClient.d.ts +1 -1
- package/dist/react-native/utils/PlaywrightServiceClient.d.ts.map +1 -1
- package/dist/react-native/utils/PlaywrightServiceClient.js.map +1 -1
- package/dist/react-native/utils/playwrightReporterStorageManager.d.ts.map +1 -1
- package/dist/react-native/utils/playwrightReporterStorageManager.js.map +1 -1
- package/dist/react-native/utils/utils.d.ts +2 -2
- package/dist/react-native/utils/utils.d.ts.map +1 -1
- package/dist/react-native/utils/utils.js.map +1 -1
- package/package.json +6 -24
- package/dist/commonjs/core/playwrightServiceUtils-cjs.cjs.map +0 -1
- package/dist/commonjs/core/playwrightServiceUtils-cjs.d.cts.map +0 -1
- package/dist/commonjs/utils/getPackageVersion-cjs.cjs.map +0 -1
- package/dist/commonjs/utils/getPackageVersion-cjs.d.cts.map +0 -1
|
@@ -1,112 +1,125 @@
|
|
|
1
|
-
// Copyright (c) Microsoft Corporation.
|
|
2
|
-
// Licensed under the MIT License.
|
|
3
1
|
import { BlobServiceClient } from "@azure/storage-blob";
|
|
4
2
|
import { coreLogger } from "../common/logger.js";
|
|
5
3
|
import { readFileSync, writeFileSync, existsSync, createReadStream } from "fs";
|
|
6
4
|
import { join } from "path";
|
|
7
5
|
import { UploadConstants } from "../common/constants.js";
|
|
8
6
|
import { ServiceErrorMessageConstants } from "../common/messages.js";
|
|
9
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
populateValuesFromServiceUrl,
|
|
9
|
+
calculateOptimalConcurrency,
|
|
10
|
+
collectAllFiles,
|
|
11
|
+
getStorageAccountNameFromUri
|
|
12
|
+
} from "./utils.js";
|
|
10
13
|
import { PlaywrightServiceConfig } from "../common/playwrightServiceConfig.js";
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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 };
|
|
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
|
+
}
|
|
94
80
|
}
|
|
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 };
|
|
95
107
|
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
+
}
|
|
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>
|
|
110
123
|
<html>
|
|
111
124
|
<head>
|
|
112
125
|
<meta charset="utf-8">
|
|
@@ -200,253 +213,265 @@ export class PlaywrightReporterStorageManager {
|
|
|
200
213
|
location.replace(localViewerUrl.toString());
|
|
201
214
|
}
|
|
202
215
|
})();
|
|
203
|
-
|
|
216
|
+
<\/script>
|
|
204
217
|
</body>
|
|
205
218
|
</html>
|
|
206
219
|
`;
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
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;
|
|
214
227
|
}
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
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
|
-
}
|
|
249
|
-
}
|
|
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
|
-
};
|
|
305
|
-
}
|
|
306
|
-
const uploadedFiles = results
|
|
307
|
-
.filter((r) => r.status === "fulfilled")
|
|
308
|
-
.map((r) => r.value);
|
|
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}`);
|
|
309
248
|
return {
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
uploadTime,
|
|
249
|
+
success: false,
|
|
250
|
+
errorMessage: ServiceErrorMessageConstants.PLAYWRIGHT_TEST_REPORT_NOT_FOUND.formatWithFolder(
|
|
251
|
+
folderName
|
|
252
|
+
)
|
|
315
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 };
|
|
316
270
|
}
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
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);
|
|
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 };
|
|
330
279
|
}
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
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}`);
|
|
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 });
|
|
386
315
|
}
|
|
387
|
-
|
|
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
|
+
};
|
|
328
|
+
}
|
|
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));
|
|
388
361
|
}
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
await new Promise((_resolve) => setTimeout(_resolve, delay));
|
|
415
|
-
}
|
|
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;
|
|
381
|
+
}
|
|
382
|
+
});
|
|
383
|
+
const executing = [];
|
|
384
|
+
for (const promise of batchPromises) {
|
|
385
|
+
if (executing.length >= concurrency) {
|
|
386
|
+
await Promise.race(executing);
|
|
416
387
|
}
|
|
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}`);
|
|
417
401
|
}
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
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
|
+
);
|
|
428
432
|
}
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
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
|
+
}
|
|
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
|
|
439
448
|
}
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
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
|
+
}
|
|
448
469
|
}
|
|
449
|
-
|
|
470
|
+
);
|
|
450
471
|
}
|
|
472
|
+
coreLogger.info(`Successfully uploaded: ${fileInfo.relativePath}`);
|
|
473
|
+
}
|
|
451
474
|
}
|
|
452
|
-
|
|
475
|
+
export {
|
|
476
|
+
PlaywrightReporterStorageManager
|
|
477
|
+
};
|