diff --git a/src/reporter/command/command-formatter.ts b/src/reporter/command/command-formatter.ts index 8388a014e1..19acd45553 100644 --- a/src/reporter/command/command-formatter.ts +++ b/src/reporter/command/command-formatter.ts @@ -1,7 +1,21 @@ +import { isEmpty } from 'lodash'; import { ExecuteSelectorCommand, ExecuteClientFunctionCommand } from '../../test-run/commands/observation'; import { NavigateToCommand, SetNativeDialogHandlerCommand, UseRoleCommand } from '../../test-run/commands/actions'; import { createReplicator, SelectorNodeTransform } from '../../client-functions/replicator'; import { Command, FormattedCommand, SelectorInfo } from './interfaces'; +import { Dictionary } from '../../configuration/interfaces'; +import diff from '../../utils/diff'; + +import { + ActionOptions, + ResizeToFitDeviceOptions, + AssertionOptions +} from '../../test-run/commands/options'; + + +function isCommandOptions (obj: object): boolean { + return obj instanceof ActionOptions || obj instanceof ResizeToFitDeviceOptions || obj instanceof AssertionOptions; +} export class CommandFormatter { private _elements: HTMLElement[] = []; @@ -49,8 +63,9 @@ export class CommandFormatter { private _prepareSelector (command: Command, propertyName: string): SelectorInfo { const selectorChain = command.apiFnChain as string[]; + const expression = selectorChain.join(''); - const expression = selectorChain.join(''); + const result: SelectorInfo = { expression }; let element = null; @@ -58,9 +73,12 @@ export class CommandFormatter { element = this._getElementByPropertyName(propertyName); if (element) - return { expression, element }; + result.element = element; + + if (command.timeout) + result.timeout = command.timeout; - return { expression }; + return result; } private _prepareClientFunction (command: Command): object { @@ -91,12 +109,18 @@ export class CommandFormatter { const sourceProperties = this._command._getAssignableProperties().map(prop => prop.name); sourceProperties.forEach((key: string) => { - const prop = this._command[key]; + const property = this._command[key]; - if (prop instanceof ExecuteSelectorCommand) - formattedCommand[key] = this._prepareSelector(prop, key); + if (property instanceof ExecuteSelectorCommand) + formattedCommand[key] = this._prepareSelector(property, key); + else if (isCommandOptions(property)) { + const modifiedOptions = CommandFormatter._getModifiedOptions(property); + + if (!isEmpty(modifiedOptions)) + formattedCommand[key] = modifiedOptions; + } else - formattedCommand[key] = prop; + formattedCommand[key] = property; }); } @@ -108,4 +132,11 @@ export class CommandFormatter { this._elements = Array.isArray(decoded) ? decoded : [decoded]; } + + private static _getModifiedOptions (commandOptions: object): Dictionary | null { + const constructor = commandOptions.constructor as ObjectConstructor; + const defaultOptions = new constructor(); + + return diff(defaultOptions as Dictionary, commandOptions as Dictionary); + } } diff --git a/src/reporter/command/interfaces.ts b/src/reporter/command/interfaces.ts index 2da2d943dd..ce21f0cab6 100644 --- a/src/reporter/command/interfaces.ts +++ b/src/reporter/command/interfaces.ts @@ -12,5 +12,6 @@ export interface FormattedCommand { export interface SelectorInfo { expression: string; + timeout?: number; element?: HTMLElement; } diff --git a/src/utils/diff.ts b/src/utils/diff.ts new file mode 100644 index 0000000000..6dab71d1ad --- /dev/null +++ b/src/utils/diff.ts @@ -0,0 +1,38 @@ +import { set, isObjectLike } from 'lodash'; +import { Dictionary } from '../configuration/interfaces'; + +function getFullPropertyPath (property: string, parentProperty: string): string { + if (parentProperty) + return `${parentProperty}.${property}`; + + return property; +} + +function diff (source: Dictionary, modified: Dictionary, result: Dictionary, parentProperty: string = ''): void { + for (const property in source) { + const fullPropertyPath = getFullPropertyPath(property, parentProperty); + + if (!modified.hasOwnProperty(property)) + continue; + + const sourceValue = source[property] as Dictionary; + const modifiedValue = modified[property] as Dictionary; + + if (sourceValue !== modifiedValue) { + if (isObjectLike(sourceValue) && isObjectLike(modifiedValue)) + diff(sourceValue, modifiedValue, result, fullPropertyPath); + else + set(result, fullPropertyPath, modifiedValue); + } + } +} + +export default (source: Dictionary, modified: Dictionary) => { + const result = {}; + + if (isObjectLike(source) && isObjectLike(modified)) + diff(source, modified, result); + + return result; + +}; diff --git a/test/functional/fixtures/reporter/test.js b/test/functional/fixtures/reporter/test.js index 0a967e1a15..0cbb7a9c4b 100644 --- a/test/functional/fixtures/reporter/test.js +++ b/test/functional/fixtures/reporter/test.js @@ -8,8 +8,6 @@ const { createSyncTestStream } = require('../../utils/stream'); -const { ClickOptions, AssertionOptions } = require('../../../../lib/test-run/commands/options'); - describe('Reporter', () => { const stdoutWrite = process.stdout.write; const stderrWrite = process.stderr.write; @@ -273,8 +271,10 @@ describe('Reporter', () => { action: 'done', command: { type: 'click', - options: new ClickOptions(), - selector: 'Selector(\'#target\')' + selector: 'Selector(\'#target\')', + options: { + offsetX: 10 + } }, test: { id: 'test-id', @@ -305,7 +305,6 @@ describe('Reporter', () => { action: 'done', command: { type: 'click', - options: new ClickOptions(), selector: 'Selector(\'#non-existing-target\')' }, err: 'E24' @@ -334,7 +333,9 @@ describe('Reporter', () => { expected: true, expected2: void 0, message: 'assertion message', - options: new AssertionOptions({ timeout: 100 }) + options: { + timeout: 100 + } } }, ]); @@ -360,8 +361,7 @@ describe('Reporter', () => { assertionType: 'eql', expected: 'target', expected2: void 0, - message: null, - options: new AssertionOptions() + message: null } }, ]); @@ -486,7 +486,6 @@ describe('Reporter', () => { name: 'click', action: 'done', command: { - options: new ClickOptions(), selector: 'Selector(\'#target\')', type: 'click' }, @@ -537,7 +536,6 @@ describe('Reporter', () => { name: 'click', action: 'done', command: { - options: new ClickOptions(), selector: 'Selector(\'#non-existing-element\')', type: 'click' }, @@ -585,7 +583,7 @@ describe('Reporter', () => { describe('Action snapshots', () => { it('Basic', () => { const expected = [ - { expression: 'Selector(\'#input\')', element: { tagName: 'input', attributes: { value: '100', type: 'text', id: 'input' } } }, + { expression: 'Selector(\'#input\')', timeout: 11000, element: { tagName: 'input', attributes: { value: '100', type: 'text', id: 'input' } } }, { expression: 'Selector(\'#obscuredInput\')', element: { tagName: 'div', attributes: { id: 'fixed' } } }, { expression: 'Selector(\'#obscuredInput\')', element: { tagName: 'div', attributes: { id: 'fixed' } } }, { expression: 'Selector(\'#obscuredDiv\')', element: { tagName: 'div', attributes: { id: 'obscuredDiv' } } }, diff --git a/test/functional/fixtures/reporter/testcafe-fixtures/index-test.js b/test/functional/fixtures/reporter/testcafe-fixtures/index-test.js index e63ab00018..4ca1f565a8 100644 --- a/test/functional/fixtures/reporter/testcafe-fixtures/index-test.js +++ b/test/functional/fixtures/reporter/testcafe-fixtures/index-test.js @@ -23,7 +23,7 @@ test('Simple test', async t => { }); test('Simple command test', async t => { - await t.click(Selector('#target')); + await t.click(Selector('#target'), { offsetX: 10 }); }); test('Simple command err test', async t => { diff --git a/test/functional/fixtures/reporter/testcafe-fixtures/snapshots-test.js b/test/functional/fixtures/reporter/testcafe-fixtures/snapshots-test.js index 4074a11cd2..bd09d2c498 100644 --- a/test/functional/fixtures/reporter/testcafe-fixtures/snapshots-test.js +++ b/test/functional/fixtures/reporter/testcafe-fixtures/snapshots-test.js @@ -4,7 +4,7 @@ fixture `Reporter snapshots` .page `http://localhost:3000/fixtures/reporter/pages/snapshots.html`; test('Basic', async t => { - await t.click('#input'); + await t.click(Selector('#input', { timeout: 11000 })); await t.click('#obscuredInput'); await t.dragToElement('#obscuredInput', '#obscuredDiv'); await t.selectEditableContent('#p1', '#p2'); diff --git a/test/server/data/test-controller-reporter-expected/index.js b/test/server/data/test-controller-reporter-expected/index.js index 60e09b73be..342a816fd4 100644 --- a/test/server/data/test-controller-reporter-expected/index.js +++ b/test/server/data/test-controller-reporter-expected/index.js @@ -6,7 +6,6 @@ const mouseOptions = Object.assign({ modifiers: { alt: true, ctrl: true, - meta: true, shift: true, }, offsetX: 1, @@ -16,8 +15,7 @@ const mouseOptions = Object.assign({ const clickOptions = Object.assign({ caretPos: 1 }, mouseOptions); const dragToElementOptions = Object.assign({ - destinationOffsetX: 3, - destinationOffsetY: 4 + destinationOffsetX: 3 }, mouseOptions); const typeTextOptions = Object.assign({ @@ -347,7 +345,12 @@ module.exports = [ selector: { expression:'Selector(\'#target\')' }, path: 'screenshotPath', type: 'take-element-screenshot', - options: new ElementScreenshotOptions() + options: { + includeMargins: true, + crop: { + top: -100 + } + } }, test: { id: 'test-id', diff --git a/test/server/test-controller-events-test.js b/test/server/test-controller-events-test.js index 6461afa018..467db5051c 100644 --- a/test/server/test-controller-events-test.js +++ b/test/server/test-controller-events-test.js @@ -64,16 +64,31 @@ const options = { modifiers: { alt: true, ctrl: true, - meta: true, shift: true }, offsetX: 1, offsetY: 2, destinationOffsetX: 3, - destinationOffsetY: 4, speed: 1, replace: true, - paste: true, + paste: true +}; + +const actionsWithoutOptions = { + click: ['#target'], + rightClick: ['#target'], + doubleClick: ['#target'], + hover: ['#target'], + drag: ['#target', 100, 200], + dragToElement: ['#target', '#target'], + typeText: ['#input', 'test'], + selectText: ['#input', 1, 3], + selectTextAreaContent: ['#textarea', 1, 2, 3, 4], + selectEditableContent: ['#contenteditable', '#contenteditable'], + pressKey: ['enter'], + takeScreenshot: [{ path: 'screenshotPath', fullPage: true }], + takeElementScreenshot: ['#target', 'screenshotPath'], + resizeWindowToFitDevice: ['Sony Xperia Z'] }; const actions = { @@ -93,7 +108,7 @@ const actions = { setFilesToUpload: ['#file', '../test.js'], clearUpload: ['#file'], takeScreenshot: [{ path: 'screenshotPath', fullPage: true }], - takeElementScreenshot: ['#target', 'screenshotPath'], + takeElementScreenshot: ['#target', 'screenshotPath', { includeMargins: true, crop: { top: -100 } }], resizeWindow: [200, 200], resizeWindowToFitDevice: ['Sony Xperia Z', { portraitOrientation: true }], maximizeWindow: [], @@ -225,4 +240,77 @@ describe('TestController action events', () => { expect(resultDuration).to.be.a('number').with.above(0); }); }); + + it('Default command options should not be passed to the `reportTestActionDone` method', async () => { + const log = []; + + initializeReporter({ + async reportTestActionDone (name, { command }) { + log.push(name); + + if (command.options) + log.push(command.options); + } + }, task); + + const actionsKeys = Object.keys(actionsWithoutOptions); + + for (let i = 0; i < actionsKeys.length; i++) + await testController[actionsKeys[i]].apply(testController, actionsWithoutOptions[actionsKeys[i]]); + + expect(log).eql(actionsKeys); + }); + + it('Show only modified action options', async () => { + const doneLog = []; + + initializeReporter({ + async reportTestActionDone (name, { command }) { + const item = { name }; + + if (command.options) + item.options = command.options; + + doneLog.push(item); + } + }, task); + + await testController.click('#target', { caretPos: 1, modifiers: { shift: true } }); + await testController.click('#target', { modifiers: { ctrl: false } }); + + await testController.resizeWindowToFitDevice('iPhone 5', { portraitOrientation: true }); + await testController.resizeWindowToFitDevice('iPhone 5', { portraitOrientation: false }); + + await testController.expect(true).eql(true, 'message', { timeout: 500 }); + await testController.expect(true).eql(true); + + const expectedLog = [ + { + name: 'click', + options: { + caretPos: 1, + modifiers: { + shift: true + } + } + }, + { name: 'click' }, + { + name: 'resizeWindowToFitDevice', + options: { + portraitOrientation: true + } + }, + { name: 'resizeWindowToFitDevice' }, + { + name: 'eql', + options: { + timeout: 500 + } + }, + { name: 'eql' } + ]; + + expect(doneLog).eql(expectedLog); + }); }); diff --git a/test/server/util-test.js b/test/server/util-test.js index b08d568842..f3eb568afc 100644 --- a/test/server/util-test.js +++ b/test/server/util-test.js @@ -21,6 +21,8 @@ const prepareReporters = require('../../lib/utils/prepare-report const { replaceLeadingSpacesWithNbsp } = require('../../lib/errors/test-run/utils'); const createTempProfile = require('../../lib/browser/provider/built-in/dedicated/chrome/create-temp-profile'); const parseUserAgent = require('../../lib/utils/parse-user-agent'); +const diff = require('../../lib/utils/diff'); + const { buildChromeArgs, @@ -39,6 +41,29 @@ describe('Utils', () => { expect(escapeUserAgent('Chrome 67.0.3396 / Windows 8.1.0.0')).eql('Chrome_67.0.3396_Windows_8.1.0.0'); }); + it('Diff', () => { + expect(diff(null, null)).eql({}); + expect(diff(void 0, void 0)).eql({}); + expect(diff(1, 2)).eql({}); + expect(diff({ a: void 0 }, { b: void 0 })).eql({}); + expect(diff({ a: null }, { b: null })).eql({}); + expect(diff({ a: null }, { a: 1 })).eql({ a: 1 }); + expect(diff({ a: 1 }, { a: 1 })).eql({}); + expect(diff({ a: 1 }, { a: void 0 })).eql({ a: void 0 }); + expect(diff({ a: 1 }, { a: null })).eql({ a: null }); + expect(diff({ a: 1, b: 1 }, { a: 1, b: 1 })).eql({}); + expect(diff({ a: 1, b: {} }, { a: 1, b: {} })).eql({}); + expect(diff({ a: 1, b: { c: 3 } }, { a: 1, b: { c: 3 } })).eql({}); + expect(diff({ a: 1, b: { c: { d: 4 } } }, { a: 1, b: { c: { d: 4 } } })).eql({}); + expect(diff({ a: 0 }, { a: 1 })).eql({ a: 1 }); + expect(diff({ a: 1 }, { a: 0 })).eql({ a: 0 }); + expect(diff({ a: 1 }, { a: 2 })).eql({ a: 2 }); + expect(diff({ a: 1, b: 1 }, { a: 1, b: 2 })).eql({ b: 2 }); + expect(diff({ a: 1, b: { c: 3 } }, { a: 1, b: { c: 4 } })).eql({ b: { c: 4 } }); + expect(diff({ a: 1, b: { c: 3 } }, { a: 2, b: { c: 4 } })).eql({ a: 2, b: { c: 4 } }); + expect(diff({ a: 1, b: { c: { d: 4 } } }, { a: 1, b: { c: { d: 5 } } })).eql({ b: { c: { d: 5 } } }); + }); + it('Parse user agent', () => { const expectedEmptyParsedUA = { name: 'Other',