Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Convert to TypeScript #450

Merged
merged 14 commits into from
Nov 1, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 18 additions & 5 deletions .eslintrc.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
module.exports = {
root: true,
extends: ['eslint:recommended', 'prettier'],
plugins: ['prettier'],
parser: 'typescript-eslint-parser',
plugins: [
'prettier',
'typescript',
],
parserOptions: {
ecmaVersion: 2017,
sourceType: 'module',
Expand All @@ -18,7 +22,7 @@ module.exports = {
},
overrides: [
{
files: ['index.js', 'config/ember-try.js'],
files: ['index.js', 'config/ember-try.js', 'scripts/**'],
excludedFiles: ['addon-test-support/**', 'tests/**'],
parserOptions: {
ecmaVersion: 2015,
Expand All @@ -35,13 +39,22 @@ module.exports = {
}),
},
{
files: ['tests/**/*.js'],
files: ['tests/**/*.[jt]s'],
env: {
qunit: true
}
},
{
files: ['index.js', 'addon-test-support/**/*.js', 'config/**/*.js'],
files: ['**/*.ts'],
rules: {
// the TypeScript compiler already takes care of this and
// leaving it enabled results in false positives for interface imports
'no-unused-vars': false,
'no-undef': false,
}
},
{
files: ['index.js', 'addon-test-support/**/*.[jt]s', 'config/**/*.js'],
plugins: [
'disable-features',
],
Expand All @@ -51,7 +64,7 @@ module.exports = {
}
},
{
files: ['addon-test-support/**/*.js'],
files: ['addon-test-support/**/*.[jt]s'],
excludedFiles: ['addon-test-support/ember-test-helpers/legacy-0-6-x/**'],
rules: {
'valid-jsdoc': ['error', {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/* globals Promise */
import { Promise as RSVPPromise } from 'rsvp';

export const nextTick =
export const nextTick: Function =
typeof Promise === 'undefined' ? setTimeout : cb => Promise.resolve().then(cb);
export const futureTick = setTimeout;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,13 @@ import { nextTickPromise } from '../-utils';
@param {string} text the text to fill into the target element
@return {Promise<void>} resolves when the application is settled
*/
export default function fillIn(target, text) {
export default function fillIn(target: any, text: string): Promise<void> {
return nextTickPromise().then(() => {
if (!target) {
throw new Error('Must pass an element or selector to `fillIn`.');
}

let element = getElement(target);
let element = getElement(target) as any;
if (!element) {
throw new Error(`Element not found when calling \`fillIn('${target}')\`.`);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export default function fireEvent(element, eventType, options = {}) {
event = buildKeyboardEvent(eventType, options);
} else if (MOUSE_EVENT_TYPES.indexOf(eventType) > -1) {
let rect;
if (element instanceof Window) {
if (element instanceof Window && element.document.documentElement) {
rect = element.document.documentElement.getBoundingClientRect();
} else if (element.nodeType === Node.DOCUMENT_NODE) {
rect = element.documentElement.getBoundingClientRect();
Expand Down Expand Up @@ -75,7 +75,7 @@ export default function fireEvent(element, eventType, options = {}) {
}

// eslint-disable-next-line require-jsdoc
function buildBasicEvent(type, options = {}) {
function buildBasicEvent(type, options: any = {}) {
let event = document.createEvent('Events');

let bubbles = options.bubbles !== undefined ? options.bubbles : true;
Expand All @@ -94,7 +94,7 @@ function buildBasicEvent(type, options = {}) {
// eslint-disable-next-line require-jsdoc
function buildMouseEvent(type, options = {}) {
let event;
let eventOpts = assign({ view: window }, DEFAULT_EVENT_OPTIONS, options);
let eventOpts: any = assign({ view: window }, DEFAULT_EVENT_OPTIONS, options);
if (MOUSE_EVENT_CONSTRUCTOR) {
event = new MouseEvent(type, eventOpts);
} else {
Expand Down Expand Up @@ -127,7 +127,7 @@ function buildMouseEvent(type, options = {}) {

// eslint-disable-next-line require-jsdoc
function buildKeyboardEvent(type, options = {}) {
let eventOpts = assign({}, DEFAULT_EVENT_OPTIONS, options);
let eventOpts: any = assign({}, DEFAULT_EVENT_OPTIONS, options);
let event, eventMethodName;

try {
Expand Down Expand Up @@ -195,7 +195,7 @@ function buildKeyboardEvent(type, options = {}) {
}

// eslint-disable-next-line require-jsdoc
function buildFileEvent(type, element, options = {}) {
function buildFileEvent(type, element, options: any = {}) {
let event = buildBasicEvent(type);
let files;
if (Array.isArray(options)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ function keyCodeFromKey(key) {
if (!keyCode) {
keyCode = keys.find(keyCode => keyFromKeyCode[keyCode] === key.toLowerCase());
}
return parseInt(keyCode);
return keyCode !== undefined ? parseInt(keyCode) : undefined;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ export default function typeIn(target, text, options = { delay: 50 }) {

// eslint-disable-next-line require-jsdoc
function fillOut(element, text, delay) {
const inputFunctions = text.split('').map(character => keyEntry(element, character, delay));
const inputFunctions = text.split('').map(character => keyEntry(element, character));
return inputFunctions.reduce((currentPromise, func) => {
return currentPromise.then(() => delayedExecute(func, delay));
}, Promise.resolve());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,10 @@ import { nextTickPromise } from '../-utils';
@param {number} [options.count=null] the number of elements that should match the provided selector (null means one or more)
@return {Promise<Element|Element[]>} resolves when the element(s) appear on the page
*/
export default function waitFor(selector, { timeout = 1000, count = null, timeoutMessage } = {}) {
export default function waitFor(
selector,
{ timeout = 1000, count = null, timeoutMessage = '' } = {}
) {
return nextTickPromise().then(() => {
if (!selector) {
throw new Error('Must pass a selector to `waitFor`.');
Expand All @@ -31,6 +34,7 @@ export default function waitFor(selector, { timeout = 1000, count = null, timeou
if (elements.length === count) {
return toArray(elements);
}
return;
};
} else {
callback = () => getElement(selector);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,14 @@ import waitUntil from './wait-until';
// This utilizes a local utility method present in Ember since around 2.8.0 to
// properly consider pending AJAX requests done within legacy acceptance tests.
const _internalPendingRequests = (() => {
if (Ember.__loader.registry['ember-testing/test/pending_requests']) {
let loader = (Ember as any).__loader;

if (loader.registry['ember-testing/test/pending_requests']) {
// Ember <= 3.1
return Ember.__loader.require('ember-testing/test/pending_requests').pendingRequests;
} else if (Ember.__loader.registry['ember-testing/lib/test/pending_requests']) {
return loader.require('ember-testing/test/pending_requests').pendingRequests;
} else if (loader.registry['ember-testing/lib/test/pending_requests']) {
// Ember >= 3.2
return Ember.__loader.require('ember-testing/lib/test/pending_requests').pendingRequests;
return loader.require('ember-testing/lib/test/pending_requests').pendingRequests;
}

return () => 0;
Expand Down Expand Up @@ -111,12 +113,14 @@ export function _setupAJAXHooks() {
}

let _internalCheckWaiters;
if (Ember.__loader.registry['ember-testing/test/waiters']) {

let loader = (Ember as any).__loader;
if (loader.registry['ember-testing/test/waiters']) {
// Ember <= 3.1
_internalCheckWaiters = Ember.__loader.require('ember-testing/test/waiters').checkWaiters;
} else if (Ember.__loader.registry['ember-testing/lib/test/waiters']) {
_internalCheckWaiters = loader.require('ember-testing/test/waiters').checkWaiters;
} else if (loader.registry['ember-testing/lib/test/waiters']) {
// Ember >= 3.2
_internalCheckWaiters = Ember.__loader.require('ember-testing/lib/test/waiters').checkWaiters;
_internalCheckWaiters = loader.require('ember-testing/lib/test/waiters').checkWaiters;
}

/**
Expand All @@ -126,8 +130,8 @@ if (Ember.__loader.registry['ember-testing/test/waiters']) {
function checkWaiters() {
if (_internalCheckWaiters) {
return _internalCheckWaiters();
} else if (Ember.Test.waiters) {
if (Ember.Test.waiters.any(([context, callback]) => !callback.call(context))) {
} else if ((Ember.Test as any).waiters) {
if ((Ember.Test as any).waiters.any(([context, callback]) => !callback.call(context))) {
return true;
}
}
Expand Down Expand Up @@ -158,8 +162,8 @@ export function getSettledState() {
let pendingRequestCount = pendingRequests();

return {
hasPendingTimers: Boolean(run.hasScheduledTimers()),
hasRunLoop: Boolean(run.currentRunLoop),
hasPendingTimers: Boolean((run as any).hasScheduledTimers()),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't Ember.run have stuff in @types/ember? Why do we have to cast here?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it does, but unfortunately those two are missing from the published types 😢

Copy link

@mike-north mike-north Nov 1, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Turbo87 @rwjblue The only reason they're missing is because there's no indication that they're part of Ember's public API. We look to https://www.emberjs.com/api/ as the source of truth.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ya, seems fine though this library (IMHO by necessity) will have to use what amount to private APIs for applications so that it can absorb any private API churn and expose actually public stable APIs to our users.

Copy link

@mike-north mike-north Nov 1, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ya, seems fine though this library (IMHO by necessity) will have to use what amount to private APIs for applications so that it can absorb any private API churn and expose actually public stable APIs to our users.

In the future, we may want to discuss the pros/cons of publishing an additional types package for authors of libraries like this, to layer on top of the public ember types. I have some quality/stability concerns around each author aiming to define/maintain this type info themselves.

Currently, this is often thought of as a (false) choice between type-checking and keeping to the confines of the public API.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah. The issue is somewhat complex, the layers of “public” are quite different. For example:

  • intended for consumption by apps
  • intended for consumption by tooling
  • intended for consumption by “privileged” addons

Things like the modifier and component manager APIs are good examples of things intended for addons but not apps.

Things like default outlet templates / using outlet state / etc are examples of things Ember intentionally exposes for this addon to be able to provide its APIs, but those things should never be made exposed to other addons or apps...

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is highly motivating me to get back to the work I started in the spring: we really need an RFC clarifying a lot of this. (And quite a few other things around types and stability, too.) That's going to become increasingly important as more and more of core pieces of Ember land. I'll see if I can block out some time to do that in the next two weeks.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addendum: as a stopgap I think publishing something like an "intimate APIs" with LOUD caveats about what they do and don't represent, and of course not installing them by default from ember-cli-typescript, would probably work for the short-to-medium term fix we need to prevent the problem we're going to start seeing (if we're not already) of multiple, differing type definitions for the same information.

hasRunLoop: Boolean((run as any).currentRunLoop),
hasPendingWaiters: checkWaiters(),
hasPendingRequests: pendingRequestCount > 0,
pendingRequestCount,
Expand Down Expand Up @@ -193,6 +197,6 @@ export function isSettled() {
@public
@returns {Promise<void>} resolves when settled
*/
export default function settled() {
return waitUntil(isSettled, { timeout: Infinity });
export default function settled(): Promise<void> {
return waitUntil(isSettled, { timeout: Infinity }).then(() => {});
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/* globals EmberENV */
import { get } from '@ember/object';
import { nextTickPromise } from './-utils';
import { getContext } from './setup-context';
import global from './global';
import hasEmberVersion from './has-ember-version';
import settled from './settled';

Expand All @@ -20,7 +20,7 @@ export function visit() {
return owner.visit(...arguments);
})
.then(() => {
if (EmberENV._APPLICATION_TEMPLATE_WRAPPER !== false) {
if (global.EmberENV._APPLICATION_TEMPLATE_WRAPPER !== false) {
context.element = document.querySelector('#ember-testing > .ember-view');
} else {
context.element = document.querySelector('#ember-testing');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,8 +128,8 @@ export const CLEANUP = Object.create(null);
@param {Resolver} [options.resolver] a resolver to use for customizing normal resolution
@returns {Promise<Object>} resolves with the context that was setup
*/
export default function(context, options = {}) {
Ember.testing = true;
export default function(context, options: any = {}) {
(Ember as any).testing = true;
setContext(context);

let contextGuid = guidFor(context);
Expand All @@ -143,7 +143,7 @@ export default function(context, options = {}) {
}
})
.then(() => {
let testElementContainer = document.getElementById('ember-testing-container');
let testElementContainer = document.getElementById('ember-testing-container')!; // TODO remove "!"
let fixtureResetValue = testElementContainer.innerHTML;

// push this into the final cleanup bucket, to be ran _after_ the owner
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ export default function setupRenderingContext(context) {
// `Ember._ContainerProxyMixin` and `Ember._RegistryProxyMixin` in this scenario we need to
// manually start the event dispatcher.
if (owner._emberTestHelpersMockOwner) {
let dispatcher = owner.lookup('event_dispatcher:main') || Ember.EventDispatcher.create();
let dispatcher = owner.lookup('event_dispatcher:main') || (Ember.EventDispatcher as any).create();
dispatcher.setup({}, '#ember-testing');
}

Expand All @@ -199,7 +199,7 @@ export default function setupRenderingContext(context) {

// initially render a simple empty template
return render(EMPTY_TEMPLATE).then(() => {
run(toplevelView, 'appendTo', getRootElement());
(run as Function)(toplevelView, 'appendTo', getRootElement());

return settled();
});
Expand All @@ -216,7 +216,7 @@ export default function setupRenderingContext(context) {
// and therefore we cannot update the `this.element` until after the
// rendering is completed
value:
EmberENV._APPLICATION_TEMPLATE_WRAPPER !== false
global.EmberENV._APPLICATION_TEMPLATE_WRAPPER !== false
? getRootElement().querySelector('.ember-view')
: getRootElement(),

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export default function teardownContext(context) {
_teardownAJAXHooks();

run(owner, 'destroy');
Ember.testing = false;
(Ember as any).testing = false;

unsetContext();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,15 +36,15 @@ export default function validateErrorHandler(callback = Ember.onerror) {
let error = new Error('Error handler validation error!');

let originalEmberTesting = Ember.testing;
Ember.testing = true;
(Ember as any).testing = true;
try {
callback(error);
} catch (e) {
if (e === error) {
return VALID;
}
} finally {
Ember.testing = originalEmberTesting;
(Ember as any).testing = originalEmberTesting;
}

return INVALID;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ const MAX_TIMEOUT = 10;
@param {string} [options.timeoutMessage='waitUntil timed out'] the message to use in the reject on timeout
@returns {Promise} resolves with the callback value when it returns a truthy value
*/
export default function waitUntil(callback, options = {}) {
export default function waitUntil(callback, options: any = {}) {
let timeout = 'timeout' in options ? options.timeout : 1000;
let timeoutMessage = 'timeoutMessage' in options ? options.timeoutMessage : 'waitUntil timed out';

Expand Down
Empty file.
26 changes: 25 additions & 1 deletion index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
'use strict';

const BroccoliDebug = require('broccoli-debug');
const debugTree = BroccoliDebug.buildDebugCallback('ember-test-helpers');

module.exports = {
name: 'ember-test-helpers',

Expand Down Expand Up @@ -27,9 +30,30 @@ module.exports = {
// so that can have our `import`'s be
// import { ... } from 'ember-test-helpers';

return this.preprocessJs(tree, '/', this.name, {
let input = debugTree(tree, 'addon-test-support:input');

let compiler = this.project._incrementalTsCompiler;
if (compiler) {
// eslint-disable-next-line node/no-unpublished-require
let TypescriptOutput = require('ember-cli-typescript/js/lib/incremental-typescript-compiler/typescript-output-plugin');
// eslint-disable-next-line node/no-unpublished-require
let MergeTrees = require('broccoli-merge-trees');

let tsTree = debugTree(
new TypescriptOutput(compiler, {
'addon-test-support/@ember/test-helpers': '@ember/test-helpers',
}),
'addon-test-support:ts'
);

input = debugTree(new MergeTrees([input, tsTree]), 'addon-test-support:merged');
}

let output = this.preprocessJs(input, '/', this.name, {
registry: this.registry,
});

return debugTree(output, 'addon-test-support:output');
},

treeForVendor(rawVendorTree) {
Expand Down
Loading