@testomatio/reporter
Version:
Testomatio Reporter Client
439 lines (437 loc) • 16.9 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.CodeceptReporter = CodeceptReporter;
const debug_1 = __importDefault(require("debug"));
const picocolors_1 = __importDefault(require("picocolors"));
const client_js_1 = __importDefault(require("../client.js"));
const constants_js_1 = require("../constants.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 codeceptjs_1 = __importDefault(require("codeceptjs"));
const debug = (0, debug_1.default)('@testomatio/reporter:adapter:codeceptjs');
// @ts-ignore
if (!global.codeceptjs) {
// @ts-ignore
global.codeceptjs = codeceptjs_1.default;
}
// @ts-ignore
const { event, recorder, codecept, output } = global.codeceptjs;
const [, MAJOR_VERSION, MINOR_VERSION] = codecept.version().match(/(\d+)\.(\d+)/).map(Number);
// Constants for hook execution order
const HOOK_EXECUTION_ORDER = {
PRE_TEST: ['BeforeSuiteHook', 'BeforeHook'],
POST_TEST: ['AfterHook', 'AfterSuiteHook']
};
// codeceptjs workers are self-contained
data_storage_js_1.dataStorage.isFileStorage = false;
const DATA_REGEXP = /[|\s]+?(\{".*\}|\[.*\])/;
if (MAJOR_VERSION < 3) {
console.log('🔴 This reporter works with CodeceptJS 3+, please update your tests');
}
if (MAJOR_VERSION === 3 && MINOR_VERSION < 7) {
console.log('🔴 CodeceptJS 3.7+ is supported, please upgrade CodeceptJS or use 1.6 version of `@testomatio/reporter`');
}
function CodeceptReporter(config) {
const failedTests = [];
let videos = [];
let traces = [];
const reportTestPromises = [];
const testTimeMap = {};
const { apiKey } = config;
const client = new client_js_1.default({ apiKey });
// Store original output methods for fallback
const originalOutput = {
debug: output.debug,
log: output.log,
step: output.step,
say: output.say,
};
output.debug = function (msg) {
originalOutput.debug(msg);
data_storage_js_1.dataStorage.putData('log', repeat(this?.stepShift || 0) + picocolors_1.default.cyan(msg.toString()));
};
output.say = function (message, color = 'cyan') {
originalOutput.say(message, color);
const sayMsg = repeat(this?.stepShift || 0) + ` ${picocolors_1.default.bold(picocolors_1.default[color](message))}`;
data_storage_js_1.dataStorage.putData('log', sayMsg);
};
output.log = function (msg) {
originalOutput.log(msg);
data_storage_js_1.dataStorage.putData('log', repeat(this?.stepShift || 0) + picocolors_1.default.gray(msg));
};
output.stepShift = 0;
recorder.startUnlessRunning();
const hookSteps = new Map();
let currentHook = null;
event.dispatcher.on(event.workers.before, () => {
recorder.add('Creating new run', async () => {
await client.createRun();
process.env.TESTOMATIO_RUN = client.runId;
process.env.TESTOMATIO_PROCEED = 'true';
debug('Run ID:', client.runId);
});
});
event.dispatcher.on(event.workers.after, () => {
client.updateRunStatus('finished');
});
// Listening to events
event.dispatcher.on(event.all.before, () => {
// clear tmp dir
// fileSystem.clearDir(TESTOMAT_TMP_STORAGE_DIR);
// recorder.add('Creating new run', () => );
recorder.add('Creating new run', () => {
return client.createRun();
});
videos = [];
traces = [];
if (!global.testomatioDataStore)
global.testomatioDataStore = {};
});
// Hook event listeners
event.dispatcher.on(event.hook.started, (hook) => {
output.stepShift = 2;
currentHook = hook.name;
let title = hook.hookName;
if (hook.suite)
title += ' ' + hook.suite.fullTitle();
if (hook.test)
title += ' ' + hook.test.fullTitle();
if (hook.ctx.currentTest)
title += ' ' + hook.ctx.currentTest.fullTitle();
index_js_1.services.setContext(title);
hookSteps.set(hook.name, []);
});
event.dispatcher.on(event.hook.finished, () => {
currentHook = null;
output.stepShift = 2;
index_js_1.services.setContext(null);
});
// mark as failed all tests inside the failed hook
event.dispatcher.on(event.hook.failed, hook => {
if (hook.name !== 'BeforeSuiteHook')
return;
const suite = hook.runnable.parent;
if (!suite)
return;
const error = hook?.ctx?.currentTest?.err;
for (const test of suite.tests) {
client.addTestRun('failed', {
...stripExampleFromTitle(test.title),
rid: test.uid,
test_id: (0, utils_js_1.getTestomatIdFromTestTitle)(test.title),
suite_title: stripTagsFromTitle(suite.title),
error,
time: hook?.runnable?.duration,
});
}
});
event.dispatcher.on(event.suite.before, suite => {
data_storage_js_1.dataStorage.setContext(suite.fullTitle());
});
event.dispatcher.on(event.suite.after, () => {
index_js_1.services.setContext(null);
});
event.dispatcher.on(event.test.before, test => {
initializeTestDataStore();
index_js_1.services.setContext(test.fullTitle());
});
event.dispatcher.on(event.test.started, test => {
index_js_1.services.setContext(test.fullTitle());
testTimeMap[test.uid] = Date.now();
});
event.dispatcher.on(event.all.result, async (result) => {
debug('waiting for all tests to be reported');
// all tests were reported and we can upload videos
await Promise.all(reportTestPromises);
await uploadAttachments(client, videos, '🎞️ Uploading', 'video');
await uploadAttachments(client, traces, '📁 Uploading', 'trace');
client.updateRunStatus('finished');
});
event.dispatcher.on(event.test.after, test => {
const { uid, tags, title, artifacts } = test.simplify();
const error = test.err || null;
failedTests.push(uid || title);
const testObj = getTestAndMessage(title);
const files = buildArtifactFiles(artifacts);
const logs = getTestLogs(test);
const manuallyAttachedArtifacts = index_js_1.services.artifacts.get(test.fullTitle());
const keyValues = index_js_1.services.keyValues.get(test.fullTitle());
const stepHierarchy = buildUnifiedStepHierarchy(test.steps, hookSteps);
const links = index_js_1.services.links.get(test.fullTitle());
index_js_1.services.setContext(null);
client.addTestRun(test.state, {
...stripExampleFromTitle(title),
rid: uid,
test_id: (0, utils_js_1.getTestomatIdFromTestTitle)(`${title} ${tags?.join(' ')}`),
suite_title: test.parent && stripTagsFromTitle(stripExampleFromTitle(test.parent.title).title),
error,
message: testObj.message,
time: test.duration,
files,
steps: stepHierarchy, // Array of step objects per API schema
logs,
links,
manuallyAttachedArtifacts,
meta: { ...keyValues, ...test.meta },
});
processArtifactsForUpload(artifacts, uid, title, videos, traces);
});
event.dispatcher.on(event.step.started, step => {
const stepText = `${repeat(output.stepShift)} ${step.toCliStyled ? step.toCliStyled() : step.toString()}`;
data_storage_js_1.dataStorage.putData('log', stepText);
});
event.dispatcher.on(event.step.finished, step => {
processMetaStepsForDisplay(step);
captureHookStep(step, currentHook, hookSteps);
});
}
async function uploadAttachments(client, attachments, messagePrefix, attachmentType) {
if (!attachments?.length)
return;
if (client.uploader.isEnabled) {
console.log(constants_js_1.APP_PREFIX, `Attachments: ${messagePrefix} ${attachments.length} ${attachmentType} ...`);
}
const promises = attachments.map(async (attachment) => {
const { rid, title, path, type } = attachment;
const file = { path, type, title };
// we are storing file if upload is disabled
if (!client.uploader.isEnabled)
return client.uploader.storeUploadedFile(path, client.runId, rid, false);
return client.addTestRun(undefined, {
...stripExampleFromTitle(title),
rid,
files: [file],
});
});
await Promise.all(promises);
}
function getTestAndMessage(title) {
const testObj = { message: '' };
const testArr = title.split(/\s(\|\s\{.*?\})/);
testObj.title = testArr[0];
return testObj;
}
function stripExampleFromTitle(title) {
const res = title.match(DATA_REGEXP);
if (!res)
return { title, example: null };
try {
const example = JSON.parse(res[1]);
title = title.replace(DATA_REGEXP, '').trim();
return { title, example };
}
catch (e) {
// If JSON parsing fails, return title without example
debug('Failed to parse example JSON:', res[1], e.message);
return { title: title.replace(DATA_REGEXP, '').trim(), example: null };
}
}
function stripTagsFromTitle(title) {
// Remove @tags from the end of titles (e.g., "Hooks Test Suite @hooks" -> "Hooks Test Suite")
return title.replace(/\s+@[\w-]+\s*$/, '').trim();
}
function repeat(num) {
return ''.padStart(num, ' ');
}
// Helper functions for cleaner event handling
function initializeTestDataStore() {
if (!global.testomatioDataStore)
global.testomatioDataStore = {};
global.testomatioDataStore.steps = [];
}
function buildArtifactFiles(artifacts) {
const files = [];
if (artifacts.screenshot) {
files.push({ path: artifacts.screenshot, type: 'image/png' });
}
return files;
}
function processArtifactsForUpload(artifacts, uid, title, videos, traces) {
for (const aid in artifacts) {
if (aid.startsWith('video')) {
videos.push({ rid: uid, title, path: artifacts[aid], type: 'video/webm' });
}
if (aid.startsWith('trace')) {
traces.push({ rid: uid, title, path: artifacts[aid], type: 'application/zip' });
}
}
}
function processMetaStepsForDisplay(step) {
const metaSteps = [];
let processingStep = step;
while (processingStep.metaStep) {
metaSteps.unshift(processingStep.metaStep);
processingStep = processingStep.metaStep;
}
}
function captureHookStep(step, currentHook, hookSteps) {
if (!currentHook)
return;
const startTime = step.startTime;
const endTime = step.endTime;
const hookStepsArray = hookSteps.get(currentHook) || [];
hookStepsArray.push({
name: step.name,
actor: step.actor,
args: step.args,
status: step.status,
startTime,
endTime,
helperMethod: step.helperMethod
});
hookSteps.set(currentHook, hookStepsArray);
}
// TODO: think about moving to some common utils
function getTestLogs(test) {
// Contexts for each log section
const suiteTitle = test.parent.fullTitle();
const testTitle = test.fullTitle();
const beforeSuiteLogsArr = index_js_1.services.logger.getLogs(`BeforeSuite ${suiteTitle}`);
const beforeLogsArr = index_js_1.services.logger.getLogs(`Before ${testTitle}`);
const testLogsArr = index_js_1.services.logger.getLogs(testTitle);
const afterLogsArr = index_js_1.services.logger.getLogs(`After ${testTitle}`);
const afterSuiteLogsArr = index_js_1.services.logger.getLogs(`AfterSuite ${suiteTitle}`);
const beforeSuiteLogs = beforeSuiteLogsArr ? beforeSuiteLogsArr.join('\n').trim() : '';
const beforeLogs = beforeLogsArr ? beforeLogsArr.join('\n').trim() : '';
const testLogs = testLogsArr ? testLogsArr.join('\n').trim() : '';
const afterLogs = afterLogsArr ? afterLogsArr.join('\n').trim() : '';
const afterSuiteLogs = afterSuiteLogsArr ? afterSuiteLogsArr.join('\n').trim() : '';
let logs = '';
if (beforeSuiteLogs) {
logs += `${picocolors_1.default.bold('--- BeforeSuite ---')}\n${beforeSuiteLogs}`;
}
if (beforeLogs) {
logs += `\n${picocolors_1.default.bold('--- Before ---')}\n${beforeLogs}`;
}
if (testLogs) {
logs += `\n${picocolors_1.default.bold('--- Test ---')}\n${testLogs}`;
}
if (afterLogs) {
logs += `\n${picocolors_1.default.bold('--- After ---')}\n${afterLogs}`;
}
if (afterSuiteLogs) {
logs += `\n${picocolors_1.default.bold('--- AfterSuite ---')}\n${afterSuiteLogs}`;
}
return logs;
}
// Build step hierarchy using CodeceptJS built-in methods
function buildUnifiedStepHierarchy(steps, hookSteps) {
const hierarchy = [];
// Add pre-test hooks
addHooksToHierarchy(hierarchy, hookSteps, HOOK_EXECUTION_ORDER.PRE_TEST);
// Process test steps if they exist
if (steps && steps.length > 0) {
processTestSteps(steps, hierarchy);
}
// Add post-test hooks
addHooksToHierarchy(hierarchy, hookSteps, HOOK_EXECUTION_ORDER.POST_TEST);
return hierarchy;
}
function addHooksToHierarchy(hierarchy, hookSteps, hookNames) {
for (const hookName of hookNames) {
if (hookSteps.has(hookName)) {
const hookSection = createHookSection(hookName, hookSteps.get(hookName));
if (hookSection)
hierarchy.push(hookSection);
}
}
}
function processTestSteps(steps, hierarchy) {
const sectionMap = new Map();
for (const step of steps) {
const formattedStep = formatCodeceptStep(step);
if (!formattedStep)
continue;
if (step.metaStep) {
// Step belongs to a section (meta step)
const sectionKey = step.metaStep;
let sectionStep = sectionMap.get(sectionKey);
if (!sectionStep) {
sectionStep = createSectionStep(step.metaStep);
sectionMap.set(sectionKey, sectionStep);
hierarchy.push(sectionStep);
}
sectionStep.steps.push(formattedStep);
sectionStep.duration += formattedStep.duration || 0;
}
else {
// Regular step
hierarchy.push(formattedStep);
}
}
}
function createSectionStep(metaStep) {
return {
category: 'user',
title: metaStep.toString(), // Use built-in toString method
duration: metaStep.duration || 0, // Use built-in duration
steps: []
};
}
function createHookSection(hookName, steps) {
if (!steps || steps.length === 0)
return null;
const hookSection = {
category: 'hook',
title: formatHookName(hookName),
duration: 0,
steps: []
};
for (const step of steps) {
const formattedStep = formatHookStep(step);
if (formattedStep) {
hookSection.steps.push(formattedStep);
hookSection.duration += formattedStep.duration || 0;
}
}
return hookSection.steps.length > 0 ? hookSection : null;
}
function formatHookName(hookName) {
return hookName.replace(/Hook$/, '');
}
// Format CodeceptJS step using its built-in methods
function formatCodeceptStep(step) {
if (!step)
return null;
const category = step.constructor.name === 'HelperStep' ? 'framework' : 'user';
const title = (0, utils_js_1.truncate)(step); // Use built-in toString
const duration = step.duration || 0; // Use built-in duration
const formattedStep = {
category,
title,
duration
};
// Add error if step failed
if (step.status === 'failed' && step.err) {
formattedStep.error = {
message: step.err.message || 'Step failed',
stack: step.err.stack || ''
};
}
return formattedStep;
}
function formatHookStep(step) {
if (!step)
return null;
// For hook steps, construct title from available properties
let title = step.name;
if (step.actor && step.name) {
title = `${step.actor} ${step.name}`;
if (step.args && step.args.length > 0) {
const argsStr = step.args.map(arg => (0, utils_js_1.truncate)(JSON.stringify(arg))).join(', ');
title += ` ${argsStr}`;
}
}
title = (0, utils_js_1.truncate)(title);
return {
category: 'hook',
title,
duration: step.duration || 0
};
}
module.exports = CodeceptReporter;
module.exports.CodeceptReporter = CodeceptReporter;