UNPKG

@qavajs/format-report-portal

Version:
327 lines (296 loc) 12.4 kB
const { Formatter, Status } = require('@cucumber/cucumber'); const RPClient = require('@reportportal/client-javascript'); const { retry } = require('./utils'); const RP_ATTRIBUTE_PREFIX = /^rp_attribute:\s*/; const isAttribute = (attachment) => attachment.mediaType === 'text/x.cucumber.log+plain' && RP_ATTRIBUTE_PREFIX.test(attachment.body) class RPFormatter extends Formatter { launchId = null; constructor(options) { super(options); const rpEnable = options.parsedArgvOptions?.rpConfig?.enable; if (rpEnable !== undefined && !rpEnable) return undefined; options.eventBroadcaster.on('envelope', this.processEnvelope.bind(this)); this.rpConfig = options.parsedArgvOptions.rpConfig; this.rpClient = new RPClient(this.rpConfig); this.promiseQ = []; this.stepDefinitions = {}; if (this.rpConfig.legacyTimeFormat) { this.rpClient.helpers.now = () => { return Date.now() }; } } async processEnvelope(envelope) { try { if (envelope.stepDefinition || envelope.hook) { return this.readStepDefinition(envelope); } if (envelope.testRunStarted) { const startLaunch = this.startLaunch(); this.promiseQ.push(startLaunch); return await startLaunch; } if (envelope.testCaseFinished) { const finishTest = this.finishTest(envelope) this.promiseQ.push(finishTest); return await finishTest; } if (envelope.testRunFinished) { return await this.finishLaunch(); } } catch (err) { if (this.rpConfig.ignoreErrors) { console.error(err); } else { throw err; } } } readStepDefinition(stepDefinition) { const definition = stepDefinition.stepDefinition ?? stepDefinition.hook; this.stepDefinitions[definition.id] = definition; } async startLaunch() { const launchObj = this.rpClient.startLaunch({ name: this.rpConfig.launch, startTime: this.rpClient.helpers.now(), description: this.rpConfig.description, attributes: this.rpConfig.tags?.filter(tag => tag), mode: this.rpConfig.mode, debug: this.rpConfig.debug }); this.launchId = launchObj.tempId; this.features = {}; await launchObj.promise; } async finishLaunch() { await Promise.allSettled(this.promiseQ); for (const featureName in this.features) { await this.rpClient.finishTestItem(this.features[featureName], { status: 'PASSED' }).promise; } const launch = await this.rpClient.finishLaunch(this.launchId, { endTime: this.rpClient.helpers.now() }).promise; } async finishTest(envelope) { const testCase = this.eventDataCollector.getTestCaseAttempt(envelope.testCaseFinished.testCaseStartedId); const featureName = testCase.gherkinDocument.feature.name; if (!this.features[featureName]) { await retry(async () => { const featureItem = this.rpClient.startTestItem({ attributes: this.rpConfig.tagsAsAttributes ? this.prepareTags(testCase.gherkinDocument.feature.tags) : [], description: this.rpConfig.tagsAsAttributes ? '' : `${this.formatTags(testCase.gherkinDocument.feature.tags)}\n` + testCase.gherkinDocument.feature.description, name: featureName, startTime: this.rpClient.helpers.now(), type: 'SUITE' }, this.launchId); this.features[featureName] = featureItem.tempId; await featureItem.promise; }, this.rpConfig.retry); } const featureTempId = this.features[featureName]; let startTime = this.rpClient.helpers.now(); let endTime; const steps = this.getStepResults(testCase); const attributes = steps .reduce((attachments, step) => { const attrs = step.attachment .filter(isAttribute) .map(attachment => attachment.body.replace(RP_ATTRIBUTE_PREFIX, '')); return [...new Set([...attachments, ...attrs])] }, []) .map(attachment => { const [key, value] = attachment.split(':'); return key && value ? { key, value, system: false } : { value: key, system: false } }); // Start test const retryTest = Boolean(testCase.attempt); const testItem = await retry(async () => { const testItem = this.rpClient.startTestItem({ description: this.rpConfig.tagsAsAttributes ? '' : this.formatTags(testCase.pickle.tags), name: testCase.pickle.name, startTime, type: 'STEP', attributes: [ ...attributes, ...(this.rpConfig.tagsAsAttributes ? this.prepareTags(testCase.pickle.tags) : []) ], retry: retryTest }, this.launchId, featureTempId); await testItem.promise; return testItem; }, this.rpConfig.retry); //send steps for (const step of steps) { const duration = step.result.duration; endTime = startTime + (duration.seconds * 1_000) + Math.floor(duration.nanos / 1_000_000); const nestedTestItem = await retry(async () => { const nestedTestItem = this.rpClient.startTestItem({ description: 'test description', name: this.getStepText(step, steps), startTime, type: 'STEP', hasStats: false }, this.launchId, testItem.tempId); await nestedTestItem.promise; return nestedTestItem; }, this.rpConfig.retry); if (step.result.message) { await retry(async () => { const log = await this.rpClient.sendLog(nestedTestItem.tempId, { level: 'ERROR', message: this.getMessage(step), time: startTime }); await log.promise; }, this.rpConfig.retry); } if (step.attachment) { for (const attachment of step.attachment) { await retry(async () => { await this.sendAttachment(attachment, nestedTestItem, startTime); }, this.rpConfig.retry); } } await retry(async () => { const nestedItemFinish = this.rpClient.finishTestItem(nestedTestItem.tempId, { status: this.getStatus(step), endTime }); await nestedItemFinish.promise; startTime = endTime; }, this.rpConfig.retry); } //finish test item const status = Object.values(testCase.stepResults).some(step => step.status !== Status.PASSED) ? Status.FAILED.toLowerCase() : Status.PASSED.toLowerCase() const testItemFinish = this.rpClient.finishTestItem(testItem.tempId, { status, endTime }); await testItemFinish.promise; } getStepResults(testCase) { return testCase.testCase.testSteps.map(step => ({ id: step.id, stepDefinitionId: step.pickleStepId ?? step.hookId, result: testCase.stepResults[step.id], pickle: testCase.pickle.steps.find(pickle => pickle.id === step.pickleStepId), attachment: testCase.stepAttachments[step.id] ?? [] })) } getStepText(step, steps) { if (!step.pickle) return this.hookKeyword(step, steps); const messageParts = [step.pickle.text]; if (step.pickle.argument) { if (step.pickle.argument.dataTable) messageParts.push( this.formatTable(step.pickle.argument.dataTable) ) if (step.pickle.argument.docString) messageParts.push(this.formatDocString(step.pickle.argument.docString)) } return messageParts.join('\n') } hookKeyword(step, steps) { const hook = this.stepDefinitions[step.stepDefinitionId]; if (hook?.name) return hook.name; const stepsBefore = steps.slice(0, steps.findIndex((element) => element === step)); return stepsBefore.every(element => element.pickle === undefined) ? 'Before' : 'After' } getMessage(step) { return step.result.message } getStatus(step) { switch (step.result.status) { case Status.PASSED: return Status.PASSED.toLowerCase(); case Status.SKIPPED: return Status.SKIPPED.toLowerCase(); default: return Status.FAILED.toLowerCase() } } formatTable(dataTable) { const TR = '<tr>'; const TRE = '</tr>'; const TD = '<td>'; const TDE = '</td>'; const formatRow = row => TR + row.cells.map(cell => TD + cell.value + TDE).join('') + TRE; return '<table><tbody>' + dataTable.rows.map(formatRow).join('') + '</tbody></table>' } formatDocString(docString) { return '<pre><code>' + docString.content + '</code></pre>' } formatTags(tags) { return tags.map(tag => '<code>' + tag.name + '</code>').join('') } prepareTags(tags) { return tags.map(tag => tag.name) } prepareContent(attachment) { return ['text/plain', 'application/json'].includes(attachment.mediaType) ? Buffer.from(attachment.body).toString('base64') : attachment.body } async sendAttachment(attachment, testItem, startTime) { let log; if (attachment.mediaType === 'text/x.cucumber.log+plain' && RP_ATTRIBUTE_PREFIX.test(attachment.body)) return; if (attachment.mediaType === 'text/x.cucumber.log+plain') { log = await this.rpClient.sendLog(testItem.tempId, { level: 'INFO', message: attachment.body, time: startTime }); } else if (attachment.mediaType === 'text/x.response.json') { log = await this.rpClient.sendLog(testItem.tempId, { level: 'INFO', message: this.responseBody(attachment.body), time: startTime }); } else { const attachmentData = { name: 'attachment', type: attachment.mediaType, content: this.prepareContent(attachment), }; log = await this.rpClient.sendLog(testItem.tempId, { level: 'INFO', message: attachment.fileName || 'Attachment', time: startTime }, attachmentData); } await log.promise; } renderHeaders(headers) { return Object.entries(headers).map(([key, value]) => `${key}: ${value}`).join('\n'); } responseBody(log) { const payload = JSON.parse(log); const isText = header => ['application/json', 'text/plain', 'text/html'].some(mime => (header ?? '').includes(mime)); const reqBody = isText(payload.request.headers['content-type']) ? Buffer.from(payload.request.body, 'base64').toString() : payload.request.body; const resBody = isText(payload.response.headers['content-type']) ? Buffer.from(payload.response.body, 'base64').toString() : payload.request.body; return `${payload.request.method} ${payload.request.url} - ${payload.response.status} **Request** body: \`\`\` ${reqBody} \`\`\` headers: \`\`\` ${this.renderHeaders(payload.request.headers)} \`\`\` **Response** body: \`\`\` ${resBody} \`\`\` headers: \`\`\` ${this.renderHeaders(payload.response.headers)} \`\`\` ` } } module.exports = RPFormatter