diff --git a/addon/src/-private/action.js b/addon/src/-private/action.js
index d1afabf1..0fccfed5 100644
--- a/addon/src/-private/action.js
+++ b/addon/src/-private/action.js
@@ -1,44 +1,35 @@
import { getter } from '../macros/index';
-import { buildSelector } from './build-selector';
+import { Query } from './query';
import { run } from './run';
-export default function action(options, cb) {
+export default function action(options, callback) {
return getter(function (key) {
return function (...args) {
- ({ options, cb } = normalizeArgs(key, options, cb, args));
-
- const node = this;
- return run(
- {
- node,
- filters: {
- ...options,
- },
- },
- () => {
- return cb.bind(this)(...args);
- }
+ const { locator, cb, label } = normalizeArgs(
+ key,
+ options,
+ callback,
+ args
);
+
+ const query = new Query(this, locator);
+
+ return run(query, label, () => {
+ return cb.bind(this)(...args);
+ });
};
});
}
-function normalizeArgs(key, options, cb, args) {
- const formattedKey = `${key}(${
+function normalizeArgs(key, locator, cb, args) {
+ const label = `${key}(${
args.length ? `"${args.map((a) => String(a)).join('", "')}"` : ``
})`;
- if (typeof options === 'function') {
- cb = options;
- options = {
- pageObjectKey: formattedKey,
- };
- } else {
- options = {
- ...options,
- pageObjectKey: formattedKey,
- };
+ if (typeof locator === 'function') {
+ cb = locator;
+ locator = {};
}
- return { options, cb };
+ return { locator, cb, label };
}
diff --git a/addon/src/-private/better-errors.js b/addon/src/-private/better-errors.js
index 333685fa..ebb260a8 100644
--- a/addon/src/-private/better-errors.js
+++ b/addon/src/-private/better-errors.js
@@ -1,14 +1,11 @@
import Ceibo from '@ro0gr/ceibo';
-import { buildSelector } from './build-selector';
export const ELEMENT_NOT_FOUND = 'Element not found.';
-export function throwContextualError(context, e) {
- const { filters, node } = context;
-
- const selector = buildSelector(node, filters.selector, filters);
-
- throwBetterError(node, filters.pageObjectKey, e, { selector });
+export function throwQueryError(query, label, e) {
+ throwBetterError(query.node, label, e, {
+ selector: query.toString(),
+ });
}
/**
diff --git a/addon/src/-private/build-selector.js b/addon/src/-private/build-selector.js
index 54d2f91e..8aa56d47 100644
--- a/addon/src/-private/build-selector.js
+++ b/addon/src/-private/build-selector.js
@@ -1,5 +1,4 @@
-import Ceibo from '@ro0gr/ceibo';
-import deprecate from './deprecate';
+import { Query } from './query';
/**
* @public
@@ -46,89 +45,10 @@ import deprecate from './deprecate';
*/
export function buildSelector(node, targetSelector, options) {
- return new Selector(node, options.scope, targetSelector, options).toString();
-}
-
-export class Selector {
- constructor(node, scope, selector, filters) {
- this.targetNode = node;
- this.targetScope = scope || '';
- this.targetSelector = selector || '';
- this.targetFilters = filters;
- }
-
- toString() {
- let scope;
- let filters;
-
- if (this.targetFilters.resetScope) {
- scope = this.targetScope;
- } else {
- scope = this.calculateScope(this.targetNode, this.targetScope);
- }
-
- if (`${scope} ${this.targetSelector}`.indexOf(',') > -1) {
- deprecate(
- 'comma-separated-selectors',
- 'Usage of comma separated selectors is deprecated in ember-cli-page-object',
- '1.16.0',
- '2.0.0'
- );
- }
-
- filters = this.calculateFilters(this.targetFilters);
-
- let selector = `${scope} ${this.targetSelector}${filters}`.trim();
-
- if (!selector.length) {
- // When an empty selector is resolved take the first direct child of the
- // testing container.
- selector = ':first';
- }
-
- return selector;
- }
-
- calculateFilters() {
- let filters = [];
-
- if (this.targetFilters.visible) {
- filters.push(`:visible`);
- }
-
- if (this.targetFilters.contains) {
- filters.push(`:contains("${this.targetFilters.contains}")`);
- }
-
- if (typeof this.targetFilters.at === 'number') {
- filters.push(`:eq(${this.targetFilters.at})`);
- } else if (this.targetFilters.last) {
- filters.push(':last');
- }
-
- return filters.join('');
- }
-
- calculateScope(node, targetScope) {
- let scopes = this.getScopes(node);
-
- scopes.reverse();
- scopes.push(targetScope);
-
- return scopes.join(' ').trim();
- }
-
- getScopes(node) {
- let scopes = [];
-
- if (node.scope) {
- scopes.push(node.scope);
- }
-
- if (!node.resetScope && Ceibo.parent(node)) {
- scopes = scopes.concat(this.calculateScope(Ceibo.parent(node)));
- }
+ const q = new Query(node, {
+ ...options,
+ selector: targetSelector,
+ });
- return scopes;
- }
+ return q.toString();
}
diff --git a/addon/src/-private/element.js b/addon/src/-private/element.js
index 63492695..176b348f 100644
--- a/addon/src/-private/element.js
+++ b/addon/src/-private/element.js
@@ -16,3 +16,11 @@ export function isVisible(element) {
element.getClientRects().length
);
}
+
+export function text(element) {
+ return element.textContent;
+}
+
+export function containsText(element, searchText) {
+ return text(element).indexOf(searchText) > -1;
+}
diff --git a/addon/src/-private/finders.js b/addon/src/-private/finders.js
index 296882b0..f6df5ece 100644
--- a/addon/src/-private/finders.js
+++ b/addon/src/-private/finders.js
@@ -1,31 +1,24 @@
-import { $, findClosestValue, guardMultiple } from './helpers';
-import { getAdapter } from '../adapters/index';
-import { buildSelector } from './build-selector';
-import { throwBetterError, ELEMENT_NOT_FOUND } from './better-errors';
-
-function getContainer(pageObjectNode, options) {
- return (
- options.testContainer ||
- findClosestValue(pageObjectNode, 'testContainer') ||
- getAdapter().testContainer
- );
-}
+import { $, guardMultiple } from './helpers';
+import { throwQueryError, ELEMENT_NOT_FOUND } from './better-errors';
+import { Query } from './query';
/**
* Finds a single element, otherwise fails
*
* @private
*/
-export function findOne(pageObjectNode, targetSelector, options = {}) {
- const elements = findMany(pageObjectNode, targetSelector, options);
+export function findOne(pageObjectNode, selector, options = {}) {
+ const query = new Query(pageObjectNode, {
+ ...options,
+ selector,
+ });
+
+ const elements = query.all();
- const selector = buildSelector(pageObjectNode, targetSelector, options);
- guardMultiple(elements, selector);
+ guardMultiple(elements, query);
if (elements.length === 0) {
- throwBetterError(pageObjectNode, options.pageObjectKey, ELEMENT_NOT_FOUND, {
- selector,
- });
+ throwQueryError(query, options.pageObjectKey, ELEMENT_NOT_FOUND);
}
return elements[0];
@@ -36,33 +29,31 @@ export function findOne(pageObjectNode, targetSelector, options = {}) {
*
* @private
*/
-export function findMany(pageObjectNode, targetSelector, options = {}) {
- const selector = buildSelector(pageObjectNode, targetSelector, options);
- const container = getContainer(pageObjectNode, options);
+export function findMany(pageObjectNode, selector, options = {}) {
+ const query = new Query(pageObjectNode, {
+ ...options,
+ selector,
+ });
- return $(selector, container).toArray();
+ return query.all();
}
/**
* @private
* @deprecated
*/
-export function findElementWithAssert(
- pageObjectNode,
- targetSelector,
- options = {}
-) {
- const selector = buildSelector(pageObjectNode, targetSelector, options);
- const container = getContainer(pageObjectNode, options);
+export function findElementWithAssert(pageObjectNode, selector, options = {}) {
+ const query = new Query(pageObjectNode, {
+ ...options,
+ selector,
+ });
- let $elements = $(selector, container);
+ let $elements = $(query.all());
- guardMultiple($elements, selector, options.multiple);
+ guardMultiple($elements, query, options.multiple);
if ($elements.length === 0) {
- throwBetterError(pageObjectNode, options.pageObjectKey, ELEMENT_NOT_FOUND, {
- selector,
- });
+ throwQueryError(query, options.pageObjectKey, ELEMENT_NOT_FOUND);
}
return $elements;
@@ -72,13 +63,15 @@ export function findElementWithAssert(
* @private
* @deprecated
*/
-export function findElement(pageObjectNode, targetSelector, options = {}) {
- const selector = buildSelector(pageObjectNode, targetSelector, options);
- const container = getContainer(pageObjectNode, options);
+export function findElement(pageObjectNode, selector, options = {}) {
+ const query = new Query(pageObjectNode, {
+ ...options,
+ selector,
+ });
- let $elements = $(selector, container);
+ let $elements = $(query.all());
- guardMultiple($elements, selector, options.multiple);
+ guardMultiple($elements, query, options.multiple);
return $elements;
}
diff --git a/addon/src/-private/helpers.js b/addon/src/-private/helpers.js
index 00c9282b..07f489c6 100644
--- a/addon/src/-private/helpers.js
+++ b/addon/src/-private/helpers.js
@@ -15,10 +15,7 @@ if (macroCondition(dependencySatisfies('@ember/jquery', '*'))) {
}
export { jQuery as $ };
-
-function isPresent(value) {
- return typeof value !== 'undefined';
-}
+import { Query } from './query';
export function guardMultiple(items, selector, supportMultiple) {
if (items.length > 1 && !supportMultiple) {
@@ -48,21 +45,6 @@ export function getRoot(node) {
return root;
}
-function getAllValuesForProperty(node, property) {
- let iterator = node;
- let values = [];
-
- while (isPresent(iterator)) {
- if (isPresent(iterator[property])) {
- values.push(iterator[property]);
- }
-
- iterator = Ceibo.parent(iterator);
- }
-
- return values;
-}
-
/**
* @public
*
@@ -72,29 +54,7 @@ function getAllValuesForProperty(node, property) {
* @return {string} Full scope of node
*/
export function fullScope(node) {
- let scopes = getAllValuesForProperty(node, 'scope');
+ const q = new Query(node);
- return scopes.reverse().join(' ');
-}
-
-/**
- * @public
- *
- * Returns the value of property defined on the closest ancestor of given
- * node.
- *
- * @param {Ceibo} node - Node of the tree
- * @param {string} property - Property to look for
- * @return {?Object} The value of property on closest node to the given node
- */
-export function findClosestValue(node, property) {
- if (typeof node[property] !== 'undefined') {
- return node[property];
- }
-
- let parent = Ceibo.parent(node);
-
- if (typeof parent !== 'undefined') {
- return findClosestValue(parent, property);
- }
+ return q.toString();
}
diff --git a/addon/src/-private/query.js b/addon/src/-private/query.js
new file mode 100644
index 00000000..b9f38f1f
--- /dev/null
+++ b/addon/src/-private/query.js
@@ -0,0 +1,288 @@
+import Ceibo from '@ro0gr/ceibo';
+import { getAdapter } from '../adapters/index';
+import { $ } from './helpers';
+import { isVisible, containsText } from './element';
+import deprecate from './deprecate';
+
+/**
+ * @public
+ *
+ * Builds a CSS selector from a target selector and a PageObject or a node in a PageObject, along with optional parameters.
+ *
+ * @example
+ *
+ * const component = PageObject.create({ scope: '.component'});
+ *
+ * buildSelector(component, '.my-element');
+ * // returns '.component .my-element'
+ *
+ * @example
+ *
+ * const page = PageObject.create({});
+ *
+ * buildSelector(page, '.my-element', { at: 0 });
+ * // returns '.my-element:eq(0)'
+ *
+ * @example
+ *
+ * const page = PageObject.create({});
+ *
+ * buildSelector(page, '.my-element', { contains: "Example" });
+ * // returns ".my-element :contains('Example')"
+ *
+ * @example
+ *
+ * const page = PageObject.create({});
+ *
+ * buildSelector(page, '.my-element', { last: true });
+ * // returns '.my-element:last'
+ *
+ * @param {Ceibo} node - Node of the tree
+ * @param {string} targetSelector - CSS selector
+ * @param {Object} options - Additional options
+ * @param {boolean} options.resetScope - Do not use inherited scope
+ * @param {string} options.contains - Filter by using :contains('foo') pseudo-class
+ * @param {number} options.at - Filter by index using :eq(x) pseudo-class
+ * @param {boolean} options.last - Filter by using :last pseudo-class
+ * @param {boolean} options.visible - Filter by using :visible pseudo-class
+ * @return {string} Fully qualified selector
+ *
+ * @todo: update doc
+ */
+export class Query {
+ constructor(node, locator) {
+ this.node = node;
+
+ this.selector = new Selector(node, locator);
+ }
+
+ all() {
+ const elements = search(this.selector);
+
+ // filters
+ const { visible, contains } = this.selector.locator;
+ let filteredElements = elements.filter((element) => {
+ if (visible && !isVisible(element)) {
+ return false;
+ }
+
+ if (contains && !containsText(element, contains)) {
+ return false;
+ }
+
+ return true;
+ });
+
+ // pick by index if specified
+ const { at, last } = this.selector.locator;
+ return (
+ last
+ ? [filteredElements.pop()]
+ : typeof at === 'number'
+ ? [filteredElements[at]]
+ : filteredElements
+ ).filter(Boolean);
+ }
+
+ // @todo: tests for filters via findOne
+ toString() {
+ return this.selector.toString();
+ }
+}
+
+class Selector {
+ constructor(node, locator) {
+ this.node = node;
+
+ if (locator) {
+ this.locator =
+ typeof locator === 'string' ? { selector: locator } : locator;
+ }
+ }
+
+ get container() {
+ return (
+ (this.locator && this.locator.testContainer) ||
+ findClosestValue(this.node, 'testContainer') ||
+ getAdapter().testContainer
+ );
+ }
+
+ get path() {
+ const { locator } = this;
+
+ const wayBackToRoot = [
+ locator && {
+ scope: [locator.scope, locator.selector].filter(Boolean).join(' '),
+ resetScope: locator.resetScope,
+ },
+
+ ...mapToRoot(this.node, (n) => {
+ return {
+ scope: n.scope,
+ resetScope: n.resetScope,
+ };
+ }),
+ ].filter((n) => n && Boolean(n.scope));
+
+ const startIndex = wayBackToRoot.findIndex((node) => node.resetScope);
+ const breadcrumbs =
+ startIndex > -1 ? wayBackToRoot.slice(0, startIndex + 1) : wayBackToRoot;
+
+ const path = breadcrumbs
+ .reverse()
+ .map((n) => n.scope)
+ .map((locator) => {
+ if (typeof locator === 'string') {
+ return { selector: locator };
+ } else {
+ return locator;
+ }
+ })
+ .reduce((batches, locator) => {
+ const [currentBatch] = batches.slice(-1);
+
+ if (
+ !currentBatch ||
+ typeof currentBatch[0].at === 'number' ||
+ typeof locator.at === 'number'
+ ) {
+ batches.push([locator]);
+ } else {
+ currentBatch.push(locator);
+ }
+
+ return batches;
+ }, []);
+
+ return path.length
+ ? path
+ : [
+ [
+ {
+ selector: ':first-child',
+ at: 0,
+ },
+ ],
+ ];
+ }
+
+ toString() {
+ const { locator } = this;
+ const modifiers = [];
+ if (locator) {
+ if (typeof locator.at === 'number') {
+ modifiers.push(`eq(${locator.at})`);
+ } else if (locator.last) {
+ modifiers.push('last');
+ }
+
+ if (locator.visible) {
+ modifiers.push(`visible`);
+ }
+
+ if (locator.contains) {
+ modifiers.push(`contains("${locator.contains}")`);
+ }
+ }
+
+ const pathSelector = this.path
+ .map((subpath) => {
+ return subpath
+ .map((locator) => {
+ if (typeof locator.at === 'number') {
+ return `${locator.selector}:eq(${locator.at})`;
+ }
+
+ return locator.selector;
+ })
+ .join(' ');
+ })
+ .join(' ');
+
+ return modifiers.length
+ ? `${pathSelector}:${modifiers.join(':')}`
+ : pathSelector;
+ }
+}
+
+function getQueryEngine() {
+ return JQueryQueryEngine;
+}
+
+class JQueryQueryEngine {
+ static all(path, containerElement) {
+ const selector = path.join(' ');
+
+ validate(selector);
+ return $(selector, containerElement).toArray();
+ }
+
+ static serialize(path) {
+ return path.join(' ');
+ }
+}
+
+function validate(selector) {
+ if (selector.indexOf(',') > -1) {
+ deprecate(
+ 'comma-separated-selectors',
+ 'Usage of comma separated selectors is deprecated in ember-cli-page-object',
+ '1.16.0',
+ '2.0.0'
+ );
+ }
+}
+
+function mapToRoot(node, mapper) {
+ let iterator = node;
+ let values = [];
+
+ while (typeof iterator !== 'undefined') {
+ values.push(mapper(iterator));
+
+ iterator = Ceibo.parent(iterator);
+ }
+
+ return values;
+}
+
+function findClosestValue(node, property) {
+ if (typeof node[property] !== 'undefined') {
+ return node[property];
+ }
+
+ let parent = Ceibo.parent(node);
+
+ if (typeof parent !== 'undefined') {
+ return findClosestValue(parent, property);
+ }
+}
+
+function search(selector) {
+ const { path, container } = selector;
+
+ return path
+ .reduce(
+ (queryRootElements, subpath) => {
+ return (
+ queryRootElements
+ .map((root) => {
+ const selectors = subpath.map((locator) => locator.selector);
+
+ const elements = getQueryEngine().all(selectors, root);
+
+ const { at } = subpath[0];
+
+ return typeof at === 'number' ? elements[at] : elements;
+ })
+ // IE compatibility for `Array.prototype.flat()`
+ .reduce((flattened, batchResults) => {
+ return flattened.concat(batchResults);
+ }, [])
+ );
+ },
+ [container]
+ )
+ .filter(Boolean);
+}
diff --git a/addon/src/-private/run.js b/addon/src/-private/run.js
index be636e20..3f36992e 100644
--- a/addon/src/-private/run.js
+++ b/addon/src/-private/run.js
@@ -1,6 +1,6 @@
import { resolve } from 'rsvp';
import { getRoot } from './helpers';
-import { throwContextualError } from './better-errors';
+import { throwQueryError } from './better-errors';
import { chainable, isChainedNode } from './chainable';
/**
@@ -11,7 +11,7 @@ import { chainable, isChainedNode } from './chainable';
* @param {Function} cb Some async activity callback
* @returns {Ceibo}
*/
-export function run(query, cb) {
+export function run(query, label, cb) {
const { node } = query;
const root = getRoot(node);
@@ -19,28 +19,30 @@ export function run(query, cb) {
// Our root is already the root of the chained tree,
// we need to wait on its promise if it has one so the
// previous invocations can resolve before we run ours.
- root._promise = resolve(root._promise).then(() => invokeHelper(query, cb));
+ root._promise = resolve(root._promise).then(() =>
+ invokeHelper(query, label, cb)
+ );
return node;
} else {
// Store our invocation result on the chained root
// so that chained calls can find it to wait on it.
- root._chainedTree._promise = invokeHelper(query, cb);
+ root._chainedTree._promise = invokeHelper(query, label, cb);
return chainable(node);
}
}
-function invokeHelper(query, cb) {
+function invokeHelper(query, label, cb) {
let res;
try {
res = cb();
} catch (e) {
- throwContextualError(query, e);
+ throwQueryError(query, label, e);
}
- return Promise.resolve(res).catch((e) => {
- throwContextualError(query, e);
+ return resolve(res).catch((e) => {
+ throwQueryError(query, label, e);
});
}
diff --git a/addon/src/properties/collection.js b/addon/src/properties/collection.js
index 6552bf69..6e436939 100644
--- a/addon/src/properties/collection.js
+++ b/addon/src/properties/collection.js
@@ -1,9 +1,9 @@
import Ceibo from '@ro0gr/ceibo';
-import { buildSelector } from '../-private/build-selector';
import { isPageObject, getPageObjectDefinition } from '../-private/meta';
import { create } from '../create';
import { count } from './count';
import { throwBetterError } from '../-private/better-errors';
+import { getter } from '../macros/index';
/**
* Creates a enumerable that represents a collection of items. The collection is zero-indexed
@@ -177,6 +177,7 @@ export class Collection {
this._itemCounter = create(
{
+ // @todo: use locator
count: count(scope, {
resetScope: this.definition.resetScope,
testContainer: this.definition.testContainer,
@@ -197,11 +198,15 @@ export class Collection {
if (typeof this._items[index] === 'undefined') {
let { scope, definition, parent } = this;
- let itemScope = buildSelector({}, scope, { at: index });
let finalizedDefinition = { ...definition };
- finalizedDefinition.scope = itemScope;
+ finalizedDefinition.scope = getter(function () {
+ return {
+ selector: scope,
+ at: index,
+ };
+ });
let tree = create(finalizedDefinition, { parent });
diff --git a/addon/src/properties/contains.js b/addon/src/properties/contains.js
index 227fee8e..691b4394 100644
--- a/addon/src/properties/contains.js
+++ b/addon/src/properties/contains.js
@@ -1,4 +1,4 @@
-import { $ } from '../-private/helpers';
+import { containsText } from '../-private/element';
import { findOne } from '../-private/finders';
import { getter } from '../macros/index';
@@ -69,9 +69,9 @@ export function contains(selector, userOptions = {}) {
...userOptions,
};
- return (
- $(findOne(this, selector, options)).text().indexOf(textToSearch) > -1
- );
+ const element = findOne(this, selector, options);
+
+ return containsText(element, textToSearch);
};
});
}
diff --git a/addon/src/properties/text.js b/addon/src/properties/text.js
index 7dc209fe..d095a9de 100644
--- a/addon/src/properties/text.js
+++ b/addon/src/properties/text.js
@@ -1,6 +1,6 @@
-import { $ } from '../-private/helpers';
import { findOne } from '../-private/finders';
import { getter } from '../macros/index';
+import { text as textContent } from '../-private/element';
function identity(v) {
return v;
@@ -93,7 +93,7 @@ export function text(selector, userOptions = {}) {
};
let f = options.normalize === false ? identity : normalizeText;
- return f($(findOne(this, selector, options)).text());
+ return f(textContent(findOne(this, selector, options)));
});
}
diff --git a/test-app/.eslintrc.js b/test-app/.eslintrc.js
index f3cbd56d..2f53cded 100644
--- a/test-app/.eslintrc.js
+++ b/test-app/.eslintrc.js
@@ -25,6 +25,7 @@ module.exports = {
},
rules: {
'ember/no-global-jquery': 0,
+ 'ember/no-jquery': 0,
'no-console': ['error', { allow: ['warn', 'error'] }],
},
overrides: [
diff --git a/test-app/tests/unit/extend/find-many-test.ts b/test-app/tests/unit/extend/find-many-test.ts
index b3f90a08..7eaa2598 100644
--- a/test-app/tests/unit/extend/find-many-test.ts
+++ b/test-app/tests/unit/extend/find-many-test.ts
@@ -73,16 +73,46 @@ module(`Extend | findMany`, function(hooks) {
assert.deepEqual(findMany(page, '.lorem', { resetScope: true }), findAll('.lorem'));
});
- test('contains param', async function(assert) {
+ test('contains', async function(assert) {
let page = create({});
await render(hbs`
- Word
- Word
+
+ Word
+ Word
+
+
+ Word
+ Word
+
+ `);
+
+ assert.deepEqual(
+ findMany(page, '.lorem', { contains: 'Word' }).map((el) => el.id),
+ ['a', 'b']
+ );
+ });
+
+ test('contains with nested selector', async function(assert) {
+ let page = create({});
+
+ await render(hbs`
+
+
+ Word
+ Word
+
+
+ Word
+ Word
+
`);
- assert.deepEqual(findMany(page, '.lorem', { contains: 'Word' }), findAll('.lorem').slice(1, 3));
+ assert.deepEqual(
+ findMany(page, '.lorem *', { contains: 'Word' }).map((el) => el.id),
+ ['ab', 'bb']
+ );
});
test('scope param', async function(assert) {
diff --git a/test-app/tests/unit/extend/find-one-test.ts b/test-app/tests/unit/extend/find-one-test.ts
index 44bea303..204c7c98 100644
--- a/test-app/tests/unit/extend/find-one-test.ts
+++ b/test-app/tests/unit/extend/find-one-test.ts
@@ -17,9 +17,16 @@ module(`Extend | findOne`, function(hooks) {
});
test('finds deeper in scope', async function(assert) {
- let page = create({ scope: '.lorem' });
-
- await render(hbs``);
+ let page = create({
+ scope: '.lorem'
+ });
+
+ await render(hbs`
+
+
+
+
+ `);
assert.equal(findOne(page, '.dolor', {}), find('.lorem .dolor'));
});
diff --git a/tests/unit/-private/query-test.js b/tests/unit/-private/query-test.js
new file mode 100644
index 00000000..afd1abc2
--- /dev/null
+++ b/tests/unit/-private/query-test.js
@@ -0,0 +1,142 @@
+import { test, module, todo } from 'qunit';
+import { create } from 'ember-cli-page-object';
+import { Query } from 'ember-cli-page-object/-private/query';
+
+module('Unit | -private/query', function () {
+ module('toString()', function () {
+ test('it works', function (assert) {
+ const q = new Query();
+ assert.equal(q.toString(), ':first-child:eq(0)');
+ });
+
+ test('respects node scope', function (assert) {
+ const page = create({
+ scope: '.selector',
+ });
+
+ const q = new Query(page);
+ assert.equal(q.toString(), '.selector');
+ });
+
+ test('scope as a getter', function (assert) {
+ const page = create({
+ scope: {
+ selector: '.selector',
+ at: 2,
+ },
+ });
+
+ const q = new Query(page);
+ assert.equal(q.toString(), '.selector:eq(2)');
+ });
+
+ module('locator', function () {
+ test('accepts string', function (assert) {
+ const page = create({
+ scope: '.selector',
+ });
+
+ const q = new Query(page, '.child');
+ assert.equal(q.toString(), '.selector .child');
+ });
+
+ test('selector', function (assert) {
+ const page = create({
+ scope: '.selector',
+ });
+
+ const q = new Query(page, {
+ selector: '.child',
+ });
+
+ assert.equal(q.toString(), '.selector .child');
+ });
+
+ test('at', function (assert) {
+ const page = create({
+ scope: '.selector',
+ });
+
+ const q = new Query(page, {
+ at: 9,
+ });
+
+ assert.equal(q.toString(), '.selector:eq(9)');
+ });
+
+ test('last', function (assert) {
+ const page = create({
+ scope: '.selector',
+ });
+
+ const q = new Query(page, {
+ last: true,
+ });
+
+ assert.equal(q.toString(), '.selector:last');
+ });
+
+ test('visible', function (assert) {
+ const page = create({
+ scope: '.selector',
+ });
+
+ const q = new Query(page, {
+ visible: true,
+ });
+
+ assert.equal(q.toString(), '.selector:visible');
+ });
+
+ test('contains', function (assert) {
+ const page = create({
+ scope: '.selector',
+ });
+
+ const q = new Query(page, {
+ contains: 'some text',
+ });
+
+ assert.equal(q.toString(), '.selector:contains("some text")');
+ });
+
+ todo('respects testContainer', function (assert) {
+ const page = create({
+ scope: '.selector',
+ });
+
+ const q1 = new Query(page, {
+ testContainer: '.external',
+ });
+
+ assert.equal(q1.toString(), '.selector');
+
+ const q2 = new Query(page, {
+ selector: '.nestedSelector',
+ testContainer: '.external',
+ });
+
+ assert.equal(q2.toString(), '.external .nestedSelector');
+ });
+
+ test('respects resetScope', function (assert) {
+ const page = create({
+ scope: '.selector',
+ });
+
+ const q1 = new Query(page, {
+ selector: '.independent-selector',
+ resetScope: true,
+ });
+
+ assert.equal(q1.toString(), '.independent-selector');
+
+ const q2 = new Query(page, {
+ resetScope: true,
+ });
+
+ assert.equal(q2.toString(), '.selector');
+ });
+ });
+ });
+});