From cb7416c02ab50b9a34812c676157040e6b8c78be Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Wed, 2 Sep 2026 12:48:07 -0400 Subject: [PATCH 1/6] Move internally-used mixins to internal copies RFC #1116 deprecates applying Ember's mixins, but Ember's own internals are built on several of them. Those mixins need to exist twice: an internal copy that applies silently, and a public copy that emits the deprecation. This is the mechanical half -- the six mixins under @ember/-internals move to `-internal` filenames with their contents unchanged, and their importers follow. The public, deprecating copies are added in the next commit. Add deprecating public mixin wrappers (RFC #1116 follow-up) Every mixin Ember applies to its own classes now exists twice: the internal copy applies silently, and the public copy wraps it and emits the deprecation from `init`. The public copies keep the API documentation; the internal copies carry the implementation. `Mixin` itself gets the same treatment -- `InternalMixin` holds the machinery and `Mixin.create` is the deprecating subclass -- which lets the `INTERNAL_MIXIN_CREATE` symbol go away entirely. Because the two copies are distinct objects, `meta.hasMixin` would no longer match a public mixin against an object that only ever had the internal copy applied. `DeprecatedMixin` overrides `detect` to look through to the wrapped copy, so `EmberArray.detect(someArrayProxy)` keeps working while the deprecation is live. Ember's own tests that exercise a mixin's behavior now apply the internal copy, matching what the framework does; tests that specifically cover the deprecation keep using the public copy. `EmberObject` already applies the internal `Observable`, so `observable_test` no longer re-applies the public one on top. The internal copies are kept out of `renamed-modules` so they are not advertised as importable module paths. Move deprecations to 7.5 Set deprecate flags, and update untils --- .../@ember/-internals/deprecations/index.ts | 77 ++ .../-internals/glimmer/lib/component.ts | 10 +- .../-internals/glimmer/lib/helpers/each-in.ts | 2 +- .../tests/integration/helpers/tracked-test.js | 2 +- .../runtime/lib/mixins/-proxy-internal.ts | 123 +++ .../-internals/runtime/lib/mixins/-proxy.ts | 105 +-- .../lib/mixins/action_handler-internal.ts | 51 ++ .../runtime/lib/mixins/action_handler.ts | 48 +- .../runtime/lib/mixins/comparable.ts | 5 +- .../lib/mixins/container_proxy-internal.ts | 43 ++ .../runtime/lib/mixins/container_proxy.ts | 39 +- .../lib/mixins/registry_proxy-internal.ts | 56 ++ .../runtime/lib/mixins/registry_proxy.ts | 50 +- .../mixins/target_action_support-internal.ts | 112 +++ .../lib/mixins/target_action_support.ts | 104 +-- .../runtime/tests/mixins/array_test.js | 3 +- .../tests/mixins/container_proxy_test.js | 2 +- .../mixins/target_action_support_test.js | 2 +- .../utils/lib/internal-mixin-create.ts | 11 - .../lib/mixins/action_support-internal.ts | 51 ++ .../views/lib/mixins/action_support.ts | 49 +- .../-internals/views/lib/views/core_view.ts | 6 +- packages/@ember/array/index-internal.ts | 641 ++++++++++++++++ packages/@ember/array/index.ts | 644 +--------------- packages/@ember/array/proxy.ts | 10 +- .../@ember/controller/controller-internal.ts | 91 +++ packages/@ember/controller/index.ts | 91 +-- packages/@ember/engine/index.ts | 5 +- packages/@ember/engine/instance.ts | 11 +- packages/@ember/enumerable/-internal.ts | 14 + packages/@ember/enumerable/index.ts | 18 +- .../@ember/enumerable/mutable-internal.ts | 15 + packages/@ember/enumerable/mutable.ts | 19 +- packages/@ember/object/core.ts | 19 +- packages/@ember/object/evented.ts | 14 +- packages/@ember/object/index.ts | 6 +- .../lib/computed/reduce_computed_macros.ts | 9 +- packages/@ember/object/mixin-internal.ts | 710 ++++++++++++++++++ packages/@ember/object/mixin.ts | 680 +---------------- packages/@ember/object/observable-internal.ts | 194 +++++ packages/@ember/object/observable.ts | 152 +--- packages/@ember/object/promise-proxy-mixin.ts | 5 +- packages/@ember/object/proxy.ts | 5 +- packages/@ember/object/tests/evented_test.js | 16 +- packages/@ember/object/tests/events_test.js | 68 +- .../object/tests/mixin/deprecation_test.js | 4 +- .../@ember/object/tests/observable_test.js | 5 +- packages/@ember/routing/route.ts | 6 +- .../ember/tests/service_injection_test.js | 2 +- rollup.config.mjs | 13 +- 50 files changed, 2499 insertions(+), 1919 deletions(-) create mode 100644 packages/@ember/-internals/runtime/lib/mixins/-proxy-internal.ts create mode 100644 packages/@ember/-internals/runtime/lib/mixins/action_handler-internal.ts create mode 100644 packages/@ember/-internals/runtime/lib/mixins/container_proxy-internal.ts create mode 100644 packages/@ember/-internals/runtime/lib/mixins/registry_proxy-internal.ts create mode 100644 packages/@ember/-internals/runtime/lib/mixins/target_action_support-internal.ts delete mode 100644 packages/@ember/-internals/utils/lib/internal-mixin-create.ts create mode 100644 packages/@ember/-internals/views/lib/mixins/action_support-internal.ts create mode 100644 packages/@ember/array/index-internal.ts create mode 100644 packages/@ember/controller/controller-internal.ts create mode 100644 packages/@ember/enumerable/-internal.ts create mode 100644 packages/@ember/enumerable/mutable-internal.ts create mode 100644 packages/@ember/object/mixin-internal.ts create mode 100644 packages/@ember/object/observable-internal.ts diff --git a/packages/@ember/-internals/deprecations/index.ts b/packages/@ember/-internals/deprecations/index.ts index b8a83a7054d..fa1e86489b1 100644 --- a/packages/@ember/-internals/deprecations/index.ts +++ b/packages/@ember/-internals/deprecations/index.ts @@ -168,6 +168,83 @@ export const DEPRECATIONS = { until: '8.0.0', url: 'https://deprecations.emberjs.com/id/deprecate-ember-utils', }), + DEPRECATE_OBSERVABLE_MIXIN: deprecation({ + id: 'deprecate-observable-mixin', + for: 'ember-source', + since: { available: '7.5.0' }, + until: '8.0.0', + url: 'https://deprecations.emberjs.com/id/deprecate-observable-mixin', + }), + DEPRECATE_ENUMERABLE_MIXIN: deprecation({ + id: 'deprecate-enumerable-mixin', + for: 'ember-source', + since: { available: '7.5.0' }, + until: '7.9.0', + url: 'https://deprecations.emberjs.com/id/deprecate-enumerable-mixin', + }), + DEPRECATE_MUTABLE_ENUMERABLE_MIXIN: deprecation({ + id: 'deprecate-mutable-enumerable-mixin', + for: 'ember-source', + since: { available: '7.5.0' }, + until: '7.9.0', + url: 'https://deprecations.emberjs.com/id/deprecate-mutable-enumerable-mixin', + }), + DEPRECATE_EMBER_ARRAY_MIXIN: deprecation({ + id: 'deprecate-ember-array-mixin', + for: 'ember-source', + since: { available: '7.5.0' }, + until: '8.0.0', + url: 'https://deprecations.emberjs.com/id/deprecate-ember-array-mixin', + }), + DEPRECATE_MUTABLE_ARRAY_MIXIN: deprecation({ + id: 'deprecate-mutable-array-mixin', + for: 'ember-source', + since: { available: '7.5.0' }, + until: '8.0.0', + url: 'https://deprecations.emberjs.com/id/deprecate-mutable-array-mixin', + }), + DEPRECATE_NATIVE_ARRAY_MIXIN: deprecation({ + id: 'deprecate-native-array-mixin', + for: 'ember-source', + since: { available: '7.5.0' }, + until: '8.0.0', + url: 'https://deprecations.emberjs.com/id/deprecate-native-array-mixin', + }), + DEPRECATE_CONTROLLER_MIXIN: deprecation({ + id: 'deprecate-controller-mixin', + for: 'ember-source', + since: { available: '7.5.0' }, + until: '7.9.0', + url: 'https://deprecations.emberjs.com/id/deprecate-controller-mixin', + }), + DEPRECATE_ACTION_HANDLER_MIXIN: deprecation({ + id: 'deprecate-action-handler-mixin', + for: 'ember-source', + since: { available: '7.5.0' }, + until: '7.9.0', + url: 'https://deprecations.emberjs.com/id/deprecate-action-handler-mixin', + }), + DEPRECATE_PROXY_MIXIN: deprecation({ + id: 'deprecate-proxy-mixin', + for: 'ember-source', + since: { available: '7.5.0' }, + until: '7.9.0', + url: 'https://deprecations.emberjs.com/id/deprecate-proxy-mixin', + }), + DEPRECATE_CONTAINER_PROXY_MIXIN: deprecation({ + id: 'deprecate-container-proxy-mixin', + for: 'ember-source', + since: { available: '7.5.0' }, + until: '7.9.0', + url: 'https://deprecations.emberjs.com/id/deprecate-container-proxy-mixin', + }), + DEPRECATE_REGISTRY_PROXY_MIXIN: deprecation({ + id: 'deprecate-registry-proxy-mixin', + for: 'ember-source', + since: { available: '7.5.0' }, + until: '7.9.0', + url: 'https://deprecations.emberjs.com/id/deprecate-registry-proxy-mixin', + }), }; export function deprecateUntil(message: string, deprecation: DeprecationObject) { diff --git a/packages/@ember/-internals/glimmer/lib/component.ts b/packages/@ember/-internals/glimmer/lib/component.ts index 5c0735cd780..490a6595227 100644 --- a/packages/@ember/-internals/glimmer/lib/component.ts +++ b/packages/@ember/-internals/glimmer/lib/component.ts @@ -4,9 +4,11 @@ import { get } from '@ember/-internals/metal/lib/property_get'; import { PROPERTY_DID_CHANGE } from '@ember/-internals/metal/lib/property_events'; import type { PropertyDidChange } from '@ember/-internals/metal/lib/property_events'; import { getOwner } from '@ember/-internals/owner'; -import TargetActionSupport from '@ember/-internals/runtime/lib/mixins/target_action_support'; +import type TargetActionSupport from '@ember/-internals/runtime/lib/mixins/target_action_support'; +import InternalTargetActionSupport from '@ember/-internals/runtime/lib/mixins/target_action_support-internal'; import type ViewStates from '@ember/-internals/views/lib/views/states'; -import ActionSupport from '@ember/-internals/views/lib/mixins/action_support'; +import type ActionSupport from '@ember/-internals/views/lib/mixins/action_support'; +import InternalActionSupport from '@ember/-internals/views/lib/mixins/action_support-internal'; import { addChildView, getChildViews, @@ -803,8 +805,8 @@ interface Component class Component extends CoreView.extend( - TargetActionSupport, - ActionSupport, + InternalTargetActionSupport, + InternalActionSupport, { // These need to be overridable via extend/create but should still // have a default. Defining them here is the best way to achieve that. diff --git a/packages/@ember/-internals/glimmer/lib/helpers/each-in.ts b/packages/@ember/-internals/glimmer/lib/helpers/each-in.ts index 745442038d9..ed10579f268 100644 --- a/packages/@ember/-internals/glimmer/lib/helpers/each-in.ts +++ b/packages/@ember/-internals/glimmer/lib/helpers/each-in.ts @@ -2,7 +2,7 @@ @module @ember/helper */ import { tagForObject } from '@ember/-internals/metal/lib/tags'; -import { contentFor as _contentFor } from '@ember/-internals/runtime/lib/mixins/-proxy'; +import { contentFor as _contentFor } from '@ember/-internals/runtime/lib/mixins/-proxy-internal'; import { isProxy } from '@ember/-internals/utils/lib/is_proxy'; import { assert } from '@ember/debug'; import type { CapturedArguments } from '@glimmer/interfaces'; diff --git a/packages/@ember/-internals/glimmer/tests/integration/helpers/tracked-test.js b/packages/@ember/-internals/glimmer/tests/integration/helpers/tracked-test.js index a2a12be40cc..2aed389bace 100644 --- a/packages/@ember/-internals/glimmer/tests/integration/helpers/tracked-test.js +++ b/packages/@ember/-internals/glimmer/tests/integration/helpers/tracked-test.js @@ -1,7 +1,7 @@ import EmberObject from '@ember/object'; import { A } from '@ember/array'; -import MutableArray from '@ember/array/mutable'; import { tracked, notifyPropertyChange } from '@ember/-internals/metal'; +import { InternalMutableArray as MutableArray } from '@ember/array/index-internal'; import Service, { service } from '@ember/service'; import { moduleFor, RenderingTestCase, runTask } from 'internal-test-helpers'; import { precompileTemplate } from '@ember/template-compilation'; diff --git a/packages/@ember/-internals/runtime/lib/mixins/-proxy-internal.ts b/packages/@ember/-internals/runtime/lib/mixins/-proxy-internal.ts new file mode 100644 index 00000000000..27592c3f89f --- /dev/null +++ b/packages/@ember/-internals/runtime/lib/mixins/-proxy-internal.ts @@ -0,0 +1,123 @@ +/** +@module ember +*/ + +import { meta } from '@ember/-internals/meta/lib/meta'; +import { InternalMixin } from '@ember/object/mixin-internal'; +import { get } from '@ember/-internals/metal/lib/property_get'; +import { set } from '@ember/-internals/metal/lib/property_set'; +import { defineProperty } from '@ember/-internals/metal/lib/properties'; +import { tagForObject, tagForProperty } from '@ember/-internals/metal/lib/tags'; +import computed from '@ember/-internals/metal/lib/computed'; +import { setProxy, isProxy } from '@ember/-internals/utils/lib/is_proxy'; +import { setupMandatorySetter } from '@ember/-internals/utils/lib/mandatory-setter'; +import { isObject } from '@ember/-internals/utils/lib/spec'; +import { assert } from '@ember/debug'; +import { DEBUG } from '@glimmer/env'; +import { setCustomTagFor } from '@glimmer/manager/lib/util/args-proxy'; +import type { UpdatableTag, Tag } from '@glimmer/interfaces'; +import { combine, UPDATE_TAG as updateTag } from '@glimmer/validator/lib/validators'; +import { tagFor, tagMetaFor } from '@glimmer/validator/lib/meta'; + +export function contentFor(proxy: { content: T | null }): T | null { + let content = get(proxy, 'content'); + // SAFETY: Ideally we'd assert instead of casting, but @glimmer/validator doesn't give us + // sufficient public types for this. Previously this code was .js and worked correctly so + // hopefully this is sufficiently reliable. + updateTag(tagForObject(proxy) as UpdatableTag, tagForObject(content)); + return content; +} + +function customTagForProxy(proxy: object, key: string, addMandatorySetter?: boolean): Tag { + assert('Expected a proxy', isProxy(proxy)); + + let meta = tagMetaFor(proxy); + let tag = tagFor(proxy, key, meta); + + if (DEBUG) { + // TODO: Replace this with something more first class for tracking tags in DEBUG + // SAFETY: This is not an officially supported property but setting shouldn't cause issues. + (tag as any)._propertyKey = key; + } + + if (key in proxy) { + if (DEBUG && addMandatorySetter) { + assert('[BUG] setupMandatorySetter should be set when debugging', setupMandatorySetter); + setupMandatorySetter(tag, proxy, key); + } + + return tag; + } else { + let tags: Tag[] = [tag, tagFor(proxy, 'content', meta)]; + + let content = contentFor(proxy); + + if (isObject(content)) { + tags.push(tagForProperty(content, key, addMandatorySetter)); + } + + return combine(tags); + } +} + +/** + The internal counterpart to the public `ProxyMixin`. Ember's own internals + apply this so that they do not trigger the deprecation that the public + mixin emits. The public API documentation lives on the public copy. + + @internal +*/ +const InternalProxyMixin = /*@__PURE__*/ InternalMixin.create({ + /** + The object whose properties will be forwarded. + + @property content + @type {unknown} + @default null + */ + content: null, + + init() { + this._super(...arguments); + setProxy(this); + tagForObject(this); + setCustomTagFor(this, customTagForProxy); + }, + + willDestroy() { + this.set('content', null); + this._super(...arguments); + }, + + isTruthy: computed('content', function () { + return Boolean(get(this, 'content')); + }), + + unknownProperty(key: string) { + let content = contentFor(this); + return content ? get(content, key) : undefined; + }, + + setUnknownProperty(key: string, value: unknown) { + let m = meta(this); + + if (m.isInitializing() || m.isPrototypeMeta(this)) { + // if marked as prototype or object is initializing then just + // defineProperty rather than delegate + defineProperty(this, key, null, value); + return value; + } + + let content = contentFor(this); + + assert( + `Cannot delegate set('${key}', ${value}) to the 'content' property of object proxy ${this}: its 'content' is undefined.`, + content + ); + + // SAFETY: We don't actually guarantee that this is an object, so this isn't necessarily safe :( + return set(content as object, key, value); + }, +}); + +export default InternalProxyMixin; diff --git a/packages/@ember/-internals/runtime/lib/mixins/-proxy.ts b/packages/@ember/-internals/runtime/lib/mixins/-proxy.ts index cf3e23234e6..2e43d3c85a3 100644 --- a/packages/@ember/-internals/runtime/lib/mixins/-proxy.ts +++ b/packages/@ember/-internals/runtime/lib/mixins/-proxy.ts @@ -2,69 +2,17 @@ @module ember */ -import { meta } from '@ember/-internals/meta/lib/meta'; -import Mixin from '@ember/object/mixin'; -import { INTERNAL_MIXIN_CREATE } from '@ember/-internals/utils/lib/internal-mixin-create'; -import { get } from '@ember/-internals/metal/lib/property_get'; -import { set } from '@ember/-internals/metal/lib/property_set'; -import { defineProperty } from '@ember/-internals/metal/lib/properties'; -import { tagForObject, tagForProperty } from '@ember/-internals/metal/lib/tags'; -import computed from '@ember/-internals/metal/lib/computed'; -import { setProxy, isProxy } from '@ember/-internals/utils/lib/is_proxy'; -import { setupMandatorySetter } from '@ember/-internals/utils/lib/mandatory-setter'; -import { isObject } from '@ember/-internals/utils/lib/spec'; -import { assert } from '@ember/debug'; -import { DEBUG } from '@glimmer/env'; -import { setCustomTagFor } from '@glimmer/manager/lib/util/args-proxy'; -import type { UpdatableTag, Tag } from '@glimmer/interfaces'; -import { combine, UPDATE_TAG as updateTag } from '@glimmer/validator/lib/validators'; -import { tagFor, tagMetaFor } from '@glimmer/validator/lib/meta'; +import { DeprecatedMixin } from '@ember/object/mixin-internal'; +import { deprecateUntil, DEPRECATIONS } from '@ember/-internals/deprecations'; +import InternalProxyMixin from '@ember/-internals/runtime/lib/mixins/-proxy-internal'; -export function contentFor(proxy: ProxyMixin): T | null { - let content = get(proxy, 'content'); - // SAFETY: Ideally we'd assert instead of casting, but @glimmer/validator doesn't give us - // sufficient public types for this. Previously this code was .js and worked correctly so - // hopefully this is sufficiently reliable. - updateTag(tagForObject(proxy) as UpdatableTag, tagForObject(content)); - return content; -} - -function customTagForProxy(proxy: object, key: string, addMandatorySetter?: boolean): Tag { - assert('Expected a proxy', isProxy(proxy)); - - let meta = tagMetaFor(proxy); - let tag = tagFor(proxy, key, meta); - - if (DEBUG) { - // TODO: Replace this with something more first class for tracking tags in DEBUG - // SAFETY: This is not an officially supported property but setting shouldn't cause issues. - (tag as any)._propertyKey = key; - } - - if (key in proxy) { - if (DEBUG && addMandatorySetter) { - assert('[BUG] setupMandatorySetter should be set when debugging', setupMandatorySetter); - setupMandatorySetter(tag, proxy, key); - } - - return tag; - } else { - let tags: Tag[] = [tag, tagFor(proxy, 'content', meta)]; - - let content = contentFor(proxy); - - if (isObject(content)) { - tags.push(tagForProperty(content, key, addMandatorySetter)); - } - - return combine(tags); - } -} +export { contentFor } from '@ember/-internals/runtime/lib/mixins/-proxy-internal'; /** `ProxyMixin` forwards all properties not defined by the proxy itself to a proxied `content` object. See ObjectProxy for more details. + @deprecated @class ProxyMixin @namespace Ember @private @@ -91,7 +39,7 @@ interface ProxyMixin { setUnknownProperty(key: string, value: V): V; } -const ProxyMixin = /*@__PURE__*/ Mixin[INTERNAL_MIXIN_CREATE]({ +const ProxyMixin = /*@__PURE__*/ DeprecatedMixin.create(InternalProxyMixin, { /** The object whose properties will be forwarded. @@ -100,48 +48,13 @@ const ProxyMixin = /*@__PURE__*/ Mixin[INTERNAL_MIXIN_CREATE]({ @default null @public */ - content: null, init() { this._super(...arguments); - setProxy(this); - tagForObject(this); - setCustomTagFor(this, customTagForProxy); - }, - - willDestroy() { - this.set('content', null); - this._super(...arguments); - }, - - isTruthy: computed('content', function () { - return Boolean(get(this, 'content')); - }), - - unknownProperty(key: string) { - let content = contentFor(this); - return content ? get(content, key) : undefined; - }, - - setUnknownProperty(key: string, value: unknown) { - let m = meta(this); - - if (m.isInitializing() || m.isPrototypeMeta(this)) { - // if marked as prototype or object is initializing then just - // defineProperty rather than delegate - defineProperty(this, key, null, value); - return value; - } - - let content = contentFor(this); - - assert( - `Cannot delegate set('${key}', ${value}) to the 'content' property of object proxy ${this}: its 'content' is undefined.`, - content + deprecateUntil( + 'The `ProxyMixin` mixin is deprecated. Access the underlying object directly instead.', + DEPRECATIONS.DEPRECATE_PROXY_MIXIN ); - - // SAFETY: We don't actually guarantee that this is an object, so this isn't necessarily safe :( - return set(content as object, key, value); }, }); diff --git a/packages/@ember/-internals/runtime/lib/mixins/action_handler-internal.ts b/packages/@ember/-internals/runtime/lib/mixins/action_handler-internal.ts new file mode 100644 index 00000000000..38cb4c78409 --- /dev/null +++ b/packages/@ember/-internals/runtime/lib/mixins/action_handler-internal.ts @@ -0,0 +1,51 @@ +/** +@module ember +*/ + +import { InternalMixin } from '@ember/object/mixin-internal'; +import { get } from '@ember/-internals/metal/lib/property_get'; +import { assert } from '@ember/debug'; +import { deprecateUntil, DEPRECATIONS } from '@ember/-internals/deprecations'; + +/** + The internal counterpart to the public `ActionHandler` mixin. Ember's own + internals apply this so that they do not trigger the deprecation that the + public mixin emits. The public API documentation lives on the public copy. + + @internal +*/ +interface InternalActionHandler { + actions?: Record unknown>; + send(actionName: string, ...args: unknown[]): void; +} +const InternalActionHandler = InternalMixin.create({ + mergedProperties: ['actions'], + + send(actionName: string, ...args: any[]) { + deprecateUntil( + `Calling \`.send()\` on ${this} is deprecated. Invoke the corresponding method directly.`, + DEPRECATIONS.DEPRECATE_TARGET_ACTION_SUPPORT + ); + assert( + `Attempted to call .send() with the action '${actionName}' on the destroyed object '${this}'.`, + !this.isDestroying && !this.isDestroyed + ); + if (this.actions && this.actions[actionName]) { + let shouldBubble = this.actions[actionName].apply(this, args) === true; + if (!shouldBubble) { + return; + } + } + + let target = get(this, 'target'); + if (target) { + assert( + `The \`target\` for ${this} (${target}) does not have a \`send\` method`, + typeof target.send === 'function' + ); + target.send(...arguments); + } + }, +}); + +export default InternalActionHandler; diff --git a/packages/@ember/-internals/runtime/lib/mixins/action_handler.ts b/packages/@ember/-internals/runtime/lib/mixins/action_handler.ts index 0270c1e32df..f3e4b13e8bf 100644 --- a/packages/@ember/-internals/runtime/lib/mixins/action_handler.ts +++ b/packages/@ember/-internals/runtime/lib/mixins/action_handler.ts @@ -2,11 +2,9 @@ @module ember */ -import Mixin from '@ember/object/mixin'; -import { INTERNAL_MIXIN_CREATE } from '@ember/-internals/utils/lib/internal-mixin-create'; -import { get } from '@ember/-internals/metal/lib/property_get'; -import { assert } from '@ember/debug'; +import { DeprecatedMixin } from '@ember/object/mixin-internal'; import { deprecateUntil, DEPRECATIONS } from '@ember/-internals/deprecations'; +import InternalActionHandler from '@ember/-internals/runtime/lib/mixins/action_handler-internal'; /** `ActionHandler` is available on some familiar classes including @@ -15,16 +13,21 @@ import { deprecateUntil, DEPRECATIONS } from '@ember/-internals/deprecations'; and `Route` and available to the above classes through inheritance.) + @deprecated @class ActionHandler @namespace Ember @private */ -interface ActionHandler { - actions?: Record unknown>; - send(actionName: string, ...args: unknown[]): void; -} -const ActionHandler = Mixin[INTERNAL_MIXIN_CREATE]({ - mergedProperties: ['actions'], +// eslint-disable-next-line @typescript-eslint/no-empty-object-type +interface ActionHandler extends InternalActionHandler {} +const ActionHandler = DeprecatedMixin.create(InternalActionHandler, { + init() { + this._super(...arguments); + deprecateUntil( + 'The `ActionHandler` mixin is deprecated. Use the `@action` decorator and direct method calls instead.', + DEPRECATIONS.DEPRECATE_ACTION_HANDLER_MIXIN + ); + }, /** The collection of functions, keyed by name, available on this @@ -205,31 +208,6 @@ const ActionHandler = Mixin[INTERNAL_MIXIN_CREATE]({ @param {*} context a context to send with the action @public */ - send(actionName: string, ...args: any[]) { - deprecateUntil( - `Calling \`.send()\` on ${this} is deprecated. Invoke the corresponding method directly.`, - DEPRECATIONS.DEPRECATE_TARGET_ACTION_SUPPORT - ); - assert( - `Attempted to call .send() with the action '${actionName}' on the destroyed object '${this}'.`, - !this.isDestroying && !this.isDestroyed - ); - if (this.actions && this.actions[actionName]) { - let shouldBubble = this.actions[actionName].apply(this, args) === true; - if (!shouldBubble) { - return; - } - } - - let target = get(this, 'target'); - if (target) { - assert( - `The \`target\` for ${this} (${target}) does not have a \`send\` method`, - typeof target.send === 'function' - ); - target.send(...arguments); - } - }, }); export default ActionHandler; diff --git a/packages/@ember/-internals/runtime/lib/mixins/comparable.ts b/packages/@ember/-internals/runtime/lib/mixins/comparable.ts index 20047cfcb00..ea1662d67ae 100644 --- a/packages/@ember/-internals/runtime/lib/mixins/comparable.ts +++ b/packages/@ember/-internals/runtime/lib/mixins/comparable.ts @@ -1,5 +1,4 @@ -import Mixin from '@ember/object/mixin'; -import { INTERNAL_MIXIN_CREATE } from '@ember/-internals/utils/lib/internal-mixin-create'; +import { InternalMixin } from '@ember/object/mixin-internal'; import { deprecateUntil, DEPRECATIONS } from '@ember/-internals/deprecations'; /** @@ -20,7 +19,7 @@ import { deprecateUntil, DEPRECATIONS } from '@ember/-internals/deprecations'; interface Comparable { compare: ((a: unknown, b: unknown) => -1 | 0 | 1) | null; } -const Comparable = Mixin[INTERNAL_MIXIN_CREATE]({ +const Comparable = InternalMixin.create({ /** __Required.__ You must implement this method to apply this mixin. diff --git a/packages/@ember/-internals/runtime/lib/mixins/container_proxy-internal.ts b/packages/@ember/-internals/runtime/lib/mixins/container_proxy-internal.ts new file mode 100644 index 00000000000..eb3c3313e1c --- /dev/null +++ b/packages/@ember/-internals/runtime/lib/mixins/container_proxy-internal.ts @@ -0,0 +1,43 @@ +import { schedule, join } from '@ember/runloop'; +/** +@module ember +*/ +import { InternalMixin } from '@ember/object/mixin-internal'; + +/** + The internal counterpart to the public `ContainerProxyMixin`. Ember's own + internals apply this so that they do not trigger the deprecation that the + public mixin emits. The public API documentation lives on the public copy. + + @internal +*/ +const InternalContainerProxyMixin = InternalMixin.create({ + __container__: null, + + ownerInjection() { + return this.__container__.ownerInjection(); + }, + + lookup(fullName: string, options: object) { + return this.__container__.lookup(fullName, options); + }, + + destroy() { + let container = this.__container__; + + if (container) { + join(() => { + container.destroy(); + schedule('destroy', container, 'finalizeDestroy'); + }); + } + + this._super(); + }, + + factoryFor(fullName: string) { + return this.__container__.factoryFor(fullName); + }, +}); + +export default InternalContainerProxyMixin; diff --git a/packages/@ember/-internals/runtime/lib/mixins/container_proxy.ts b/packages/@ember/-internals/runtime/lib/mixins/container_proxy.ts index 34ca8f41fba..f67d30d8ebd 100644 --- a/packages/@ember/-internals/runtime/lib/mixins/container_proxy.ts +++ b/packages/@ember/-internals/runtime/lib/mixins/container_proxy.ts @@ -1,11 +1,11 @@ -import { schedule, join } from '@ember/runloop'; /** @module ember */ import type Container from '@ember/-internals/container/lib/container'; -import Mixin from '@ember/object/mixin'; -import { INTERNAL_MIXIN_CREATE } from '@ember/-internals/utils/lib/internal-mixin-create'; +import { DeprecatedMixin } from '@ember/object/mixin-internal'; +import { deprecateUntil, DEPRECATIONS } from '@ember/-internals/deprecations'; import type { ContainerProxy } from '@ember/-internals/owner'; +import InternalContainerProxyMixin from '@ember/-internals/runtime/lib/mixins/container_proxy-internal'; // This is defined as a separate interface so that it can be used in the definition of // `Owner` without also including the `__container__` property. @@ -17,43 +17,26 @@ import type { ContainerProxy } from '@ember/-internals/owner'; @class ContainerProxyMixin @extends ContainerProxy @private + @deprecated Use the owner API from `@ember/owner` instead. */ interface ContainerProxyMixin extends ContainerProxy { /** @internal */ __container__: Container; } -const ContainerProxyMixin = Mixin[INTERNAL_MIXIN_CREATE]({ +const ContainerProxyMixin = DeprecatedMixin.create(InternalContainerProxyMixin, { /** The container stores state. @private @property {Ember.Container} __container__ */ - __container__: null, - ownerInjection() { - return this.__container__.ownerInjection(); - }, - - lookup(fullName: string, options: object) { - return this.__container__.lookup(fullName, options); - }, - - destroy() { - let container = this.__container__; - - if (container) { - join(() => { - container.destroy(); - schedule('destroy', container, 'finalizeDestroy'); - }); - } - - this._super(); - }, - - factoryFor(fullName: string) { - return this.__container__.factoryFor(fullName); + init() { + this._super(...arguments); + deprecateUntil( + 'The `ContainerProxyMixin` mixin is deprecated. Use the owner API from `@ember/owner` instead.', + DEPRECATIONS.DEPRECATE_CONTAINER_PROXY_MIXIN + ); }, }); diff --git a/packages/@ember/-internals/runtime/lib/mixins/registry_proxy-internal.ts b/packages/@ember/-internals/runtime/lib/mixins/registry_proxy-internal.ts new file mode 100644 index 00000000000..9c198c1ceb3 --- /dev/null +++ b/packages/@ember/-internals/runtime/lib/mixins/registry_proxy-internal.ts @@ -0,0 +1,56 @@ +/** +@module ember +*/ + +import type Registry from '@ember/-internals/container/lib/registry'; +import type { AnyFn } from '@ember/-internals/utility-types'; + +import { assert } from '@ember/debug'; +import { InternalMixin } from '@ember/object/mixin-internal'; + +/** + The internal counterpart to the public `RegistryProxyMixin`. Ember's own + internals apply this so that they do not trigger the deprecation that the + public mixin emits. The public API documentation lives on the public copy. + + @internal +*/ +const InternalRegistryProxyMixin = InternalMixin.create({ + __registry__: null, + + resolveRegistration(fullName: string) { + assert('fullName must be a proper full name', this.__registry__.isValidFullName(fullName)); + return this.__registry__.resolve(fullName); + }, + + register: registryAlias('register'), + unregister: registryAlias('unregister'), + hasRegistration: registryAlias('has'), + registeredOption: registryAlias('getOption'), + registerOptions: registryAlias('options'), + registeredOptions: registryAlias('getOptions'), + registerOptionsForType: registryAlias('optionsForType'), + registeredOptionsForType: registryAlias('getOptionsForType'), +}); + +type AliasMethods = + | 'register' + | 'unregister' + | 'has' + | 'getOption' + | 'options' + | 'getOptions' + | 'optionsForType' + | 'getOptionsForType'; + +function registryAlias(name: N) { + return function (this: { __registry__: Registry }, ...args: Parameters) { + // We need this cast because `Parameters` is deferred so that it is not + // possible for TS to see it will always produce the right type. However, + // since `AnyFn` has a rest type, it is allowed. See discussion on [this + // issue](https://github.com/microsoft/TypeScript/issues/47615). + return (this.__registry__[name] as AnyFn)(...args); + }; +} + +export default InternalRegistryProxyMixin; diff --git a/packages/@ember/-internals/runtime/lib/mixins/registry_proxy.ts b/packages/@ember/-internals/runtime/lib/mixins/registry_proxy.ts index 9081904d5d7..ab2936d320a 100644 --- a/packages/@ember/-internals/runtime/lib/mixins/registry_proxy.ts +++ b/packages/@ember/-internals/runtime/lib/mixins/registry_proxy.ts @@ -4,11 +4,10 @@ import type Registry from '@ember/-internals/container/lib/registry'; import type { RegistryProxy } from '@ember/-internals/owner'; -import type { AnyFn } from '@ember/-internals/utility-types'; -import { assert } from '@ember/debug'; -import Mixin from '@ember/object/mixin'; -import { INTERNAL_MIXIN_CREATE } from '@ember/-internals/utils/lib/internal-mixin-create'; +import { DeprecatedMixin } from '@ember/object/mixin-internal'; +import { deprecateUntil, DEPRECATIONS } from '@ember/-internals/deprecations'; +import InternalRegistryProxyMixin from '@ember/-internals/runtime/lib/mixins/registry_proxy-internal'; /** RegistryProxyMixin is used to provide public access to specific @@ -17,47 +16,20 @@ import { INTERNAL_MIXIN_CREATE } from '@ember/-internals/utils/lib/internal-mixi @class RegistryProxyMixin @extends RegistryProxy @private + @deprecated Use the owner API from `@ember/owner` instead. */ interface RegistryProxyMixin extends RegistryProxy { /** @internal */ __registry__: Registry; } -const RegistryProxyMixin = Mixin[INTERNAL_MIXIN_CREATE]({ - __registry__: null, - - resolveRegistration(fullName: string) { - assert('fullName must be a proper full name', this.__registry__.isValidFullName(fullName)); - return this.__registry__.resolve(fullName); +const RegistryProxyMixin = DeprecatedMixin.create(InternalRegistryProxyMixin, { + init() { + this._super(...arguments); + deprecateUntil( + 'The `RegistryProxyMixin` mixin is deprecated. Use the owner API from `@ember/owner` instead.', + DEPRECATIONS.DEPRECATE_REGISTRY_PROXY_MIXIN + ); }, - - register: registryAlias('register'), - unregister: registryAlias('unregister'), - hasRegistration: registryAlias('has'), - registeredOption: registryAlias('getOption'), - registerOptions: registryAlias('options'), - registeredOptions: registryAlias('getOptions'), - registerOptionsForType: registryAlias('optionsForType'), - registeredOptionsForType: registryAlias('getOptionsForType'), }); -type AliasMethods = - | 'register' - | 'unregister' - | 'has' - | 'getOption' - | 'options' - | 'getOptions' - | 'optionsForType' - | 'getOptionsForType'; - -function registryAlias(name: N) { - return function (this: RegistryProxyMixin, ...args: Parameters) { - // We need this cast because `Parameters` is deferred so that it is not - // possible for TS to see it will always produce the right type. However, - // since `AnyFn` has a rest type, it is allowed. See discussion on [this - // issue](https://github.com/microsoft/TypeScript/issues/47615). - return (this.__registry__[name] as AnyFn)(...args); - }; -} - export default RegistryProxyMixin; diff --git a/packages/@ember/-internals/runtime/lib/mixins/target_action_support-internal.ts b/packages/@ember/-internals/runtime/lib/mixins/target_action_support-internal.ts new file mode 100644 index 00000000000..780c91659ab --- /dev/null +++ b/packages/@ember/-internals/runtime/lib/mixins/target_action_support-internal.ts @@ -0,0 +1,112 @@ +/** +@module ember +*/ + +import { context } from '@ember/-internals/environment/lib/context'; +import { get } from '@ember/-internals/metal/lib/property_get'; +import computed from '@ember/-internals/metal/lib/computed'; +import { InternalMixin } from '@ember/object/mixin-internal'; +import { assert } from '@ember/debug'; +import { deprecateUntil, DEPRECATIONS } from '@ember/-internals/deprecations'; +import { DEBUG } from '@glimmer/env'; + +/** + The internal counterpart to the public `TargetActionSupport` mixin. Ember's + own internals apply this so that they do not trigger the deprecation that + the public mixin emits. The public API documentation lives on the public + copy. + + @internal +*/ +const InternalTargetActionSupport = InternalMixin.create({ + target: null, + action: null, + actionContext: null, + + actionContextObject: computed('actionContext', function () { + let actionContext = get(this, 'actionContext'); + + if (typeof actionContext === 'string') { + let value = get(this, actionContext); + if (value === undefined) { + value = get(context.lookup, actionContext); + } + return value; + } else { + return actionContext; + } + }), + + triggerAction(opts: { action?: string; target?: unknown; actionContext?: unknown } = {}) { + deprecateUntil( + `Calling \`triggerAction\` on ${this} is deprecated. Invoke the target method directly.`, + DEPRECATIONS.DEPRECATE_TARGET_ACTION_SUPPORT + ); + + let { action, target, actionContext } = opts; + action = action || get(this, 'action'); + target = target || getTarget(this); + + if (actionContext === undefined) { + actionContext = get(this, 'actionContextObject') || this; + } + + let context = Array.isArray(actionContext) ? actionContext : [actionContext]; + + if (target && action) { + let ret; + + if (isSendable(target)) { + ret = target.send(action, ...context); + } else { + assert( + `The action '${action}' did not exist on ${target}`, + typeof (target as any)[action] === 'function' + ); + ret = (target as any)[action](...context); + } + + if (ret !== false) { + return true; + } + } + + return false; + }, +}); + +interface Sendable { + send(action: string, ...context: unknown[]): unknown; +} + +function isSendable(obj: unknown): obj is Sendable { + return obj != null && typeof obj === 'object' && typeof (obj as Sendable).send === 'function'; +} + +function getTarget(instance: { _target?: unknown }) { + let target = get(instance, 'target'); + if (target) { + if (typeof target === 'string') { + let value = get(instance, target); + if (value === undefined) { + value = get(context.lookup, target); + } + + return value; + } else { + return target; + } + } + + if (instance._target) { + return instance._target; + } + + return null; +} + +if (DEBUG) { + Object.seal(InternalTargetActionSupport); +} + +export default InternalTargetActionSupport; diff --git a/packages/@ember/-internals/runtime/lib/mixins/target_action_support.ts b/packages/@ember/-internals/runtime/lib/mixins/target_action_support.ts index 6682e3b80e3..858a668f559 100644 --- a/packages/@ember/-internals/runtime/lib/mixins/target_action_support.ts +++ b/packages/@ember/-internals/runtime/lib/mixins/target_action_support.ts @@ -2,14 +2,10 @@ @module ember */ -import { context } from '@ember/-internals/environment/lib/context'; -import { get } from '@ember/-internals/metal/lib/property_get'; -import computed from '@ember/-internals/metal/lib/computed'; -import Mixin from '@ember/object/mixin'; -import { INTERNAL_MIXIN_CREATE } from '@ember/-internals/utils/lib/internal-mixin-create'; -import { assert } from '@ember/debug'; +import { DeprecatedMixin } from '@ember/object/mixin-internal'; import { deprecateUntil, DEPRECATIONS } from '@ember/-internals/deprecations'; import { DEBUG } from '@glimmer/env'; +import InternalTargetActionSupport from '@ember/-internals/runtime/lib/mixins/target_action_support-internal'; /** `TargetActionSupport` is a mixin that can be included in a class @@ -22,36 +18,13 @@ doing more complex event handling in Components. @namespace Ember @extends Mixin @private +@deprecated Use a direct method call or closure action instead. */ interface TargetActionSupport { target: unknown; action: string | null; actionContext: unknown; actionContextObject: unknown; - triggerAction(opts?: object): unknown; - - /** @internal */ - _target?: unknown; -} -const TargetActionSupport = Mixin[INTERNAL_MIXIN_CREATE]({ - target: null, - action: null, - actionContext: null, - - actionContextObject: computed('actionContext', function () { - let actionContext = get(this, 'actionContext'); - - if (typeof actionContext === 'string') { - let value = get(this, actionContext); - if (value === undefined) { - value = get(context.lookup, actionContext); - } - return value; - } else { - return actionContext; - } - }), - /** The following is private and vestigial. Send an `action` with an `actionContext` to a `target`. The action, actionContext @@ -111,74 +84,21 @@ const TargetActionSupport = Mixin[INTERNAL_MIXIN_CREATE]({ @return {Boolean} true if the action was sent successfully and did not return false @private */ - triggerAction(opts: { action?: string; target?: unknown; actionContext?: unknown } = {}) { + triggerAction(opts?: object): unknown; + + /** @internal */ + _target?: unknown; +} +const TargetActionSupport = DeprecatedMixin.create(InternalTargetActionSupport, { + init() { + this._super(...arguments); deprecateUntil( - `Calling \`triggerAction\` on ${this} is deprecated. Invoke the target method directly.`, + 'The `TargetActionSupport` mixin is deprecated. Invoke the target method directly instead.', DEPRECATIONS.DEPRECATE_TARGET_ACTION_SUPPORT ); - - let { action, target, actionContext } = opts; - action = action || get(this, 'action'); - target = target || getTarget(this); - - if (actionContext === undefined) { - actionContext = get(this, 'actionContextObject') || this; - } - - let context = Array.isArray(actionContext) ? actionContext : [actionContext]; - - if (target && action) { - let ret; - - if (isSendable(target)) { - ret = target.send(action, ...context); - } else { - assert( - `The action '${action}' did not exist on ${target}`, - typeof (target as any)[action] === 'function' - ); - ret = (target as any)[action](...context); - } - - if (ret !== false) { - return true; - } - } - - return false; }, }); -interface Sendable { - send(action: string, ...context: unknown[]): unknown; -} - -function isSendable(obj: unknown): obj is Sendable { - return obj != null && typeof obj === 'object' && typeof (obj as Sendable).send === 'function'; -} - -function getTarget(instance: TargetActionSupport) { - let target = get(instance, 'target'); - if (target) { - if (typeof target === 'string') { - let value = get(instance, target); - if (value === undefined) { - value = get(context.lookup, target); - } - - return value; - } else { - return target; - } - } - - if (instance._target) { - return instance._target; - } - - return null; -} - if (DEBUG) { Object.seal(TargetActionSupport); } diff --git a/packages/@ember/-internals/runtime/tests/mixins/array_test.js b/packages/@ember/-internals/runtime/tests/mixins/array_test.js index bf05aa13c7d..3ec1baed666 100644 --- a/packages/@ember/-internals/runtime/tests/mixins/array_test.js +++ b/packages/@ember/-internals/runtime/tests/mixins/array_test.js @@ -7,7 +7,8 @@ import { arrayContentWillChange, } from '@ember/-internals/metal'; import EmberObject, { get, set, computed, observer as emberObserver } from '@ember/object'; -import EmberArray, { A as emberA } from '@ember/array'; +import { A as emberA } from '@ember/array'; +import { InternalEmberArray as EmberArray } from '@ember/array/index-internal'; import { moduleFor, AbstractTestCase, runLoopSettled } from 'internal-test-helpers'; /* diff --git a/packages/@ember/-internals/runtime/tests/mixins/container_proxy_test.js b/packages/@ember/-internals/runtime/tests/mixins/container_proxy_test.js index 1f9cbcfa175..445e4eceedc 100644 --- a/packages/@ember/-internals/runtime/tests/mixins/container_proxy_test.js +++ b/packages/@ember/-internals/runtime/tests/mixins/container_proxy_test.js @@ -1,6 +1,6 @@ import { getOwner } from '@ember/-internals/owner'; import { Container, Registry } from '@ember/-internals/container'; -import ContainerProxy from '../../lib/mixins/container_proxy'; +import ContainerProxy from '../../lib/mixins/container_proxy-internal'; import EmberObject from '@ember/object'; import { run, schedule } from '@ember/runloop'; import { moduleFor, AbstractTestCase } from 'internal-test-helpers'; diff --git a/packages/@ember/-internals/runtime/tests/mixins/target_action_support_test.js b/packages/@ember/-internals/runtime/tests/mixins/target_action_support_test.js index 13fae5111ed..e2b205a93db 100644 --- a/packages/@ember/-internals/runtime/tests/mixins/target_action_support_test.js +++ b/packages/@ember/-internals/runtime/tests/mixins/target_action_support_test.js @@ -1,6 +1,6 @@ import { context } from '@ember/-internals/environment'; import EmberObject from '@ember/object'; -import TargetActionSupport from '../../lib/mixins/target_action_support'; +import TargetActionSupport from '../../lib/mixins/target_action_support-internal'; import { expectDeprecation, moduleFor, AbstractTestCase, testUnless } from 'internal-test-helpers'; import { DEPRECATIONS } from '../../../deprecations'; diff --git a/packages/@ember/-internals/utils/lib/internal-mixin-create.ts b/packages/@ember/-internals/utils/lib/internal-mixin-create.ts deleted file mode 100644 index c01119d15aa..00000000000 --- a/packages/@ember/-internals/utils/lib/internal-mixin-create.ts +++ /dev/null @@ -1,11 +0,0 @@ -/** - Key for the internal Mixin constructor. - - Ember's own internals are built on mixins, so they need a way to construct - one without triggering the mixin deprecation that `Mixin.create` emits. This - is deliberately a Symbol so that it does not show up as a discoverable - property name and cannot be reached by name from application code. - - @private -*/ -export const INTERNAL_MIXIN_CREATE = Symbol('__internal__mixin__'); diff --git a/packages/@ember/-internals/views/lib/mixins/action_support-internal.ts b/packages/@ember/-internals/views/lib/mixins/action_support-internal.ts new file mode 100644 index 00000000000..6e22243b742 --- /dev/null +++ b/packages/@ember/-internals/views/lib/mixins/action_support-internal.ts @@ -0,0 +1,51 @@ +/** + @module ember +*/ +import { get } from '@ember/-internals/metal/lib/property_get'; +import { InternalMixin } from '@ember/object/mixin-internal'; +import inspect from '@ember/debug/lib/inspect'; +import { assert } from '@ember/debug'; +import { deprecateUntil, DEPRECATIONS } from '@ember/-internals/deprecations'; + +/** + The internal counterpart to the public `ActionSupport` mixin. Ember's own + internals apply this so that they do not trigger the deprecation that the + public mixin emits. The public API documentation lives on the public copy. + + @internal +*/ +const InternalActionSupport = InternalMixin.create({ + send(actionName: string, ...args: unknown[]) { + deprecateUntil( + `Calling \`.send()\` on ${this} is deprecated. Invoke the corresponding method directly.`, + DEPRECATIONS.DEPRECATE_TARGET_ACTION_SUPPORT + ); + + assert( + `Attempted to call .send() with the action '${actionName}' on the destroyed object '${this}'.`, + !this.isDestroying && !this.isDestroyed + ); + + let action = this.actions && this.actions[actionName]; + + if (action) { + let shouldBubble = action.apply(this, args) === true; + if (!shouldBubble) { + return; + } + } + + let target = get(this, 'target'); + if (target) { + assert( + `The \`target\` for ${this} (${target}) does not have a \`send\` method`, + typeof target.send === 'function' + ); + target.send(...arguments); + } else { + assert(`${inspect(this)} had no action handler for: ${actionName}`, action); + } + }, +}); + +export default InternalActionSupport; diff --git a/packages/@ember/-internals/views/lib/mixins/action_support.ts b/packages/@ember/-internals/views/lib/mixins/action_support.ts index 1db3b87fa79..27885c83e9c 100644 --- a/packages/@ember/-internals/views/lib/mixins/action_support.ts +++ b/packages/@ember/-internals/views/lib/mixins/action_support.ts @@ -1,52 +1,35 @@ /** @module ember */ -import { get } from '@ember/-internals/metal/lib/property_get'; -import Mixin from '@ember/object/mixin'; -import { INTERNAL_MIXIN_CREATE } from '@ember/-internals/utils/lib/internal-mixin-create'; -import inspect from '@ember/debug/lib/inspect'; -import { assert } from '@ember/debug'; +import { DeprecatedMixin } from '@ember/object/mixin-internal'; import { deprecateUntil, DEPRECATIONS } from '@ember/-internals/deprecations'; +import InternalActionSupport from '@ember/-internals/views/lib/mixins/action_support-internal'; /** @class ActionSupport @namespace Ember @private + @deprecated Invoke the corresponding method directly instead. */ interface ActionSupport { + /** + Calls an action passed to a component. + + @method send + @deprecated Invoke the corresponding method directly instead. + @param {String} actionName The action to trigger + @param {*} args Arguments to pass on with the action + @private + */ send(actionName: string, ...args: unknown[]): void; } -const ActionSupport = Mixin[INTERNAL_MIXIN_CREATE]({ - send(actionName: string, ...args: unknown[]) { +const ActionSupport = DeprecatedMixin.create(InternalActionSupport, { + init() { + this._super(...arguments); deprecateUntil( - `Calling \`.send()\` on ${this} is deprecated. Invoke the corresponding method directly.`, + 'The `ActionSupport` mixin is deprecated. Invoke the corresponding method directly instead.', DEPRECATIONS.DEPRECATE_TARGET_ACTION_SUPPORT ); - - assert( - `Attempted to call .send() with the action '${actionName}' on the destroyed object '${this}'.`, - !this.isDestroying && !this.isDestroyed - ); - - let action = this.actions && this.actions[actionName]; - - if (action) { - let shouldBubble = action.apply(this, args) === true; - if (!shouldBubble) { - return; - } - } - - let target = get(this, 'target'); - if (target) { - assert( - `The \`target\` for ${this} (${target}) does not have a \`send\` method`, - typeof target.send === 'function' - ); - target.send(...arguments); - } else { - assert(`${inspect(this)} had no action handler for: ${actionName}`, action); - } }, }); diff --git a/packages/@ember/-internals/views/lib/views/core_view.ts b/packages/@ember/-internals/views/lib/views/core_view.ts index c1a7d1c0a15..ca811fbd822 100644 --- a/packages/@ember/-internals/views/lib/views/core_view.ts +++ b/packages/@ember/-internals/views/lib/views/core_view.ts @@ -8,7 +8,7 @@ import { eventedOff, eventedHas, } from '@ember/-internals/metal/lib/evented-methods'; -import ActionHandler from '@ember/-internals/runtime/lib/mixins/action_handler'; +import InternalActionHandler from '@ember/-internals/runtime/lib/mixins/action_handler-internal'; import Evented from '@ember/object/evented'; import { FrameworkObject } from '@ember/object/-internals'; import type { ViewState } from './states'; @@ -31,8 +31,8 @@ import states from './states'; @private */ -interface CoreView extends ActionHandler, View {} -class CoreView extends FrameworkObject.extend(ActionHandler) { +interface CoreView extends InternalActionHandler, View {} +class CoreView extends FrameworkObject.extend(InternalActionHandler) { static { // The deprecated Evented mixin is no longer applied, but instances still // provide its methods, so `Evented.detect` must keep returning true. diff --git a/packages/@ember/array/index-internal.ts b/packages/@ember/array/index-internal.ts new file mode 100644 index 00000000000..2746d544132 --- /dev/null +++ b/packages/@ember/array/index-internal.ts @@ -0,0 +1,641 @@ +/** +@module @ember/array +*/ +import { objectAt } from '@ember/-internals/metal/lib/object-at'; +import { replaceInNativeArray, replace } from '@ember/-internals/metal/lib/array'; +import computed from '@ember/-internals/metal/lib/computed'; +import { + beginPropertyChanges, + endPropertyChanges, +} from '@ember/-internals/metal/lib/property_events'; +import { get } from '@ember/-internals/metal/lib/property_get'; +import { set } from '@ember/-internals/metal/lib/property_set'; +import { InternalMixin } from '@ember/object/mixin-internal'; +import { assert } from '@ember/debug'; +import InternalEnumerable from '@ember/enumerable/-internal'; +import InternalMutableEnumerable from '@ember/enumerable/mutable-internal'; +import sortCompare from './lib/sort-compare'; +import InternalObservable from '@ember/object/observable-internal'; +import type { ComputedPropertyCallback } from '@ember/-internals/metal/lib/computed'; +import { isEmberArray, setEmberArray } from '@ember/array/-internals'; +import isArray from './lib/is-array'; +import type EmberArray from '@ember/array'; +import type { MutableArray, NativeArray } from '@ember/array'; + +const EMPTY_ARRAY = Object.freeze([] as const); + +const identityFunction = (item: T) => item; + +export function uniqBy( + array: T[] | EmberArray, + keyOrFunc: string | ((item: T) => unknown) = identityFunction +): T[] | EmberArray { + assert(`first argument passed to \`uniqBy\` should be array`, isArray(array)); + + let ret = A(); + let seen = new Set(); + let getter = typeof keyOrFunc === 'function' ? keyOrFunc : (item: T) => get(item, keyOrFunc); + + array.forEach((item) => { + let val = getter(item); + if (!seen.has(val)) { + seen.add(val); + ret.push(item); + } + }); + + return ret; +} + +function iter(key: string): (item: T) => boolean; +function iter(key: string, value: unknown): (item: T) => boolean; +function iter(...args: [key: string] | [key: string, value: unknown]) { + let valueProvided = args.length === 2; + let [key, value] = args; + + return valueProvided + ? (item: T) => value === get(item, key) + : (item: T) => Boolean(get(item, key)); +} + +function findIndex( + array: EmberArray, + predicate: (item: T, index: number, arr: EmberArray) => unknown, + startAt: number +): number { + let len = array.length; + for (let index = startAt; index < len; index++) { + // SAFETY: Because we're checking the index this value should always be set. + let item = objectAt(array, index)!; + if (predicate(item, index, array)) { + return index; + } + } + return -1; +} + +function find( + array: EmberArray, + callback: (this: Target | null, item: T, index: number, arr: EmberArray) => unknown, + target: Target | null = null +) { + let predicate = callback.bind(target); + let index = findIndex(array, predicate, 0); + return index === -1 ? undefined : objectAt(array, index); +} + +function any( + array: EmberArray, + callback: (this: Target | null, item: T, index: number, arr: EmberArray) => unknown, + target: Target | null = null +) { + let predicate = callback.bind(target); + return findIndex(array, predicate, 0) !== -1; +} + +function every( + array: EmberArray, + callback: (this: Target | null | void, item: T, index: number, arr: EmberArray) => unknown, + target: Target | null = null +) { + let cb = callback.bind(target); + let predicate = (item: T, index: number, array: EmberArray) => !cb(item, index, array); + return findIndex(array, predicate, 0) === -1; +} + +function indexOf(array: EmberArray, val: T, startAt = 0, withNaNCheck: boolean) { + let len = array.length; + + if (startAt < 0) { + startAt += len; + } + + // SameValueZero comparison (NaN !== NaN) + let predicate = + withNaNCheck && val !== val ? (item: T) => item !== item : (item: T) => item === val; + return findIndex(array, predicate, startAt); +} + +export function removeAt>( + array: A, + index: number, + len?: number +): A { + assert(`\`removeAt\` index provided is out of range`, index > -1 && index < array.length); + replace(array, index, len ?? 1, EMPTY_ARRAY); + return array; +} + +function insertAt(array: MutableArray, index: number, item: T) { + assert(`\`insertAt\` index provided is out of range`, index > -1 && index <= array.length); + replace(array, index, 0, [item]); + return item; +} + +export { isArray }; + +/* + This allows us to define computed properties that are not enumerable. +*/ +function nonEnumerableComputed(callback: ComputedPropertyCallback) { + let property = computed(callback); + property.enumerable = false; + return property; +} + +function mapBy(this: EmberArray, key: string) { + return this.map((next) => get(next, key)); +} + +// .......................................................... +// ARRAY +// + +/** + The internal counterparts to the public `EmberArray`, `MutableArray` and + `NativeArray` mixins. Ember's own internals apply these so that they do not + trigger the deprecations the public mixins emit. The public API + documentation lives on the public copies in `@ember/array`. + + @internal +*/ +const InternalEmberArray = InternalMixin.create(InternalEnumerable, { + init() { + this._super(...arguments); + setEmberArray(this); + }, + + objectsAt(indexes: number[]) { + return indexes.map((idx) => objectAt(this, idx)); + }, + + '[]': nonEnumerableComputed({ + get() { + return this; + }, + set(_key, value) { + this.replace(0, this.length, value); + return this; + }, + }), + + firstObject: nonEnumerableComputed(function () { + return objectAt(this, 0); + }).readOnly(), + + lastObject: nonEnumerableComputed(function () { + return objectAt(this, this.length - 1); + }).readOnly(), + + // Add any extra methods to EmberArray that are native to the built-in Array. + slice(beginIndex = 0, endIndex?: number) { + let ret = A(); + let length = this.length; + + if (beginIndex < 0) { + beginIndex = length + beginIndex; + } + + let validatedEndIndex: number; + if (endIndex === undefined || endIndex > length) { + validatedEndIndex = length; + } else if (endIndex < 0) { + validatedEndIndex = length + endIndex; + } else { + validatedEndIndex = endIndex; + } + + while (beginIndex < validatedEndIndex) { + ret[ret.length] = objectAt(this, beginIndex++); + } + + return ret; + }, + + indexOf(object: T, startAt?: number) { + return indexOf(this, object, startAt, false); + }, + + lastIndexOf(object: T, startAt?: number) { + let len = this.length; + + if (startAt === undefined || startAt >= len) { + startAt = len - 1; + } + + if (startAt < 0) { + startAt += len; + } + + for (let idx = startAt; idx >= 0; idx--) { + if (objectAt(this, idx) === object) { + return idx; + } + } + + return -1; + }, + + forEach(callback: (item: T, index: number, arr: EmberArray) => void, target = null) { + assert('`forEach` expects a function as first argument.', typeof callback === 'function'); + + let length = this.length; + + for (let index = 0; index < length; index++) { + let item = this.objectAt(index); + callback.call(target, item, index, this); + } + + return this; + }, + + getEach: mapBy, + + setEach(key: string, value: unknown) { + return this.forEach((item: object) => set(item, key, value)); + }, + + map( + this: EmberArray, + callback: (item: T, index: number, arr: EmberArray) => unknown, + target = null + ) { + assert('`map` expects a function as first argument.', typeof callback === 'function'); + + let ret = A(); + + this.forEach((x, idx, i) => (ret[idx] = callback.call(target, x, idx, i))); + + return ret; + }, + + mapBy, + + filter( + this: EmberArray, + callback: (item: T, index: number, arr: EmberArray) => unknown, + target = null + ) { + assert('`filter` expects a function as first argument.', typeof callback === 'function'); + + let ret = A(); + + this.forEach((x, idx, i) => { + if (callback.call(target, x, idx, i)) { + ret.push(x); + } + }); + + return ret; + }, + + reject( + this: EmberArray, + callback: (item: T, index: number, arr: EmberArray) => unknown, + target = null + ) { + assert('`reject` expects a function as first argument.', typeof callback === 'function'); + return this.filter(function () { + // @ts-expect-error TS doesn't like us using arguments like this + return !callback.apply(target, arguments); + }); + }, + + filterBy() { + // @ts-expect-error TS doesn't like the ...arguments spread here. + return this.filter(iter(...arguments)); + }, + + rejectBy() { + // @ts-expect-error TS doesn't like the ...arguments spread here. + return this.reject(iter(...arguments)); + }, + + find(callback: (item: T, index: number, arr: EmberArray) => unknown, target = null) { + assert('`find` expects a function as first argument.', typeof callback === 'function'); + return find(this, callback, target); + }, + + findBy() { + // @ts-expect-error TS doesn't like the ...arguments spread here. + let callback = iter(...arguments); + return find(this, callback); + }, + + every(callback: (item: T, index: number, arr: EmberArray) => unknown, target = null) { + assert('`every` expects a function as first argument.', typeof callback === 'function'); + return every(this, callback, target); + }, + + isEvery() { + // @ts-expect-error TS doesn't like the ...arguments spread here. + let callback = iter(...arguments); + return every(this, callback); + }, + + any(callback: (item: T, index: number, arr: EmberArray) => unknown, target = null) { + assert('`any` expects a function as first argument.', typeof callback === 'function'); + return any(this, callback, target); + }, + + isAny() { + // @ts-expect-error TS doesn't like us using arguments like this + let callback = iter(...arguments); + return any(this, callback); + }, + + // FIXME: When called without initialValue, behavior does not match native behavior + reduce( + this: EmberArray, + callback: (summation: V, current: T, index: number, arr: EmberArray) => V, + initialValue?: V + ) { + assert('`reduce` expects a function as first argument.', typeof callback === 'function'); + + let hasInitialValue = arguments.length > 1; + let ret: any = initialValue; + let startIndex = 0; + + if (!hasInitialValue) { + if (this.length === 0) { + throw new TypeError('Reduce of empty array with no initial value'); + } + ret = this.objectAt(0); + startIndex = 1; + } + + for (let i = startIndex; i < this.length; i++) { + let item = this.objectAt(i) as T; + ret = callback(ret, item, i, this); + } + + return ret; + }, + + invoke(this: EmberArray, methodName: string, ...args: unknown[]) { + let ret = A(); + + // SAFETY: This is not entirely safe and the code will not work with Ember proxies + this.forEach((item: T) => ret.push((item as any)[methodName]?.(...args))); + + return ret; + }, + + toArray(this: EmberArray) { + return this.map((item: T) => item); + }, + + compact(this: EmberArray) { + return this.filter((value: T) => value != null); + }, + + includes(this: EmberArray, object: T, startAt?: number) { + return indexOf(this, object, startAt, true) !== -1; + }, + + sortBy(this: EmberArray) { + let sortKeys = arguments; + + return this.toArray().sort((a: T, b: T) => { + for (let i = 0; i < sortKeys.length; i++) { + let key = sortKeys[i]; + let propA = get(a, key); + let propB = get(b, key); + // return 1 or -1 else continue to the next sortKey + let compareValue = sortCompare(propA, propB); + + if (compareValue) { + return compareValue; + } + } + return 0; + }); + }, + + uniq() { + return uniqBy(this); + }, + + uniqBy(key: string) { + return uniqBy(this, key); + }, + + without(this: EmberArray, value: T) { + if (!this.includes(value)) { + return this; // nothing to do + } + + // SameValueZero comparison (NaN !== NaN) + let predicate = value === value ? (item: T) => item !== value : (item: T) => item === item; + return this.filter(predicate); + }, +}); + +const InternalMutableArray = InternalMixin.create(InternalEmberArray, InternalMutableEnumerable, { + clear() { + let len = this.length; + if (len === 0) { + return this; + } + + this.replace(0, len, EMPTY_ARRAY); + return this; + }, + + insertAt(idx: number, object: unknown) { + insertAt(this, idx, object); + return this; + }, + + removeAt(start: number, len?: number) { + return removeAt(this, start, len); + }, + + pushObject(this: MutableArray, obj: T) { + return insertAt(this, this.length, obj); + }, + + pushObjects(this: MutableArray, objects: T[]) { + this.replace(this.length, 0, objects); + return this; + }, + + popObject() { + let len = this.length; + if (len === 0) { + return null; + } + + let ret = objectAt(this, len - 1); + this.removeAt(len - 1, 1); + return ret; + }, + + shiftObject() { + if (this.length === 0) { + return null; + } + + let ret = objectAt(this, 0); + this.removeAt(0); + return ret; + }, + + unshiftObject(this: MutableArray, obj: T) { + return insertAt(this, 0, obj); + }, + + unshiftObjects(this: MutableArray, objects: T[]) { + this.replace(0, 0, objects); + return this; + }, + + reverseObjects() { + let len = this.length; + if (len === 0) { + return this; + } + + let objects = this.toArray().reverse(); + this.replace(0, len, objects); + return this; + }, + + setObjects(this: MutableArray, objects: T[]) { + if (objects.length === 0) { + return this.clear(); + } + + let len = this.length; + this.replace(0, len, objects); + return this; + }, + + removeObject(this: MutableArray, obj: T) { + let loc = this.length || 0; + while (--loc >= 0) { + let curObject = objectAt(this, loc); + + if (curObject === obj) { + this.removeAt(loc); + } + } + return this; + }, + + removeObjects(this: MutableArray, objects: T[]) { + beginPropertyChanges(); + for (let i = objects.length - 1; i >= 0; i--) { + // SAFETY: Due to the loop structure we know this will always exist. + this.removeObject(objects[i]!); + } + endPropertyChanges(); + return this; + }, + + addObject(this: MutableArray, obj: T) { + let included = this.includes(obj); + + if (!included) { + this.pushObject(obj); + } + + return this; + }, + + addObjects(this: MutableArray, objects: T[]) { + beginPropertyChanges(); + objects.forEach((obj) => this.addObject(obj)); + endPropertyChanges(); + return this; + }, +}); + +/** + Creates an `NativeArray` from an Array-like object. + Does not modify the original object's contents. + + This exists primarily for historic reasons and should not be used + in new code. Prefer native [Array](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array) + or [trackedArray](/ember/release/functions/@ember%2Freactive%2Fcollections/trackedArray). + + Example + + ```app/components/my-component.js + import Component from '@ember/component'; + import { A } from '@ember/array'; + + export default Component.extend({ + tagName: 'ul', + classNames: ['pagination'], + + init() { + this._super(...arguments); + + if (!this.get('content')) { + this.set('content', A()); + this.set('otherContent', A([1,2,3])); + } + } + }); + ``` + + @method A + @static + @for @ember/array + @return {Ember.NativeArray} +*/ + +// Add Ember.Array to Array.prototype. Remove methods with native +// implementations and supply some more optimized versions of generic methods +// because they are so common. +/** +@module ember +*/ + +let InternalNativeArray = InternalMixin.create(InternalMutableArray, InternalObservable, { + objectAt(idx: number) { + return this[idx]; + }, + + // primitive for array support. + replace(start: number, deleteCount: number, items = EMPTY_ARRAY) { + assert('The third argument to replace needs to be an array.', Array.isArray(items)); + + replaceInNativeArray(this, start, deleteCount, items); + + return this; + }, +}); + +// Remove any methods implemented natively so we don't override them +const ignore = ['length']; +InternalNativeArray.keys().forEach((methodName) => { + // SAFETY: It's safe to read unknown properties from an object + if ((Array.prototype as any)[methodName]) { + ignore.push(methodName); + } +}); + +InternalNativeArray = InternalNativeArray.without(...ignore); + +let A: (arr?: Array) => NativeArray; + +A = function (this: unknown, arr?: Array) { + assert( + 'You cannot create an Ember Array with `new A()`, please update to calling A as a function: `A()`', + !(this instanceof A) + ); + + if (isEmberArray(arr)) { + // SAFETY: If it's a true native array and it is also an EmberArray then it should be an Ember NativeArray + return arr as unknown as NativeArray; + } else { + // SAFETY: This will return an NativeArray but TS can't infer that. + return InternalNativeArray.apply(arr ?? []) as NativeArray; + } +}; + +export { A, InternalEmberArray, InternalMutableArray, InternalNativeArray }; + +export default InternalEmberArray; diff --git a/packages/@ember/array/index.ts b/packages/@ember/array/index.ts index b7ab885ebc6..882e71fe473 100644 --- a/packages/@ember/array/index.ts +++ b/packages/@ember/array/index.ts @@ -1,159 +1,25 @@ /** @module @ember/array */ -import { objectAt } from '@ember/-internals/metal/lib/object-at'; -import { replaceInNativeArray, replace } from '@ember/-internals/metal/lib/array'; -import computed from '@ember/-internals/metal/lib/computed'; -import { - beginPropertyChanges, - endPropertyChanges, -} from '@ember/-internals/metal/lib/property_events'; -import { get } from '@ember/-internals/metal/lib/property_get'; -import { set } from '@ember/-internals/metal/lib/property_set'; -import Mixin from '@ember/object/mixin'; -import { INTERNAL_MIXIN_CREATE } from '@ember/-internals/utils/lib/internal-mixin-create'; -import { assert } from '@ember/debug'; -import Enumerable from '@ember/enumerable'; -import MutableEnumerable from '@ember/enumerable/mutable'; -import sortCompare from './lib/sort-compare'; -import Observable from '@ember/object/observable'; +import { DeprecatedMixin } from '@ember/object/mixin-internal'; +import type Enumerable from '@ember/enumerable'; +import type MutableEnumerable from '@ember/enumerable/mutable'; +import type Observable from '@ember/object/observable'; import type { MethodNamesOf, MethodParams, MethodReturns } from '@ember/-internals/utility-types'; -import type { ComputedPropertyCallback } from '@ember/-internals/metal/lib/computed'; -import { isEmberArray, setEmberArray } from '@ember/array/-internals'; import isArray from './lib/is-array'; +import { deprecateUntil, DEPRECATIONS } from '@ember/-internals/deprecations'; +import InternalEmberArrayMixin, { + A, + InternalMutableArray as InternalMutableArrayMixin, + InternalNativeArray as InternalNativeArrayMixin, +} from '@ember/array/index-internal'; export { default as makeArray } from './make'; - -export type EmberArrayLike = EmberArray | NativeArray; - -const EMPTY_ARRAY = Object.freeze([] as const); - -const identityFunction = (item: T) => item; - -export function uniqBy( - array: T[] | EmberArray, - keyOrFunc: string | ((item: T) => unknown) = identityFunction -): T[] | EmberArray { - assert(`first argument passed to \`uniqBy\` should be array`, isArray(array)); - - let ret = A(); - let seen = new Set(); - let getter = typeof keyOrFunc === 'function' ? keyOrFunc : (item: T) => get(item, keyOrFunc); - - array.forEach((item) => { - let val = getter(item); - if (!seen.has(val)) { - seen.add(val); - ret.push(item); - } - }); - - return ret; -} - -function iter(key: string): (item: T) => boolean; -function iter(key: string, value: unknown): (item: T) => boolean; -function iter(...args: [key: string] | [key: string, value: unknown]) { - let valueProvided = args.length === 2; - let [key, value] = args; - - return valueProvided - ? (item: T) => value === get(item, key) - : (item: T) => Boolean(get(item, key)); -} - -function findIndex( - array: EmberArray, - predicate: (item: T, index: number, arr: EmberArray) => unknown, - startAt: number -): number { - let len = array.length; - for (let index = startAt; index < len; index++) { - // SAFETY: Because we're checking the index this value should always be set. - let item = objectAt(array, index)!; - if (predicate(item, index, array)) { - return index; - } - } - return -1; -} - -function find( - array: EmberArray, - callback: (this: Target | null, item: T, index: number, arr: EmberArray) => unknown, - target: Target | null = null -) { - let predicate = callback.bind(target); - let index = findIndex(array, predicate, 0); - return index === -1 ? undefined : objectAt(array, index); -} - -function any( - array: EmberArray, - callback: (this: Target | null, item: T, index: number, arr: EmberArray) => unknown, - target: Target | null = null -) { - let predicate = callback.bind(target); - return findIndex(array, predicate, 0) !== -1; -} - -function every( - array: EmberArray, - callback: (this: Target | null | void, item: T, index: number, arr: EmberArray) => unknown, - target: Target | null = null -) { - let cb = callback.bind(target); - let predicate = (item: T, index: number, array: EmberArray) => !cb(item, index, array); - return findIndex(array, predicate, 0) === -1; -} - -function indexOf(array: EmberArray, val: T, startAt = 0, withNaNCheck: boolean) { - let len = array.length; - - if (startAt < 0) { - startAt += len; - } - - // SameValueZero comparison (NaN !== NaN) - let predicate = - withNaNCheck && val !== val ? (item: T) => item !== item : (item: T) => item === val; - return findIndex(array, predicate, startAt); -} - -export function removeAt>( - array: A, - index: number, - len?: number -): A { - assert(`\`removeAt\` index provided is out of range`, index > -1 && index < array.length); - replace(array, index, len ?? 1, EMPTY_ARRAY); - return array; -} - -function insertAt(array: MutableArray, index: number, item: T) { - assert(`\`insertAt\` index provided is out of range`, index > -1 && index <= array.length); - replace(array, index, 0, [item]); - return item; -} - +export { uniqBy, removeAt } from '@ember/array/index-internal'; export { isArray }; -/* - This allows us to define computed properties that are not enumerable. -*/ -function nonEnumerableComputed(callback: ComputedPropertyCallback) { - let property = computed(callback); - property.enumerable = false; - return property; -} - -function mapBy(this: EmberArray, key: string) { - return this.map((next) => get(next, key)); -} +export type EmberArrayLike = EmberArray | NativeArray; -// .......................................................... -// ARRAY -// /** This mixin implements Observer-friendly Array-like behavior. It is not a concrete implementation, but it can be used up by other classes that want @@ -1140,275 +1006,13 @@ interface EmberArray extends Enumerable { */ without(value: T): NativeArray; } -const EmberArray = Mixin[INTERNAL_MIXIN_CREATE](Enumerable, { +const EmberArray = DeprecatedMixin.create(InternalEmberArrayMixin, { init() { this._super(...arguments); - setEmberArray(this); - }, - - objectsAt(indexes: number[]) { - return indexes.map((idx) => objectAt(this, idx)); - }, - - '[]': nonEnumerableComputed({ - get() { - return this; - }, - set(_key, value) { - this.replace(0, this.length, value); - return this; - }, - }), - - firstObject: nonEnumerableComputed(function () { - return objectAt(this, 0); - }).readOnly(), - - lastObject: nonEnumerableComputed(function () { - return objectAt(this, this.length - 1); - }).readOnly(), - - // Add any extra methods to EmberArray that are native to the built-in Array. - slice(beginIndex = 0, endIndex?: number) { - let ret = A(); - let length = this.length; - - if (beginIndex < 0) { - beginIndex = length + beginIndex; - } - - let validatedEndIndex: number; - if (endIndex === undefined || endIndex > length) { - validatedEndIndex = length; - } else if (endIndex < 0) { - validatedEndIndex = length + endIndex; - } else { - validatedEndIndex = endIndex; - } - - while (beginIndex < validatedEndIndex) { - ret[ret.length] = objectAt(this, beginIndex++); - } - - return ret; - }, - - indexOf(object: T, startAt?: number) { - return indexOf(this, object, startAt, false); - }, - - lastIndexOf(object: T, startAt?: number) { - let len = this.length; - - if (startAt === undefined || startAt >= len) { - startAt = len - 1; - } - - if (startAt < 0) { - startAt += len; - } - - for (let idx = startAt; idx >= 0; idx--) { - if (objectAt(this, idx) === object) { - return idx; - } - } - - return -1; - }, - - forEach(callback: (item: T, index: number, arr: EmberArray) => void, target = null) { - assert('`forEach` expects a function as first argument.', typeof callback === 'function'); - - let length = this.length; - - for (let index = 0; index < length; index++) { - let item = this.objectAt(index); - callback.call(target, item, index, this); - } - - return this; - }, - - getEach: mapBy, - - setEach(key: string, value: unknown) { - return this.forEach((item: object) => set(item, key, value)); - }, - - map( - this: EmberArray, - callback: (item: T, index: number, arr: EmberArray) => unknown, - target = null - ) { - assert('`map` expects a function as first argument.', typeof callback === 'function'); - - let ret = A(); - - this.forEach((x, idx, i) => (ret[idx] = callback.call(target, x, idx, i))); - - return ret; - }, - - mapBy, - - filter( - this: EmberArray, - callback: (item: T, index: number, arr: EmberArray) => unknown, - target = null - ) { - assert('`filter` expects a function as first argument.', typeof callback === 'function'); - - let ret = A(); - - this.forEach((x, idx, i) => { - if (callback.call(target, x, idx, i)) { - ret.push(x); - } - }); - - return ret; - }, - - reject( - this: EmberArray, - callback: (item: T, index: number, arr: EmberArray) => unknown, - target = null - ) { - assert('`reject` expects a function as first argument.', typeof callback === 'function'); - return this.filter(function () { - // @ts-expect-error TS doesn't like us using arguments like this - return !callback.apply(target, arguments); - }); - }, - - filterBy() { - // @ts-expect-error TS doesn't like the ...arguments spread here. - return this.filter(iter(...arguments)); - }, - - rejectBy() { - // @ts-expect-error TS doesn't like the ...arguments spread here. - return this.reject(iter(...arguments)); - }, - - find(callback: (item: T, index: number, arr: EmberArray) => unknown, target = null) { - assert('`find` expects a function as first argument.', typeof callback === 'function'); - return find(this, callback, target); - }, - - findBy() { - // @ts-expect-error TS doesn't like the ...arguments spread here. - let callback = iter(...arguments); - return find(this, callback); - }, - - every(callback: (item: T, index: number, arr: EmberArray) => unknown, target = null) { - assert('`every` expects a function as first argument.', typeof callback === 'function'); - return every(this, callback, target); - }, - - isEvery() { - // @ts-expect-error TS doesn't like the ...arguments spread here. - let callback = iter(...arguments); - return every(this, callback); - }, - - any(callback: (item: T, index: number, arr: EmberArray) => unknown, target = null) { - assert('`any` expects a function as first argument.', typeof callback === 'function'); - return any(this, callback, target); - }, - - isAny() { - // @ts-expect-error TS doesn't like us using arguments like this - let callback = iter(...arguments); - return any(this, callback); - }, - - // FIXME: When called without initialValue, behavior does not match native behavior - reduce( - this: EmberArray, - callback: (summation: V, current: T, index: number, arr: EmberArray) => V, - initialValue?: V - ) { - assert('`reduce` expects a function as first argument.', typeof callback === 'function'); - - let hasInitialValue = arguments.length > 1; - let ret: any = initialValue; - let startIndex = 0; - - if (!hasInitialValue) { - if (this.length === 0) { - throw new TypeError('Reduce of empty array with no initial value'); - } - ret = this.objectAt(0); - startIndex = 1; - } - - for (let i = startIndex; i < this.length; i++) { - let item = this.objectAt(i) as T; - ret = callback(ret, item, i, this); - } - - return ret; - }, - - invoke(this: EmberArray, methodName: string, ...args: unknown[]) { - let ret = A(); - - // SAFETY: This is not entirely safe and the code will not work with Ember proxies - this.forEach((item: T) => ret.push((item as any)[methodName]?.(...args))); - - return ret; - }, - - toArray(this: EmberArray) { - return this.map((item: T) => item); - }, - - compact(this: EmberArray) { - return this.filter((value: T) => value != null); - }, - - includes(this: EmberArray, object: T, startAt?: number) { - return indexOf(this, object, startAt, true) !== -1; - }, - - sortBy(this: EmberArray) { - let sortKeys = arguments; - - return this.toArray().sort((a: T, b: T) => { - for (let i = 0; i < sortKeys.length; i++) { - let key = sortKeys[i]; - let propA = get(a, key); - let propB = get(b, key); - // return 1 or -1 else continue to the next sortKey - let compareValue = sortCompare(propA, propB); - - if (compareValue) { - return compareValue; - } - } - return 0; - }); - }, - - uniq() { - return uniqBy(this); - }, - - uniqBy(key: string) { - return uniqBy(this, key); - }, - - without(this: EmberArray, value: T) { - if (!this.includes(value)) { - return this; // nothing to do - } - - // SameValueZero comparison (NaN !== NaN) - let predicate = value === value ? (item: T) => item !== value : (item: T) => item === item; - return this.filter(predicate); + deprecateUntil( + 'The `EmberArray` mixin is deprecated. Use native arrays and array methods instead.', + DEPRECATIONS.DEPRECATE_EMBER_ARRAY_MIXIN + ); }, }); @@ -1688,169 +1292,16 @@ interface MutableArray extends EmberArray, MutableEnumerable { */ addObjects(objects: T[]): this; } -const MutableArray = Mixin[INTERNAL_MIXIN_CREATE](EmberArray, MutableEnumerable, { - clear() { - let len = this.length; - if (len === 0) { - return this; - } - - this.replace(0, len, EMPTY_ARRAY); - return this; - }, - - insertAt(idx: number, object: unknown) { - insertAt(this, idx, object); - return this; - }, - - removeAt(start: number, len?: number) { - return removeAt(this, start, len); - }, - - pushObject(this: MutableArray, obj: T) { - return insertAt(this, this.length, obj); - }, - - pushObjects(this: MutableArray, objects: T[]) { - this.replace(this.length, 0, objects); - return this; - }, - - popObject() { - let len = this.length; - if (len === 0) { - return null; - } - - let ret = objectAt(this, len - 1); - this.removeAt(len - 1, 1); - return ret; - }, - - shiftObject() { - if (this.length === 0) { - return null; - } - - let ret = objectAt(this, 0); - this.removeAt(0); - return ret; - }, - - unshiftObject(this: MutableArray, obj: T) { - return insertAt(this, 0, obj); - }, - - unshiftObjects(this: MutableArray, objects: T[]) { - this.replace(0, 0, objects); - return this; - }, - - reverseObjects() { - let len = this.length; - if (len === 0) { - return this; - } - - let objects = this.toArray().reverse(); - this.replace(0, len, objects); - return this; - }, - - setObjects(this: MutableArray, objects: T[]) { - if (objects.length === 0) { - return this.clear(); - } - - let len = this.length; - this.replace(0, len, objects); - return this; - }, - - removeObject(this: MutableArray, obj: T) { - let loc = this.length || 0; - while (--loc >= 0) { - let curObject = objectAt(this, loc); - - if (curObject === obj) { - this.removeAt(loc); - } - } - return this; - }, - - removeObjects(this: MutableArray, objects: T[]) { - beginPropertyChanges(); - for (let i = objects.length - 1; i >= 0; i--) { - // SAFETY: Due to the loop structure we know this will always exist. - this.removeObject(objects[i]!); - } - endPropertyChanges(); - return this; - }, - - addObject(this: MutableArray, obj: T) { - let included = this.includes(obj); - - if (!included) { - this.pushObject(obj); - } - - return this; - }, - - addObjects(this: MutableArray, objects: T[]) { - beginPropertyChanges(); - objects.forEach((obj) => this.addObject(obj)); - endPropertyChanges(); - return this; +const MutableArray = DeprecatedMixin.create(InternalMutableArrayMixin, { + init() { + this._super(...arguments); + deprecateUntil( + 'The `MutableArray` mixin is deprecated. Use native arrays and array methods instead.', + DEPRECATIONS.DEPRECATE_MUTABLE_ARRAY_MIXIN + ); }, }); -/** - Creates an `NativeArray` from an Array-like object. - Does not modify the original object's contents. - - This exists primarily for historic reasons and should not be used - in new code. Prefer native [Array](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array) - or [trackedArray](/ember/release/functions/@ember%2Freactive%2Fcollections/trackedArray). - - Example - - ```app/components/my-component.js - import Component from '@ember/component'; - import { A } from '@ember/array'; - - export default Component.extend({ - tagName: 'ul', - classNames: ['pagination'], - - init() { - this._super(...arguments); - - if (!this.get('content')) { - this.set('content', A()); - this.set('otherContent', A([1,2,3])); - } - } - }); - ``` - - @method A - @static - @for @ember/array - @return {Ember.NativeArray} - @public -*/ - -// Add Ember.Array to Array.prototype. Remove methods with native -// implementations and supply some more optimized versions of generic methods -// because they are so common. -/** -@module ember -*/ - type AnyArray = EmberArray | Array | ReadonlyArray; /** @@ -2024,49 +1475,16 @@ interface MutableArrayWithoutNative extends Omit< */ interface NativeArray extends Array, Observable, MutableArrayWithoutNative {} -let NativeArray = Mixin[INTERNAL_MIXIN_CREATE](MutableArray, Observable, { - objectAt(idx: number) { - return this[idx]; - }, - - // primitive for array support. - replace(start: number, deleteCount: number, items = EMPTY_ARRAY) { - assert('The third argument to replace needs to be an array.', Array.isArray(items)); - - replaceInNativeArray(this, start, deleteCount, items); - - return this; +const NativeArray = DeprecatedMixin.create(InternalNativeArrayMixin, { + init() { + this._super(...arguments); + deprecateUntil( + 'The `NativeArray` mixin is deprecated. Use native arrays and array methods instead.', + DEPRECATIONS.DEPRECATE_NATIVE_ARRAY_MIXIN + ); }, }); -// Remove any methods implemented natively so we don't override them -const ignore = ['length']; -NativeArray.keys().forEach((methodName) => { - // SAFETY: It's safe to read unknown properties from an object - if ((Array.prototype as any)[methodName]) { - ignore.push(methodName); - } -}); - -NativeArray = NativeArray.without(...ignore); - -let A: (arr?: Array) => NativeArray; - -A = function (this: unknown, arr?: Array) { - assert( - 'You cannot create an Ember Array with `new A()`, please update to calling A as a function: `A()`', - !(this instanceof A) - ); - - if (isEmberArray(arr)) { - // SAFETY: If it's a true native array and it is also an EmberArray then it should be an Ember NativeArray - return arr as unknown as NativeArray; - } else { - // SAFETY: This will return an NativeArray but TS can't infer that. - return NativeArray.apply(arr ?? []) as NativeArray; - } -}; - export { A, NativeArray, MutableArray }; export default EmberArray; diff --git a/packages/@ember/array/proxy.ts b/packages/@ember/array/proxy.ts index 192c55215ee..7489ad40d8b 100644 --- a/packages/@ember/array/proxy.ts +++ b/packages/@ember/array/proxy.ts @@ -15,8 +15,10 @@ import { get } from '@ember/-internals/metal/lib/property_get'; import type { PropertyDidChange } from '@ember/-internals/metal/lib/property_events'; import { isObject } from '@ember/-internals/utils/lib/spec'; import EmberObject from '@ember/object'; -import EmberArray, { type NativeArray } from '@ember/array'; -import MutableArray from '@ember/array/mutable'; +import type EmberArray from '@ember/array'; +import type { NativeArray } from '@ember/array'; +import type MutableArray from '@ember/array/mutable'; +import { InternalEmberArray, InternalMutableArray } from '@ember/array/index-internal'; import { assert } from '@ember/debug'; import { DEPRECATIONS, deprecateUntil } from '@ember/-internals/deprecations'; import { setCustomTagFor } from '@glimmer/manager/lib/util/args-proxy'; @@ -330,7 +332,7 @@ class ArrayProxy extends EmberObject implements PropertyDidChange { assert( `ArrayProxy expects a native Array, EmberArray, or ArrayProxy, but you passed ${typeof arrangedContent}`, (function (arr: unknown): arr is EmberArray { - return Array.isArray(arr) || EmberArray.detect(arr); + return Array.isArray(arr) || InternalEmberArray.detect(arr); })(arrangedContent) ); assert( @@ -412,7 +414,7 @@ class ArrayProxy extends EmberObject implements PropertyDidChange { } } -ArrayProxy.reopen(MutableArray, { +ArrayProxy.reopen(InternalMutableArray, { arrangedContent: alias('content'), }); diff --git a/packages/@ember/controller/controller-internal.ts b/packages/@ember/controller/controller-internal.ts new file mode 100644 index 00000000000..ad50466ef13 --- /dev/null +++ b/packages/@ember/controller/controller-internal.ts @@ -0,0 +1,91 @@ +import { getOwner } from '@ember/-internals/owner'; // This is imported from -internals to avoid circularity +import computed from '@ember/-internals/metal/lib/computed'; +import { get } from '@ember/-internals/metal/lib/property_get'; +import { InternalMixin } from '@ember/object/mixin-internal'; +import InternalActionHandler from '@ember/-internals/runtime/lib/mixins/action_handler-internal'; + +const MODEL = Symbol('MODEL'); + +/** + The internal counterpart to the public `ControllerMixin`. Ember's own + internals apply this so that they do not trigger the deprecation that the + public mixin emits. The public API documentation lives on the public copy + in `@ember/controller`. + + @internal +*/ +const InternalControllerMixin = InternalMixin.create(InternalActionHandler, { + /* ducktype as a controller */ + isController: true, + + concatenatedProperties: ['queryParams'], + + target: null, + + store: null, + + init() { + this._super(...arguments); + let owner = getOwner(this); + if (owner) { + this.namespace = owner.lookup('application:main'); + this.target = owner.lookup('router:main'); + } + }, + + model: computed({ + get() { + return this[MODEL]; + }, + + set(_key, value) { + return (this[MODEL] = value); + }, + }), + + queryParams: null, + + /** + This property is updated to various different callback functions depending on + the current "state" of the backing route. It is used by + `Controller.prototype._qpChanged`. + + The methods backing each state can be found in the `Route.prototype._qp` computed + property return value (the `.states` property). The current values are listed here for + the sanity of future travelers: + + * `inactive` - This state is used when this controller instance is not part of the active + route hierarchy. Set in `Route.prototype._reset` (a `router.js` microlib hook) and + `Route.prototype.actions.finalizeQueryParamChange`. + * `active` - This state is used when this controller instance is part of the active + route hierarchy. Set in `Route.prototype.actions.finalizeQueryParamChange`. + * `allowOverrides` - This state is used in `Route.prototype.setup` (`route.js` microlib hook). + + @method _qpDelegate + @private + */ + _qpDelegate: null, // set by route + + /** + During `Route#setup` observers are created to invoke this method + when any of the query params declared in `Controller#queryParams` property + are changed. + + When invoked this method uses the currently active query param update delegate + (see `Controller.prototype._qpDelegate` for details) and invokes it with + the QP key/value being changed. + + @method _qpChanged + @private + */ + _qpChanged(controller: any, _prop: string) { + let dotIndex = _prop.indexOf('.[]'); + let prop = dotIndex === -1 ? _prop : _prop.slice(0, dotIndex); + + let delegate = controller._qpDelegate; + let value = get(controller, prop); + delegate(prop, value); + }, +}); + +export default InternalControllerMixin; diff --git a/packages/@ember/controller/index.ts b/packages/@ember/controller/index.ts index b83b4e6ae98..3ea52f076de 100644 --- a/packages/@ember/controller/index.ts +++ b/packages/@ember/controller/index.ts @@ -1,6 +1,3 @@ -import { getOwner } from '@ember/-internals/owner'; // This is imported from -internals to avoid circularity -import computed from '@ember/-internals/metal/lib/computed'; -import { get } from '@ember/-internals/metal/lib/property_get'; import { FrameworkObject } from '@ember/object/-internals'; import metalInject from '@ember/-internals/metal/lib/injected_property'; import type { InjectionDecorator } from '@ember/-internals/metal/lib/decorator-util'; @@ -8,10 +5,11 @@ import type { DecoratorPropertyDescriptor, ElementDescriptor, } from '@ember/-internals/metal/lib/decorator'; -import Mixin from '@ember/object/mixin'; -import { INTERNAL_MIXIN_CREATE } from '@ember/-internals/utils/lib/internal-mixin-create'; +import { DeprecatedMixin } from '@ember/object/mixin-internal'; import type { RouteArgs } from '@ember/routing/-internals'; -import ActionHandler from '@ember/-internals/runtime/lib/mixins/action_handler'; +import type InternalActionHandler from '@ember/-internals/runtime/lib/mixins/action_handler-internal'; +import { deprecateUntil, DEPRECATIONS } from '@ember/-internals/deprecations'; +import InternalControllerMixin from '@ember/controller/controller-internal'; import type { Transition } from 'router_js'; export type ControllerQueryParamType = 'boolean' | 'number' | 'array' | 'string'; @@ -20,8 +18,6 @@ export type ControllerQueryParam = | Record | Record; -const MODEL = Symbol('MODEL'); - /** @module @ember/controller */ @@ -31,8 +27,9 @@ const MODEL = Symbol('MODEL'); @namespace Ember @uses Ember.ActionHandler @private + @deprecated Extend `Controller` from `@ember/controller` instead. */ -interface ControllerMixin extends ActionHandler { +interface ControllerMixin extends InternalActionHandler { /** @internal */ _qpDelegate: unknown | null; @@ -235,77 +232,13 @@ interface ControllerMixin extends ActionHandler { */ replaceRoute(...args: RouteArgs): Transition; } -const ControllerMixin = Mixin[INTERNAL_MIXIN_CREATE](ActionHandler, { - /* ducktype as a controller */ - isController: true, - - concatenatedProperties: ['queryParams'], - - target: null, - - store: null, - +const ControllerMixin = DeprecatedMixin.create(InternalControllerMixin, { init() { this._super(...arguments); - let owner = getOwner(this); - if (owner) { - this.namespace = owner.lookup('application:main'); - this.target = owner.lookup('router:main'); - } - }, - - model: computed({ - get() { - return this[MODEL]; - }, - - set(_key, value) { - return (this[MODEL] = value); - }, - }), - - queryParams: null, - - /** - This property is updated to various different callback functions depending on - the current "state" of the backing route. It is used by - `Controller.prototype._qpChanged`. - - The methods backing each state can be found in the `Route.prototype._qp` computed - property return value (the `.states` property). The current values are listed here for - the sanity of future travelers: - - * `inactive` - This state is used when this controller instance is not part of the active - route hierarchy. Set in `Route.prototype._reset` (a `router.js` microlib hook) and - `Route.prototype.actions.finalizeQueryParamChange`. - * `active` - This state is used when this controller instance is part of the active - route hierarchy. Set in `Route.prototype.actions.finalizeQueryParamChange`. - * `allowOverrides` - This state is used in `Route.prototype.setup` (`route.js` microlib hook). - - @method _qpDelegate - @private - */ - _qpDelegate: null, // set by route - - /** - During `Route#setup` observers are created to invoke this method - when any of the query params declared in `Controller#queryParams` property - are changed. - - When invoked this method uses the currently active query param update delegate - (see `Controller.prototype._qpDelegate` for details) and invokes it with - the QP key/value being changed. - - @method _qpChanged - @private - */ - _qpChanged(controller: any, _prop: string) { - let dotIndex = _prop.indexOf('.[]'); - let prop = dotIndex === -1 ? _prop : _prop.slice(0, dotIndex); - - let delegate = controller._qpDelegate; - let value = get(controller, prop); - delegate(prop, value); + deprecateUntil( + 'The `ControllerMixin` mixin is deprecated. Extend `Controller` from `@ember/controller` instead.', + DEPRECATIONS.DEPRECATE_CONTROLLER_MIXIN + ); }, }); @@ -317,7 +250,7 @@ const ControllerMixin = Mixin[INTERNAL_MIXIN_CREATE](ActionHandler, { @public */ interface Controller<_T = unknown> extends FrameworkObject, ControllerMixin<_T> {} -class Controller<_T = unknown> extends FrameworkObject.extend(ControllerMixin) {} +class Controller<_T = unknown> extends FrameworkObject.extend(InternalControllerMixin) {} /** Creates a property that lazily looks up another controller in the container. diff --git a/packages/@ember/engine/index.ts b/packages/@ember/engine/index.ts index 478de9e9b13..5f244c81f01 100644 --- a/packages/@ember/engine/index.ts +++ b/packages/@ember/engine/index.ts @@ -14,7 +14,8 @@ import type { EngineInstanceOptions } from '@ember/engine/instance'; import EngineInstance from '@ember/engine/instance'; import { RoutingService } from '@ember/routing/-internals'; import { setupEngineRegistry } from '@ember/-internals/glimmer/lib/setup-registry'; -import RegistryProxyMixin from '@ember/-internals/runtime/lib/mixins/registry_proxy'; +import type RegistryProxyMixin from '@ember/-internals/runtime/lib/mixins/registry_proxy'; +import InternalRegistryProxyMixin from '@ember/-internals/runtime/lib/mixins/registry_proxy-internal'; import { StrictResolver } from './lib/strict-resolver'; function props(obj: object) { @@ -56,7 +57,7 @@ export interface Initializer { */ // eslint-disable-next-line @typescript-eslint/no-empty-object-type interface Engine extends RegistryProxyMixin {} -class Engine extends Namespace.extend(RegistryProxyMixin) { +class Engine extends Namespace.extend(InternalRegistryProxyMixin) { static initializers: Record> = Object.create(null); static instanceInitializers: Record> = Object.create(null); diff --git a/packages/@ember/engine/instance.ts b/packages/@ember/engine/instance.ts index b49d1280121..0bedeef2162 100644 --- a/packages/@ember/engine/instance.ts +++ b/packages/@ember/engine/instance.ts @@ -8,8 +8,10 @@ import { assert } from '@ember/debug'; import { default as Registry, privatize as P } from '@ember/-internals/container/lib/registry'; import { guidFor } from '@ember/-internals/utils/lib/guid'; import { ENGINE_PARENT, getEngineParent, setEngineParent } from './parent'; -import ContainerProxyMixin from '@ember/-internals/runtime/lib/mixins/container_proxy'; -import RegistryProxyMixin from '@ember/-internals/runtime/lib/mixins/registry_proxy'; +import type ContainerProxyMixin from '@ember/-internals/runtime/lib/mixins/container_proxy'; +import type RegistryProxyMixin from '@ember/-internals/runtime/lib/mixins/registry_proxy'; +import InternalContainerProxyMixin from '@ember/-internals/runtime/lib/mixins/container_proxy-internal'; +import InternalRegistryProxyMixin from '@ember/-internals/runtime/lib/mixins/registry_proxy-internal'; import type { InternalOwner } from '@ember/-internals/owner'; import type Owner from '@ember/-internals/owner'; import { type FullName, isFactory } from '@ember/-internals/owner'; @@ -59,7 +61,10 @@ export interface EngineInstanceOptions { // type checking, we have broken part of our public API contract. Medium-term, // the goal here is to `EngineInstance` simple be `Owner`. interface EngineInstance extends RegistryProxyMixin, ContainerProxyMixin, InternalOwner, Owner {} -class EngineInstance extends EmberObject.extend(RegistryProxyMixin, ContainerProxyMixin) { +class EngineInstance extends EmberObject.extend( + InternalRegistryProxyMixin, + InternalContainerProxyMixin +) { /** @private @method setupRegistry diff --git a/packages/@ember/enumerable/-internal.ts b/packages/@ember/enumerable/-internal.ts new file mode 100644 index 00000000000..0a1c573248b --- /dev/null +++ b/packages/@ember/enumerable/-internal.ts @@ -0,0 +1,14 @@ +import { InternalMixin } from '@ember/object/mixin-internal'; + +/** + The internal counterpart to the public `Enumerable` mixin. Ember's own + internals apply this so that they do not trigger the deprecation that the + public mixin emits. + + @internal +*/ +// eslint-disable-next-line @typescript-eslint/no-empty-object-type +interface InternalEnumerable {} +const InternalEnumerable = InternalMixin.create(); + +export default InternalEnumerable; diff --git a/packages/@ember/enumerable/index.ts b/packages/@ember/enumerable/index.ts index 1e9ec615cfc..d3e5c2f3a30 100644 --- a/packages/@ember/enumerable/index.ts +++ b/packages/@ember/enumerable/index.ts @@ -1,5 +1,6 @@ -import Mixin from '@ember/object/mixin'; -import { INTERNAL_MIXIN_CREATE } from '@ember/-internals/utils/lib/internal-mixin-create'; +import { DeprecatedMixin } from '@ember/object/mixin-internal'; +import { deprecateUntil, DEPRECATIONS } from '@ember/-internals/deprecations'; +import InternalEnumerable from '@ember/enumerable/-internal'; /** @module @ember/enumerable @@ -13,9 +14,18 @@ import { INTERNAL_MIXIN_CREATE } from '@ember/-internals/utils/lib/internal-mixi @class Enumerable @private + @deprecated Use native arrays and array methods instead. */ // eslint-disable-next-line @typescript-eslint/no-empty-object-type -interface Enumerable {} -const Enumerable = Mixin[INTERNAL_MIXIN_CREATE](); +interface Enumerable extends InternalEnumerable {} +const Enumerable = DeprecatedMixin.create(InternalEnumerable, { + init() { + this._super(...arguments); + deprecateUntil( + 'The `Enumerable` mixin is deprecated. Use native arrays and array methods instead.', + DEPRECATIONS.DEPRECATE_ENUMERABLE_MIXIN + ); + }, +}); export default Enumerable; diff --git a/packages/@ember/enumerable/mutable-internal.ts b/packages/@ember/enumerable/mutable-internal.ts new file mode 100644 index 00000000000..96631833036 --- /dev/null +++ b/packages/@ember/enumerable/mutable-internal.ts @@ -0,0 +1,15 @@ +import InternalEnumerable from '@ember/enumerable/-internal'; +import { InternalMixin } from '@ember/object/mixin-internal'; + +/** + The internal counterpart to the public `MutableEnumerable` mixin. Ember's own + internals apply this so that they do not trigger the deprecation that the + public mixin emits. + + @internal +*/ +// eslint-disable-next-line @typescript-eslint/no-empty-object-type +interface InternalMutableEnumerable extends InternalEnumerable {} +const InternalMutableEnumerable = InternalMixin.create(InternalEnumerable); + +export default InternalMutableEnumerable; diff --git a/packages/@ember/enumerable/mutable.ts b/packages/@ember/enumerable/mutable.ts index 0fce726a0e0..38acfe00ff6 100644 --- a/packages/@ember/enumerable/mutable.ts +++ b/packages/@ember/enumerable/mutable.ts @@ -1,6 +1,6 @@ -import Enumerable from '@ember/enumerable'; -import Mixin from '@ember/object/mixin'; -import { INTERNAL_MIXIN_CREATE } from '@ember/-internals/utils/lib/internal-mixin-create'; +import InternalMutableEnumerable from '@ember/enumerable/mutable-internal'; +import { DeprecatedMixin } from '@ember/object/mixin-internal'; +import { deprecateUntil, DEPRECATIONS } from '@ember/-internals/deprecations'; /** @module ember @@ -15,9 +15,18 @@ import { INTERNAL_MIXIN_CREATE } from '@ember/-internals/utils/lib/internal-mixi @namespace Ember @uses Enumerable @private + @deprecated Use native arrays and array methods instead. */ // eslint-disable-next-line @typescript-eslint/no-empty-object-type -interface MutableEnumerable extends Enumerable {} -const MutableEnumerable = Mixin[INTERNAL_MIXIN_CREATE](Enumerable); +interface MutableEnumerable extends InternalMutableEnumerable {} +const MutableEnumerable = DeprecatedMixin.create(InternalMutableEnumerable, { + init() { + this._super(...arguments); + deprecateUntil( + 'The `MutableEnumerable` mixin is deprecated. Use native arrays and array methods instead.', + DEPRECATIONS.DEPRECATE_MUTABLE_ENUMERABLE_MIXIN + ); + }, +}); export default MutableEnumerable; diff --git a/packages/@ember/object/core.ts b/packages/@ember/object/core.ts index 2dcec80ec1c..e68f0dcb3c6 100644 --- a/packages/@ember/object/core.ts +++ b/packages/@ember/object/core.ts @@ -2,6 +2,7 @@ @module @ember/object/core */ +import { InternalMixin } from '@ember/object/mixin-internal'; import { getFactoryFor, setFactoryFor } from '@ember/-internals/container/lib/container'; import { type default as Owner, getOwner } from '@ember/-internals/owner'; import { guidFor } from '@ember/-internals/utils/lib/guid'; @@ -15,9 +16,9 @@ import { defineProperty } from '@ember/-internals/metal/lib/properties'; import { descriptorForProperty, isClassicDecorator } from '@ember/-internals/metal/lib/decorator'; import { DEBUG_INJECTION_FUNCTIONS } from '@ember/-internals/metal/lib/injected_property'; import { finalizeDecoratedClass } from '@ember/-internals/metal/lib/decorator-util'; -import Mixin, { applyMixin } from '@ember/object/mixin'; -import { INTERNAL_MIXIN_CREATE } from '@ember/-internals/utils/lib/internal-mixin-create'; -import ActionHandler from '@ember/-internals/runtime/lib/mixins/action_handler'; +import type Mixin from '@ember/object/mixin'; +import { applyMixin } from '@ember/object/mixin'; +import InternalActionHandler from '@ember/-internals/runtime/lib/mixins/action_handler-internal'; import makeArray from '@ember/array/make'; import { assert } from '@ember/debug'; import { DEBUG } from '@glimmer/env'; @@ -53,7 +54,7 @@ function hasToStringExtension(val: unknown): val is HasToStringExtension { typeof (val as HasToStringExtension).toStringExtension === 'function' ); } -const reopen = Mixin.prototype.reopen; +const reopen = InternalMixin.prototype.reopen; const wasApplied = new WeakSet(); const prototypeMixinMap = new WeakMap(); @@ -80,7 +81,7 @@ function initialize(obj: CoreObject, properties?: unknown) { assert( 'EmberObject.create no longer supports mixing in other ' + 'definitions, use .extend & .create separately instead.', - !(properties instanceof Mixin) + !(properties instanceof InternalMixin) ); let concatenatedProperties = obj.concatenatedProperties; @@ -107,7 +108,7 @@ function initialize(obj: CoreObject, properties?: unknown) { assert( '`actions` must be provided at extend time, not at create time, ' + 'when Ember.ActionHandler is used (i.e. views, controllers & routes).', - !(keyName === 'actions' && ActionHandler.detect(obj)) + !(keyName === 'actions' && InternalActionHandler.detect(obj)) ); let possibleDesc = descriptorForProperty(obj, keyName, m); @@ -853,7 +854,7 @@ class CoreObject { // make sure that it gets properly applied. Reusing the same mixin after // the first `proto` call will cause it to get skipped. if (prototypeMixinMap.has(this)) { - prototypeMixinMap.set(this, Mixin[INTERNAL_MIXIN_CREATE](this.PrototypeMixin)); + prototypeMixinMap.set(this, InternalMixin.create(this.PrototypeMixin)); } } } @@ -1012,7 +1013,7 @@ class CoreObject { static get PrototypeMixin() { let prototypeMixin = prototypeMixinMap.get(this); if (prototypeMixin === undefined) { - prototypeMixin = Mixin[INTERNAL_MIXIN_CREATE](); + prototypeMixin = InternalMixin.create(); prototypeMixin.ownerConstructor = this; prototypeMixinMap.set(this, prototypeMixin); } @@ -1066,7 +1067,7 @@ function flattenProps(this: typeof CoreObject, ...props: Array { - return Array.isArray(obj) || EmberArray.detect(obj); + return Array.isArray(obj) || InternalEmberArray.detect(obj); } function reduceMacro( diff --git a/packages/@ember/object/mixin-internal.ts b/packages/@ember/object/mixin-internal.ts new file mode 100644 index 00000000000..5e40f1ad1d9 --- /dev/null +++ b/packages/@ember/object/mixin-internal.ts @@ -0,0 +1,710 @@ +import { INIT_FACTORY } from '@ember/-internals/container/lib/container'; +import type { Meta } from '@ember/-internals/meta/lib/meta'; +import { meta as metaFor, peekMeta } from '@ember/-internals/meta/lib/meta'; +import { observerListenerMetaFor, ROOT, wrap } from '@ember/-internals/utils/lib/super'; +import { assert } from '@ember/debug'; +import { DEBUG } from '@glimmer/env'; +import type { + ComputedDecorator, + ComputedPropertyGetter, + ComputedPropertyObj, + ComputedPropertySetter, +} from '@ember/-internals/metal/lib/computed'; +import { type ComputedDescriptor, isClassicDecorator } from '@ember/-internals/metal/lib/decorator'; +import { ComputedProperty } from '@ember/-internals/metal/lib/computed'; +import { + descriptorForDecorator, + makeComputedDecorator, + nativeDescDecorator, +} from '@ember/-internals/metal/lib/decorator'; +import { setUnprocessedMixins } from '@ember/-internals/metal/lib/namespace_search'; +import { + addObserver, + removeObserver, + revalidateObservers, +} from '@ember/-internals/metal/lib/observer'; +import { defineDecorator, defineValue } from '@ember/-internals/metal/lib/properties'; +import { addListener, removeListener } from '@ember/-internals/metal/lib/events'; + +const a_concat = Array.prototype.concat; +const { isArray } = Array; + +function extractAccessors(properties: { [key: string]: any } | undefined) { + if (properties !== undefined) { + for (let key of Object.keys(properties)) { + let desc = Object.getOwnPropertyDescriptor(properties, key)!; + + if (desc.get !== undefined || desc.set !== undefined) { + Object.defineProperty(properties, key, { value: nativeDescDecorator(desc) }); + } + } + } + + return properties; +} + +function concatenatedMixinProperties( + concatProp: string, + props: { [key: string]: any }, + values: { [key: string]: any }, + base: { [key: string]: any } +) { + // reset before adding each new mixin to pickup concats from previous + let concats = values[concatProp] || base[concatProp]; + if (props[concatProp]) { + concats = concats ? a_concat.call(concats, props[concatProp]) : props[concatProp]; + } + return concats; +} + +function giveDecoratorSuper( + key: string, + decorator: ComputedDecorator, + property: ComputedProperty | true, + descs: { [key: string]: any } +): ComputedDecorator { + if (property === true) { + return decorator; + } + + let originalGetter = property._getter; + + if (originalGetter === undefined) { + return decorator; + } + + let superDesc = descs[key]; + + // Check to see if the super property is a decorator first, if so load its descriptor + let superProperty: ComputedProperty | true | undefined = + typeof superDesc === 'function' ? descriptorForDecorator(superDesc) : superDesc; + + if (superProperty === undefined || superProperty === true) { + return decorator; + } + + let superGetter = superProperty._getter; + + if (superGetter === undefined) { + return decorator; + } + + let get = wrap(originalGetter, superGetter) as ComputedPropertyGetter; + let set; + let originalSetter = property._setter; + let superSetter = superProperty._setter; + + if (superSetter !== undefined) { + if (originalSetter !== undefined) { + set = wrap(originalSetter, superSetter) as ComputedPropertySetter; + } else { + // If the super property has a setter, we default to using it no matter what. + // This is clearly very broken and weird, but it's what was here so we have + // to keep it until the next major at least. + // + // TODO: Add a deprecation here. + set = superSetter; + } + } else { + set = originalSetter; + } + + // only create a new CP if we must + if (get !== originalGetter || set !== originalSetter) { + // Since multiple mixins may inherit from the same parent, we need + // to clone the computed property so that other mixins do not receive + // the wrapped version. + let dependentKeys = property._dependentKeys || []; + let newProperty = new ComputedProperty([ + ...dependentKeys, + { + get, + set, + } as ComputedPropertyObj, + ]); + + newProperty._readOnly = property._readOnly; + newProperty._meta = property._meta; + newProperty.enumerable = property.enumerable; + + // SAFETY: We passed in the impl for this class + return makeComputedDecorator(newProperty, ComputedProperty) as ComputedDecorator; + } + + return decorator; +} + +function giveMethodSuper( + key: string, + method: Function, + values: { [key: string]: any }, + descs: { [key: string]: any } +) { + // Methods overwrite computed properties, and do not call super to them. + if (descs[key] !== undefined) { + return method; + } + + // Find the original method in a parent mixin + let superMethod = values[key]; + + // Only wrap the new method if the original method was a function + if (typeof superMethod === 'function') { + return wrap(method, superMethod); + } + + return method; +} + +function simpleMakeArray(value: unknown) { + if (!value) { + return []; + } else if (!Array.isArray(value)) { + return [value]; + } else { + return value; + } +} + +function applyConcatenatedProperties(key: string, value: any, values: { [key: string]: any }) { + let baseValue = values[key]; + let ret = simpleMakeArray(baseValue).concat(simpleMakeArray(value)); + + if (DEBUG) { + // it is possible to use concatenatedProperties with strings (which cannot be frozen) + // only freeze objects... + if (typeof ret === 'object' && ret !== null) { + // prevent mutating `concatenatedProperties` array after it is applied + Object.freeze(ret); + } + } + + return ret; +} + +function applyMergedProperties( + key: string, + value: { [key: string]: any }, + values: { [key: string]: any } +): { [key: string]: any } { + let baseValue = values[key]; + + assert( + `You passed in \`${JSON.stringify( + value + )}\` as the value for \`${key}\` but \`${key}\` cannot be an Array`, + !isArray(value) + ); + + if (!baseValue) { + return value; + } + + let newBase = Object.assign({}, baseValue); + let hasFunction = false; + + let props = Object.keys(value); + + for (let prop of props) { + let propValue = value[prop]; + + if (typeof propValue === 'function') { + hasFunction = true; + newBase[prop] = giveMethodSuper(prop, propValue, baseValue, {}); + } else { + newBase[prop] = propValue; + } + } + + if (hasFunction) { + newBase._super = ROOT; + } + + return newBase; +} + +function mergeMixins( + mixins: MixinLike[], + meta: Meta, + descs: { [key: string]: object }, + values: { [key: string]: object }, + base: { [key: string]: object }, + keys: string[], + keysWithSuper: string[] +): void { + let currentMixin: MixinLike | undefined; + + for (let i = 0; i < mixins.length; i++) { + currentMixin = mixins[i]; + assert( + `Expected hash or Mixin instance, got ${Object.prototype.toString.call(currentMixin)}`, + typeof currentMixin === 'object' && + currentMixin !== null && + Object.prototype.toString.call(currentMixin) !== '[object Array]' + ); + + if (MIXINS.has(currentMixin)) { + if (meta.hasMixin(currentMixin)) { + continue; + } + meta.addMixin(currentMixin); + + let { properties, mixins } = currentMixin; + + if (properties !== undefined) { + mergeProps(meta, properties, descs, values, base, keys, keysWithSuper); + } else if (mixins !== undefined) { + mergeMixins(mixins, meta, descs, values, base, keys, keysWithSuper); + + if (currentMixin instanceof InternalMixin && currentMixin._without !== undefined) { + currentMixin._without.forEach((keyName: string) => { + // deleting the key means we won't process the value + let index = keys.indexOf(keyName); + + if (index !== -1) { + keys.splice(index, 1); + } + }); + } + } + } else { + mergeProps( + meta, + currentMixin as Record, + descs, + values, + base, + keys, + keysWithSuper + ); + } + } +} + +function mergeProps( + meta: Meta, + props: { [key: string]: unknown }, + descs: { [key: string]: unknown }, + values: { [key: string]: unknown }, + base: { [key: string]: unknown }, + keys: string[], + keysWithSuper: string[] +) { + let concats = concatenatedMixinProperties('concatenatedProperties', props, values, base); + let mergings = concatenatedMixinProperties('mergedProperties', props, values, base); + + let propKeys = Object.keys(props); + + for (let key of propKeys) { + let value = props[key]; + + if (value === undefined) continue; + + if (keys.indexOf(key) === -1) { + keys.push(key); + + let desc = meta.peekDescriptors(key); + + if (desc === undefined) { + // If the value is a classic decorator, we don't want to actually + // access it, because that will execute the decorator while we're + // building the class. + if (!isClassicDecorator(value)) { + // The superclass did not have a CP, which means it may have + // observers or listeners on that property. + let prev = (values[key] = base[key]); + + if (typeof prev === 'function') { + updateObserversAndListeners(base, key, prev, false); + } + } + } else { + descs[key] = desc; + + // The super desc will be overwritten on descs, so save off the fact that + // there was a super so we know to Object.defineProperty when writing + // the value + keysWithSuper.push(key); + + desc.teardown(base, key, meta); + } + } + + let isFunction = typeof value === 'function'; + + if (isFunction) { + let desc: ComputedDescriptor | undefined | true = descriptorForDecorator(value as Function); + + if (desc !== undefined) { + // Wrap descriptor function to implement _super() if needed + descs[key] = giveDecoratorSuper( + key, + value as ComputedDecorator, + desc as ComputedProperty, + descs + ); + values[key] = undefined; + + continue; + } + } + + if ( + (concats && concats.indexOf(key) >= 0) || + key === 'concatenatedProperties' || + key === 'mergedProperties' + ) { + value = applyConcatenatedProperties(key, value, values); + } else if (mergings && mergings.indexOf(key) > -1) { + value = applyMergedProperties(key, value as object, values); + } else if (isFunction) { + value = giveMethodSuper(key, value as Function, values, descs); + } + + values[key] = value; + descs[key] = undefined; + } +} + +function updateObserversAndListeners(obj: object, key: string, fn: Function, add: boolean) { + let meta = observerListenerMetaFor(fn); + + if (meta === undefined) return; + + let { observers, listeners } = meta; + + if (observers !== undefined) { + let updateObserver = add ? addObserver : removeObserver; + + for (let path of observers.paths) { + updateObserver(obj, path, null, key, observers.sync); + } + } + + if (listeners !== undefined) { + let updateListener = add ? addListener : removeListener; + + for (let listener of listeners) { + updateListener(obj, listener, null, key); + } + } +} + +export function applyMixin( + obj: Record, + mixins: Array>, + _hideKeys = false +) { + let descs = Object.create(null); + let values = Object.create(null); + let meta = metaFor(obj); + let keys: string[] = []; + let keysWithSuper: string[] = []; + + (obj as any)._super = ROOT; + + // Go through all mixins and hashes passed in, and: + // + // * Handle concatenated properties + // * Handle merged properties + // * Set up _super wrapping if necessary + // * Set up computed property descriptors + // * Copying `toString` in broken browsers + mergeMixins(mixins, meta, descs, values, obj, keys, keysWithSuper); + + for (let key of keys) { + let value = values[key]; + let desc = descs[key]; + + if (value !== undefined) { + if (typeof value === 'function') { + updateObserversAndListeners(obj, key, value, true); + } + + defineValue(obj, key, value, keysWithSuper.indexOf(key) !== -1, !_hideKeys); + } else if (desc !== undefined) { + defineDecorator(obj, key, desc, meta); + } + } + + if (!meta.isPrototypeMeta(obj)) { + revalidateObservers(obj); + } + + return obj; +} + +/** + @method mixin + @param obj + @param mixins* + @return obj + @private +*/ +export function mixin(obj: object, ...args: any[]) { + applyMixin(obj, args); + return obj; +} + +const MIXINS = new WeakSet(); + +/** + The mixin machinery, without the deprecation that the public `Mixin` class + emits from `create`. Ember's own internals are built on mixins, so they build + them through this class; `Mixin` in `@ember/object/mixin` is the public, + deprecating subclass. + + @internal +*/ +export class InternalMixin { + /** @internal */ + declare static _disableDebugSeal?: boolean; + + /** @internal */ + mixins: InternalMixin[] | undefined; + + /** @internal */ + properties: { [key: string]: any } | undefined; + + /** @internal */ + ownerConstructor: any; + + /** @internal */ + _without: any[] | undefined; + + declare [INIT_FACTORY]?: null; + + /** @internal */ + constructor(mixins: InternalMixin[] | undefined, properties?: { [key: string]: any }) { + MIXINS.add(this); + this.properties = extractAccessors(properties); + this.mixins = buildMixinsArray(mixins); + this.ownerConstructor = undefined; + this._without = undefined; + + if (DEBUG) { + // Eagerly add INIT_FACTORY to avoid issues in DEBUG as a result of Object.seal(mixin) + this[INIT_FACTORY] = null; + + if (InternalMixin._disableDebugSeal !== true) { + Object.seal(this); + } + } + } + + /** @internal */ + static create(...args: any[]): InstanceType { + setUnprocessedMixins(); + let M = this; + return new M(args, undefined) as InstanceType; + } + + // returns the mixins currently applied to the specified object + // TODO: Make `mixin` + /** @internal */ + static mixins(obj: object): InternalMixin[] { + let meta = peekMeta(obj); + let ret: InternalMixin[] = []; + if (meta === null) { + return ret; + } + + meta.forEachMixins((currentMixin: InternalMixin) => { + // skip primitive mixins since these are always anonymous + if (!currentMixin.properties) { + ret.push(currentMixin); + } + }); + + return ret; + } + + /** + @method reopen + @param arguments* + @private + @internal + */ + reopen(...args: Array>): this { + if (args.length === 0) { + return this; + } + + if (this.properties) { + let currentMixin = new InternalMixin(undefined, this.properties); + this.properties = undefined; + this.mixins = [currentMixin]; + } else if (!this.mixins) { + this.mixins = []; + } + + this.mixins = this.mixins.concat(buildMixinsArray(args) as InternalMixin[]); + return this; + } + + /** + @method apply + @param obj + @return applied object + @private + @internal + */ + apply(obj: object, _hideKeys = false) { + // Ember.NativeArray is a normal Ember.Mixin that we mix into `Array.prototype` when prototype extensions are enabled + // mutating a native object prototype like this should _not_ result in enumerable properties being added (or we have significant + // issues with things like deep equality checks from test frameworks, or things like jQuery.extend(true, [], [])). + // + // _hideKeys disables enumerablity when applying the mixin. This is a hack, and we should stop mutating the array prototype by default 😫 + return applyMixin(obj, [this], _hideKeys); + } + + /** @internal */ + applyPartial(obj: object) { + return applyMixin(obj, [this]); + } + + /** + @method detect + @param obj + @return {Boolean} + @private + @internal + */ + detect(obj: any): boolean { + if (typeof obj !== 'object' || obj === null) { + return false; + } + if (MIXINS.has(obj)) { + return _detect(obj, this); + } + let meta = peekMeta(obj); + if (meta === null) { + return false; + } + return meta.hasMixin(this); + } + + /** @internal */ + without(...args: any[]) { + let ret = new InternalMixin([this]); + ret._without = args; + return ret; + } + + /** @internal */ + keys() { + let keys = _keys(this); + assert('[BUG] Missing keys for mixin!', keys); + return keys; + } + + /** @internal */ + toString() { + return '(unknown mixin)'; + } +} + +if (DEBUG) { + Object.defineProperty(InternalMixin, '_disableDebugSeal', { + configurable: true, + enumerable: false, + writable: true, + value: false, + }); +} + +function buildMixinsArray(mixins: MixinLike[] | undefined): InternalMixin[] | undefined { + let length = (mixins && mixins.length) || 0; + let m: InternalMixin[] | undefined = undefined; + + if (length > 0) { + m = new Array(length); + for (let i = 0; i < length; i++) { + let x = mixins![i]; + assert( + `Expected hash or Mixin instance, got ${Object.prototype.toString.call(x)}`, + typeof x === 'object' && + x !== null && + Object.prototype.toString.call(x) !== '[object Array]' + ); + + if (MIXINS.has(x)) { + m[i] = x as InternalMixin; + } else { + m[i] = new InternalMixin(undefined, x); + } + } + } + + return m; +} + +type MixinLike = InternalMixin | { [key: string]: any }; + +if (DEBUG) { + Object.seal(InternalMixin.prototype); +} + +function _detect(curMixin: InternalMixin, targetMixin: InternalMixin, seen = new Set()): boolean { + if (seen.has(curMixin)) { + return false; + } + seen.add(curMixin); + + if (curMixin === targetMixin) { + return true; + } + let mixins = curMixin.mixins; + if (mixins) { + return mixins.some((mixin) => _detect(mixin, targetMixin, seen)); + } + + return false; +} + +function _keys(mixin: InternalMixin, ret = new Set(), seen = new Set()) { + if (seen.has(mixin)) { + return; + } + seen.add(mixin); + + if (mixin.properties) { + let props = Object.keys(mixin.properties); + for (let prop of props) { + ret.add(prop); + } + } else if (mixin.mixins) { + mixin.mixins.forEach((x: any) => _keys(x, ret, seen)); + } + + return ret; +} + +/** + The public, deprecating face of a mixin that Ember also uses internally. + + RFC #1116 deprecates applying Ember's mixins, but Ember's own internals still + need them. Each such mixin therefore exists twice: an internal copy that + applies silently, and a public copy -- built with this class -- that wraps the + internal one and emits the deprecation from `init`. + + Because the two copies are distinct objects, `meta.hasMixin` would not match a + public mixin against an object that only ever had the internal copy applied. + `detect` is overridden here to look through to the wrapped copy, so that code + like `EmberArray.detect(someArrayProxy)` keeps working while the deprecation + is live. + + @internal +*/ +export class DeprecatedMixin extends InternalMixin { + detect(obj: any): boolean { + if (super.detect(obj)) { + return true; + } + + // The internal copy being wrapped is always the first constituent mixin, + // i.e. `DeprecatedMixin.create(internalCopy, { init() {} })`. + let internalMixin = this.mixins?.[0]; + return internalMixin !== undefined && internalMixin.detect(obj); + } +} diff --git a/packages/@ember/object/mixin.ts b/packages/@ember/object/mixin.ts index 29840c29956..d9c7d50b185 100644 --- a/packages/@ember/object/mixin.ts +++ b/packages/@ember/object/mixin.ts @@ -1,457 +1,10 @@ /** @module @ember/object/mixin */ -import { INIT_FACTORY } from '@ember/-internals/container/lib/container'; -import type { Meta } from '@ember/-internals/meta/lib/meta'; -import { meta as metaFor, peekMeta } from '@ember/-internals/meta/lib/meta'; -import { observerListenerMetaFor, ROOT, wrap } from '@ember/-internals/utils/lib/super'; -import { INTERNAL_MIXIN_CREATE } from '@ember/-internals/utils/lib/internal-mixin-create'; -import { assert } from '@ember/debug'; -import { DEBUG } from '@glimmer/env'; -import type { - ComputedDecorator, - ComputedPropertyGetter, - ComputedPropertyObj, - ComputedPropertySetter, -} from '@ember/-internals/metal/lib/computed'; -import { type ComputedDescriptor, isClassicDecorator } from '@ember/-internals/metal/lib/decorator'; -import { ComputedProperty } from '@ember/-internals/metal/lib/computed'; -import { - descriptorForDecorator, - makeComputedDecorator, - nativeDescDecorator, -} from '@ember/-internals/metal/lib/decorator'; -import { setUnprocessedMixins } from '@ember/-internals/metal/lib/namespace_search'; -import { - addObserver, - removeObserver, - revalidateObservers, -} from '@ember/-internals/metal/lib/observer'; -import { defineDecorator, defineValue } from '@ember/-internals/metal/lib/properties'; -import { addListener, removeListener } from '@ember/-internals/metal/lib/events'; +import { InternalMixin } from '@ember/object/mixin-internal'; import { deprecateUntil, DEPRECATIONS } from '@ember/-internals/deprecations'; -const a_concat = Array.prototype.concat; -const { isArray } = Array; - -function extractAccessors(properties: { [key: string]: any } | undefined) { - if (properties !== undefined) { - for (let key of Object.keys(properties)) { - let desc = Object.getOwnPropertyDescriptor(properties, key)!; - - if (desc.get !== undefined || desc.set !== undefined) { - Object.defineProperty(properties, key, { value: nativeDescDecorator(desc) }); - } - } - } - - return properties; -} - -function concatenatedMixinProperties( - concatProp: string, - props: { [key: string]: any }, - values: { [key: string]: any }, - base: { [key: string]: any } -) { - // reset before adding each new mixin to pickup concats from previous - let concats = values[concatProp] || base[concatProp]; - if (props[concatProp]) { - concats = concats ? a_concat.call(concats, props[concatProp]) : props[concatProp]; - } - return concats; -} - -function giveDecoratorSuper( - key: string, - decorator: ComputedDecorator, - property: ComputedProperty | true, - descs: { [key: string]: any } -): ComputedDecorator { - if (property === true) { - return decorator; - } - - let originalGetter = property._getter; - - if (originalGetter === undefined) { - return decorator; - } - - let superDesc = descs[key]; - - // Check to see if the super property is a decorator first, if so load its descriptor - let superProperty: ComputedProperty | true | undefined = - typeof superDesc === 'function' ? descriptorForDecorator(superDesc) : superDesc; - - if (superProperty === undefined || superProperty === true) { - return decorator; - } - - let superGetter = superProperty._getter; - - if (superGetter === undefined) { - return decorator; - } - - let get = wrap(originalGetter, superGetter) as ComputedPropertyGetter; - let set; - let originalSetter = property._setter; - let superSetter = superProperty._setter; - - if (superSetter !== undefined) { - if (originalSetter !== undefined) { - set = wrap(originalSetter, superSetter) as ComputedPropertySetter; - } else { - // If the super property has a setter, we default to using it no matter what. - // This is clearly very broken and weird, but it's what was here so we have - // to keep it until the next major at least. - // - // TODO: Add a deprecation here. - set = superSetter; - } - } else { - set = originalSetter; - } - - // only create a new CP if we must - if (get !== originalGetter || set !== originalSetter) { - // Since multiple mixins may inherit from the same parent, we need - // to clone the computed property so that other mixins do not receive - // the wrapped version. - let dependentKeys = property._dependentKeys || []; - let newProperty = new ComputedProperty([ - ...dependentKeys, - { - get, - set, - } as ComputedPropertyObj, - ]); - - newProperty._readOnly = property._readOnly; - newProperty._meta = property._meta; - newProperty.enumerable = property.enumerable; - - // SAFETY: We passed in the impl for this class - return makeComputedDecorator(newProperty, ComputedProperty) as ComputedDecorator; - } - - return decorator; -} - -function giveMethodSuper( - key: string, - method: Function, - values: { [key: string]: any }, - descs: { [key: string]: any } -) { - // Methods overwrite computed properties, and do not call super to them. - if (descs[key] !== undefined) { - return method; - } - - // Find the original method in a parent mixin - let superMethod = values[key]; - - // Only wrap the new method if the original method was a function - if (typeof superMethod === 'function') { - return wrap(method, superMethod); - } - - return method; -} - -function simpleMakeArray(value: unknown) { - if (!value) { - return []; - } else if (!Array.isArray(value)) { - return [value]; - } else { - return value; - } -} - -function applyConcatenatedProperties(key: string, value: any, values: { [key: string]: any }) { - let baseValue = values[key]; - let ret = simpleMakeArray(baseValue).concat(simpleMakeArray(value)); - - if (DEBUG) { - // it is possible to use concatenatedProperties with strings (which cannot be frozen) - // only freeze objects... - if (typeof ret === 'object' && ret !== null) { - // prevent mutating `concatenatedProperties` array after it is applied - Object.freeze(ret); - } - } - - return ret; -} - -function applyMergedProperties( - key: string, - value: { [key: string]: any }, - values: { [key: string]: any } -): { [key: string]: any } { - let baseValue = values[key]; - - assert( - `You passed in \`${JSON.stringify( - value - )}\` as the value for \`${key}\` but \`${key}\` cannot be an Array`, - !isArray(value) - ); - - if (!baseValue) { - return value; - } - - let newBase = Object.assign({}, baseValue); - let hasFunction = false; - - let props = Object.keys(value); - - for (let prop of props) { - let propValue = value[prop]; - - if (typeof propValue === 'function') { - hasFunction = true; - newBase[prop] = giveMethodSuper(prop, propValue, baseValue, {}); - } else { - newBase[prop] = propValue; - } - } - - if (hasFunction) { - newBase._super = ROOT; - } - - return newBase; -} - -function mergeMixins( - mixins: MixinLike[], - meta: Meta, - descs: { [key: string]: object }, - values: { [key: string]: object }, - base: { [key: string]: object }, - keys: string[], - keysWithSuper: string[] -): void { - let currentMixin: MixinLike | undefined; - - for (let i = 0; i < mixins.length; i++) { - currentMixin = mixins[i]; - assert( - `Expected hash or Mixin instance, got ${Object.prototype.toString.call(currentMixin)}`, - typeof currentMixin === 'object' && - currentMixin !== null && - Object.prototype.toString.call(currentMixin) !== '[object Array]' - ); - - if (MIXINS.has(currentMixin)) { - if (meta.hasMixin(currentMixin)) { - continue; - } - meta.addMixin(currentMixin); - - let { properties, mixins } = currentMixin; - - if (properties !== undefined) { - mergeProps(meta, properties, descs, values, base, keys, keysWithSuper); - } else if (mixins !== undefined) { - mergeMixins(mixins, meta, descs, values, base, keys, keysWithSuper); - - if (currentMixin instanceof Mixin && currentMixin._without !== undefined) { - currentMixin._without.forEach((keyName: string) => { - // deleting the key means we won't process the value - let index = keys.indexOf(keyName); - - if (index !== -1) { - keys.splice(index, 1); - } - }); - } - } - } else { - mergeProps( - meta, - currentMixin as Record, - descs, - values, - base, - keys, - keysWithSuper - ); - } - } -} - -function mergeProps( - meta: Meta, - props: { [key: string]: unknown }, - descs: { [key: string]: unknown }, - values: { [key: string]: unknown }, - base: { [key: string]: unknown }, - keys: string[], - keysWithSuper: string[] -) { - let concats = concatenatedMixinProperties('concatenatedProperties', props, values, base); - let mergings = concatenatedMixinProperties('mergedProperties', props, values, base); - - let propKeys = Object.keys(props); - - for (let key of propKeys) { - let value = props[key]; - - if (value === undefined) continue; - - if (keys.indexOf(key) === -1) { - keys.push(key); - - let desc = meta.peekDescriptors(key); - - if (desc === undefined) { - // If the value is a classic decorator, we don't want to actually - // access it, because that will execute the decorator while we're - // building the class. - if (!isClassicDecorator(value)) { - // The superclass did not have a CP, which means it may have - // observers or listeners on that property. - let prev = (values[key] = base[key]); - - if (typeof prev === 'function') { - updateObserversAndListeners(base, key, prev, false); - } - } - } else { - descs[key] = desc; - - // The super desc will be overwritten on descs, so save off the fact that - // there was a super so we know to Object.defineProperty when writing - // the value - keysWithSuper.push(key); - - desc.teardown(base, key, meta); - } - } - - let isFunction = typeof value === 'function'; - - if (isFunction) { - let desc: ComputedDescriptor | undefined | true = descriptorForDecorator(value as Function); - - if (desc !== undefined) { - // Wrap descriptor function to implement _super() if needed - descs[key] = giveDecoratorSuper( - key, - value as ComputedDecorator, - desc as ComputedProperty, - descs - ); - values[key] = undefined; - - continue; - } - } - - if ( - (concats && concats.indexOf(key) >= 0) || - key === 'concatenatedProperties' || - key === 'mergedProperties' - ) { - value = applyConcatenatedProperties(key, value, values); - } else if (mergings && mergings.indexOf(key) > -1) { - value = applyMergedProperties(key, value as object, values); - } else if (isFunction) { - value = giveMethodSuper(key, value as Function, values, descs); - } - - values[key] = value; - descs[key] = undefined; - } -} - -function updateObserversAndListeners(obj: object, key: string, fn: Function, add: boolean) { - let meta = observerListenerMetaFor(fn); - - if (meta === undefined) return; - - let { observers, listeners } = meta; - - if (observers !== undefined) { - let updateObserver = add ? addObserver : removeObserver; - - for (let path of observers.paths) { - updateObserver(obj, path, null, key, observers.sync); - } - } - - if (listeners !== undefined) { - let updateListener = add ? addListener : removeListener; - - for (let listener of listeners) { - updateListener(obj, listener, null, key); - } - } -} - -export function applyMixin( - obj: Record, - mixins: Array>, - _hideKeys = false -) { - let descs = Object.create(null); - let values = Object.create(null); - let meta = metaFor(obj); - let keys: string[] = []; - let keysWithSuper: string[] = []; - - (obj as any)._super = ROOT; - - // Go through all mixins and hashes passed in, and: - // - // * Handle concatenated properties - // * Handle merged properties - // * Set up _super wrapping if necessary - // * Set up computed property descriptors - // * Copying `toString` in broken browsers - mergeMixins(mixins, meta, descs, values, obj, keys, keysWithSuper); - - for (let key of keys) { - let value = values[key]; - let desc = descs[key]; - - if (value !== undefined) { - if (typeof value === 'function') { - updateObserversAndListeners(obj, key, value, true); - } - - defineValue(obj, key, value, keysWithSuper.indexOf(key) !== -1, !_hideKeys); - } else if (desc !== undefined) { - defineDecorator(obj, key, desc, meta); - } - } - - if (!meta.isPrototypeMeta(obj)) { - revalidateObservers(obj); - } - - return obj; -} - -/** - @method mixin - @param obj - @param mixins* - @return obj - @private -*/ -export function mixin(obj: object, ...args: any[]) { - applyMixin(obj, args); - return obj; -} - -const MIXINS = new WeakSet(); +export { applyMixin, mixin } from '@ember/object/mixin-internal'; /** The `Mixin` class allows you to create mixins, whose properties can be @@ -532,45 +85,11 @@ const MIXINS = new WeakSet(); }); ``` + @deprecated @class Mixin @public */ -export default class Mixin { - /** @internal */ - declare static _disableDebugSeal?: boolean; - - /** @internal */ - mixins: Mixin[] | undefined; - - /** @internal */ - properties: { [key: string]: any } | undefined; - - /** @internal */ - ownerConstructor: any; - - /** @internal */ - _without: any[] | undefined; - - declare [INIT_FACTORY]?: null; - - /** @internal */ - constructor(mixins: Mixin[] | undefined, properties?: { [key: string]: any }) { - MIXINS.add(this); - this.properties = extractAccessors(properties); - this.mixins = buildMixinsArray(mixins); - this.ownerConstructor = undefined; - this._without = undefined; - - if (DEBUG) { - // Eagerly add INIT_FACTORY to avoid issues in DEBUG as a result of Object.seal(mixin) - this[INIT_FACTORY] = null; - - if (Mixin._disableDebugSeal !== true) { - Object.seal(this); - } - } - } - +export default class Mixin extends InternalMixin { /** @method create @for @ember/object/mixin @@ -578,198 +97,11 @@ export default class Mixin { @param arguments* @public */ - static create(...args: any[]): InstanceType { + static override create(...args: any[]): InstanceType { deprecateUntil( `Using mixins is deprecated. Refactor to composition patterns or class decorators.`, DEPRECATIONS.DEPRECATE_MIXINS ); - return this[INTERNAL_MIXIN_CREATE](...args); - } - - /** @internal */ - static [INTERNAL_MIXIN_CREATE](...args: any[]): InstanceType { - setUnprocessedMixins(); - let M = this; - return new M(args, undefined) as InstanceType; + return super.create(...args); } - - // returns the mixins currently applied to the specified object - // TODO: Make `mixin` - /** @internal */ - static mixins(obj: object): Mixin[] { - let meta = peekMeta(obj); - let ret: Mixin[] = []; - if (meta === null) { - return ret; - } - - meta.forEachMixins((currentMixin: Mixin) => { - // skip primitive mixins since these are always anonymous - if (!currentMixin.properties) { - ret.push(currentMixin); - } - }); - - return ret; - } - - /** - @method reopen - @param arguments* - @private - @internal - */ - reopen(...args: Array>): this { - if (args.length === 0) { - return this; - } - - if (this.properties) { - let currentMixin = new Mixin(undefined, this.properties); - this.properties = undefined; - this.mixins = [currentMixin]; - } else if (!this.mixins) { - this.mixins = []; - } - - this.mixins = this.mixins.concat(buildMixinsArray(args) as Mixin[]); - return this; - } - - /** - @method apply - @param obj - @return applied object - @private - @internal - */ - apply(obj: object, _hideKeys = false) { - // Ember.NativeArray is a normal Ember.Mixin that we mix into `Array.prototype` when prototype extensions are enabled - // mutating a native object prototype like this should _not_ result in enumerable properties being added (or we have significant - // issues with things like deep equality checks from test frameworks, or things like jQuery.extend(true, [], [])). - // - // _hideKeys disables enumerablity when applying the mixin. This is a hack, and we should stop mutating the array prototype by default 😫 - return applyMixin(obj, [this], _hideKeys); - } - - /** @internal */ - applyPartial(obj: object) { - return applyMixin(obj, [this]); - } - - /** - @method detect - @param obj - @return {Boolean} - @private - @internal - */ - detect(obj: any): boolean { - if (typeof obj !== 'object' || obj === null) { - return false; - } - if (MIXINS.has(obj)) { - return _detect(obj, this); - } - let meta = peekMeta(obj); - if (meta === null) { - return false; - } - return meta.hasMixin(this); - } - - /** @internal */ - without(...args: any[]) { - let ret = new Mixin([this]); - ret._without = args; - return ret; - } - - /** @internal */ - keys() { - let keys = _keys(this); - assert('[BUG] Missing keys for mixin!', keys); - return keys; - } - - /** @internal */ - toString() { - return '(unknown mixin)'; - } -} - -if (DEBUG) { - Object.defineProperty(Mixin, '_disableDebugSeal', { - configurable: true, - enumerable: false, - writable: true, - value: false, - }); -} - -function buildMixinsArray(mixins: MixinLike[] | undefined): Mixin[] | undefined { - let length = (mixins && mixins.length) || 0; - let m: Mixin[] | undefined = undefined; - - if (length > 0) { - m = new Array(length); - for (let i = 0; i < length; i++) { - let x = mixins![i]; - assert( - `Expected hash or Mixin instance, got ${Object.prototype.toString.call(x)}`, - typeof x === 'object' && - x !== null && - Object.prototype.toString.call(x) !== '[object Array]' - ); - - if (MIXINS.has(x)) { - m[i] = x as Mixin; - } else { - m[i] = new Mixin(undefined, x); - } - } - } - - return m; -} - -type MixinLike = Mixin | { [key: string]: any }; - -if (DEBUG) { - Object.seal(Mixin.prototype); -} - -function _detect(curMixin: Mixin, targetMixin: Mixin, seen = new Set()): boolean { - if (seen.has(curMixin)) { - return false; - } - seen.add(curMixin); - - if (curMixin === targetMixin) { - return true; - } - let mixins = curMixin.mixins; - if (mixins) { - return mixins.some((mixin) => _detect(mixin, targetMixin, seen)); - } - - return false; -} - -function _keys(mixin: Mixin, ret = new Set(), seen = new Set()) { - if (seen.has(mixin)) { - return; - } - seen.add(mixin); - - if (mixin.properties) { - let props = Object.keys(mixin.properties); - for (let prop of props) { - ret.add(prop); - } - } else if (mixin.mixins) { - mixin.mixins.forEach((x: any) => _keys(x, ret, seen)); - } - - return ret; } diff --git a/packages/@ember/object/observable-internal.ts b/packages/@ember/object/observable-internal.ts new file mode 100644 index 00000000000..5a8da475e2f --- /dev/null +++ b/packages/@ember/object/observable-internal.ts @@ -0,0 +1,194 @@ +/** +@module @ember/object/observable +*/ + +import { peekMeta } from '@ember/-internals/meta/lib/meta'; +import { hasListeners } from '@ember/-internals/metal/lib/events'; +import { + beginPropertyChanges, + notifyPropertyChange, + endPropertyChanges, +} from '@ember/-internals/metal/lib/property_events'; +import { addObserver, removeObserver } from '@ember/-internals/metal/lib/observer'; +import { get } from '@ember/-internals/metal/lib/property_get'; +import { set } from '@ember/-internals/metal/lib/property_set'; +import getProperties from '@ember/-internals/metal/lib/get_properties'; +import setProperties from '@ember/-internals/metal/lib/set_properties'; + +import { InternalMixin } from '@ember/object/mixin-internal'; +import { assert } from '@ember/debug'; + +export type ObserverMethod = + | keyof Target + | ((this: Target, sender: Sender, key: string, value: any, rev: number) => void); + +/** + The internal counterpart to the public `Observable` mixin. Ember's own + internals apply this so that they do not trigger the deprecation that the + public mixin emits. The public API documentation lives on the public copy. + + @internal +*/ +interface InternalObservable { + get(key: K): this[K]; + get(key: string): unknown; + + getProperties>(list: L): { [Key in L[number]]: this[Key] }; + getProperties>(...list: L): { [Key in L[number]]: this[Key] }; + getProperties(list: L): { [Key in L[number]]: unknown }; + getProperties(...list: L): { [Key in L[number]]: unknown }; + + // NOT TYPE SAFE! + set(key: K, value: T): T; + set(key: string, value: T): T; + + // NOT TYPE SAFE! + setProperties(hash: P): P; + setProperties>(hash: T): T; + + notifyPropertyChange(keyName: string): this; + + addObserver(key: keyof this, target: Target, method: ObserverMethod): this; + addObserver(key: keyof this, method: ObserverMethod): this; + + removeObserver( + key: keyof this, + target: Target, + method: ObserverMethod + ): this; + removeObserver(key: keyof this, method: ObserverMethod): this; + + // NOT TYPE SAFE! + incrementProperty(keyName: keyof this, increment?: number): number; + + // NOT TYPE SAFE! + decrementProperty(keyName: keyof this, decrement?: number): number; + + // NOT TYPE SAFE! + toggleProperty(keyName: keyof this): boolean; + + cacheFor(key: K): unknown; +} +const InternalObservable = InternalMixin.create({ + get(keyName: string) { + return get(this, keyName); + }, + + getProperties(...args: string[]) { + return getProperties(this, ...args); + }, + + set(keyName: string, value: unknown) { + return set(this, keyName, value); + }, + + setProperties(hash: object) { + return setProperties(this, hash); + }, + + /** + Begins a grouping of property changes. + + You can use this method to group property changes so that notifications + will not be sent until the changes are finished. If you plan to make a + large number of changes to an object at one time, you should call this + method at the beginning of the changes to begin deferring change + notifications. When you are done making changes, call + `endPropertyChanges()` to deliver the deferred change notifications and end + deferring. + + @method beginPropertyChanges + @return {Observable} + @private + */ + beginPropertyChanges() { + beginPropertyChanges(); + return this; + }, + + /** + Ends a grouping of property changes. + + You can use this method to group property changes so that notifications + will not be sent until the changes are finished. If you plan to make a + large number of changes to an object at one time, you should call + `beginPropertyChanges()` at the beginning of the changes to defer change + notifications. When you are done making changes, call this method to + deliver the deferred change notifications and end deferring. + + @method endPropertyChanges + @return {Observable} + @private + */ + endPropertyChanges() { + endPropertyChanges(); + return this; + }, + + notifyPropertyChange(keyName: string) { + notifyPropertyChange(this, keyName); + return this; + }, + + addObserver( + key: string, + target: object | Function | null, + method?: string | Function, + sync?: boolean + ) { + addObserver(this, key, target, method, sync); + return this; + }, + + removeObserver( + key: string, + target: object | Function | null, + method?: string | Function, + sync?: boolean + ) { + removeObserver(this, key, target, method, sync); + return this; + }, + + /** + Returns `true` if the object currently has observers registered for a + particular key. You can use this method to potentially defer performing + an expensive action until someone begins observing a particular property + on the object. + + @method hasObserverFor + @param {String} key Key to check + @return {Boolean} + @private + */ + hasObserverFor(key: string) { + return hasListeners(this, `${key}:change`); + }, + + incrementProperty(keyName: string, increment = 1) { + assert( + 'Must pass a numeric value to incrementProperty', + !isNaN(parseFloat(String(increment))) && isFinite(increment) + ); + return set(this, keyName, (parseFloat(get(this, keyName)) || 0) + increment); + }, + + decrementProperty(keyName: string, decrement = 1) { + assert( + 'Must pass a numeric value to decrementProperty', + (typeof decrement === 'number' || !isNaN(parseFloat(decrement))) && isFinite(decrement) + ); + return set(this, keyName, (get(this, keyName) || 0) - decrement); + }, + + toggleProperty(keyName: string) { + return set(this, keyName, !get(this, keyName)); + }, + + cacheFor(keyName: string) { + let meta = peekMeta(this); + return meta !== null ? meta.valueFor(keyName) : undefined; + }, +}); + +export default InternalObservable; diff --git a/packages/@ember/object/observable.ts b/packages/@ember/object/observable.ts index b928183b051..1320fd97574 100644 --- a/packages/@ember/object/observable.ts +++ b/packages/@ember/object/observable.ts @@ -2,26 +2,12 @@ @module @ember/object/observable */ -import { peekMeta } from '@ember/-internals/meta/lib/meta'; -import { hasListeners } from '@ember/-internals/metal/lib/events'; -import { - beginPropertyChanges, - notifyPropertyChange, - endPropertyChanges, -} from '@ember/-internals/metal/lib/property_events'; -import { addObserver, removeObserver } from '@ember/-internals/metal/lib/observer'; -import { get } from '@ember/-internals/metal/lib/property_get'; -import { set } from '@ember/-internals/metal/lib/property_set'; -import getProperties from '@ember/-internals/metal/lib/get_properties'; -import setProperties from '@ember/-internals/metal/lib/set_properties'; - -import Mixin from '@ember/object/mixin'; -import { INTERNAL_MIXIN_CREATE } from '@ember/-internals/utils/lib/internal-mixin-create'; -import { assert } from '@ember/debug'; - -export type ObserverMethod = - | keyof Target - | ((this: Target, sender: Sender, key: string, value: any, rev: number) => void); +import { DeprecatedMixin } from '@ember/object/mixin-internal'; +import { deprecateUntil, DEPRECATIONS } from '@ember/-internals/deprecations'; +import InternalObservable from '@ember/object/observable-internal'; +import type { ObserverMethod } from '@ember/object/observable-internal'; + +export type { ObserverMethod } from '@ember/object/observable-internal'; /** ## Overview @@ -93,7 +79,7 @@ export type ObserverMethod = @class Observable @public */ -interface Observable { +interface Observable extends InternalObservable { /** Retrieves the value of a property from the object. @@ -421,125 +407,13 @@ interface Observable { */ cacheFor(key: K): unknown; } -const Observable = Mixin[INTERNAL_MIXIN_CREATE]({ - get(keyName: string) { - return get(this, keyName); - }, - - getProperties(...args: string[]) { - return getProperties(this, ...args); - }, - - set(keyName: string, value: unknown) { - return set(this, keyName, value); - }, - - setProperties(hash: object) { - return setProperties(this, hash); - }, - - /** - Begins a grouping of property changes. - - You can use this method to group property changes so that notifications - will not be sent until the changes are finished. If you plan to make a - large number of changes to an object at one time, you should call this - method at the beginning of the changes to begin deferring change - notifications. When you are done making changes, call - `endPropertyChanges()` to deliver the deferred change notifications and end - deferring. - - @method beginPropertyChanges - @return {Observable} - @private - */ - beginPropertyChanges() { - beginPropertyChanges(); - return this; - }, - - /** - Ends a grouping of property changes. - - You can use this method to group property changes so that notifications - will not be sent until the changes are finished. If you plan to make a - large number of changes to an object at one time, you should call - `beginPropertyChanges()` at the beginning of the changes to defer change - notifications. When you are done making changes, call this method to - deliver the deferred change notifications and end deferring. - - @method endPropertyChanges - @return {Observable} - @private - */ - endPropertyChanges() { - endPropertyChanges(); - return this; - }, - - notifyPropertyChange(keyName: string) { - notifyPropertyChange(this, keyName); - return this; - }, - - addObserver( - key: string, - target: object | Function | null, - method?: string | Function, - sync?: boolean - ) { - addObserver(this, key, target, method, sync); - return this; - }, - - removeObserver( - key: string, - target: object | Function | null, - method?: string | Function, - sync?: boolean - ) { - removeObserver(this, key, target, method, sync); - return this; - }, - - /** - Returns `true` if the object currently has observers registered for a - particular key. You can use this method to potentially defer performing - an expensive action until someone begins observing a particular property - on the object. - - @method hasObserverFor - @param {String} key Key to check - @return {Boolean} - @private - */ - hasObserverFor(key: string) { - return hasListeners(this, `${key}:change`); - }, - - incrementProperty(keyName: string, increment = 1) { - assert( - 'Must pass a numeric value to incrementProperty', - !isNaN(parseFloat(String(increment))) && isFinite(increment) - ); - return set(this, keyName, (parseFloat(get(this, keyName)) || 0) + increment); - }, - - decrementProperty(keyName: string, decrement = 1) { - assert( - 'Must pass a numeric value to decrementProperty', - (typeof decrement === 'number' || !isNaN(parseFloat(decrement))) && isFinite(decrement) +const Observable = DeprecatedMixin.create(InternalObservable, { + init() { + this._super(...arguments); + deprecateUntil( + 'The `Observable` mixin is deprecated. Use native classes with tracked properties and native property access instead.', + DEPRECATIONS.DEPRECATE_OBSERVABLE_MIXIN ); - return set(this, keyName, (get(this, keyName) || 0) - decrement); - }, - - toggleProperty(keyName: string) { - return set(this, keyName, !get(this, keyName)); - }, - - cacheFor(keyName: string) { - let meta = peekMeta(this); - return meta !== null ? meta.valueFor(keyName) : undefined; }, }); diff --git a/packages/@ember/object/promise-proxy-mixin.ts b/packages/@ember/object/promise-proxy-mixin.ts index 977d645b68c..60e5bd72747 100644 --- a/packages/@ember/object/promise-proxy-mixin.ts +++ b/packages/@ember/object/promise-proxy-mixin.ts @@ -1,8 +1,7 @@ import { get } from '@ember/-internals/metal/lib/property_get'; import setProperties from '@ember/-internals/metal/lib/set_properties'; import computed from '@ember/-internals/metal/lib/computed'; -import Mixin from '@ember/object/mixin'; -import { INTERNAL_MIXIN_CREATE } from '@ember/-internals/utils/lib/internal-mixin-create'; +import { InternalMixin } from '@ember/object/mixin-internal'; import { DEPRECATIONS, deprecateUntil } from '@ember/-internals/deprecations'; import type { AnyFn, MethodNamesOf } from '@ember/-internals/utility-types'; import type RSVP from 'rsvp'; @@ -214,7 +213,7 @@ interface PromiseProxyMixin { */ finally: this['promise']['finally']; } -const PromiseProxyMixin = Mixin[INTERNAL_MIXIN_CREATE]({ +const PromiseProxyMixin = InternalMixin.create({ init() { this._super(...arguments); diff --git a/packages/@ember/object/proxy.ts b/packages/@ember/object/proxy.ts index be037bfc153..a4904485c0c 100644 --- a/packages/@ember/object/proxy.ts +++ b/packages/@ember/object/proxy.ts @@ -3,7 +3,8 @@ */ import { FrameworkObject } from '@ember/object/-internals'; -import _ProxyMixin from '@ember/-internals/runtime/lib/mixins/-proxy'; +import type _ProxyMixin from '@ember/-internals/runtime/lib/mixins/-proxy'; +import InternalProxyMixin from '@ember/-internals/runtime/lib/mixins/-proxy-internal'; import { DEPRECATIONS, deprecateUntil } from '@ember/-internals/deprecations'; /** @@ -132,6 +133,6 @@ class ObjectProxy extends FrameworkObject { ); } } -ObjectProxy.PrototypeMixin.reopen(_ProxyMixin); +ObjectProxy.PrototypeMixin.reopen(InternalProxyMixin); export default ObjectProxy; diff --git a/packages/@ember/object/tests/evented_test.js b/packages/@ember/object/tests/evented_test.js index 6e2831d95da..51d4fe99283 100644 --- a/packages/@ember/object/tests/evented_test.js +++ b/packages/@ember/object/tests/evented_test.js @@ -31,11 +31,17 @@ moduleFor( DEPRECATIONS.DEPRECATE_EVENTED.isRemoved )} @test works properly on proxy-ish objects`](assert) { let eventedProxyObj; - eventedProxyObj = class extends CoreObject.extend(EventedMixin) { - unknownProperty() { - return true; - } - }.create(); + expectDeprecation( + () => { + eventedProxyObj = class extends CoreObject.extend(EventedMixin) { + unknownProperty() { + return true; + } + }.create(); + }, + /The `Evented` mixin is deprecated/, + DEPRECATIONS.DEPRECATE_EVENTED.isEnabled + ); let noop = function () {}; diff --git a/packages/@ember/object/tests/events_test.js b/packages/@ember/object/tests/events_test.js index c9c600060c6..f8b1b34d6ad 100644 --- a/packages/@ember/object/tests/events_test.js +++ b/packages/@ember/object/tests/events_test.js @@ -15,7 +15,13 @@ moduleFor( }; let obj; - obj = EmberObject.extend(Evented).create(); + expectDeprecation( + () => { + obj = EmberObject.extend(Evented).create(); + }, + /The `Evented` mixin is deprecated/, + DEPRECATIONS.DEPRECATE_EVENTED.isEnabled + ); expectDeprecation( () => { @@ -56,7 +62,13 @@ moduleFor( }; let obj; - obj = EmberObject.extend(Evented).create(); + expectDeprecation( + () => { + obj = EmberObject.extend(Evented).create(); + }, + /The `Evented` mixin is deprecated/, + DEPRECATIONS.DEPRECATE_EVENTED.isEnabled + ); expectDeprecation( () => { @@ -93,7 +105,13 @@ moduleFor( let self, args; let obj; - obj = EmberObject.extend(Evented).create(); + expectDeprecation( + () => { + obj = EmberObject.extend(Evented).create(); + }, + /The `Evented` mixin is deprecated/, + DEPRECATIONS.DEPRECATE_EVENTED.isEnabled + ); expectDeprecation( () => { @@ -125,7 +143,13 @@ moduleFor( let count = 0; let obj; - obj = EmberObject.extend(Evented).create(); + expectDeprecation( + () => { + obj = EmberObject.extend(Evented).create(); + }, + /The `Evented` mixin is deprecated/, + DEPRECATIONS.DEPRECATE_EVENTED.isEnabled + ); expectDeprecation( () => { @@ -170,7 +194,13 @@ moduleFor( let self, args; let obj; - obj = EmberObject.extend(Evented).create(); + expectDeprecation( + () => { + obj = EmberObject.extend(Evented).create(); + }, + /The `Evented` mixin is deprecated/, + DEPRECATIONS.DEPRECATE_EVENTED.isEnabled + ); let target = {}; expectDeprecation( @@ -208,7 +238,13 @@ moduleFor( }; let obj; - obj = EmberObject.extend(Evented).create(); + expectDeprecation( + () => { + obj = EmberObject.extend(Evented).create(); + }, + /The `Evented` mixin is deprecated/, + DEPRECATIONS.DEPRECATE_EVENTED.isEnabled + ); expectDeprecation( () => { @@ -243,9 +279,15 @@ moduleFor( DEPRECATIONS.DEPRECATE_EVENTED.isRemoved )} @test a listener registered with one can be removed with off`](assert) { let obj; - obj = class extends EmberObject.extend(Evented) { - F() {} - }.create(); + expectDeprecation( + () => { + obj = class extends EmberObject.extend(Evented) { + F() {} + }.create(); + }, + /The `Evented` mixin is deprecated/, + DEPRECATIONS.DEPRECATE_EVENTED.isEnabled + ); let F = function () {}; expectDeprecation( @@ -305,7 +347,13 @@ moduleFor( DEPRECATIONS.DEPRECATE_EVENTED.isRemoved )} @test adding and removing listeners should be chainable`](assert) { let obj; - obj = EmberObject.extend(Evented).create(); + expectDeprecation( + () => { + obj = EmberObject.extend(Evented).create(); + }, + /The `Evented` mixin is deprecated/, + DEPRECATIONS.DEPRECATE_EVENTED.isEnabled + ); let F = function () {}; let ret; diff --git a/packages/@ember/object/tests/mixin/deprecation_test.js b/packages/@ember/object/tests/mixin/deprecation_test.js index 9e1229a6530..3ca0380982a 100644 --- a/packages/@ember/object/tests/mixin/deprecation_test.js +++ b/packages/@ember/object/tests/mixin/deprecation_test.js @@ -1,6 +1,6 @@ import EmberObject from '@ember/object'; import Mixin from '@ember/object/mixin'; -import { INTERNAL_MIXIN_CREATE } from '@ember/-internals/utils/lib/internal-mixin-create'; +import { InternalMixin } from '@ember/object/mixin-internal'; import { moduleFor, AbstractTestCase, @@ -42,7 +42,7 @@ moduleFor( let MixinA; expectNoDeprecation(() => { - MixinA = Mixin[INTERNAL_MIXIN_CREATE]({ + MixinA = InternalMixin.create({ foo: 'FOO', }); }); diff --git a/packages/@ember/object/tests/observable_test.js b/packages/@ember/object/tests/observable_test.js index ccb78081bc8..a6c3374cf73 100644 --- a/packages/@ember/object/tests/observable_test.js +++ b/packages/@ember/object/tests/observable_test.js @@ -2,7 +2,6 @@ import { context } from '@ember/-internals/environment'; import { run } from '@ember/runloop'; import { get, computed } from '@ember/object'; import EmberObject, { observer } from '@ember/object'; -import Observable from '@ember/object/observable'; import { A as emberA } from '@ember/array'; import { moduleFor, AbstractTestCase, runLoopSettled } from 'internal-test-helpers'; @@ -33,7 +32,7 @@ import { moduleFor, AbstractTestCase, runLoopSettled } from 'internal-test-helpe let object, objectA, objectB, objectC, objectD, objectE, objectF, lookup; -const ObservableObject = EmberObject.extend(Observable); +const ObservableObject = EmberObject.extend(); const originalLookup = context.lookup; class ObservableTestCase extends AbstractTestCase { @@ -58,7 +57,7 @@ moduleFor( 'object.get()', class extends ObservableTestCase { beforeEach() { - object = ObservableObject.extend(Observable, { + object = ObservableObject.extend({ computed: computed(function () { return 'value'; }), diff --git a/packages/@ember/routing/route.ts b/packages/@ember/routing/route.ts index d3078d53886..3390ca0e0de 100644 --- a/packages/@ember/routing/route.ts +++ b/packages/@ember/routing/route.ts @@ -21,7 +21,7 @@ import EmberObject from '@ember/object'; import Evented from '@ember/object/evented'; import { copyDefaultValue } from '@ember/-internals/routing/route-managers/classic/query-params'; import { meta as metaFor } from '@ember/-internals/meta/lib/meta'; -import ActionHandler from '@ember/-internals/runtime/lib/mixins/action_handler'; +import InternalActionHandler from '@ember/-internals/runtime/lib/mixins/action_handler-internal'; import { isProxy } from '@ember/-internals/utils/lib/is_proxy'; import lookupDescriptor from '@ember/-internals/utils/lib/lookup-descriptor'; import type { AnyFn } from '@ember/-internals/utility-types'; @@ -107,7 +107,7 @@ type MaybeReturnType = T extends AnyFn ? ReturnType : unknown; @public */ // eslint-disable-next-line @typescript-eslint/no-unused-vars -interface Route extends ActionHandler { +interface Route extends InternalActionHandler { /** The `willTransition` action is fired at the beginning of any attempted transition with a `Transition` object as the sole @@ -281,7 +281,7 @@ interface Route extends ActionHandler { error?(error: Error, transition: Transition): boolean | void; } -class Route extends EmberObject.extend(ActionHandler) { +class Route extends EmberObject.extend(InternalActionHandler) { static { // The deprecated Evented mixin is no longer applied, but instances still // provide its methods, so `Evented.detect` must keep returning true. diff --git a/packages/ember/tests/service_injection_test.js b/packages/ember/tests/service_injection_test.js index 1e6033f9e1d..abcd39580fd 100644 --- a/packages/ember/tests/service_injection_test.js +++ b/packages/ember/tests/service_injection_test.js @@ -1,7 +1,7 @@ import { getOwner } from '@ember/-internals/owner'; import Controller from '@ember/controller'; import Service, { service } from '@ember/service'; -import { _ProxyMixin } from '@ember/-internals/runtime'; +import _ProxyMixin from '@ember/-internals/runtime/lib/mixins/-proxy-internal'; import { moduleFor, ApplicationTestCase } from 'internal-test-helpers'; import { computed } from '@ember/object'; import { precompileTemplate } from '@ember/template-compilation'; diff --git a/rollup.config.mjs b/rollup.config.mjs index b1ecfdb13b5..f74fdab472d 100644 --- a/rollup.config.mjs +++ b/rollup.config.mjs @@ -537,6 +537,16 @@ function pruneEmptyBundles() { }; } +// Mixins that Ember deprecates but still uses internally exist twice: a public +// copy that emits the deprecation, and an internal copy that applies silently. +// The internal copies are an implementation detail, so they are kept out of +// `renamed-modules` rather than being advertised as importable module paths. +// Matches `foo-internal.js` and `-internal.js`, but deliberately not the +// long-standing `-internals.js` modules, which stay listed. +function isInternalCopy(name) { + return /(^|\/|-)internal\.js$/.test(name); +} + function packageMeta() { return { name: 'package-meta', @@ -547,7 +557,8 @@ function packageMeta() { (name) => name.startsWith('packages/') && !name.startsWith('packages/shared-chunks/') && - name.endsWith('.js') + name.endsWith('.js') && + !isInternalCopy(name) ) .sort() .map((name) => { From 4fcb39e71e07c3030fad009c00d3b6695f9043e1 Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Thu, 1 Oct 2026 12:46:45 -0400 Subject: [PATCH 2/6] Attempt to guard the isProxy guard in each-in --- .../@ember/-internals/glimmer/lib/helpers/each-in.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/@ember/-internals/glimmer/lib/helpers/each-in.ts b/packages/@ember/-internals/glimmer/lib/helpers/each-in.ts index ed10579f268..630deff238d 100644 --- a/packages/@ember/-internals/glimmer/lib/helpers/each-in.ts +++ b/packages/@ember/-internals/glimmer/lib/helpers/each-in.ts @@ -9,6 +9,7 @@ import type { CapturedArguments } from '@glimmer/interfaces'; import { createComputeRef, valueForRef } from '@glimmer/reference/lib/reference'; import { consumeTag } from '@glimmer/validator/lib/tracking'; import { internalHelper } from './internal-helper'; +import { DEPRECATIONS } from '../../../deprecations'; /** The `{{#each}}` keyword loops over elements in a collection. It is an extension @@ -311,10 +312,12 @@ export default internalHelper(({ positional }: CapturedArguments) => { consumeTag(tagForObject(iterable)); - if (isProxy(iterable)) { - // this is because the each-in doesn't actually get(proxy, 'key') but bypasses it - // and the proxy's tag is lazy updated on access - iterable = _contentFor(iterable); + if (!DEPRECATIONS.DEPRECATE_PROXY_MIXIN.isRemoved) { + if (isProxy(iterable)) { + // this is because the each-in doesn't actually get(proxy, 'key') but bypasses it + // and the proxy's tag is lazy updated on access + iterable = _contentFor(iterable); + } } return new EachInWrapper(iterable); From b5641c6170b56f6c56f14017f887e849bc6c461b Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Tue, 6 Oct 2026 19:18:20 -0400 Subject: [PATCH 3/6] Add missing @deprecated --- packages/@ember/object/observable.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/@ember/object/observable.ts b/packages/@ember/object/observable.ts index 1320fd97574..d4b6bb90eef 100644 --- a/packages/@ember/object/observable.ts +++ b/packages/@ember/object/observable.ts @@ -76,6 +76,7 @@ export type { ObserverMethod } from '@ember/object/observable-internal'; resulting value of the computed property is unchanged. This is necessary because computed properties are not computed until `get` is called. + @deprecated @class Observable @public */ From d7863d86e13cb6013c54f2604d1d7a2a43c9707f Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Sat, 10 Oct 2026 12:01:38 -0400 Subject: [PATCH 4/6] Use the public mixins in tests, not the internal copies Tests imported the silent `-internal` copies of the deprecated mixins. Such a test stays after the deprecation is removed, and it keeps the internal copy in use. These tests now import the public mixin, expect its deprecation, and use `testUnless(...isRemoved)`. They go away with the deprecation. - `EmberArray` in `runtime/tests/mixins/array_test.js` - `MutableArray` in `glimmer/tests/integration/helpers/tracked-test.js` - `ContainerProxyMixin` in `container_proxy_test.js` - `TargetActionSupport` in `target_action_support_test.js` - `_ProxyMixin` in `ember/tests/service_injection_test.js` `TestArray#init` in `array_test.js` now calls `super.init`. Without that call, the deprecation of the mixin does not fire. Three tests in `array_test.js` do not use the mixin. They moved to their own module, so they still run after the removal. `mixin/deprecation_test.js` still imports `InternalMixin`. That test checks that the internal constructor does not fire the deprecation. Co-Authored-By: Claude Opus 5.5 --- .../tests/integration/helpers/tracked-test.js | 20 ++++- .../runtime/tests/mixins/array_test.js | 78 +++++++++++++++---- .../tests/mixins/container_proxy_test.js | 21 +++-- .../mixins/target_action_support_test.js | 29 ++++--- .../ember/tests/service_injection_test.js | 19 ++++- 5 files changed, 127 insertions(+), 40 deletions(-) diff --git a/packages/@ember/-internals/glimmer/tests/integration/helpers/tracked-test.js b/packages/@ember/-internals/glimmer/tests/integration/helpers/tracked-test.js index 2aed389bace..2fd141bebd5 100644 --- a/packages/@ember/-internals/glimmer/tests/integration/helpers/tracked-test.js +++ b/packages/@ember/-internals/glimmer/tests/integration/helpers/tracked-test.js @@ -1,9 +1,16 @@ import EmberObject from '@ember/object'; import { A } from '@ember/array'; +import MutableArray from '@ember/array/mutable'; import { tracked, notifyPropertyChange } from '@ember/-internals/metal'; -import { InternalMutableArray as MutableArray } from '@ember/array/index-internal'; import Service, { service } from '@ember/service'; -import { moduleFor, RenderingTestCase, runTask } from 'internal-test-helpers'; +import { + expectDeprecation, + moduleFor, + RenderingTestCase, + runTask, + testUnless, +} from 'internal-test-helpers'; +import { DEPRECATIONS } from '@ember/-internals/deprecations'; import { precompileTemplate } from '@ember/template-compilation'; import { setComponentTemplate } from '@glimmer/manager'; @@ -172,7 +179,14 @@ moduleFor( this.assertText('1, 2, 3, 4'); } - '@test custom ember array properties rerender when updated'() { + [`${testUnless( + DEPRECATIONS.DEPRECATE_MUTABLE_ARRAY_MIXIN.isRemoved + )} @test custom ember array properties rerender when updated`]() { + expectDeprecation( + /The `MutableArray` mixin is deprecated/, + DEPRECATIONS.DEPRECATE_MUTABLE_ARRAY_MIXIN.isEnabled + ); + let CustomArray = class extends EmberObject.extend(MutableArray) { init() { super.init(...arguments); diff --git a/packages/@ember/-internals/runtime/tests/mixins/array_test.js b/packages/@ember/-internals/runtime/tests/mixins/array_test.js index 3ec1baed666..8e47fdd49a5 100644 --- a/packages/@ember/-internals/runtime/tests/mixins/array_test.js +++ b/packages/@ember/-internals/runtime/tests/mixins/array_test.js @@ -7,9 +7,21 @@ import { arrayContentWillChange, } from '@ember/-internals/metal'; import EmberObject, { get, set, computed, observer as emberObserver } from '@ember/object'; -import { A as emberA } from '@ember/array'; -import { InternalEmberArray as EmberArray } from '@ember/array/index-internal'; -import { moduleFor, AbstractTestCase, runLoopSettled } from 'internal-test-helpers'; +import EmberArray, { A as emberA } from '@ember/array'; +import { + expectDeprecation, + moduleFor, + AbstractTestCase, + runLoopSettled, + testUnless, +} from 'internal-test-helpers'; +import { DEPRECATIONS } from '../../../deprecations'; + +const MIXIN = DEPRECATIONS.DEPRECATE_EMBER_ARRAY_MIXIN; + +function expectMixinDeprecation() { + expectDeprecation(/The `EmberArray` mixin is deprecated/, MIXIN.isEnabled); +} /* Implement a basic fake mutable array. This validates that any non-native @@ -19,6 +31,7 @@ const TestArray = class extends EmberObject.extend(EmberArray) { _content = null; init() { + super.init(...arguments); this._content = this._content || []; } @@ -50,7 +63,11 @@ const TestArray = class extends EmberObject.extend(EmberArray) { moduleFor( 'Ember.Array', class extends AbstractTestCase { - ['@test the return value of slice has Ember.Array applied'](assert) { + [`${testUnless(MIXIN.isRemoved)} @test the return value of slice has Ember.Array applied`]( + assert + ) { + expectMixinDeprecation(); + let x = EmberObject.extend(EmberArray).create({ length: 0, }); @@ -58,7 +75,9 @@ moduleFor( assert.equal(EmberArray.detect(y), true, 'mixin should be applied'); } - ['@test slice supports negative index arguments'](assert) { + [`${testUnless(MIXIN.isRemoved)} @test slice supports negative index arguments`](assert) { + expectMixinDeprecation(); + let testArray = TestArray.create({ _content: [1, 2, 3, 4] }); assert.deepEqual(testArray.slice(-2), [3, 4], 'slice(-2)'); @@ -97,7 +116,9 @@ let obj, observer; moduleFor( 'mixins/array/arrayContent[Will|Did]Change', class extends AbstractTestCase { - async ['@test should notify observers of []'](assert) { + async [`${testUnless(MIXIN.isRemoved)} @test should notify observers of []`](assert) { + expectMixinDeprecation(); + obj = DummyArray.extend({ enumerablePropertyDidChange: emberObserver('[]', function () { this._count++; @@ -130,6 +151,8 @@ moduleFor( 'notify observers of length', class extends AbstractTestCase { beforeEach(assert) { + expectMixinDeprecation(); + obj = DummyArray.extend({ lengthDidChange: emberObserver('length', function () { this._after++; @@ -146,7 +169,9 @@ moduleFor( obj = undefined; } - async ['@test should notify observers when call with no params'](assert) { + async [`${testUnless(MIXIN.isRemoved)} @test should notify observers when call with no params`]( + assert + ) { arrayContentWillChange(obj); await runLoopSettled(); @@ -159,7 +184,9 @@ moduleFor( } // API variation that included items only - async ['@test should not notify when passed lengths are same'](assert) { + async [`${testUnless(MIXIN.isRemoved)} @test should not notify when passed lengths are same`]( + assert + ) { arrayContentWillChange(obj, 0, 1, 1); await runLoopSettled(); @@ -171,7 +198,9 @@ moduleFor( assert.equal(obj._after, 0); } - async ['@test should notify when passed lengths are different'](assert) { + async [`${testUnless(MIXIN.isRemoved)} @test should notify when passed lengths are different`]( + assert + ) { arrayContentWillChange(obj, 0, 1, 2); await runLoopSettled(); @@ -193,6 +222,8 @@ moduleFor( 'notify array observers (internal)', class extends AbstractTestCase { beforeEach(assert) { + expectMixinDeprecation(); + obj = DummyArray.create(); observer = class extends EmberObject { @@ -220,7 +251,9 @@ moduleFor( obj = observer = null; } - ['@test should notify array observers when called with no params'](assert) { + [`${testUnless(MIXIN.isRemoved)} @test should notify array observers when called with no params`]( + assert + ) { arrayContentWillChange(obj); assert.deepEqual(observer._before, [obj, 0, -1, -1]); @@ -229,7 +262,9 @@ moduleFor( } // API variation that included items only - ['@test should notify when called with same length items'](assert) { + [`${testUnless(MIXIN.isRemoved)} @test should notify when called with same length items`]( + assert + ) { arrayContentWillChange(obj, 0, 1, 1); assert.deepEqual(observer._before, [obj, 0, 1, 1]); @@ -237,7 +272,9 @@ moduleFor( assert.deepEqual(observer._after, [obj, 0, 1, 1]); } - ['@test should notify when called with diff length items'](assert) { + [`${testUnless(MIXIN.isRemoved)} @test should notify when called with diff length items`]( + assert + ) { arrayContentWillChange(obj, 0, 2, 1); assert.deepEqual(observer._before, [obj, 0, 2, 1]); @@ -245,7 +282,7 @@ moduleFor( assert.deepEqual(observer._after, [obj, 0, 2, 1]); } - ['@test removing array observer should disable'](assert) { + [`${testUnless(MIXIN.isRemoved)} @test removing array observer should disable`](assert) { removeArrayObserver(obj, observer, { willChange: 'arrayWillChange', didChange: 'arrayDidChange', @@ -269,6 +306,8 @@ moduleFor( 'EmberArray.@each support', class extends AbstractTestCase { beforeEach() { + expectMixinDeprecation(); + ary = TestArray.create({ _content: [ { isDone: true, desc: 'Todo 1' }, @@ -284,7 +323,9 @@ moduleFor( ary = null; } - async ['@test adding an object should notify (@each.isDone)'](assert) { + async [`${testUnless(MIXIN.isRemoved)} @test adding an object should notify (@each.isDone)`]( + assert + ) { let called = 0; let observerObject = EmberObject.create({ @@ -306,7 +347,9 @@ moduleFor( assert.equal(called, 1, 'calls observer when object is pushed'); } - async ['@test using @each to observe arrays that does not return objects raise error'](assert) { + async [`${testUnless(MIXIN.isRemoved)} @test using @each to observe arrays that does not return objects raise error`]( + assert + ) { let called = 0; let observerObject = EmberObject.create({ @@ -333,7 +376,12 @@ moduleFor( await runLoopSettled(); assert.equal(called, 0, 'not calls observer when object is pushed'); } + } +); +moduleFor( + '@each support', + class extends AbstractTestCase { ['@test `objectAt` returns correct object'](assert) { let arr = ['first', 'second', 'third', 'fourth']; assert.equal(objectAt(arr, 2), 'third'); diff --git a/packages/@ember/-internals/runtime/tests/mixins/container_proxy_test.js b/packages/@ember/-internals/runtime/tests/mixins/container_proxy_test.js index 445e4eceedc..a8f3b97eec9 100644 --- a/packages/@ember/-internals/runtime/tests/mixins/container_proxy_test.js +++ b/packages/@ember/-internals/runtime/tests/mixins/container_proxy_test.js @@ -1,15 +1,20 @@ import { getOwner } from '@ember/-internals/owner'; import { Container, Registry } from '@ember/-internals/container'; -import ContainerProxy from '../../lib/mixins/container_proxy-internal'; +import ContainerProxy from '../../lib/mixins/container_proxy'; import EmberObject from '@ember/object'; import { run, schedule } from '@ember/runloop'; -import { moduleFor, AbstractTestCase } from 'internal-test-helpers'; +import { expectDeprecation, moduleFor, AbstractTestCase, testUnless } from 'internal-test-helpers'; +import { DEPRECATIONS } from '../../../deprecations'; import { destroy } from '@glimmer/destroyable'; +const MIXIN = DEPRECATIONS.DEPRECATE_CONTAINER_PROXY_MIXIN; + moduleFor( '@ember/-internals/runtime/mixins/container_proxy', class extends AbstractTestCase { beforeEach() { + expectDeprecation(/The `ContainerProxyMixin` mixin is deprecated/, MIXIN.isEnabled); + this.Owner = EmberObject.extend(ContainerProxy); this.instance = this.Owner.create(); @@ -20,14 +25,14 @@ moduleFor( }); } - ['@test provides ownerInjection helper method'](assert) { + [`${testUnless(MIXIN.isRemoved)} @test provides ownerInjection helper method`](assert) { let result = this.instance.ownerInjection(); assert.equal(getOwner(result), this.instance, 'returns an object with an associated owner'); } - ['@test actions queue completes before destruction'](assert) { - assert.expect(1); + [`${testUnless(MIXIN.isRemoved)} @test actions queue completes before destruction`](assert) { + assert.expect(2); this.registry.register( 'service:auth', @@ -46,10 +51,12 @@ moduleFor( }); } - '@test being destroyed by @ember/destroyable properly destroys the container and created instances'( + [`${testUnless( + MIXIN.isRemoved + )} @test being destroyed by @ember/destroyable properly destroys the container and created instances`]( assert ) { - assert.expect(1); + assert.expect(2); this.registry.register( 'service:foo', diff --git a/packages/@ember/-internals/runtime/tests/mixins/target_action_support_test.js b/packages/@ember/-internals/runtime/tests/mixins/target_action_support_test.js index e2b205a93db..962635b1f05 100644 --- a/packages/@ember/-internals/runtime/tests/mixins/target_action_support_test.js +++ b/packages/@ember/-internals/runtime/tests/mixins/target_action_support_test.js @@ -1,6 +1,6 @@ import { context } from '@ember/-internals/environment'; import EmberObject from '@ember/object'; -import TargetActionSupport from '../../lib/mixins/target_action_support-internal'; +import TargetActionSupport from '../../lib/mixins/target_action_support'; import { expectDeprecation, moduleFor, AbstractTestCase, testUnless } from 'internal-test-helpers'; import { DEPRECATIONS } from '../../../deprecations'; @@ -12,6 +12,11 @@ moduleFor( class extends AbstractTestCase { beforeEach() { context.lookup = lookup = {}; + + expectDeprecation( + /The `TargetActionSupport` mixin is deprecated/, + DEPRECATIONS.DEPRECATE_TARGET_ACTION_SUPPORT.isEnabled + ); } afterEach() { @@ -21,7 +26,7 @@ moduleFor( [`${testUnless( DEPRECATIONS.DEPRECATE_TARGET_ACTION_SUPPORT.isRemoved )} @test it should return false if no target or action are specified`](assert) { - assert.expect(2); + assert.expect(3); expectDeprecation( /Calling `triggerAction` on/, @@ -36,7 +41,7 @@ moduleFor( [`${testUnless( DEPRECATIONS.DEPRECATE_TARGET_ACTION_SUPPORT.isRemoved )} @test it should support actions specified as strings`](assert) { - assert.expect(3); + assert.expect(4); expectDeprecation( /Calling `triggerAction` on/, @@ -59,7 +64,7 @@ moduleFor( [`${testUnless( DEPRECATIONS.DEPRECATE_TARGET_ACTION_SUPPORT.isRemoved )} @test it should invoke the send() method on objects that implement it`](assert) { - assert.expect(4); + assert.expect(5); expectDeprecation( /Calling `triggerAction` on/, @@ -83,7 +88,7 @@ moduleFor( [`${testUnless( DEPRECATIONS.DEPRECATE_TARGET_ACTION_SUPPORT.isRemoved )} @test it should find targets specified using a property path`](assert) { - assert.expect(3); + assert.expect(4); expectDeprecation( /Calling `triggerAction` on/, @@ -110,7 +115,7 @@ moduleFor( [`${testUnless( DEPRECATIONS.DEPRECATE_TARGET_ACTION_SUPPORT.isRemoved )} @test it should use an actionContext object specified as a property on the object`](assert) { - assert.expect(3); + assert.expect(4); expectDeprecation( /Calling `triggerAction` on/, @@ -136,7 +141,7 @@ moduleFor( [`${testUnless( DEPRECATIONS.DEPRECATE_TARGET_ACTION_SUPPORT.isRemoved )} @test it should find an actionContext specified as a property path`](assert) { - assert.expect(3); + assert.expect(4); expectDeprecation( /Calling `triggerAction` on/, @@ -163,7 +168,7 @@ moduleFor( [`${testUnless( DEPRECATIONS.DEPRECATE_TARGET_ACTION_SUPPORT.isRemoved )} @test it should use the target specified in the argument`](assert) { - assert.expect(3); + assert.expect(4); expectDeprecation( /Calling `triggerAction` on/, @@ -188,7 +193,7 @@ moduleFor( [`${testUnless( DEPRECATIONS.DEPRECATE_TARGET_ACTION_SUPPORT.isRemoved )} @test it should use the action specified in the argument`](assert) { - assert.expect(3); + assert.expect(4); expectDeprecation( /Calling `triggerAction` on/, @@ -212,7 +217,7 @@ moduleFor( [`${testUnless( DEPRECATIONS.DEPRECATE_TARGET_ACTION_SUPPORT.isRemoved )} @test it should use the actionContext specified in the argument`](assert) { - assert.expect(3); + assert.expect(4); expectDeprecation( /Calling `triggerAction` on/, @@ -238,7 +243,7 @@ moduleFor( [`${testUnless( DEPRECATIONS.DEPRECATE_TARGET_ACTION_SUPPORT.isRemoved )} @test it should allow multiple arguments from actionContext`](assert) { - assert.expect(4); + assert.expect(5); expectDeprecation( /Calling `triggerAction` on/, @@ -272,7 +277,7 @@ moduleFor( [`${testUnless( DEPRECATIONS.DEPRECATE_TARGET_ACTION_SUPPORT.isRemoved )} @test it should use a null value specified in the actionContext argument`](assert) { - assert.expect(3); + assert.expect(4); expectDeprecation( /Calling `triggerAction` on/, diff --git a/packages/ember/tests/service_injection_test.js b/packages/ember/tests/service_injection_test.js index abcd39580fd..f30de7e8507 100644 --- a/packages/ember/tests/service_injection_test.js +++ b/packages/ember/tests/service_injection_test.js @@ -1,8 +1,14 @@ import { getOwner } from '@ember/-internals/owner'; import Controller from '@ember/controller'; import Service, { service } from '@ember/service'; -import _ProxyMixin from '@ember/-internals/runtime/lib/mixins/-proxy-internal'; -import { moduleFor, ApplicationTestCase } from 'internal-test-helpers'; +import { _ProxyMixin } from '@ember/-internals/runtime'; +import { + expectDeprecation, + moduleFor, + ApplicationTestCase, + testUnless, +} from 'internal-test-helpers'; +import { DEPRECATIONS } from '@ember/-internals/deprecations'; import { computed } from '@ember/object'; import { precompileTemplate } from '@ember/template-compilation'; @@ -27,7 +33,14 @@ moduleFor( assert.ok(controller.get('myService') instanceof MyService); } - async ['@test Service can be an object proxy and access owner in init GH#16484'](assert) { + async [`${testUnless( + DEPRECATIONS.DEPRECATE_PROXY_MIXIN.isRemoved + )} @test Service can be an object proxy and access owner in init GH#16484`](assert) { + expectDeprecation( + /The `ProxyMixin` mixin is deprecated/, + DEPRECATIONS.DEPRECATE_PROXY_MIXIN.isEnabled + ); + let serviceOwner; this.add( From 44e5959da023aa320824ec0c3ba1c22d2835e294 Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Sat, 10 Oct 2026 12:37:43 -0400 Subject: [PATCH 5/6] Fix the assertion count in the TargetActionSupport tests A production build runs one assertion for all deprecation expectations of a test. A development build runs one assertion for each expectation. Two expectations made `assert.expect` fail in the production build jobs. Each test now has one expectation. Its pattern matches the mixin deprecation and the `triggerAction` deprecation. The `assert.expect` counts are back to their values on the base branch. Co-Authored-By: Claude Opus 5.5 --- .../mixins/target_action_support_test.js | 86 ++++++------------- 1 file changed, 26 insertions(+), 60 deletions(-) diff --git a/packages/@ember/-internals/runtime/tests/mixins/target_action_support_test.js b/packages/@ember/-internals/runtime/tests/mixins/target_action_support_test.js index 962635b1f05..d1ea1cf96a0 100644 --- a/packages/@ember/-internals/runtime/tests/mixins/target_action_support_test.js +++ b/packages/@ember/-internals/runtime/tests/mixins/target_action_support_test.js @@ -4,6 +4,10 @@ import TargetActionSupport from '../../lib/mixins/target_action_support'; import { expectDeprecation, moduleFor, AbstractTestCase, testUnless } from 'internal-test-helpers'; import { DEPRECATIONS } from '../../../deprecations'; +// A production build runs one assertion for all deprecation expectations. +// One pattern keeps the assertion count equal in both builds. +const DEPRECATION = /The `TargetActionSupport` mixin is deprecated|Calling `triggerAction` on/; + let originalLookup = context.lookup; let lookup; @@ -12,11 +16,6 @@ moduleFor( class extends AbstractTestCase { beforeEach() { context.lookup = lookup = {}; - - expectDeprecation( - /The `TargetActionSupport` mixin is deprecated/, - DEPRECATIONS.DEPRECATE_TARGET_ACTION_SUPPORT.isEnabled - ); } afterEach() { @@ -26,12 +25,9 @@ moduleFor( [`${testUnless( DEPRECATIONS.DEPRECATE_TARGET_ACTION_SUPPORT.isRemoved )} @test it should return false if no target or action are specified`](assert) { - assert.expect(3); + assert.expect(2); - expectDeprecation( - /Calling `triggerAction` on/, - DEPRECATIONS.DEPRECATE_TARGET_ACTION_SUPPORT.isEnabled - ); + expectDeprecation(DEPRECATION, DEPRECATIONS.DEPRECATE_TARGET_ACTION_SUPPORT.isEnabled); let obj = EmberObject.extend(TargetActionSupport).create(); @@ -41,12 +37,9 @@ moduleFor( [`${testUnless( DEPRECATIONS.DEPRECATE_TARGET_ACTION_SUPPORT.isRemoved )} @test it should support actions specified as strings`](assert) { - assert.expect(4); + assert.expect(3); - expectDeprecation( - /Calling `triggerAction` on/, - DEPRECATIONS.DEPRECATE_TARGET_ACTION_SUPPORT.isEnabled - ); + expectDeprecation(DEPRECATION, DEPRECATIONS.DEPRECATE_TARGET_ACTION_SUPPORT.isEnabled); let obj = EmberObject.extend(TargetActionSupport).create({ target: EmberObject.create({ @@ -64,12 +57,9 @@ moduleFor( [`${testUnless( DEPRECATIONS.DEPRECATE_TARGET_ACTION_SUPPORT.isRemoved )} @test it should invoke the send() method on objects that implement it`](assert) { - assert.expect(5); + assert.expect(4); - expectDeprecation( - /Calling `triggerAction` on/, - DEPRECATIONS.DEPRECATE_TARGET_ACTION_SUPPORT.isEnabled - ); + expectDeprecation(DEPRECATION, DEPRECATIONS.DEPRECATE_TARGET_ACTION_SUPPORT.isEnabled); let obj = EmberObject.extend(TargetActionSupport).create({ target: EmberObject.create({ @@ -88,12 +78,9 @@ moduleFor( [`${testUnless( DEPRECATIONS.DEPRECATE_TARGET_ACTION_SUPPORT.isRemoved )} @test it should find targets specified using a property path`](assert) { - assert.expect(4); + assert.expect(3); - expectDeprecation( - /Calling `triggerAction` on/, - DEPRECATIONS.DEPRECATE_TARGET_ACTION_SUPPORT.isEnabled - ); + expectDeprecation(DEPRECATION, DEPRECATIONS.DEPRECATE_TARGET_ACTION_SUPPORT.isEnabled); let Test = {}; lookup.Test = Test; @@ -115,12 +102,9 @@ moduleFor( [`${testUnless( DEPRECATIONS.DEPRECATE_TARGET_ACTION_SUPPORT.isRemoved )} @test it should use an actionContext object specified as a property on the object`](assert) { - assert.expect(4); + assert.expect(3); - expectDeprecation( - /Calling `triggerAction` on/, - DEPRECATIONS.DEPRECATE_TARGET_ACTION_SUPPORT.isEnabled - ); + expectDeprecation(DEPRECATION, DEPRECATIONS.DEPRECATE_TARGET_ACTION_SUPPORT.isEnabled); let obj = EmberObject.extend(TargetActionSupport).create({ action: 'anEvent', @@ -141,12 +125,9 @@ moduleFor( [`${testUnless( DEPRECATIONS.DEPRECATE_TARGET_ACTION_SUPPORT.isRemoved )} @test it should find an actionContext specified as a property path`](assert) { - assert.expect(4); + assert.expect(3); - expectDeprecation( - /Calling `triggerAction` on/, - DEPRECATIONS.DEPRECATE_TARGET_ACTION_SUPPORT.isEnabled - ); + expectDeprecation(DEPRECATION, DEPRECATIONS.DEPRECATE_TARGET_ACTION_SUPPORT.isEnabled); let Test = {}; lookup.Test = Test; @@ -168,12 +149,9 @@ moduleFor( [`${testUnless( DEPRECATIONS.DEPRECATE_TARGET_ACTION_SUPPORT.isRemoved )} @test it should use the target specified in the argument`](assert) { - assert.expect(4); + assert.expect(3); - expectDeprecation( - /Calling `triggerAction` on/, - DEPRECATIONS.DEPRECATE_TARGET_ACTION_SUPPORT.isEnabled - ); + expectDeprecation(DEPRECATION, DEPRECATIONS.DEPRECATE_TARGET_ACTION_SUPPORT.isEnabled); let targetObj = EmberObject.create({ anEvent() { @@ -193,12 +171,9 @@ moduleFor( [`${testUnless( DEPRECATIONS.DEPRECATE_TARGET_ACTION_SUPPORT.isRemoved )} @test it should use the action specified in the argument`](assert) { - assert.expect(4); + assert.expect(3); - expectDeprecation( - /Calling `triggerAction` on/, - DEPRECATIONS.DEPRECATE_TARGET_ACTION_SUPPORT.isEnabled - ); + expectDeprecation(DEPRECATION, DEPRECATIONS.DEPRECATE_TARGET_ACTION_SUPPORT.isEnabled); let obj = EmberObject.extend(TargetActionSupport).create({ target: EmberObject.create({ @@ -217,12 +192,9 @@ moduleFor( [`${testUnless( DEPRECATIONS.DEPRECATE_TARGET_ACTION_SUPPORT.isRemoved )} @test it should use the actionContext specified in the argument`](assert) { - assert.expect(4); + assert.expect(3); - expectDeprecation( - /Calling `triggerAction` on/, - DEPRECATIONS.DEPRECATE_TARGET_ACTION_SUPPORT.isEnabled - ); + expectDeprecation(DEPRECATION, DEPRECATIONS.DEPRECATE_TARGET_ACTION_SUPPORT.isEnabled); let context = {}; let obj = EmberObject.extend(TargetActionSupport).create({ @@ -243,12 +215,9 @@ moduleFor( [`${testUnless( DEPRECATIONS.DEPRECATE_TARGET_ACTION_SUPPORT.isRemoved )} @test it should allow multiple arguments from actionContext`](assert) { - assert.expect(5); + assert.expect(4); - expectDeprecation( - /Calling `triggerAction` on/, - DEPRECATIONS.DEPRECATE_TARGET_ACTION_SUPPORT.isEnabled - ); + expectDeprecation(DEPRECATION, DEPRECATIONS.DEPRECATE_TARGET_ACTION_SUPPORT.isEnabled); let param1 = 'someParam'; let param2 = 'someOtherParam'; @@ -277,12 +246,9 @@ moduleFor( [`${testUnless( DEPRECATIONS.DEPRECATE_TARGET_ACTION_SUPPORT.isRemoved )} @test it should use a null value specified in the actionContext argument`](assert) { - assert.expect(4); + assert.expect(3); - expectDeprecation( - /Calling `triggerAction` on/, - DEPRECATIONS.DEPRECATE_TARGET_ACTION_SUPPORT.isEnabled - ); + expectDeprecation(DEPRECATION, DEPRECATIONS.DEPRECATE_TARGET_ACTION_SUPPORT.isEnabled); let obj = EmberObject.extend(TargetActionSupport).create({ target: EmberObject.create({ From d9562ed9802f847f876be9ff8fe02fec103b242e Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Sat, 10 Oct 2026 12:24:33 -0400 Subject: [PATCH 6/6] Deprecate A(), the array computed macros, and Ember array use on query param arrays (RFC 1114) RFC 1114 deprecates Ember Array. The mixins (EmberArray, MutableArray, NativeArray, Enumerable) and ArrayProxy already have deprecations. This adds the parts that had none. - `A` from `@ember/array` reports `deprecate-ember-array-a`. - The 14 array macros from `@ember/object/computed` report `deprecate-array-computed-macros`. Each macro reports its own name. `mapBy`, `filterBy` and `union` no longer call another public macro, so one use gives one deprecation. - Query param arrays (the copy of a default value, and a value from the URL) are Ember arrays that report `deprecate-query-param-ember-array` when application code uses an Ember array member, for example `pushObject` or `firstObject`. One call from application code gives one report. All three are available in 7.5.0, are not enabled, and go away in 8.0.0. Each path that gives application code an Ember array now reports, or gives a native array. The data adapter hooks return native arrays, and `releaseMethods` is a native array. Tests about Ember arrays use the public `A`, expect its deprecation, and do not run when it is removed. Shared fixtures of the Ember array suites silence the deprecation, the same way as the `ArrayProxy` fixtures. Co-Authored-By: Claude Opus 5.5 --- .../@ember/-internals/deprecations/index.ts | 21 + .../integration/components/tracked-test.js | 9 +- .../tests/integration/helpers/tracked-test.js | 10 +- .../tests/integration/syntax/each-test.js | 45 +- .../tests/utils/shared-conditional-tests.js | 22 +- .../-internals/metal/tests/alias_test.js | 17 +- .../route-managers/classic/query-params.ts | 21 +- .../runtime/tests/array/any-test.js | 3 +- .../-internals/runtime/tests/helpers/array.js | 16 +- .../mixins/observable/chained_test.js | 17 +- .../runtime/tests/mixins/array_test.js | 13 +- .../tests/mixins/mutable_enumerable_test.js | 6 +- .../tests/mutable-array/removeObjects-test.js | 3 +- .../array_proxy/arranged_content_test.js | 115 +- .../system/array_proxy/content_change_test.js | 19 +- .../tests/system/array_proxy/length_test.js | 51 +- .../tests/system/native_array/a_test.js | 22 +- .../tests/system/native_array/replace_test.js | 15 +- packages/@ember/array/index-internal.ts | 103 +- packages/@ember/array/index.ts | 41 +- packages/@ember/debug/data-adapter.ts | 29 +- .../enumerable/tests/enumerable_test.js | 6 +- .../lib/computed/reduce_computed_macros.ts | 171 +- .../tests/computed/computed_macros_test.js | 7 +- .../computed/reduce_computed_macros_test.js | 3895 +++++++++-------- packages/@ember/object/tests/computed_test.js | 11 +- .../@ember/object/tests/observable_test.js | 17 +- packages/@ember/routing/router.ts | 4 +- .../ember/tests/routing/query_params_test.js | 24 +- 29 files changed, 2537 insertions(+), 2196 deletions(-) diff --git a/packages/@ember/-internals/deprecations/index.ts b/packages/@ember/-internals/deprecations/index.ts index fa1e86489b1..e65c517ec35 100644 --- a/packages/@ember/-internals/deprecations/index.ts +++ b/packages/@ember/-internals/deprecations/index.ts @@ -210,6 +210,27 @@ export const DEPRECATIONS = { until: '8.0.0', url: 'https://deprecations.emberjs.com/id/deprecate-native-array-mixin', }), + DEPRECATE_EMBER_ARRAY_A: deprecation({ + id: 'deprecate-ember-array-a', + for: 'ember-source', + since: { available: '7.5.0' }, + until: '8.0.0', + url: 'https://deprecations.emberjs.com/id/deprecate-ember-array-a', + }), + DEPRECATE_QUERY_PARAM_EMBER_ARRAY: deprecation({ + id: 'deprecate-query-param-ember-array', + for: 'ember-source', + since: { available: '7.5.0' }, + until: '8.0.0', + url: 'https://deprecations.emberjs.com/id/deprecate-query-param-ember-array', + }), + DEPRECATE_ARRAY_COMPUTED_MACROS: deprecation({ + id: 'deprecate-array-computed-macros', + for: 'ember-source', + since: { available: '7.5.0' }, + until: '8.0.0', + url: 'https://deprecations.emberjs.com/id/deprecate-array-computed-macros', + }), DEPRECATE_CONTROLLER_MIXIN: deprecation({ id: 'deprecate-controller-mixin', for: 'ember-source', diff --git a/packages/@ember/-internals/glimmer/tests/integration/components/tracked-test.js b/packages/@ember/-internals/glimmer/tests/integration/components/tracked-test.js index 7a33c7dbd17..295cec50409 100644 --- a/packages/@ember/-internals/glimmer/tests/integration/components/tracked-test.js +++ b/packages/@ember/-internals/glimmer/tests/integration/components/tracked-test.js @@ -20,6 +20,8 @@ import Component from '@glimmer/component'; import { precompileTemplate } from '@ember/template-compilation'; import { setComponentTemplate } from '@glimmer/manager'; +const EMBER_A = DEPRECATIONS.DEPRECATE_EMBER_ARRAY_A; + moduleFor( 'Component Tracked Properties', class extends RenderingTestCase { @@ -98,9 +100,10 @@ moduleFor( } [`${testUnless( - DEPRECATIONS.DEPRECATE_ARRAY_PROXY.isRemoved + DEPRECATIONS.DEPRECATE_ARRAY_PROXY.isRemoved || EMBER_A.isRemoved )} @test creating an array proxy inside a tracking context and immediately updating its content before usage does not trigger backtracking assertion`]() { expectDeprecation(/`ArrayProxy` is deprecated/, DEPRECATIONS.DEPRECATE_ARRAY_PROXY.isEnabled); + expectDeprecation(/`A` from `@ember\/array` is deprecated/, EMBER_A.isEnabled); class LoaderComponent extends Component { get data() { @@ -354,7 +357,9 @@ moduleFor( this.assertText('1'); } - '@test array properties rerender when updated'() { + [`${testUnless(EMBER_A.isRemoved)} @test array properties rerender when updated`]() { + expectDeprecation(/`A` from `@ember\/array` is deprecated/, EMBER_A.isEnabled); + class NumListComponent extends Component { @tracked numbers = A([1, 2, 3]); diff --git a/packages/@ember/-internals/glimmer/tests/integration/helpers/tracked-test.js b/packages/@ember/-internals/glimmer/tests/integration/helpers/tracked-test.js index 2fd141bebd5..767e2882815 100644 --- a/packages/@ember/-internals/glimmer/tests/integration/helpers/tracked-test.js +++ b/packages/@ember/-internals/glimmer/tests/integration/helpers/tracked-test.js @@ -16,6 +16,8 @@ import { setComponentTemplate } from '@glimmer/manager'; import Component from '@glimmer/component'; +const EMBER_A = DEPRECATIONS.DEPRECATE_EMBER_ARRAY_A; + moduleFor( 'Helper Tracked Properties', class extends RenderingTestCase { @@ -147,7 +149,9 @@ moduleFor( assert.strictEqual(computeCount, 2, 'compute is called exactly 2 times'); } - '@test array properties rerender when updated'() { + [`${testUnless(EMBER_A.isRemoved)} @test array properties rerender when updated`]() { + expectDeprecation(/`A` from `@ember\/array` is deprecated/, EMBER_A.isEnabled); + class NumListComponent extends Component { @tracked numbers = A([1, 2, 3]); @@ -402,7 +406,9 @@ moduleFor( this.assertText('sal-value'); } - '@test each-in autotracks arrays acorrectly'() { + [`${testUnless(EMBER_A.isRemoved)} @test each-in autotracks arrays acorrectly`]() { + expectDeprecation(/`A` from `@ember\/array` is deprecated/, EMBER_A.isEnabled); + let obj = EmberObject.create({ arr: A([1]) }); this.owner.register( diff --git a/packages/@ember/-internals/glimmer/tests/integration/syntax/each-test.js b/packages/@ember/-internals/glimmer/tests/integration/syntax/each-test.js index db550e99be9..5cd9ba15b2e 100644 --- a/packages/@ember/-internals/glimmer/tests/integration/syntax/each-test.js +++ b/packages/@ember/-internals/glimmer/tests/integration/syntax/each-test.js @@ -6,12 +6,13 @@ import { runTask, ignoreDeprecation, defineSimpleModifier, + testUnless, } from 'internal-test-helpers'; import { DEPRECATIONS } from '@ember/-internals/deprecations'; import { notifyPropertyChange } from '@ember/-internals/metal'; import { get, set, computed } from '@ember/object'; -import { A as emberA } from '@ember/array'; +import { A } from '@ember/array'; import ArrayProxy from '@ember/array/proxy'; import { RSVP } from '@ember/-internals/runtime'; import { precompileTemplate } from '@ember/template-compilation'; @@ -156,6 +157,12 @@ class BasicEachTest extends TogglingEachTest {} // `ArrayProxy` is deprecated; the deprecation itself is asserted by the // dedicated `ArrayProxy` tests, so it is silenced in these rendering fixtures. const ARRAY_PROXY_REMOVED = DEPRECATIONS.DEPRECATE_ARRAY_PROXY.isRemoved; +const EMBER_A_REMOVED = DEPRECATIONS.DEPRECATE_EMBER_ARRAY_A.isRemoved; + +// The tests of `A` cover its deprecation. +function emberA(items) { + return ignoreDeprecation(() => A(items)); +} function arrayProxy(props) { return ignoreDeprecation(() => ArrayProxy.create(props)); @@ -163,12 +170,11 @@ function arrayProxy(props) { const TRUTHY_CASES = [ ['hello'], - emberA(['hello']), + ...(EMBER_A_REMOVED ? [] : [emberA(['hello'])]), new Set(['hello']), new ForEachable(['hello']), - ...(ARRAY_PROXY_REMOVED - ? [] - : [arrayProxy({ content: ['hello'] }), arrayProxy({ content: emberA(['hello']) })]), + ...(ARRAY_PROXY_REMOVED ? [] : [arrayProxy({ content: ['hello'] })]), + ...(ARRAY_PROXY_REMOVED || EMBER_A_REMOVED ? [] : [arrayProxy({ content: emberA(['hello']) })]), new ArrayIterable(['hello']), ]; @@ -179,12 +185,11 @@ const FALSY_CASES = [ '', 0, [], - emberA([]), + ...(EMBER_A_REMOVED ? [] : [emberA([])]), new Set([]), new ForEachable([]), - ...(ARRAY_PROXY_REMOVED - ? [] - : [arrayProxy({ content: [] }), arrayProxy({ content: emberA([]) })]), + ...(ARRAY_PROXY_REMOVED ? [] : [arrayProxy({ content: [] })]), + ...(ARRAY_PROXY_REMOVED || EMBER_A_REMOVED ? [] : [arrayProxy({ content: emberA([]) })]), new ArrayIterable([]), ]; @@ -1040,15 +1045,17 @@ moduleFor( } ); -moduleFor( - 'Syntax test: {{#each}} with emberA-wrapped arrays', - class extends EachTest { - createList(items) { - let wrapped = emberA(items); - return { list: wrapped, delegate: wrapped }; +if (!EMBER_A_REMOVED) { + moduleFor( + 'Syntax test: {{#each}} with emberA-wrapped arrays', + class extends EachTest { + createList(items) { + let wrapped = emberA(items); + return { list: wrapped, delegate: wrapped }; + } } - } -); + ); +} moduleFor( 'Syntax test: {{#each}} with native Set', @@ -1088,7 +1095,7 @@ moduleFor( } ); -if (!ARRAY_PROXY_REMOVED) { +if (!ARRAY_PROXY_REMOVED && !EMBER_A_REMOVED) { moduleFor( 'Syntax test: {{#each}} with array proxies, modifying itself', class extends EachTest { @@ -1187,7 +1194,7 @@ moduleFor( moduleFor( 'Syntax test: {{#each}} with sparse arrays', class extends RenderingTestCase { - ['@test it should itterate over holes']() { + [`${testUnless(EMBER_A_REMOVED)} @test it should itterate over holes`]() { let sparseArray = []; sparseArray[3] = 'foo'; sparseArray[4] = 'bar'; diff --git a/packages/@ember/-internals/glimmer/tests/utils/shared-conditional-tests.js b/packages/@ember/-internals/glimmer/tests/utils/shared-conditional-tests.js index 07ac2d0ecc4..c3c5a7927d6 100644 --- a/packages/@ember/-internals/glimmer/tests/utils/shared-conditional-tests.js +++ b/packages/@ember/-internals/glimmer/tests/utils/shared-conditional-tests.js @@ -13,7 +13,7 @@ import { htmlSafe } from '@ember/-internals/glimmer'; import { get, set } from '@ember/object'; import EmberObject from '@ember/object'; import ObjectProxy from '@ember/object/proxy'; -import { A as emberA, removeAt } from '@ember/array'; +import { A, removeAt } from '@ember/array'; import ArrayProxy from '@ember/array/proxy'; import { precompileTemplate } from '@ember/template-compilation'; import { setComponentTemplate } from '@glimmer/manager'; @@ -25,6 +25,12 @@ import Component from '@glimmer/component'; // itself is asserted by the dedicated proxy tests, so it is silenced here. const OBJECT_PROXY_REMOVED = DEPRECATIONS.DEPRECATE_OBJECT_PROXY.isRemoved; const ARRAY_PROXY_REMOVED = DEPRECATIONS.DEPRECATE_ARRAY_PROXY.isRemoved; +const EMBER_A_REMOVED = DEPRECATIONS.DEPRECATE_EMBER_ARRAY_A.isRemoved; + +// The tests of `A` cover its deprecation. +function emberA(items) { + return ignoreDeprecation(() => A(items)); +} function objectProxy(props) { return ignoreDeprecation(() => ObjectProxy.create(props)); @@ -342,7 +348,7 @@ export const ArrayTestCases = { this.assertText('T1F2'); }, - ['@test it considers empty Ember arrays falsy']() { + [`${testUnless(EMBER_A_REMOVED)} @test it considers empty Ember arrays falsy`]() { this.renderValues(emberA(['hello']), emberA()); this.assertText('T1F2'); @@ -401,7 +407,9 @@ export const ArrayTestCases = { this.assertText('T1F2'); }, - [`${testUnless(ARRAY_PROXY_REMOVED)} @test it considers array proxies with empty arrays falsy`]() { + [`${testUnless( + ARRAY_PROXY_REMOVED || EMBER_A_REMOVED + )} @test it considers array proxies with empty arrays falsy`]() { this.renderValues( arrayProxy({ content: emberA(['hello']) }), arrayProxy({ content: emberA() }) @@ -443,7 +451,7 @@ const IfUnlessWithTestCases = [ 'undefined', 1, ['hello'], - emberA(['hello']), + ...unlessRemoved(EMBER_A_REMOVED, () => [emberA(['hello'])]), {}, { foo: 'bar' }, EmberObject.create(), @@ -467,7 +475,7 @@ const IfUnlessWithTestCases = [ '', 0, [], - emberA(), + ...unlessRemoved(EMBER_A_REMOVED, () => [emberA()]), ...unlessRemoved(OBJECT_PROXY_REMOVED, () => [objectProxy({ content: undefined })]), htmlSafe(''), ]), @@ -481,7 +489,7 @@ const IfUnlessWithTestCases = [ 'undefined', 1, ['hello'], - emberA(['hello']), + ...unlessRemoved(EMBER_A_REMOVED, () => [emberA(['hello'])]), ...unlessRemoved(ARRAY_PROXY_REMOVED, () => [ arrayProxy({ content: ['hello'] }), arrayProxy({ content: [] }), @@ -505,7 +513,7 @@ const IfUnlessWithTestCases = [ '', 0, [], - emberA(), + ...unlessRemoved(EMBER_A_REMOVED, () => [emberA()]), ]), ObjectTestCases, diff --git a/packages/@ember/-internals/metal/tests/alias_test.js b/packages/@ember/-internals/metal/tests/alias_test.js index a9c4a2635ce..c447eaf0e3d 100644 --- a/packages/@ember/-internals/metal/tests/alias_test.js +++ b/packages/@ember/-internals/metal/tests/alias_test.js @@ -10,10 +10,19 @@ import { } from '..'; import EmberObject from '@ember/object'; import { A } from '@ember/array'; -import { moduleFor, AbstractTestCase, runLoopSettled } from 'internal-test-helpers'; +import { + moduleFor, + AbstractTestCase, + runLoopSettled, + expectDeprecation, + testUnless, +} from 'internal-test-helpers'; +import { DEPRECATIONS } from '@ember/-internals/deprecations'; import { destroy } from '@glimmer/destroyable'; import { valueForTag, validateTag } from '@glimmer/validator'; +const EMBER_A = DEPRECATIONS.DEPRECATE_EMBER_ARRAY_A; + let obj, count; function incrementCount() { @@ -69,7 +78,11 @@ moduleFor( assert.equal(count, 1); } - ['@test nested aliases should trigger computed property invalidation [GH#19279]'](assert) { + [`${testUnless( + EMBER_A.isRemoved + )} @test nested aliases should trigger computed property invalidation [GH#19279]`](assert) { + expectDeprecation(/`A` from `@ember\/array` is deprecated/, EMBER_A.isEnabled); + let AttributeModel = class extends EmberObject { @alias('additives.length') countAdditives; diff --git a/packages/@ember/-internals/routing/route-managers/classic/query-params.ts b/packages/@ember/-internals/routing/route-managers/classic/query-params.ts index b20cf6c0f78..74f67dd8c23 100644 --- a/packages/@ember/-internals/routing/route-managers/classic/query-params.ts +++ b/packages/@ember/-internals/routing/route-managers/classic/query-params.ts @@ -6,7 +6,8 @@ functions below. */ -import { A as emberA } from '@ember/array'; +import { reportingA } from '@ember/array/index-internal'; +import { DEPRECATIONS, deprecateUntil } from '@ember/-internals/deprecations'; import { assert } from '@ember/debug'; import { flushAsyncObservers } from '@ember/-internals/metal/lib/observer'; import { get } from '@ember/-internals/metal/lib/property_get'; @@ -18,13 +19,27 @@ import type { InternalRouteInfo, Transition } from 'router_js'; import { hasClassicInterop, STATE_SYMBOL } from 'router_js'; import { ClassicRouteBucket } from './bucket'; -// Returns a fresh native array when the value is an array so callers cannot +function reportEmberArrayUse(name: string) { + deprecateUntil( + `\`${name}\` was used on the array value of a query param. This value becomes a native array. Use native array methods, and assign a new array to the property to change the query param.`, + DEPRECATIONS.DEPRECATE_QUERY_PARAM_EMBER_ARRAY + ); +} + +/** + Makes the array that a controller gets as the value of a query param. + */ +export function queryParamArray(value: T[]): T[] { + return reportingA(value, reportEmberArrayUse); +} + +// Returns a fresh array when the value is an array so callers cannot // mutate the shared default. Also used by `@ember/routing/route`'s QP meta // building. export function copyDefaultValue(value: T): T { if (Array.isArray(value)) { // SAFETY: We lost the type data about the array if we don't cast. - return emberA(value.slice()) as unknown as T; + return queryParamArray(value.slice()) as unknown as T; } return value; } diff --git a/packages/@ember/-internals/runtime/tests/array/any-test.js b/packages/@ember/-internals/runtime/tests/array/any-test.js index 58928a04fe2..f6b07e9a8bc 100644 --- a/packages/@ember/-internals/runtime/tests/array/any-test.js +++ b/packages/@ember/-internals/runtime/tests/array/any-test.js @@ -1,6 +1,5 @@ -import { A as emberA } from '@ember/array'; import { AbstractTestCase } from 'internal-test-helpers'; -import { runArrayTests } from '../helpers/array'; +import { runArrayTests, emberA } from '../helpers/array'; class AnyTests extends AbstractTestCase { '@test any should should invoke callback on each item as long as you return false'() { diff --git a/packages/@ember/-internals/runtime/tests/helpers/array.js b/packages/@ember/-internals/runtime/tests/helpers/array.js index b86937a4dfc..8dae42dac2a 100644 --- a/packages/@ember/-internals/runtime/tests/helpers/array.js +++ b/packages/@ember/-internals/runtime/tests/helpers/array.js @@ -1,5 +1,5 @@ import ArrayProxy from '@ember/array/proxy'; -import EmberArray, { A as emberA } from '@ember/array'; +import EmberArray, { A } from '@ember/array'; import MutableArray from '@ember/array/mutable'; import { generateGuid, guidFor } from '@ember/-internals/utils'; import { @@ -12,6 +12,12 @@ import EmberObject, { get, computed } from '@ember/object'; import { moduleFor, ignoreDeprecation } from 'internal-test-helpers'; import { DEPRECATIONS } from '@ember/-internals/deprecations'; +// These suites test the shared array APIs. +// The tests of `A` cover its deprecation. +export function emberA(array) { + return ignoreDeprecation(() => A(array)); +} + export function newFixture(cnt) { let ret = []; while (--cnt >= 0) { @@ -156,7 +162,9 @@ class ArrayProxyHelpers extends AbstractArrayHelper { // These suites exercise the shared array APIs rather than `ArrayProxy` // itself, so we let the `ArrayProxy` deprecation pass silently here. The // deprecation itself is covered by the dedicated `ArrayProxy` tests. - return ignoreDeprecation(() => ArrayProxy.create({ content: emberA(super.newObject(ary)) })); + let content = emberA(super.newObject(ary)); + + return ignoreDeprecation(() => ArrayProxy.create({ content })); } mutate(obj) { @@ -290,6 +298,10 @@ export function runArrayTests(name, Tests, ...types) { return; } + if (DEPRECATIONS.DEPRECATE_EMBER_ARRAY_A.isRemoved) { + return; + } + for (let type of requested) { moduleFor(`${type}: ${name}`, Tests, ARRAY_TEST_HELPERS[type]); } diff --git a/packages/@ember/-internals/runtime/tests/legacy_1x/mixins/observable/chained_test.js b/packages/@ember/-internals/runtime/tests/legacy_1x/mixins/observable/chained_test.js index a650428eee4..2d71afca123 100644 --- a/packages/@ember/-internals/runtime/tests/legacy_1x/mixins/observable/chained_test.js +++ b/packages/@ember/-internals/runtime/tests/legacy_1x/mixins/observable/chained_test.js @@ -1,7 +1,16 @@ import { addObserver } from '@ember/-internals/metal'; import EmberObject, { get, set } from '@ember/object'; import { A as emberA } from '@ember/array'; -import { moduleFor, AbstractTestCase, runLoopSettled } from 'internal-test-helpers'; +import { + moduleFor, + AbstractTestCase, + runLoopSettled, + expectDeprecation, + testUnless, +} from 'internal-test-helpers'; +import { DEPRECATIONS } from '@ember/-internals/deprecations'; + +const EMBER_A = DEPRECATIONS.DEPRECATE_EMBER_ARRAY_A; /* NOTE: This test is adapted from the 1.x series of unit tests. The tests @@ -17,9 +26,13 @@ import { moduleFor, AbstractTestCase, runLoopSettled } from 'internal-test-helpe moduleFor( 'Ember.Observable - Observing with @each', class extends AbstractTestCase { - async ['@test chained observers on enumerable properties are triggered when the observed property of any item changes']( + async [`${testUnless( + EMBER_A.isRemoved + )} @test chained observers on enumerable properties are triggered when the observed property of any item changes`]( assert ) { + expectDeprecation(/`A` from `@ember\/array` is deprecated/, EMBER_A.isEnabled); + let family = EmberObject.create({ momma: null }); let momma = EmberObject.create({ children: [] }); diff --git a/packages/@ember/-internals/runtime/tests/mixins/array_test.js b/packages/@ember/-internals/runtime/tests/mixins/array_test.js index 8e47fdd49a5..fc4ff4d0e5c 100644 --- a/packages/@ember/-internals/runtime/tests/mixins/array_test.js +++ b/packages/@ember/-internals/runtime/tests/mixins/array_test.js @@ -18,6 +18,7 @@ import { import { DEPRECATIONS } from '../../../deprecations'; const MIXIN = DEPRECATIONS.DEPRECATE_EMBER_ARRAY_MIXIN; +const EMBER_A = DEPRECATIONS.DEPRECATE_EMBER_ARRAY_A; function expectMixinDeprecation() { expectDeprecation(/The `EmberArray` mixin is deprecated/, MIXIN.isEnabled); @@ -388,9 +389,13 @@ moduleFor( assert.equal(objectAt(arr, 4), undefined); } - ['@test should be clear caches for computed properties that have dependent keys on arrays that are changed after object initialization']( + [`${testUnless( + EMBER_A.isRemoved + )} @test should be clear caches for computed properties that have dependent keys on arrays that are changed after object initialization`]( assert ) { + expectDeprecation(/`A` from `@ember\/array` is deprecated/, EMBER_A.isEnabled); + let obj = class extends EmberObject { init() { super.init(...arguments); @@ -410,9 +415,13 @@ moduleFor( assert.equal('BYE!', get(obj, 'common')); } - async ['@test observers that contain @each in the path should fire only once the first time they are accessed']( + async [`${testUnless( + EMBER_A.isRemoved + )} @test observers that contain @each in the path should fire only once the first time they are accessed`]( assert ) { + expectDeprecation(/`A` from `@ember\/array` is deprecated/, EMBER_A.isEnabled); + let count = 0; let obj = EmberObject.extend({ diff --git a/packages/@ember/-internals/runtime/tests/mixins/mutable_enumerable_test.js b/packages/@ember/-internals/runtime/tests/mixins/mutable_enumerable_test.js index b08ad58d3fe..806c6872edd 100644 --- a/packages/@ember/-internals/runtime/tests/mixins/mutable_enumerable_test.js +++ b/packages/@ember/-internals/runtime/tests/mixins/mutable_enumerable_test.js @@ -4,10 +4,14 @@ import { A } from '@ember/array'; import { moduleFor, AbstractTestCase, expectDeprecation, testUnless } from 'internal-test-helpers'; import { DEPRECATIONS } from '@ember/-internals/deprecations'; +const EMBER_A = DEPRECATIONS.DEPRECATE_EMBER_ARRAY_A; + moduleFor( 'MutableEnumerable', class extends AbstractTestCase { - ['@test should be mixed into A()'](assert) { + [`${testUnless(EMBER_A.isRemoved)} @test should be mixed into A()`](assert) { + expectDeprecation(/`A` from `@ember\/array` is deprecated/, EMBER_A.isEnabled); + assert.ok(MutableEnumerable.detect(A())); } diff --git a/packages/@ember/-internals/runtime/tests/mutable-array/removeObjects-test.js b/packages/@ember/-internals/runtime/tests/mutable-array/removeObjects-test.js index 13d71267781..540886c3436 100644 --- a/packages/@ember/-internals/runtime/tests/mutable-array/removeObjects-test.js +++ b/packages/@ember/-internals/runtime/tests/mutable-array/removeObjects-test.js @@ -1,7 +1,6 @@ import { get } from '@ember/object'; import { AbstractTestCase, runLoopSettled } from 'internal-test-helpers'; -import { runArrayTests, newFixture, newObjectsFixture } from '../helpers/array'; -import { A as emberA } from '@ember/array'; +import { runArrayTests, newFixture, newObjectsFixture, emberA } from '../helpers/array'; import { destroy } from '@glimmer/destroyable'; class RemoveObjectsTests extends AbstractTestCase { diff --git a/packages/@ember/-internals/runtime/tests/system/array_proxy/arranged_content_test.js b/packages/@ember/-internals/runtime/tests/system/array_proxy/arranged_content_test.js index 35e4e2884fd..74ce310ed9d 100644 --- a/packages/@ember/-internals/runtime/tests/system/array_proxy/arranged_content_test.js +++ b/packages/@ember/-internals/runtime/tests/system/array_proxy/arranged_content_test.js @@ -6,13 +6,18 @@ import { A as emberA } from '@ember/array'; import { moduleFor, AbstractTestCase, expectDeprecation, testUnless } from 'internal-test-helpers'; import { DEPRECATIONS } from '@ember/-internals/deprecations'; +const { DEPRECATE_ARRAY_PROXY, DEPRECATE_EMBER_ARRAY_A } = DEPRECATIONS; +const REMOVED = DEPRECATE_ARRAY_PROXY.isRemoved || DEPRECATE_EMBER_ARRAY_A.isRemoved; +const ENABLED = DEPRECATE_ARRAY_PROXY.isEnabled || DEPRECATE_EMBER_ARRAY_A.isEnabled; +const DEPRECATION = /`ArrayProxy` is deprecated|`A` from `@ember\/array` is deprecated/; + let array; moduleFor( 'ArrayProxy - arrangedContent', class extends AbstractTestCase { beforeEach() { - expectDeprecation(/`ArrayProxy` is deprecated/, DEPRECATIONS.DEPRECATE_ARRAY_PROXY.isEnabled); + expectDeprecation(DEPRECATION, ENABLED); run(() => { array = class extends ArrayProxy { @computed('content.[]') @@ -43,7 +48,7 @@ moduleFor( run(() => array.destroy()); } - [`${testUnless(DEPRECATIONS.DEPRECATE_ARRAY_PROXY.isRemoved)} @test compact - returns arrangedContent without nulls and undefined`]( + [`${testUnless(REMOVED)} @test compact - returns arrangedContent without nulls and undefined`]( assert ) { run(() => array.set('content', emberA([1, 3, null, 2, undefined]))); @@ -51,78 +56,64 @@ moduleFor( assert.deepEqual(array.compact(), [3, 2, 1]); } - [`${testUnless(DEPRECATIONS.DEPRECATE_ARRAY_PROXY.isRemoved)} @test indexOf - returns index of object in arrangedContent`]( - assert - ) { + [`${testUnless(REMOVED)} @test indexOf - returns index of object in arrangedContent`](assert) { assert.equal(array.indexOf(4), 1, 'returns arranged index'); } - [`${testUnless(DEPRECATIONS.DEPRECATE_ARRAY_PROXY.isRemoved)} @test lastIndexOf - returns last index of object in arrangedContent`]( + [`${testUnless(REMOVED)} @test lastIndexOf - returns last index of object in arrangedContent`]( assert ) { array.get('content').pushObject(4); assert.equal(array.lastIndexOf(4), 2, 'returns last arranged index'); } - [`${testUnless(DEPRECATIONS.DEPRECATE_ARRAY_PROXY.isRemoved)} @test objectAt - returns object at index in arrangedContent`]( - assert - ) { + [`${testUnless(REMOVED)} @test objectAt - returns object at index in arrangedContent`](assert) { assert.equal(objectAt(array, 1), 4, 'returns object at index'); } // Not sure if we need a specific test for it, since it's internal - [`${testUnless(DEPRECATIONS.DEPRECATE_ARRAY_PROXY.isRemoved)} @test objectAtContent - returns object at index in arrangedContent`]( + [`${testUnless(REMOVED)} @test objectAtContent - returns object at index in arrangedContent`]( assert ) { assert.equal(array.objectAtContent(1), 4, 'returns object at index'); } - [`${testUnless(DEPRECATIONS.DEPRECATE_ARRAY_PROXY.isRemoved)} @test objectsAt - returns objects at indices in arrangedContent`]( + [`${testUnless(REMOVED)} @test objectsAt - returns objects at indices in arrangedContent`]( assert ) { assert.deepEqual(array.objectsAt([0, 2, 4]), [5, 2, undefined], 'returns objects at indices'); } - [`${testUnless(DEPRECATIONS.DEPRECATE_ARRAY_PROXY.isRemoved)} @test replace - mutating an arranged ArrayProxy is not allowed`]() { + [`${testUnless(REMOVED)} @test replace - mutating an arranged ArrayProxy is not allowed`]() { expectAssertion(() => { array.replace(0, 0, [3]); }, /Mutating an arranged ArrayProxy is not allowed/); } - [`${testUnless(DEPRECATIONS.DEPRECATE_ARRAY_PROXY.isRemoved)} @test replaceContent - does a standard array replace on content`]( + [`${testUnless(REMOVED)} @test replaceContent - does a standard array replace on content`]( assert ) { run(() => array.replaceContent(1, 2, [3])); assert.deepEqual(array.get('content'), [1, 3, 5]); } - [`${testUnless(DEPRECATIONS.DEPRECATE_ARRAY_PROXY.isRemoved)} @test slice - returns a slice of the arrangedContent`]( - assert - ) { + [`${testUnless(REMOVED)} @test slice - returns a slice of the arrangedContent`](assert) { assert.deepEqual(array.slice(1, 3), [4, 2], 'returns sliced arrangedContent'); } - [`${testUnless(DEPRECATIONS.DEPRECATE_ARRAY_PROXY.isRemoved)} @test toArray - returns copy of arrangedContent`]( - assert - ) { + [`${testUnless(REMOVED)} @test toArray - returns copy of arrangedContent`](assert) { assert.deepEqual(array.toArray(), [5, 4, 2, 1]); } - [`${testUnless(DEPRECATIONS.DEPRECATE_ARRAY_PROXY.isRemoved)} @test without - returns arrangedContent without object`]( - assert - ) { + [`${testUnless(REMOVED)} @test without - returns arrangedContent without object`](assert) { assert.deepEqual(array.without(2), [5, 4, 1], 'returns arranged without object'); } - [`${testUnless(DEPRECATIONS.DEPRECATE_ARRAY_PROXY.isRemoved)} @test lastObject - returns last arranged object`]( - assert - ) { + [`${testUnless(REMOVED)} @test lastObject - returns last arranged object`](assert) { assert.equal(array.get('lastObject'), 1, 'returns last arranged object'); } - [`${testUnless(DEPRECATIONS.DEPRECATE_ARRAY_PROXY.isRemoved)} @test firstObject - returns first arranged object`]( - assert - ) { + [`${testUnless(REMOVED)} @test firstObject - returns first arranged object`](assert) { assert.equal(array.get('firstObject'), 5, 'returns first arranged object'); } } @@ -132,7 +123,7 @@ moduleFor( 'ArrayProxy - arrangedContent matching content', class extends AbstractTestCase { beforeEach() { - expectDeprecation(/`ArrayProxy` is deprecated/, DEPRECATIONS.DEPRECATE_ARRAY_PROXY.isEnabled); + expectDeprecation(DEPRECATION, ENABLED); run(function () { array = ArrayProxy.create({ content: emberA([1, 2, 4, 5]), @@ -146,27 +137,21 @@ moduleFor( }); } - [`${testUnless(DEPRECATIONS.DEPRECATE_ARRAY_PROXY.isRemoved)} @test insertAt - inserts object at specified index`]( - assert - ) { + [`${testUnless(REMOVED)} @test insertAt - inserts object at specified index`](assert) { run(function () { array.insertAt(2, 3); }); assert.deepEqual(array.get('content'), [1, 2, 3, 4, 5]); } - [`${testUnless(DEPRECATIONS.DEPRECATE_ARRAY_PROXY.isRemoved)} @test replace - does a standard array replace`]( - assert - ) { + [`${testUnless(REMOVED)} @test replace - does a standard array replace`](assert) { run(function () { array.replace(1, 2, [3]); }); assert.deepEqual(array.get('content'), [1, 3, 5]); } - [`${testUnless(DEPRECATIONS.DEPRECATE_ARRAY_PROXY.isRemoved)} @test reverseObjects - reverses content`]( - assert - ) { + [`${testUnless(REMOVED)} @test reverseObjects - reverses content`](assert) { run(function () { array.reverseObjects(); }); @@ -179,7 +164,7 @@ moduleFor( 'ArrayProxy - arrangedContent with transforms', class extends AbstractTestCase { beforeEach() { - expectDeprecation(/`ArrayProxy` is deprecated/, DEPRECATIONS.DEPRECATE_ARRAY_PROXY.isEnabled); + expectDeprecation(DEPRECATION, ENABLED); run(function () { array = class extends ArrayProxy { @computed('content.[]') @@ -217,33 +202,29 @@ moduleFor( }); } - [`${testUnless(DEPRECATIONS.DEPRECATE_ARRAY_PROXY.isRemoved)} @test indexOf - returns index of object in arrangedContent`]( - assert - ) { + [`${testUnless(REMOVED)} @test indexOf - returns index of object in arrangedContent`](assert) { assert.equal(array.indexOf('4'), 1, 'returns arranged index'); } - [`${testUnless(DEPRECATIONS.DEPRECATE_ARRAY_PROXY.isRemoved)} @test lastIndexOf - returns last index of object in arrangedContent`]( + [`${testUnless(REMOVED)} @test lastIndexOf - returns last index of object in arrangedContent`]( assert ) { array.get('content').pushObject(4); assert.equal(array.lastIndexOf('4'), 2, 'returns last arranged index'); } - [`${testUnless(DEPRECATIONS.DEPRECATE_ARRAY_PROXY.isRemoved)} @test objectAt - returns object at index in arrangedContent`]( - assert - ) { + [`${testUnless(REMOVED)} @test objectAt - returns object at index in arrangedContent`](assert) { assert.equal(objectAt(array, 1), '4', 'returns object at index'); } // Not sure if we need a specific test for it, since it's internal - [`${testUnless(DEPRECATIONS.DEPRECATE_ARRAY_PROXY.isRemoved)} @test objectAtContent - returns object at index in arrangedContent`]( + [`${testUnless(REMOVED)} @test objectAtContent - returns object at index in arrangedContent`]( assert ) { assert.equal(array.objectAtContent(1), '4', 'returns object at index'); } - [`${testUnless(DEPRECATIONS.DEPRECATE_ARRAY_PROXY.isRemoved)} @test objectsAt - returns objects at indices in arrangedContent`]( + [`${testUnless(REMOVED)} @test objectsAt - returns objects at indices in arrangedContent`]( assert ) { assert.deepEqual( @@ -253,33 +234,23 @@ moduleFor( ); } - [`${testUnless(DEPRECATIONS.DEPRECATE_ARRAY_PROXY.isRemoved)} @test slice - returns a slice of the arrangedContent`]( - assert - ) { + [`${testUnless(REMOVED)} @test slice - returns a slice of the arrangedContent`](assert) { assert.deepEqual(array.slice(1, 3), ['4', '2'], 'returns sliced arrangedContent'); } - [`${testUnless(DEPRECATIONS.DEPRECATE_ARRAY_PROXY.isRemoved)} @test toArray - returns copy of arrangedContent`]( - assert - ) { + [`${testUnless(REMOVED)} @test toArray - returns copy of arrangedContent`](assert) { assert.deepEqual(array.toArray(), ['5', '4', '2', '1']); } - [`${testUnless(DEPRECATIONS.DEPRECATE_ARRAY_PROXY.isRemoved)} @test without - returns arrangedContent without object`]( - assert - ) { + [`${testUnless(REMOVED)} @test without - returns arrangedContent without object`](assert) { assert.deepEqual(array.without('2'), ['5', '4', '1'], 'returns arranged without object'); } - [`${testUnless(DEPRECATIONS.DEPRECATE_ARRAY_PROXY.isRemoved)} @test lastObject - returns last arranged object`]( - assert - ) { + [`${testUnless(REMOVED)} @test lastObject - returns last arranged object`](assert) { assert.equal(array.get('lastObject'), '1', 'returns last arranged object'); } - [`${testUnless(DEPRECATIONS.DEPRECATE_ARRAY_PROXY.isRemoved)} @test firstObject - returns first arranged object`]( - assert - ) { + [`${testUnless(REMOVED)} @test firstObject - returns first arranged object`](assert) { assert.equal(array.get('firstObject'), '5', 'returns first arranged object'); } } @@ -289,7 +260,7 @@ moduleFor( 'ArrayProxy - with transforms', class extends AbstractTestCase { beforeEach() { - expectDeprecation(/`ArrayProxy` is deprecated/, DEPRECATIONS.DEPRECATE_ARRAY_PROXY.isEnabled); + expectDeprecation(DEPRECATION, ENABLED); run(function () { array = class extends ArrayProxy { objectAtContent(idx) { @@ -308,31 +279,23 @@ moduleFor( }); } - [`${testUnless(DEPRECATIONS.DEPRECATE_ARRAY_PROXY.isRemoved)} @test popObject - removes last object in arrangedContent`]( - assert - ) { + [`${testUnless(REMOVED)} @test popObject - removes last object in arrangedContent`](assert) { let popped = array.popObject(); assert.equal(popped, '5', 'returns last object'); assert.deepEqual(array.toArray(), ['1', '2', '4'], 'removes from content'); } - [`${testUnless(DEPRECATIONS.DEPRECATE_ARRAY_PROXY.isRemoved)} @test removeObject - removes object from content`]( - assert - ) { + [`${testUnless(REMOVED)} @test removeObject - removes object from content`](assert) { array.removeObject('2'); assert.deepEqual(array.toArray(), ['1', '4', '5']); } - [`${testUnless(DEPRECATIONS.DEPRECATE_ARRAY_PROXY.isRemoved)} @test removeObjects - removes objects from content`]( - assert - ) { + [`${testUnless(REMOVED)} @test removeObjects - removes objects from content`](assert) { array.removeObjects(['2', '4', '6']); assert.deepEqual(array.toArray(), ['1', '5']); } - [`${testUnless(DEPRECATIONS.DEPRECATE_ARRAY_PROXY.isRemoved)} @test shiftObject - removes from start of arrangedContent`]( - assert - ) { + [`${testUnless(REMOVED)} @test shiftObject - removes from start of arrangedContent`](assert) { let shifted = array.shiftObject(); assert.equal(shifted, '1', 'returns first object'); assert.deepEqual(array.toArray(), ['2', '4', '5'], 'removes object from content'); diff --git a/packages/@ember/-internals/runtime/tests/system/array_proxy/content_change_test.js b/packages/@ember/-internals/runtime/tests/system/array_proxy/content_change_test.js index c60cb0c9dfe..1bf363df3ed 100644 --- a/packages/@ember/-internals/runtime/tests/system/array_proxy/content_change_test.js +++ b/packages/@ember/-internals/runtime/tests/system/array_proxy/content_change_test.js @@ -6,14 +6,19 @@ import { A as emberA } from '@ember/array'; import { moduleFor, AbstractTestCase, expectDeprecation, testUnless } from 'internal-test-helpers'; import { DEPRECATIONS } from '@ember/-internals/deprecations'; +const { DEPRECATE_ARRAY_PROXY, DEPRECATE_EMBER_ARRAY_A } = DEPRECATIONS; +const REMOVED = DEPRECATE_ARRAY_PROXY.isRemoved || DEPRECATE_EMBER_ARRAY_A.isRemoved; +const ENABLED = DEPRECATE_ARRAY_PROXY.isEnabled || DEPRECATE_EMBER_ARRAY_A.isEnabled; +const DEPRECATION = /`ArrayProxy` is deprecated|`A` from `@ember\/array` is deprecated/; + moduleFor( 'ArrayProxy - content change', class extends AbstractTestCase { beforeEach() { - expectDeprecation(/`ArrayProxy` is deprecated/, DEPRECATIONS.DEPRECATE_ARRAY_PROXY.isEnabled); + expectDeprecation(DEPRECATION, ENABLED); } - [`${testUnless(DEPRECATIONS.DEPRECATE_ARRAY_PROXY.isRemoved)} @test The ArrayProxy doesn't explode when assigned a destroyed object`]( + [`${testUnless(REMOVED)} @test The ArrayProxy doesn't explode when assigned a destroyed object`]( assert ) { let proxy1 = ArrayProxy.create(); @@ -26,7 +31,7 @@ moduleFor( assert.ok(true, 'No exception was raised'); } - [`${testUnless(DEPRECATIONS.DEPRECATE_ARRAY_PROXY.isRemoved)} @test should update if content changes while change events are deferred`]( + [`${testUnless(REMOVED)} @test should update if content changes while change events are deferred`]( assert ) { let proxy = ArrayProxy.create(); @@ -39,9 +44,7 @@ moduleFor( }); } - [`${testUnless(DEPRECATIONS.DEPRECATE_ARRAY_PROXY.isRemoved)} @test objectAt recomputes the object cache correctly`]( - assert - ) { + [`${testUnless(REMOVED)} @test objectAt recomputes the object cache correctly`](assert) { let indexes = []; let proxy = class extends ArrayProxy { @@ -71,9 +74,7 @@ moduleFor( assert.deepEqual(indexes, [2, 3, 4]); } - [`${testUnless(DEPRECATIONS.DEPRECATE_ARRAY_PROXY.isRemoved)} @test negative indexes are handled correctly`]( - assert - ) { + [`${testUnless(REMOVED)} @test negative indexes are handled correctly`](assert) { let indexes = []; let proxy = class extends ArrayProxy { diff --git a/packages/@ember/-internals/runtime/tests/system/array_proxy/length_test.js b/packages/@ember/-internals/runtime/tests/system/array_proxy/length_test.js index 13408d36ee7..2899972052a 100644 --- a/packages/@ember/-internals/runtime/tests/system/array_proxy/length_test.js +++ b/packages/@ember/-internals/runtime/tests/system/array_proxy/length_test.js @@ -14,16 +14,19 @@ import { set, get } from '@ember/object'; import { createCache, getValue } from '@glimmer/validator'; import { DEPRECATIONS } from '@ember/-internals/deprecations'; +const { DEPRECATE_ARRAY_PROXY, DEPRECATE_EMBER_ARRAY_A } = DEPRECATIONS; +const REMOVED = DEPRECATE_ARRAY_PROXY.isRemoved || DEPRECATE_EMBER_ARRAY_A.isRemoved; +const ENABLED = DEPRECATE_ARRAY_PROXY.isEnabled || DEPRECATE_EMBER_ARRAY_A.isEnabled; +const DEPRECATION = /`ArrayProxy` is deprecated|`A` from `@ember\/array` is deprecated/; + moduleFor( 'Ember.ArrayProxy - content change (length)', class extends AbstractTestCase { beforeEach() { - expectDeprecation(/`ArrayProxy` is deprecated/, DEPRECATIONS.DEPRECATE_ARRAY_PROXY.isEnabled); + expectDeprecation(DEPRECATION, ENABLED); } - [`${testUnless(DEPRECATIONS.DEPRECATE_ARRAY_PROXY.isRemoved)} @test should update length for null content`]( - assert - ) { + [`${testUnless(REMOVED)} @test should update length for null content`](assert) { let proxy = ArrayProxy.create({ content: a([1, 2, 3]), }); @@ -35,7 +38,7 @@ moduleFor( assert.equal(proxy.get('length'), 0, 'length updates'); } - [`${testUnless(DEPRECATIONS.DEPRECATE_ARRAY_PROXY.isRemoved)} @test should update length for null content when there is a computed property watching length`]( + [`${testUnless(REMOVED)} @test should update length for null content when there is a computed property watching length`]( assert ) { let proxy = class extends ArrayProxy { @@ -56,9 +59,7 @@ moduleFor( assert.equal(proxy.get('length'), 0, 'length updates'); } - [`${testUnless(DEPRECATIONS.DEPRECATE_ARRAY_PROXY.isRemoved)} @test getting length does not recompute the object cache`]( - assert - ) { + [`${testUnless(REMOVED)} @test getting length does not recompute the object cache`](assert) { let indexes = []; let proxy = class extends ArrayProxy { @@ -84,9 +85,7 @@ moduleFor( assert.deepEqual(indexes, []); } - [`${testUnless(DEPRECATIONS.DEPRECATE_ARRAY_PROXY.isRemoved)} @test accessing length after content set to null`]( - assert - ) { + [`${testUnless(REMOVED)} @test accessing length after content set to null`](assert) { let obj = ArrayProxy.create({ content: ['foo', 'bar'] }); assert.equal(obj.length, 2, 'precond'); @@ -97,7 +96,7 @@ moduleFor( assert.deepEqual(obj.content, null, 'content was updated'); } - [`${testUnless(DEPRECATIONS.DEPRECATE_ARRAY_PROXY.isRemoved)} @test accessing length after content set to null in willDestroy`]( + [`${testUnless(REMOVED)} @test accessing length after content set to null in willDestroy`]( assert ) { let obj = class extends ArrayProxy { @@ -117,9 +116,7 @@ moduleFor( assert.deepEqual(obj.content, null, 'content was updated'); } - [`${testUnless(DEPRECATIONS.DEPRECATE_ARRAY_PROXY.isRemoved)} @test setting length to 0`]( - assert - ) { + [`${testUnless(REMOVED)} @test setting length to 0`](assert) { let obj = ArrayProxy.create({ content: ['foo', 'bar'] }); assert.equal(obj.length, 2, 'precond'); @@ -130,9 +127,7 @@ moduleFor( assert.deepEqual(obj.content, [], 'content length was truncated'); } - [`${testUnless(DEPRECATIONS.DEPRECATE_ARRAY_PROXY.isRemoved)} @test setting length to smaller value`]( - assert - ) { + [`${testUnless(REMOVED)} @test setting length to smaller value`](assert) { let obj = ArrayProxy.create({ content: ['foo', 'bar'] }); assert.equal(obj.length, 2, 'precond'); @@ -143,9 +138,7 @@ moduleFor( assert.deepEqual(obj.content, ['foo'], 'content length was truncated'); } - [`${testUnless(DEPRECATIONS.DEPRECATE_ARRAY_PROXY.isRemoved)} @test setting length to larger value`]( - assert - ) { + [`${testUnless(REMOVED)} @test setting length to larger value`](assert) { let obj = ArrayProxy.create({ content: ['foo', 'bar'] }); assert.equal(obj.length, 2, 'precond'); @@ -156,9 +149,7 @@ moduleFor( assert.deepEqual(obj.content, ['foo', 'bar', undefined], 'content length was updated'); } - [`${testUnless(DEPRECATIONS.DEPRECATE_ARRAY_PROXY.isRemoved)} @test setting length after content set to null`]( - assert - ) { + [`${testUnless(REMOVED)} @test setting length after content set to null`](assert) { let obj = ArrayProxy.create({ content: ['foo', 'bar'] }); assert.equal(obj.length, 2, 'precond'); @@ -170,9 +161,7 @@ moduleFor( assert.equal(obj.length, 0, 'length is still updated'); } - [`${testUnless(DEPRECATIONS.DEPRECATE_ARRAY_PROXY.isRemoved)} @test setting length to greater than zero`]( - assert - ) { + [`${testUnless(REMOVED)} @test setting length to greater than zero`](assert) { let obj = ArrayProxy.create({ content: ['foo', 'bar'] }); assert.equal(obj.length, 2, 'precond'); @@ -183,9 +172,7 @@ moduleFor( assert.deepEqual(obj.content, ['foo'], 'content length was truncated'); } - async [`${testUnless(DEPRECATIONS.DEPRECATE_ARRAY_PROXY.isRemoved)} @test array proxy + aliasedProperty complex test`]( - assert - ) { + async [`${testUnless(REMOVED)} @test array proxy + aliasedProperty complex test`](assert) { let aCalled, bCalled, cCalled, dCalled, eCalled; aCalled = bCalled = cCalled = dCalled = eCalled = 0; @@ -239,7 +226,7 @@ moduleFor( obj.destroy(); } - async [`${testUnless(DEPRECATIONS.DEPRECATE_ARRAY_PROXY.isRemoved)} @test array proxy length is reactive when accessed normally`]( + async [`${testUnless(REMOVED)} @test array proxy length is reactive when accessed normally`]( assert ) { let proxy = ArrayProxy.create({ @@ -263,7 +250,7 @@ moduleFor( assert.equal(getValue(lengthCache), 0, 'length is correct'); } - async [`${testUnless(DEPRECATIONS.DEPRECATE_ARRAY_PROXY.isRemoved)} @test array proxy length is reactive when accessed using get`]( + async [`${testUnless(REMOVED)} @test array proxy length is reactive when accessed using get`]( assert ) { let proxy = ArrayProxy.create({ diff --git a/packages/@ember/-internals/runtime/tests/system/native_array/a_test.js b/packages/@ember/-internals/runtime/tests/system/native_array/a_test.js index e9b72d65d1a..8cdac4536af 100644 --- a/packages/@ember/-internals/runtime/tests/system/native_array/a_test.js +++ b/packages/@ember/-internals/runtime/tests/system/native_array/a_test.js @@ -1,11 +1,15 @@ -import EmberArray from '@ember/array'; -import { A } from '@ember/array'; -import { moduleFor, AbstractTestCase } from 'internal-test-helpers'; +import EmberArray, { A } from '@ember/array'; +import { DEPRECATIONS } from '@ember/-internals/deprecations'; +import { moduleFor, AbstractTestCase, expectDeprecation, testUnless } from 'internal-test-helpers'; + +const { isEnabled, isRemoved } = DEPRECATIONS.DEPRECATE_EMBER_ARRAY_A; moduleFor( 'Ember.A', class extends AbstractTestCase { - ['@test Ember.A'](assert) { + [`${testUnless(isRemoved)} @test Ember.A`](assert) { + expectDeprecation(/`A` from `@ember\/array` is deprecated/, isEnabled); + assert.deepEqual(A([1, 2]), [1, 2], 'array values were not be modified'); assert.deepEqual(A(), [], 'returned an array with no arguments'); assert.deepEqual(A(null), [], 'returned an array with a null argument'); @@ -13,7 +17,15 @@ moduleFor( assert.ok(EmberArray.detect(A([1, 2])), 'returned an ember array'); } - ['@test new Ember.A'](assert) { + [`${testUnless(isRemoved)} @test Ember.A returns an Ember array as it is`](assert) { + expectDeprecation(/`A` from `@ember\/array` is deprecated/, isEnabled); + + let array = A([1, 2]); + + assert.strictEqual(A(array), array); + } + + [`${testUnless(isRemoved)} @test new Ember.A`](assert) { expectAssertion(() => { assert.deepEqual(new A([1, 2]), [1, 2], 'array values were not be modified'); assert.deepEqual(new A(), [], 'returned an array with no arguments'); diff --git a/packages/@ember/-internals/runtime/tests/system/native_array/replace_test.js b/packages/@ember/-internals/runtime/tests/system/native_array/replace_test.js index dbcdba82f50..2b821856100 100644 --- a/packages/@ember/-internals/runtime/tests/system/native_array/replace_test.js +++ b/packages/@ember/-internals/runtime/tests/system/native_array/replace_test.js @@ -1,16 +1,25 @@ import { A } from '@ember/array'; -import { moduleFor, AbstractTestCase } from 'internal-test-helpers'; +import { moduleFor, AbstractTestCase, expectDeprecation, testUnless } from 'internal-test-helpers'; +import { DEPRECATIONS } from '@ember/-internals/deprecations'; + +const EMBER_A = DEPRECATIONS.DEPRECATE_EMBER_ARRAY_A; moduleFor( 'NativeArray.replace', class extends AbstractTestCase { - ['@test raises assertion if third argument is not an array']() { + [`${testUnless(EMBER_A.isRemoved)} @test raises assertion if third argument is not an array`]() { + expectDeprecation(/`A` from `@ember\/array` is deprecated/, EMBER_A.isEnabled); + expectAssertion(function () { A([1, 2, 3]).replace(1, 1, ''); }, 'The third argument to replace needs to be an array.'); } - ['@test it does not raise an assertion if third parameter is not passed'](assert) { + [`${testUnless( + EMBER_A.isRemoved + )} @test it does not raise an assertion if third parameter is not passed`](assert) { + expectDeprecation(/`A` from `@ember\/array` is deprecated/, EMBER_A.isEnabled); + assert.deepEqual(A([1, 2, 3]).replace(1, 2), A([1]), 'no assertion raised'); } } diff --git a/packages/@ember/array/index-internal.ts b/packages/@ember/array/index-internal.ts index 2746d544132..20c8f021f54 100644 --- a/packages/@ember/array/index-internal.ts +++ b/packages/@ember/array/index-internal.ts @@ -551,41 +551,6 @@ const InternalMutableArray = InternalMixin.create(InternalEmberArray, InternalMu }, }); -/** - Creates an `NativeArray` from an Array-like object. - Does not modify the original object's contents. - - This exists primarily for historic reasons and should not be used - in new code. Prefer native [Array](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array) - or [trackedArray](/ember/release/functions/@ember%2Freactive%2Fcollections/trackedArray). - - Example - - ```app/components/my-component.js - import Component from '@ember/component'; - import { A } from '@ember/array'; - - export default Component.extend({ - tagName: 'ul', - classNames: ['pagination'], - - init() { - this._super(...arguments); - - if (!this.get('content')) { - this.set('content', A()); - this.set('otherContent', A([1,2,3])); - } - } - }); - ``` - - @method A - @static - @for @ember/array - @return {Ember.NativeArray} -*/ - // Add Ember.Array to Array.prototype. Remove methods with native // implementations and supply some more optimized versions of generic methods // because they are so common. @@ -636,6 +601,74 @@ A = function (this: unknown, arr?: Array) { } }; +const NATIVE_ARRAY_KEYS: string[] = []; +InternalNativeArray.keys().forEach((key) => { + if (!key.startsWith('_')) { + NATIVE_ARRAY_KEYS.push(key); + } +}); + +/** + Makes an Ember array for a value that Ember gives to application code. + + The array calls `report` with the name of each Ember array member + that application code uses. + + One call from application code gives one report, + because the members call each other. +*/ +export function reportingA(arr: T[], report: (name: string) => void): NativeArray { + let array = A(arr); + let isInside = false; + + function reportOnce(name: string, callback: () => R): R { + if (isInside) { + return callback(); + } + + report(name); + isInside = true; + + try { + return callback(); + } finally { + isInside = false; + } + } + + for (let name of NATIVE_ARRAY_KEYS) { + let descriptor = Object.getOwnPropertyDescriptor(array, name); + + if (descriptor === undefined) { + continue; + } + + let { value, get, set } = descriptor; + + if (typeof value === 'function') { + descriptor.value = function (this: unknown, ...args: unknown[]) { + return reportOnce(name, () => value.apply(this, args)); + }; + } else if (get !== undefined) { + descriptor.get = function (this: unknown) { + return reportOnce(name, () => get.call(this)); + }; + + if (set !== undefined) { + descriptor.set = function (this: unknown, newValue: unknown) { + reportOnce(name, () => set.call(this, newValue)); + }; + } + } else { + continue; + } + + Object.defineProperty(array, name, descriptor); + } + + return array; +} + export { A, InternalEmberArray, InternalMutableArray, InternalNativeArray }; export default InternalEmberArray; diff --git a/packages/@ember/array/index.ts b/packages/@ember/array/index.ts index 882e71fe473..5119fce4778 100644 --- a/packages/@ember/array/index.ts +++ b/packages/@ember/array/index.ts @@ -7,9 +7,10 @@ import type MutableEnumerable from '@ember/enumerable/mutable'; import type Observable from '@ember/object/observable'; import type { MethodNamesOf, MethodParams, MethodReturns } from '@ember/-internals/utility-types'; import isArray from './lib/is-array'; +import { assert } from '@ember/debug'; import { deprecateUntil, DEPRECATIONS } from '@ember/-internals/deprecations'; import InternalEmberArrayMixin, { - A, + A as internalA, InternalMutableArray as InternalMutableArrayMixin, InternalNativeArray as InternalNativeArrayMixin, } from '@ember/array/index-internal'; @@ -1485,6 +1486,44 @@ const NativeArray = DeprecatedMixin.create(InternalNativeArrayMixin, { }, }); +/** + Creates an `NativeArray` from an Array-like object. + Does not modify the original object's contents. + + Use a native [Array](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array) + or [trackedArray](/ember/release/functions/@ember%2Freactive%2Fcollections/trackedArray) instead. + + Example + + ```javascript + import { A } from '@ember/array'; + + let content = A(); + let otherContent = A([1, 2, 3]); + ``` + + @method A + @static + @for @ember/array + @return {Ember.NativeArray} + @deprecated Use a native array or `trackedArray` from `@ember/reactive/collections` instead. +*/ +let A: (arr?: Array) => NativeArray; + +A = function (this: unknown, arr?: Array) { + assert( + 'You cannot create an Ember Array with `new A()`, please update to calling A as a function: `A()`', + !(this instanceof A) + ); + + deprecateUntil( + '`A` from `@ember/array` is deprecated. Use a native array or `trackedArray` from `@ember/reactive/collections` instead.', + DEPRECATIONS.DEPRECATE_EMBER_ARRAY_A + ); + + return internalA(arr); +}; + export { A, NativeArray, MutableArray }; export default EmberArray; diff --git a/packages/@ember/debug/data-adapter.ts b/packages/@ember/debug/data-adapter.ts index 4b37faaff4b..42fa2855325 100644 --- a/packages/@ember/debug/data-adapter.ts +++ b/packages/@ember/debug/data-adapter.ts @@ -4,9 +4,7 @@ import { _backburner, next } from '@ember/runloop'; import { get } from '@ember/-internals/metal/lib/property_get'; import { dasherize } from '@ember/-internals/string'; import Namespace from '@ember/application/namespace'; -import type { NativeArray } from '@ember/array'; import EmberObject from '@ember/object'; -import { A as emberA } from '@ember/array'; import type { Cache } from '@glimmer/validator/lib/tracking'; import { consumeTag, createCache, getValue, untrack } from '@glimmer/validator/lib/tracking'; import { tagFor } from '@glimmer/validator/lib/meta'; @@ -34,7 +32,7 @@ type WrappedType = { type WrappedRecord = { object: T; columnValues: object; - searchKeywords: NativeArray; + searchKeywords: unknown[]; filterValues: object; color: RecordColor | null; }; @@ -96,7 +94,7 @@ class RecordsWatcher { } constructor( - records: NativeArray, + records: T[], recordsAdded: RecordCallback, recordsUpdated: RecordCallback, recordsRemoved: RecordCallback, @@ -221,7 +219,7 @@ class TypeWatcher { @public */ export default class DataAdapter extends EmberObject { - releaseMethods = emberA<() => void>(); + releaseMethods: Array<() => void> = []; recordsWatchers: Map void; revalidate: () => void }> = new Map(); typeWatchers: Map void; revalidate: () => void }> = new Map(); flushWatchers: (() => void) | null = null; @@ -321,7 +319,7 @@ export default class DataAdapter extends EmberObject { name: string; desc: string; }> { - return emberA(); + return []; } /** @@ -343,7 +341,7 @@ export default class DataAdapter extends EmberObject { typesUpdated: (types: WrappedType[]) => void ) { let modelTypes = this.getModelTypes(); - let releaseMethods = emberA<() => void>(); + let releaseMethods: Array<() => void> = []; let typesToSend; typesToSend = modelTypes.map((type) => { @@ -357,9 +355,12 @@ export default class DataAdapter extends EmberObject { let release = () => { releaseMethods.forEach((fn) => fn()); - this.releaseMethods.removeObject(release); + let index = this.releaseMethods.indexOf(release); + if (index !== -1) { + this.releaseMethods.splice(index, 1); + } }; - this.releaseMethods.pushObject(release); + this.releaseMethods.push(release); return release; } @@ -486,7 +487,7 @@ export default class DataAdapter extends EmberObject { desc: {String} Humanized description (what would show in a table column name). */ columnsForType(_klass: unknown): Column[] { - return emberA(); + return []; } /** @@ -613,8 +614,8 @@ export default class DataAdapter extends EmberObject { This array will be observed for changes, so it should update when new records are added/removed. */ - getRecords(_klass: unknown, _name: string): NativeArray { - return emberA(); + getRecords(_klass: unknown, _name: string): T[] { + return []; } /** @@ -656,8 +657,8 @@ export default class DataAdapter extends EmberObject { @method getRecordKeywords @return {Array} Relevant keywords for search. */ - getRecordKeywords(_record: T) { - return emberA(); + getRecordKeywords(_record: T): unknown[] { + return []; } /** diff --git a/packages/@ember/enumerable/tests/enumerable_test.js b/packages/@ember/enumerable/tests/enumerable_test.js index ee7e07e0139..6d1cc049ad4 100644 --- a/packages/@ember/enumerable/tests/enumerable_test.js +++ b/packages/@ember/enumerable/tests/enumerable_test.js @@ -4,10 +4,14 @@ import { A } from '@ember/array'; import { moduleFor, AbstractTestCase, expectDeprecation, testUnless } from 'internal-test-helpers'; import { DEPRECATIONS } from '@ember/-internals/deprecations'; +const EMBER_A = DEPRECATIONS.DEPRECATE_EMBER_ARRAY_A; + moduleFor( 'Enumerable', class extends AbstractTestCase { - ['@test should be mixed into A()'](assert) { + [`${testUnless(EMBER_A.isRemoved)} @test should be mixed into A()`](assert) { + expectDeprecation(/`A` from `@ember\/array` is deprecated/, EMBER_A.isEnabled); + assert.ok(Enumerable.detect(A())); } diff --git a/packages/@ember/object/lib/computed/reduce_computed_macros.ts b/packages/@ember/object/lib/computed/reduce_computed_macros.ts index d1a4b5fe2c0..d3988eb0353 100644 --- a/packages/@ember/object/lib/computed/reduce_computed_macros.ts +++ b/packages/@ember/object/lib/computed/reduce_computed_macros.ts @@ -3,6 +3,7 @@ */ import { DEBUG } from '@glimmer/env'; import { assert } from '@ember/debug'; +import { DEPRECATIONS, deprecateUntil } from '@ember/-internals/deprecations'; import { autoComputed } from '@ember/-internals/metal/lib/computed'; import { isDecoratorCall } from '@ember/-internals/metal/lib/decorator'; import type { FieldDecorator } from '@ember/-internals/metal/lib/decorator-util'; @@ -21,6 +22,13 @@ function isNativeOrEmberArray(obj: unknown): obj is unknown[] | EmberArray number, @@ -65,6 +73,67 @@ function arrayMacro( }).readOnly() as FieldDecorator; } +type MapCallback = (value: unknown, index: number) => unknown; + +function mapMacro(dependentKey: string, additionalDependentKeys: string[], callback: MapCallback) { + return arrayMacro(dependentKey, additionalDependentKeys, function (this: unknown, value) { + // This is so dumb... + return Array.isArray(value) ? value.map(callback, this) : value.map(callback, this); + }); +} + +type FilterCallback = ( + value: unknown, + index: number, + array: unknown[] | EmberArray +) => unknown; + +function filterMacro( + dependentKey: string, + additionalDependentKeys: string[], + callback: FilterCallback +) { + return arrayMacro( + dependentKey, + additionalDependentKeys, + function (this: unknown, value: unknown[] | EmberArray) { + // This is a really silly way to keep TS happy + return Array.isArray(value) + ? value.filter( + callback as (value: unknown, index: number, array: unknown[]) => unknown, + this + ) + : value.filter( + callback as (value: unknown, index: number, array: EmberArray) => unknown, + this + ); + } + ); +} + +function uniqMacro(dependentKeys: string[]) { + return multiArrayMacro( + dependentKeys, + function (this: unknown, dependentKeys) { + let uniq = emberA(); + let seen = new Set(); + dependentKeys.forEach((dependentKey) => { + let value = get(this, dependentKey); + if (isNativeOrEmberArray(value)) { + value.forEach((item: unknown) => { + if (!seen.has(item)) { + seen.add(item); + uniq.push(item); + } + }); + } + }); + return uniq; + }, + 'uniq' + ); +} + function multiArrayMacro( _dependentKeys: string[], callback: (dependentKeys: string[]) => unknown[], @@ -108,6 +177,7 @@ function multiArrayMacro( dependentKey's array @since 1.4.0 @public + @deprecated Use a getter with native array methods instead. */ export function sum(dependentKey: string) { assert( @@ -115,6 +185,8 @@ export function sum(dependentKey: string) { !isDecoratorCall(Array.prototype.slice.call(arguments)) ); + deprecateArrayMacro('sum'); + return reduceMacro(dependentKey, (sum: number, item: number) => sum + item, 0, 'sum'); } @@ -172,6 +244,7 @@ export function sum(dependentKey: string) { @return {ComputedProperty} computes the largest value in the dependentKey's array @public + @deprecated Use a getter with native array methods instead. */ export function max(dependentKey: string) { assert( @@ -179,6 +252,8 @@ export function max(dependentKey: string) { !isDecoratorCall(Array.prototype.slice.call(arguments)) ); + deprecateArrayMacro('max'); + return reduceMacro(dependentKey, (max, item) => Math.max(max, item), -Infinity, 'max'); } @@ -235,6 +310,7 @@ export function max(dependentKey: string) { @param {String} dependentKey @return {ComputedProperty} computes the smallest value in the dependentKey's array @public + @deprecated Use a getter with native array methods instead. */ export function min(dependentKey: string) { assert( @@ -242,6 +318,8 @@ export function min(dependentKey: string) { !isDecoratorCall(Array.prototype.slice.call(arguments)) ); + deprecateArrayMacro('min'); + return reduceMacro(dependentKey, (min, item) => Math.min(min, item), Infinity, 'min'); } @@ -319,6 +397,7 @@ export function min(dependentKey: string) { @param {Function} callback @return {ComputedProperty} an array mapped via the callback @public + @deprecated Use a getter with native array methods instead. */ export function map( dependentKey: string, @@ -339,6 +418,8 @@ export function map( !isDecoratorCall(Array.prototype.slice.call(arguments)) ); + deprecateArrayMacro('map'); + assert( 'The final parameter provided to map must be a callback function', typeof callback === 'function' || @@ -363,10 +444,7 @@ export function map( const cCallback = callback; assert('[BUG] Missing callback', cCallback); - return arrayMacro(dependentKey, additionalDependentKeys, function (this: unknown, value) { - // This is so dumb... - return Array.isArray(value) ? value.map(cCallback, this) : value.map(cCallback, this); - }); + return mapMacro(dependentKey, additionalDependentKeys, cCallback); } /** @@ -416,6 +494,7 @@ export function map( @param {String} propertyKey @return {ComputedProperty} an array mapped to the specified key @public + @deprecated Use a getter with native array methods instead. */ export function mapBy(dependentKey: string, propertyKey: string) { assert( @@ -423,6 +502,8 @@ export function mapBy(dependentKey: string, propertyKey: string) { !isDecoratorCall(Array.prototype.slice.call(arguments)) ); + deprecateArrayMacro('mapBy'); + assert( '`mapBy` computed macro expects a property string for its second argument, ' + 'perhaps you meant to use "map"', @@ -433,7 +514,7 @@ export function mapBy(dependentKey: string, propertyKey: string) { !/[[\]{}]/g.test(dependentKey) ); - return map(`${dependentKey}.@each.${propertyKey}`, (item) => get(item, propertyKey)); + return mapMacro(`${dependentKey}.@each.${propertyKey}`, [], (item) => get(item, propertyKey)); } /** @@ -543,6 +624,7 @@ export function mapBy(dependentKey: string, propertyKey: string) { @param {Function} callback @return {ComputedProperty} the filtered array @public + @deprecated Use a getter with native array methods instead. */ export function filter( dependentKey: string, @@ -565,6 +647,8 @@ export function filter( !isDecoratorCall(Array.prototype.slice.call(arguments)) ); + deprecateArrayMacro('filter'); + assert( 'The final parameter provided to filter must be a callback function', typeof callback === 'function' || @@ -586,24 +670,9 @@ export function filter( additionalDependentKeys = additionalDependentKeysOrCallback; } - const cCallback = callback; + assert('[BUG] Missing callback', callback); - return arrayMacro( - dependentKey, - additionalDependentKeys, - function (this: unknown, value: unknown[] | EmberArray) { - // This is a really silly way to keep TS happy - return Array.isArray(value) - ? value.filter( - cCallback as (value: unknown, index: number, array: unknown[]) => unknown, - this - ) - : value.filter( - cCallback as (value: unknown, index: number, array: EmberArray) => unknown, - this - ); - } - ); + return filterMacro(dependentKey, additionalDependentKeys, callback); } /** @@ -640,6 +709,7 @@ export function filter( @param {*} value @return {ComputedProperty} the filtered array @public + @deprecated Use a getter with native array methods instead. */ export function filterBy(dependentKey: string, propertyKey: string, value?: unknown) { assert( @@ -647,6 +717,8 @@ export function filterBy(dependentKey: string, propertyKey: string, value?: unkn !isDecoratorCall(Array.prototype.slice.call(arguments)) ); + deprecateArrayMacro('filterBy'); + assert( `Dependent key passed to \`filterBy\` computed macro shouldn't contain brace expanding pattern.`, !/[[\]{}]/g.test(dependentKey) @@ -659,7 +731,7 @@ export function filterBy(dependentKey: string, propertyKey: string, value?: unkn callback = (item: unknown) => get(item, propertyKey) === value; } - return filter(`${dependentKey}.@each.${propertyKey}`, callback); + return filterMacro(`${dependentKey}.@each.${propertyKey}`, [], callback); } /** @@ -697,6 +769,7 @@ export function filterBy(dependentKey: string, propertyKey: string, value?: unkn @return {ComputedProperty} computes a new array with all the unique elements from the dependent array @public + @deprecated Use a getter with native array methods instead. */ export function uniq(dependentKey: string, ...additionalDependentKeys: string[]): FieldDecorator { assert( @@ -704,30 +777,9 @@ export function uniq(dependentKey: string, ...additionalDependentKeys: string[]) !isDecoratorCall(Array.prototype.slice.call(arguments)) ); - let args = [dependentKey, ...additionalDependentKeys]; - - return multiArrayMacro( - args, - function (this: unknown, dependentKeys) { - let uniq = emberA(); - let seen = new Set(); + deprecateArrayMacro('uniq'); - dependentKeys.forEach((dependentKey) => { - let value = get(this, dependentKey); - if (isNativeOrEmberArray(value)) { - value.forEach((item: unknown) => { - if (!seen.has(item)) { - seen.add(item); - uniq.push(item); - } - }); - } - }); - - return uniq; - }, - 'uniq' - ); + return uniqMacro([dependentKey, ...additionalDependentKeys]); } /** @@ -766,6 +818,7 @@ export function uniq(dependentKey: string, ...additionalDependentKeys: string[]) @return {ComputedProperty} computes a new array with all the unique elements from the dependent array @public + @deprecated Use a getter with native array methods instead. */ export function uniqBy(dependentKey: string, propertyKey: string) { assert( @@ -773,6 +826,8 @@ export function uniqBy(dependentKey: string, propertyKey: string) { !isDecoratorCall(Array.prototype.slice.call(arguments)) ); + deprecateArrayMacro('uniqBy'); + assert( `Dependent key passed to \`uniqBy\` computed macro shouldn't contain brace expanding pattern.`, !/[[\]{}]/g.test(dependentKey) @@ -828,8 +883,18 @@ export function uniqBy(dependentKey: string, propertyKey: string) { @return {ComputedProperty} computes a new array with all the unique elements from one or more dependent arrays. @public + @deprecated Use a getter with native array methods instead. */ -export let union = uniq; +export function union(dependentKey: string, ...additionalDependentKeys: string[]): FieldDecorator { + assert( + 'You attempted to use @uniq/@union as a decorator directly, but it requires atleast one dependent key parameter', + !isDecoratorCall(Array.prototype.slice.call(arguments)) + ); + + deprecateArrayMacro('union'); + + return uniqMacro([dependentKey, ...additionalDependentKeys]); +} /** A computed property which returns a new array with all the elements @@ -865,6 +930,7 @@ export let union = uniq; @return {ComputedProperty} computes a new array with all the duplicated elements from the dependent arrays @public + @deprecated Use a getter with native array methods instead. */ export function intersect(dependentKey: string, ...additionalDependentKeys: string[]) { assert( @@ -872,6 +938,8 @@ export function intersect(dependentKey: string, ...additionalDependentKeys: stri !isDecoratorCall(Array.prototype.slice.call(arguments)) ); + deprecateArrayMacro('intersect'); + let args = [dependentKey, ...additionalDependentKeys]; return multiArrayMacro( @@ -954,6 +1022,7 @@ export function intersect(dependentKey: string, ...additionalDependentKeys: stri @return {ComputedProperty} computes a new array with all the items from the first dependent array that are not in the second dependent array @public + @deprecated Use a getter with native array methods instead. */ export function setDiff(setAProperty: string, setBProperty: string) { assert( @@ -961,6 +1030,8 @@ export function setDiff(setAProperty: string, setBProperty: string) { !isDecoratorCall(Array.prototype.slice.call(arguments)) ); + deprecateArrayMacro('setDiff'); + assert('`setDiff` computed macro requires exactly two dependent arrays.', arguments.length === 2); assert( `Dependent keys passed to \`setDiff\` computed macro shouldn't contain brace expanding pattern.`, @@ -1012,6 +1083,7 @@ export function setDiff(setAProperty: string, setBProperty: string) { @return {ComputedProperty} computed property which maps values of all passed in properties to an array. @public + @deprecated Use a getter with native array methods instead. */ export function collect(dependentKey: string, ...additionalDependentKeys: string[]) { assert( @@ -1019,6 +1091,8 @@ export function collect(dependentKey: string, ...additionalDependentKeys: string !isDecoratorCall(Array.prototype.slice.call(arguments)) ); + deprecateArrayMacro('collect'); + let dependentKeys = [dependentKey, ...additionalDependentKeys]; return multiArrayMacro( @@ -1179,6 +1253,7 @@ type SortDefinition = (itemA: any, itemB: any) => number; @return {ComputedProperty} computes a new sorted array based on the sort property array or callback function @public + @deprecated Use a getter with native array methods instead. */ export function sort(itemsKey: string, sortDefinition: SortDefinition | string): FieldDecorator; export function sort( @@ -1196,6 +1271,8 @@ export function sort( !isDecoratorCall(Array.prototype.slice.call(arguments)) ); + deprecateArrayMacro('sort'); + if (DEBUG) { let argumentsValid = false; diff --git a/packages/@ember/object/tests/computed/computed_macros_test.js b/packages/@ember/object/tests/computed/computed_macros_test.js index c127c6aa945..5a2bac30aa6 100644 --- a/packages/@ember/object/tests/computed/computed_macros_test.js +++ b/packages/@ember/object/tests/computed/computed_macros_test.js @@ -23,11 +23,13 @@ import { DEPRECATIONS } from '@ember/-internals/deprecations'; import { moduleFor, AbstractTestCase, expectDeprecation, testUnless } from 'internal-test-helpers'; const { isEnabled, isRemoved } = DEPRECATIONS.DEPRECATE_EMBER_UTILS; +const EMBER_A = DEPRECATIONS.DEPRECATE_EMBER_ARRAY_A; moduleFor( 'CP macros', class extends AbstractTestCase { - [`${testUnless(isRemoved)} @test empty part 1/2`](assert) { + [`${testUnless(isRemoved || EMBER_A.isRemoved)} @test empty part 1/2`](assert) { + expectDeprecation(/`A` from `@ember\/array` is deprecated/, EMBER_A.isEnabled); expectDeprecation(/`empty` from `@ember\/object\/computed` is deprecated/, isEnabled); expectDeprecation(/`isEmpty` from `@ember\/utils` is deprecated/, isEnabled); @@ -51,7 +53,8 @@ moduleFor( assert.equal(get(obj, 'noLannistersKnown'), false, 'empty respects array mutations'); } - [`${testUnless(isRemoved)} @test notEmpty part 1/2`](assert) { + [`${testUnless(isRemoved || EMBER_A.isRemoved)} @test notEmpty part 1/2`](assert) { + expectDeprecation(/`A` from `@ember\/array` is deprecated/, EMBER_A.isEnabled); expectDeprecation(/`notEmpty` from `@ember\/object\/computed` is deprecated/, isEnabled); expectDeprecation(/`isEmpty` from `@ember\/utils` is deprecated/, isEnabled); diff --git a/packages/@ember/object/tests/computed/reduce_computed_macros_test.js b/packages/@ember/object/tests/computed/reduce_computed_macros_test.js index b481387b089..3820d785067 100644 --- a/packages/@ember/object/tests/computed/reduce_computed_macros_test.js +++ b/packages/@ember/object/tests/computed/reduce_computed_macros_test.js @@ -9,7 +9,7 @@ import EmberObject, { observer, } from '@ember/object'; import ObjectProxy from '@ember/object/proxy'; -import { isArray, A as emberA, removeAt } from '@ember/array'; +import { A as emberA, isArray, removeAt } from '@ember/array'; import { sum, min, @@ -35,649 +35,768 @@ import { } from 'internal-test-helpers'; import { DEPRECATIONS } from '@ember/-internals/deprecations'; -let obj; -moduleFor( - 'map', - class extends AbstractTestCase { - beforeEach() { - obj = class extends EmberObject { - @map('array.@each.v', (item) => item.v) - mapped; - - @map('arrayObjects.@each.v', (item) => ({ - name: item.v.name, - })) - mappedObjects; - }.create({ - arrayObjects: emberA([{ v: { name: 'Robert' } }, { v: { name: 'Leanna' } }]), - - array: emberA([{ v: 1 }, { v: 3 }, { v: 2 }, { v: 1 }]), - }); - } +const { isEnabled, isRemoved } = DEPRECATIONS.DEPRECATE_ARRAY_COMPUTED_MACROS; +const EMBER_A_REMOVED = DEPRECATIONS.DEPRECATE_EMBER_ARRAY_A.isRemoved; - afterEach() { - run(obj, 'destroy'); - } +class ArrayMacroTestCase extends AbstractTestCase { + constructor(assert) { + super(assert); - ['@test map is readOnly'](assert) { - assert.throws(function () { - obj.set('mapped', 1); - }, /Cannot set read-only property "mapped" on object:/); + if (isEnabled) { + expectDeprecation( + /from `@ember\/object\/computed` is deprecated. Use a getter with native array methods instead|`A` from `@ember\/array` is deprecated/ + ); } + } +} - ['@test it maps simple properties'](assert) { - assert.deepEqual(obj.get('mapped'), [1, 3, 2, 1]); +if (!isRemoved && !EMBER_A_REMOVED) { + let obj; + moduleFor( + 'map', + class extends ArrayMacroTestCase { + beforeEach() { + obj = class extends EmberObject { + @map('array.@each.v', (item) => item.v) + mapped; - obj.get('array').pushObject({ v: 5 }); + @map('arrayObjects.@each.v', (item) => ({ + name: item.v.name, + })) + mappedObjects; + }.create({ + arrayObjects: emberA([{ v: { name: 'Robert' } }, { v: { name: 'Leanna' } }]), - assert.deepEqual(obj.get('mapped'), [1, 3, 2, 1, 5]); + array: emberA([{ v: 1 }, { v: 3 }, { v: 2 }, { v: 1 }]), + }); + } - removeAt(obj.get('array'), 3); + afterEach() { + run(obj, 'destroy'); + } - assert.deepEqual(obj.get('mapped'), [1, 3, 2, 5]); - } + ['@test map is readOnly'](assert) { + assert.throws(function () { + obj.set('mapped', 1); + }, /Cannot set read-only property "mapped" on object:/); + } - ['@test it maps simple unshifted properties'](assert) { - let array = emberA(); + ['@test it maps simple properties'](assert) { + assert.deepEqual(obj.get('mapped'), [1, 3, 2, 1]); - obj = class extends EmberObject { - @map('array', (item) => item.toUpperCase()) - mapped; - }.create({ - array, - }); + obj.get('array').pushObject({ v: 5 }); - array.unshiftObject('c'); - array.unshiftObject('b'); - array.unshiftObject('a'); + assert.deepEqual(obj.get('mapped'), [1, 3, 2, 1, 5]); - array.popObject(); + removeAt(obj.get('array'), 3); - assert.deepEqual( - obj.get('mapped'), - ['A', 'B'], - 'properties unshifted in sequence are mapped correctly' - ); - } + assert.deepEqual(obj.get('mapped'), [1, 3, 2, 5]); + } - ['@test it has the correct `this`'](assert) { - obj = class extends EmberObject { - @map('array', function (item) { - assert.equal(this, obj, 'should have correct context'); - return this.upperCase(item); - }) - mapped; + ['@test it maps simple unshifted properties'](assert) { + let array = emberA(); - upperCase(string) { - return string.toUpperCase(); - } - }.create({ - array: ['a', 'b', 'c'], - }); + obj = class extends EmberObject { + @map('array', (item) => item.toUpperCase()) + mapped; + }.create({ + array, + }); - assert.deepEqual( - obj.get('mapped'), - ['A', 'B', 'C'], - 'properties unshifted in sequence are mapped correctly' - ); - } + array.unshiftObject('c'); + array.unshiftObject('b'); + array.unshiftObject('a'); - ['@test it passes the index to the callback'](assert) { - let array = ['a', 'b', 'c']; + array.popObject(); - obj = class extends EmberObject { - @map('array', (item, index) => index) - mapped; - }.create({ - array, - }); + assert.deepEqual( + obj.get('mapped'), + ['A', 'B'], + 'properties unshifted in sequence are mapped correctly' + ); + } - assert.deepEqual(obj.get('mapped'), [0, 1, 2], 'index is passed to callback correctly'); - } + ['@test it has the correct `this`'](assert) { + obj = class extends EmberObject { + @map('array', function (item) { + assert.equal(this, obj, 'should have correct context'); + return this.upperCase(item); + }) + mapped; + + upperCase(string) { + return string.toUpperCase(); + } + }.create({ + array: ['a', 'b', 'c'], + }); - ['@test it maps objects'](assert) { - assert.deepEqual(obj.get('mappedObjects'), [{ name: 'Robert' }, { name: 'Leanna' }]); + assert.deepEqual( + obj.get('mapped'), + ['A', 'B', 'C'], + 'properties unshifted in sequence are mapped correctly' + ); + } - obj.get('arrayObjects').pushObject({ - v: { name: 'Eddard' }, - }); + ['@test it passes the index to the callback'](assert) { + let array = ['a', 'b', 'c']; - assert.deepEqual(obj.get('mappedObjects'), [ - { name: 'Robert' }, - { name: 'Leanna' }, - { name: 'Eddard' }, - ]); + obj = class extends EmberObject { + @map('array', (item, index) => index) + mapped; + }.create({ + array, + }); - removeAt(obj.get('arrayObjects'), 1); + assert.deepEqual(obj.get('mapped'), [0, 1, 2], 'index is passed to callback correctly'); + } - assert.deepEqual(obj.get('mappedObjects'), [{ name: 'Robert' }, { name: 'Eddard' }]); + ['@test it maps objects'](assert) { + assert.deepEqual(obj.get('mappedObjects'), [{ name: 'Robert' }, { name: 'Leanna' }]); - set(obj.get('arrayObjects')[0], 'v', { name: 'Stannis' }); + obj.get('arrayObjects').pushObject({ + v: { name: 'Eddard' }, + }); - assert.deepEqual(obj.get('mappedObjects'), [{ name: 'Stannis' }, { name: 'Eddard' }]); - } + assert.deepEqual(obj.get('mappedObjects'), [ + { name: 'Robert' }, + { name: 'Leanna' }, + { name: 'Eddard' }, + ]); - ['@test it maps unshifted objects with property observers'](assert) { - let array = emberA(); - let cObj = { v: 'c' }; + removeAt(obj.get('arrayObjects'), 1); - obj = class extends EmberObject { - @map('array.@each.v', (item) => get(item, 'v').toUpperCase()) - mapped; - }.create({ - array, - }); + assert.deepEqual(obj.get('mappedObjects'), [{ name: 'Robert' }, { name: 'Eddard' }]); - array.unshiftObject(cObj); - array.unshiftObject({ v: 'b' }); - array.unshiftObject({ v: 'a' }); + set(obj.get('arrayObjects')[0], 'v', { name: 'Stannis' }); - set(cObj, 'v', 'd'); + assert.deepEqual(obj.get('mappedObjects'), [{ name: 'Stannis' }, { name: 'Eddard' }]); + } - assert.deepEqual(array.mapBy('v'), ['a', 'b', 'd'], 'precond - unmapped array is correct'); - assert.deepEqual( - obj.get('mapped'), - ['A', 'B', 'D'], - 'properties unshifted in sequence are mapped correctly' - ); - } + ['@test it maps unshifted objects with property observers'](assert) { + let array = emberA(); + let cObj = { v: 'c' }; - ['@test it updates if additional dependent keys are modified'](assert) { - obj = class extends EmberObject { - @map('array', ['key'], function (item) { - return item[this.key]; - }) - mapped; - }.create({ - key: 'name', - array: emberA([{ name: 'Cercei', house: 'Lannister' }]), - }); + obj = class extends EmberObject { + @map('array.@each.v', (item) => get(item, 'v').toUpperCase()) + mapped; + }.create({ + array, + }); - assert.deepEqual( - obj.get('mapped'), - ['Cercei'], - 'precond - mapped array is initially correct' - ); + array.unshiftObject(cObj); + array.unshiftObject({ v: 'b' }); + array.unshiftObject({ v: 'a' }); - obj.set('key', 'house'); - assert.deepEqual( - obj.get('mapped'), - ['Lannister'], - 'mapped prop updates correctly when additional dependency is updated' - ); - } + set(cObj, 'v', 'd'); - ['@test it throws on bad inputs']() { - expectAssertion(() => { - map('items.@each.{prop}', 'foo'); - }, /The final parameter provided to map must be a callback function/); + assert.deepEqual(array.mapBy('v'), ['a', 'b', 'd'], 'precond - unmapped array is correct'); + assert.deepEqual( + obj.get('mapped'), + ['A', 'B', 'D'], + 'properties unshifted in sequence are mapped correctly' + ); + } - expectAssertion(() => { - map('items.@each.{prop}', 'foo', function () {}); - }, /The second parameter provided to map must either be the callback or an array of additional dependent keys/); + ['@test it updates if additional dependent keys are modified'](assert) { + obj = class extends EmberObject { + @map('array', ['key'], function (item) { + return item[this.key]; + }) + mapped; + }.create({ + key: 'name', + array: emberA([{ name: 'Cercei', house: 'Lannister' }]), + }); - expectAssertion(() => { - map('items.@each.{prop}', function () {}, ['foo']); - }, /The final parameter provided to map must be a callback function/); + assert.deepEqual( + obj.get('mapped'), + ['Cercei'], + 'precond - mapped array is initially correct' + ); - expectAssertion(() => { - map('items.@each.{prop}', ['foo']); - }, /The final parameter provided to map must be a callback function/); - } - } -); + obj.set('key', 'house'); + assert.deepEqual( + obj.get('mapped'), + ['Lannister'], + 'mapped prop updates correctly when additional dependency is updated' + ); + } -moduleFor( - 'mapBy', - class extends AbstractTestCase { - beforeEach() { - obj = class extends EmberObject { - @mapBy('array', 'v') - mapped; - }.create({ - array: emberA([{ v: 1 }, { v: 3 }, { v: 2 }, { v: 1 }]), - }); - } + ['@test it throws on bad inputs']() { + expectAssertion(() => { + map('items.@each.{prop}', 'foo'); + }, /The final parameter provided to map must be a callback function/); - afterEach() { - run(obj, 'destroy'); - } + expectAssertion(() => { + map('items.@each.{prop}', 'foo', function () {}); + }, /The second parameter provided to map must either be the callback or an array of additional dependent keys/); + + expectAssertion(() => { + map('items.@each.{prop}', function () {}, ['foo']); + }, /The final parameter provided to map must be a callback function/); - ['@test mapBy is readOnly'](assert) { - assert.throws(function () { - obj.set('mapped', 1); - }, /Cannot set read-only property "mapped" on object:/); + expectAssertion(() => { + map('items.@each.{prop}', ['foo']); + }, /The final parameter provided to map must be a callback function/); + } } + ); - ['@test it maps properties'](assert) { - assert.deepEqual(obj.get('mapped'), [1, 3, 2, 1]); + moduleFor( + 'mapBy', + class extends ArrayMacroTestCase { + beforeEach() { + obj = class extends EmberObject { + @mapBy('array', 'v') + mapped; + }.create({ + array: emberA([{ v: 1 }, { v: 3 }, { v: 2 }, { v: 1 }]), + }); + } - obj.get('array').pushObject({ v: 5 }); + afterEach() { + run(obj, 'destroy'); + } - assert.deepEqual(obj.get('mapped'), [1, 3, 2, 1, 5]); + ['@test mapBy is readOnly'](assert) { + assert.throws(function () { + obj.set('mapped', 1); + }, /Cannot set read-only property "mapped" on object:/); + } - removeAt(obj.get('array'), 3); + ['@test it maps properties'](assert) { + assert.deepEqual(obj.get('mapped'), [1, 3, 2, 1]); - assert.deepEqual(obj.get('mapped'), [1, 3, 2, 5]); - } + obj.get('array').pushObject({ v: 5 }); - async ['@test it is observable'](assert) { - let calls = 0; + assert.deepEqual(obj.get('mapped'), [1, 3, 2, 1, 5]); - assert.deepEqual(obj.get('mapped'), [1, 3, 2, 1]); + removeAt(obj.get('array'), 3); - addObserver(obj, 'mapped.@each', () => calls++); + assert.deepEqual(obj.get('mapped'), [1, 3, 2, 5]); + } - obj.get('array').pushObject({ v: 5 }); - await runLoopSettled(); + async ['@test it is observable'](assert) { + let calls = 0; - assert.equal(calls, 1, 'mapBy is observable'); - } - } -); + assert.deepEqual(obj.get('mapped'), [1, 3, 2, 1]); -moduleFor( - 'filter', - class extends AbstractTestCase { - beforeEach() { - obj = class extends EmberObject { - @filter('array', (item) => item % 2 === 0) - filtered; - }.create({ - array: emberA([1, 2, 3, 4, 5, 6, 7, 8]), - }); - } + addObserver(obj, 'mapped.@each', () => calls++); - afterEach() { - run(obj, 'destroy'); - } + obj.get('array').pushObject({ v: 5 }); + await runLoopSettled(); - ['@test filter is readOnly'](assert) { - assert.throws(function () { - obj.set('filtered', 1); - }, /Cannot set read-only property "filtered" on object:/); + assert.equal(calls, 1, 'mapBy is observable'); + } } + ); - ['@test it filters according to the specified filter function'](assert) { - assert.deepEqual( - obj.get('filtered'), - [2, 4, 6, 8], - 'filter filters by the specified function' - ); - } + moduleFor( + 'filter', + class extends ArrayMacroTestCase { + beforeEach() { + obj = class extends EmberObject { + @filter('array', (item) => item % 2 === 0) + filtered; + }.create({ + array: emberA([1, 2, 3, 4, 5, 6, 7, 8]), + }); + } - ['@test it passes the index to the callback'](assert) { - obj = class extends EmberObject { - @filter('array', (item, index) => index === 1) - filtered; - }.create({ - array: ['a', 'b', 'c'], - }); + afterEach() { + run(obj, 'destroy'); + } - assert.deepEqual(get(obj, 'filtered'), ['b'], 'index is passed to callback correctly'); - } + ['@test filter is readOnly'](assert) { + assert.throws(function () { + obj.set('filtered', 1); + }, /Cannot set read-only property "filtered" on object:/); + } - ['@test it has the correct `this`'](assert) { - obj = class extends EmberObject { - @filter('array', function (item, index) { - assert.equal(this, obj); - return this.isOne(index); - }) - filtered; + ['@test it filters according to the specified filter function'](assert) { + assert.deepEqual( + obj.get('filtered'), + [2, 4, 6, 8], + 'filter filters by the specified function' + ); + } - isOne(value) { - return value === 1; - } - }.create({ - array: ['a', 'b', 'c'], - }); + ['@test it passes the index to the callback'](assert) { + obj = class extends EmberObject { + @filter('array', (item, index) => index === 1) + filtered; + }.create({ + array: ['a', 'b', 'c'], + }); - assert.deepEqual(get(obj, 'filtered'), ['b'], 'index is passed to callback correctly'); - } + assert.deepEqual(get(obj, 'filtered'), ['b'], 'index is passed to callback correctly'); + } - ['@test it passes the array to the callback'](assert) { - obj = class extends EmberObject { - @filter('array', (item, index, array) => index === get(array, 'length') - 2) - filtered; - }.create({ - array: emberA(['a', 'b', 'c']), - }); + ['@test it has the correct `this`'](assert) { + obj = class extends EmberObject { + @filter('array', function (item, index) { + assert.equal(this, obj); + return this.isOne(index); + }) + filtered; + + isOne(value) { + return value === 1; + } + }.create({ + array: ['a', 'b', 'c'], + }); - assert.deepEqual(obj.get('filtered'), ['b'], 'array is passed to callback correctly'); - } + assert.deepEqual(get(obj, 'filtered'), ['b'], 'index is passed to callback correctly'); + } - ['@test it caches properly'](assert) { - let array = obj.get('array'); + ['@test it passes the array to the callback'](assert) { + obj = class extends EmberObject { + @filter('array', (item, index, array) => index === get(array, 'length') - 2) + filtered; + }.create({ + array: emberA(['a', 'b', 'c']), + }); - let filtered = obj.get('filtered'); - assert.ok(filtered === obj.get('filtered')); + assert.deepEqual(obj.get('filtered'), ['b'], 'array is passed to callback correctly'); + } - array.addObject(11); - let newFiltered = obj.get('filtered'); + ['@test it caches properly'](assert) { + let array = obj.get('array'); - assert.ok(filtered !== newFiltered); + let filtered = obj.get('filtered'); + assert.ok(filtered === obj.get('filtered')); - assert.ok(obj.get('filtered') === newFiltered); - } + array.addObject(11); + let newFiltered = obj.get('filtered'); - ['@test it updates as the array is modified'](assert) { - let array = obj.get('array'); + assert.ok(filtered !== newFiltered); - assert.deepEqual( - obj.get('filtered'), - [2, 4, 6, 8], - 'precond - filtered array is initially correct' - ); + assert.ok(obj.get('filtered') === newFiltered); + } - array.addObject(11); - assert.deepEqual( - obj.get('filtered'), - [2, 4, 6, 8], - 'objects not passing the filter are not added' - ); + ['@test it updates as the array is modified'](assert) { + let array = obj.get('array'); - array.addObject(12); - assert.deepEqual( - obj.get('filtered'), - [2, 4, 6, 8, 12], - 'objects passing the filter are added' - ); + assert.deepEqual( + obj.get('filtered'), + [2, 4, 6, 8], + 'precond - filtered array is initially correct' + ); - array.removeObject(3); - array.removeObject(4); + array.addObject(11); + assert.deepEqual( + obj.get('filtered'), + [2, 4, 6, 8], + 'objects not passing the filter are not added' + ); - assert.deepEqual( - obj.get('filtered'), - [2, 6, 8, 12], - 'objects removed from the dependent array are removed from the computed array' - ); - } + array.addObject(12); + assert.deepEqual( + obj.get('filtered'), + [2, 4, 6, 8, 12], + 'objects passing the filter are added' + ); - ['@test the dependent array can be cleared one at a time'](assert) { - let array = get(obj, 'array'); + array.removeObject(3); + array.removeObject(4); - assert.deepEqual( - obj.get('filtered'), - [2, 4, 6, 8], - 'precond - filtered array is initially correct' - ); + assert.deepEqual( + obj.get('filtered'), + [2, 6, 8, 12], + 'objects removed from the dependent array are removed from the computed array' + ); + } - // clear 1-8 but in a random order - array.removeObject(3); - array.removeObject(1); - array.removeObject(2); - array.removeObject(4); - array.removeObject(8); - array.removeObject(6); - array.removeObject(5); - array.removeObject(7); - - assert.deepEqual(obj.get('filtered'), [], 'filtered array cleared correctly'); - } + ['@test the dependent array can be cleared one at a time'](assert) { + let array = get(obj, 'array'); - ['@test the dependent array can be `clear`ed directly (#3272)'](assert) { - assert.deepEqual( - obj.get('filtered'), - [2, 4, 6, 8], - 'precond - filtered array is initially correct' - ); + assert.deepEqual( + obj.get('filtered'), + [2, 4, 6, 8], + 'precond - filtered array is initially correct' + ); - obj.get('array').clear(); + // clear 1-8 but in a random order + array.removeObject(3); + array.removeObject(1); + array.removeObject(2); + array.removeObject(4); + array.removeObject(8); + array.removeObject(6); + array.removeObject(5); + array.removeObject(7); - assert.deepEqual(obj.get('filtered'), [], 'filtered array cleared correctly'); - } + assert.deepEqual(obj.get('filtered'), [], 'filtered array cleared correctly'); + } - ['@test it updates as the array is replaced'](assert) { - assert.deepEqual( - obj.get('filtered'), - [2, 4, 6, 8], - 'precond - filtered array is initially correct' - ); + ['@test the dependent array can be `clear`ed directly (#3272)'](assert) { + assert.deepEqual( + obj.get('filtered'), + [2, 4, 6, 8], + 'precond - filtered array is initially correct' + ); - obj.set('array', [20, 21, 22, 23, 24]); + obj.get('array').clear(); - assert.deepEqual( - obj.get('filtered'), - [20, 22, 24], - 'computed array is updated when array is changed' - ); - } + assert.deepEqual(obj.get('filtered'), [], 'filtered array cleared correctly'); + } - ['@test it updates properly on @each with {} dependencies'](assert) { - let item = EmberObject.create({ prop: true }); + ['@test it updates as the array is replaced'](assert) { + assert.deepEqual( + obj.get('filtered'), + [2, 4, 6, 8], + 'precond - filtered array is initially correct' + ); - obj = class extends EmberObject { - @filter('items.@each.{prop}', (item) => item.get('prop') === true) - filtered; - }.create({ - items: emberA([item]), - }); + obj.set('array', [20, 21, 22, 23, 24]); - assert.deepEqual(obj.get('filtered'), [item]); + assert.deepEqual( + obj.get('filtered'), + [20, 22, 24], + 'computed array is updated when array is changed' + ); + } - item.set('prop', false); + ['@test it updates properly on @each with {} dependencies'](assert) { + let item = EmberObject.create({ prop: true }); - assert.deepEqual(obj.get('filtered'), []); - } + obj = class extends EmberObject { + @filter('items.@each.{prop}', (item) => item.get('prop') === true) + filtered; + }.create({ + items: emberA([item]), + }); - ['@test it updates if additional dependent keys are modified'](assert) { - obj = class extends EmberObject { - @filter('array', ['modulo'], function (item) { - return item % this.modulo === 0; - }) - filtered; - }.create({ - modulo: 2, - array: emberA([1, 2, 3, 4, 5, 6, 7, 8]), - }); + assert.deepEqual(obj.get('filtered'), [item]); - assert.deepEqual( - obj.get('filtered'), - [2, 4, 6, 8], - 'precond - filtered array is initially correct' - ); + item.set('prop', false); - obj.set('modulo', 3); - assert.deepEqual( - obj.get('filtered'), - [3, 6], - 'filtered prop updates correctly when additional dependency is updated' - ); - } + assert.deepEqual(obj.get('filtered'), []); + } - ['@test it throws on bad inputs']() { - expectAssertion(() => { - filter('items.@each.{prop}', 'foo'); - }, /The final parameter provided to filter must be a callback function/); + ['@test it updates if additional dependent keys are modified'](assert) { + obj = class extends EmberObject { + @filter('array', ['modulo'], function (item) { + return item % this.modulo === 0; + }) + filtered; + }.create({ + modulo: 2, + array: emberA([1, 2, 3, 4, 5, 6, 7, 8]), + }); - expectAssertion(() => { - filter('items.@each.{prop}', 'foo', function () {}); - }, /The second parameter provided to filter must either be the callback or an array of additional dependent keys/); + assert.deepEqual( + obj.get('filtered'), + [2, 4, 6, 8], + 'precond - filtered array is initially correct' + ); - expectAssertion(() => { - filter('items.@each.{prop}', function () {}, ['foo']); - }, /The final parameter provided to filter must be a callback function/); + obj.set('modulo', 3); + assert.deepEqual( + obj.get('filtered'), + [3, 6], + 'filtered prop updates correctly when additional dependency is updated' + ); + } - expectAssertion(() => { - filter('items.@each.{prop}', ['foo']); - }, /The final parameter provided to filter must be a callback function/); - } - } -); + ['@test it throws on bad inputs']() { + expectAssertion(() => { + filter('items.@each.{prop}', 'foo'); + }, /The final parameter provided to filter must be a callback function/); -moduleFor( - 'filterBy', - class extends AbstractTestCase { - beforeEach() { - obj = class extends EmberObject { - @filterBy('array', 'a', 1) - a1s; - @filterBy('array', 'a') - as; - @filterBy('array', 'b') - bs; - }.create({ - array: emberA([ - { name: 'one', a: 1, b: false }, - { name: 'two', a: 2, b: false }, - { name: 'three', a: 1, b: true }, - { name: 'four', b: true }, - ]), - }); - } + expectAssertion(() => { + filter('items.@each.{prop}', 'foo', function () {}); + }, /The second parameter provided to filter must either be the callback or an array of additional dependent keys/); - afterEach() { - run(obj, 'destroy'); - } + expectAssertion(() => { + filter('items.@each.{prop}', function () {}, ['foo']); + }, /The final parameter provided to filter must be a callback function/); - ['@test filterBy is readOnly'](assert) { - assert.throws(function () { - obj.set('as', 1); - }, /Cannot set read-only property "as" on object:/); + expectAssertion(() => { + filter('items.@each.{prop}', ['foo']); + }, /The final parameter provided to filter must be a callback function/); + } } + ); - ['@test properties can be filtered by truthiness'](assert) { - assert.deepEqual( - obj.get('as').mapBy('name'), - ['one', 'two', 'three'], - 'properties can be filtered by existence' - ); - assert.deepEqual(obj.get('bs').mapBy('name'), ['three', 'four'], 'booleans can be filtered'); + moduleFor( + 'filterBy', + class extends ArrayMacroTestCase { + beforeEach() { + obj = class extends EmberObject { + @filterBy('array', 'a', 1) + a1s; + @filterBy('array', 'a') + as; + @filterBy('array', 'b') + bs; + }.create({ + array: emberA([ + { name: 'one', a: 1, b: false }, + { name: 'two', a: 2, b: false }, + { name: 'three', a: 1, b: true }, + { name: 'four', b: true }, + ]), + }); + } - set(obj.get('array')[0], 'a', undefined); - set(obj.get('array')[3], 'a', true); + afterEach() { + run(obj, 'destroy'); + } - set(obj.get('array')[0], 'b', true); - set(obj.get('array')[3], 'b', false); + ['@test filterBy is readOnly'](assert) { + assert.throws(function () { + obj.set('as', 1); + }, /Cannot set read-only property "as" on object:/); + } - assert.deepEqual( - obj.get('as').mapBy('name'), - ['two', 'three', 'four'], - 'arrays computed by filter property respond to property changes' - ); - assert.deepEqual( - obj.get('bs').mapBy('name'), - ['one', 'three'], - 'arrays computed by filtered property respond to property changes' - ); + ['@test properties can be filtered by truthiness'](assert) { + assert.deepEqual( + obj.get('as').mapBy('name'), + ['one', 'two', 'three'], + 'properties can be filtered by existence' + ); + assert.deepEqual( + obj.get('bs').mapBy('name'), + ['three', 'four'], + 'booleans can be filtered' + ); - obj.get('array').pushObject({ name: 'five', a: 6, b: true }); + set(obj.get('array')[0], 'a', undefined); + set(obj.get('array')[3], 'a', true); - assert.deepEqual( - obj.get('as').mapBy('name'), - ['two', 'three', 'four', 'five'], - 'arrays computed by filter property respond to added objects' - ); - assert.deepEqual( - obj.get('bs').mapBy('name'), - ['one', 'three', 'five'], - 'arrays computed by filtered property respond to added objects' - ); + set(obj.get('array')[0], 'b', true); + set(obj.get('array')[3], 'b', false); - obj.get('array').popObject(); + assert.deepEqual( + obj.get('as').mapBy('name'), + ['two', 'three', 'four'], + 'arrays computed by filter property respond to property changes' + ); + assert.deepEqual( + obj.get('bs').mapBy('name'), + ['one', 'three'], + 'arrays computed by filtered property respond to property changes' + ); - assert.deepEqual( - obj.get('as').mapBy('name'), - ['two', 'three', 'four'], - 'arrays computed by filter property respond to removed objects' - ); - assert.deepEqual( - obj.get('bs').mapBy('name'), - ['one', 'three'], - 'arrays computed by filtered property respond to removed objects' - ); + obj.get('array').pushObject({ name: 'five', a: 6, b: true }); - obj.set('array', [{ name: 'six', a: 12, b: true }]); + assert.deepEqual( + obj.get('as').mapBy('name'), + ['two', 'three', 'four', 'five'], + 'arrays computed by filter property respond to added objects' + ); + assert.deepEqual( + obj.get('bs').mapBy('name'), + ['one', 'three', 'five'], + 'arrays computed by filtered property respond to added objects' + ); - assert.deepEqual( - obj.get('as').mapBy('name'), - ['six'], - 'arrays computed by filter property respond to array changes' - ); - assert.deepEqual( - obj.get('bs').mapBy('name'), - ['six'], - 'arrays computed by filtered property respond to array changes' - ); - } + obj.get('array').popObject(); - ['@test properties can be filtered by values'](assert) { - assert.deepEqual( - obj.get('a1s').mapBy('name'), - ['one', 'three'], - 'properties can be filtered by matching value' - ); + assert.deepEqual( + obj.get('as').mapBy('name'), + ['two', 'three', 'four'], + 'arrays computed by filter property respond to removed objects' + ); + assert.deepEqual( + obj.get('bs').mapBy('name'), + ['one', 'three'], + 'arrays computed by filtered property respond to removed objects' + ); - obj.get('array').pushObject({ name: 'five', a: 1 }); + obj.set('array', [{ name: 'six', a: 12, b: true }]); - assert.deepEqual( - obj.get('a1s').mapBy('name'), - ['one', 'three', 'five'], - 'arrays computed by matching value respond to added objects' - ); + assert.deepEqual( + obj.get('as').mapBy('name'), + ['six'], + 'arrays computed by filter property respond to array changes' + ); + assert.deepEqual( + obj.get('bs').mapBy('name'), + ['six'], + 'arrays computed by filtered property respond to array changes' + ); + } - obj.get('array').popObject(); + ['@test properties can be filtered by values'](assert) { + assert.deepEqual( + obj.get('a1s').mapBy('name'), + ['one', 'three'], + 'properties can be filtered by matching value' + ); - assert.deepEqual( - obj.get('a1s').mapBy('name'), - ['one', 'three'], - 'arrays computed by matching value respond to removed objects' - ); + obj.get('array').pushObject({ name: 'five', a: 1 }); - set(obj.get('array')[1], 'a', 1); - set(obj.get('array')[2], 'a', 2); + assert.deepEqual( + obj.get('a1s').mapBy('name'), + ['one', 'three', 'five'], + 'arrays computed by matching value respond to added objects' + ); - assert.deepEqual( - obj.get('a1s').mapBy('name'), - ['one', 'two'], - 'arrays computed by matching value respond to modified properties' - ); - } + obj.get('array').popObject(); - ['@test properties values can be replaced'](assert) { - obj = class extends EmberObject { - @filterBy('array', 'a', 1) - a1s; - @filterBy('a1s', 'b') - a1bs; - }.create({ - array: [], - }); + assert.deepEqual( + obj.get('a1s').mapBy('name'), + ['one', 'three'], + 'arrays computed by matching value respond to removed objects' + ); - assert.deepEqual( - obj.get('a1bs').mapBy('name'), - [], - 'properties can be filtered by matching value' - ); + set(obj.get('array')[1], 'a', 1); + set(obj.get('array')[2], 'a', 2); - set(obj, 'array', [{ name: 'item1', a: 1, b: true }]); + assert.deepEqual( + obj.get('a1s').mapBy('name'), + ['one', 'two'], + 'arrays computed by matching value respond to modified properties' + ); + } - assert.deepEqual( - obj.get('a1bs').mapBy('name'), - ['item1'], - 'properties can be filtered by matching value' - ); + ['@test properties values can be replaced'](assert) { + obj = class extends EmberObject { + @filterBy('array', 'a', 1) + a1s; + @filterBy('a1s', 'b') + a1bs; + }.create({ + array: [], + }); + + assert.deepEqual( + obj.get('a1bs').mapBy('name'), + [], + 'properties can be filtered by matching value' + ); + + set(obj, 'array', [{ name: 'item1', a: 1, b: true }]); + + assert.deepEqual( + obj.get('a1bs').mapBy('name'), + ['item1'], + 'properties can be filtered by matching value' + ); + } } - } -); + ); + + [ + ['uniq', uniq], + ['union', union], + ].forEach((tuple) => { + let [name, macro] = tuple; + + moduleFor( + `CP macro \`${name}\``, + class extends ArrayMacroTestCase { + beforeEach() { + obj = class extends EmberObject { + @macro('array', 'array2', 'array3') + union; + }.create({ + array: emberA([1, 2, 3, 4, 5, 6]), + array2: emberA([4, 5, 6, 7, 8, 9, 4, 5, 6, 7, 8, 9]), + array3: emberA([1, 8, 10]), + }); + } + + afterEach() { + run(obj, 'destroy'); + } + + [`@test ${name} is readOnly`](assert) { + assert.throws(function () { + obj.set('union', 1); + }, /Cannot set read-only property "union" on object:/); + } + + ['@test does not include duplicates'](assert) { + let array = obj.get('array'); + let array2 = obj.get('array2'); + + assert.deepEqual( + obj.get('union').sort((x, y) => x - y), + [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + name + ' does not include duplicates' + ); + + array.pushObject(8); -[ - ['uniq', uniq], - ['union', union], -].forEach((tuple) => { - let [name, macro] = tuple; + assert.deepEqual( + obj.get('union').sort((x, y) => x - y), + [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + name + ' does not add existing items' + ); + + array.pushObject(11); + + assert.deepEqual( + obj.get('union').sort((x, y) => x - y), + [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11], + name + ' adds new items' + ); + + removeAt(array2, 6); // remove 7 + + assert.deepEqual( + obj.get('union').sort((x, y) => x - y), + [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11], + name + ' does not remove items that are still in the dependent array' + ); + + array2.removeObject(7); + + assert.deepEqual( + obj.get('union').sort((x, y) => x - y), + [1, 2, 3, 4, 5, 6, 8, 9, 10, 11], + name + ' removes items when their last instance is gone' + ); + } + + ['@test has set-union semantics'](assert) { + let array = obj.get('array'); + + assert.deepEqual( + obj.get('union').sort((x, y) => x - y), + [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + name + ' is initially correct' + ); + + array.removeObject(6); + + assert.deepEqual( + obj.get('union').sort((x, y) => x - y), + [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + 'objects are not removed if they exist in other dependent arrays' + ); + + array.clear(); + + assert.deepEqual( + obj.get('union').sort((x, y) => x - y), + [1, 4, 5, 6, 7, 8, 9, 10], + 'objects are removed when they are no longer in any dependent array' + ); + } + } + ); + }); moduleFor( - `CP macro \`${name}\``, - class extends AbstractTestCase { + 'CP Macro `uniqBy`', + class extends ArrayMacroTestCase { beforeEach() { obj = class extends EmberObject { - @macro('array', 'array2', 'array3') - union; + list = null; + @uniqBy('list', 'id') + uniqueById; }.create({ - array: emberA([1, 2, 3, 4, 5, 6]), - array2: emberA([4, 5, 6, 7, 8, 9, 4, 5, 6, 7, 8, 9]), - array3: emberA([1, 8, 10]), + list: emberA([ + { id: 1, value: 'one' }, + { id: 2, value: 'two' }, + { id: 1, value: 'one' }, + ]), }); } @@ -685,1360 +804,1272 @@ moduleFor( run(obj, 'destroy'); } - [`@test ${name} is readOnly`](assert) { + ['@test uniqBy is readOnly'](assert) { assert.throws(function () { - obj.set('union', 1); - }, /Cannot set read-only property "union" on object:/); + obj.set('uniqueById', 1); + }, /Cannot set read-only property "uniqueById" on object:/); } - ['@test does not include duplicates'](assert) { - let array = obj.get('array'); - let array2 = obj.get('array2'); + assert.deepEqual(obj.get('uniqueById'), [ + { id: 1, value: 'one' }, + { id: 2, value: 'two' }, + ]); + } + + ['@test it does not share state among instances'](assert) { + let MyObject = class extends EmberObject { + list = []; + @uniqBy('list', 'name') + uniqueByName; + }; + let a = MyObject.create({ + list: [{ name: 'bob' }, { name: 'mitch' }, { name: 'mitch' }], + }); + let b = MyObject.create({ + list: [{ name: 'warren' }, { name: 'mitch' }], + }); + + assert.deepEqual(a.get('uniqueByName'), [{ name: 'bob' }, { name: 'mitch' }]); + // Making sure that 'mitch' appears + assert.deepEqual(b.get('uniqueByName'), [{ name: 'warren' }, { name: 'mitch' }]); + } + + ['@test it handles changes to the dependent array'](assert) { + obj.get('list').pushObject({ id: 3, value: 'three' }); assert.deepEqual( - obj.get('union').sort((x, y) => x - y), - [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], - name + ' does not include duplicates' + obj.get('uniqueById'), + [ + { id: 1, value: 'one' }, + { id: 2, value: 'two' }, + { id: 3, value: 'three' }, + ], + 'The list includes three' ); - array.pushObject(8); + obj.get('list').pushObject({ id: 3, value: 'three' }); assert.deepEqual( - obj.get('union').sort((x, y) => x - y), - [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], - name + ' does not add existing items' + obj.get('uniqueById'), + [ + { id: 1, value: 'one' }, + { id: 2, value: 'two' }, + { id: 3, value: 'three' }, + ], + 'The list does not include a duplicate three' ); + } - array.pushObject(11); + ['@test it returns an empty array when computed on a non-array'](assert) { + let MyObject = class extends EmberObject { + list = null; + @uniqBy('list', 'name') + uniq; + }; + let a = MyObject.create({ list: 'not an array' }); + + assert.deepEqual(a.get('uniq'), []); + } + } + ); + + moduleFor( + 'CP Macro `intersect`', + class extends ArrayMacroTestCase { + beforeEach() { + obj = class extends EmberObject { + @intersect('array', 'array2', 'array3') + intersection; + }.create({ + array: emberA([1, 2, 3, 4, 5, 6]), + array2: emberA([3, 3, 3, 4, 5]), + array3: emberA([3, 5, 6, 7, 8]), + }); + } + + afterEach() { + run(obj, 'destroy'); + } + + ['@test intersect is readOnly'](assert) { + assert.throws(function () { + obj.set('intersection', 1); + }, /Cannot set read-only property "intersection" on object:/); + } + + ['@test it has set-intersection semantics'](assert) { + let array2 = obj.get('array2'); + let array3 = obj.get('array3'); assert.deepEqual( - obj.get('union').sort((x, y) => x - y), - [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11], - name + ' adds new items' + obj.get('intersection').sort((x, y) => x - y), + [3, 5], + 'intersection is initially correct' ); - removeAt(array2, 6); // remove 7 + array2.shiftObject(); assert.deepEqual( - obj.get('union').sort((x, y) => x - y), - [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11], - name + ' does not remove items that are still in the dependent array' + obj.get('intersection').sort((x, y) => x - y), + [3, 5], + 'objects are not removed when they are still in all dependent arrays' ); - array2.removeObject(7); + array2.shiftObject(); assert.deepEqual( - obj.get('union').sort((x, y) => x - y), - [1, 2, 3, 4, 5, 6, 8, 9, 10, 11], - name + ' removes items when their last instance is gone' + obj.get('intersection').sort((x, y) => x - y), + [3, 5], + 'objects are not removed when they are still in all dependent arrays' ); - } - ['@test has set-union semantics'](assert) { - let array = obj.get('array'); + array2.shiftObject(); assert.deepEqual( - obj.get('union').sort((x, y) => x - y), - [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], - name + ' is initially correct' + obj.get('intersection'), + [5], + 'objects are removed once they are gone from all dependent arrays' ); - array.removeObject(6); + array2.pushObject(1); assert.deepEqual( - obj.get('union').sort((x, y) => x - y), - [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], - 'objects are not removed if they exist in other dependent arrays' + obj.get('intersection'), + [5], + 'objects are not added as long as they are missing from any dependent array' ); - array.clear(); + array3.pushObject(1); assert.deepEqual( - obj.get('union').sort((x, y) => x - y), - [1, 4, 5, 6, 7, 8, 9, 10], - 'objects are removed when they are no longer in any dependent array' + obj.get('intersection').sort((x, y) => x - y), + [1, 5], + 'objects added once they belong to all dependent arrays' ); } } ); -}); - -moduleFor( - 'CP Macro `uniqBy`', - class extends AbstractTestCase { - beforeEach() { - obj = class extends EmberObject { - list = null; - @uniqBy('list', 'id') - uniqueById; - }.create({ - list: emberA([ - { id: 1, value: 'one' }, - { id: 2, value: 'two' }, - { id: 1, value: 'one' }, - ]), - }); - } - - afterEach() { - run(obj, 'destroy'); - } - - ['@test uniqBy is readOnly'](assert) { - assert.throws(function () { - obj.set('uniqueById', 1); - }, /Cannot set read-only property "uniqueById" on object:/); - } - ['@test does not include duplicates'](assert) { - assert.deepEqual(obj.get('uniqueById'), [ - { id: 1, value: 'one' }, - { id: 2, value: 'two' }, - ]); - } - - ['@test it does not share state among instances'](assert) { - let MyObject = class extends EmberObject { - list = []; - @uniqBy('list', 'name') - uniqueByName; - }; - let a = MyObject.create({ - list: [{ name: 'bob' }, { name: 'mitch' }, { name: 'mitch' }], - }); - let b = MyObject.create({ - list: [{ name: 'warren' }, { name: 'mitch' }], - }); - - assert.deepEqual(a.get('uniqueByName'), [{ name: 'bob' }, { name: 'mitch' }]); - // Making sure that 'mitch' appears - assert.deepEqual(b.get('uniqueByName'), [{ name: 'warren' }, { name: 'mitch' }]); - } - - ['@test it handles changes to the dependent array'](assert) { - obj.get('list').pushObject({ id: 3, value: 'three' }); - - assert.deepEqual( - obj.get('uniqueById'), - [ - { id: 1, value: 'one' }, - { id: 2, value: 'two' }, - { id: 3, value: 'three' }, - ], - 'The list includes three' - ); - - obj.get('list').pushObject({ id: 3, value: 'three' }); - - assert.deepEqual( - obj.get('uniqueById'), - [ - { id: 1, value: 'one' }, - { id: 2, value: 'two' }, - { id: 3, value: 'three' }, - ], - 'The list does not include a duplicate three' - ); - } - ['@test it returns an empty array when computed on a non-array'](assert) { - let MyObject = class extends EmberObject { - list = null; - @uniqBy('list', 'name') - uniq; - }; - let a = MyObject.create({ list: 'not an array' }); + moduleFor( + 'setDiff', + class extends ArrayMacroTestCase { + beforeEach() { + obj = class extends EmberObject { + @setDiff('array', 'array2') + diff; + }.create({ + array: emberA([1, 2, 3, 4, 5, 6, 7]), + array2: emberA([3, 4, 5, 10]), + }); + } - assert.deepEqual(a.get('uniq'), []); - } - } -); + afterEach() { + run(obj, 'destroy'); + } -moduleFor( - 'CP Macro `intersect`', - class extends AbstractTestCase { - beforeEach() { - obj = class extends EmberObject { - @intersect('array', 'array2', 'array3') - intersection; - }.create({ - array: emberA([1, 2, 3, 4, 5, 6]), - array2: emberA([3, 3, 3, 4, 5]), - array3: emberA([3, 5, 6, 7, 8]), - }); - } + ['@test setDiff is readOnly'](assert) { + assert.throws(function () { + obj.set('diff', 1); + }, /Cannot set read-only property "diff" on object:/); + } - afterEach() { - run(obj, 'destroy'); - } + ['@test it asserts if given fewer or more than two dependent properties']() { + expectAssertion( + function () { + let TestClass = class extends EmberObject { + @setDiff('array') + diff; + }; + TestClass.create({ + array: emberA([1, 2, 3, 4, 5, 6, 7]), + array2: emberA([3, 4, 5]), + }); + }, + /`setDiff` computed macro requires exactly two dependent arrays/, + 'setDiff requires two dependent arrays' + ); - ['@test intersect is readOnly'](assert) { - assert.throws(function () { - obj.set('intersection', 1); - }, /Cannot set read-only property "intersection" on object:/); - } + expectAssertion( + function () { + let TestClass = class extends EmberObject { + @setDiff('array', 'array2', 'array3') + diff; + }; + TestClass.create({ + array: emberA([1, 2, 3, 4, 5, 6, 7]), + array2: emberA([3, 4, 5]), + array3: emberA([7]), + }); + }, + /`setDiff` computed macro requires exactly two dependent arrays/, + 'setDiff requires two dependent arrays' + ); + } - ['@test it has set-intersection semantics'](assert) { - let array2 = obj.get('array2'); - let array3 = obj.get('array3'); + ['@test it has set-diff semantics'](assert) { + let array1 = obj.get('array'); + let array2 = obj.get('array2'); - assert.deepEqual( - obj.get('intersection').sort((x, y) => x - y), - [3, 5], - 'intersection is initially correct' - ); + assert.deepEqual( + obj.get('diff').sort((x, y) => x - y), + [1, 2, 6, 7], + 'set-diff is initially correct' + ); - array2.shiftObject(); + array2.popObject(); - assert.deepEqual( - obj.get('intersection').sort((x, y) => x - y), - [3, 5], - 'objects are not removed when they are still in all dependent arrays' - ); + assert.deepEqual( + obj.get('diff').sort((x, y) => x - y), + [1, 2, 6, 7], + 'removing objects from the remove set has no effect if the object is not in the keep set' + ); - array2.shiftObject(); + array2.shiftObject(); - assert.deepEqual( - obj.get('intersection').sort((x, y) => x - y), - [3, 5], - 'objects are not removed when they are still in all dependent arrays' - ); + assert.deepEqual( + obj.get('diff').sort((x, y) => x - y), + [1, 2, 3, 6, 7], + "removing objects from the remove set adds them if they're in the keep set" + ); - array2.shiftObject(); + array1.removeObject(3); - assert.deepEqual( - obj.get('intersection'), - [5], - 'objects are removed once they are gone from all dependent arrays' - ); + assert.deepEqual( + obj.get('diff').sort((x, y) => x - y), + [1, 2, 6, 7], + 'removing objects from the keep array removes them from the computed array' + ); - array2.pushObject(1); + array1.pushObject(5); - assert.deepEqual( - obj.get('intersection'), - [5], - 'objects are not added as long as they are missing from any dependent array' - ); + assert.deepEqual( + obj.get('diff').sort((x, y) => x - y), + [1, 2, 6, 7], + 'objects added to the keep array that are in the remove array are not added to the computed array' + ); - array3.pushObject(1); + array1.pushObject(22); - assert.deepEqual( - obj.get('intersection').sort((x, y) => x - y), - [1, 5], - 'objects added once they belong to all dependent arrays' - ); + assert.deepEqual( + obj.get('diff').sort((x, y) => x - y), + [1, 2, 6, 7, 22], + 'objects added to the keep array not in the remove array are added to the computed array' + ); + } } - } -); + ); -moduleFor( - 'setDiff', - class extends AbstractTestCase { + class SortWithSortPropertiesTestCase extends ArrayMacroTestCase { beforeEach() { - obj = class extends EmberObject { - @setDiff('array', 'array2') - diff; - }.create({ - array: emberA([1, 2, 3, 4, 5, 6, 7]), - array2: emberA([3, 4, 5, 10]), - }); + this.obj = this.buildObject(); } afterEach() { - run(obj, 'destroy'); + if (this.obj) { + this.cleanupObject(); + } } - ['@test setDiff is readOnly'](assert) { - assert.throws(function () { - obj.set('diff', 1); - }, /Cannot set read-only property "diff" on object:/); + ['@test sort is readOnly'](assert) { + assert.throws(() => { + set(this.obj, 'sortedItems', 1); + }, /Cannot set read-only property "sortedItems" on object:/); } - ['@test it asserts if given fewer or more than two dependent properties']() { - expectAssertion( - function () { - let TestClass = class extends EmberObject { - @setDiff('array') - diff; - }; - TestClass.create({ - array: emberA([1, 2, 3, 4, 5, 6, 7]), - array2: emberA([3, 4, 5]), - }); - }, - /`setDiff` computed macro requires exactly two dependent arrays/, - 'setDiff requires two dependent arrays' + ['@test arrays are initially sorted'](assert) { + assert.deepEqual( + this.obj.sortedItems.mapBy('fname'), + ['Cersei', 'Jaime', 'Bran', 'Robb'], + 'array is initially sorted' ); + } - expectAssertion( - function () { - let TestClass = class extends EmberObject { - @setDiff('array', 'array2', 'array3') - diff; - }; - TestClass.create({ - array: emberA([1, 2, 3, 4, 5, 6, 7]), - array2: emberA([3, 4, 5]), - array3: emberA([7]), - }); - }, - /`setDiff` computed macro requires exactly two dependent arrays/, - 'setDiff requires two dependent arrays' + ['@test default sort order is correct'](assert) { + assert.deepEqual( + this.obj.sortedItems.mapBy('fname'), + ['Cersei', 'Jaime', 'Bran', 'Robb'], + 'array is initially sorted' ); } - ['@test it has set-diff semantics'](assert) { - let array1 = obj.get('array'); - let array2 = obj.get('array2'); - + ['@test changing the dependent array updates the sorted array'](assert) { assert.deepEqual( - obj.get('diff').sort((x, y) => x - y), - [1, 2, 6, 7], - 'set-diff is initially correct' + this.obj.sortedItems.mapBy('fname'), + ['Cersei', 'Jaime', 'Bran', 'Robb'], + 'precond - array is initially sorted' ); - array2.popObject(); + set(this.obj, 'items', [ + { fname: 'Roose', lname: 'Bolton' }, + { fname: 'Theon', lname: 'Greyjoy' }, + { fname: 'Ramsey', lname: 'Bolton' }, + { fname: 'Stannis', lname: 'Baratheon' }, + ]); assert.deepEqual( - obj.get('diff').sort((x, y) => x - y), - [1, 2, 6, 7], - 'removing objects from the remove set has no effect if the object is not in the keep set' + this.obj.sortedItems.mapBy('fname'), + ['Stannis', 'Ramsey', 'Roose', 'Theon'], + 'changing dependent array updates sorted array' ); + } - array2.shiftObject(); + ['@test adding to the dependent array updates the sorted array'](assert) { + let items = this.obj.items; assert.deepEqual( - obj.get('diff').sort((x, y) => x - y), - [1, 2, 3, 6, 7], - "removing objects from the remove set adds them if they're in the keep set" + this.obj.sortedItems.mapBy('fname'), + ['Cersei', 'Jaime', 'Bran', 'Robb'], + 'precond - array is initially sorted' ); - array1.removeObject(3); + items.pushObject({ + fname: 'Tyrion', + lname: 'Lannister', + }); assert.deepEqual( - obj.get('diff').sort((x, y) => x - y), - [1, 2, 6, 7], - 'removing objects from the keep array removes them from the computed array' + this.obj.sortedItems.mapBy('fname'), + ['Cersei', 'Jaime', 'Tyrion', 'Bran', 'Robb'], + 'Adding to the dependent array updates the sorted array' ); + } - array1.pushObject(5); - + ['@test removing from the dependent array updates the sorted array'](assert) { assert.deepEqual( - obj.get('diff').sort((x, y) => x - y), - [1, 2, 6, 7], - 'objects added to the keep array that are in the remove array are not added to the computed array' + this.obj.sortedItems.mapBy('fname'), + ['Cersei', 'Jaime', 'Bran', 'Robb'], + 'precond - array is initially sorted' ); - array1.pushObject(22); + this.obj.items.popObject(); assert.deepEqual( - obj.get('diff').sort((x, y) => x - y), - [1, 2, 6, 7, 22], - 'objects added to the keep array not in the remove array are added to the computed array' + this.obj.sortedItems.mapBy('fname'), + ['Cersei', 'Jaime', 'Robb'], + 'Removing from the dependent array updates the sorted array' ); } - } -); - -class SortWithSortPropertiesTestCase extends AbstractTestCase { - beforeEach() { - this.obj = this.buildObject(); - } - - afterEach() { - if (this.obj) { - this.cleanupObject(); - } - } - - ['@test sort is readOnly'](assert) { - assert.throws(() => { - set(this.obj, 'sortedItems', 1); - }, /Cannot set read-only property "sortedItems" on object:/); - } - ['@test arrays are initially sorted'](assert) { - assert.deepEqual( - this.obj.sortedItems.mapBy('fname'), - ['Cersei', 'Jaime', 'Bran', 'Robb'], - 'array is initially sorted' - ); - } + ['@test distinct items may be sort-equal, although their relative order will not be guaranteed']( + assert + ) { + // We recreate jaime and "Cersei" here only for test stability: we want + // their guid-ordering to be deterministic + let jaimeInDisguise = { + fname: 'Cersei', + lname: 'Lannister', + age: 34, + }; - ['@test default sort order is correct'](assert) { - assert.deepEqual( - this.obj.sortedItems.mapBy('fname'), - ['Cersei', 'Jaime', 'Bran', 'Robb'], - 'array is initially sorted' - ); - } + let jaime = { + fname: 'Jaime', + lname: 'Lannister', + age: 34, + }; - ['@test changing the dependent array updates the sorted array'](assert) { - assert.deepEqual( - this.obj.sortedItems.mapBy('fname'), - ['Cersei', 'Jaime', 'Bran', 'Robb'], - 'precond - array is initially sorted' - ); + let items = this.obj.items; - set(this.obj, 'items', [ - { fname: 'Roose', lname: 'Bolton' }, - { fname: 'Theon', lname: 'Greyjoy' }, - { fname: 'Ramsey', lname: 'Bolton' }, - { fname: 'Stannis', lname: 'Baratheon' }, - ]); - - assert.deepEqual( - this.obj.sortedItems.mapBy('fname'), - ['Stannis', 'Ramsey', 'Roose', 'Theon'], - 'changing dependent array updates sorted array' - ); - } + items.replace(0, 1, [jaime]); + items.replace(1, 1, [jaimeInDisguise]); - ['@test adding to the dependent array updates the sorted array'](assert) { - let items = this.obj.items; + assert.deepEqual( + this.obj.sortedItems.mapBy('fname'), + ['Cersei', 'Jaime', 'Bran', 'Robb'], + 'precond - array is initially sorted' + ); - assert.deepEqual( - this.obj.sortedItems.mapBy('fname'), - ['Cersei', 'Jaime', 'Bran', 'Robb'], - 'precond - array is initially sorted' - ); + set(jaimeInDisguise, 'fname', 'Jaime'); - items.pushObject({ - fname: 'Tyrion', - lname: 'Lannister', - }); + assert.deepEqual( + this.obj.sortedItems.mapBy('fname'), + ['Jaime', 'Jaime', 'Bran', 'Robb'], + 'sorted array is updated' + ); - assert.deepEqual( - this.obj.sortedItems.mapBy('fname'), - ['Cersei', 'Jaime', 'Tyrion', 'Bran', 'Robb'], - 'Adding to the dependent array updates the sorted array' - ); - } + set(jaimeInDisguise, 'fname', 'Cersei'); - ['@test removing from the dependent array updates the sorted array'](assert) { - assert.deepEqual( - this.obj.sortedItems.mapBy('fname'), - ['Cersei', 'Jaime', 'Bran', 'Robb'], - 'precond - array is initially sorted' - ); + assert.deepEqual( + this.obj.sortedItems.mapBy('fname'), + ['Cersei', 'Jaime', 'Bran', 'Robb'], + 'sorted array is updated' + ); + } - this.obj.items.popObject(); + [`${testUnless( + DEPRECATIONS.DEPRECATE_OBJECT_PROXY.isRemoved + )} @test guid sort-order fallback with a search proxy is not confused by non-search ObjectProxys`]( + assert + ) { + expectDeprecation( + /`ObjectProxy` is deprecated/, + DEPRECATIONS.DEPRECATE_OBJECT_PROXY.isEnabled + ); - assert.deepEqual( - this.obj.sortedItems.mapBy('fname'), - ['Cersei', 'Jaime', 'Robb'], - 'Removing from the dependent array updates the sorted array' - ); - } + let tyrion = { + fname: 'Tyrion', + lname: 'Lannister', + }; - ['@test distinct items may be sort-equal, although their relative order will not be guaranteed']( - assert - ) { - // We recreate jaime and "Cersei" here only for test stability: we want - // their guid-ordering to be deterministic - let jaimeInDisguise = { - fname: 'Cersei', - lname: 'Lannister', - age: 34, - }; - - let jaime = { - fname: 'Jaime', - lname: 'Lannister', - age: 34, - }; - - let items = this.obj.items; - - items.replace(0, 1, [jaime]); - items.replace(1, 1, [jaimeInDisguise]); - - assert.deepEqual( - this.obj.sortedItems.mapBy('fname'), - ['Cersei', 'Jaime', 'Bran', 'Robb'], - 'precond - array is initially sorted' - ); + let tyrionInDisguise = ObjectProxy.create({ + fname: 'Yollo', + lname: '', + content: tyrion, + }); - set(jaimeInDisguise, 'fname', 'Jaime'); + let items = this.obj.items; - assert.deepEqual( - this.obj.sortedItems.mapBy('fname'), - ['Jaime', 'Jaime', 'Bran', 'Robb'], - 'sorted array is updated' - ); + items.pushObject(tyrion); - set(jaimeInDisguise, 'fname', 'Cersei'); + assert.deepEqual(this.obj.sortedItems.mapBy('fname'), [ + 'Cersei', + 'Jaime', + 'Tyrion', + 'Bran', + 'Robb', + ]); - assert.deepEqual( - this.obj.sortedItems.mapBy('fname'), - ['Cersei', 'Jaime', 'Bran', 'Robb'], - 'sorted array is updated' - ); - } + items.pushObject(tyrionInDisguise); - [`${testUnless( - DEPRECATIONS.DEPRECATE_OBJECT_PROXY.isRemoved - )} @test guid sort-order fallback with a search proxy is not confused by non-search ObjectProxys`]( - assert - ) { - expectDeprecation(/`ObjectProxy` is deprecated/, DEPRECATIONS.DEPRECATE_OBJECT_PROXY.isEnabled); - - let tyrion = { - fname: 'Tyrion', - lname: 'Lannister', - }; - - let tyrionInDisguise = ObjectProxy.create({ - fname: 'Yollo', - lname: '', - content: tyrion, - }); - - let items = this.obj.items; - - items.pushObject(tyrion); - - assert.deepEqual(this.obj.sortedItems.mapBy('fname'), [ - 'Cersei', - 'Jaime', - 'Tyrion', - 'Bran', - 'Robb', - ]); - - items.pushObject(tyrionInDisguise); - - assert.deepEqual(this.obj.sortedItems.mapBy('fname'), [ - 'Yollo', - 'Cersei', - 'Jaime', - 'Tyrion', - 'Bran', - 'Robb', - ]); - } + assert.deepEqual(this.obj.sortedItems.mapBy('fname'), [ + 'Yollo', + 'Cersei', + 'Jaime', + 'Tyrion', + 'Bran', + 'Robb', + ]); + } - ['@test updating sort properties detaches observers for old sort properties'](assert) { - let objectToRemove = this.obj.items[3]; + ['@test updating sort properties detaches observers for old sort properties'](assert) { + let objectToRemove = this.obj.items[3]; - assert.deepEqual( - this.obj.sortedItems.mapBy('fname'), - ['Cersei', 'Jaime', 'Bran', 'Robb'], - 'precond - array is initially sorted' - ); + assert.deepEqual( + this.obj.sortedItems.mapBy('fname'), + ['Cersei', 'Jaime', 'Bran', 'Robb'], + 'precond - array is initially sorted' + ); - set(this.obj, 'itemSorting', emberA(['fname:desc'])); + set(this.obj, 'itemSorting', emberA(['fname:desc'])); - assert.deepEqual( - this.obj.sortedItems.mapBy('fname'), - ['Robb', 'Jaime', 'Cersei', 'Bran'], - 'after updating sort properties array is updated' - ); + assert.deepEqual( + this.obj.sortedItems.mapBy('fname'), + ['Robb', 'Jaime', 'Cersei', 'Bran'], + 'after updating sort properties array is updated' + ); - this.obj.items.removeObject(objectToRemove); + this.obj.items.removeObject(objectToRemove); - assert.deepEqual( - this.obj.sortedItems.mapBy('fname'), - ['Robb', 'Jaime', 'Cersei'], - 'after removing item array is updated' - ); + assert.deepEqual( + this.obj.sortedItems.mapBy('fname'), + ['Robb', 'Jaime', 'Cersei'], + 'after removing item array is updated' + ); - set(objectToRemove, 'lname', 'Updated-Stark'); + set(objectToRemove, 'lname', 'Updated-Stark'); - assert.deepEqual( - this.obj.sortedItems.mapBy('fname'), - ['Robb', 'Jaime', 'Cersei'], - 'after changing removed item array is not updated' - ); - } + assert.deepEqual( + this.obj.sortedItems.mapBy('fname'), + ['Robb', 'Jaime', 'Cersei'], + 'after changing removed item array is not updated' + ); + } - ['@test sort works if array property is null (non array value) on first evaluation of computed prop']( - assert - ) { - set(this.obj, 'items', null); - assert.deepEqual(this.obj.sortedItems, []); - set(this.obj, 'items', emberA([{ fname: 'Cersei', lname: 'Lanister' }])); - assert.deepEqual(this.obj.sortedItems, [{ fname: 'Cersei', lname: 'Lanister' }]); - } + ['@test sort works if array property is null (non array value) on first evaluation of computed prop']( + assert + ) { + set(this.obj, 'items', null); + assert.deepEqual(this.obj.sortedItems, []); + set(this.obj, 'items', emberA([{ fname: 'Cersei', lname: 'Lanister' }])); + assert.deepEqual(this.obj.sortedItems, [{ fname: 'Cersei', lname: 'Lanister' }]); + } - ['@test updating sort properties updates the sorted array'](assert) { - assert.deepEqual( - this.obj.sortedItems.mapBy('fname'), - ['Cersei', 'Jaime', 'Bran', 'Robb'], - 'precond - array is initially sorted' - ); + ['@test updating sort properties updates the sorted array'](assert) { + assert.deepEqual( + this.obj.sortedItems.mapBy('fname'), + ['Cersei', 'Jaime', 'Bran', 'Robb'], + 'precond - array is initially sorted' + ); - set(this.obj, 'itemSorting', emberA(['fname:desc'])); + set(this.obj, 'itemSorting', emberA(['fname:desc'])); - assert.deepEqual( - this.obj.sortedItems.mapBy('fname'), - ['Robb', 'Jaime', 'Cersei', 'Bran'], - 'after updating sort properties array is updated' - ); - } + assert.deepEqual( + this.obj.sortedItems.mapBy('fname'), + ['Robb', 'Jaime', 'Cersei', 'Bran'], + 'after updating sort properties array is updated' + ); + } - ['@test updating sort properties invalidates the sorted array'](assert) { - let sortProps = this.obj.itemSorting; + ['@test updating sort properties invalidates the sorted array'](assert) { + let sortProps = this.obj.itemSorting; - assert.deepEqual( - this.obj.sortedItems.mapBy('fname'), - ['Cersei', 'Jaime', 'Bran', 'Robb'], - 'precond - array is initially sorted' - ); + assert.deepEqual( + this.obj.sortedItems.mapBy('fname'), + ['Cersei', 'Jaime', 'Bran', 'Robb'], + 'precond - array is initially sorted' + ); - sortProps.clear(); - sortProps.pushObject('fname'); + sortProps.clear(); + sortProps.pushObject('fname'); - assert.deepEqual( - this.obj.sortedItems.mapBy('fname'), - ['Bran', 'Cersei', 'Jaime', 'Robb'], - 'after updating sort properties array is updated' - ); - } + assert.deepEqual( + this.obj.sortedItems.mapBy('fname'), + ['Bran', 'Cersei', 'Jaime', 'Robb'], + 'after updating sort properties array is updated' + ); + } - ['@test updating new sort properties invalidates the sorted array'](assert) { - assert.deepEqual( - this.obj.sortedItems.mapBy('fname'), - ['Cersei', 'Jaime', 'Bran', 'Robb'], - 'precond - array is initially sorted' - ); + ['@test updating new sort properties invalidates the sorted array'](assert) { + assert.deepEqual( + this.obj.sortedItems.mapBy('fname'), + ['Cersei', 'Jaime', 'Bran', 'Robb'], + 'precond - array is initially sorted' + ); - set(this.obj, 'itemSorting', emberA(['age:desc', 'fname:asc'])); + set(this.obj, 'itemSorting', emberA(['age:desc', 'fname:asc'])); - assert.deepEqual( - this.obj.sortedItems.mapBy('fname'), - ['Cersei', 'Jaime', 'Robb', 'Bran'], - 'precond - array is correct after item sorting is changed' - ); + assert.deepEqual( + this.obj.sortedItems.mapBy('fname'), + ['Cersei', 'Jaime', 'Robb', 'Bran'], + 'precond - array is correct after item sorting is changed' + ); - set(this.obj.items[1], 'age', 29); + set(this.obj.items[1], 'age', 29); - assert.deepEqual( - this.obj.sortedItems.mapBy('fname'), - ['Jaime', 'Cersei', 'Robb', 'Bran'], - 'after updating sort properties array is updated' - ); - } + assert.deepEqual( + this.obj.sortedItems.mapBy('fname'), + ['Jaime', 'Cersei', 'Robb', 'Bran'], + 'after updating sort properties array is updated' + ); + } - ['@test sort direction defaults to ascending'](assert) { - assert.deepEqual(this.obj.sortedItems.mapBy('fname'), ['Cersei', 'Jaime', 'Bran', 'Robb']); - } + ['@test sort direction defaults to ascending'](assert) { + assert.deepEqual(this.obj.sortedItems.mapBy('fname'), ['Cersei', 'Jaime', 'Bran', 'Robb']); + } - ['@test sort direction defaults to ascending (with sort property change)'](assert) { - assert.deepEqual( - this.obj.sortedItems.mapBy('fname'), - ['Cersei', 'Jaime', 'Bran', 'Robb'], - 'precond - array is initially sorted' - ); + ['@test sort direction defaults to ascending (with sort property change)'](assert) { + assert.deepEqual( + this.obj.sortedItems.mapBy('fname'), + ['Cersei', 'Jaime', 'Bran', 'Robb'], + 'precond - array is initially sorted' + ); - set(this.obj, 'itemSorting', emberA(['fname'])); + set(this.obj, 'itemSorting', emberA(['fname'])); - assert.deepEqual( - this.obj.sortedItems.mapBy('fname'), - ['Bran', 'Cersei', 'Jaime', 'Robb'], - 'sort direction defaults to ascending' - ); - } + assert.deepEqual( + this.obj.sortedItems.mapBy('fname'), + ['Bran', 'Cersei', 'Jaime', 'Robb'], + 'sort direction defaults to ascending' + ); + } - ["@test updating an item's sort properties updates the sorted array"](assert) { - let tyrionInDisguise = this.obj.items[1]; + ["@test updating an item's sort properties updates the sorted array"](assert) { + let tyrionInDisguise = this.obj.items[1]; - assert.deepEqual( - this.obj.sortedItems.mapBy('fname'), - ['Cersei', 'Jaime', 'Bran', 'Robb'], - 'precond - array is initially sorted' - ); + assert.deepEqual( + this.obj.sortedItems.mapBy('fname'), + ['Cersei', 'Jaime', 'Bran', 'Robb'], + 'precond - array is initially sorted' + ); - set(tyrionInDisguise, 'fname', 'Tyrion'); + set(tyrionInDisguise, 'fname', 'Tyrion'); - assert.deepEqual( - this.obj.sortedItems.mapBy('fname'), - ['Jaime', 'Tyrion', 'Bran', 'Robb'], - "updating an item's sort properties updates the sorted array" - ); - } + assert.deepEqual( + this.obj.sortedItems.mapBy('fname'), + ['Jaime', 'Tyrion', 'Bran', 'Robb'], + "updating an item's sort properties updates the sorted array" + ); + } - ["@test updating several of an item's sort properties updated the sorted array"](assert) { - let sansaInDisguise = this.obj.items[1]; + ["@test updating several of an item's sort properties updated the sorted array"](assert) { + let sansaInDisguise = this.obj.items[1]; - assert.deepEqual( - this.obj.sortedItems.mapBy('fname'), - ['Cersei', 'Jaime', 'Bran', 'Robb'], - 'precond - array is initially sorted' - ); + assert.deepEqual( + this.obj.sortedItems.mapBy('fname'), + ['Cersei', 'Jaime', 'Bran', 'Robb'], + 'precond - array is initially sorted' + ); - setProperties(sansaInDisguise, { - fname: 'Sansa', - lname: 'Stark', - }); + setProperties(sansaInDisguise, { + fname: 'Sansa', + lname: 'Stark', + }); - assert.deepEqual( - this.obj.sortedItems.mapBy('fname'), - ['Jaime', 'Bran', 'Robb', 'Sansa'], - "updating an item's sort properties updates the sorted array" - ); - } + assert.deepEqual( + this.obj.sortedItems.mapBy('fname'), + ['Jaime', 'Bran', 'Robb', 'Sansa'], + "updating an item's sort properties updates the sorted array" + ); + } - ["@test updating an item's sort properties does not error when binary search does a self compare (#3273)"]( - assert - ) { - let jaime = { - name: 'Jaime', - status: 1, - }; + ["@test updating an item's sort properties does not error when binary search does a self compare (#3273)"]( + assert + ) { + let jaime = { + name: 'Jaime', + status: 1, + }; - let cersei = { - name: 'Cersei', - status: 2, - }; + let cersei = { + name: 'Cersei', + status: 2, + }; - this.cleanupObject(); - this.obj = this.buildObject([jaime, cersei], ['status']); + this.cleanupObject(); + this.obj = this.buildObject([jaime, cersei], ['status']); - assert.deepEqual(this.obj.sortedItems, [jaime, cersei], 'precond - array is initially sorted'); + assert.deepEqual( + this.obj.sortedItems, + [jaime, cersei], + 'precond - array is initially sorted' + ); - set(cersei, 'status', 3); + set(cersei, 'status', 3); - assert.deepEqual(this.obj.sortedItems, [jaime, cersei], 'array is sorted correctly'); + assert.deepEqual(this.obj.sortedItems, [jaime, cersei], 'array is sorted correctly'); - set(cersei, 'status', 2); + set(cersei, 'status', 2); - assert.deepEqual(this.obj.sortedItems, [jaime, cersei], 'array is sorted correctly'); - } + assert.deepEqual(this.obj.sortedItems, [jaime, cersei], 'array is sorted correctly'); + } - ['@test array should not be sorted if sort properties array is empty'](assert) { - this.cleanupObject(); - // This bug only manifests when array.sort(() => 0) is not equal to array. - // In order for this to happen, the browser must use an unstable sort and the - // array must be sufficient large. On Chrome, 12 items is currently sufficient. - this.obj = this.buildObject(emberA([6, 7, 8, 9, 10, 11, 0, 1, 2, 3, 4, 5]), []); - - assert.deepEqual( - this.obj.sortedItems, - [6, 7, 8, 9, 10, 11, 0, 1, 2, 3, 4, 5], - 'array is not changed' - ); - } + ['@test array should not be sorted if sort properties array is empty'](assert) { + this.cleanupObject(); + // This bug only manifests when array.sort(() => 0) is not equal to array. + // In order for this to happen, the browser must use an unstable sort and the + // array must be sufficient large. On Chrome, 12 items is currently sufficient. + this.obj = this.buildObject(emberA([6, 7, 8, 9, 10, 11, 0, 1, 2, 3, 4, 5]), []); - ['@test array should update if items to be sorted is replaced when sort properties array is empty']( - assert - ) { - this.cleanupObject(); - this.obj = this.buildObject(emberA([6, 7, 8, 9, 10, 11, 0, 1, 2, 3, 4, 5]), emberA([])); + assert.deepEqual( + this.obj.sortedItems, + [6, 7, 8, 9, 10, 11, 0, 1, 2, 3, 4, 5], + 'array is not changed' + ); + } - assert.deepEqual( - this.obj.sortedItems, - [6, 7, 8, 9, 10, 11, 0, 1, 2, 3, 4, 5], - 'array is not changed' - ); + ['@test array should update if items to be sorted is replaced when sort properties array is empty']( + assert + ) { + this.cleanupObject(); + this.obj = this.buildObject(emberA([6, 7, 8, 9, 10, 11, 0, 1, 2, 3, 4, 5]), emberA([])); - set(this.obj, 'items', emberA([5, 6, 7, 8, 9, 10, 11, 0, 1, 2, 3, 4])); + assert.deepEqual( + this.obj.sortedItems, + [6, 7, 8, 9, 10, 11, 0, 1, 2, 3, 4, 5], + 'array is not changed' + ); - assert.deepEqual( - this.obj.sortedItems, - [5, 6, 7, 8, 9, 10, 11, 0, 1, 2, 3, 4], - 'array was updated' - ); - } + set(this.obj, 'items', emberA([5, 6, 7, 8, 9, 10, 11, 0, 1, 2, 3, 4])); - ['@test array should update if items to be sorted is mutated when sort properties array is empty']( - assert - ) { - this.cleanupObject(); - this.obj = this.buildObject(emberA([6, 7, 8, 9, 10, 11, 0, 1, 2, 3, 4, 5]), emberA([])); + assert.deepEqual( + this.obj.sortedItems, + [5, 6, 7, 8, 9, 10, 11, 0, 1, 2, 3, 4], + 'array was updated' + ); + } - assert.deepEqual( - this.obj.sortedItems, - [6, 7, 8, 9, 10, 11, 0, 1, 2, 3, 4, 5], - 'array is not changed' - ); + ['@test array should update if items to be sorted is mutated when sort properties array is empty']( + assert + ) { + this.cleanupObject(); + this.obj = this.buildObject(emberA([6, 7, 8, 9, 10, 11, 0, 1, 2, 3, 4, 5]), emberA([])); - this.obj.items.pushObject(12); + assert.deepEqual( + this.obj.sortedItems, + [6, 7, 8, 9, 10, 11, 0, 1, 2, 3, 4, 5], + 'array is not changed' + ); - assert.deepEqual( - this.obj.sortedItems, - [6, 7, 8, 9, 10, 11, 0, 1, 2, 3, 4, 5, 12], - 'array was updated' - ); - } + this.obj.items.pushObject(12); - ['@test array observers do not leak'](assert) { - let daria = { name: 'Daria' }; - let jane = { name: 'Jane' }; + assert.deepEqual( + this.obj.sortedItems, + [6, 7, 8, 9, 10, 11, 0, 1, 2, 3, 4, 5, 12], + 'array was updated' + ); + } - let sisters = [jane, daria]; - let sortProps = emberA(['name']); + ['@test array observers do not leak'](assert) { + let daria = { name: 'Daria' }; + let jane = { name: 'Jane' }; - this.cleanupObject(); - this.obj = this.buildObject(sisters, sortProps); + let sisters = [jane, daria]; + let sortProps = emberA(['name']); - this.obj.sortedItems; - this.cleanupObject(); + this.cleanupObject(); + this.obj = this.buildObject(sisters, sortProps); - try { - sortProps.pushObject({ - name: 'Anna', - }); - assert.ok(true); - } catch (e) { - assert.ok(false, e); + this.obj.sortedItems; + this.cleanupObject(); + + try { + sortProps.pushObject({ + name: 'Anna', + }); + assert.ok(true); + } catch (e) { + assert.ok(false, e); + } } - } - ['@test property paths in sort properties update the sorted array'](assert) { - let jaime = { - relatedObj: { status: 1, firstName: 'Jaime', lastName: 'Lannister' }, - }; + ['@test property paths in sort properties update the sorted array'](assert) { + let jaime = { + relatedObj: { status: 1, firstName: 'Jaime', lastName: 'Lannister' }, + }; - let cersei = { - relatedObj: { status: 2, firstName: 'Cersei', lastName: 'Lannister' }, - }; + let cersei = { + relatedObj: { status: 2, firstName: 'Cersei', lastName: 'Lannister' }, + }; - let sansa = EmberObject.create({ - relatedObj: { status: 3, firstName: 'Sansa', lastName: 'Stark' }, - }); + let sansa = EmberObject.create({ + relatedObj: { status: 3, firstName: 'Sansa', lastName: 'Stark' }, + }); - this.cleanupObject(); - this.obj = this.buildObject([jaime, cersei, sansa], ['relatedObj.status']); + this.cleanupObject(); + this.obj = this.buildObject([jaime, cersei, sansa], ['relatedObj.status']); - assert.deepEqual( - this.obj.sortedItems, - [jaime, cersei, sansa], - 'precond - array is initially sorted' - ); + assert.deepEqual( + this.obj.sortedItems, + [jaime, cersei, sansa], + 'precond - array is initially sorted' + ); - set(cersei, 'status', 3); + set(cersei, 'status', 3); - assert.deepEqual(this.obj.sortedItems, [jaime, cersei, sansa], 'array is sorted correctly'); + assert.deepEqual(this.obj.sortedItems, [jaime, cersei, sansa], 'array is sorted correctly'); - set(cersei, 'status', 1); + set(cersei, 'status', 1); - assert.deepEqual(this.obj.sortedItems, [jaime, cersei, sansa], 'array is sorted correctly'); + assert.deepEqual(this.obj.sortedItems, [jaime, cersei, sansa], 'array is sorted correctly'); - sansa.set('status', 1); + sansa.set('status', 1); - assert.deepEqual(this.obj.sortedItems, [jaime, cersei, sansa], 'array is sorted correctly'); + assert.deepEqual(this.obj.sortedItems, [jaime, cersei, sansa], 'array is sorted correctly'); - set(this.obj, 'itemSorting', ['relatedObj.firstName']); + set(this.obj, 'itemSorting', ['relatedObj.firstName']); - assert.deepEqual(this.obj.sortedItems, [cersei, jaime, sansa], 'array is sorted correctly'); - } + assert.deepEqual(this.obj.sortedItems, [cersei, jaime, sansa], 'array is sorted correctly'); + } - ['@test if the dependentKey is neither an array nor object, it will return an empty array']( - assert - ) { - set(this.obj, 'items', null); - assert.ok(isArray(this.obj.sortedItems), 'returns an empty arrays'); + ['@test if the dependentKey is neither an array nor object, it will return an empty array']( + assert + ) { + set(this.obj, 'items', null); + assert.ok(isArray(this.obj.sortedItems), 'returns an empty arrays'); - set(this.obj, 'array', undefined); - assert.ok(isArray(this.obj.sortedItems), 'returns an empty arrays'); + set(this.obj, 'array', undefined); + assert.ok(isArray(this.obj.sortedItems), 'returns an empty arrays'); - set(this.obj, 'array', 'not an array'); - assert.ok(isArray(this.obj.sortedItems), 'returns an empty arrays'); + set(this.obj, 'array', 'not an array'); + assert.ok(isArray(this.obj.sortedItems), 'returns an empty arrays'); + } } -} -moduleFor( - 'sort - sortProperties - Ember.Object', - class extends SortWithSortPropertiesTestCase { - buildObject(_items, _itemSorting) { - let items = - _items || - emberA([ - { fname: 'Jaime', lname: 'Lannister', age: 34 }, - { fname: 'Cersei', lname: 'Lannister', age: 34 }, - { fname: 'Robb', lname: 'Stark', age: 16 }, - { fname: 'Bran', lname: 'Stark', age: 8 }, - ]); - - let itemSorting = _itemSorting || emberA(['lname', 'fname']); - - return class extends EmberObject { - @sort('items', 'itemSorting') - sortedItems; - }.create({ - itemSorting, - items, - }); - } + moduleFor( + 'sort - sortProperties - Ember.Object', + class extends SortWithSortPropertiesTestCase { + buildObject(_items, _itemSorting) { + let items = + _items || + emberA([ + { fname: 'Jaime', lname: 'Lannister', age: 34 }, + { fname: 'Cersei', lname: 'Lannister', age: 34 }, + { fname: 'Robb', lname: 'Stark', age: 16 }, + { fname: 'Bran', lname: 'Stark', age: 8 }, + ]); + + let itemSorting = _itemSorting || emberA(['lname', 'fname']); + + return class extends EmberObject { + @sort('items', 'itemSorting') + sortedItems; + }.create({ + itemSorting, + items, + }); + } - cleanupObject() { - run(this.obj, 'destroy'); + cleanupObject() { + run(this.obj, 'destroy'); + } } - } -); - -moduleFor( - 'sort - sortProperties - Native Class', - class extends SortWithSortPropertiesTestCase { - buildObject(_items, _itemSorting) { - let items = - _items || - emberA([ - { fname: 'Jaime', lname: 'Lannister', age: 34 }, - { fname: 'Cersei', lname: 'Lannister', age: 34 }, - { fname: 'Robb', lname: 'Stark', age: 16 }, - { fname: 'Bran', lname: 'Stark', age: 8 }, - ]); - - let itemSorting = _itemSorting || emberA(['lname', 'fname']); + ); - return new (class { - items = items; - itemSorting = itemSorting; + moduleFor( + 'sort - sortProperties - Native Class', + class extends SortWithSortPropertiesTestCase { + buildObject(_items, _itemSorting) { + let items = + _items || + emberA([ + { fname: 'Jaime', lname: 'Lannister', age: 34 }, + { fname: 'Cersei', lname: 'Lannister', age: 34 }, + { fname: 'Robb', lname: 'Stark', age: 16 }, + { fname: 'Bran', lname: 'Stark', age: 8 }, + ]); + + let itemSorting = _itemSorting || emberA(['lname', 'fname']); + + return new (class { + items = items; + itemSorting = itemSorting; + + @sort('items', 'itemSorting') + sortedItems; + })(); + } - @sort('items', 'itemSorting') - sortedItems; - })(); + cleanupObject() {} } + ); - cleanupObject() {} - } -); + function sortByLnameFname(a, b) { + let lna = get(a, 'lname'); + let lnb = get(b, 'lname'); -function sortByLnameFname(a, b) { - let lna = get(a, 'lname'); - let lnb = get(b, 'lname'); + if (lna !== lnb) { + return lna > lnb ? 1 : -1; + } - if (lna !== lnb) { - return lna > lnb ? 1 : -1; + return sortByFnameAsc(a, b); } - return sortByFnameAsc(a, b); -} - -function sortByFnameAsc(a, b) { - let fna = get(a, 'fname'); - let fnb = get(b, 'fname'); + function sortByFnameAsc(a, b) { + let fna = get(a, 'fname'); + let fnb = get(b, 'fname'); - if (fna === fnb) { - return 0; + if (fna === fnb) { + return 0; + } + return fna > fnb ? 1 : -1; } - return fna > fnb ? 1 : -1; -} -moduleFor( - 'sort - sort function', - class extends AbstractTestCase { - beforeEach() { - obj = class extends EmberObject { - @sort('items.@each.fname', sortByLnameFname) - sortedItems; - }.create({ - items: emberA([ - { fname: 'Jaime', lname: 'Lannister', age: 34 }, - { fname: 'Cersei', lname: 'Lannister', age: 34 }, - { fname: 'Robb', lname: 'Stark', age: 16 }, - { fname: 'Bran', lname: 'Stark', age: 8 }, - ]), - }); - } + moduleFor( + 'sort - sort function', + class extends ArrayMacroTestCase { + beforeEach() { + obj = class extends EmberObject { + @sort('items.@each.fname', sortByLnameFname) + sortedItems; + }.create({ + items: emberA([ + { fname: 'Jaime', lname: 'Lannister', age: 34 }, + { fname: 'Cersei', lname: 'Lannister', age: 34 }, + { fname: 'Robb', lname: 'Stark', age: 16 }, + { fname: 'Bran', lname: 'Stark', age: 8 }, + ]), + }); + } - afterEach() { - run(obj, 'destroy'); - } + afterEach() { + run(obj, 'destroy'); + } - ['@test sort has correct `this`'](assert) { - let obj = class extends EmberObject { - @sort('items.@each.fname', function (a, b) { - assert.equal(this, obj, 'expected the object to be `this`'); - return this.sortByLastName(a, b); - }) - sortedItems; - sortByLastName(a, b) { - return sortByFnameAsc(a, b); - } - }.create({ - items: emberA([ - { fname: 'Jaime', lname: 'Lannister', age: 34 }, - { fname: 'Cersei', lname: 'Lannister', age: 34 }, - { fname: 'Robb', lname: 'Stark', age: 16 }, - { fname: 'Bran', lname: 'Stark', age: 8 }, - ]), - }); + ['@test sort has correct `this`'](assert) { + let obj = class extends EmberObject { + @sort('items.@each.fname', function (a, b) { + assert.equal(this, obj, 'expected the object to be `this`'); + return this.sortByLastName(a, b); + }) + sortedItems; + sortByLastName(a, b) { + return sortByFnameAsc(a, b); + } + }.create({ + items: emberA([ + { fname: 'Jaime', lname: 'Lannister', age: 34 }, + { fname: 'Cersei', lname: 'Lannister', age: 34 }, + { fname: 'Robb', lname: 'Stark', age: 16 }, + { fname: 'Bran', lname: 'Stark', age: 8 }, + ]), + }); - obj.get('sortedItems'); - } + obj.get('sortedItems'); + } - ['@test sort (with function) is readOnly'](assert) { - assert.throws(function () { - obj.set('sortedItems', 1); - }, /Cannot set read-only property "sortedItems" on object:/); - } + ['@test sort (with function) is readOnly'](assert) { + assert.throws(function () { + obj.set('sortedItems', 1); + }, /Cannot set read-only property "sortedItems" on object:/); + } - ['@test arrays are initially sorted'](assert) { - assert.deepEqual( - obj.get('sortedItems').mapBy('fname'), - ['Cersei', 'Jaime', 'Bran', 'Robb'], - 'array is initially sorted' - ); - } + ['@test arrays are initially sorted'](assert) { + assert.deepEqual( + obj.get('sortedItems').mapBy('fname'), + ['Cersei', 'Jaime', 'Bran', 'Robb'], + 'array is initially sorted' + ); + } - ['@test default sort order is correct'](assert) { - assert.deepEqual( - obj.get('sortedItems').mapBy('fname'), - ['Cersei', 'Jaime', 'Bran', 'Robb'], - 'array is initially sorted' - ); - } + ['@test default sort order is correct'](assert) { + assert.deepEqual( + obj.get('sortedItems').mapBy('fname'), + ['Cersei', 'Jaime', 'Bran', 'Robb'], + 'array is initially sorted' + ); + } - ['@test changing the dependent array updates the sorted array'](assert) { - assert.deepEqual( - obj.get('sortedItems').mapBy('fname'), - ['Cersei', 'Jaime', 'Bran', 'Robb'], - 'precond - array is initially sorted' - ); + ['@test changing the dependent array updates the sorted array'](assert) { + assert.deepEqual( + obj.get('sortedItems').mapBy('fname'), + ['Cersei', 'Jaime', 'Bran', 'Robb'], + 'precond - array is initially sorted' + ); - obj.set('items', [ - { fname: 'Roose', lname: 'Bolton' }, - { fname: 'Theon', lname: 'Greyjoy' }, - { fname: 'Ramsey', lname: 'Bolton' }, - { fname: 'Stannis', lname: 'Baratheon' }, - ]); + obj.set('items', [ + { fname: 'Roose', lname: 'Bolton' }, + { fname: 'Theon', lname: 'Greyjoy' }, + { fname: 'Ramsey', lname: 'Bolton' }, + { fname: 'Stannis', lname: 'Baratheon' }, + ]); - assert.deepEqual( - obj.get('sortedItems').mapBy('fname'), - ['Stannis', 'Ramsey', 'Roose', 'Theon'], - 'changing dependent array updates sorted array' - ); - } + assert.deepEqual( + obj.get('sortedItems').mapBy('fname'), + ['Stannis', 'Ramsey', 'Roose', 'Theon'], + 'changing dependent array updates sorted array' + ); + } - ['@test adding to the dependent array updates the sorted array'](assert) { - let items = obj.get('items'); + ['@test adding to the dependent array updates the sorted array'](assert) { + let items = obj.get('items'); - assert.deepEqual( - obj.get('sortedItems').mapBy('fname'), - ['Cersei', 'Jaime', 'Bran', 'Robb'], - 'precond - array is initially sorted' - ); + assert.deepEqual( + obj.get('sortedItems').mapBy('fname'), + ['Cersei', 'Jaime', 'Bran', 'Robb'], + 'precond - array is initially sorted' + ); - items.pushObject({ - fname: 'Tyrion', - lname: 'Lannister', - }); + items.pushObject({ + fname: 'Tyrion', + lname: 'Lannister', + }); - assert.deepEqual( - obj.get('sortedItems').mapBy('fname'), - ['Cersei', 'Jaime', 'Tyrion', 'Bran', 'Robb'], - 'Adding to the dependent array updates the sorted array' - ); - } + assert.deepEqual( + obj.get('sortedItems').mapBy('fname'), + ['Cersei', 'Jaime', 'Tyrion', 'Bran', 'Robb'], + 'Adding to the dependent array updates the sorted array' + ); + } - ['@test removing from the dependent array updates the sorted array'](assert) { - assert.deepEqual( - obj.get('sortedItems').mapBy('fname'), - ['Cersei', 'Jaime', 'Bran', 'Robb'], - 'precond - array is initially sorted' - ); + ['@test removing from the dependent array updates the sorted array'](assert) { + assert.deepEqual( + obj.get('sortedItems').mapBy('fname'), + ['Cersei', 'Jaime', 'Bran', 'Robb'], + 'precond - array is initially sorted' + ); - obj.get('items').popObject(); + obj.get('items').popObject(); - assert.deepEqual( - obj.get('sortedItems').mapBy('fname'), - ['Cersei', 'Jaime', 'Robb'], - 'Removing from the dependent array updates the sorted array' - ); - } + assert.deepEqual( + obj.get('sortedItems').mapBy('fname'), + ['Cersei', 'Jaime', 'Robb'], + 'Removing from the dependent array updates the sorted array' + ); + } - ['@test distinct items may be sort-equal, although their relative order will not be guaranteed']( - assert - ) { - // We recreate jaime and "Cersei" here only for test stability: we want - // their guid-ordering to be deterministic - let jaimeInDisguise = { - fname: 'Cersei', - lname: 'Lannister', - age: 34, - }; + ['@test distinct items may be sort-equal, although their relative order will not be guaranteed']( + assert + ) { + // We recreate jaime and "Cersei" here only for test stability: we want + // their guid-ordering to be deterministic + let jaimeInDisguise = { + fname: 'Cersei', + lname: 'Lannister', + age: 34, + }; - let jaime = { - fname: 'Jaime', - lname: 'Lannister', - age: 34, - }; + let jaime = { + fname: 'Jaime', + lname: 'Lannister', + age: 34, + }; - let items = obj.get('items'); + let items = obj.get('items'); - items.replace(0, 1, [jaime]); - items.replace(1, 1, [jaimeInDisguise]); + items.replace(0, 1, [jaime]); + items.replace(1, 1, [jaimeInDisguise]); - assert.deepEqual( - obj.get('sortedItems').mapBy('fname'), - ['Cersei', 'Jaime', 'Bran', 'Robb'], - 'precond - array is initially sorted' - ); + assert.deepEqual( + obj.get('sortedItems').mapBy('fname'), + ['Cersei', 'Jaime', 'Bran', 'Robb'], + 'precond - array is initially sorted' + ); - set(jaimeInDisguise, 'fname', 'Jaime'); + set(jaimeInDisguise, 'fname', 'Jaime'); - assert.deepEqual( - obj.get('sortedItems').mapBy('fname'), - ['Jaime', 'Jaime', 'Bran', 'Robb'], - 'sorted array is updated' - ); + assert.deepEqual( + obj.get('sortedItems').mapBy('fname'), + ['Jaime', 'Jaime', 'Bran', 'Robb'], + 'sorted array is updated' + ); - set(jaimeInDisguise, 'fname', 'Cersei'); + set(jaimeInDisguise, 'fname', 'Cersei'); - assert.deepEqual( - obj.get('sortedItems').mapBy('fname'), - ['Cersei', 'Jaime', 'Bran', 'Robb'], - 'sorted array is updated' - ); - } + assert.deepEqual( + obj.get('sortedItems').mapBy('fname'), + ['Cersei', 'Jaime', 'Bran', 'Robb'], + 'sorted array is updated' + ); + } - [`${testUnless( - DEPRECATIONS.DEPRECATE_OBJECT_PROXY.isRemoved - )} @test guid sort-order fallback with a search proxy is not confused by non-search ObjectProxys`]( - assert - ) { - expectDeprecation( - /`ObjectProxy` is deprecated/, - DEPRECATIONS.DEPRECATE_OBJECT_PROXY.isEnabled - ); + [`${testUnless( + DEPRECATIONS.DEPRECATE_OBJECT_PROXY.isRemoved + )} @test guid sort-order fallback with a search proxy is not confused by non-search ObjectProxys`]( + assert + ) { + expectDeprecation( + /`ObjectProxy` is deprecated/, + DEPRECATIONS.DEPRECATE_OBJECT_PROXY.isEnabled + ); - let tyrion = { - fname: 'Tyrion', - lname: 'Lannister', - }; + let tyrion = { + fname: 'Tyrion', + lname: 'Lannister', + }; - let tyrionInDisguise = ObjectProxy.create({ - fname: 'Yollo', - lname: '', - content: tyrion, - }); + let tyrionInDisguise = ObjectProxy.create({ + fname: 'Yollo', + lname: '', + content: tyrion, + }); - let items = obj.get('items'); + let items = obj.get('items'); - items.pushObject(tyrion); + items.pushObject(tyrion); - assert.deepEqual(obj.get('sortedItems').mapBy('fname'), [ - 'Cersei', - 'Jaime', - 'Tyrion', - 'Bran', - 'Robb', - ]); + assert.deepEqual(obj.get('sortedItems').mapBy('fname'), [ + 'Cersei', + 'Jaime', + 'Tyrion', + 'Bran', + 'Robb', + ]); - items.pushObject(tyrionInDisguise); + items.pushObject(tyrionInDisguise); - assert.deepEqual(obj.get('sortedItems').mapBy('fname'), [ - 'Yollo', - 'Cersei', - 'Jaime', - 'Tyrion', - 'Bran', - 'Robb', - ]); - } + assert.deepEqual(obj.get('sortedItems').mapBy('fname'), [ + 'Yollo', + 'Cersei', + 'Jaime', + 'Tyrion', + 'Bran', + 'Robb', + ]); + } - ['@test changing item properties specified via @each triggers a resort of the modified item']( - assert - ) { - let items = get(obj, 'items'); + ['@test changing item properties specified via @each triggers a resort of the modified item']( + assert + ) { + let items = get(obj, 'items'); - let tyrionInDisguise = items[1]; + let tyrionInDisguise = items[1]; - assert.deepEqual( - obj.get('sortedItems').mapBy('fname'), - ['Cersei', 'Jaime', 'Bran', 'Robb'], - 'precond - array is initially sorted' - ); + assert.deepEqual( + obj.get('sortedItems').mapBy('fname'), + ['Cersei', 'Jaime', 'Bran', 'Robb'], + 'precond - array is initially sorted' + ); - set(tyrionInDisguise, 'fname', 'Tyrion'); + set(tyrionInDisguise, 'fname', 'Tyrion'); - assert.deepEqual( - obj.get('sortedItems').mapBy('fname'), - ['Jaime', 'Tyrion', 'Bran', 'Robb'], - 'updating a specified property on an item resorts it' - ); - } + assert.deepEqual( + obj.get('sortedItems').mapBy('fname'), + ['Jaime', 'Tyrion', 'Bran', 'Robb'], + 'updating a specified property on an item resorts it' + ); + } - ['@test sort updates if additional dependent keys are present'](assert) { - obj = class extends EmberObject { - @sort('items', ['sortFunction'], function () { - return this.sortFunction(...arguments); - }) - sortedItems; - }.create({ - sortFunction: sortByLnameFname, - items: emberA([ - { fname: 'Jaime', lname: 'Lannister', age: 34 }, - { fname: 'Cersei', lname: 'Lannister', age: 34 }, - { fname: 'Robb', lname: 'Stark', age: 16 }, - { fname: 'Bran', lname: 'Stark', age: 8 }, - ]), - }); + ['@test sort updates if additional dependent keys are present'](assert) { + obj = class extends EmberObject { + @sort('items', ['sortFunction'], function () { + return this.sortFunction(...arguments); + }) + sortedItems; + }.create({ + sortFunction: sortByLnameFname, + items: emberA([ + { fname: 'Jaime', lname: 'Lannister', age: 34 }, + { fname: 'Cersei', lname: 'Lannister', age: 34 }, + { fname: 'Robb', lname: 'Stark', age: 16 }, + { fname: 'Bran', lname: 'Stark', age: 8 }, + ]), + }); - assert.deepEqual( - obj.get('sortedItems').mapBy('fname'), - ['Cersei', 'Jaime', 'Bran', 'Robb'], - 'array is initially sorted' - ); + assert.deepEqual( + obj.get('sortedItems').mapBy('fname'), + ['Cersei', 'Jaime', 'Bran', 'Robb'], + 'array is initially sorted' + ); - obj.set('sortFunction', (a, b) => { - if (a.age > b.age) { - return -1; - } else if (a.age < b.age) { - return 1; - } + obj.set('sortFunction', (a, b) => { + if (a.age > b.age) { + return -1; + } else if (a.age < b.age) { + return 1; + } - return 0; - }); + return 0; + }); - assert.deepEqual( - obj.get('sortedItems').mapBy('fname'), - ['Jaime', 'Cersei', 'Robb', 'Bran'], - 'array is updated when dependent key changes' - ); - } + assert.deepEqual( + obj.get('sortedItems').mapBy('fname'), + ['Jaime', 'Cersei', 'Robb', 'Bran'], + 'array is updated when dependent key changes' + ); + } - ['@test it throws on bad inputs']() { - expectAssertion(() => { - sort('foo', 'bar', 'baz'); - }, /`sort` computed macro can either be used with an array of sort properties or with a sort function/); + ['@test it throws on bad inputs']() { + expectAssertion(() => { + sort('foo', 'bar', 'baz'); + }, /`sort` computed macro can either be used with an array of sort properties or with a sort function/); - expectAssertion(() => { - sort('foo', ['bar'], 'baz'); - }, /`sort` computed macro can either be used with an array of sort properties or with a sort function/); + expectAssertion(() => { + sort('foo', ['bar'], 'baz'); + }, /`sort` computed macro can either be used with an array of sort properties or with a sort function/); - expectAssertion(() => { - sort('foo', 'bar', function () {}); - }, /`sort` computed macro can either be used with an array of sort properties or with a sort function/); + expectAssertion(() => { + sort('foo', 'bar', function () {}); + }, /`sort` computed macro can either be used with an array of sort properties or with a sort function/); - expectAssertion(() => { - sort('foo', ['bar'], function () {}, 'baz'); - }, /`sort` computed macro can either be used with an array of sort properties or with a sort function/); + expectAssertion(() => { + sort('foo', ['bar'], function () {}, 'baz'); + }, /`sort` computed macro can either be used with an array of sort properties or with a sort function/); + } } - } -); + ); -moduleFor( - 'sort - stability', - class extends AbstractTestCase { - beforeEach() { - obj = class extends EmberObject { - sortProps = ['count', 'name']; - @sort('items', 'sortProps') - sortedItems; - }.create({ - items: [ - { name: 'A', count: 1, thing: 4 }, - { name: 'B', count: 1, thing: 3 }, - { name: 'C', count: 1, thing: 2 }, - { name: 'D', count: 1, thing: 4 }, - ], - }); - } + moduleFor( + 'sort - stability', + class extends ArrayMacroTestCase { + beforeEach() { + obj = class extends EmberObject { + sortProps = ['count', 'name']; + @sort('items', 'sortProps') + sortedItems; + }.create({ + items: [ + { name: 'A', count: 1, thing: 4 }, + { name: 'B', count: 1, thing: 3 }, + { name: 'C', count: 1, thing: 2 }, + { name: 'D', count: 1, thing: 4 }, + ], + }); + } - afterEach() { - run(obj, 'destroy'); - } + afterEach() { + run(obj, 'destroy'); + } - ['@test sorts correctly as only one property changes'](assert) { - assert.deepEqual(obj.get('sortedItems').mapBy('name'), ['A', 'B', 'C', 'D'], 'initial'); + ['@test sorts correctly as only one property changes'](assert) { + assert.deepEqual(obj.get('sortedItems').mapBy('name'), ['A', 'B', 'C', 'D'], 'initial'); - set(obj.get('items')[3], 'count', 2); + set(obj.get('items')[3], 'count', 2); - assert.deepEqual(obj.get('sortedItems').mapBy('name'), ['A', 'B', 'C', 'D'], 'final'); + assert.deepEqual(obj.get('sortedItems').mapBy('name'), ['A', 'B', 'C', 'D'], 'final'); + } } - } -); + ); -let klass; -moduleFor( - 'sort - concurrency', - class extends AbstractTestCase { - beforeEach() { - klass = class extends EmberObject { - sortProps = ['count']; - @sort('items', 'sortProps') - sortedItems; - @sort('items.@each.count', (a, b) => a.count - b.count) - customSortedItems; - }; - obj = klass.create({ - items: emberA([ - { name: 'A', count: 1, thing: 4, id: 1 }, - { name: 'B', count: 2, thing: 3, id: 2 }, - { name: 'C', count: 3, thing: 2, id: 3 }, - { name: 'D', count: 4, thing: 1, id: 4 }, - ]), - }); - } + let klass; + moduleFor( + 'sort - concurrency', + class extends ArrayMacroTestCase { + beforeEach() { + klass = class extends EmberObject { + sortProps = ['count']; + @sort('items', 'sortProps') + sortedItems; + @sort('items.@each.count', (a, b) => a.count - b.count) + customSortedItems; + }; + obj = klass.create({ + items: emberA([ + { name: 'A', count: 1, thing: 4, id: 1 }, + { name: 'B', count: 2, thing: 3, id: 2 }, + { name: 'C', count: 3, thing: 2, id: 3 }, + { name: 'D', count: 4, thing: 1, id: 4 }, + ]), + }); + } - afterEach() { - run(obj, 'destroy'); - } + afterEach() { + run(obj, 'destroy'); + } - ['@test sorts correctly after mutation to the sort properties'](assert) { - let sorted = obj.get('sortedItems'); - assert.deepEqual(sorted.mapBy('name'), ['A', 'B', 'C', 'D'], 'initial'); + ['@test sorts correctly after mutation to the sort properties'](assert) { + let sorted = obj.get('sortedItems'); + assert.deepEqual(sorted.mapBy('name'), ['A', 'B', 'C', 'D'], 'initial'); - set(obj.get('items')[1], 'count', 5); - set(obj.get('items')[2], 'count', 6); + set(obj.get('items')[1], 'count', 5); + set(obj.get('items')[2], 'count', 6); - assert.deepEqual(obj.get('sortedItems').mapBy('name'), ['A', 'D', 'B', 'C'], 'final'); - } + assert.deepEqual(obj.get('sortedItems').mapBy('name'), ['A', 'D', 'B', 'C'], 'final'); + } - ['@test sort correctly after mutation to the sort'](assert) { - assert.deepEqual(obj.get('customSortedItems').mapBy('name'), ['A', 'B', 'C', 'D'], 'initial'); + ['@test sort correctly after mutation to the sort'](assert) { + assert.deepEqual( + obj.get('customSortedItems').mapBy('name'), + ['A', 'B', 'C', 'D'], + 'initial' + ); - set(obj.get('items')[1], 'count', 5); - set(obj.get('items')[2], 'count', 6); + set(obj.get('items')[1], 'count', 5); + set(obj.get('items')[2], 'count', 6); - assert.deepEqual(obj.get('customSortedItems').mapBy('name'), ['A', 'D', 'B', 'C'], 'final'); + assert.deepEqual(obj.get('customSortedItems').mapBy('name'), ['A', 'D', 'B', 'C'], 'final'); - assert.deepEqual(obj.get('sortedItems').mapBy('name'), ['A', 'D', 'B', 'C'], 'final'); - } + assert.deepEqual(obj.get('sortedItems').mapBy('name'), ['A', 'D', 'B', 'C'], 'final'); + } - ['@test sort correctly on multiple instances of the same class'](assert) { - let obj2 = klass.create({ - items: emberA([ - { name: 'W', count: 23, thing: 4 }, - { name: 'X', count: 24, thing: 3 }, - { name: 'Y', count: 25, thing: 2 }, - { name: 'Z', count: 26, thing: 1 }, - ]), - }); + ['@test sort correctly on multiple instances of the same class'](assert) { + let obj2 = klass.create({ + items: emberA([ + { name: 'W', count: 23, thing: 4 }, + { name: 'X', count: 24, thing: 3 }, + { name: 'Y', count: 25, thing: 2 }, + { name: 'Z', count: 26, thing: 1 }, + ]), + }); - assert.deepEqual(obj2.get('sortedItems').mapBy('name'), ['W', 'X', 'Y', 'Z'], 'initial'); - assert.deepEqual(obj.get('sortedItems').mapBy('name'), ['A', 'B', 'C', 'D'], 'initial'); + assert.deepEqual(obj2.get('sortedItems').mapBy('name'), ['W', 'X', 'Y', 'Z'], 'initial'); + assert.deepEqual(obj.get('sortedItems').mapBy('name'), ['A', 'B', 'C', 'D'], 'initial'); - set(obj.get('items')[1], 'count', 5); - set(obj.get('items')[2], 'count', 6); - set(obj2.get('items')[1], 'count', 27); - set(obj2.get('items')[2], 'count', 28); + set(obj.get('items')[1], 'count', 5); + set(obj.get('items')[2], 'count', 6); + set(obj2.get('items')[1], 'count', 27); + set(obj2.get('items')[2], 'count', 28); - assert.deepEqual(obj.get('sortedItems').mapBy('name'), ['A', 'D', 'B', 'C'], 'final'); - assert.deepEqual(obj2.get('sortedItems').mapBy('name'), ['W', 'Z', 'X', 'Y'], 'final'); + assert.deepEqual(obj.get('sortedItems').mapBy('name'), ['A', 'D', 'B', 'C'], 'final'); + assert.deepEqual(obj2.get('sortedItems').mapBy('name'), ['W', 'Z', 'X', 'Y'], 'final'); - obj.set('sortProps', ['thing']); + obj.set('sortProps', ['thing']); - assert.deepEqual(obj.get('sortedItems').mapBy('name'), ['D', 'C', 'B', 'A'], 'final'); + assert.deepEqual(obj.get('sortedItems').mapBy('name'), ['D', 'C', 'B', 'A'], 'final'); - obj2.notifyPropertyChange('sortedItems'); // invalidate to flush, to get DK refreshed - obj2.get('sortedItems'); // flush to get updated DK + obj2.notifyPropertyChange('sortedItems'); // invalidate to flush, to get DK refreshed + obj2.get('sortedItems'); // flush to get updated DK - obj2.set('items.firstObject.count', 9999); + obj2.set('items.firstObject.count', 9999); - assert.deepEqual(obj2.get('sortedItems').mapBy('name'), ['Z', 'X', 'Y', 'W'], 'final'); - } + assert.deepEqual(obj2.get('sortedItems').mapBy('name'), ['Z', 'X', 'Y', 'W'], 'final'); + } - ['@test sort correctly when multiple sorts are chained on the same instance of a class']( - assert - ) { - let obj2 = klass - .extend({ - items: computed('sibling.sortedItems.[]', function () { - return this.get('sibling.sortedItems'); - }), - asdf: observer('sibling.sortedItems.[]', function () { - this.get('sibling.sortedItems'); - }), - }) - .create({ - sibling: obj, - }); + ['@test sort correctly when multiple sorts are chained on the same instance of a class']( + assert + ) { + let obj2 = klass + .extend({ + items: computed('sibling.sortedItems.[]', function () { + return this.get('sibling.sortedItems'); + }), + asdf: observer('sibling.sortedItems.[]', function () { + this.get('sibling.sortedItems'); + }), + }) + .create({ + sibling: obj, + }); - /* + /* ┌───────────┐ ┌────────────┐ │sortedProps│ │sortedProps2│ └───────────┘ └────────────┘ @@ -2054,406 +2085,440 @@ moduleFor( └───────────┘ ┗━━━━━━━━━━━┛ ┗━━━━━━━━━━━━┛ */ - assert.deepEqual( - obj.get('sortedItems').mapBy('name'), - ['A', 'B', 'C', 'D'], - 'obj.sortedItems.name should be sorted alpha' - ); - assert.deepEqual( - obj2.get('sortedItems').mapBy('name'), - ['A', 'B', 'C', 'D'], - 'obj2.sortedItems.name should be sorted alpha' - ); + assert.deepEqual( + obj.get('sortedItems').mapBy('name'), + ['A', 'B', 'C', 'D'], + 'obj.sortedItems.name should be sorted alpha' + ); + assert.deepEqual( + obj2.get('sortedItems').mapBy('name'), + ['A', 'B', 'C', 'D'], + 'obj2.sortedItems.name should be sorted alpha' + ); - set(obj.get('items')[1], 'count', 5); - set(obj.get('items')[2], 'count', 6); + set(obj.get('items')[1], 'count', 5); + set(obj.get('items')[2], 'count', 6); - assert.deepEqual( - obj.get('sortedItems').mapBy('name'), - ['A', 'D', 'B', 'C'], - 'obj.sortedItems.name should now have changed' - ); - assert.deepEqual( - obj2.get('sortedItems').mapBy('name'), - ['A', 'D', 'B', 'C'], - 'obj2.sortedItems.name should still mirror sortedItems2' - ); + assert.deepEqual( + obj.get('sortedItems').mapBy('name'), + ['A', 'D', 'B', 'C'], + 'obj.sortedItems.name should now have changed' + ); + assert.deepEqual( + obj2.get('sortedItems').mapBy('name'), + ['A', 'D', 'B', 'C'], + 'obj2.sortedItems.name should still mirror sortedItems2' + ); - obj.set('sortProps', ['thing']); - obj2.set('sortProps', ['id']); + obj.set('sortProps', ['thing']); + obj2.set('sortProps', ['id']); - assert.deepEqual( - obj2.get('sortedItems').mapBy('name'), - ['A', 'B', 'C', 'D'], - 'we now sort obj2 by id, so we expect a b c d' - ); - assert.deepEqual( - obj.get('sortedItems').mapBy('name'), - ['D', 'C', 'B', 'A'], - 'we now sort obj by thing' - ); + assert.deepEqual( + obj2.get('sortedItems').mapBy('name'), + ['A', 'B', 'C', 'D'], + 'we now sort obj2 by id, so we expect a b c d' + ); + assert.deepEqual( + obj.get('sortedItems').mapBy('name'), + ['D', 'C', 'B', 'A'], + 'we now sort obj by thing' + ); - obj2.destroy(); + obj2.destroy(); + } } - } -); + ); -moduleFor( - 'max', - class extends AbstractTestCase { - beforeEach() { - obj = class extends EmberObject { - @max('items') - max; - }.create({ - items: emberA([1, 2, 3]), - }); - } + moduleFor( + 'max', + class extends ArrayMacroTestCase { + beforeEach() { + obj = class extends EmberObject { + @max('items') + max; + }.create({ + items: emberA([1, 2, 3]), + }); + } - afterEach() { - run(obj, 'destroy'); - } + afterEach() { + run(obj, 'destroy'); + } - ['@test max is readOnly'](assert) { - assert.throws(function () { - obj.set('max', 1); - }, /Cannot set read-only property "max" on object:/); - } + ['@test max is readOnly'](assert) { + assert.throws(function () { + obj.set('max', 1); + }, /Cannot set read-only property "max" on object:/); + } - ['@test max tracks the max number as objects are added'](assert) { - assert.equal(obj.get('max'), 3, 'precond - max is initially correct'); + ['@test max tracks the max number as objects are added'](assert) { + assert.equal(obj.get('max'), 3, 'precond - max is initially correct'); - let items = obj.get('items'); + let items = obj.get('items'); - items.pushObject(5); + items.pushObject(5); - assert.equal(obj.get('max'), 5, 'max updates when a larger number is added'); + assert.equal(obj.get('max'), 5, 'max updates when a larger number is added'); - items.pushObject(2); + items.pushObject(2); - assert.equal(obj.get('max'), 5, 'max does not update when a smaller number is added'); - } + assert.equal(obj.get('max'), 5, 'max does not update when a smaller number is added'); + } - ['@test max recomputes when the current max is removed'](assert) { - assert.equal(obj.get('max'), 3, 'precond - max is initially correct'); + ['@test max recomputes when the current max is removed'](assert) { + assert.equal(obj.get('max'), 3, 'precond - max is initially correct'); - obj.get('items').removeObject(2); + obj.get('items').removeObject(2); - assert.equal(obj.get('max'), 3, 'max is unchanged when a non-max item is removed'); + assert.equal(obj.get('max'), 3, 'max is unchanged when a non-max item is removed'); - obj.get('items').removeObject(3); + obj.get('items').removeObject(3); - assert.equal(obj.get('max'), 1, 'max is recomputed when the current max is removed'); + assert.equal(obj.get('max'), 1, 'max is recomputed when the current max is removed'); + } } - } -); + ); -moduleFor( - 'min', - class extends AbstractTestCase { - beforeEach() { - obj = class extends EmberObject { - @min('items') - min; - }.create({ - items: emberA([1, 2, 3]), - }); - } + moduleFor( + 'min', + class extends ArrayMacroTestCase { + beforeEach() { + obj = class extends EmberObject { + @min('items') + min; + }.create({ + items: emberA([1, 2, 3]), + }); + } - afterEach() { - run(obj, 'destroy'); - } + afterEach() { + run(obj, 'destroy'); + } - ['@test min is readOnly'](assert) { - assert.throws(function () { - obj.set('min', 1); - }, /Cannot set read-only property "min" on object:/); - } + ['@test min is readOnly'](assert) { + assert.throws(function () { + obj.set('min', 1); + }, /Cannot set read-only property "min" on object:/); + } - ['@test min tracks the min number as objects are added'](assert) { - assert.equal(obj.get('min'), 1, 'precond - min is initially correct'); + ['@test min tracks the min number as objects are added'](assert) { + assert.equal(obj.get('min'), 1, 'precond - min is initially correct'); - obj.get('items').pushObject(-2); + obj.get('items').pushObject(-2); - assert.equal(obj.get('min'), -2, 'min updates when a smaller number is added'); + assert.equal(obj.get('min'), -2, 'min updates when a smaller number is added'); - obj.get('items').pushObject(2); + obj.get('items').pushObject(2); - assert.equal(obj.get('min'), -2, 'min does not update when a larger number is added'); - } + assert.equal(obj.get('min'), -2, 'min does not update when a larger number is added'); + } - ['@test min recomputes when the current min is removed'](assert) { - let items = obj.get('items'); + ['@test min recomputes when the current min is removed'](assert) { + let items = obj.get('items'); - assert.equal(obj.get('min'), 1, 'precond - min is initially correct'); + assert.equal(obj.get('min'), 1, 'precond - min is initially correct'); - items.removeObject(2); + items.removeObject(2); - assert.equal(obj.get('min'), 1, 'min is unchanged when a non-min item is removed'); + assert.equal(obj.get('min'), 1, 'min is unchanged when a non-min item is removed'); - items.removeObject(1); + items.removeObject(1); - assert.equal(obj.get('min'), 3, 'min is recomputed when the current min is removed'); + assert.equal(obj.get('min'), 3, 'min is recomputed when the current min is removed'); + } } - } -); + ); -moduleFor( - 'Ember.arrayComputed - mixed sugar', - class extends AbstractTestCase { - beforeEach() { - obj = class extends EmberObject { - @filterBy('items', 'lname', 'Lannister') - lannisters; - lannisterSorting = emberA(['fname']); - @sort('lannisters', 'lannisterSorting') - sortedLannisters; - - @filterBy('items', 'lname', 'Stark') - starks; - @mapBy('starks', 'age') - starkAges; - @max('starkAges') - oldestStarkAge; - }.create({ - items: emberA([ - { fname: 'Jaime', lname: 'Lannister', age: 34 }, - { fname: 'Cersei', lname: 'Lannister', age: 34 }, - { fname: 'Robb', lname: 'Stark', age: 16 }, - { fname: 'Bran', lname: 'Stark', age: 8 }, - ]), - }); - } + moduleFor( + 'Ember.arrayComputed - mixed sugar', + class extends ArrayMacroTestCase { + beforeEach() { + obj = class extends EmberObject { + @filterBy('items', 'lname', 'Lannister') + lannisters; + lannisterSorting = emberA(['fname']); + @sort('lannisters', 'lannisterSorting') + sortedLannisters; + + @filterBy('items', 'lname', 'Stark') + starks; + @mapBy('starks', 'age') + starkAges; + @max('starkAges') + oldestStarkAge; + }.create({ + items: emberA([ + { fname: 'Jaime', lname: 'Lannister', age: 34 }, + { fname: 'Cersei', lname: 'Lannister', age: 34 }, + { fname: 'Robb', lname: 'Stark', age: 16 }, + { fname: 'Bran', lname: 'Stark', age: 8 }, + ]), + }); + } - afterEach() { - run(obj, 'destroy'); - } + afterEach() { + run(obj, 'destroy'); + } - ['@test filtering and sorting can be combined'](assert) { - let items = obj.get('items'); + ['@test filtering and sorting can be combined'](assert) { + let items = obj.get('items'); - assert.deepEqual( - obj.get('sortedLannisters').mapBy('fname'), - ['Cersei', 'Jaime'], - 'precond - array is initially filtered and sorted' - ); + assert.deepEqual( + obj.get('sortedLannisters').mapBy('fname'), + ['Cersei', 'Jaime'], + 'precond - array is initially filtered and sorted' + ); - items.pushObject({ fname: 'Tywin', lname: 'Lannister' }); - items.pushObject({ fname: 'Lyanna', lname: 'Stark' }); - items.pushObject({ fname: 'Gerion', lname: 'Lannister' }); + items.pushObject({ fname: 'Tywin', lname: 'Lannister' }); + items.pushObject({ fname: 'Lyanna', lname: 'Stark' }); + items.pushObject({ fname: 'Gerion', lname: 'Lannister' }); - assert.deepEqual( - obj.get('sortedLannisters').mapBy('fname'), - ['Cersei', 'Gerion', 'Jaime', 'Tywin'], - 'updates propagate to array' - ); - } + assert.deepEqual( + obj.get('sortedLannisters').mapBy('fname'), + ['Cersei', 'Gerion', 'Jaime', 'Tywin'], + 'updates propagate to array' + ); + } - ['@test filtering, sorting and reduce (max) can be combined'](assert) { - let items = obj.get('items'); + ['@test filtering, sorting and reduce (max) can be combined'](assert) { + let items = obj.get('items'); - assert.equal(16, obj.get('oldestStarkAge'), 'precond - end of chain is initially correct'); + assert.equal(16, obj.get('oldestStarkAge'), 'precond - end of chain is initially correct'); - items.pushObject({ fname: 'Rickon', lname: 'Stark', age: 5 }); + items.pushObject({ fname: 'Rickon', lname: 'Stark', age: 5 }); - assert.equal(16, obj.get('oldestStarkAge'), 'chain is updated correctly'); + assert.equal(16, obj.get('oldestStarkAge'), 'chain is updated correctly'); - items.pushObject({ fname: 'Eddard', lname: 'Stark', age: 35 }); + items.pushObject({ fname: 'Eddard', lname: 'Stark', age: 35 }); - assert.equal(35, obj.get('oldestStarkAge'), 'chain is updated correctly'); + assert.equal(35, obj.get('oldestStarkAge'), 'chain is updated correctly'); + } } - } -); + ); -function todo(name, priority) { - return EmberObject.create({ name: name, priority: priority }); -} + function todo(name, priority) { + return EmberObject.create({ name: name, priority: priority }); + } -function priorityComparator(todoA, todoB) { - let pa = parseInt(get(todoA, 'priority'), 10); - let pb = parseInt(get(todoB, 'priority'), 10); + function priorityComparator(todoA, todoB) { + let pa = parseInt(get(todoA, 'priority'), 10); + let pb = parseInt(get(todoB, 'priority'), 10); - return pa - pb; -} + return pa - pb; + } -function evenPriorities(todo) { - let p = parseInt(get(todo, 'priority'), 10); + function evenPriorities(todo) { + let p = parseInt(get(todo, 'priority'), 10); - return p % 2 === 0; -} + return p % 2 === 0; + } -moduleFor( - 'Ember.arrayComputed - chains', - class extends AbstractTestCase { - beforeEach() { - obj = class extends EmberObject { - @sort('todos.@each.priority', priorityComparator) - sorted; - @filter('sorted.@each.priority', evenPriorities) - filtered; - }.create({ - todos: emberA([todo('E', 4), todo('D', 3), todo('C', 2), todo('B', 1), todo('A', 0)]), - }); - } + moduleFor( + 'Ember.arrayComputed - chains', + class extends ArrayMacroTestCase { + beforeEach() { + obj = class extends EmberObject { + @sort('todos.@each.priority', priorityComparator) + sorted; + @filter('sorted.@each.priority', evenPriorities) + filtered; + }.create({ + todos: emberA([todo('E', 4), todo('D', 3), todo('C', 2), todo('B', 1), todo('A', 0)]), + }); + } - afterEach() { - run(obj, 'destroy'); - } + afterEach() { + run(obj, 'destroy'); + } - ['@test it can filter and sort when both depend on the same item property'](assert) { - assert.deepEqual( - obj.get('todos').mapBy('name'), - ['E', 'D', 'C', 'B', 'A'], - 'precond - todos initially correct' - ); - assert.deepEqual( - obj.get('sorted').mapBy('name'), - ['A', 'B', 'C', 'D', 'E'], - 'precond - sorted initially correct' - ); - assert.deepEqual( - obj.get('filtered').mapBy('name'), - ['A', 'C', 'E'], - 'precond - filtered initially correct' - ); + ['@test it can filter and sort when both depend on the same item property'](assert) { + assert.deepEqual( + obj.get('todos').mapBy('name'), + ['E', 'D', 'C', 'B', 'A'], + 'precond - todos initially correct' + ); + assert.deepEqual( + obj.get('sorted').mapBy('name'), + ['A', 'B', 'C', 'D', 'E'], + 'precond - sorted initially correct' + ); + assert.deepEqual( + obj.get('filtered').mapBy('name'), + ['A', 'C', 'E'], + 'precond - filtered initially correct' + ); - set(obj.get('todos')[1], 'priority', 6); + set(obj.get('todos')[1], 'priority', 6); - assert.deepEqual( - obj.get('todos').mapBy('name'), - ['E', 'D', 'C', 'B', 'A'], - 'precond - todos remain correct' - ); - assert.deepEqual( - obj.get('sorted').mapBy('name'), - ['A', 'B', 'C', 'E', 'D'], - 'precond - sorted updated correctly' - ); - assert.deepEqual( - obj.get('filtered').mapBy('name'), - ['A', 'C', 'E', 'D'], - 'filtered updated correctly' - ); + assert.deepEqual( + obj.get('todos').mapBy('name'), + ['E', 'D', 'C', 'B', 'A'], + 'precond - todos remain correct' + ); + assert.deepEqual( + obj.get('sorted').mapBy('name'), + ['A', 'B', 'C', 'E', 'D'], + 'precond - sorted updated correctly' + ); + assert.deepEqual( + obj.get('filtered').mapBy('name'), + ['A', 'C', 'E', 'D'], + 'filtered updated correctly' + ); + } } - } -); + ); -let userFnCalls; -moduleFor( - 'Chaining array and reduced CPs', - class extends AbstractTestCase { - beforeEach() { - userFnCalls = 0; - obj = class extends EmberObject { - @mapBy('array', 'v') - mapped; - @max('mapped') - max; - } - .extend({ - maxDidChange: observer('max', () => userFnCalls++), - }) - .create({ - array: emberA([{ v: 1 }, { v: 3 }, { v: 2 }, { v: 1 }]), - }); - } + let userFnCalls; + moduleFor( + 'Chaining array and reduced CPs', + class extends ArrayMacroTestCase { + beforeEach() { + userFnCalls = 0; + obj = class extends EmberObject { + @mapBy('array', 'v') + mapped; + @max('mapped') + max; + } + .extend({ + maxDidChange: observer('max', () => userFnCalls++), + }) + .create({ + array: emberA([{ v: 1 }, { v: 3 }, { v: 2 }, { v: 1 }]), + }); + } - afterEach() { - run(obj, 'destroy'); - } + afterEach() { + run(obj, 'destroy'); + } - async ['@test it computes interdependent array computed properties'](assert) { - assert.equal(userFnCalls, 0, 'object defined observers have not fired yet'); - assert.equal(obj.get('max'), 3, 'sanity - it properly computes the maximum value'); + async ['@test it computes interdependent array computed properties'](assert) { + assert.equal(userFnCalls, 0, 'object defined observers have not fired yet'); + assert.equal(obj.get('max'), 3, 'sanity - it properly computes the maximum value'); - let calls = 0; + let calls = 0; - addObserver(obj, 'max', () => calls++); + addObserver(obj, 'max', () => calls++); - obj.get('array').pushObject({ v: 5 }); - await runLoopSettled(); + obj.get('array').pushObject({ v: 5 }); + await runLoopSettled(); - assert.equal(obj.get('max'), 5, 'maximum value is updated correctly'); - assert.equal(userFnCalls, 1, 'object defined observers fire'); - assert.equal(calls, 1, 'runtime created observers fire'); + assert.equal(obj.get('max'), 5, 'maximum value is updated correctly'); + assert.equal(userFnCalls, 1, 'object defined observers fire'); + assert.equal(calls, 1, 'runtime created observers fire'); - obj.get('array').pushObject({ v: 7 }); - await runLoopSettled(); + obj.get('array').pushObject({ v: 7 }); + await runLoopSettled(); - assert.equal(obj.get('max'), 7, 'maximum value is updated correctly again'); - assert.equal(userFnCalls, 2, 'object defined observers fire again'); - assert.equal(calls, 2, 'runtime created observers fire again'); + assert.equal(obj.get('max'), 7, 'maximum value is updated correctly again'); + assert.equal(userFnCalls, 2, 'object defined observers fire again'); + assert.equal(calls, 2, 'runtime created observers fire again'); + } } - } -); + ); -moduleFor( - 'sum', - class extends AbstractTestCase { - beforeEach() { - obj = class extends EmberObject { - @sum('array') - total; - }.create({ - array: emberA([1, 2, 3]), - }); - } + moduleFor( + 'sum', + class extends ArrayMacroTestCase { + beforeEach() { + obj = class extends EmberObject { + @sum('array') + total; + }.create({ + array: emberA([1, 2, 3]), + }); + } - afterEach() { - run(obj, 'destroy'); - } + afterEach() { + run(obj, 'destroy'); + } - ['@test sum is readOnly'](assert) { - assert.throws(function () { - obj.set('total', 1); - }, /Cannot set read-only property "total" on object:/); - } + ['@test sum is readOnly'](assert) { + assert.throws(function () { + obj.set('total', 1); + }, /Cannot set read-only property "total" on object:/); + } - ['@test sums the values in the dependentKey'](assert) { - assert.equal(obj.get('total'), 6, 'sums the values'); - } + ['@test sums the values in the dependentKey'](assert) { + assert.equal(obj.get('total'), 6, 'sums the values'); + } - ['@test if the dependentKey is neither an array nor object, it will return `0`'](assert) { - set(obj, 'array', null); - assert.equal(get(obj, 'total'), 0, 'returns 0'); + ['@test if the dependentKey is neither an array nor object, it will return `0`'](assert) { + set(obj, 'array', null); + assert.equal(get(obj, 'total'), 0, 'returns 0'); - set(obj, 'array', undefined); - assert.equal(get(obj, 'total'), 0, 'returns 0'); + set(obj, 'array', undefined); + assert.equal(get(obj, 'total'), 0, 'returns 0'); - set(obj, 'array', 'not an array'); - assert.equal(get(obj, 'total'), 0, 'returns 0'); - } + set(obj, 'array', 'not an array'); + assert.equal(get(obj, 'total'), 0, 'returns 0'); + } - ['@test updates when array is modified'](assert) { - obj.get('array').pushObject(1); + ['@test updates when array is modified'](assert) { + obj.get('array').pushObject(1); - assert.equal(obj.get('total'), 7, 'recomputed when elements are added'); + assert.equal(obj.get('total'), 7, 'recomputed when elements are added'); - obj.get('array').popObject(); + obj.get('array').popObject(); - assert.equal(obj.get('total'), 6, 'recomputes when elements are removed'); + assert.equal(obj.get('total'), 6, 'recomputes when elements are removed'); + } } - } -); + ); -moduleFor( - 'collect', - class extends AbstractTestCase { - ['@test works'](assert) { - let obj = { one: 'foo', two: 'bar', three: null }; - defineProperty(obj, 'all', collect('one', 'two', 'three', 'four')); + moduleFor( + 'collect', + class extends ArrayMacroTestCase { + ['@test works'](assert) { + let obj = { one: 'foo', two: 'bar', three: null }; + defineProperty(obj, 'all', collect('one', 'two', 'three', 'four')); - assert.deepEqual(get(obj, 'all'), ['foo', 'bar', null, null], 'have all of them'); + assert.deepEqual(get(obj, 'all'), ['foo', 'bar', null, null], 'have all of them'); - set(obj, 'four', true); + set(obj, 'four', true); - assert.deepEqual(get(obj, 'all'), ['foo', 'bar', null, true], 'have all of them'); + assert.deepEqual(get(obj, 'all'), ['foo', 'bar', null, true], 'have all of them'); - let a = []; - set(obj, 'one', 0); - set(obj, 'three', a); + let a = []; + set(obj, 'one', 0); + set(obj, 'three', a); - assert.deepEqual(get(obj, 'all'), [0, 'bar', a, true], 'have all of them'); + assert.deepEqual(get(obj, 'all'), [0, 'bar', a, true], 'have all of them'); + } } - } -); + ); + + moduleFor( + 'array computed macros deprecation', + class extends AbstractTestCase { + ['@test each array macro is deprecated']() { + let callback = () => {}; + let macros = { + sum: () => sum('array'), + max: () => max('array'), + min: () => min('array'), + map: () => map('array', callback), + mapBy: () => mapBy('array', 'key'), + filter: () => filter('array', callback), + filterBy: () => filterBy('array', 'key'), + uniq: () => uniq('array'), + uniqBy: () => uniqBy('array', 'key'), + union: () => union('array'), + intersect: () => intersect('array'), + setDiff: () => setDiff('array', 'other'), + collect: () => collect('array'), + sort: () => sort('array', 'sorting'), + }; + + for (let [name, callMacro] of Object.entries(macros)) { + expectDeprecation( + callMacro, + `\`${name}\` from \`@ember/object/computed\` is deprecated. Use a getter with native array methods instead.`, + isEnabled + ); + } + } + } + ); +} diff --git a/packages/@ember/object/tests/computed_test.js b/packages/@ember/object/tests/computed_test.js index 59d294d421f..1b9c9b8fb6a 100644 --- a/packages/@ember/object/tests/computed_test.js +++ b/packages/@ember/object/tests/computed_test.js @@ -1,8 +1,11 @@ import { notifyPropertyChange } from '@ember/-internals/metal'; import { alias, oneWay as reads } from '@ember/object/computed'; -import { A as emberA, isArray } from '@ember/array'; +import { isArray, A as emberA } from '@ember/array'; import EmberObject, { defineProperty, get, set, computed, observer } from '@ember/object'; -import { moduleFor, AbstractTestCase } from 'internal-test-helpers'; +import { moduleFor, AbstractTestCase, expectDeprecation, testUnless } from 'internal-test-helpers'; +import { DEPRECATIONS } from '@ember/-internals/deprecations'; + +const EMBER_A = DEPRECATIONS.DEPRECATE_EMBER_ARRAY_A; function K() { return this; @@ -392,7 +395,9 @@ moduleFor( assert.equal(instance.bar, 456, 'setters work'); } - ['@test @each on maybe array'](assert) { + [`${testUnless(EMBER_A.isRemoved)} @test @each on maybe array`](assert) { + expectDeprecation(/`A` from `@ember\/array` is deprecated/, EMBER_A.isEnabled); + let Normalizer = EmberObject.extend({ options: null, // null | undefined | { value: any } | Array<{ value: any }> diff --git a/packages/@ember/object/tests/observable_test.js b/packages/@ember/object/tests/observable_test.js index a6c3374cf73..690b39ac525 100644 --- a/packages/@ember/object/tests/observable_test.js +++ b/packages/@ember/object/tests/observable_test.js @@ -3,7 +3,16 @@ import { run } from '@ember/runloop'; import { get, computed } from '@ember/object'; import EmberObject, { observer } from '@ember/object'; import { A as emberA } from '@ember/array'; -import { moduleFor, AbstractTestCase, runLoopSettled } from 'internal-test-helpers'; +import { + moduleFor, + AbstractTestCase, + runLoopSettled, + expectDeprecation, + testUnless, +} from 'internal-test-helpers'; +import { DEPRECATIONS } from '@ember/-internals/deprecations'; + +const EMBER_A = DEPRECATIONS.DEPRECATE_EMBER_ARRAY_A; /* NOTE: This test is adapted from the 1.x series of unit tests. The tests @@ -718,7 +727,11 @@ moduleFor( ); } - async ['@test should notify array observer when array changes'](assert) { + async [`${testUnless( + EMBER_A.isRemoved + )} @test should notify array observer when array changes`](assert) { + expectDeprecation(/`A` from `@ember\/array` is deprecated/, EMBER_A.isEnabled); + object.set('normalArray', emberA([1, 2, 3, 4, 5])); await runLoopSettled(); object.abnormal = 'zeroValue'; diff --git a/packages/@ember/routing/router.ts b/packages/@ember/routing/router.ts index 3f6a356eaa1..92fa651716e 100644 --- a/packages/@ember/routing/router.ts +++ b/packages/@ember/routing/router.ts @@ -29,7 +29,7 @@ import type { import type RouterService from '@ember/routing/router-service'; import EmberObject from '@ember/object'; import Evented from '@ember/object/evented'; -import { A as emberA } from '@ember/array'; +import { queryParamArray } from '@ember/-internals/routing/route-managers/classic/query-params'; import { assert, info } from '@ember/debug'; import { cancel, later, once, run } from '@ember/runloop'; import { associateDestroyableChild } from '@glimmer/destroyable'; @@ -1166,7 +1166,7 @@ class EmberRouter extends EmberObject { } else if (defaultType === 'number') { return Number(value).valueOf(); } else if (defaultType === 'array') { - return emberA(JSON.parse(value as string)); + return queryParamArray(JSON.parse(value as string)); } return value; } diff --git a/packages/ember/tests/routing/query_params_test.js b/packages/ember/tests/routing/query_params_test.js index 5b9c265cf2a..3d403186a2c 100644 --- a/packages/ember/tests/routing/query_params_test.js +++ b/packages/ember/tests/routing/query_params_test.js @@ -10,7 +10,17 @@ import { PARAMS_SYMBOL } from 'router_js'; import { service } from '@ember/service'; import { precompileTemplate } from '@ember/template-compilation'; -import { QueryParamTestCase, moduleFor, getTextOf, runLoopSettled } from 'internal-test-helpers'; +import { + QueryParamTestCase, + moduleFor, + getTextOf, + runLoopSettled, + expectDeprecation, + testUnless, +} from 'internal-test-helpers'; +import { DEPRECATIONS } from '@ember/-internals/deprecations'; + +const QP_EMBER_ARRAY = DEPRECATIONS.DEPRECATE_QUERY_PARAM_EMBER_ARRAY; moduleFor( 'Query Params - main', @@ -1314,8 +1324,16 @@ moduleFor( }); } - async ['@test Array query params can be pushed/popped'](assert) { - assert.expect(17); + async [`${testUnless(QP_EMBER_ARRAY.isRemoved)} @test Array query params can be pushed/popped`]( + assert + ) { + assert.expect(QP_EMBER_ARRAY.isEnabled ? 20 : 17); + + if (QP_EMBER_ARRAY.isEnabled) { + expectDeprecation(/`pushObject` was used on the array value of a query param/); + expectDeprecation(/`popObject` was used on the array value of a query param/); + expectDeprecation(/`unshiftObject` was used on the array value of a query param/); + } this.router.map(function () { this.route('home', { path: '/' });