diff --git a/src/browser/connection/index.js b/src/browser/connection/index.js index adb6358411d..07611e1aa21 100644 --- a/src/browser/connection/index.js +++ b/src/browser/connection/index.js @@ -11,14 +11,10 @@ import STATUS from './status'; import { GeneralError } from '../../errors/runtime'; import MESSAGE from '../../errors/runtime/message'; - -// Const const IDLE_PAGE_TEMPLATE = read('../../client/browser/idle-page/index.html.mustache'); - var connections = {}; - export default class BrowserConnection extends EventEmitter { constructor (gateway, browserInfo, permanent) { super(); @@ -202,7 +198,11 @@ export default class BrowserConnection extends EventEmitter { establish (userAgent) { this.ready = true; - this.browserInfo.userAgent = parseUserAgent(userAgent).toString(); + const parsedUserAgent = parseUserAgent(userAgent); + + this.browserInfo.userAgent = parsedUserAgent.toString(); + this.browserInfo.fullUserAgent = userAgent; + this.browserInfo.parsedUserAgent = parsedUserAgent; this._waitForHeartbeat(); this.emit('ready'); diff --git a/src/cli/argument-parser.js b/src/cli/argument-parser.js index 8780e22ba81..b0408d3e7ed 100644 --- a/src/cli/argument-parser.js +++ b/src/cli/argument-parser.js @@ -19,7 +19,7 @@ const DEFAULT_TEST_LOOKUP_DIRS = ['test/', 'tests/']; const TEST_FILE_GLOB_PATTERN = `./**/*@(${Compiler.getSupportedTestFileExtensions().join('|')})`; const DESCRIPTION = dedent(` - In the browser list, you can use browser names (e.g. "ie9", "chrome", etc.) as well as paths to executables. + In the browser list, you can use browser names (e.g. "ie", "chrome", etc.) as well as paths to executables. To run tests against all installed browsers, use the "all" alias. @@ -33,7 +33,6 @@ const DESCRIPTION = dedent(` More info: https://devexpress.github.io/testcafe/documentation `); - export default class CLIArgumentParser { constructor (cwd) { this.program = new Command('testcafe'); @@ -89,6 +88,7 @@ export default class CLIArgumentParser { .option('-r, --reporter ', 'specify the reporters and optionally files where reports are saved') .option('-s, --screenshots ', 'enable screenshot capturing and specify the path to save the screenshots to') .option('-S, --screenshots-on-fails', 'take a screenshot whenever a test fails') + .option('-p, --screenshot-path-pattern ', 'use patterns to compose screenshot file names and paths: ${BROWSER}, ${BROWSER_VERSION}, ${OS}, etc.') .option('-q, --quarantine-mode', 'enable the quarantine mode') .option('-d, --debug-mode', 'execute test steps one by one pausing the test after each step') .option('-e, --skip-js-errors', 'make tests not fail when a JS error happens on a page') diff --git a/src/cli/cli.js b/src/cli/cli.js index 2c89aa00974..ed526cd7b32 100644 --- a/src/cli/cli.js +++ b/src/cli/cli.js @@ -93,7 +93,7 @@ async function runTests (argParser) { .browsers(browsers) .concurrency(concurrency) .filter(argParser.filter) - .screenshots(opts.screenshots, opts.screenshotsOnFails) + .screenshots(opts.screenshots, opts.screenshotsOnFails, opts.screenshotPathPattern) .startApp(opts.app, opts.appInitDelay); runner.once('done-bootstrapping', () => log.hideSpinner()); diff --git a/src/runner/index.js b/src/runner/index.js index 425892e331e..b9c7f484d72 100644 --- a/src/runner/index.js +++ b/src/runner/index.js @@ -11,12 +11,10 @@ import { GeneralError } from '../errors/runtime'; import MESSAGE from '../errors/runtime/message'; import { assertType, is } from '../errors/runtime/type-assertions'; - const DEFAULT_SELECTOR_TIMEOUT = 10000; const DEFAULT_ASSERTION_TIMEOUT = 3000; const DEFAULT_PAGE_LOAD_TIMEOUT = 3000; - export default class Runner extends EventEmitter { constructor (proxy, browserConnectionGateway) { super(); @@ -30,6 +28,7 @@ export default class Runner extends EventEmitter { proxyBypass: null, screenshotPath: null, takeScreenshotsOnFails: false, + screenshotPathPattern: null, skipJsErrors: false, quarantineMode: false, debugMode: false, @@ -198,9 +197,10 @@ export default class Runner extends EventEmitter { return this; } - screenshots (path, takeOnFails = false) { + screenshots (path, takeOnFails = false, pattern = null) { this.opts.takeScreenshotsOnFails = takeOnFails; this.opts.screenshotPath = path; + this.opts.screenshotPathPattern = pattern; return this; } diff --git a/src/runner/task.js b/src/runner/task.js index 7793506f1b9..554e1d1d39d 100644 --- a/src/runner/task.js +++ b/src/runner/task.js @@ -12,7 +12,7 @@ export default class Task extends EventEmitter { this.running = false; this.browserConnectionGroups = browserConnectionGroups; this.tests = tests; - this.screenshots = new Screenshots(opts.screenshotPath); + this.screenshots = new Screenshots(opts.screenshotPath, opts.screenshotPathPattern); this.warningLog = new WarningLog(); this.fixtureHookController = new FixtureHookController(tests, browserConnectionGroups.length); diff --git a/src/screenshots/capturer.js b/src/screenshots/capturer.js index 55060ca966f..d347e598eb0 100644 --- a/src/screenshots/capturer.js +++ b/src/screenshots/capturer.js @@ -1,46 +1,21 @@ import { join as joinPath, dirname, basename } from 'path'; -import sanitizeFilename from 'sanitize-filename'; import { generateThumbnail } from 'testcafe-browser-tools'; import cropScreenshot from './crop'; import { ensureDir } from '../utils/promisified-functions'; import { isInQueue, addToQueue } from '../utils/async-queue'; import WARNING_MESSAGE from '../notifications/warning-message'; - - -const PNG_EXTENSION_RE = /(\.png)$/; - +import escapeUserAgent from '../utils/escape-user-agent'; +import correctFilePath from '../utils/correct-file-path'; export default class Capturer { - constructor (baseScreenshotsPath, testEntry, connection, namingOptions, warningLog) { - this.enabled = !!baseScreenshotsPath; - this.baseScreenshotsPath = baseScreenshotsPath; - this.testEntry = testEntry; - this.provider = connection.provider; - this.browserId = connection.id; - this.baseDirName = namingOptions.baseDirName; - this.userAgentName = namingOptions.userAgentName; - this.quarantine = namingOptions.quarantine; - this.attemptNumber = this.quarantine ? this.quarantine.getNextAttemptNumber() : null; - this.testIndex = namingOptions.testIndex; - this.screenshotIndex = 1; - this.errorScreenshotIndex = 1; - this.warningLog = warningLog; - - var testDirName = `test-${this.testIndex}`; - var screenshotsPath = this.enabled ? joinPath(this.baseScreenshotsPath, this.baseDirName, testDirName) : ''; - - this.screenshotsPath = screenshotsPath; - this.screenshotPathForReport = screenshotsPath; - } - - static _correctFilePath (path) { - var correctedPath = path - .replace(/\\/g, '/') - .split('/') - .map(str => sanitizeFilename(str)) - .join('/'); - - return PNG_EXTENSION_RE.test(correctedPath) ? correctedPath : `${correctedPath}.png`; + constructor (baseScreenshotsPath, testEntry, connection, pathPattern, warningLog) { + this.enabled = !!baseScreenshotsPath; + this.baseScreenshotsPath = baseScreenshotsPath; + this.testEntry = testEntry; + this.provider = connection.provider; + this.browserId = connection.id; + this.warningLog = warningLog; + this.pathPattern = pathPattern; } static _getDimensionWithoutScrollbar (fullDimension, documentDimension, bodyDimension) { @@ -80,30 +55,50 @@ export default class Capturer { }; } - _getFileName (forError) { - var fileName = `${forError ? this.errorScreenshotIndex : this.screenshotIndex}.png`; + _joinWithBaseScreenshotPath (path) { + return joinPath(this.baseScreenshotsPath, path); + } + + _updateScreenshotPathForTestEntry (customPath) { + // NOTE: if test contains takeScreenshot action with custom path + // we should specify the most common screenshot folder in report + let screenshotPathForTestEntry = this.baseScreenshotsPath; + + if (!customPath) { + const pathForReport = this.pathPattern.getPathForReport(); + + screenshotPathForTestEntry = this._joinWithBaseScreenshotPath(pathForReport); + } + + this.testEntry.path = screenshotPathForTestEntry; + } + + _incrementFileIndexes (forError) { if (forError) - this.errorScreenshotIndex++; + this.pathPattern.data.errorFileIndex++; + else - this.screenshotIndex++; + this.pathPattern.data.fileIndex++; + } + + _getCustomScreenshotPath (customPath) { + const correctedCustomPath = correctFilePath(customPath); - return fileName; + return this._joinWithBaseScreenshotPath(correctedCustomPath); } - _getScreenshotPath (fileName, customPath) { - if (customPath) - return joinPath(this.baseScreenshotsPath, Capturer._correctFilePath(customPath)); + _getScreenshotPath (forError) { + const path = this.pathPattern.getPath(forError); - var screenshotPath = this.attemptNumber !== null ? - joinPath(this.screenshotsPath, `run-${this.attemptNumber}`) : this.screenshotsPath; + this._incrementFileIndexes(forError); - return joinPath(screenshotPath, this.userAgentName, fileName); + return this._joinWithBaseScreenshotPath(path); } _getThumbnailPath (screenshotPath) { - var imageName = basename(screenshotPath); - var imageDir = dirname(screenshotPath); + const imageName = basename(screenshotPath); + const imageDir = dirname(screenshotPath); return joinPath(imageDir, 'thumbnails', imageName); } @@ -117,12 +112,8 @@ export default class Capturer { if (!this.enabled) return null; - var fileName = this._getFileName(forError); - - fileName = forError ? joinPath('errors', fileName) : fileName; - - var screenshotPath = this._getScreenshotPath(fileName, customPath); - var thumbnailPath = this._getThumbnailPath(screenshotPath); + const screenshotPath = customPath ? this._getCustomScreenshotPath(customPath) : this._getScreenshotPath(forError); + const thumbnailPath = this._getThumbnailPath(screenshotPath); if (isInQueue(screenshotPath)) this.warningLog.addWarning(WARNING_MESSAGE.screenshotRewritingError, screenshotPath); @@ -135,18 +126,13 @@ export default class Capturer { await generateThumbnail(screenshotPath, thumbnailPath); }); - // NOTE: if test contains takeScreenshot action with custom path - // we should specify the most common screenshot folder in report - if (customPath) - this.screenshotPathForReport = this.baseScreenshotsPath; - - this.testEntry.path = this.screenshotPathForReport; + this._updateScreenshotPathForTestEntry(customPath); const screenshot = { screenshotPath, thumbnailPath, - userAgent: this.userAgentName, - quarantineAttemptID: this.attemptNumber, + userAgent: escapeUserAgent(this.pathPattern.data.parsedUserAgent), + quarantineAttemptID: this.pathPattern.data.quarantineAttempt, takenOnFail: forError, }; @@ -155,7 +141,6 @@ export default class Capturer { return screenshotPath; } - async captureAction (options) { return await this._capture(false, options); } diff --git a/src/screenshots/index.js b/src/screenshots/index.js index 4e7ed786e53..df6f5cf2eb1 100644 --- a/src/screenshots/index.js +++ b/src/screenshots/index.js @@ -1,56 +1,19 @@ import { find } from 'lodash'; -import sanitizeFilename from 'sanitize-filename'; import moment from 'moment'; import Capturer from './capturer'; +import PathPattern from './path-pattern'; export default class Screenshots { - constructor (path) { - this.enabled = !!path; - this.screenshotsPath = path; - this.testEntries = []; - this.screenshotBaseDirName = Screenshots._getScreenshotBaseDirName(); - this.userAgentNames = []; - } - - static _getScreenshotBaseDirName () { - var now = Date.now(); - - return moment(now).format('YYYY-MM-DD_hh-mm-ss'); - } - - static _escapeUserAgent (userAgent) { - return sanitizeFilename(userAgent.toString()).replace(/\s+/g, '_'); - } - - _getUsedUserAgent (name, testIndex, quarantineAttemptNum) { - var userAgent = null; - - for (var i = 0; i < this.userAgentNames.length; i++) { - userAgent = this.userAgentNames[i]; - - if (userAgent.name === name && userAgent.testIndex === testIndex && - userAgent.quarantineAttemptNum === quarantineAttemptNum) - return userAgent; - } - - return null; - } - - _getUserAgentName (userAgent, testIndex, quarantineAttemptNum) { - var userAgentName = Screenshots._escapeUserAgent(userAgent); - var usedUserAgent = this._getUsedUserAgent(userAgentName, testIndex, quarantineAttemptNum); - - if (usedUserAgent) { - usedUserAgent.index++; - return `${userAgentName}_${usedUserAgent.index}`; - } - - this.userAgentNames.push({ name: userAgentName, index: 0, testIndex, quarantineAttemptNum }); - return userAgentName; + constructor (path, pattern) { + this.enabled = !!path; + this.screenshotsPath = path; + this.screenshotsPattern = pattern; + this.testEntries = []; + this.now = moment(); } _addTestEntry (test) { - var testEntry = { + const testEntry = { test: test, path: this.screenshotsPath || '', screenshots: [] @@ -65,6 +28,15 @@ export default class Screenshots { return find(this.testEntries, entry => entry.test === test); } + _ensureTestEntry (test) { + let testEntry = this._getTestEntry(test); + + if (!testEntry) + testEntry = this._addTestEntry(test); + + return testEntry; + } + getScreenshotsInfo (test) { return this._getTestEntry(test).screenshots; } @@ -78,20 +50,16 @@ export default class Screenshots { } createCapturerFor (test, testIndex, quarantine, connection, warningLog) { - var testEntry = this._getTestEntry(test); - - if (!testEntry) - testEntry = this._addTestEntry(test); - - const quarantineAttemptNum = quarantine ? quarantine.getNextAttemptNumber() : null; - - var namingOptions = { + const testEntry = this._ensureTestEntry(test); + const pathPattern = new PathPattern(this.screenshotsPattern, { testIndex, - quarantine, - baseDirName: this.screenshotBaseDirName, - userAgentName: this._getUserAgentName(connection.userAgent, testIndex, quarantineAttemptNum) - }; - - return new Capturer(this.screenshotsPath, testEntry, connection, namingOptions, warningLog); + quarantineAttempt: quarantine ? quarantine.getNextAttemptNumber() : null, + now: this.now, + fixture: test.fixture.name, + test: test.name, + parsedUserAgent: connection.browserInfo.parsedUserAgent, + }); + + return new Capturer(this.screenshotsPath, testEntry, connection, pathPattern, warningLog); } } diff --git a/src/screenshots/path-pattern.js b/src/screenshots/path-pattern.js new file mode 100644 index 00000000000..fd241d3296f --- /dev/null +++ b/src/screenshots/path-pattern.js @@ -0,0 +1,119 @@ +import { escapeRegExp as escapeRe } from 'lodash'; +import correctFilePath from '../utils/correct-file-path'; +import escapeUserAgent from '../utils/escape-user-agent'; + +const DATE_FORMAT = 'YYYY-MM-DD'; +const TIME_FORMAT = 'HH-mm-ss'; + +const SCRENSHOT_EXTENTION = 'png'; + +const ERRORS_FOLDER = 'errors'; + +const PLACEHOLDERS = { + DATE: '${DATE}', + TIME: '${TIME}', + TEST_INDEX: '${TEST_INDEX}', + FILE_INDEX: '${FILE_INDEX}', + QUARANTINE_ATTEMPT: '${QUARANTINE_ATTEMPT}', + FIXTURE: '${FIXTURE}', + TEST: '${TEST}', + USERAGENT: '${USERAGENT}', + BROWSER: '${BROWSER}', + BROWSER_VERSION: '${BROWSER_VERSION}', + OS: '${OS}', + OS_VERSION: '${OS_VERSION}' +}; + +const DEFAULT_PATH_PATTERN_FOR_REPORT = `${PLACEHOLDERS.DATE}_${PLACEHOLDERS.TIME}\\test-${PLACEHOLDERS.TEST_INDEX}`; +const DEFAULT_PATH_PATTERN = `${DEFAULT_PATH_PATTERN_FOR_REPORT}\\${PLACEHOLDERS.USERAGENT}\\${PLACEHOLDERS.FILE_INDEX}.${SCRENSHOT_EXTENTION}`; +const QUARANTINE_MODE_DEFAULT_PATH_PATTERN = `${DEFAULT_PATH_PATTERN_FOR_REPORT}\\run-${PLACEHOLDERS.QUARANTINE_ATTEMPT}\\${PLACEHOLDERS.USERAGENT}\\${PLACEHOLDERS.FILE_INDEX}.${SCRENSHOT_EXTENTION}`; + +export default class PathPattern { + constructor (pattern, data) { + this.pattern = this._ensurePattern(pattern, data.quarantineAttempt); + this.data = this._addDefaultFields(data); + this.placeholderToDataMap = this._createPlaceholderToDataMap(); + } + + _ensurePattern (pattern, quarantineAttempt) { + if (pattern) + return pattern; + + return quarantineAttempt ? QUARANTINE_MODE_DEFAULT_PATH_PATTERN : DEFAULT_PATH_PATTERN; + } + + _addDefaultFields (data) { + const defaultFields = { + formattedDate: data.now.format(DATE_FORMAT), + formattedTime: data.now.format(TIME_FORMAT), + fileIndex: 1, + errorFileIndex: 1 + }; + + return Object.assign({}, defaultFields, data); + } + + _createPlaceholderToDataMap () { + return { + [PLACEHOLDERS.DATE]: this.data.formattedDate, + [PLACEHOLDERS.TIME]: this.data.formattedTime, + [PLACEHOLDERS.TEST_INDEX]: this.data.testIndex, + [PLACEHOLDERS.QUARANTINE_ATTEMPT]: this.data.quarantineAttempt || 1, + [PLACEHOLDERS.FIXTURE]: this.data.fixture, + [PLACEHOLDERS.TEST]: this.data.test, + [PLACEHOLDERS.FILE_INDEX]: forError => forError ? this.data.errorFileIndex : this.data.fileIndex, + [PLACEHOLDERS.USERAGENT]: this.data.parsedUserAgent.toString(), + [PLACEHOLDERS.BROWSER]: this.data.parsedUserAgent.browser, + [PLACEHOLDERS.BROWSER_VERSION]: this.data.parsedUserAgent.browserVersion, + [PLACEHOLDERS.OS]: this.data.parsedUserAgent.os, + [PLACEHOLDERS.OS_VERSION]: this.data.parsedUserAgent.osVersion + }; + } + + static _buildPath (pattern, placeholderToDataMap, forError) { + let resultFilePath = pattern; + + for (const placeholder in placeholderToDataMap) { + const findPlaceholderRegExp = new RegExp(escapeRe(placeholder), 'g'); + + resultFilePath = resultFilePath.replace(findPlaceholderRegExp, () => { + if (placeholder === PLACEHOLDERS.FILE_INDEX) { + const getFileIndexFn = placeholderToDataMap[placeholder]; + let result = getFileIndexFn(forError); + + if (forError) + result = `${ERRORS_FOLDER}\\${result}`; + + return result; + } + + else if (placeholder === PLACEHOLDERS.USERAGENT) { + const userAgent = placeholderToDataMap[placeholder]; + + return escapeUserAgent(userAgent); + } + + return placeholderToDataMap[placeholder]; + }); + } + + return resultFilePath; + } + + getPath (forError) { + const path = PathPattern._buildPath(this.pattern, this.placeholderToDataMap, forError); + + return correctFilePath(path, SCRENSHOT_EXTENTION); + } + + getPathForReport () { + const path = PathPattern._buildPath(DEFAULT_PATH_PATTERN_FOR_REPORT, this.placeholderToDataMap); + + return correctFilePath(path); + } + + // For testing purposes + static get PLACEHOLDERS () { + return PLACEHOLDERS; + } +} diff --git a/src/utils/correct-file-path.js b/src/utils/correct-file-path.js new file mode 100644 index 00000000000..3d03ae73bb2 --- /dev/null +++ b/src/utils/correct-file-path.js @@ -0,0 +1,17 @@ +import sanitizeFilename from 'sanitize-filename'; +import { escapeRegExp as escapeRe } from 'lodash'; + +export default function (path, expectedExtention) { + const correctedPath = path + .replace(/\\/g, '/') + .split('/') + .map(str => sanitizeFilename(str)) + .join('/'); + + if (!expectedExtention) + return correctedPath; + + const extentionRe = new RegExp(escapeRe(expectedExtention)); + + return extentionRe.test(correctedPath) ? correctedPath : `${correctedPath}.${expectedExtention}`; +} diff --git a/src/utils/escape-user-agent.js b/src/utils/escape-user-agent.js new file mode 100644 index 00000000000..6c9a7a05cc8 --- /dev/null +++ b/src/utils/escape-user-agent.js @@ -0,0 +1,5 @@ +import sanitizeFilename from 'sanitize-filename'; + +export default function escapeUserAgent (userAgent) { + return sanitizeFilename(userAgent.toString()).replace(/\s+/g, '_'); +} diff --git a/test/functional/assertion-helper.js b/test/functional/assertion-helper.js index 3e7c3b21bb0..439c31b6e52 100644 --- a/test/functional/assertion-helper.js +++ b/test/functional/assertion-helper.js @@ -244,7 +244,7 @@ exports.checkScreenshotsCreated = function ({ forError, customPath, screenshotsC var taskDirPath = path.join(SCREENSHOTS_PATH, taskDirs[0]); if (customPath) { - var customDirExists = taskDirPath.indexOf(customPath) !== -1; + var customDirExists = taskDirPath.includes(customPath); var hasScreenshots = getScreenshotFilesCount(taskDirPath, customPath) === expectedScreenshotCount * expectedSubDirCount; @@ -314,3 +314,7 @@ exports.removeScreenshotDir = function () { del(SCREENSHOTS_PATH); }; +exports.SCREENSHOTS_PATH = SCREENSHOTS_PATH; + +exports.THUMBNAILS_DIR_NAME = THUMBNAILS_DIR_NAME; + diff --git a/test/functional/fixtures/api/es-next/take-screenshot/test.js b/test/functional/fixtures/api/es-next/take-screenshot/test.js index 0e93feae57e..6948c0a9faa 100644 --- a/test/functional/fixtures/api/es-next/take-screenshot/test.js +++ b/test/functional/fixtures/api/es-next/take-screenshot/test.js @@ -1,18 +1,20 @@ -var path = require('path'); -var expect = require('chai').expect; -var config = require('../../../../config.js'); -var assertionHelper = require('../../../../assertion-helper.js'); - -var CUSTOM_SCREENSHOT_DIR = '___test-screenshots___'; -var SCREENSHOT_PATH_MESSAGE_RE = /^___test-screenshots___[\\/]\d{4,4}-\d{2,2}-\d{2,2}_\d{2,2}-\d{2,2}-\d{2,2}[\\/]test-1$/; -var SCREENSHOT_ON_FAIL_PATH_MESSAGE_RE = /^.*run-1/; -var SLASH_RE = /[\\/]/g; +const path = require('path'); +const fs = require('fs'); +const expect = require('chai').expect; +const config = require('../../../../config.js'); +const assertionHelper = require('../../../../assertion-helper.js'); + +const SCREENSHOTS_PATH = assertionHelper.SCREENSHOTS_PATH; +const THUMBNAILS_DIR_NAME = assertionHelper.THUMBNAILS_DIR_NAME; +const SCREENSHOT_PATH_MESSAGE_RE = /^___test-screenshots___[\\/]\d{4,4}-\d{2,2}-\d{2,2}_\d{2,2}-\d{2,2}-\d{2,2}[\\/]test-1$/; +const SCREENSHOT_ON_FAIL_PATH_MESSAGE_RE = /^.*run-1/; +const SLASH_RE = /[\\/]/g; var getReporter = function (scope) { const userAgents = { }; function patchScreenshotPath (screenshotPath) { - return screenshotPath.replace(SCREENSHOT_ON_FAIL_PATH_MESSAGE_RE, '').replace(CUSTOM_SCREENSHOT_DIR, '').replace(SLASH_RE, '_'); + return screenshotPath.replace(SCREENSHOT_ON_FAIL_PATH_MESSAGE_RE, '').replace(SCREENSHOTS_PATH, '').replace(SLASH_RE, '_'); } function prepareScreenshot (screenshot, quarantine) { @@ -58,7 +60,7 @@ describe('[API] t.takeScreenshot()', function () { return runTests('./testcafe-fixtures/take-screenshot.js', 'Take a screenshot with a custom path (OS separator)', { setScreenshotPath: true }) .then(function () { - expect(testReport.screenshotPath).eql(CUSTOM_SCREENSHOT_DIR); + expect(testReport.screenshotPath).eql(SCREENSHOTS_PATH); const screenshotsCheckingOptions = { forError: false, screenshotsCount: 2, customPath: 'custom' }; @@ -70,7 +72,7 @@ describe('[API] t.takeScreenshot()', function () { return runTests('./testcafe-fixtures/take-screenshot.js', 'Take a screenshot with a custom path (DOS separator)', { setScreenshotPath: true }) .then(function () { - expect(testReport.screenshotPath).contains(CUSTOM_SCREENSHOT_DIR); + expect(testReport.screenshotPath).contains(SCREENSHOTS_PATH); const screenshotsCheckingOptions = { forError: false, screenshotsCount: 2, customPath: 'custom' }; @@ -136,7 +138,7 @@ describe('[API] t.takeScreenshot()', function () { return runTests('./testcafe-fixtures/take-screenshot.js', 'Take screenshots with same path', { setScreenshotPath: true }).then(function () { - const screenshotFileName = path.join(CUSTOM_SCREENSHOT_DIR, '1.png'); + const screenshotFileName = path.join(SCREENSHOTS_PATH, '1.png'); expect(testReport.warnings).eql([ `The file at "${screenshotFileName}" already exists. It has just been rewritten ` + @@ -203,6 +205,28 @@ describe('[API] t.takeScreenshot()', function () { expect(result.unstable).eql(true); }); }); + + it('Should allow to use a custom path pattern', function () { + return runTests('./testcafe-fixtures/take-screenshot.js', 'Take a screenshot', + { + setScreenshotPath: true, + screenshotPathPattern: '${TEST}-${FILE_INDEX}', + only: 'chrome' + }) + .then(() => { + expect(SCREENSHOT_PATH_MESSAGE_RE.test(testReport.screenshotPath)).eql(true); + + const screenshot1Path = path.join(assertionHelper.SCREENSHOTS_PATH, 'Take a screenshot-1.png'); + const screenshot2Path = path.join(assertionHelper.SCREENSHOTS_PATH, 'Take a screenshot-2.png'); + const thumbnail1Path = path.join(assertionHelper.SCREENSHOTS_PATH, THUMBNAILS_DIR_NAME, 'Take a screenshot-1.png'); + const thumbnail2Path = path.join(assertionHelper.SCREENSHOTS_PATH, THUMBNAILS_DIR_NAME, 'Take a screenshot-2.png'); + + expect(fs.existsSync(screenshot1Path)).eql(true); + expect(fs.existsSync(screenshot2Path)).eql(true); + expect(fs.existsSync(thumbnail1Path)).eql(true); + expect(fs.existsSync(thumbnail2Path)).eql(true); + }); + }); } }); diff --git a/test/functional/setup.js b/test/functional/setup.js index 7b0a48efec6..9cbf4bcf3b6 100644 --- a/test/functional/setup.js +++ b/test/functional/setup.js @@ -158,25 +158,26 @@ before(function () { global.testCafe = testCafe; global.runTests = function (fixture, testName, opts) { - var report = ''; - var runner = testCafe.createRunner(); - var fixturePath = typeof fixture !== 'string' || path.isAbsolute(fixture) ? fixture : path.join(path.dirname(caller()), fixture); - var skipJsErrors = opts && opts.skipJsErrors; - var disablePageReloads = opts && opts.disablePageReloads; - var quarantineMode = opts && opts.quarantineMode; - var selectorTimeout = opts && opts.selectorTimeout || FUNCTIONAL_TESTS_SELECTOR_TIMEOUT; - var assertionTimeout = opts && opts.assertionTimeout || FUNCTIONAL_TESTS_ASSERTION_TIMEOUT; - var pageLoadTimeout = opts && opts.pageLoadTimeout || FUNCTIONAL_TESTS_PAGE_LOAD_TIMEOUT; - var onlyOption = opts && opts.only; - var skipOption = opts && opts.skip; - var screenshotPath = opts && opts.setScreenshotPath ? '___test-screenshots___' : ''; - var screenshotsOnFails = opts && opts.screenshotsOnFails; - var speed = opts && opts.speed; - var appCommand = opts && opts.appCommand; - var appInitDelay = opts && opts.appInitDelay; - var externalProxyHost = opts && opts.useProxy; - var proxyBypass = opts && opts.proxyBypass; - var customReporters = opts && opts.reporters; + let report = ''; + const runner = testCafe.createRunner(); + const fixturePath = typeof fixture !== 'string' || path.isAbsolute(fixture) ? fixture : path.join(path.dirname(caller()), fixture); + const skipJsErrors = opts && opts.skipJsErrors; + const disablePageReloads = opts && opts.disablePageReloads; + const quarantineMode = opts && opts.quarantineMode; + const selectorTimeout = opts && opts.selectorTimeout || FUNCTIONAL_TESTS_SELECTOR_TIMEOUT; + const assertionTimeout = opts && opts.assertionTimeout || FUNCTIONAL_TESTS_ASSERTION_TIMEOUT; + const pageLoadTimeout = opts && opts.pageLoadTimeout || FUNCTIONAL_TESTS_PAGE_LOAD_TIMEOUT; + const onlyOption = opts && opts.only; + const skipOption = opts && opts.skip; + const screenshotPath = opts && opts.setScreenshotPath ? '___test-screenshots___' : ''; + const screenshotPathPattern = opts && opts.screenshotPathPattern; + const screenshotsOnFails = opts && opts.screenshotsOnFails; + const speed = opts && opts.speed; + const appCommand = opts && opts.appCommand; + const appInitDelay = opts && opts.appInitDelay; + const externalProxyHost = opts && opts.useProxy; + const proxyBypass = opts && opts.proxyBypass; + const customReporters = opts && opts.reporters; var actualBrowsers = browsersInfo.filter(function (browserInfo) { var { alias, userAgent } = browserInfo.settings; @@ -227,7 +228,7 @@ before(function () { return testName ? test === testName : true; }) .src(fixturePath) - .screenshots(screenshotPath, screenshotsOnFails) + .screenshots(screenshotPath, screenshotsOnFails, screenshotPathPattern) .startApp(appCommand, appInitDelay) .run({ skipJsErrors, disablePageReloads, quarantineMode, selectorTimeout, assertionTimeout, pageLoadTimeout, speed }) .then(function () { diff --git a/test/server/cli-argument-parser-test.js b/test/server/cli-argument-parser-test.js index 77593bcecac..dda2ff54366 100644 --- a/test/server/cli-argument-parser-test.js +++ b/test/server/cli-argument-parser-test.js @@ -413,6 +413,7 @@ describe('CLI argument parser', function () { { long: '--list-browsers', short: '-b' }, { long: '--reporter', short: '-r' }, { long: '--screenshots', short: '-s' }, + { long: '--screenshot-path-pattern', short: '-p' }, { long: '--screenshots-on-fails', short: '-S' }, { long: '--quarantine-mode', short: '-q' }, { long: '--debug-mode', short: '-d' }, diff --git a/test/server/path-pattern-test.js b/test/server/path-pattern-test.js new file mode 100644 index 00000000000..e9310a9a66c --- /dev/null +++ b/test/server/path-pattern-test.js @@ -0,0 +1,78 @@ +const PathPattern = require('../../lib/screenshots/path-pattern'); +const expect = require('chai').expect; +const moment = require('moment'); + +describe('Screenshot path pattern', () => { + const createPathPattern = (pattern, data) => { + data = data || {}; + data.now = data.now || moment(); + data.parsedUserAgent = data.parsedUserAgent || {}; + data.quarantineAttempt = data.quarantineAttempt || null; + + return new PathPattern(pattern, data); + }; + + describe('Default pattern', () => { + it('Normal run', () => { + const pathPattern = createPathPattern(); + + expect(pathPattern.pattern).eql('${DATE}_${TIME}\\test-${TEST_INDEX}\\${USERAGENT}\\${FILE_INDEX}.png'); + }); + + it('Quarantine mode', () => { + const pathPattern = createPathPattern(void 0, { quarantineAttempt: 1 }); + + expect(pathPattern.pattern).eql('${DATE}_${TIME}\\test-${TEST_INDEX}\\run-${QUARANTINE_ATTEMPT}\\${USERAGENT}\\${FILE_INDEX}.png'); + }); + }); + + it('Should replace all placeholders', () => { + const pattern = Object.getOwnPropertyNames(PathPattern.PLACEHOLDERS).map(name => PathPattern.PLACEHOLDERS[name]).join('#'); + const dateStr = '2010-01-02'; + const timeStr = '11:12:13'; + const data = { + now: moment(dateStr + ' ' + timeStr), + testIndex: 12, + fileIndex: 34, + quarantineAttempt: 56, + fixture: 'fixture', + test: 'test', + parsedUserAgent: { + browser: 'Chrome', + browserVersion: '67.0.3396', + os: 'Windows', + osVersion: '8.1.0.0', + toString: function () { + return 'full_user_agent'; + } + } + }; + const expectedParsedPattern = [ + dateStr, + timeStr.replace(/:/g, '-'), + data.testIndex, + data.fileIndex, + data.quarantineAttempt, + data.fixture, + data.test, + data.parsedUserAgent.toString(), + data.parsedUserAgent.browser, + data.parsedUserAgent.browserVersion, + data.parsedUserAgent.os, + data.parsedUserAgent.osVersion + ].join('#') + '.png'; + + const pathPattern = createPathPattern(pattern, data); + + const path = pathPattern.getPath(false); + + expect(path).eql(expectedParsedPattern); + }); + + it('Should add `errors` folder before filename', () => { + const pathPattern = createPathPattern('${FILE_INDEX}'); + const path = pathPattern.getPath(true); + + expect(path).eql('errors/1.png'); + }); +}); diff --git a/test/server/util-test.js b/test/server/util-test.js new file mode 100644 index 00000000000..a27d888643c --- /dev/null +++ b/test/server/util-test.js @@ -0,0 +1,16 @@ +const expect = require('chai').expect; +const correctFilePath = require('../../lib/utils/correct-file-path'); +const escapeUserAgent = require('../../lib/utils/escape-user-agent'); + +describe('Utils', () => { + it('Correct File Path', () => { + expect(correctFilePath('\\test')).eql('/test'); + expect(correctFilePath('"')).eql(''); + expect(correctFilePath('test.png', 'test.png')); + expect(correctFilePath('test', 'png')).eql('test.png'); + }); + + it('Escape user agent', () => { + expect(escapeUserAgent('Chrome 67.0.3396 / Windows 8.1.0.0')).eql('Chrome_67.0.3396_Windows_8.1.0.0'); + }); +});