From ff2d5ea0083c8b5ad709b1d446bf75887e354ee2 Mon Sep 17 00:00:00 2001 From: Chris Krycho Date: Wed, 20 Jul 2022 18:52:16 -0600 Subject: [PATCH 01/24] Upgrade to TS 4.7 --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index bc91196f1..6b1883c80 100644 --- a/package.json +++ b/package.json @@ -93,7 +93,7 @@ "release-it": "~14.11.6", "release-it-lerna-changelog": "^3.1.0", "rimraf": "^3.0.2", - "typescript": "^4.3.5", + "typescript": "^4.7.4", "webpack": "^5.57.1" }, "engines": { diff --git a/yarn.lock b/yarn.lock index a913c62a9..ac8425a1e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -13786,10 +13786,10 @@ typescript-memoize@^1.0.1: resolved "https://registry.yarnpkg.com/typescript-memoize/-/typescript-memoize-1.1.0.tgz#4a8f512d06fc995167c703a3592219901db8bc79" integrity sha512-LQPKVXK8QrBBkL/zclE6YgSWn0I8ew5m0Lf+XL00IwMhlotqRLlzHV+BRrljVQIc+NohUAuQP7mg4HQwrx5Xbg== -typescript@^4.3.5: - version "4.3.5" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.3.5.tgz#4d1c37cc16e893973c45a06886b7113234f119f4" - integrity sha512-DqQgihaQ9cUrskJo9kIyW/+g0Vxsk8cDtZ52a3NGh0YNTfpUSArXSohyUGnvbPazEPLu398C0UxmKSOrPumUzA== +typescript@^4.7.4: + version "4.7.4" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.7.4.tgz#1a88596d1cf47d59507a1bcdfb5b9dfe4d488235" + integrity sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ== uc.micro@^1.0.1, uc.micro@^1.0.5: version "1.0.6" From db9a6d35e60394f98d449805b1606ceea9ed934f Mon Sep 17 00:00:00 2001 From: Chris Krycho Date: Wed, 20 Jul 2022 16:28:47 -0600 Subject: [PATCH 02/24] Introduce public TypeScript support - **Add supported TypeScript versions to CI.** We initially target just 4.7 and `next`. We could also add more, earlier versions, but per RFC 0800 are not obliged to and starting with 4.7 gives us access (once we bump our required Node version) to support for ESM. While it will likely be quite some time before we take advantage of that, given how much other work the ecosystem has to do, it is also rare to do breaking changes in this library, so this is a good starting point! - Add the TS support policy and currently supported versions to the README. - Use `@tsconfig/ember` to define the compiler options. This simplifies our maintenance story, and along with the CI and READMe tweaks brings us in line with the requirements of the SemVer spec. --- .github/workflows/ci-build.yml | 24 ++++++++++++++++ README.md | 3 ++ package.json | 1 + tsconfig.json | 50 ++++++---------------------------- yarn.lock | 5 ++++ 5 files changed, 41 insertions(+), 42 deletions(-) diff --git a/.github/workflows/ci-build.yml b/.github/workflows/ci-build.yml index 5e91bbefb..c0c28d80c 100644 --- a/.github/workflows/ci-build.yml +++ b/.github/workflows/ci-build.yml @@ -71,3 +71,27 @@ jobs: run: yarn install --frozen-lockfile - name: test run: node_modules/.bin/ember try:one ${{ matrix.ember-try-scenario }} --skip-cleanup + + types: + runs-on: ubuntu-latest + + needs: test + + strategy: + fail-fast: false + matrix: + ts-version: + - 4.7 + - next + + steps: + - uses: actions/checkout@v2 + - uses: volta-cli/action@v1 + with: + node-version: 12.x + - name: install dependencies + run: yarn install --frozen-lockfile + - name: install TS version + run: yarn install --dev typescript@${{matrix.ts-version}} + - name: test types + run: yarn lint:ts diff --git a/README.md b/README.md index 537f84adc..2b5589d10 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,9 @@ Compatibility - Ember 3.8 or above - Ember CLI 3.8 or above - Node.js 10 or above +- TypeScript 4.7 + - SemVer policy: [simple majors](https://www.semver-ts.org/#simple-majors) + - the public API is defined by [API.md](./API.md). Installation diff --git a/package.json b/package.json index 6b1883c80..5d10b6647 100644 --- a/package.json +++ b/package.json @@ -56,6 +56,7 @@ "@glimmer/component": "^1.0.4", "@glimmer/interfaces": "^0.84.1", "@glimmer/reference": "^0.84.1", + "@tsconfig/ember": "^1.0.1", "@types/ember": "^3.16.5", "@types/ember-testing-helpers": "^0.0.4", "@types/rsvp": "^4.0.4", diff --git a/tsconfig.json b/tsconfig.json index 26310d93b..8cd45103f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,49 +1,15 @@ { + "extends": "@tsconfig/ember/tsconfig.json", "compilerOptions": { - "target": "es2019", - "allowJs": false, - "moduleResolution": "node", - "allowSyntheticDefaultImports": true, - "noImplicitAny": true, - "noImplicitThis": true, - "alwaysStrict": true, - "strictNullChecks": true, - "strictPropertyInitialization": true, - "noFallthroughCasesInSwitch": true, - "noUnusedLocals": true, - "noUnusedParameters": false, - "noImplicitReturns": true, - "noEmitOnError": false, - "noEmit": true, - "declaration": true, - "inlineSourceMap": true, - "inlineSources": true, - "module": "ES2015", "paths": { - "dummy/tests/*": [ - "./tests/*" - ], - "dummy/*": [ - "./tests/dummy/app/*", - "./app/*" - ], - "@ember/test-helpers": [ - "./addon-test-support/@ember/test-helpers" - ], - "@ember/test-helpers/*": [ - "./addon-test-support/@ember/test-helpers/*" - ], + "dummy/tests/*": ["./tests/*"], + "dummy/*": ["./tests/dummy/app/*", "./app/*"], + "@ember/test-helpers": ["./addon-test-support/@ember/test-helpers"], + "@ember/test-helpers/*": ["./addon-test-support/@ember/test-helpers/*"], "@ember/destroyable": ["./node_modules/ember-destroyable-polyfill"], - "*": [ - "./types/*", - "./node_modules/@types/*" - ] + "*": ["./types/*", "./node_modules/@types/*"] } }, - "exclude": [ - "node_modules" - ], - "include": [ - "./addon-test-support/**/*.ts" - ] + "exclude": ["node_modules"], + "include": ["./addon-test-support/**/*.ts"] } diff --git a/yarn.lock b/yarn.lock index ac8425a1e..23b306f74 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2168,6 +2168,11 @@ dependencies: defer-to-connect "^2.0.0" +"@tsconfig/ember@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@tsconfig/ember/-/ember-1.0.1.tgz#d7556d81f108438c17e4030acb4e1be6b2974e88" + integrity sha512-aPzLw5BfQxsFPrh5fNDOK4SbSkp2q5fMlrKVeniVjMz1lAcyOh2eH5THkKKcBi1YN1/fbMdAWN/dKGW6lg2+8g== + "@types/acorn@^4.0.3": version "4.0.6" resolved "https://registry.yarnpkg.com/@types/acorn/-/acorn-4.0.6.tgz#d61ca5480300ac41a7d973dd5b84d0a591154a22" From 30e9c1f93bb69a10f73bfad532f764d7430201f6 Mon Sep 17 00:00:00 2001 From: Chris Krycho Date: Wed, 20 Jul 2022 18:51:50 -0600 Subject: [PATCH 03/24] Upgrade to Ember v4 types Since this library was not generally using deprecated APIs, this simply allows us to get the latest and greatest type definitions. The one deprecated API that *is* in use is direct access to the `run` namespace. However, that is already covered via a `hasEmberVersion` check. In several cases, introduce additional safety handling where TypeScript can now identify failure modes (both because of improvements to the compiler and because our tsconfig is now stricter). In others, introduce hard errors for cases where TypeScript points out the need for *some* variety of validation, but the failure scenario is one where there is nothing actionable whatsoever. Along the way, fix a number of lingering type errors, some of which were pedantic but a number of which represented actual failure conditions. --- .../test-helpers/-internal/build-registry.ts | 12 +- .../test-helpers/-internal/debug-info.ts | 55 ++- .../test-helpers/-internal/deprecations.ts | 6 +- .../@ember/test-helpers/-internal/warnings.ts | 4 +- .../@ember/test-helpers/build-owner.ts | 3 +- .../test-helpers/dom/get-root-element.ts | 7 +- .../@ember/test-helpers/dom/tab.ts | 3 +- .../@ember/test-helpers/dom/tap.ts | 2 +- .../test-helpers/dom/trigger-key-event.ts | 2 +- .../@ember/test-helpers/dom/type-in.ts | 2 +- .../@ember/test-helpers/has-ember-version.ts | 7 +- .../@ember/test-helpers/index.ts | 15 + .../@ember/test-helpers/resolver.ts | 2 +- .../test-helpers/setup-application-context.ts | 18 +- .../@ember/test-helpers/setup-context.ts | 38 +- .../test-helpers/setup-rendering-context.ts | 15 +- .../@ember/test-helpers/wait-until.ts | 6 +- package.json | 13 +- tsconfig.json | 3 +- types/@ember/runloop.d.ts | 374 ------------------ yarn.lock | 157 ++++---- 21 files changed, 210 insertions(+), 534 deletions(-) delete mode 100644 types/@ember/runloop.d.ts diff --git a/addon-test-support/@ember/test-helpers/-internal/build-registry.ts b/addon-test-support/@ember/test-helpers/-internal/build-registry.ts index ccad02a6c..26d8fcd2a 100644 --- a/addon-test-support/@ember/test-helpers/-internal/build-registry.ts +++ b/addon-test-support/@ember/test-helpers/-internal/build-registry.ts @@ -1,4 +1,4 @@ -import type Resolver from '@ember/application/resolver'; +import type Resolver from 'ember-resolver'; import ApplicationInstance from '@ember/application/instance'; import Application from '@ember/application'; import EmberObject from '@ember/object'; @@ -31,9 +31,11 @@ function exposeRegistryMethodsWithoutDeprecations(container: any) { for (let i = 0, l = methods.length; i < l; i++) { let method = methods[i]; - if (method in container) { + if (method && method in container) { container[method] = function (...args: unknown[]) { - return container._registry[method](...args); + // SAFETY: `method` is defined because we *just* checked that it is in + // the conditional wrapping this. + return container._registry[method!](...args); }; } } @@ -57,10 +59,10 @@ const Owner = EmberObject.extend(RegistryProxyMixin, ContainerProxyMixin, { * @see {@link https://github.com/emberjs/ember.js/blob/v4.5.0-alpha.5/packages/%40ember/engine/instance.ts#L152-L167} */ unregister(fullName: string) { - this.__container__.reset(fullName); + this['__container__'].reset(fullName); // We overwrote this method from RegistryProxyMixin. - this.__registry__.unregister(fullName); + this['__registry__'].unregister(fullName); }, }); diff --git a/addon-test-support/@ember/test-helpers/-internal/debug-info.ts b/addon-test-support/@ember/test-helpers/-internal/debug-info.ts index 7756edd75..72777cb82 100644 --- a/addon-test-support/@ember/test-helpers/-internal/debug-info.ts +++ b/addon-test-support/@ember/test-helpers/-internal/debug-info.ts @@ -1,15 +1,10 @@ +import { _backburner } from '@ember/runloop'; import { - _backburner, DebugInfo as BackburnerDebugInfo, QueueItem, - DeferredActionQueues, -} from '@ember/runloop'; +} from '@ember/runloop/-private/backburner'; import { DebugInfoHelper, debugInfoHelpers } from './debug-info-helpers'; -import { - getPendingWaiterState, - PendingWaiterState, - TestWaiterDebugInfo, -} from '@ember/test-waiters'; +import { getPendingWaiterState, PendingWaiterState } from '@ember/test-waiters'; const PENDING_AJAX_REQUESTS = 'Pending AJAX requests'; const PENDING_TEST_WAITERS = 'Pending test waiters'; @@ -17,7 +12,6 @@ const SCHEDULED_ASYNC = 'Scheduled async'; const SCHEDULED_AUTORUN = 'Scheduled autorun'; type MaybeDebugInfo = BackburnerDebugInfo | null; -type WaiterDebugInfo = true | unknown[]; interface SettledState { hasPendingTimers: boolean; @@ -37,7 +31,7 @@ interface SummaryInfo { pendingTimersCount: number; hasPendingTimers: boolean; pendingTimersStackTraces: (string | undefined)[]; - pendingScheduledQueueItemCount: Number; + pendingScheduledQueueItemCount: number; pendingScheduledQueueItemStackTraces: (string | undefined)[]; hasRunLoop: boolean; isRenderPending: boolean; @@ -112,9 +106,12 @@ export class TestDebugInfo implements DebugInfo { this._summaryInfo.pendingScheduledQueueItemCount = this._debugInfo.instanceStack .filter((q) => q) - .reduce((total: Number, item) => { - Object.keys(item).forEach((queueName: string) => { - total += item[queueName].length; + .reduce((total, item) => { + Object.keys(item).forEach((queueName) => { + // SAFETY: this cast is *not* safe, but the underlying type is + // not currently able to be safer than this because it was + // built as a bag-of-queues *and* a structured item originally. + total += (item[queueName] as QueueItem[]).length; }); return total; @@ -122,21 +119,17 @@ export class TestDebugInfo implements DebugInfo { this._summaryInfo.pendingScheduledQueueItemStackTraces = this._debugInfo.instanceStack .filter((q) => q) - .reduce( - ( - stacks: string[], - deferredActionQueues: DeferredActionQueues - ) => { - Object.keys(deferredActionQueues).forEach((queue) => { - deferredActionQueues[queue].forEach( - (queueItem: QueueItem) => - queueItem.stack && stacks.push(queueItem.stack) - ); - }); - return stacks; - }, - [] - ); + .reduce((stacks, deferredActionQueues) => { + Object.keys(deferredActionQueues).forEach((queue) => { + // SAFETY: this cast is *not* safe, but the underlying type is + // not currently able to be safer than this because it was + // built as a bag-of-queues *and* a structured item originally. + (deferredActionQueues[queue] as QueueItem[]).forEach( + (queueItem) => queueItem.stack && stacks.push(queueItem.stack) + ); + }); + return stacks; + }, [] as string[]); } if (this._summaryInfo.hasPendingTestWaiters) { @@ -165,12 +158,12 @@ export class TestDebugInfo implements DebugInfo { Object.keys(summary.pendingTestWaiterInfo.waiters).forEach( (waiterName) => { - let waiterDebugInfo: WaiterDebugInfo = + let waiterDebugInfo = summary.pendingTestWaiterInfo.waiters[waiterName]; if (Array.isArray(waiterDebugInfo)) { _console.group(waiterName); - waiterDebugInfo.forEach((debugInfo: TestWaiterDebugInfo) => { + waiterDebugInfo.forEach((debugInfo) => { _console.log( `${debugInfo.label ? debugInfo.label : 'stack'}: ${ debugInfo.stack @@ -221,7 +214,7 @@ export class TestDebugInfo implements DebugInfo { }); } - _formatCount(title: string, count: Number): string { + _formatCount(title: string, count: number): string { return `${title}: ${count}`; } } diff --git a/addon-test-support/@ember/test-helpers/-internal/deprecations.ts b/addon-test-support/@ember/test-helpers/-internal/deprecations.ts index 54d4b5274..f5618f082 100644 --- a/addon-test-support/@ember/test-helpers/-internal/deprecations.ts +++ b/addon-test-support/@ember/test-helpers/-internal/deprecations.ts @@ -13,7 +13,7 @@ export interface DeprecationOptions { export interface DeprecationFailure { message: string; - options: DeprecationOptions; + options?: DeprecationOptions; } const DEPRECATIONS = new WeakMap>(); @@ -92,7 +92,7 @@ if (typeof URLSearchParams !== 'undefined') { // those deprecations will be squelched if (disabledDeprecations) { registerDeprecationHandler((message, options, next) => { - if (!disabledDeprecations.includes(options.id)) { + if (!options || !disabledDeprecations.includes(options.id)) { next.apply(null, [message, options]); } }); @@ -102,7 +102,7 @@ if (typeof URLSearchParams !== 'undefined') { // `some-other-thing` deprecation is triggered, this `debugger` will be hit` if (debugDeprecations) { registerDeprecationHandler((message, options, next) => { - if (debugDeprecations.includes(options.id)) { + if (options && debugDeprecations.includes(options.id)) { debugger; // eslint-disable-line no-debugger } diff --git a/addon-test-support/@ember/test-helpers/-internal/warnings.ts b/addon-test-support/@ember/test-helpers/-internal/warnings.ts index 78b3cbbd6..48ac966dd 100644 --- a/addon-test-support/@ember/test-helpers/-internal/warnings.ts +++ b/addon-test-support/@ember/test-helpers/-internal/warnings.ts @@ -89,7 +89,7 @@ if (typeof URLSearchParams !== 'undefined') { // those warnings will be squelched if (disabledWarnings) { registerWarnHandler((message, options, next) => { - if (!disabledWarnings.includes(options.id)) { + if (!options || !disabledWarnings.includes(options.id)) { next.apply(null, [message, options]); } }); @@ -99,7 +99,7 @@ if (typeof URLSearchParams !== 'undefined') { // `some-other-thing` warning is triggered, this `debugger` will be hit` if (debugWarnings) { registerWarnHandler((message, options, next) => { - if (debugWarnings.includes(options.id)) { + if (options && debugWarnings.includes(options.id)) { debugger; // eslint-disable-line no-debugger } diff --git a/addon-test-support/@ember/test-helpers/build-owner.ts b/addon-test-support/@ember/test-helpers/build-owner.ts index 7a75712d2..3edb1ff8d 100644 --- a/addon-test-support/@ember/test-helpers/build-owner.ts +++ b/addon-test-support/@ember/test-helpers/build-owner.ts @@ -1,5 +1,5 @@ import Application from '@ember/application'; -import type Resolver from '@ember/application/resolver'; +import type Resolver from 'ember-resolver'; import { Promise } from './-utils'; @@ -13,6 +13,7 @@ export interface Owner ContainerProxyMixin, RegistryProxyMixin { _emberTestHelpersMockOwner?: boolean; + rootElement?: string | Element; _lookupFactory?(key: string): any; diff --git a/addon-test-support/@ember/test-helpers/dom/get-root-element.ts b/addon-test-support/@ember/test-helpers/dom/get-root-element.ts index 3b016adc6..b677a5992 100644 --- a/addon-test-support/@ember/test-helpers/dom/get-root-element.ts +++ b/addon-test-support/@ember/test-helpers/dom/get-root-element.ts @@ -1,4 +1,4 @@ -import { getContext } from '../setup-context'; +import { getContext, isTestContext } from '../setup-context'; import { isDocument, isElement } from './-target'; /** @@ -9,14 +9,15 @@ import { isDocument, isElement } from './-target'; */ export default function getRootElement(): Element | Document { let context = getContext(); - let owner = context && context.owner; - if (!owner) { + if (!context || !isTestContext(context) || !context.owner) { throw new Error( 'Must setup rendering context before attempting to interact with elements.' ); } + let owner = context.owner; + let rootElement; // When the host app uses `setApplication` (instead of `setResolver`) the owner has // a `rootElement` set on it with the element or id to be used diff --git a/addon-test-support/@ember/test-helpers/dom/tab.ts b/addon-test-support/@ember/test-helpers/dom/tab.ts index 97231f138..35fa7accc 100644 --- a/addon-test-support/@ember/test-helpers/dom/tab.ts +++ b/addon-test-support/@ember/test-helpers/dom/tab.ts @@ -87,8 +87,7 @@ function compileFocusAreas(root: Element = document.body) { ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP; }, - }, - false + } ); let node: Node | null; diff --git a/addon-test-support/@ember/test-helpers/dom/tap.ts b/addon-test-support/@ember/test-helpers/dom/tap.ts index e651e628f..176f2b84e 100644 --- a/addon-test-support/@ember/test-helpers/dom/tap.ts +++ b/addon-test-support/@ember/test-helpers/dom/tap.ts @@ -79,7 +79,7 @@ export default function tap( return fireEvent(element, 'touchstart', options) .then((touchstartEv) => fireEvent(element as Element, 'touchend', options).then( - (touchendEv) => [touchstartEv, touchendEv] + (touchendEv) => [touchstartEv, touchendEv] as const ) ) .then(([touchstartEv, touchendEv]) => diff --git a/addon-test-support/@ember/test-helpers/dom/trigger-key-event.ts b/addon-test-support/@ember/test-helpers/dom/trigger-key-event.ts index 56cc8eae5..259a7efcc 100644 --- a/addon-test-support/@ember/test-helpers/dom/trigger-key-event.ts +++ b/addon-test-support/@ember/test-helpers/dom/trigger-key-event.ts @@ -159,7 +159,7 @@ export function __triggerKeyEvent__( }; } else if (typeof key === 'string' && key.length !== 0) { let firstCharacter = key[0]; - if (firstCharacter !== firstCharacter.toUpperCase()) { + if (!firstCharacter || firstCharacter !== firstCharacter.toUpperCase()) { throw new Error( `Must provide a \`key\` to \`triggerKeyEvent\` that starts with an uppercase character but you passed \`${key}\`.` ); diff --git a/addon-test-support/@ember/test-helpers/dom/type-in.ts b/addon-test-support/@ember/test-helpers/dom/type-in.ts index 26873436e..2368845de 100644 --- a/addon-test-support/@ember/test-helpers/dom/type-in.ts +++ b/addon-test-support/@ember/test-helpers/dom/type-in.ts @@ -112,7 +112,7 @@ function fillOut( .map((character) => keyEntry(element, character)); return inputFunctions.reduce((currentPromise, func) => { return currentPromise.then(() => delayedExecute(delay)).then(func); - }, Promise.resolve(undefined)); + }, Promise.resolve()); } // eslint-disable-next-line require-jsdoc diff --git a/addon-test-support/@ember/test-helpers/has-ember-version.ts b/addon-test-support/@ember/test-helpers/has-ember-version.ts index 661dab798..bb5fb3a3f 100644 --- a/addon-test-support/@ember/test-helpers/has-ember-version.ts +++ b/addon-test-support/@ember/test-helpers/has-ember-version.ts @@ -10,7 +10,12 @@ import Ember from 'ember'; @returns {boolean} true if the Ember version is >= MAJOR.MINOR specified, false otherwise */ export default function hasEmberVersion(major: number, minor: number): boolean { - let numbers = Ember.VERSION.split('-')[0].split('.'); + let numbers = Ember.VERSION.split('-')[0]?.split('.'); + + if (!numbers || !numbers[0] || !numbers[1]) { + throw new Error('`Ember.VERSION` is not set.'); + } + let actualMajor = parseInt(numbers[0], 10); let actualMinor = parseInt(numbers[1], 10); return actualMajor > major || (actualMajor === major && actualMinor >= minor); diff --git a/addon-test-support/@ember/test-helpers/index.ts b/addon-test-support/@ember/test-helpers/index.ts index 7b3ba77e5..483c2dafc 100644 --- a/addon-test-support/@ember/test-helpers/index.ts +++ b/addon-test-support/@ember/test-helpers/index.ts @@ -1,3 +1,8 @@ +import { + Backburner, + DeferredActionQueues, +} from '@ember/runloop/-private/backburner'; + export { setResolver, getResolver } from './resolver'; export { getApplication, setApplication } from './application'; export { @@ -54,3 +59,13 @@ export { default as find } from './dom/find'; export { default as findAll } from './dom/find-all'; export { default as typeIn } from './dom/type-in'; export { default as scrollTo } from './dom/scroll-to'; + +// Declaration-merge for our internal purposes. +declare module '@ember/runloop' { + interface PrivateBackburner extends Backburner { + hasTimers(): boolean; + currentInstance: DeferredActionQueues | null; + } + + export const _backburner: PrivateBackburner; +} diff --git a/addon-test-support/@ember/test-helpers/resolver.ts b/addon-test-support/@ember/test-helpers/resolver.ts index 1716e91c9..4b57c9176 100644 --- a/addon-test-support/@ember/test-helpers/resolver.ts +++ b/addon-test-support/@ember/test-helpers/resolver.ts @@ -1,4 +1,4 @@ -import type Resolver from '@ember/application/resolver'; +import type Resolver from 'ember-resolver'; let __resolver__: Resolver | undefined; diff --git a/addon-test-support/@ember/test-helpers/setup-application-context.ts b/addon-test-support/@ember/test-helpers/setup-application-context.ts index 93a8bdb1a..acacf2a3d 100644 --- a/addon-test-support/@ember/test-helpers/setup-application-context.ts +++ b/addon-test-support/@ember/test-helpers/setup-application-context.ts @@ -11,6 +11,7 @@ import hasEmberVersion from './has-ember-version'; import settled from './settled'; import getTestMetadata, { ITestMetadata } from './test-metadata'; import { runHooks } from './-internal/helper-hooks'; +import { Router } from '@ember/routing'; export interface ApplicationTestContext extends TestContext { element?: Element | null; @@ -74,7 +75,7 @@ export function hasPendingTransitions(): boolean | null { */ export function setupRouterSettlednessTracking() { const context = getContext(); - if (context === undefined) { + if (context === undefined || !isTestContext(context)) { throw new Error( 'Cannot setupRouterSettlednessTracking outside of a test context' ); @@ -87,9 +88,9 @@ export function setupRouterSettlednessTracking() { HAS_SETUP_ROUTER.set(context, true); let { owner } = context; - let router; + let router: Router; if (CAN_USE_ROUTER_EVENTS) { - router = owner.lookup('service:router'); + router = owner.lookup('service:router') as Router; // track pending transitions via the public routeWillChange / routeDidChange APIs // routeWillChange can fire many times and is only useful to know when we have _started_ @@ -97,7 +98,7 @@ export function setupRouterSettlednessTracking() { router.on('routeWillChange', () => (routerTransitionsPending = true)); router.on('routeDidChange', () => (routerTransitionsPending = false)); } else { - router = owner.lookup('router:main'); + router = owner.lookup('router:main') as Router; ROUTER.set(context, router); } @@ -173,7 +174,7 @@ export function currentRouteName(): string { let router = context.owner.lookup('router:main'); - return get(router, 'currentRouteName'); + return get(router, 'currentRouteName') as string; } const HAS_CURRENT_URL_ON_ROUTER = hasEmberVersion(2, 13); @@ -193,9 +194,12 @@ export function currentURL(): string { let router = context.owner.lookup('router:main'); if (HAS_CURRENT_URL_ON_ROUTER) { - return get(router, 'currentURL'); + return get(router, 'currentURL') as string; } else { - return get(router, 'location').getURL(); + // SAFETY: this is *positively ancient* and should probably be removed at + // some point; old routers which don't have `currentURL` *should* have a + // `location` with `getURL()` per the docs for 2.12. + return (get(router, 'location') as any).getURL(); } } diff --git a/addon-test-support/@ember/test-helpers/setup-context.ts b/addon-test-support/@ember/test-helpers/setup-context.ts index b85e9663c..43bad1255 100644 --- a/addon-test-support/@ember/test-helpers/setup-context.ts +++ b/addon-test-support/@ember/test-helpers/setup-context.ts @@ -1,6 +1,6 @@ import { _backburner, run } from '@ember/runloop'; import { set, setProperties, get, getProperties } from '@ember/object'; -import type Resolver from '@ember/application/resolver'; +import type Resolver from 'ember-resolver'; import { setOwner } from '@ember/application'; import buildOwner, { Owner } from './build-owner'; @@ -61,15 +61,15 @@ registerWarnHandler((message, options, next) => { }); export interface BaseContext { - [key: string]: any; + [key: string]: unknown; } export interface TestContext extends BaseContext { owner: Owner; - set(key: string, value: any): any; - setProperties(hash: { [key: string]: any }): { [key: string]: any }; - get(key: string): any; + set(key: string, value: T): T; + setProperties>(hash: T): T; + get(key: string): unknown; getProperties(...args: string[]): Pick; pauseTest(): Promise; @@ -79,8 +79,8 @@ export interface TestContext extends BaseContext { // eslint-disable-next-line require-jsdoc export function isTestContext(context: BaseContext): context is TestContext { return ( - typeof context.pauseTest === 'function' && - typeof context.resumeTest === 'function' + typeof context['pauseTest'] === 'function' && + typeof context['resumeTest'] === 'function' ); } @@ -189,13 +189,16 @@ export function resumeTest(): void { function cleanup(context: BaseContext) { _teardownAJAXHooks(); + // SAFETY: this is intimate API *designed* for us to override. (Ember as any).testing = false; unsetContext(); - // this should not be required, but until https://github.com/emberjs/ember.js/pull/19106 - // lands in a 3.20 patch release - context.owner.destroy(); + if (isTestContext(context)) { + // this should not be required, but until https://github.com/emberjs/ember.js/pull/19106 + // lands in a 3.20 patch release + context.owner.destroy(); + } } /** @@ -368,10 +371,11 @@ export default function setupContext( context: BaseContext, options: { resolver?: Resolver } = {} ): Promise { + // SAFETY: this is intimate API *designed* for us to override. (Ember as any).testing = true; setContext(context); - let testMetadata: ITestMetadata = getTestMetadata(context); + let testMetadata = getTestMetadata(context); testMetadata.setupTypes.push('setupContext'); _backburner.DEBUG = true; @@ -417,7 +421,7 @@ export default function setupContext( Object.defineProperty(context, 'set', { configurable: true, enumerable: true, - value(key: string, value: any): any { + value(key: string, value: unknown): unknown { let ret = run(function () { if (ComponentRenderMap.has(context)) { assert( @@ -487,17 +491,17 @@ export default function setupContext( writable: false, }); - let resume: Function | undefined; - context.resumeTest = function resumeTest() { + let resume: ((value?: unknown) => void) | undefined; + context['resumeTest'] = function resumeTest() { assert( 'Testing has not been paused. There is nothing to resume.', - Boolean(resume) + !!resume ); - (resume as Function)(); + resume(); global.resumeTest = resume = undefined; }; - context.pauseTest = function pauseTest() { + context['pauseTest'] = function pauseTest() { console.info('Testing paused. Use `resumeTest()` to continue.'); // eslint-disable-line no-console return new Promise((resolve) => { diff --git a/addon-test-support/@ember/test-helpers/setup-rendering-context.ts b/addon-test-support/@ember/test-helpers/setup-rendering-context.ts index 7caba3837..ae20a0596 100644 --- a/addon-test-support/@ember/test-helpers/setup-rendering-context.ts +++ b/addon-test-support/@ember/test-helpers/setup-rendering-context.ts @@ -42,8 +42,8 @@ export function isRenderingTestContext( ): context is RenderingTestContext { return ( isTestContext(context) && - typeof context.render === 'function' && - typeof context.clearRender === 'function' + typeof context['render'] === 'function' && + typeof context['clearRender'] === 'function' ); } @@ -117,7 +117,10 @@ export function render( let testMetadata = getTestMetadata(context); testMetadata.usedHelpers.push('render'); - let toplevelView = owner.lookup('-top-level-view:main'); + // SAFETY: this is all wildly unsafe, because it is all using private API. + // At some point we should define a path forward for this kind of internal + // API. For now, just flagging it as *NOT* being safe! + let toplevelView = owner.lookup('-top-level-view:main') as any; let OutletTemplate = lookupOutletTemplate(owner); let ownerToRenderFrom = options?.owner || owner; @@ -213,7 +216,11 @@ export function render( // setting outletState should ensureInstance, since we know we need to // render), but on Ember < 3.23 that is not guaranteed. if (!hasEmberVersion(3, 23)) { - run.backburner.ensureInstance(); + // SAFETY: this was correct and type checked on the Ember v3 types, but + // since the `run` namespace does not exist in Ember v4, this no longer + // can be type checked. When (eventually) dropping support for Ember v3, + // and therefore for versions before 3.23, this can be removed entirely. + (run as any).backburner.ensureInstance(); } // returning settled here because the actual rendering does not happen until diff --git a/addon-test-support/@ember/test-helpers/wait-until.ts b/addon-test-support/@ember/test-helpers/wait-until.ts index 269039b34..dbe235bb0 100644 --- a/addon-test-support/@ember/test-helpers/wait-until.ts +++ b/addon-test-support/@ember/test-helpers/wait-until.ts @@ -49,10 +49,8 @@ export default function waitUntil( // eslint-disable-next-line require-jsdoc function scheduleCheck(timeoutsIndex: number) { - let interval = TIMEOUTS[timeoutsIndex]; - if (interval === undefined) { - interval = MAX_TIMEOUT; - } + let knownTimeout = TIMEOUTS[timeoutsIndex]; + let interval = knownTimeout === undefined ? MAX_TIMEOUT : knownTimeout; futureTick(function () { time += interval; diff --git a/package.json b/package.json index 5d10b6647..f18f6aa24 100644 --- a/package.json +++ b/package.json @@ -57,7 +57,18 @@ "@glimmer/interfaces": "^0.84.1", "@glimmer/reference": "^0.84.1", "@tsconfig/ember": "^1.0.1", - "@types/ember": "^3.16.5", + "@types/ember": "~4.0.0", + "@types/ember__application": "~4.0.0", + "@types/ember__component": "~4.0.8", + "@types/ember__debug": "~4.0.1", + "@types/ember__destroyable": "~4.0.0", + "@types/ember__engine": "~4.0.0", + "@types/ember__object": "~4.0.2", + "@types/ember__routing": "~4.0.7", + "@types/ember__polyfills": "~4.0.0", + "@types/ember__runloop": "~4.0.1", + "@types/ember__service": "~4.0.0", + "@types/ember__test": "~4.0.0", "@types/ember-testing-helpers": "^0.0.4", "@types/rsvp": "^4.0.4", "@typescript-eslint/eslint-plugin": "^4.29.0", diff --git a/tsconfig.json b/tsconfig.json index 8cd45103f..4a68608c9 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,8 +6,7 @@ "dummy/*": ["./tests/dummy/app/*", "./app/*"], "@ember/test-helpers": ["./addon-test-support/@ember/test-helpers"], "@ember/test-helpers/*": ["./addon-test-support/@ember/test-helpers/*"], - "@ember/destroyable": ["./node_modules/ember-destroyable-polyfill"], - "*": ["./types/*", "./node_modules/@types/*"] + "*": ["./types/*"] } }, "exclude": ["node_modules"], diff --git a/types/@ember/runloop.d.ts b/types/@ember/runloop.d.ts deleted file mode 100644 index d3b6cb069..000000000 --- a/types/@ember/runloop.d.ts +++ /dev/null @@ -1,374 +0,0 @@ -/** - * Temporarily copying these types into @ember/test-helpers until - * https://github.com/DefinitelyTyped/DefinitelyTyped/pull/32970 lands. - */ -import { RunMethod, EmberRunQueues } from '@ember/runloop/-private/types'; -import { EmberRunTimer } from '@ember/runloop/types'; - -export interface QueueItem { - method: string; - target: object; - args: object[]; - stack: string | undefined; -} - -export interface DeferredActionQueues { - [index: string]: any; - queues: object; - schedule( - queueName: string, - target: any, - method: any, - args: any, - onceFlag: boolean, - stack: any - ): any; - flush(fromAutorun: boolean): any; -} - -export interface DebugInfo { - autorun: Error | undefined | null; - counters: object; - timers: QueueItem[]; - instanceStack: DeferredActionQueues[]; -} - -export interface Backburner { - currentInstance: DeferredActionQueues | null; - hasTimers(): boolean; - join(...args: any[]): void; - on(...args: any[]): void; - scheduleOnce(...args: any[]): void; - schedule( - queueName: string, - target: object | null, - method: () => void | string - ): void; - ensureInstance(): void; - DEBUG: boolean; - getDebugInfo(): DebugInfo; -} - -export interface RunNamespace { - /** - * Runs the passed target and method inside of a RunLoop, ensuring any - * deferred actions including bindings and views updates are flushed at the - * end. - */ - (method: (...args: any[]) => Ret): Ret; - (target: Target, method: RunMethod): Ret; - /** - * If no run-loop is present, it creates a new one. If a run loop is - * present it will queue itself to run on the existing run-loops action - * queue. - */ - join(method: (...args: any[]) => Ret, ...args: any[]): Ret | undefined; - join( - target: Target, - method: RunMethod, - ...args: any[] - ): Ret | undefined; - /** - * Allows you to specify which context to call the specified function in while - * adding the execution of that function to the Ember run loop. This ability - * makes this method a great way to asynchronously integrate third-party libraries - * into your Ember application. - */ - bind( - target: Target, - method: RunMethod, - ...args: any[] - ): (...args: any[]) => Ret; - /** - * Begins a new RunLoop. Any deferred actions invoked after the begin will - * be buffered until you invoke a matching call to `run.end()`. This is - * a lower-level way to use a RunLoop instead of using `run()`. - */ - begin(): void; - /** - * Ends a RunLoop. This must be called sometime after you call - * `run.begin()` to flush any deferred actions. This is a lower-level way - * to use a RunLoop instead of using `run()`. - */ - end(): void; - /** - * Adds the passed target/method and any optional arguments to the named - * queue to be executed at the end of the RunLoop. If you have not already - * started a RunLoop when calling this method one will be started for you - * automatically. - */ - schedule( - queue: EmberRunQueues, - target: Target, - method: RunMethod, - ...args: any[] - ): EmberRunTimer; - schedule( - queue: EmberRunQueues, - method: (args: any[]) => any, - ...args: any[] - ): EmberRunTimer; - /** - * Invokes the passed target/method and optional arguments after a specified - * period of time. The last parameter of this method must always be a number - * of milliseconds. - */ - later(method: (...args: any[]) => any, wait: number): EmberRunTimer; - later( - target: Target, - method: RunMethod, - wait: number - ): EmberRunTimer; - later( - target: Target, - method: RunMethod, - arg0: any, - wait: number - ): EmberRunTimer; - later( - target: Target, - method: RunMethod, - arg0: any, - arg1: any, - wait: number - ): EmberRunTimer; - later( - target: Target, - method: RunMethod, - arg0: any, - arg1: any, - arg2: any, - wait: number - ): EmberRunTimer; - later( - target: Target, - method: RunMethod, - arg0: any, - arg1: any, - arg2: any, - arg3: any, - wait: number - ): EmberRunTimer; - later( - target: Target, - method: RunMethod, - arg0: any, - arg1: any, - arg2: any, - arg3: any, - arg4: any, - wait: number - ): EmberRunTimer; - later( - target: Target, - method: RunMethod, - arg0: any, - arg1: any, - arg2: any, - arg3: any, - arg4: any, - arg5: any, - wait: number - ): EmberRunTimer; - /** - * Schedule a function to run one time during the current RunLoop. This is equivalent - * to calling `scheduleOnce` with the "actions" queue. - */ - once( - target: Target, - method: RunMethod, - ...args: any[] - ): EmberRunTimer; - /** - * Schedules a function to run one time in a given queue of the current RunLoop. - * Calling this method with the same queue/target/method combination will have - * no effect (past the initial call). - */ - scheduleOnce( - queue: EmberRunQueues, - target: Target, - method: RunMethod, - ...args: any[] - ): EmberRunTimer; - /** - * Schedules an item to run from within a separate run loop, after - * control has been returned to the system. This is equivalent to calling - * `run.later` with a wait time of 1ms. - */ - next( - target: Target, - method: RunMethod, - ...args: any[] - ): EmberRunTimer; - /** - * Cancels a scheduled item. Must be a value returned by `run.later()`, - * `run.once()`, `run.scheduleOnce()`, `run.next()`, `run.debounce()`, or - * `run.throttle()`. - */ - cancel(timer: EmberRunTimer): boolean; - /** - * Delay calling the target method until the debounce period has elapsed - * with no additional debounce calls. If `debounce` is called again before - * the specified time has elapsed, the timer is reset and the entire period - * must pass again before the target method is called. - */ - debounce( - method: (...args: any[]) => any, - wait: number, - immediate?: boolean - ): EmberRunTimer; - debounce( - target: Target, - method: RunMethod, - wait: number, - immediate?: boolean - ): EmberRunTimer; - debounce( - target: Target, - method: RunMethod, - arg0: any, - wait: number, - immediate?: boolean - ): EmberRunTimer; - debounce( - target: Target, - method: RunMethod, - arg0: any, - arg1: any, - wait: number, - immediate?: boolean - ): EmberRunTimer; - debounce( - target: Target, - method: RunMethod, - arg0: any, - arg1: any, - arg2: any, - wait: number, - immediate?: boolean - ): EmberRunTimer; - debounce( - target: Target, - method: RunMethod, - arg0: any, - arg1: any, - arg2: any, - arg3: any, - wait: number, - immediate?: boolean - ): EmberRunTimer; - debounce( - target: Target, - method: RunMethod, - arg0: any, - arg1: any, - arg2: any, - arg3: any, - arg4: any, - wait: number, - immediate?: boolean - ): EmberRunTimer; - debounce( - target: Target, - method: RunMethod, - arg0: any, - arg1: any, - arg2: any, - arg3: any, - arg4: any, - arg5: any, - wait: number, - immediate?: boolean - ): EmberRunTimer; - /** - * Ensure that the target method is never called more frequently than - * the specified spacing period. The target method is called immediately. - */ - throttle( - method: (...args: any[]) => any, - spacing: number, - immediate?: boolean - ): EmberRunTimer; - throttle( - target: Target, - method: RunMethod, - spacing: number, - immediate?: boolean - ): EmberRunTimer; - throttle( - target: Target, - method: RunMethod, - arg0: any, - spacing: number, - immediate?: boolean - ): EmberRunTimer; - throttle( - target: Target, - method: RunMethod, - arg0: any, - arg1: any, - spacing: number, - immediate?: boolean - ): EmberRunTimer; - throttle( - target: Target, - method: RunMethod, - arg0: any, - arg1: any, - arg2: any, - spacing: number, - immediate?: boolean - ): EmberRunTimer; - throttle( - target: Target, - method: RunMethod, - arg0: any, - arg1: any, - arg2: any, - arg3: any, - spacing: number, - immediate?: boolean - ): EmberRunTimer; - throttle( - target: Target, - method: RunMethod, - arg0: any, - arg1: any, - arg2: any, - arg3: any, - arg4: any, - spacing: number, - immediate?: boolean - ): EmberRunTimer; - throttle( - target: Target, - method: RunMethod, - arg0: any, - arg1: any, - arg2: any, - arg3: any, - arg4: any, - arg5: any, - spacing: number, - immediate?: boolean - ): EmberRunTimer; - - queues: EmberRunQueues[]; - - backburner: Backburner; -} - -export const _backburner: Backburner; -export const run: RunNamespace; -export const begin: typeof run.begin; -export const bind: typeof run.bind; -export const cancel: typeof run.cancel; -export const debounce: typeof run.debounce; -export const end: typeof run.end; -export const join: typeof run.join; -export const later: typeof run.later; -export const next: typeof run.next; -export const once: typeof run.once; -export const schedule: typeof run.schedule; -export const scheduleOnce: typeof run.scheduleOnce; -export const throttle: typeof run.throttle; diff --git a/yarn.lock b/yarn.lock index 23b306f74..5017b883a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2232,6 +2232,13 @@ resolved "https://registry.yarnpkg.com/@types/cors/-/cors-2.8.12.tgz#6b2c510a7ad7039e98e7b8d3d6598f4359e5c080" integrity sha512-vt+kDhq/M2ayberEtJcIN/hxXy1Pk+59g2FV/ZQceeaTyCtCucjL2Q7FXlFjtWn4n15KCr1NE2lNNFhp0lEThw== +"@types/ember-resolver@*": + version "5.0.11" + resolved "https://registry.yarnpkg.com/@types/ember-resolver/-/ember-resolver-5.0.11.tgz#db931fb5c2d6bda4e29adea132fb48c7ed17aa62" + integrity sha512-2BL9d8kBdNUO9Je6KBF7Q34BSwbQG6vzCzTeSopt8FAmLDfaDU9xDDdyZobpfy9GR36mCSeG9b9wr4bgYh/MYw== + dependencies: + "@types/ember__object" "*" + "@types/ember-testing-helpers@^0.0.4": version "0.0.4" resolved "https://registry.yarnpkg.com/@types/ember-testing-helpers/-/ember-testing-helpers-0.0.4.tgz#d305b418d477c6f84fcd4dcb851a3efadbc4a2bd" @@ -2240,17 +2247,16 @@ "@types/jquery" "*" "@types/rsvp" "*" -"@types/ember@^3.16.5": - version "3.16.5" - resolved "https://registry.yarnpkg.com/@types/ember/-/ember-3.16.5.tgz#c2d6b0f178761c0c2fbc6fc39b4b6958c256d0ac" - integrity sha512-8BzT1g8r7xQsN2p7qIUZ0AXWEVpJ5LmaRWP3iT79PLyIQfTAYvHSueUl14lrB8renETjwr4+ZvVPKurn9TKxNA== +"@types/ember@*", "@types/ember@~4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@types/ember/-/ember-4.0.0.tgz#0c29294fa0e5aa07ba6090f60243707dde8fc411" + integrity sha512-IR4o8OkFgoiRKVLRI8URvyNhEBSkjO5DXp2900/TptxOl0Retu8/tKtFaRTwkqteg2a0/6zXAA1rpFb3BbxNpA== dependencies: "@types/ember__application" "*" "@types/ember__array" "*" "@types/ember__component" "*" "@types/ember__controller" "*" "@types/ember__debug" "*" - "@types/ember__destroyable" "*" "@types/ember__engine" "*" "@types/ember__error" "*" "@types/ember__object" "*" @@ -2263,131 +2269,136 @@ "@types/ember__test" "*" "@types/ember__utils" "*" "@types/htmlbars-inline-precompile" "*" - "@types/jquery" "*" "@types/rsvp" "*" -"@types/ember__application@*": - version "3.16.3" - resolved "https://registry.yarnpkg.com/@types/ember__application/-/ember__application-3.16.3.tgz#f16e852b3200d5601b6f073be5a030cfadebb778" - integrity sha512-kx7euIQ+zy7EjyBMoWTOMPxkbGmLitwKp7Cxga2xeKnpMPrZCIaLcFM50XnbnbjzmlSMmJEn5EDIEYwlqnfzvg== +"@types/ember__application@*", "@types/ember__application@~4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@types/ember__application/-/ember__application-4.0.0.tgz#a4d2fead37845550dad83bb1fd8afd52052563a7" + integrity sha512-1Atwevfyu1/vjiezPPdP4s96BxWGelEQlCJRU5ZQV9WlzVuMTuCDPumZ1lQdS4/EYycFZeod030FjE3CT9mZFA== dependencies: + "@types/ember" "*" + "@types/ember-resolver" "*" "@types/ember__application" "*" "@types/ember__engine" "*" "@types/ember__object" "*" "@types/ember__routing" "*" "@types/ember__array@*": - version "3.16.4" - resolved "https://registry.yarnpkg.com/@types/ember__array/-/ember__array-3.16.4.tgz#d61b5b876e4976de03aa027ea89cb48cd640d49d" - integrity sha512-K21LKDNDW3ug0fLsFUTHZPyaFnzUUZEOtsmzmmeXKI6apJcoaz/yF3V0fvM2FUWNLs6rXB3PXegtzik6lq44Yw== + version "4.0.1" + resolved "https://registry.yarnpkg.com/@types/ember__array/-/ember__array-4.0.1.tgz#b62126ed080b29351a5633bd28e595a5cfee27ce" + integrity sha512-fwrYcYmFbsEnu77Xn9z3WSAp6tqpwn8Wksx8RzGg5pib6VmFD/dkT5jefwoKtlcImsxUNEoP1VgWKrdrpGaQcg== dependencies: + "@types/ember" "*" "@types/ember__array" "*" "@types/ember__object" "*" -"@types/ember__component@*": - version "3.16.6" - resolved "https://registry.yarnpkg.com/@types/ember__component/-/ember__component-3.16.6.tgz#affc4798ee97f58747f5d173b8f739ddab26e551" - integrity sha512-Zi82wppu0wtijXKAsyn75KBauJKIIHoYk5cbk7OmOKE2zmV1qzCfsxjM8x9iTJpu97T1P0vXWZ91aXM7KeQFMw== +"@types/ember__component@*", "@types/ember__component@~4.0.8": + version "4.0.8" + resolved "https://registry.yarnpkg.com/@types/ember__component/-/ember__component-4.0.8.tgz#09a5f954f734fcbe6c988a173f4de4fa09084470" + integrity sha512-YVGn/kpWtpZAu6I2XtS9fsZV+78/sON5NyKzK5EOUyMiCwwpbUr5XL8dTSdkHehYrsfzJikcYvqpmwbNZSJxGQ== dependencies: + "@types/ember" "*" "@types/ember__component" "*" "@types/ember__object" "*" - "@types/jquery" "*" "@types/ember__controller@*": - version "3.16.5" - resolved "https://registry.yarnpkg.com/@types/ember__controller/-/ember__controller-3.16.5.tgz#29a7b9fb11bc940097964d84d11945b89f713226" - integrity sha512-T+MJI6HMJ/HOf91Eq/nhESNPLHeUwIacidTiLZiLysMWzbrcT9Opih8+vf7V6CNTxaWfMiSWn5iSB2WblhzLyw== + version "4.0.0" + resolved "https://registry.yarnpkg.com/@types/ember__controller/-/ember__controller-4.0.0.tgz#f8891840ebb84cb54eba82385e3b2dbe805d692a" + integrity sha512-rxJt8McWaaIZFsu2z+IB7TvgSjglAPb07Pj0F7OGvZQ3j9NV7kreqqics/cHQIEBG3GgVAewBE+xI5D6PNq/vg== dependencies: "@types/ember__object" "*" -"@types/ember__debug@*": - version "3.16.4" - resolved "https://registry.yarnpkg.com/@types/ember__debug/-/ember__debug-3.16.4.tgz#b5fca2a568505f7e0a39b5ab5f489f8d32990caf" - integrity sha512-Tlz6TB2nRbRczGa+v2+nGPvF8Cw0WRp29t3uT6Ryog6W2Alocqclsqc57EsGgLi1g4jEPP53NUI3dIJGa3AUig== +"@types/ember__debug@*", "@types/ember__debug@~4.0.1": + version "4.0.1" + resolved "https://registry.yarnpkg.com/@types/ember__debug/-/ember__debug-4.0.1.tgz#1e4a8a1045484295dddc7bd4356d0b3014b0d509" + integrity sha512-qrKk6Ujh6oev7TSB0eB7AEmQWKCt5t84k/K3hDvJXUiLU3YueN0kyt7aPoIAkVjC111A9FqDugl9n60+N5yeEw== dependencies: + "@types/ember-resolver" "*" "@types/ember__debug" "*" - "@types/ember__engine" "*" "@types/ember__object" "*" -"@types/ember__destroyable@*": - version "3.22.0" - resolved "https://registry.yarnpkg.com/@types/ember__destroyable/-/ember__destroyable-3.22.0.tgz#2af2c27f5d8996694c3f0fe906e2536b2e4c5aca" - integrity sha512-T5wZGK1MwEelNIv1bbAvRQZPo9zvfjpGyyFPwjz+sakjImKVcQzb/yq1SgGyT0QTAQAT7l0L+kFru9+fSVVo5A== +"@types/ember__destroyable@~4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@types/ember__destroyable/-/ember__destroyable-4.0.0.tgz#997af44863323979796fbd153e9ad76498defb33" + integrity sha512-QxyRhCOlQmc056tbWvHOQZ7vIjlFOFYMOU82P3pcCFfxyi55+NRhj4ySruymcBCfrzKQmVQLCbbOGhyFMLqq0Q== -"@types/ember__engine@*": - version "3.16.3" - resolved "https://registry.yarnpkg.com/@types/ember__engine/-/ember__engine-3.16.3.tgz#f61114922ed7d1a65f468bfd3e9dd5b128f32822" - integrity sha512-D9cLOlkQjT+b+9vszgAfxnTelx1H/GiL9FNmPcYQbLd+Ta8+FdKssb2Vt4DbHZrc5MsBJ8LMRs5/xPhkHuCDMA== +"@types/ember__engine@*", "@types/ember__engine@~4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@types/ember__engine/-/ember__engine-4.0.0.tgz#e39c06d98c7a085912508e8257c48a70196c1a87" + integrity sha512-AfJHIWaBeZ+TZWJbSoUz7LK+z8uNPjMqmucz8C5u+EV2NDiaq02oGPTB4SeKInLNBMga8c5xvz0gVefZJnTBnQ== dependencies: + "@types/ember-resolver" "*" "@types/ember__engine" "*" "@types/ember__object" "*" "@types/ember__error@*": - version "3.16.1" - resolved "https://registry.yarnpkg.com/@types/ember__error/-/ember__error-3.16.1.tgz#752d977f4ee35d4fa66bcfeebae6e85240fc62a6" - integrity sha512-bnB58krc18B8qgSMsRBbrVbNb4msyb8pMzS9Yo3brw/bRjuPb1ONUrjieAVHeespXlXNJOusvvX/pji641iCPQ== + version "4.0.0" + resolved "https://registry.yarnpkg.com/@types/ember__error/-/ember__error-4.0.0.tgz#c73037e65c1c3d7060b97f98135ba73c712972b1" + integrity sha512-1WVMR65/QTqPzMWafK2vKEwGafILxRxItbWJng6eEJyKDHRvvHFCl3XzJ4dQjdFcfOlozsn0mmEYCpjKoyzMqA== -"@types/ember__object@*": - version "3.12.6" - resolved "https://registry.yarnpkg.com/@types/ember__object/-/ember__object-3.12.6.tgz#5f77662881e3c6f877d63e08b46861c52a36714a" - integrity sha512-LAGldyJmFpErWLCm1HOAGd3G4E7Sem+AzQycKH+zSiYSwKVxNSpzUIU1yewlScHn5WvKwLEVE2H6mPvwvkQ+yA== +"@types/ember__object@*", "@types/ember__object@~4.0.2": + version "4.0.2" + resolved "https://registry.yarnpkg.com/@types/ember__object/-/ember__object-4.0.2.tgz#070f1a36961f7df777e1fceed2551a648db0fd23" + integrity sha512-m3xjqjs7bGVT0+QXlgIoDMsp/oqePobnf4IiVoFdXLBpGCICiOAEi7HuUtCLi57WTvx0lYsS9hE1vgGyZn9qnw== dependencies: + "@types/ember" "*" "@types/ember__object" "*" "@types/rsvp" "*" -"@types/ember__polyfills@*": - version "3.12.1" - resolved "https://registry.yarnpkg.com/@types/ember__polyfills/-/ember__polyfills-3.12.1.tgz#aed838e35a3e8670d247333d4c7ea2c2f7b3c43e" - integrity sha512-Xw9RxFizB8guT6YGg3VNi5tjbzAjqk+bLtAJ1oVl2I1FylKrRFh0bwobxT2K0BF/i0QFEYlqckHpN/OoCpkvkA== +"@types/ember__polyfills@*", "@types/ember__polyfills@~4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@types/ember__polyfills/-/ember__polyfills-4.0.0.tgz#d83ae94ff2890ad47798315426d9916f39ff4ae6" + integrity sha512-Yk85J18y1Ys6agoIBLdJWu6ZkWe68oaC9JPyW7BhOINVNKm89PXrR/yxdOJ1/vN1Hj7ZZQKq+4X6fz3sxebavA== -"@types/ember__routing@*": - version "3.16.15" - resolved "https://registry.yarnpkg.com/@types/ember__routing/-/ember__routing-3.16.15.tgz#14e7e98ae331d05b19aacc29c9759c9f3dc222ec" - integrity sha512-M+QujBvUQZJgcLo/vj1aYVdEZaQWxuD+GM2CLp2jmkb4RYGhdYPuNYK7KkDMhJH5vMICOeK7KVVKrmN1KhAQHg== +"@types/ember__routing@*", "@types/ember__routing@~4.0.7": + version "4.0.7" + resolved "https://registry.yarnpkg.com/@types/ember__routing/-/ember__routing-4.0.7.tgz#16cc442fcee2dda06dc79bb3d7fd5002f154ad07" + integrity sha512-iWsk8C0WIqqK6dgK1UdJisyQLs6rVq/+A2f7fXjdLJHA+SAHK8aqfCHHFkTuiy4sXkM/PBINmP9qHpk4KrScbw== dependencies: - "@types/ember__component" "*" + "@types/ember" "*" "@types/ember__controller" "*" "@types/ember__object" "*" "@types/ember__routing" "*" "@types/ember__service" "*" -"@types/ember__runloop@*": - version "3.16.3" - resolved "https://registry.yarnpkg.com/@types/ember__runloop/-/ember__runloop-3.16.3.tgz#c37ed507aed0f642ef19cbc4b5d0b3a167e3ada6" - integrity sha512-iYT7+9z6lVOi4RSyM9tBwIOidRI0Y5nyaRtIMP1DhP8n2UZjvVG6ao4PkpFnpFWR4R8Ajj2p13SaPGxpEV62jg== +"@types/ember__runloop@*", "@types/ember__runloop@~4.0.1": + version "4.0.1" + resolved "https://registry.yarnpkg.com/@types/ember__runloop/-/ember__runloop-4.0.1.tgz#7f6e45af7dbf1158655ef3ad852852b0bf87065f" + integrity sha512-3HrsavVrdgxUkYptQUv/e9RwJG02cV9WbnJxKSvwl9ZYpeX4JbuDVucjTWk5BAvJUVtbiQLPGzLEHZ6daoCbbg== dependencies: + "@types/ember" "*" "@types/ember__runloop" "*" -"@types/ember__service@*": - version "3.16.1" - resolved "https://registry.yarnpkg.com/@types/ember__service/-/ember__service-3.16.1.tgz#e8f941ec50ff4a7531487dc60830b4e6c7da6a47" - integrity sha512-XYl75IZGE+ZqRiCr9tBLXNYBVM9WX18AQHw/73QSQP/7sfyv5QFP/C/KvJrwP9wJmqh0BS2lVAXP3Nx4/BRNTw== +"@types/ember__service@*", "@types/ember__service@~4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@types/ember__service/-/ember__service-4.0.0.tgz#ae6164e3b5d927fe17513b49867b52dc0222490d" + integrity sha512-FbN2y6tRb6NIV+kmzQcxRAoB17vH7qHCfzcKlxsmt2EI7fboLTcdeKpZKPBEromRXg83fx67QX1b95WcwSGtaw== dependencies: "@types/ember__object" "*" "@types/ember__string@*": - version "3.16.3" - resolved "https://registry.yarnpkg.com/@types/ember__string/-/ember__string-3.16.3.tgz#6c474d422dfae5c382a3c52bd3c994048d04b72e" - integrity sha512-0T9ofzm9LL/bSG5u1SxKx/j2h/bHKkl5NKjGCNbFQxEKBw4f2cs6+AMDgWke9z+qrRRIz9vGEtMXnA3yJrO2xA== - dependencies: - "@types/ember__template" "*" + version "3.0.9" + resolved "https://registry.yarnpkg.com/@types/ember__string/-/ember__string-3.0.9.tgz#669188ccea5a61777a36bf88a05ba6875dc9b7d7" + integrity sha512-v9QwhhfTTgJH6PCviWlz3JgcraYdSWQoTg2XN5Z7bPgXMJYXczxB/N22L9FnuFgDYdN87yXdTJv6E9rw2YGEhw== "@types/ember__template@*": - version "3.16.1" - resolved "https://registry.yarnpkg.com/@types/ember__template/-/ember__template-3.16.1.tgz#30d7f50a49b190934db0f5a56dd76ad86c21efc6" - integrity sha512-APQINizzizl2LHWGMFBCanRjKZQsdzqn7b+us17zbNhnx/R0IZAJq901x/i7eozCRwxsDKmGzNABSCIu6uc1Tg== + version "4.0.0" + resolved "https://registry.yarnpkg.com/@types/ember__template/-/ember__template-4.0.0.tgz#3423b6ddc3a6cf0b13a1e0fd5f1a84eec664a095" + integrity sha512-51bAEQecMKpDYRXMmVVfU7excrtxDJixRU7huUsAm4acBCqL2+TmMgTqZEkOQSNy6qnKUc2ktSzX28a9//C6pA== -"@types/ember__test@*": - version "3.16.1" - resolved "https://registry.yarnpkg.com/@types/ember__test/-/ember__test-3.16.1.tgz#8407e42b9835a13ef0c6ef7a7ce3aa3d7ebcb7ed" - integrity sha512-0ICnkM4BDwOKhqmLQRpfvNuZlb6QOqE+FhP5fPaWXWy7bgcL9CY7kMRc7N+wZQbTvbSKqgEdfbvjd0bJsIrz5w== +"@types/ember__test@*", "@types/ember__test@~4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@types/ember__test/-/ember__test-4.0.0.tgz#1a7dcbe24fedfc34fa60547b03f130a14397c4b6" + integrity sha512-vI/qhZkexJLN25lp1UAfjJv4R6pPtrQlAmPDXkKd8PNjwRk3KANFVRzdghN7HWhXgQ+s91PbvxEnZ3eZiRPdcQ== dependencies: "@types/ember__application" "*" "@types/ember__utils@*": - version "3.16.2" - resolved "https://registry.yarnpkg.com/@types/ember__utils/-/ember__utils-3.16.2.tgz#3fa9a0666a3e8204262e2a2960289aaf01f29467" - integrity sha512-tBbqewgegiKSpGZvGh3pbcoXwLCMvKVdLRE97vys75nAEz/vBzkGJm+PDz1HVaTkRukWbRhlDiTm2qFH8qRnSw== + version "4.0.0" + resolved "https://registry.yarnpkg.com/@types/ember__utils/-/ember__utils-4.0.0.tgz#a7e9e11334b5e203324e6155ff74c2b33ec21567" + integrity sha512-gwSFUm+6t6StkQxSllbn9lqRms/dXMCQDieRfaTGN8IRatnKjJoEPME3A0T6O9afsU6viBQyqlPyFxsOWknYkg== + dependencies: + "@types/ember" "*" "@types/eslint-scope@^3.7.0": version "3.7.1" From 16babd823f0d6ca007edbb88fc7787784ec3339f Mon Sep 17 00:00:00 2001 From: Chris Krycho Date: Wed, 20 Jul 2022 20:00:20 -0600 Subject: [PATCH 04/24] Publish types in `public-types` with `typesVersions` Publish types to a `public-types` directory, in parallel to the in-place Babel-powered emit for JS. In the future, we should probably go ahead and remove the Babel emit and get this the rest of the way into a v2 addon format with a single publish step. However, this minimizes the delta introduced in this particular PR, and thus keeps the risk lower. This does *not* correctly publish types for `ember-test-helpers`. However, there remains only one export from that package the `has-ember-version` module and helper, which is also available from `@ember/test-helpers`. Accordingly, we can simply ignore that (and plan to go ahead and eventually remove it). --- .gitignore | 1 + package.json | 12 +++++++++++- tsconfig.json | 5 +++++ 3 files changed, 17 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 6978e3523..f6a13b6ea 100644 --- a/.gitignore +++ b/.gitignore @@ -28,3 +28,4 @@ # tyescript related output files addon-test-support/**/*.js addon-test-support/**/*.d.ts +public-types diff --git a/package.json b/package.json index f18f6aa24..e132d1586 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,10 @@ "test": "tests" }, "scripts": { - "prepack": "yarn babel --extensions '.ts' --presets @babel/preset-typescript addon-test-support --out-dir addon-test-support/ --ignore '**/*.d.ts'", + "build:types": "tsc", + "build:js": "yarn babel --extensions '.ts' --presets @babel/preset-typescript addon-test-support --out-dir addon-test-support/ --ignore '**/*.d.ts'", + "build": "npm-run-all --parallel build:*", + "prepack": "yarn build", "postpack": "rimraf addon-test-support/**/*.js", "clean": "git clean -x -f", "docs": "documentation build --document-exported \"addon-test-support/@ember/test-helpers/index.js\" --config documentation.yml --markdown-toc-max-depth 3 -f md -o API.md", @@ -152,5 +155,12 @@ "volta": { "node": "12.22.4", "yarn": "1.22.4" + }, + "typesVersions": { + "*": { + "*": [ + "public-types/*" + ] + } } } diff --git a/tsconfig.json b/tsconfig.json index 4a68608c9..241317076 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,11 @@ { "extends": "@tsconfig/ember/tsconfig.json", "compilerOptions": { + // Use TS to emit declarations + "noEmit": false, + "emitDeclarationOnly": true, + "outDir": "public-types", + "paths": { "dummy/tests/*": ["./tests/*"], "dummy/*": ["./tests/dummy/app/*", "./app/*"], From 979f5354e42232f36128507eb8f5d50bd53cedd5 Mon Sep 17 00:00:00 2001 From: Chris Krycho Date: Thu, 21 Jul 2022 07:12:33 -0600 Subject: [PATCH 05/24] Split apart tsconfig for check vs. for type emit This lets the `lint:ts` script *only* do type-checking, while letting the `build` script share most of the config but updating the emit settings to generate type declarations. --- package.json | 2 +- tsconfig.json | 5 ----- types.tsconfig.json | 9 +++++++++ 3 files changed, 10 insertions(+), 6 deletions(-) create mode 100644 types.tsconfig.json diff --git a/package.json b/package.json index e132d1586..d78a57296 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "test": "tests" }, "scripts": { - "build:types": "tsc", + "build:types": "tsc --project types.tsconfig.json", "build:js": "yarn babel --extensions '.ts' --presets @babel/preset-typescript addon-test-support --out-dir addon-test-support/ --ignore '**/*.d.ts'", "build": "npm-run-all --parallel build:*", "prepack": "yarn build", diff --git a/tsconfig.json b/tsconfig.json index 241317076..4a68608c9 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,11 +1,6 @@ { "extends": "@tsconfig/ember/tsconfig.json", "compilerOptions": { - // Use TS to emit declarations - "noEmit": false, - "emitDeclarationOnly": true, - "outDir": "public-types", - "paths": { "dummy/tests/*": ["./tests/*"], "dummy/*": ["./tests/dummy/app/*", "./app/*"], diff --git a/types.tsconfig.json b/types.tsconfig.json new file mode 100644 index 000000000..074a04244 --- /dev/null +++ b/types.tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + // Use TS to emit declarations + "noEmit": false, + "emitDeclarationOnly": true, + "outDir": "public-types" + } +} From bd26ef1635d71228dfd8403d1406cf537cd048d1 Mon Sep 17 00:00:00 2001 From: Chris Krycho Date: Thu, 21 Jul 2022 07:15:00 -0600 Subject: [PATCH 06/24] Ignore generated types in ESLint config --- .eslintignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.eslintignore b/.eslintignore index 2a54281cf..76a71708b 100644 --- a/.eslintignore +++ b/.eslintignore @@ -23,3 +23,4 @@ addon-test-support/**/*.js addon-test-support/**/*.d.ts +public-types From 2930c2ff8bd3163acf5a4f6b2a6c48d4c1b2842f Mon Sep 17 00:00:00 2001 From: Chris Krycho Date: Thu, 21 Jul 2022 08:02:30 -0600 Subject: [PATCH 07/24] Update `any` to `unknown` in DOM targeting --- .../@ember/test-helpers/dom/-target.ts | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/addon-test-support/@ember/test-helpers/dom/-target.ts b/addon-test-support/@ember/test-helpers/dom/-target.ts index 90be41b7d..9683664fe 100644 --- a/addon-test-support/@ember/test-helpers/dom/-target.ts +++ b/addon-test-support/@ember/test-helpers/dom/-target.ts @@ -7,8 +7,12 @@ export interface HTMLElementContentEditable extends HTMLElement { } // eslint-disable-next-line require-jsdoc -export function isElement(target: any): target is Element { - return target.nodeType === Node.ELEMENT_NODE; +export function isElement(target: unknown): target is Element { + return ( + target !== null && + typeof target === 'object' && + Reflect.get(target, 'nodeType') === Node.ELEMENT_NODE + ); } // eslint-disable-next-line require-jsdoc @@ -17,8 +21,12 @@ export function isWindow(target: Target): target is Window { } // eslint-disable-next-line require-jsdoc -export function isDocument(target: any): target is Document { - return target.nodeType === Node.DOCUMENT_NODE; +export function isDocument(target: unknown): target is Document { + return ( + target !== null && + typeof target === 'object' && + Reflect.get(target, 'nodeType') === Node.DOCUMENT_NODE + ); } // eslint-disable-next-line require-jsdoc From 196e548734fa6a5d632b61da580a2dc448122b7a Mon Sep 17 00:00:00 2001 From: Chris Krycho Date: Thu, 21 Jul 2022 08:03:44 -0600 Subject: [PATCH 08/24] Use `TouchEventInit` instead of `object` in `tap` Note that while this might appear breaking because of its increased specificity, (1) we have not been publishing types previously but (2) even if we were, this is a *bug fix*. --- addon-test-support/@ember/test-helpers/dom/tap.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addon-test-support/@ember/test-helpers/dom/tap.ts b/addon-test-support/@ember/test-helpers/dom/tap.ts index 176f2b84e..20be8c671 100644 --- a/addon-test-support/@ember/test-helpers/dom/tap.ts +++ b/addon-test-support/@ember/test-helpers/dom/tap.ts @@ -56,7 +56,7 @@ registerHook('tap', 'start', (target: Target) => { */ export default function tap( target: Target, - options: object = {} + options: TouchEventInit = {} ): Promise { return Promise.resolve() .then(() => { From 5c2f0c0cba4780c197225d659b427ecdb41ff372 Mon Sep 17 00:00:00 2001 From: Chris Krycho Date: Thu, 21 Jul 2022 10:26:18 -0600 Subject: [PATCH 09/24] Add note about types to CONTRIBUTING.md --- CONTRIBUTING.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index dd9412f62..6ecf99d99 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -11,6 +11,10 @@ * `yarn lint` * `yarn lint:fix` +## Types + +When updating the API, you will need to update the type tests (in `tests/api.ts`) as well. The kinds of changes required will make it clear whether the change is backwards compatible or not! + ## Running tests * `ember test` – Runs the test suite on the current Ember version From da12a6afa1ccbb882a984309c4c910d80d8981e5 Mon Sep 17 00:00:00 2001 From: Chris Krycho Date: Thu, 21 Jul 2022 10:32:23 -0600 Subject: [PATCH 10/24] Improve TestMetadata implementation and export it The type needs to be public (though non-user-constructible) so TS users can reference it without `ReturnType` shenanigans. The previous approach to exporting the type for internal usage used a Java/C#-style interface, separate from the single implementing class. Replace that with an `export type` statement and stop exporting the class itself, and use the name `TestMetadata` (no leading `I`). As a bonus, get rid of a couple needless type annotations where it can be inferred instead, and drop the corresponding now-unused imports. --- API.md | 2 +- .../@ember/test-helpers/index.ts | 2 +- .../test-helpers/setup-application-context.ts | 4 +-- .../@ember/test-helpers/setup-context.ts | 2 +- .../test-helpers/setup-rendering-context.ts | 4 +-- .../@ember/test-helpers/test-metadata.ts | 33 +++++++++++-------- tests/unit/test-metadata-test.js | 4 +-- 7 files changed, 28 insertions(+), 23 deletions(-) diff --git a/API.md b/API.md index bd1c47c0a..55941b20c 100644 --- a/API.md +++ b/API.md @@ -934,7 +934,7 @@ a new test metadata object if one does not exist. * `context` **BaseContext** the context to use -Returns **ITestMetadata** the test metadata for the provided context +Returns **TestMetadata** the test metadata for the provided context ## getDeprecations diff --git a/addon-test-support/@ember/test-helpers/index.ts b/addon-test-support/@ember/test-helpers/index.ts index 483c2dafc..949b8fec9 100644 --- a/addon-test-support/@ember/test-helpers/index.ts +++ b/addon-test-support/@ember/test-helpers/index.ts @@ -36,7 +36,7 @@ export { default as validateErrorHandler } from './validate-error-handler'; export { default as setupOnerror, resetOnerror } from './setup-onerror'; export { getDebugInfo } from './-internal/debug-info'; export { default as registerDebugInfoHelper } from './-internal/debug-info-helpers'; -export { default as getTestMetadata } from './test-metadata'; +export { default as getTestMetadata, TestMetadata } from './test-metadata'; export { registerHook as _registerHook, runHooks as _runHooks, diff --git a/addon-test-support/@ember/test-helpers/setup-application-context.ts b/addon-test-support/@ember/test-helpers/setup-application-context.ts index acacf2a3d..187613d10 100644 --- a/addon-test-support/@ember/test-helpers/setup-application-context.ts +++ b/addon-test-support/@ember/test-helpers/setup-application-context.ts @@ -9,7 +9,7 @@ import { import global from './global'; import hasEmberVersion from './has-ember-version'; import settled from './settled'; -import getTestMetadata, { ITestMetadata } from './test-metadata'; +import getTestMetadata from './test-metadata'; import { runHooks } from './-internal/helper-hooks'; import { Router } from '@ember/routing'; @@ -219,7 +219,7 @@ export function currentURL(): string { export default function setupApplicationContext( context: TestContext ): Promise { - let testMetadata: ITestMetadata = getTestMetadata(context); + let testMetadata = getTestMetadata(context); testMetadata.setupTypes.push('setupApplicationContext'); return Promise.resolve(); diff --git a/addon-test-support/@ember/test-helpers/setup-context.ts b/addon-test-support/@ember/test-helpers/setup-context.ts index 43bad1255..086710866 100644 --- a/addon-test-support/@ember/test-helpers/setup-context.ts +++ b/addon-test-support/@ember/test-helpers/setup-context.ts @@ -16,7 +16,7 @@ import global from './global'; import { getResolver } from './resolver'; import { getApplication } from './application'; import { Promise } from './-utils'; -import getTestMetadata, { ITestMetadata } from './test-metadata'; +import getTestMetadata from './test-metadata'; import { registerDestructor, associateDestroyableChild, diff --git a/addon-test-support/@ember/test-helpers/setup-rendering-context.ts b/addon-test-support/@ember/test-helpers/setup-rendering-context.ts index ae20a0596..4dbbecbe8 100644 --- a/addon-test-support/@ember/test-helpers/setup-rendering-context.ts +++ b/addon-test-support/@ember/test-helpers/setup-rendering-context.ts @@ -13,7 +13,7 @@ import settled from './settled'; import { hbs, TemplateFactory } from 'ember-cli-htmlbars'; import getRootElement from './dom/get-root-element'; import { Owner } from './build-owner'; -import getTestMetadata, { ITestMetadata } from './test-metadata'; +import getTestMetadata, { TestMetadata } from './test-metadata'; import { assert, deprecate } from '@ember/debug'; import { runHooks } from './-internal/helper-hooks'; import hasEmberVersion from './has-ember-version'; @@ -276,7 +276,7 @@ export function clearRender(): Promise { export default function setupRenderingContext( context: TestContext ): Promise { - let testMetadata: ITestMetadata = getTestMetadata(context); + let testMetadata = getTestMetadata(context); testMetadata.setupTypes.push('setupRenderingContext'); return Promise.resolve() diff --git a/addon-test-support/@ember/test-helpers/test-metadata.ts b/addon-test-support/@ember/test-helpers/test-metadata.ts index 2ee839dc3..009d06bd7 100644 --- a/addon-test-support/@ember/test-helpers/test-metadata.ts +++ b/addon-test-support/@ember/test-helpers/test-metadata.ts @@ -1,16 +1,6 @@ import { BaseContext } from './setup-context'; -export interface ITestMetadata { - testName?: string; - setupTypes: string[]; - usedHelpers: string[]; - [key: string]: any; - - readonly isRendering: boolean; - readonly isApplication: boolean; -} - -export class TestMetadata implements ITestMetadata { +class TestMetadata { [key: string]: any; testName?: string; setupTypes: string[]; @@ -33,16 +23,31 @@ export class TestMetadata implements ITestMetadata { } } -const TEST_METADATA = new WeakMap(); +export { + // Exported only for testing purposes. + TestMetadata as __TestMetadata, +}; + +// Only export the type side of the item: this way the only way (it is legal) to +// construct it is here, but users can still reference the type. +export type { + /** + * A non-user-constructible interface representing the metadata associated + * with a test, designed for test frameworks to use e.g. with their reporters. + */ + TestMetadata, +}; + +const TEST_METADATA = new WeakMap(); /** * Gets the test metadata associated with the provided test context. Will create * a new test metadata object if one does not exist. * * @param {BaseContext} context the context to use - * @returns {ITestMetadata} the test metadata for the provided context + * @returns {TestMetadata} the test metadata for the provided context */ -export default function getTestMetadata(context: BaseContext): ITestMetadata { +export default function getTestMetadata(context: BaseContext): TestMetadata { if (!TEST_METADATA.has(context)) { TEST_METADATA.set(context, new TestMetadata()); } diff --git a/tests/unit/test-metadata-test.js b/tests/unit/test-metadata-test.js index d4837a8bc..5d5622bab 100644 --- a/tests/unit/test-metadata-test.js +++ b/tests/unit/test-metadata-test.js @@ -1,13 +1,13 @@ import QUnit, { module, test } from 'qunit'; import { getTestMetadata } from '@ember/test-helpers'; -import { TestMetadata } from '@ember/test-helpers/test-metadata'; +import { __TestMetadata } from '@ember/test-helpers/test-metadata'; module('Test Metadata', function () { test('getTestMetadata returns default test metadata', function (assert) { let test = QUnit.config.current; let testMetadata = getTestMetadata(test); - assert.ok(testMetadata instanceof TestMetadata); + assert.ok(testMetadata instanceof __TestMetadata); assert.deepEqual(testMetadata.setupTypes, []); }); From 05b61cd18a84da6bb227f089b558c1255059ea14 Mon Sep 17 00:00:00 2001 From: Chris Krycho Date: Thu, 21 Jul 2022 10:33:23 -0600 Subject: [PATCH 11/24] Introduce an explicit ErrorHandlerValidation type This is only used internally, but allows us to present a less specific public API, so that callers can rely on fewer details. --- .../@ember/test-helpers/validate-error-handler.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/addon-test-support/@ember/test-helpers/validate-error-handler.ts b/addon-test-support/@ember/test-helpers/validate-error-handler.ts index 887ef9c17..c158af07f 100644 --- a/addon-test-support/@ember/test-helpers/validate-error-handler.ts +++ b/addon-test-support/@ember/test-helpers/validate-error-handler.ts @@ -1,4 +1,9 @@ import Ember from 'ember'; + +type ErrorHandlerValidation = + | Readonly<{ isValid: true; message: null }> + | Readonly<{ isValid: false; message: string }>; + const VALID = Object.freeze({ isValid: true, message: null }); const INVALID = Object.freeze({ isValid: false, @@ -28,7 +33,9 @@ const INVALID = Object.freeze({ * assert.ok(result.isValid, result.message); * }); */ -export default function validateErrorHandler(callback = Ember.onerror) { +export default function validateErrorHandler( + callback = Ember.onerror +): ErrorHandlerValidation { if (callback === undefined || callback === null) { return VALID; } From 3f9beee8f6cb406def143cca50e0699ad3acb633 Mon Sep 17 00:00:00 2001 From: Chris Krycho Date: Thu, 21 Jul 2022 10:33:38 -0600 Subject: [PATCH 12/24] Add/fix docs for internal DebugInfo type --- .../@ember/test-helpers/-internal/debug-info.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/addon-test-support/@ember/test-helpers/-internal/debug-info.ts b/addon-test-support/@ember/test-helpers/-internal/debug-info.ts index 72777cb82..7875582f5 100644 --- a/addon-test-support/@ember/test-helpers/-internal/debug-info.ts +++ b/addon-test-support/@ember/test-helpers/-internal/debug-info.ts @@ -37,12 +37,16 @@ interface SummaryInfo { isRenderPending: boolean; } +/** + * The base functionality which may be present on the `SettledState` interface + * in the `settled` module (**not** the one in this module). + */ export default interface DebugInfo { toConsole: () => void; } /** - * Determins if the `getDebugInfo` method is available in the + * Determines if the `getDebugInfo` method is available in the * running verison of backburner. * * @returns {boolean} True if `getDebugInfo` is present in backburner, otherwise false. From 18fe0c1fdac0818645702d8bda559520b785d6e9 Mon Sep 17 00:00:00 2001 From: Chris Krycho Date: Thu, 21 Jul 2022 10:34:28 -0600 Subject: [PATCH 13/24] Use `() => void` instead of `CallableFunction` --- .../@ember/test-helpers/-internal/deprecations.ts | 4 ++-- .../@ember/test-helpers/-internal/warnings.ts | 4 ++-- addon-test-support/@ember/test-helpers/setup-context.ts | 8 ++++---- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/addon-test-support/@ember/test-helpers/-internal/deprecations.ts b/addon-test-support/@ember/test-helpers/-internal/deprecations.ts index f5618f082..3d96acee9 100644 --- a/addon-test-support/@ember/test-helpers/-internal/deprecations.ts +++ b/addon-test-support/@ember/test-helpers/-internal/deprecations.ts @@ -53,12 +53,12 @@ export function getDeprecationsForContext( * * @private * @param {BaseContext} [context] the test context - * @param {CallableFunction} [callback] The callback that when executed will have its DeprecationFailure recorded + * @param {Function} [callback] The callback that when executed will have its DeprecationFailure recorded * @return {Array} The Deprecation Failures associated with the corresponding baseContext which occured while the CallbackFunction was executed */ export function getDeprecationsDuringCallbackForContext( context: BaseContext, - callback: CallableFunction + callback: () => void ): Array | Promise> { if (!context) { throw new TypeError( diff --git a/addon-test-support/@ember/test-helpers/-internal/warnings.ts b/addon-test-support/@ember/test-helpers/-internal/warnings.ts index 48ac966dd..c69ce5206 100644 --- a/addon-test-support/@ember/test-helpers/-internal/warnings.ts +++ b/addon-test-support/@ember/test-helpers/-internal/warnings.ts @@ -48,12 +48,12 @@ export function getWarningsForContext(context: BaseContext): Array { * * @private * @param {BaseContext} [context] the test context - * @param {CallableFunction} [callback] The callback that when executed will have its warnings recorded + * @param {Function} [callback] The callback that when executed will have its warnings recorded * @return {Array} The warnings associated with the corresponding baseContext which occured while the CallbackFunction was executed */ export function getWarningsDuringCallbackForContext( context: BaseContext, - callback: CallableFunction + callback: () => void ): Array | Promise> { if (!context) { throw new TypeError( diff --git a/addon-test-support/@ember/test-helpers/setup-context.ts b/addon-test-support/@ember/test-helpers/setup-context.ts index 086710866..ac2beb31d 100644 --- a/addon-test-support/@ember/test-helpers/setup-context.ts +++ b/addon-test-support/@ember/test-helpers/setup-context.ts @@ -234,7 +234,7 @@ export function getDeprecations(): Array { * Returns deprecations which have occured so far for a the current test context * * @public - * @param {CallableFunction} [callback] The callback that when executed will have its DeprecationFailure recorded + * @param {Function} [callback] The callback that when executed will have its DeprecationFailure recorded * @returns {Array | Promise>} An array of deprecation messages * @example Usage via ember-qunit * @@ -259,7 +259,7 @@ export function getDeprecations(): Array { * }); */ export function getDeprecationsDuringCallback( - callback: CallableFunction + callback: () => void ): Array | Promise> { const context = getContext(); @@ -305,7 +305,7 @@ export function getWarnings(): Array { * Returns warnings which have occured so far for a the current test context * * @public - * @param {CallableFunction} [callback] The callback that when executed will have its warnings recorded + * @param {Function} [callback] The callback that when executed will have its warnings recorded * @returns {Array | Promise>} An array of warnings information * @example Usage via ember-qunit * @@ -332,7 +332,7 @@ export function getWarnings(): Array { * }); */ export function getWarningsDuringCallback( - callback: CallableFunction + callback: () => void ): Array | Promise> { const context = getContext(); From 232ed5fe48feecab290841f5399d6eed48559569 Mon Sep 17 00:00:00 2001 From: Chris Krycho Date: Thu, 21 Jul 2022 10:35:02 -0600 Subject: [PATCH 14/24] Fix doc for `setupApplicationContext` return type --- .../@ember/test-helpers/setup-application-context.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addon-test-support/@ember/test-helpers/setup-application-context.ts b/addon-test-support/@ember/test-helpers/setup-application-context.ts index 187613d10..f75983256 100644 --- a/addon-test-support/@ember/test-helpers/setup-application-context.ts +++ b/addon-test-support/@ember/test-helpers/setup-application-context.ts @@ -214,7 +214,7 @@ export function currentURL(): string { @public @param {Object} context the context to setup - @returns {Promise} resolves with the context that was setup + @returns {Promise} resolves when the context is set up */ export default function setupApplicationContext( context: TestContext From ef1db30056b36c7e021bf0a7a7d610ba9f5ac8f9 Mon Sep 17 00:00:00 2001 From: Chris Krycho Date: Thu, 21 Jul 2022 10:35:43 -0600 Subject: [PATCH 15/24] Use a Record of `unknown` instead of an index of `any` for `visit` --- .../@ember/test-helpers/setup-application-context.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addon-test-support/@ember/test-helpers/setup-application-context.ts b/addon-test-support/@ember/test-helpers/setup-application-context.ts index f75983256..1b4f949cb 100644 --- a/addon-test-support/@ember/test-helpers/setup-application-context.ts +++ b/addon-test-support/@ember/test-helpers/setup-application-context.ts @@ -121,7 +121,7 @@ export function setupRouterSettlednessTracking() { */ export function visit( url: string, - options?: { [key: string]: any } + options?: Record ): Promise { const context = getContext(); if (!context || !isApplicationTestContext(context)) { From b325ba63a6017aa34666a9541d51c5d401508526 Mon Sep 17 00:00:00 2001 From: Chris Krycho Date: Thu, 21 Jul 2022 10:36:23 -0600 Subject: [PATCH 16/24] Export `Warning` and `DeprecationFailure` types --- addon-test-support/@ember/test-helpers/index.ts | 2 ++ .../@ember/test-helpers/setup-context.ts | 11 +++++++++++ 2 files changed, 13 insertions(+) diff --git a/addon-test-support/@ember/test-helpers/index.ts b/addon-test-support/@ember/test-helpers/index.ts index 949b8fec9..4b94e4c2b 100644 --- a/addon-test-support/@ember/test-helpers/index.ts +++ b/addon-test-support/@ember/test-helpers/index.ts @@ -14,8 +14,10 @@ export { resumeTest, getDeprecations, getDeprecationsDuringCallback, + DeprecationFailure, getWarnings, getWarningsDuringCallback, + Warning, } from './setup-context'; export { default as teardownContext } from './teardown-context'; export { diff --git a/addon-test-support/@ember/test-helpers/setup-context.ts b/addon-test-support/@ember/test-helpers/setup-context.ts index ac2beb31d..cdcdf26a3 100644 --- a/addon-test-support/@ember/test-helpers/setup-context.ts +++ b/addon-test-support/@ember/test-helpers/setup-context.ts @@ -64,6 +64,13 @@ export interface BaseContext { [key: string]: unknown; } +/** + * The public API for the test context, which test authors can depend on being + * available. + * + * Note: this is *not* user-constructible; it becomes available by calling + * `setupContext()` with a `BaseContext`. + */ export interface TestContext extends BaseContext { owner: Owner; @@ -230,6 +237,8 @@ export function getDeprecations(): Array { return getDeprecationsForContext(context); } +export type { DeprecationFailure }; + /** * Returns deprecations which have occured so far for a the current test context * @@ -301,6 +310,8 @@ export function getWarnings(): Array { return getWarningsForContext(context); } +export type { Warning }; + /** * Returns warnings which have occured so far for a the current test context * From 302b3ff5d198ccd470e612bbe4563873a4b5809f Mon Sep 17 00:00:00 2001 From: Chris Krycho Date: Thu, 21 Jul 2022 10:36:50 -0600 Subject: [PATCH 17/24] Export `DebugInfo`, `BaseContext`, and `TestContext` types --- addon-test-support/@ember/test-helpers/index.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/addon-test-support/@ember/test-helpers/index.ts b/addon-test-support/@ember/test-helpers/index.ts index 4b94e4c2b..06b19ae1b 100644 --- a/addon-test-support/@ember/test-helpers/index.ts +++ b/addon-test-support/@ember/test-helpers/index.ts @@ -18,6 +18,8 @@ export { getWarnings, getWarningsDuringCallback, Warning, + BaseContext, + TestContext, } from './setup-context'; export { default as teardownContext } from './teardown-context'; export { @@ -36,7 +38,7 @@ export { default as settled, isSettled, getSettledState } from './settled'; export { default as waitUntil } from './wait-until'; export { default as validateErrorHandler } from './validate-error-handler'; export { default as setupOnerror, resetOnerror } from './setup-onerror'; -export { getDebugInfo } from './-internal/debug-info'; +export { getDebugInfo, default as DebugInfo } from './-internal/debug-info'; export { default as registerDebugInfoHelper } from './-internal/debug-info-helpers'; export { default as getTestMetadata, TestMetadata } from './test-metadata'; export { From 8af3efadc4f4aa4dc9ac24d26a1d9cdb6a42a4a9 Mon Sep 17 00:00:00 2001 From: Chris Krycho Date: Thu, 21 Jul 2022 08:03:13 -0600 Subject: [PATCH 18/24] Introduce type test suite for whole public API This is intentionally quite specific throughout, so any changes will cause failures here, but I used it to make sure we have public imports for all public-facing types *and* to loosen several of the types so that they can be further expanded in the future. --- package.json | 1 + tests/api.ts | 246 ++++++++++++++++++++++++++++++++++++++++++++++++++ tsconfig.json | 3 +- yarn.lock | 5 + 4 files changed, 253 insertions(+), 2 deletions(-) create mode 100644 tests/api.ts diff --git a/package.json b/package.json index d78a57296..f37ce3365 100644 --- a/package.json +++ b/package.json @@ -98,6 +98,7 @@ "eslint-plugin-disable-features": "^0.1.3", "eslint-plugin-node": "^11.1.0", "eslint-plugin-prettier": "^4.0.0", + "expect-type": "^0.13.0", "fs-extra": "^10.0.0", "latest-version": "^5.0.0", "loader.js": "^4.7.0", diff --git a/tests/api.ts b/tests/api.ts new file mode 100644 index 000000000..a2c51db97 --- /dev/null +++ b/tests/api.ts @@ -0,0 +1,246 @@ +import { expectTypeOf } from 'expect-type'; + +import { + // DOM Interaction Helpers + blur, + click, + doubleClick, + fillIn, + focus, + scrollTo, + select, + tap, + triggerEvent, + triggerKeyEvent, + typeIn, + // DOM Query Helpers + find, + findAll, + getRootElement, + // Routing Helpers + visit, + currentRouteName, + currentURL, + // Rendering Helpers + render, + clearRender, + // Wait Helpers + waitFor, + waitUntil, + settled, + isSettled, + getSettledState, + // Pause Helpers + pauseTest, + resumeTest, + // Debug Helpers + getDebugInfo, + registerDebugInfoHelper, + // Test Framework APIs + setResolver, + getResolver, + setupContext, + getContext, + setContext, + unsetContext, + teardownContext, + setupRenderingContext, + getApplication, + setApplication, + setupApplicationContext, + validateErrorHandler, + setupOnerror, + resetOnerror, + getTestMetadata, + // deprecation and warning APIs + getDeprecations, + getDeprecationsDuringCallback, + getWarnings, + getWarningsDuringCallback, + BaseContext, + TestContext, + TestMetadata, + DebugInfo as InternalDebugInfo, + DeprecationFailure, + Warning, +} from '@ember/test-helpers'; +import { ComponentInstance } from '@glimmer/interfaces'; +import { Owner } from '@ember/test-helpers/build-owner'; +import { DebugInfo as BackburnerDebugInfo } from '@ember/runloop/-private/backburner'; +import EmberResolver from 'ember-resolver'; +import Application from '@ember/application'; +import { TemplateFactory } from 'ember-cli-htmlbars'; + +type Target = string | Element | Document | Window; + +// DOM Interaction Helpers +expectTypeOf(blur).toEqualTypeOf<(target?: Target) => Promise>(); +expectTypeOf(click).toEqualTypeOf< + (target: Target, options?: MouseEventInit) => Promise +>(); +expectTypeOf(doubleClick).toEqualTypeOf< + (target: Target, options?: MouseEventInit) => Promise +>(); +expectTypeOf(fillIn).toEqualTypeOf< + (target: Target, text: string) => Promise +>(); +expectTypeOf(focus).toEqualTypeOf<(target: Target) => Promise>(); +expectTypeOf(scrollTo).toEqualTypeOf< + (target: string | HTMLElement, x: number, y: number) => Promise +>(); +expectTypeOf(select).toEqualTypeOf< + ( + target: Target, + options: string | string[], + keepPreviouslySelected?: boolean + ) => Promise +>(); +expectTypeOf(tap).toEqualTypeOf< + (target: Target, options?: TouchEventInit) => Promise +>(); +expectTypeOf(triggerEvent).toEqualTypeOf< + (target: Target, eventType: string, options?: object) => Promise +>(); +expectTypeOf(triggerKeyEvent).toEqualTypeOf< + ( + target: Target, + eventType: 'keydown' | 'keyup' | 'keypress', + key: number | string, + modifiers?: { + ctrlKey?: boolean; + altKey?: boolean; + shiftKey?: boolean; + metaKey?: boolean; + } + ) => Promise +>(); +expectTypeOf(typeIn).toEqualTypeOf< + ( + target: Target, + text: string, + options?: { + delay?: number; + } + ) => Promise +>(); + +// DOM Query Helpers +expectTypeOf(find).toEqualTypeOf<(selector: string) => Element | null>(); +expectTypeOf(findAll).toEqualTypeOf<(selector: string) => Array>(); +expectTypeOf(getRootElement).toEqualTypeOf<() => Element | Document>(); + +// Routing Helpers +expectTypeOf(visit).toEqualTypeOf< + (url: string, options?: Record) => Promise +>(); +expectTypeOf(currentRouteName).toEqualTypeOf<() => string>(); +expectTypeOf(currentURL).toEqualTypeOf<() => string>(); + +// Rendering Helpers +expectTypeOf(render).toMatchTypeOf< + ( + templateOrComponent: TemplateFactory | ComponentInstance, + options?: { owner?: Owner } + ) => Promise +>(); +expectTypeOf(clearRender).toEqualTypeOf<() => Promise>(); + +// Wait Helpers +expectTypeOf(waitFor).toEqualTypeOf< + ( + selector: string, + options?: { + timeout?: number; + count?: number | null; + timeoutMessage?: string; + } + ) => Promise> +>(); +expectTypeOf(waitUntil).toEqualTypeOf< + ( + callback: () => T | void | false | 0 | '' | null | undefined, + options?: { + timeout?: number; + timeoutMessage?: string; + } + ) => Promise +>(); +expectTypeOf(settled).toEqualTypeOf<() => Promise>(); +expectTypeOf(isSettled).toEqualTypeOf<() => boolean>(); +expectTypeOf(getSettledState).toEqualTypeOf< + () => { + hasRunLoop: boolean; + hasPendingTimers: boolean; + hasPendingWaiters: boolean; + hasPendingRequests: boolean; + hasPendingTransitions: boolean | null; + isRenderPending: boolean; + pendingRequestCount: number; + debugInfo?: InternalDebugInfo; + } +>(); + +// Pause Helpers +expectTypeOf(pauseTest).toEqualTypeOf<() => Promise>(); +expectTypeOf(resumeTest).toEqualTypeOf<() => void>(); + +// Debug Helpers +expectTypeOf(getDebugInfo).toEqualTypeOf<() => BackburnerDebugInfo | null>(); +expectTypeOf(registerDebugInfoHelper).toEqualTypeOf< + (debugInfoHelper: { name: string; log: () => void }) => void +>(); + +// Test Framework APIs +expectTypeOf(setResolver).toEqualTypeOf<(resolver: EmberResolver) => void>(); +expectTypeOf(getResolver).toEqualTypeOf<() => EmberResolver | undefined>(); +expectTypeOf(setupContext).toEqualTypeOf< + ( + context: BaseContext, + options?: { resolver?: EmberResolver } + ) => Promise +>(); +expectTypeOf(getContext).toEqualTypeOf<() => BaseContext | undefined>(); +expectTypeOf(setContext).toEqualTypeOf<(context: BaseContext) => void>(); +expectTypeOf(unsetContext).toEqualTypeOf<() => void>(); +expectTypeOf(teardownContext).toEqualTypeOf< + ( + context: TestContext, + options?: { waitForSettled?: boolean } + ) => Promise +>(); +expectTypeOf(setupRenderingContext).toEqualTypeOf< + (context: TestContext) => Promise +>(); +expectTypeOf(getApplication).toEqualTypeOf<() => Application | undefined>(); +expectTypeOf(setApplication).toEqualTypeOf< + (application: Application) => void +>(); +expectTypeOf(setupApplicationContext).toEqualTypeOf< + (context: TestContext) => Promise +>(); +expectTypeOf(validateErrorHandler).toMatchTypeOf< + ( + callback?: (error: Error) => void + ) => + | Readonly<{ isValid: true; message: null }> + | Readonly<{ isValid: false; message: string }> +>(); +expectTypeOf(setupOnerror).toEqualTypeOf< + (onError?: (error: Error) => void) => void +>(); +expectTypeOf(resetOnerror).toEqualTypeOf<() => void>(); +expectTypeOf(getTestMetadata).toEqualTypeOf< + (context: BaseContext) => TestMetadata +>(); + +// deprecation and warning APIs +expectTypeOf(getDeprecations).toEqualTypeOf<() => Array>(); +expectTypeOf(getDeprecationsDuringCallback).toEqualTypeOf< + ( + callback: () => void + ) => Array | Promise> +>(); +expectTypeOf(getWarnings).toEqualTypeOf<() => Array>(); +expectTypeOf(getWarningsDuringCallback).toEqualTypeOf< + (callback: () => void) => Array | Promise> +>(); diff --git a/tsconfig.json b/tsconfig.json index 4a68608c9..27ab413da 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -9,6 +9,5 @@ "*": ["./types/*"] } }, - "exclude": ["node_modules"], - "include": ["./addon-test-support/**/*.ts"] + "include": ["./addon-test-support/**/*.ts", "tests/**/*.ts"] } diff --git a/yarn.lock b/yarn.lock index 5017b883a..8aa60c807 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7164,6 +7164,11 @@ expand-tilde@^2.0.0, expand-tilde@^2.0.2: dependencies: homedir-polyfill "^1.0.1" +expect-type@^0.13.0: + version "0.13.0" + resolved "https://registry.yarnpkg.com/expect-type/-/expect-type-0.13.0.tgz#916646a7a73f3ee77039a634ee9035efe1876eb2" + integrity sha512-CclevazQfrqo8EvbLPmP7osnb1SZXkw47XPPvUUpeMz4HuGzDltE7CaIt3RLyT9UQrwVK/LDn+KVcC0hcgjgDg== + express@^4.10.7, express@^4.17.1: version "4.17.1" resolved "https://registry.yarnpkg.com/express/-/express-4.17.1.tgz#4491fc38605cf51f8629d39c2b5d026f98a4c134" From cc3844b038295118b3f8f3269d3cae34b4bc42de Mon Sep 17 00:00:00 2001 From: Chris Krycho Date: Thu, 21 Jul 2022 10:43:20 -0600 Subject: [PATCH 19/24] Fix `deprecate` calls to use full API The previous cast `as any` hid a real type error, as it does! --- .../@ember/test-helpers/setup-rendering-context.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/addon-test-support/@ember/test-helpers/setup-rendering-context.ts b/addon-test-support/@ember/test-helpers/setup-rendering-context.ts index 4dbbecbe8..09ae56345 100644 --- a/addon-test-support/@ember/test-helpers/setup-rendering-context.ts +++ b/addon-test-support/@ember/test-helpers/setup-rendering-context.ts @@ -293,8 +293,9 @@ export default function setupRenderingContext( for: '@ember/test-helpers', since: { enabled: '2.0.0', + available: '2.0.0', }, - } as any // @types/ember is missing since + for + } ); return render(template); @@ -310,8 +311,9 @@ export default function setupRenderingContext( for: '@ember/test-helpers', since: { enabled: '2.0.0', + available: '2.0.0', }, - } as any // @types/ember is missing since + for + } ); return clearRender(); From a51eae8c502d10b5e12c456f4367d44c4c5b3e8b Mon Sep 17 00:00:00 2001 From: Chris Krycho Date: Fri, 22 Jul 2022 10:52:51 -0600 Subject: [PATCH 20/24] Get rid of a couple unsafe casts - use `assert`s in `setup-application-context` for router lookups - bind an intermediate term in a conditional in `build-registry` to preserve the control-flow-based narrowing in the loop --- .../test-helpers/-internal/build-registry.ts | 11 +++++------ .../test-helpers/setup-application-context.ts | 15 ++++++++++++--- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/addon-test-support/@ember/test-helpers/-internal/build-registry.ts b/addon-test-support/@ember/test-helpers/-internal/build-registry.ts index 26d8fcd2a..45777ccfe 100644 --- a/addon-test-support/@ember/test-helpers/-internal/build-registry.ts +++ b/addon-test-support/@ember/test-helpers/-internal/build-registry.ts @@ -29,13 +29,12 @@ function exposeRegistryMethodsWithoutDeprecations(container: any) { ]; for (let i = 0, l = methods.length; i < l; i++) { - let method = methods[i]; + let methodName = methods[i]; - if (method && method in container) { - container[method] = function (...args: unknown[]) { - // SAFETY: `method` is defined because we *just* checked that it is in - // the conditional wrapping this. - return container._registry[method!](...args); + if (methodName && methodName in container) { + const knownMethod = methodName; + container[knownMethod] = function (...args: unknown[]) { + return container._registry[knownMethod](...args); }; } } diff --git a/addon-test-support/@ember/test-helpers/setup-application-context.ts b/addon-test-support/@ember/test-helpers/setup-application-context.ts index 1b4f949cb..89c705191 100644 --- a/addon-test-support/@ember/test-helpers/setup-application-context.ts +++ b/addon-test-support/@ember/test-helpers/setup-application-context.ts @@ -12,6 +12,8 @@ import settled from './settled'; import getTestMetadata from './test-metadata'; import { runHooks } from './-internal/helper-hooks'; import { Router } from '@ember/routing'; +import RouterService from '@ember/routing/router-service'; +import { assert } from '@ember/debug'; export interface ApplicationTestContext extends TestContext { element?: Element | null; @@ -88,9 +90,14 @@ export function setupRouterSettlednessTracking() { HAS_SETUP_ROUTER.set(context, true); let { owner } = context; - let router: Router; + let router: Router | RouterService; if (CAN_USE_ROUTER_EVENTS) { - router = owner.lookup('service:router') as Router; + let routerService = owner.lookup('service:router'); + assert( + 'router service is not available', + routerService instanceof RouterService + ); + router = routerService; // track pending transitions via the public routeWillChange / routeDidChange APIs // routeWillChange can fire many times and is only useful to know when we have _started_ @@ -98,7 +105,9 @@ export function setupRouterSettlednessTracking() { router.on('routeWillChange', () => (routerTransitionsPending = true)); router.on('routeDidChange', () => (routerTransitionsPending = false)); } else { - router = owner.lookup('router:main') as Router; + let mainRouter = owner.lookup('router:main'); + assert('router:main is not available', mainRouter instanceof Router); + router = mainRouter; ROUTER.set(context, router); } From aa186f24593c3f633ec01ed183db555f3097d23a Mon Sep 17 00:00:00 2001 From: Chris Krycho Date: Fri, 22 Jul 2022 11:39:44 -0600 Subject: [PATCH 21/24] Use `assert` to replace two `as string` casts --- .../@ember/test-helpers/setup-application-context.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/addon-test-support/@ember/test-helpers/setup-application-context.ts b/addon-test-support/@ember/test-helpers/setup-application-context.ts index 89c705191..8dea87c2f 100644 --- a/addon-test-support/@ember/test-helpers/setup-application-context.ts +++ b/addon-test-support/@ember/test-helpers/setup-application-context.ts @@ -182,8 +182,12 @@ export function currentRouteName(): string { } let router = context.owner.lookup('router:main'); - - return get(router, 'currentRouteName') as string; + let currentRouteName = get(router, 'currentRouteName'); + assert( + 'currentRouteName shoudl be a string', + typeof currentRouteName === 'string' + ); + return currentRouteName; } const HAS_CURRENT_URL_ON_ROUTER = hasEmberVersion(2, 13); @@ -203,7 +207,9 @@ export function currentURL(): string { let router = context.owner.lookup('router:main'); if (HAS_CURRENT_URL_ON_ROUTER) { - return get(router, 'currentURL') as string; + let currentUrl = get(router, 'currentURL'); + assert('currentUrl should be a string', typeof currentUrl === 'string'); + return currentUrl; } else { // SAFETY: this is *positively ancient* and should probably be removed at // some point; old routers which don't have `currentURL` *should* have a From 8ebf813e3b617eae1481758eea76462be73c42d3 Mon Sep 17 00:00:00 2001 From: Chris Krycho Date: Mon, 25 Jul 2022 07:49:55 -0600 Subject: [PATCH 22/24] assert only what we legally can on RouterService The class is not exported (though its type is), since it is not intended to be subclassed. Assert that it at least exists, and introduce an unsafe cast and a `SAFETY` comment to explain the cast. --- .../test-helpers/setup-application-context.ts | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/addon-test-support/@ember/test-helpers/setup-application-context.ts b/addon-test-support/@ember/test-helpers/setup-application-context.ts index 8dea87c2f..0611f5887 100644 --- a/addon-test-support/@ember/test-helpers/setup-application-context.ts +++ b/addon-test-support/@ember/test-helpers/setup-application-context.ts @@ -12,7 +12,7 @@ import settled from './settled'; import getTestMetadata from './test-metadata'; import { runHooks } from './-internal/helper-hooks'; import { Router } from '@ember/routing'; -import RouterService from '@ember/routing/router-service'; +import type RouterService from '@ember/routing/router-service'; import { assert } from '@ember/debug'; export interface ApplicationTestContext extends TestContext { @@ -92,12 +92,13 @@ export function setupRouterSettlednessTracking() { let { owner } = context; let router: Router | RouterService; if (CAN_USE_ROUTER_EVENTS) { + // SAFETY: unfortunately we cannot `assert` here at present because the + // class is not exported, only the type, since it is not designed to be + // sub-classed. The most we can do at present is assert that it at least + // *exists* and assume that if it does exist, it is bound correctly. let routerService = owner.lookup('service:router'); - assert( - 'router service is not available', - routerService instanceof RouterService - ); - router = routerService; + assert('router service is not set up correctly', !!routerService); + router = routerService as RouterService; // track pending transitions via the public routeWillChange / routeDidChange APIs // routeWillChange can fire many times and is only useful to know when we have _started_ From 02525ddcf7bac64bb1ccb36fd1c37c190fc78207 Mon Sep 17 00:00:00 2001 From: Chris Krycho Date: Mon, 25 Jul 2022 08:17:31 -0600 Subject: [PATCH 23/24] Introduce a (safe) lie in `currentURL()` types The test framework *itself* will sometimes see a `null` value for the result of `currentURL()`, but end users never will. In the medium term, we should think about designs which account for this correctly, because as it stands the tests do not get identical behavior to end users! --- .../test-helpers/setup-application-context.ts | 19 ++++++++++++++++--- .../test-helpers/setup-rendering-context.ts | 1 + 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/addon-test-support/@ember/test-helpers/setup-application-context.ts b/addon-test-support/@ember/test-helpers/setup-application-context.ts index 0611f5887..aecf8ae93 100644 --- a/addon-test-support/@ember/test-helpers/setup-application-context.ts +++ b/addon-test-support/@ember/test-helpers/setup-application-context.ts @@ -208,9 +208,22 @@ export function currentURL(): string { let router = context.owner.lookup('router:main'); if (HAS_CURRENT_URL_ON_ROUTER) { - let currentUrl = get(router, 'currentURL'); - assert('currentUrl should be a string', typeof currentUrl === 'string'); - return currentUrl; + let routerCurrentURL = get(router, 'currentURL'); + + // SAFETY: this path is a lie for the sake of the public-facing types. The + // framework itself sees this path, but users never do. A user who calls the + // API without calling `visit()` will see an `UnrecognizedURLError`, rather + // than getting back `null`. + if (routerCurrentURL === null) { + return routerCurrentURL as never as string; + } + + assert( + `currentUrl should be a string, but was ${typeof routerCurrentURL}`, + typeof routerCurrentURL === 'string' + ); + + return routerCurrentURL; } else { // SAFETY: this is *positively ancient* and should probably be removed at // some point; old routers which don't have `currentURL` *should* have a diff --git a/addon-test-support/@ember/test-helpers/setup-rendering-context.ts b/addon-test-support/@ember/test-helpers/setup-rendering-context.ts index 09ae56345..b570ef685 100644 --- a/addon-test-support/@ember/test-helpers/setup-rendering-context.ts +++ b/addon-test-support/@ember/test-helpers/setup-rendering-context.ts @@ -22,6 +22,7 @@ import { macroCondition, dependencySatisfies } from '@embroider/macros'; import { ComponentRenderMap, SetUsage } from './setup-context'; import { ensureSafeComponent } from '@embroider/util'; import type { ComponentInstance } from '@glimmer/interfaces'; +import ViewMixin from '@ember/component/-private/view-mixin'; const OUTLET_TEMPLATE = hbs`{{outlet}}`; const EMPTY_TEMPLATE = hbs``; From 8e7de3a79e4e68f540817cb7be3e9f8654d3da1f Mon Sep 17 00:00:00 2001 From: Chris Krycho Date: Mon, 25 Jul 2022 07:49:55 -0600 Subject: [PATCH 24/24] assert only what we legally can on Router As with `RouterService`, the class is not exported (though its type is), at least on the versions where we end up resolving it, since it is not intended to be subclassed. Assert that it at least exists, and introduce an unsafe cast and a `SAFETY` comment to explain the cast. --- .../@ember/test-helpers/setup-application-context.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/addon-test-support/@ember/test-helpers/setup-application-context.ts b/addon-test-support/@ember/test-helpers/setup-application-context.ts index aecf8ae93..35ac29756 100644 --- a/addon-test-support/@ember/test-helpers/setup-application-context.ts +++ b/addon-test-support/@ember/test-helpers/setup-application-context.ts @@ -11,7 +11,7 @@ import hasEmberVersion from './has-ember-version'; import settled from './settled'; import getTestMetadata from './test-metadata'; import { runHooks } from './-internal/helper-hooks'; -import { Router } from '@ember/routing'; +import type { Router } from '@ember/routing'; import type RouterService from '@ember/routing/router-service'; import { assert } from '@ember/debug'; @@ -106,9 +106,11 @@ export function setupRouterSettlednessTracking() { router.on('routeWillChange', () => (routerTransitionsPending = true)); router.on('routeDidChange', () => (routerTransitionsPending = false)); } else { + // SAFETY: similarly, this cast cannot be made safer because on the versions + // where we fall into this path, this is *also* not an exported class. let mainRouter = owner.lookup('router:main'); - assert('router:main is not available', mainRouter instanceof Router); - router = mainRouter; + assert('router:main is not available', !!mainRouter); + router = mainRouter as Router; ROUTER.set(context, router); }