diff --git a/addon/-private/execution_context.js b/addon/-private/execution_context.js index fb425eeb..9165df45 100644 --- a/addon/-private/execution_context.js +++ b/addon/-private/execution_context.js @@ -1,10 +1,10 @@ import { getContext } from './helpers'; -import AcceptanceExecutionContext from './execution_context/acceptance'; -import IntegrationExecutionContext from './execution_context/integration'; +import AcceptanceEmberExecutionContext from './execution_context/acceptance'; +import IntegrationEmberExecutionContext from './execution_context/integration'; const executioncontexts = { - acceptance: AcceptanceExecutionContext, - integration: IntegrationExecutionContext + acceptance: AcceptanceEmberExecutionContext, + integration: IntegrationEmberExecutionContext }; /* diff --git a/addon/-private/execution_context/acceptance-native-dom.js b/addon/-private/execution_context/acceptance-native-dom.js new file mode 100644 index 00000000..3b037ac5 --- /dev/null +++ b/addon/-private/execution_context/acceptance-native-dom.js @@ -0,0 +1,23 @@ +import ExecutionContext from './native-dom-context'; + +import { + visit +} from 'ember-native-dom-helpers'; + +export default function AcceptanceExecutionContext(pageObjectNode) { + ExecutionContext.call(this, pageObjectNode); +} + +AcceptanceExecutionContext.prototype = Object.create(ExecutionContext.prototype); + +AcceptanceExecutionContext.prototype.visit = function() { + return visit(...arguments); +}; + +AcceptanceExecutionContext.prototype.runAsync = function(cb) { + window.wait().then(() => { + cb(this); + }); + + return this.pageObjectNode; +}; diff --git a/addon/-private/execution_context/helpers.js b/addon/-private/execution_context/helpers.js index 67ebf1c8..1841af1c 100644 --- a/addon/-private/execution_context/helpers.js +++ b/addon/-private/execution_context/helpers.js @@ -2,6 +2,8 @@ import { throwBetterError } from '../better-errors'; +import $ from '-jquery'; + /** * @private * @@ -17,7 +19,9 @@ import { * * @throws Will throw an error if called on a contenteditable element that has `contenteditable="false"` */ -export function fillElement($selection, content, { selector, pageObjectNode, pageObjectKey }) { +export function fillElement(selection, content, { selector, pageObjectNode, pageObjectKey }) { + const $selection = $(selection); + if ($selection.is('[contenteditable][contenteditable!="false"]')) { $selection.html(content); } else if ($selection.is('[contenteditable="false"]')) { diff --git a/addon/-private/execution_context/integration-native-dom.js b/addon/-private/execution_context/integration-native-dom.js new file mode 100644 index 00000000..9aabdce1 --- /dev/null +++ b/addon/-private/execution_context/integration-native-dom.js @@ -0,0 +1,20 @@ +import ExecutionContext from './native-dom-context'; + +import Ember from 'ember'; +const { run } = Ember; + +export default function IntegrationExecutionContext(pageObjectNode, testContext) { + ExecutionContext.call(this, pageObjectNode, testContext); +} + +IntegrationExecutionContext.prototype = Object.create(ExecutionContext.prototype); + +IntegrationExecutionContext.prototype.visit = function() {}; + +IntegrationExecutionContext.prototype.runAsync = function(cb) { + run(() => { + cb(this); + }); + + return this.pageObjectNode; +}; diff --git a/addon/-private/execution_context/integration.js b/addon/-private/execution_context/integration.js index f8962fe0..5a21cb79 100644 --- a/addon/-private/execution_context/integration.js +++ b/addon/-private/execution_context/integration.js @@ -32,8 +32,7 @@ IntegrationExecutionContext.prototype = { return this.pageObjectNode; }, - // Do nothing in integration test - visit: $.noop, + visit() {}, click(selector, container) { this.$(selector, container).click(); diff --git a/addon/-private/execution_context/native-dom-context.js b/addon/-private/execution_context/native-dom-context.js new file mode 100644 index 00000000..49388620 --- /dev/null +++ b/addon/-private/execution_context/native-dom-context.js @@ -0,0 +1,138 @@ +import $ from '-jquery'; + +import { + click, + triggerEvent, + keyEvent +} from 'ember-native-dom-helpers'; + +import { + guardMultiple, + buildSelector, + findClosestValue +} from '../helpers'; +import { + fillElement +} from './helpers'; +import { + ELEMENT_NOT_FOUND, + throwBetterError +} from '../better-errors'; + +const KEYBOARD_EVENT_TYPES = ['keydown', 'keypress', 'keyup']; + +export default function ExecutionContext(pageObjectNode, testContext) { + this.pageObjectNode = pageObjectNode; + this.testContext = testContext; +} + +ExecutionContext.prototype = { + run(cb) { + return cb(this); + }, + + runAsync() { + throw new Error('not implemented'); + }, + + click(selector, container) { + const el = this.$(selector, container)[0]; + click(el); + }, + + fillIn(selector, container, options, content) { + let elements = this.$(selector, container).toArray(); + + elements.forEach((el) => { + fillElement(el, content, { + selector, + pageObjectNode: this.pageObjectNode, + pageObjectKey: options.pageObjectKey + }); + + triggerEvent(el, 'input'); + triggerEvent(el, 'change'); + }); + }, + + $(selector, container) { + if (container) { + return $(selector, container); + } else { + // @todo: we should fixed usage of private `_element` + // after https://github.com/emberjs/ember-test-helpers/issues/184 is resolved + let testsContainer = this.testContext ? + this.testContext._element : + '#ember-testing'; + + return $(selector, testsContainer); + } + }, + + triggerEvent(selector, container, eventName, eventOptions) { + const element = this.$(selector, container)[0]; + + // `keyCode` is a deprecated property. + // @see: https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/keyCode + // Due to this deprecation `ember-native-dom-helpers` doesn't accept `keyCode` as a `KeyboardEvent` option. + if (typeof eventOptions.key === 'undefined' && typeof eventOptions.keyCode !== 'undefined') { + eventOptions.key = eventOptions.keyCode.toString(); + delete eventOptions.keyCode; + } + + if (KEYBOARD_EVENT_TYPES.indexOf(eventName) > -1) { + keyEvent(element, eventName, eventOptions.key, eventOptions); + } else { + triggerEvent(element, eventName, eventOptions); + } + }, + + assertElementExists(selector, options) { + let container = options.testContainer || findClosestValue(this.pageObjectNode, 'testContainer'); + + let result = this.$(selector, container); + + if (result.length === 0) { + throwBetterError( + this.pageObjectNode, + options.pageObjectKey, + ELEMENT_NOT_FOUND, + { selector } + ); + } + }, + + find(selector, options) { + let container = options.testContainer || findClosestValue(this.pageObjectNode, 'testContainer'); + + selector = buildSelector(this.pageObjectNode, selector, options); + + let result = this.$(selector, container); + + guardMultiple(result, selector, options.multiple); + + return result; + }, + + findWithAssert(selector, options) { + let container = options.testContainer || findClosestValue(this.pageObjectNode, 'testContainer'); + + selector = buildSelector(this.pageObjectNode, selector, options); + + let result = this.$(selector, container); + + if (result.length === 0) { + throwBetterError( + this.pageObjectNode, + options.pageObjectKey, + ELEMENT_NOT_FOUND, + { selector } + ); + } + + guardMultiple(result, selector, options.multiple); + + return result; + } +}; + diff --git a/addon/-private/helpers.js b/addon/-private/helpers.js index 18b3aa4f..8f17b044 100644 --- a/addon/-private/helpers.js +++ b/addon/-private/helpers.js @@ -1,7 +1,9 @@ import Ember from 'ember'; import Ceibo from 'ceibo'; -const { $, assert, get, isPresent } = Ember; +const { assert, get, isPresent } = Ember; + +import $ from '-jquery'; class Selector { constructor(node, scope, selector, filters) { diff --git a/addon/-private/properties/visitable.js b/addon/-private/properties/visitable.js index 67350341..f58e0d31 100644 --- a/addon/-private/properties/visitable.js +++ b/addon/-private/properties/visitable.js @@ -1,8 +1,7 @@ -import Ember from 'ember'; import { assign } from '../helpers'; import { getExecutionContext } from '../execution_context'; -const { $ } = Ember; +import $ from '-jquery'; function fillInDynamicSegments(path, params) { return path.split('/').map(function(segment) { diff --git a/addon/extend.js b/addon/extend.js index 70eed79d..eb1928a9 100644 --- a/addon/extend.js +++ b/addon/extend.js @@ -1,4 +1,21 @@ export { findElement } from './-private/extend/find-element'; export { findElementWithAssert } from './-private/extend/find-element-with-assert'; export { buildSelector, getContext, fullScope } from './-private/helpers'; -export { register as registerExecutionContext } from './-private/execution_context'; +import { register as registerExecutionContext } from './-private/execution_context'; + +import IntegrationNativeDOMContext from './-private/execution_context/integration-native-dom'; +import AcceptanceNativeDOMContext from './-private/execution_context/acceptance-native-dom'; +import IntegrationEmberContext from './-private/execution_context/integration'; +import AcceptanceEmberContext from './-private/execution_context/acceptance'; + +function useNativeDOMHelpers(flag = true) { + if (flag) { + registerExecutionContext('integration', IntegrationNativeDOMContext); + registerExecutionContext('acceptance', AcceptanceNativeDOMContext); + } else { + registerExecutionContext('integration', IntegrationEmberContext); + registerExecutionContext('acceptance', AcceptanceEmberContext); + } +} + +export { registerExecutionContext, useNativeDOMHelpers }; diff --git a/index.js b/index.js index d22aac1d..5a8ddd8d 100644 --- a/index.js +++ b/index.js @@ -11,6 +11,13 @@ module.exports = { enabled: this._shouldIncludeFiles(), import: ['index.js'] }; + }, + jquery: function() { + return { + enabled: this._shouldIncludeFiles(), + vendor: ['dist/jquery.js'], + destDir: 'ecpo-jquery' + } } } }, @@ -23,9 +30,37 @@ module.exports = { this.app = app; + if (this._shouldIncludeFiles()) { + this.importJquery(); + } + this._super.included.apply(this, arguments); }, + /* + * Import an amd '-jquery' shim which is used by ember-cli-page-object internally + * + * We don't want ember-cli-page-object's jquery ocassionaly leak into a real application. + * The following combo of shims supposed to isolate `ember-cli-page-object`'s `jquery` + * from the rest of application and expose internal version via amd module. + */ + importJquery: function() { + // jquery itself is included in the very beggining of vendor.js. + // At this point we don't have `define()` defined so we can't create an amd shim here. + // + // However we have to store reference to jquery and dispose it from the window + // in order to prevent its leakage to the application. + this.import('vendor/shims/ecpo-jquery-global.js', { + prepend: true + }); + this.import('vendor/ecpo-jquery/dist/jquery.js', { + prepend: true + }); + + // finally define an amd shim for our internal jquery version + this.import('vendor/shims/ecpo-jquery.js'); + }, + treeFor: function(/*name*/) { if (!this._shouldIncludeFiles()) { return; diff --git a/package.json b/package.json index 78e7e4ae..c77cf303 100644 --- a/package.json +++ b/package.json @@ -79,14 +79,16 @@ "ember-cli-babel": "^5.1.7", "ember-cli-get-component-path-option": "^1.0.0", "ember-cli-is-package-missing": "^1.0.0", - "ember-cli-node-assets": "^0.1.2", + "ember-cli-node-assets": "^0.2.2", "ember-cli-normalize-entity-name": "^1.0.0", "ember-cli-path-utils": "^1.0.0", "ember-cli-string-utils": "^1.0.0", "ember-cli-test-info": "^1.0.0", "ember-cli-valid-component-name": "^1.0.0", "ember-cli-version-checker": "^1.2.0", + "ember-native-dom-helpers": "^0.5.3", "ember-test-helpers": "^0.6.3", + "jquery": "^3.2.1", "rsvp": "^3.2.1" }, "ember-addon": { diff --git a/tests/helpers/module-for-acceptance.js b/tests/helpers/module-for-acceptance.js index 76996fd0..eaa20493 100644 --- a/tests/helpers/module-for-acceptance.js +++ b/tests/helpers/module-for-acceptance.js @@ -2,22 +2,34 @@ import { module } from 'qunit'; import Ember from 'ember'; import startApp from '../helpers/start-app'; import destroyApp from '../helpers/destroy-app'; +import { useNativeDOMHelpers } from 'ember-cli-page-object/extend'; const { RSVP: { Promise } } = Ember; export default function(name, options = {}) { - module(name, { - beforeEach() { - this.application = startApp(); + [false, true].forEach(_useNativeDOMHelpers => { + let moduleName = name; + if (_useNativeDOMHelpers) { + moduleName += ' [native-dom-helpers]'; + } - if (options.beforeEach) { - return options.beforeEach.apply(this, arguments); - } - }, + module(moduleName, { + beforeEach() { + this.application = startApp(); - afterEach() { - let afterEach = options.afterEach && options.afterEach.apply(this, arguments); - return Promise.resolve(afterEach).then(() => destroyApp(this.application)); - } + useNativeDOMHelpers(_useNativeDOMHelpers); + + if (options.beforeEach) { + return options.beforeEach.apply(this, arguments); + } + }, + + afterEach() { + useNativeDOMHelpers(false); + + let afterEach = options.afterEach && options.afterEach.apply(this, arguments); + return Promise.resolve(afterEach).then(() => destroyApp(this.application)); + } + }); }); } diff --git a/tests/helpers/properties.js b/tests/helpers/properties.js index adfaa2ca..3f9a21e3 100644 --- a/tests/helpers/properties.js +++ b/tests/helpers/properties.js @@ -1,37 +1,53 @@ import AcceptanceExecutionContext from 'ember-cli-page-object/-private/execution_context/acceptance'; import IntegrationExecutionContext from 'ember-cli-page-object/-private/execution_context/integration'; import { AcceptanceAdapter, moduleForAcceptance, testForAcceptance } from './properties/acceptance-adapter'; + import { IntegrationAdapter, moduleForIntegration, testForIntegration } from './properties/integration-adapter'; +import { useNativeDOMHelpers } from 'ember-cli-page-object/extend'; + export function moduleForProperty(name, cbOrOptions, cb) { let options = cb ? cbOrOptions : {}; cb = cb || cbOrOptions; - // Generate acceptance tests - moduleForAcceptance(`Acceptance mode | Property | ${name}`, { - beforeEach() { - this.adapter = new AcceptanceAdapter(AcceptanceExecutionContext); - }, + [true, false].forEach(_useNativeDOMHelpers => { + // Generate acceptance tests - afterEach() { - this.adapter.revert(); + let moduleNamePrefix = 'Acceptance mode '; + if (_useNativeDOMHelpers) { + moduleNamePrefix += ' [native-dom-helpers]'; } - }); - cb(testForAcceptance, 'acceptance'); - - if (options.acceptanceOnly) { - return; - } - - // Generate integration tests - moduleForIntegration('html-render', `Integration mode | Property | ${name}`, { - integration: true, - beforeEach() { - this.adapter = new IntegrationAdapter(IntegrationExecutionContext); - }, - afterEach() { - this.adapter.revert(); + moduleForAcceptance(`${moduleNamePrefix} | Property | ${name}`, { + beforeEach() { + useNativeDOMHelpers(_useNativeDOMHelpers); + + this.adapter = new AcceptanceAdapter(AcceptanceExecutionContext); + }, + + afterEach() { + useNativeDOMHelpers(false); + this.adapter.revert(); + } + }); + cb(testForAcceptance, 'acceptance'); + + if (options.acceptanceOnly) { + return; } + + // Generate integration tests + moduleForIntegration('html-render', `Integration mode | Property | ${name}`, { + integration: true, + beforeEach() { + useNativeDOMHelpers(_useNativeDOMHelpers); + + this.adapter = new IntegrationAdapter(IntegrationExecutionContext); + }, + afterEach() { + useNativeDOMHelpers(false); + this.adapter.revert(); + } + }); + cb(testForIntegration, 'integration'); }); - cb(testForIntegration, 'integration'); } diff --git a/vendor/shims/ecpo-jquery-global.js b/vendor/shims/ecpo-jquery-global.js new file mode 100644 index 00000000..eed60bae --- /dev/null +++ b/vendor/shims/ecpo-jquery-global.js @@ -0,0 +1,9 @@ +// Temporary store our own jquery version in a global variable for the further definition of amd module. +// We can't define amd module here cause we don't have `define()` in the very beginning of vendor.js +// +// It's important to include this shim right after our own `jquery` is included. +// This way we ensure nothing catches our own `jquery` and we can safely dispose it from the global `window`. +(function() { + window.__ecpoJQuery__ = self['$'].noConflict(); + delete self['jQuery']; +})(); diff --git a/vendor/shims/ecpo-jquery.js b/vendor/shims/ecpo-jquery.js new file mode 100644 index 00000000..62d0744b --- /dev/null +++ b/vendor/shims/ecpo-jquery.js @@ -0,0 +1,13 @@ +// Define an amd '-jquery' shim which is used by ember-cli-page-object internally +(function() { + var jquery = window.__ecpoJQuery__; + delete window.__ecpoJQuery__; + + function vendorModule() { + 'use strict'; + + return { 'default': jquery }; + } + + define('-jquery', [], vendorModule); +})();