@testomatio/reporter
Version:
Testomatio Reporter Client
259 lines (257 loc) • 9.93 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.extractTags = extractTags;
const picocolors_1 = __importDefault(require("picocolors"));
const crypto_1 = __importDefault(require("crypto"));
const os_1 = __importDefault(require("os"));
const path_1 = __importDefault(require("path"));
const uuid_1 = require("uuid");
const fs_1 = __importDefault(require("fs"));
const constants_js_1 = require("../constants.js");
const client_js_1 = __importDefault(require("../client.js"));
const utils_js_1 = require("../utils/utils.js");
const index_js_1 = require("../services/index.js");
const data_storage_js_1 = require("../data-storage.js");
const constants_js_2 = require("../utils/constants.js");
const reportTestPromises = [];
class PlaywrightReporter {
constructor(config = {}) {
this.client = new client_js_1.default({ apiKey: config?.apiKey });
this.uploads = [];
}
onBegin(config, suite) {
// clean data storage
utils_js_1.fileSystem.clearDir(constants_js_1.TESTOMAT_TMP_STORAGE_DIR);
if (!this.client)
return;
this.suite = suite;
this.config = config;
this.client.createRun();
}
onTestBegin(testInfo) {
const fullTestTitle = getTestContextName(testInfo);
data_storage_js_1.dataStorage.setContext(fullTestTitle);
}
onTestEnd(test, result) {
// test.parent.project().__projectId
if (!this.client)
return;
const { title } = test;
const { error, duration } = result;
const suite_title = test.parent ? test.parent?.title : path_1.default.basename(test?.location?.file);
const steps = [];
for (const step of result.steps) {
const appendedStep = appendStep(step);
if (appendedStep) {
steps.push(appendedStep);
}
}
// Extract and normalize tags
const tags = extractTags(test);
const fullTestTitle = getTestContextName(test);
let logs = '';
if (result.stderr.length || result.stdout.length) {
logs = `\n\n${picocolors_1.default.bold('Logs:')}\n${picocolors_1.default.red(result.stderr.join(''))}\n${result.stdout.join('')}`;
}
const manuallyAttachedArtifacts = index_js_1.services.artifacts.get(fullTestTitle);
const testMeta = index_js_1.services.keyValues.get(fullTestTitle);
const links = index_js_1.services.links.get(fullTestTitle);
const rid = test.id || test.testId || (0, uuid_1.v4)();
/**
* @type {{
* browser?: string,
* dependencies: string[],
* isMobile?: boolean
* metadata: Record<string, any>,
* name: string,
* }}
*/
const project = {
browser: test.parent.project().use.defaultBrowserType,
dependencies: test.parent.project().dependencies,
isMobile: test.parent.project().use.isMobile,
metadata: test.parent.project().metadata,
name: test.parent.project().name,
};
let status = result.status;
// process test.fail() annotation
if (test.expectedStatus === 'failed') {
// actual status = expected
if (result.status === 'failed')
status = 'passed';
// actual status != expected
if (result.status === 'passed')
status = 'failed';
}
const reportTestPromise = this.client.addTestRun(checkStatus(status), {
rid: `${rid}-${project.name}`,
error,
test_id: (0, utils_js_1.getTestomatIdFromTestTitle)(`${title} ${tags.join(' ')}`),
suite_title,
title,
tags,
steps: steps.length ? steps : undefined,
time: duration,
logs,
links,
manuallyAttachedArtifacts,
meta: {
browser: project.browser,
isMobile: project.isMobile,
project: project.name,
projectDependencies: project.dependencies?.length ? project.dependencies : null,
...testMeta,
...project.metadata, // metadata has any type (in playwright), but we will stringify it in client.js
...test.annotations?.reduce((acc, annotation) => {
acc[annotation.type] = annotation.description;
return acc;
}, {}),
},
file: test.location?.file,
});
this.uploads.push({
rid: `${rid}-${project.name}`,
title: test.title,
files: result.attachments.filter(a => a.body || a.path),
file: test.location?.file,
});
// remove empty uploads
this.uploads = this.uploads.filter(anUpload => anUpload.files.length);
reportTestPromises.push(reportTestPromise);
}
#getArtifactPath(artifact) {
if (artifact.path) {
if (path_1.default.isAbsolute(artifact.path))
return artifact.path;
return path_1.default.join(this.config.outputDir || this.config.projects[0].outputDir, artifact.path);
}
if (artifact.body) {
let filePath = generateTmpFilepath(artifact.name);
const hasExtension = artifact.name && path_1.default.extname(artifact.name);
if (!hasExtension && artifact.contentType) {
const extension = constants_js_2.extensionMap[artifact.contentType] || artifact.contentType.split('/')[1];
if (extension)
filePath += `.${extension}`;
}
fs_1.default.writeFileSync(filePath, artifact.body);
return filePath;
}
return null;
}
async onEnd(result) {
if (!this.client)
return;
await Promise.all(reportTestPromises);
if (this.uploads.length) {
if (this.client.uploader.isEnabled)
console.log(constants_js_1.APP_PREFIX, `🎞️ Uploading ${this.uploads.length} files...`);
const promises = [];
// ? possible move to addTestRun (needs investigation if files are ready)
for (const upload of this.uploads) {
const { rid, file, title } = upload;
const files = upload.files.map(attachment => ({
path: this.#getArtifactPath(attachment),
title,
type: attachment.contentType,
}));
if (!this.client.uploader.isEnabled) {
files.forEach(f => this.client.uploader.storeUploadedFile(f, this.client.runId, rid, false));
continue;
}
promises.push(this.client.addTestRun(undefined, {
rid,
title,
files,
file,
}));
}
await Promise.all(promises);
}
await this.client.updateRunStatus(checkStatus(result.status));
}
}
function checkStatus(status) {
return ({
skipped: constants_js_1.STATUS.SKIPPED,
timedOut: constants_js_1.STATUS.FAILED,
passed: constants_js_1.STATUS.PASSED,
}[status] || constants_js_1.STATUS.FAILED);
}
function appendStep(step, shift = 0) {
// nesting too deep, ignore those steps
if (shift >= 10)
return;
let newCategory = step.category;
switch (newCategory) {
case 'test.step':
newCategory = 'user';
break;
case 'hook':
newCategory = 'hook';
break;
case 'attach':
return null; // Skip steps with category 'attach'
default:
newCategory = 'framework';
}
const formattedSteps = [];
for (const child of step.steps || []) {
const appendedChild = appendStep(child, shift + 2);
if (appendedChild) {
formattedSteps.push(appendedChild);
}
}
const resultStep = {
category: newCategory,
title: step.title,
duration: step.duration,
};
if (formattedSteps.length) {
resultStep.steps = formattedSteps.filter(s => !!s);
}
if (step.error !== undefined) {
resultStep.error = step.error;
}
return resultStep;
}
function generateTmpFilepath(filename = '') {
filename = filename || `tmp.${crypto_1.default.randomBytes(16).toString('hex')}`;
const tmpdir = os_1.default.tmpdir();
return path_1.default.join(tmpdir, filename);
}
/**
* Extracts and normalizes tags from test title, test options, and suite level
* @param {*} test - testInfo object from Playwright
* @returns {string[]} - array of normalized tags
*/
function extractTags(test) {
const tagsSet = new Set();
// Extract tags from test title (@tag format)
const titleTagsMatch = test.title.match(/@\w+/g);
if (titleTagsMatch) {
titleTagsMatch.forEach(tag => {
tagsSet.add(tag.replace('@', '').toLowerCase());
});
}
// Extract tags from test.tags (Playwright built-in tags)
if (test.tags && Array.isArray(test.tags)) {
test.tags.forEach(tag => {
const normalizedTag = typeof tag === 'string' ? tag.replace('@', '').toLowerCase() : String(tag).toLowerCase();
tagsSet.add(normalizedTag);
});
}
return Array.from(tagsSet);
}
/**
* Returns filename + test title
* @param {*} test - testInfo object from Playwright
* @returns
*/
function getTestContextName(test) {
return `${test._requireFile || ''}_${test.title}`;
}
module.exports = PlaywrightReporter;
module.exports.extractTags = extractTags;