Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Screenshot file patterns (closes #1602, #1651, #1974, #1975) #2086

Closed
wants to merge 7 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion src/browser/connection/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,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
2 changes: 1 addition & 1 deletion src/cli/argument-parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ export default class CLIArgumentParser {
.option('-b, --list-browsers [provider]', 'output the aliases for local browsers or browsers available through the specified browser provider')
.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('-p, --screenshot-path-pattern <pattern>', 'use patterns to compose screenshot file names and paths: ${BROWSER}, ${BROWSER_VERSION}, ${OS}, ${OS_VERSION}, ${USERAGENT}, ${DATE}, ${TIME}, ${DTF_<FORMAT>}, ${FIXTURE}, ${TEST}, ${TEST_INDEX}, ${FILE_INDEX}, ${QUARANTINE_ATTEMPT}')
.option('-S, --screenshots-on-fails', 'take a screenshot whenever a test fails')
.option('-q, --quarantine-mode', 'enable the quarantine mode')
.option('-d, --debug-mode', 'execute test steps one by one pausing the test after each step')
Expand Down Expand Up @@ -156,7 +157,6 @@ export default class CLIArgumentParser {
}
}


_parseSelectorTimeout () {
if (this.opts.selectorTimeout) {
assertType(is.nonNegativeNumberString, null, 'Selector timeout', this.opts.selectorTimeout);
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
57 changes: 11 additions & 46 deletions src/screenshots/capturer.js
Original file line number Diff line number Diff line change
@@ -1,47 +1,27 @@
import { join as joinPath, dirname } 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) {
constructor (baseScreenshotsPath, testEntry, connection, pathPattern, 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.quarantineAttemptNum = namingOptions.quarantineAttemptNum;
this.testIndex = namingOptions.testIndex;
this.screenshotIndex = 1;
this.errorScreenshotIndex = 1;
this.warningLog = warningLog;
this.pathPattern = pathPattern;

var testDirName = `test-${this.testIndex}`;
var screenshotsPath = this.enabled ? joinPath(this.baseScreenshotsPath, this.baseDirName, testDirName) : '';
const screenshotsPath = this.enabled ? this.baseScreenshotsPath : '';

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`;
}

static _getDimensionWithoutScrollbar (fullDimension, documentDimension, bodyDimension) {
if (bodyDimension > fullDimension)
return documentDimension;
Expand Down Expand Up @@ -79,26 +59,16 @@ export default class Capturer {
};
}

_getFileName (forError) {
var fileName = `${forError ? this.errorScreenshotIndex : this.screenshotIndex}.png`;

if (forError)
this.errorScreenshotIndex++;
else
this.screenshotIndex++;

return fileName;
_getCustomScreenshotPath (customPath) {
return joinPath(this.baseScreenshotsPath, correctFilePath(customPath));
}

_getScreenshotPath (fileName, customPath) {
if (customPath)
return joinPath(this.baseScreenshotsPath, Capturer._correctFilePath(customPath));
_getScreenshotPath (forError) {
const parsedPath = this.pathPattern.getPath(forError);

var screenshotPath = this.quarantineAttemptNum !== null ?
joinPath(this.screenshotsPath, `run-${this.quarantineAttemptNum}`) :
this.screenshotsPath;
this.pathPattern.incrementFileIndexes(forError);

return joinPath(screenshotPath, this.userAgentName, fileName);
return joinPath(this.baseScreenshotsPath, parsedPath);
}

async _takeScreenshot (filePath, pageWidth, pageHeight) {
Expand All @@ -110,11 +80,7 @@ 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);
const screenshotPath = customPath ? this._getCustomScreenshotPath(customPath) : this._getScreenshotPath(forError);

if (isInQueue(screenshotPath))
this.warningLog.addWarning(WARNING_MESSAGE.screenshotRewritingError, screenshotPath);
Expand All @@ -138,7 +104,6 @@ export default class Capturer {
return screenshotPath;
}


async captureAction (options) {
return await this._capture(false, options);
}
Expand Down
65 changes: 15 additions & 50 deletions src/screenshots/index.js
Original file line number Diff line number Diff line change
@@ -1,52 +1,15 @@
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) {
constructor (path, pattern) {
this.enabled = !!path;
this.screenshotsPath = path;
this.screenshotsPattern = pattern;
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;
this.now = moment();
}

_addTestEntry (test) {
Expand All @@ -73,19 +36,21 @@ export default class Screenshots {
return this._getTestEntry(test).path;
}

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

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

var namingOptions = {
const pathPattern = new PathPattern(this.screenshotsPattern, {
testIndex,
quarantineAttemptNum,
baseDirName: this.screenshotBaseDirName,
userAgentName: this._getUserAgentName(connection.userAgent, testIndex, quarantineAttemptNum)
};

return new Capturer(this.screenshotsPath, testEntry, connection, namingOptions, warningLog);
quarantineAttempt,
now: this.now,
fixture: test.fixture.name,
test: test.name,
parsedUserAgent: connection.browserInfo.parsedUserAgent,
});

return new Capturer(this.screenshotsPath, testEntry, connection, pathPattern, warningLog);
}
}
124 changes: 124 additions & 0 deletions src/screenshots/path-pattern.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import { escapeRegExp as escapeRe } from 'lodash';
import sanitizeFilename from 'sanitize-filename';
import correctFilePath from '../utils/correct-file-path';

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 = `${PLACEHOLDERS.DATE}_${PLACEHOLDERS.TIME}\\test-${PLACEHOLDERS.TEST_INDEX}\\${PLACEHOLDERS.USERAGENT}\\${PLACEHOLDERS.FILE_INDEX}.${SCRENSHOT_EXTENTION}`;
const QUARANTINE_MODE_DEFAULT_PATH_PATTERN = `${PLACEHOLDERS.DATE}_${PLACEHOLDERS.TIME}\\test-${PLACEHOLDERS.TEST_INDEX}\\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 = {
date: data.now.format(DATE_FORMAT),
time: data.now.format(TIME_FORMAT),
fileIndex: 1,
errorFileIndex: 1
};

return Object.assign({}, defaultFields, data);
}

_createPlaceholderToDataMap () {
return {
[PLACEHOLDERS.DATE]: this.data.date,
[PLACEHOLDERS.TIME]: this.data.time,
[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 _escapeUserAgent (userAgent) {
return sanitizeFilename(userAgent.toString()).replace(/\s+/g, '_');
}

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 PathPattern._escapeUserAgent(userAgent);
}

return placeholderToDataMap[placeholder];
});
}

return resultFilePath;
}

getPath (forError) {
const path = PathPattern._buildPath(this.pattern, this.placeholderToDataMap, forError);

return correctFilePath(path, SCRENSHOT_EXTENTION);
}

incrementFileIndexes (forError) {
if (forError)
this.data.errorFileIndex++;

else
this.data.fileIndex++;
}

// For testing purposes
static get PLACEHOLDERS () {
return PLACEHOLDERS;
}
}
Loading