Skip to content
Closed
Changes from all commits
Commits
File filter

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
384 changes: 384 additions & 0 deletions agent-js-codecept/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,384 @@
const RPClient = require('@reportportal/client-javascript');
const fs = require('fs');
const path = require('path');
const debug = require('debug')('codeceptjs:reportportal');
const { event, recorder, output, container } = codeceptjs;

const helpers = container.helpers();
let helper;

const rp_FAILED = 'FAILED';
const rp_PASSED = 'PASSED';
const rp_SUITE = 'SUITE';
const rp_TEST = 'TEST';
const rp_STEP = 'STEP';

const screenshotHelpers = [
'WebDriver',
'Protractor',
'Appium',
'Nightmare',
'Puppeteer',
'TestCafe',
'Playwright',
];

for (const helperName of screenshotHelpers) {
if (Object.keys(helpers).indexOf(helperName) > -1) {
helper = helpers[helperName];
}
}

const defaultConfig = {
token: '',
endpoint: '',
projectName: '',
launchName: 'codeceptjs tests',
launchDescription: '',
attributes: [],
debug: false,
rerun: undefined,
enabled: false
};

const requiredFields = ['projectName', 'token', 'endpoint'];


module.exports = (config) => {
config = Object.assign(defaultConfig, config);

for (let field of requiredFields) {
if (!config[field]) throw new Error(`ReportPortal config is invalid. Key ${field} is missing in config.\nRequired fields: ${requiredFields} `)
}

let reportUrl;
let launchObj;
let suiteObj;
let testObj;
let stepObj;
let failedStep;
let rpClient;

let suiteStatus = rp_PASSED;
let launchStatus = rp_PASSED;
let currentMetaSteps = [];

function logCurrent(data, file) {
const obj = stepObj || testObj;
if (obj) rpClient.sendLog(obj.tempId, data, file);
}

function sleep(ms, value) {
return new Promise(resolve => setTimeout(resolve, ms, value));
}

event.dispatcher.on(event.all.before, async () => {
launchObj = startLaunch();
let maxTries = 10;
for (let count=0; count<maxTries; ++count){
if (count > 0) {
await sleep(10000);
}
try {
await launchObj.promise;
return;
}
catch (err) {
output.error(`❌ Unable to connect ReportPortal, retrying...`);
output.error(err)
}
}
output.print(`📋 Writing results to ReportPortal: ${config.projectName} > ${config.endpoint}`);

const outputLog = output.log;
const outputDebug = output.debug;
const outputError = output.error;

output.log = (message) => {
outputLog(message);
logCurrent({ level: 'trace', message });
}

output.debug = (message) => {
outputDebug(message);
logCurrent({ level: 'debug', message });
}

output.error = (message) => {
outputError(message);
logCurrent({ level: 'error', message });
}

});

event.dispatcher.on(event.suite.before, (suite) => {
recorder.add(async () => {
suiteObj = startTestItem(suite.title, rp_SUITE);
debug(`${suiteObj.tempId}: The suiteId '${suite.title}' is started.`);
suite.tempId = suiteObj.tempId;
suiteStatus = rp_PASSED;
});
});

event.dispatcher.on(event.test.before, (test) => {
recorder.add(async () => {
currentMetaSteps = [];
stepObj = null;
testObj = startTestItem(test.title, rp_TEST, suiteObj.tempId);
test.tempId = testObj.tempId;
failedStep = null;
debug(`${testObj.tempId}: The testId '${test.title}' is started.`);
})
});

event.dispatcher.on(event.step.before, (step) => {
recorder.add(async () => {
const parent = await startMetaSteps(step);
stepObj = startTestItem(step.toString().slice(0, 300), rp_STEP, parent.tempId);
step.tempId = stepObj.tempId;
})
});

event.dispatcher.on(event.step.after, (step) => {
recorder.add(() => finishStep(step));
});

event.dispatcher.on(event.step.failed, (step) => {
for (const metaStep of currentMetaSteps) {
if (metaStep) metaStep.status = 'failed';
}
if (step && step.tempId) failedStep = Object.assign({}, step);
});

event.dispatcher.on(event.step.passed, (step, err) => {
for (const metaStep of currentMetaSteps) {
metaStep.status = 'passed';
}
failedStep = null;
});

event.dispatcher.on(event.test.failed, async (test, err) => {
launchStatus = rp_FAILED;
suiteStatus = rp_FAILED;

if (failedStep && failedStep.tempId) {
const step = failedStep;

debug(`Attaching screenshot & error to failed step`);

const screenshot = await attachScreenshot();

resp = await rpClient.sendLog(step.tempId, {
level: 'ERROR',
message: `${err.stack}`,
time: step.startTime,
}, screenshot).promise;

}

if (!test.tempId) return;

debug(`${test.tempId}: Test '${test.title}' failed.`);

if (!failedStep) {
await rpClient.sendLog(test.tempId, {
level: 'ERROR',
message: `${err.stack}`,
}).promise;
}

rpClient.finishTestItem(test.tempId, {
endTime: test.endTime || rpClient.helpers.now(),
status: rp_FAILED,
message: `${err.stack}`,
});
});

event.dispatcher.on(event.test.passed, (test, err) => {
debug(`${test.tempId}: Test '${test.title}' passed.`);
rpClient.finishTestItem(test.tempId, {
endTime: test.endTime || rpClient.helpers.now(),
status: rp_PASSED,
});
});

event.dispatcher.on(event.test.after, (test) => {
recorder.add(async () => {
debug(`closing ${currentMetaSteps.length} metasteps for failed test`);
if (failedStep) await finishStep(failedStep);
await Promise.all(currentMetaSteps.reverse().map(m => finishStep(m)));
stepObj = null;
testObj = null;
});
});

event.dispatcher.on(event.suite.after, (suite) => {
recorder.add(async () => {
debug(`${suite.tempId}: Suite '${suite.title}' finished ${suiteStatus}.`);
return rpClient.finishTestItem(suite.tempId, {
endTime: suite.endTime || rpClient.helpers.now(),
status: rpStatus(suiteStatus)
});
});
});

function startTestItem(testTitle, method, parentId = null) {
try {
const hasStats = method !== rp_STEP;
return rpClient.startTestItem({
name: testTitle,
type: method,
hasStats,
}, launchObj.tempId, parentId);
} catch (error) {
output.err(error);
}

}

event.dispatcher.on(event.all.result, async () => {
// await recorder.promise;
debug('Finishing launch...');
if (suiteObj) {
rpClient.finishTestItem(suiteObj.tempId, {
status: suiteStatus,
}).promise;
}
await finishLaunch();
});

function startLaunch(suiteTitle) {
rpClient = new RPClient({
token: config.token,
endpoint: config.endpoint,
project: config.projectName,
debug: config.debug,
});

return rpClient.startLaunch({
name: config.launchName || suiteTitle,
description: config.launchDescription,
attributes: config.launchAttributes,
rerun: config.rerun,
rerunOf: config.rerunOf,
});
}

async function attachScreenshot() {
if (!helper) return undefined;

const fileName = `${rpClient.helpers.now()}.png`;
try {
await helper.saveScreenshot(fileName);
} catch (err) {
output.error(`Couldn't save screenshot`);
return undefined;
}

const content = fs.readFileSync(path.join(global.output_dir, fileName));
fs.unlinkSync(path.join(global.output_dir, fileName));

return {
name: 'failed.png',
type: 'image/png',
content,
}
}

async function finishLaunch() {
try {
debug(`${launchObj.tempId} Finished launch: ${launchStatus}`)
const launch = rpClient.finishLaunch(launchObj.tempId, {
status: launchStatus,
});

const response = await launch.promise;

reportUrl = response.link;
output.print(` 📋 Report #${response.number} saved ➡`, response.link);
event.emit('reportportal.result', response);
} catch (error) {
console.log(error);
debug(error);
}
}

async function startMetaSteps(step) {
let metaStepObj = {};
const metaSteps = metaStepsToArray(step.metaStep);

// close current metasteps
for (let j = currentMetaSteps.length-1; j >= metaSteps.length; j--) {
await finishStep(currentMetaSteps[j]);
}

for (const i in metaSteps) {
const metaStep = metaSteps[i];
if (isEqualMetaStep(metaStep, currentMetaSteps[i])) {
metaStep.tempId = currentMetaSteps[i].tempId;
continue;
}
// close metasteps other than current
for (let j = currentMetaSteps.length-1; j >= i; j--) {
await finishStep(currentMetaSteps[j]);
delete currentMetaSteps[j];
}

metaStepObj = currentMetaSteps[i-1] || metaStepObj;

const isNested = !!metaStepObj.tempId;
metaStepObj = startTestItem(metaStep.toString(), rp_STEP, metaStepObj.tempId || testObj.tempId);
metaStep.tempId = metaStepObj.tempId;
debug(`${metaStep.tempId}: The stepId '${metaStep.toString()}' is started. Nested: ${isNested}`);
}

currentMetaSteps = metaSteps;
return currentMetaSteps[currentMetaSteps.length - 1] || testObj;
}

function finishStep(step) {
if (!step) return;
if (!step.tempId) {
debug(`WARNING: '${step.toString()}' step can't be closed, it has no tempId`);
return;
}
debug(`Finishing '${step.toString()}' step`);

return rpClient.finishTestItem(step.tempId, {
endTime: rpClient.helpers.now(),
status: rpStatus(step.status),
});
}

return {
addLog: logCurrent,
};
};

function metaStepsToArray(step) {
let metaSteps = [];
iterateMetaSteps(step, metaStep => metaSteps.push(metaStep));
return metaSteps;
}

function iterateMetaSteps(step, fn) {
if (step && step.metaStep) iterateMetaSteps(step.metaStep, fn);
if (step) fn(step);
}


const isEqualMetaStep = (metastep1, metastep2) => {
if (!metastep1 && !metastep2) return true;
if (!metastep1 || !metastep2) return false;
return metastep1.actor === metastep2.actor
&& metastep1.name === metastep2.name
&& metastep1.args.join(',') === metastep2.args.join(',');
};


function rpStatus(status) {
if (status === 'success') return rp_PASSED;
if (status === 'failed') return rp_FAILED;
return status;
}