@azure/microsoft-playwright-testing
Version:
Package to integrate your Playwright test suite with Microsoft Playwright Testing service
580 lines (574 loc) • 24.1 kB
JavaScript
"use strict";
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
Object.defineProperty(exports, "__esModule", { value: true });
const tslib_1 = require("tslib");
const child_process_1 = require("child_process");
const logger_1 = require("../common/logger");
const crypto_1 = require("crypto");
const fs_1 = tslib_1.__importDefault(require("fs"));
const path_1 = tslib_1.__importDefault(require("path"));
const constants_1 = require("../common/constants");
const mptTokenDetails_1 = require("../model/mptTokenDetails");
const shard_1 = require("../model/shard");
const testResult_1 = require("../model/testResult");
const testRun_1 = require("../model/testRun");
const cIInfoProvider_1 = require("./cIInfoProvider");
const cIInfoProvider_2 = require("./cIInfoProvider");
const utils_1 = require("./utils");
class ReporterUtils {
// eslint-disable-next-line @azure/azure-sdk/ts-use-interface-parameters
constructor(envVariables, config, _) {
this.config = config;
this.envVariables = envVariables;
this.startTime = Date.now();
this.totalTests = 0;
this.failedTests = 0;
this.skippedTests = 0;
this.passedTests = 0;
this.flakyTests = 0;
}
async getTestRunObject(ciInfo) {
const testRun = new testRun_1.TestRun();
const runName = this.envVariables.runName || (await this.getRunName(ciInfo));
testRun.testRunId = this.envVariables.runId;
testRun.displayName = ReporterUtils.isNullOrEmpty(runName) ? this.envVariables.runId : runName;
testRun.creatorName = this.envVariables.userName;
testRun.creatorId = this.envVariables.userId;
testRun.startTime = ReporterUtils.timestampToRFC3339(this.startTime);
testRun.ciConfig = {
ciProviderName: ciInfo.provider,
branch: ciInfo.branch,
author: ciInfo.author,
commitId: ciInfo.commitId,
revisionUrl: ciInfo.revisionUrl,
};
testRun.testRunConfig = this.getTestRunConfig();
testRun.cloudReportingEnabled = "true";
return testRun;
}
getTestRunShardStartObject() {
const shard = new shard_1.Shard();
if (this.config.shard !== null && this.config.shard !== undefined) {
this.envVariables.shardId = this.config.shard.current.toString();
}
else {
this.envVariables.shardId = "1";
}
shard.shardId = this.envVariables.shardId;
shard.summary = {
startTime: ReporterUtils.timestampToRFC3339(this.startTime),
};
shard.uploadCompleted = false;
return shard;
}
getTestRunShardEndObject(result,
// eslint-disable-next-line @azure/azure-sdk/ts-use-interface-parameters
shard, errorMessages, attachmentMetadata, workers) {
var _a;
shard.shardId = (_a = this.envVariables.shardId) !== null && _a !== void 0 ? _a : "1";
shard.summary.totalTime = result.duration;
shard.summary.endTime = ReporterUtils.timestampToRFC3339(Date.now());
shard.summary.status = shard_1.TestRunStatus.CLIENT_COMPLETE;
shard.summary.errorMessages = errorMessages;
shard.summary.uploadMetadata = attachmentMetadata;
shard.uploadCompleted = true;
shard.workers = workers;
return shard;
}
getOSName(result, data) {
var _a, _b;
try {
for (const attachment of result.attachments) {
if (attachment.name === data) {
const match = (_a = attachment === null || attachment === void 0 ? void 0 : attachment.contentType) === null || _a === void 0 ? void 0 : _a.match(/charset=(.*)/);
const charset = match && match.length > 1 ? match[1] : "utf-8";
return ((_b = attachment.body) === null || _b === void 0 ? void 0 : _b.toString(charset || "utf-8").toUpperCase()) || "";
}
}
}
catch (error) {
logger_1.reporterLogger.error(`Error in fetching OS - ${error}`);
}
return "";
}
getTestResultObject(test, result, jobName) {
var _a, _b, _c, _d;
switch (test.outcome()) {
case "skipped":
this.skippedTests++;
this.totalTests++;
break;
case "expected":
this.passedTests++;
this.totalTests++;
break;
case "unexpected":
if (result.retry === test.retries) {
this.failedTests++;
this.totalTests++;
}
break;
case "flaky":
this.totalTests++;
this.flakyTests++;
break;
default:
break;
}
const testResult = new testResult_1.TestResult();
testResult.runId = this.envVariables.runId;
testResult.shardId = this.envVariables.runId + "_" + this.envVariables.shardId;
testResult.accountId = this.envVariables.accountId;
testResult.suiteId = ReporterUtils.calculateSha1(`${test.parent.title}-${test.location.file}`);
testResult.testId = testResult.suiteId.concat(`-${ReporterUtils.calculateSha1(test.title)}`);
testResult.testCombinationId = test.id;
testResult.testExecutionId = (0, crypto_1.randomUUID)();
testResult.testTitle = test.title;
testResult.suiteTitle = this.extractRootParentTitle(test.parent);
testResult.fileName = test.location.file;
testResult.status = this.getTestStatus(test, result);
testResult.lineNumber = test.location.line;
testResult.retry = result.retry ? result.retry : 0;
let browserName = (_b = (_a = test.parent.project()) === null || _a === void 0 ? void 0 : _a.use.browserName) === null || _b === void 0 ? void 0 : _b.toLowerCase();
if (!browserName) {
browserName = (_d = (_c = test.parent.project()) === null || _c === void 0 ? void 0 : _c.use.defaultBrowserType) === null || _d === void 0 ? void 0 : _d.toLowerCase();
}
testResult.webTestConfig = {
jobName: jobName,
projectName: test.parent.project().name,
browserType: browserName ? browserName.toUpperCase() : "",
os: this.getOSName(result, constants_1.Constants.OS),
};
testResult.annotations = this.extractTestAnnotations(test.annotations);
testResult.tags = this.extractTestTags(test);
testResult.resultsSummary = {
status: result.status.toUpperCase(),
duration: result.duration,
startTime: result.startTime.toISOString().replace(/\.\d+Z$/, "Z"),
attachmentsMetadata: this.getAttachmentStatus(result),
};
testResult.artifactsPath = result.attachments
.filter((attachment) => (attachment === null || attachment === void 0 ? void 0 : attachment.path) !== null && (attachment === null || attachment === void 0 ? void 0 : attachment.path) !== undefined) // Filter attachments with defined and non-null path property
.map((attachment) => `${testResult.testExecutionId}/${ReporterUtils.getFileRelativePath(attachment.path)}`);
return testResult;
}
generateMarkdownSummary(testRunUrl) {
try {
if (cIInfoProvider_2.CIInfoProvider.getCIProvider() === cIInfoProvider_1.CI_PROVIDERS.GITHUB) {
const markdownContent = `
#### Microsoft Playwright Testing run summary
#### Results:
 **Passed:** ${this.passedTests}
 **Failed:** ${this.failedTests}
 **Flaky:** ${this.flakyTests}
 **Skipped:** ${this.skippedTests}
#### For more details, visit the [service dashboard](${testRunUrl}).
`;
fs_1.default.writeFileSync(process.env["GITHUB_STEP_SUMMARY"], markdownContent);
}
}
catch (err) {
logger_1.reporterLogger.error(`\nCould not generate markdown summary - ${err}`);
}
}
getRawTestResultObject(result) {
const rawTestResult = {
steps: this.dedupeSteps(result.steps).map((step) => this.serializeTestStep(step)),
errors: this.getTestError(result),
stdErr: result.stderr ? JSON.stringify(result.stderr, null, 2) : "",
stdOut: result.stdout ? JSON.stringify(result.stdout, null, 2) : "",
};
return rawTestResult;
}
static getRunId(cIInfo) {
if (cIInfo === null || cIInfo.provider === cIInfoProvider_1.CI_PROVIDERS.DEFAULT) {
return (0, crypto_1.randomUUID)();
}
const concatString = `${cIInfo.provider}-${cIInfo.repo}-${cIInfo.runId}-${cIInfo.runAttempt}`;
const runId = ReporterUtils.calculateSha1(concatString);
return runId;
}
static calculateSha1(buffer) {
const hash = (0, crypto_1.createHash)("sha1");
hash.update(buffer);
return hash.digest("hex");
}
/*
public static getTokenDetails(accessToken: string) {
let tokenDetails = new MPTTokenDetails();
try {
const token = accessToken.split('.')[1];
const _token = Buffer.from(token, 'base64');
tokenDetails = JSON.parse(_token.toString());
} catch (err) {
throw err;
}
return tokenDetails;
}
*/
static getTokenDetails(accessToken, tokenType) {
const token = accessToken.split(".")[1];
const _token = Buffer.from(token, "base64");
const tokenDetails = JSON.parse(_token.toString());
switch (tokenType) {
case mptTokenDetails_1.TokenType.MPT:
return tokenDetails;
case mptTokenDetails_1.TokenType.ENTRA:
return tokenDetails;
default:
throw new Error("Unsupported token type");
}
}
static hasAudienceClaim(token) {
try {
// Split the token into its three parts
const parts = token.split(".");
if (parts.length !== 3) {
throw new Error("Invalid token format");
}
// Base64 decode the payload
const payload = parts[1];
const decodedPayload = Buffer.from(payload, "base64");
// Parse the decoded payload as JSON
const payloadObject = JSON.parse(decodedPayload.toString());
// Check if the payload has an 'aud' claim
return "aud" in payloadObject;
}
catch (error) {
return false;
}
}
static timestampToRFC3339(timestamp) {
const date = new Date(timestamp);
const dateString = date.toISOString().replace(/\.\d+Z$/, "Z");
return dateString;
}
static getFileRelativePath(filePath) {
if (filePath) {
let parts = filePath.split("/");
if (parts.length > 1) {
return parts[parts.length - 1];
}
parts = filePath.split("\\");
if (parts.length > 1) {
return parts[parts.length - 1];
}
}
return filePath;
}
// eslint-disable-next-line @azure/azure-sdk/ts-use-interface-parameters
static isTimeGreaterThanCurrentPlus10Minutes(sasUri) {
try {
const url = new URL(sasUri.uri);
const params = new URLSearchParams(url.search);
const expiryTime = params.get("se"); // 'se' is the query parameter for the expiry time
if (expiryTime) {
const timestampFromIsoString = new Date(expiryTime).getTime();
const currentTimestampPlus10Minutes = Date.now() + 10 * 60 * 1000;
const isSasValidityGreaterThanCurrentTimePlus10Minutes = timestampFromIsoString > currentTimestampPlus10Minutes;
if (!isSasValidityGreaterThanCurrentTimePlus10Minutes) {
logger_1.reporterLogger.info(`Sas rotation required because close to expiry, SasUriValidTillTime: ${timestampFromIsoString}, CurrentTime: ${currentTimestampPlus10Minutes}`);
}
return isSasValidityGreaterThanCurrentTimePlus10Minutes;
}
logger_1.reporterLogger.error(`Sas rotation required because expiry param not found.`);
return false;
}
catch (error) {
logger_1.reporterLogger.error(`Sas rotation required because of ${error}.`);
return false;
}
}
static getFileSize(attachmentPath) {
try {
const stats = fs_1.default.statSync(attachmentPath);
return stats.size;
}
catch (error) {
return 0;
}
}
static getBufferSize(attachmentBody) {
try {
const fileSizeInBytes = attachmentBody.length;
return fileSizeInBytes;
}
catch (error) {
return 0;
}
}
redactAccessToken(info) {
if (!info || ReporterUtils.isNullOrEmpty(this.envVariables.accessToken)) {
return "";
}
const accessTokenRegex = new RegExp(this.envVariables.accessToken, "g");
return info.replace(accessTokenRegex, constants_1.Constants.DEFAULT_REDACTED_MESSAGE);
}
static getRegionFromAccountID(accountId) {
if (accountId.includes("_")) {
return accountId.split("_")[0];
}
else {
return;
} // Handling for older workspaces without region in id
}
progressBar(current, total) {
const width = 40;
const percent = current / total;
const completed = Math.round(width * percent);
const remaining = width - completed;
if (current % Math.round(total / 5) === 0 || current === total) {
process.stdout.write("\r");
process.stdout.write(`[${"=".repeat(completed)}${" ".repeat(remaining)}] ${Math.round(percent * 100)}%`);
}
}
getTestRunConfig() {
const testRunConfig = {
workers: this.config.workers,
pwVersion: this.config.version,
timeout: this.config.globalTimeout,
repeatEach: this.config.projects[0].repeatEach,
retries: this.config.projects[0].retries,
shards: this.config.shard ? this.config.shard : { total: 1 },
testFramework: {
name: constants_1.Constants.TEST_FRAMEWORK_NAME,
version: this.config.version,
runnerName: constants_1.Constants.TEST_FRAMEWORK_RUNNERNAME,
},
testType: constants_1.Constants.TEST_TYPE,
testSdkLanguage: constants_1.Constants.TEST_SDK_LANGUAGE,
reporterPackageVersion: (0, utils_1.getPackageVersion)(),
};
return testRunConfig;
}
relativeLocation(location) {
if (!location) {
return undefined;
}
const file = this.toPosixPath(path_1.default.relative(this.config.rootDir, location.file));
return {
file,
line: location.line,
column: location.column,
};
}
extractTestTags(input) {
let tags = [];
if ("tags" in input && Array.isArray(input.tags) && input.tags.length > 0) {
tags = input.tags.map((tag) => tag.slice(1));
return tags;
}
// Check if the input string contains tags directly
const regex = /@(\w+)/g;
const matches = input.title.match(regex);
if (matches) {
tags = tags.concat(matches.map((match) => match.slice(1)));
}
// Try parsing the input string as a JavaScript object
try {
const obj = JSON.parse(`{${input}}`);
if (obj.tag && Array.isArray(obj.tag)) {
tags = tags.concat(obj.tag);
}
}
catch (error) {
// Ignore parsing errors
}
return tags;
}
extractTestAnnotations(annotations) {
const result = annotations.map((annotation) => {
if (annotation.type && annotation.description) {
return `${annotation.type}: ${annotation.description}`;
}
return annotation.type;
});
return result;
}
toPosixPath(aPath) {
return aPath.split(path_1.default.sep).join(path_1.default.posix.sep);
}
getAttachmentStatus(testResult) {
let attachmentStatus = "";
for (const attachment of testResult.attachments) {
if (attachment.contentType.includes("image")) {
if (attachmentStatus !== "") {
attachmentStatus += ",";
}
attachmentStatus += "image";
}
else if (attachment.contentType.includes("video")) {
if (attachmentStatus !== "") {
attachmentStatus += ",";
}
attachmentStatus += "video";
}
else if (attachment.contentType === "application/zip") {
if (attachmentStatus !== "") {
attachmentStatus += ",";
}
attachmentStatus += "trace";
}
else if (attachment.contentType === "text/plain") {
if (attachmentStatus !== "") {
attachmentStatus += ",";
}
attachmentStatus += "txt";
}
}
return attachmentStatus;
}
dedupeSteps(steps) {
var _a, _b, _c, _d, _e, _f, _g;
const result = [];
let lastResult = undefined;
for (const step of steps) {
const canDedupe = !step.error && step.duration >= 0 && ((_a = step.location) === null || _a === void 0 ? void 0 : _a.file) && !step.steps.length;
const lastStep = lastResult === null || lastResult === void 0 ? void 0 : lastResult.step;
if (canDedupe &&
lastResult &&
lastStep &&
step.category === lastStep.category &&
step.title === lastStep.title &&
((_b = step.location) === null || _b === void 0 ? void 0 : _b.file) === ((_c = lastStep.location) === null || _c === void 0 ? void 0 : _c.file) &&
((_d = step.location) === null || _d === void 0 ? void 0 : _d.line) === ((_e = lastStep.location) === null || _e === void 0 ? void 0 : _e.line) &&
((_f = step.location) === null || _f === void 0 ? void 0 : _f.column) === ((_g = lastStep.location) === null || _g === void 0 ? void 0 : _g.column)) {
++lastResult.count;
lastResult.duration += step.duration;
continue;
}
lastResult = { step, count: 1, duration: step.duration };
result.push(lastResult);
if (!canDedupe) {
lastResult = undefined;
}
}
return result;
}
serializeTestStep(dedupedStep) {
const { step, duration, count } = dedupedStep;
const result = {
title: step.title,
category: step.category,
startTime: step.startTime.toISOString(),
duration,
error: step.error ? step.error.message : undefined,
location: this.relativeLocation(step.location),
steps: this.dedupeSteps(step.steps).map((subStep) => this.serializeTestStep(subStep)),
count: count,
};
return result;
}
getTestStatus(test, result) {
if (test.expectedStatus === result.status) {
if (result.status === "skipped") {
return "SKIPPED";
}
else {
return "PASSED";
}
}
else if (result.status === "interrupted") {
return "SKIPPED";
}
else {
return "FAILED";
}
}
extractRootParentTitle(suite) {
// Traverse through the parent properties until reaching the root parent
let currentSuite = suite;
let depthCount = 0;
let suiteTitle = currentSuite.title;
const projectName = currentSuite.project().name;
while ((currentSuite === null || currentSuite === void 0 ? void 0 : currentSuite.parent) && !ReporterUtils.isNullOrEmpty(currentSuite.parent.title)) {
if (depthCount > 10 || currentSuite.parent.title === projectName) {
break;
}
suiteTitle = suiteTitle + " > " + currentSuite.parent.title;
currentSuite = currentSuite.parent;
depthCount++;
}
return suiteTitle;
}
async getRunName(ciInfo) {
var _a;
if (ciInfo.provider === cIInfoProvider_1.CI_PROVIDERS.GITHUB &&
process.env["GITHUB_EVENT_NAME"] === "pull_request") {
const prNumber = `${(_a = process.env["GITHUB_REF_NAME"]) === null || _a === void 0 ? void 0 : _a.split("/")[0]}`;
const prLink = `${process.env["GITHUB_REPOSITORY"]}/pull/${prNumber}`;
return `PR# ${prNumber} on Repo: ${process.env["GITHUB_REPOSITORY"]} (${prLink})`;
}
let gitCommitMessage = null;
try {
const gitVersion = await this.runCommand(constants_1.Constants.GIT_VERSION_COMMAND);
if (ReporterUtils.isNullOrEmpty(gitVersion)) {
throw new Error("Git is not installed on the machine");
}
const isInsideWorkTree = await this.runCommand(constants_1.Constants.GIT_REV_PARSE);
if (isInsideWorkTree !== "true") {
throw new Error("Not inside a git repository");
}
gitCommitMessage = await this.runCommand(constants_1.Constants.GIT_COMMIT_MESSAGE_COMMAND);
return gitCommitMessage;
}
catch (err) {
logger_1.reporterLogger.error(`\nError in getting git commit message: ${err}.`);
return "";
}
}
async runCommand(command) {
return new Promise((resolve, reject) => {
(0, child_process_1.exec)(command, (error, stdout, stderr) => {
if (error) {
reject(error);
return;
}
if (stderr) {
reject(new Error(stderr));
return;
}
resolve(stdout.trim());
});
});
}
static isNullOrEmpty(str) {
return !str || str.trim() === "";
}
getTestError(result) {
if (!result.errors || result.errors.length === 0)
return "";
const errorMessages = [];
result.errors.forEach((error) => {
if (error.message)
errorMessages.push({ message: error.message });
if (error.snippet && error.location) {
errorMessages.push({
message: error.snippet + "\n\n" + this.getReadableLineLocation(error.location),
});
}
else if (error.snippet)
errorMessages.push({ message: error.snippet });
});
return JSON.stringify(errorMessages, null, 2);
}
getReadableLineLocation(location) {
return `at ${location.file}:${location.line}:${location.column}`;
}
}
ReporterUtils.getReporterBackOffOptions = {
numOfAttempts: 3,
jitter: "full",
retry: (error) => {
if (error.response) {
const status = error.response.status;
if (constants_1.Constants.NON_RETRYABLE_STATUS_CODES.includes(status)) {
return false;
}
}
return true;
},
};
exports.default = ReporterUtils;
//# sourceMappingURL=reporterUtils.js.map