Skip to content

Commit

Permalink
Screenshot file pattern
Browse files Browse the repository at this point in the history
  • Loading branch information
[email protected] authored and [email protected] committed Jun 27, 2018
1 parent c2b1400 commit ec61e6f
Show file tree
Hide file tree
Showing 15 changed files with 364 additions and 160 deletions.
10 changes: 5 additions & 5 deletions src/browser/connection/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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');
Expand Down
9 changes: 4 additions & 5 deletions src/cli/argument-parser.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { resolve, join as pathJoin, dirname } from 'path';
import { Command } from 'commander';
import Promise from 'pinkie';
import dedent from 'dedent';
import isGlob from 'is-glob';
import globby from 'globby';
import { readSync as read } from 'read-file-relative';
Expand All @@ -18,8 +17,8 @@ const REMOTE_ALIAS_RE = /^remote(?::(\d*))?$/;
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.
const DESCRIPTION = `
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.
Expand All @@ -31,8 +30,7 @@ const DESCRIPTION = dedent(`
You can use one or more file paths or glob patterns to specify which tests to run.
More info: https://devexpress.github.io/testcafe/documentation
`);

`;

export default class CLIArgumentParser {
constructor (cwd) {
Expand Down Expand Up @@ -89,6 +87,7 @@ export default class CLIArgumentParser {
.option('-r, --reporter <name[:outputFile][,...]>', 'specify the reporters and optionally files where reports are saved')
.option('-s, --screenshots <path>', '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 <pattern>', 'use patterns to compose screenshot file names and paths: ${BROWSER}, ${BROWSER_VERSION}, ${OS}, ${OS_VERSION}, ${USERAGENT}, ${DATE}, ${TIME}, ${FIXTURE}, ${TEST}, ${TEST_INDEX}, ${FILE_INDEX}, ${QUARANTINE_ATTEMPT}')
.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')
Expand Down
2 changes: 1 addition & 1 deletion src/cli/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand Down
6 changes: 3 additions & 3 deletions src/runner/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -30,6 +28,7 @@ export default class Runner extends EventEmitter {
proxyBypass: null,
screenshotPath: null,
takeScreenshotsOnFails: false,
screenshotPathPattern: null,
skipJsErrors: false,
quarantineMode: false,
debugMode: false,
Expand Down Expand Up @@ -198,9 +197,10 @@ export default class Runner extends EventEmitter {
return this;
}

screenshots (path, takeOnFails = false) {
screenshots (path, takeOnFails = false, pattern) {
this.opts.takeScreenshotsOnFails = takeOnFails;
this.opts.screenshotPath = path;
this.opts.screenshotPathPattern = pattern;

return this;
}
Expand Down
2 changes: 1 addition & 1 deletion src/runner/task.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
106 changes: 45 additions & 61 deletions src/screenshots/capturer.js
Original file line number Diff line number Diff line change
@@ -1,46 +1,20 @@
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 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) {
Expand Down Expand Up @@ -80,25 +54,45 @@ 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) {
Expand All @@ -117,12 +111,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);
Expand All @@ -135,18 +125,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: this.pathPattern.data.parsedUserAgent.toString(),
quarantineAttemptID: this.pathPattern.data.quarantineAttempt,
takenOnFail: forError,
};

Expand All @@ -155,7 +140,6 @@ export default class Capturer {
return screenshotPath;
}


async captureAction (options) {
return await this._capture(false, options);
}
Expand Down
73 changes: 18 additions & 55 deletions src/screenshots/index.js
Original file line number Diff line number Diff line change
@@ -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: []
Expand Down Expand Up @@ -78,20 +41,20 @@ export default class Screenshots {
}

createCapturerFor (test, testIndex, quarantine, connection, warningLog) {
var testEntry = this._getTestEntry(test);
let testEntry = this._getTestEntry(test);

if (!testEntry)
testEntry = this._addTestEntry(test);

const quarantineAttemptNum = quarantine ? quarantine.getNextAttemptNumber() : null;

var namingOptions = {
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);
}
}
Loading

0 comments on commit ec61e6f

Please sign in to comment.