From ad85de426bcaf75dd2cd61f6750af8f65d2f2cd8 Mon Sep 17 00:00:00 2001 From: Peter Wagenet Date: Thu, 12 May 2022 12:23:28 -0700 Subject: [PATCH 1/2] Convert ember-testing to TS --- .../@ember/-internals/error-handling/index.ts | 4 +- .../@ember/application/lib/application.ts | 12 +++- packages/@ember/application/lib/lazy_load.ts | 2 +- packages/@ember/debug/index.ts | 2 +- packages/@ember/engine/index.ts | 3 +- packages/ember-testing/{index.js => index.ts} | 0 .../lib/adapters/{adapter.js => adapter.ts} | 11 +++- .../lib/adapters/{qunit.js => qunit.ts} | 23 ++++++-- .../ext/{application.js => application.ts} | 49 +++++++++++----- packages/ember-testing/lib/ext/rsvp.js | 19 ------- packages/ember-testing/lib/ext/rsvp.ts | 22 ++++++++ .../lib/{helpers.js => helpers.ts} | 0 .../ember-testing/lib/helpers/and_then.js | 3 - .../ember-testing/lib/helpers/and_then.ts | 8 +++ .../{current_path.js => current_path.ts} | 12 +++- ...nt_route_name.js => current_route_name.ts} | 12 +++- .../ember-testing/lib/helpers/current_url.js | 27 --------- .../ember-testing/lib/helpers/current_url.ts | 37 ++++++++++++ .../helpers/{pause_test.js => pause_test.ts} | 2 +- packages/ember-testing/lib/helpers/visit.js | 42 -------------- packages/ember-testing/lib/helpers/visit.ts | 56 +++++++++++++++++++ .../lib/helpers/{wait.js => wait.ts} | 10 +++- packages/ember-testing/lib/initializers.js | 17 ------ packages/ember-testing/lib/initializers.ts | 19 +++++++ ...up_for_testing.js => setup_for_testing.ts} | 6 +- .../ember-testing/lib/{test.js => test.ts} | 0 .../lib/test/{adapter.js => adapter.ts} | 8 ++- .../lib/test/{helpers.js => helpers.ts} | 19 +++++-- ...inject_helpers.js => on_inject_helpers.ts} | 12 ++-- ...ending_requests.js => pending_requests.ts} | 6 +- .../lib/test/{promise.js => promise.ts} | 37 +++++++----- .../ember-testing/lib/test/{run.js => run.ts} | 2 +- .../lib/test/{waiters.js => waiters.ts} | 36 ++++++++---- 33 files changed, 336 insertions(+), 182 deletions(-) rename packages/ember-testing/{index.js => index.ts} (100%) rename packages/ember-testing/lib/adapters/{adapter.js => adapter.ts} (83%) rename packages/ember-testing/lib/adapters/{qunit.js => qunit.ts} (70%) rename packages/ember-testing/lib/ext/{application.js => application.ts} (73%) delete mode 100644 packages/ember-testing/lib/ext/rsvp.js create mode 100644 packages/ember-testing/lib/ext/rsvp.ts rename packages/ember-testing/lib/{helpers.js => helpers.ts} (100%) delete mode 100644 packages/ember-testing/lib/helpers/and_then.js create mode 100644 packages/ember-testing/lib/helpers/and_then.ts rename packages/ember-testing/lib/helpers/{current_path.js => current_path.ts} (55%) rename packages/ember-testing/lib/helpers/{current_route_name.js => current_route_name.ts} (57%) delete mode 100644 packages/ember-testing/lib/helpers/current_url.js create mode 100644 packages/ember-testing/lib/helpers/current_url.ts rename packages/ember-testing/lib/helpers/{pause_test.js => pause_test.ts} (95%) delete mode 100644 packages/ember-testing/lib/helpers/visit.js create mode 100644 packages/ember-testing/lib/helpers/visit.ts rename packages/ember-testing/lib/helpers/{wait.js => wait.ts} (83%) delete mode 100644 packages/ember-testing/lib/initializers.js create mode 100644 packages/ember-testing/lib/initializers.ts rename packages/ember-testing/lib/{setup_for_testing.js => setup_for_testing.ts} (79%) rename packages/ember-testing/lib/{test.js => test.ts} (100%) rename packages/ember-testing/lib/test/{adapter.js => adapter.ts} (73%) rename packages/ember-testing/lib/test/{helpers.js => helpers.ts} (82%) rename packages/ember-testing/lib/test/{on_inject_helpers.js => on_inject_helpers.ts} (65%) rename packages/ember-testing/lib/test/{pending_requests.js => pending_requests.ts} (66%) rename packages/ember-testing/lib/test/{promise.js => promise.ts} (59%) rename packages/ember-testing/lib/test/{run.js => run.ts} (76%) rename packages/ember-testing/lib/test/{waiters.js => waiters.ts} (71%) diff --git a/packages/@ember/-internals/error-handling/index.ts b/packages/@ember/-internals/error-handling/index.ts index 6235842fcd0..405322b7b96 100644 --- a/packages/@ember/-internals/error-handling/index.ts +++ b/packages/@ember/-internals/error-handling/index.ts @@ -14,12 +14,12 @@ export function setOnerror(handler: Function) { onerror = handler; } -let dispatchOverride: Function | undefined; +let dispatchOverride: Function | null = null; // allows testing adapter to override dispatch export function getDispatchOverride() { return dispatchOverride; } -export function setDispatchOverride(handler: Function) { +export function setDispatchOverride(handler: Function | null) { dispatchOverride = handler; } diff --git a/packages/@ember/application/lib/application.ts b/packages/@ember/application/lib/application.ts index 87de87ba743..8a332f6ffa7 100644 --- a/packages/@ember/application/lib/application.ts +++ b/packages/@ember/application/lib/application.ts @@ -23,7 +23,7 @@ import { } from '@ember/-internals/routing'; import type { BootOptions } from '../instance'; import ApplicationInstance from '../instance'; -import Engine from '@ember/engine'; +import Engine, { buildInitializerMethod } from '@ember/engine'; import type { Container, Registry } from '@ember/-internals/container'; import { privatize as P } from '@ember/-internals/container'; import { setupApplicationRegistry } from '@ember/-internals/glimmer'; @@ -220,6 +220,16 @@ class Application extends Engine { return registry; } + static initializer = buildInitializerMethod<'initializers', Application>( + 'initializers', + 'initializer' + ); + + static instanceInitializer = buildInitializerMethod<'instanceInitializers', ApplicationInstance>( + 'instanceInitializers', + 'instance initializer' + ); + /** The root DOM element of the Application. This can be specified as an element or a [selector string](https://developer.mozilla.org/en-US/docs/Learn/CSS/Building_blocks/Selectors#reference_table_of_selectors). diff --git a/packages/@ember/application/lib/lazy_load.ts b/packages/@ember/application/lib/lazy_load.ts index cf733e702a8..123e971ba2b 100644 --- a/packages/@ember/application/lib/lazy_load.ts +++ b/packages/@ember/application/lib/lazy_load.ts @@ -33,7 +33,7 @@ export let _loaded = loaded; @param callback {Function} callback to be called @private */ -export function onLoad(name: string, callback: (obj: unknown) => void) { +export function onLoad(name: string, callback: (obj: any) => void) { let object = loaded[name]; let hooks = (loadHooks[name] ??= []); diff --git a/packages/@ember/debug/index.ts b/packages/@ember/debug/index.ts index 168c0408d3a..c458b3e4be2 100644 --- a/packages/@ember/debug/index.ts +++ b/packages/@ember/debug/index.ts @@ -30,7 +30,7 @@ export type AssertFunc = (desc: string, condition?: unknown) => asserts conditio export type DebugFunc = (message: string) => void; export type DebugSealFunc = (obj: object) => void; export type DebugFreezeFunc = (obj: object) => void; -export type InfoFunc = (message: string, options: object) => void; +export type InfoFunc = (message: string, options?: object) => void; export type RunInDebugFunc = (func: () => void) => void; export type DeprecateFuncFunc = ( message: string, diff --git a/packages/@ember/engine/index.ts b/packages/@ember/engine/index.ts index f27f649bf72..4c33154b453 100644 --- a/packages/@ember/engine/index.ts +++ b/packages/@ember/engine/index.ts @@ -467,7 +467,8 @@ function resolverFor(namespace: Engine) { return ResolverClass.create(props); } -function buildInitializerMethod< +/** @internal */ +export function buildInitializerMethod< B extends 'initializers' | 'instanceInitializers', T extends B extends 'initializers' ? Engine : EngineInstance >(bucketName: B, humanName: string) { diff --git a/packages/ember-testing/index.js b/packages/ember-testing/index.ts similarity index 100% rename from packages/ember-testing/index.js rename to packages/ember-testing/index.ts diff --git a/packages/ember-testing/lib/adapters/adapter.js b/packages/ember-testing/lib/adapters/adapter.ts similarity index 83% rename from packages/ember-testing/lib/adapters/adapter.js rename to packages/ember-testing/lib/adapters/adapter.ts index 5fde61882b1..cba6d525a6e 100644 --- a/packages/ember-testing/lib/adapters/adapter.js +++ b/packages/ember-testing/lib/adapters/adapter.ts @@ -11,7 +11,12 @@ import { Object as EmberObject } from '@ember/-internals/runtime'; @class TestAdapter @public */ -export default EmberObject.extend({ +interface Adapter extends EmberObject { + asyncStart(): void; + asyncEnd(): void; + exception(error: unknown): never; +} +const Adapter = EmberObject.extend({ /** This callback will be called whenever an async operation is about to start. @@ -48,7 +53,9 @@ export default EmberObject.extend({ @method exception @param {String} error The exception to be raised. */ - exception(error) { + exception(error: unknown) { throw error; }, }); + +export default Adapter; diff --git a/packages/ember-testing/lib/adapters/qunit.js b/packages/ember-testing/lib/adapters/qunit.ts similarity index 70% rename from packages/ember-testing/lib/adapters/qunit.js rename to packages/ember-testing/lib/adapters/qunit.ts index eed764ceeb2..36b61f99da1 100644 --- a/packages/ember-testing/lib/adapters/qunit.js +++ b/packages/ember-testing/lib/adapters/qunit.ts @@ -2,6 +2,15 @@ import { inspect } from '@ember/-internals/utils'; import Adapter from './adapter'; + +interface VeryOldQunit { + stop(): void; +} + +function isVeryOldQunit(obj: unknown): obj is VeryOldQunit { + return obj != null && typeof (obj as VeryOldQunit).stop === 'function'; +} + /** @module ember */ @@ -14,13 +23,15 @@ import Adapter from './adapter'; @extends TestAdapter @public */ -export default Adapter.extend({ +// eslint-disable-next-line @typescript-eslint/no-empty-interface +interface QUnitAdapter extends Adapter {} +const QUnitAdapter = Adapter.extend({ init() { this.doneCallbacks = []; }, asyncStart() { - if (typeof QUnit.stop === 'function') { + if (isVeryOldQunit(QUnit)) { // very old QUnit version // eslint-disable-next-line qunit/no-qunit-stop QUnit.stop(); @@ -28,11 +39,12 @@ export default Adapter.extend({ this.doneCallbacks.push(QUnit.config.current ? QUnit.config.current.assert.async() : null); } }, + asyncEnd() { // checking for QUnit.stop here (even though we _need_ QUnit.start) because // QUnit.start() still exists in QUnit 2.x (it just throws an error when calling // inside a test context) - if (typeof QUnit.stop === 'function') { + if (isVeryOldQunit(QUnit)) { QUnit.start(); } else { let done = this.doneCallbacks.pop(); @@ -42,7 +54,10 @@ export default Adapter.extend({ } } }, - exception(error) { + + exception(error: unknown) { QUnit.config.current.assert.ok(false, inspect(error)); }, }); + +export default QUnitAdapter; diff --git a/packages/ember-testing/lib/ext/application.js b/packages/ember-testing/lib/ext/application.ts similarity index 73% rename from packages/ember-testing/lib/ext/application.js rename to packages/ember-testing/lib/ext/application.ts index aaefc303179..11e497ea548 100644 --- a/packages/ember-testing/lib/ext/application.js +++ b/packages/ember-testing/lib/ext/application.ts @@ -5,6 +5,19 @@ import TestPromise, { resolve, getLastPromise } from '../test/promise'; import run from '../test/run'; import { invokeInjectHelpersCallbacks } from '../test/on_inject_helpers'; import { asyncStart, asyncEnd } from '../test/adapter'; +import type Application from '@ember/application'; +import type { AnyFn } from '@ember/-internals/utils/types'; +import { assert } from '@ember/debug'; + +export interface TestableApp extends Application { + testing?: boolean; + testHelpers: Record unknown>; + originalMethods: Record unknown>; + setupForTesting(): void; + helperContainer: object | null; + injectTestHelpers(helperContainer: unknown): void; + removeTestHelpers(): void; +} EmberApplication.reopen({ /** @@ -105,7 +118,7 @@ EmberApplication.reopen({ @method injectTestHelpers @public */ - injectTestHelpers(helperContainer) { + injectTestHelpers(this: TestableApp, helperContainer: object) { if (helperContainer) { this.helperContainer = helperContainer; } else { @@ -113,7 +126,7 @@ EmberApplication.reopen({ } this.reopen({ - willDestroy() { + willDestroy(this: TestableApp) { this._super(...arguments); this.removeTestHelpers(); }, @@ -121,9 +134,12 @@ EmberApplication.reopen({ this.testHelpers = {}; for (let name in helpers) { - this.originalMethods[name] = this.helperContainer[name]; - this.testHelpers[name] = this.helperContainer[name] = helper(this, name); - protoWrap(TestPromise.prototype, name, helper(this, name), helpers[name].meta.wait); + // SAFETY: It is safe to access a property on an object + this.originalMethods[name] = (this.helperContainer as any)[name]; + // SAFETY: It is not quite as safe to do this, but it _seems_ to be ok. + this.testHelpers[name] = (this.helperContainer as any)[name] = helper(this, name); + // SAFETY: We checked that it exists + protoWrap(TestPromise.prototype, name, helper(this, name), helpers[name]!.meta.wait); } invokeInjectHelpersCallbacks(this); @@ -149,7 +165,8 @@ EmberApplication.reopen({ for (let name in helpers) { this.helperContainer[name] = this.originalMethods[name]; - delete TestPromise.prototype[name]; + // SAFETY: This is a weird thing, but it's not technically unsafe here. + delete (TestPromise.prototype as any)[name]; delete this.testHelpers[name]; delete this.originalMethods[name]; } @@ -159,26 +176,30 @@ EmberApplication.reopen({ // This method is no longer needed // But still here for backwards compatibility // of helper chaining -function protoWrap(proto, name, callback, isAsync) { - proto[name] = function (...args) { +function protoWrap(proto: TestPromise, name: string, callback: AnyFn, isAsync: boolean) { + // SAFETY: This isn't entirely safe, but it _seems_ to be ok. + (proto as any)[name] = function (...args: unknown[]) { if (isAsync) { return callback.apply(this, args); } else { - return this.then(function () { + // SAFETY: This is not actually safe. + return (this as any).then(function (this: any) { return callback.apply(this, args); }); } }; } -function helper(app, name) { - let fn = helpers[name].method; - let meta = helpers[name].meta; +function helper(app: TestableApp, name: string) { + let helper = helpers[name]; + assert(`[BUG] Missing helper: ${name}`, helper); + let fn = helper.method; + let meta = helper.meta; if (!meta.wait) { - return (...args) => fn.apply(app, [app, ...args]); + return (...args: unknown[]) => fn.apply(app, [app, ...args]); } - return (...args) => { + return (...args: unknown[]) => { let lastPromise = run(() => resolve(getLastPromise())); // wait for last helper's promise to resolve and then diff --git a/packages/ember-testing/lib/ext/rsvp.js b/packages/ember-testing/lib/ext/rsvp.js deleted file mode 100644 index 814a40a5f0f..00000000000 --- a/packages/ember-testing/lib/ext/rsvp.js +++ /dev/null @@ -1,19 +0,0 @@ -import { RSVP } from '@ember/-internals/runtime'; -import { _backburner } from '@ember/runloop'; -import { isTesting } from '@ember/debug'; -import { asyncStart, asyncEnd } from '../test/adapter'; - -RSVP.configure('async', function (callback, promise) { - // if schedule will cause autorun, we need to inform adapter - if (isTesting() && !_backburner.currentInstance) { - asyncStart(); - _backburner.schedule('actions', () => { - asyncEnd(); - callback(promise); - }); - } else { - _backburner.schedule('actions', () => callback(promise)); - } -}); - -export default RSVP; diff --git a/packages/ember-testing/lib/ext/rsvp.ts b/packages/ember-testing/lib/ext/rsvp.ts new file mode 100644 index 00000000000..51302fefeef --- /dev/null +++ b/packages/ember-testing/lib/ext/rsvp.ts @@ -0,0 +1,22 @@ +import { RSVP } from '@ember/-internals/runtime'; +import { _backburner } from '@ember/runloop'; +import { isTesting } from '@ember/debug'; +import { asyncStart, asyncEnd } from '../test/adapter'; + +RSVP.configure( + 'async', + function (callback: (promise: Promise) => void, promise: Promise) { + // if schedule will cause autorun, we need to inform adapter + if (isTesting() && !_backburner.currentInstance) { + asyncStart(); + _backburner.schedule('actions', () => { + asyncEnd(); + callback(promise); + }); + } else { + _backburner.schedule('actions', () => callback(promise)); + } + } +); + +export default RSVP; diff --git a/packages/ember-testing/lib/helpers.js b/packages/ember-testing/lib/helpers.ts similarity index 100% rename from packages/ember-testing/lib/helpers.js rename to packages/ember-testing/lib/helpers.ts diff --git a/packages/ember-testing/lib/helpers/and_then.js b/packages/ember-testing/lib/helpers/and_then.js deleted file mode 100644 index 5e451dccb95..00000000000 --- a/packages/ember-testing/lib/helpers/and_then.js +++ /dev/null @@ -1,3 +0,0 @@ -export default function andThen(app, callback) { - return app.testHelpers.wait(callback(app)); -} diff --git a/packages/ember-testing/lib/helpers/and_then.ts b/packages/ember-testing/lib/helpers/and_then.ts new file mode 100644 index 00000000000..ba38bca6520 --- /dev/null +++ b/packages/ember-testing/lib/helpers/and_then.ts @@ -0,0 +1,8 @@ +import { assert } from '@ember/debug'; +import type { TestableApp } from '../ext/application'; + +export default function andThen(app: TestableApp, callback: (app: TestableApp) => unknown) { + let wait = app.testHelpers['wait']; + assert('[BUG] Missing wait helper', wait); + return wait(callback(app)); +} diff --git a/packages/ember-testing/lib/helpers/current_path.js b/packages/ember-testing/lib/helpers/current_path.ts similarity index 55% rename from packages/ember-testing/lib/helpers/current_path.js rename to packages/ember-testing/lib/helpers/current_path.ts index a709da7639e..eb1523ab6ee 100644 --- a/packages/ember-testing/lib/helpers/current_path.js +++ b/packages/ember-testing/lib/helpers/current_path.ts @@ -2,6 +2,9 @@ @module ember */ import { get } from '@ember/-internals/metal'; +import { RoutingService } from '@ember/-internals/routing'; +import type Application from '@ember/application'; +import { assert } from '@ember/debug'; /** Returns the current path. @@ -21,7 +24,14 @@ click('#some-link-id').then(validateURL); @since 1.5.0 @public */ -export default function currentPath(app) { +export default function currentPath(app: Application) { + assert('[BUG] app.__container__ is not set', app.__container__); + let routingService = app.__container__.lookup('service:-routing'); + assert( + '[BUG] service:-routing is not a RoutingService', + routingService instanceof RoutingService + ); + return get(routingService, 'currentPath'); } diff --git a/packages/ember-testing/lib/helpers/current_route_name.js b/packages/ember-testing/lib/helpers/current_route_name.ts similarity index 57% rename from packages/ember-testing/lib/helpers/current_route_name.js rename to packages/ember-testing/lib/helpers/current_route_name.ts index 175c432d054..2e0ad85d0cb 100644 --- a/packages/ember-testing/lib/helpers/current_route_name.js +++ b/packages/ember-testing/lib/helpers/current_route_name.ts @@ -2,6 +2,9 @@ @module ember */ import { get } from '@ember/-internals/metal'; +import { RoutingService } from '@ember/-internals/routing'; +import type Application from '@ember/application'; +import { assert } from '@ember/debug'; /** Returns the currently active route name. @@ -19,7 +22,14 @@ visit('/some/path').then(validateRouteName) @since 1.5.0 @public */ -export default function currentRouteName(app) { +export default function currentRouteName(app: Application) { + assert('[BUG] app.__container__ is not set', app.__container__); + let routingService = app.__container__.lookup('service:-routing'); + assert( + '[BUG] service:-routing is not a RoutingService', + routingService instanceof RoutingService + ); + return get(routingService, 'currentRouteName'); } diff --git a/packages/ember-testing/lib/helpers/current_url.js b/packages/ember-testing/lib/helpers/current_url.js deleted file mode 100644 index d5dd1ff5ed4..00000000000 --- a/packages/ember-testing/lib/helpers/current_url.js +++ /dev/null @@ -1,27 +0,0 @@ -/** -@module ember -*/ -import { get } from '@ember/-internals/metal'; - -/** - Returns the current URL. - -Example: - -```javascript -function validateURL() { - equal(currentURL(), '/some/path', "correct URL was transitioned into."); -} - -click('#some-link-id').then(validateURL); -``` - -@method currentURL -@return {Object} The currently active URL. -@since 1.5.0 -@public -*/ -export default function currentURL(app) { - let router = app.__container__.lookup('router:main'); - return get(router, 'location').getURL(); -} diff --git a/packages/ember-testing/lib/helpers/current_url.ts b/packages/ember-testing/lib/helpers/current_url.ts new file mode 100644 index 00000000000..69148442667 --- /dev/null +++ b/packages/ember-testing/lib/helpers/current_url.ts @@ -0,0 +1,37 @@ +/** +@module ember +*/ +import { get } from '@ember/-internals/metal'; +import type Application from '@ember/application'; +import { assert } from '@ember/debug'; +import Router from '@ember/routing/router'; + +/** + Returns the current URL. + +Example: + +```javascript +function validateURL() { + equal(currentURL(), '/some/path', "correct URL was transitioned into."); +} + +click('#some-link-id').then(validateURL); +``` + +@method currentURL +@return {Object} The currently active URL. +@since 1.5.0 +@public +*/ +export default function currentURL(app: Application) { + assert('[BUG] app.__container__ is not set', app.__container__); + + let router = app.__container__.lookup('router:main'); + assert('[BUG] router:main is not a Router', router instanceof Router); + + let location = get(router, 'location'); + assert('[BUG] location is still a string', typeof location !== 'string'); + + return location.getURL(); +} diff --git a/packages/ember-testing/lib/helpers/pause_test.js b/packages/ember-testing/lib/helpers/pause_test.ts similarity index 95% rename from packages/ember-testing/lib/helpers/pause_test.js rename to packages/ember-testing/lib/helpers/pause_test.ts index c4dd718035d..b0b2321fc35 100644 --- a/packages/ember-testing/lib/helpers/pause_test.js +++ b/packages/ember-testing/lib/helpers/pause_test.ts @@ -4,7 +4,7 @@ import { RSVP } from '@ember/-internals/runtime'; import { assert, info } from '@ember/debug'; -let resume; +let resume: undefined | ((value?: unknown) => void); /** Resumes a test paused by `pauseTest`. diff --git a/packages/ember-testing/lib/helpers/visit.js b/packages/ember-testing/lib/helpers/visit.js deleted file mode 100644 index 326e61a713b..00000000000 --- a/packages/ember-testing/lib/helpers/visit.js +++ /dev/null @@ -1,42 +0,0 @@ -import { run } from '@ember/runloop'; - -/** - Loads a route, sets up any controllers, and renders any templates associated - with the route as though a real user had triggered the route change while - using your app. - - Example: - - ```javascript - visit('posts/index').then(function() { - // assert something - }); - ``` - - @method visit - @param {String} url the name of the route - @return {RSVP.Promise} - @public -*/ -export default function visit(app, url) { - let router = app.__container__.lookup('router:main'); - let shouldHandleURL = false; - - app.boot().then(() => { - router.location.setURL(url); - - if (shouldHandleURL) { - run(app.__deprecatedInstance__, 'handleURL', url); - } - }); - - if (app._readinessDeferrals > 0) { - router.initialURL = url; - run(app, 'advanceReadiness'); - delete router.initialURL; - } else { - shouldHandleURL = true; - } - - return app.testHelpers.wait(); -} diff --git a/packages/ember-testing/lib/helpers/visit.ts b/packages/ember-testing/lib/helpers/visit.ts new file mode 100644 index 00000000000..e882ab7d58b --- /dev/null +++ b/packages/ember-testing/lib/helpers/visit.ts @@ -0,0 +1,56 @@ +import { assert } from '@ember/debug'; +import Router from '@ember/routing/router'; +import { run } from '@ember/runloop'; +import type { TestableApp } from '../ext/application'; + +/** + Loads a route, sets up any controllers, and renders any templates associated + with the route as though a real user had triggered the route change while + using your app. + + Example: + + ```javascript + visit('posts/index').then(function() { + // assert something + }); + ``` + + @method visit + @param {String} url the name of the route + @return {RSVP.Promise} + @public +*/ +export default function visit(app: TestableApp, url: string) { + assert('[BUG] Missing container', app.__container__); + + const router = app.__container__.lookup('router:main'); + assert('[BUG] router:main is not a Router', router instanceof Router); + + let shouldHandleURL = false; + + app.boot().then(() => { + assert('[BUG] router.location is still a string', typeof router.location !== 'string'); + router.location.setURL(url); + + if (shouldHandleURL) { + assert("[BUG] __deprecatedInstance__ isn't set", app.__deprecatedInstance__); + run(app.__deprecatedInstance__, 'handleURL', url); + } + }); + + if (app._readinessDeferrals > 0) { + // SAFETY: This should be safe, though it is odd. + (router as any).initialURL = url; + run(app, 'advanceReadiness'); + delete (router as any).initialURL; + } else { + shouldHandleURL = true; + } + + let wait = app.testHelpers['wait']; + + assert('[BUG] missing wait helper', wait); + + return wait(); +} diff --git a/packages/ember-testing/lib/helpers/wait.js b/packages/ember-testing/lib/helpers/wait.ts similarity index 83% rename from packages/ember-testing/lib/helpers/wait.js rename to packages/ember-testing/lib/helpers/wait.ts index 232a76ab9ce..631a76bfc5f 100644 --- a/packages/ember-testing/lib/helpers/wait.js +++ b/packages/ember-testing/lib/helpers/wait.ts @@ -5,6 +5,9 @@ import { checkWaiters } from '../test/waiters'; import { RSVP } from '@ember/-internals/runtime'; import { _getCurrentRunLoop, _hasScheduledTimers, run } from '@ember/runloop'; import { pendingRequests } from '../test/pending_requests'; +import type Application from '@ember/application'; +import { assert } from '@ember/debug'; +import Router from '@ember/routing/router'; /** Causes the run loop to process any pending events. This is used to ensure that @@ -36,9 +39,12 @@ import { pendingRequests } from '../test/pending_requests'; @public @since 1.0.0 */ -export default function wait(app, value) { +export default function wait(app: Application, value: T): Promise { return new RSVP.Promise(function (resolve) { - let router = app.__container__.lookup('router:main'); + assert('[BUG] Missing container', app.__container__); + + const router = app.__container__.lookup('router:main'); + assert('[BUG] Expected router:main to be a subclass of Ember Router', router instanceof Router); // Every 10ms, poll for the async thing to have finished let watcher = setInterval(() => { diff --git a/packages/ember-testing/lib/initializers.js b/packages/ember-testing/lib/initializers.js deleted file mode 100644 index 0ff1d963fb6..00000000000 --- a/packages/ember-testing/lib/initializers.js +++ /dev/null @@ -1,17 +0,0 @@ -import { onLoad } from '@ember/application'; - -let name = 'deferReadiness in `testing` mode'; - -onLoad('Ember.Application', function (Application) { - if (!Application.initializers[name]) { - Application.initializer({ - name: name, - - initialize(application) { - if (application.testing) { - application.deferReadiness(); - } - }, - }); - } -}); diff --git a/packages/ember-testing/lib/initializers.ts b/packages/ember-testing/lib/initializers.ts new file mode 100644 index 00000000000..debe4fdd517 --- /dev/null +++ b/packages/ember-testing/lib/initializers.ts @@ -0,0 +1,19 @@ +import { onLoad } from '@ember/application'; +import type Application from '@ember/application'; +import type { TestableApp } from './ext/application'; + +let name = 'deferReadiness in `testing` mode'; + +onLoad('Ember.Application', function (ApplicationClass: typeof Application) { + if (!ApplicationClass.initializers[name]) { + ApplicationClass.initializer({ + name: name, + + initialize(application) { + if ((application as TestableApp).testing) { + application.deferReadiness(); + } + }, + }); + } +}); diff --git a/packages/ember-testing/lib/setup_for_testing.js b/packages/ember-testing/lib/setup_for_testing.ts similarity index 79% rename from packages/ember-testing/lib/setup_for_testing.js rename to packages/ember-testing/lib/setup_for_testing.ts index 91c615d5fdb..7444158c262 100644 --- a/packages/ember-testing/lib/setup_for_testing.js +++ b/packages/ember-testing/lib/setup_for_testing.ts @@ -23,6 +23,10 @@ export default function setupForTesting() { let adapter = getAdapter(); // if adapter is not manually set default to QUnit if (!adapter) { - setAdapter(typeof self.QUnit === 'undefined' ? Adapter.create() : QUnitAdapter.create()); + setAdapter( + typeof (self as any).QUnit === 'undefined' + ? (Adapter.create() as Adapter) + : (QUnitAdapter.create() as QUnitAdapter) + ); } } diff --git a/packages/ember-testing/lib/test.js b/packages/ember-testing/lib/test.ts similarity index 100% rename from packages/ember-testing/lib/test.js rename to packages/ember-testing/lib/test.ts diff --git a/packages/ember-testing/lib/test/adapter.js b/packages/ember-testing/lib/test/adapter.ts similarity index 73% rename from packages/ember-testing/lib/test/adapter.js rename to packages/ember-testing/lib/test/adapter.ts index bfecab54869..610d7e60cab 100644 --- a/packages/ember-testing/lib/test/adapter.js +++ b/packages/ember-testing/lib/test/adapter.ts @@ -1,11 +1,12 @@ import { setDispatchOverride } from '@ember/-internals/error-handling'; +import type Adapter from '../adapters/adapter'; -let adapter; +let adapter: Adapter; export function getAdapter() { return adapter; } -export function setAdapter(value) { +export function setAdapter(value: Adapter) { adapter = value; if (value && typeof value.exception === 'function') { setDispatchOverride(adapterDispatch); @@ -26,8 +27,9 @@ export function asyncEnd() { } } -function adapterDispatch(error) { +function adapterDispatch(error: unknown) { adapter.exception(error); + // @ts-expect-error Normally unreachable console.error(error.stack); // eslint-disable-line no-console } diff --git a/packages/ember-testing/lib/test/helpers.js b/packages/ember-testing/lib/test/helpers.ts similarity index 82% rename from packages/ember-testing/lib/test/helpers.js rename to packages/ember-testing/lib/test/helpers.ts index 63a3774a8c9..4d278fd0a0e 100644 --- a/packages/ember-testing/lib/test/helpers.js +++ b/packages/ember-testing/lib/test/helpers.ts @@ -1,6 +1,13 @@ +import type { AnyFn } from '@ember/-internals/utils/types'; import TestPromise from './promise'; -export const helpers = {}; +export const helpers: Record< + string, + { + method: AnyFn; + meta: { wait: boolean }; + } +> = {}; /** @module @ember/test */ @@ -42,7 +49,7 @@ export const helpers = {}; @param {Function} helperMethod @param options {Object} */ -export function registerHelper(name, helperMethod) { +export function registerHelper(name: string, helperMethod: AnyFn) { helpers[name] = { method: helperMethod, meta: { wait: false }, @@ -95,7 +102,7 @@ export function registerHelper(name, helperMethod) { @param {Function} helperMethod @since 1.2.0 */ -export function registerAsyncHelper(name, helperMethod) { +export function registerAsyncHelper(name: string, helperMethod: AnyFn) { helpers[name] = { method: helperMethod, meta: { wait: true }, @@ -119,7 +126,9 @@ export function registerAsyncHelper(name, helperMethod) { @for @ember/test @param {String} name The helper to remove. */ -export function unregisterHelper(name) { +export function unregisterHelper(name: string) { delete helpers[name]; - delete TestPromise.prototype[name]; + // SAFETY: This isn't necessarily a safe thing to do, but in terms of the immediate types here + // it won't error. + delete (TestPromise.prototype as any)[name]; } diff --git a/packages/ember-testing/lib/test/on_inject_helpers.js b/packages/ember-testing/lib/test/on_inject_helpers.ts similarity index 65% rename from packages/ember-testing/lib/test/on_inject_helpers.js rename to packages/ember-testing/lib/test/on_inject_helpers.ts index d4679f13855..b6f1c54122e 100644 --- a/packages/ember-testing/lib/test/on_inject_helpers.js +++ b/packages/ember-testing/lib/test/on_inject_helpers.ts @@ -1,4 +1,6 @@ -export const callbacks = []; +import type Application from '@ember/application'; + +export const callbacks: Array<(app: Application) => void> = []; /** Used to register callbacks to be fired whenever `App.injectTestHelpers` @@ -27,12 +29,12 @@ export const callbacks = []; @method onInjectHelpers @param {Function} callback The function to be called. */ -export function onInjectHelpers(callback) { +export function onInjectHelpers(callback: (app: Application) => void) { callbacks.push(callback); } -export function invokeInjectHelpersCallbacks(app) { - for (let i = 0; i < callbacks.length; i++) { - callbacks[i](app); +export function invokeInjectHelpersCallbacks(app: Application) { + for (let callback of callbacks) { + callback(app); } } diff --git a/packages/ember-testing/lib/test/pending_requests.js b/packages/ember-testing/lib/test/pending_requests.ts similarity index 66% rename from packages/ember-testing/lib/test/pending_requests.js rename to packages/ember-testing/lib/test/pending_requests.ts index 7ebb01b7894..bd8bfbc0b31 100644 --- a/packages/ember-testing/lib/test/pending_requests.js +++ b/packages/ember-testing/lib/test/pending_requests.ts @@ -1,4 +1,4 @@ -let requests = []; +let requests: unknown[] = []; export function pendingRequests() { return requests.length; @@ -8,11 +8,11 @@ export function clearPendingRequests() { requests.length = 0; } -export function incrementPendingRequests(_, xhr) { +export function incrementPendingRequests(_: unknown, xhr: unknown) { requests.push(xhr); } -export function decrementPendingRequests(_, xhr) { +export function decrementPendingRequests(_: unknown, xhr: unknown) { setTimeout(function () { for (let i = 0; i < requests.length; i++) { if (xhr === requests[i]) { diff --git a/packages/ember-testing/lib/test/promise.js b/packages/ember-testing/lib/test/promise.ts similarity index 59% rename from packages/ember-testing/lib/test/promise.js rename to packages/ember-testing/lib/test/promise.ts index 5ab18027d65..ccd095e98b7 100644 --- a/packages/ember-testing/lib/test/promise.js +++ b/packages/ember-testing/lib/test/promise.ts @@ -1,20 +1,31 @@ import { RSVP } from '@ember/-internals/runtime'; import run from './run'; -let lastPromise; +let lastPromise: TestPromise | null = null; -export default class TestPromise extends RSVP.Promise { - constructor() { - super(...arguments); +type Executor = ( + resolve: (value?: T | PromiseLike) => void, + reject: (reason?: any) => void +) => void; + +type OnFulfilled = (value: T) => TResult | PromiseLike; + +export default class TestPromise extends RSVP.Promise { + constructor(executor: Executor, label?: string) { + super(executor, label); lastPromise = this; } - then(_onFulfillment, ...args) { - let onFulfillment = - typeof _onFulfillment === 'function' - ? (result) => isolate(_onFulfillment, result) + then( + onFulfilled?: OnFulfilled | null, + onRejected?: ((reason: any) => TResult2 | PromiseLike) | null, + label?: string + ): RSVP.Promise { + let normalizedOnFulfilled = + typeof onFulfilled === 'function' + ? (result: T) => isolate(onFulfilled, result) : undefined; - return super.then(onFulfillment, ...args); + return super.then(normalizedOnFulfilled, onRejected, label); } } @@ -31,7 +42,7 @@ export default class TestPromise extends RSVP.Promise { @param {Function} resolver The function used to resolve the promise. @param {String} label An optional string for identifying the promise. */ -export function promise(resolver, label) { +export function promise(resolver: Executor, label: string) { let fullLabel = `Ember.Test.promise: ${label || ''}`; return new TestPromise(resolver, fullLabel); } @@ -47,7 +58,7 @@ export function promise(resolver, label) { @param {Mixed} The value to resolve @since 1.2.0 */ -export function resolve(result, label) { +export function resolve(result: unknown, label?: string) { return TestPromise.resolve(result, label); } @@ -61,11 +72,11 @@ export function getLastPromise() { // 1. Set `Ember.Test.lastPromise` to null // 2. Invoke method // 3. Return the last promise created during method -function isolate(onFulfillment, result) { +function isolate(onFulfilled: OnFulfilled, result: T) { // Reset lastPromise for nested helpers lastPromise = null; - let value = onFulfillment(result); + let value = onFulfilled(result); let promise = lastPromise; lastPromise = null; diff --git a/packages/ember-testing/lib/test/run.js b/packages/ember-testing/lib/test/run.ts similarity index 76% rename from packages/ember-testing/lib/test/run.js rename to packages/ember-testing/lib/test/run.ts index d8bcd0592f4..c9ce6a5603d 100644 --- a/packages/ember-testing/lib/test/run.js +++ b/packages/ember-testing/lib/test/run.ts @@ -1,6 +1,6 @@ import { _getCurrentRunLoop, run as emberRun } from '@ember/runloop'; -export default function run(fn) { +export default function run(fn: () => T): T { if (!_getCurrentRunLoop()) { return emberRun(fn); } else { diff --git a/packages/ember-testing/lib/test/waiters.js b/packages/ember-testing/lib/test/waiters.ts similarity index 71% rename from packages/ember-testing/lib/test/waiters.js rename to packages/ember-testing/lib/test/waiters.ts index 12f891a1122..5b53310d976 100644 --- a/packages/ember-testing/lib/test/waiters.js +++ b/packages/ember-testing/lib/test/waiters.ts @@ -1,8 +1,8 @@ /** @module @ember/test */ -const contexts = []; -const callbacks = []; +const contexts: unknown[] = []; +const callbacks: Array<() => unknown> = []; /** This allows ember-testing to play nicely with other asynchronous @@ -40,16 +40,27 @@ const callbacks = []; @param {Function} callback @since 1.2.0 */ -export function registerWaiter(context, callback) { - if (arguments.length === 1) { - callback = context; - context = null; +export function registerWaiter(context: T, callback: (this: T) => unknown): void; +export function registerWaiter(callback: (this: null) => unknown): void; +export function registerWaiter( + ...args: [context: T, callback: (this: T) => unknown] | [callback: (this: null) => unknown] +): void { + let checkedCallback: () => unknown; + let checkedContext: T | null; + + if (args.length === 1) { + checkedContext = null; + checkedCallback = args[0]; + } else { + checkedContext = args[0]; + checkedCallback = args[1]; } - if (indexOf(context, callback) > -1) { + + if (indexOf(checkedContext, checkedCallback) > -1) { return; } - contexts.push(context); - callbacks.push(callback); + contexts.push(checkedContext); + callbacks.push(checkedCallback); } /** @@ -64,7 +75,7 @@ export function registerWaiter(context, callback) { @param {Function} callback @since 1.2.0 */ -export function unregisterWaiter(context, callback) { +export function unregisterWaiter(context: unknown, callback: unknown) { if (!callbacks.length) { return; } @@ -100,14 +111,15 @@ export function checkWaiters() { for (let i = 0; i < callbacks.length; i++) { let context = contexts[i]; let callback = callbacks[i]; - if (!callback.call(context)) { + // SAFETY: The loop ensures that this exists + if (!callback!.call(context)) { return true; } } return false; } -function indexOf(context, callback) { +function indexOf(context: unknown, callback: unknown) { for (let i = 0; i < callbacks.length; i++) { if (callbacks[i] === callback && contexts[i] === context) { return i; From 09a73ada1e5140ea0f0179dcbe6245255c336674 Mon Sep 17 00:00:00 2001 From: Peter Wagenet Date: Thu, 12 May 2022 14:51:31 -0700 Subject: [PATCH 2/2] Convert @ember/test to TS --- packages/@ember/test/adapter.js | 3 --- packages/@ember/test/adapter.ts | 3 +++ packages/@ember/test/{index.js => index.ts} | 11 ++++++----- 3 files changed, 9 insertions(+), 8 deletions(-) delete mode 100644 packages/@ember/test/adapter.js create mode 100644 packages/@ember/test/adapter.ts rename packages/@ember/test/{index.js => index.ts} (62%) diff --git a/packages/@ember/test/adapter.js b/packages/@ember/test/adapter.js deleted file mode 100644 index e0f19949e64..00000000000 --- a/packages/@ember/test/adapter.js +++ /dev/null @@ -1,3 +0,0 @@ -import { Test } from 'ember-testing'; - -export default Test.Adapter; diff --git a/packages/@ember/test/adapter.ts b/packages/@ember/test/adapter.ts new file mode 100644 index 00000000000..1832534f0d7 --- /dev/null +++ b/packages/@ember/test/adapter.ts @@ -0,0 +1,3 @@ +import { Adapter } from 'ember-testing'; + +export default Adapter; diff --git a/packages/@ember/test/index.js b/packages/@ember/test/index.ts similarity index 62% rename from packages/@ember/test/index.js rename to packages/@ember/test/index.ts index 3f226dbcac6..2cf30f846ce 100644 --- a/packages/@ember/test/index.js +++ b/packages/@ember/test/index.ts @@ -1,10 +1,11 @@ import require, { has } from 'require'; +import { type Test } from 'ember-testing'; -export let registerAsyncHelper; -export let registerHelper; -export let registerWaiter; -export let unregisterHelper; -export let unregisterWaiter; +export let registerAsyncHelper: typeof Test['registerAsyncHelper'] | (() => never); +export let registerHelper: typeof Test['registerHelper'] | (() => never); +export let registerWaiter: typeof Test['registerWaiter'] | (() => never); +export let unregisterHelper: typeof Test['unregisterHelper'] | (() => never); +export let unregisterWaiter: typeof Test['unregisterWaiter'] | (() => never); if (has('ember-testing')) { let { Test } = require('ember-testing');