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

Implemented DOM Node extensions mechanism (closes #749) #795

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
51 changes: 40 additions & 11 deletions src/client-functions/selector-builder/add-api.js
@@ -1,16 +1,18 @@
import { assign } from 'lodash';
import { isNil as isNullOrUndefined, assign } from 'lodash';
import clientFunctionBuilderSymbol from '../builder-symbol';
import { ELEMENT_SNAPSHOT_PROPERTIES, NODE_SNAPSHOT_PROPERTIES } from './snapshot-properties';
import { CantObtainInfoForElementSpecifiedBySelectorError } from '../../errors/test-run';
import getCallsite from '../../errors/get-callsite';
import ClientFunctionBuilder from '../client-function-builder';
import ClientFunctionResultPromise from '../result-promise';
import {
assertStringOrRegExp,
assertNumber,
assertFunction,
assertFunctionOrString,
assertFunctionOrStringOrNumber
} from '../../errors/runtime/type-assertions';
assertFunctionOrStringOrNumber,
assertStringOrRegExp,
assertObject
} from '../../errors/runtime/type-assertions';

const SNAPSHOT_PROPERTIES = NODE_SNAPSHOT_PROPERTIES.concat(ELEMENT_SNAPSHOT_PROPERTIES);

Expand Down Expand Up @@ -88,8 +90,23 @@ async function getSnapshot (getSelector, callsite) {
return node;
}

