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'); + }); + }); + }); +});