Skip to content

Upload videos #150

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion lib/plugin/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,10 @@
const ipc = require('node-ipc');
const { connectIPCClient } = require('./ipcClient');
const { IPC_EVENTS } = require('./../ipcEvents');
const { prepareReporterOptions } = require('./../utils');

const registerReportPortalPlugin = (on, config, callbacks) => {
connectIPCClient(config);
connectIPCClient(prepareReporterOptions(config));

on('task', {
rp_Log(log) {
Expand Down
58 changes: 55 additions & 3 deletions lib/reporter.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,21 @@
*/

const RPClient = require('@reportportal/client-javascript');

const { entityType, logLevels } = require('./constants');
const { entityType, testItemStatuses, logLevels } = require('./constants');
const {
getScreenshotAttachment,
getTestStartObject,
getTestEndObject,
getHookStartObject,
getAgentInfo,
getVideoFile,
} = require('./utils');

const { createMergeLaunchLockFile, deleteMergeLaunchLockFile } = require('./mergeLaunchesUtils');
const { mergeParallelLaunches } = require('./mergeLaunches');

const { FAILED, PASSED } = testItemStatuses;

const promiseErrorHandler = (promise, message = '') =>
promise.catch((err) => {
console.error(message, err);
Expand Down Expand Up @@ -102,13 +104,26 @@ class Reporter {
const { tempId, promise } = this.client.startTestItem(suite, this.tempLaunchId, parentId);
promiseErrorHandler(promise, 'Fail to start suite');
this.testItemIds.set(suite.id, tempId);
this.suitesStackTempInfo.push({ tempId, startTime: suite.startTime });
this.suitesStackTempInfo.push({
tempId,
startTime: suite.startTime,
title: suite.name || '',
id: suite.id,
testFileName: suite.testFileName,
});
}

suiteEnd(suite) {
const suiteId = this.testItemIds.get(suite.id);
const suiteTestCaseId = this.suiteTestCaseIds.get(suite.title);
const suiteStatus = this.suiteStatuses.get(suite.title);

// if suite fails, also the root suite status must fail
if (suite.status === FAILED && suiteStatus !== PASSED) {
this.suitesStackTempInfo[0].status = FAILED;
}
this.sendVideoOnFinishSuite(suite);

const finishTestItemPromise = this.client.finishTestItem(
suiteId,
Object.assign(
Expand Down Expand Up @@ -141,6 +156,39 @@ class Reporter {
}
}

sendVideoOnFinishSuite(suite) {
if (!this.suitesStackTempInfo.length || suite.id !== this.suitesStackTempInfo[0].id) {
return;
}
// do not upload video if root suite passes and videoUploadOnPasses is false
const videoUploadOnPasses = this.config.reporterOptions.videoUploadOnPasses || false;
const rootSuite = this.suitesStackTempInfo[0];
const failed = rootSuite.status && rootSuite.status === testItemStatuses.FAILED;
if (!failed && !videoUploadOnPasses) {
return;
}

const testFileName = this.suitesStackTempInfo[0].testFileName;
if (!testFileName) return;

const videosFolder = this.config.reporterOptions.videosFolder;
const specFileName = testFileName.split('/').pop();
const videoFileDetails = getVideoFile(specFileName, videosFolder);
if (!videoFileDetails) return;

const suiteId = this.testItemIds.get(suite.id);
const sendVideoPromise = this.client.sendLog(
suiteId,
{
message: `Video: '${suite.title}' (${specFileName}.mp4)`,
level: logLevels.INFO,
time: new Date().valueOf(),
},
videoFileDetails,
).promise;
promiseErrorHandler(sendVideoPromise, 'Fail to save video');
}

testEnd(test) {
let testId = this.testItemIds.get(test.id);
if (!testId) {
Expand Down Expand Up @@ -256,6 +304,10 @@ class Reporter {
setTestItemStatus({ status, suiteTitle }) {
if (suiteTitle) {
this.suiteStatuses.set(suiteTitle, status);
const rootSuite = this.suitesStackTempInfo.length && this.suitesStackTempInfo[0];
if (rootSuite && status === testItemStatuses.FAILED) {
this.suitesStackTempInfo[0].status = status;
}
} else {
Object.assign(this.currentTestFinishParams, status && { status });
}
Expand Down
53 changes: 48 additions & 5 deletions lib/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,21 @@ const getScreenshotAttachment = (absolutePath) => {
};
};

const getVideoFile = (specFileName, videosFolder = '**') => {
if (specFileName == null) return specFileName;
const fileName = specFileName.toLowerCase().endsWith('.mp4')
? specFileName
: `${specFileName}.mp4`;
const videoFile = glob.sync(`${videosFolder}/${fileName}`);
if (!videoFile.length) return undefined;

return {
name: fileName,
type: 'video/mp4',
content: base64Encode(videoFile[0]),
};
};

const getCodeRef = (testItemPath, testFileName) =>
`${testFileName.replace(/\\/g, '/')}/${testItemPath.join('/')}`;

Expand Down Expand Up @@ -87,6 +102,23 @@ const getConfig = (initialConfig) => {
};
};

const prepareReporterOptions = (config) => {
// copy this options from cypess config to report portal options
const passthroughOptions = {
videosFolder: config.videosFolder,
screenshotsFolder: config.screenshotsFolder,
videoUploadOnPasses: config.videoUploadOnPasses,
};

return {
...config,
reporterOptions: {
...passthroughOptions,
...config.reporterOptions,
},
};
};

Comment on lines +105 to +121
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@AmsterGet I introduced a way of passing Cypress config options into reporterOptions to access selected options within the reporter. Reporter will work without any of the current passed through options, but videoUploadOnPasses is important to supported expected behavior from user perspective.

const getLaunchStartObject = (config) => {
const launchAttributes = (config.reporterOptions.attributes || []).concat(
getSystemAttributes(config),
Expand All @@ -112,13 +144,22 @@ const getSuiteStartObject = (suite, testFileName) => ({
attributes: [],
codeRef: getCodeRef(suite.titlePath(), testFileName),
parentId: !suite.root ? suite.parent.id : undefined,
testFileName,
});

const getSuiteEndObject = (suite) => ({
id: suite.id,
title: suite.title,
endTime: new Date().valueOf(),
});
const getSuiteEndObject = (suite) => {
let failed = false;
if (suite.tests != null) {
const states = suite.tests.map((test) => test.state);
failed = states.includes(testItemStatuses.FAILED);
}
return {
id: suite.id,
status: failed ? testItemStatuses.FAILED : undefined,
title: suite.title,
endTime: new Date().valueOf(),
};
};

const getTestInfo = (test, testFileName, status, err) => ({
id: test.id,
Expand Down Expand Up @@ -256,7 +297,9 @@ module.exports = {
getHookStartObject,
getTotalSpecs,
getConfig,
prepareReporterOptions,
getExcludeSpecPattern,
getFixtureFolderPattern,
getSpecPattern,
getVideoFile,
};
188 changes: 188 additions & 0 deletions test/reporter.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,194 @@ describe('reporter script', () => {
});
});

describe('sendVideoOnFinishSuite', function() {
let customSuiteNameAttachment;

beforeAll(() => {
mockFS({
example: {
videos: {
'custom suite name.cy.ts.mp4': Buffer.from([1, 2, 7, 9, 3, 0, 5]),
},
},
});
});

afterAll(() => {
mockFS.restore();
reporter.config.reporterOptions.videosFolder = undefined;
});

beforeEach(() => {
customSuiteNameAttachment = {
name: `custom suite name.cy.ts.mp4`,
type: 'video/mp4',
content: Buffer.from([1, 2, 7, 9, 3, 0, 5]).toString('base64'),
};
reporter.suitesStackTempInfo = [
{ id: 'root', title: 'root suite', testFileName: 'custom suite name.cy.ts' },
{ id: 'suite', title: 'any suite' },
];
reporter.testItemIds.set('root', 'suiteTempId');
reporter.config.reporterOptions.videosFolder = 'example/videos';
});

afterEach(() => {
reporter.suitesStackTempInfo = [];
delete reporter.config.reporterOptions.videoUploadOnPasses;
});

it('sendLog with video attachment - fail root suite if any suite fails', function() {
const suiteEndObject = {
id: 'suite',
title: 'suite title',
status: 'failed',
};

expect(reporter.suitesStackTempInfo[0].status).not.toBeDefined();
reporter.suiteEnd(suiteEndObject);
expect(reporter.suitesStackTempInfo[0].status).toEqual('failed');
});

it('sendLog with video attachment - fail root suite if setTestItemStatus fails for any suite', function() {
expect(reporter.suitesStackTempInfo[0].status).not.toBeDefined();
reporter.setTestItemStatus({ status: 'failed', suiteTitle: 'any suite' });
expect(reporter.suitesStackTempInfo[0].status).toEqual('failed');
});

it('sendLog with video attachment - send log with video for failed root suite', function() {
const spySendVideoOnFinishSuite = jest.spyOn(reporter.client, 'sendLog');

const suiteEndObject = {
id: 'root',
title: 'suite title',
status: 'failed',
};

reporter.suiteEnd(suiteEndObject);

expect(spySendVideoOnFinishSuite).toHaveBeenCalledTimes(1);
expect(spySendVideoOnFinishSuite).toHaveBeenCalledWith(
'suiteTempId',
{
message: `Video: '${suiteEndObject.title}' (custom suite name.cy.ts.mp4)`,
level: 'info',
time: new Date().valueOf(),
},
customSuiteNameAttachment,
);
});

it('sendLog with video attachment - do not send if suite is not root suite', function() {
const spySendVideoOnFinishSuite = jest.spyOn(reporter.client, 'sendLog');

const suiteEndObject = {
id: 'suite',
title: 'suite title',
status: 'passed',
};

reporter.suiteEnd(suiteEndObject);

expect(spySendVideoOnFinishSuite).not.toHaveBeenCalled();
});

it('sendLog with video attachment - do not send if root suite passed and videoUploadOnPasses is false', function() {
const spySendVideoOnFinishSuite = jest.spyOn(reporter.client, 'sendLog');

const suiteEndObject = {
id: 'root',
title: 'suite title',
status: 'passed',
};

reporter.suiteEnd(suiteEndObject);

expect(spySendVideoOnFinishSuite).not.toHaveBeenCalled();
});

it('sendLog with video attachment - do not send if failed but setTestItemStatus passed', function() {
const spySendVideoOnFinishSuite = jest.spyOn(reporter.client, 'sendLog');

reporter.setTestItemStatus({ status: 'passed', suiteTitle: 'suite title' });

const suiteEndObject = {
id: 'root',
title: 'suite title',
status: 'failed',
};

reporter.suiteEnd(suiteEndObject);

expect(spySendVideoOnFinishSuite).not.toHaveBeenCalled();
});

it('sendLog with video attachment - do not send if video not found in videosFolder', function() {
const spySendVideoOnFinishSuite = jest.spyOn(reporter.client, 'sendLog');

const suiteEndObject = {
id: 'root',
title: 'suite title',
status: 'failed',
};

reporter.config.reporterOptions.videosFolder = 'example/screenshots';
reporter.suiteEnd(suiteEndObject);

expect(spySendVideoOnFinishSuite).not.toHaveBeenCalled();
});

it('sendLog with video attachment - send if root suite passed and videoUploadOnPasses is true', function() {
const spySendVideoOnFinishSuite = jest.spyOn(reporter.client, 'sendLog');
reporter.config.reporterOptions.videoUploadOnPasses = true;

const suiteEndObject = {
id: 'root',
title: 'suite title',
status: 'passed',
};

reporter.suiteEnd(suiteEndObject);

expect(spySendVideoOnFinishSuite).toHaveBeenCalledTimes(1);
expect(spySendVideoOnFinishSuite).toHaveBeenCalledWith(
'suiteTempId',
{
message: `Video: '${suiteEndObject.title}' (custom suite name.cy.ts.mp4)`,
level: 'info',
time: new Date().valueOf(),
},
customSuiteNameAttachment,
);
});

it('sendLog with video attachment - send if passed but setTestItemStatus failed', function() {
const spySendVideoOnFinishSuite = jest.spyOn(reporter.client, 'sendLog');
reporter.config.reporterOptions.videoUploadOnPasses = true;

reporter.setTestItemStatus({ status: 'failed', suiteTitle: 'suite title' });

const suiteEndObject = {
id: 'root',
title: 'suite title',
status: 'passed',
};

reporter.suiteEnd(suiteEndObject);

expect(spySendVideoOnFinishSuite).toHaveBeenCalledTimes(1);
expect(spySendVideoOnFinishSuite).toHaveBeenCalledWith(
'suiteTempId',
{
message: `Video: '${suiteEndObject.title}' (custom suite name.cy.ts.mp4)`,
level: 'info',
time: new Date().valueOf(),
},
customSuiteNameAttachment,
);
});
});

describe('testStart', function() {
it('startTestItem should be called with parameters', function() {
const spyStartTestItem = jest.spyOn(reporter.client, 'startTestItem');
Expand Down
Loading