Skip to content

Commit

Permalink
Node snapshot properties shorthands for Selector (closes DevExpress#771
Browse files Browse the repository at this point in the history
…) (DevExpress#899)

* Node snapshot properties shorthands for Selector (closes DevExpress#771)

* Fix review issues

* Remove no-var eslint rule

* Fix tests. Add yarn.lock to .gitignore
  • Loading branch information
inikulin committed Oct 24, 2016
1 parent 5adaf32 commit fc41d4b
Show file tree
Hide file tree
Showing 20 changed files with 453 additions and 196 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ site
.sass-cache
.publish
___test-screenshots___
yarn.lock
38 changes: 10 additions & 28 deletions examples/basic/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,20 +48,16 @@ test('Text typing basics', async t => {
.typeText(page.nameInput, 'Paker', { replace: true }) // Replace with last name
.typeText(page.nameInput, 'r', { caretPos: 2 }); // Correct last name

const nameInput = await page.nameInput();

// Check result
expect(nameInput.value).eql('Parker');
expect(await page.nameInput.value).eql('Parker');
});


test('Click an array of labels and then check their states', async t => {
for (const feature of page.featureList) {
await t.click(feature.label);

const checkbox = await feature.checkbox();

expect(checkbox.checked).to.be.true;
expect(await feature.checkbox.checked).to.be.true;
}
});

Expand All @@ -72,30 +68,24 @@ test('Dealing with text using keyboard', async t => {
.click(page.nameInput, { caretPos: 5 }) // Move caret position
.pressKey('backspace'); // Erase a character

let nameInput = await page.nameInput();

// Check result
expect(nameInput.value).eql('Pete Parker');
expect(await page.nameInput.value).eql('Pete Parker');

await t.pressKey('home right . delete delete delete'); // Pick even shorter form for name

nameInput = await page.nameInput();

// Check result
expect(nameInput.value).eql('P. Parker');
expect(await page.nameInput.value).eql('P. Parker');
});


test('Moving the slider', async t => {
const initialOffset = (await page.slider.handle()).offsetLeft;
const initialOffset = await page.slider.handle.offsetLeft;

await t
.click(page.triedTestCafeCheckbox)
.dragToElement(page.slider.handle, page.slider.tick.with({ text: '9' }));

const newOffset = (await page.slider.handle()).offsetLeft;

expect(newOffset).gt(initialOffset);
expect(await page.slider.handle.offsetLeft).gt(initialOffset);
});


Expand All @@ -105,10 +95,8 @@ test('Dealing with text using selection', async t => {
.selectText(page.nameInput, 7, 1)
.pressKey('delete');

const nameInput = await page.nameInput();

// Check result
expect(nameInput.value).eql('Tfe');
expect(await page.nameInput.value).eql('Tfe');
});


Expand All @@ -123,9 +111,7 @@ test('Handle native confirmation dialog', async t => {

await t.click(page.submitButton);

const results = await page.results();

expect(results.innerText).contains('Peter Parker');
expect(await page.results.innerText).contains('Peter Parker');
});


Expand All @@ -134,9 +120,7 @@ test('Pick option from select', async t => {
.click(page.interfaceSelect)
.click(page.interfaceSelectOption.with({ text: 'Both' }));

const select = await page.interfaceSelect();

expect(select.value).eql('Both');
expect(await page.interfaceSelect.value).eql('Both');
});


Expand Down Expand Up @@ -165,7 +149,5 @@ test('Filling a form', async t => {
.wait(500)
.click(page.submitButton);

const results = await page.results();

expect(results.innerText).contains('Bruce Wayne');
expect(await page.results.innerText).contains('Bruce Wayne');
});
2 changes: 1 addition & 1 deletion src/api/test-controller.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import Promise from 'pinkie';
import { identity, assign, isNil as isNullOrUndefined } from 'lodash';
import { MissingAwaitError } from '../errors/test-run';
import getCallsite from '../errors/get-callsite';
import { getCallsite } from '../errors/callsite';
import ClientFunctionBuilder from '../client-functions/client-function-builder';
import SelectorBuilder from '../client-functions/selector-builder';

Expand Down
2 changes: 1 addition & 1 deletion src/client-functions/client-function-builder.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import TestRun from '../test-run';
import compileClientFunction from '../compiler/es-next/compile-client-function';
import { APIError, ClientFunctionAPIError } from '../errors/runtime';
import MESSAGE from '../errors/runtime/message';
import getCallsite from '../errors/get-callsite';
import { getCallsite } from '../errors/callsite';

const DEFAULT_EXECUTION_CALLSITE_NAME = '__$$clientFunction$$';

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { ELEMENT_SNAPSHOT_PROPERTIES, NODE_SNAPSHOT_PROPERTIES } from './node-snapshot-properties';
import { CantObtainInfoForElementSpecifiedBySelectorError } from '../../errors/test-run';
import { getCallsite, getCallsiteForGetter } from '../../errors/callsite';

const SNAPSHOT_PROPERTIES = NODE_SNAPSHOT_PROPERTIES.concat(ELEMENT_SNAPSHOT_PROPERTIES);

async function getSnapshot (selector, callsite) {
var node = null;

try {
node = await selector();
}

catch (err) {
err.callsite = callsite;
throw err;
}

if (!node)
throw new CantObtainInfoForElementSpecifiedBySelectorError(callsite);

return node;
}

export default function createSnapshotPropertyShorthands (obj, selector) {
SNAPSHOT_PROPERTIES.forEach(prop => {
Object.defineProperty(obj, prop, {
get: async () => {
var callsite = getCallsiteForGetter();
var snapshot = await getSnapshot(selector, callsite);

return snapshot[prop];
}
});
});

obj.getStyleProperty = async prop => {
var callsite = getCallsite('getStyleProperty');
var snapshot = await getSnapshot(selector, callsite);

return snapshot.style[prop];
};

obj.getAttribute = async attrName => {
var callsite = getCallsite('getAttribute');
var snapshot = await getSnapshot(selector, callsite);

return snapshot.attributes[attrName];
};

obj.getBoundingClientRectProperty = async prop => {
var callsite = getCallsite('getBoundingClientRectProperty');
var snapshot = await getSnapshot(selector, callsite);

return snapshot.boundingClientRect[prop];
};

obj.hasClass = async name => {
var callsite = getCallsite('hasClass');
var snapshot = await getSnapshot(selector, callsite);

return snapshot.classNames ? snapshot.classNames.indexOf(name) > -1 : false;
};
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import Promise from 'pinkie';
import { isFinite, isRegExp, isNil as isNullOrUndefined, assign, escapeRegExp as escapeRe } from 'lodash';
import dedent from 'dedent';
import ClientFunctionBuilder from './client-function-builder';
import { SelectorNodeTransform } from './replicator';
import { APIError, ClientFunctionAPIError } from '../errors/runtime';
import functionBuilderSymbol from './builder-symbol';
import MESSAGE from '../errors/runtime/message';
import { ExecuteSelectorCommand } from '../test-run/commands/observation';
import defineLazyProperty from '../utils/define-lazy-property';
import ClientFunctionBuilder from '../client-function-builder';
import { SelectorNodeTransform } from '../replicator';
import { APIError, ClientFunctionAPIError } from '../../errors/runtime';
import functionBuilderSymbol from '../builder-symbol';
import MESSAGE from '../../errors/runtime/message';
import { ExecuteSelectorCommand } from '../../test-run/commands/observation';
import defineLazyProperty from '../../utils/define-lazy-property';
import createSnapshotPropertyShorthands from './create-snapshot-property-shorthands';

export default class SelectorBuilder extends ClientFunctionBuilder {
constructor (fn, options, callsiteNames) {
Expand Down Expand Up @@ -106,6 +107,9 @@ export default class SelectorBuilder extends ClientFunctionBuilder {

this._defineSelectorPropertyWithBoundArgs(lazyPromise, args);

// OPTIMIZATION: use buffer function as selector not to trigger lazy property ahead of time
createSnapshotPropertyShorthands(lazyPromise, () => lazyPromise.selector);

return lazyPromise;
}

Expand Down Expand Up @@ -187,6 +191,12 @@ export default class SelectorBuilder extends ClientFunctionBuilder {
});
}

_decorateFunction (selectorFn) {
super._decorateFunction(selectorFn);

createSnapshotPropertyShorthands(selectorFn, selectorFn);
}

_decorateFunctionResult (nodeSnapshot, selectorArgs) {
this._defineSelectorPropertyWithBoundArgs(nodeSnapshot, selectorArgs);

Expand Down Expand Up @@ -221,7 +231,6 @@ export default class SelectorBuilder extends ClientFunctionBuilder {
if (nodeSnapshot.classNames)
nodeSnapshot.hasClass = name => nodeSnapshot.classNames.indexOf(name) > -1;


return nodeSnapshot;
}
}
41 changes: 41 additions & 0 deletions src/client-functions/selector-builder/node-snapshot-properties.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// -------------------------------------------------------------
// WARNING: this file is used by both the client and the server.
// Do not use any browser or node-specific API!
// -------------------------------------------------------------

export const NODE_SNAPSHOT_PROPERTIES = [
'nodeType',
'textContent',
'childNodeCount',
'hasChildNodes',
'childElementCount',
'hasChildElements'
];

export const ELEMENT_SNAPSHOT_PROPERTIES = [
'tagName',
'visible',
'focused',
'attributes',
'boundingClientRect',
'classNames',
'style',
'innerText',
'namespaceURI',
'id',
'value',
'checked',
'selected',
'scrollWidth',
'scrollHeight',
'scrollLeft',
'scrollTop',
'offsetWidth',
'offsetHeight',
'offsetLeft',
'offsetTop',
'clientWidth',
'clientHeight',
'clientLeft',
'clientTop'
];
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import { InvalidSelectorResultError } from '../../../../../errors/test-run';
import { domUtils } from '../../../deps/testcafe-core';
import { getInnerText, getTextContent } from './sandboxed-node-properties';

// NOTE: save original ctors and methods because they may be overwritten by page code
var isArray = Array.isArray;
var Node = window.Node;
var HTMLCollection = window.HTMLCollection;
var NodeList = window.NodeList;

function isArrayOfNodes (obj) {
if (!isArray(obj))
return false;

for (var i = 0; i < obj.length; i++) {
if (!(obj[i] instanceof Node))
return false;
}

return true;
}

function hasText (node, textRe) {
// Element
if (node.nodeType === 1) {
var text = getInnerText(node);

// NOTE: In Firefox, <option> elements don't have `innerText`.
// So, we fallback to `textContent` in that case (see GH-861).
if (domUtils.isOptionElement(node)) {
var textContent = getTextContent(node);

if (!text && textContent)
text = textContent;
}

return textRe.test(text);
}

// Document
if (node.nodeType === 9) {
// NOTE: latest version of Edge doesn't have `innerText` for `document`,
// `html` and `body`. So we check their children instead.
var head = node.querySelector('head');
var body = node.querySelector('body');

return hasChildrenWithText(head, textRe) || hasChildrenWithText(body, textRe);
}

// DocumentFragment
if (node.nodeType === 11)
return hasChildrenWithText(node, textRe);

return textRe.test(getTextContent(node));
}

function hasChildrenWithText (node, textRe) {
var cnCount = node.childNodes.length;

for (var i = 0; i < cnCount; i++) {
if (hasText(node.childNodes[i], textRe))
return true;
}

return false;
}

function filterNodeCollectionByText (collection, textRe) {
var count = collection.length;
var filtered = [];

for (var i = 0; i < count; i++) {
if (hasText(collection[i], textRe))
filtered.push(collection[i]);
}

return filtered;
}


// Selector filter
Object.defineProperty(window, '%testCafeSelectorFilter%', {
value: (node, options) => {
if (node === null || node === void 0)
return node;

if (node instanceof Node) {
if (options.text)
return hasText(node, options.text) ? node : null;

return node;
}

if (node instanceof HTMLCollection || node instanceof NodeList || isArrayOfNodes(node)) {
if (options.text)
node = filterNodeCollectionByText(node, options.text);

return node[options.index];
}

throw new InvalidSelectorResultError();
}
});
Loading

0 comments on commit fc41d4b

Please sign in to comment.