diff --git a/addon/index.js b/addon/index.js new file mode 100644 index 0000000..a6b9e7d --- /dev/null +++ b/addon/index.js @@ -0,0 +1,39 @@ +import { assign } from '@ember/polyfills'; + +import { + create as upstreamCreate +} from 'ember-cli-page-object'; + +function buildDescriptor(node, keyName, descriptor /*, descriptorBuilder*/) { + descriptor = assign({ + configurable: true, + enumerable: true, + }, descriptor); + + if (descriptor.value) { + descriptor.writable = false; + } else { + descriptor.get = function() { + return descriptor.get.call(this, keyName); + }; + } + + if (typeof node.__propertyNames__ === 'undefined') { + node.__propertyNames__ = []; + } + + node.__propertyNames__.push(keyName); + + Object.defineProperty(node, keyName, descriptor); +} + +export function create(definition/* , options = {} */) { + // options = assign({ + // builder: { + // descriptor: buildDescriptor + // } + // }, options); + + // return upstreamCreate(definition, options); + return upstreamCreate(definition); +} diff --git a/jsconfig.json b/jsconfig.json new file mode 100644 index 0000000..f408cac --- /dev/null +++ b/jsconfig.json @@ -0,0 +1 @@ +{"compilerOptions":{"target":"es6","experimentalDecorators":true},"exclude":["node_modules","bower_components","tmp","vendor",".git","dist"]} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 537101a..816893b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1923,6 +1923,12 @@ "redeyed": "1.0.1" } }, + "ceibo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ceibo/-/ceibo-2.0.0.tgz", + "integrity": "sha1-mmHrBUqRwJk0WI1ORdndLDvwTu4=", + "dev": true + }, "center-align": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/center-align/-/center-align-0.1.3.tgz", @@ -3121,6 +3127,20 @@ "integrity": "sha1-IMtop5D+D94kiN39jvu332/nZvI=", "dev": true }, + "ember-cli-node-assets": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/ember-cli-node-assets/-/ember-cli-node-assets-0.2.2.tgz", + "integrity": "sha1-0tVWJufMZhn4gtf+VXUfkmYCJwg=", + "dev": true, + "requires": { + "broccoli-funnel": "1.2.0", + "broccoli-merge-trees": "1.2.4", + "broccoli-source": "1.1.0", + "debug": "2.6.9", + "lodash": "4.17.5", + "resolve": "1.6.0" + } + }, "ember-cli-normalize-entity-name": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/ember-cli-normalize-entity-name/-/ember-cli-normalize-entity-name-1.0.0.tgz", @@ -3130,6 +3150,28 @@ "silent-error": "1.1.0" } }, + "ember-cli-page-object": { + "version": "1.15.0-beta.2", + "resolved": "https://registry.npmjs.org/ember-cli-page-object/-/ember-cli-page-object-1.15.0-beta.2.tgz", + "integrity": "sha512-2NsAHTtV54LgwF8IrMBLUOzfaECB4M1pUq3OwLELXOlJJ3OMZZXr+2BomX6VxdmyDkuQ5jwPehI5cKY1JI1lKA==", + "dev": true, + "requires": { + "ceibo": "2.0.0", + "ember-cli-babel": "6.12.0", + "ember-cli-node-assets": "0.2.2", + "ember-native-dom-helpers": "0.5.10", + "jquery": "3.3.1", + "rsvp": "4.8.2" + }, + "dependencies": { + "rsvp": { + "version": "4.8.2", + "resolved": "https://registry.npmjs.org/rsvp/-/rsvp-4.8.2.tgz", + "integrity": "sha512-8CU1Wjxvzt6bt8zln+hCjyieneU9s0LRW+lPRsjyVCY8Vm1kTbK7btBIrCGg6yY9U4undLDm/b1hKEEi1tLypg==", + "dev": true + } + } + }, "ember-cli-path-utils": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/ember-cli-path-utils/-/ember-cli-path-utils-1.0.0.tgz", @@ -3297,6 +3339,16 @@ } } }, + "ember-native-dom-helpers": { + "version": "0.5.10", + "resolved": "https://registry.npmjs.org/ember-native-dom-helpers/-/ember-native-dom-helpers-0.5.10.tgz", + "integrity": "sha512-bPJX49vlgnBGwFn/3WJPPJjjyd7/atvzW5j01u1dbyFf3bXvHg9Rs1qaZJdk8js0qZ1FINadIEC9vWtgN3w7tg==", + "dev": true, + "requires": { + "broccoli-funnel": "1.2.0", + "ember-cli-babel": "6.12.0" + } + }, "ember-qunit": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/ember-qunit/-/ember-qunit-3.3.2.tgz", diff --git a/package.json b/package.json index 6426134..1dd480c 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "ember-cli-htmlbars": "^2.0.1", "ember-cli-htmlbars-inline-precompile": "^1.0.0", "ember-cli-inject-live-reload": "^1.4.1", + "ember-cli-page-object": "^1.15.0-beta.2", "ember-cli-qunit": "^4.1.1", "ember-cli-shims": "^1.2.0", "ember-cli-sri": "^2.1.0", diff --git a/tests/basic-test.js b/tests/basic-test.js new file mode 100644 index 0000000..072b2ae --- /dev/null +++ b/tests/basic-test.js @@ -0,0 +1,36 @@ +import { module, test } from 'qunit'; +import { setupRenderingTest } from 'ember-qunit'; +import { render } from '@ember/test-helpers'; +import { create } from 'ember-cli-page-object-qunit'; +import hbs from 'htmlbars-inline-precompile'; + +const page = create({ + screen: { + scope: '.screen', + result: { + scope: 'result', + } + } +}); + +module('basic test', function(hooks) { + setupRenderingTest(hooks); + + test('1', async function(assert) { + await render(hbs` +
+
`); + + page.screen.as(s => { + assert.po(s).isPresent(); + assert.po(s).isVisible(); + assert.po(s).isNotHidden(); + + s.result.as(r => { + assert.po(r).hasText('91'); + assert.po(r).contains('91'); + assert.po(r).doesNotContain('99'); + }) + }); + }); +}) diff --git a/tests/helpers/assertions.js b/tests/helpers/assertions.js new file mode 100644 index 0000000..fbc976f --- /dev/null +++ b/tests/helpers/assertions.js @@ -0,0 +1,143 @@ +import Ceibo from 'ceibo'; + +import { + camelize, + decamelize +} from '@ember/string'; + +export class Assertions { + constructor(pageObject, testContext) { + this.po = pageObject; + this.testContext = testContext; + + for (let key in this.po.__propertNames__) { + let inverseKey = buildInverseKey(key); + + this[key] = this.__wrap__(key, true, humanizeString(key)) + this[inverseKey] = this.__wrap__(key, false, humanizeString(inverseKey)); + } + } + + __wrap__(k, isPositive = true, defaultMessage = undefined) { + return (...assertionArgs) => { + let message, + result; + + if (typeof this.po[k] === 'function') { + const poMethod = this.po[k]; + const methodArgs = Array.prototype.slice.call(assertionArgs, 0, poMethod.length); + + message = assertionArgs[poMethod.length] || (defaultMessage + ' ' + argsToString(methodArgs)); + result = poMethod.apply(this.po, methodArgs); + } else { + message = assertionArgs[0] || defaultMessage; + result = this.po[k]; + } + + this._pushResult(result === (isPositive === true), message) + } + } + + hasText(expected, message = `has text "${expected}"`) { + const actual = trimWhitespace(this.po.text); + const result = expected === actual; + + this._pushResult(result, message, { + expected, + actual + }); + } + + doesNotHaveText(expected, message = `has no text "${expected}"`) { + const actual = trimWhitespace(this.po.text); + const result = expected !== actual; + + this._pushResult(result, message || `does not have valid text "${expected}"`, { + expected, + actual + }); + } + + _pushResult(result, message, options = {}) { + if (!message) { + throw new Error('no message provided for the test result'); + } + + if (typeof result !== 'boolean') { + throw new Error('test result must be a boolean'); + } + + let { + actual = result, + expected = true + } = options; + + let prefix = `${buildFullPath(this.po)}`; + + message = `${prefix}: ${message}`; + + this.testContext.pushResult({ + result, + actual, + expected, + message + }); + } +} + +function trimWhitespace(string) { + return string + .replace(/[\t\r\n]/g, ' ') + .replace(/ +/g, ' ') + .replace(/^ /, '') + .replace(/ $/, ''); +} + +export function buildFullPath(node) { + let path = []; + let current = node; + + do { + path.unshift(Ceibo.meta(current).key); + current = Ceibo.parent(current) + } while (Ceibo.parent(current)); + + return path.join('/'); +} + +export function buildSelector(node) { + let path = []; + let current = node; + + do { + path.unshift(current.scope); + current = Ceibo.parent(current) + } while (Ceibo.parent(current)); + + return path.join(' '); +} + +function humanizeString(input) { + return decamelize(input).replace(/_/g, ' '); +} + +function argsToString(args) { + return args.map(a => typeof a === 'string' ? `"${a}"` : a).join(', ') +} + +function buildInverseKey(key) { + const [leader, ...restWords] = decamelize(key).split('_'); + + if (restWords.length === 0) { + return camelize(`doesNot ${leader.replace(/s$/, '')}`); + } + + let inverseLeader; + switch (leader) { + case 'is': inverseLeader = 'isNot'; break; + case 'has': inverseLeader = 'doesNotHave'; break; + case 'have': inverseLeader = 'doesNotHave'; break; + } + + return camelize([inverseLeader, ...restWords].join(' ')); +} diff --git a/tests/helpers/index.js b/tests/helpers/index.js new file mode 100644 index 0000000..a2a0fac --- /dev/null +++ b/tests/helpers/index.js @@ -0,0 +1 @@ +export { Assertions } from './assertions'; diff --git a/tests/test-helper.js b/tests/test-helper.js index 0382a84..e25c740 100644 --- a/tests/test-helper.js +++ b/tests/test-helper.js @@ -1,7 +1,17 @@ +import QUnit from 'qunit'; +import { start } from 'ember-qunit'; +import { setApplication } from '@ember/test-helpers'; + import Application from '../app'; import config from '../config/environment'; -import { setApplication } from '@ember/test-helpers'; -import { start } from 'ember-qunit'; + +import { Assertions } from './helpers'; + +QUnit.extend(QUnit.assert, { + po(node) { + return new Assertions(node, this); + } +}); setApplication(Application.create(config.APP));