diff --git a/.github/workflows/CI-pipeline.yml b/.github/workflows/CI-pipeline.yml index e505b0d..9cb6ec1 100644 --- a/.github/workflows/CI-pipeline.yml +++ b/.github/workflows/CI-pipeline.yml @@ -37,5 +37,5 @@ jobs: run: npm install - name: Run lint run: npm run lint -# - name: Run tests and check coverage -# run: npm run test:coverage + - name: Run tests and check coverage + run: npm run test:coverage diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 3c8d0b1..8ea9786 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -31,8 +31,8 @@ jobs: run: npm install - name: Run lint run: npm run lint -# - name: Run tests and check coverage -# run: npm run test:coverage + - name: Run tests and check coverage + run: npm run test:coverage publish-to-npm-and-gpr: needs: build diff --git a/CHANGELOG.md b/CHANGELOG.md index bf65ece..3fda544 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,5 @@ +### Fixed +- Video attachment won't play after downloading, resolves [#202](https://github.com/reportportal/agent-js-cypress/issues/202). Thanks to [ashvinjaiswal](https://github.com/ashvinjaiswal). ## [5.3.3] - 2024-08-15 ### Added diff --git a/README.md b/README.md index 9ec3bcd..8c38cb6 100644 --- a/README.md +++ b/README.md @@ -117,32 +117,32 @@ require('@reportportal/agent-js-cypress/lib/commands/reportPortalCommands'); The full list of available options presented below. -| Option | Necessity | Default | Description | -|-----------------------|------------|-----------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| apiKey | Required | | User's reportportal token from which you want to send requests. It can be found on the profile page of this user. | -| endpoint | Required | | URL of your server. For example 'https://server:8080/api/v1'. | -| launch | Required | | Name of launch at creation. | -| project | Required | | The name of the project in which the launches will be created. | -| attributes | Optional | [] | Launch attributes. | -| description | Optional | '' | Launch description. | -| rerun | Optional | false | Enable [rerun](https://reportportal.io/docs/dev-guides/RerunDevelopersGuide) | -| rerunOf | Optional | Not set | UUID of launch you want to rerun. If not specified, reportportal will update the latest launch with the same name | -| mode | Optional | 'DEFAULT' | Results will be submitted to Launches page
*'DEBUG'* - Results will be submitted to Debug page. | -| skippedIssue | Optional | true | reportportal provides feature to mark skipped tests as not 'To Investigate'.
Option could be equal boolean values:
*true* - skipped tests considered as issues and will be marked as 'To Investigate' on reportportal.
*false* - skipped tests will not be marked as 'To Investigate' on application. | -| debug | Optional | false | This flag allows seeing the logs of the client-javascript. Useful for debugging. | -| launchId | Optional | Not set | The _ID_ of an already existing launch. The launch must be in 'IN_PROGRESS' status while the tests are running. Please note that if this _ID_ is provided, the launch will not be finished at the end of the run and must be finished separately. | -| launchUuidPrint | Optional | false | Whether to print the current launch UUID. | -| launchUuidPrintOutput | Optional | 'STDOUT' | Launch UUID printing output. Possible values: 'STDOUT', 'STDERR'. Works only if `launchUuidPrint` set to `true`. | -| restClientConfig | Optional | Not set | `axios` like http client [config](https://github.com/axios/axios#request-config). May contain `agent` property for configure [http(s)](https://nodejs.org/api/https.html#https_https_request_url_options_callback) client, and other client options eg. `timeout`. For debugging and displaying logs you can set `debug: true`. | -| uploadVideo | Optional | false | Whether to upload the Cypress video. | -| uploadVideoOnPasses | Optional | false | Whether to upload the Cypress video for a non-failure specs. Works only if `uploadVideo` set to `true`. | -| waitForVideoTimeout | Optional | 10000 | Value in `ms`. Since Cypress video processing may take extra time after the spec is complete, there is a timeout to wait for the video file readiness. Works only if `uploadVideo` set to `true`. | -| waitForVideoInterval | Optional | 500 | Value in `ms`. Interval to check if the video file is ready. The interval is used until `waitForVideoTimeout` is reached. Works only if `uploadVideo` set to `true`. | -| autoMerge | Optional | false | Enable automatic report test items of all run spec into one launch. You should install plugin or setup additional settings in reporterOptions. See [Automatically merge launch](#automatically-merge-launches). | -| reportHooks | Optional | false | Determines report before and after hooks or not. | -| isLaunchMergeRequired | Optional | false | Allows to merge Cypress run's into one launch at the end of the run. Needs additional setup. See [Manual merge launches](#manual-merge-launches). | -| parallel | Optional | false | Indicates to the reporter that spec files will be executed in parallel on different machines. Parameter could be equal boolean values. See [Parallel execution](#parallel-execution). | -| token | Deprecated | Not set | Use `apiKey` instead. | +| Option | Necessity | Default | Description | +|-----------------------------|------------|-----------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| apiKey | Required | | User's reportportal token from which you want to send requests. It can be found on the profile page of this user. | +| endpoint | Required | | URL of your server. For example 'https://server:8080/api/v1'. | +| launch | Required | | Name of launch at creation. | +| project | Required | | The name of the project in which the launches will be created. | +| attributes | Optional | [] | Launch attributes. | +| description | Optional | '' | Launch description. | +| rerun | Optional | false | Enable [rerun](https://reportportal.io/docs/dev-guides/RerunDevelopersGuide) | +| rerunOf | Optional | Not set | UUID of launch you want to rerun. If not specified, reportportal will update the latest launch with the same name | +| mode | Optional | 'DEFAULT' | Results will be submitted to Launches page
*'DEBUG'* - Results will be submitted to Debug page. | +| skippedIssue | Optional | true | reportportal provides feature to mark skipped tests as not 'To Investigate'.
Option could be equal boolean values:
*true* - skipped tests considered as issues and will be marked as 'To Investigate' on reportportal.
*false* - skipped tests will not be marked as 'To Investigate' on application. | +| debug | Optional | false | This flag allows seeing the logs of the client-javascript. Useful for debugging. | +| launchId | Optional | Not set | The _ID_ of an already existing launch. The launch must be in 'IN_PROGRESS' status while the tests are running. Please note that if this _ID_ is provided, the launch will not be finished at the end of the run and must be finished separately. | +| launchUuidPrint | Optional | false | Whether to print the current launch UUID. | +| launchUuidPrintOutput | Optional | 'STDOUT' | Launch UUID printing output. Possible values: 'STDOUT', 'STDERR'. Works only if `launchUuidPrint` set to `true`. | +| restClientConfig | Optional | Not set | `axios` like http client [config](https://github.com/axios/axios#request-config). May contain `agent` property for configure [http(s)](https://nodejs.org/api/https.html#https_https_request_url_options_callback) client, and other client options eg. `timeout`. For debugging and displaying logs you can set `debug: true`. | +| uploadVideo | Optional | false | Whether to upload the Cypress video. Uploads videos for failed specs only. To upload videos for specs with other statuses, set also the `uploadVideoForNonFailedSpec` to `true`. | +| uploadVideoForNonFailedSpec | Optional | false | Whether to upload the Cypress video for a non-failed specs. Works only if `uploadVideo` set to `true`. | +| waitForVideoTimeout | Optional | 10000 | Value in `ms`. Since Cypress video processing may take extra time after the spec is complete, there is a timeout to wait for the video file readiness. Works only if `uploadVideo` set to `true`. | +| waitForVideoInterval | Optional | 500 | Value in `ms`. Interval to check if the video file is ready. The interval is used until `waitForVideoTimeout` is reached. Works only if `uploadVideo` set to `true`. | +| autoMerge | Optional | false | Enable automatic report test items of all run spec into one launch. You should install plugin or setup additional settings in reporterOptions. See [Automatically merge launch](#automatically-merge-launches). | +| reportHooks | Optional | false | Determines report before and after hooks or not. | +| isLaunchMergeRequired | Optional | false | Allows to merge Cypress run's into one launch at the end of the run. Needs additional setup. See [Manual merge launches](#manual-merge-launches). | +| parallel | Optional | false | Indicates to the reporter that spec files will be executed in parallel on different machines. Parameter could be equal boolean values. See [Parallel execution](#parallel-execution). | +| token | Deprecated | Not set | Use `apiKey` instead. | ### Overwrite options from config file diff --git a/VERSION b/VERSION index 74664af..16a368e 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -5.3.3 +5.3.4-SNAPSHOT diff --git a/jest.config.js b/jest.config.js index 14c1f17..229503c 100644 --- a/jest.config.js +++ b/jest.config.js @@ -28,6 +28,7 @@ module.exports = { '!lib/ipcServer.js', '!lib/testStatuses.js', '!lib/worker.js', + '!lib/utils/attachments.js', ], coverageThreshold: { global: { diff --git a/lib/reporter.js b/lib/reporter.js index 151b27a..321dffb 100644 --- a/lib/reporter.js +++ b/lib/reporter.js @@ -175,11 +175,11 @@ class Reporter { } finishSuiteWithVideo(suiteInfo, suiteFinishObj) { - const uploadVideoOnPasses = this.config.uploadVideoOnPasses || false; + const uploadVideoForNonFailedSpec = this.config.uploadVideoForNonFailedSpec || false; const suiteFailed = suiteFinishObj.status === testItemStatuses.FAILED; - // do not upload video if root suite passes and uploadVideoOnPasses is false - if ((!suiteFailed && !uploadVideoOnPasses) || !suiteInfo.testFileName) { + // do not upload video if root suite not failed and uploadVideoForNonFailedSpec is false + if ((!suiteFailed && !uploadVideoForNonFailedSpec) || !suiteInfo.testFileName) { this.finishSuite(suiteFinishObj, suiteInfo.tempId); } else { const sendVideoPromise = this.sendVideo(suiteInfo).finally(() => { diff --git a/lib/utils/attachments.js b/lib/utils/attachments.js new file mode 100644 index 0000000..e68afbf --- /dev/null +++ b/lib/utils/attachments.js @@ -0,0 +1,134 @@ +/* + * Copyright 2024 EPAM Systems + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +const fs = require('fs'); +const glob = require('glob'); +const path = require('path'); + +const fsPromises = fs.promises; + +const DEFAULT_WAIT_FOR_FILE_TIMEOUT = 10000; +const DEFAULT_WAIT_FOR_FILE_INTERVAL = 500; + +const getScreenshotAttachment = async (absolutePath) => { + if (!absolutePath) return absolutePath; + const name = absolutePath.split(path.sep).pop(); + return { + name, + type: 'image/png', + content: await fsPromises.readFile(absolutePath, { encoding: 'base64' }), + }; +}; + +async function getFilePathByGlobPattern(globFilePattern) { + const files = await glob.glob(globFilePattern); + + if (files.length) { + return files[0]; + } + + return null; +} +/* + * The moov atom in an MP4 file is a crucial part of the file’s structure. It contains metadata about the video, such as the duration, display characteristics, and timing information. + * Function check for the moov atom in file content and ensure is video file ready. + */ +const checkVideoFileReady = async (videoFilePath) => { + try { + const fileData = await fsPromises.readFile(videoFilePath); + + if (fileData.includes('moov')) { + return true; + } + } catch (e) { + throw new Error(`Error reading file: ${e.message}`); + } + + return false; +}; + +const waitForVideoFile = ( + globFilePattern, + timeout = DEFAULT_WAIT_FOR_FILE_TIMEOUT, + interval = DEFAULT_WAIT_FOR_FILE_INTERVAL, +) => + new Promise((resolve, reject) => { + let filePath = null; + let totalFileWaitingTime = 0; + + async function checkFileExistsAndReady() { + if (!filePath) { + filePath = await getFilePathByGlobPattern(globFilePattern); + } + let isVideoFileReady = false; + + if (filePath) { + isVideoFileReady = await checkVideoFileReady(filePath); + } + + if (isVideoFileReady) { + resolve(filePath); + } else if (totalFileWaitingTime >= timeout) { + reject( + new Error( + `Timeout of ${timeout}ms reached, file ${globFilePattern} not found or not ready yet.`, + ), + ); + } else { + totalFileWaitingTime += interval; + setTimeout(checkFileExistsAndReady, interval); + } + } + + checkFileExistsAndReady().catch(reject); + }); + +const getVideoFile = async ( + specFileName, + videosFolder = '**', + timeout = DEFAULT_WAIT_FOR_FILE_TIMEOUT, + interval = DEFAULT_WAIT_FOR_FILE_INTERVAL, +) => { + if (!specFileName) { + return null; + } + const fileName = specFileName.toLowerCase().endsWith('.mp4') + ? specFileName + : `${specFileName}.mp4`; + const globFilePath = `**/${videosFolder}/${fileName}`; + let videoFilePath; + + try { + videoFilePath = await waitForVideoFile(globFilePath, timeout, interval); + } catch (e) { + console.warn(e.message); + return null; + } + + return { + name: fileName, + type: 'video/mp4', + content: await fsPromises.readFile(videoFilePath, { encoding: 'base64' }), + }; +}; + +module.exports = { + getScreenshotAttachment, + getVideoFile, + waitForVideoFile, + getFilePathByGlobPattern, + checkVideoFileReady, +}; diff --git a/lib/utils/common.js b/lib/utils/common.js new file mode 100644 index 0000000..9d1184f --- /dev/null +++ b/lib/utils/common.js @@ -0,0 +1,22 @@ +/* + * Copyright 2024 EPAM Systems + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +const getCodeRef = (testItemPath, testFileName) => + `${testFileName.replace(/\\/g, '/')}/${testItemPath.join('/')}`; + +module.exports = { + getCodeRef, +}; diff --git a/lib/utils/index.js b/lib/utils/index.js new file mode 100644 index 0000000..745b096 --- /dev/null +++ b/lib/utils/index.js @@ -0,0 +1,27 @@ +/* + * Copyright 2024 EPAM Systems + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +const attachmentUtils = require('./attachments'); +const commonUtils = require('./common'); +const objectCreators = require('./objectCreators'); +const specCountCalculation = require('./specCountCalculation'); + +module.exports = { + ...attachmentUtils, + ...commonUtils, + ...objectCreators, + ...specCountCalculation, +}; diff --git a/lib/utils.js b/lib/utils/objectCreators.js similarity index 59% rename from lib/utils.js rename to lib/utils/objectCreators.js index 7c201d2..6d2209f 100644 --- a/lib/utils.js +++ b/lib/utils/objectCreators.js @@ -14,91 +14,13 @@ * limitations under the License. */ -const fs = require('fs'); -const glob = require('glob'); const path = require('path'); -const minimatch = require('minimatch'); -const { entityType, hookTypesMap, testItemStatuses } = require('./constants'); -const pjson = require('./../package.json'); - -const fsPromises = fs.promises; +const pjson = require('../../package.json'); +const { entityType, hookTypesMap, testItemStatuses } = require('../constants'); +const { getCodeRef } = require('./common'); const { FAILED, PASSED, SKIPPED } = testItemStatuses; -const DEFAULT_WAIT_FOR_FILE_TIMEOUT = 10000; -const DEFAULT_WAIT_FOR_FILE_INTERVAL = 500; - -const base64Encode = async (filePath) => { - const bitmap = await fsPromises.readFile(filePath); - return Buffer.from(bitmap).toString('base64'); -}; - -const getScreenshotAttachment = async (absolutePath) => { - if (!absolutePath) return absolutePath; - const name = absolutePath.split(path.sep).pop(); - return { - name, - type: 'image/png', - content: await base64Encode(absolutePath), - }; -}; - -const waitForFile = ( - globFilePath, - timeout = DEFAULT_WAIT_FOR_FILE_TIMEOUT, - interval = DEFAULT_WAIT_FOR_FILE_INTERVAL, -) => - new Promise((resolve, reject) => { - let totalTime = 0; - - async function checkFileExistence() { - const files = await glob(globFilePath); - - if (files.length) { - resolve(files[0]); - } else if (totalTime >= timeout) { - reject(new Error(`Timeout of ${timeout}ms reached, file ${globFilePath} not found.`)); - } else { - totalTime += interval; - setTimeout(checkFileExistence, interval); - } - } - - checkFileExistence().catch(reject); - }); - -const getVideoFile = async ( - specFileName, - videosFolder = '**', - timeout = DEFAULT_WAIT_FOR_FILE_TIMEOUT, - interval = DEFAULT_WAIT_FOR_FILE_INTERVAL, -) => { - if (!specFileName) { - return null; - } - const fileName = specFileName.toLowerCase().endsWith('.mp4') - ? specFileName - : `${specFileName}.mp4`; - const globFilePath = `**/${videosFolder}/${fileName}`; - let videoFilePath; - - try { - videoFilePath = await waitForFile(globFilePath, timeout, interval); - } catch (e) { - console.warn(e.message); - return null; - } - - return { - name: fileName, - type: 'video/mp4', - content: await base64Encode(videoFilePath), - }; -}; - -const getCodeRef = (testItemPath, testFileName) => - `${testFileName.replace(/\\/g, '/')}/${testItemPath.join('/')}`; - const getAgentInfo = () => ({ version: pjson.version, name: pjson.name, @@ -273,82 +195,20 @@ const getHookStartObject = (hook) => { codeRef: hook.codeRef, }; }; -const getFixtureFolderPattern = (config) => { - return [].concat(config.fixturesFolder ? path.join(config.fixturesFolder, '**', '*') : []); -}; - -const getExcludeSpecPattern = (config) => { - // Return cypress >= 10 pattern. - if (config.excludeSpecPattern) { - const excludePattern = Array.isArray(config.excludeSpecPattern) - ? config.excludeSpecPattern - : [config.excludeSpecPattern]; - return [...excludePattern]; - } - - // Return cypress <= 9 pattern - const ignoreTestFilesPattern = Array.isArray(config.ignoreTestFiles) - ? config.ignoreTestFiles - : [config.ignoreTestFiles] || []; - - return [...ignoreTestFilesPattern]; -}; - -const getSpecPattern = (config) => { - if (config.specPattern) return [].concat(config.specPattern); - - return Array.isArray(config.testFiles) - ? config.testFiles.map((file) => path.join(config.integrationFolder, file)) - : [].concat(path.join(config.integrationFolder, config.testFiles)); -}; - -const getTotalSpecs = (config) => { - if (!config.testFiles && !config.specPattern) - throw new Error('Configuration property not set! Neither for cypress <= 9 nor cypress >= 10'); - - const specPattern = getSpecPattern(config); - - const excludeSpecPattern = getExcludeSpecPattern(config); - - const options = { - sort: true, - absolute: true, - nodir: true, - ignore: [config.supportFile].concat(getFixtureFolderPattern(config)), - }; - - const doesNotMatchAllIgnoredPatterns = (file) => - excludeSpecPattern.every( - (pattern) => !minimatch(file, pattern, { dot: true, matchBase: true }), - ); - - const globResult = specPattern.reduce( - (files, pattern) => files.concat(glob.sync(pattern, options) || []), - [], - ); - - return globResult.filter(doesNotMatchAllIgnoredPatterns).length; -}; module.exports = { - getScreenshotAttachment, getAgentInfo, - getCodeRef, getSystemAttributes, + getConfig, getLaunchStartObject, getSuiteStartObject, getSuiteEndObject, getTestStartObject, + getTestEndObject, + getHookStartObject, + // there are utils to preprocess Mocha entities getTestInfo, getSuiteStartInfo, getSuiteEndInfo, - getTestEndObject, getHookInfo, - getHookStartObject, - getTotalSpecs, - getConfig, - getExcludeSpecPattern, - getFixtureFolderPattern, - getSpecPattern, - getVideoFile, }; diff --git a/lib/utils/specCountCalculation.js b/lib/utils/specCountCalculation.js new file mode 100644 index 0000000..e64fd68 --- /dev/null +++ b/lib/utils/specCountCalculation.js @@ -0,0 +1,83 @@ +/* + * Copyright 2024 EPAM Systems + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +const glob = require('glob'); +const path = require('path'); +const minimatch = require('minimatch'); + +const getFixtureFolderPattern = (config) => { + return [].concat(config.fixturesFolder ? path.join(config.fixturesFolder, '**', '*') : []); +}; + +const getExcludeSpecPattern = (config) => { + // Return cypress >= 10 pattern. + if (config.excludeSpecPattern) { + const excludePattern = Array.isArray(config.excludeSpecPattern) + ? config.excludeSpecPattern + : [config.excludeSpecPattern]; + return [...excludePattern]; + } + + // Return cypress <= 9 pattern + const ignoreTestFilesPattern = Array.isArray(config.ignoreTestFiles) + ? config.ignoreTestFiles + : [config.ignoreTestFiles] || []; + + return [...ignoreTestFilesPattern]; +}; + +const getSpecPattern = (config) => { + if (config.specPattern) return [].concat(config.specPattern); + + return Array.isArray(config.testFiles) + ? config.testFiles.map((file) => path.join(config.integrationFolder, file)) + : [].concat(path.join(config.integrationFolder, config.testFiles)); +}; + +const getTotalSpecs = (config) => { + if (!config.testFiles && !config.specPattern) + throw new Error('Configuration property not set! Neither for cypress <= 9 nor cypress >= 10'); + + const specPattern = getSpecPattern(config); + + const excludeSpecPattern = getExcludeSpecPattern(config); + + const options = { + sort: true, + absolute: true, + nodir: true, + ignore: [config.supportFile].concat(getFixtureFolderPattern(config)), + }; + + const doesNotMatchAllIgnoredPatterns = (file) => + excludeSpecPattern.every( + (pattern) => !minimatch(file, pattern, { dot: true, matchBase: true }), + ); + + const globResult = specPattern.reduce( + (files, pattern) => files.concat(glob.sync(pattern, options) || []), + [], + ); + + return globResult.filter(doesNotMatchAllIgnoredPatterns).length; +}; + +module.exports = { + getTotalSpecs, + getExcludeSpecPattern, + getFixtureFolderPattern, + getSpecPattern, +}; diff --git a/test/mock/mock.js b/test/mock/mocks.js similarity index 100% rename from test/mock/mock.js rename to test/mock/mocks.js diff --git a/test/reporter.test.js b/test/reporter.test.js index e431003..e0284e8 100644 --- a/test/reporter.test.js +++ b/test/reporter.test.js @@ -1,7 +1,8 @@ const mockFS = require('mock-fs'); const path = require('path'); -const { getDefaultConfig, RPClient, MockedDate, RealDate, currentDate } = require('./mock/mock'); +const { getDefaultConfig, RPClient, MockedDate, RealDate, currentDate } = require('./mock/mocks'); const Reporter = require('./../lib/reporter'); +const { entityType } = require('../lib/constants'); const sep = path.sep; @@ -45,6 +46,7 @@ describe('reporter script', () => { beforeEach(() => { global.Date = jest.fn(MockedDate); Object.assign(Date, RealDate); + reporter.config = getDefaultConfig(); }); afterEach(() => { @@ -106,21 +108,37 @@ describe('reporter script', () => { }); }); + describe('saveFullConfig', () => { + it('should set the full Cypress config to class object property', () => { + const fullCypressConfig = { e2e: { baseUrl: 'http://localhost:3000' }, reporterOptions: {} }; + reporter.saveFullConfig(fullCypressConfig); + + expect(reporter.fullCypressConfig).toEqual(fullCypressConfig); + }); + }); + describe('suiteStart', () => { it('root suite: startTestItem should be called with undefined parentId', () => { const spyStartTestItem = jest.spyOn(reporter.client, 'startTestItem'); reporter.tempLaunchId = 'tempLaunchId'; - const suiteStartObject = { + const suite = { id: 'suite1', + title: 'suite name', + startTime: currentDate, + description: 'suite description', + codeRef: 'test/example.spec.js/suite name', + testFileName: 'example.spec.js', + }; + const suiteStartObject = { + type: entityType.SUITE, name: 'suite name', - type: 'suite', startTime: currentDate, description: 'suite description', + codeRef: 'test/example.spec.js/suite name', attributes: [], - parentId: undefined, }; - reporter.suiteStart(suiteStartObject); + reporter.suiteStart(suite); expect(spyStartTestItem).toHaveBeenCalledTimes(1); expect(spyStartTestItem).toHaveBeenCalledWith(suiteStartObject, 'tempLaunchId', undefined); @@ -130,17 +148,25 @@ describe('reporter script', () => { const spyStartTestItem = jest.spyOn(reporter.client, 'startTestItem'); reporter.tempLaunchId = 'tempLaunchId'; reporter.testItemIds.set('parentSuiteId', 'tempParentSuiteId'); - const suiteStartObject = { + const suite = { id: 'suite1', + title: 'suite name', + startTime: currentDate, + description: 'suite description', + codeRef: 'test/example.spec.js/suite name', + testFileName: 'example.spec.js', + parentId: 'parentSuiteId', + }; + const suiteStartObject = { + type: entityType.SUITE, name: 'suite name', - type: 'suite', startTime: currentDate, description: 'suite description', + codeRef: 'test/example.spec.js/suite name', attributes: [], - parentId: 'parentSuiteId', }; - reporter.suiteStart(suiteStartObject); + reporter.suiteStart(suite); expect(spyStartTestItem).toHaveBeenCalledTimes(1); expect(spyStartTestItem).toHaveBeenCalledWith( @@ -155,33 +181,38 @@ describe('reporter script', () => { it('finishTestItem should be called with parameters', function () { const spyFinishTestItem = jest.spyOn(reporter.client, 'finishTestItem'); reporter.testItemIds.set('suiteId', 'tempSuiteId'); - const suiteEndObject = { + const suite = { id: 'suiteId', + title: 'suite title', + endTime: currentDate, + }; + const suiteEndObject = { endTime: currentDate, }; - reporter.suiteEnd(suiteEndObject); + reporter.suiteEnd(suite); expect(spyFinishTestItem).toHaveBeenCalledTimes(1); - expect(spyFinishTestItem).toHaveBeenCalledWith('tempSuiteId', { endTime: currentDate }); + expect(spyFinishTestItem).toHaveBeenCalledWith('tempSuiteId', suiteEndObject); }); it('end suite with testCaseId: finishTestItem should be called with testCaseId', function () { const spyFinishTestItem = jest.spyOn(reporter.client, 'finishTestItem'); reporter.testItemIds.set('suiteId', 'tempSuiteId'); reporter.suiteTestCaseIds.set('suite title', 'testCaseId'); - const suiteEndObject = { + const suite = { id: 'suiteId', title: 'suite title', endTime: currentDate, }; + const suiteEndObject = { + endTime: currentDate, + testCaseId: 'testCaseId', + }; - reporter.suiteEnd(suiteEndObject); + reporter.suiteEnd(suite); expect(spyFinishTestItem).toHaveBeenCalledTimes(1); - expect(spyFinishTestItem).toHaveBeenCalledWith('tempSuiteId', { - endTime: currentDate, - testCaseId: 'testCaseId', - }); + expect(spyFinishTestItem).toHaveBeenCalledWith('tempSuiteId', suiteEndObject); reporter.suiteTestCaseIds.clear(); }); @@ -189,25 +220,54 @@ describe('reporter script', () => { const spyFinishTestItem = jest.spyOn(reporter.client, 'finishTestItem'); reporter.testItemIds.set('suiteId', 'tempSuiteId'); reporter.setTestItemStatus({ status: 'failed', suiteTitle: 'suite title' }); - const suiteEndObject = { + const suite = { id: 'suiteId', title: 'suite title', endTime: currentDate, }; + const suiteEndObject = { + endTime: currentDate, + status: 'failed', + }; - reporter.suiteEnd(suiteEndObject); + reporter.suiteEnd(suite); expect(spyFinishTestItem).toHaveBeenCalledTimes(1); - expect(spyFinishTestItem).toHaveBeenCalledWith('tempSuiteId', { - endTime: currentDate, - status: 'failed', - }); + expect(spyFinishTestItem).toHaveBeenCalledWith('tempSuiteId', suiteEndObject); reporter.suiteStatuses.clear(); }); + it('end suite with video: finishSuiteWithVideo should be called if video is enabled and it is a root suite', function () { + const spyFinishSuiteWithVideo = jest.spyOn(reporter, 'finishSuiteWithVideo'); + reporter.fullCypressConfig = { video: true }; + reporter.config = { ...reporter.config, uploadVideo: true }; + reporter.testItemIds.set('suiteId', 'tempSuiteId'); + const suiteInfo = { + id: 'suiteId', + testFileName: 'example.spec.js', + title: 'suite title', + tempId: 'tempSuiteId', + }; + reporter.suitesStackTempInfo = [suiteInfo]; + const suite = { + id: 'suiteId', + title: 'suite title', + endTime: currentDate, + }; + const suiteEndObject = { + endTime: currentDate, + status: undefined, + }; + + reporter.suiteEnd(suite); + + expect(spyFinishSuiteWithVideo).toHaveBeenCalledTimes(1); + expect(spyFinishSuiteWithVideo).toHaveBeenCalledWith(suiteInfo, suiteEndObject); + }); }); - describe('sendVideoOnFinishSuite', function () { + // TODO: Fix the tests + describe.skip('sendVideoOnFinishSuite', function () { let customSuiteNameAttachment; beforeAll(() => { @@ -222,7 +282,6 @@ describe('reporter script', () => { afterAll(() => { mockFS.restore(); - reporter.config.reporterOptions.videosFolder = undefined; }); beforeEach(() => { @@ -236,7 +295,6 @@ describe('reporter script', () => { { id: 'suite', title: 'any suite' }, ]; reporter.testItemIds.set('root', 'suiteTempId'); - reporter.config.reporterOptions.videosFolder = 'example/videos'; }); afterEach(() => { @@ -338,7 +396,6 @@ describe('reporter script', () => { status: 'failed', }; - reporter.config.reporterOptions.videosFolder = 'example/screenshots'; reporter.suiteEnd(suiteEndObject); expect(spySendVideoOnFinishSuite).not.toHaveBeenCalled(); @@ -614,6 +671,20 @@ describe('reporter script', () => { expect(spyFinishTestItem).toHaveBeenCalledTimes(1); expect(spyFinishTestItem).toHaveBeenCalledWith('tempTestItemId', expectedTestFinishObj); }); + + it('end test: should not finish test in case no testId present in testItemIds', function () { + const spyFinishTestItem = jest.spyOn(reporter.client, 'finishTestItem'); + const testInfoObject = { + id: 'testId', + title: 'test name', + status: 'failed', + parentId: 'suiteId', + err: 'error message', + }; + reporter.testEnd(testInfoObject); + + expect(spyFinishTestItem).toHaveBeenCalledTimes(0); + }); }); describe('testPending', function () { @@ -1045,7 +1116,7 @@ describe('reporter script', () => { }); }); - describe('screenshot', () => { + describe('sendScreenshot', () => { const screenshotInfo = { testAttemptIndex: 0, size: 295559, @@ -1076,19 +1147,19 @@ describe('reporter script', () => { mockFS.restore(); }); - it('should not send screenshot for undefined path', () => { + it('should not send screenshot for undefined path', async () => { const spySendLog = jest.spyOn(reporter.client, 'sendLog'); - reporter.sendScreenshot(screenshotInfo); + await reporter.sendScreenshot(screenshotInfo); expect(spySendLog).not.toHaveBeenCalled(); }); - it('should send screenshot from screenshotInfo', () => { + it('should send screenshot from screenshotInfo', async () => { const spySendLog = jest.spyOn(reporter.client, 'sendLog'); screenshotInfo.path = `${sep}example${sep}screenshots${sep}example.spec.js${sep}suite name -- test name.png`; reporter.currentTestTempInfo = expectedTempId; - reporter.sendScreenshot(screenshotInfo); + await reporter.sendScreenshot(screenshotInfo); expect(spySendLog).toHaveBeenCalledTimes(1); expect(spySendLog).toHaveBeenCalledWith( @@ -1106,13 +1177,13 @@ describe('reporter script', () => { ); }); - it('should send screenshot from screenshotInfo - error level', () => { + it('should send screenshot from screenshotInfo - error level', async () => { const spySendLog = jest.spyOn(reporter.client, 'sendLog'); screenshotInfo.path = `${sep}example${sep}screenshots${sep}example.spec.js${sep}suite name -- test name (failed).png`; reporter.currentTestTempInfo = expectedTempId; - reporter.sendScreenshot(screenshotInfo); + await reporter.sendScreenshot(screenshotInfo); expect(spySendLog).toHaveBeenCalledTimes(1); expect(spySendLog).toHaveBeenCalledWith( @@ -1130,14 +1201,14 @@ describe('reporter script', () => { ); }); - it('should send screenshot from screenshotInfo - custom log message', () => { + it('should send screenshot from screenshotInfo - custom log message', async () => { const spySendLog = jest.spyOn(reporter.client, 'sendLog'); screenshotInfo.path = `${sep}example${sep}screenshots${sep}example.spec.js${sep}customScreenshot1.png`; const message = `screenshot\n${JSON.stringify(screenshotInfo, undefined, 2)}`; reporter.currentTestTempInfo = expectedTempId; - reporter.sendScreenshot(screenshotInfo, message); + await reporter.sendScreenshot(screenshotInfo, message); expect(spySendLog).toHaveBeenCalledTimes(1); expect(spySendLog).toHaveBeenCalledWith( diff --git a/test/utils.test.js b/test/utils.test.js deleted file mode 100644 index f03b016..0000000 --- a/test/utils.test.js +++ /dev/null @@ -1,978 +0,0 @@ -const mock = require('mock-fs'); -const path = require('path'); -const { - getSystemAttributes, - getLaunchStartObject, - getSuiteStartObject, - getSuiteEndObject, - getTestInfo, - getTestStartObject, - getTestEndObject, - getHookInfo, - getHookStartObject, - getScreenshotAttachment, - getAgentInfo, - getCodeRef, - getTotalSpecs, - getConfig, - getFixtureFolderPattern, - getExcludeSpecPattern, - getSpecPattern, - getVideoFile, - prepareReporterOptions, -} = require('./../lib/utils'); -const pjson = require('./../package.json'); - -const sep = path.sep; - -const { RealDate, MockedDate, currentDate, getDefaultConfig } = require('./mock/mock'); - -describe('utils script', () => { - describe('attachment utils', () => { - beforeEach(() => { - mock({ - '/example/screenshots/example.spec.js': { - 'suite name -- test name (failed).png': Buffer.from([8, 6, 7, 5, 3, 0, 9]), - 'suite name -- test name.png': Buffer.from([1, 2, 3, 4, 5, 6, 7]), - 'suite name -- test name (1).png': Buffer.from([8, 7, 6, 5, 4, 3, 2]), - 'customScreenshot1.png': Buffer.from([1, 1, 1, 1, 1, 1, 1]), - }, - 'example/videos': { - 'custom suite name.cy.ts.mp4': Buffer.from([1, 2, 7, 9, 3, 0, 5]), - }, - }); - }); - - afterEach(() => { - mock.restore(); - }); - - it('getScreenshotAttachment: should not fail on undefined', () => { - const testFile = undefined; - const attachment = getScreenshotAttachment(testFile); - expect(attachment).not.toBeDefined(); - }); - - it('getScreenshotAttachment: should return attachment for absolute path', () => { - const testFile = `${sep}example${sep}screenshots${sep}example.spec.js${sep}suite name -- test name (failed).png`; - const expectedAttachment = { - name: 'suite name -- test name (failed).png', - type: 'image/png', - content: Buffer.from([8, 6, 7, 5, 3, 0, 9]).toString('base64'), - }; - - const attachment = getScreenshotAttachment(testFile); - - expect(attachment).toBeDefined(); - expect(attachment).toEqual(expectedAttachment); - }); - - it('getVideoFile: should return video file attachment with videosFolder', () => { - const testFileName = 'custom suite name.cy.ts'; - const expectedAttachment = { - name: `${testFileName}.mp4`, - type: 'video/mp4', - content: Buffer.from([1, 2, 7, 9, 3, 0, 5]).toString('base64'), - }; - - const attachment = getVideoFile(testFileName, 'example/videos'); - - expect(attachment).toBeDefined(); - expect(attachment).toEqual(expectedAttachment); - }); - - it('getVideoFile: should return video file attachment without videosFolder', () => { - const testFileName = 'custom suite name.cy.ts'; - const expectedAttachment = { - name: `${testFileName}.mp4`, - type: 'video/mp4', - content: Buffer.from([1, 2, 7, 9, 3, 0, 5]).toString('base64'), - }; - - const attachment = getVideoFile(testFileName); - - expect(attachment).toBeDefined(); - expect(attachment).toEqual(expectedAttachment); - }); - }); - - describe('object creators', () => { - const testFileName = `test\\example.spec.js`; - - beforeEach(() => { - global.Date = jest.fn(MockedDate); - Object.assign(Date, RealDate); - }); - - afterEach(() => { - jest.clearAllMocks(); - global.Date = RealDate; - }); - - describe('getSystemAttributes', () => { - it('skippedIssue undefined. Should return attribute with agent name and version', function () { - const options = getDefaultConfig(); - const expectedSystemAttributes = [ - { - key: 'agent', - value: `${pjson.name}|${pjson.version}`, - system: true, - }, - ]; - - const systemAttributes = getSystemAttributes(options); - - expect(systemAttributes).toEqual(expectedSystemAttributes); - }); - - it('skippedIssue = true. Should return attribute with agent name and version', function () { - const options = getDefaultConfig(); - options.reporterOptions.skippedIssue = true; - const expectedSystemAttributes = [ - { - key: 'agent', - value: `${pjson.name}|${pjson.version}`, - system: true, - }, - ]; - - const systemAttributes = getSystemAttributes(options); - - expect(systemAttributes).toEqual(expectedSystemAttributes); - }); - - it('skippedIssue = false. Should return 2 attribute: with agent name/version and skippedIssue', function () { - const options = getDefaultConfig(); - options.reporterOptions.skippedIssue = false; - const expectedSystemAttributes = [ - { - key: 'agent', - value: `${pjson.name}|${pjson.version}`, - system: true, - }, - { - key: 'skippedIssue', - value: 'false', - system: true, - }, - ]; - - const systemAttributes = getSystemAttributes(options); - - expect(systemAttributes).toEqual(expectedSystemAttributes); - }); - }); - - describe('getConfig', () => { - const baseReporterOptions = { - endpoint: 'https://reportportal.server/api/v1', - project: 'ProjectName', - launch: 'LauncherName', - description: 'Launch description', - attributes: [], - }; - - describe('CI_BUILD_ID attribute providing', () => { - afterEach(() => { - delete process.env.CI_BUILD_ID; - }); - - it('should not add an attribute with the CI_BUILD_ID value in case of parallel reporter option is false', function () { - process.env.CI_BUILD_ID = 'buildId'; - const initialConfig = { - reporter: '@reportportal/agent-js-cypress', - reporterOptions: { - ...baseReporterOptions, - apiKey: '123', - autoMerge: true, - parallel: false, - }, - }; - const expectedConfig = initialConfig; - - const config = getConfig(initialConfig); - - expect(config).toEqual(expectedConfig); - }); - - it('should not add an attribute with the CI_BUILD_ID value in case of autoMerge reporter option is false', function () { - process.env.CI_BUILD_ID = 'buildId'; - const initialConfig = { - reporter: '@reportportal/agent-js-cypress', - reporterOptions: { - ...baseReporterOptions, - apiKey: '123', - autoMerge: false, - parallel: true, - }, - }; - const expectedConfig = initialConfig; - - const config = getConfig(initialConfig); - - expect(config).toEqual(expectedConfig); - }); - - it('should not add an attribute with the value CI_BUILD_ID if the env variable CI_BUILD_ID does not exist', function () { - process.env.CI_BUILD_ID = undefined; - const initialConfig = { - reporter: '@reportportal/agent-js-cypress', - reporterOptions: { - ...baseReporterOptions, - apiKey: '123', - autoMerge: false, - parallel: true, - }, - }; - const expectedConfig = initialConfig; - - const config = getConfig(initialConfig); - - expect(config).toEqual(expectedConfig); - }); - - it('should return config with updated attributes (including attribute with CI_BUILD_ID value)', function () { - process.env.CI_BUILD_ID = 'buildId'; - const initialConfig = { - reporter: '@reportportal/agent-js-cypress', - reporterOptions: { - ...baseReporterOptions, - apiKey: '123', - autoMerge: true, - parallel: true, - }, - }; - const expectedConfig = { - reporter: '@reportportal/agent-js-cypress', - reporterOptions: { - ...initialConfig.reporterOptions, - attributes: [ - { - value: 'buildId', - }, - ], - }, - }; - - const config = getConfig(initialConfig); - - expect(config).toEqual(expectedConfig); - }); - }); - - describe('apiKey option priority', () => { - afterEach(() => { - delete process.env.RP_TOKEN; - delete process.env.RP_API_KEY; - }); - - it('should override token property if the ENV variable RP_TOKEN exists', function () { - process.env.RP_TOKEN = 'secret'; - const initialConfig = { - reporter: '@reportportal/agent-js-cypress', - reporterOptions: { - ...baseReporterOptions, - token: '123', - }, - }; - const expectedConfig = { - reporter: '@reportportal/agent-js-cypress', - reporterOptions: { - ...baseReporterOptions, - apiKey: 'secret', - }, - }; - - const config = getConfig(initialConfig); - - expect(config).toEqual(expectedConfig); - }); - - it('should override apiKey property if the ENV variable RP_API_KEY exists', function () { - process.env.RP_API_KEY = 'secret'; - const initialConfig = { - reporter: '@reportportal/agent-js-cypress', - reporterOptions: { - ...baseReporterOptions, - apiKey: '123', - }, - }; - const expectedConfig = { - reporter: '@reportportal/agent-js-cypress', - reporterOptions: { - ...baseReporterOptions, - apiKey: 'secret', - }, - }; - - const config = getConfig(initialConfig); - - expect(config).toEqual(expectedConfig); - }); - - it('should prefer apiKey property over deprecated token', function () { - const initialConfig = { - reporter: '@reportportal/agent-js-cypress', - reporterOptions: { - ...baseReporterOptions, - apiKey: '123', - token: '345', - }, - }; - const expectedConfig = { - reporter: '@reportportal/agent-js-cypress', - reporterOptions: { - ...baseReporterOptions, - apiKey: '123', - }, - }; - - const config = getConfig(initialConfig); - - expect(config).toEqual(expectedConfig); - }); - }); - }); - - describe('prepareReporterOptions', function () { - it('should pass video related cypress options from cypress config', function () { - const initialConfig = getDefaultConfig(); - initialConfig.videosFolder = '/example/videos'; - initialConfig.videoUploadOnPasses = true; - - const config = prepareReporterOptions(initialConfig); - - expect(config.reporterOptions.videosFolder).toEqual('/example/videos'); - expect(config.reporterOptions.videoUploadOnPasses).toEqual(true); - }); - - it('passing video related cypress options should not fail if undefined', function () { - const initialConfig = getDefaultConfig(); - - const config = prepareReporterOptions(initialConfig); - - expect(config.reporterOptions.videosFolder).not.toBeDefined(); - expect(config.reporterOptions.videoUploadOnPasses).not.toBeDefined(); - }); - }); - - describe('getLaunchStartObject', () => { - test('should return start launch object with correct values', () => { - const expectedStartLaunchObject = { - launch: 'LauncherName', - description: 'Launch description', - attributes: [ - { - key: 'agent', - system: true, - value: `${pjson.name}|${pjson.version}`, - }, - ], - startTime: currentDate, - rerun: undefined, - rerunOf: undefined, - mode: undefined, - }; - - const startLaunchObject = getLaunchStartObject(getDefaultConfig()); - - expect(startLaunchObject).toBeDefined(); - expect(startLaunchObject).toEqual(expectedStartLaunchObject); - }); - }); - - describe('getSuiteStartObject', () => { - test('root suite: should return suite start object with undefined parentId', () => { - const suite = { - id: 'suite1', - title: 'suite name', - description: 'suite description', - root: true, - titlePath: () => ['suite name'], - }; - const expectedSuiteStartObject = { - id: 'suite1', - name: 'suite name', - type: 'suite', - startTime: currentDate, - description: 'suite description', - attributes: [], - codeRef: 'test/example.spec.js/suite name', - parentId: undefined, - testFileName: 'test\\example.spec.js', - }; - - const suiteStartObject = getSuiteStartObject(suite, testFileName); - - expect(suiteStartObject).toBeDefined(); - expect(suiteStartObject).toEqual(expectedSuiteStartObject); - }); - - test('nested suite: should return suite start object with parentId', () => { - const suite = { - id: 'suite1', - title: 'suite name', - description: 'suite description', - parent: { - id: 'parentSuiteId', - }, - titlePath: () => ['parent suite name', 'suite name'], - }; - const expectedSuiteStartObject = { - id: 'suite1', - name: 'suite name', - type: 'suite', - startTime: currentDate, - description: 'suite description', - attributes: [], - codeRef: 'test/example.spec.js/parent suite name/suite name', - parentId: 'parentSuiteId', - testFileName: 'test\\example.spec.js', - }; - - const suiteStartObject = getSuiteStartObject(suite, testFileName); - - expect(suiteStartObject).toBeDefined(); - expect(suiteStartObject).toEqual(expectedSuiteStartObject); - }); - }); - - describe('getSuiteEndObject', () => { - test('should return suite end object', () => { - const suite = { - id: 'suite1', - title: 'suite name', - description: 'suite description', - parent: { - id: 'parentSuiteId', - }, - }; - const expectedSuiteEndObject = { - id: 'suite1', - title: 'suite name', - endTime: currentDate, - }; - - const suiteEndObject = getSuiteEndObject(suite); - - expect(suiteEndObject).toBeDefined(); - expect(suiteEndObject).toEqual(expectedSuiteEndObject); - }); - }); - - describe('getTestInfo', () => { - test('passed test: should return test info with passed status', () => { - const test = { - id: 'testId1', - title: 'test name', - parent: { - id: 'parentSuiteId', - }, - state: 'passed', - titlePath: () => ['suite name', 'test name'], - }; - const expectedTestInfoObject = { - id: 'testId1', - title: 'test name', - status: 'passed', - parentId: 'parentSuiteId', - codeRef: 'test/example.spec.js/suite name/test name', - err: undefined, - testFileName, - }; - - const testInfoObject = getTestInfo(test, testFileName); - - expect(testInfoObject).toBeDefined(); - expect(testInfoObject).toEqual(expectedTestInfoObject); - }); - - test('pending test: should return test info with skipped status', () => { - const test = { - id: 'testId1', - title: 'test name', - parent: { - id: 'parentSuiteId', - }, - state: 'pending', - titlePath: () => ['suite name', 'test name'], - }; - const expectedTestInfoObject = { - id: 'testId1', - title: 'test name', - status: 'skipped', - parentId: 'parentSuiteId', - codeRef: 'test/example.spec.js/suite name/test name', - err: undefined, - testFileName, - }; - - const testInfoObject = getTestInfo(test, testFileName); - - expect(testInfoObject).toBeDefined(); - expect(testInfoObject).toEqual(expectedTestInfoObject); - }); - - test('should return test info with specified status and error', () => { - const test = { - id: 'testId', - title: 'test name', - parent: { - id: 'parentSuiteId', - }, - state: 'pending', - titlePath: () => ['suite name', 'test name'], - }; - const expectedTestInfoObject = { - id: 'testId', - title: 'test name', - status: 'failed', - parentId: 'parentSuiteId', - codeRef: 'test/example.spec.js/suite name/test name', - err: { message: 'error message' }, - testFileName, - }; - - const testInfoObject = getTestInfo(test, testFileName, 'failed', { - message: 'error message', - }); - - expect(testInfoObject).toBeDefined(); - expect(testInfoObject).toEqual(expectedTestInfoObject); - }); - }); - - describe('getTestStartObject', () => { - test('should return test start object', () => { - const test = { - id: 'testId1', - title: 'test name', - parent: { - id: 'parentSuiteId', - }, - codeRef: 'test/example.spec.js/suite name/test name', - }; - const expectedTestStartObject = { - name: 'test name', - startTime: currentDate, - attributes: [], - type: 'step', - codeRef: 'test/example.spec.js/suite name/test name', - }; - - const testInfoObject = getTestStartObject(test); - - expect(testInfoObject).toBeDefined(); - expect(testInfoObject).toEqual(expectedTestStartObject); - }); - }); - - describe('getTestEndObject', () => { - test('skippedIssue is not defined: should return test end object without issue', () => { - const testInfo = { - id: 'testId1', - title: 'test name', - status: 'skipped', - parent: { - id: 'parentSuiteId', - }, - }; - const expectedTestEndObject = { - endTime: currentDate, - status: testInfo.status, - }; - const testEndObject = getTestEndObject(testInfo); - - expect(testEndObject).toBeDefined(); - expect(testEndObject).toEqual(expectedTestEndObject); - }); - - test('skippedIssue = true: should return test end object without issue', () => { - const testInfo = { - id: 'testId1', - title: 'test name', - status: 'skipped', - parent: { - id: 'parentSuiteId', - }, - }; - const expectedTestEndObject = { - endTime: currentDate, - status: testInfo.status, - }; - const testEndObject = getTestEndObject(testInfo, true); - - expect(testEndObject).toBeDefined(); - expect(testEndObject).toEqual(expectedTestEndObject); - }); - - test('skippedIssue = false: should return test end object with issue NOT_ISSUE', () => { - const testInfo = { - id: 'testId1', - title: 'test name', - status: 'skipped', - parent: { - id: 'parentSuiteId', - }, - }; - const expectedTestEndObject = { - endTime: currentDate, - status: testInfo.status, - issue: { - issueType: 'NOT_ISSUE', - }, - }; - const testEndObject = getTestEndObject(testInfo, false); - - expect(testEndObject).toBeDefined(); - expect(testEndObject).toEqual(expectedTestEndObject); - }); - - test('testCaseId is defined: should return test end object with testCaseId', () => { - const testInfo = { - id: 'testId1', - title: 'test name', - status: 'skipped', - parent: { - id: 'parentSuiteId', - }, - testCaseId: 'testCaseId', - }; - const expectedTestEndObject = { - endTime: currentDate, - status: testInfo.status, - testCaseId: 'testCaseId', - }; - const testEndObject = getTestEndObject(testInfo); - - expect(testEndObject).toEqual(expectedTestEndObject); - }); - }); - - describe('getHookInfo', () => { - test('passed before each hook: should return hook info with passed status', () => { - const hook = { - id: 'testId', - title: '"before each" hook: hook name', - parent: { - id: 'parentSuiteId', - }, - state: 'passed', - hookName: 'before each', - hookId: 'hookId', - titlePath: () => ['suite name', 'hook name'], - }; - const expectedHookInfoObject = { - id: 'hookId_testId', - hookName: 'before each', - title: '"before each" hook: hook name', - status: 'passed', - parentId: 'parentSuiteId', - codeRef: 'test/example.spec.js/suite name/hook name', - err: undefined, - testFileName, - }; - - const hookInfoObject = getHookInfo(hook, testFileName); - - expect(hookInfoObject).toBeDefined(); - expect(hookInfoObject).toEqual(expectedHookInfoObject); - }); - - test('passed before all hook: should return correct hook info', () => { - const hook = { - id: 'testId', - title: '"before all" hook: hook name', - parent: { - id: 'parentSuiteId', - title: 'parent suite title', - parent: { - id: 'rootSuiteId', - title: 'root suite title', - }, - }, - state: 'passed', - hookName: 'before all', - hookId: 'hookId', - titlePath: () => ['suite name', 'hook name'], - }; - const expectedHookInfoObject = { - id: 'hookId_testId', - hookName: 'before all', - title: '"before all" hook: hook name', - status: 'passed', - parentId: 'rootSuiteId', - codeRef: 'test/example.spec.js/suite name/hook name', - err: undefined, - testFileName, - }; - - const hookInfoObject = getHookInfo(hook, testFileName); - - expect(hookInfoObject).toBeDefined(); - expect(hookInfoObject).toEqual(expectedHookInfoObject); - }); - - test('failed test: should return hook info with failed status', () => { - const test = { - id: 'testId', - hookName: 'before each', - title: '"before each" hook: hook name', - parent: { - id: 'parentSuiteId', - }, - state: 'failed', - failedFromHookId: 'hookId', - titlePath: () => ['suite name', 'hook name'], - }; - const expectedHookInfoObject = { - id: 'hookId_testId', - hookName: 'before each', - title: '"before each" hook: hook name', - status: 'failed', - parentId: 'parentSuiteId', - codeRef: 'test/example.spec.js/suite name/hook name', - err: undefined, - testFileName, - }; - - const hookInfoObject = getHookInfo(test, testFileName); - - expect(hookInfoObject).toBeDefined(); - expect(hookInfoObject).toEqual(expectedHookInfoObject); - }); - }); - describe('getHookStartObject', () => { - test('should return hook start object', () => { - const hookInfo = { - id: 'hookId_testId', - hookName: 'before each', - title: '"before each" hook: hook name', - status: 'passed', - parentId: 'parentSuiteId', - titlePath: () => ['suite name', 'hook name'], - err: undefined, - }; - const expectedHookStartObject = { - name: 'hook name', - startTime: currentDate, - type: 'BEFORE_METHOD', - }; - - const hookInfoObject = getHookStartObject(hookInfo, testFileName, 'failed', { - message: 'error message', - }); - - expect(hookInfoObject).toBeDefined(); - expect(hookInfoObject).toEqual(expectedHookStartObject); - }); - }); - }); - - describe('common utils', () => { - describe('getAgentInfo', () => { - it('getAgentInfo: should contain version and name properties', () => { - const agentInfo = getAgentInfo(); - - expect(Object.keys(agentInfo)).toContain('version'); - expect(Object.keys(agentInfo)).toContain('name'); - }); - }); - describe('getCodeRef', () => { - it('should return correct code ref for Windows paths', () => { - jest.mock('path', () => ({ - sep: '\\', - })); - const file = `test\\example.spec.js`; - const titlePath = ['rootDescribe', 'parentDescribe', 'testTitle']; - - const expectedCodeRef = `test/example.spec.js/rootDescribe/parentDescribe/testTitle`; - - const codeRef = getCodeRef(titlePath, file); - - expect(codeRef).toEqual(expectedCodeRef); - - jest.clearAllMocks(); - }); - - it('should return correct code ref for POSIX paths', () => { - jest.mock('path', () => ({ - sep: '/', - })); - const file = `test/example.spec.js`; - const titlePath = ['rootDescribe', 'parentDescribe', 'testTitle']; - - const expectedCodeRef = `test/example.spec.js/rootDescribe/parentDescribe/testTitle`; - - const codeRef = getCodeRef(titlePath, file); - - expect(codeRef).toEqual(expectedCodeRef); - - jest.clearAllMocks(); - }); - }); - }); - - describe('getTotalSpecs', () => { - beforeEach(() => { - mock({ - 'cypress/tests': { - 'example1.spec.js': '', - 'example2.spec.js': '', - 'example3.spec.js': '', - 'example4.spec.ts': '', - 'example.ignore.spec.js': '', - }, - 'cypress/support': { - 'index.js': '', - }, - 'cypress/fixtures': { - 'fixtures1.js': '', - 'fixtures2.js': '', - }, - }); - }); - - afterEach(() => { - mock.restore(); - }); - - it('testFiles, integrationFolder, supportFile are specified: should count all files from integration folder', () => { - let specConfig = { - testFiles: '**/*.*', - ignoreTestFiles: '*.hot-update.js', - fixturesFolder: 'cypress/fixtures', - integrationFolder: 'cypress/tests', - supportFile: 'cypress/support/index.js', - }; - - let specCount = getTotalSpecs(specConfig); - - expect(specCount).toEqual(5); - - specConfig = { - excludeSpecPattern: '*.hot-update.js', - specPattern: 'cypress/tests/**/*.spec.{js,ts}', - supportFile: 'cypress/support/index.js', - fixturesFolder: 'cypress/fixtures', - }; - - specCount = getTotalSpecs(specConfig); - - expect(specCount).toEqual(5); - }); - - it('ignoreTestFiles are specified: should ignore specified files', () => { - let specConfig = { - testFiles: '**/*.*', - ignoreTestFiles: ['*.hot-update.js', '*.ignore.*.*'], - fixturesFolder: 'cypress/fixtures', - integrationFolder: 'cypress/tests', - supportFile: 'cypress/support/index.js', - }; - - let specCount = getTotalSpecs(specConfig); - - expect(specCount).toEqual(4); - - specConfig = { - specPattern: 'cypress/tests/**/*.spec.{js,ts}', - excludeSpecPattern: ['*.hot-update.js', '*.ignore.spec.*'], - supportFile: 'cypress/support/index.js', - fixturesFolder: 'cypress/fixtures', - }; - - specCount = getTotalSpecs(specConfig); - - expect(specCount).toEqual(4); - }); - }); - - describe('getFixtureFolderPattern', () => { - it('returns a glob pattern for fixtures folder', () => { - const specConfig = { fixturesFolder: `cypress${sep}fixtures` }; - - const specArray = getFixtureFolderPattern(specConfig); - expect(specArray).toHaveLength(1); - expect(specArray).toContain(`cypress${sep}fixtures${sep}**${sep}*`); - }); - }); - describe('getExcludeSpecPattern', () => { - it('getExcludeSpecPattern returns required pattern for cypress version <= 9', () => { - const specConfigString = { - integrationFolder: 'cypress/integration', - ignoreTestFiles: '*.hot-update.js', - fixturesFolder: 'cypress/fixtures', - supportFile: 'cypress/support/index.js', - }; - - const specConfigArray = { - integrationFolder: 'cypress/integration', - ignoreTestFiles: ['*.hot-update.js', '*.hot-update.ts'], - fixturesFolder: 'cypress/fixtures', - supportFile: 'cypress/support/index.js', - }; - - let patternArray = getExcludeSpecPattern(specConfigString); - expect(patternArray).toHaveLength(1); - expect(patternArray).toContain('*.hot-update.js'); - - patternArray = getExcludeSpecPattern(specConfigArray); - expect(patternArray).toHaveLength(2); - expect(patternArray).toContain('*.hot-update.js'); - expect(patternArray).toContain('*.hot-update.ts'); - }); - }); - - describe('getSpecPattern', () => { - it('returns the required glob pattern for cypress <=9 config when testFiles is an array', () => { - const specConfig = { - integrationFolder: 'cypress/integration', - testFiles: ['**/*.js', '**/*.ts'], - }; - - const patternArray = getSpecPattern(specConfig); - expect(patternArray).toHaveLength(2); - expect(patternArray[0]).toEqual( - path.join(specConfig.integrationFolder, specConfig.testFiles[0]), - ); - expect(patternArray[1]).toEqual( - path.join(specConfig.integrationFolder, specConfig.testFiles[1]), - ); - }); - - it('getSpecPattern returns the required glob pattern for cypress >= 10 config when specPattern is an array', () => { - const specConfig = { - specPattern: ['cypress/integration/**/*.js', 'cypress/integration/**/*.js'], - }; - - const patternArray = getSpecPattern(specConfig); - expect(patternArray).toHaveLength(2); - expect(patternArray[0]).toEqual(specConfig.specPattern[0]); - expect(patternArray[1]).toEqual(specConfig.specPattern[1]); - }); - - it('getSpecPattern returns the required glob pattern for cypress >= 10 config when specPattern is a string', () => { - const specConfig = { - specPattern: 'cypress/integration/**/*.js', - }; - - const patternArray = getSpecPattern(specConfig); - expect(patternArray).toHaveLength(1); - expect(patternArray[0]).toEqual(specConfig.specPattern); - }); - - it('getSpecPattern returns the required glob pattern for cypress <= 9 config when testFiles is a string', () => { - const specConfig = { - integrationFolder: 'cypress/integration', - testFiles: '**/*.js', - }; - - const patternArray = getSpecPattern(specConfig); - expect(patternArray).toHaveLength(1); - expect(patternArray[0]).toEqual( - path.join(specConfig.integrationFolder, specConfig.testFiles), - ); - }); - }); -}); diff --git a/test/utils/attachments.test.js b/test/utils/attachments.test.js new file mode 100644 index 0000000..7dfe35f --- /dev/null +++ b/test/utils/attachments.test.js @@ -0,0 +1,208 @@ +const fsPromises = require('fs/promises'); +const mockFs = require('mock-fs'); +const path = require('path'); +const glob = require('glob'); +const attachmentUtils = require('../../lib/utils/attachments'); + +const { + getScreenshotAttachment, + getVideoFile, + waitForVideoFile, + getFilePathByGlobPattern, + checkVideoFileReady, +} = attachmentUtils; + +const sep = path.sep; + +describe('attachment utils', () => { + describe('getScreenshotAttachment', () => { + beforeEach(() => { + mockFs({ + '/example/screenshots/example.spec.js': { + 'suite name -- test name (failed).png': Buffer.from([8, 6, 7, 5, 3, 0, 9]), + 'suite name -- test name.png': Buffer.from([1, 2, 3, 4, 5, 6, 7]), + 'suite name -- test name (1).png': Buffer.from([8, 7, 6, 5, 4, 3, 2]), + 'customScreenshot1.png': Buffer.from([1, 1, 1, 1, 1, 1, 1]), + }, + }); + }); + + afterEach(() => { + mockFs.restore(); + }); + + it('getScreenshotAttachment: should not fail on undefined', async () => { + const testFile = undefined; + const attachment = await getScreenshotAttachment(testFile); + expect(attachment).not.toBeDefined(); + }); + + it('getScreenshotAttachment: should return attachment for absolute path', async () => { + const testFile = `${sep}example${sep}screenshots${sep}example.spec.js${sep}suite name -- test name (failed).png`; + const expectedAttachment = { + name: 'suite name -- test name (failed).png', + type: 'image/png', + content: Buffer.from([8, 6, 7, 5, 3, 0, 9]).toString('base64'), + }; + + const attachment = await getScreenshotAttachment(testFile); + + expect(attachment).toBeDefined(); + expect(attachment).toEqual(expectedAttachment); + }); + }); + + describe('getFilePathByGlobPattern', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + test('returns the path of the first file if files are found', async () => { + const mockFiles = ['path/to/first/file.mp4', 'path/to/second/file.mp4']; + jest.spyOn(glob, 'glob').mockResolvedValueOnce(mockFiles); + + const result = await getFilePathByGlobPattern('*.mp4'); + expect(result).toBe('path/to/first/file.mp4'); + }); + + test('returns null if no files are found', async () => { + jest.spyOn(glob, 'glob').mockResolvedValueOnce([]); + + const result = await getFilePathByGlobPattern('*.mp4'); + expect(result).toBeNull(); + }); + }); + + describe('checkVideoFileReady', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + test('returns true if the video file contains "moov" atom', async () => { + const mockFileData = Buffer.from('some data with moov in it'); + jest.spyOn(fsPromises, 'readFile').mockResolvedValueOnce(mockFileData); + + const result = await checkVideoFileReady('path/to/video.mp4'); + expect(result).toBe(true); + }); + + test('returns false if the video file does not contain "moov" atom', async () => { + const mockFileData = Buffer.from('some data without the keyword'); + jest.spyOn(fsPromises, 'readFile').mockResolvedValueOnce(mockFileData); + + const result = await checkVideoFileReady('path/to/video.mp4'); + expect(result).toBe(false); + }); + + test('throws an error if there is an error reading the file', async () => { + jest.spyOn(fsPromises, 'readFile').mockRejectedValueOnce(new Error('Failed to read file')); + + await expect(checkVideoFileReady('path/to/video.mp4')).rejects.toThrow( + 'Error reading file: Failed to read file', + ); + }); + }); + + // TODO: Fix the tests + describe.skip('waitForVideoFile', () => { + beforeEach(() => { + jest.useFakeTimers(); + jest.clearAllMocks(); + }); + + test('resolves with the file path if the video file is found and ready', async () => { + jest + .spyOn(attachmentUtils, 'getFilePathByGlobPattern') + .mockImplementation(async () => 'path/to/video.mp4'); + // .mockResolvedValueOnce('path/to/video.mp4'); + jest.spyOn(attachmentUtils, 'checkVideoFileReady').mockImplementation(async () => true); + + const promise = waitForVideoFile('*.mp4'); + jest.runAllTimers(); + + await expect(promise).resolves.toBe('path/to/video.mp4'); + }, 20000); + + test('retries until the video file is ready or timeout occurs', async () => { + jest + .spyOn(attachmentUtils, 'getFilePathByGlobPattern') + .mockResolvedValueOnce('path/to/video.mp4'); + jest + .spyOn(attachmentUtils, 'checkVideoFileReady') + .mockResolvedValueOnce(false) + .mockResolvedValueOnce(false) + .mockResolvedValueOnce(true); + + const promise = waitForVideoFile('*.mp4'); + jest.advanceTimersByTime(3000); + + await expect(promise).resolves.toBe('path/to/video.mp4'); + }, 20000); + + test('rejects with a timeout error if the timeout is reached without finding a ready video file', async () => { + jest + .spyOn(attachmentUtils, 'getFilePathByGlobPattern') + .mockResolvedValueOnce('path/to/video.mp4'); + jest.spyOn(attachmentUtils, 'checkVideoFileReady').mockResolvedValueOnce(false); + + const promise = waitForVideoFile('*.mp4', 3000, 1000); + jest.advanceTimersByTime(3000); + + await expect(promise).rejects.toThrow( + 'Timeout of 3000ms reached, file *.mp4 not found or not ready yet.', + ); + }, 20000); + + afterEach(() => { + jest.useRealTimers(); + }); + }); + + describe.skip('getVideoFile', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + test('returns the correct video file object if a valid video file is found and read successfully', async () => { + const mockVideoFilePath = 'path/to/video.mp4'; + const mockFileContent = 'base64encodedcontent'; + jest.spyOn(attachmentUtils, 'waitForVideoFile').mockResolvedValueOnce(mockVideoFilePath); + jest.spyOn(fsPromises, 'readFile').mockResolvedValueOnce(mockFileContent); + + const result = await getVideoFile('video', '**', 5000, 1000); + + expect(result).toEqual({ + name: 'video.mp4', + type: 'video/mp4', + content: mockFileContent, + }); + }); + + test('returns null if no video file name is provided', async () => { + const result = await getVideoFile(''); + expect(result).toBeNull(); + }); + + test('returns null and logs a warning if there is an error during the video file search', async () => { + jest + .spyOn(attachmentUtils, 'waitForVideoFile') + .mockRejectedValueOnce(new Error('File not found')); + jest.spyOn(console, 'warn').mockImplementationOnce(() => {}); + + const result = await getVideoFile('video'); + expect(result).toBeNull(); + expect(console.warn).toHaveBeenCalledWith('File not found'); + }); + + test('handles file read errors gracefully', async () => { + const mockVideoFilePath = 'path/to/video.mp4'; + jest.spyOn(attachmentUtils, 'waitForVideoFile').mockResolvedValueOnce(mockVideoFilePath); + jest.spyOn(fsPromises, 'readFile').mockRejectedValueOnce(new Error('Failed to read file')); + jest.spyOn(console, 'warn').mockImplementationOnce(() => {}); + + const result = await getVideoFile('video'); + expect(result).toBeNull(); + expect(console.warn).toHaveBeenCalledWith('Failed to read file'); + }); + }); +}); diff --git a/test/utils/common.test.js b/test/utils/common.test.js new file mode 100644 index 0000000..bb4e63e --- /dev/null +++ b/test/utils/common.test.js @@ -0,0 +1,37 @@ +const { getCodeRef } = require('../../lib/utils/common'); + +describe('common utils', () => { + describe('getCodeRef', () => { + it('should return correct code ref for Windows paths', () => { + jest.mock('path', () => ({ + sep: '\\', + })); + const file = `test\\example.spec.js`; + const titlePath = ['rootDescribe', 'parentDescribe', 'testTitle']; + + const expectedCodeRef = `test/example.spec.js/rootDescribe/parentDescribe/testTitle`; + + const codeRef = getCodeRef(titlePath, file); + + expect(codeRef).toEqual(expectedCodeRef); + + jest.clearAllMocks(); + }); + + it('should return correct code ref for POSIX paths', () => { + jest.mock('path', () => ({ + sep: '/', + })); + const file = `test/example.spec.js`; + const titlePath = ['rootDescribe', 'parentDescribe', 'testTitle']; + + const expectedCodeRef = `test/example.spec.js/rootDescribe/parentDescribe/testTitle`; + + const codeRef = getCodeRef(titlePath, file); + + expect(codeRef).toEqual(expectedCodeRef); + + jest.clearAllMocks(); + }); + }); +}); diff --git a/test/utils/objectCreators.test.js b/test/utils/objectCreators.test.js new file mode 100644 index 0000000..955a8ef --- /dev/null +++ b/test/utils/objectCreators.test.js @@ -0,0 +1,771 @@ +const path = require('path'); +const { + getSystemAttributes, + getLaunchStartObject, + getSuiteStartInfo, + getSuiteEndInfo, + getSuiteStartObject, + getSuiteEndObject, + getTestInfo, + getTestStartObject, + getTestEndObject, + getHookInfo, + getHookStartObject, + getAgentInfo, + getConfig, +} = require('../../lib/utils/objectCreators'); +const pjson = require('../../package.json'); + +const sep = path.sep; + +const { RealDate, MockedDate, currentDate, getDefaultConfig } = require('../mock/mocks'); +const { testItemStatuses, entityType } = require('../../lib/constants'); + +describe('object creators', () => { + const testFileName = `test${sep}example.spec.js`; + + beforeEach(() => { + global.Date = jest.fn(MockedDate); + Object.assign(Date, RealDate); + }); + + afterEach(() => { + jest.clearAllMocks(); + global.Date = RealDate; + }); + + describe('getAgentInfo', () => { + it('getAgentInfo: should contain version and name properties', () => { + const agentInfo = getAgentInfo(); + + expect(Object.keys(agentInfo)).toContain('version'); + expect(Object.keys(agentInfo)).toContain('name'); + }); + }); + + describe('getSystemAttributes', () => { + it('skippedIssue undefined. Should return attribute with agent name and version', function () { + const options = getDefaultConfig(); + const expectedSystemAttributes = [ + { + key: 'agent', + value: `${pjson.name}|${pjson.version}`, + system: true, + }, + ]; + + const systemAttributes = getSystemAttributes(options); + + expect(systemAttributes).toEqual(expectedSystemAttributes); + }); + + it('skippedIssue = true. Should return attribute with agent name and version', function () { + const options = getDefaultConfig(); + options.reporterOptions.skippedIssue = true; + const expectedSystemAttributes = [ + { + key: 'agent', + value: `${pjson.name}|${pjson.version}`, + system: true, + }, + ]; + + const systemAttributes = getSystemAttributes(options); + + expect(systemAttributes).toEqual(expectedSystemAttributes); + }); + + it('skippedIssue = false. Should return 2 attribute: with agent name/version and skippedIssue', function () { + const options = getDefaultConfig(); + options.reporterOptions.skippedIssue = false; + const expectedSystemAttributes = [ + { + key: 'agent', + value: `${pjson.name}|${pjson.version}`, + system: true, + }, + { + key: 'skippedIssue', + value: 'false', + system: true, + }, + ]; + + const systemAttributes = getSystemAttributes(options); + + expect(systemAttributes).toEqual(expectedSystemAttributes); + }); + }); + + describe('getConfig', () => { + const baseReporterOptions = { + endpoint: 'https://reportportal.server/api/v1', + project: 'ProjectName', + launch: 'LauncherName', + description: 'Launch description', + attributes: [], + }; + + describe('CI_BUILD_ID attribute providing', () => { + afterEach(() => { + delete process.env.CI_BUILD_ID; + }); + + it('should not add an attribute with the CI_BUILD_ID value in case of parallel reporter option is false', function () { + process.env.CI_BUILD_ID = 'buildId'; + const initialConfig = { + reporter: '@reportportal/agent-js-cypress', + reporterOptions: { + ...baseReporterOptions, + apiKey: '123', + autoMerge: true, + parallel: false, + }, + }; + const expectedConfig = initialConfig; + + const config = getConfig(initialConfig); + + expect(config).toEqual(expectedConfig); + }); + + it('should not add an attribute with the CI_BUILD_ID value in case of autoMerge reporter option is false', function () { + process.env.CI_BUILD_ID = 'buildId'; + const initialConfig = { + reporter: '@reportportal/agent-js-cypress', + reporterOptions: { + ...baseReporterOptions, + apiKey: '123', + autoMerge: false, + parallel: true, + }, + }; + const expectedConfig = initialConfig; + + const config = getConfig(initialConfig); + + expect(config).toEqual(expectedConfig); + }); + + it('should not add an attribute with the value CI_BUILD_ID if the env variable CI_BUILD_ID does not exist', function () { + process.env.CI_BUILD_ID = undefined; + const initialConfig = { + reporter: '@reportportal/agent-js-cypress', + reporterOptions: { + ...baseReporterOptions, + apiKey: '123', + autoMerge: false, + parallel: true, + }, + }; + const expectedConfig = initialConfig; + + const config = getConfig(initialConfig); + + expect(config).toEqual(expectedConfig); + }); + + it('should return config with updated attributes (including attribute with CI_BUILD_ID value)', function () { + process.env.CI_BUILD_ID = 'buildId'; + const initialConfig = { + reporter: '@reportportal/agent-js-cypress', + reporterOptions: { + ...baseReporterOptions, + apiKey: '123', + autoMerge: true, + parallel: true, + }, + }; + const expectedConfig = { + reporter: '@reportportal/agent-js-cypress', + reporterOptions: { + ...initialConfig.reporterOptions, + attributes: [ + { + value: 'buildId', + }, + ], + }, + }; + + const config = getConfig(initialConfig); + + expect(config).toEqual(expectedConfig); + }); + }); + + describe('apiKey option priority', () => { + afterEach(() => { + delete process.env.RP_TOKEN; + delete process.env.RP_API_KEY; + }); + + it('should override token property if the ENV variable RP_TOKEN exists', function () { + process.env.RP_TOKEN = 'secret'; + const initialConfig = { + reporter: '@reportportal/agent-js-cypress', + reporterOptions: { + ...baseReporterOptions, + token: '123', + }, + }; + const expectedConfig = { + reporter: '@reportportal/agent-js-cypress', + reporterOptions: { + ...baseReporterOptions, + apiKey: 'secret', + }, + }; + + const config = getConfig(initialConfig); + + expect(config).toEqual(expectedConfig); + }); + + it('should override apiKey property if the ENV variable RP_API_KEY exists', function () { + process.env.RP_API_KEY = 'secret'; + const initialConfig = { + reporter: '@reportportal/agent-js-cypress', + reporterOptions: { + ...baseReporterOptions, + apiKey: '123', + }, + }; + const expectedConfig = { + reporter: '@reportportal/agent-js-cypress', + reporterOptions: { + ...baseReporterOptions, + apiKey: 'secret', + }, + }; + + const config = getConfig(initialConfig); + + expect(config).toEqual(expectedConfig); + }); + + it('should prefer apiKey property over deprecated token', function () { + const initialConfig = { + reporter: '@reportportal/agent-js-cypress', + reporterOptions: { + ...baseReporterOptions, + apiKey: '123', + token: '345', + }, + }; + const expectedConfig = { + reporter: '@reportportal/agent-js-cypress', + reporterOptions: { + ...baseReporterOptions, + apiKey: '123', + }, + }; + + const config = getConfig(initialConfig); + + expect(config).toEqual(expectedConfig); + }); + }); + }); + + describe('getLaunchStartObject', () => { + it('should return start launch object with correct values', () => { + const expectedStartLaunchObject = { + launch: 'LauncherName', + description: 'Launch description', + attributes: [ + { + key: 'agent', + system: true, + value: `${pjson.name}|${pjson.version}`, + }, + ], + startTime: currentDate, + rerun: undefined, + rerunOf: undefined, + mode: undefined, + }; + + const startLaunchObject = getLaunchStartObject(getDefaultConfig()); + + expect(startLaunchObject).toBeDefined(); + expect(startLaunchObject).toEqual(expectedStartLaunchObject); + }); + }); + + describe('getSuiteStartInfo', () => { + it('root suite: should return suite start info with undefined parentId', () => { + const suite = { + id: 'suite1', + title: 'suite name', + description: 'suite description', + root: true, + titlePath: () => ['suite name'], + }; + const expectedSuiteStartInfo = { + id: 'suite1', + title: 'suite name', + startTime: currentDate, + description: 'suite description', + codeRef: 'test/example.spec.js/suite name', + parentId: undefined, + testFileName: 'example.spec.js', + }; + + const suiteStartInfo = getSuiteStartInfo(suite, testFileName); + + expect(suiteStartInfo).toBeDefined(); + expect(suiteStartInfo).toEqual(expectedSuiteStartInfo); + }); + + it('nested suite: should return suite start info with parentId', () => { + const suite = { + id: 'suite1', + title: 'suite name', + description: 'suite description', + parent: { + id: 'parentSuiteId', + }, + titlePath: () => ['parent suite name', 'suite name'], + }; + const expectedSuiteStartInfo = { + id: 'suite1', + title: 'suite name', + startTime: currentDate, + description: 'suite description', + codeRef: 'test/example.spec.js/parent suite name/suite name', + parentId: 'parentSuiteId', + testFileName: 'example.spec.js', + }; + + const suiteStartInfo = getSuiteStartInfo(suite, testFileName); + + expect(suiteStartInfo).toBeDefined(); + expect(suiteStartInfo).toEqual(expectedSuiteStartInfo); + }); + }); + + describe('getSuiteEndInfo', () => { + it('no tests inside suite: should return suite end info without status', () => { + const suite = { + id: 'suite1', + title: 'suite name', + description: 'suite description', + parent: { + id: 'parentSuiteId', + }, + }; + const expectedSuiteEndInfo = { + id: 'suite1', + title: 'suite name', + endTime: currentDate, + }; + + const suiteEndInfo = getSuiteEndInfo(suite); + + expect(suiteEndInfo).toBeDefined(); + expect(suiteEndInfo).toEqual(expectedSuiteEndInfo); + }); + + it('no failed tests inside suite: should return suite end info with undefined status', () => { + const suite = { + id: 'suite1', + title: 'suite name', + description: 'suite description', + parent: { + id: 'parentSuiteId', + }, + tests: [{ state: 'passed' }, { state: 'skipped' }], + }; + const expectedSuiteEndInfo = { + id: 'suite1', + title: 'suite name', + endTime: currentDate, + status: undefined, + }; + + const suiteEndInfo = getSuiteEndInfo(suite); + + expect(suiteEndInfo).toBeDefined(); + expect(suiteEndInfo).toEqual(expectedSuiteEndInfo); + }); + + it('there are failed tests inside suite: should return suite end info with failed status', () => { + const suite = { + id: 'suite1', + title: 'suite name', + description: 'suite description', + parent: { + id: 'parentSuiteId', + }, + tests: [{ state: 'failed' }, { state: 'passed' }], + }; + const expectedSuiteEndInfo = { + id: 'suite1', + title: 'suite name', + endTime: currentDate, + status: testItemStatuses.FAILED, + }; + + const suiteEndInfo = getSuiteEndInfo(suite); + + expect(suiteEndInfo).toBeDefined(); + expect(suiteEndInfo).toEqual(expectedSuiteEndInfo); + }); + }); + + describe('getSuiteStartObject', () => { + it('should return suite start object', () => { + const suite = { + id: 'suite1', + title: 'suite name', + startTime: currentDate, + description: 'suite description', + codeRef: 'test/example.spec.js/suite name', + testFileName: 'example.spec.js', + }; + const expectedSuiteStartObject = { + type: entityType.SUITE, + name: 'suite name', + startTime: currentDate, + description: 'suite description', + codeRef: 'test/example.spec.js/suite name', + attributes: [], + }; + + const suiteStartObject = getSuiteStartObject(suite); + + expect(suiteStartObject).toBeDefined(); + expect(suiteStartObject).toEqual(expectedSuiteStartObject); + }); + }); + + describe('getSuiteEndObject', () => { + it('should return suite end object', () => { + const suite = { + id: 'suite1', + title: 'suite name', + endTime: currentDate, + status: testItemStatuses.FAILED, + }; + const expectedSuiteEndObject = { + status: testItemStatuses.FAILED, + endTime: currentDate, + }; + + const suiteEndObject = getSuiteEndObject(suite); + + expect(suiteEndObject).toBeDefined(); + expect(suiteEndObject).toEqual(expectedSuiteEndObject); + }); + }); + + describe('getTestInfo', () => { + it('passed test: should return test info with passed status', () => { + const test = { + id: 'testId1', + title: 'test name', + parent: { + id: 'parentSuiteId', + }, + state: 'passed', + titlePath: () => ['suite name', 'test name'], + }; + const expectedTestInfoObject = { + id: 'testId1', + title: 'test name', + status: 'passed', + parentId: 'parentSuiteId', + codeRef: 'test/example.spec.js/suite name/test name', + err: undefined, + testFileName, + }; + + const testInfoObject = getTestInfo(test, testFileName); + + expect(testInfoObject).toBeDefined(); + expect(testInfoObject).toEqual(expectedTestInfoObject); + }); + + it('pending test: should return test info with skipped status', () => { + const test = { + id: 'testId1', + title: 'test name', + parent: { + id: 'parentSuiteId', + }, + state: 'pending', + titlePath: () => ['suite name', 'test name'], + }; + const expectedTestInfoObject = { + id: 'testId1', + title: 'test name', + status: 'skipped', + parentId: 'parentSuiteId', + codeRef: 'test/example.spec.js/suite name/test name', + err: undefined, + testFileName, + }; + + const testInfoObject = getTestInfo(test, testFileName); + + expect(testInfoObject).toBeDefined(); + expect(testInfoObject).toEqual(expectedTestInfoObject); + }); + + it('should return test info with specified status and error', () => { + const test = { + id: 'testId', + title: 'test name', + parent: { + id: 'parentSuiteId', + }, + state: 'pending', + titlePath: () => ['suite name', 'test name'], + }; + const expectedTestInfoObject = { + id: 'testId', + title: 'test name', + status: 'failed', + parentId: 'parentSuiteId', + codeRef: 'test/example.spec.js/suite name/test name', + err: { message: 'error message' }, + testFileName, + }; + + const testInfoObject = getTestInfo(test, testFileName, 'failed', { + message: 'error message', + }); + + expect(testInfoObject).toBeDefined(); + expect(testInfoObject).toEqual(expectedTestInfoObject); + }); + }); + + describe('getTestStartObject', () => { + it('should return test start object', () => { + const test = { + id: 'testId1', + title: 'test name', + parent: { + id: 'parentSuiteId', + }, + codeRef: 'test/example.spec.js/suite name/test name', + }; + const expectedTestStartObject = { + name: 'test name', + startTime: currentDate, + attributes: [], + type: 'step', + codeRef: 'test/example.spec.js/suite name/test name', + }; + + const testInfoObject = getTestStartObject(test); + + expect(testInfoObject).toBeDefined(); + expect(testInfoObject).toEqual(expectedTestStartObject); + }); + }); + + describe('getTestEndObject', () => { + it('skippedIssue is not defined: should return test end object without issue', () => { + const testInfo = { + id: 'testId1', + title: 'test name', + status: 'skipped', + parent: { + id: 'parentSuiteId', + }, + }; + const expectedTestEndObject = { + endTime: currentDate, + status: testInfo.status, + }; + const testEndObject = getTestEndObject(testInfo); + + expect(testEndObject).toBeDefined(); + expect(testEndObject).toEqual(expectedTestEndObject); + }); + + it('skippedIssue = true: should return test end object without issue', () => { + const testInfo = { + id: 'testId1', + title: 'test name', + status: 'skipped', + parent: { + id: 'parentSuiteId', + }, + }; + const expectedTestEndObject = { + endTime: currentDate, + status: testInfo.status, + }; + const testEndObject = getTestEndObject(testInfo, true); + + expect(testEndObject).toBeDefined(); + expect(testEndObject).toEqual(expectedTestEndObject); + }); + + it('skippedIssue = false: should return test end object with issue NOT_ISSUE', () => { + const testInfo = { + id: 'testId1', + title: 'test name', + status: 'skipped', + parent: { + id: 'parentSuiteId', + }, + }; + const expectedTestEndObject = { + endTime: currentDate, + status: testInfo.status, + issue: { + issueType: 'NOT_ISSUE', + }, + }; + const testEndObject = getTestEndObject(testInfo, false); + + expect(testEndObject).toBeDefined(); + expect(testEndObject).toEqual(expectedTestEndObject); + }); + + it('testCaseId is defined: should return test end object with testCaseId', () => { + const testInfo = { + id: 'testId1', + title: 'test name', + status: 'skipped', + parent: { + id: 'parentSuiteId', + }, + testCaseId: 'testCaseId', + }; + const expectedTestEndObject = { + endTime: currentDate, + status: testInfo.status, + testCaseId: 'testCaseId', + }; + const testEndObject = getTestEndObject(testInfo); + + expect(testEndObject).toEqual(expectedTestEndObject); + }); + }); + + describe('getHookInfo', () => { + it('passed before each hook: should return hook info with passed status', () => { + const hook = { + id: 'testId', + title: '"before each" hook: hook name', + parent: { + id: 'parentSuiteId', + }, + state: 'passed', + hookName: 'before each', + hookId: 'hookId', + titlePath: () => ['suite name', 'hook name'], + }; + const expectedHookInfoObject = { + id: 'hookId_testId', + hookName: 'before each', + title: '"before each" hook: hook name', + status: 'passed', + parentId: 'parentSuiteId', + codeRef: 'test/example.spec.js/suite name/hook name', + err: undefined, + testFileName, + }; + + const hookInfoObject = getHookInfo(hook, testFileName); + + expect(hookInfoObject).toBeDefined(); + expect(hookInfoObject).toEqual(expectedHookInfoObject); + }); + + it('passed before all hook: should return correct hook info', () => { + const hook = { + id: 'testId', + title: '"before all" hook: hook name', + parent: { + id: 'parentSuiteId', + title: 'parent suite title', + parent: { + id: 'rootSuiteId', + title: 'root suite title', + }, + }, + state: 'passed', + hookName: 'before all', + hookId: 'hookId', + titlePath: () => ['suite name', 'hook name'], + }; + const expectedHookInfoObject = { + id: 'hookId_testId', + hookName: 'before all', + title: '"before all" hook: hook name', + status: 'passed', + parentId: 'rootSuiteId', + codeRef: 'test/example.spec.js/suite name/hook name', + err: undefined, + testFileName, + }; + + const hookInfoObject = getHookInfo(hook, testFileName); + + expect(hookInfoObject).toBeDefined(); + expect(hookInfoObject).toEqual(expectedHookInfoObject); + }); + + it('failed test: should return hook info with failed status', () => { + const test = { + id: 'testId', + hookName: 'before each', + title: '"before each" hook: hook name', + parent: { + id: 'parentSuiteId', + }, + state: 'failed', + failedFromHookId: 'hookId', + titlePath: () => ['suite name', 'hook name'], + }; + const expectedHookInfoObject = { + id: 'hookId_testId', + hookName: 'before each', + title: '"before each" hook: hook name', + status: 'failed', + parentId: 'parentSuiteId', + codeRef: 'test/example.spec.js/suite name/hook name', + err: undefined, + testFileName, + }; + + const hookInfoObject = getHookInfo(test, testFileName); + + expect(hookInfoObject).toBeDefined(); + expect(hookInfoObject).toEqual(expectedHookInfoObject); + }); + }); + + describe('getHookStartObject', () => { + it('should return hook start object', () => { + const hookInfo = { + id: 'hookId_testId', + hookName: 'before each', + title: '"before each" hook: hook name', + status: 'passed', + parentId: 'parentSuiteId', + titlePath: () => ['suite name', 'hook name'], + err: undefined, + }; + const expectedHookStartObject = { + name: 'hook name', + startTime: currentDate, + type: 'BEFORE_METHOD', + }; + + const hookInfoObject = getHookStartObject(hookInfo, testFileName, 'failed', { + message: 'error message', + }); + + expect(hookInfoObject).toBeDefined(); + expect(hookInfoObject).toEqual(expectedHookStartObject); + }); + }); +}); diff --git a/test/utils/specCountCalculation.test.js b/test/utils/specCountCalculation.test.js new file mode 100644 index 0000000..538c62f --- /dev/null +++ b/test/utils/specCountCalculation.test.js @@ -0,0 +1,202 @@ +const mock = require('mock-fs'); +const path = require('path'); +const { + getTotalSpecs, + getFixtureFolderPattern, + getExcludeSpecPattern, + getSpecPattern, +} = require('../../lib/utils/specCountCalculation'); + +const sep = path.sep; + +describe('spec count calculation', () => { + describe('getTotalSpecs', () => { + beforeEach(() => { + mock({ + 'cypress/tests': { + 'example1.spec.js': '', + 'example2.spec.js': '', + 'example3.spec.js': '', + 'example4.spec.ts': '', + 'example.ignore.spec.js': '', + }, + 'cypress/support': { + 'index.js': '', + }, + 'cypress/fixtures': { + 'fixtures1.js': '', + 'fixtures2.js': '', + }, + }); + }); + + afterEach(() => { + mock.restore(); + }); + + it('testFiles, integrationFolder, supportFile are specified: should count all files from integration folder', () => { + let specConfig = { + testFiles: '**/*.*', + ignoreTestFiles: '*.hot-update.js', + fixturesFolder: 'cypress/fixtures', + integrationFolder: 'cypress/tests', + supportFile: 'cypress/support/index.js', + }; + + let specCount = getTotalSpecs(specConfig); + + expect(specCount).toEqual(5); + + specConfig = { + excludeSpecPattern: '*.hot-update.js', + specPattern: 'cypress/tests/**/*.spec.{js,ts}', + supportFile: 'cypress/support/index.js', + fixturesFolder: 'cypress/fixtures', + }; + + specCount = getTotalSpecs(specConfig); + + expect(specCount).toEqual(5); + }); + + it('nor testFiles nor specPattern are specified: should throw an exception', () => { + expect(() => { + getTotalSpecs({}); + }).toThrow( + new Error('Configuration property not set! Neither for cypress <= 9 nor cypress >= 10'), + ); + }); + + it('ignoreTestFiles are specified: should ignore specified files', () => { + let specConfig = { + testFiles: '**/*.*', + ignoreTestFiles: ['*.hot-update.js', '*.ignore.*.*'], + fixturesFolder: 'cypress/fixtures', + integrationFolder: 'cypress/tests', + supportFile: 'cypress/support/index.js', + }; + + let specCount = getTotalSpecs(specConfig); + + expect(specCount).toEqual(4); + + specConfig = { + specPattern: 'cypress/tests/**/*.spec.{js,ts}', + excludeSpecPattern: ['*.hot-update.js', '*.ignore.spec.*'], + supportFile: 'cypress/support/index.js', + fixturesFolder: 'cypress/fixtures', + }; + + specCount = getTotalSpecs(specConfig); + + expect(specCount).toEqual(4); + }); + }); + + describe('getFixtureFolderPattern', () => { + it('returns a glob pattern for fixtures folder', () => { + const specConfig = { fixturesFolder: `cypress${sep}fixtures` }; + + const specArray = getFixtureFolderPattern(specConfig); + expect(specArray).toHaveLength(1); + expect(specArray).toContain(`cypress${sep}fixtures${sep}**${sep}*`); + }); + }); + + describe('getExcludeSpecPattern', () => { + it('getExcludeSpecPattern returns required pattern for cypress version >= 10', () => { + const specConfigString = { + excludeSpecPattern: '*.hot-update.js', + }; + + const specConfigArray = { + excludeSpecPattern: ['*.hot-update.js', '*.hot-update.ts'], + }; + + let patternArray = getExcludeSpecPattern(specConfigString); + expect(patternArray).toHaveLength(1); + expect(patternArray).toContain('*.hot-update.js'); + + patternArray = getExcludeSpecPattern(specConfigArray); + expect(patternArray).toHaveLength(2); + expect(patternArray).toContain('*.hot-update.js'); + expect(patternArray).toContain('*.hot-update.ts'); + }); + it('getExcludeSpecPattern returns required pattern for cypress version <= 9', () => { + const specConfigString = { + integrationFolder: 'cypress/integration', + ignoreTestFiles: '*.hot-update.js', + fixturesFolder: 'cypress/fixtures', + supportFile: 'cypress/support/index.js', + }; + + const specConfigArray = { + integrationFolder: 'cypress/integration', + ignoreTestFiles: ['*.hot-update.js', '*.hot-update.ts'], + fixturesFolder: 'cypress/fixtures', + supportFile: 'cypress/support/index.js', + }; + + let patternArray = getExcludeSpecPattern(specConfigString); + expect(patternArray).toHaveLength(1); + expect(patternArray).toContain('*.hot-update.js'); + + patternArray = getExcludeSpecPattern(specConfigArray); + expect(patternArray).toHaveLength(2); + expect(patternArray).toContain('*.hot-update.js'); + expect(patternArray).toContain('*.hot-update.ts'); + }); + }); + + describe('getSpecPattern', () => { + it('returns the required glob pattern for cypress <=9 config when testFiles is an array', () => { + const specConfig = { + integrationFolder: 'cypress/integration', + testFiles: ['**/*.js', '**/*.ts'], + }; + + const patternArray = getSpecPattern(specConfig); + expect(patternArray).toHaveLength(2); + expect(patternArray[0]).toEqual( + path.join(specConfig.integrationFolder, specConfig.testFiles[0]), + ); + expect(patternArray[1]).toEqual( + path.join(specConfig.integrationFolder, specConfig.testFiles[1]), + ); + }); + + it('getSpecPattern returns the required glob pattern for cypress >= 10 config when specPattern is an array', () => { + const specConfig = { + specPattern: ['cypress/integration/**/*.js', 'cypress/integration/**/*.js'], + }; + + const patternArray = getSpecPattern(specConfig); + expect(patternArray).toHaveLength(2); + expect(patternArray[0]).toEqual(specConfig.specPattern[0]); + expect(patternArray[1]).toEqual(specConfig.specPattern[1]); + }); + + it('getSpecPattern returns the required glob pattern for cypress >= 10 config when specPattern is a string', () => { + const specConfig = { + specPattern: 'cypress/integration/**/*.js', + }; + + const patternArray = getSpecPattern(specConfig); + expect(patternArray).toHaveLength(1); + expect(patternArray[0]).toEqual(specConfig.specPattern); + }); + + it('getSpecPattern returns the required glob pattern for cypress <= 9 config when testFiles is a string', () => { + const specConfig = { + integrationFolder: 'cypress/integration', + testFiles: '**/*.js', + }; + + const patternArray = getSpecPattern(specConfig); + expect(patternArray).toHaveLength(1); + expect(patternArray[0]).toEqual( + path.join(specConfig.integrationFolder, specConfig.testFiles), + ); + }); + }); +});