function addSnapshotPropertyShorthands (obj, getSelector) {
SNAPSHOT_PROPERTIES.forEach(prop => {
function assertAddCustomDOMPropertiesOptions (properties) {
if (!isNullOrUndefined(properties)) {
assertObject('addCustomDOMProperties', '"addCustomDOMProperties" option', properties);

Object.keys(properties).forEach(prop => {
assertFunction('addCustomDOMProperties', `Custom DOM properties method '${prop}'`, properties[prop]);
});
}
}

function addSnapshotPropertyShorthands (obj, getSelector, customDOMProperties) {
var properties = SNAPSHOT_PROPERTIES;

if (customDOMProperties)
properties = properties.concat(Object.keys(customDOMProperties));

properties.forEach(prop => {
Object.defineProperty(obj, prop, {
get: () => {
var callsite = getCallsite('get');
Expand Down Expand Up @@ -193,6 +210,7 @@ function convertFilterToClientFunctionIfNecessary (callsiteName, filter, depende

function createDerivativeSelectorWithFilter (getSelector, SelectorBuilder, selectorFn, filter, additionalDependencies) {
var collectionModeSelectorBuilder = new SelectorBuilder(getSelector(), { collectionMode: true });
var customDOMProperties = collectionModeSelectorBuilder.options.customDOMProperties;

var dependencies = {
selector: collectionModeSelectorBuilder.getFunction(),
Expand All @@ -202,7 +220,7 @@ function createDerivativeSelectorWithFilter (getSelector, SelectorBuilder, selec

dependencies = assign(dependencies, additionalDependencies);

var builder = new SelectorBuilder(selectorFn, { dependencies }, { instantiation: 'Selector' });
var builder = new SelectorBuilder(selectorFn, { dependencies, customDOMProperties }, { instantiation: 'Selector' });

return builder.getFunction();
}
Expand Down Expand Up @@ -244,7 +262,17 @@ function addFilterMethods (obj, getSelector, SelectorBuilder) {
};
}

function addHierachicalSelectors (obj, getSelector, SelectorBuilder) {
function addCustomDOMPropertiesMethod (obj, getSelector, SelectorBuilder) {
obj.addCustomDOMProperties = properties => {
assertAddCustomDOMPropertiesOptions(properties);

var builder = new SelectorBuilder(getSelector(), { customDOMProperties: properties }, { instantiation: 'Selector' });

return builder.getFunction();
};
}

function addHierarchicalSelectors (obj, getSelector, SelectorBuilder) {
// Find
obj.find = (filter, dependencies) => {
assertFunctionOrString('find', '"filter" argument', filter);
Expand Down Expand Up @@ -369,9 +397,10 @@ function addHierachicalSelectors (obj, getSelector, SelectorBuilder) {
};
}

export default function addAPI (obj, getSelector, SelectorBuilder) {
addSnapshotPropertyShorthands(obj, getSelector);
export default function addAPI (obj, getSelector, SelectorBuilder, customDOMProperties) {
addSnapshotPropertyShorthands(obj, getSelector, customDOMProperties);
addCustomDOMPropertiesMethod(obj, getSelector, SelectorBuilder);
addFilterMethods(obj, getSelector, SelectorBuilder);
addHierachicalSelectors(obj, getSelector, SelectorBuilder);
addHierarchicalSelectors(obj, getSelector, SelectorBuilder);
addCounterProperties(obj, getSelector, SelectorBuilder);
}
19 changes: 10 additions & 9 deletions src/client-functions/selector-builder/index.js
@@ -1,4 +1,4 @@
import { isNil as isNullOrUndefined, assign, escapeRegExp as escapeRe } from 'lodash';
import { isNil as isNullOrUndefined, merge, escapeRegExp as escapeRe } from 'lodash';
import dedent from 'dedent';
import ClientFunctionBuilder from '../client-function-builder';
import { SelectorNodeTransform } from '../replicator';
Expand Down Expand Up @@ -30,7 +30,7 @@ export default class SelectorBuilder extends ClientFunctionBuilder {
fn = builder.fn;

if (options === void 0 || typeof options === 'object')
options = assign({}, builder.options, options, { sourceSelectorBuilder: builder });
options = merge({}, builder.options, options, { sourceSelectorBuilder: builder });
}

super(fn, options, callsiteNames);
Expand Down Expand Up @@ -78,7 +78,6 @@ export default class SelectorBuilder extends ClientFunctionBuilder {
`(function(){return document.querySelectorAll(${JSON.stringify(this.fn)});});` :
super._getCompiledFnCode();


if (code) {
return dedent(
`(function(){
Expand All @@ -104,27 +103,29 @@ export default class SelectorBuilder extends ClientFunctionBuilder {
this._addBoundArgsSelectorGetter(resultPromise, args);

// OPTIMIZATION: use buffer function as selector not to trigger lazy property ahead of time
addAPI(resultPromise, () => resultPromise.selector, SelectorBuilder);
addAPI(resultPromise, () => resultPromise.selector, SelectorBuilder, this.options.customDOMProperties);

return resultPromise;
}

getFunctionDependencies () {
var dependencies = super.getFunctionDependencies();
var text = this.options.text;
var dependencies = super.getFunctionDependencies();
var text = this.options.text;
var customDOMProperties = this.options.customDOMProperties;

if (typeof text === 'string')
text = new RegExp(escapeRe(text));

return assign({}, dependencies, {
return merge({}, dependencies, {
filterOptions: {
counterMode: this.options.counterMode,
collectionMode: this.options.collectionMode,
index: isNullOrUndefined(this.options.index) ? null : this.options.index,
text: text
},

boundArgs: this.options.boundArgs
boundArgs: this.options.boundArgs,
customDOMProperties: customDOMProperties
});
}

Expand Down Expand Up @@ -174,7 +175,7 @@ export default class SelectorBuilder extends ClientFunctionBuilder {
_decorateFunction (selectorFn) {
super._decorateFunction(selectorFn);

addAPI(selectorFn, () => selectorFn, SelectorBuilder);
addAPI(selectorFn, () => selectorFn, SelectorBuilder, this.options.customDOMProperties);
}

_processResult (result, selectorArgs) {
Expand Down
Expand Up @@ -43,7 +43,7 @@ export default class ClientFunctionExecutor {
});
}

// Overridable methods
//Overridable methods
_createReplicator () {
return createReplicator([
new ClientFunctionNodeTransform(this.command.instantiationCallsiteName),
Expand Down
24 changes: 20 additions & 4 deletions src/client/driver/command-executors/client-functions/replicator.js
@@ -1,7 +1,7 @@
import Replicator from 'replicator';
import evalFunction from './eval-function';
import { NodeSnapshot, ElementSnapshot } from './selector-executor/node-snapshots';
import { DomNodeClientFunctionResultError } from '../../../../errors/test-run';
import { DomNodeClientFunctionResultError, UncaughtErrorInCustomDOMPropertyCode } from '../../../../errors/test-run';

// NOTE: save original ctors because they may be overwritten by page code
var Node = window.Node;
Expand Down Expand Up @@ -39,16 +39,32 @@ export class FunctionTransform {
}

export class SelectorNodeTransform {
constructor () {
this.type = 'Node';
constructor (customDOMProperties) {
this.type = 'Node';
this.customDOMProperties = customDOMProperties || {};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Formatting issue, assignments should be aligned

}

_extend (snapshot, node) {
Object.keys(this.customDOMProperties).forEach(prop => {
try {
snapshot[prop] = this.customDOMProperties[prop](node);
}
catch (err) {
throw new UncaughtErrorInCustomDOMPropertyCode(this.instantiationCallsiteName, err, prop);
}
});
}

shouldTransform (type, val) {
return val instanceof Node;
}

toSerializable (node) {
return node.nodeType === 1 ? new ElementSnapshot(node) : new NodeSnapshot(node);
var snapshot = node.nodeType === 1 ? new ElementSnapshot(node) : new NodeSnapshot(node);

this._extend(snapshot, node);

return snapshot;
}
}

Expand Down
Expand Up @@ -38,11 +38,14 @@ export default class SelectorExecutor extends ClientFunctionExecutor {

this.timeout = Math.max(this.timeout - elapsed, 0);
}

var customDOMProperties = this.dependencies && this.dependencies.customDOMProperties;

this.replicator.addTransforms([new SelectorNodeTransform(customDOMProperties)]);
}

_createReplicator () {
return createReplicator([
new SelectorNodeTransform(),
new FunctionTransform()
]);
}
Expand Down
1 change: 0 additions & 1 deletion src/errors/runtime/message.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 10 additions & 0 deletions src/errors/test-run/index.js
Expand Up @@ -127,6 +127,16 @@ export class UncaughtErrorInClientFunctionCode extends TestRunErrorBase {
}
}

export class UncaughtErrorInCustomDOMPropertyCode extends TestRunErrorBase {
constructor (instantiationCallsiteName, err, prop) {
super(TYPE.uncaughtErrorInCustomDOMPropertyCode, err, prop);

this.errMsg = String(err);
this.property = prop;
this.instantiationCallsiteName = instantiationCallsiteName;
}
}


// Assertion errors
//--------------------------------------------------------------------
Expand Down
6 changes: 6 additions & 0 deletions src/errors/test-run/templates.js
Expand Up @@ -79,6 +79,12 @@ export default {
${escapeHtml(err.errMsg)}
`),

[TYPE.uncaughtErrorInCustomDOMPropertyCode]: err => markup(err, `
An error occurred when trying to calculate a custom Selector property "${err.property}":

${escapeHtml(err.errMsg)}
`),

[TYPE.clientFunctionExecutionInterruptionError]: err => markup(err, `
${err.instantiationCallsiteName} execution was interrupted by page unload. This problem may appear if you trigger page navigation from ${err.instantiationCallsiteName} code.
`),
Expand Down
1 change: 1 addition & 0 deletions src/errors/test-run/type.js
Expand Up @@ -8,6 +8,7 @@ export default {
uncaughtErrorInTestCode: 'uncaughtErrorInTestCode',
uncaughtNonErrorObjectInTestCode: 'uncaughtNonErrorObjectInTestCode',
uncaughtErrorInClientFunctionCode: 'uncaughtErrorInClientFunctionCode',
uncaughtErrorInCustomDOMPropertyCode: 'uncaughtErrorInCustomDOMPropertyCode',
missingAwaitError: 'missingAwaitError',
actionIntegerOptionError: 'actionIntegerOptionError',
actionPositiveIntegerOptionError: 'actionPositiveIntegerOptionError',
Expand Down
49 changes: 49 additions & 0 deletions test/functional/fixtures/api/es-next/selector/test.js
Expand Up @@ -61,6 +61,10 @@ describe('[API] Selector', function () {
return runTests('./testcafe-fixtures/selector-test.js', 'Snapshot `hasClass` method');
});

it('Should provide "extend" method in node snapshot', function () {
return runTests('./testcafe-fixtures/selector-test.js', 'Selector `extend` method');
});

it('Should wait for element to appear on new page', function () {
return runTests('./testcafe-fixtures/selector-test.js', 'Element on new page');
});
Expand Down Expand Up @@ -238,6 +242,51 @@ describe('[API] Selector', function () {
expect(errs[0]).contains('> 853 | await Selector(() => [].someUndefMethod()).exists;');
});
});

it('Should raise error if addCustomDOMProperties method argument is not object',
function () {
return runTests('./testcafe-fixtures/selector-test.js', 'Add custom DOM properties method - argument is not object', {
shouldFail: true,
only: 'chrome'
})
.catch(function (errs) {
expect(errs[0]).contains(
'"addCustomDOMProperties" option is expected to be an object, but it was number.'
);
expect(errs[0]).contains("> 938 | await Selector('rect').addCustomDOMProperties(42);");
});
}
);

it('Should raise error if at least one of custom DOM properties is not function',
function () {
return runTests('./testcafe-fixtures/selector-test.js', 'Add custom DOM properties method - property is not function', {
shouldFail: true,
only: 'chrome'
})
.catch(function (errs) {
expect(errs[0]).contains(
"Custom DOM properties method \'prop1\' is expected to be a string or a function, but it was number"
);
expect(errs[0]).contains("> 942 | await Selector('rect').addCustomDOMProperties({ prop1: 1, prop2: () => 42 });");
});
}
);

it('Should raise error if custom DOM property throws an error',
function () {
return runTests('./testcafe-fixtures/selector-test.js', 'Add custom DOM properties method - property throws an error', {
shouldFail: true,
only: 'chrome'
})
.catch(function (errs) {
expect(errs[0]).contains(
'An error occurred when trying to calculate a custom Selector property "prop": Error: test'
);
expect(errs[0]).contains('> 952 | await el();');
});
}
);
});

describe('Regression', function () {
Expand Down