From 3ad2fcd907d68545663ecabd31a2550df4284619 Mon Sep 17 00:00:00 2001 From: Ruslan Hrabovyi Date: Tue, 12 Nov 2019 00:30:58 +0200 Subject: [PATCH 01/27] cache execution_context --- .../-private/execution_context.js | 56 +++++++++++-------- addon-test-support/properties/blurrable.js | 5 +- .../properties/click-on-text.js | 5 +- addon-test-support/properties/clickable.js | 5 +- addon-test-support/properties/fillable.js | 5 +- addon-test-support/properties/focusable.js | 5 +- addon-test-support/properties/triggerable.js | 5 +- addon-test-support/properties/visitable.js | 6 +- 8 files changed, 46 insertions(+), 46 deletions(-) diff --git a/addon-test-support/-private/execution_context.js b/addon-test-support/-private/execution_context.js index ea0578de..31854919 100644 --- a/addon-test-support/-private/execution_context.js +++ b/addon-test-support/-private/execution_context.js @@ -3,6 +3,7 @@ import { getContext as getEmberTestHelpersContext, visit } from './compatibility import AcceptanceExecutionContext from './execution_context/acceptance'; import IntegrationExecutionContext from './execution_context/integration'; import Rfc268Context from './execution_context/rfc268'; +import { getRoot } from './helpers'; const executioncontexts = { acceptance: AcceptanceExecutionContext, @@ -13,32 +14,39 @@ const executioncontexts = { /* * @private */ -export function getExecutionContext(pageObjectNode) { - // Our `getContext(pageObjectNode)` will return a context only if the test - // called `page.setContext(this)`, which is only supposed to happen in - // integration tests (i.e. pre-RFC232/RFC268). However, the integration - // context does work with RFC232 (`setupRenderingContext()`) tests, and before - // the RFC268 execution context was implemented, some users may have migrated - // their tests to RFC232 tests, leaving the `page.setContext(this)` in place. - // So, in order to not break those tests, we need to check for that case - // first, and only if that hasn't happened, check to see if we're in an - // RFC232/RFC268 test, and if not, fall back on assuming a pre-RFC268 - // acceptance test, which is the only remaining supported scenario. - let integrationTestContext = getIntegrationTestContext(pageObjectNode); - let contextName; - if (integrationTestContext) { - contextName = 'integration'; - } else if (isAcceptanceTest()) { - contextName = 'acceptance'; - } else if (supportsRfc268()) { - contextName = 'rfc268'; - } +export function getExecutionContext(node) { + const chainedRoot = getRoot(node)._chainedTree; + if (!chainedRoot) { + let root = getRoot(node) - if (!contextName) { - throw new Error('Can not detect test type. Please make sure you use the latest version of "@ember/test-helpers".'); - } + return root.__exectution_context__; + } else { + // Our `getContext(pageObjectNode)` will return a context only if the test + // called `page.setContext(this)`, which is only supposed to happen in + // integration tests (i.e. pre-RFC232/RFC268). However, the integration + // context does work with RFC232 (`setupRenderingContext()`) tests, and before + // the RFC268 execution context was implemented, some users may have migrated + // their tests to RFC232 tests, leaving the `page.setContext(this)` in place. + // So, in order to not break those tests, we need to check for that case + // first, and only if that hasn't happened, check to see if we're in an + // RFC232/RFC268 test, and if not, fall back on assuming a pre-RFC268 + // acceptance test, which is the only remaining supported scenario. + let integrationTestContext = getIntegrationTestContext(node); + let contextName; + if (integrationTestContext) { + contextName = 'integration'; + } else if (isAcceptanceTest()) { + contextName = 'acceptance'; + } else if (supportsRfc268()) { + contextName = 'rfc268'; + } - return new executioncontexts[contextName](pageObjectNode, integrationTestContext); + if (!contextName) { + throw new Error('Can not detect test type. Please make sure you use the latest version of "@ember/test-helpers".'); + } + + return chainedRoot.__exectution_context__ = new executioncontexts[contextName](node, integrationTestContext); + } } /** diff --git a/addon-test-support/properties/blurrable.js b/addon-test-support/properties/blurrable.js index 377d6bd1..08c74764 100644 --- a/addon-test-support/properties/blurrable.js +++ b/addon-test-support/properties/blurrable.js @@ -1,5 +1,5 @@ import { assign } from '../-private/helpers'; -import { getExecutionContext } from '../-private/execution_context'; +import { run } from '../-private/action'; /** * @@ -68,10 +68,9 @@ export function blurrable(selector, userOptions = {}) { get(key) { return function() { - const executionContext = getExecutionContext(this); const options = assign({ pageObjectKey: `${key}()` }, userOptions); - return executionContext.runAsync((context) => { + return run(this, (context) => { return context.blur(selector, options); }); }; diff --git a/addon-test-support/properties/click-on-text.js b/addon-test-support/properties/click-on-text.js index 351629ac..8a4ca50a 100644 --- a/addon-test-support/properties/click-on-text.js +++ b/addon-test-support/properties/click-on-text.js @@ -1,6 +1,6 @@ import { assign, findClosestValue } from '../-private/helpers'; -import { getExecutionContext } from '../-private/execution_context'; import { buildSelector } from './click-on-text/helpers'; +import { run } from '../-private/action'; /** * Clicks on an element containing specified text. @@ -90,10 +90,9 @@ export function clickOnText(selector, userOptions = {}) { get(key) { return function(textToClick) { - let executionContext = getExecutionContext(this); let options = assign({ pageObjectKey: `${key}("${textToClick}")`, contains: textToClick }, userOptions); - return executionContext.runAsync((context) => { + return run(this, (context) => { let fullSelector = buildSelector(this, context, selector, options); let container = options.testContainer || findClosestValue(this, 'testContainer'); diff --git a/addon-test-support/properties/clickable.js b/addon-test-support/properties/clickable.js index 356e07f8..0b450885 100644 --- a/addon-test-support/properties/clickable.js +++ b/addon-test-support/properties/clickable.js @@ -3,7 +3,7 @@ import { buildSelector, findClosestValue } from '../-private/helpers'; -import { getExecutionContext } from '../-private/execution_context'; +import { run } from '../-private/action'; /** * Clicks elements matched by a selector. @@ -72,10 +72,9 @@ export function clickable(selector, userOptions = {}) { get(key) { return function() { - let executionContext = getExecutionContext(this); let options = assign({ pageObjectKey: `${key}()` }, userOptions); - return executionContext.runAsync((context) => { + return run(this, (context) => { let fullSelector = buildSelector(this, selector, options); let container = options.testContainer || findClosestValue(this, 'testContainer'); diff --git a/addon-test-support/properties/fillable.js b/addon-test-support/properties/fillable.js index a76726a7..18534eb6 100644 --- a/addon-test-support/properties/fillable.js +++ b/addon-test-support/properties/fillable.js @@ -3,7 +3,7 @@ import { buildSelector, findClosestValue } from '../-private/helpers'; -import { getExecutionContext } from '../-private/execution_context'; +import { run } from '../-private/action'; /** * Alias for `fillable`, which works for inputs, HTML select menus, and @@ -131,10 +131,9 @@ export function fillable(selector, userOptions = {}) { clue = contentOrClue; } - let executionContext = getExecutionContext(this); let options = assign({ pageObjectKey: `${key}()` }, userOptions); - return executionContext.runAsync((context) => { + return run(this, (context) => { let fullSelector = buildSelector(this, selector, options); let container = options.testContainer || findClosestValue(this, 'testContainer'); diff --git a/addon-test-support/properties/focusable.js b/addon-test-support/properties/focusable.js index 8f49b83a..0cc4b4b3 100644 --- a/addon-test-support/properties/focusable.js +++ b/addon-test-support/properties/focusable.js @@ -1,5 +1,5 @@ import { assign } from '../-private/helpers'; -import { getExecutionContext } from '../-private/execution_context'; +import { run } from '../-private/action'; /** * @@ -68,10 +68,9 @@ export function focusable(selector, userOptions = {}) { get(key) { return function() { - const executionContext = getExecutionContext(this); const options = assign({ pageObjectKey: `${key}()` }, userOptions); - return executionContext.runAsync((context) => { + return run(this, (context) => { return context.focus(selector, options); }); }; diff --git a/addon-test-support/properties/triggerable.js b/addon-test-support/properties/triggerable.js index 4f1f8bb6..3b0ebbc8 100644 --- a/addon-test-support/properties/triggerable.js +++ b/addon-test-support/properties/triggerable.js @@ -3,7 +3,7 @@ import { buildSelector, findClosestValue } from '../-private/helpers'; -import { getExecutionContext } from '../-private/execution_context'; +import { run } from '../-private/action'; /** * @@ -88,11 +88,10 @@ export function triggerable(event, selector, userOptions = {}) { get(key) { return function(eventProperties = {}) { - const executionContext = getExecutionContext(this); const options = assign({ pageObjectKey: `${key}()` }, userOptions); const staticEventProperties = assign({}, options.eventProperties); - return executionContext.runAsync((context) => { + return run(this, (context) => { const fullSelector = buildSelector(this, selector, options); const container = options.testContainer || findClosestValue(this, 'testContainer'); diff --git a/addon-test-support/properties/visitable.js b/addon-test-support/properties/visitable.js index c6539f77..81e83360 100644 --- a/addon-test-support/properties/visitable.js +++ b/addon-test-support/properties/visitable.js @@ -1,5 +1,5 @@ import { assign } from '../-private/helpers'; -import { getExecutionContext } from '../-private/execution_context'; +import { run } from '../-private/action'; import $ from '-jquery'; @@ -95,9 +95,7 @@ export function visitable(path) { get() { return function(dynamicSegmentsAndQueryParams = {}) { - let executionContext = getExecutionContext(this); - - return executionContext.runAsync((context) => { + return run(this, (context) => { let params = assign({}, dynamicSegmentsAndQueryParams); let fullPath = fillInDynamicSegments(path, params); From fdf2f0d7fd30c227ab8ae6c311a71c77e66d63a2 Mon Sep 17 00:00:00 2001 From: Ruslan Hrabovyi Date: Tue, 12 Nov 2019 23:08:04 +0200 Subject: [PATCH 02/27] Make execution context aware of the current action promise --- addon-test-support/-private/action.js | 25 ++++++++++--------- addon-test-support/-private/dsl.js | 11 ++++---- .../-private/execution_context.js | 4 +-- addon-test-support/macros/alias.js | 5 +--- 4 files changed, 22 insertions(+), 23 deletions(-) diff --git a/addon-test-support/-private/action.js b/addon-test-support/-private/action.js index 4a58e7b8..c75620b2 100644 --- a/addon-test-support/-private/action.js +++ b/addon-test-support/-private/action.js @@ -11,32 +11,33 @@ import Ceibo from 'ceibo'; * @returns {Ceibo} */ export function run(node, cb) { - const adapter = getExecutionContext(node); const chainedRoot = getRoot(node)._chainedTree; - if (typeof adapter.andThen === 'function') { + let executionContext; + if (!chainedRoot) { + executionContext = getRoot(node).__execution_context__; + } else { + executionContext = chainedRoot.__execution_context__ = getExecutionContext(node); + } + + if (typeof executionContext.andThen === 'function') { // With old ember-testing helpers, we don't make the difference between // chanined VS independent action invocations. Awaiting for the previous // action settlement, before invoke a new action, is a part of // the legacy testing helpers adapters for backward compat reasons - chainedRoot._promise = adapter.andThen(cb); - - return node; + executionContext._promise = executionContext.andThen(cb); } else if (!chainedRoot) { // 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. - let root = getRoot(node) - root._promise = resolve(root._promise).then(() => cb(adapter)); - - return node; + executionContext._promise = resolve(executionContext._promise).then(() => cb(executionContext)); } else { // Store our invocation result on the chained root // so that chained calls can find it to wait on it. - chainedRoot._promise = cb(adapter); - - return chainable(node); + executionContext._promise = cb(executionContext); } + + return chainable(node); } export function chainable(branch) { diff --git a/addon-test-support/-private/dsl.js b/addon-test-support/-private/dsl.js index 2916a1ca..92602149 100644 --- a/addon-test-support/-private/dsl.js +++ b/addon-test-support/-private/dsl.js @@ -11,15 +11,16 @@ import { isVisible } from '../properties/is-visible'; import { text } from '../properties/text'; import { value } from '../properties/value'; -import { getRoot } from './helpers'; +import { getExecutionContext } from './execution_context'; const thenDescriptor = { isDescriptor: true, - value() { - const root = getRoot(this); - const chainedRoot = root._chainedTree || root; + get() { + return function() { + const { _promise } = getExecutionContext(this); - return chainedRoot._promise.then(...arguments); + return _promise.then(...arguments); + } } }; diff --git a/addon-test-support/-private/execution_context.js b/addon-test-support/-private/execution_context.js index 31854919..72a3e2d5 100644 --- a/addon-test-support/-private/execution_context.js +++ b/addon-test-support/-private/execution_context.js @@ -19,7 +19,7 @@ export function getExecutionContext(node) { if (!chainedRoot) { let root = getRoot(node) - return root.__exectution_context__; + return root.__execution_context__; } else { // Our `getContext(pageObjectNode)` will return a context only if the test // called `page.setContext(this)`, which is only supposed to happen in @@ -45,7 +45,7 @@ export function getExecutionContext(node) { throw new Error('Can not detect test type. Please make sure you use the latest version of "@ember/test-helpers".'); } - return chainedRoot.__exectution_context__ = new executioncontexts[contextName](node, integrationTestContext); + return new executioncontexts[contextName](node, integrationTestContext); } } diff --git a/addon-test-support/macros/alias.js b/addon-test-support/macros/alias.js index 482d35d9..0dd91a0e 100644 --- a/addon-test-support/macros/alias.js +++ b/addon-test-support/macros/alias.js @@ -4,7 +4,6 @@ import { objectHasProperty } from '../-private/helpers'; import { chainable } from '../-private/action' -import { getExecutionContext } from '../-private/execution_context' const ALIASED_PROP_NOT_FOUND = 'PageObject does not contain aliased property'; @@ -99,9 +98,7 @@ export function alias(pathToProp, options = {}) { // child node rather than this node. value(...args); - return (typeof getExecutionContext(this).andThen === 'function') - ? this - : chainable(this); + return chainable(this); }; } }; From 46bf062b15b24258d4fc834b9b7cb69fa54440e9 Mon Sep 17 00:00:00 2001 From: Ruslan Hrabovyi Date: Sun, 17 Feb 2019 22:56:55 +0200 Subject: [PATCH 03/27] Allow passing of an Error instance to the throwBetterError --- addon-test-support/-private/better-errors.js | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/addon-test-support/-private/better-errors.js b/addon-test-support/-private/better-errors.js index a1a457d7..97fb53ef 100644 --- a/addon-test-support/-private/better-errors.js +++ b/addon-test-support/-private/better-errors.js @@ -8,13 +8,15 @@ export const ELEMENT_NOT_FOUND = 'Element not found.'; * * @param {Ceibo} node PageObject node containing the property that triggered the error * @param {string} key Key of PageObject property tht triggered the error - * @param {string} msg Error message + * @param {Error|string} err Error or error text * @param {Object} options * @param {string} options.selector Selector of element targeted by PageObject property * @return {Ember.Error} */ -export function throwBetterError(node, key, msg, { selector } = {}) { - let path = [key]; +export function throwBetterError(node, key, err, { selector } = {}) { + let fullErrorMessage = typeof err === Error ? err.message : err.toString(); + + let path = []; let current; for (current = node; current; current = Ceibo.parent(current)) { @@ -22,11 +24,16 @@ export function throwBetterError(node, key, msg, { selector } = {}) { } path[0] = 'page'; + if (key && key.trim().length > 0) { + path.push(key); + } - let fullErrorMessage = `${msg}\n\nPageObject: '${path.join('.')}'`; + if (path.length > 0) { + fullErrorMessage += `\n\nPageObject: '${path.join('.')}'`; + } - if (selector) { - fullErrorMessage = `${fullErrorMessage}\n Selector: '${selector}'`; + if (typeof selector === 'string' && selector.trim().length > 0) { + fullErrorMessage += `\n Selector: '${selector}'`; } console.error(fullErrorMessage); From 7ea4ef1503808dd37392f25a7004dd6b458e5c68 Mon Sep 17 00:00:00 2001 From: Ruslan Hrabovyi Date: Fri, 25 Jan 2019 03:14:12 +0200 Subject: [PATCH 04/27] [Needs Tests] Implement action utility + invokeHelper --- .../-private/execution_context/acceptance.js | 2 +- .../-private/execution_context/integration.js | 8 ++-- .../native-events-context.js | 2 +- .../-private/execution_context/rfc268.js | 2 +- .../-private/{action.js => run.js} | 2 +- addon-test-support/extend/action.js | 41 +++++++++++++++++++ addon-test-support/extend/index.js | 1 + addon-test-support/macros/alias.js | 2 +- 8 files changed, 51 insertions(+), 9 deletions(-) rename addon-test-support/-private/{action.js => run.js} (98%) create mode 100644 addon-test-support/extend/action.js diff --git a/addon-test-support/-private/execution_context/acceptance.js b/addon-test-support/-private/execution_context/acceptance.js index ef17352d..59ab49dd 100644 --- a/addon-test-support/-private/execution_context/acceptance.js +++ b/addon-test-support/-private/execution_context/acceptance.js @@ -1,4 +1,4 @@ -import { run } from '../action'; +import run from '../run'; import { guardMultiple, buildSelector, diff --git a/addon-test-support/-private/execution_context/integration.js b/addon-test-support/-private/execution_context/integration.js index e8985d84..bf5a2da5 100644 --- a/addon-test-support/-private/execution_context/integration.js +++ b/addon-test-support/-private/execution_context/integration.js @@ -1,6 +1,6 @@ import $ from '-jquery'; -import { run } from '@ember/runloop'; -import { run as runAction } from '../action'; +import { run as emberRunloopRun } from '@ember/runloop'; +import run from '../run'; import { guardMultiple, buildSelector, @@ -23,7 +23,7 @@ export default function IntegrationExecutionContext(pageObjectNode, testContext) IntegrationExecutionContext.prototype = { andThen(cb) { - run(() => { + emberRunloopRun(() => { cb(this) }); @@ -31,7 +31,7 @@ IntegrationExecutionContext.prototype = { }, runAsync(cb) { - return runAction(this.pageObjectNode, cb); + return run(this.pageObjectNode, cb); }, visit() {}, diff --git a/addon-test-support/-private/execution_context/native-events-context.js b/addon-test-support/-private/execution_context/native-events-context.js index e345f39b..094c8b9b 100644 --- a/addon-test-support/-private/execution_context/native-events-context.js +++ b/addon-test-support/-private/execution_context/native-events-context.js @@ -8,7 +8,7 @@ import { blur } from 'ember-native-dom-helpers'; -import { run } from '../action'; +import run from '../run'; import { guardMultiple, buildSelector, diff --git a/addon-test-support/-private/execution_context/rfc268.js b/addon-test-support/-private/execution_context/rfc268.js index f7ffff9f..217ff7b0 100644 --- a/addon-test-support/-private/execution_context/rfc268.js +++ b/addon-test-support/-private/execution_context/rfc268.js @@ -1,5 +1,5 @@ import $ from '-jquery'; -import { run } from '../action'; +import run from '../run'; import { guardMultiple, buildSelector, diff --git a/addon-test-support/-private/action.js b/addon-test-support/-private/run.js similarity index 98% rename from addon-test-support/-private/action.js rename to addon-test-support/-private/run.js index c75620b2..3fa5f5e2 100644 --- a/addon-test-support/-private/action.js +++ b/addon-test-support/-private/run.js @@ -10,7 +10,7 @@ import Ceibo from 'ceibo'; * @param {Function} cb Some async activity callback * @returns {Ceibo} */ -export function run(node, cb) { +export default function run(node, cb) { const chainedRoot = getRoot(node)._chainedTree; let executionContext; diff --git a/addon-test-support/extend/action.js b/addon-test-support/extend/action.js new file mode 100644 index 00000000..aea057c6 --- /dev/null +++ b/addon-test-support/extend/action.js @@ -0,0 +1,41 @@ +import { findElementWithAssert } from 'ember-cli-page-object/extend'; +import { throwBetterError } from '../-private/better-errors'; +import run from '../-private/run'; +import { getExecutionContext } from '../-private/execution_context'; + +export default function action(fn) { + return { + isDescriptor: true, + + get(key) { + return function(...args) { + return run(this, (context) => { + // @todo: better handling of possible arg types + const formattedArgs = args.length ? `"${args.join('", "')}"` : ''; + + context.key = `${key}(${formattedArgs})`; + + return fn.bind(this)(...args); + }); + } + } + } +} + +export function invokeHelper(node, selector, query, cb) { + const context = getExecutionContext(node); + + const _query = Object.assign({ multiple: true }, query); + + try { + const domElements = findElementWithAssert(node, selector, _query).get(); + + return Promise.all(domElements.map((element) => { + return cb(context, element); + })).then(undefined, (e) => { + throwBetterError(node, context.key, e, { selector }) + }); + } catch (e) { + throwBetterError(node, context.key, e, { selector }) + } +} diff --git a/addon-test-support/extend/index.js b/addon-test-support/extend/index.js index 00ea76fc..45495e4c 100644 --- a/addon-test-support/extend/index.js +++ b/addon-test-support/extend/index.js @@ -1,3 +1,4 @@ +export { default as action } from './action'; export { findElement } from './find-element'; export { findElementWithAssert } from './find-element-with-assert'; export { buildSelector, getContext, fullScope } from '../-private/helpers'; diff --git a/addon-test-support/macros/alias.js b/addon-test-support/macros/alias.js index 0dd91a0e..6eb437e2 100644 --- a/addon-test-support/macros/alias.js +++ b/addon-test-support/macros/alias.js @@ -3,7 +3,7 @@ import { getProperty, objectHasProperty } from '../-private/helpers'; -import { chainable } from '../-private/action' +import { chainable } from '../-private/run' const ALIASED_PROP_NOT_FOUND = 'PageObject does not contain aliased property'; From d7e660e5619cdaff0ad81cd857bcc915fd185a06 Mon Sep 17 00:00:00 2001 From: Ruslan Hrabovyi Date: Sun, 24 Feb 2019 11:52:51 +0200 Subject: [PATCH 05/27] Add negative test case for fillable with clue --- tests/integration/properties/fillable-test.js | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 tests/integration/properties/fillable-test.js diff --git a/tests/integration/properties/fillable-test.js b/tests/integration/properties/fillable-test.js new file mode 100644 index 00000000..1f267797 --- /dev/null +++ b/tests/integration/properties/fillable-test.js @@ -0,0 +1,37 @@ +import { module, test } from 'qunit'; +import { setupRenderingTest } from 'ember-qunit'; +import { create, fillable } from 'ember-cli-page-object'; + +import require from 'require'; +import RenderingAdapter from '../../helpers/properties/rendering-adapter'; +if (require.has('@ember/test-helpers')) { + + module('fillable', function(hooks) { + setupRenderingTest(hooks); + + const { createTemplate, throws } = new RenderingAdapter(); + + module('clue', function() { + test(`by clue: raises an error when can't find an element by clue`, async function(assert) { + let clue = 'clue'; + const expectedMessage = `Element not found. + +PageObject: 'page.fillInByClue()' + Selector: '.scope input[data-test="${clue}"],.scope input[aria-label="${clue}"],.scope input[placeholder="${clue}"],.scope input[name="${clue}"],.scope input#${clue},.scope textarea[data-test="${clue}"],.scope textarea[aria-label="${clue}"],.scope textarea[placeholder="${clue}"],.scope textarea[name="${clue}"],.scope textarea#${clue},.scope select[data-test="${clue}"],.scope select[aria-label="${clue}"],.scope select[placeholder="${clue}"],.scope select[name="${clue}"],.scope select#${clue},.scope [contenteditable][data-test="${clue}"],.scope [contenteditable][aria-label="${clue}"],.scope [contenteditable][placeholder="${clue}"],.scope [contenteditable][name="${clue}"],.scope [contenteditable]#${clue}'`; + + let page = create({ + scope: '.scope', + fillInByClue: fillable() + }); + + await createTemplate(this, page, ``); + + await throws(assert, function() { + return page.fillInByClue(clue, 'dummy text'); + }, function(e) { + return e.message === expectedMessage; + }, 'Not found error with a full selector has been raised'); + }); + }) + }); +} From ab0879af3b1a2c411eb7ea4e8aa385128d237438 Mon Sep 17 00:00:00 2001 From: Ruslan Hrabovyi Date: Mon, 28 Oct 2019 00:40:57 +0200 Subject: [PATCH 06/27] ExecutionContext: fillable --- .../-private/execution_context/acceptance.js | 16 +-- .../-private/execution_context/helpers.js | 15 +-- .../-private/execution_context/integration.js | 14 +-- .../native-events-context.js | 18 +--- .../-private/execution_context/rfc268.js | 4 +- addon-test-support/properties/fillable.js | 100 +++++++++++------- tests/integration/properties/fillable-test.js | 6 +- .../unit/-private/properties/fillable-test.js | 2 +- 8 files changed, 83 insertions(+), 92 deletions(-) diff --git a/addon-test-support/-private/execution_context/acceptance.js b/addon-test-support/-private/execution_context/acceptance.js index 59ab49dd..bce8dcc4 100644 --- a/addon-test-support/-private/execution_context/acceptance.js +++ b/addon-test-support/-private/execution_context/acceptance.js @@ -38,21 +38,15 @@ AcceptanceExecutionContext.prototype = { click(selector, container); }, - fillIn(selector, container, options, content) { - let $selection = find(selector, container || findClosestValue(this.pageObjectNode, 'testContainer')); - + fillIn(element, content) { /* global focus */ - focus($selection); + focus(element); - fillElement($selection, content, { - selector, - pageObjectNode: this.pageObjectNode, - pageObjectKey: options.pageObjectKey - }); + fillElement(element, content); /* global triggerEvent */ - triggerEvent(selector, container, 'input'); - triggerEvent(selector, container, 'change'); + triggerEvent(element, 'input'); + triggerEvent(element, 'change'); }, triggerEvent(selector, container, options, eventName, eventOptions) { diff --git a/addon-test-support/-private/execution_context/helpers.js b/addon-test-support/-private/execution_context/helpers.js index 7cc81be7..e3cd8409 100644 --- a/addon-test-support/-private/execution_context/helpers.js +++ b/addon-test-support/-private/execution_context/helpers.js @@ -1,8 +1,5 @@ -import { - throwBetterError -} from '../better-errors'; - import $ from '-jquery'; +import { throwBetterError } from '../better-errors'; /** * @private @@ -19,19 +16,13 @@ import $ from '-jquery'; * * @throws Will throw an error if called on a contenteditable element that has `contenteditable="false"` */ -export function fillElement(selection, content, { selector, pageObjectNode, pageObjectKey }) { +export function fillElement(selection, content) { const $selection = $(selection); if ($selection.is('[contenteditable][contenteditable!="false"]')) { $selection.html(content); } else if ($selection.is('[contenteditable="false"]')) { - throwBetterError( - pageObjectNode, - pageObjectKey, - 'Element cannot be filled because it has `contenteditable="false"`.', { - selector - } - ); + throw new Error('Element cannot be filled because it has `contenteditable="false"`.'); } else { $selection.val(content); } diff --git a/addon-test-support/-private/execution_context/integration.js b/addon-test-support/-private/execution_context/integration.js index bf5a2da5..020814c5 100644 --- a/addon-test-support/-private/execution_context/integration.js +++ b/addon-test-support/-private/execution_context/integration.js @@ -40,17 +40,11 @@ IntegrationExecutionContext.prototype = { this.$(selector, container).click(); }, - fillIn(selector, container, options, content) { - let $selection = this.$(selector, container); + fillIn(element, content) { + fillElement(element, content); - fillElement($selection, content, { - selector, - pageObjectNode: this.pageObjectNode, - pageObjectKey: options.pageObjectKey - }); - - $selection.trigger('input'); - $selection.change(); + $(element).trigger('input'); + $(element).change(); }, $(selector, container) { diff --git a/addon-test-support/-private/execution_context/native-events-context.js b/addon-test-support/-private/execution_context/native-events-context.js index 094c8b9b..0662ff6b 100644 --- a/addon-test-support/-private/execution_context/native-events-context.js +++ b/addon-test-support/-private/execution_context/native-events-context.js @@ -40,19 +40,11 @@ ExecutionContext.prototype = { click(el); }, - fillIn(selector, container, options, content) { - let elements = this.$(selector, container).toArray(); - - elements.forEach((el) => { - fillElement(el, content, { - selector, - pageObjectNode: this.pageObjectNode, - pageObjectKey: options.pageObjectKey - }); - - triggerEvent(el, 'input'); - triggerEvent(el, 'change'); - }); + fillIn(element, content) { + fillElement(element, content); + + triggerEvent(element, 'input'); + triggerEvent(element, 'change'); }, $(selector, container) { diff --git a/addon-test-support/-private/execution_context/rfc268.js b/addon-test-support/-private/execution_context/rfc268.js index 217ff7b0..42d48590 100644 --- a/addon-test-support/-private/execution_context/rfc268.js +++ b/addon-test-support/-private/execution_context/rfc268.js @@ -37,8 +37,8 @@ ExecutionContext.prototype = { return this.invokeHelper(selector, options, click); }, - fillIn(selector, container, options, content) { - return this.invokeHelper(selector, options, fillIn, content); + fillIn(selector, content) { + return fillIn(selector, content); }, triggerEvent(selector, container, options, eventName, eventOptions) { diff --git a/addon-test-support/properties/fillable.js b/addon-test-support/properties/fillable.js index 18534eb6..201d7a05 100644 --- a/addon-test-support/properties/fillable.js +++ b/addon-test-support/properties/fillable.js @@ -1,9 +1,7 @@ -import { - assign, - buildSelector, - findClosestValue -} from '../-private/helpers'; -import { run } from '../-private/action'; +import { findElement, action as _action } from 'ember-cli-page-object/extend'; +import run from '../-private/run'; +import { assign, buildSelector } from '../-private/helpers'; +import { throwBetterError, ELEMENT_NOT_FOUND } from '../-private/better-errors'; /** * Alias for `fillable`, which works for inputs, HTML select menus, and @@ -117,44 +115,68 @@ import { run } from '../-private/action'; * @param {string} options.testContainer - Context where to search elements in the DOM * @return {Descriptor} */ -export function fillable(selector, userOptions = {}) { - return { - isDescriptor: true, +export function fillable(selector = '', userOptions = {}) { + return new Action(function(key, contentOrClue, content) { + return run(this, ({ fillIn }) => { + let options = assign({ pageObjectKey: `${key}()` }, userOptions); - get(key) { - return function(contentOrClue, content) { - let clue; + let clue; + if (content === undefined) { + content = contentOrClue; + } else { + clue = contentOrClue; + } - if (content === undefined) { - content = contentOrClue; - } else { - clue = contentOrClue; - } + let scopeSelector = clue + ? `${selector} ${getClueSelector(this, selector, options, clue)}` + : selector; - let options = assign({ pageObjectKey: `${key}()` }, userOptions); + return _action(this, scopeSelector, options, + (element) => fillIn(element, content) + ); + }); + }) +} + +class Action { + constructor(fn) { + this.isDescriptor = true; + + this.get = function(key) { + return function() { + return fn.bind(this)(key, ...arguments); + } + }; + } +} + + +function getClueSelector(pageObject, selector, options, clue) { + let cssClues = ['input', 'textarea', 'select', '[contenteditable]'].map((tag) => [ + `${tag}[data-test="${clue}"]`, + `${tag}[aria-label="${clue}"]`, + `${tag}[placeholder="${clue}"]`, + `${tag}[name="${clue}"]`, + `${tag}#${clue}` + ]) + .reduce((total, other) => total.concat(other), []) + + const clueScope = cssClues.find(extraScope => { + return findElement(pageObject, `${selector} ${extraScope}`, options).get(0); + }); - return run(this, (context) => { - let fullSelector = buildSelector(this, selector, options); - let container = options.testContainer || findClosestValue(this, 'testContainer'); + if (!clueScope) { + const pageObjectSelector = buildSelector(pageObject, '', options); + const possibleSelectors = cssClues.map((cssClue) => { + const childSelector = `${selector} ${cssClue}`.trim(); - if (clue) { - fullSelector = ['input', 'textarea', 'select', '[contenteditable]'] - .map((tag) => [ - `${fullSelector} ${tag}[data-test="${clue}"]`, - `${fullSelector} ${tag}[aria-label="${clue}"]`, - `${fullSelector} ${tag}[placeholder="${clue}"]`, - `${fullSelector} ${tag}[name="${clue}"]`, - `${fullSelector} ${tag}#${clue}` - ]) - .reduce((total, other) => total.concat(other), []) - .join(','); - } + return `${pageObjectSelector} ${childSelector}`; + }); - context.assertElementExists(fullSelector, options); + throwBetterError(pageObject, options.pageObjectKey, ELEMENT_NOT_FOUND, { + selector: possibleSelectors.join(',') + }) + } - return context.fillIn(fullSelector, container, options, content); - }); - }; - } - }; + return clueScope; } diff --git a/tests/integration/properties/fillable-test.js b/tests/integration/properties/fillable-test.js index 1f267797..d6d0c390 100644 --- a/tests/integration/properties/fillable-test.js +++ b/tests/integration/properties/fillable-test.js @@ -16,7 +16,7 @@ if (require.has('@ember/test-helpers')) { let clue = 'clue'; const expectedMessage = `Element not found. -PageObject: 'page.fillInByClue()' +PageObject: 'page.fillInByClue("${clue}", "dummy text")' Selector: '.scope input[data-test="${clue}"],.scope input[aria-label="${clue}"],.scope input[placeholder="${clue}"],.scope input[name="${clue}"],.scope input#${clue},.scope textarea[data-test="${clue}"],.scope textarea[aria-label="${clue}"],.scope textarea[placeholder="${clue}"],.scope textarea[name="${clue}"],.scope textarea#${clue},.scope select[data-test="${clue}"],.scope select[aria-label="${clue}"],.scope select[placeholder="${clue}"],.scope select[name="${clue}"],.scope select#${clue},.scope [contenteditable][data-test="${clue}"],.scope [contenteditable][aria-label="${clue}"],.scope [contenteditable][placeholder="${clue}"],.scope [contenteditable][name="${clue}"],.scope [contenteditable]#${clue}'`; let page = create({ @@ -28,9 +28,7 @@ PageObject: 'page.fillInByClue()' await throws(assert, function() { return page.fillInByClue(clue, 'dummy text'); - }, function(e) { - return e.message === expectedMessage; - }, 'Not found error with a full selector has been raised'); + }, new Error(expectedMessage), 'Not found error with a full selector has been raised'); }); }) }); diff --git a/tests/unit/-private/properties/fillable-test.js b/tests/unit/-private/properties/fillable-test.js index 2f85a510..8e96b73e 100644 --- a/tests/unit/-private/properties/fillable-test.js +++ b/tests/unit/-private/properties/fillable-test.js @@ -222,7 +222,7 @@ moduleForProperty('fillable', function(test) { this.adapter.throws(assert, function() { return page.foo.bar.baz.qux('lorem'); - }, /page\.foo\.bar\.baz\.qux\(\)/, 'Element not found'); + }, /page\.foo\.bar\.baz\.qux\("lorem"\)/, 'Element not found'); }); test('raises an error when the element has contenteditable="false"', async function(assert) { From 0305adc177655f7415003bdf0d64c28abff97a07 Mon Sep 17 00:00:00 2001 From: Ruslan Hrabovyi Date: Fri, 15 Nov 2019 23:46:35 +0200 Subject: [PATCH 07/27] Restore fillable --- addon-test-support/properties/fillable.js | 53 ++++++++--------------- 1 file changed, 19 insertions(+), 34 deletions(-) diff --git a/addon-test-support/properties/fillable.js b/addon-test-support/properties/fillable.js index 201d7a05..d2882bec 100644 --- a/addon-test-support/properties/fillable.js +++ b/addon-test-support/properties/fillable.js @@ -1,7 +1,7 @@ -import { findElement, action as _action } from 'ember-cli-page-object/extend'; -import run from '../-private/run'; -import { assign, buildSelector } from '../-private/helpers'; +import { findElement } from 'ember-cli-page-object/extend'; +import { buildSelector } from 'ember-cli-page-object'; import { throwBetterError, ELEMENT_NOT_FOUND } from '../-private/better-errors'; +import action, { invokeHelper } from '../extend/action'; /** * Alias for `fillable`, which works for inputs, HTML select menus, and @@ -116,42 +116,27 @@ import { throwBetterError, ELEMENT_NOT_FOUND } from '../-private/better-errors'; * @return {Descriptor} */ export function fillable(selector = '', userOptions = {}) { - return new Action(function(key, contentOrClue, content) { - return run(this, ({ fillIn }) => { - let options = assign({ pageObjectKey: `${key}()` }, userOptions); + return action(function(contentOrClue, content) { + let clue; + if (content === undefined) { + content = contentOrClue; + } else { + clue = contentOrClue; + } - let clue; - if (content === undefined) { - content = contentOrClue; - } else { - clue = contentOrClue; - } - - let scopeSelector = clue - ? `${selector} ${getClueSelector(this, selector, options, clue)}` - : selector; - - return _action(this, scopeSelector, options, - (element) => fillIn(element, content) - ); - }); - }) -} - -class Action { - constructor(fn) { - this.isDescriptor = true; + let scopeSelector = clue + ? `${selector} ${getSelectorByClue(this, selector, userOptions, clue)}` + : selector; - this.get = function(key) { - return function() { - return fn.bind(this)(key, ...arguments); + return invokeHelper(this, scopeSelector, userOptions, + ({ fillIn }, element) => { + return fillIn(element, content); } - }; - } + ); + }) } - -function getClueSelector(pageObject, selector, options, clue) { +function getSelectorByClue(pageObject, selector, options, clue) { let cssClues = ['input', 'textarea', 'select', '[contenteditable]'].map((tag) => [ `${tag}[data-test="${clue}"]`, `${tag}[aria-label="${clue}"]`, From 2ca4babe46f1878f5dbc838bb19ee9e9e41b5f90 Mon Sep 17 00:00:00 2001 From: Ruslan Hrabovyi Date: Mon, 28 Oct 2019 01:17:59 +0200 Subject: [PATCH 08/27] ExectuionContext: click --- .../-private/execution_context/acceptance.js | 5 +-- .../-private/execution_context/integration.js | 4 +- .../native-events-context.js | 3 +- .../-private/execution_context/rfc268.js | 4 +- .../properties/click-on-text.js | 38 +++++++++---------- .../properties/click-on-text/helpers.js | 26 ------------- addon-test-support/properties/clickable.js | 30 +++------------ 7 files changed, 32 insertions(+), 78 deletions(-) delete mode 100644 addon-test-support/properties/click-on-text/helpers.js diff --git a/addon-test-support/-private/execution_context/acceptance.js b/addon-test-support/-private/execution_context/acceptance.js index bce8dcc4..ab74e781 100644 --- a/addon-test-support/-private/execution_context/acceptance.js +++ b/addon-test-support/-private/execution_context/acceptance.js @@ -33,11 +33,10 @@ AcceptanceExecutionContext.prototype = { visit(path); }, - click(selector, container) { + click(element) { /* global click */ - click(selector, container); + click(element); }, - fillIn(element, content) { /* global focus */ focus(element); diff --git a/addon-test-support/-private/execution_context/integration.js b/addon-test-support/-private/execution_context/integration.js index 020814c5..59b94a9e 100644 --- a/addon-test-support/-private/execution_context/integration.js +++ b/addon-test-support/-private/execution_context/integration.js @@ -36,8 +36,8 @@ IntegrationExecutionContext.prototype = { visit() {}, - click(selector, container) { - this.$(selector, container).click(); + click(element) { + $(element).click(); }, fillIn(element, content) { diff --git a/addon-test-support/-private/execution_context/native-events-context.js b/addon-test-support/-private/execution_context/native-events-context.js index 0662ff6b..81788010 100644 --- a/addon-test-support/-private/execution_context/native-events-context.js +++ b/addon-test-support/-private/execution_context/native-events-context.js @@ -35,8 +35,7 @@ ExecutionContext.prototype = { return run(this.pageObjectNode, cb); }, - click(selector, container) { - const el = this.$(selector, container)[0]; + click(el) { click(el); }, diff --git a/addon-test-support/-private/execution_context/rfc268.js b/addon-test-support/-private/execution_context/rfc268.js index 42d48590..53123297 100644 --- a/addon-test-support/-private/execution_context/rfc268.js +++ b/addon-test-support/-private/execution_context/rfc268.js @@ -33,8 +33,8 @@ ExecutionContext.prototype = { return visit(path); }, - click(selector, container, options) { - return this.invokeHelper(selector, options, click); + click(element) { + return click(element); }, fillIn(selector, content) { diff --git a/addon-test-support/properties/click-on-text.js b/addon-test-support/properties/click-on-text.js index 8a4ca50a..59353640 100644 --- a/addon-test-support/properties/click-on-text.js +++ b/addon-test-support/properties/click-on-text.js @@ -1,6 +1,6 @@ -import { assign, findClosestValue } from '../-private/helpers'; -import { buildSelector } from './click-on-text/helpers'; -import { run } from '../-private/action'; +import { findElement } from '../extend/index'; +import { assign } from '../-private/helpers'; +import action, { invokeHelper } from '../extend/action'; /** * Clicks on an element containing specified text. @@ -84,23 +84,23 @@ import { run } from '../-private/action'; * @param {string} options.testContainer - Context where to search elements in the DOM * @return {Descriptor} */ -export function clickOnText(selector, userOptions = {}) { - return { - isDescriptor: true, +export function clickOnText(scope, options = {}) { + return action(function(textToClick) { + const query = assign({}, options, { + contains: textToClick, + // we want to find the deepest node containing a text to click. + last: true + }); - get(key) { - return function(textToClick) { - let options = assign({ pageObjectKey: `${key}("${textToClick}")`, contains: textToClick }, userOptions); + const childSelector = `${scope || ''} `; - return run(this, (context) => { - let fullSelector = buildSelector(this, context, selector, options); - let container = options.testContainer || findClosestValue(this, 'testContainer'); + let selector; + if (findElement(this, childSelector, query).length) { + selector = childSelector; + } else { + selector = scope; + } - context.assertElementExists(fullSelector, options); - - return context.click(fullSelector, container, options); - }); - }; - } - }; + return invokeHelper(this, selector, query, ({click}, element) => click(element)); + }); } diff --git a/addon-test-support/properties/click-on-text/helpers.js b/addon-test-support/properties/click-on-text/helpers.js deleted file mode 100644 index 9a66fed7..00000000 --- a/addon-test-support/properties/click-on-text/helpers.js +++ /dev/null @@ -1,26 +0,0 @@ -import { - assign, - buildSelector as originalBuildSelector -} from '../../-private/helpers'; - -function childSelector(pageObjectNode, context, selector, options) { - // Suppose that we have something like `
` - // In this case
and