diff --git a/packages/@ember/-internals/deprecations/index.ts b/packages/@ember/-internals/deprecations/index.ts index b8a83a7054d..e65c517ec35 100644 --- a/packages/@ember/-internals/deprecations/index.ts +++ b/packages/@ember/-internals/deprecations/index.ts @@ -168,6 +168,104 @@ 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_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', + 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..630deff238d 100644 --- a/packages/@ember/-internals/glimmer/lib/helpers/each-in.ts +++ b/packages/@ember/-internals/glimmer/lib/helpers/each-in.ts @@ -2,13 +2,14 @@ @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'; 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); 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 a2a12be40cc..767e2882815 100644 --- a/packages/@ember/-internals/glimmer/tests/integration/helpers/tracked-test.js +++ b/packages/@ember/-internals/glimmer/tests/integration/helpers/tracked-test.js @@ -3,12 +3,21 @@ import { A } from '@ember/array'; import MutableArray from '@ember/array/mutable'; import { tracked, notifyPropertyChange } from '@ember/-internals/metal'; 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'; import Component from '@glimmer/component'; +const EMBER_A = DEPRECATIONS.DEPRECATE_EMBER_ARRAY_A; + moduleFor( 'Helper Tracked Properties', class extends RenderingTestCase { @@ -140,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]); @@ -172,7 +183,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); @@ -388,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/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/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 bf05aa13c7d..fc4ff4d0e5c 100644 --- a/packages/@ember/-internals/runtime/tests/mixins/array_test.js +++ b/packages/@ember/-internals/runtime/tests/mixins/array_test.js @@ -8,7 +8,21 @@ import { } from '@ember/-internals/metal'; import EmberObject, { get, set, computed, observer as emberObserver } from '@ember/object'; import EmberArray, { A as emberA } from '@ember/array'; -import { moduleFor, AbstractTestCase, runLoopSettled } from 'internal-test-helpers'; +import { + expectDeprecation, + moduleFor, + AbstractTestCase, + runLoopSettled, + testUnless, +} from 'internal-test-helpers'; +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); +} /* Implement a basic fake mutable array. This validates that any non-native @@ -18,6 +32,7 @@ const TestArray = class extends EmberObject.extend(EmberArray) { _content = null; init() { + super.init(...arguments); this._content = this._content || []; } @@ -49,7 +64,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, }); @@ -57,7 +76,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)'); @@ -96,7 +117,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++; @@ -129,6 +152,8 @@ moduleFor( 'notify observers of length', class extends AbstractTestCase { beforeEach(assert) { + expectMixinDeprecation(); + obj = DummyArray.extend({ lengthDidChange: emberObserver('length', function () { this._after++; @@ -145,7 +170,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(); @@ -158,7 +185,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(); @@ -170,7 +199,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(); @@ -192,6 +223,8 @@ moduleFor( 'notify array observers (internal)', class extends AbstractTestCase { beforeEach(assert) { + expectMixinDeprecation(); + obj = DummyArray.create(); observer = class extends EmberObject { @@ -219,7 +252,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]); @@ -228,7 +263,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]); @@ -236,7 +273,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]); @@ -244,7 +283,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', @@ -268,6 +307,8 @@ moduleFor( 'EmberArray.@each support', class extends AbstractTestCase { beforeEach() { + expectMixinDeprecation(); + ary = TestArray.create({ _content: [ { isDone: true, desc: 'Todo 1' }, @@ -283,7 +324,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({ @@ -305,7 +348,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({ @@ -332,16 +377,25 @@ 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'); 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); @@ -361,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/container_proxy_test.js b/packages/@ember/-internals/runtime/tests/mixins/container_proxy_test.js index 1f9cbcfa175..a8f3b97eec9 100644 --- a/packages/@ember/-internals/runtime/tests/mixins/container_proxy_test.js +++ b/packages/@ember/-internals/runtime/tests/mixins/container_proxy_test.js @@ -3,13 +3,18 @@ import { Container, Registry } from '@ember/-internals/container'; 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/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/mixins/target_action_support_test.js b/packages/@ember/-internals/runtime/tests/mixins/target_action_support_test.js index 13fae5111ed..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; @@ -23,10 +27,7 @@ moduleFor( )} @test it should return false if no target or action are specified`](assert) { 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(); @@ -38,10 +39,7 @@ moduleFor( )} @test it should support actions specified as strings`](assert) { 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({ @@ -61,10 +59,7 @@ moduleFor( )} @test it should invoke the send() method on objects that implement it`](assert) { 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({ @@ -85,10 +80,7 @@ moduleFor( )} @test it should find targets specified using a property path`](assert) { 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; @@ -112,10 +104,7 @@ moduleFor( )} @test it should use an actionContext object specified as a property on the object`](assert) { 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', @@ -138,10 +127,7 @@ moduleFor( )} @test it should find an actionContext specified as a property path`](assert) { 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; @@ -165,10 +151,7 @@ moduleFor( )} @test it should use the target specified in the argument`](assert) { 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() { @@ -190,10 +173,7 @@ moduleFor( )} @test it should use the action specified in the argument`](assert) { 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({ @@ -214,10 +194,7 @@ moduleFor( )} @test it should use the actionContext specified in the argument`](assert) { 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({ @@ -240,10 +217,7 @@ moduleFor( )} @test it should allow multiple arguments from actionContext`](assert) { 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'; @@ -274,10 +248,7 @@ moduleFor( )} @test it should use a null value specified in the actionContext argument`](assert) { 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({ 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/-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..20c8f021f54 --- /dev/null +++ b/packages/@ember/array/index-internal.ts @@ -0,0 +1,674 @@ +/** +@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; + }, +}); + +// 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; + } +}; + +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 b7ab885ebc6..5119fce4778 100644 --- a/packages/@ember/array/index.ts +++ b/packages/@ember/array/index.ts @@ -1,159 +1,26 @@ /** @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 { assert } from '@ember/debug'; +import { deprecateUntil, DEPRECATIONS } from '@ember/-internals/deprecations'; +import InternalEmberArrayMixin, { + A as internalA, + 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 +1007,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 +1293,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,32 +1476,38 @@ interface MutableArrayWithoutNative extends Omit< */ interface NativeArray extends Array, Observable, MutableArrayWithoutNative {} -let NativeArray = Mixin[INTERNAL_MIXIN_CREATE](MutableArray, Observable, { - objectAt(idx: number) { - return this[idx]; +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 + ); }, +}); - // 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)); +/** + Creates an `NativeArray` from an Array-like object. + Does not modify the original object's contents. - replaceInNativeArray(this, start, deleteCount, items); + 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. - return this; - }, -}); + Example -// 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); - } -}); + ```javascript + import { A } from '@ember/array'; -NativeArray = NativeArray.without(...ignore); + 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) { @@ -2058,13 +1516,12 @@ A = function (this: unknown, arr?: Array) { !(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; - } + 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 }; 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/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/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/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/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 deprecateArrayMacro(name: string) { + deprecateUntil( + `\`${name}\` from \`@ember/object/computed\` is deprecated. Use a getter with native array methods instead.`, + DEPRECATIONS.DEPRECATE_ARRAY_COMPUTED_MACROS + ); } function reduceMacro( @@ -60,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[], @@ -103,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( @@ -110,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'); } @@ -167,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( @@ -174,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'); } @@ -230,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( @@ -237,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'); } @@ -314,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, @@ -334,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' || @@ -358,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); } /** @@ -411,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( @@ -418,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"', @@ -428,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)); } /** @@ -538,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, @@ -560,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' || @@ -581,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); } /** @@ -635,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( @@ -642,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) @@ -654,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); } /** @@ -692,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( @@ -699,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]); } /** @@ -761,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( @@ -768,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) @@ -823,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 @@ -860,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( @@ -867,6 +938,8 @@ export function intersect(dependentKey: string, ...additionalDependentKeys: stri !isDecoratorCall(Array.prototype.slice.call(arguments)) ); + deprecateArrayMacro('intersect'); + let args = [dependentKey, ...additionalDependentKeys]; return multiArrayMacro( @@ -949,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( @@ -956,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.`, @@ -1007,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( @@ -1014,6 +1091,8 @@ export function collect(dependentKey: string, ...additionalDependentKeys: string !isDecoratorCall(Array.prototype.slice.call(arguments)) ); + deprecateArrayMacro('collect'); + let dependentKeys = [dependentKey, ...additionalDependentKeys]; return multiArrayMacro( @@ -1174,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( @@ -1191,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/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..d4b6bb90eef 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 @@ -90,10 +76,11 @@ export type ObserverMethod = 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 */ -interface Observable { +interface Observable extends InternalObservable { /** Retrieves the value of a property from the object. @@ -421,125 +408,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/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/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..690b39ac525 100644 --- a/packages/@ember/object/tests/observable_test.js +++ b/packages/@ember/object/tests/observable_test.js @@ -2,9 +2,17 @@ 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'; +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 @@ -33,7 +41,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 +66,7 @@ moduleFor( 'object.get()', class extends ObservableTestCase { beforeEach() { - object = ObservableObject.extend(Observable, { + object = ObservableObject.extend({ computed: computed(function () { return 'value'; }), @@ -719,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/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/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: '/' }); diff --git a/packages/ember/tests/service_injection_test.js b/packages/ember/tests/service_injection_test.js index 1e6033f9e1d..f30de7e8507 100644 --- a/packages/ember/tests/service_injection_test.js +++ b/packages/ember/tests/service_injection_test.js @@ -2,7 +2,13 @@ import { getOwner } from '@ember/-internals/owner'; import Controller from '@ember/controller'; import Service, { service } from '@ember/service'; import { _ProxyMixin } from '@ember/-internals/runtime'; -import { moduleFor, ApplicationTestCase } from 'internal-test-helpers'; +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( 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) => {