diff --git a/addon-test-support/-private/action.js b/addon-test-support/-private/action.js index b89acfea..1344bcb6 100644 --- a/addon-test-support/-private/action.js +++ b/addon-test-support/-private/action.js @@ -1,8 +1,5 @@ -import { resolve } from 'rsvp'; -import { getExecutionContext } from './execution_context'; -import { getRoot, assign, buildSelector } from './helpers'; -import Ceibo from 'ceibo'; -import { throwBetterError } from './better-errors'; +import { assign } from './helpers'; +import { run } from './run'; export default function action(query, cb) { return { @@ -10,138 +7,31 @@ export default function action(query, cb) { get(key) { return function (...args) { - let formattedKey = `${key}(${ - args.length - ? `"${args.map((a) => String(a)).join('", "')}"` - : `` - })`; + ({ query, cb } = normalizeArgs(key, query, cb, args)); - if (typeof query === 'function') { - cb = query; - query = { - key: formattedKey - }; - } else { - query = assign({}, query, { - key: formattedKey - }); - } - - return run(this, query, (context) => { - const selector = buildSelector(this, query.selector, query); - - let res; - try { - res = cb.bind(context)(...args); - } catch(e) { - throwBetterError(this, query.key, e, { selector }); - } - - return resolve(res).catch((e) => { - throwBetterError(this, query.key, e, { selector }); - }); - }) + return run(this, query, (executionContext) => { + return cb.bind(executionContext)(...args) + }); } } } } -/** - * Run action - * - * @param {Ceibo} node Page object node to run action on - * @param {object} query - * @param {Function} cb Some async activity callback - * @returns {Ceibo} - */ -function run(node, query, cb) { - const adapter = getExecutionContext(node); - const executionContext = Object.freeze({ - query, - node, - adapter - }); - - const chainedRoot = getRoot(node)._chainedTree; - - if (typeof adapter.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.bind(executionContext)(cb); - - return node; - } 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(executionContext)); +function normalizeArgs(key, query, cb, args) { + let formattedKey = `${key}(${args.length + ? `"${args.map((a) => String(a)).join('", "')}"` + : ``})`; - return node; + if (typeof query === 'function') { + cb = query; + query = { + key: formattedKey + }; } else { - // Store our invocation result on the chained root - // so that chained calls can find it to wait on it. - chainedRoot._promise = cb(executionContext); - - return chainable(node); - } -} - -export function chainable(branch) { - if (isChainedNode(branch)) { - return branch; - } - - // See explanation in `create.js` -- here instead of returning the node on - // which our method was invoked, we find and return our node's mirror in the - // chained tree so calls to it can be recognized as chained calls, and - // trigger the chained-call waiting behavior. - - // Collecting node keys to build a path to our node, and then use that - // to walk back down the chained tree to our mirror node. - let path = []; - let node; - - for (node = branch; node; node = Ceibo.parent(node)) { - path.unshift(Ceibo.meta(node).key); + query = assign({}, query, { + key: formattedKey + }); } - // The path will end with the root's key, 'root', so shift that back off - path.shift(); - - node = getRoot(branch)._chainedTree; - path.forEach((key) => { - node = getChildNode(node, key) - }); - - return node; -} - -function isChainedNode(node) { - let root = getRoot(node); - return !root._chainedTree; -} - -function getChildNode(node, key) { - // Normally an item's key is just its property name, but collection - // items' keys also include their index. Collection item keys look like - // `foo[2]` and legacy collection item keys look like `foo(2)`. - let match; - if ((match = /\[(\d+)\]$/.exec(key))) { - // This is a collection item - let [ indexStr, index ] = match; - let name = key.slice(0, -indexStr.length); - - return node[name].objectAt(parseInt(index, 10)); - } else if ((match = /\((\d+)\)$/.exec(key))) { - // This is a legacy collection item - let [ indexStr, index ] = match; - let name = key.slice(0, -indexStr.length); - - return node[name](parseInt(index, 10)); - } else { - return node[key]; - } + return { query, cb }; } diff --git a/addon-test-support/-private/better-errors.js b/addon-test-support/-private/better-errors.js index 8af04721..6ded55f8 100644 --- a/addon-test-support/-private/better-errors.js +++ b/addon-test-support/-private/better-errors.js @@ -1,7 +1,16 @@ import Ceibo from 'ceibo'; +import { buildSelector } from './helpers'; export const ELEMENT_NOT_FOUND = 'Element not found.'; +export function throwContextualError(context, e) { + const { query, node } = context; + + const selector = buildSelector(node, query.selector, query); + + throwBetterError(node, query.key, e, { selector }); +} + /** * Throws an error with a descriptive message. * diff --git a/addon-test-support/-private/chainable.js b/addon-test-support/-private/chainable.js new file mode 100644 index 00000000..ac11085f --- /dev/null +++ b/addon-test-support/-private/chainable.js @@ -0,0 +1,57 @@ +import Ceibo from 'ceibo'; +import { getRoot } from './helpers'; + +export function isChainedNode(node) { + let root = getRoot(node); + + return !root._chainedTree; +} + +export function chainable(branch) { + if (isChainedNode(branch)) { + return branch; + } + + // See explanation in `create.js` -- here instead of returning the node on + // which our method was invoked, we find and return our node's mirror in the + // chained tree so calls to it can be recognized as chained calls, and + // trigger the chained-call waiting behavior. + // Collecting node keys to build a path to our node, and then use that + // to walk back down the chained tree to our mirror node. + let path = []; + let node; + for (node = branch; node; node = Ceibo.parent(node)) { + path.unshift(Ceibo.meta(node).key); + } + + // The path will end with the root's key, 'root', so shift that back off + path.shift(); + node = getRoot(branch)._chainedTree; + path.forEach((key) => { + node = getChildNode(node, key); + }); + + return node; +} + +function getChildNode(node, key) { + // Normally an item's key is just its property name, but collection + // items' keys also include their index. Collection item keys look like + // `foo[2]` and legacy collection item keys look like `foo(2)`. + let match; + if ((match = /\[(\d+)\]$/.exec(key))) { + // This is a collection item + let [ indexStr, index ] = match; + let name = key.slice(0, -indexStr.length); + + return node[name].objectAt(parseInt(index, 10)); + } else if ((match = /\((\d+)\)$/.exec(key))) { + // This is a legacy collection item + let [ indexStr, index ] = match; + let name = key.slice(0, -indexStr.length); + + return node[name](parseInt(index, 10)); + } else { + return node[key]; + } +} diff --git a/addon-test-support/-private/compatibility.js b/addon-test-support/-private/compatibility.js deleted file mode 100644 index 4a221097..00000000 --- a/addon-test-support/-private/compatibility.js +++ /dev/null @@ -1,93 +0,0 @@ -// -// This is a wrapper around `@ember/test-helpers` that we need for compatibility -// reasons. Apps and addons aren't supposed to depend directly on -// `@ember/test-helpers`, but just use the one that their version of -// `ember-qunit` or `ember-mocha` provides. This compatibility module does three -// jobs for us: -// -// 1. Helps us determine if we are running an RFC232/268 test or not -// 2. Provides the test helpers needed to run RFC232/268 tests -// 3. Provides a `wait` implementation for non-RFC232/268 (legacy) tests -// -// To accomplish (1) and (2) we need to determine if `@ember/test-helpers` is -// present. If it isn't, we can't possibly be running RFC232/268 tests because -// they rely on it. If it is, then we need its `getContext()` method to see if -// any of the the RFC232/268 setup methods have been called. So, to keep this -// complexity encapsulated in this file, if `@ember/test-helpers` is not -// present, we export a stub `getContext()` function that returns null, -// indicating that we are not running RFC232/268 tests, and then the rest of the -// addon code won't try to access any of the other `@ember/test-helpers` -// helpers. -// -// To accomplish (3), we need to determine if `ember-test-helpers` is present. -// Because it's built with legacy support, anytime `@ember/test-helpers` is -// present, `ember-test-helpers` will also be present. So we can check for -// `ember-test-helpers/wait` and export it if present. If it's not present, we -// don't want to throw an exception immediately because acceptance tests don't -// need it, so we export a `wait` function that throws an exception if and when -// it's called. -// -// Once we drop support for pre-RFC268 tests, including all calls to `wait`, we -// can delete this file and import `@ember/test-helpers` directly. -// - -// When a module imports `require`, it gets a dynamically generated module that -// handles relative imports correctly, so there's no way to get at it to stub it -// from another module/test. So instead we use the global require, which is only -// available via window.require, so our tests can stub it out. -const { require } = window; - -let helpers; -let waitFn; - -if (require.has('@ember/test-helpers')) { - helpers = require('@ember/test-helpers'); -} else { - helpers = { - getContext() { - return null; - } - }; -} - -if (require.has('ember-test-helpers/wait')) { - // This is implemented as a function that calls `ember-test-helpers/wait` - // rather than just assigning `helpers.wait = require(...).default` because - // since this code executes while modules are initially loading, under certain - // conditions `ember-test-helpers/wait` can still be in the pending state - // at this point, so its exports are still undefined. - waitFn = (...args) => require('ember-test-helpers/wait').default(...args); -} else { - waitFn = () => { - throw new Error('ember-test-helpers or @ember/test-helpers must be installed'); - }; -} - -export function getContext(...args) { - return helpers.getContext(...args); -} -export function getRootElement(...args) { - return helpers.getRootElement(...args); -} -export function visit(...args) { - return helpers.visit(...args); -} -export function click(...args) { - return helpers.click(...args); -} -export function fillIn(...args) { - return helpers.fillIn(...args); -} -export function triggerEvent(...args) { - return helpers.triggerEvent(...args); -} -export function triggerKeyEvent(...args) { - return helpers.triggerKeyEvent(...args); -} -export function focus(...args) { - return helpers.focus(...args); -} -export function blur(...args) { - return helpers.blur(...args); -} -export let wait = waitFn; diff --git a/addon-test-support/-private/context.js b/addon-test-support/-private/context.js deleted file mode 100644 index 23736589..00000000 --- a/addon-test-support/-private/context.js +++ /dev/null @@ -1,89 +0,0 @@ -import { deprecate } from '@ember/application/deprecations'; - -/** - * @public - * - * Render a component's template in the context of a test. - * - * Throws an error if a test's context has not been set on the page. - * - * Returns the page object, which allows for method chaining. - * - * @example - * - * page.setContext(this) - * .render(hbs`{{my-component}}`) - * .clickOnText('Hi!'); - * - * @param {Object} template - A compiled component template - * @return {PageObject} - the page object - */ -export function render(template) { - deprecate('PageObject.render() is deprecated. Please use "htmlbars-inline-precompile" instead.', false, { - id: 'ember-cli-page-object.page-render', - until: '2.0.0', - url: 'https://ember-cli-page-object.js.org/docs/v1.16.x/deprecations/#page-render' - }); - - if (!this.context) { - let message = 'You must set a context on the page object before calling calling `render()`'; - let error = new Error(message); - - throw error; - } - - this.context.render(template); - - return this; -} - -/** - * @public - * - * Sets the page's test context. - * - * Returns the page object, which allows for method chaining. - * - * @example - * - * page.setContext(this) - * .render(hbs`{{my-component}}`) - * .clickOnText('Hi!'); - * - * @param {Object} context - A component integration test's `this` context - * @return {PageObject} - the page object - */ -export function setContext(context) { - deprecate('setContext() is deprecated. Please make sure you use "@ember/test-helpers" of v1 or higher.', false, { - id: 'ember-cli-page-object.set-context', - until: '2.0.0', - url: 'https://ember-cli-page-object.js.org/docs/v1.16.x/deprecations/#set-context', - }); - - if (context) { - this.context = context; - } - - return this; -} - -/** - * @public - * - * Unsets the page's test context. - * - * Useful in a component test's `afterEach()` hook, to make sure the context has been cleared after each test. - * - * @example - * - * page.removeContext(); - * - * @return {PageObject} - the page object - */ -export function removeContext() { - if (this.context) { - delete this.context; - } - - return this; -} diff --git a/addon-test-support/-private/execution_context.js b/addon-test-support/-private/execution_context.js deleted file mode 100644 index 1f023d58..00000000 --- a/addon-test-support/-private/execution_context.js +++ /dev/null @@ -1,94 +0,0 @@ -import { getContext as getIntegrationTestContext } from './helpers'; -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'; - -const executioncontexts = { - acceptance: AcceptanceExecutionContext, - integration: IntegrationExecutionContext, - rfc268: Rfc268Context -}; - -/* - * @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 (supportsRfc268()) { - contextName = 'rfc268'; - } else if (isAcceptanceTest()) { - contextName = 'acceptance'; - } else { - throw new Error(`Looks like you attempt to access page object property outside of test context. -If that's not the case, please make sure you use the latest version of "@ember/test-helpers".`); - } - - return new executioncontexts[contextName](pageObjectNode, integrationTestContext); -} - -/** - * @private - */ -function isAcceptanceTest() { - return window.visit && window.andThen; -} - -/** - * @private - */ -export function supportsRfc268() { - // `getContext()` returns: - // - falsey, if @ember/test-helpers is not available (stubbed in - // compatibility.js) - // - falsey, if @ember/test-helpers is available but none of the - // `ember-qunit` setupTest() methods has been called (e.g., - // `setupRenderingTest()`) - // - truthy, if @ember/test-helpers is available and one of the `ember-qunit` - // setupTest() methods has been called. - // - // Note that if `page.setContext(this)` has been called, we'll never get here - // and will just be running with the integration context (even if the test is - // an RFC268 test). - let hasValidTestContext = Boolean(getEmberTestHelpersContext()); - if (!hasValidTestContext) { - return false; - } - - // There are a few versions of `@ember/test-helpers` that have support for - // `ember-qunit`'s `setupRenderingTest()` method, but do not have the DOM - // helpers (`click`, `fillIn`, etc.) that the RFC268 execution context uses. - // `visit` was the last helper to be added to `@ember/test-helpers`, so we - // check for it, and if we can't find it, we can't use the RFC268 execution - // context, so we throw an exception. - let hasExpectedTestHelpers = Boolean(visit); - if (!hasExpectedTestHelpers) { - throw new Error([ - 'You are trying to use ember-cli-page-object with RFC232/RFC268 support', - '(setupRenderingContext()/setupApplicationContext()) which requires at', - 'least ember-qunit@3.2.0 or ember-mocha@0.13.0-beta.3.' - ].join()); - } - - return true; -} - -/* - * @private - */ -export function register(type, definition) { - executioncontexts[type] = definition; -} diff --git a/addon-test-support/-private/execution_context/acceptance-native-events.js b/addon-test-support/-private/execution_context/acceptance-native-events.js deleted file mode 100644 index ed0df848..00000000 --- a/addon-test-support/-private/execution_context/acceptance-native-events.js +++ /dev/null @@ -1,19 +0,0 @@ -import { visit } from 'ember-native-dom-helpers'; -import ExecutionContext from './native-events-context'; -import { wait } from '../compatibility'; - -export default function AcceptanceNativeEventsExecutionContext(pageObjectNode) { - ExecutionContext.call(this, pageObjectNode); -} - -AcceptanceNativeEventsExecutionContext.prototype = Object.create(ExecutionContext.prototype); - -AcceptanceNativeEventsExecutionContext.prototype.visit = function() { - visit(...arguments); -}; - -AcceptanceNativeEventsExecutionContext.prototype.andThen = function(cb) { - return (window.wait || wait)().then(() => { - cb(this); - }); -} diff --git a/addon-test-support/-private/execution_context/integration-native-events.js b/addon-test-support/-private/execution_context/integration-native-events.js deleted file mode 100644 index f2c33b5d..00000000 --- a/addon-test-support/-private/execution_context/integration-native-events.js +++ /dev/null @@ -1,19 +0,0 @@ -import { run } from '@ember/runloop'; -import ExecutionContext from './native-events-context'; -import wait from 'ember-test-helpers/wait'; - -export default function IntegrationNativeEventsExecutionContext(pageObjectNode, testContext) { - ExecutionContext.call(this, pageObjectNode, testContext); -} - -IntegrationNativeEventsExecutionContext.prototype = Object.create(ExecutionContext.prototype); - -IntegrationNativeEventsExecutionContext.prototype.visit = function() {}; - -IntegrationNativeEventsExecutionContext.prototype.andThen = function(cb) { - run(() => { - cb(this); - }); - - return wait(); -}; diff --git a/addon-test-support/-private/finders.js b/addon-test-support/-private/finders.js index c30928bf..455b90f3 100644 --- a/addon-test-support/-private/finders.js +++ b/addon-test-support/-private/finders.js @@ -4,13 +4,13 @@ import { findClosestValue, guardMultiple } from './helpers'; -import { getExecutionContext } from './execution_context'; +import { getAdapter } from '../adapters'; import { throwBetterError, ELEMENT_NOT_FOUND } from './better-errors'; function getContainer(pageObjectNode, options) { return options.testContainer || findClosestValue(pageObjectNode, 'testContainer') - || getExecutionContext(pageObjectNode).testContainer; + || getAdapter().testContainer; } /** diff --git a/addon-test-support/-private/helpers.js b/addon-test-support/-private/helpers.js index 491b3286..08fa9826 100644 --- a/addon-test-support/-private/helpers.js +++ b/addon-test-support/-private/helpers.js @@ -4,7 +4,6 @@ import { get } from '@ember/object'; import { isPresent } from '@ember/utils'; import Ceibo from 'ceibo'; import { deprecate } from '@ember/application/deprecations'; -import { getContext as getEmberTestHelpersContext } from './compatibility'; import $ from '-jquery'; @@ -166,30 +165,6 @@ export function getRoot(node) { return root; } -/** - * @public - * - * Return a test context if one was provided during `create()` or via `setContext()` - * - * @param {Ceibo} node - Node of the tree - * @return {Object} `moduleForComponent` test's `this` context, or null - */ -export function getContext(node) { - let root = getRoot(node); - let { context } = root; - - if (typeof context === 'object' && context !== null && typeof context.$ === 'function') { - return context; - } - - context = getEmberTestHelpersContext(); - if (typeof context === 'object' && context !== null && typeof context.$ === 'function' && !context.element) { - return context - } - - return null; -} - function getAllValuesForProperty(node, property) { let iterator = node; let values = []; diff --git a/addon-test-support/-private/run.js b/addon-test-support/-private/run.js new file mode 100644 index 00000000..d3b06d87 --- /dev/null +++ b/addon-test-support/-private/run.js @@ -0,0 +1,54 @@ +import { resolve } from 'rsvp'; +import { getAdapter } from '../adapters'; +import { getRoot } from './helpers'; +import { throwContextualError } from './better-errors'; +import { chainable, isChainedNode } from './chainable'; + +/** + * Run action + * + * @param {Ceibo} node Page object node to run action on + * @param {object} query + * @param {Function} cb Some async activity callback + * @returns {Ceibo} + */ +export function run(node, query, cb) { + const adapter = getAdapter(); + + const executionContext = Object.freeze({ + query, + node, + adapter + }); + + const root = getRoot(node); + if (isChainedNode(node)) { + // 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(executionContext, 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(executionContext, cb); + + return chainable(node); + } +} + +function invokeHelper(context, cb) { + let res; + + try { + res = cb(context); + } catch (e) { + throwContextualError(context, e); + } + + return resolve(res).catch((e) => { + throwContextualError(context, e); + }); +} diff --git a/addon-test-support/adapter.js b/addon-test-support/adapter.js new file mode 100644 index 00000000..e3e8c224 --- /dev/null +++ b/addon-test-support/adapter.js @@ -0,0 +1,23 @@ +export default class Adapter { + get testContainer() { + throw new Error('`testContainer` is not implemented for the adater'); + } + visit( /* path */) { + throw new Error('`visit` is not implemented for the adater'); + } + click( /* element */) { + throw new Error('`click` is not implemented for the adater'); + } + fillIn( /*element, content*/) { + throw new Error('`fillIn` is not implemented for the adater'); + } + triggerEvent( /*element, eventName, eventOptions*/) { + throw new Error('`triggerEvent` is not implemented for the adater'); + } + focus( /* element */) { + throw new Error('`focus` is not implemented for the adater'); + } + blur( /* element */) { + throw new Error('`blur` is not implemented for the adater'); + } +} diff --git a/addon-test-support/adapters/acceptance-native-events.js b/addon-test-support/adapters/acceptance-native-events.js new file mode 100644 index 00000000..44f79ea7 --- /dev/null +++ b/addon-test-support/adapters/acceptance-native-events.js @@ -0,0 +1,14 @@ +import { visit } from 'ember-native-dom-helpers'; +import Adapter from './native-events'; + +export default class AcceptanceNativeEventsAdapter extends Adapter { + visit() { + visit(...arguments); + + return this.wait(); + } + + wait() { + return (window.wait || super.wait)(); + } +} diff --git a/addon-test-support/-private/execution_context/acceptance.js b/addon-test-support/adapters/acceptance.js similarity index 71% rename from addon-test-support/-private/execution_context/acceptance.js rename to addon-test-support/adapters/acceptance.js index 81609f7d..4aa61778 100644 --- a/addon-test-support/-private/execution_context/acceptance.js +++ b/addon-test-support/adapters/acceptance.js @@ -4,30 +4,30 @@ import { assertFocusable } from './helpers'; -export default function AcceptanceExecutionContext(pageObjectNode) { - this.pageObjectNode = pageObjectNode; -} +import Adapter from "../adapter"; -AcceptanceExecutionContext.prototype = { +export default class AcceptanceAdapter extends Adapter { get testContainer() { return '#ember-testing'; - }, + } - andThen(cb) { - return window.wait().then(() => { - cb(this); - }); - }, + wait() { + return window.wait(); + } visit(path) { /* global visit */ visit(path); - }, + + return this.wait(); + } click(element) { /* global click */ click(element); - }, + + return this.wait(); + } fillIn(element, content) { /* global focus */ @@ -38,22 +38,30 @@ AcceptanceExecutionContext.prototype = { /* global triggerEvent */ triggerEvent(element, 'input'); triggerEvent(element, 'change'); - }, + + return this.wait(); + } triggerEvent(element, eventName, eventOptions) { /* global triggerEvent */ triggerEvent(element, eventName, eventOptions); - }, + + return this.wait(); + } focus(element) { assertFocusable(element); $(element).focus(); - }, + + return this.wait(); + } blur(element) { assertFocusable(element); $(element).blur(); + + return this.wait(); } -}; +} diff --git a/addon-test-support/-private/execution_context/helpers.js b/addon-test-support/adapters/helpers.js similarity index 100% rename from addon-test-support/-private/execution_context/helpers.js rename to addon-test-support/adapters/helpers.js diff --git a/addon-test-support/adapters/index.js b/addon-test-support/adapters/index.js new file mode 100644 index 00000000..4ff21aaf --- /dev/null +++ b/addon-test-support/adapters/index.js @@ -0,0 +1,30 @@ +import Adapter from "../adapter"; + +let _adapter; + +/* + * @private + */ +export function getAdapter() { + if (!_adapter) { + throw new Error(`Adapter is required. + +Please use \`setAdapter(\`, to instruct "ember-cli-page-object" about the adapter you want to use.`); + } + + return _adapter; +} + +export function setAdapter(adapter) { + if (adapter === null) { + _adapter = null; + + return; + } + + if (false === adapter instanceof Adapter) { + throw new Error('Invalid adapter type'); + } + + _adapter = adapter; +} diff --git a/addon-test-support/adapters/integration-native-events.js b/addon-test-support/adapters/integration-native-events.js new file mode 100644 index 00000000..ab19157f --- /dev/null +++ b/addon-test-support/adapters/integration-native-events.js @@ -0,0 +1,3 @@ +import Adapter from './native-events'; + +export default class IntegrationNativeEventsAdapter extends Adapter {} diff --git a/addon-test-support/-private/execution_context/integration.js b/addon-test-support/adapters/integration.js similarity index 69% rename from addon-test-support/-private/execution_context/integration.js rename to addon-test-support/adapters/integration.js index 575e5d69..fbf4e303 100644 --- a/addon-test-support/-private/execution_context/integration.js +++ b/addon-test-support/adapters/integration.js @@ -1,60 +1,59 @@ import $ from '-jquery'; -import { run } from '@ember/runloop'; import { fillElement, assertFocusable } from './helpers'; import wait from 'ember-test-helpers/wait'; +import Adapter from "../adapter"; -export default function IntegrationExecutionContext(pageObjectNode, testContext) { - this.pageObjectNode = pageObjectNode; - this.testContext = testContext; -} - -IntegrationExecutionContext.prototype = { +export default class IntegrationAdapter extends Adapter { get testContainer() { // @todo: fix usage of private `_element` return this.testContext && this.testContext._element ? this.testContext._element : '#ember-testing'; - }, - - andThen(cb) { - run(() => { - cb(this) - }); + } + wait() { return wait(); - }, - - visit() {}, + } click(element) { $(element).click(); - }, + + return this.wait(); + } fillIn(element, content) { fillElement(element, content); $(element).trigger('input'); $(element).change(); - }, + + return this.wait(); + } triggerEvent(element, eventName, eventOptions) { let event = $.Event(eventName, eventOptions); $(element).trigger(event); - }, + + return this.wait(); + } focus(element) { assertFocusable(element); $(element).focus(); - }, + + return this.wait(); + } blur(element) { assertFocusable(element); $(element).blur(); - }, -}; + + return this.wait(); + } +} diff --git a/addon-test-support/-private/execution_context/native-events-context.js b/addon-test-support/adapters/native-events.js similarity index 58% rename from addon-test-support/-private/execution_context/native-events-context.js rename to addon-test-support/adapters/native-events.js index f2daf8fb..c71387bc 100644 --- a/addon-test-support/-private/execution_context/native-events-context.js +++ b/addon-test-support/adapters/native-events.js @@ -6,29 +6,43 @@ import { blur } from 'ember-native-dom-helpers'; -import { - fillElement, - assertFocusable -} from './helpers'; - -const KEYBOARD_EVENT_TYPES = ['keydown', 'keypress', 'keyup']; +import { fillElement, assertFocusable } from './helpers'; +import Adapter from "../adapter"; -export default function ExecutionContext(pageObjectNode, testContext) { - this.pageObjectNode = pageObjectNode; - this.testContext = testContext; +const { require } = window; +let waitFn; +if (require.has('ember-test-helpers/wait')) { + // This is implemented as a function that calls `ember-test-helpers/wait` + // rather than just assigning `helpers.wait = require(...).default` because + // since this code executes while modules are initially loading, under certain + // conditions `ember-test-helpers/wait` can still be in the pending state + // at this point, so its exports are still undefined. + waitFn = (...args) => require('ember-test-helpers/wait').default(...args); +} else { + waitFn = () => { + throw new Error('ember-test-helpers or @ember/test-helpers must be installed'); + }; } -ExecutionContext.prototype = { +const KEYBOARD_EVENT_TYPES = ['keydown', 'keypress', 'keyup']; + +export default class NativeEventsAdapter extends Adapter { get testContainer() { // @todo: fix usage of private `_element` return this.testContext && this.testContext._element ? this.testContext._element : '#ember-testing'; - }, + } + + wait() { + return waitFn(); + } click(element) { click(element); - }, + + return this.wait(); + } fillIn(element, content) { @@ -36,7 +50,9 @@ ExecutionContext.prototype = { triggerEvent(element, 'input'); triggerEvent(element, 'change'); - }, + + return this.wait(); + } triggerEvent(element, eventName, eventOptions) { // `keyCode` is a deprecated property. @@ -52,18 +68,23 @@ ExecutionContext.prototype = { } else { triggerEvent(element, eventName, eventOptions); } - }, + + return this.wait(); + } focus(element) { assertFocusable(element); focus(element); - }, + + return this.wait(); + } blur(element) { assertFocusable(element); blur(element); - } -}; + return this.wait(); + } +} diff --git a/addon-test-support/-private/execution_context/rfc268.js b/addon-test-support/adapters/rfc268.js similarity index 79% rename from addon-test-support/-private/execution_context/rfc268.js rename to addon-test-support/adapters/rfc268.js index 88f9b6c8..072a8121 100644 --- a/addon-test-support/-private/execution_context/rfc268.js +++ b/addon-test-support/adapters/rfc268.js @@ -7,28 +7,25 @@ import { triggerKeyEvent, focus, blur -} from '../compatibility'; +} from '@ember/test-helpers'; +import Adapter from "../adapter"; -export default function ExecutionContext(pageObjectNode) { - this.pageObjectNode = pageObjectNode; -} - -ExecutionContext.prototype = { +export default class RFC268Adapter extends Adapter { get testContainer() { return getRootElement(); - }, + } visit(path) { return visit(path); - }, + } click(element) { return click(element); - }, + } fillIn(element, content) { return fillIn(element, content); - }, + } triggerEvent(element, eventName, eventOptions) { if (typeof eventOptions.key !== 'undefined' || typeof eventOptions.keyCode !== 'undefined') { @@ -38,13 +35,13 @@ ExecutionContext.prototype = { } return triggerEvent(element, eventName, eventOptions); - }, + } focus(element) { return focus(element); - }, + } blur(element) { return blur(element); } -}; +} diff --git a/addon-test-support/create.js b/addon-test-support/create.js index cf0ccdbd..94e406b7 100644 --- a/addon-test-support/create.js +++ b/addon-test-support/create.js @@ -1,6 +1,5 @@ import Ceibo from 'ceibo'; import { deprecate } from '@ember/application/deprecations'; -import { render, setContext, removeContext } from './-private/context'; import { assign, getPageObjectDefinition, isPageObject, storePageObjectDefinition } from './-private/helpers'; import { visitable } from './properties/visitable'; import dsl from './-private/dsl'; @@ -202,14 +201,18 @@ export function create(definitionOrUrl, definitionOrOptions, optionsOrNothing) { options = definitionOrOptions || {}; } - let { context } = definition; // in the instance where the definition is a page object, we must use the stored definition directly // or else we will fire off the Ceibo created getters which will error definition = isPageObject(definition) ? assign({}, getPageObjectDefinition(definition)) : assignDescriptors({}, definition); - delete definition.context; + if (definition.context) { + // this is supposed to prevent an infinite recursion, for users who has not migrated + // from the ModuleForComponent tests yet. + // @todo: cover by test + throw new Error('"context" key is not allowed to be passed at definition root.'); + } deprecate('Passing an URL argument to `create()` is deprecated', typeof url !== 'string', { id: 'ember-cli-page-object.create-url-argument', @@ -241,17 +244,5 @@ export function create(definitionOrUrl, definitionOrOptions, optionsOrNothing) { object: buildObject }; - let page = Ceibo.create(definition, assign({ builder }, options)); - - if (page) { - page.render = render; - page.setContext = setContext; - page.removeContext = removeContext; - - if (typeof context !== 'undefined') { - page.setContext(context); - } - } - - return page; + return Ceibo.create(definition, assign({ builder }, options)); } diff --git a/addon-test-support/extend/index.js b/addon-test-support/extend/index.js index 775bb651..640a73d5 100644 --- a/addon-test-support/extend/index.js +++ b/addon-test-support/extend/index.js @@ -2,24 +2,4 @@ export { findElement } from './find-element'; export { findElementWithAssert } from './find-element-with-assert'; export { findOne } from './find-one'; export { findMany } from './find-many'; -export { buildSelector, getContext, fullScope } from '../-private/helpers'; -import { - register as registerExecutionContext -} from '../-private/execution_context'; - -import IntegrationNativeEventsContext from '../-private/execution_context/integration-native-events'; -import AcceptanceNativeEventsContext from '../-private/execution_context/acceptance-native-events'; -import IntegrationEmberContext from '../-private/execution_context/integration'; -import AcceptanceEmberContext from '../-private/execution_context/acceptance'; - -function useNativeEvents(flag = true) { - if (flag) { - registerExecutionContext('integration', IntegrationNativeEventsContext); - registerExecutionContext('acceptance', AcceptanceNativeEventsContext); - } else { - registerExecutionContext('integration', IntegrationEmberContext); - registerExecutionContext('acceptance', AcceptanceEmberContext); - } -} - -export { registerExecutionContext, useNativeEvents }; +export { buildSelector, fullScope } from '../-private/helpers'; diff --git a/addon-test-support/index.js b/addon-test-support/index.js index c44140ef..8e6d7d48 100644 --- a/addon-test-support/index.js +++ b/addon-test-support/index.js @@ -22,7 +22,7 @@ import { visitable } from './properties/visitable'; export { visitable }; export { findElement } from './extend/find-element'; export { findElementWithAssert } from './extend/find-element-with-assert'; -export { buildSelector, getContext } from './-private/helpers'; +export { buildSelector } from './-private/helpers'; export default { attribute, diff --git a/addon-test-support/macros/alias.js b/addon-test-support/macros/alias.js index 482d35d9..8d058279 100644 --- a/addon-test-support/macros/alias.js +++ b/addon-test-support/macros/alias.js @@ -3,8 +3,7 @@ import { getProperty, objectHasProperty } from '../-private/helpers'; -import { chainable } from '../-private/action' -import { getExecutionContext } from '../-private/execution_context' +import { chainable } from "../-private/chainable"; 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); }; } }; diff --git a/config/ember-try.js b/config/ember-try.js index 4d4d7823..f8da378f 100644 --- a/config/ember-try.js +++ b/config/ember-try.js @@ -22,7 +22,9 @@ module.exports = function() { }, npm: { devDependencies: { - 'ember-source': null + 'ember-source': null, + 'ember-cli-qunit': '^4.0.0', + 'ember-qunit': null } } }, @@ -46,7 +48,9 @@ module.exports = function() { name: 'ember-lts-2.12', npm: { devDependencies: { - 'ember-source': '~2.12.0' + 'ember-source': '~2.12.0', + 'ember-cli-qunit': '^4.0.0', + 'ember-qunit': null } } }, @@ -58,7 +62,9 @@ module.exports = function() { npm: { devDependencies: { '@ember/jquery': '^0.5.1', - 'ember-source': '~2.16.0' + 'ember-source': '~2.16.0', + 'ember-cli-qunit': '^4.0.0', + 'ember-qunit': null } } }, diff --git a/guides/native-events.md b/guides/native-events.md deleted file mode 100644 index 7ac15f9f..00000000 --- a/guides/native-events.md +++ /dev/null @@ -1,51 +0,0 @@ ---- -layout: page -title: Native Events Mode ---- - -{% raw %} -By default, `ember-cli-page-object` uses global ember test helpers such as `click`, `fillIn`, `find`, etc. -While it works great, this approach has one downside: global ember test helpers require `jQuery` to be bundled within your `Ember` global. - -As a result, if you want to drop a dependency on `jQuery` in your app or addon, you won't be able to use the standard `Ember` test helpers. - -In order to solve this problem, `ember-cli-page-object` provides an integration with [`ember-native-dom-helpers`](https://github.com/cibernox/ember-native-dom-helpers). - -In general, `native-events` mode doesn't require you to rewrite your existing page object declarations. However, you should take into account that with `native-events` mode enabled, your test suite triggers real DOM events instead of `jQuery` alternatives. - -## Usage - -You can enable `native-events` mode by simply adding this snippet into your `test-helper.js`: - -```js -// tests/test-helper.js -import { useNativeEvents } from 'ember-cli-page-object/extend'; -... - -useNativeEvents(); -``` - -## Migration from jQuery events to native DOM events - -If you want to use `native-events` mode in your test suite, you have to ensure that your app is ready to handle native DOM events rather than jQuery events. - -Consider a component event handler like this: - -```js -export default Component.extend({ - doubleClick() { - set(this, "doubleClicked", true); - return true; - } -}) -``` - -`native-events` mode won't work out of the box with a handler like this. The native double-click won't have any effect on the component because Ember's event dispatcher handles events via jQuery. - -In order to fix this, you should replace the default event dispatcher with `ember-native-dom-event-dispatcher`: - -```sh -npm i --save-dev ember-native-dom-event-dispatcher -``` - -{% endraw %} diff --git a/index.js b/index.js index 3ef7b161..2c8f8508 100644 --- a/index.js +++ b/index.js @@ -46,17 +46,33 @@ module.exports = { // `import { clickable } from 'ember-cli-page-object/test-support';` // // which is a default behavior in ember-cli - const reexportsTree = mergeTrees([ - 'index', - 'extend', - 'macros', - '-private/execution_context' // @see: https://github.com/san650/ember-cli-page-object/pull/400#issuecomment-384021927 - ].map(publicModuleName => - writeFile( - `/${this.moduleName()}/${publicModuleName}.js`, - `export * from '${this.moduleName()}/test-support/${publicModuleName}';` + const reexportsTree = mergeTrees( + [ + 'index', + 'extend', + 'macros', + 'adapter', + 'adapters', + ].map(publicModuleName => + writeFile( + `/${this.moduleName()}/${publicModuleName}.js`, + `export * from '${this.moduleName()}/test-support/${publicModuleName}';` + ) + ).concat( + [ + 'adapters/acceptance-native-events', + 'adapters/acceptance', + 'adapters/integration-native-events', + 'adapters/integration', + 'adapters/rfc268', + ].map(publicModuleName => + writeFile( + `/${this.moduleName()}/${publicModuleName}.js`, + `export { default } from '${this.moduleName()}/test-support/${publicModuleName}';` + ) + ) ) - )); + ); return mergeTrees([ testSupportTree, diff --git a/tests/acceptance/actions-test.ts b/tests/acceptance/actions-test.ts index d06dea24..ba1cdd5a 100644 --- a/tests/acceptance/actions-test.ts +++ b/tests/acceptance/actions-test.ts @@ -83,7 +83,7 @@ test('fill in by attribute', async function(assert) { fillIn: fillable() }); - page.visit(); + await page.visit(); await page .fillIn('input1', 'input 1') diff --git a/tests/acceptance/rfc268-actions-test.js b/tests/acceptance/rfc268-actions-test.js index 03ce7426..8d321dd8 100644 --- a/tests/acceptance/rfc268-actions-test.js +++ b/tests/acceptance/rfc268-actions-test.js @@ -1,26 +1,26 @@ import { module, test } from 'qunit'; -import { setupApplicationTest } from 'ember-qunit'; import require from 'require'; -import PageObject from '../page-object'; +import { + clickable, + clickOnText, + collection, + create, + fillable, + isVisible, + value, + visitable +} from 'ember-cli-page-object'; import { alias } from 'ember-cli-page-object/macros'; +import { setupApplicationTest } from 'dummy/tests/helpers' + if (require.has('@ember/test-helpers')) { const { settled, waitUntil } = require('@ember/test-helpers'); module('Acceptance | actions [rfc268]', function(hooks) { setupApplicationTest(hooks); - let { - clickOnText, - clickable, - collection, - fillable, - isVisible, - value, - visitable - } = PageObject; - - let page = PageObject.create({ + let page = create({ visit: visitable('/calculator'), keys: { clickOn: clickOnText('.numbers'), @@ -231,7 +231,7 @@ if (require.has('@ember/test-helpers')) { }); test('fill in by attribute', async function(assert) { - let page = PageObject.create({ + let page = create({ visit: visitable('/inputs'), fillIn: fillable() }); diff --git a/tests/acceptance/rfc268-default-properties-test.ts b/tests/acceptance/rfc268-default-properties-test.ts index 4ba64ea9..ece5dee8 100644 --- a/tests/acceptance/rfc268-default-properties-test.ts +++ b/tests/acceptance/rfc268-default-properties-test.ts @@ -1,5 +1,5 @@ import { module, test } from 'qunit'; -import { setupApplicationTest } from 'ember-qunit'; +import { setupApplicationTest } from '../helpers'; import PageObject from 'ember-cli-page-object'; import require from 'require'; diff --git a/tests/acceptance/rfc268-test.js b/tests/acceptance/rfc268-test.js new file mode 100644 index 00000000..5d2f518b --- /dev/null +++ b/tests/acceptance/rfc268-test.js @@ -0,0 +1,33 @@ +import { module, test } from 'qunit'; +import require from 'require'; +import { setAdapter } from 'ember-cli-page-object/adapters'; +import { create, value, visitable } from 'ember-cli-page-object'; + +if (require.has('@ember/test-helpers')) { + const { setupApplicationTest } = require('ember-qunit'); + + const Rfc268Adapter = require('ember-cli-page-object/adapters/rfc268').default; + + module('Acceptance | rfc268', function(hooks) { + setupApplicationTest(hooks); + + hooks.beforeEach(function() { + setAdapter(new Rfc268Adapter); + }) + + hooks.afterEach(function() { + setAdapter(null); + }) + + let page = create({ + visit: visitable('/calculator'), + screen: value('.screen input'), + }); + + test('it works', async function(assert) { + await page.visit(); + + assert.equal(page.screen, ''); + }); + }); +} diff --git a/tests/expect-ember-error.d.ts b/tests/expect-ember-error.d.ts deleted file mode 100644 index 83997bee..00000000 --- a/tests/expect-ember-error.d.ts +++ /dev/null @@ -1 +0,0 @@ -export default function expectEmberError(assert: Assert, callback: () => void, matcher: RegExp, message: string): void; diff --git a/tests/expect-ember-error.js b/tests/expect-ember-error.js deleted file mode 100644 index 18fa7628..00000000 --- a/tests/expect-ember-error.js +++ /dev/null @@ -1,43 +0,0 @@ - -import { run } from '@ember/runloop'; -import { VERSION as EmberVersion } from '@ember/version'; -import Ember from 'ember'; - -// ember@2.11.3 introduces a breaking change in how the errors are propagated -// in backburner which causes some test that were previously working, to fail. -// -// See https://github.com/emberjs/ember.js/pull/14898#issuecomment-285510703 - -// Starting from ember@2.17 this regression has been fixed -// -// See https://github.com/emberjs/ember.js/pull/15871 -function useHack() { - var [major, minor] = EmberVersion.split('.'); - major = Number(major); - minor = Number(minor); - - return (major === 2 && minor >= 11 && minor < 17); -} - -export default function expectEmberError(assert, callback, matcher, message) { - if (useHack()) { - let origTestAdapter = Ember.Test.adapter; - let testError; - let TestAdapter = Ember.Test.QUnitAdapter.extend({ - exception(error) { - testError = error; - } - }); - - run(() => { Ember.Test.adapter = TestAdapter.create(); }); - callback(); - run(() => { - Ember.Test.adapter.destroy(); - }); - Ember.Test.adapter = origTestAdapter; - - assert.ok(testError && testError.message.match(matcher), message); - } else { - assert.throws(callback, matcher, message); - } -} diff --git a/tests/helpers/index.ts b/tests/helpers/index.ts new file mode 100644 index 00000000..e28d7a96 --- /dev/null +++ b/tests/helpers/index.ts @@ -0,0 +1,40 @@ +import { + setupApplicationTest as upstreamSetupApplicationTest, + setupRenderingTest as upstreamSetupRenderingTest, +} from 'ember-qunit'; + +// @ts-ignore +import { setAdapter } from 'ember-cli-page-object/test-support/adapters'; + +export function setupApplicationTest(hooks: NestedHooks) { + const Rfc268Adapter = requireRfc268Adapter(); + + upstreamSetupApplicationTest(hooks); + + hooks.beforeEach(function() { + setAdapter(new Rfc268Adapter()); + }); +} + +export function setupRenderingTest(hooks: NestedHooks) { + const Rfc268Adapter = requireRfc268Adapter(); + + upstreamSetupRenderingTest(hooks); + + hooks.beforeEach(function() { + setAdapter(new Rfc268Adapter()); + }); +} + +function requireRfc268Adapter() { + const { require } = window; + const hasRfc268 = ( require as any).has('@ember/test-helpers'); + + if (!hasRfc268) { + throw new Error(`"@ember/test-helpers" not installed.`) + } + + return require('ember-cli-page-object/test-support/adapters/rfc268').default; +} + +export { setupTest } from 'ember-qunit'; diff --git a/tests/helpers/module-for-acceptance.js b/tests/helpers/module-for-acceptance.js index cec42355..1981b63f 100644 --- a/tests/helpers/module-for-acceptance.js +++ b/tests/helpers/module-for-acceptance.js @@ -2,8 +2,10 @@ import { module } from 'qunit'; import { resolve } from 'rsvp'; import startApp from '../helpers/start-app'; import destroyApp from '../helpers/destroy-app'; -import { useNativeEvents } from 'ember-cli-page-object/extend'; import Ember from 'ember'; +import { setAdapter } from 'ember-cli-page-object/test-support/adapters'; +import ModuleForAcceptanceAdapter from 'ember-cli-page-object/test-support/adapters/acceptance'; +import ModuleForAcceptanceNativeDOMAdapter from 'ember-cli-page-object/test-support/adapters/acceptance-native-events'; export default function(name, options = {}) { [false, true].forEach(_useNativeEvents => { @@ -18,7 +20,11 @@ export default function(name, options = {}) { beforeEach() { this.application = startApp(); - useNativeEvents(_useNativeEvents); + if (_useNativeEvents) { + setAdapter(new ModuleForAcceptanceNativeDOMAdapter()); + } else { + setAdapter(new ModuleForAcceptanceAdapter()); + } if (options.beforeEach) { return options.beforeEach.apply(this, arguments); @@ -26,7 +32,7 @@ export default function(name, options = {}) { }, afterEach() { - useNativeEvents(false); + setAdapter(null); let afterEach = options.afterEach && options.afterEach.apply(this, arguments); return resolve(afterEach).then(() => destroyApp(this.application)); diff --git a/tests/helpers/properties.js b/tests/helpers/properties.js index 6ea4cb4b..1bdbd507 100644 --- a/tests/helpers/properties.js +++ b/tests/helpers/properties.js @@ -18,10 +18,13 @@ import { } from 'ember-qunit' import { module, test } from 'qunit'; -import { useNativeEvents } from 'ember-cli-page-object/extend'; - import Ember from 'ember'; import require from 'require'; +import { setAdapter } from 'ember-cli-page-object/test-support/adapters'; +import ModuleForAcceptanceAdapter from 'ember-cli-page-object/test-support/adapters/acceptance'; +import ModuleForIntegrationAdapter from 'ember-cli-page-object/test-support/adapters/integration'; +import ModuleForAcceptanceNativeDOMAdapter from 'ember-cli-page-object/test-support/adapters/acceptance-native-events'; +import ModuleForIntegrationNativeDOMAdapter from 'ember-cli-page-object/test-support/adapters/integration-native-events'; export function moduleForProperty(name, cbOrOptions, cb) { let options = cb ? cbOrOptions : {}; @@ -39,13 +42,17 @@ export function moduleForProperty(name, cbOrOptions, cb) { moduleForAcceptance(`${moduleNamePrefix} | Property | ${name}`, { beforeEach() { - useNativeEvents(_useNativeEvents); + if (_useNativeEvents) { + setAdapter(new ModuleForAcceptanceNativeDOMAdapter()); + } else { + setAdapter(new ModuleForAcceptanceAdapter()); + } this.adapter = new AcceptanceAdapter(this); }, afterEach() { - useNativeEvents(false); + setAdapter(null); } }); cb(testForAcceptance, 'acceptance'); @@ -61,12 +68,16 @@ export function moduleForProperty(name, cbOrOptions, cb) { moduleForIntegration('html-render', `${moduleNamePrefix} | Property | ${name}`, { integration: true, beforeEach() { - useNativeEvents(_useNativeEvents); + if (_useNativeEvents) { + setAdapter(new ModuleForIntegrationNativeDOMAdapter()); + } else { + setAdapter(new ModuleForIntegrationAdapter()); + } this.adapter = new IntegrationAdapter(this); }, afterEach() { - useNativeEvents(false); + setAdapter(null); } }); cb(testForIntegration, 'integration'); @@ -76,12 +87,15 @@ export function moduleForProperty(name, cbOrOptions, cb) { if (require.has('@ember/test-helpers')) { // Generate rfc268 tests + const Rfc268Adapter = require('ember-cli-page-object/test-support/adapters/rfc268').default; + module(`Application mode | Property | ${name}`, function(hooks) { setupApplicationTest(hooks); let adapter = new ApplicationAdapter(hooks); hooks.beforeEach(function() { this.adapter = adapter; + setAdapter(new Rfc268Adapter()); }); cb(test, 'application'); }); @@ -93,6 +107,7 @@ export function moduleForProperty(name, cbOrOptions, cb) { let adapter = new RenderingAdapter(hooks); hooks.beforeEach(function() { this.adapter = adapter; + setAdapter(new Rfc268Adapter()); }); cb(test, 'rendering'); }); diff --git a/tests/helpers/properties/acceptance-adapter.js b/tests/helpers/properties/acceptance-adapter.js index e758c04f..edcda17b 100644 --- a/tests/helpers/properties/acceptance-adapter.js +++ b/tests/helpers/properties/acceptance-adapter.js @@ -37,12 +37,20 @@ AcceptanceAdapter.prototype = { return window.currentURL(); }, - throws(assert, block, expected, message) { - let done = assert.async(); + async throws(assert, block, expected, message) { + let error; - block().then().catch((error) => { - assert.ok(expected.test(error.toString()), message); - }).finally(done); + try { + await block(); + } catch (e) { + error = e; + } + + assert.throws(() => { + if (error) { + throw error; + } + }, expected, message); }, async await() { diff --git a/tests/helpers/properties/integration-adapter.js b/tests/helpers/properties/integration-adapter.js index f8288d24..12c7e2be 100644 --- a/tests/helpers/properties/integration-adapter.js +++ b/tests/helpers/properties/integration-adapter.js @@ -1,9 +1,20 @@ import { run } from '@ember/runloop'; import $ from '-jquery'; export { moduleForComponent as moduleForIntegration, test as testForIntegration } from 'ember-qunit'; -import expectEmberError from '../../expect-ember-error'; import hbs from 'htmlbars-inline-precompile'; -import { supportsRfc268 } from 'ember-cli-page-object/test-support/-private/execution_context'; +import require from 'require'; + +function render(test, ...args) { + if ('render' in test) { + return test.render(...args); + } else if (require.has('@ember/test-helpers')) { + return require('@ember/test-helpers').render(...args); + } else if (require.has('ember-test-helpers')) { + return require('ember-test-helpers').getContext().render(...args); + } + + throw "can not figure out `render`" +} export function IntegrationAdapter(context) { this.context = context; @@ -16,7 +27,7 @@ IntegrationAdapter.prototype = { return $(selector, isAlternative ? '#alternate-ember-testing' : '#ember-testing'); }, - createTemplate(test, page, template, options) { + async createTemplate(test, page, template, options) { template = template || ''; if (!(test && page)) { @@ -32,16 +43,12 @@ IntegrationAdapter.prototype = { test.set('raw', template); } - if (!supportsRfc268()) { - page.setContext(test); - } - - this.context.render(hbs`{{html-render html=raw}}`); + await render(test, hbs`{{html-render html=raw}}`); }, throws(assert, block, expected, message) { run(() => { - expectEmberError(assert, block, expected, message); + assert.throws(block, expected, message); }); }, diff --git a/tests/integration/actions-test.ts b/tests/integration/actions-test.ts index 61eaf3c8..10599763 100644 --- a/tests/integration/actions-test.ts +++ b/tests/integration/actions-test.ts @@ -2,12 +2,11 @@ import 'qunit-dom'; import { run } from '@ember/runloop'; import $ from '-jquery'; import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from '../helpers'; import { createCalculatorTemplate, createInputsTemplate } from './test-helper'; -import expectEmberError from '../expect-ember-error'; import { alias } from 'ember-cli-page-object/macros'; import { @@ -98,14 +97,6 @@ if (require.has('@ember/test-helpers')) { module('Integration | actions', function(hooks) { setupRenderingTest(hooks); - hooks.beforeEach(function(this: any) { - ( page as any ).setContext(this); - }); - - hooks.afterEach(function() { - ( page as any ).removeContext(); - }); - test('Actions work when defined inside collections', async function(assert) { let template = createCalculatorTemplate(); @@ -263,16 +254,16 @@ if (require.has('@ember/test-helpers')) { assert.throws(() => page.nonExistant.attribute, message, 'attribute query did not throw an error'); }); run(() => { - expectEmberError(assert, () => page.nonExistant.clickOnText('qux'), message, 'clickOnText action did not throw an error'); + assert.throws(() => page.nonExistant.clickOnText('qux'), message, 'clickOnText action did not throw an error'); }); run(() => { - expectEmberError(assert, () => page.nonExistant.clickable(), message, 'clickable action did not throw an error'); + assert.throws(() => page.nonExistant.clickable(), message, 'clickable action did not throw an error'); }); run(() => { assert.throws(() => page.nonExistant.contains('something'), message, 'contains action did not throw an error'); }); run(() => { - expectEmberError(assert, () => page.nonExistant.fillable('baz'), message, 'fillable action did not throw an error'); + assert.throws(() => page.nonExistant.fillable('baz'), message, 'fillable action did not throw an error'); }); run(() => { assert.throws(() => page.nonExistant.hasClass, message, 'hasClass query did not throw an error'); @@ -307,7 +298,6 @@ if (require.has('@ember/test-helpers')) { $('#alternate-ember-testing').html(''); let page = create({ - context: this, clickOnText: clickOnText('button', { testContainer: '#alternate-ember-testing' }), clickable: clickable('button', { testContainer: '#alternate-ember-testing' }), fillable: fillable('input', { testContainer: '#alternate-ember-testing' }) @@ -325,7 +315,6 @@ if (require.has('@ember/test-helpers')) { $('#alternate-ember-testing').html(''); let page = create({ - context: this, testContainer: '#alternate-ember-testing', clickOnText: clickOnText('button'), clickable: clickable('button'), diff --git a/tests/integration/context-test.js b/tests/integration/context-test.js deleted file mode 100644 index c05316b3..00000000 --- a/tests/integration/context-test.js +++ /dev/null @@ -1,70 +0,0 @@ -import Ember from 'ember'; -import { moduleForComponent, test } from 'ember-qunit'; -import { createCalculatorTemplate } from './test-helper'; - -import PageObject from 'ember-cli-page-object'; - -if (Ember.hasOwnProperty('$')) { - moduleForComponent('calculating-device', 'Integration | context', { - integration: true - }); - - test('Test\'s `this` context\'s methods are accessible to the page object', function(assert) { - assert.expect(2); - - let page = PageObject.create({ - context: this - }); - - assert.ok(page.context); - - assert.deepEqual(this, page.context); - }); - - test('Test\'s `this.$()` is accessible by the page object', function(assert) { - assert.expect(2); - - let page = PageObject.create({ - context: this - }); - - this.render(createCalculatorTemplate()); - - assert.ok(page.context.$()); - assert.deepEqual(page.context.$(), this.$()); - }); - - test('`setContext(this)` and `removeContext()` set and remove the test context from the page', function(assert) { - assert.expect(3); - - let page = PageObject.create({}); - - assert.notOk(page.context); - - page.setContext(this); - - assert.deepEqual(page.context, this); - - page.removeContext(); - - assert.notOk(page.context); - }); - - test('`render()` throws an error when no context has been set', function(assert) { - assert.expect(2); - - let errorMessage; - - let page = PageObject.create({}); - - assert.notOk(page.context); - - assert.throws(function() { - page.render(createCalculatorTemplate()); - }, function(err) { - errorMessage = err.message; - - return errorMessage === 'You must set a context on the page object before calling calling `render()`'; - }, `render did not throw an error when no context was set. Actual message: ${errorMessage}`); - }); -} diff --git a/tests/integration/default-properties-test.ts b/tests/integration/default-properties-test.ts index 1721ce0c..af281a2f 100644 --- a/tests/integration/default-properties-test.ts +++ b/tests/integration/default-properties-test.ts @@ -1,5 +1,5 @@ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from '../helpers'; import { createCalculatorTemplate } from './test-helper'; import { create } from 'ember-cli-page-object'; @@ -13,8 +13,6 @@ if (require.has('@ember/test-helpers')) { test('Adds default properties', async function(assert) { let page = create({ - context: this, - one: { scope: '.numbers button:nth-of-type(1)' }, @@ -41,8 +39,6 @@ if (require.has('@ember/test-helpers')) { test('Overrides default properties', function(assert) { let page = create({ - context: this, - dummy: { click() { return 'click'; diff --git a/tests/integration/deprecations/comma-separated-selector-test.js b/tests/integration/deprecations/comma-separated-selector-test.js index af90c97b..819e9949 100644 --- a/tests/integration/deprecations/comma-separated-selector-test.js +++ b/tests/integration/deprecations/comma-separated-selector-test.js @@ -1,5 +1,5 @@ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from '../../helpers'; import hbs from 'htmlbars-inline-precompile'; import { create, text } from 'dummy/tests/page-object'; diff --git a/tests/integration/deprecations/page-render-test.js b/tests/integration/deprecations/page-render-test.js deleted file mode 100644 index 62c5736f..00000000 --- a/tests/integration/deprecations/page-render-test.js +++ /dev/null @@ -1,20 +0,0 @@ -import { moduleForComponent, test } from 'ember-qunit'; - -import hbs from 'htmlbars-inline-precompile'; -import { create } from 'ember-cli-page-object'; - -moduleForComponent('calculating-device', 'Deprecation | page.render()', { - integration: true, - - beforeEach() { - this.page = create({ - context: this - }); - } -}); - -test('page.render() leads to the deprecation', function(assert) { - this.page.render(hbs``); - - assert.expectDeprecation('PageObject.render() is deprecated. Please use "htmlbars-inline-precompile" instead.') -}); diff --git a/tests/integration/deprecations/set-context-test.js b/tests/integration/deprecations/set-context-test.js deleted file mode 100644 index 8daca5a2..00000000 --- a/tests/integration/deprecations/set-context-test.js +++ /dev/null @@ -1,23 +0,0 @@ -import { module, test } from 'qunit'; -import { create } from 'ember-cli-page-object'; -import require from 'require'; - -if (require.has('@ember/test-helpers')) { - const DEPRECATION_MESSAGE = 'setContext() is deprecated. Please make sure you use "@ember/test-helpers" of v1 or higher.'; - - module('Deprecation | setContext', function() { - test('passing page context to create is deprecated', function(assert) { - this.page = create().setContext(this); - - assert.expectDeprecation(DEPRECATION_MESSAGE) - }); - - test('passing page context to create is deprecated', function(assert) { - this.page = create({ - context: this - }); - - assert.expectDeprecation(DEPRECATION_MESSAGE) - }); - }); -} diff --git a/tests/integration/hooks-test.js b/tests/integration/hooks-test.js deleted file mode 100644 index 3d9b6a4f..00000000 --- a/tests/integration/hooks-test.js +++ /dev/null @@ -1,57 +0,0 @@ -import { moduleForComponent, test } from 'ember-qunit'; -import { createCalculatorTemplate } from './test-helper'; - -import PageObject from 'ember-cli-page-object'; -import Ember from 'ember'; - -const page = PageObject.create({}); - -let firstThis; -let secondThis; - -moduleForComponent('calculating-device', 'Integration | hooks', { - integration: true, - - beforeEach() { - page.setContext(this); - - if (!firstThis) { - firstThis = page.context; - } else { - secondThis = page.context; - } - }, - - afterEach() { - page.removeContext(); - } -}); - -if (Ember.hasOwnProperty('$')) { - test('When set in the `beforeEach()` qunit hook, test\'s `this` context\'s methods are accessible to the page object', function(assert) { - assert.expect(5); - - assert.ok(page.context); - assert.deepEqual(this, page.context); - assert.equal(page.context, firstThis); - - this.render(createCalculatorTemplate()); - - assert.ok(page.context.$()); - assert.deepEqual(page.context.$(), this.$()); - }); - - test('Setting the page\'s context in `beforeEach()` assigns the correct context in each test', function(assert) { - assert.expect(6); - - assert.ok(page.context); - assert.deepEqual(this, page.context); - assert.equal(page.context, secondThis); - assert.notEqual(page.context, firstThis); - - this.render(createCalculatorTemplate()); - - assert.ok(page.context.$()); - assert.deepEqual(page.context.$(), this.$()); - }); -} diff --git a/tests/unit/-private/action-test.js b/tests/unit/-private/action-test.js index 189245ce..c3b9d737 100644 --- a/tests/unit/-private/action-test.js +++ b/tests/unit/-private/action-test.js @@ -1,15 +1,17 @@ -import require from 'require'; import { module, test } from 'qunit'; -import { setupTest } from 'ember-qunit'; import { create } from 'ember-cli-page-object' import action from 'ember-cli-page-object/test-support/-private/action' import { isPageObject } from 'ember-cli-page-object/test-support/-private/helpers' +import { setAdapter } from 'ember-cli-page-object/test-support/adapters'; +import Adapter from 'ember-cli-page-object/test-support/adapter'; + +class DummyAdapter extends Adapter {} // Normally it should work with "0". // However, since Promises are not supported in IE11, for async we rely on "RSVP" library, // Unfortunatelly, for some of old "ember" versions there are timing lags coming from "RSVP", // so we use a value larger than "0" to appease out test matrix. -const DEFAULT_NEXT_TICK_TIMEOUT = 10; +const DEFAULT_NEXT_TICK_TIMEOUT = 20; const next = (timeout = DEFAULT_NEXT_TICK_TIMEOUT) => { return new Promise((r) => setTimeout(r, timeout)); @@ -26,112 +28,112 @@ class Deferred { } } -// Though we don't use adapters in the following tests, with the current implementation, -// we need `getExecutionContext(` to auto-detect some context, otherwise fails. -// So, for now, until Adapters is not a thing, we rely on implicit RFC268 execution -// context provoked by `setupTest(`. -// -// @todo: remove the guard once we have `setAdapter(` -if (require.has('@ember/test-helpers')) { - module('Unit | action', function(hooks) { - setupTest(hooks); - - let invoked, - finished, - executionContext; - - const testable = (query) => { - return action(query, function(id, deferred) { - invoked.push(id); - executionContext = this; +module('Unit | action', function(hooks) { + hooks.beforeEach(function() { + setAdapter(new DummyAdapter); + }); - return deferred.promise.then(() => { - finished.push(id); - }); - }) - } + let invoked, + finished, + executionContext; - hooks.beforeEach(function() { - invoked = []; - finished = []; - executionContext = null; - }) + const testable = (query) => { + return action(query, function(id, deferred) { + invoked.push(id); + executionContext = this; - test('it works', async function(assert) { - const p = create({ - scope: 'it works', - - run: testable({ selector: '.Selector' }) + return deferred.promise.then(() => { + finished.push(id); }); + }) + } - const d1 = new Deferred(); - p.run(1, d1); - - assert.equal(typeof executionContext === 'object' && executionContext !== null, true); - assert.deepEqual(executionContext.query, { - key: `run("1", "[object Object]")`, - selector: '.Selector' - }); - assert.equal(executionContext.node, p, ''); + hooks.beforeEach(function() { + invoked = []; + finished = []; + executionContext = null; + }) - // @todo: check adapter + test('it works', async function(assert) { + const p = create({ + scope: 'it works', - assert.deepEqual(invoked, [1]); - assert.deepEqual(finished, []); + run: testable({ selector: '.Selector' }) + }); - await d1.resolve(); + const d1 = new Deferred(); + p.run(1, d1); - assert.deepEqual(invoked, [1]); - assert.deepEqual(finished, [1]); + assert.equal(typeof executionContext === 'object' && executionContext !== null, true); + assert.deepEqual(executionContext.query, { + key: `run("1", "[object Object]")`, + selector: '.Selector' }); + assert.equal(executionContext.node, p, ''); + assert.equal(executionContext.adapter instanceof DummyAdapter, true) - test('this is frozen', async function(assert) { - const p = create({ - scope: 'it works', + assert.deepEqual(invoked, [1]); + assert.deepEqual(finished, []); - run: action(function() { - executionContext = this; - }) - }); + await d1.resolve(); + + assert.deepEqual(invoked, [1]); + assert.deepEqual(finished, [1]); + }); - await p.run(); + test('this is frozen', async function(assert) { + const p = create({ + scope: 'it works', - assert.throws(() => { - executionContext.test = 1; + run: action(function() { + executionContext = this; }) }); - test('it handles sync errors', async function(assert) { - const p = create({ - scope: '.Scope', + await p.run(); - run: action({ selector: '.Selector' }, function() { - throw new Error('it was so fast!'); - }) + assert.throws(() => { + executionContext.test = 1; + }) + }); + + test('it handles sync errors', async function(assert) { + const p = create({ + scope: '.Scope', + + run: action({ selector: '.Selector' }, function() { + throw new Error('it was so fast!'); }) + }) - assert.throws(() => p.run(1), new Error(`it was so fast! + assert.throws(() => p.run(1), new Error(`it was so fast! PageObject: 'page.run("1")' Selector: '.Scope .Selector'`)); - }); + }); - test('it handles sync errors w/o query', async function(assert) { - const p = create({ - scope: '.Scope', + test('it handles sync errors w/o query', async function(assert) { + const p = create({ + scope: '.Scope', - run: action(function() { - throw new Error('it was so fast!'); - }) + run: action(function() { + throw new Error('it was so fast!'); }) + }) - assert.throws(() => p.run(1), new Error(`it was so fast! + assert.throws(() => p.run(1), new Error(`it was so fast! PageObject: 'page.run("1")' Selector: '.Scope'`)); - }); + }); - test('it handles async errors', async function(assert) { + test('it handles async errors', async function(assert) { + // when test against some old `ember-cli-qunit`-only scenarios, QUnit@1 is installed. + // There is no `assert.rejects(` support, so we just ignore the test in such cases. + // It's still tested on newer scenarios with QUnit@2, even against old "ember-test-helpers" scenarios. + // + // @todo: remove after `ember-cli-qunit` support is removed. + if (typeof assert.rejects === 'function') { const p = create({ scope: '.Scope', @@ -146,165 +148,174 @@ PageObject: 'page.run("1")' PageObject: 'page.run("1")' Selector: '.Scope .Selector'`)); - }); + } else { + assert.expect(0); + } + }); - module('chainability', function() { - test('it works', async function(assert) { - const p = create({ - scope: '.root', + module('chainability', function() { + test('it works', async function(assert) { + const p = create({ + scope: '.root', - run: testable({ selector: '.Selector1' }), + run: testable({ selector: '.Selector1' }), - child: { - scope: '.child', + child: { + scope: '.child', - run: testable({ selector: '.Selector2' }), - } - }); - - const d1 = new Deferred(); - const running1 = p.run(1, d1); - assert.equal(isPageObject(running1), true); - assert.notStrictEqual(running1, p); - assert.equal(running1.scope, '.root'); - assert.deepEqual(invoked, [1]); - assert.deepEqual(finished, []); - - const d2 = new Deferred(); - const running2 = running1.run(2, d2); - assert.strictEqual(running1, running2); - assert.deepEqual(invoked, [1]); - assert.deepEqual(finished, []); - - const d3 = new Deferred(); - const running3 = running2.child.run(3, d3); - assert.equal(isPageObject(running3), true); - assert.notStrictEqual(running1, running3); - assert.equal(running3.scope, '.child'); - assert.deepEqual(invoked, [1]); - assert.deepEqual(finished, []); - - d1.resolve(); - await next(); - - assert.deepEqual(invoked, [1, 2]); - assert.deepEqual(finished, [1]); - - d2.resolve(); - await next(); - - assert.deepEqual(invoked, [1, 2, 3]); - assert.deepEqual(finished, [1, 2]); - - await d3.resolve(); - assert.deepEqual(invoked, [1, 2, 3]); - assert.deepEqual(finished, [1, 2, 3]); + run: testable({ selector: '.Selector2' }), + } }); - test('concurrent from same root', async function(assert) { - const p = create({ - scope: '.root', - - run: testable({ selector: '.Selector1' }) - }); + const d1 = new Deferred(); + const running1 = p.run(1, d1); + assert.equal(isPageObject(running1), true); + assert.notStrictEqual(running1, p); + assert.equal(running1.scope, '.root'); + assert.deepEqual(invoked, [1]); + assert.deepEqual(finished, []); - const d1 = new Deferred(); - const running1 = p.run('1', d1); - assert.equal(isPageObject(running1), true); - assert.notStrictEqual(running1, p); - assert.equal(running1.scope, '.root'); - assert.deepEqual(invoked, ['1']); - assert.deepEqual(finished, []); + const d2 = new Deferred(); + const running2 = running1.run(2, d2); + assert.strictEqual(running1, running2); + assert.deepEqual(invoked, [1]); + assert.deepEqual(finished, []); - const d11 = new Deferred(); - const running11 = p.run('1.1', d11); - assert.strictEqual(running1, running11); - assert.deepEqual(invoked, ['1', '1.1']); - assert.deepEqual(finished, []); + const d3 = new Deferred(); + const running3 = running2.child.run(3, d3); + assert.equal(isPageObject(running3), true); + assert.notStrictEqual(running1, running3); + assert.equal(running3.scope, '.child'); + assert.deepEqual(invoked, [1]); + assert.deepEqual(finished, []); - const d12 = new Deferred(); - const running12 = p.run('1.2', d12); - assert.strictEqual(running1, running12); - assert.deepEqual(invoked, ['1', '1.1', '1.2']); - assert.deepEqual(finished, []); + d1.resolve(); + await next(); - d1.resolve(); - assert.deepEqual(invoked, ['1', '1.1', '1.2']); - assert.deepEqual(finished, []); + assert.deepEqual(invoked, [1, 2]); + assert.deepEqual(finished, [1]); - await next(); - assert.deepEqual(finished, ['1']); + d2.resolve(); + await next(); - d12.resolve(); - assert.deepEqual(finished, ['1']); + assert.deepEqual(invoked, [1, 2, 3]); + assert.deepEqual(finished, [1, 2]); - await next(); - assert.deepEqual(finished, ['1', '1.2']); + await d3.resolve(); + assert.deepEqual(invoked, [1, 2, 3]); + assert.deepEqual(finished, [1, 2, 3]); + }); - d11.resolve(); - assert.deepEqual(finished, ['1', '1.2']); + test('concurrent from same root', async function(assert) { + const p = create({ + scope: '.root', - await next(); - assert.deepEqual(finished, ['1', '1.2', '1.1']); + run: testable({ selector: '.Selector1' }) }); - test('concurrent from same chain root', async function(assert) { - const p = create({ - scope: '.root', + const d1 = new Deferred(); + const running1 = p.run('1', d1); + assert.equal(isPageObject(running1), true); + assert.notStrictEqual(running1, p); + assert.equal(running1.scope, '.root'); + assert.deepEqual(invoked, ['1']); + assert.deepEqual(finished, []); + + const d11 = new Deferred(); + const running11 = p.run('1.1', d11); + assert.strictEqual(running1, running11); + assert.deepEqual(invoked, ['1', '1.1']); + assert.deepEqual(finished, []); + + const d12 = new Deferred(); + const running12 = p.run('1.2', d12); + assert.strictEqual(running1, running12); + assert.deepEqual(invoked, ['1', '1.1', '1.2']); + assert.deepEqual(finished, []); + + d1.resolve(); + assert.deepEqual(invoked, ['1', '1.1', '1.2']); + assert.deepEqual(finished, []); + + await next(); + assert.deepEqual(finished, ['1']); - run: testable({ selector: '.Selector1' }) - }); - - const d1 = new Deferred(); - const running1 = p.run('1', d1); - assert.equal(isPageObject(running1), true); - assert.notStrictEqual(running1, p); - assert.equal(running1.scope, '.root'); - assert.deepEqual(invoked, ['1']); - assert.deepEqual(finished, []); - - const d11 = new Deferred(); - const running11 = running1.run('1.1', d11); - assert.strictEqual(running1, running11); - assert.deepEqual(invoked, ['1']); - assert.deepEqual(finished, []); - - const d12 = new Deferred(); - const running12 = running1.run('1.2', d12); - assert.strictEqual(running1, running12); - assert.deepEqual(invoked, ['1']); - assert.deepEqual(finished, []); - - d1.resolve(); - assert.deepEqual(invoked, ['1']); - assert.deepEqual(finished, []); - - await next(); - assert.deepEqual(invoked, ['1', '1.1']); - assert.deepEqual(finished, ['1']); - - await next(); - assert.deepEqual(invoked, ['1', '1.1']); - assert.deepEqual(finished, ['1']); - - d12.resolve(); - assert.deepEqual(invoked, ['1', '1.1']); - assert.deepEqual(finished, ['1']); - - await next(); - assert.deepEqual(invoked, ['1', '1.1']); - assert.deepEqual(finished, ['1']); - - d11.resolve(); - assert.deepEqual(invoked, ['1', '1.1']); - assert.deepEqual(finished, ['1']); - - await next(); - assert.deepEqual(invoked, ['1', '1.1', '1.2']); - assert.deepEqual(finished, ['1', '1.1', '1.2']); + d12.resolve(); + assert.deepEqual(finished, ['1']); + + await next(); + assert.deepEqual(finished, ['1', '1.2']); + + d11.resolve(); + assert.deepEqual(finished, ['1', '1.2']); + + await next(); + assert.deepEqual(finished, ['1', '1.2', '1.1']); + }); + + test('concurrent from same chain root', async function(assert) { + const p = create({ + scope: '.root', + + run: testable({ selector: '.Selector1' }) }); - test('it handles errors', async function(assert) { + const d1 = new Deferred(); + const running1 = p.run('1', d1); + assert.equal(isPageObject(running1), true); + assert.notStrictEqual(running1, p); + assert.equal(running1.scope, '.root'); + assert.deepEqual(invoked, ['1']); + assert.deepEqual(finished, []); + + const d11 = new Deferred(); + const running11 = running1.run('1.1', d11); + assert.strictEqual(running1, running11); + assert.deepEqual(invoked, ['1']); + assert.deepEqual(finished, []); + + const d12 = new Deferred(); + const running12 = running1.run('1.2', d12); + assert.strictEqual(running1, running12); + assert.deepEqual(invoked, ['1']); + assert.deepEqual(finished, []); + + d1.resolve(); + assert.deepEqual(invoked, ['1']); + assert.deepEqual(finished, []); + + await next(); + assert.deepEqual(invoked, ['1', '1.1']); + assert.deepEqual(finished, ['1']); + + await next(); + assert.deepEqual(invoked, ['1', '1.1']); + assert.deepEqual(finished, ['1']); + + d12.resolve(); + assert.deepEqual(invoked, ['1', '1.1']); + assert.deepEqual(finished, ['1']); + + await next(); + assert.deepEqual(invoked, ['1', '1.1']); + assert.deepEqual(finished, ['1']); + + d11.resolve(); + assert.deepEqual(invoked, ['1', '1.1']); + assert.deepEqual(finished, ['1']); + + await next(); + assert.deepEqual(invoked, ['1', '1.1', '1.2']); + assert.deepEqual(finished, ['1', '1.1', '1.2']); + }); + + test('it handles errors', async function(assert) { + // when test against some old `ember-cli-qunit`-only scenarios, QUnit@1 is installed. + // There is no `assert.rejects(` support, so we just ignore the test in such cases. + // It's still tested on newer scenarios with QUnit@2, even against old "ember-test-helpers" scenarios. + // + // @todo: remove after `ember-cli-qunit` support is removed. + if (typeof assert.rejects === 'function') { const p = create({ scope: '.root', @@ -325,7 +336,9 @@ PageObject: 'page.run("1")' PageObject: 'page.child.run("1")' Selector: '.root .child .Selector2'`)); - }); + } else { + assert.expect(0); + } }); }); -} +}); diff --git a/tests/unit/-private/compatibility-test.js b/tests/unit/-private/compatibility-test.js deleted file mode 100644 index 04fb0b21..00000000 --- a/tests/unit/-private/compatibility-test.js +++ /dev/null @@ -1,41 +0,0 @@ -import { test, module } from 'qunit'; -import { wait } from 'ember-cli-page-object/test-support/-private/compatibility'; - -module('Unit | compatibility', function(hooks) { - let originalHasFn; - - hooks.beforeEach(function() { - window.require.unsee('ember-cli-page-object/test-support/-private/compatibility'); - originalHasFn = window.require.has; - }); - hooks.afterEach(function() { - window.require.has = originalHasFn; - // So it doesn't stay cached with exports based on a stubbed require.has - window.require.unsee('ember-cli-page-object/test-support/-private/compatibility'); - }); - - module('wait', function() { - // Under normal circumstances, `wait` is expected not to throw an error, since - // the necessary Ember test helpers are included by ember-qunit & ember-mocha. - test('does not throw', function(assert) { - assert.expect(0); - wait(); - }); - - test('throws an error if test helpers are not available', function(assert) { - window.require.has = () => false; - const wait = window.require('ember-cli-page-object/test-support/-private/compatibility').wait; - - assert.throws(wait); - }); - }); - - module('getContext', function() { - test('exists and returns null if @ember/test-helpers are not available', function(assert) { - window.require.has = () => false; - const getContext = window.require('ember-cli-page-object/test-support/-private/compatibility').getContext; - - assert.notOk(getContext()); - }); - }); -}); diff --git a/tests/unit/-private/properties/create-test.js b/tests/unit/-private/properties/create-test.js index 269d0ed4..b6a4991b 100644 --- a/tests/unit/-private/properties/create-test.js +++ b/tests/unit/-private/properties/create-test.js @@ -64,7 +64,6 @@ moduleForProperty('create', function(test, adapter) { test('does not mutate definition object', async function(assert) { let prop = text('.baz'); let expected = { - context: '.a-context', scope: '.a-scope', foo: { baz: prop @@ -73,7 +72,6 @@ moduleForProperty('create', function(test, adapter) { bar: prop }; let actual = { - context: '.a-context', scope: '.a-scope', foo: { baz: prop @@ -96,4 +94,10 @@ moduleForProperty('create', function(test, adapter) { assert.ok(page.contains('ipsum')); }); + + test('"context" key is not allowed', async function(assert) { + assert.throws(() => create({ + context: {} + }), new Error('"context" key is not allowed to be passed at definition root.')); + }); }); diff --git a/tests/unit/-private/supports-rfc268-test.js b/tests/unit/-private/supports-rfc268-test.js deleted file mode 100644 index c49491c3..00000000 --- a/tests/unit/-private/supports-rfc268-test.js +++ /dev/null @@ -1,60 +0,0 @@ -import { test, module } from 'qunit'; -import { setupTest } from 'ember-qunit'; -import require from 'require'; -import { create } from 'ember-cli-page-object'; - -if (require.has('@ember/test-helpers')) { - module('Unit | supports rfc268', function(hooks) { - function getExecutionContext(pageObject) { - window.require.unsee('ember-cli-page-object/test-support/-private/execution_context'); - return require('ember-cli-page-object/test-support/-private/execution_context').getExecutionContext(pageObject); - } - - function supportsRfc268() { - window.require.unsee('ember-cli-page-object/test-support/-private/execution_context'); - return require('ember-cli-page-object/test-support/-private/execution_context').supportsRfc268(); - } - - hooks.afterEach(function() { - // Make sure we don't leave this module in a mutated state - window.require.unsee('ember-cli-page-object/test-support/-private/execution_context'); - }); - - module('with context', function(hooks) { - setupTest(hooks); - - let compatModule; - let originalVisit; - - hooks.beforeEach(function() { - compatModule = require('ember-cli-page-object/test-support/-private/compatibility'); - originalVisit = compatModule.visit; - }); - - hooks.afterEach(function() { - compatModule.visit = originalVisit; - }); - - test('works', function(assert) { - assert.ok(supportsRfc268()); - }); - - test('throws without visit() present', function(assert) { - compatModule.visit = undefined; - - assert.throws(() => supportsRfc268()); - }); - }); - - module('without context', function() { - test('throws', function(assert) { - assert.throws( - () => getExecutionContext(create()), - new Error(`Looks like you attempt to access page object property outside of test context. -If that's not the case, please make sure you use the latest version of "@ember/test-helpers".`), - 'Throws with a correct message' - ); - }); - }); - }); -} diff --git a/tests/unit/-private/execution_context/acceptance-test.js b/tests/unit/adapters/acceptance-test.js similarity index 62% rename from tests/unit/-private/execution_context/acceptance-test.js rename to tests/unit/adapters/acceptance-test.js index e31b015e..e05c88eb 100644 --- a/tests/unit/-private/execution_context/acceptance-test.js +++ b/tests/unit/adapters/acceptance-test.js @@ -9,7 +9,7 @@ const node = create( }) ); -moduleForAcceptance('Acceptance | acceptance context | actions', { +moduleForAcceptance('Acceptance | acceptance adapter | actions', { beforeEach(assert) { this.application.register( 'component:test-component', @@ -20,6 +20,20 @@ moduleForAcceptance('Acceptance | acceptance context | actions', { } }); +test('sync invocations', async function(assert) { + node.click() + node.click(); + + await node; + + assert.verifySteps([ + 'begin #0', + 'begin #1', + 'complete #0', + 'complete #1' + ]); +}); + test('async invocations', async function(assert) { await node.click() await node.click(); @@ -32,45 +46,26 @@ test('async invocations', async function(assert) { ]) }); -test('sync invocations', async function(assert) { - node.click() - node.click(); +test('async chained invocations', async function(assert) { + await node.click().click(); - return window.andThen(() => { - assert.verifySteps([ - 'begin #0', - 'complete #0', - 'begin #1', - 'complete #1' - ]) - }); + assert.verifySteps([ + 'begin #0', + 'complete #0', + 'begin #1', + 'complete #1' + ]); }); test('sync chained invocations', async function(assert) { - node.click() - .click(); + node.click().click(); - return window.andThen(() => { - assert.verifySteps([ - 'begin #0', - 'complete #0', - 'begin #1', - 'complete #1', - ]) - }) -}); + await node; -test('async chained invocations', async function(assert) { - await node.click() - .click(); - - return window.andThen(() => { - assert.verifySteps([ - 'begin #0', - 'complete #0', - 'begin #1', - 'complete #1' - ]) - }) + assert.verifySteps([ + 'begin #0', + 'complete #0', + 'begin #1', + 'complete #1', + ]) }); - diff --git a/tests/unit/-private/execution_context/helpers.ts b/tests/unit/adapters/helpers.ts similarity index 100% rename from tests/unit/-private/execution_context/helpers.ts rename to tests/unit/adapters/helpers.ts diff --git a/tests/unit/-private/execution_context/integration-native-test.js b/tests/unit/adapters/integration-native-test.js similarity index 54% rename from tests/unit/-private/execution_context/integration-native-test.js rename to tests/unit/adapters/integration-native-test.js index 1e441d34..e134e45d 100644 --- a/tests/unit/-private/execution_context/integration-native-test.js +++ b/tests/unit/adapters/integration-native-test.js @@ -1,32 +1,44 @@ import Ember from 'ember'; import hbs from 'htmlbars-inline-precompile'; import { moduleForComponent, test } from 'ember-qunit'; -import wait from 'ember-test-helpers/wait'; import { create } from 'ember-cli-page-object' -import { useNativeEvents } from 'ember-cli-page-object/extend' import { createClickTrackerComponent, ClickTrackerDef } from './helpers'; +import { setAdapter } from 'ember-cli-page-object/test-support/adapters'; +import ModuleForComponentNativeDOMAdapter from 'ember-cli-page-object/test-support/adapters/integration-native-events'; const node = create(ClickTrackerDef); if (Ember.hasOwnProperty('$')) { - moduleForComponent('', 'Integration | integration context | actions [native-events]', { + moduleForComponent('', 'Integration | integration adapter | actions [native-events]', { integration: true, beforeEach(assert) { this.register('component:action-tracker', createClickTrackerComponent(assert)) - node.setContext(this); + setAdapter(new ModuleForComponentNativeDOMAdapter()); this.render(hbs`{{action-tracker}}`); }, afterEach() { - node.removeContext(); - - useNativeEvents(false); + setAdapter(null); } }); + test('sync invocations', async function(assert) { + node.click() + node.click(); + + await node; + + assert.verifySteps([ + 'begin #0', + 'begin #1', + 'complete #0', + 'complete #1' + ]) + }); + test('async invocations', async function(assert) { await node.click() await node.click(); @@ -39,45 +51,28 @@ if (Ember.hasOwnProperty('$')) { ]) }); - test('sync invocations', async function(assert) { - node.click() - node.click(); - - return wait().then(() => { - assert.verifySteps([ - 'begin #0', - 'begin #1', - 'complete #0', - 'complete #1' - ]) - }); - }); - - test('chained sync invocations', async function(assert) { - node.click() + test('async chained invocations', async function(assert) { + await node.click() .click(); - return wait().then(() => { - assert.verifySteps([ - 'begin #0', - 'begin #1', - 'complete #0', - 'complete #1', - ]) - }) + assert.verifySteps([ + 'begin #0', + 'complete #0', + 'begin #1', + 'complete #1' + ]); }); - test('async chained invocations', async function(assert) { - await node.click() - .click(); + test('sync chained invocations', async function(assert) { + node.click().click(); - return wait().then(() => { - assert.verifySteps([ - 'begin #0', - 'begin #1', - 'complete #0', - 'complete #1' - ]) - }) + await node; + + assert.verifySteps([ + 'begin #0', + 'complete #0', + 'begin #1', + 'complete #1', + ]) }); } diff --git a/tests/unit/-private/execution_context/integration-test.js b/tests/unit/adapters/integration-test.js similarity index 57% rename from tests/unit/-private/execution_context/integration-test.js rename to tests/unit/adapters/integration-test.js index ea48e0be..45859709 100644 --- a/tests/unit/-private/execution_context/integration-test.js +++ b/tests/unit/adapters/integration-test.js @@ -1,29 +1,44 @@ import Ember from 'ember'; import { moduleForComponent, test } from 'ember-qunit'; -import wait from 'ember-test-helpers/wait'; import { create } from 'ember-cli-page-object' import hbs from 'htmlbars-inline-precompile'; import { createClickTrackerComponent, ClickTrackerDef } from './helpers'; +import { setAdapter } from 'ember-cli-page-object/test-support/adapters'; +import ModuleForComponentAdapter from 'ember-cli-page-object/test-support/adapters/integration'; if (Ember.hasOwnProperty('$')) { const node = create(ClickTrackerDef); - moduleForComponent('', 'Integration | integration context | actions', { + moduleForComponent('', 'Integration | integration adapter | actions', { integration: true, beforeEach(assert) { this.register('component:action-tracker', createClickTrackerComponent(assert)) - node.setContext(this); + setAdapter(new ModuleForComponentAdapter()); this.render(hbs`{{action-tracker}}`); }, afterEach() { - node.removeContext(); + setAdapter(null); } }); + test('sync invocations', async function(assert) { + node.click() + node.click(); + + await node; + + assert.verifySteps([ + 'begin #0', + 'begin #1', + 'complete #0', + 'complete #1' + ]); + }); + test('async invocations', async function(assert) { await node.click() await node.click(); @@ -36,45 +51,27 @@ if (Ember.hasOwnProperty('$')) { ]) }); - test('sync invocations', async function(assert) { - node.click() - node.click(); + test('async chained invocations', async function(assert) { + await node.click().click(); - return wait().then(() => { - assert.verifySteps([ - 'begin #0', - 'begin #1', - 'complete #0', - 'complete #1' - ]) - }); + assert.verifySteps([ + 'begin #0', + 'complete #0', + 'begin #1', + 'complete #1' + ]); }); test('sync chained invocations', async function(assert) { - node.click() - .click(); - - return wait().then(() => { - assert.verifySteps([ - 'begin #0', - 'begin #1', - 'complete #0', - 'complete #1', - ]) - }) - }); + node.click().click(); - test('async chained invocations', async function(assert) { - await node.click() - .click(); - - return wait().then(() => { - assert.verifySteps([ - 'begin #0', - 'begin #1', - 'complete #0', - 'complete #1' - ]) - }) + await node; + + assert.verifySteps([ + 'begin #0', + 'complete #0', + 'begin #1', + 'complete #1' + ]); }); } diff --git a/tests/unit/-private/execution_context/rfc268-test.ts b/tests/unit/adapters/rfc268-test.ts similarity index 83% rename from tests/unit/-private/execution_context/rfc268-test.ts rename to tests/unit/adapters/rfc268-test.ts index 52c59520..05df91a0 100644 --- a/tests/unit/-private/execution_context/rfc268-test.ts +++ b/tests/unit/adapters/rfc268-test.ts @@ -1,6 +1,6 @@ import require from 'require'; import { test, module } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from '../../helpers'; import hbs from 'htmlbars-inline-precompile'; import { create } from 'ember-cli-page-object' import { createClickTrackerComponent, ClickTrackerDef } from './helpers'; @@ -9,9 +9,9 @@ import { TestContext } from 'ember-test-helpers'; const node = create(ClickTrackerDef); if (require.has('@ember/test-helpers')) { - const { render, settled } = require('@ember/test-helpers'); + const { render } = require('@ember/test-helpers'); - module('Integration | rfc268 context | actions', function(hooks) { + module('Integration | rfc268 adapter | actions', function(hooks) { setupRenderingTest(hooks); hooks.beforeEach(function(this: TestContext, assert) { @@ -22,9 +22,9 @@ if (require.has('@ember/test-helpers')) { test('sync invocations', async function(assert) { node.click() - await node.click(); + node.click(); - await settled(); + await node; assert.verifySteps([ 'begin #0', @@ -60,9 +60,7 @@ if (require.has('@ember/test-helpers')) { test('sync chained invocations', async function(assert) { node.click().click(); - await settled(); - await settled(); - await settled(); + await node; assert.verifySteps([ 'begin #0', diff --git a/tests/unit/addon-exports-test.js b/tests/unit/addon-exports-test.js index c95083d4..5c98121b 100644 --- a/tests/unit/addon-exports-test.js +++ b/tests/unit/addon-exports-test.js @@ -31,12 +31,11 @@ const EXPECTED_METHODS = [ const HELPER_METHODS = [ 'buildSelector', - 'getContext', 'findElement', 'findElementWithAssert' ]; -const EXTEND_METHODS = HELPER_METHODS.concat('registerExecutionContext').concat([ +const EXTEND_METHODS = HELPER_METHODS.concat([ 'findOne', 'findMany' ]); diff --git a/tests/unit/extend/find-element-with-assert-test.ts b/tests/unit/extend/find-element-with-assert-test.ts index 060b205e..aa0ae061 100644 --- a/tests/unit/extend/find-element-with-assert-test.ts +++ b/tests/unit/extend/find-element-with-assert-test.ts @@ -1,5 +1,5 @@ import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; +import { setupRenderingTest } from '../../helpers'; import { create } from 'ember-cli-page-object'; import { findElementWithAssert } from 'ember-cli-page-object/extend'; import hbs from 'htmlbars-inline-precompile'; diff --git a/types/index.d.ts b/types/index.d.ts index 4fa01c25..346b0f1f 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -75,15 +75,24 @@ declare module 'ember-cli-page-object/macros' { function alias(path: string, options?: { chainable: boolean }): any; } +declare module 'ember-cli-page-object/adapter' { + export default class Adapter {} +} + +declare module 'ember-cli-page-object/adapters/rfc268' { + import Adapter from 'ember-cli-page-object/adapter'; + + export default class RFC268Adapter extends Adapter {} +} + +declare module 'ember-cli-page-object/adapters' { + import Adapter from 'ember-cli-page-object/adapter'; + + export function setAdapter(adapter: Adapter): void +} + declare module 'ember-cli-page-object/-private' { import 'jquery'; - import { - clickable, - clickOnText, - fillable, - focusable, - blurrable - } from 'ember-cli-page-object'; interface GetterDescriptor { isGetter: true;