UNPKG

@azure/microsoft-playwright-testing

Version:

Package to integrate your Playwright test suite with Microsoft Playwright Testing service

376 lines 18.7 kB
"use strict"; // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. Object.defineProperty(exports, "__esModule", { value: true }); const tslib_1 = require("tslib"); const logger_1 = require("../common/logger"); const constants_1 = require("../common/constants"); const environmentVariables_1 = require("../common/environmentVariables"); const multimap_1 = require("../common/multimap"); const mptTokenDetails_1 = require("../model/mptTokenDetails"); const cIInfoProvider_1 = require("../utils/cIInfoProvider"); const reporterUtils_1 = tslib_1.__importDefault(require("../utils/reporterUtils")); const serviceClient_1 = require("../utils/serviceClient"); const storageClient_1 = require("../utils/storageClient"); const messages_1 = require("../common/messages"); const utils_1 = require("../utils/utils"); /** * @public * * Extends Playwright's Reporter class to enable Microsoft Playwright Testing's Reporting * feature to publish test results and related artifacts and * view them in the service portal for faster and easier troubleshooting. * * @example * * ``` * import { defineConfig } from "@playwright/test"; * * export default defineConfig({ * reporter: [["@azure/microsoft-playwright-testing/reporter"]] * }); * ``` */ class MPTReporter { constructor(config) { this.isTokenValid = true; this.enableGitHubSummary = true; this.isRegionValid = true; this.isTestRunStartSuccess = false; this.ciInfo = cIInfoProvider_1.CIInfoProvider.getCIInfo(); this.testRawResults = new multimap_1.MultiMap(); this._testEndPromises = []; this.testResultBatch = new Set(); this.errorMessages = []; this.informationalMessages = []; this.processedErrorMessageKeys = []; this.uploadMetadata = { numTestResults: 0, numTotalAttachments: 0, sizeTotalAttachments: 0, }; this.numWorkers = -1; this.testRunUrl = ""; this.enableResultPublish = true; this._addInformationalMessage = (message) => { this.informationalMessages.push(message); }; this._addKeyToInformationMessage = (key) => { this.processedErrorMessageKeys.push(key); }; this._isInformationMessagePresent = (key) => { return this.processedErrorMessageKeys.includes(key); }; this._reporterFailureHandler = (error) => { if (!this._isInformationMessagePresent(error.key)) { this._addKeyToInformationMessage(error.key); this._addInformationalMessage(error.message); } this.isTokenValid = false; }; this.renewSasUriIfNeeded = async () => { if (this.sasUri === undefined || !reporterUtils_1.default.isTimeGreaterThanCurrentPlus10Minutes(this.sasUri)) { this.sasUri = await this.serviceClient.createStorageUri(); logger_1.reporterLogger.info(`\nFetched SAS URI with validity: ${this.sasUri.expiresAt} and access: ${this.sasUri.accessLevel}.`); } }; if ((config === null || config === void 0 ? void 0 : config.enableGitHubSummary) !== undefined) { this.enableGitHubSummary = config.enableGitHubSummary; } if ((config === null || config === void 0 ? void 0 : config.enableResultPublish) !== undefined) { this.enableResultPublish = config.enableResultPublish; } } _addError(errorMessage) { if (this.errorMessages.length < constants_1.Constants.ERROR_MESSAGES_MAX_LENGTH) { this.errorMessages.push(this.reporterUtils.redactAccessToken(errorMessage)); } } /** * @public * * Called once before running tests. * * @param config - Resolved configuration. * @param suite - The root suite that contains all projects, files and test cases. */ onBegin(config, suite) { if (!this.enableResultPublish) return; this.initializeMPTReporter(); this.reporterUtils = new reporterUtils_1.default(this.envVariables, config, suite); if (this.isTokenValid && this.isRegionValid) { this.serviceClient = new serviceClient_1.ServiceClient(this.envVariables, this.reporterUtils, this._addInformationalMessage, this._isInformationMessagePresent, this._addKeyToInformationMessage); this.promiseOnBegin = this._onBegin(); } } /** * @public * * Called after a test has been finished in the worker process. * * @param test - Test that has been finished. * @param result - Result of the test run. */ onTestEnd(test, result) { this.numWorkers = Math.max(this.numWorkers, result.parallelIndex + 1); this.processTestResult(result); if (!this.enableResultPublish) return; // Process test result this._onTestEnd(test, result); // Upload the test results batch try { if (this.testResultBatch.size >= constants_1.Constants.TEST_BATCH_SIZE) { const currResultBatch = [...this.testResultBatch]; if (this.isTestRunStartSuccess) { this._testEndPromises.push(this.serviceClient.postTestResults(currResultBatch)); logger_1.reporterLogger.info(`\nAdded test results batch for upload.`); this.testResultBatch.clear(); } } } catch (err) { this._addError(`Name: ${err.name}, Message: ${err.message}, Stack: ${err.stack}`); logger_1.reporterLogger.error(`\nError in uploading test results: ${err.message}.`); } } /** * @public * * Called after all tests have been run, or testing has been interrupted. Note that this method may return a [Promise] * and Playwright Test will await it. Reporter is allowed to override the status and hence affect the exit code of the * test runner. * * @param result - Result of the full test run, `status` can be one of: * - `'passed'` - Everything went as expected. * - `'failed'` - Any test has failed. * - `'timedout'` - The * {@link https://playwright.dev/docs/api/class-testconfig#test-config-global-timeout | testConfig.globalTimeout} has * been reached. * - `'interrupted'` - Interrupted by the user. */ async onEnd(result) { if (this.enableResultPublish) { await this._onEnd(result); if (!this.isTestRunStartSuccess) { this._addError(`\nUnable to initialize test run report.`); } else { let count = 0; process.stdout.write("\nUploading test results."); await Promise.allSettled(this._testEndPromises).then((values) => { values.forEach((value) => { if (value.status === "fulfilled") { count++; this.reporterUtils.progressBar(count, this._testEndPromises.length); } return value.status; }); logger_1.reporterLogger.info(`\nTest result processing completed.`); return values; }); try { await this.serviceClient.postTestRunShardEnd(result, this.shard, this.errorMessages, this.uploadMetadata, this.numWorkers); logger_1.reporterLogger.info(`\nTest run successfully uploaded.`); if (this.enableGitHubSummary) { this.reporterUtils.generateMarkdownSummary(this.testRunUrl); } process.stdout.write(`\nTest report: ${this.testRunUrl}\n`); } catch (err) { this._addError(`Name: ${err.name}, Message: ${err.message}, Stack: ${err.stack}`); logger_1.reporterLogger.error(`\nError in completing test run: ${err.message}`); process.stdout.write(`\nUnable to complete test results upload.`); } } } this.displayAdditionalInformation(); } async _onBegin() { process.stdout.write(`\n`); try { const testRunResponse = await this.serviceClient.patchTestRun(this.ciInfo); logger_1.reporterLogger.info(`\nTest run report successfully initialized: ${testRunResponse === null || testRunResponse === void 0 ? void 0 : testRunResponse.displayName}.`); process.stdout.write(`Initializing reporting for this test run. You can view the results at: https://playwright.microsoft.com/workspaces/${encodeURIComponent(this.envVariables.accountId)}/runs/${encodeURIComponent(this.envVariables.runId)}\n`); const shardResponse = await this.serviceClient.postTestRunShardStart(); this.shard = shardResponse; // Set test report link as environment variable. If/else to check if environment variable defined or not. if (constants_1.Constants.DEFAULT_SERVICE_ENDPOINT && this.envVariables.accountId && this.envVariables.runId) { this.testRunUrl = `${constants_1.Constants.DEFAULT_DASHBOARD_ENDPOINT}/workspaces/${encodeURIComponent(this.envVariables.accountId)}/runs/${encodeURIComponent(this.envVariables.runId)}`; } return true; } catch (err) { this._addError(`Name: ${err.name}, Message: ${err.message}, Stack: ${err.stack}`); logger_1.reporterLogger.error(`\nError in initializing test run report: ${err.message}.`); return false; } } async _onTestEnd(test, result) { try { this.isTestRunStartSuccess = await this.promiseOnBegin; if (!this.isTestRunStartSuccess) { this._addError(`\nUnable to initialize test run report.`); return; } this.uploadMetadata.numTestResults++; const testResultObject = this.reporterUtils.getTestResultObject(test, result, this.ciInfo.jobId); this.testResultBatch.add(testResultObject); // Store test attachments in array const testAttachments = []; const otherAttachments = []; for (const attachment of result.attachments) { if (attachment.path !== undefined && attachment.path !== "") { testAttachments.push(attachment.path); this.uploadMetadata.numTotalAttachments++; this.uploadMetadata.sizeTotalAttachments += reporterUtils_1.default.getFileSize(attachment.path); } else if (attachment.body instanceof Buffer) { otherAttachments.push(attachment); this.uploadMetadata.numTotalAttachments++; this.uploadMetadata.sizeTotalAttachments += reporterUtils_1.default.getBufferSize(attachment.body); } } // Get raw result object and store it in map const rawTestResult = this.reporterUtils.getRawTestResultObject(result); this.testRawResults.set(testResultObject.testExecutionId, JSON.stringify(rawTestResult)); this._testEndPromises.push(this._uploadTestResultAttachments(testResultObject.testExecutionId, testAttachments, otherAttachments)); } catch (err) { this._addError(`Name: ${err.name}, Message: ${err.message}, Stack: ${err.stack}`); logger_1.reporterLogger.error(`\nError in processing test result: ${err.message}.`); } } async _onEnd(_) { this.isTestRunStartSuccess = await this.promiseOnBegin; if (!this.isTestRunStartSuccess) { this._addError(`\nUnable to initialize test run report.`); return; } try { // Upload the remaining test results if (this.testResultBatch.size > 0) { await this.serviceClient.postTestResults([...this.testResultBatch]); logger_1.reporterLogger.info(`\nUploaded test results batch successfully.`); this.testResultBatch.clear(); } } catch (err) { this._addError(`Name: ${err.name}, Message: ${err.message}, Stack: ${err.stack}`); logger_1.reporterLogger.error(`\nError in uploading test run information: ${err.message}`); } } async _uploadTestResultAttachments(testExecutionId, testAttachments, otherAttachments) { var _a; try { this.isTestRunStartSuccess = await this.promiseOnBegin; if (!this.isTestRunStartSuccess) { this._addError(`\nUnable to initialize test run report.`); return; } for (const attachmentPath of testAttachments) { const fileRelativePath = `${testExecutionId}/${reporterUtils_1.default.getFileRelativePath(attachmentPath)}`; await this.renewSasUriIfNeeded(); await this.storageClient.uploadFile(this.sasUri.uri, attachmentPath, fileRelativePath); } for (const otherAttachment of otherAttachments) { await this.renewSasUriIfNeeded(); const match = (_a = otherAttachment === null || otherAttachment === void 0 ? void 0 : otherAttachment.contentType) === null || _a === void 0 ? void 0 : _a.match(/charset=(.*)/); const charset = match && match.length > 1 ? match[1] : "utf-8"; await this.storageClient.uploadBuffer(this.sasUri.uri, otherAttachment.body.toString(charset || "utf-8"), `${testExecutionId}/${otherAttachment.name}`); } const rawTestResult = this.testRawResults.get(testExecutionId); await this.renewSasUriIfNeeded(); await this.storageClient.uploadBuffer(this.sasUri.uri, rawTestResult[0], `${testExecutionId}/rawTestResult.json`); } catch (err) { this._addError(`Name: ${err.name}, Message: ${err.message}, Stack: ${err.stack}`); logger_1.reporterLogger.error(`\nError in uploading test result attachments: ${err.message}`); } } initializeMPTReporter() { this.envVariables = new environmentVariables_1.EnvironmentVariables(); if (process.env[constants_1.InternalEnvironmentVariables.MPT_SETUP_FATAL_ERROR] === "true") { this.isTokenValid = false; return; } if (!process.env["PLAYWRIGHT_SERVICE_REPORTING_URL"]) { process.stdout.write("\nReporting service url not found."); this.isTokenValid = false; return; } logger_1.reporterLogger.info(`Reporting url - ${process.env["PLAYWRIGHT_SERVICE_REPORTING_URL"]}`); if (this.envVariables.accessToken === undefined || this.envVariables.accessToken === "") { process.stdout.write(`\n${messages_1.ServiceErrorMessageConstants.NO_AUTH_ERROR.message}`); this.isTokenValid = false; } else if (reporterUtils_1.default.hasAudienceClaim(this.envVariables.accessToken)) { const result = (0, utils_1.populateValuesFromServiceUrl)(); this.envVariables.region = result.region; this.envVariables.accountId = result.accountId; const entraTokenDetails = reporterUtils_1.default.getTokenDetails(this.envVariables.accessToken, mptTokenDetails_1.TokenType.ENTRA); this.envVariables.userId = entraTokenDetails.oid; this.envVariables.userName = entraTokenDetails.name; } else { const mptTokenDetails = reporterUtils_1.default.getTokenDetails(this.envVariables.accessToken, mptTokenDetails_1.TokenType.MPT); (0, utils_1.validateMptPAT)(this._reporterFailureHandler); this.envVariables.accountId = mptTokenDetails.aid; this.envVariables.userId = mptTokenDetails.oid; this.envVariables.userName = mptTokenDetails.userName; this.envVariables.region = reporterUtils_1.default.getRegionFromAccountID(this.envVariables.accountId); } this.storageClient = new storageClient_1.StorageClient(); if (this.envVariables.region && !constants_1.Constants.SupportedRegions.includes(this.envVariables.region) && this.isTokenValid) { process.stdout.write(`\nUnsupported region's workspace used to generate the input Access Token; the supported regions are ${constants_1.Constants.SupportedRegions.join(", ")}`); this.isRegionValid = false; } if (this.envVariables.runId === undefined || this.envVariables.runId === "") { this.envVariables.runId = reporterUtils_1.default.getRunId(this.ciInfo); } } displayAdditionalInformation() { if (this.informationalMessages.length > 0) console.info(); // Add a new line before displaying the messages this.informationalMessages.forEach((message, index) => { console.info(`${index + 1}. ${message}`); }); } processTestResult(result) { if (process.env[constants_1.InternalEnvironmentVariables.MPT_CLOUD_HOSTED_BROWSER_USED] && result.status !== "passed") { result.errors.forEach((error) => { constants_1.TestResultErrorConstants.forEach((testResultErrorParseObj) => { if (this.processedErrorMessageKeys.includes(testResultErrorParseObj.key)) { return; } const errorMessage = error.message; if (!errorMessage) return; const match = errorMessage.match(testResultErrorParseObj.pattern); if (match) { this.processedErrorMessageKeys.push(testResultErrorParseObj.key); this._addInformationalMessage(testResultErrorParseObj.message); } }); }); } } /** * @public * * Whether this reporter uses stdio for reporting. When it does not, Playwright Test could add some output to enhance * user experience. If your reporter does not print to the terminal, it is strongly recommended to return `false`. */ printsToStdio() { return true; } } exports.default = MPTReporter; //# sourceMappingURL=mptReporter.js.map