From 405d42388472c30e3d7d98c59a000b7ebe4a37c0 Mon Sep 17 00:00:00 2001 From: Chris Garrett Date: Mon, 15 Apr 2019 15:15:51 -0700 Subject: [PATCH] [FEATURE] Removes Chains in Tracked Properties Flag This PR updates the tracked properties feature flag to fully remove chains. This was primarily is response to core issues we found with interop with computed properties, which required us to make computed properties and observers more lazy in general. At a high level, these are the major changes: * The `getCurrentTracker`/`setCurrentTracker` system was a leaky abstraction, since errors could cause a child tracker to never reset. In order to make it more bulletproof, it has been changed to `track`/`consume`/`isTracking`. We wrap any change to the stack of trackers with a `try/catch` to make sure we always clean up in `track`. `isTracking` is used only when we don't want to eagerly create a tag if there is no tracking context. * Observers have been made asynchronous. They now flush during specific phases of the runloop - just before render, and potentially after render if anything was scheduled (this will begin a new runloop). They poll now to check if any changes occured, rather than firing synchronously when the change occurs, which is how we made them work entirely using tags without chains. Observers inherited from EmberObject classes begin polling at the same time as `finalizeChains`. * Computed properties have been updated to only use chains when checking for dirtiness. Since computed properties were lazy before, this isn't much of a change overall. Computed properties also no longer autotrack, unless they have been marked by a (currently private) `_auto()` modifier. * Both observers and computeds accomplish this laziness by following and reading the tags of their dependencies _after calculation_. They entangle all dependencies at this point, _unless_ the dependency is an uncalculated computed property. If they encounter one of these, they setup _lazy chains_, which will be followed and updated the next time the computed property is calculated. This also makes aliases lazily observe. * Computed properties have also been updated to install a native setter, per the track props update RFC. * Query parameters use synchronous observers to update various things. They should really be refactored, but that's going to take a while. In the meantime, we flush the observers synchronously for them specifically. * A `UNKNOWN_PROPERTY_TAG` system has been added _privately and internally only_. This system allows proxies to return a special tag that invalidates when their _content's_ properties change. This system could be made more public in the future, but it is purposefully private for the time being. It was necessary to match existing semantics in many tests. * A new mandatory setter system has been added. This system is now one-way - once a value has been consumed, there is no way to remove the setter and "unconsume" it. This is the nature of tags being lazy and having no teardown. There were a few additional changes that were required as well: * `visit` and `transitionTo` for application tests had to be made async in order to work properly with observers. Most of the work occured in another PR, but some had to be finished up here. * Many, many tests needed to be updated. Most of these were for the fact that observers are now async, and required us to wait on the runloop settling. --- .../tests/data_adapter_test.js | 37 +- .../glimmer/lib/component-managers/custom.ts | 14 +- .../glimmer/lib/utils/references.ts | 45 +- .../integration/application/engine-test.js | 20 +- .../integration/application/rendering-test.js | 15 +- .../components/curly-components-test.js | 48 +- .../link-to/query-params-angle-test.js | 71 +- .../link-to/query-params-curly-test.js | 72 +- .../link-to/rendering-angle-test.js | 14 +- .../components/link-to/routing-angle-test.js | 4 +- .../components/link-to/routing-curly-test.js | 6 +- .../tests/integration/helpers/unbound-test.js | 18 +- .../glimmer/tests/integration/mount-test.js | 15 +- packages/@ember/-internals/meta/lib/meta.ts | 65 + packages/@ember/-internals/metal/index.ts | 13 +- packages/@ember/-internals/metal/lib/alias.ts | 45 +- .../-internals/metal/lib/array_events.ts | 9 +- .../@ember/-internals/metal/lib/chain-tags.ts | 128 ++ .../@ember/-internals/metal/lib/computed.ts | 112 +- .../-internals/metal/lib/computed_cache.ts | 14 +- .../@ember/-internals/metal/lib/decorator.ts | 22 +- packages/@ember/-internals/metal/lib/mixin.ts | 9 +- .../@ember/-internals/metal/lib/observer.ts | 148 +- .../@ember/-internals/metal/lib/properties.ts | 8 +- .../-internals/metal/lib/property_events.ts | 28 +- .../-internals/metal/lib/property_get.ts | 13 +- .../-internals/metal/lib/property_set.ts | 20 +- packages/@ember/-internals/metal/lib/tags.ts | 24 +- .../@ember/-internals/metal/lib/tracked.ts | 38 +- .../@ember/-internals/metal/lib/watch_key.ts | 7 +- .../tests/accessors/mandatory_setters_test.js | 3 +- .../-internals/metal/tests/alias_test.js | 122 +- .../-internals/metal/tests/chains_test.js | 337 ++--- .../-internals/metal/tests/computed_test.js | 195 +-- .../metal/tests/mixin/observer_test.js | 53 +- .../-internals/metal/tests/observer_test.js | 346 +++-- .../metal/tests/performance_test.js | 6 +- .../tests/tracked/classic_classes_test.js | 4 +- .../-internals/metal/tests/tracked/support.js | 17 - .../metal/tests/tracked/validation_test.js | 4 +- .../metal/tests/watching/is_watching_test.js | 139 +- .../metal/tests/watching/unwatch_test.js | 199 +-- .../metal/tests/watching/watch_test.js | 449 +++--- .../-internals/routing/lib/ext/controller.ts | 3 +- .../-internals/routing/lib/system/route.ts | 20 +- .../-internals/runtime/lib/mixins/-proxy.js | 19 +- .../runtime/lib/system/array_proxy.js | 71 +- .../runtime/lib/system/core_object.js | 19 +- .../runtime/tests/ext/function_test.js | 12 +- .../mixins/observable/chained_test.js | 24 +- .../mixins/observable/observable_test.js | 42 +- .../mixins/observable/propertyChanges_test.js | 231 ++-- .../runtime/tests/mixins/array_test.js | 54 +- .../runtime/tests/mixins/observable_test.js | 6 +- .../tests/mutable-array/addObject-test.js | 12 +- .../runtime/tests/mutable-array/clear-test.js | 12 +- .../tests/mutable-array/insertAt-test.js | 32 +- .../tests/mutable-array/popObject-test.js | 17 +- .../tests/mutable-array/pushObject-test.js | 17 +- .../tests/mutable-array/removeAt-test.js | 32 +- .../tests/mutable-array/removeObject-test.js | 12 +- .../tests/mutable-array/removeObjects-test.js | 37 +- .../tests/mutable-array/replace-test.js | 42 +- .../mutable-array/reverseObjects-test.js | 7 +- .../tests/mutable-array/setObjects-test.js | 12 +- .../tests/mutable-array/shiftObject-test.js | 17 +- .../tests/mutable-array/unshiftObject-test.js | 17 +- .../mutable-array/unshiftObjects-test.js | 17 +- .../tests/system/array_proxy/length_test.js | 10 +- .../watching_and_listening_test.js | 6 + .../runtime/tests/system/core_object_test.js | 5 +- .../tests/system/object/destroy_test.js | 29 +- .../system/object/es-compatibility-test.js | 74 +- .../tests/system/object/extend_test.js | 6 +- .../tests/system/object/observer_test.js | 40 +- .../runtime/tests/system/object_proxy_test.js | 41 +- packages/@ember/-internals/utils/index.ts | 5 + .../-internals/utils/lib/mandatory-setter.ts | 126 ++ .../views/lib/views/states/in_dom.js | 21 +- .../lib/computed/reduce_computed_macros.js | 111 +- .../computed/reduce_computed_macros_test.js | 8 +- packages/@ember/runloop/index.js | 19 + .../tests/routing/decoupled_basic_test.js | 27 +- .../ember/tests/routing/query_params_test.js | 573 ++++---- ..._dependent_state_with_query_params_test.js | 1205 ++++++++--------- .../overlapping_query_params_test.js | 146 +- .../router_service_test/events_test.js | 36 +- .../ember/tests/routing/substates_test.js | 27 +- .../lib/ember-dev/setup-qunit.ts | 33 +- .../lib/test-cases/abstract-application.js | 4 +- .../lib/test-cases/application.js | 10 +- .../lib/test-cases/query-param.js | 12 +- 92 files changed, 3769 insertions(+), 2545 deletions(-) create mode 100644 packages/@ember/-internals/metal/lib/chain-tags.ts delete mode 100644 packages/@ember/-internals/metal/tests/tracked/support.js create mode 100644 packages/@ember/-internals/utils/lib/mandatory-setter.ts diff --git a/packages/@ember/-internals/extension-support/tests/data_adapter_test.js b/packages/@ember/-internals/extension-support/tests/data_adapter_test.js index 311fce5028f..555a3f36804 100644 --- a/packages/@ember/-internals/extension-support/tests/data_adapter_test.js +++ b/packages/@ember/-internals/extension-support/tests/data_adapter_test.js @@ -2,7 +2,7 @@ import { run } from '@ember/runloop'; import { get, set, addObserver, removeObserver } from '@ember/-internals/metal'; import { Object as EmberObject, A as emberA } from '@ember/-internals/runtime'; import EmberDataAdapter from '../lib/data_adapter'; -import { moduleFor, ApplicationTestCase } from 'internal-test-helpers'; +import { moduleFor, ApplicationTestCase, runLoopSettled } from 'internal-test-helpers'; let adapter; const Model = EmberObject.extend(); @@ -267,23 +267,30 @@ moduleFor( ); this.add('model:post', PostClass); - return this.visit('/').then(() => { - adapter = this.applicationInstance.lookup('data-adapter:main'); + let release; - function recordsAdded() { - set(post, 'title', 'Post Modified'); - } + return this.visit('/') + .then(() => { + adapter = this.applicationInstance.lookup('data-adapter:main'); - function recordsUpdated(records) { - updatesCalled++; - assert.equal(records[0].columnValues.title, 'Post Modified'); - } + function recordsAdded() { + set(post, 'title', 'Post Modified'); + } - let release = adapter.watchRecords('post', recordsAdded, recordsUpdated); - release(); - set(post, 'title', 'New Title'); - assert.equal(updatesCalled, 1, 'Release function removes observers'); - }); + function recordsUpdated(records) { + updatesCalled++; + assert.equal(records[0].columnValues.title, 'Post Modified'); + } + + release = adapter.watchRecords('post', recordsAdded, recordsUpdated); + + return runLoopSettled(); + }) + .then(() => { + release(); + set(post, 'title', 'New Title'); + assert.equal(updatesCalled, 1, 'Release function removes observers'); + }); } ['@test _nameToClass does not error when not found'](assert) { diff --git a/packages/@ember/-internals/glimmer/lib/component-managers/custom.ts b/packages/@ember/-internals/glimmer/lib/component-managers/custom.ts index ec13c77e33f..b79966d8c6c 100644 --- a/packages/@ember/-internals/glimmer/lib/component-managers/custom.ts +++ b/packages/@ember/-internals/glimmer/lib/component-managers/custom.ts @@ -1,4 +1,4 @@ -import { getCurrentTracker } from '@ember/-internals/metal'; +import { consume } from '@ember/-internals/metal'; import { Factory } from '@ember/-internals/owner'; import { HAS_NATIVE_PROXY } from '@ember/-internals/utils'; import { OwnedTemplateMeta } from '@ember/-internals/views'; @@ -171,12 +171,8 @@ export default class CustomComponentManager get(_target, prop) { assert('args can only be strings', typeof prop === 'string'); - let tracker = getCurrentTracker(); let ref = capturedArgs.named.get(prop as string); - - if (tracker) { - tracker.add(ref.tag); - } + consume(ref.tag); return ref.value(); }, @@ -200,11 +196,7 @@ export default class CustomComponentManager Object.defineProperty(namedArgsProxy, name, { get() { let ref = capturedArgs.named.get(name); - let tracker = getCurrentTracker(); - - if (tracker) { - tracker.add(ref.tag); - } + consume(ref.tag); return ref.value(); }, diff --git a/packages/@ember/-internals/glimmer/lib/utils/references.ts b/packages/@ember/-internals/glimmer/lib/utils/references.ts index 582b0fdf9d1..524bcdeafa8 100644 --- a/packages/@ember/-internals/glimmer/lib/utils/references.ts +++ b/packages/@ember/-internals/glimmer/lib/utils/references.ts @@ -1,12 +1,11 @@ import { + consume, didRender, get, - getCurrentTracker, set, - setCurrentTracker, tagFor, tagForProperty, - Tracker, + track, watchKey, } from '@ember/-internals/metal'; import { isProxy, symbol } from '@ember/-internals/utils'; @@ -190,7 +189,7 @@ export class RootPropertyReference extends PropertyReference this.tag = this.propertyTag; } - if (DEBUG) { + if (DEBUG && !EMBER_METAL_TRACKED_PROPERTIES) { watchKey(parentValue, propertyKey); } } @@ -202,22 +201,17 @@ export class RootPropertyReference extends PropertyReference (this.tag.inner as ITwoWayFlushDetectionTag).didCompute(parentValue); } - let parent: Option = null; - let tracker: Option = null; - - if (EMBER_METAL_TRACKED_PROPERTIES) { - parent = getCurrentTracker(); - tracker = setCurrentTracker(); - } - - let ret = get(parentValue, propertyKey); + let ret; if (EMBER_METAL_TRACKED_PROPERTIES) { - setCurrentTracker(parent); - let tag = tracker!.combine(); - if (parent) parent.add(tag); + let tag = track(() => { + ret = get(parentValue, propertyKey); + }); + consume(tag); this.propertyTag.inner.update(tag); + } else { + ret = get(parentValue, propertyKey); } return ret; @@ -262,7 +256,7 @@ export class NestedPropertyReference extends PropertyReference { if ((parentValueType === 'object' && _parentValue !== null) || parentValueType === 'function') { let parentValue = _parentValue as object; - if (DEBUG) { + if (DEBUG && !EMBER_METAL_TRACKED_PROPERTIES) { watchKey(parentValue, propertyKey); } @@ -270,23 +264,18 @@ export class NestedPropertyReference extends PropertyReference { (this.tag.inner as ITwoWayFlushDetectionTag).didCompute(parentValue); } - let parent: Option = null; - let tracker: Option = null; + let ret; if (EMBER_METAL_TRACKED_PROPERTIES) { - parent = getCurrentTracker(); - tracker = setCurrentTracker(); - } + let tag = track(() => { + ret = get(parentValue, propertyKey); + }); - let ret = get(parentValue, propertyKey); - - if (EMBER_METAL_TRACKED_PROPERTIES) { - setCurrentTracker(parent!); - let tag = tracker!.combine(); - if (parent) parent.add(tag); + consume(tag); propertyTag.inner.update(tag); } else { + ret = get(parentValue, propertyKey); propertyTag.inner.update(tagForProperty(parentValue, propertyKey)); } diff --git a/packages/@ember/-internals/glimmer/tests/integration/application/engine-test.js b/packages/@ember/-internals/glimmer/tests/integration/application/engine-test.js index 5afce6dc3df..322e0f27109 100644 --- a/packages/@ember/-internals/glimmer/tests/integration/application/engine-test.js +++ b/packages/@ember/-internals/glimmer/tests/integration/application/engine-test.js @@ -856,7 +856,7 @@ moduleFor( }); } - ['@test query params in customized controllerName have stickiness by default between model']( + async ['@test query params in customized controllerName have stickiness by default between model']( assert ) { assert.expect(2); @@ -867,16 +867,16 @@ moduleFor( this.register('template:author', compile(tmpl)); }); - return this.visit('/blog/author/1?official=true').then(() => { - let suffix1 = '/blog/author/1?official=true'; - let href1 = this.element.querySelector('.author-1').href; - let suffix1337 = '/blog/author/1337'; - let href1337 = this.element.querySelector('.author-1337').href; + await this.visit('/blog/author/1?official=true'); - // check if link ends with the suffix - assert.ok(this.stringsEndWith(href1, suffix1), `${href1} ends with ${suffix1}`); - assert.ok(this.stringsEndWith(href1337, suffix1337), `${href1337} ends with ${suffix1337}`); - }); + let suffix1 = '/blog/author/1?official=true'; + let href1 = this.element.querySelector('.author-1').href; + let suffix1337 = '/blog/author/1337'; + let href1337 = this.element.querySelector('.author-1337').href; + + // check if link ends with the suffix + assert.ok(this.stringsEndWith(href1, suffix1), `${href1} ends with ${suffix1}`); + assert.ok(this.stringsEndWith(href1337, suffix1337), `${href1337} ends with ${suffix1337}`); } ['@test visit() routable engine which errors on init'](assert) { diff --git a/packages/@ember/-internals/glimmer/tests/integration/application/rendering-test.js b/packages/@ember/-internals/glimmer/tests/integration/application/rendering-test.js index 1121c1c4449..8ab713853da 100644 --- a/packages/@ember/-internals/glimmer/tests/integration/application/rendering-test.js +++ b/packages/@ember/-internals/glimmer/tests/integration/application/rendering-test.js @@ -1,4 +1,10 @@ -import { moduleFor, ApplicationTestCase, strip } from 'internal-test-helpers'; +import { + moduleFor, + ApplicationTestCase, + strip, + runTask, + runLoopSettled, +} from 'internal-test-helpers'; import { ENV } from '@ember/-internals/environment'; import Controller from '@ember/controller'; @@ -499,7 +505,12 @@ moduleFor( await this.visit('/'); - assert.rejectsAssertion(this.visit('/routeWithError'), expectedBacktrackingMessage); + assert.throwsAssertion( + () => runTask(() => this.visit('/routeWithError')), + expectedBacktrackingMessage + ); + + await runLoopSettled(); } ['@test route templates with {{{undefined}}} [GH#14924] [GH#16172]']() { diff --git a/packages/@ember/-internals/glimmer/tests/integration/components/curly-components-test.js b/packages/@ember/-internals/glimmer/tests/integration/components/curly-components-test.js index 86ce77a5976..50db70bf830 100644 --- a/packages/@ember/-internals/glimmer/tests/integration/components/curly-components-test.js +++ b/packages/@ember/-internals/glimmer/tests/integration/components/curly-components-test.js @@ -9,11 +9,13 @@ import { equalsElement, styles, runTask, + runLoopSettled, } from 'internal-test-helpers'; import { run } from '@ember/runloop'; import { DEBUG } from '@glimmer/env'; import { alias, set, get, observer, on, computed } from '@ember/-internals/metal'; +import { EMBER_METAL_TRACKED_PROPERTIES } from '@ember/canary-features'; import Service, { inject as injectService } from '@ember/service'; import { Object as EmberObject, A as emberA } from '@ember/-internals/runtime'; import { jQueryDisabled } from '@ember/-internals/views'; @@ -2526,15 +2528,11 @@ moduleFor( ); } - ["@test when a property is changed during children's rendering"](assert) { - let outer, middle; + ["@test when a property is changed during children's rendering"]() { + let middle; this.registerComponent('x-outer', { ComponentClass: Component.extend({ - init() { - this._super(...arguments); - outer = this; - }, value: 1, }), template: '{{#x-middle}}{{x-inner value=value}}{{/x-middle}}', @@ -2554,35 +2552,17 @@ moduleFor( this.registerComponent('x-inner', { ComponentClass: Component.extend({ value: null, - pushDataUp: observer('value', function() { + didReceiveAttrs() { middle.set('value', this.get('value')); - }), + }, }), template: '
{{value}}
', }); - this.render('{{x-outer}}'); - - assert.equal(this.$('#inner-value').text(), '1', 'initial render of inner'); - assert.equal( - this.$('#middle-value').text(), - '', - 'initial render of middle (observers do not run during init)' - ); - - runTask(() => this.rerender()); - - assert.equal(this.$('#inner-value').text(), '1', 'initial render of inner'); - assert.equal( - this.$('#middle-value').text(), - '', - 'initial render of middle (observers do not run during init)' - ); - let expectedBacktrackingMessage = /modified "value" twice on <.+?> in a single render\. It was rendered in "component:x-middle" and modified in "component:x-inner"/; expectAssertion(() => { - runTask(() => outer.set('value', 2)); + this.render('{{x-outer}}'); }, expectedBacktrackingMessage); } @@ -2741,9 +2721,13 @@ moduleFor( this.assertText('initial value - initial value'); if (DEBUG) { + let message = EMBER_METAL_TRACKED_PROPERTIES + ? /You attempted to update .*, but it is being tracked by a tracking context/ + : /You must use set\(\) to set the `bar` property \(of .+\) to `foo-bar`\./; + expectAssertion(() => { component.bar = 'foo-bar'; - }, /You must use set\(\) to set the `bar` property \(of .+\) to `foo-bar`\./); + }, message); this.assertText('initial value - initial value'); } @@ -3208,7 +3192,7 @@ moduleFor( this.assertText('things'); } - ['@test didReceiveAttrs fires after .init() but before observers become active'](assert) { + async ['@test didReceiveAttrs fires after .init() but before observers become active'](assert) { let barCopyDidChangeCount = 0; this.registerComponent('foo-bar', { @@ -3231,13 +3215,15 @@ moduleFor( template: '{{bar}}-{{barCopy}}', }); - this.render(`{{foo-bar bar=bar}}`, { bar: 3 }); + await this.render(`{{foo-bar bar=bar}}`, { bar: 3 }); this.assertText('3-4'); assert.strictEqual(barCopyDidChangeCount, 1, 'expected observer firing for: barCopy'); - runTask(() => set(this.context, 'bar', 7)); + set(this.context, 'bar', 7); + + await runLoopSettled(); this.assertText('7-8'); diff --git a/packages/@ember/-internals/glimmer/tests/integration/components/link-to/query-params-angle-test.js b/packages/@ember/-internals/glimmer/tests/integration/components/link-to/query-params-angle-test.js index 36c22206b0e..edc83c633f7 100644 --- a/packages/@ember/-internals/glimmer/tests/integration/components/link-to/query-params-angle-test.js +++ b/packages/@ember/-internals/glimmer/tests/integration/components/link-to/query-params-angle-test.js @@ -7,6 +7,7 @@ import { classes as classMatcher, moduleFor, runTask, + runLoopSettled, } from 'internal-test-helpers'; if (EMBER_GLIMMER_ANGLE_BRACKET_BUILT_INS) { @@ -290,7 +291,7 @@ if (EMBER_GLIMMER_ANGLE_BRACKET_BUILT_INS) { ); }); } - ['@test href updates when unsupplied controller QP props change'](assert) { + async ['@test href updates when unsupplied controller QP props change'](assert) { this.addTemplate( 'index', ` @@ -300,20 +301,22 @@ if (EMBER_GLIMMER_ANGLE_BRACKET_BUILT_INS) { ` ); - return this.visit('/').then(() => { - let indexController = this.getController('index'); - let theLink = this.$('#the-link'); + await this.visit('/'); - assert.equal(theLink.attr('href'), '/?foo=lol'); + let indexController = this.getController('index'); + let theLink = this.$('#the-link'); - runTask(() => indexController.set('bar', 'BORF')); + assert.equal(theLink.attr('href'), '/?foo=lol'); - assert.equal(theLink.attr('href'), '/?bar=BORF&foo=lol'); + indexController.set('bar', 'BORF'); + await runLoopSettled(); - runTask(() => indexController.set('foo', 'YEAH')); + assert.equal(theLink.attr('href'), '/?bar=BORF&foo=lol'); - assert.equal(theLink.attr('href'), '/?bar=BORF&foo=lol'); - }); + indexController.set('foo', 'YEAH'); + await runLoopSettled(); + + assert.equal(theLink.attr('href'), '/?bar=BORF&foo=lol'); } ['@test The component with only query params always transitions to the current route with the query params applied']( @@ -590,7 +593,7 @@ if (EMBER_GLIMMER_ANGLE_BRACKET_BUILT_INS) { }); } - ['@test The component disregards query-params in activeness computation when current-when is specified']( + async ['@test The component disregards query-params in activeness computation when current-when is specified']( assert ) { let appLink; @@ -627,38 +630,38 @@ if (EMBER_GLIMMER_ANGLE_BRACKET_BUILT_INS) { }) ); - return this.visit('/') - .then(() => { - appLink = this.$('#app-link'); + await this.visit('/'); - assert.equal(appLink.attr('href'), '/parent'); - this.shouldNotBeActive(assert, '#app-link'); + appLink = this.$('#app-link'); - return this.visit('/parent?page=2'); - }) - .then(() => { - appLink = this.$('#app-link'); - let router = this.appRouter; + assert.equal(appLink.attr('href'), '/parent'); + this.shouldNotBeActive(assert, '#app-link'); - assert.equal(appLink.attr('href'), '/parent'); - this.shouldBeActive(assert, '#app-link'); - assert.equal(this.$('#parent-link').attr('href'), '/parent'); - this.shouldBeActive(assert, '#parent-link'); + await this.visit('/parent?page=2'); - let parentController = this.getController('parent'); + appLink = this.$('#app-link'); + let router = this.appRouter; - assert.equal(parentController.get('page'), 2); + assert.equal(appLink.attr('href'), '/parent'); + this.shouldBeActive(assert, '#app-link'); + assert.equal(this.$('#parent-link').attr('href'), '/parent'); + this.shouldBeActive(assert, '#parent-link'); - runTask(() => parentController.set('page', 3)); + let parentController = this.getController('parent'); - assert.equal(router.get('location.path'), '/parent?page=3'); - this.shouldBeActive(assert, '#app-link'); - this.shouldBeActive(assert, '#parent-link'); + assert.equal(parentController.get('page'), 2); - runTask(() => this.click('#app-link')); + parentController.set('page', 3); + await runLoopSettled(); - assert.equal(router.get('location.path'), '/parent'); - }); + assert.equal(router.get('location.path'), '/parent?page=3'); + this.shouldBeActive(assert, '#app-link'); + this.shouldBeActive(assert, '#parent-link'); + + this.click('#app-link'); + await runLoopSettled(); + + assert.equal(router.get('location.path'), '/parent'); } ['@test the component default query params while in active transition regression test']( diff --git a/packages/@ember/-internals/glimmer/tests/integration/components/link-to/query-params-curly-test.js b/packages/@ember/-internals/glimmer/tests/integration/components/link-to/query-params-curly-test.js index 0a366a7a447..8b84cd81af2 100644 --- a/packages/@ember/-internals/glimmer/tests/integration/components/link-to/query-params-curly-test.js +++ b/packages/@ember/-internals/glimmer/tests/integration/components/link-to/query-params-curly-test.js @@ -7,6 +7,7 @@ import { classes as classMatcher, moduleFor, runTask, + runLoopSettled, } from 'internal-test-helpers'; moduleFor( @@ -300,26 +301,29 @@ moduleFor( ); }); } - ['@test href updates when unsupplied controller QP props change'](assert) { + + async ['@test href updates when unsupplied controller QP props change'](assert) { this.addTemplate( 'index', `{{#link-to (query-params foo='lol') id='the-link'}}Index{{/link-to}}` ); - return this.visit('/').then(() => { - let indexController = this.getController('index'); - let theLink = this.$('#the-link'); + await this.visit('/'); - assert.equal(theLink.attr('href'), '/?foo=lol'); + let indexController = this.getController('index'); + let theLink = this.$('#the-link'); - runTask(() => indexController.set('bar', 'BORF')); + assert.equal(theLink.attr('href'), '/?foo=lol'); - assert.equal(theLink.attr('href'), '/?bar=BORF&foo=lol'); + indexController.set('bar', 'BORF'); + await runLoopSettled(); - runTask(() => indexController.set('foo', 'YEAH')); + assert.equal(theLink.attr('href'), '/?bar=BORF&foo=lol'); - assert.equal(theLink.attr('href'), '/?bar=BORF&foo=lol'); - }); + indexController.set('foo', 'YEAH'); + await runLoopSettled(); + + assert.equal(theLink.attr('href'), '/?bar=BORF&foo=lol'); } ['@test The {{link-to}} with only query params always transitions to the current route with the query params applied']( @@ -594,7 +598,7 @@ moduleFor( }); } - ['@test The {{link-to}} component disregards query-params in activeness computation when current-when is specified']( + async ['@test The {{link-to}} component disregards query-params in activeness computation when current-when is specified']( assert ) { let appLink; @@ -631,38 +635,38 @@ moduleFor( }) ); - return this.visit('/') - .then(() => { - appLink = this.$('#app-link'); + await this.visit('/'); - assert.equal(appLink.attr('href'), '/parent'); - this.shouldNotBeActive(assert, '#app-link'); + appLink = this.$('#app-link'); - return this.visit('/parent?page=2'); - }) - .then(() => { - appLink = this.$('#app-link'); - let router = this.appRouter; + assert.equal(appLink.attr('href'), '/parent'); + this.shouldNotBeActive(assert, '#app-link'); - assert.equal(appLink.attr('href'), '/parent'); - this.shouldBeActive(assert, '#app-link'); - assert.equal(this.$('#parent-link').attr('href'), '/parent'); - this.shouldBeActive(assert, '#parent-link'); + await this.visit('/parent?page=2'); - let parentController = this.getController('parent'); + appLink = this.$('#app-link'); + let router = this.appRouter; - assert.equal(parentController.get('page'), 2); + assert.equal(appLink.attr('href'), '/parent'); + this.shouldBeActive(assert, '#app-link'); + assert.equal(this.$('#parent-link').attr('href'), '/parent'); + this.shouldBeActive(assert, '#parent-link'); - runTask(() => parentController.set('page', 3)); + let parentController = this.getController('parent'); - assert.equal(router.get('location.path'), '/parent?page=3'); - this.shouldBeActive(assert, '#app-link'); - this.shouldBeActive(assert, '#parent-link'); + assert.equal(parentController.get('page'), 2); - runTask(() => this.click('#app-link')); + parentController.set('page', 3); + await runLoopSettled(); - assert.equal(router.get('location.path'), '/parent'); - }); + assert.equal(router.get('location.path'), '/parent?page=3'); + this.shouldBeActive(assert, '#app-link'); + this.shouldBeActive(assert, '#parent-link'); + + this.click('#app-link'); + await runLoopSettled(); + + assert.equal(router.get('location.path'), '/parent'); } ['@test {{link-to}} default query params while in active transition regression test'](assert) { diff --git a/packages/@ember/-internals/glimmer/tests/integration/components/link-to/rendering-angle-test.js b/packages/@ember/-internals/glimmer/tests/integration/components/link-to/rendering-angle-test.js index ab642cac0d0..b64b15f7d9d 100644 --- a/packages/@ember/-internals/glimmer/tests/integration/components/link-to/rendering-angle-test.js +++ b/packages/@ember/-internals/glimmer/tests/integration/components/link-to/rendering-angle-test.js @@ -1,4 +1,10 @@ -import { moduleFor, ApplicationTestCase, RenderingTestCase, runTask } from 'internal-test-helpers'; +import { + moduleFor, + ApplicationTestCase, + RenderingTestCase, + runTask, + runLoopSettled, +} from 'internal-test-helpers'; import { EMBER_GLIMMER_ANGLE_BRACKET_BUILT_INS } from '@ember/canary-features'; import Controller from '@ember/controller'; @@ -12,10 +18,12 @@ if (EMBER_GLIMMER_ANGLE_BRACKET_BUILT_INS) { async [`@test throws a useful error if you invoke it wrong`](assert) { this.addTemplate('application', `Index`); - await assert.rejectsAssertion( - this.visit('/'), + assert.throwsAssertion( + () => runTask(() => this.visit('/')), /You must provide at least one of the `@route`, `@model`, `@models` or `@query` argument to ``/ ); + + await runLoopSettled(); } ['@test should be able to be inserted in DOM when the router is not present']() { diff --git a/packages/@ember/-internals/glimmer/tests/integration/components/link-to/routing-angle-test.js b/packages/@ember/-internals/glimmer/tests/integration/components/link-to/routing-angle-test.js index 56dff76011e..e8a8bc11c5d 100644 --- a/packages/@ember/-internals/glimmer/tests/integration/components/link-to/routing-angle-test.js +++ b/packages/@ember/-internals/glimmer/tests/integration/components/link-to/routing-angle-test.js @@ -1447,8 +1447,8 @@ if (EMBER_GLIMMER_ANGLE_BRACKET_BUILT_INS) { this.addTemplate('application', `Post`); - await assert.rejects( - this.visit('/'), + await assert.throws( + () => runTask(() => this.visit('/')), /(You attempted to generate a link for the "post" route, but did not pass the models required for generating its dynamic segments.|You must provide param `post_id` to `generate`)/ ); } diff --git a/packages/@ember/-internals/glimmer/tests/integration/components/link-to/routing-curly-test.js b/packages/@ember/-internals/glimmer/tests/integration/components/link-to/routing-curly-test.js index 269cc39b875..9ef650e1fba 100644 --- a/packages/@ember/-internals/glimmer/tests/integration/components/link-to/routing-curly-test.js +++ b/packages/@ember/-internals/glimmer/tests/integration/components/link-to/routing-curly-test.js @@ -1665,10 +1665,12 @@ moduleFor( this.addTemplate('application', `{{#link-to 'post'}}Post{{/link-to}}`); - await assert.rejects( - this.visit('/'), + assert.throws( + () => runTask(() => this.visit('/')), /(You attempted to define a `\{\{link-to "post"\}\}` but did not pass the parameters required for generating its dynamic segments.|You must provide param `post_id` to `generate`)/ ); + + await runLoopSettled(); } [`@test the {{link-to}} component does not throw an error if its route has exited`](assert) { diff --git a/packages/@ember/-internals/glimmer/tests/integration/helpers/unbound-test.js b/packages/@ember/-internals/glimmer/tests/integration/helpers/unbound-test.js index ccb890d6133..04ddd14f4c3 100644 --- a/packages/@ember/-internals/glimmer/tests/integration/helpers/unbound-test.js +++ b/packages/@ember/-internals/glimmer/tests/integration/helpers/unbound-test.js @@ -1,4 +1,10 @@ -import { RenderingTestCase, moduleFor, strip, runTask } from 'internal-test-helpers'; +import { + RenderingTestCase, + moduleFor, + strip, + runTask, + runLoopSettled, +} from 'internal-test-helpers'; import { set, get, setProperties } from '@ember/-internals/metal'; import { A as emberA } from '@ember/-internals/runtime'; @@ -360,7 +366,7 @@ moduleFor( this.assertText('abc abc'); } - ['@test should be able to render an unbound helper invocation for helpers with dependent keys']() { + async ['@test should be able to render an unbound helper invocation for helpers with dependent keys']() { this.registerHelper('capitalizeName', { destroy() { this.removeObserver('value.firstName', this, this.recompute); @@ -410,10 +416,12 @@ moduleFor( this.assertText('SHOOBY SHOOBY shoobytaylor shoobytaylor'); runTask(() => this.rerender()); + await runLoopSettled(); this.assertText('SHOOBY SHOOBY shoobytaylor shoobytaylor'); runTask(() => set(this.context, 'person.firstName', 'sally')); + await runLoopSettled(); this.assertText('SALLY SHOOBY sallytaylor shoobytaylor'); @@ -423,6 +431,7 @@ moduleFor( lastName: 'taylor', }) ); + await runLoopSettled(); this.assertText('SHOOBY SHOOBY shoobytaylor shoobytaylor'); } @@ -476,7 +485,7 @@ moduleFor( this.assertText('SHOOBY SHOOBYCINDY CINDY'); } - ['@test should be able to render an unbound helper invocation with bound hash options']() { + async ['@test should be able to render an unbound helper invocation with bound hash options']() { this.registerHelper('capitalizeName', { destroy() { this.removeObserver('value.firstName', this, this.recompute); @@ -522,14 +531,17 @@ moduleFor( }, } ); + await runLoopSettled(); this.assertText('SHOOBY SHOOBY shoobytaylor shoobytaylor'); runTask(() => this.rerender()); + await runLoopSettled(); this.assertText('SHOOBY SHOOBY shoobytaylor shoobytaylor'); runTask(() => set(this.context, 'person.firstName', 'sally')); + await runLoopSettled(); this.assertText('SALLY SHOOBY sallytaylor shoobytaylor'); diff --git a/packages/@ember/-internals/glimmer/tests/integration/mount-test.js b/packages/@ember/-internals/glimmer/tests/integration/mount-test.js index 5b851bfdec4..c6fceb400d4 100644 --- a/packages/@ember/-internals/glimmer/tests/integration/mount-test.js +++ b/packages/@ember/-internals/glimmer/tests/integration/mount-test.js @@ -1,4 +1,10 @@ -import { moduleFor, ApplicationTestCase, RenderingTestCase, runTask } from 'internal-test-helpers'; +import { + moduleFor, + ApplicationTestCase, + RenderingTestCase, + runTask, + runLoopSettled, +} from 'internal-test-helpers'; import { getOwner } from '@ember/-internals/owner'; import { compile, Component } from '../utils/helpers'; @@ -134,7 +140,12 @@ moduleFor( await this.visit('/'); - await assert.rejectsAssertion(this.visit('/route-with-mount'), expectedBacktrackingMessage); + assert.throwsAssertion( + () => runTask(() => this.visit('/route-with-mount')), + expectedBacktrackingMessage + ); + + await runLoopSettled(); } ['@test it renders with a bound engine name']() { diff --git a/packages/@ember/-internals/meta/lib/meta.ts b/packages/@ember/-internals/meta/lib/meta.ts index ece69a5de18..3d54fd98906 100644 --- a/packages/@ember/-internals/meta/lib/meta.ts +++ b/packages/@ember/-internals/meta/lib/meta.ts @@ -13,6 +13,7 @@ export interface MetaCounters { metaCalls: number; metaInstantiated: number; matchingListenersCalls: number; + observerEventsCalls: number; addToListenersCalls: number; removeFromListenersCalls: number; removeAllListenersCalls: number; @@ -21,6 +22,8 @@ export interface MetaCounters { parentListenersUsed: number; flattenedListenersCalls: number; reopensAfterFlatten: number; + readableLazyChainsCalls: number; + writableLazyChainsCalls: number; } let counters: MetaCounters | undefined; @@ -33,6 +36,7 @@ if (DEBUG) { metaCalls: 0, metaInstantiated: 0, matchingListenersCalls: 0, + observerEventsCalls: 0, addToListenersCalls: 0, removeFromListenersCalls: 0, removeAllListenersCalls: 0, @@ -41,6 +45,8 @@ if (DEBUG) { parentListenersUsed: 0, flattenedListenersCalls: 0, reopensAfterFlatten: 0, + readableLazyChainsCalls: 0, + writableLazyChainsCalls: 0, }; } @@ -93,6 +99,7 @@ export class Meta { _tag: Tag | undefined; _tags: any | undefined; _flags: MetaFlags; + _lazyChains: Map> | undefined; source: object; proto: object | undefined; _parent: Meta | undefined | null; @@ -351,6 +358,32 @@ export class Meta { return this._tag; } + writableLazyChainsFor(key: string) { + if (DEBUG) { + counters!.writableLazyChainsCalls++; + } + + let lazyChains = this._getOrCreateOwnMap('_lazyChains'); + + if (!(key in lazyChains)) { + lazyChains[key] = []; + } + + return lazyChains[key]; + } + + readableLazyChainsFor(key: string) { + if (DEBUG) { + counters!.readableLazyChainsCalls++; + } + + let lazyChains = this._lazyChains; + + if (lazyChains !== undefined) { + return lazyChains[key]; + } + } + writableChainWatchers(create: (source: object) => any) { assert( this.isMetaDestroyed() @@ -703,6 +736,38 @@ export class Meta { return result; } + + observerEvents() { + let listeners = this.flattenedListeners(); + let result; + + if (DEBUG) { + counters!.observerEventsCalls++; + } + + if (listeners !== undefined) { + for (let index = 0; index < listeners.length; index++) { + let listener = listeners[index]; + + // REMOVE listeners are placeholders that tell us not to + // inherit, so they never match. Only ADD and ONCE can match. + if ( + (listener.kind === ListenerKind.ADD || listener.kind === ListenerKind.ONCE) && + listener.event.indexOf(':change') !== -1 + ) { + if (result === undefined) { + // we create this array only after we've found a listener that + // matches to avoid allocations when no matches are found. + result = [] as any[]; + } + + result.push(listener.event); + } + } + } + + return result; + } } export interface Meta { diff --git a/packages/@ember/-internals/metal/index.ts b/packages/@ember/-internals/metal/index.ts index 789ec9e540b..c69d2b77557 100644 --- a/packages/@ember/-internals/metal/index.ts +++ b/packages/@ember/-internals/metal/index.ts @@ -36,12 +36,14 @@ export { export { defineProperty } from './lib/properties'; export { isElementDescriptor, nativeDescDecorator } from './lib/decorator'; export { + descriptorForDecorator, descriptorForProperty, isClassicDecorator, setClassicDecorator, } from './lib/descriptor_map'; export { watchKey, unwatchKey } from './lib/watch_key'; export { ChainNode, finishChains, removeChainWatcher } from './lib/chains'; +export { getChainTagsForKey } from './lib/chain-tags'; export { watchPath, unwatchPath } from './lib/watch_path'; export { isWatching, unwatch, watch, watcherCount } from './lib/watching'; export { default as libraries, Libraries } from './lib/libraries'; @@ -49,12 +51,17 @@ export { default as getProperties } from './lib/get_properties'; export { default as setProperties } from './lib/set_properties'; export { default as expandProperties } from './lib/expand_properties'; -export { addObserver, removeObserver } from './lib/observer'; +export { + addObserver, + activateObserver, + removeObserver, + flushInvalidActiveObservers, +} from './lib/observer'; export { Mixin, aliasMethod, mixin, observer, applyMixin } from './lib/mixin'; export { default as inject, DEBUG_INJECTION_FUNCTIONS } from './lib/injected_property'; -export { tagForProperty, tagFor, markObjectAsDirty } from './lib/tags'; +export { tagForProperty, tagFor, markObjectAsDirty, UNKNOWN_PROPERTY_TAG } from './lib/tags'; export { default as runInTransaction, didRender, assertNotRendered } from './lib/transaction'; -export { Tracker, tracked, getCurrentTracker, setCurrentTracker } from './lib/tracked'; +export { consume, Tracker, tracked, track } from './lib/tracked'; export { NAMESPACES, diff --git a/packages/@ember/-internals/metal/lib/alias.ts b/packages/@ember/-internals/metal/lib/alias.ts index a638f75738d..2788e86ea0c 100644 --- a/packages/@ember/-internals/metal/lib/alias.ts +++ b/packages/@ember/-internals/metal/lib/alias.ts @@ -1,8 +1,10 @@ import { Meta, meta as metaFor } from '@ember/-internals/meta'; import { inspect } from '@ember/-internals/utils'; +import { EMBER_METAL_TRACKED_PROPERTIES } from '@ember/canary-features'; import { assert } from '@ember/debug'; import EmberError from '@ember/error'; -import { getCachedValueFor, getCacheFor } from './computed_cache'; +import { finishLazyChains, getChainTagsForKey } from './chain-tags'; +import { getCachedValueFor, getCacheFor, setLastRevisionFor } from './computed_cache'; import { addDependentKeys, ComputedDescriptor, @@ -15,6 +17,8 @@ import { descriptorForDecorator } from './descriptor_map'; import { defineProperty } from './properties'; import { get } from './property_get'; import { set } from './property_set'; +import { tagForProperty, update } from './tags'; +import { consume, track } from './tracked'; const CONSUMED = Object.freeze({}); @@ -57,31 +61,58 @@ export class AliasedProperty extends ComputedDescriptor { constructor(altKey: string) { super(); + this.altKey = altKey; - this._dependentKeys = [altKey]; + if (!EMBER_METAL_TRACKED_PROPERTIES) { + this._dependentKeys = [altKey]; + } } setup(obj: object, keyName: string, propertyDesc: PropertyDescriptor, meta: Meta): void { assert(`Setting alias '${keyName}' on self`, this.altKey !== keyName); super.setup(obj, keyName, propertyDesc, meta); - if (meta.peekWatching(keyName) > 0) { + if (!EMBER_METAL_TRACKED_PROPERTIES && meta.peekWatching(keyName) > 0) { this.consume(obj, keyName, meta); } } teardown(obj: object, keyName: string, meta: Meta): void { - this.unconsume(obj, keyName, meta); + if (!EMBER_METAL_TRACKED_PROPERTIES) { + this.unconsume(obj, keyName, meta); + } super.teardown(obj, keyName, meta); } willWatch(obj: object, keyName: string, meta: Meta): void { - this.consume(obj, keyName, meta); + if (!EMBER_METAL_TRACKED_PROPERTIES) { + this.consume(obj, keyName, meta); + } } get(obj: object, keyName: string): any { - let ret = get(obj, this.altKey); - this.consume(obj, keyName, metaFor(obj)); + let ret: any; + + if (EMBER_METAL_TRACKED_PROPERTIES) { + let propertyTag = tagForProperty(obj, keyName); + + // We don't use the tag since CPs are not automatic, we just want to avoid + // anything tracking while we get the altKey + track(() => { + ret = get(obj, this.altKey); + }); + + let altPropertyTag = getChainTagsForKey(obj, this.altKey); + update(propertyTag, altPropertyTag); + consume(propertyTag); + + finishLazyChains(obj, keyName, ret); + setLastRevisionFor(obj, keyName, propertyTag.value()); + } else { + ret = get(obj, this.altKey); + this.consume(obj, keyName, metaFor(obj)); + } + return ret; } diff --git a/packages/@ember/-internals/metal/lib/array_events.ts b/packages/@ember/-internals/metal/lib/array_events.ts index 8865167f769..4b157ee2bab 100644 --- a/packages/@ember/-internals/metal/lib/array_events.ts +++ b/packages/@ember/-internals/metal/lib/array_events.ts @@ -1,4 +1,5 @@ import { peekMeta } from '@ember/-internals/meta'; +import { EMBER_METAL_TRACKED_PROPERTIES } from '@ember/canary-features'; import { peekCacheFor } from './computed_cache'; import { eachProxyArrayDidChange, eachProxyArrayWillChange } from './each_proxy_events'; import { sendEvent } from './events'; @@ -24,7 +25,9 @@ export function arrayContentWillChange( } } - eachProxyArrayWillChange(array, startIdx, removeAmt, addAmt); + if (!EMBER_METAL_TRACKED_PROPERTIES) { + eachProxyArrayWillChange(array, startIdx, removeAmt, addAmt); + } sendEvent(array, '@array:before', [array, startIdx, removeAmt, addAmt]); @@ -59,7 +62,9 @@ export function arrayContentDidChange( notifyPropertyChange(array, '[]', meta); - eachProxyArrayDidChange(array, startIdx, removeAmt, addAmt); + if (!EMBER_METAL_TRACKED_PROPERTIES) { + eachProxyArrayDidChange(array, startIdx, removeAmt, addAmt); + } sendEvent(array, '@array:change', [array, startIdx, removeAmt, addAmt]); diff --git a/packages/@ember/-internals/metal/lib/chain-tags.ts b/packages/@ember/-internals/metal/lib/chain-tags.ts new file mode 100644 index 00000000000..073070e0b6e --- /dev/null +++ b/packages/@ember/-internals/metal/lib/chain-tags.ts @@ -0,0 +1,128 @@ +import { meta as metaFor, peekMeta } from '@ember/-internals/meta'; +import { isEmberArray } from '@ember/-internals/utils'; +import { assert } from '@ember/debug'; +import { combine, CONSTANT_TAG, Tag, UpdatableTag } from '@glimmer/reference'; +import { getLastRevisionFor, peekCacheFor } from './computed_cache'; +import { descriptorForProperty } from './descriptor_map'; +import get from './property_get'; +import { tagForProperty } from './tags'; +import { track } from './tracked'; + +export function finishLazyChains(obj: any, key: string, value: any) { + let meta = peekMeta(obj); + let lazyTags = meta !== null ? meta.readableLazyChainsFor(key) : undefined; + + if (lazyTags === undefined) { + return; + } + + if (value === null || (typeof value !== 'object' && typeof value !== 'function')) { + lazyTags.clear(); + return; + } + + while (lazyTags.length > 0) { + let [path, tag] = lazyTags.pop()!; + + tag.inner.update(getChainTagsForKey(value, path)); + } +} + +export function getChainTagsForKeys(obj: any, keys: string[]) { + let chainTags: Tag[] = []; + + for (let i = 0; i < keys.length; i++) { + chainTags.push(getChainTagsForKey(obj, keys[i])); + } + + return combine(chainTags); +} + +export function getChainTagsForKey(obj: any, key: string) { + let chainTags: Tag[] = []; + + let current: any = obj; + let segments = key.split('.'); + + // prevent closures + let segment: string, descriptor: any; + + while (segments.length > 0) { + segment = segments.shift()!; + + if (segment === '@each' && segments.length > 0) { + assert( + `When using @each, the value you are attempting to watch must be an array, was: ${current.toString()}`, + Array.isArray(current) || isEmberArray(current) + ); + + segment = segments.shift()!; + + // Push the tags for each item's property + let tags = (current as Array).map(item => { + assert( + `When using @each to observe the array \`${current.toString()}\`, the items in the array must be objects`, + typeof item === 'object' + ); + + return tagForProperty(item, segment); + }); + + // Push the tag for the array length itself + chainTags.push(...tags, tagForProperty(current, '[]')); + + // There shouldn't be any more segments after an `@each`, so break + assert(`When using @each, you can only chain one property level deep`, segments.length === 0); + + break; + } + + let propertyTag = tagForProperty(current, segment); + + chainTags.push(propertyTag); + + descriptor = descriptorForProperty(current, segment); + + if (descriptor === undefined) { + // TODO: Assert that current[segment] isn't an undecorated, non-MANDATORY_SETTER getter + + if (!(segment in current) && typeof current.unknownProperty === 'function') { + current = current.unknownProperty(segment); + } else { + current = current[segment]; + } + } else { + let lastRevision = getLastRevisionFor(current, segment); + + if (propertyTag.validate(lastRevision)) { + if (typeof descriptor.altKey === 'string') { + // it's an alias, so just get the altkey without tracking + track(() => { + current = get(obj, descriptor.altKey); + }); + } else { + current = peekCacheFor(current).get(segment); + } + } else if (segments.length > 0) { + let placeholderTag = UpdatableTag.create(CONSTANT_TAG); + + metaFor(current) + .writableLazyChainsFor(segment) + .push([segments.join('.'), placeholderTag]); + + chainTags.push(placeholderTag); + + break; + } + } + + let currentType = typeof current; + + if (current === null || (currentType !== 'object' && currentType !== 'function')) { + // we've hit the end of the chain for now, break out + break; + } + } + + return combine(chainTags); +} diff --git a/packages/@ember/-internals/metal/lib/computed.ts b/packages/@ember/-internals/metal/lib/computed.ts index f48b247b9c9..4f4c463d806 100644 --- a/packages/@ember/-internals/metal/lib/computed.ts +++ b/packages/@ember/-internals/metal/lib/computed.ts @@ -6,6 +6,8 @@ import { } from '@ember/canary-features'; import { assert, deprecate, warn } from '@ember/debug'; import EmberError from '@ember/error'; +import { combine, Tag } from '@glimmer/reference'; +import { finishLazyChains, getChainTagsForKeys } from './chain-tags'; import { getCachedValueFor, getCacheFor, @@ -32,7 +34,7 @@ import { defineProperty } from './properties'; import { notifyPropertyChange } from './property_events'; import { set } from './property_set'; import { tagFor, tagForProperty, update } from './tags'; -import { getCurrentTracker, setCurrentTracker } from './tracked'; +import { consume, track } from './tracked'; export type ComputedPropertyGetter = (keyName: string) => any; export type ComputedPropertySetter = (keyName: string, value: any, cachedValue?: any) => any; @@ -297,10 +299,6 @@ export class ComputedProperty extends ComputedDescriptor { if (args.length > 0) { this._property(...(args as string[])); } - - if (EMBER_METAL_TRACKED_PROPERTIES) { - this._auto = false; - } } setup(obj: object, keyName: string, propertyDesc: DecoratorPropertyDescriptor, meta: Meta) { @@ -521,19 +519,14 @@ export class ComputedProperty extends ComputedDescriptor { } let cache = getCacheFor(obj); - let propertyTag; + let propertyTag: Tag; if (EMBER_METAL_TRACKED_PROPERTIES) { propertyTag = tagForProperty(obj, keyName); if (cache.has(keyName)) { - // special-case for computed with no dependent keys used to - // trigger cacheable behavior. - if (!this._auto && (!this._dependentKeys || this._dependentKeys.length === 0)) { - return cache.get(keyName); - } - let lastRevision = getLastRevisionFor(obj, keyName); + if (propertyTag.validate(lastRevision)) { return cache.get(keyName); } @@ -544,41 +537,58 @@ export class ComputedProperty extends ComputedDescriptor { } } - let parent: any; - let tracker: any; + let ret; if (EMBER_METAL_TRACKED_PROPERTIES) { - parent = getCurrentTracker(); - tracker = setCurrentTracker(); - } + assert( + `Attempted to access the computed ${obj}.${keyName} on a destroyed object, which is not allowed`, + !metaFor(obj).isMetaDestroyed() + ); - let ret = this._getter!.call(obj, keyName); + // Create a tracker that absorbs any trackable actions inside the CP + let tag = track(() => { + ret = this._getter!.call(obj, keyName); + }); - if (EMBER_METAL_TRACKED_PROPERTIES) { - setCurrentTracker(parent!); - let tag = tracker!.combine(); - if (parent) { - parent.add(tag); - - // Add the tag of the returned value if it is an array, since arrays - // should always cause updates if they are consumed and then changed - if (Array.isArray(ret) || isEmberArray(ret)) { - parent.add(tagFor(ret)); - } + finishLazyChains(obj, keyName, ret); + + let upstreamTags: Tag[] = []; + + if (this._auto === true) { + upstreamTags.push(tag); } - update(propertyTag as any, tag); - setLastRevisionFor(obj, keyName, (propertyTag as any).value()); + if (this._dependentKeys !== undefined) { + upstreamTags.push(getChainTagsForKeys(obj, this._dependentKeys)); + } + + if (upstreamTags.length > 0) { + update(propertyTag!, combine(upstreamTags)); + } + + setLastRevisionFor(obj, keyName, propertyTag!.value()); + + consume(propertyTag!); + + // Add the tag of the returned value if it is an array, since arrays + // should always cause updates if they are consumed and then changed + if (Array.isArray(ret) || isEmberArray(ret)) { + consume(tagFor(ret)); + } + } else { + ret = this._getter!.call(obj, keyName); } cache.set(keyName, ret); - let meta = metaFor(obj); - let chainWatchers = meta.readableChainWatchers(); - if (chainWatchers !== undefined) { - chainWatchers.revalidate(keyName); + if (!EMBER_METAL_TRACKED_PROPERTIES) { + let meta = metaFor(obj); + let chainWatchers = meta.readableChainWatchers(); + if (chainWatchers !== undefined) { + chainWatchers.revalidate(keyName); + } + addDependentKeys(this, obj, keyName, meta); } - addDependentKeys(this, obj, keyName, meta); return ret; } @@ -596,7 +606,23 @@ export class ComputedProperty extends ComputedDescriptor { return this.volatileSet(obj, keyName, value); } - return this.setWithSuspend(obj, keyName, value); + if (EMBER_METAL_TRACKED_PROPERTIES) { + let ret = this._set(obj, keyName, value); + + finishLazyChains(obj, keyName, ret); + + let propertyTag = tagForProperty(obj, keyName); + + if (this._dependentKeys !== undefined) { + update(propertyTag, getChainTagsForKeys(obj, this._dependentKeys)); + } + + setLastRevisionFor(obj, keyName, propertyTag.value()); + + return ret; + } else { + return this.setWithSuspend(obj, keyName, value); + } } _throwReadOnlyError(obj: object, keyName: string): never { @@ -649,7 +675,7 @@ export class ComputedProperty extends ComputedDescriptor { } let meta = metaFor(obj); - if (!hadCachedValue) { + if (!EMBER_METAL_TRACKED_PROPERTIES && !hadCachedValue) { addDependentKeys(this, obj, keyName, meta); } @@ -657,11 +683,6 @@ export class ComputedProperty extends ComputedDescriptor { notifyPropertyChange(obj, keyName, meta); - if (EMBER_METAL_TRACKED_PROPERTIES) { - let propertyTag = tagForProperty(obj, keyName); - setLastRevisionFor(obj, keyName, propertyTag.value()); - } - return ret; } @@ -676,13 +697,12 @@ export class ComputedProperty extends ComputedDescriptor { super.teardown(obj, keyName, meta); } - auto!: () => ComputedProperty; + auto!: () => void; } if (EMBER_METAL_TRACKED_PROPERTIES) { - ComputedProperty.prototype.auto = function(): ComputedProperty { + ComputedProperty.prototype.auto = function() { this._auto = true; - return this; }; } diff --git a/packages/@ember/-internals/metal/lib/computed_cache.ts b/packages/@ember/-internals/metal/lib/computed_cache.ts index 12f7c5d97b1..8294a36f915 100644 --- a/packages/@ember/-internals/metal/lib/computed_cache.ts +++ b/packages/@ember/-internals/metal/lib/computed_cache.ts @@ -24,10 +24,6 @@ export function getCacheFor(obj: object): Map { if (cache === undefined) { cache = new Map(); - if (EMBER_METAL_TRACKED_PROPERTIES) { - COMPUTED_PROPERTY_LAST_REVISION!.set(obj, new Map()); - } - COMPUTED_PROPERTY_CACHED_VALUES.set(obj, cache); } return cache; @@ -45,8 +41,14 @@ export let getLastRevisionFor: (obj: object, key: string) => number; if (EMBER_METAL_TRACKED_PROPERTIES) { setLastRevisionFor = (obj, key, revision) => { - let lastRevision = COMPUTED_PROPERTY_LAST_REVISION!.get(obj); - lastRevision!.set(key, revision); + let cache = COMPUTED_PROPERTY_LAST_REVISION!.get(obj); + + if (cache === undefined) { + cache = new Map(); + COMPUTED_PROPERTY_LAST_REVISION!.set(obj, cache); + } + + cache!.set(key, revision); }; getLastRevisionFor = (obj, key) => { diff --git a/packages/@ember/-internals/metal/lib/decorator.ts b/packages/@ember/-internals/metal/lib/decorator.ts index 8b5d3513b6a..b6718074db4 100644 --- a/packages/@ember/-internals/metal/lib/decorator.ts +++ b/packages/@ember/-internals/metal/lib/decorator.ts @@ -1,5 +1,8 @@ import { Meta, meta as metaFor } from '@ember/-internals/meta'; -import { EMBER_NATIVE_DECORATOR_SUPPORT } from '@ember/canary-features'; +import { + EMBER_METAL_TRACKED_PROPERTIES, + EMBER_NATIVE_DECORATOR_SUPPORT, +} from '@ember/canary-features'; import { assert } from '@ember/debug'; import { setClassicDecorator } from './descriptor_map'; import { unwatch, watch } from './watching'; @@ -136,6 +139,15 @@ function DESCRIPTOR_GETTER_FUNCTION(name: string, descriptor: ComputedDescriptor }; } +function DESCRIPTOR_SETTER_FUNCTION( + name: string, + descriptor: ComputedDescriptor +): (value: any) => void { + return function CPSETTER_FUNCTION(this: object, value: any): void { + return descriptor.set(this, name, value); + }; +} + export function makeComputedDecorator( desc: ComputedDescriptor, DecoratorClass: { prototype: object } @@ -163,11 +175,17 @@ export function makeComputedDecorator( let meta = arguments.length === 3 ? metaFor(target) : maybeMeta; desc.setup(target, key, propertyDesc, meta!); - return { + let computedDesc: PropertyDescriptor = { enumerable: desc.enumerable, configurable: desc.configurable, get: DESCRIPTOR_GETTER_FUNCTION(key, desc), }; + + if (EMBER_METAL_TRACKED_PROPERTIES) { + computedDesc.set = DESCRIPTOR_SETTER_FUNCTION(key, desc); + } + + return computedDesc; }; setClassicDecorator(decorator, desc); diff --git a/packages/@ember/-internals/metal/lib/mixin.ts b/packages/@ember/-internals/metal/lib/mixin.ts index 4c3e25d59af..d8dd8dcbd75 100644 --- a/packages/@ember/-internals/metal/lib/mixin.ts +++ b/packages/@ember/-internals/metal/lib/mixin.ts @@ -13,6 +13,7 @@ import { setObservers, wrap, } from '@ember/-internals/utils'; +import { EMBER_METAL_TRACKED_PROPERTIES } from '@ember/canary-features'; import { assert, deprecate } from '@ember/debug'; import { ALIAS_METHOD } from '@ember/deprecated-features'; import { assign } from '@ember/polyfills'; @@ -32,7 +33,7 @@ import { import { addListener, removeListener } from './events'; import expandProperties from './expand_properties'; import { classToString, setUnprocessedMixins } from './namespace_search'; -import { addObserver, removeObserver } from './observer'; +import { addObserver, removeObserver, revalidateObservers } from './observer'; import { defineProperty } from './properties'; const a_concat = Array.prototype.concat; @@ -477,6 +478,12 @@ export function applyMixin(obj: { [key: string]: any }, mixins: Mixin[]) { defineProperty(obj, key, desc, value, meta); } + if (EMBER_METAL_TRACKED_PROPERTIES) { + if (!meta.isPrototypeMeta(obj)) { + revalidateObservers(obj); + } + } + return obj; } diff --git a/packages/@ember/-internals/metal/lib/observer.ts b/packages/@ember/-internals/metal/lib/observer.ts index 66ed9f8aff8..def5ed01dee 100644 --- a/packages/@ember/-internals/metal/lib/observer.ts +++ b/packages/@ember/-internals/metal/lib/observer.ts @@ -1,7 +1,21 @@ +import { peekMeta } from '@ember/-internals/meta'; +import { EMBER_METAL_TRACKED_PROPERTIES } from '@ember/canary-features'; +import { schedule } from '@ember/runloop'; +import { CURRENT_TAG, Tag } from '@glimmer/reference'; +import { getChainTagsForKey } from './chain-tags'; import changeEvent from './change_event'; -import { addListener, removeListener } from './events'; +import { addListener, removeListener, sendEvent } from './events'; import { unwatch, watch } from './watching'; +interface ActiveObserver { + tag: Tag; + path: string; + lastRevision: number; + count: number; +} + +const ACTIVE_OBSERVERS: Map> = new Map(); + /** @module @ember/object */ @@ -22,8 +36,19 @@ export function addObserver( target: object | Function | null, method: string | Function | undefined ): void { - addListener(obj, changeEvent(path), target, method); - watch(obj, path); + let eventName = changeEvent(path); + + addListener(obj, eventName, target, method); + + if (EMBER_METAL_TRACKED_PROPERTIES) { + let meta = peekMeta(obj); + + if (meta === null || !(meta.isPrototypeMeta(obj) || meta.isInitializing())) { + activateObserver(obj, eventName); + } + } else { + watch(obj, path); + } } /** @@ -42,6 +67,119 @@ export function removeObserver( target: object | Function | null, method: string | Function | undefined ): void { - unwatch(obj, path); - removeListener(obj, changeEvent(path), target, method); + let eventName = changeEvent(path); + + if (EMBER_METAL_TRACKED_PROPERTIES) { + let meta = peekMeta(obj); + + if (meta === null || !(meta.isPrototypeMeta(obj) || meta.isInitializing())) { + deactivateObserver(obj, eventName); + } + } else { + unwatch(obj, path); + } + + removeListener(obj, eventName, target, method); +} + +function getOrCreateActiveObserversFor(target: object) { + if (!ACTIVE_OBSERVERS.has(target)) { + ACTIVE_OBSERVERS.set(target, new Map()); + } + + return ACTIVE_OBSERVERS.get(target)!; +} + +export function activateObserver(target: object, eventName: string) { + let activeObservers = getOrCreateActiveObserversFor(target); + + if (activeObservers.has(eventName)) { + activeObservers.get(eventName)!.count++; + } else { + let [path] = eventName.split(':'); + let tag = getChainTagsForKey(target, path); + + activeObservers.set(eventName, { + count: 1, + path, + tag, + lastRevision: tag.value(), + }); + } +} + +export function deactivateObserver(target: object, eventName: string) { + let activeObservers = ACTIVE_OBSERVERS.get(target); + + if (activeObservers !== undefined) { + let observer = activeObservers.get(eventName)!; + + observer.count--; + + if (observer.count === 0) { + activeObservers.delete(eventName); + + if (activeObservers.size === 0) { + ACTIVE_OBSERVERS.delete(target); + } + } + } +} + +/** + * Primarily used for cases where we are redefining a class, e.g. mixins/reopen + * being applied later. Revalidates all the observers, resetting their tags. + * + * @private + * @param target + */ +export function revalidateObservers(target: object) { + if (!ACTIVE_OBSERVERS.has(target)) { + return; + } + + ACTIVE_OBSERVERS.get(target)!.forEach(observer => { + observer.tag = getChainTagsForKey(target, observer.path); + observer.lastRevision = observer.tag.value(); + }); +} + +let lastKnownRevision = 0; + +export function flushInvalidActiveObservers(shouldSchedule = true) { + if (lastKnownRevision === CURRENT_TAG.value()) { + return; + } + + lastKnownRevision = CURRENT_TAG.value(); + + ACTIVE_OBSERVERS.forEach((activeObservers, target) => { + let meta = peekMeta(target); + + if (meta && (meta.isSourceDestroying() || meta.isMetaDestroyed())) { + ACTIVE_OBSERVERS.delete(target); + return; + } + + activeObservers.forEach((observer, eventName) => { + if (!observer.tag.validate(observer.lastRevision)) { + let sendObserver = () => { + try { + sendEvent(target, eventName, [target, observer.path]); + } finally { + observer.tag = getChainTagsForKey(target, observer.path); + observer.lastRevision = observer.tag.value(); + } + }; + + if (shouldSchedule) { + schedule('actions', sendObserver); + } else { + // TODO: we need to schedule eagerly in exactly one location (_internalReset), + // for query params. We should get rid of this ASAP + sendObserver(); + } + } + }); + }); } diff --git a/packages/@ember/-internals/metal/lib/properties.ts b/packages/@ember/-internals/metal/lib/properties.ts index 0c468490a6f..64bb57fff1c 100644 --- a/packages/@ember/-internals/metal/lib/properties.ts +++ b/packages/@ember/-internals/metal/lib/properties.ts @@ -3,6 +3,8 @@ */ import { Meta, meta as metaFor, peekMeta, UNDEFINED } from '@ember/-internals/meta'; +import { setWithMandatorySetter } from '@ember/-internals/utils'; +import { EMBER_METAL_TRACKED_PROPERTIES } from '@ember/canary-features'; import { assert } from '@ember/debug'; import { DEBUG } from '@glimmer/env'; import { Decorator } from './decorator'; @@ -184,7 +186,11 @@ export function defineProperty( value, }); } else { - obj[keyName] = data; + if (EMBER_METAL_TRACKED_PROPERTIES && DEBUG) { + setWithMandatorySetter!(obj, keyName, data); + } else { + obj[keyName] = data; + } } } else { value = desc; diff --git a/packages/@ember/-internals/metal/lib/property_events.ts b/packages/@ember/-internals/metal/lib/property_events.ts index 985e7c54b8b..2d2795776ba 100644 --- a/packages/@ember/-internals/metal/lib/property_events.ts +++ b/packages/@ember/-internals/metal/lib/property_events.ts @@ -1,5 +1,6 @@ import { Meta, peekMeta } from '@ember/-internals/meta'; import { symbol } from '@ember/-internals/utils'; +import { EMBER_METAL_TRACKED_PROPERTIES } from '@ember/canary-features'; import { DEBUG } from '@glimmer/env'; import changeEvent from './change_event'; import { descriptorForProperty } from './descriptor_map'; @@ -42,29 +43,28 @@ function notifyPropertyChange(obj: object, keyName: string, _meta?: Meta | null) return; } - let possibleDesc = descriptorForProperty(obj, keyName, meta); + if (!EMBER_METAL_TRACKED_PROPERTIES) { + let possibleDesc = descriptorForProperty(obj, keyName, meta); - if (possibleDesc !== undefined && typeof possibleDesc.didChange === 'function') { - possibleDesc.didChange(obj, keyName); + if (possibleDesc !== undefined && typeof possibleDesc.didChange === 'function') { + possibleDesc.didChange(obj, keyName); + } + + if (meta !== null && meta.peekWatching(keyName) > 0) { + dependentKeysDidChange(obj, keyName, meta); + chainsDidChange(obj, keyName, meta); + notifyObservers(obj, keyName, meta); + } } - if (meta !== null && meta.peekWatching(keyName) > 0) { - dependentKeysDidChange(obj, keyName, meta); - chainsDidChange(obj, keyName, meta); - notifyObservers(obj, keyName, meta); + if (meta !== null) { + markObjectAsDirty(obj, keyName, meta); } if (PROPERTY_DID_CHANGE in obj) { obj[PROPERTY_DID_CHANGE](keyName); } - if (meta !== null) { - if (meta.isSourceDestroying()) { - return; - } - markObjectAsDirty(obj, keyName, meta); - } - if (DEBUG) { assertNotRendered(obj, keyName); } diff --git a/packages/@ember/-internals/metal/lib/property_get.ts b/packages/@ember/-internals/metal/lib/property_get.ts index fe9975a7c3a..dca63ea827b 100644 --- a/packages/@ember/-internals/metal/lib/property_get.ts +++ b/packages/@ember/-internals/metal/lib/property_get.ts @@ -8,7 +8,7 @@ import { DEBUG } from '@glimmer/env'; import { descriptorForProperty } from './descriptor_map'; import { isPath } from './path_cache'; import { tagFor, tagForProperty } from './tags'; -import { getCurrentTracker } from './tracked'; +import { consume, isTracking } from './tracked'; export const PROXY_CONTENT = symbol('PROXY_CONTENT'); @@ -103,12 +103,11 @@ export function get(obj: object, keyName: string): any { let value: any; if (isObjectLike) { - let tracker = null; + let tracking = isTracking(); if (EMBER_METAL_TRACKED_PROPERTIES) { - tracker = getCurrentTracker(); - if (tracker !== null) { - tracker.add(tagForProperty(obj, keyName)); + if (tracking) { + consume(tagForProperty(obj, keyName)); } } @@ -127,10 +126,10 @@ export function get(obj: object, keyName: string): any { // should always cause updates if they are consumed and then changed if ( EMBER_METAL_TRACKED_PROPERTIES && - tracker !== null && + tracking && (Array.isArray(value) || isEmberArray(value)) ) { - tracker.add(tagFor(value)); + consume(tagFor(value)); } } else { value = obj[keyName]; diff --git a/packages/@ember/-internals/metal/lib/property_set.ts b/packages/@ember/-internals/metal/lib/property_set.ts index 2e3d6a420aa..2b21a846296 100644 --- a/packages/@ember/-internals/metal/lib/property_set.ts +++ b/packages/@ember/-internals/metal/lib/property_set.ts @@ -1,5 +1,11 @@ import { Meta, peekMeta } from '@ember/-internals/meta'; -import { HAS_NATIVE_PROXY, lookupDescriptor, toString } from '@ember/-internals/utils'; +import { + HAS_NATIVE_PROXY, + lookupDescriptor, + setWithMandatorySetter as trackedSetWithMandatorySetter, + toString, +} from '@ember/-internals/utils'; +import { EMBER_METAL_TRACKED_PROPERTIES } from '@ember/canary-features'; import { assert } from '@ember/debug'; import EmberError from '@ember/error'; import { DEBUG } from '@glimmer/env'; @@ -15,10 +21,10 @@ interface ExtendedObject { } let setWithMandatorySetter: >( - meta: Meta | null, obj: T, keyName: K, - value: T[K] + value: T[K], + meta: Meta | null ) => void; let makeEnumerable: (obj: object, keyName: string) => void; @@ -103,7 +109,11 @@ export function set(obj: object, keyName: string, value: any, tolerant?: boolean (obj as ExtendedObject).setUnknownProperty!(keyName, value); } else { if (DEBUG) { - setWithMandatorySetter(meta, obj, keyName, value); + if (EMBER_METAL_TRACKED_PROPERTIES) { + trackedSetWithMandatorySetter!(obj, keyName, value); + } else { + setWithMandatorySetter(obj, keyName, value, meta); + } } else { obj[keyName] = value; } @@ -117,7 +127,7 @@ export function set(obj: object, keyName: string, value: any, tolerant?: boolean } if (DEBUG) { - setWithMandatorySetter = (meta, obj, keyName, value) => { + setWithMandatorySetter = (obj, keyName, value, meta) => { if (meta !== null && meta.peekWatching(keyName) > 0) { makeEnumerable(obj, keyName); meta.writeValue(obj, keyName, value); diff --git a/packages/@ember/-internals/metal/lib/tags.ts b/packages/@ember/-internals/metal/lib/tags.ts index d23b689dc66..278e6ccf599 100644 --- a/packages/@ember/-internals/metal/lib/tags.ts +++ b/packages/@ember/-internals/metal/lib/tags.ts @@ -1,7 +1,8 @@ import { Meta, meta as metaFor } from '@ember/-internals/meta'; -import { isProxy } from '@ember/-internals/utils'; +import { isProxy, setupMandatorySetter, symbol } from '@ember/-internals/utils'; import { EMBER_METAL_TRACKED_PROPERTIES } from '@ember/canary-features'; import { backburner } from '@ember/runloop'; +import { DEBUG } from '@glimmer/env'; import { combine, CONSTANT_TAG, @@ -11,6 +12,8 @@ import { UpdatableTag, } from '@glimmer/reference'; +export const UNKNOWN_PROPERTY_TAG = symbol('UNKNOWN_PROPERTY_TAG'); + function makeTag(): TagWrapper { return DirtyableTag.create(); } @@ -22,7 +25,11 @@ export function tagForProperty(object: any, propertyKey: string | symbol, _meta? } let meta = _meta === undefined ? metaFor(object) : _meta; - if (isProxy(object)) { + if (EMBER_METAL_TRACKED_PROPERTIES) { + if (!(propertyKey in object) && typeof object[UNKNOWN_PROPERTY_TAG] === 'function') { + return object[UNKNOWN_PROPERTY_TAG](propertyKey); + } + } else if (isProxy(object)) { return tagFor(object, meta); } @@ -34,6 +41,15 @@ export function tagForProperty(object: any, propertyKey: string | symbol, _meta? if (EMBER_METAL_TRACKED_PROPERTIES) { let pair = combine([makeTag(), UpdatableTag.create(CONSTANT_TAG)]); + + if (DEBUG) { + if (EMBER_METAL_TRACKED_PROPERTIES) { + setupMandatorySetter!(object, propertyKey); + } + + (pair as any)._propertyKey = propertyKey; + } + return (tags[propertyKey] = pair); } else { return (tags[propertyKey] = makeTag()); @@ -61,6 +77,7 @@ if (EMBER_METAL_TRACKED_PROPERTIES) { }; update = (outer, inner) => { + (outer.inner! as any).lastChecked = 0; (outer.inner! as any).second.inner.update(inner); }; } else { @@ -69,7 +86,8 @@ if (EMBER_METAL_TRACKED_PROPERTIES) { }; } -export function markObjectAsDirty(obj: object, propertyKey: string, meta: Meta): void { +export function markObjectAsDirty(obj: object, propertyKey: string, _meta?: Meta): void { + let meta = _meta === undefined ? metaFor(obj) : _meta; let objectTag = meta.readableTag(); if (objectTag !== undefined) { diff --git a/packages/@ember/-internals/metal/lib/tracked.ts b/packages/@ember/-internals/metal/lib/tracked.ts index 07d52c9b31f..391c1c81b3f 100644 --- a/packages/@ember/-internals/metal/lib/tracked.ts +++ b/packages/@ember/-internals/metal/lib/tracked.ts @@ -5,7 +5,7 @@ import { DEBUG } from '@glimmer/env'; import { combine, CONSTANT_TAG, Tag } from '@glimmer/reference'; import { Decorator, DecoratorPropertyDescriptor, isElementDescriptor } from './decorator'; import { setClassicDecorator } from './descriptor_map'; -import { dirty, ensureRunloop, tagFor, tagForProperty, update } from './tags'; +import { markObjectAsDirty, tagFor, tagForProperty, update } from './tags'; type Option = T | null; @@ -222,12 +222,13 @@ function descriptorForField([_target, key, desc]: [ }, set(newValue: any): void { - tagFor(this).inner!['dirty'](); - dirty(tagForProperty(this, key)); + markObjectAsDirty(this, key); this[secretKey] = newValue; - propertyDidChange(); + if (propertyDidChange !== null) { + propertyDidChange(); + } }, }; } @@ -249,14 +250,29 @@ function descriptorForField([_target, key, desc]: [ */ let CURRENT_TRACKER: Option = null; -export function getCurrentTracker(): Option { - return CURRENT_TRACKER; +export function track(callback: () => void) { + let parent = CURRENT_TRACKER; + let current = new Tracker(); + + CURRENT_TRACKER = current; + + try { + callback(); + } finally { + CURRENT_TRACKER = parent; + } + + return current.combine(); +} + +export function consume(tag: Tag) { + if (CURRENT_TRACKER !== null) { + CURRENT_TRACKER.add(tag); + } } -export function setCurrentTracker(): Tracker; -export function setCurrentTracker(tracker: Option): Option; -export function setCurrentTracker(tracker: Option = new Tracker()): Option { - return (CURRENT_TRACKER = tracker); +export function isTracking() { + return CURRENT_TRACKER !== null; } export type Key = string; @@ -265,7 +281,7 @@ export interface Interceptors { [key: string]: boolean; } -let propertyDidChange = ensureRunloop; +let propertyDidChange: (() => void) | null = null; export function setPropertyDidChange(cb: () => void): void { propertyDidChange = cb; diff --git a/packages/@ember/-internals/metal/lib/watch_key.ts b/packages/@ember/-internals/metal/lib/watch_key.ts index d087258bf27..8d07d1da1d0 100644 --- a/packages/@ember/-internals/metal/lib/watch_key.ts +++ b/packages/@ember/-internals/metal/lib/watch_key.ts @@ -1,5 +1,6 @@ import { Meta, meta as metaFor, peekMeta, UNDEFINED } from '@ember/-internals/meta'; import { lookupDescriptor } from '@ember/-internals/utils'; +import { EMBER_METAL_TRACKED_PROPERTIES } from '@ember/canary-features'; import { DEBUG } from '@glimmer/env'; import { descriptorForProperty, isClassicDecorator } from './descriptor_map'; import { @@ -33,8 +34,10 @@ export function watchKey(obj: object, keyName: string, _meta?: Meta): void { possibleDesc.willWatch(obj, keyName, meta); } - if (typeof (obj as MaybeHasWillWatchProperty).willWatchProperty === 'function') { - (obj as MaybeHasWillWatchProperty).willWatchProperty!(keyName); + if (!EMBER_METAL_TRACKED_PROPERTIES) { + if (typeof (obj as MaybeHasWillWatchProperty).willWatchProperty === 'function') { + (obj as MaybeHasWillWatchProperty).willWatchProperty!(keyName); + } } if (DEBUG) { diff --git a/packages/@ember/-internals/metal/tests/accessors/mandatory_setters_test.js b/packages/@ember/-internals/metal/tests/accessors/mandatory_setters_test.js index 7d69568aa19..0078ebb47e6 100644 --- a/packages/@ember/-internals/metal/tests/accessors/mandatory_setters_test.js +++ b/packages/@ember/-internals/metal/tests/accessors/mandatory_setters_test.js @@ -1,6 +1,7 @@ import { DEBUG } from '@glimmer/env'; import { get, set, watch, unwatch } from '../..'; import { meta as metaFor } from '@ember/-internals/meta'; +import { EMBER_METAL_TRACKED_PROPERTIES } from '@ember/canary-features'; import { moduleFor, AbstractTestCase } from 'internal-test-helpers'; function hasMandatorySetter(object, property) { @@ -15,7 +16,7 @@ function hasMetaValue(object, property) { return metaFor(object).peekValues(property) !== undefined; } -if (DEBUG) { +if (DEBUG && !EMBER_METAL_TRACKED_PROPERTIES) { moduleFor( 'mandory-setters', class extends AbstractTestCase { diff --git a/packages/@ember/-internals/metal/tests/alias_test.js b/packages/@ember/-internals/metal/tests/alias_test.js index 29a7defff42..a7d624ad993 100644 --- a/packages/@ember/-internals/metal/tests/alias_test.js +++ b/packages/@ember/-internals/metal/tests/alias_test.js @@ -9,8 +9,9 @@ import { tagFor, tagForProperty, } from '..'; -import { meta } from '@ember/-internals/meta'; -import { moduleFor, AbstractTestCase } from 'internal-test-helpers'; +import { Object as EmberObject } from '@ember/-internals/runtime'; +import { EMBER_METAL_TRACKED_PROPERTIES } from '@ember/canary-features'; +import { moduleFor, AbstractTestCase, runLoopSettled } from 'internal-test-helpers'; let obj, count; @@ -41,71 +42,94 @@ moduleFor( assert.equal(get(obj, 'foo.faz'), 'BAR'); } - ['@test old dependent keys should not trigger property changes'](assert) { + async ['@test old dependent keys should not trigger property changes'](assert) { let obj1 = Object.create(null); defineProperty(obj1, 'foo', null, null); defineProperty(obj1, 'bar', alias('foo')); defineProperty(obj1, 'baz', alias('foo')); defineProperty(obj1, 'baz', alias('bar')); // redefine baz + + // bootstrap the alias + obj1.baz; + addObserver(obj1, 'baz', incrementCount); set(obj1, 'foo', 'FOO'); + await runLoopSettled(); + assert.equal(count, 1); removeObserver(obj1, 'baz', incrementCount); set(obj1, 'foo', 'OOF'); + await runLoopSettled(); + assert.equal(count, 1); } - [`@test inheriting an observer of the alias from the prototype then + async [`@test inheriting an observer of the alias from the prototype then redefining the alias on the instance to another property dependent on same key does not call the observer twice`](assert) { - let obj1 = Object.create(null); - obj1.incrementCount = incrementCount; + let obj1 = EmberObject.extend({ + foo: null, + bar: alias('foo'), + baz: alias('foo'), - meta(obj1).proto = obj1; + incrementCount, + }); - defineProperty(obj1, 'foo', null, null); - defineProperty(obj1, 'bar', alias('foo')); - defineProperty(obj1, 'baz', alias('foo')); - addObserver(obj1, 'baz', null, 'incrementCount'); + addObserver(obj1.prototype, 'baz', null, 'incrementCount'); - let obj2 = Object.create(obj1); + let obj2 = obj1.create(); defineProperty(obj2, 'baz', alias('bar')); // override baz + // bootstrap the alias + obj2.baz; + set(obj2, 'foo', 'FOO'); + await runLoopSettled(); + assert.equal(count, 1); removeObserver(obj2, 'baz', null, 'incrementCount'); set(obj2, 'foo', 'OOF'); + await runLoopSettled(); + assert.equal(count, 1); } - ['@test an observer of the alias works if added after defining the alias'](assert) { + async ['@test an observer of the alias works if added after defining the alias'](assert) { defineProperty(obj, 'bar', alias('foo.faz')); + + // bootstrap the alias + obj.bar; + addObserver(obj, 'bar', incrementCount); - assert.ok(isWatching(obj, 'foo.faz')); set(obj, 'foo.faz', 'BAR'); + + await runLoopSettled(); assert.equal(count, 1); } - ['@test an observer of the alias works if added before defining the alias'](assert) { + async ['@test an observer of the alias works if added before defining the alias'](assert) { addObserver(obj, 'bar', incrementCount); defineProperty(obj, 'bar', alias('foo.faz')); - assert.ok(isWatching(obj, 'foo.faz')); + + // bootstrap the alias + obj.bar; + set(obj, 'foo.faz', 'BAR'); + + await runLoopSettled(); assert.equal(count, 1); } - ['@test object with alias is dirtied if interior object of alias is set after consumption']( - assert - ) { + ['@test alias is dirtied if interior object of alias is set after consumption'](assert) { defineProperty(obj, 'bar', alias('foo.faz')); get(obj, 'bar'); - let tag = tagFor(obj); + let tag = EMBER_METAL_TRACKED_PROPERTIES ? tagForProperty(obj, 'bar') : tagFor(obj); let tagValue = tag.value(); set(obj, 'foo.faz', 'BAR'); @@ -120,50 +144,62 @@ moduleFor( } ['@test destroyed alias does not disturb watch count'](assert) { - defineProperty(obj, 'bar', alias('foo.faz')); + if (!EMBER_METAL_TRACKED_PROPERTIES) { + defineProperty(obj, 'bar', alias('foo.faz')); - assert.equal(get(obj, 'bar'), 'FOO'); - assert.ok(isWatching(obj, 'foo.faz')); + assert.equal(get(obj, 'bar'), 'FOO'); + assert.ok(isWatching(obj, 'foo.faz')); - defineProperty(obj, 'bar', null); + defineProperty(obj, 'bar', null); - assert.notOk(isWatching(obj, 'foo.faz')); + assert.notOk(isWatching(obj, 'foo.faz')); + } else { + assert.expect(0); + } } ['@test setting on oneWay alias does not disturb watch count'](assert) { - defineProperty(obj, 'bar', alias('foo.faz').oneWay()); + if (!EMBER_METAL_TRACKED_PROPERTIES) { + defineProperty(obj, 'bar', alias('foo.faz').oneWay()); - assert.equal(get(obj, 'bar'), 'FOO'); - assert.ok(isWatching(obj, 'foo.faz')); + assert.equal(get(obj, 'bar'), 'FOO'); + assert.ok(isWatching(obj, 'foo.faz')); - set(obj, 'bar', null); + set(obj, 'bar', null); - assert.notOk(isWatching(obj, 'foo.faz')); + assert.notOk(isWatching(obj, 'foo.faz')); + } else { + assert.expect(0); + } } ['@test redefined alias with observer does not disturb watch count'](assert) { - defineProperty(obj, 'bar', alias('foo.faz').oneWay()); + if (!EMBER_METAL_TRACKED_PROPERTIES) { + defineProperty(obj, 'bar', alias('foo.faz').oneWay()); - assert.equal(get(obj, 'bar'), 'FOO'); - assert.ok(isWatching(obj, 'foo.faz')); + assert.equal(get(obj, 'bar'), 'FOO'); + assert.ok(isWatching(obj, 'foo.faz')); - addObserver(obj, 'bar', incrementCount); + addObserver(obj, 'bar', incrementCount); - assert.equal(count, 0); + assert.equal(count, 0); - set(obj, 'bar', null); + set(obj, 'bar', null); - assert.equal(count, 1); - assert.notOk(isWatching(obj, 'foo.faz')); + assert.equal(count, 1); + assert.notOk(isWatching(obj, 'foo.faz')); - defineProperty(obj, 'bar', alias('foo.faz')); + defineProperty(obj, 'bar', alias('foo.faz')); - assert.equal(count, 1); - assert.ok(isWatching(obj, 'foo.faz')); + assert.equal(count, 1); + assert.ok(isWatching(obj, 'foo.faz')); - set(obj, 'foo.faz', 'great'); + set(obj, 'foo.faz', 'great'); - assert.equal(count, 2); + assert.equal(count, 2); + } else { + assert.expect(0); + } } ['@test property tags are bumped when the source changes [GH#17243]'](assert) { diff --git a/packages/@ember/-internals/metal/tests/chains_test.js b/packages/@ember/-internals/metal/tests/chains_test.js index 033274ea696..f148fc789ad 100644 --- a/packages/@ember/-internals/metal/tests/chains_test.js +++ b/packages/@ember/-internals/metal/tests/chains_test.js @@ -13,198 +13,201 @@ import { watcherCount, } from '..'; import { meta, peekMeta } from '@ember/-internals/meta'; +import { EMBER_METAL_TRACKED_PROPERTIES } from '@ember/canary-features'; import { moduleFor, AbstractTestCase } from 'internal-test-helpers'; -moduleFor( - 'Chains', - class extends AbstractTestCase { - ['@test finishChains should properly copy chains from prototypes to instances'](assert) { - function didChange() {} +if (!EMBER_METAL_TRACKED_PROPERTIES) { + moduleFor( + 'Chains', + class extends AbstractTestCase { + ['@test finishChains should properly copy chains from prototypes to instances'](assert) { + function didChange() {} - let obj = {}; - addObserver(obj, 'foo.bar', null, didChange); + let obj = {}; + addObserver(obj, 'foo.bar', null, didChange); - let childObj = Object.create(obj); + let childObj = Object.create(obj); - let parentMeta = meta(obj); - let childMeta = meta(childObj); + let parentMeta = meta(obj); + let childMeta = meta(childObj); - finishChains(childMeta); + finishChains(childMeta); - assert.ok( - parentMeta.readableChains() !== childMeta.readableChains(), - 'The chains object is copied' - ); - } + assert.ok( + parentMeta.readableChains() !== childMeta.readableChains(), + 'The chains object is copied' + ); + } - ['@test does not observe primitive values'](assert) { - let obj = { - foo: { bar: 'STRING' }, - }; + ['@test does not observe primitive values'](assert) { + let obj = { + foo: { bar: 'STRING' }, + }; - addObserver(obj, 'foo.bar.baz', null, function() {}); - let meta = peekMeta(obj); - assert.notOk(meta._object); - } + addObserver(obj, 'foo.bar.baz', null, function() {}); + let meta = peekMeta(obj); + assert.notOk(meta._object); + } - ['@test observer and CP chains'](assert) { - let obj = {}; - - defineProperty(obj, 'foo', computed('qux.[]', function() {})); - defineProperty(obj, 'qux', computed(function() {})); - - // create DK chains - get(obj, 'foo'); - - // create observer chain - addObserver(obj, 'qux.length', function() {}); - - /* - +-----+ - | qux | root CP - +-----+ - ^ - +------+-----+ - | | - +--------+ +----+ - | length | | [] | chainWatchers - +--------+ +----+ - observer CP(foo, 'qux.[]') - */ - - // invalidate qux - notifyPropertyChange(obj, 'qux'); - - // CP chain is blown away - - /* - +-----+ - | qux | root CP - +-----+ - ^ - +------+xxxxxx - | x - +--------+ xxxxxx - | length | x [] x chainWatchers - +--------+ xxxxxx - observer CP(foo, 'qux.[]') - */ - - get(obj, 'qux'); // CP chain re-recreated - assert.ok(true, 'no crash'); - } + ['@test observer and CP chains'](assert) { + let obj = {}; + + defineProperty(obj, 'foo', computed('qux.[]', function() {})); + defineProperty(obj, 'qux', computed(function() {})); + + // create DK chains + get(obj, 'foo'); + + // create observer chain + addObserver(obj, 'qux.length', function() {}); + + /* + +-----+ + | qux | root CP + +-----+ + ^ + +------+-----+ + | | + +--------+ +----+ + | length | | [] | chainWatchers + +--------+ +----+ + observer CP(foo, 'qux.[]') + */ + + // invalidate qux + notifyPropertyChange(obj, 'qux'); + + // CP chain is blown away + + /* + +-----+ + | qux | root CP + +-----+ + ^ + +------+xxxxxx + | x + +--------+ xxxxxx + | length | x [] x chainWatchers + +--------+ xxxxxx + observer CP(foo, 'qux.[]') + */ + + get(obj, 'qux'); // CP chain re-recreated + assert.ok(true, 'no crash'); + } - ['@test checks cache correctly'](assert) { - let obj = {}; - let parentChainNode = new ChainNode(null, null, obj); - let chainNode = new ChainNode(parentChainNode, 'foo'); - - defineProperty( - obj, - 'foo', - computed(function() { - return undefined; - }) - ); - get(obj, 'foo'); - - assert.strictEqual(chainNode.value(), undefined); - } + ['@test checks cache correctly'](assert) { + let obj = {}; + let parentChainNode = new ChainNode(null, null, obj); + let chainNode = new ChainNode(parentChainNode, 'foo'); + + defineProperty( + obj, + 'foo', + computed(function() { + return undefined; + }) + ); + get(obj, 'foo'); + + assert.strictEqual(chainNode.value(), undefined); + } - ['@test chains are watched correctly'](assert) { - let obj = { foo: { bar: { baz: 1 } } }; + ['@test chains are watched correctly'](assert) { + let obj = { foo: { bar: { baz: 1 } } }; - watch(obj, 'foo.bar.baz'); + watch(obj, 'foo.bar.baz'); - assert.equal(watcherCount(obj, 'foo'), 1); - assert.equal(watcherCount(obj, 'foo.bar'), 0); - assert.equal(watcherCount(obj, 'foo.bar.baz'), 1); - assert.equal(watcherCount(obj.foo, 'bar'), 1); - assert.equal(watcherCount(obj.foo, 'bar.baz'), 0); - assert.equal(watcherCount(obj.foo.bar, 'baz'), 1); + assert.equal(watcherCount(obj, 'foo'), 1); + assert.equal(watcherCount(obj, 'foo.bar'), 0); + assert.equal(watcherCount(obj, 'foo.bar.baz'), 1); + assert.equal(watcherCount(obj.foo, 'bar'), 1); + assert.equal(watcherCount(obj.foo, 'bar.baz'), 0); + assert.equal(watcherCount(obj.foo.bar, 'baz'), 1); - unwatch(obj, 'foo.bar.baz'); + unwatch(obj, 'foo.bar.baz'); - assert.equal(watcherCount(obj, 'foo'), 0); - assert.equal(watcherCount(obj, 'foo.bar'), 0); - assert.equal(watcherCount(obj, 'foo.bar.baz'), 0); - assert.equal(watcherCount(obj.foo, 'bar'), 0); - assert.equal(watcherCount(obj.foo, 'bar.baz'), 0); - assert.equal(watcherCount(obj.foo.bar, 'baz'), 0); - } + assert.equal(watcherCount(obj, 'foo'), 0); + assert.equal(watcherCount(obj, 'foo.bar'), 0); + assert.equal(watcherCount(obj, 'foo.bar.baz'), 0); + assert.equal(watcherCount(obj.foo, 'bar'), 0); + assert.equal(watcherCount(obj.foo, 'bar.baz'), 0); + assert.equal(watcherCount(obj.foo.bar, 'baz'), 0); + } - ['@test chains with single character keys are watched correctly'](assert) { - let obj = { a: { b: { c: 1 } } }; + ['@test chains with single character keys are watched correctly'](assert) { + let obj = { a: { b: { c: 1 } } }; - watch(obj, 'a.b.c'); + watch(obj, 'a.b.c'); - assert.equal(watcherCount(obj, 'a'), 1); - assert.equal(watcherCount(obj, 'a.b'), 0); - assert.equal(watcherCount(obj, 'a.b.c'), 1); - assert.equal(watcherCount(obj.a, 'b'), 1); - assert.equal(watcherCount(obj.a, 'b.c'), 0); - assert.equal(watcherCount(obj.a.b, 'c'), 1); + assert.equal(watcherCount(obj, 'a'), 1); + assert.equal(watcherCount(obj, 'a.b'), 0); + assert.equal(watcherCount(obj, 'a.b.c'), 1); + assert.equal(watcherCount(obj.a, 'b'), 1); + assert.equal(watcherCount(obj.a, 'b.c'), 0); + assert.equal(watcherCount(obj.a.b, 'c'), 1); - unwatch(obj, 'a.b.c'); + unwatch(obj, 'a.b.c'); - assert.equal(watcherCount(obj, 'a'), 0); - assert.equal(watcherCount(obj, 'a.b'), 0); - assert.equal(watcherCount(obj, 'a.b.c'), 0); - assert.equal(watcherCount(obj.a, 'b'), 0); - assert.equal(watcherCount(obj.a, 'b.c'), 0); - assert.equal(watcherCount(obj.a.b, 'c'), 0); - } + assert.equal(watcherCount(obj, 'a'), 0); + assert.equal(watcherCount(obj, 'a.b'), 0); + assert.equal(watcherCount(obj, 'a.b.c'), 0); + assert.equal(watcherCount(obj.a, 'b'), 0); + assert.equal(watcherCount(obj.a, 'b.c'), 0); + assert.equal(watcherCount(obj.a.b, 'c'), 0); + } - ['@test writable chains is not defined more than once'](assert) { - assert.expect(0); + ['@test writable chains is not defined more than once'](assert) { + assert.expect(0); - class Base { - constructor() { - finishChains(meta(this)); - } + class Base { + constructor() { + finishChains(meta(this)); + } - didChange() {} - } + didChange() {} + } - Base.prototype.foo = { - bar: { - baz: { - value: 123, + Base.prototype.foo = { + bar: { + baz: { + value: 123, + }, }, - }, - }; - - // Define a standard computed property, which will eventually setup dependencies - defineProperty( - Base.prototype, - 'bar', - computed('foo.bar', { - get() { - return this.foo.bar; + }; + + // Define a standard computed property, which will eventually setup dependencies + defineProperty( + Base.prototype, + 'bar', + computed('foo.bar', { + get() { + return this.foo.bar; + }, + }) + ); + + // Define some aliases, which will proxy chains along + defineProperty(Base.prototype, 'baz', alias('bar.baz')); + defineProperty(Base.prototype, 'value', alias('baz.value')); + + // Define an observer, which will eagerly attempt to setup chains and watch + // their values. This follows the aliases eagerly, and forces the first + // computed to actually set up its values/dependencies for chains. If + // writableChains was not already defined, this results in multiple root + // chain nodes being defined on the same object meta. + addObserver(Base.prototype, 'value', null, 'didChange'); + + class Child extends Base {} + + let childObj = new Child(); + + set(childObj, 'foo.bar', { + baz: { + value: 456, }, - }) - ); - - // Define some aliases, which will proxy chains along - defineProperty(Base.prototype, 'baz', alias('bar.baz')); - defineProperty(Base.prototype, 'value', alias('baz.value')); - - // Define an observer, which will eagerly attempt to setup chains and watch - // their values. This follows the aliases eagerly, and forces the first - // computed to actually set up its values/dependencies for chains. If - // writableChains was not already defined, this results in multiple root - // chain nodes being defined on the same object meta. - addObserver(Base.prototype, 'value', null, 'didChange'); - - class Child extends Base {} - - let childObj = new Child(); - - set(childObj, 'foo.bar', { - baz: { - value: 456, - }, - }); + }); + } } - } -); + ); +} diff --git a/packages/@ember/-internals/metal/tests/computed_test.js b/packages/@ember/-internals/metal/tests/computed_test.js index 7c6216613f6..c6fb44a55a8 100644 --- a/packages/@ember/-internals/metal/tests/computed_test.js +++ b/packages/@ember/-internals/metal/tests/computed_test.js @@ -11,7 +11,8 @@ import { addObserver, } from '..'; import { meta as metaFor } from '@ember/-internals/meta'; -import { moduleFor, AbstractTestCase } from 'internal-test-helpers'; +import { EMBER_METAL_TRACKED_PROPERTIES } from '@ember/canary-features'; +import { moduleFor, AbstractTestCase, runLoopSettled } from 'internal-test-helpers'; let obj, count; @@ -503,67 +504,6 @@ moduleFor( obj = count = null; } - ['@test should lazily watch dependent keys on set'](assert) { - assert.equal(isWatching(obj, 'bar'), false, 'precond not watching dependent key'); - set(obj, 'foo', 'bar'); - assert.equal(isWatching(obj, 'bar'), true, 'lazily watching dependent key'); - } - - ['@test should lazily watch dependent keys on get'](assert) { - assert.equal(isWatching(obj, 'bar'), false, 'precond not watching dependent key'); - get(obj, 'foo'); - assert.equal(isWatching(obj, 'bar'), true, 'lazily watching dependent key'); - } - - ['@test local dependent key should invalidate cache'](assert) { - assert.equal(isWatching(obj, 'bar'), false, 'precond not watching dependent key'); - assert.equal(get(obj, 'foo'), 'bar 1', 'get once'); - assert.equal(isWatching(obj, 'bar'), true, 'lazily setup watching dependent key'); - assert.equal(get(obj, 'foo'), 'bar 1', 'cached retrieve'); - - set(obj, 'bar', 'BIFF'); // should invalidate foo - - assert.equal(get(obj, 'foo'), 'bar 2', 'should recache'); - assert.equal(get(obj, 'foo'), 'bar 2', 'cached retrieve'); - } - - ['@test should invalidate multiple nested dependent keys'](assert) { - let count = 0; - defineProperty( - obj, - 'bar', - computed('baz', function() { - count++; - get(this, 'baz'); - return 'baz ' + count; - }) - ); - - assert.equal(isWatching(obj, 'bar'), false, 'precond not watching dependent key'); - assert.equal(isWatching(obj, 'baz'), false, 'precond not watching dependent key'); - assert.equal(get(obj, 'foo'), 'bar 1', 'get once'); - assert.equal(isWatching(obj, 'bar'), true, 'lazily setup watching dependent key'); - assert.equal(isWatching(obj, 'baz'), true, 'lazily setup watching dependent key'); - assert.equal(get(obj, 'foo'), 'bar 1', 'cached retrieve'); - - set(obj, 'baz', 'BIFF'); // should invalidate bar -> foo - assert.equal( - isWatching(obj, 'bar'), - false, - 'should not be watching dependent key after cache cleared' - ); - assert.equal( - isWatching(obj, 'baz'), - false, - 'should not be watching dependent key after cache cleared' - ); - - assert.equal(get(obj, 'foo'), 'bar 2', 'should recache'); - assert.equal(get(obj, 'foo'), 'bar 2', 'cached retrieve'); - assert.equal(isWatching(obj, 'bar'), true, 'lazily setup watching dependent key'); - assert.equal(isWatching(obj, 'baz'), true, 'lazily setup watching dependent key'); - } - ['@test circular keys should not blow up'](assert) { let func = function() { count++; @@ -590,9 +530,7 @@ moduleFor( } ['@test redefining a property should undo old dependent keys'](assert) { - assert.equal(isWatching(obj, 'bar'), false, 'precond not watching dependent key'); assert.equal(get(obj, 'foo'), 'bar 1'); - assert.equal(isWatching(obj, 'bar'), true, 'lazily watching dependent key'); defineProperty( obj, @@ -603,12 +541,6 @@ moduleFor( }) ); - assert.equal( - isWatching(obj, 'bar'), - false, - 'after redefining should not be watching dependent key' - ); - assert.equal(get(obj, 'foo'), 'baz 2'); set(obj, 'bar', 'BIFF'); // should not kill cache @@ -660,23 +592,111 @@ moduleFor( }, /cannot contain spaces/); } - ['@test throws an assertion if an uncached `get` is called after object is destroyed'](assert) { - assert.equal(isWatching(obj, 'bar'), false, 'precond not watching dependent key'); - + ['@test throws an assertion if an uncached `get` is called after object is destroyed']() { let meta = metaFor(obj); meta.destroy(); obj.toString = () => ''; - expectAssertion(() => { - get(obj, 'foo'); - }, 'Cannot modify dependent keys for `foo` on `` after it has been destroyed.'); + let message = EMBER_METAL_TRACKED_PROPERTIES + ? 'Attempted to access the computed .foo on a destroyed object, which is not allowed' + : 'Cannot modify dependent keys for `foo` on `` after it has been destroyed.'; - assert.equal(isWatching(obj, 'bar'), false, 'deps were not updated'); + expectAssertion(() => get(obj, 'foo'), message); } } ); +if (!EMBER_METAL_TRACKED_PROPERTIES) { + moduleFor( + 'computed - dependentkey - watching', + class extends AbstractTestCase { + beforeEach() { + obj = { bar: 'baz' }; + count = 0; + let getterAndSetter = function() { + count++; + get(this, 'bar'); + return 'bar ' + count; + }; + defineProperty( + obj, + 'foo', + computed('bar', { + get: getterAndSetter, + set: getterAndSetter, + }) + ); + } + + afterEach() { + obj = count = null; + } + + ['@test should lazily watch dependent keys on set'](assert) { + assert.equal(isWatching(obj, 'bar'), false, 'precond not watching dependent key'); + set(obj, 'foo', 'bar'); + assert.equal(isWatching(obj, 'bar'), true, 'lazily watching dependent key'); + } + + ['@test should lazily watch dependent keys on get'](assert) { + assert.equal(isWatching(obj, 'bar'), false, 'precond not watching dependent key'); + get(obj, 'foo'); + assert.equal(isWatching(obj, 'bar'), true, 'lazily watching dependent key'); + } + + ['@test local dependent key should invalidate cache'](assert) { + assert.equal(isWatching(obj, 'bar'), false, 'precond not watching dependent key'); + assert.equal(get(obj, 'foo'), 'bar 1', 'get once'); + assert.equal(isWatching(obj, 'bar'), true, 'lazily setup watching dependent key'); + assert.equal(get(obj, 'foo'), 'bar 1', 'cached retrieve'); + + set(obj, 'bar', 'BIFF'); // should invalidate foo + + assert.equal(get(obj, 'foo'), 'bar 2', 'should recache'); + assert.equal(get(obj, 'foo'), 'bar 2', 'cached retrieve'); + } + + ['@test should invalidate multiple nested dependent keys'](assert) { + let count = 0; + defineProperty( + obj, + 'bar', + computed('baz', function() { + count++; + get(this, 'baz'); + return 'baz ' + count; + }) + ); + + assert.equal(isWatching(obj, 'bar'), false, 'precond not watching dependent key'); + assert.equal(isWatching(obj, 'baz'), false, 'precond not watching dependent key'); + assert.equal(get(obj, 'foo'), 'bar 1', 'get once'); + assert.equal(isWatching(obj, 'bar'), true, 'lazily setup watching dependent key'); + assert.equal(isWatching(obj, 'baz'), true, 'lazily setup watching dependent key'); + assert.equal(get(obj, 'foo'), 'bar 1', 'cached retrieve'); + + set(obj, 'baz', 'BIFF'); // should invalidate bar -> foo + assert.equal( + isWatching(obj, 'bar'), + false, + 'should not be watching dependent key after cache cleared' + ); + assert.equal( + isWatching(obj, 'baz'), + false, + 'should not be watching dependent key after cache cleared' + ); + + assert.equal(get(obj, 'foo'), 'bar 2', 'should recache'); + assert.equal(get(obj, 'foo'), 'bar 2', 'cached retrieve'); + assert.equal(isWatching(obj, 'bar'), true, 'lazily setup watching dependent key'); + assert.equal(isWatching(obj, 'baz'), true, 'lazily setup watching dependent key'); + } + } + ); +} + // .......................................................... // CHAINED DEPENDENT KEYS // @@ -900,7 +920,7 @@ moduleFor( moduleFor( 'computed - setter', class extends AbstractTestCase { - ['@test setting a watched computed property'](assert) { + async ['@test setting a watched computed property'](assert) { let obj = { firstName: 'Yehuda', lastName: 'Katz', @@ -945,12 +965,14 @@ moduleFor( assert.equal(get(obj, 'firstName'), 'Kris'); assert.equal(get(obj, 'lastName'), 'Selden'); + await runLoopSettled(); + assert.equal(fullNameDidChange, 1); assert.equal(firstNameDidChange, 1); assert.equal(lastNameDidChange, 1); } - ['@test setting a cached computed property that modifies the value you give it'](assert) { + async ['@test setting a cached computed property that modifies the value you give it'](assert) { let obj = { foo: 0, }; @@ -975,16 +997,22 @@ moduleFor( }); assert.equal(get(obj, 'plusOne'), 1); + set(obj, 'plusOne', 1); + await runLoopSettled(); + assert.equal(get(obj, 'plusOne'), 2); + set(obj, 'plusOne', 1); - assert.equal(get(obj, 'plusOne'), 2); + await runLoopSettled(); + assert.equal(get(obj, 'plusOne'), 2); assert.equal(plusOneDidChange, 1); set(obj, 'foo', 5); - assert.equal(get(obj, 'plusOne'), 6); + await runLoopSettled(); + assert.equal(get(obj, 'plusOne'), 6); assert.equal(plusOneDidChange, 2); } } @@ -993,7 +1021,7 @@ moduleFor( moduleFor( 'computed - default setter', class extends AbstractTestCase { - ["@test when setting a value on a computed property that doesn't handle sets"](assert) { + async ["@test when setting a value on a computed property that doesn't handle sets"](assert) { let obj = {}; let observerFired = false; @@ -1013,6 +1041,9 @@ moduleFor( assert.equal(get(obj, 'foo'), 'bar', 'The set value is properly returned'); assert.ok(typeof obj.foo === 'string', 'The computed property was removed'); + + await runLoopSettled(); + assert.ok(observerFired, 'The observer was still notified'); } } diff --git a/packages/@ember/-internals/metal/tests/mixin/observer_test.js b/packages/@ember/-internals/metal/tests/mixin/observer_test.js index b6dc006b5de..ae12a083a3b 100644 --- a/packages/@ember/-internals/metal/tests/mixin/observer_test.js +++ b/packages/@ember/-internals/metal/tests/mixin/observer_test.js @@ -1,10 +1,10 @@ -import { set, get, observer, mixin, Mixin, isWatching } from '../..'; -import { moduleFor, AbstractTestCase } from 'internal-test-helpers'; +import { set, get, observer, mixin, Mixin } from '../..'; +import { moduleFor, AbstractTestCase, runLoopSettled } from 'internal-test-helpers'; moduleFor( 'Mixin observer', class extends AbstractTestCase { - ['@test global observer helper'](assert) { + async ['@test global observer helper'](assert) { let MyMixin = Mixin.create({ count: 0, @@ -17,10 +17,12 @@ moduleFor( assert.equal(get(obj, 'count'), 0, 'should not invoke observer immediately'); set(obj, 'bar', 'BAZ'); + await runLoopSettled(); + assert.equal(get(obj, 'count'), 1, 'should invoke observer after change'); } - ['@test global observer helper takes multiple params'](assert) { + async ['@test global observer helper takes multiple params'](assert) { let MyMixin = Mixin.create({ count: 0, @@ -33,11 +35,15 @@ moduleFor( assert.equal(get(obj, 'count'), 0, 'should not invoke observer immediately'); set(obj, 'bar', 'BAZ'); + await runLoopSettled(); + set(obj, 'baz', 'BAZ'); + await runLoopSettled(); + assert.equal(get(obj, 'count'), 2, 'should invoke observer after change'); } - ['@test replacing observer should remove old observer'](assert) { + async ['@test replacing observer should remove old observer'](assert) { let MyMixin = Mixin.create({ count: 0, @@ -56,13 +62,17 @@ moduleFor( assert.equal(get(obj, 'count'), 0, 'should not invoke observer immediately'); set(obj, 'bar', 'BAZ'); + await runLoopSettled(); + assert.equal(get(obj, 'count'), 0, 'should not invoke observer after change'); set(obj, 'baz', 'BAZ'); + await runLoopSettled(); + assert.equal(get(obj, 'count'), 10, 'should invoke observer after change'); } - ['@test observing chain with property before'](assert) { + async ['@test observing chain with property before'](assert) { let obj2 = { baz: 'baz' }; let MyMixin = Mixin.create({ @@ -77,10 +87,12 @@ moduleFor( assert.equal(get(obj, 'count'), 0, 'should not invoke observer immediately'); set(obj2, 'baz', 'BAZ'); + await runLoopSettled(); + assert.equal(get(obj, 'count'), 1, 'should invoke observer after change'); } - ['@test observing chain with property after'](assert) { + async ['@test observing chain with property after'](assert) { let obj2 = { baz: 'baz' }; let MyMixin = Mixin.create({ @@ -95,10 +107,12 @@ moduleFor( assert.equal(get(obj, 'count'), 0, 'should not invoke observer immediately'); set(obj2, 'baz', 'BAZ'); + await runLoopSettled(); + assert.equal(get(obj, 'count'), 1, 'should invoke observer after change'); } - ['@test observing chain with property in mixin applied later'](assert) { + async ['@test observing chain with property in mixin applied later'](assert) { let obj2 = { baz: 'baz' }; let MyMixin = Mixin.create({ @@ -117,10 +131,12 @@ moduleFor( assert.equal(get(obj, 'count'), 0, 'should not invoke observer immediately'); set(obj2, 'baz', 'BAZ'); + await runLoopSettled(); + assert.equal(get(obj, 'count'), 1, 'should invoke observer after change'); } - ['@test observing chain with existing property'](assert) { + async ['@test observing chain with existing property'](assert) { let obj2 = { baz: 'baz' }; let MyMixin = Mixin.create({ @@ -134,10 +150,12 @@ moduleFor( assert.equal(get(obj, 'count'), 0, 'should not invoke observer immediately'); set(obj2, 'baz', 'BAZ'); + await runLoopSettled(); + assert.equal(get(obj, 'count'), 1, 'should invoke observer after change'); } - ['@test observing chain with property in mixin before'](assert) { + async ['@test observing chain with property in mixin before'](assert) { let obj2 = { baz: 'baz' }; let MyMixin2 = Mixin.create({ bar: obj2 }); @@ -152,10 +170,12 @@ moduleFor( assert.equal(get(obj, 'count'), 0, 'should not invoke observer immediately'); set(obj2, 'baz', 'BAZ'); + await runLoopSettled(); + assert.equal(get(obj, 'count'), 1, 'should invoke observer after change'); } - ['@test observing chain with property in mixin after'](assert) { + async ['@test observing chain with property in mixin after'](assert) { let obj2 = { baz: 'baz' }; let MyMixin2 = Mixin.create({ bar: obj2 }); @@ -170,10 +190,12 @@ moduleFor( assert.equal(get(obj, 'count'), 0, 'should not invoke observer immediately'); set(obj2, 'baz', 'BAZ'); + await runLoopSettled(); + assert.equal(get(obj, 'count'), 1, 'should invoke observer after change'); } - ['@test observing chain with overridden property'](assert) { + async ['@test observing chain with overridden property'](assert) { let obj2 = { baz: 'baz' }; let obj3 = { baz: 'foo' }; @@ -189,13 +211,14 @@ moduleFor( let obj = mixin({ bar: obj2 }, MyMixin, MyMixin2); assert.equal(get(obj, 'count'), 0, 'should not invoke observer immediately'); - assert.equal(isWatching(obj2, 'baz'), false, 'should not be watching baz'); - assert.equal(isWatching(obj3, 'baz'), true, 'should be watching baz'); - set(obj2, 'baz', 'BAZ'); + await runLoopSettled(); + assert.equal(get(obj, 'count'), 0, 'should not invoke observer after change'); set(obj3, 'baz', 'BEAR'); + await runLoopSettled(); + assert.equal(get(obj, 'count'), 1, 'should invoke observer after change'); } } diff --git a/packages/@ember/-internals/metal/tests/observer_test.js b/packages/@ember/-internals/metal/tests/observer_test.js index d84c06438c4..7798dfc5bd5 100644 --- a/packages/@ember/-internals/metal/tests/observer_test.js +++ b/packages/@ember/-internals/metal/tests/observer_test.js @@ -1,4 +1,5 @@ import { ENV } from '@ember/-internals/environment'; +import { EMBER_METAL_TRACKED_PROPERTIES } from '@ember/canary-features'; import { addObserver, removeObserver, @@ -15,7 +16,7 @@ import { get, set, } from '..'; -import { moduleFor, AbstractTestCase } from 'internal-test-helpers'; +import { moduleFor, AbstractTestCase, runLoopSettled } from 'internal-test-helpers'; import { FUNCTION_PROTOTYPE_EXTENSIONS } from '@ember/deprecated-features'; function K() {} @@ -37,7 +38,7 @@ moduleFor( }, 'observer called without a function'); } - ['@test observer should fire when property is modified'](assert) { + async ['@test observer should fire when property is modified'](assert) { let obj = {}; let count = 0; @@ -47,10 +48,12 @@ moduleFor( }); set(obj, 'foo', 'bar'); + await runLoopSettled(); + assert.equal(count, 1, 'should have invoked observer'); } - ['@test observer should fire when dependent property is modified'](assert) { + async ['@test observer should fire when dependent property is modified'](assert) { let obj = { bar: 'bar' }; defineProperty( obj, @@ -69,10 +72,14 @@ moduleFor( }); set(obj, 'bar', 'baz'); + await runLoopSettled(); + assert.equal(count, 1, 'should have invoked observer'); } - ['@test observer should continue to fire after dependent properties are accessed'](assert) { + async ['@test observer should continue to fire after dependent properties are accessed']( + assert + ) { let observerCount = 0; let obj = {}; @@ -99,12 +106,13 @@ moduleFor( for (let i = 0; i < 10; i++) { notifyPropertyChange(obj, 'prop'); + await runLoopSettled(); } assert.equal(observerCount, 10, 'should continue to fire indefinitely'); } - ['@test observer added via Function.prototype extensions and brace expansion should fire when property changes']( + async ['@test observer added via Function.prototype extensions and brace expansion should fire when property changes']( assert ) { if (!FUNCTION_PROTOTYPE_EXTENSIONS && ENV.EXTEND_PROTOTYPES.Function) { @@ -120,19 +128,25 @@ moduleFor( }, /Function prototype extensions have been deprecated, please migrate from function\(\){}.observes\('foo'\) to observer\('foo', function\(\) {}\)/); set(obj, 'foo', 'foo'); + await runLoopSettled(); + assert.equal(count, 1, 'observer specified via brace expansion invoked on property change'); set(obj, 'bar', 'bar'); + await runLoopSettled(); + assert.equal(count, 2, 'observer specified via brace expansion invoked on property change'); set(obj, 'baz', 'baz'); + await runLoopSettled(); + assert.equal(count, 2, 'observer not invoked on unspecified property'); } else { assert.expect(0); } } - ['@test observer specified via Function.prototype extensions via brace expansion should fire when dependent property changes']( + async ['@test observer specified via Function.prototype extensions via brace expansion should fire when dependent property changes']( assert ) { if (!FUNCTION_PROTOTYPE_EXTENSIONS && ENV.EXTEND_PROTOTYPES.Function) { @@ -165,6 +179,8 @@ moduleFor( get(obj, 'foo'); set(obj, 'baz', 'Baz'); + await runLoopSettled(); + // fire once for foo, once for bar assert.equal( count, @@ -173,13 +189,15 @@ moduleFor( ); set(obj, 'quux', 'Quux'); + await runLoopSettled(); + assert.equal(count, 2, 'observer not fired on unspecified property'); } else { assert.expect(0); } } - ['@test observers watching multiple properties via brace expansion should fire when the properties change']( + async ['@test observers watching multiple properties via brace expansion should fire when the properties change']( assert ) { let obj = {}; @@ -192,16 +210,22 @@ moduleFor( }); set(obj, 'foo', 'foo'); + await runLoopSettled(); + assert.equal(count, 1, 'observer specified via brace expansion invoked on property change'); set(obj, 'bar', 'bar'); + await runLoopSettled(); + assert.equal(count, 2, 'observer specified via brace expansion invoked on property change'); set(obj, 'baz', 'baz'); + await runLoopSettled(); + assert.equal(count, 2, 'observer not invoked on unspecified property'); } - ['@test observers watching multiple properties via brace expansion should fire when dependent properties change']( + async ['@test observers watching multiple properties via brace expansion should fire when dependent properties change']( assert ) { let obj = { baz: 'Initial' }; @@ -231,6 +255,8 @@ moduleFor( get(obj, 'foo'); set(obj, 'baz', 'Baz'); + await runLoopSettled(); + // fire once for foo, once for bar assert.equal( count, @@ -239,10 +265,17 @@ moduleFor( ); set(obj, 'quux', 'Quux'); + await runLoopSettled(); + assert.equal(count, 2, 'observer not fired on unspecified property'); } ['@test nested observers should fire in order'](assert) { + if (EMBER_METAL_TRACKED_PROPERTIES) { + // We can no longer guarantee order + return assert.expect(0); + } + let obj = { foo: 'foo', bar: 'bar' }; let fooCount = 0; let barCount = 0; @@ -261,7 +294,7 @@ moduleFor( assert.equal(fooCount, 1, 'foo should have fired'); } - ['@test removing an chain observer on change should not fail'](assert) { + async ['@test removing an chain observer on change should not fail'](assert) { let foo = { bar: 'bar' }; let obj1 = { foo: foo }; let obj2 = { foo: foo }; @@ -294,6 +327,7 @@ moduleFor( addObserver(obj4, 'foo.bar', observer4); set(foo, 'bar', 'baz'); + await runLoopSettled(); assert.equal(count1, 1, 'observer1 fired'); assert.equal(count2, 1, 'observer2 fired'); @@ -301,7 +335,7 @@ moduleFor( assert.equal(count4, 0, 'observer4 did not fire'); } - ['@test deferring property change notifications'](assert) { + async ['@test deferring property change notifications'](assert) { let obj = { foo: 'foo' }; let fooCount = 0; @@ -309,15 +343,28 @@ moduleFor( fooCount++; }); - beginPropertyChanges(); + if (!EMBER_METAL_TRACKED_PROPERTIES) { + beginPropertyChanges(); + } + set(obj, 'foo', 'BIFF'); set(obj, 'foo', 'BAZ'); - endPropertyChanges(); + + if (!EMBER_METAL_TRACKED_PROPERTIES) { + endPropertyChanges(); + } + + await runLoopSettled(); assert.equal(fooCount, 1, 'foo should have fired once'); } - ['@test deferring property change notifications safely despite exceptions'](assert) { + async ['@test deferring property change notifications safely despite exceptions'](assert) { + if (EMBER_METAL_TRACKED_PROPERTIES) { + // changeProperties isn't a thing anymore + return assert.expect(0); + } + let obj = { foo: 'foo' }; let fooCount = 0; let exc = new Error('Something unexpected happened!'); @@ -350,6 +397,11 @@ moduleFor( } ['@test addObserver should propagate through prototype'](assert) { + if (EMBER_METAL_TRACKED_PROPERTIES) { + // We no longer inherit unless it's an EmberObject + return assert.expect(0); + } + let obj = { foo: 'foo', count: 0 }; let obj2; @@ -369,7 +421,7 @@ moduleFor( assert.equal(obj2.count, 0, 'should not have invoked observer on inherited'); } - ['@test addObserver should respect targets with methods'](assert) { + async ['@test addObserver should respect targets with methods'](assert) { let observed = { foo: 'foo' }; let target1 = { @@ -402,11 +454,13 @@ moduleFor( addObserver(observed, 'foo', target2, target2.didChange); set(observed, 'foo', 'BAZ'); + await runLoopSettled(); + assert.equal(target1.count, 1, 'target1 observer should have fired'); assert.equal(target2.count, 1, 'target2 observer should have fired'); } - ['@test addObserver should allow multiple objects to observe a property'](assert) { + async ['@test addObserver should allow multiple objects to observe a property'](assert) { let observed = { foo: 'foo' }; let target1 = { @@ -429,6 +483,8 @@ moduleFor( addObserver(observed, 'foo', target2, 'didChange'); set(observed, 'foo', 'BAZ'); + await runLoopSettled(); + assert.equal(target1.count, 1, 'target1 observer should have fired'); assert.equal(target2.count, 1, 'target2 observer should have fired'); } @@ -442,7 +498,7 @@ moduleFor( moduleFor( 'removeObserver', class extends AbstractTestCase { - ['@test removing observer should stop firing'](assert) { + async ['@test removing observer should stop firing'](assert) { let obj = {}; let count = 0; function F() { @@ -451,15 +507,19 @@ moduleFor( addObserver(obj, 'foo', F); set(obj, 'foo', 'bar'); + await runLoopSettled(); + assert.equal(count, 1, 'should have invoked observer'); removeObserver(obj, 'foo', F); set(obj, 'foo', 'baz'); + await runLoopSettled(); + assert.equal(count, 1, "removed observer shouldn't fire"); } - ['@test local observers can be removed'](assert) { + async ['@test local observers can be removed'](assert) { let barObserved = 0; let MyMixin = Mixin.create({ @@ -476,17 +536,20 @@ moduleFor( MyMixin.apply(obj); set(obj, 'bar', 'HI!'); + await runLoopSettled(); + assert.equal(barObserved, 2, 'precond - observers should be fired'); removeObserver(obj, 'bar', null, 'foo1'); barObserved = 0; set(obj, 'bar', 'HI AGAIN!'); + await runLoopSettled(); assert.equal(barObserved, 1, 'removed observers should not be called'); } - ['@test removeObserver should respect targets with methods'](assert) { + async ['@test removeObserver should respect targets with methods'](assert) { let observed = { foo: 'foo' }; let target1 = { @@ -509,6 +572,8 @@ moduleFor( addObserver(observed, 'foo', target2, target2.didChange); set(observed, 'foo', 'BAZ'); + await runLoopSettled(); + assert.equal(target1.count, 1, 'target1 observer should have fired'); assert.equal(target2.count, 1, 'target2 observer should have fired'); @@ -517,6 +582,8 @@ moduleFor( target1.count = target2.count = 0; set(observed, 'foo', 'BAZ'); + await runLoopSettled(); + assert.equal(target1.count, 0, 'target1 observer should not fire again'); assert.equal(target2.count, 0, 'target2 observer should not fire again'); } @@ -559,7 +626,7 @@ moduleFor( obj = count = null; } - ['@test depending on a chain with a computed property'](assert) { + async ['@test depending on a chain with a computed property'](assert) { defineProperty( obj, 'computed', @@ -580,11 +647,12 @@ moduleFor( ); set(obj, 'computed.foo', 'baz'); + await runLoopSettled(); assert.equal(changed, 1, 'should fire observer'); } - ['@test depending on a simple chain'](assert) { + async ['@test depending on a simple chain'](assert) { let val; addObserver(obj, 'foo.bar.baz.biff', function(target, key) { val = get(target, key); @@ -592,36 +660,50 @@ moduleFor( }); set(get(obj, 'foo.bar.baz'), 'biff', 'BUZZ'); + await runLoopSettled(); + assert.equal(val, 'BUZZ'); assert.equal(count, 1); set(get(obj, 'foo.bar'), 'baz', { biff: 'BLARG' }); + await runLoopSettled(); + assert.equal(val, 'BLARG'); assert.equal(count, 2); set(get(obj, 'foo'), 'bar', { baz: { biff: 'BOOM' } }); + await runLoopSettled(); + assert.equal(val, 'BOOM'); assert.equal(count, 3); set(obj, 'foo', { bar: { baz: { biff: 'BLARG' } } }); + await runLoopSettled(); + assert.equal(val, 'BLARG'); assert.equal(count, 4); set(get(obj, 'foo.bar.baz'), 'biff', 'BUZZ'); + await runLoopSettled(); + assert.equal(val, 'BUZZ'); assert.equal(count, 5); let foo = get(obj, 'foo'); set(obj, 'foo', 'BOO'); + await runLoopSettled(); + assert.equal(val, undefined); assert.equal(count, 6); set(foo.bar.baz, 'biff', 'BOOM'); + await runLoopSettled(); + assert.equal(count, 6, 'should be not have invoked observer'); } - ['@test depending on a chain with a capitalized first key'](assert) { + async ['@test depending on a chain with a capitalized first key'](assert) { let val; addObserver(obj, 'Capital.foo.bar.baz.biff', function(target, key) { @@ -630,32 +712,46 @@ moduleFor( }); set(get(obj, 'Capital.foo.bar.baz'), 'biff', 'BUZZ'); + await runLoopSettled(); + assert.equal(val, 'BUZZ'); assert.equal(count, 1); set(get(obj, 'Capital.foo.bar'), 'baz', { biff: 'BLARG' }); + await runLoopSettled(); + assert.equal(val, 'BLARG'); assert.equal(count, 2); set(get(obj, 'Capital.foo'), 'bar', { baz: { biff: 'BOOM' } }); + await runLoopSettled(); + assert.equal(val, 'BOOM'); assert.equal(count, 3); set(obj, 'Capital.foo', { bar: { baz: { biff: 'BLARG' } } }); + await runLoopSettled(); + assert.equal(val, 'BLARG'); assert.equal(count, 4); set(get(obj, 'Capital.foo.bar.baz'), 'biff', 'BUZZ'); + await runLoopSettled(); + assert.equal(val, 'BUZZ'); assert.equal(count, 5); let foo = get(obj, 'foo'); set(obj, 'Capital.foo', 'BOO'); + await runLoopSettled(); + assert.equal(val, undefined); assert.equal(count, 6); set(foo.bar.baz, 'biff', 'BOOM'); + await runLoopSettled(); + assert.equal(count, 6, 'should be not have invoked observer'); } } @@ -668,7 +764,7 @@ moduleFor( moduleFor( 'props/observer_test - setting identical values', class extends AbstractTestCase { - ['@test setting simple prop should not trigger'](assert) { + async ['@test setting simple prop should not trigger'](assert) { let obj = { foo: 'bar' }; let count = 0; @@ -677,19 +773,27 @@ moduleFor( }); set(obj, 'foo', 'bar'); + await runLoopSettled(); + assert.equal(count, 0, 'should not trigger observer'); set(obj, 'foo', 'baz'); + await runLoopSettled(); + assert.equal(count, 1, 'should trigger observer'); set(obj, 'foo', 'baz'); + await runLoopSettled(); + assert.equal(count, 1, 'should not trigger observer again'); } // The issue here is when a computed property is directly set with a value, then has a // dependent key change (which triggers a cache expiration and recomputation), observers will // not be fired if the CP setter is called with the last set value. - ['@test setting a cached computed property whose value has changed should trigger'](assert) { + async ['@test setting a cached computed property whose value has changed should trigger']( + assert + ) { let obj = {}; defineProperty( @@ -710,129 +814,137 @@ moduleFor( addObserver(obj, 'foo', function() { count++; }); - set(obj, 'foo', 'bar'); + await runLoopSettled(); + assert.equal(count, 1); assert.equal(get(obj, 'foo'), 'bar'); set(obj, 'baz', 'qux'); + await runLoopSettled(); + assert.equal(count, 2); assert.equal(get(obj, 'foo'), 'qux'); - get(obj, 'foo'); set(obj, 'foo', 'bar'); + await runLoopSettled(); + assert.equal(count, 3); assert.equal(get(obj, 'foo'), 'bar'); } } ); -moduleFor( - 'changeProperties', - class extends AbstractTestCase { - ['@test observers added/removed during changeProperties should do the right thing.'](assert) { - let obj = { - foo: 0, - }; - function Observer() { - this.didChangeCount = 0; - } - Observer.prototype = { - add() { - addObserver(obj, 'foo', this, 'didChange'); - }, - remove() { - removeObserver(obj, 'foo', this, 'didChange'); - }, - didChange() { - this.didChangeCount++; - }, - }; - let addedBeforeFirstChangeObserver = new Observer(); - let addedAfterFirstChangeObserver = new Observer(); - let addedAfterLastChangeObserver = new Observer(); - let removedBeforeFirstChangeObserver = new Observer(); - let removedBeforeLastChangeObserver = new Observer(); - let removedAfterLastChangeObserver = new Observer(); - removedBeforeFirstChangeObserver.add(); - removedBeforeLastChangeObserver.add(); - removedAfterLastChangeObserver.add(); - changeProperties(function() { - removedBeforeFirstChangeObserver.remove(); - addedBeforeFirstChangeObserver.add(); +if (!EMBER_METAL_TRACKED_PROPERTIES) { + moduleFor( + 'changeProperties', + class extends AbstractTestCase { + ['@test observers added/removed during changeProperties should do the right thing.'](assert) { + let obj = { + foo: 0, + }; + function Observer() { + this.didChangeCount = 0; + } + Observer.prototype = { + add() { + addObserver(obj, 'foo', this, 'didChange'); + }, + remove() { + removeObserver(obj, 'foo', this, 'didChange'); + }, + didChange() { + this.didChangeCount++; + }, + }; + let addedBeforeFirstChangeObserver = new Observer(); + let addedAfterFirstChangeObserver = new Observer(); + let addedAfterLastChangeObserver = new Observer(); + let removedBeforeFirstChangeObserver = new Observer(); + let removedBeforeLastChangeObserver = new Observer(); + let removedAfterLastChangeObserver = new Observer(); + removedBeforeFirstChangeObserver.add(); + removedBeforeLastChangeObserver.add(); + removedAfterLastChangeObserver.add(); + changeProperties(function() { + removedBeforeFirstChangeObserver.remove(); + addedBeforeFirstChangeObserver.add(); - set(obj, 'foo', 1); + set(obj, 'foo', 1); - assert.equal( - addedBeforeFirstChangeObserver.didChangeCount, - 0, - 'addObserver called before the first change is deferred' - ); + assert.equal( + addedBeforeFirstChangeObserver.didChangeCount, + 0, + 'addObserver called before the first change is deferred' + ); - addedAfterFirstChangeObserver.add(); - removedBeforeLastChangeObserver.remove(); + addedAfterFirstChangeObserver.add(); + removedBeforeLastChangeObserver.remove(); - set(obj, 'foo', 2); + set(obj, 'foo', 2); + assert.equal( + addedAfterFirstChangeObserver.didChangeCount, + 0, + 'addObserver called after the first change is deferred' + ); + + addedAfterLastChangeObserver.add(); + removedAfterLastChangeObserver.remove(); + }); + + assert.equal( + removedBeforeFirstChangeObserver.didChangeCount, + 0, + 'removeObserver called before the first change sees none' + ); + assert.equal( + addedBeforeFirstChangeObserver.didChangeCount, + 1, + 'addObserver called before the first change sees only 1' + ); assert.equal( addedAfterFirstChangeObserver.didChangeCount, + 1, + 'addObserver called after the first change sees 1' + ); + assert.equal( + addedAfterLastChangeObserver.didChangeCount, + 1, + 'addObserver called after the last change sees 1' + ); + assert.equal( + removedBeforeLastChangeObserver.didChangeCount, 0, - 'addObserver called after the first change is deferred' + 'removeObserver called before the last change sees none' ); + assert.equal( + removedAfterLastChangeObserver.didChangeCount, + 0, + 'removeObserver called after the last change sees none' + ); + } - addedAfterLastChangeObserver.add(); - removedAfterLastChangeObserver.remove(); - }); - - assert.equal( - removedBeforeFirstChangeObserver.didChangeCount, - 0, - 'removeObserver called before the first change sees none' - ); - assert.equal( - addedBeforeFirstChangeObserver.didChangeCount, - 1, - 'addObserver called before the first change sees only 1' - ); - assert.equal( - addedAfterFirstChangeObserver.didChangeCount, - 1, - 'addObserver called after the first change sees 1' - ); - assert.equal( - addedAfterLastChangeObserver.didChangeCount, - 1, - 'addObserver called after the last change sees 1' - ); - assert.equal( - removedBeforeLastChangeObserver.didChangeCount, - 0, - 'removeObserver called before the last change sees none' - ); - assert.equal( - removedAfterLastChangeObserver.didChangeCount, - 0, - 'removeObserver called after the last change sees none' - ); - } - - ['@test calling changeProperties while executing deferred observers works correctly'](assert) { - let obj = { foo: 0 }; - let fooDidChange = 0; + ['@test calling changeProperties while executing deferred observers works correctly']( + assert + ) { + let obj = { foo: 0 }; + let fooDidChange = 0; - addObserver(obj, 'foo', () => { - fooDidChange++; - changeProperties(() => {}); - }); + addObserver(obj, 'foo', () => { + fooDidChange++; + changeProperties(() => {}); + }); - changeProperties(() => { - set(obj, 'foo', 1); - }); + changeProperties(() => { + set(obj, 'foo', 1); + }); - assert.equal(fooDidChange, 1); + assert.equal(fooDidChange, 1); + } } - } -); + ); +} moduleFor( 'Keys behavior with observers', diff --git a/packages/@ember/-internals/metal/tests/performance_test.js b/packages/@ember/-internals/metal/tests/performance_test.js index a820ffeb8a0..0a591a25202 100644 --- a/packages/@ember/-internals/metal/tests/performance_test.js +++ b/packages/@ember/-internals/metal/tests/performance_test.js @@ -8,7 +8,7 @@ import { endPropertyChanges, addObserver, } from '..'; -import { moduleFor, AbstractTestCase } from 'internal-test-helpers'; +import { moduleFor, AbstractTestCase, runLoopSettled } from 'internal-test-helpers'; /* This test file is designed to capture performance regressions related to @@ -20,7 +20,7 @@ import { moduleFor, AbstractTestCase } from 'internal-test-helpers'; moduleFor( 'Computed Properties - Number of times evaluated', class extends AbstractTestCase { - ['@test computed properties that depend on multiple properties should run only once per run loop']( + async ['@test computed properties that depend on multiple properties should run only once per run loop']( assert ) { let obj = { a: 'a', b: 'b', c: 'c' }; @@ -52,6 +52,8 @@ moduleFor( get(obj, 'abc'); + await runLoopSettled(); + assert.equal(cpCount, 1, 'The computed property is only invoked once'); assert.equal(obsCount, 1, 'The observer is only invoked once'); } diff --git a/packages/@ember/-internals/metal/tests/tracked/classic_classes_test.js b/packages/@ember/-internals/metal/tests/tracked/classic_classes_test.js index abf8e0e6725..5ad6b333555 100644 --- a/packages/@ember/-internals/metal/tests/tracked/classic_classes_test.js +++ b/packages/@ember/-internals/metal/tests/tracked/classic_classes_test.js @@ -1,7 +1,5 @@ import { AbstractTestCase, moduleFor } from 'internal-test-helpers'; -import { defineProperty, tracked, nativeDescDecorator } from '../..'; - -import { track } from './support'; +import { defineProperty, tracked, track, nativeDescDecorator } from '../..'; import { EMBER_METAL_TRACKED_PROPERTIES, diff --git a/packages/@ember/-internals/metal/tests/tracked/support.js b/packages/@ember/-internals/metal/tests/tracked/support.js deleted file mode 100644 index e58a979df9f..00000000000 --- a/packages/@ember/-internals/metal/tests/tracked/support.js +++ /dev/null @@ -1,17 +0,0 @@ -import { getCurrentTracker, setCurrentTracker } from '../..'; - -/** - Creates an autotrack stack so we can test field changes as they flow through - getters/setters, and through the system overall - - @private -*/ -export function track(fn) { - let parent = getCurrentTracker(); - let tracker = setCurrentTracker(); - - fn(); - - setCurrentTracker(parent); - return tracker.combine(); -} diff --git a/packages/@ember/-internals/metal/tests/tracked/validation_test.js b/packages/@ember/-internals/metal/tests/tracked/validation_test.js index 9047877ed74..25782d10b66 100644 --- a/packages/@ember/-internals/metal/tests/tracked/validation_test.js +++ b/packages/@ember/-internals/metal/tests/tracked/validation_test.js @@ -5,6 +5,7 @@ import { set, tagForProperty, tracked, + track, notifyPropertyChange, } from '../..'; @@ -14,7 +15,6 @@ import { } from '@ember/canary-features'; import { EMBER_ARRAY } from '@ember/-internals/utils'; import { AbstractTestCase, moduleFor } from 'internal-test-helpers'; -import { track } from './support'; if (EMBER_METAL_TRACKED_PROPERTIES && EMBER_NATIVE_DECORATOR_SUPPORT) { moduleFor( @@ -184,7 +184,7 @@ if (EMBER_METAL_TRACKED_PROPERTIES && EMBER_NATIVE_DECORATOR_SUPPORT) { defineProperty( EmberObject.prototype, 'full', - computed('name', function() { + computed('name.first', 'name.last', function() { let name = get(this, 'name'); return `${name.first} ${name.last}`; }) diff --git a/packages/@ember/-internals/metal/tests/watching/is_watching_test.js b/packages/@ember/-internals/metal/tests/watching/is_watching_test.js index 19cbbed261b..508d98fda21 100644 --- a/packages/@ember/-internals/metal/tests/watching/is_watching_test.js +++ b/packages/@ember/-internals/metal/tests/watching/is_watching_test.js @@ -8,6 +8,7 @@ import { removeObserver, isWatching, } from '../..'; +import { EMBER_METAL_TRACKED_PROPERTIES } from '@ember/canary-features'; import { moduleFor, AbstractTestCase } from 'internal-test-helpers'; function testObserver(assert, setup, teardown, key = 'key') { @@ -20,77 +21,79 @@ function testObserver(assert, setup, teardown, key = 'key') { assert.equal(isWatching(obj, key), false, 'isWatching is false after observers are removed'); } -moduleFor( - 'isWatching', - class extends AbstractTestCase { - ['@test isWatching is true for regular local observers'](assert) { - testObserver( - assert, - (obj, key, fn) => { - Mixin.create({ - [fn]: observer(key, function() {}), - }).apply(obj); - }, - (obj, key, fn) => removeObserver(obj, key, obj, fn) - ); - } +if (!EMBER_METAL_TRACKED_PROPERTIES) { + moduleFor( + 'isWatching', + class extends AbstractTestCase { + ['@test isWatching is true for regular local observers'](assert) { + testObserver( + assert, + (obj, key, fn) => { + Mixin.create({ + [fn]: observer(key, function() {}), + }).apply(obj); + }, + (obj, key, fn) => removeObserver(obj, key, obj, fn) + ); + } - ['@test isWatching is true for nonlocal observers'](assert) { - testObserver( - assert, - (obj, key, fn) => { - addObserver(obj, key, obj, fn); - }, - (obj, key, fn) => removeObserver(obj, key, obj, fn) - ); - } + ['@test isWatching is true for nonlocal observers'](assert) { + testObserver( + assert, + (obj, key, fn) => { + addObserver(obj, key, obj, fn); + }, + (obj, key, fn) => removeObserver(obj, key, obj, fn) + ); + } - ['@test isWatching is true for chained observers'](assert) { - testObserver( - assert, - function(obj, key, fn) { - addObserver(obj, key + '.bar', obj, fn); - }, - function(obj, key, fn) { - removeObserver(obj, key + '.bar', obj, fn); - } - ); - } + ['@test isWatching is true for chained observers'](assert) { + testObserver( + assert, + function(obj, key, fn) { + addObserver(obj, key + '.bar', obj, fn); + }, + function(obj, key, fn) { + removeObserver(obj, key + '.bar', obj, fn); + } + ); + } - ['@test isWatching is true for computed properties'](assert) { - testObserver( - assert, - (obj, key, fn) => { - defineProperty(obj, fn, computed(key, function() {})); - get(obj, fn); - }, - (obj, key, fn) => defineProperty(obj, fn, null) - ); - } + ['@test isWatching is true for computed properties'](assert) { + testObserver( + assert, + (obj, key, fn) => { + defineProperty(obj, fn, computed(key, function() {})); + get(obj, fn); + }, + (obj, key, fn) => defineProperty(obj, fn, null) + ); + } - ['@test isWatching is true for chained computed properties'](assert) { - testObserver( - assert, - (obj, key, fn) => { - defineProperty(obj, fn, computed(key + '.bar', function() {})); - get(obj, fn); - }, - (obj, key, fn) => defineProperty(obj, fn, null) - ); - } + ['@test isWatching is true for chained computed properties'](assert) { + testObserver( + assert, + (obj, key, fn) => { + defineProperty(obj, fn, computed(key + '.bar', function() {})); + get(obj, fn); + }, + (obj, key, fn) => defineProperty(obj, fn, null) + ); + } - // can't watch length on Array - it is special... - // But you should be able to watch a length property of an object - ["@test isWatching is true for 'length' property on object"](assert) { - testObserver( - assert, - (obj, key, fn) => { - defineProperty(obj, 'length', null, '26.2 miles'); - addObserver(obj, 'length', obj, fn); - }, - (obj, key, fn) => removeObserver(obj, 'length', obj, fn), - 'length' - ); + // can't watch length on Array - it is special... + // But you should be able to watch a length property of an object + ["@test isWatching is true for 'length' property on object"](assert) { + testObserver( + assert, + (obj, key, fn) => { + defineProperty(obj, 'length', null, '26.2 miles'); + addObserver(obj, 'length', obj, fn); + }, + (obj, key, fn) => removeObserver(obj, 'length', obj, fn), + 'length' + ); + } } - } -); + ); +} diff --git a/packages/@ember/-internals/metal/tests/watching/unwatch_test.js b/packages/@ember/-internals/metal/tests/watching/unwatch_test.js index e66e103a6e6..42f93ca3462 100644 --- a/packages/@ember/-internals/metal/tests/watching/unwatch_test.js +++ b/packages/@ember/-internals/metal/tests/watching/unwatch_test.js @@ -1,5 +1,6 @@ import { watch, unwatch, defineProperty, addListener, computed, set } from '../..'; import { moduleFor, AbstractTestCase } from 'internal-test-helpers'; +import { EMBER_METAL_TRACKED_PROPERTIES } from '@ember/canary-features'; let didCount; @@ -7,103 +8,105 @@ function addListeners(obj, keyPath) { addListener(obj, keyPath + ':change', () => didCount++); } -moduleFor( - 'unwatch', - class extends AbstractTestCase { - beforeEach() { - didCount = 0; - } - - ['@test unwatching a computed property - regular get/set'](assert) { - let obj = {}; - - defineProperty( - obj, - 'foo', - computed({ - get() { - return this.__foo; +if (!EMBER_METAL_TRACKED_PROPERTIES) { + moduleFor( + 'unwatch', + class extends AbstractTestCase { + beforeEach() { + didCount = 0; + } + + ['@test unwatching a computed property - regular get/set'](assert) { + let obj = {}; + + defineProperty( + obj, + 'foo', + computed({ + get() { + return this.__foo; + }, + set(keyName, value) { + this.__foo = value; + return this.__foo; + }, + }) + ); + addListeners(obj, 'foo'); + + watch(obj, 'foo'); + set(obj, 'foo', 'bar'); + assert.equal(didCount, 1, 'should have invoked didCount'); + + unwatch(obj, 'foo'); + didCount = 0; + set(obj, 'foo', 'BAZ'); + assert.equal(didCount, 0, 'should NOT have invoked didCount'); + } + + ['@test unwatching a regular property - regular get/set'](assert) { + let obj = { foo: 'BIFF' }; + addListeners(obj, 'foo'); + + watch(obj, 'foo'); + set(obj, 'foo', 'bar'); + assert.equal(didCount, 1, 'should have invoked didCount'); + + unwatch(obj, 'foo'); + didCount = 0; + set(obj, 'foo', 'BAZ'); + assert.equal(didCount, 0, 'should NOT have invoked didCount'); + } + + ['@test unwatching should be nested'](assert) { + let obj = { foo: 'BIFF' }; + addListeners(obj, 'foo'); + + watch(obj, 'foo'); + watch(obj, 'foo'); + set(obj, 'foo', 'bar'); + assert.equal(didCount, 1, 'should have invoked didCount'); + + unwatch(obj, 'foo'); + didCount = 0; + set(obj, 'foo', 'BAZ'); + assert.equal(didCount, 1, 'should NOT have invoked didCount'); + + unwatch(obj, 'foo'); + didCount = 0; + set(obj, 'foo', 'BAZ'); + assert.equal(didCount, 0, 'should NOT have invoked didCount'); + } + + ['@test unwatching "length" property on an object'](assert) { + let obj = { foo: 'RUN' }; + addListeners(obj, 'length'); + + // Can watch length when it is undefined + watch(obj, 'length'); + set(obj, 'length', '10k'); + assert.equal(didCount, 1, 'should have invoked didCount'); + + // Should stop watching despite length now being defined (making object 'array-like') + unwatch(obj, 'length'); + didCount = 0; + set(obj, 'length', '5k'); + assert.equal(didCount, 0, 'should NOT have invoked didCount'); + } + + ['@test unwatching should not destroy non MANDATORY_SETTER descriptor'](assert) { + let obj = { + get foo() { + return 'RUN'; }, - set(keyName, value) { - this.__foo = value; - return this.__foo; - }, - }) - ); - addListeners(obj, 'foo'); - - watch(obj, 'foo'); - set(obj, 'foo', 'bar'); - assert.equal(didCount, 1, 'should have invoked didCount'); - - unwatch(obj, 'foo'); - didCount = 0; - set(obj, 'foo', 'BAZ'); - assert.equal(didCount, 0, 'should NOT have invoked didCount'); - } - - ['@test unwatching a regular property - regular get/set'](assert) { - let obj = { foo: 'BIFF' }; - addListeners(obj, 'foo'); - - watch(obj, 'foo'); - set(obj, 'foo', 'bar'); - assert.equal(didCount, 1, 'should have invoked didCount'); - - unwatch(obj, 'foo'); - didCount = 0; - set(obj, 'foo', 'BAZ'); - assert.equal(didCount, 0, 'should NOT have invoked didCount'); - } - - ['@test unwatching should be nested'](assert) { - let obj = { foo: 'BIFF' }; - addListeners(obj, 'foo'); - - watch(obj, 'foo'); - watch(obj, 'foo'); - set(obj, 'foo', 'bar'); - assert.equal(didCount, 1, 'should have invoked didCount'); - - unwatch(obj, 'foo'); - didCount = 0; - set(obj, 'foo', 'BAZ'); - assert.equal(didCount, 1, 'should NOT have invoked didCount'); - - unwatch(obj, 'foo'); - didCount = 0; - set(obj, 'foo', 'BAZ'); - assert.equal(didCount, 0, 'should NOT have invoked didCount'); - } - - ['@test unwatching "length" property on an object'](assert) { - let obj = { foo: 'RUN' }; - addListeners(obj, 'length'); - - // Can watch length when it is undefined - watch(obj, 'length'); - set(obj, 'length', '10k'); - assert.equal(didCount, 1, 'should have invoked didCount'); - - // Should stop watching despite length now being defined (making object 'array-like') - unwatch(obj, 'length'); - didCount = 0; - set(obj, 'length', '5k'); - assert.equal(didCount, 0, 'should NOT have invoked didCount'); + }; + + assert.equal(obj.foo, 'RUN', 'obj.foo'); + watch(obj, 'foo'); + assert.equal(obj.foo, 'RUN', 'obj.foo after watch'); + unwatch(obj, 'foo'); + assert.equal(obj.foo, 'RUN', 'obj.foo after unwatch'); + } } - - ['@test unwatching should not destroy non MANDATORY_SETTER descriptor'](assert) { - let obj = { - get foo() { - return 'RUN'; - }, - }; - - assert.equal(obj.foo, 'RUN', 'obj.foo'); - watch(obj, 'foo'); - assert.equal(obj.foo, 'RUN', 'obj.foo after watch'); - unwatch(obj, 'foo'); - assert.equal(obj.foo, 'RUN', 'obj.foo after unwatch'); - } - } -); + ); +} diff --git a/packages/@ember/-internals/metal/tests/watching/watch_test.js b/packages/@ember/-internals/metal/tests/watching/watch_test.js index 28cfe577733..28e8bae44f3 100644 --- a/packages/@ember/-internals/metal/tests/watching/watch_test.js +++ b/packages/@ember/-internals/metal/tests/watching/watch_test.js @@ -1,6 +1,7 @@ -import { context } from '@ember/-internals/environment'; import { set, get, computed, defineProperty, addListener, watch, unwatch } from '../..'; +import { context } from '@ember/-internals/environment'; import { deleteMeta, meta } from '@ember/-internals/meta'; +import { EMBER_METAL_TRACKED_PROPERTIES } from '@ember/canary-features'; import { moduleFor, AbstractTestCase } from 'internal-test-helpers'; let didCount, didKeys, originalLookup; @@ -12,244 +13,246 @@ function addListeners(obj, keyPath) { }); } -moduleFor( - 'watch', - class extends AbstractTestCase { - beforeEach() { - didCount = 0; - didKeys = []; - - originalLookup = context.lookup; - context.lookup = {}; - } - - afterEach() { - context.lookup = originalLookup; - } - - ['@test watching a computed property'](assert) { - let obj = {}; - defineProperty( - obj, - 'foo', - computed({ - get() { - return this.__foo; - }, - set(keyName, value) { - if (value !== undefined) { - this.__foo = value; - } - return this.__foo; - }, - }) - ); - addListeners(obj, 'foo'); - - watch(obj, 'foo'); - set(obj, 'foo', 'bar'); - assert.equal(didCount, 1, 'should have invoked didCount'); - } - - ['@test watching a regular defined property'](assert) { - let obj = { foo: 'baz' }; - addListeners(obj, 'foo'); - - watch(obj, 'foo'); - assert.equal(get(obj, 'foo'), 'baz', 'should have original prop'); - - set(obj, 'foo', 'bar'); - assert.equal(didCount, 1, 'should have invoked didCount'); - - assert.equal(get(obj, 'foo'), 'bar', 'should get new value'); - assert.equal(obj.foo, 'bar', 'property should be accessible on obj'); - } - - ['@test watching a regular undefined property'](assert) { - let obj = {}; - addListeners(obj, 'foo'); - - watch(obj, 'foo'); - - assert.equal('foo' in obj, false, 'precond undefined'); - - set(obj, 'foo', 'bar'); - - assert.equal(didCount, 1, 'should have invoked didCount'); - - assert.equal(get(obj, 'foo'), 'bar', 'should get new value'); - assert.equal(obj.foo, 'bar', 'property should be accessible on obj'); - } - - ['@test watches should inherit'](assert) { - let obj = { foo: 'baz' }; - let objB = Object.create(obj); - - addListeners(obj, 'foo'); - watch(obj, 'foo'); - assert.equal(get(obj, 'foo'), 'baz', 'should have original prop'); - - set(objB, 'foo', 'bar'); - set(obj, 'foo', 'baz'); - assert.equal(didCount, 1, 'should have invoked didCount once only'); - } - - ['@test watching an object THEN defining it should work also'](assert) { - let obj = {}; - addListeners(obj, 'foo'); - - watch(obj, 'foo'); - - defineProperty(obj, 'foo'); - set(obj, 'foo', 'bar'); - - assert.equal(get(obj, 'foo'), 'bar', 'should have set'); - assert.equal(didCount, 1, 'should have invoked didChange once'); - } - - ['@test watching a chain then defining the property'](assert) { - let obj = {}; - let foo = { bar: 'bar' }; - addListeners(obj, 'foo.bar'); - addListeners(foo, 'bar'); - - watch(obj, 'foo.bar'); +if (!EMBER_METAL_TRACKED_PROPERTIES) { + moduleFor( + 'watch', + class extends AbstractTestCase { + beforeEach() { + didCount = 0; + didKeys = []; + + originalLookup = context.lookup; + context.lookup = {}; + } + + afterEach() { + context.lookup = originalLookup; + } + + ['@test watching a computed property'](assert) { + let obj = {}; + defineProperty( + obj, + 'foo', + computed({ + get() { + return this.__foo; + }, + set(keyName, value) { + if (value !== undefined) { + this.__foo = value; + } + return this.__foo; + }, + }) + ); + addListeners(obj, 'foo'); + + watch(obj, 'foo'); + set(obj, 'foo', 'bar'); + assert.equal(didCount, 1, 'should have invoked didCount'); + } + + ['@test watching a regular defined property'](assert) { + let obj = { foo: 'baz' }; + addListeners(obj, 'foo'); + + watch(obj, 'foo'); + assert.equal(get(obj, 'foo'), 'baz', 'should have original prop'); + + set(obj, 'foo', 'bar'); + assert.equal(didCount, 1, 'should have invoked didCount'); + + assert.equal(get(obj, 'foo'), 'bar', 'should get new value'); + assert.equal(obj.foo, 'bar', 'property should be accessible on obj'); + } + + ['@test watching a regular undefined property'](assert) { + let obj = {}; + addListeners(obj, 'foo'); + + watch(obj, 'foo'); + + assert.equal('foo' in obj, false, 'precond undefined'); + + set(obj, 'foo', 'bar'); + + assert.equal(didCount, 1, 'should have invoked didCount'); + + assert.equal(get(obj, 'foo'), 'bar', 'should get new value'); + assert.equal(obj.foo, 'bar', 'property should be accessible on obj'); + } + + ['@test watches should inherit'](assert) { + let obj = { foo: 'baz' }; + let objB = Object.create(obj); + + addListeners(obj, 'foo'); + watch(obj, 'foo'); + assert.equal(get(obj, 'foo'), 'baz', 'should have original prop'); + + set(objB, 'foo', 'bar'); + set(obj, 'foo', 'baz'); + assert.equal(didCount, 1, 'should have invoked didCount once only'); + } + + ['@test watching an object THEN defining it should work also'](assert) { + let obj = {}; + addListeners(obj, 'foo'); + + watch(obj, 'foo'); + + defineProperty(obj, 'foo'); + set(obj, 'foo', 'bar'); + + assert.equal(get(obj, 'foo'), 'bar', 'should have set'); + assert.equal(didCount, 1, 'should have invoked didChange once'); + } + + ['@test watching a chain then defining the property'](assert) { + let obj = {}; + let foo = { bar: 'bar' }; + addListeners(obj, 'foo.bar'); + addListeners(foo, 'bar'); + + watch(obj, 'foo.bar'); + + defineProperty(obj, 'foo', undefined, foo); + set(foo, 'bar', 'baz'); + + assert.deepEqual( + didKeys, + ['foo.bar', 'bar'], + 'should have invoked didChange with bar, foo.bar' + ); + assert.equal(didCount, 2, 'should have invoked didChange twice'); + } + + ['@test watching a chain then defining the nested property'](assert) { + let bar = {}; + let obj = { foo: bar }; + let baz = { baz: 'baz' }; + addListeners(obj, 'foo.bar.baz'); + addListeners(baz, 'baz'); + + watch(obj, 'foo.bar.baz'); + + defineProperty(bar, 'bar', undefined, baz); + set(baz, 'baz', 'BOO'); - defineProperty(obj, 'foo', undefined, foo); - set(foo, 'bar', 'baz'); + assert.deepEqual( + didKeys, + ['foo.bar.baz', 'baz'], + 'should have invoked didChange with bar, foo.bar' + ); + assert.equal(didCount, 2, 'should have invoked didChange twice'); + } - assert.deepEqual( - didKeys, - ['foo.bar', 'bar'], - 'should have invoked didChange with bar, foo.bar' - ); - assert.equal(didCount, 2, 'should have invoked didChange twice'); - } - - ['@test watching a chain then defining the nested property'](assert) { - let bar = {}; - let obj = { foo: bar }; - let baz = { baz: 'baz' }; - addListeners(obj, 'foo.bar.baz'); - addListeners(baz, 'baz'); + ['@test watching an object value then unwatching should restore old value'](assert) { + let obj = { foo: { bar: { baz: { biff: 'BIFF' } } } }; + addListeners(obj, 'foo.bar.baz.biff'); - watch(obj, 'foo.bar.baz'); + watch(obj, 'foo.bar.baz.biff'); - defineProperty(bar, 'bar', undefined, baz); - set(baz, 'baz', 'BOO'); - - assert.deepEqual( - didKeys, - ['foo.bar.baz', 'baz'], - 'should have invoked didChange with bar, foo.bar' - ); - assert.equal(didCount, 2, 'should have invoked didChange twice'); - } + let foo = get(obj, 'foo'); + assert.equal(get(get(get(foo, 'bar'), 'baz'), 'biff'), 'BIFF', 'biff should exist'); - ['@test watching an object value then unwatching should restore old value'](assert) { - let obj = { foo: { bar: { baz: { biff: 'BIFF' } } } }; - addListeners(obj, 'foo.bar.baz.biff'); + unwatch(obj, 'foo.bar.baz.biff'); + assert.equal(get(get(get(foo, 'bar'), 'baz'), 'biff'), 'BIFF', 'biff should exist'); + } - watch(obj, 'foo.bar.baz.biff'); + ['@test when watching another object, destroy should remove chain watchers from the other object']( + assert + ) { + let objA = {}; + let objB = { foo: 'bar' }; + objA.b = objB; + addListeners(objA, 'b.foo'); - let foo = get(obj, 'foo'); - assert.equal(get(get(get(foo, 'bar'), 'baz'), 'biff'), 'BIFF', 'biff should exist'); + watch(objA, 'b.foo'); - unwatch(obj, 'foo.bar.baz.biff'); - assert.equal(get(get(get(foo, 'bar'), 'baz'), 'biff'), 'BIFF', 'biff should exist'); - } - - ['@test when watching another object, destroy should remove chain watchers from the other object']( - assert - ) { - let objA = {}; - let objB = { foo: 'bar' }; - objA.b = objB; - addListeners(objA, 'b.foo'); - - watch(objA, 'b.foo'); - - let meta_objB = meta(objB); - let chainNode = meta(objA).readableChains().chains.b.chains.foo; - - assert.equal(meta_objB.peekWatching('foo'), 1, 'should be watching foo'); - assert.equal( - meta_objB.readableChainWatchers().has('foo', chainNode), - true, - 'should have chain watcher' - ); - - deleteMeta(objA); - - assert.equal(meta_objB.peekWatching('foo'), 0, 'should not be watching foo'); - assert.equal( - meta_objB.readableChainWatchers().has('foo', chainNode), - false, - 'should not have chain watcher' - ); - } + let meta_objB = meta(objB); + let chainNode = meta(objA).readableChains().chains.b.chains.foo; - // TESTS for length property + assert.equal(meta_objB.peekWatching('foo'), 1, 'should be watching foo'); + assert.equal( + meta_objB.readableChainWatchers().has('foo', chainNode), + true, + 'should have chain watcher' + ); + + deleteMeta(objA); + + assert.equal(meta_objB.peekWatching('foo'), 0, 'should not be watching foo'); + assert.equal( + meta_objB.readableChainWatchers().has('foo', chainNode), + false, + 'should not have chain watcher' + ); + } + + // TESTS for length property + + ['@test watching "length" property on an object'](assert) { + let obj = { length: '26.2 miles' }; + addListeners(obj, 'length'); + + watch(obj, 'length'); + assert.equal(get(obj, 'length'), '26.2 miles', 'should have original prop'); - ['@test watching "length" property on an object'](assert) { - let obj = { length: '26.2 miles' }; - addListeners(obj, 'length'); - - watch(obj, 'length'); - assert.equal(get(obj, 'length'), '26.2 miles', 'should have original prop'); - - set(obj, 'length', '10k'); - assert.equal(didCount, 1, 'should have invoked didCount'); - - assert.equal(get(obj, 'length'), '10k', 'should get new value'); - assert.equal(obj.length, '10k', 'property should be accessible on obj'); - } - - ['@test watching "length" property on an array'](assert) { - let arr = []; - addListeners(arr, 'length'); - - watch(arr, 'length'); - assert.equal(get(arr, 'length'), 0, 'should have original prop'); - - set(arr, 'length', '10'); - assert.equal(didCount, 1, 'should NOT have invoked didCount'); - - assert.equal(get(arr, 'length'), 10, 'should get new value'); - assert.equal(arr.length, 10, 'property should be accessible on arr'); - } - - ['@test watch + ES5 getter'](assert) { - let parent = { b: 1 }; - let child = { - get b() { - return parent.b; - }, - }; + set(obj, 'length', '10k'); + assert.equal(didCount, 1, 'should have invoked didCount'); + + assert.equal(get(obj, 'length'), '10k', 'should get new value'); + assert.equal(obj.length, '10k', 'property should be accessible on obj'); + } + + ['@test watching "length" property on an array'](assert) { + let arr = []; + addListeners(arr, 'length'); + + watch(arr, 'length'); + assert.equal(get(arr, 'length'), 0, 'should have original prop'); + + set(arr, 'length', '10'); + assert.equal(didCount, 1, 'should NOT have invoked didCount'); + + assert.equal(get(arr, 'length'), 10, 'should get new value'); + assert.equal(arr.length, 10, 'property should be accessible on arr'); + } + + ['@test watch + ES5 getter'](assert) { + let parent = { b: 1 }; + let child = { + get b() { + return parent.b; + }, + }; - assert.equal(parent.b, 1, 'parent.b should be 1'); - assert.equal(child.b, 1, 'child.b should be 1'); - assert.equal(get(child, 'b'), 1, 'get(child, "b") should be 1'); + assert.equal(parent.b, 1, 'parent.b should be 1'); + assert.equal(child.b, 1, 'child.b should be 1'); + assert.equal(get(child, 'b'), 1, 'get(child, "b") should be 1'); - watch(child, 'b'); + watch(child, 'b'); - assert.equal(parent.b, 1, 'parent.b should be 1 (after watch)'); - assert.equal(child.b, 1, 'child.b should be 1 (after watch)'); + assert.equal(parent.b, 1, 'parent.b should be 1 (after watch)'); + assert.equal(child.b, 1, 'child.b should be 1 (after watch)'); - assert.equal(get(child, 'b'), 1, 'get(child, "b") should be 1 (after watch)'); - } + assert.equal(get(child, 'b'), 1, 'get(child, "b") should be 1 (after watch)'); + } - ['@test watch + set + no-descriptor'](assert) { - let child = {}; + ['@test watch + set + no-descriptor'](assert) { + let child = {}; - assert.equal(child.b, undefined, 'child.b '); - assert.equal(get(child, 'b'), undefined, 'get(child, "b")'); + assert.equal(child.b, undefined, 'child.b '); + assert.equal(get(child, 'b'), undefined, 'get(child, "b")'); - watch(child, 'b'); - set(child, 'b', 1); + watch(child, 'b'); + set(child, 'b', 1); - assert.equal(child.b, 1, 'child.b (after watch)'); - assert.equal(get(child, 'b'), 1, 'get(child, "b") (after watch)'); + assert.equal(child.b, 1, 'child.b (after watch)'); + assert.equal(get(child, 'b'), 1, 'get(child, "b") (after watch)'); + } } - } -); + ); +} diff --git a/packages/@ember/-internals/routing/lib/ext/controller.ts b/packages/@ember/-internals/routing/lib/ext/controller.ts index 2df998b1170..2ce89c4098e 100644 --- a/packages/@ember/-internals/routing/lib/ext/controller.ts +++ b/packages/@ember/-internals/routing/lib/ext/controller.ts @@ -66,7 +66,8 @@ ControllerMixin.reopen({ @private */ _qpChanged(controller: any, _prop: string) { - let prop = _prop.substr(0, _prop.length - 3); + let dotIndex = _prop.indexOf('.[]'); + let prop = dotIndex === -1 ? _prop : _prop.slice(0, dotIndex); let delegate = controller._qpDelegate; let value = get(controller, prop); diff --git a/packages/@ember/-internals/routing/lib/system/route.ts b/packages/@ember/-internals/routing/lib/system/route.ts index 08ea961f87a..af8ded76420 100644 --- a/packages/@ember/-internals/routing/lib/system/route.ts +++ b/packages/@ember/-internals/routing/lib/system/route.ts @@ -1,6 +1,7 @@ import { computed, defineProperty, + flushInvalidActiveObservers, get, getProperties, isEmpty, @@ -15,7 +16,10 @@ import { Object as EmberObject, typeOf, } from '@ember/-internals/runtime'; -import { EMBER_ROUTING_BUILD_ROUTEINFO_METADATA } from '@ember/canary-features'; +import { + EMBER_METAL_TRACKED_PROPERTIES, + EMBER_ROUTING_BUILD_ROUTEINFO_METADATA, +} from '@ember/canary-features'; import { assert, deprecate, info, isTesting } from '@ember/debug'; import { ROUTER_EVENTS } from '@ember/deprecated-features'; import { assign } from '@ember/polyfills'; @@ -358,6 +362,13 @@ class Route extends EmberObject implements IRoute { controller._qpDelegate = get(this, '_qp.states.inactive'); this.resetController(controller, isExiting, transition); + + // TODO: Once tags are enabled by default, we should refactor QP changes to + // use autotracking. This will likely be a large refactor, and for now we + // just need to trigger observers eagerly. + if (EMBER_METAL_TRACKED_PROPERTIES) { + flushInvalidActiveObservers(false); + } } /** @@ -941,6 +952,13 @@ class Route extends EmberObject implements IRoute { if (this._environment.options.shouldRender) { this.renderTemplate(controller, context); } + + // TODO: Once tags are enabled by default, we should refactor QP changes to + // use autotracking. This will likely be a large refactor, and for now we + // just need to trigger observers eagerly. + if (EMBER_METAL_TRACKED_PROPERTIES) { + flushInvalidActiveObservers(false); + } } /* diff --git a/packages/@ember/-internals/runtime/lib/mixins/-proxy.js b/packages/@ember/-internals/runtime/lib/mixins/-proxy.js index 2daa77edab1..54f652be69c 100644 --- a/packages/@ember/-internals/runtime/lib/mixins/-proxy.js +++ b/packages/@ember/-internals/runtime/lib/mixins/-proxy.js @@ -14,8 +14,11 @@ import { Mixin, tagFor, computed, + UNKNOWN_PROPERTY_TAG, + getChainTagsForKey, } from '@ember/-internals/metal'; import { setProxy } from '@ember/-internals/utils'; +import { EMBER_METAL_TRACKED_PROPERTIES } from '@ember/canary-features'; import { assert } from '@ember/debug'; export function contentFor(proxy, m) { @@ -63,13 +66,17 @@ export default Mixin.create({ }), willWatchProperty(key) { - let contentKey = `content.${key}`; - addObserver(this, contentKey, null, '_contentPropertyDidChange'); + if (!EMBER_METAL_TRACKED_PROPERTIES) { + let contentKey = `content.${key}`; + addObserver(this, contentKey, null, '_contentPropertyDidChange'); + } }, didUnwatchProperty(key) { - let contentKey = `content.${key}`; - removeObserver(this, contentKey, null, '_contentPropertyDidChange'); + if (!EMBER_METAL_TRACKED_PROPERTIES) { + let contentKey = `content.${key}`; + removeObserver(this, contentKey, null, '_contentPropertyDidChange'); + } }, _contentPropertyDidChange(content, contentKey) { @@ -80,6 +87,10 @@ export default Mixin.create({ notifyPropertyChange(this, key); }, + [UNKNOWN_PROPERTY_TAG](key) { + return getChainTagsForKey(this, `content.${key}`); + }, + unknownProperty(key) { let content = contentFor(this); if (content) { diff --git a/packages/@ember/-internals/runtime/lib/system/array_proxy.js b/packages/@ember/-internals/runtime/lib/system/array_proxy.js index 77dd14b9d16..bed68bcf40d 100644 --- a/packages/@ember/-internals/runtime/lib/system/array_proxy.js +++ b/packages/@ember/-internals/runtime/lib/system/array_proxy.js @@ -10,7 +10,9 @@ import { addArrayObserver, removeArrayObserver, replace, + getChainTagsForKey, } from '@ember/-internals/metal'; +import { EMBER_METAL_TRACKED_PROPERTIES } from '@ember/canary-features'; import EmberObject from './object'; import { isArray, MutableArray } from '../mixins/array'; import { assert } from '@ember/debug'; @@ -101,6 +103,13 @@ export default class ArrayProxy extends EmberObject { this._length = 0; this._arrangedContent = null; + + if (EMBER_METAL_TRACKED_PROPERTIES) { + this._arrangedContentIsUpdating = false; + this._arrangedContentTag = getChainTagsForKey(this, 'arrangedContent'); + this._arrangedContentRevision = this._arrangedContentTag.value(); + } + this._addArrangedContentArrayObsever(); } @@ -164,6 +173,10 @@ export default class ArrayProxy extends EmberObject { // Overriding objectAt is not supported. objectAt(idx) { + if (EMBER_METAL_TRACKED_PROPERTIES) { + this._revalidate(); + } + if (this._objects === null) { this._objects = []; } @@ -187,6 +200,10 @@ export default class ArrayProxy extends EmberObject { // Overriding length is not supported. get length() { + if (EMBER_METAL_TRACKED_PROPERTIES) { + this._revalidate(); + } + if (this._lengthDirty) { let arrangedContent = get(this, 'arrangedContent'); this._length = arrangedContent ? get(arrangedContent, 'length') : 0; @@ -217,26 +234,34 @@ export default class ArrayProxy extends EmberObject { } [PROPERTY_DID_CHANGE](key) { - if (key === 'arrangedContent') { - let oldLength = this._objects === null ? 0 : this._objects.length; - let arrangedContent = get(this, 'arrangedContent'); - let newLength = arrangedContent ? get(arrangedContent, 'length') : 0; + if (EMBER_METAL_TRACKED_PROPERTIES) { + this._revalidate(); + } else { + if (key === 'arrangedContent') { + this._updateArrangedContentArray(); + } else if (key === 'content') { + this._invalidate(); + } + } + } + + _updateArrangedContentArray() { + let oldLength = this._objects === null ? 0 : this._objects.length; + let arrangedContent = get(this, 'arrangedContent'); + let newLength = arrangedContent ? get(arrangedContent, 'length') : 0; - this._removeArrangedContentArrayObsever(); - this.arrayContentWillChange(0, oldLength, newLength); + this._removeArrangedContentArrayObsever(); + this.arrayContentWillChange(0, oldLength, newLength); - this._invalidate(); + this._invalidate(); - this.arrayContentDidChange(0, oldLength, newLength); - this._addArrangedContentArrayObsever(); - } else if (key === 'content') { - this._invalidate(); - } + this.arrayContentDidChange(0, oldLength, newLength); + this._addArrangedContentArrayObsever(); } _addArrangedContentArrayObsever() { let arrangedContent = get(this, 'arrangedContent'); - if (arrangedContent) { + if (arrangedContent && !arrangedContent.isDestroyed) { assert("Can't set ArrayProxy's content to itself", arrangedContent !== this); assert( `ArrayProxy expects an Array or ArrayProxy, but you passed ${typeof arrangedContent}`, @@ -281,6 +306,24 @@ export default class ArrayProxy extends EmberObject { } } +let _revalidate; + +if (EMBER_METAL_TRACKED_PROPERTIES) { + _revalidate = function() { + if ( + !this._arrangedContentIsUpdating && + !this._arrangedContentTag.validate(this._arrangedContentRevision) + ) { + this._arrangedContentIsUpdating = true; + this._updateArrangedContentArray(); + this._arrangedContentIsUpdating = false; + + this._arrangedContentTag = getChainTagsForKey(this, 'arrangedContent'); + this._arrangedContentRevision = this._arrangedContentTag.value(); + } + }; +} + ArrayProxy.reopen(MutableArray, { /** The array that the proxy pretends to be. In the default `ArrayProxy` @@ -291,4 +334,6 @@ ArrayProxy.reopen(MutableArray, { @public */ arrangedContent: alias('content'), + + _revalidate, }); diff --git a/packages/@ember/-internals/runtime/lib/system/core_object.js b/packages/@ember/-internals/runtime/lib/system/core_object.js index 67a7dd1f324..b7e9fcc14b0 100644 --- a/packages/@ember/-internals/runtime/lib/system/core_object.js +++ b/packages/@ember/-internals/runtime/lib/system/core_object.js @@ -12,6 +12,7 @@ import { HAS_NATIVE_PROXY, isInternalSymbol, } from '@ember/-internals/utils'; +import { EMBER_METAL_TRACKED_PROPERTIES } from '@ember/canary-features'; import { schedule } from '@ember/runloop'; import { meta, peekMeta, deleteMeta } from '@ember/-internals/meta'; import { @@ -19,6 +20,7 @@ import { finishChains, sendEvent, Mixin, + activateObserver, applyMixin, defineProperty, descriptorForProperty, @@ -129,9 +131,21 @@ function initialize(obj, properties) { } obj.init(properties); - // re-enable chains m.unsetInitializing(); - finishChains(m); + + if (EMBER_METAL_TRACKED_PROPERTIES) { + let observerEvents = m.observerEvents(); + + if (observerEvents !== undefined) { + for (let i = 0; i < observerEvents.length; i++) { + activateObserver(obj, observerEvents[i]); + } + } + } else { + // re-enable chains + finishChains(m); + } + sendEvent(obj, 'init', undefined, undefined, undefined, m); } @@ -267,6 +281,7 @@ class CoreObject { // disable chains let m = meta(self); + m.setInitializing(); assert( diff --git a/packages/@ember/-internals/runtime/tests/ext/function_test.js b/packages/@ember/-internals/runtime/tests/ext/function_test.js index 8ace67fe307..47b2975414c 100644 --- a/packages/@ember/-internals/runtime/tests/ext/function_test.js +++ b/packages/@ember/-internals/runtime/tests/ext/function_test.js @@ -2,13 +2,13 @@ import { ENV } from '@ember/-internals/environment'; import { Mixin, mixin, get, set } from '@ember/-internals/metal'; import EmberObject from '../../lib/system/object'; import Evented from '../../lib/mixins/evented'; -import { moduleFor, AbstractTestCase } from 'internal-test-helpers'; +import { moduleFor, AbstractTestCase, runLoopSettled } from 'internal-test-helpers'; import { FUNCTION_PROTOTYPE_EXTENSIONS } from '@ember/deprecated-features'; moduleFor( 'Function.prototype.observes() helper', class extends AbstractTestCase { - ['@test global observer helper takes multiple params'](assert) { + async ['@test global observer helper takes multiple params'](assert) { if (!FUNCTION_PROTOTYPE_EXTENSIONS || !ENV.EXTEND_PROTOTYPES.Function) { assert.ok( 'undefined' === typeof Function.prototype.observes, @@ -32,7 +32,11 @@ moduleFor( assert.equal(get(obj, 'count'), 0, 'should not invoke observer immediately'); set(obj, 'bar', 'BAZ'); + await runLoopSettled(); + set(obj, 'baz', 'BAZ'); + await runLoopSettled(); + assert.equal(get(obj, 'count'), 2, 'should invoke observer after change'); } } @@ -69,7 +73,7 @@ moduleFor( assert.equal(get(obj, 'count'), 2, 'should invoke listeners when events trigger'); } - ['@test can be chained with observes'](assert) { + async ['@test can be chained with observes'](assert) { if (!FUNCTION_PROTOTYPE_EXTENSIONS || !ENV.EXTEND_PROTOTYPES.Function) { assert.ok('Function.prototype helper disabled'); return; @@ -93,6 +97,8 @@ moduleFor( set(obj, 'bay', 'BAY'); obj.trigger('bar'); + await runLoopSettled(); + assert.equal(get(obj, 'count'), 2, 'should invoke observer and listener'); } } 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 bd4ded52c08..fcc31dce395 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,8 +1,7 @@ -import { run } from '@ember/runloop'; import { get, set, addObserver } from '@ember/-internals/metal'; import EmberObject from '../../../../lib/system/object'; import { A as emberA } from '../../../../lib/mixins/array'; -import { moduleFor, AbstractTestCase } from 'internal-test-helpers'; +import { moduleFor, AbstractTestCase, runLoopSettled } from 'internal-test-helpers'; /* NOTE: This test is adapted from the 1.x series of unit tests. The tests @@ -18,7 +17,7 @@ import { moduleFor, AbstractTestCase } from 'internal-test-helpers'; moduleFor( 'Ember.Observable - Observing with @each', class extends AbstractTestCase { - ['@test chained observers on enumerable properties are triggered when the observed property of any item changes']( + async ['@test chained observers on enumerable properties are triggered when the observed property of any item changes']( assert ) { let family = EmberObject.create({ momma: null }); @@ -38,21 +37,32 @@ moduleFor( }); observerFiredCount = 0; - run(() => get(momma, 'children').setEach('name', 'Juan')); + + for (let i = 0; i < momma.children.length; i++) { + momma.children[i].set('name', 'Juan'); + await runLoopSettled(); + } assert.equal(observerFiredCount, 3, 'observer fired after changing child names'); observerFiredCount = 0; - run(() => get(momma, 'children').pushObject(child4)); + get(momma, 'children').pushObject(child4); + await runLoopSettled(); + assert.equal(observerFiredCount, 1, 'observer fired after adding a new item'); observerFiredCount = 0; - run(() => set(child4, 'name', 'Herbert')); + set(child4, 'name', 'Herbert'); + await runLoopSettled(); + assert.equal(observerFiredCount, 1, 'observer fired after changing property on new object'); set(momma, 'children', []); + await runLoopSettled(); observerFiredCount = 0; - run(() => set(child1, 'name', 'Hanna')); + set(child1, 'name', 'Hanna'); + await runLoopSettled(); + assert.equal( observerFiredCount, 0, diff --git a/packages/@ember/-internals/runtime/tests/legacy_1x/mixins/observable/observable_test.js b/packages/@ember/-internals/runtime/tests/legacy_1x/mixins/observable/observable_test.js index 3adc1c920eb..79c5fa744e3 100644 --- a/packages/@ember/-internals/runtime/tests/legacy_1x/mixins/observable/observable_test.js +++ b/packages/@ember/-internals/runtime/tests/legacy_1x/mixins/observable/observable_test.js @@ -5,7 +5,7 @@ import { w } from '@ember/string'; import EmberObject from '../../../../lib/system/object'; import Observable from '../../../../lib/mixins/observable'; import { A as emberA } from '../../../../lib/mixins/array'; -import { moduleFor, AbstractTestCase } from 'internal-test-helpers'; +import { moduleFor, AbstractTestCase, runLoopSettled } from 'internal-test-helpers'; /* NOTE: This test is adapted from the 1.x series of unit tests. The tests @@ -748,8 +748,10 @@ moduleFor( ); } - ['@test should notify array observer when array changes'](assert) { + async ['@test should notify array observer when array changes'](assert) { get(object, 'normalArray').replace(0, 0, [6]); + await runLoopSettled(); + assert.equal(object.abnormal, 'notifiedObserver', 'observer should be notified'); } } @@ -783,20 +785,26 @@ moduleFor( }); } - ['@test should register an observer for a property'](assert) { + async ['@test should register an observer for a property'](assert) { ObjectC.addObserver('normal', ObjectC, 'action'); ObjectC.set('normal', 'newValue'); + + await runLoopSettled(); assert.equal(ObjectC.normal1, 'newZeroValue'); } - ['@test should register an observer for a property - Special case of chained property']( + async ['@test should register an observer for a property - Special case of chained property']( assert ) { ObjectC.addObserver('objectE.propertyVal', ObjectC, 'chainedObserver'); ObjectC.objectE.set('propertyVal', 'chainedPropertyValue'); + await runLoopSettled(); + assert.equal('chainedPropertyObserved', ObjectC.normal2); ObjectC.normal2 = 'dependentValue'; ObjectC.set('objectE', ''); + await runLoopSettled(); + assert.equal('chainedPropertyObserved', ObjectC.normal2); } } @@ -843,32 +851,45 @@ moduleFor( }); } - ['@test should unregister an observer for a property'](assert) { + async ['@test should unregister an observer for a property'](assert) { ObjectD.addObserver('normal', ObjectD, 'addAction'); ObjectD.set('normal', 'newValue'); + await runLoopSettled(); + assert.equal(ObjectD.normal1, 'newZeroValue'); ObjectD.set('normal1', 'zeroValue'); + await runLoopSettled(); ObjectD.removeObserver('normal', ObjectD, 'addAction'); ObjectD.set('normal', 'newValue'); assert.equal(ObjectD.normal1, 'zeroValue'); } - ["@test should unregister an observer for a property - special case when key has a '.' in it."]( + async ["@test should unregister an observer for a property - special case when key has a '.' in it."]( assert ) { ObjectD.addObserver('objectF.propertyVal', ObjectD, 'removeChainedObserver'); ObjectD.objectF.set('propertyVal', 'chainedPropertyValue'); + await runLoopSettled(); + ObjectD.removeObserver('objectF.propertyVal', ObjectD, 'removeChainedObserver'); ObjectD.normal2 = 'dependentValue'; + ObjectD.objectF.set('propertyVal', 'removedPropertyValue'); + await runLoopSettled(); + assert.equal('dependentValue', ObjectD.normal2); + ObjectD.set('objectF', ''); + await runLoopSettled(); + assert.equal('dependentValue', ObjectD.normal2); } - ['@test removing an observer inside of an observer shouldn’t cause any problems'](assert) { + async ['@test removing an observer inside of an observer shouldn’t cause any problems']( + assert + ) { // The observable system should be protected against clients removing // observers in the middle of observer notification. var encounteredError = false; @@ -876,9 +897,10 @@ moduleFor( ObjectD.addObserver('observableValue', null, 'observer1'); ObjectD.addObserver('observableValue', null, 'observer2'); ObjectD.addObserver('observableValue', null, 'observer3'); - run(function() { - ObjectD.set('observableValue', 'hi world'); - }); + + ObjectD.set('observableValue', 'hi world'); + + await runLoopSettled(); } catch (e) { encounteredError = true; } diff --git a/packages/@ember/-internals/runtime/tests/legacy_1x/mixins/observable/propertyChanges_test.js b/packages/@ember/-internals/runtime/tests/legacy_1x/mixins/observable/propertyChanges_test.js index a672cdd8754..e9c5cf52d3b 100644 --- a/packages/@ember/-internals/runtime/tests/legacy_1x/mixins/observable/propertyChanges_test.js +++ b/packages/@ember/-internals/runtime/tests/legacy_1x/mixins/observable/propertyChanges_test.js @@ -21,126 +21,129 @@ import EmberObject from '../../../../lib/system/object'; import Observable from '../../../../lib/mixins/observable'; import { computed, observer } from '@ember/-internals/metal'; import { moduleFor, AbstractTestCase } from 'internal-test-helpers'; +import { EMBER_METAL_TRACKED_PROPERTIES } from '@ember/canary-features'; const ObservableObject = EmberObject.extend(Observable); let ObjectA; -moduleFor( - 'object.propertyChanges', - class extends AbstractTestCase { - beforeEach() { - ObjectA = ObservableObject.extend({ - action: observer('foo', function() { - this.set('prop', 'changedPropValue'); - }), - notifyAction: observer('newFoo', function() { - this.set('newProp', 'changedNewPropValue'); - }), - - notifyAllAction: observer('prop', function() { - this.set('newFoo', 'changedNewFooValue'); - }), - - starObserver(target, key) { - this.starProp = key; - }, - }).create({ - starProp: null, - - foo: 'fooValue', - prop: 'propValue', - - newFoo: 'newFooValue', - newProp: 'newPropValue', - }); - } - - ['@test should observe the changes within the nested begin / end property changes'](assert) { - //start the outer nest - ObjectA.beginPropertyChanges(); - - // Inner nest - ObjectA.beginPropertyChanges(); - ObjectA.set('foo', 'changeFooValue'); - - assert.equal(ObjectA.prop, 'propValue'); - ObjectA.endPropertyChanges(); - - //end inner nest - ObjectA.set('prop', 'changePropValue'); - assert.equal(ObjectA.newFoo, 'newFooValue'); - - //close the outer nest - ObjectA.endPropertyChanges(); - - assert.equal(ObjectA.prop, 'changedPropValue'); - assert.equal(ObjectA.newFoo, 'changedNewFooValue'); - } - - ['@test should observe the changes within the begin and end property changes'](assert) { - ObjectA.beginPropertyChanges(); - ObjectA.set('foo', 'changeFooValue'); - - assert.equal(ObjectA.prop, 'propValue'); - ObjectA.endPropertyChanges(); - - assert.equal(ObjectA.prop, 'changedPropValue'); - } - - ['@test should indicate that the property of an object has just changed'](assert) { - //change the value of foo. - ObjectA.set('foo', 'changeFooValue'); - - // Indicate the subscribers of foo that the value has just changed - ObjectA.notifyPropertyChange('foo', null); - - // Values of prop has just changed - assert.equal(ObjectA.prop, 'changedPropValue'); - } +if (!EMBER_METAL_TRACKED_PROPERTIES) { + moduleFor( + 'object.propertyChanges', + class extends AbstractTestCase { + beforeEach() { + ObjectA = ObservableObject.extend({ + action: observer('foo', function() { + this.set('prop', 'changedPropValue'); + }), + notifyAction: observer('newFoo', function() { + this.set('newProp', 'changedNewPropValue'); + }), + + notifyAllAction: observer('prop', function() { + this.set('newFoo', 'changedNewFooValue'); + }), + + starObserver(target, key) { + this.starProp = key; + }, + }).create({ + starProp: null, - ['@test should notify that the property of an object has changed'](assert) { - // Notify to its subscriber that the values of 'newFoo' will be changed. In this - // case the observer is "newProp". Therefore this will call the notifyAction function - // and value of "newProp" will be changed. - ObjectA.notifyPropertyChange('newFoo', 'fooValue'); + foo: 'fooValue', + prop: 'propValue', - //value of newProp changed. - assert.equal(ObjectA.newProp, 'changedNewPropValue'); - } - - ['@test should invalidate function property cache when notifyPropertyChange is called']( - assert - ) { - let a; - - expectDeprecation(() => { - a = ObservableObject.extend({ - b: computed({ - get() { - return this._b; - }, - set(key, value) { - this._b = value; - return this; - }, - }).volatile(), - }).create({ - _b: null, + newFoo: 'newFooValue', + newProp: 'newPropValue', }); - }, /Setting a computed property as volatile has been deprecated/); - - a.set('b', 'foo'); - assert.equal(a.get('b'), 'foo', 'should have set the correct value for property b'); - - a._b = 'bar'; - a.notifyPropertyChange('b'); - a.set('b', 'foo'); - assert.equal( - a.get('b'), - 'foo', - 'should have invalidated the cache so that the newly set value is actually set' - ); + } + + ['@test should observe the changes within the nested begin / end property changes'](assert) { + //start the outer nest + ObjectA.beginPropertyChanges(); + + // Inner nest + ObjectA.beginPropertyChanges(); + ObjectA.set('foo', 'changeFooValue'); + + assert.equal(ObjectA.prop, 'propValue'); + ObjectA.endPropertyChanges(); + + //end inner nest + ObjectA.set('prop', 'changePropValue'); + assert.equal(ObjectA.newFoo, 'newFooValue'); + + //close the outer nest + ObjectA.endPropertyChanges(); + + assert.equal(ObjectA.prop, 'changedPropValue'); + assert.equal(ObjectA.newFoo, 'changedNewFooValue'); + } + + ['@test should observe the changes within the begin and end property changes'](assert) { + ObjectA.beginPropertyChanges(); + ObjectA.set('foo', 'changeFooValue'); + + assert.equal(ObjectA.prop, 'propValue'); + ObjectA.endPropertyChanges(); + + assert.equal(ObjectA.prop, 'changedPropValue'); + } + + ['@test should indicate that the property of an object has just changed'](assert) { + //change the value of foo. + ObjectA.set('foo', 'changeFooValue'); + + // Indicate the subscribers of foo that the value has just changed + ObjectA.notifyPropertyChange('foo', null); + + // Values of prop has just changed + assert.equal(ObjectA.prop, 'changedPropValue'); + } + + ['@test should notify that the property of an object has changed'](assert) { + // Notify to its subscriber that the values of 'newFoo' will be changed. In this + // case the observer is "newProp". Therefore this will call the notifyAction function + // and value of "newProp" will be changed. + ObjectA.notifyPropertyChange('newFoo', 'fooValue'); + + //value of newProp changed. + assert.equal(ObjectA.newProp, 'changedNewPropValue'); + } + + ['@test should invalidate function property cache when notifyPropertyChange is called']( + assert + ) { + let a; + + expectDeprecation(() => { + a = ObservableObject.extend({ + b: computed({ + get() { + return this._b; + }, + set(key, value) { + this._b = value; + return this; + }, + }).volatile(), + }).create({ + _b: null, + }); + }, /Setting a computed property as volatile has been deprecated/); + + a.set('b', 'foo'); + assert.equal(a.get('b'), 'foo', 'should have set the correct value for property b'); + + a._b = 'bar'; + a.notifyPropertyChange('b'); + a.set('b', 'foo'); + assert.equal( + a.get('b'), + 'foo', + 'should have invalidated the cache so that the newly set value is actually set' + ); + } } - } -); + ); +} diff --git a/packages/@ember/-internals/runtime/tests/mixins/array_test.js b/packages/@ember/-internals/runtime/tests/mixins/array_test.js index 62f0fc01728..e8f695f3324 100644 --- a/packages/@ember/-internals/runtime/tests/mixins/array_test.js +++ b/packages/@ember/-internals/runtime/tests/mixins/array_test.js @@ -13,7 +13,7 @@ import { import EmberObject from '../../lib/system/object'; import EmberArray from '../../lib/mixins/array'; import { A as emberA } from '../../lib/mixins/array'; -import { moduleFor, AbstractTestCase } from 'internal-test-helpers'; +import { moduleFor, AbstractTestCase, runLoopSettled } from 'internal-test-helpers'; /* Implement a basic fake mutable array. This validates that any non-native @@ -101,7 +101,7 @@ let obj, observer; moduleFor( 'mixins/array/arrayContent[Will|Did]Change', class extends AbstractTestCase { - ['@test should notify observers of []'](assert) { + async ['@test should notify observers of []'](assert) { obj = DummyArray.extend({ enumerablePropertyDidChange: emberObserver('[]', function() { this._count++; @@ -114,6 +114,7 @@ moduleFor( arrayContentWillChange(obj, 0, 1, 1); arrayContentDidChange(obj, 0, 1, 1); + await runLoopSettled(); assert.equal(obj._count, 1, 'should have invoked'); } @@ -143,28 +144,40 @@ moduleFor( obj = null; } - ['@test should notify observers when call with no params'](assert) { + async ['@test should notify observers when call with no params'](assert) { arrayContentWillChange(obj); + await runLoopSettled(); + assert.equal(obj._after, 0); arrayContentDidChange(obj); + await runLoopSettled(); + assert.equal(obj._after, 1); } // API variation that included items only - ['@test should not notify when passed lengths are same'](assert) { + async ['@test should not notify when passed lengths are same'](assert) { arrayContentWillChange(obj, 0, 1, 1); + await runLoopSettled(); + assert.equal(obj._after, 0); arrayContentDidChange(obj, 0, 1, 1); + await runLoopSettled(); + assert.equal(obj._after, 0); } - ['@test should notify when passed lengths are different'](assert) { + async ['@test should notify when passed lengths are different'](assert) { arrayContentWillChange(obj, 0, 1, 2); + await runLoopSettled(); + assert.equal(obj._after, 0); arrayContentDidChange(obj, 0, 1, 2); + await runLoopSettled(); + assert.equal(obj._after, 1); } } @@ -262,7 +275,7 @@ moduleFor( ary = null; } - ['@test adding an object should notify (@each.isDone)'](assert) { + async ['@test adding an object should notify (@each.isDone)'](assert) { let called = 0; let observerObject = EmberObject.create({ @@ -280,10 +293,11 @@ moduleFor( }) ); + await runLoopSettled(); assert.equal(called, 1, 'calls observer when object is pushed'); } - ['@test using @each to observe arrays that does not return objects raise error'](assert) { + async ['@test using @each to observe arrays that does not return objects raise error'](assert) { let called = 0; let observerObject = EmberObject.create({ @@ -298,17 +312,16 @@ moduleFor( }, }); - addObserver(ary, '@each.isDone', observerObject, 'wasCalled'); + ary.addObject({ + desc: 'foo', + isDone: false, + }); - expectAssertion(() => { - ary.addObject( - EmberObject.create({ - desc: 'foo', - isDone: false, - }) - ); + assert.throwsAssertion(() => { + addObserver(ary, '@each.isDone', observerObject, 'wasCalled'); }, /When using @each to observe the array/); + await runLoopSettled(); assert.equal(called, 0, 'not calls observer when object is pushed'); } @@ -339,7 +352,7 @@ moduleFor( assert.equal('BYE!', get(obj, 'common')); } - ['@test observers that contain @each in the path should fire only once the first time they are accessed']( + async ['@test observers that contain @each in the path should fire only once the first time they are accessed']( assert ) { let count = 0; @@ -354,12 +367,15 @@ moduleFor( commonDidChange: emberObserver('resources.@each.common', () => count++), }).create(); - // Observer fires second time when new object is added + // Observer fires first time when new object is added get(obj, 'resources').pushObject(EmberObject.create({ common: 'HI!' })); - // Observer fires third time when property on an object is changed + await runLoopSettled(); + + // Observer fires second time when property on an object is changed set(objectAt(get(obj, 'resources'), 0), 'common', 'BYE!'); + await runLoopSettled(); - assert.equal(count, 2, 'observers should only be called once'); + assert.equal(count, 2, 'observers should be called twice'); } } ); diff --git a/packages/@ember/-internals/runtime/tests/mixins/observable_test.js b/packages/@ember/-internals/runtime/tests/mixins/observable_test.js index ba17bc5c397..c2434a07bdd 100644 --- a/packages/@ember/-internals/runtime/tests/mixins/observable_test.js +++ b/packages/@ember/-internals/runtime/tests/mixins/observable_test.js @@ -1,6 +1,6 @@ import { computed, addObserver, get } from '@ember/-internals/metal'; import EmberObject from '../../lib/system/object'; -import { moduleFor, AbstractTestCase } from 'internal-test-helpers'; +import { moduleFor, AbstractTestCase, runLoopSettled } from 'internal-test-helpers'; moduleFor( 'mixins/observable', @@ -43,7 +43,7 @@ moduleFor( assert.equal('Cook', obj.get('lastName')); } - ['@test calling setProperties completes safely despite exceptions'](assert) { + async ['@test calling setProperties completes safely despite exceptions'](assert) { let exc = new Error('Something unexpected happened!'); let obj = EmberObject.extend({ companyName: computed({ @@ -75,6 +75,8 @@ moduleFor( } } + await runLoopSettled(); + assert.equal(firstNameChangedCount, 1, 'firstName should have fired once'); } diff --git a/packages/@ember/-internals/runtime/tests/mutable-array/addObject-test.js b/packages/@ember/-internals/runtime/tests/mutable-array/addObject-test.js index 05b92d40e77..47bb152536f 100644 --- a/packages/@ember/-internals/runtime/tests/mutable-array/addObject-test.js +++ b/packages/@ember/-internals/runtime/tests/mutable-array/addObject-test.js @@ -1,5 +1,5 @@ import { get } from '@ember/-internals/metal'; -import { AbstractTestCase } from 'internal-test-helpers'; +import { AbstractTestCase, runLoopSettled } from 'internal-test-helpers'; import { runArrayTests, newFixture } from '../helpers/array'; class AddObjectTest extends AbstractTestCase { @@ -9,7 +9,7 @@ class AddObjectTest extends AbstractTestCase { this.assert.equal(obj.addObject(before[1]), obj, 'should return receiver'); } - '@test [A,B].addObject(C) => [A,B,C] + notify'() { + async '@test [A,B].addObject(C) => [A,B,C] + notify'() { let before = newFixture(2); let item = newFixture(1)[0]; let after = [before[0], before[1], item]; @@ -20,6 +20,9 @@ class AddObjectTest extends AbstractTestCase { obj.addObject(item); + // flush observers + await runLoopSettled(); + this.assert.deepEqual(this.toArray(obj), after, 'post item results'); this.assert.equal(get(obj, 'length'), after.length, 'length'); @@ -41,7 +44,7 @@ class AddObjectTest extends AbstractTestCase { } } - '@test [A,B,C].addObject(A) => [A,B,C] + NO notify'() { + async '@test [A,B,C].addObject(A) => [A,B,C] + NO notify'() { let before = newFixture(3); let after = before; let item = before[0]; @@ -52,6 +55,9 @@ class AddObjectTest extends AbstractTestCase { obj.addObject(item); // note: item in set + // flush observers + await runLoopSettled(); + this.assert.deepEqual(this.toArray(obj), after, 'post item results'); this.assert.equal(get(obj, 'length'), after.length, 'length'); diff --git a/packages/@ember/-internals/runtime/tests/mutable-array/clear-test.js b/packages/@ember/-internals/runtime/tests/mutable-array/clear-test.js index ff858612c41..f378e4b16d0 100644 --- a/packages/@ember/-internals/runtime/tests/mutable-array/clear-test.js +++ b/packages/@ember/-internals/runtime/tests/mutable-array/clear-test.js @@ -1,14 +1,17 @@ import { get } from '@ember/-internals/metal'; -import { AbstractTestCase } from 'internal-test-helpers'; +import { AbstractTestCase, runLoopSettled } from 'internal-test-helpers'; import { runArrayTests, newFixture } from '../helpers/array'; class ClearTests extends AbstractTestCase { - '@test [].clear() => [] + notify'() { + async '@test [].clear() => [] + notify'() { let before = []; let after = []; let obj = this.newObject(before); let observer = this.newObserver(obj, '[]', '@each', 'length', 'firstObject', 'lastObject'); + // flush observers + await runLoopSettled(); + obj.getProperties('firstObject', 'lastObject'); /* Prime the cache */ this.assert.equal(obj.clear(), obj, 'return self'); @@ -31,7 +34,7 @@ class ClearTests extends AbstractTestCase { ); } - '@test [X].clear() => [] + notify'() { + async '@test [X].clear() => [] + notify'() { var obj, before, after, observer; before = newFixture(1); @@ -42,6 +45,9 @@ class ClearTests extends AbstractTestCase { this.assert.equal(obj.clear(), obj, 'return self'); + // flush observers + await runLoopSettled(); + this.assert.deepEqual(this.toArray(obj), after, 'post item results'); this.assert.equal(get(obj, 'length'), after.length, 'length'); diff --git a/packages/@ember/-internals/runtime/tests/mutable-array/insertAt-test.js b/packages/@ember/-internals/runtime/tests/mutable-array/insertAt-test.js index 72cbf89c194..7d07645c32d 100644 --- a/packages/@ember/-internals/runtime/tests/mutable-array/insertAt-test.js +++ b/packages/@ember/-internals/runtime/tests/mutable-array/insertAt-test.js @@ -1,9 +1,9 @@ import { get } from '@ember/-internals/metal'; -import { AbstractTestCase } from 'internal-test-helpers'; +import { AbstractTestCase, runLoopSettled } from 'internal-test-helpers'; import { runArrayTests, newFixture } from '../helpers/array'; class InsertAtTests extends AbstractTestCase { - '@test [].insertAt(0, X) => [X] + notify'() { + async '@test [].insertAt(0, X) => [X] + notify'() { let after = newFixture(1); let obj = this.newObject([]); let observer = this.newObserver(obj, '[]', '@each', 'length', 'firstObject', 'lastObject'); @@ -12,6 +12,9 @@ class InsertAtTests extends AbstractTestCase { obj.insertAt(0, after[0]); + // flush observers + await runLoopSettled(); + this.assert.deepEqual(this.toArray(obj), after, 'post item results'); this.assert.equal(get(obj, 'length'), after.length, 'length'); @@ -44,7 +47,7 @@ class InsertAtTests extends AbstractTestCase { expectAssertion(() => obj.insertAt(200, item), /`insertAt` index provided is out of range/); } - '@test [A].insertAt(0, X) => [X,A] + notify'() { + async '@test [A].insertAt(0, X) => [X,A] + notify'() { let item = newFixture(1)[0]; let before = newFixture(1); let after = [item, before[0]]; @@ -55,6 +58,9 @@ class InsertAtTests extends AbstractTestCase { obj.insertAt(0, item); + // flush observers + await runLoopSettled(); + this.assert.deepEqual(this.toArray(obj), after, 'post item results'); this.assert.equal(get(obj, 'length'), after.length, 'length'); @@ -74,7 +80,7 @@ class InsertAtTests extends AbstractTestCase { ); } - '@test [A].insertAt(1, X) => [A,X] + notify'() { + async '@test [A].insertAt(1, X) => [A,X] + notify'() { let item = newFixture(1)[0]; let before = newFixture(1); let after = [before[0], item]; @@ -85,6 +91,9 @@ class InsertAtTests extends AbstractTestCase { obj.insertAt(1, item); + // flush observers + await runLoopSettled(); + this.assert.deepEqual(this.toArray(obj), after, 'post item results'); this.assert.equal(get(obj, 'length'), after.length, 'length'); @@ -111,7 +120,7 @@ class InsertAtTests extends AbstractTestCase { this.assert.throws(() => obj.insertAt(200, that.newFixture(1)[0]), Error); } - '@test [A,B,C].insertAt(0,X) => [X,A,B,C] + notify'() { + async '@test [A,B,C].insertAt(0,X) => [X,A,B,C] + notify'() { let item = newFixture(1)[0]; let before = newFixture(3); let after = [item, before[0], before[1], before[2]]; @@ -122,6 +131,8 @@ class InsertAtTests extends AbstractTestCase { obj.insertAt(0, item); + await runLoopSettled(); + this.assert.deepEqual(this.toArray(obj), after, 'post item results'); this.assert.equal(get(obj, 'length'), after.length, 'length'); @@ -141,7 +152,7 @@ class InsertAtTests extends AbstractTestCase { ); } - '@test [A,B,C].insertAt(1,X) => [A,X,B,C] + notify'() { + async '@test [A,B,C].insertAt(1,X) => [A,X,B,C] + notify'() { let item = newFixture(1)[0]; let before = newFixture(3); let after = [before[0], item, before[1], before[2]]; @@ -160,6 +171,10 @@ class InsertAtTests extends AbstractTestCase { objectAtCalls.splice(0, objectAtCalls.length); obj.insertAt(1, item); + + // flush observers + await runLoopSettled(); + this.assert.deepEqual(objectAtCalls, [], 'objectAt is not called when only inserting items'); this.assert.deepEqual(this.toArray(obj), after, 'post item results'); @@ -181,7 +196,7 @@ class InsertAtTests extends AbstractTestCase { ); } - '@test [A,B,C].insertAt(3,X) => [A,B,C,X] + notify'() { + async '@test [A,B,C].insertAt(3,X) => [A,B,C,X] + notify'() { let item = newFixture(1)[0]; let before = newFixture(3); let after = [before[0], before[1], before[2], item]; @@ -192,6 +207,9 @@ class InsertAtTests extends AbstractTestCase { obj.insertAt(3, item); + // flush observers + await runLoopSettled(); + this.assert.deepEqual(this.toArray(obj), after, 'post item results'); this.assert.equal(get(obj, 'length'), after.length, 'length'); diff --git a/packages/@ember/-internals/runtime/tests/mutable-array/popObject-test.js b/packages/@ember/-internals/runtime/tests/mutable-array/popObject-test.js index 1f674d5c295..ed69e1750a4 100644 --- a/packages/@ember/-internals/runtime/tests/mutable-array/popObject-test.js +++ b/packages/@ember/-internals/runtime/tests/mutable-array/popObject-test.js @@ -1,9 +1,9 @@ import { get } from '@ember/-internals/metal'; -import { AbstractTestCase } from 'internal-test-helpers'; +import { AbstractTestCase, runLoopSettled } from 'internal-test-helpers'; import { runArrayTests, newFixture } from '../helpers/array'; class PopObjectTests extends AbstractTestCase { - '@test [].popObject() => [] + returns undefined + NO notify'() { + async '@test [].popObject() => [] + returns undefined + NO notify'() { let obj = this.newObject([]); let observer = this.newObserver(obj, '[]', '@each', 'length', 'firstObject', 'lastObject'); @@ -11,6 +11,9 @@ class PopObjectTests extends AbstractTestCase { this.assert.equal(obj.popObject(), undefined, 'popObject results'); + // flush observers + await runLoopSettled(); + this.assert.deepEqual(this.toArray(obj), [], 'post item results'); this.assert.equal(observer.validate('[]'), false, 'should NOT have notified []'); @@ -28,7 +31,7 @@ class PopObjectTests extends AbstractTestCase { ); } - '@test [X].popObject() => [] + notify'() { + async '@test [X].popObject() => [] + notify'() { let before = newFixture(1); let after = []; let obj = this.newObject(before); @@ -38,6 +41,9 @@ class PopObjectTests extends AbstractTestCase { let ret = obj.popObject(); + // flush observers + await runLoopSettled(); + this.assert.equal(ret, before[0], 'return object'); this.assert.deepEqual(this.toArray(obj), after, 'post item results'); this.assert.equal(get(obj, 'length'), after.length, 'length'); @@ -57,7 +63,7 @@ class PopObjectTests extends AbstractTestCase { ); } - '@test [A,B,C].popObject() => [A,B] + notify'() { + async '@test [A,B,C].popObject() => [A,B] + notify'() { let before = newFixture(3); let after = [before[0], before[1]]; let obj = this.newObject(before); @@ -67,6 +73,9 @@ class PopObjectTests extends AbstractTestCase { let ret = obj.popObject(); + // flush observers + await runLoopSettled(); + this.assert.equal(ret, before[2], 'return object'); this.assert.deepEqual(this.toArray(obj), after, 'post item results'); this.assert.equal(get(obj, 'length'), after.length, 'length'); diff --git a/packages/@ember/-internals/runtime/tests/mutable-array/pushObject-test.js b/packages/@ember/-internals/runtime/tests/mutable-array/pushObject-test.js index f04dd4744b0..5fc74a0c414 100644 --- a/packages/@ember/-internals/runtime/tests/mutable-array/pushObject-test.js +++ b/packages/@ember/-internals/runtime/tests/mutable-array/pushObject-test.js @@ -1,5 +1,5 @@ import { get } from '@ember/-internals/metal'; -import { AbstractTestCase } from 'internal-test-helpers'; +import { AbstractTestCase, runLoopSettled } from 'internal-test-helpers'; import { runArrayTests, newFixture } from '../helpers/array'; class PushObjectTests extends AbstractTestCase { @@ -10,7 +10,7 @@ class PushObjectTests extends AbstractTestCase { this.assert.equal(obj.pushObject(exp), exp, 'should return pushed object'); } - '@test [].pushObject(X) => [X] + notify'() { + async '@test [].pushObject(X) => [X] + notify'() { let before = []; let after = newFixture(1); let obj = this.newObject(before); @@ -20,6 +20,9 @@ class PushObjectTests extends AbstractTestCase { obj.pushObject(after[0]); + // flush observers + await runLoopSettled(); + this.assert.deepEqual(this.toArray(obj), after, 'post item results'); this.assert.equal(get(obj, 'length'), after.length, 'length'); @@ -38,7 +41,7 @@ class PushObjectTests extends AbstractTestCase { ); } - '@test [A,B,C].pushObject(X) => [A,B,C,X] + notify'() { + async '@test [A,B,C].pushObject(X) => [A,B,C,X] + notify'() { let before = newFixture(3); let item = newFixture(1)[0]; let after = [before[0], before[1], before[2], item]; @@ -49,6 +52,9 @@ class PushObjectTests extends AbstractTestCase { obj.pushObject(item); + // flush observers + await runLoopSettled(); + this.assert.deepEqual(this.toArray(obj), after, 'post item results'); this.assert.equal(get(obj, 'length'), after.length, 'length'); @@ -68,7 +74,7 @@ class PushObjectTests extends AbstractTestCase { ); } - '@test [A,B,C,C].pushObject(A) => [A,B,C,C] + notify'() { + async '@test [A,B,C,C].pushObject(A) => [A,B,C,C] + notify'() { let before = newFixture(3); let item = before[2]; // note same object as current tail. should end up twice let after = [before[0], before[1], before[2], item]; @@ -79,6 +85,9 @@ class PushObjectTests extends AbstractTestCase { obj.pushObject(item); + // flush observers + await runLoopSettled(); + this.assert.deepEqual(this.toArray(obj), after, 'post item results'); this.assert.equal(get(obj, 'length'), after.length, 'length'); diff --git a/packages/@ember/-internals/runtime/tests/mutable-array/removeAt-test.js b/packages/@ember/-internals/runtime/tests/mutable-array/removeAt-test.js index fbc3e8ca8d5..d1bc397c85c 100644 --- a/packages/@ember/-internals/runtime/tests/mutable-array/removeAt-test.js +++ b/packages/@ember/-internals/runtime/tests/mutable-array/removeAt-test.js @@ -1,10 +1,10 @@ -import { AbstractTestCase } from 'internal-test-helpers'; +import { AbstractTestCase, runLoopSettled } from 'internal-test-helpers'; import { runArrayTests, newFixture } from '../helpers/array'; import { removeAt } from '../../lib/mixins/array'; import { get } from '@ember/-internals/metal'; class RemoveAtTests extends AbstractTestCase { - '@test removeAt([X], 0) => [] + notify'() { + async '@test removeAt([X], 0) => [] + notify'() { let before = newFixture(1); let after = []; let obj = this.newObject(before); @@ -14,6 +14,9 @@ class RemoveAtTests extends AbstractTestCase { this.assert.equal(removeAt(obj, 0), obj, 'return self'); + // flush observers + await runLoopSettled(); + this.assert.deepEqual(this.toArray(obj), after, 'post item results'); this.assert.equal(get(obj, 'length'), after.length, 'length'); @@ -37,7 +40,7 @@ class RemoveAtTests extends AbstractTestCase { expectAssertion(() => removeAt(obj, 200), /`removeAt` index provided is out of range/); } - '@test removeAt([A,B], 0) => [B] + notify'() { + async '@test removeAt([A,B], 0) => [B] + notify'() { let before = newFixture(2); let after = [before[1]]; let obj = this.newObject(before); @@ -47,6 +50,9 @@ class RemoveAtTests extends AbstractTestCase { this.assert.equal(removeAt(obj, 0), obj, 'return self'); + // flush observers + await runLoopSettled(); + this.assert.deepEqual(this.toArray(obj), after, 'post item results'); this.assert.equal(get(obj, 'length'), after.length, 'length'); @@ -66,7 +72,7 @@ class RemoveAtTests extends AbstractTestCase { ); } - '@test removeAt([A,B], 1) => [A] + notify'() { + async '@test removeAt([A,B], 1) => [A] + notify'() { let before = newFixture(2); let after = [before[0]]; let obj = this.newObject(before); @@ -76,6 +82,9 @@ class RemoveAtTests extends AbstractTestCase { this.assert.equal(removeAt(obj, 1), obj, 'return self'); + // flush observers + await runLoopSettled(); + this.assert.deepEqual(this.toArray(obj), after, 'post item results'); this.assert.equal(get(obj, 'length'), after.length, 'length'); @@ -95,7 +104,7 @@ class RemoveAtTests extends AbstractTestCase { ); } - '@test removeAt([A,B,C], 1) => [A,C] + notify'() { + async '@test removeAt([A,B,C], 1) => [A,C] + notify'() { let before = newFixture(3); let after = [before[0], before[2]]; let obj = this.newObject(before); @@ -105,6 +114,9 @@ class RemoveAtTests extends AbstractTestCase { this.assert.equal(removeAt(obj, 1), obj, 'return self'); + // flush observers + await runLoopSettled(); + this.assert.deepEqual(this.toArray(obj), after, 'post item results'); this.assert.equal(get(obj, 'length'), after.length, 'length'); @@ -124,7 +136,7 @@ class RemoveAtTests extends AbstractTestCase { ); } - '@test removeAt([A,B,C,D], 1,2) => [A,D] + notify'() { + async '@test removeAt([A,B,C,D], 1,2) => [A,D] + notify'() { let before = newFixture(4); let after = [before[0], before[3]]; let obj = this.newObject(before); @@ -134,6 +146,9 @@ class RemoveAtTests extends AbstractTestCase { this.assert.equal(removeAt(obj, 1, 2), obj, 'return self'); + // flush observers + await runLoopSettled(); + this.assert.deepEqual(this.toArray(obj), after, 'post item results'); this.assert.equal(get(obj, 'length'), after.length, 'length'); @@ -153,7 +168,7 @@ class RemoveAtTests extends AbstractTestCase { ); } - '@test [A,B,C,D].removeAt(1,2) => [A,D] + notify'() { + async '@test [A,B,C,D].removeAt(1,2) => [A,D] + notify'() { var obj, before, after, observer; before = newFixture(4); @@ -164,6 +179,9 @@ class RemoveAtTests extends AbstractTestCase { this.assert.equal(obj.removeAt(1, 2), obj, 'return self'); + // flush observers + await runLoopSettled(); + this.assert.deepEqual(this.toArray(obj), after, 'post item results'); this.assert.equal(get(obj, 'length'), after.length, 'length'); diff --git a/packages/@ember/-internals/runtime/tests/mutable-array/removeObject-test.js b/packages/@ember/-internals/runtime/tests/mutable-array/removeObject-test.js index 8e70283982d..a4c91a6f622 100644 --- a/packages/@ember/-internals/runtime/tests/mutable-array/removeObject-test.js +++ b/packages/@ember/-internals/runtime/tests/mutable-array/removeObject-test.js @@ -1,5 +1,5 @@ import { get } from '@ember/-internals/metal'; -import { AbstractTestCase } from 'internal-test-helpers'; +import { AbstractTestCase, runLoopSettled } from 'internal-test-helpers'; import { runArrayTests, newFixture } from '../helpers/array'; class RemoveObjectTests extends AbstractTestCase { @@ -10,7 +10,7 @@ class RemoveObjectTests extends AbstractTestCase { this.assert.equal(obj.removeObject(before[1]), obj, 'should return receiver'); } - '@test [A,B,C].removeObject(B) => [A,C] + notify'() { + async '@test [A,B,C].removeObject(B) => [A,C] + notify'() { let before = newFixture(3); let after = [before[0], before[2]]; let obj = this.newObject(before); @@ -20,6 +20,9 @@ class RemoveObjectTests extends AbstractTestCase { obj.removeObject(before[1]); + // flush observers + await runLoopSettled(); + this.assert.deepEqual(this.toArray(obj), after, 'post item results'); this.assert.equal(get(obj, 'length'), after.length, 'length'); @@ -41,7 +44,7 @@ class RemoveObjectTests extends AbstractTestCase { } } - '@test [A,B,C].removeObject(D) => [A,B,C]'() { + async '@test [A,B,C].removeObject(D) => [A,B,C]'() { let before = newFixture(3); let after = before; let item = newFixture(1)[0]; @@ -52,6 +55,9 @@ class RemoveObjectTests extends AbstractTestCase { obj.removeObject(item); // note: item not in set + // flush observers + await runLoopSettled(); + this.assert.deepEqual(this.toArray(obj), after, 'post item results'); this.assert.equal(get(obj, 'length'), after.length, 'length'); 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 e6374f1a27b..7e18699fe97 100644 --- a/packages/@ember/-internals/runtime/tests/mutable-array/removeObjects-test.js +++ b/packages/@ember/-internals/runtime/tests/mutable-array/removeObjects-test.js @@ -1,5 +1,5 @@ import { get } from '@ember/-internals/metal'; -import { AbstractTestCase } from 'internal-test-helpers'; +import { AbstractTestCase, runLoopSettled } from 'internal-test-helpers'; import { runArrayTests, newFixture, newObjectsFixture } from '../helpers/array'; import { A as emberA } from '../../lib/mixins/array'; @@ -11,7 +11,7 @@ class RemoveObjectsTests extends AbstractTestCase { this.assert.equal(obj.removeObjects(before[1]), obj, 'should return receiver'); } - '@test [A,B,C].removeObjects([B]) => [A,C] + notify'() { + async '@test [A,B,C].removeObjects([B]) => [A,C] + notify'() { let before = emberA(newFixture(3)); let after = [before[0], before[2]]; let obj = before; @@ -21,6 +21,9 @@ class RemoveObjectsTests extends AbstractTestCase { obj.removeObjects([before[1]]); + // flush observers + await runLoopSettled(); + this.assert.deepEqual(this.toArray(obj), after, 'post item results'); this.assert.equal(get(obj, 'length'), after.length, 'length'); @@ -41,7 +44,7 @@ class RemoveObjectsTests extends AbstractTestCase { } } - '@test [{A},{B},{C}].removeObjects([{B}]) => [{A},{C}] + notify'() { + async '@test [{A},{B},{C}].removeObjects([{B}]) => [{A},{C}] + notify'() { let before = emberA(newObjectsFixture(3)); let after = [before[0], before[2]]; let obj = before; @@ -51,6 +54,9 @@ class RemoveObjectsTests extends AbstractTestCase { obj.removeObjects([before[1]]); + // flush observers + await runLoopSettled(); + this.assert.deepEqual(this.toArray(obj), after, 'post item results'); this.assert.equal(get(obj, 'length'), after.length, 'length'); @@ -71,7 +77,7 @@ class RemoveObjectsTests extends AbstractTestCase { } } - '@test [A,B,C].removeObjects([A,B]) => [C] + notify'() { + async '@test [A,B,C].removeObjects([A,B]) => [C] + notify'() { let before = emberA(newFixture(3)); let after = [before[2]]; let obj = before; @@ -81,6 +87,9 @@ class RemoveObjectsTests extends AbstractTestCase { obj.removeObjects([before[0], before[1]]); + // flush observers + await runLoopSettled(); + this.assert.deepEqual(this.toArray(obj), after, 'post item results'); this.assert.equal(get(obj, 'length'), after.length, 'length'); @@ -97,7 +106,7 @@ class RemoveObjectsTests extends AbstractTestCase { } } - '@test [{A},{B},{C}].removeObjects([{A},{B}]) => [{C}] + notify'() { + async '@test [{A},{B},{C}].removeObjects([{A},{B}]) => [{C}] + notify'() { let before = emberA(newObjectsFixture(3)); let after = [before[2]]; let obj = before; @@ -107,6 +116,9 @@ class RemoveObjectsTests extends AbstractTestCase { obj.removeObjects([before[0], before[1]]); + // flush observers + await runLoopSettled(); + this.assert.deepEqual(this.toArray(obj), after, 'post item results'); this.assert.equal(get(obj, 'length'), after.length, 'length'); @@ -123,7 +135,7 @@ class RemoveObjectsTests extends AbstractTestCase { } } - '@test [A,B,C].removeObjects([A,B,C]) => [] + notify'() { + async '@test [A,B,C].removeObjects([A,B,C]) => [] + notify'() { let before = emberA(newFixture(3)); let after = []; let obj = before; @@ -133,6 +145,9 @@ class RemoveObjectsTests extends AbstractTestCase { obj.removeObjects([before[0], before[1], before[2]]); + // flush observers + await runLoopSettled(); + this.assert.deepEqual(this.toArray(obj), after, 'post item results'); this.assert.equal(get(obj, 'length'), after.length, 'length'); @@ -145,7 +160,7 @@ class RemoveObjectsTests extends AbstractTestCase { } } - '@test [{A},{B},{C}].removeObjects([{A},{B},{C}]) => [] + notify'() { + async '@test [{A},{B},{C}].removeObjects([{A},{B},{C}]) => [] + notify'() { let before = emberA(newObjectsFixture(3)); let after = []; let obj = before; @@ -155,6 +170,9 @@ class RemoveObjectsTests extends AbstractTestCase { obj.removeObjects(before); + // flush observers + await runLoopSettled(); + this.assert.deepEqual(this.toArray(obj), after, 'post item results'); this.assert.equal(get(obj, 'length'), after.length, 'length'); @@ -167,7 +185,7 @@ class RemoveObjectsTests extends AbstractTestCase { } } - '@test [A,B,C].removeObjects([D]) => [A,B,C]'() { + async '@test [A,B,C].removeObjects([D]) => [A,B,C]'() { let before = emberA(newFixture(3)); let after = before; let item = newFixture(1)[0]; @@ -178,6 +196,9 @@ class RemoveObjectsTests extends AbstractTestCase { obj.removeObjects([item]); // Note: item not in set + // flush observers + await runLoopSettled(); + this.assert.deepEqual(this.toArray(obj), after, 'post item results'); this.assert.equal(get(obj, 'length'), after.length, 'length'); diff --git a/packages/@ember/-internals/runtime/tests/mutable-array/replace-test.js b/packages/@ember/-internals/runtime/tests/mutable-array/replace-test.js index eff2298f72c..8e446a82fe0 100644 --- a/packages/@ember/-internals/runtime/tests/mutable-array/replace-test.js +++ b/packages/@ember/-internals/runtime/tests/mutable-array/replace-test.js @@ -1,8 +1,8 @@ -import { AbstractTestCase } from 'internal-test-helpers'; +import { AbstractTestCase, runLoopSettled } from 'internal-test-helpers'; import { runArrayTests, newFixture } from '../helpers/array'; class ReplaceTests extends AbstractTestCase { - "@test [].replace(0,0,'X') => ['X'] + notify"() { + async "@test [].replace(0,0,'X') => ['X'] + notify"() { let exp = newFixture(1); let obj = this.newObject([]); let observer = this.newObserver(obj, '[]', '@each', 'length', 'firstObject', 'lastObject'); @@ -11,6 +11,9 @@ class ReplaceTests extends AbstractTestCase { obj.replace(0, 0, exp); + // flush observers + await runLoopSettled(); + this.assert.deepEqual(this.toArray(obj), exp, 'post item results'); this.assert.equal(observer.timesCalled('[]'), 1, 'should have notified [] once'); @@ -28,7 +31,7 @@ class ReplaceTests extends AbstractTestCase { ); } - '@test [].replace(0,0,"X") => ["X"] + avoid calling objectAt and notifying fistObject/lastObject when not in cache'() { + async '@test [].replace(0,0,"X") => ["X"] + avoid calling objectAt and notifying fistObject/lastObject when not in cache'() { var obj, exp, observer; var called = 0; exp = newFixture(1); @@ -40,6 +43,9 @@ class ReplaceTests extends AbstractTestCase { obj.replace(0, 0, exp); + // flush observers + await runLoopSettled(); + this.assert.equal( called, 0, @@ -57,7 +63,7 @@ class ReplaceTests extends AbstractTestCase { ); } - '@test [A,B,C,D].replace(1,2,X) => [A,X,D] + notify'() { + async '@test [A,B,C,D].replace(1,2,X) => [A,X,D] + notify'() { let before = newFixture(4); let replace = newFixture(1); let after = [before[0], replace[0], before[3]]; @@ -69,6 +75,9 @@ class ReplaceTests extends AbstractTestCase { obj.replace(1, 2, replace); + // flush observers + await runLoopSettled(); + this.assert.deepEqual(this.toArray(obj), after, 'post item results'); this.assert.equal(observer.timesCalled('[]'), 1, 'should have notified [] once'); @@ -87,7 +96,7 @@ class ReplaceTests extends AbstractTestCase { ); } - '@test [A,B,C,D].replace(1,2,[X,Y]) => [A,X,Y,D] + notify'() { + async '@test [A,B,C,D].replace(1,2,[X,Y]) => [A,X,Y,D] + notify'() { let before = newFixture(4); let replace = newFixture(2); let after = [before[0], replace[0], replace[1], before[3]]; @@ -99,6 +108,9 @@ class ReplaceTests extends AbstractTestCase { obj.replace(1, 2, replace); + // flush observers + await runLoopSettled(); + this.assert.deepEqual(this.toArray(obj), after, 'post item results'); this.assert.equal(observer.timesCalled('[]'), 1, 'should have notified [] once'); @@ -117,7 +129,7 @@ class ReplaceTests extends AbstractTestCase { ); } - '@test [A,B].replace(1,0,[X,Y]) => [A,X,Y,B] + notify'() { + async '@test [A,B].replace(1,0,[X,Y]) => [A,X,Y,B] + notify'() { let before = newFixture(2); let replace = newFixture(2); let after = [before[0], replace[0], replace[1], before[1]]; @@ -129,6 +141,9 @@ class ReplaceTests extends AbstractTestCase { obj.replace(1, 0, replace); + // flush observers + await runLoopSettled(); + this.assert.deepEqual(this.toArray(obj), after, 'post item results'); this.assert.equal(observer.timesCalled('[]'), 1, 'should have notified [] once'); @@ -147,7 +162,7 @@ class ReplaceTests extends AbstractTestCase { ); } - '@test [A,B,C,D].replace(2,2) => [A,B] + notify'() { + async '@test [A,B,C,D].replace(2,2) => [A,B] + notify'() { let before = newFixture(4); let after = [before[0], before[1]]; @@ -158,6 +173,9 @@ class ReplaceTests extends AbstractTestCase { obj.replace(2, 2); + // flush observers + await runLoopSettled(); + this.assert.deepEqual(this.toArray(obj), after, 'post item results'); this.assert.equal(observer.timesCalled('[]'), 1, 'should have notified [] once'); @@ -176,7 +194,7 @@ class ReplaceTests extends AbstractTestCase { ); } - '@test [A,B,C,D].replace(-1,1) => [A,B,C] + notify'() { + async '@test [A,B,C,D].replace(-1,1) => [A,B,C] + notify'() { let before = newFixture(4); let after = [before[0], before[1], before[2]]; @@ -187,6 +205,9 @@ class ReplaceTests extends AbstractTestCase { obj.replace(-1, 1); + // flush observers + await runLoopSettled(); + this.assert.deepEqual(this.toArray(obj), after, 'post item results'); this.assert.equal(observer.timesCalled('[]'), 1, 'should have notified [] once'); @@ -205,7 +226,7 @@ class ReplaceTests extends AbstractTestCase { ); } - '@test Adding object should notify array observer'() { + async '@test Adding object should notify array observer'() { let fixtures = newFixture(4); let obj = this.newObject(fixtures); let observer = this.newObserver(obj).observeArray(obj); @@ -213,6 +234,9 @@ class ReplaceTests extends AbstractTestCase { obj.replace(2, 2, [item]); + // flush observers + await runLoopSettled(); + this.assert.deepEqual(observer._before, [obj, 2, 2, 1], 'before'); this.assert.deepEqual(observer._after, [obj, 2, 2, 1], 'after'); } diff --git a/packages/@ember/-internals/runtime/tests/mutable-array/reverseObjects-test.js b/packages/@ember/-internals/runtime/tests/mutable-array/reverseObjects-test.js index fa3eaa8afc2..0a82e05d3bd 100644 --- a/packages/@ember/-internals/runtime/tests/mutable-array/reverseObjects-test.js +++ b/packages/@ember/-internals/runtime/tests/mutable-array/reverseObjects-test.js @@ -1,9 +1,9 @@ -import { AbstractTestCase } from 'internal-test-helpers'; +import { AbstractTestCase, runLoopSettled } from 'internal-test-helpers'; import { runArrayTests, newFixture } from '../helpers/array'; import { get } from '@ember/-internals/metal'; class ReverseObjectsTests extends AbstractTestCase { - '@test [A,B,C].reverseObjects() => [] + notify'() { + async '@test [A,B,C].reverseObjects() => [] + notify'() { let before = newFixture(3); let after = [before[2], before[1], before[0]]; let obj = this.newObject(before); @@ -13,6 +13,9 @@ class ReverseObjectsTests extends AbstractTestCase { this.assert.equal(obj.reverseObjects(), obj, 'return self'); + // flush observers + await runLoopSettled(); + this.assert.deepEqual(this.toArray(obj), after, 'post item results'); this.assert.equal(get(obj, 'length'), after.length, 'length'); diff --git a/packages/@ember/-internals/runtime/tests/mutable-array/setObjects-test.js b/packages/@ember/-internals/runtime/tests/mutable-array/setObjects-test.js index 3e9b7bcd21c..3229c83ce07 100644 --- a/packages/@ember/-internals/runtime/tests/mutable-array/setObjects-test.js +++ b/packages/@ember/-internals/runtime/tests/mutable-array/setObjects-test.js @@ -1,9 +1,9 @@ -import { AbstractTestCase } from 'internal-test-helpers'; +import { AbstractTestCase, runLoopSettled } from 'internal-test-helpers'; import { runArrayTests, newFixture } from '../helpers/array'; import { get } from '@ember/-internals/metal'; class SetObjectsTests extends AbstractTestCase { - '@test [A,B,C].setObjects([]) = > [] + notify'() { + async '@test [A,B,C].setObjects([]) = > [] + notify'() { let before = newFixture(3); let after = []; let obj = this.newObject(before); @@ -13,6 +13,9 @@ class SetObjectsTests extends AbstractTestCase { this.assert.equal(obj.setObjects(after), obj, 'return self'); + // flush observers + await runLoopSettled(); + this.assert.deepEqual(this.toArray(obj), after, 'post item results'); this.assert.equal(get(obj, 'length'), after.length, 'length'); @@ -31,7 +34,7 @@ class SetObjectsTests extends AbstractTestCase { ); } - '@test [A,B,C].setObjects([D, E, F, G]) = > [D, E, F, G] + notify'() { + async '@test [A,B,C].setObjects([D, E, F, G]) = > [D, E, F, G] + notify'() { let before = newFixture(3); let after = newFixture(4); let obj = this.newObject(before); @@ -41,6 +44,9 @@ class SetObjectsTests extends AbstractTestCase { this.assert.equal(obj.setObjects(after), obj, 'return self'); + // flush observers + await runLoopSettled(); + this.assert.deepEqual(this.toArray(obj), after, 'post item results'); this.assert.equal(get(obj, 'length'), after.length, 'length'); diff --git a/packages/@ember/-internals/runtime/tests/mutable-array/shiftObject-test.js b/packages/@ember/-internals/runtime/tests/mutable-array/shiftObject-test.js index 44729d77a00..95560280345 100644 --- a/packages/@ember/-internals/runtime/tests/mutable-array/shiftObject-test.js +++ b/packages/@ember/-internals/runtime/tests/mutable-array/shiftObject-test.js @@ -1,9 +1,9 @@ -import { AbstractTestCase } from 'internal-test-helpers'; +import { AbstractTestCase, runLoopSettled } from 'internal-test-helpers'; import { runArrayTests, newFixture } from '../helpers/array'; import { get } from '@ember/-internals/metal'; class ShiftObjectTests extends AbstractTestCase { - '@test [].shiftObject() => [] + returns undefined + NO notify'() { + async '@test [].shiftObject() => [] + returns undefined + NO notify'() { let before = []; let after = []; let obj = this.newObject(before); @@ -13,6 +13,9 @@ class ShiftObjectTests extends AbstractTestCase { this.assert.equal(obj.shiftObject(), undefined); + // flush observers + await runLoopSettled(); + this.assert.deepEqual(this.toArray(obj), after, 'post item results'); this.assert.equal(get(obj, 'length'), after.length, 'length'); @@ -44,7 +47,7 @@ class ShiftObjectTests extends AbstractTestCase { ); } - '@test [X].shiftObject() => [] + notify'() { + async '@test [X].shiftObject() => [] + notify'() { let before = newFixture(1); let after = []; let obj = this.newObject(before); @@ -54,6 +57,9 @@ class ShiftObjectTests extends AbstractTestCase { this.assert.equal(obj.shiftObject(), before[0], 'should return object'); + // flush observers + await runLoopSettled(); + this.assert.deepEqual(this.toArray(obj), after, 'post item results'); this.assert.equal(get(obj, 'length'), after.length, 'length'); @@ -72,7 +78,7 @@ class ShiftObjectTests extends AbstractTestCase { ); } - '@test [A,B,C].shiftObject() => [B,C] + notify'() { + async '@test [A,B,C].shiftObject() => [B,C] + notify'() { let before = newFixture(3); let after = [before[1], before[2]]; let obj = this.newObject(before); @@ -82,6 +88,9 @@ class ShiftObjectTests extends AbstractTestCase { this.assert.equal(obj.shiftObject(), before[0], 'should return object'); + // flush observers + await runLoopSettled(); + this.assert.deepEqual(this.toArray(obj), after, 'post item results'); this.assert.equal(get(obj, 'length'), after.length, 'length'); diff --git a/packages/@ember/-internals/runtime/tests/mutable-array/unshiftObject-test.js b/packages/@ember/-internals/runtime/tests/mutable-array/unshiftObject-test.js index 783b3ec2028..988865fa65e 100644 --- a/packages/@ember/-internals/runtime/tests/mutable-array/unshiftObject-test.js +++ b/packages/@ember/-internals/runtime/tests/mutable-array/unshiftObject-test.js @@ -1,4 +1,4 @@ -import { AbstractTestCase } from 'internal-test-helpers'; +import { AbstractTestCase, runLoopSettled } from 'internal-test-helpers'; import { get } from '@ember/-internals/metal'; import { runArrayTests, newFixture } from '../helpers/array'; @@ -10,7 +10,7 @@ class UnshiftObjectTests extends AbstractTestCase { this.assert.equal(obj.unshiftObject(item), item, 'should return unshifted object'); } - '@test [].unshiftObject(X) => [X] + notify'() { + async '@test [].unshiftObject(X) => [X] + notify'() { let before = []; let item = newFixture(1)[0]; let after = [item]; @@ -21,6 +21,9 @@ class UnshiftObjectTests extends AbstractTestCase { obj.unshiftObject(item); + // flush observers + await runLoopSettled(); + this.assert.deepEqual(this.toArray(obj), after, 'post item results'); this.assert.equal(get(obj, 'length'), after.length, 'length'); @@ -39,7 +42,7 @@ class UnshiftObjectTests extends AbstractTestCase { ); } - '@test [A,B,C].unshiftObject(X) => [X,A,B,C] + notify'() { + async '@test [A,B,C].unshiftObject(X) => [X,A,B,C] + notify'() { let before = newFixture(3); let item = newFixture(1)[0]; let after = [item, before[0], before[1], before[2]]; @@ -50,6 +53,9 @@ class UnshiftObjectTests extends AbstractTestCase { obj.unshiftObject(item); + // flush observers + await runLoopSettled(); + this.assert.deepEqual(this.toArray(obj), after, 'post item results'); this.assert.equal(get(obj, 'length'), after.length, 'length'); @@ -69,7 +75,7 @@ class UnshiftObjectTests extends AbstractTestCase { ); } - '@test [A,B,C].unshiftObject(A) => [A,A,B,C] + notify'() { + async '@test [A,B,C].unshiftObject(A) => [A,A,B,C] + notify'() { let before = newFixture(3); let item = before[0]; // note same object as current head. should end up twice let after = [item, before[0], before[1], before[2]]; @@ -80,6 +86,9 @@ class UnshiftObjectTests extends AbstractTestCase { obj.unshiftObject(item); + // flush observers + await runLoopSettled(); + this.assert.deepEqual(this.toArray(obj), after, 'post item results'); this.assert.equal(get(obj, 'length'), after.length, 'length'); diff --git a/packages/@ember/-internals/runtime/tests/mutable-array/unshiftObjects-test.js b/packages/@ember/-internals/runtime/tests/mutable-array/unshiftObjects-test.js index 9fb2d5ec982..b0074d0a12e 100644 --- a/packages/@ember/-internals/runtime/tests/mutable-array/unshiftObjects-test.js +++ b/packages/@ember/-internals/runtime/tests/mutable-array/unshiftObjects-test.js @@ -1,4 +1,4 @@ -import { AbstractTestCase } from 'internal-test-helpers'; +import { AbstractTestCase, runLoopSettled } from 'internal-test-helpers'; import { get } from '@ember/-internals/metal'; import { runArrayTests, newFixture } from '../helpers/array'; @@ -10,7 +10,7 @@ class UnshiftObjectsTests extends AbstractTestCase { this.assert.equal(obj.unshiftObjects(items), obj, 'should return receiver'); } - '@test [].unshiftObjects([A,B,C]) => [A,B,C] + notify'() { + async '@test [].unshiftObjects([A,B,C]) => [A,B,C] + notify'() { let before = []; let items = newFixture(3); let obj = this.newObject(before); @@ -20,6 +20,9 @@ class UnshiftObjectsTests extends AbstractTestCase { obj.unshiftObjects(items); + // flush observers + await runLoopSettled(); + this.assert.deepEqual(this.toArray(obj), items, 'post item results'); this.assert.equal(get(obj, 'length'), items.length, 'length'); @@ -38,7 +41,7 @@ class UnshiftObjectsTests extends AbstractTestCase { ); } - '@test [A,B,C].unshiftObjects([X,Y]) => [X,Y,A,B,C] + notify'() { + async '@test [A,B,C].unshiftObjects([X,Y]) => [X,Y,A,B,C] + notify'() { let before = newFixture(3); let items = newFixture(2); let after = items.concat(before); @@ -49,6 +52,9 @@ class UnshiftObjectsTests extends AbstractTestCase { obj.unshiftObjects(items); + // flush observers + await runLoopSettled(); + this.assert.deepEqual(this.toArray(obj), after, 'post item results'); this.assert.equal(get(obj, 'length'), after.length, 'length'); @@ -68,7 +74,7 @@ class UnshiftObjectsTests extends AbstractTestCase { ); } - '@test [A,B,C].unshiftObjects([A,B]) => [A,B,A,B,C] + notify'() { + async '@test [A,B,C].unshiftObjects([A,B]) => [A,B,A,B,C] + notify'() { let before = newFixture(3); let items = [before[0], before[1]]; // note same object as current head. should end up twice let after = items.concat(before); @@ -79,6 +85,9 @@ class UnshiftObjectsTests extends AbstractTestCase { obj.unshiftObjects(items); + // flush observers + await runLoopSettled(); + this.assert.deepEqual(this.toArray(obj), after, 'post item results'); this.assert.equal(get(obj, 'length'), after.length, 'length'); 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 d6bd014c72d..8218074dab7 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 @@ -3,7 +3,7 @@ import EmberObject from '../../../lib/system/object'; import { observer } from '@ember/-internals/metal'; import { oneWay as reads, not } from '@ember/object/computed'; import { A as a } from '../../../lib/mixins/array'; -import { moduleFor, AbstractTestCase, runTask } from 'internal-test-helpers'; +import { moduleFor, AbstractTestCase, runTask, runLoopSettled } from 'internal-test-helpers'; import { set, get } from '@ember/-internals/metal'; moduleFor( @@ -152,7 +152,7 @@ moduleFor( assert.deepEqual(obj.content, ['foo'], 'content length was truncated'); } - ['@test array proxy + aliasedProperty complex test'](assert) { + async ['@test array proxy + aliasedProperty complex test'](assert) { let aCalled, bCalled, cCalled, dCalled, eCalled; aCalled = bCalled = cCalled = dCalled = eCalled = 0; @@ -175,6 +175,11 @@ moduleFor( }) ); + // bootstrap aliases + obj.length; + + await runLoopSettled(); + assert.equal(obj.get('colors.content.length'), 3); assert.equal(obj.get('colors.length'), 3); assert.equal(obj.get('length'), 3); @@ -186,6 +191,7 @@ moduleFor( assert.equal(eCalled, 1, 'expected observer `colors.content.[]` to be called ONCE'); obj.get('colors').pushObjects(['green', 'red']); + await runLoopSettled(); assert.equal(obj.get('colors.content.length'), 5); assert.equal(obj.get('colors.length'), 5); diff --git a/packages/@ember/-internals/runtime/tests/system/array_proxy/watching_and_listening_test.js b/packages/@ember/-internals/runtime/tests/system/array_proxy/watching_and_listening_test.js index fe09c026d52..744dd5af1e3 100644 --- a/packages/@ember/-internals/runtime/tests/system/array_proxy/watching_and_listening_test.js +++ b/packages/@ember/-internals/runtime/tests/system/array_proxy/watching_and_listening_test.js @@ -3,6 +3,7 @@ import { get, addObserver, defineProperty, watcherCount, computed } from '@ember import ArrayProxy from '../../../lib/system/array_proxy'; import { A } from '../../../lib/mixins/array'; import { moduleFor, AbstractTestCase } from 'internal-test-helpers'; +import { EMBER_METAL_TRACKED_PROPERTIES } from '@ember/canary-features'; function sortedListenersFor(obj, eventName) { let listeners = peekMeta(obj).matchingListeners(eventName) || []; @@ -59,6 +60,11 @@ moduleFor( } [`@test regression test for https://github.com/emberjs/ember.js/issues/12475`](assert) { + if (EMBER_METAL_TRACKED_PROPERTIES) { + // Test is primarily about watching values, which is not necessary anymore + return assert.expect(0); + } + let item1a = { id: 1 }; let item1b = { id: 2 }; let item1c = { id: 3 }; diff --git a/packages/@ember/-internals/runtime/tests/system/core_object_test.js b/packages/@ember/-internals/runtime/tests/system/core_object_test.js index 076d39ff58e..0f809a78234 100644 --- a/packages/@ember/-internals/runtime/tests/system/core_object_test.js +++ b/packages/@ember/-internals/runtime/tests/system/core_object_test.js @@ -1,7 +1,7 @@ import { getOwner, setOwner } from '@ember/-internals/owner'; import { get, set, observer } from '@ember/-internals/metal'; import CoreObject from '../../lib/system/core_object'; -import { moduleFor, AbstractTestCase, buildOwner } from 'internal-test-helpers'; +import { moduleFor, AbstractTestCase, buildOwner, runLoopSettled } from 'internal-test-helpers'; moduleFor( 'Ember.CoreObject', @@ -107,7 +107,7 @@ moduleFor( }).create(options); } - ['@test observed properties are enumerable when set GH#14594'](assert) { + async ['@test observed properties are enumerable when set GH#14594'](assert) { let callCount = 0; let Test = CoreObject.extend({ myProp: null, @@ -126,6 +126,7 @@ moduleFor( set(test, 'anotherProp', 'nice'); assert.deepEqual(Object.keys(test).sort(), ['anotherProp', 'id', 'myProp']); + await runLoopSettled(); assert.equal(callCount, 1); } diff --git a/packages/@ember/-internals/runtime/tests/system/object/destroy_test.js b/packages/@ember/-internals/runtime/tests/system/object/destroy_test.js index 3f0b5d34aa7..50c3450fd38 100644 --- a/packages/@ember/-internals/runtime/tests/system/object/destroy_test.js +++ b/packages/@ember/-internals/runtime/tests/system/object/destroy_test.js @@ -9,7 +9,7 @@ import { import { peekMeta } from '@ember/-internals/meta'; import EmberObject from '../../../lib/system/object'; import { DEBUG } from '@glimmer/env'; -import { moduleFor, AbstractTestCase } from 'internal-test-helpers'; +import { moduleFor, AbstractTestCase, runLoopSettled } from 'internal-test-helpers'; moduleFor( '@ember/-internals/runtime/system/object/destroy_test', @@ -51,7 +51,7 @@ moduleFor( } } - ['@test observers should not fire after an object has been destroyed'](assert) { + async ['@test observers should not fire after an object has been destroyed'](assert) { let count = 0; let obj = EmberObject.extend({ fooDidChange: observer('foo', function() { @@ -60,20 +60,20 @@ moduleFor( }).create(); obj.set('foo', 'bar'); + await runLoopSettled(); assert.equal(count, 1, 'observer was fired once'); - run(() => { - beginPropertyChanges(); - obj.set('foo', 'quux'); - obj.destroy(); - endPropertyChanges(); - }); + beginPropertyChanges(); + obj.set('foo', 'quux'); + obj.destroy(); + endPropertyChanges(); + await runLoopSettled(); assert.equal(count, 1, 'observer was not called after object was destroyed'); } - ['@test destroyed objects should not see each others changes during teardown but a long lived object should']( + async ['@test destroyed objects should not see each others changes during teardown but a long lived object should']( assert ) { let shouldChange = 0; @@ -138,12 +138,11 @@ moduleFor( LongLivedObject.create(); - run(() => { - let keys = Object.keys(objs); - for (let i = 0; i < keys.length; i++) { - objs[keys[i]].destroy(); - } - }); + for (let obj in objs) { + objs[obj].destroy(); + } + + await runLoopSettled(); assert.equal(shouldNotChange, 0, 'destroyed graph objs should not see change in willDestroy'); assert.equal(shouldChange, 1, 'long lived should see change in willDestroy'); diff --git a/packages/@ember/-internals/runtime/tests/system/object/es-compatibility-test.js b/packages/@ember/-internals/runtime/tests/system/object/es-compatibility-test.js index db9255f8b55..f53318dbf91 100644 --- a/packages/@ember/-internals/runtime/tests/system/object/es-compatibility-test.js +++ b/packages/@ember/-internals/runtime/tests/system/object/es-compatibility-test.js @@ -11,7 +11,7 @@ import { removeListener, sendEvent, } from '@ember/-internals/metal'; -import { moduleFor, AbstractTestCase } from 'internal-test-helpers'; +import { moduleFor, AbstractTestCase, runLoopSettled } from 'internal-test-helpers'; moduleFor( 'EmberObject ES Compatibility', @@ -339,25 +339,31 @@ moduleFor( let a = A.create(); a.set('foo', 'something'); - assert.equal(fooDidChangeBase, 1); - assert.equal(fooDidChangeA, 1); - assert.equal(fooDidChangeB, 0); - - sendEvent(a, 'someEvent'); - assert.equal(someEventBase, 1); - assert.equal(someEventA, 1); - assert.equal(someEventB, 0); - let b = B.create(); - b.set('foo', 'something'); - assert.equal(fooDidChangeBase, 1); - assert.equal(fooDidChangeA, 1); - assert.equal(fooDidChangeB, 0); - - sendEvent(b, 'someEvent'); - assert.equal(someEventBase, 1); - assert.equal(someEventA, 1); - assert.equal(someEventB, 0); + // TODO: Generator transpilation code doesn't play nice with class definitions/hoisting + return runLoopSettled().then(async () => { + assert.equal(fooDidChangeBase, 1); + assert.equal(fooDidChangeA, 1); + assert.equal(fooDidChangeB, 0); + + sendEvent(a, 'someEvent'); + assert.equal(someEventBase, 1); + assert.equal(someEventA, 1); + assert.equal(someEventB, 0); + + let b = B.create(); + b.set('foo', 'something'); + await runLoopSettled(); + + assert.equal(fooDidChangeBase, 1); + assert.equal(fooDidChangeA, 1); + assert.equal(fooDidChangeB, 0); + + sendEvent(b, 'someEvent'); + assert.equal(someEventBase, 1); + assert.equal(someEventA, 1); + assert.equal(someEventB, 0); + }); } '@test super and _super interop between old and new methods'(assert) { @@ -506,20 +512,24 @@ moduleFor( assert.equal(d.full, 'Robert Jackson'); d.setProperties({ first: 'Kris', last: 'Selden' }); - assert.deepEqual(changes, [ - 'D fullNameDidChange before super.fullNameDidChange', - 'B fullNameDidChange', - 'D fullNameDidChange after super.fullNameDidChange', - ]); - - assert.equal(d.full, 'Kris Selden'); - d.triggerSomeEvent('event arg'); - assert.deepEqual(events, [ - 'D onSomeEvent before super.onSomeEvent', - 'B onSomeEvent event arg', - 'D onSomeEvent after super.onSomeEvent', - ]); + // TODO: Generator transpilation code doesn't play nice with class definitions/hoisting + return runLoopSettled().then(() => { + assert.deepEqual(changes, [ + 'D fullNameDidChange before super.fullNameDidChange', + 'B fullNameDidChange', + 'D fullNameDidChange after super.fullNameDidChange', + ]); + + assert.equal(d.full, 'Kris Selden'); + + d.triggerSomeEvent('event arg'); + assert.deepEqual(events, [ + 'D onSomeEvent before super.onSomeEvent', + 'B onSomeEvent event arg', + 'D onSomeEvent after super.onSomeEvent', + ]); + }); } } ); diff --git a/packages/@ember/-internals/runtime/tests/system/object/extend_test.js b/packages/@ember/-internals/runtime/tests/system/object/extend_test.js index e82a95531a9..5e0ccf9925d 100644 --- a/packages/@ember/-internals/runtime/tests/system/object/extend_test.js +++ b/packages/@ember/-internals/runtime/tests/system/object/extend_test.js @@ -1,6 +1,6 @@ import { computed, get, observer } from '@ember/-internals/metal'; import EmberObject from '../../../lib/system/object'; -import { moduleFor, AbstractTestCase } from 'internal-test-helpers'; +import { moduleFor, AbstractTestCase, runLoopSettled } from 'internal-test-helpers'; moduleFor( 'EmberObject.extend', @@ -122,7 +122,7 @@ moduleFor( ); } - ['@test Overriding a computed property with an observer'](assert) { + async ['@test Overriding a computed property with an observer'](assert) { let Parent = EmberObject.extend({ foo: computed(function() { return 'FOO'; @@ -142,10 +142,12 @@ moduleFor( assert.deepEqual(seen, []); child.set('bar', 1); + await runLoopSettled(); assert.deepEqual(seen, [1]); child.set('bar', 2); + await runLoopSettled(); assert.deepEqual(seen, [1, 2]); } diff --git a/packages/@ember/-internals/runtime/tests/system/object/observer_test.js b/packages/@ember/-internals/runtime/tests/system/object/observer_test.js index 402ef8bcc87..cc7125346a0 100644 --- a/packages/@ember/-internals/runtime/tests/system/object/observer_test.js +++ b/packages/@ember/-internals/runtime/tests/system/object/observer_test.js @@ -1,12 +1,12 @@ import { run } from '@ember/runloop'; import { observer, get, set } from '@ember/-internals/metal'; import EmberObject from '../../../lib/system/object'; -import { moduleFor, AbstractTestCase } from 'internal-test-helpers'; +import { moduleFor, AbstractTestCase, runLoopSettled } from 'internal-test-helpers'; moduleFor( 'EmberObject observer', class extends AbstractTestCase { - ['@test observer on class'](assert) { + async ['@test observer on class'](assert) { let MyClass = EmberObject.extend({ count: 0, @@ -19,10 +19,12 @@ moduleFor( assert.equal(get(obj, 'count'), 0, 'should not invoke observer immediately'); set(obj, 'bar', 'BAZ'); + await runLoopSettled(); + assert.equal(get(obj, 'count'), 1, 'should invoke observer after change'); } - ['@test observer on subclass'](assert) { + async ['@test observer on subclass'](assert) { let MyClass = EmberObject.extend({ count: 0, @@ -41,13 +43,17 @@ moduleFor( assert.equal(get(obj, 'count'), 0, 'should not invoke observer immediately'); set(obj, 'bar', 'BAZ'); + await runLoopSettled(); + assert.equal(get(obj, 'count'), 0, 'should not invoke observer after change'); set(obj, 'baz', 'BAZ'); + await runLoopSettled(); + assert.equal(get(obj, 'count'), 1, 'should invoke observer after change'); } - ['@test observer on instance'](assert) { + async ['@test observer on instance'](assert) { let obj = EmberObject.extend({ foo: observer('bar', function() { set(this, 'count', get(this, 'count') + 1); @@ -59,10 +65,12 @@ moduleFor( assert.equal(get(obj, 'count'), 0, 'should not invoke observer immediately'); set(obj, 'bar', 'BAZ'); + await runLoopSettled(); + assert.equal(get(obj, 'count'), 1, 'should invoke observer after change'); } - ['@test observer on instance overriding class'](assert) { + async ['@test observer on instance overriding class'](assert) { let MyClass = EmberObject.extend({ count: 0, @@ -81,9 +89,13 @@ moduleFor( assert.equal(get(obj, 'count'), 0, 'should not invoke observer immediately'); set(obj, 'bar', 'BAZ'); + await runLoopSettled(); + assert.equal(get(obj, 'count'), 0, 'should not invoke observer after change'); set(obj, 'baz', 'BAZ'); + await runLoopSettled(); + assert.equal(get(obj, 'count'), 1, 'should invoke observer after change'); } @@ -110,7 +122,7 @@ moduleFor( // COMPLEX PROPERTIES // - ['@test chain observer on class'](assert) { + async ['@test chain observer on class'](assert) { let MyClass = EmberObject.extend({ count: 0, @@ -131,15 +143,19 @@ moduleFor( assert.equal(get(obj2, 'count'), 0, 'should not invoke yet'); set(get(obj1, 'bar'), 'baz', 'BIFF1'); + await runLoopSettled(); + assert.equal(get(obj1, 'count'), 1, 'should invoke observer on obj1'); assert.equal(get(obj2, 'count'), 0, 'should not invoke yet'); set(get(obj2, 'bar'), 'baz', 'BIFF2'); + await runLoopSettled(); + assert.equal(get(obj1, 'count'), 1, 'should not invoke again'); assert.equal(get(obj2, 'count'), 1, 'should invoke observer on obj2'); } - ['@test chain observer on class'](assert) { + async ['@test chain observer on class'](assert) { let MyClass = EmberObject.extend({ count: 0, @@ -165,19 +181,25 @@ moduleFor( assert.equal(get(obj2, 'count'), 0, 'should not invoke yet'); set(get(obj1, 'bar'), 'baz', 'BIFF1'); + await runLoopSettled(); + assert.equal(get(obj1, 'count'), 1, 'should invoke observer on obj1'); assert.equal(get(obj2, 'count'), 0, 'should not invoke yet'); set(get(obj2, 'bar'), 'baz', 'BIFF2'); + await runLoopSettled(); + assert.equal(get(obj1, 'count'), 1, 'should not invoke again'); assert.equal(get(obj2, 'count'), 0, 'should not invoke yet'); set(get(obj2, 'bar2'), 'baz', 'BIFF3'); + await runLoopSettled(); + assert.equal(get(obj1, 'count'), 1, 'should not invoke again'); assert.equal(get(obj2, 'count'), 1, 'should invoke observer on obj2'); } - ['@test chain observer on class that has a reference to an uninitialized object will finish chains that reference it']( + async ['@test chain observer on class that has a reference to an uninitialized object will finish chains that reference it']( assert ) { let changed = false; @@ -205,10 +227,12 @@ moduleFor( assert.equal(changed, false, 'precond'); set(parent, 'one.two', 'new'); + await runLoopSettled(); assert.equal(changed, true, 'child should have been notified of change to path'); set(parent, 'one', { two: 'newer' }); + await runLoopSettled(); assert.equal(changed, true, 'child should have been notified of change to path'); } diff --git a/packages/@ember/-internals/runtime/tests/system/object_proxy_test.js b/packages/@ember/-internals/runtime/tests/system/object_proxy_test.js index 90ac165d0ca..cc718b59c46 100644 --- a/packages/@ember/-internals/runtime/tests/system/object_proxy_test.js +++ b/packages/@ember/-internals/runtime/tests/system/object_proxy_test.js @@ -9,8 +9,9 @@ import { removeObserver, } from '@ember/-internals/metal'; import { HAS_NATIVE_PROXY } from '@ember/-internals/utils'; +import { EMBER_METAL_TRACKED_PROPERTIES } from '@ember/canary-features'; import ObjectProxy from '../../lib/system/object_proxy'; -import { moduleFor, AbstractTestCase } from 'internal-test-helpers'; +import { moduleFor, AbstractTestCase, runLoopSettled } from 'internal-test-helpers'; moduleFor( 'ObjectProxy', @@ -174,7 +175,7 @@ moduleFor( } } - ['@test should work with watched properties'](assert) { + async ['@test should work with watched properties'](assert) { let content1 = { firstName: 'Tom', lastName: 'Dale' }; let content2 = { firstName: 'Yehuda', lastName: 'Katz' }; let count = 0; @@ -194,8 +195,16 @@ moduleFor( let proxy = Proxy.create(); - addObserver(proxy, 'fullName', function() { + addObserver(proxy, 'fullName', () => { last = get(proxy, 'fullName'); + }); + + // We need separate observers for each property for async observers + addObserver(proxy, 'firstName', function() { + count++; + }); + + addObserver(proxy, 'lastName', function() { count++; }); @@ -204,38 +213,51 @@ moduleFor( // setting content causes all watched properties to change set(proxy, 'content', content1); + await runLoopSettled(); + // both dependent keys changed assert.equal(count, 2); assert.equal(last, 'Tom Dale'); // setting property in content causes proxy property to change set(content1, 'lastName', 'Huda'); + await runLoopSettled(); + assert.equal(count, 3); assert.equal(last, 'Tom Huda'); // replacing content causes all watched properties to change set(proxy, 'content', content2); + await runLoopSettled(); + // both dependent keys changed assert.equal(count, 5); assert.equal(last, 'Yehuda Katz'); - // content1 is no longer watched - assert.ok(!isWatching(content1, 'firstName'), 'not watching firstName'); - assert.ok(!isWatching(content1, 'lastName'), 'not watching lastName'); + + if (!EMBER_METAL_TRACKED_PROPERTIES) { + // content1 is no longer watched + assert.ok(!isWatching(content1, 'firstName'), 'not watching firstName'); + assert.ok(!isWatching(content1, 'lastName'), 'not watching lastName'); + } // setting property in new content set(content2, 'firstName', 'Tomhuda'); + await runLoopSettled(); + assert.equal(last, 'Tomhuda Katz'); assert.equal(count, 6); // setting property in proxy syncs with new content set(proxy, 'lastName', 'Katzdale'); + await runLoopSettled(); + assert.equal(count, 7); assert.equal(last, 'Tomhuda Katzdale'); assert.equal(get(content2, 'firstName'), 'Tomhuda'); assert.equal(get(content2, 'lastName'), 'Katzdale'); } - ['@test set and get should work with paths'](assert) { + async ['@test set and get should work with paths'](assert) { let content = { foo: { bar: 'baz' } }; let proxy = ObjectProxy.create({ content }); let count = 0; @@ -249,13 +271,14 @@ moduleFor( }); proxy.set('foo.bar', 'bye'); + await runLoopSettled(); assert.equal(count, 1); assert.equal(proxy.get('foo.bar'), 'bye'); assert.equal(proxy.get('content.foo.bar'), 'bye'); } - ['@test should transition between watched and unwatched strategies'](assert) { + async ['@test should transition between watched and unwatched strategies'](assert) { let content = { foo: 'foo' }; let proxy = ObjectProxy.create({ content: content }); let count = 0; @@ -281,11 +304,13 @@ moduleFor( assert.equal(get(proxy, 'foo'), 'foo'); set(content, 'foo', 'bar'); + await runLoopSettled(); assert.equal(count, 1); assert.equal(get(proxy, 'foo'), 'bar'); set(proxy, 'foo', 'foo'); + await runLoopSettled(); assert.equal(count, 2); assert.equal(get(content, 'foo'), 'foo'); diff --git a/packages/@ember/-internals/utils/index.ts b/packages/@ember/-internals/utils/index.ts index f9cfce25d82..fa851c579cf 100644 --- a/packages/@ember/-internals/utils/index.ts +++ b/packages/@ember/-internals/utils/index.ts @@ -33,6 +33,11 @@ export { HAS_NATIVE_PROXY } from './lib/proxy-utils'; export { isProxy, setProxy } from './lib/is_proxy'; export { default as Cache } from './lib/cache'; export { EMBER_ARRAY, isEmberArray } from './lib/ember-array'; +export { + setupMandatorySetter, + teardownMandatorySetter, + setWithMandatorySetter, +} from './lib/mandatory-setter'; import symbol from './lib/symbol'; export const NAME_KEY = symbol('NAME_KEY'); diff --git a/packages/@ember/-internals/utils/lib/mandatory-setter.ts b/packages/@ember/-internals/utils/lib/mandatory-setter.ts new file mode 100644 index 00000000000..b9ce32bdc64 --- /dev/null +++ b/packages/@ember/-internals/utils/lib/mandatory-setter.ts @@ -0,0 +1,126 @@ +import { EMBER_METAL_TRACKED_PROPERTIES } from '@ember/canary-features'; +import { assert } from '@ember/debug'; +import { DEBUG } from '@glimmer/env'; + +export let setupMandatorySetter: ((obj: object, keyName: string | symbol) => void) | undefined; +export let teardownMandatorySetter: ((obj: object, keyName: string | symbol) => void) | undefined; +export let setWithMandatorySetter: + | ((obj: object, keyName: string | symbol, value: any) => void) + | undefined; + +type PropertyDescriptorWithMeta = PropertyDescriptor & { hadOwnProperty?: boolean }; + +if (DEBUG && EMBER_METAL_TRACKED_PROPERTIES) { + let MANDATORY_SETTERS: WeakMap< + object, + // @ts-ignore + { [key: string | symbol]: PropertyDescriptorWithMeta } + > = new WeakMap(); + + let getPropertyDescriptor = function( + obj: object, + keyName: string | symbol + ): PropertyDescriptorWithMeta | undefined { + let current = obj; + + while (current !== null) { + let desc = Object.getOwnPropertyDescriptor(current, keyName); + + if (desc !== undefined) { + return desc; + } + + current = Object.getPrototypeOf(current); + } + + return; + }; + + let propertyIsEnumerable = function(obj: object, key: string | symbol) { + return Object.prototype.propertyIsEnumerable.call(obj, key); + }; + + setupMandatorySetter = function(obj: object, keyName: string | symbol) { + let desc = getPropertyDescriptor(obj, keyName) || {}; + + if (desc.get || desc.set) { + // if it has a getter or setter, we can't install the mandatory setter. + // native setters are allowed, we have to assume that they will resolve + // to tracked properties. + return; + } + + if (desc && (!desc.configurable || !desc.writable)) { + // if it isn't writable anyways, so we shouldn't provide the setter. + // if it isn't configurable, we can't overwrite it anyways. + return; + } + + let setters = MANDATORY_SETTERS.get(obj); + + if (setters === undefined) { + setters = {}; + MANDATORY_SETTERS.set(obj, setters); + } + + desc.hadOwnProperty = Object.hasOwnProperty.call(obj, keyName); + + setters[keyName] = desc; + + Object.defineProperty(obj, keyName, { + configurable: true, + enumerable: propertyIsEnumerable(obj, keyName), + + get() { + if (desc.get) { + return desc.get.call(this); + } else { + return desc.value; + } + }, + + set(value: any) { + assert( + `You attempted to update ${this}.${String(keyName)} to "${String( + value + )}", but it is being tracked by a tracking context, such as a template, computed property, or observer. In order to make sure the context updates properly, you must invalidate the property when updating it. You can mark the property as \`@tracked\`, or use \`@ember/object#set\` to do this.` + ); + }, + }); + }; + + teardownMandatorySetter = function(obj: object, keyName: string | symbol) { + let setters = MANDATORY_SETTERS.get(obj); + + if (setters !== undefined && setters[keyName] !== undefined) { + Object.defineProperty(obj, keyName, setters[keyName]); + + setters[keyName] = undefined; + } + }; + + setWithMandatorySetter = function(obj: object, keyName: string | symbol, value: any) { + let setters = MANDATORY_SETTERS.get(obj); + + if (setters !== undefined && setters[keyName] !== undefined) { + let setter = setters[keyName]; + + if (setter.set) { + setter.set.call(obj, value); + } else { + setter.value = value; + + // If the object didn't have own property before, it would have changed + // the enumerability after setting the value the first time. + if (!setter.hadOwnProperty) { + let desc = getPropertyDescriptor(obj, keyName); + desc!.enumerable = true; + + Object.defineProperty(obj, keyName, desc!); + } + } + } else { + obj[keyName] = value; + } + }; +} diff --git a/packages/@ember/-internals/views/lib/views/states/in_dom.js b/packages/@ember/-internals/views/lib/views/states/in_dom.js index 50e69b4243a..4a7191cbcfa 100644 --- a/packages/@ember/-internals/views/lib/views/states/in_dom.js +++ b/packages/@ember/-internals/views/lib/views/states/in_dom.js @@ -1,5 +1,6 @@ +import { teardownMandatorySetter } from '@ember/-internals/utils'; import { assign } from '@ember/polyfills'; -import { addObserver } from '@ember/-internals/metal'; +import { EMBER_METAL_TRACKED_PROPERTIES } from '@ember/canary-features'; import EmberError from '@ember/error'; import { DEBUG } from '@glimmer/env'; import hasElement from './has_element'; @@ -11,8 +12,22 @@ const inDOM = assign({}, hasElement, { view.renderer.register(view); if (DEBUG) { - addObserver(view, 'elementId', () => { - throw new EmberError("Changing a view's elementId after creation is not allowed"); + let elementId = view.elementId; + + if (EMBER_METAL_TRACKED_PROPERTIES) { + teardownMandatorySetter(view, 'elementId'); + } + + Object.defineProperty(view, 'elementId', { + configurable: true, + enumerable: true, + + get() { + return elementId; + }, + set() { + throw new EmberError("Changing a view's elementId after creation is not allowed"); + }, }); } }, diff --git a/packages/@ember/object/lib/computed/reduce_computed_macros.js b/packages/@ember/object/lib/computed/reduce_computed_macros.js index 499582e7f2a..3904af58be3 100644 --- a/packages/@ember/object/lib/computed/reduce_computed_macros.js +++ b/packages/@ember/object/lib/computed/reduce_computed_macros.js @@ -2,10 +2,12 @@ @module @ember/object */ import { DEBUG } from '@glimmer/env'; +import { EMBER_METAL_TRACKED_PROPERTIES } from '@ember/canary-features'; import { assert } from '@ember/debug'; import { addObserver, computed, + descriptorForDecorator, get, isElementDescriptor, notifyPropertyChange, @@ -1486,56 +1488,85 @@ function propertySort(itemsKey, sortPropertiesKey) { let activeObserversMap = new WeakMap(); let sortPropertyDidChangeMap = new WeakMap(); - return computed(`${sortPropertiesKey}.[]`, function(key) { - let sortProperties = get(this, sortPropertiesKey); + if (EMBER_METAL_TRACKED_PROPERTIES) { + let cp = computed(`${itemsKey}.[]`, `${sortPropertiesKey}.[]`, function(key) { + let sortProperties = get(this, sortPropertiesKey); - assert( - `The sort definition for '${key}' on ${this} must be a function or an array of strings`, - isArray(sortProperties) && sortProperties.every(s => typeof s === 'string') - ); + assert( + `The sort definition for '${key}' on ${this} must be a function or an array of strings`, + isArray(sortProperties) && sortProperties.every(s => typeof s === 'string') + ); - // Add/remove property observers as required. - let activeObservers = activeObserversMap.get(this); + let itemsKeyIsAtThis = itemsKey === '@this'; + let normalizedSortProperties = normalizeSortProperties(sortProperties); - if (!sortPropertyDidChangeMap.has(this)) { - sortPropertyDidChangeMap.set(this, function() { - notifyPropertyChange(this, key); - }); - } + let items = itemsKeyIsAtThis ? this : get(this, itemsKey); + if (!isArray(items)) { + return emberA(); + } - let sortPropertyDidChange = sortPropertyDidChangeMap.get(this); + if (normalizedSortProperties.length === 0) { + return emberA(items.slice()); + } else { + return sortByNormalizedSortProperties(items, normalizedSortProperties); + } + }).readOnly(); - if (activeObservers !== undefined) { - activeObservers.forEach(path => removeObserver(this, path, sortPropertyDidChange)); - } + descriptorForDecorator(cp).auto(); - let itemsKeyIsAtThis = itemsKey === '@this'; - let normalizedSortProperties = normalizeSortProperties(sortProperties); - if (normalizedSortProperties.length === 0) { - let path = itemsKeyIsAtThis ? `[]` : `${itemsKey}.[]`; - addObserver(this, path, sortPropertyDidChange); - activeObservers = [path]; - } else { - activeObservers = normalizedSortProperties.map(([prop]) => { - let path = itemsKeyIsAtThis ? `@each.${prop}` : `${itemsKey}.@each.${prop}`; + return cp; + } else { + return computed(`${sortPropertiesKey}.[]`, function(key) { + let sortProperties = get(this, sortPropertiesKey); + + assert( + `The sort definition for '${key}' on ${this} must be a function or an array of strings`, + isArray(sortProperties) && sortProperties.every(s => typeof s === 'string') + ); + + // Add/remove property observers as required. + let activeObservers = activeObserversMap.get(this); + + if (!sortPropertyDidChangeMap.has(this)) { + sortPropertyDidChangeMap.set(this, function() { + notifyPropertyChange(this, key); + }); + } + + let sortPropertyDidChange = sortPropertyDidChangeMap.get(this); + + if (activeObservers !== undefined) { + activeObservers.forEach(path => removeObserver(this, path, sortPropertyDidChange)); + } + + let itemsKeyIsAtThis = itemsKey === '@this'; + let normalizedSortProperties = normalizeSortProperties(sortProperties); + if (normalizedSortProperties.length === 0) { + let path = itemsKeyIsAtThis ? `[]` : `${itemsKey}.[]`; addObserver(this, path, sortPropertyDidChange); - return path; - }); - } + activeObservers = [path]; + } else { + activeObservers = normalizedSortProperties.map(([prop]) => { + let path = itemsKeyIsAtThis ? `@each.${prop}` : `${itemsKey}.@each.${prop}`; + addObserver(this, path, sortPropertyDidChange); + return path; + }); + } - activeObserversMap.set(this, activeObservers); + activeObserversMap.set(this, activeObservers); - let items = itemsKeyIsAtThis ? this : get(this, itemsKey); - if (!isArray(items)) { - return emberA(); - } + let items = itemsKeyIsAtThis ? this : get(this, itemsKey); + if (!isArray(items)) { + return emberA(); + } - if (normalizedSortProperties.length === 0) { - return emberA(items.slice()); - } else { - return sortByNormalizedSortProperties(items, normalizedSortProperties); - } - }).readOnly(); + if (normalizedSortProperties.length === 0) { + return emberA(items.slice()); + } else { + return sortByNormalizedSortProperties(items, normalizedSortProperties); + } + }).readOnly(); + } } function normalizeSortProperties(sortProperties) { 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 3e7cafcf9f6..85b053ebc0e 100644 --- a/packages/@ember/object/tests/computed/reduce_computed_macros_test.js +++ b/packages/@ember/object/tests/computed/reduce_computed_macros_test.js @@ -35,7 +35,7 @@ import { EMBER_METAL_TRACKED_PROPERTIES, EMBER_NATIVE_DECORATOR_SUPPORT, } from '@ember/canary-features'; -import { moduleFor, AbstractTestCase } from 'internal-test-helpers'; +import { moduleFor, AbstractTestCase, runLoopSettled } from 'internal-test-helpers'; let obj; moduleFor( @@ -253,7 +253,7 @@ moduleFor( assert.deepEqual(obj.get('mapped'), [1, 3, 2, 5]); } - ['@test it is observable'](assert) { + async ['@test it is observable'](assert) { let calls = 0; assert.deepEqual(obj.get('mapped'), [1, 3, 2, 1]); @@ -261,6 +261,7 @@ moduleFor( addObserver(obj, 'mapped.@each', () => calls++); obj.get('array').pushObject({ v: 5 }); + await runLoopSettled(); assert.equal(calls, 1, 'mapBy is observable'); } @@ -2309,7 +2310,7 @@ moduleFor( run(obj, 'destroy'); } - ['@test it computes interdependent array computed properties'](assert) { + async ['@test it computes interdependent array computed properties'](assert) { assert.equal(obj.get('max'), 3, 'sanity - it properly computes the maximum value'); let calls = 0; @@ -2317,6 +2318,7 @@ moduleFor( addObserver(obj, 'max', () => calls++); 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'); diff --git a/packages/@ember/runloop/index.js b/packages/@ember/runloop/index.js index 0895915cd1b..d65e42ade90 100644 --- a/packages/@ember/runloop/index.js +++ b/packages/@ember/runloop/index.js @@ -1,6 +1,8 @@ import { assert } from '@ember/debug'; import { onErrorTarget } from '@ember/-internals/error-handling'; +import { flushInvalidActiveObservers } from '@ember/-internals/metal'; import Backburner from 'backburner'; +import { EMBER_METAL_TRACKED_PROPERTIES } from '@ember/canary-features'; let currentRunLoop = null; export function getCurrentRunLoop() { @@ -13,6 +15,22 @@ function onBegin(current) { function onEnd(current, next) { currentRunLoop = next; + + if (EMBER_METAL_TRACKED_PROPERTIES) { + flushInvalidActiveObservers(); + } +} + +let flush; + +if (EMBER_METAL_TRACKED_PROPERTIES) { + flush = function(queueName, next) { + if (queueName === 'render' || queueName === _rsvpErrorQueue) { + flushInvalidActiveObservers(); + } + + next(); + }; } export const _rsvpErrorQueue = `${Math.random()}${Date.now()}`.replace('.', ''); @@ -50,6 +68,7 @@ export const backburner = new Backburner(queues, { onEnd, onErrorTarget, onErrorMethod: 'onerror', + flush, }); /** diff --git a/packages/ember/tests/routing/decoupled_basic_test.js b/packages/ember/tests/routing/decoupled_basic_test.js index 694dffca0a1..7c42be46fb4 100644 --- a/packages/ember/tests/routing/decoupled_basic_test.js +++ b/packages/ember/tests/routing/decoupled_basic_test.js @@ -7,7 +7,7 @@ import Controller from '@ember/controller'; import { Object as EmberObject, A as emberA } from '@ember/-internals/runtime'; import { moduleFor, ApplicationTestCase, runDestroy, runTask } from 'internal-test-helpers'; import { run } from '@ember/runloop'; -import { Mixin, computed, set, addObserver, observer } from '@ember/-internals/metal'; +import { Mixin, computed, set, addObserver } from '@ember/-internals/metal'; import { getTextOf } from 'internal-test-helpers'; import { Component } from '@ember/-internals/glimmer'; import Engine from '@ember/engine'; @@ -709,7 +709,7 @@ moduleFor( this.addTemplate('special', '

{{model.id}}

'); this.addTemplate('loading', '

LOADING!

'); - let visited = this.visit('/specials/1'); + let visited = runTask(() => this.visit('/specials/1')); this.assertText('LOADING!', 'The app is in the loading state'); resolve(menuItem); @@ -794,7 +794,7 @@ moduleFor( }) ); - this.handleURLRejectsWith(this, assert, 'specials/1', 'Setup error'); + runTask(() => this.handleURLRejectsWith(this, assert, 'specials/1', 'Setup error')); resolve(menuItem); } @@ -844,7 +844,9 @@ moduleFor( }) ); - let promise = this.handleURLRejectsWith(this, assert, '/specials/1', 'Setup error'); + let promise = runTask(() => + this.handleURLRejectsWith(this, assert, '/specials/1', 'Setup error') + ); resolve(menuItem); @@ -2404,7 +2406,6 @@ moduleFor( ['@test ApplicationRoute with model does not proxy the currentPath'](assert) { let model = {}; - let currentPath; this.router.map(function() { this.route('index', { path: '/' }); @@ -2419,19 +2420,9 @@ moduleFor( }) ); - this.add( - 'controller:application', - Controller.extend({ - currentPathDidChange: observer('currentPath', function() { - expectDeprecation(() => { - currentPath = this.currentPath; - }, 'Accessing `currentPath` on `controller:application` is deprecated, use the `currentPath` property on `service:router` instead.'); - }), - }) - ); - return this.visit('/').then(() => { - assert.equal(currentPath, 'index', 'currentPath is index'); + let routerService = this.applicationInstance.lookup('service:router'); + assert.equal(routerService.currentRouteName, 'index', 'currentPath is index'); assert.equal( 'currentPath' in model, false, @@ -3367,7 +3358,7 @@ moduleFor( await assert.rejects( this.visit('/'), - function(err) { + function({ errorThrown: err }) { assert.equal(err.message, rejectedMessage); return true; }, diff --git a/packages/ember/tests/routing/query_params_test.js b/packages/ember/tests/routing/query_params_test.js index 5ce921f3784..a6bfb831501 100644 --- a/packages/ember/tests/routing/query_params_test.js +++ b/packages/ember/tests/routing/query_params_test.js @@ -7,12 +7,12 @@ import { get, computed } from '@ember/-internals/metal'; import { Route } from '@ember/-internals/routing'; import { PARAMS_SYMBOL } from 'router_js'; -import { QueryParamTestCase, moduleFor, getTextOf } from 'internal-test-helpers'; +import { QueryParamTestCase, moduleFor, getTextOf, runLoopSettled } from 'internal-test-helpers'; moduleFor( 'Query Params - main', class extends QueryParamTestCase { - refreshModelWhileLoadingTest(loadingReturn) { + async refreshModelWhileLoadingTest(loadingReturn) { let assert = this.assert; assert.expect(9); @@ -72,26 +72,25 @@ moduleFor( }) ); - return this.visit('/').then(() => { - assert.equal(appModelCount, 1, 'appModelCount is 1'); - assert.equal(indexModelCount, 1); - - let indexController = this.getController('index'); - this.setAndFlush(indexController, 'omg', 'lex'); + await this.visit('/'); + assert.equal(appModelCount, 1, 'appModelCount is 1'); + assert.equal(indexModelCount, 1); - assert.equal(appModelCount, 1, 'appModelCount is 1'); - assert.equal(indexModelCount, 2); + let indexController = this.getController('index'); + await this.setAndFlush(indexController, 'omg', 'lex'); - this.setAndFlush(indexController, 'omg', 'hello'); - assert.equal(appModelCount, 1, 'appModelCount is 1'); - assert.equal(indexModelCount, 3); + assert.equal(appModelCount, 1, 'appModelCount is 1'); + assert.equal(indexModelCount, 2); - run(function() { - promiseResolve(); - }); + await this.setAndFlush(indexController, 'omg', 'hello'); + assert.equal(appModelCount, 1, 'appModelCount is 1'); + assert.equal(indexModelCount, 3); - assert.equal(get(indexController, 'omg'), 'hello', 'At the end last value prevails'); + run(function() { + promiseResolve(); }); + + assert.equal(get(indexController, 'omg'), 'hello', 'At the end last value prevails'); } ["@test No replaceURL occurs on startup because default values don't show up in URL"](assert) { @@ -138,7 +137,9 @@ moduleFor( }); } - ['@test Single query params can be set on the controller and reflected in the url'](assert) { + async ['@test Single query params can be set on the controller and reflected in the url']( + assert + ) { assert.expect(3); this.router.map(function() { @@ -147,18 +148,19 @@ moduleFor( this.setSingleQPController('home'); - return this.visitAndAssert('/').then(() => { - let controller = this.getController('home'); + await this.visitAndAssert('/'); + let controller = this.getController('home'); - this.setAndFlush(controller, 'foo', '456'); - this.assertCurrentPath('/?foo=456'); + await this.setAndFlush(controller, 'foo', '456'); + this.assertCurrentPath('/?foo=456'); - this.setAndFlush(controller, 'foo', '987'); - this.assertCurrentPath('/?foo=987'); - }); + await this.setAndFlush(controller, 'foo', '987'); + this.assertCurrentPath('/?foo=987'); } - ['@test Query params can map to different url keys configured on the controller'](assert) { + async ['@test Query params can map to different url keys configured on the controller']( + assert + ) { assert.expect(6); this.add( @@ -170,27 +172,26 @@ moduleFor( }) ); - return this.visitAndAssert('/').then(() => { - let controller = this.getController('index'); + await this.visitAndAssert('/'); + let controller = this.getController('index'); - this.setAndFlush(controller, 'foo', 'LEX'); - this.assertCurrentPath('/?other_foo=LEX', "QP mapped correctly without 'as'"); + await this.setAndFlush(controller, 'foo', 'LEX'); + this.assertCurrentPath('/?other_foo=LEX', "QP mapped correctly without 'as'"); - this.setAndFlush(controller, 'foo', 'WOO'); - this.assertCurrentPath('/?other_foo=WOO', "QP updated correctly without 'as'"); + await this.setAndFlush(controller, 'foo', 'WOO'); + this.assertCurrentPath('/?other_foo=WOO', "QP updated correctly without 'as'"); - this.transitionTo('/?other_foo=NAW'); - assert.equal(controller.get('foo'), 'NAW', 'QP managed correctly on URL transition'); + this.transitionTo('/?other_foo=NAW'); + assert.equal(controller.get('foo'), 'NAW', 'QP managed correctly on URL transition'); - this.setAndFlush(controller, 'bar', 'NERK'); - this.assertCurrentPath('/?other_bar=NERK&other_foo=NAW', "QP mapped correctly with 'as'"); + await this.setAndFlush(controller, 'bar', 'NERK'); + this.assertCurrentPath('/?other_bar=NERK&other_foo=NAW', "QP mapped correctly with 'as'"); - this.setAndFlush(controller, 'bar', 'NUKE'); - this.assertCurrentPath('/?other_bar=NUKE&other_foo=NAW', "QP updated correctly with 'as'"); - }); + await this.setAndFlush(controller, 'bar', 'NUKE'); + this.assertCurrentPath('/?other_bar=NUKE&other_foo=NAW', "QP updated correctly with 'as'"); } - ['@test Routes have a private overridable serializeQueryParamKey hook'](assert) { + async ['@test Routes have a private overridable serializeQueryParamKey hook'](assert) { assert.expect(2); this.add( @@ -202,15 +203,14 @@ moduleFor( this.setSingleQPController('index', 'funTimes', ''); - return this.visitAndAssert('/').then(() => { - let controller = this.getController('index'); + await this.visitAndAssert('/'); + let controller = this.getController('index'); - this.setAndFlush(controller, 'funTimes', 'woot'); - this.assertCurrentPath('/?fun-times=woot'); - }); + await this.setAndFlush(controller, 'funTimes', 'woot'); + this.assertCurrentPath('/?fun-times=woot'); } - ['@test Can override inherited QP behavior by specifying queryParams as a computed property']( + async ['@test Can override inherited QP behavior by specifying queryParams as a computed property']( assert ) { assert.expect(3); @@ -222,18 +222,19 @@ moduleFor( c: true, }); - return this.visitAndAssert('/').then(() => { - let indexController = this.getController('index'); + await this.visitAndAssert('/'); + let indexController = this.getController('index'); - this.setAndFlush(indexController, 'a', 1); - this.assertCurrentPath('/', 'QP did not update due to being overriden'); + await this.setAndFlush(indexController, 'a', 1); + this.assertCurrentPath('/', 'QP did not update due to being overriden'); - this.setAndFlush(indexController, 'c', false); - this.assertCurrentPath('/?c=false', 'QP updated with overridden param'); - }); + await this.setAndFlush(indexController, 'c', false); + this.assertCurrentPath('/?c=false', 'QP updated with overridden param'); } - ['@test Can concatenate inherited QP behavior by specifying queryParams as an array'](assert) { + async ['@test Can concatenate inherited QP behavior by specifying queryParams as an array']( + assert + ) { assert.expect(3); this.setSingleQPController('index', 'a', 0, { @@ -241,15 +242,14 @@ moduleFor( c: true, }); - return this.visitAndAssert('/').then(() => { - let indexController = this.getController('index'); + await this.visitAndAssert('/'); + let indexController = this.getController('index'); - this.setAndFlush(indexController, 'a', 1); - this.assertCurrentPath('/?a=1', 'Inherited QP did update'); + await this.setAndFlush(indexController, 'a', 1); + this.assertCurrentPath('/?a=1', 'Inherited QP did update'); - this.setAndFlush(indexController, 'c', false); - this.assertCurrentPath('/?a=1&c=false', 'New QP did update'); - }); + await this.setAndFlush(indexController, 'c', false); + this.assertCurrentPath('/?a=1&c=false', 'New QP did update'); } ['@test model hooks receives query params'](assert) { @@ -528,7 +528,9 @@ moduleFor( return this.visitAndAssert('/?appomg=appyes&omg=yes'); } - ['@test can opt into full transition by setting refreshModel in route queryParams'](assert) { + async ['@test can opt into full transition by setting refreshModel in route queryParams']( + assert + ) { assert.expect(7); this.setSingleQPController('application', 'appomg', 'applol'); @@ -565,19 +567,18 @@ moduleFor( }) ); - return this.visitAndAssert('/').then(() => { - assert.equal(appModelCount, 1, 'app model hook ran'); - assert.equal(indexModelCount, 1, 'index model hook ran'); + await this.visitAndAssert('/'); + assert.equal(appModelCount, 1, 'app model hook ran'); + assert.equal(indexModelCount, 1, 'index model hook ran'); - let indexController = this.getController('index'); - this.setAndFlush(indexController, 'omg', 'lex'); + let indexController = this.getController('index'); + await this.setAndFlush(indexController, 'omg', 'lex'); - assert.equal(appModelCount, 1, 'app model hook did not run again'); - assert.equal(indexModelCount, 2, 'index model hook ran again due to refreshModel'); - }); + assert.equal(appModelCount, 1, 'app model hook did not run again'); + assert.equal(indexModelCount, 2, 'index model hook ran again due to refreshModel'); } - ['@test refreshModel and replace work together'](assert) { + async ['@test refreshModel and replace work together'](assert) { assert.expect(8); this.setSingleQPController('application', 'appomg', 'applol'); @@ -615,20 +616,19 @@ moduleFor( }) ); - return this.visitAndAssert('/').then(() => { - assert.equal(appModelCount, 1, 'app model hook ran'); - assert.equal(indexModelCount, 1, 'index model hook ran'); + await this.visitAndAssert('/'); + assert.equal(appModelCount, 1, 'app model hook ran'); + assert.equal(indexModelCount, 1, 'index model hook ran'); - let indexController = this.getController('index'); - this.expectedReplaceURL = '/?omg=lex'; - this.setAndFlush(indexController, 'omg', 'lex'); + let indexController = this.getController('index'); + this.expectedReplaceURL = '/?omg=lex'; + await this.setAndFlush(indexController, 'omg', 'lex'); - assert.equal(appModelCount, 1, 'app model hook did not run again'); - assert.equal(indexModelCount, 2, 'index model hook ran again due to refreshModel'); - }); + assert.equal(appModelCount, 1, 'app model hook did not run again'); + assert.equal(indexModelCount, 2, 'index model hook ran again due to refreshModel'); } - ['@test multiple QP value changes only cause a single model refresh'](assert) { + async ['@test multiple QP value changes only cause a single model refresh'](assert) { assert.expect(2); this.setSingleQPController('index', 'alex', 'lol'); @@ -652,14 +652,15 @@ moduleFor( }) ); - return this.visitAndAssert('/').then(() => { - let indexController = this.getController('index'); - run(indexController, 'setProperties', { - alex: 'fran', - steely: 'david', - }); - assert.equal(refreshCount, 1, 'index refresh hook only run once'); + await this.visitAndAssert('/'); + + let indexController = this.getController('index'); + await this.setAndFlush(indexController, { + alex: 'fran', + steely: 'david', }); + + assert.equal(refreshCount, 1, 'index refresh hook only run once'); } ['@test refreshModel does not cause a second transition during app boot '](assert) { @@ -685,7 +686,7 @@ moduleFor( return this.visitAndAssert('/?appomg=hello&omg=world'); } - ['@test queryParams are updated when a controller property is set and the route is refreshed. Issue #13263 ']( + async ['@test queryParams are updated when a controller property is set and the route is refreshed. Issue #13263 ']( assert ) { this.addTemplate( @@ -713,20 +714,23 @@ moduleFor( }) ); - return this.visitAndAssert('/').then(() => { - assert.equal(getTextOf(document.getElementById('test-value')), '1'); + await this.visitAndAssert('/'); + assert.equal(getTextOf(document.getElementById('test-value')), '1'); - run(document.getElementById('test-button'), 'click'); - assert.equal(getTextOf(document.getElementById('test-value')), '2'); - this.assertCurrentPath('/?foo=2'); + document.getElementById('test-button').click(); + await runLoopSettled(); - run(document.getElementById('test-button'), 'click'); - assert.equal(getTextOf(document.getElementById('test-value')), '3'); - this.assertCurrentPath('/?foo=3'); - }); + assert.equal(getTextOf(document.getElementById('test-value')), '2'); + this.assertCurrentPath('/?foo=2'); + + document.getElementById('test-button').click(); + await runLoopSettled(); + + assert.equal(getTextOf(document.getElementById('test-value')), '3'); + this.assertCurrentPath('/?foo=3'); } - ["@test Use Ember.get to retrieve query params 'refreshModel' configuration"](assert) { + async ["@test Use Ember.get to retrieve query params 'refreshModel' configuration"](assert) { assert.expect(7); this.setSingleQPController('application', 'appomg', 'applol'); @@ -763,19 +767,20 @@ moduleFor( }) ); - return this.visitAndAssert('/').then(() => { - assert.equal(appModelCount, 1); - assert.equal(indexModelCount, 1); + await this.visitAndAssert('/'); + assert.equal(appModelCount, 1); + assert.equal(indexModelCount, 1); - let indexController = this.getController('index'); - this.setAndFlush(indexController, 'omg', 'lex'); + let indexController = this.getController('index'); + await this.setAndFlush(indexController, 'omg', 'lex'); - assert.equal(appModelCount, 1); - assert.equal(indexModelCount, 2); - }); + assert.equal(appModelCount, 1); + assert.equal(indexModelCount, 2); } - ['@test can use refreshModel even with URL changes that remove QPs from address bar'](assert) { + async ['@test can use refreshModel even with URL changes that remove QPs from address bar']( + assert + ) { assert.expect(4); this.setSingleQPController('index', 'omg', 'lol'); @@ -804,15 +809,14 @@ moduleFor( }) ); - return this.visitAndAssert('/?omg=foo').then(() => { - this.transitionTo('/'); + await this.visitAndAssert('/?omg=foo'); + await this.transitionTo('/'); - let indexController = this.getController('index'); - assert.equal(indexController.get('omg'), 'lol'); - }); + let indexController = this.getController('index'); + assert.equal(indexController.get('omg'), 'lol'); } - ['@test can opt into a replace query by specifying replace:true in the Route config hash']( + async ['@test can opt into a replace query by specifying replace:true in the Route config hash']( assert ) { assert.expect(2); @@ -830,14 +834,15 @@ moduleFor( }) ); - return this.visitAndAssert('/').then(() => { - let appController = this.getController('application'); - this.expectedReplaceURL = '/?alex=wallace'; - this.setAndFlush(appController, 'alex', 'wallace'); - }); + await this.visitAndAssert('/'); + + let appController = this.getController('application'); + this.expectedReplaceURL = '/?alex=wallace'; + + await this.setAndFlush(appController, 'alex', 'wallace'); } - ['@test Route query params config can be configured using property name instead of URL key']( + async ['@test Route query params config can be configured using property name instead of URL key']( assert ) { assert.expect(2); @@ -860,14 +865,17 @@ moduleFor( }) ); - return this.visitAndAssert('/').then(() => { - let appController = this.getController('application'); - this.expectedReplaceURL = '/?commit_by=igor_seb'; - this.setAndFlush(appController, 'commitBy', 'igor_seb'); - }); + await this.visitAndAssert('/'); + + let appController = this.getController('application'); + this.expectedReplaceURL = '/?commit_by=igor_seb'; + + await this.setAndFlush(appController, 'commitBy', 'igor_seb'); } - ['@test An explicit replace:false on a changed QP always wins and causes a pushState'](assert) { + async ['@test An explicit replace:false on a changed QP always wins and causes a pushState']( + assert + ) { assert.expect(3); this.add( @@ -893,17 +901,16 @@ moduleFor( }) ); - return this.visit('/').then(() => { - let appController = this.getController('application'); - this.expectedPushURL = '/?alex=wallace&steely=jan'; - run(appController, 'setProperties', { alex: 'wallace', steely: 'jan' }); + await this.visit('/'); + let appController = this.getController('application'); + this.expectedPushURL = '/?alex=wallace&steely=jan'; + await this.setAndFlush(appController, { alex: 'wallace', steely: 'jan' }); - this.expectedPushURL = '/?alex=wallace&steely=fran'; - run(appController, 'setProperties', { steely: 'fran' }); + this.expectedPushURL = '/?alex=wallace&steely=fran'; + await this.setAndFlush(appController, { steely: 'fran' }); - this.expectedReplaceURL = '/?alex=sriracha&steely=fran'; - run(appController, 'setProperties', { alex: 'sriracha' }); - }); + this.expectedReplaceURL = '/?alex=sriracha&steely=fran'; + await this.setAndFlush(appController, 'alex', 'sriracha'); } ['@test can opt into full transition by setting refreshModel in route queryParams when transitioning from child to parent']( @@ -946,7 +953,7 @@ moduleFor( }); } - ["@test Use Ember.get to retrieve query params 'replace' configuration"](assert) { + async ["@test Use Ember.get to retrieve query params 'replace' configuration"](assert) { assert.expect(2); this.setSingleQPController('application', 'alex', 'matchneer'); @@ -963,14 +970,15 @@ moduleFor( }) ); - return this.visitAndAssert('/').then(() => { - let appController = this.getController('application'); - this.expectedReplaceURL = '/?alex=wallace'; - this.setAndFlush(appController, 'alex', 'wallace'); - }); + await this.visitAndAssert('/'); + + let appController = this.getController('application'); + this.expectedReplaceURL = '/?alex=wallace'; + + await this.setAndFlush(appController, 'alex', 'wallace'); } - ['@test can override incoming QP values in setupController'](assert) { + async ['@test can override incoming QP values in setupController'](assert) { assert.expect(3); this.router.map(function() { @@ -994,13 +1002,13 @@ moduleFor( }) ); - return this.visitAndAssert('/about').then(() => { - this.transitionTo('index'); - this.assertCurrentPath('/?omg=OVERRIDE'); - }); + await this.visitAndAssert('/about'); + await this.transitionTo('index'); + + this.assertCurrentPath('/?omg=OVERRIDE'); } - ['@test can override incoming QP array values in setupController'](assert) { + async ['@test can override incoming QP array values in setupController'](assert) { assert.expect(3); this.router.map(function() { @@ -1024,10 +1032,10 @@ moduleFor( }) ); - return this.visitAndAssert('/about').then(() => { - this.transitionTo('index'); - this.assertCurrentPath('/?omg=' + encodeURIComponent(JSON.stringify(['OVERRIDE']))); - }); + await this.visitAndAssert('/about'); + await this.transitionTo('index'); + + this.assertCurrentPath('/?omg=' + encodeURIComponent(JSON.stringify(['OVERRIDE']))); } ['@test URL transitions that remove QPs still register as QP changes'](assert) { @@ -1073,25 +1081,24 @@ moduleFor( }); } - ['@test transitionTo supports query params']() { + async ['@test transitionTo supports query params']() { this.setSingleQPController('index', 'foo', 'lol'); - return this.visitAndAssert('/').then(() => { - this.transitionTo({ queryParams: { foo: 'borf' } }); - this.assertCurrentPath('/?foo=borf', 'shorthand supported'); + await this.visitAndAssert('/'); + await this.transitionTo({ queryParams: { foo: 'borf' } }); + this.assertCurrentPath('/?foo=borf', 'shorthand supported'); - this.transitionTo({ queryParams: { 'index:foo': 'blaf' } }); - this.assertCurrentPath('/?foo=blaf', 'longform supported'); + await this.transitionTo({ queryParams: { 'index:foo': 'blaf' } }); + this.assertCurrentPath('/?foo=blaf', 'longform supported'); - this.transitionTo({ queryParams: { 'index:foo': false } }); - this.assertCurrentPath('/?foo=false', 'longform supported (bool)'); + await this.transitionTo({ queryParams: { 'index:foo': false } }); + this.assertCurrentPath('/?foo=false', 'longform supported (bool)'); - this.transitionTo({ queryParams: { foo: false } }); - this.assertCurrentPath('/?foo=false', 'shorhand supported (bool)'); - }); + await this.transitionTo({ queryParams: { foo: false } }); + this.assertCurrentPath('/?foo=false', 'shorhand supported (bool)'); } - ['@test transitionTo supports query params (multiple)']() { + async ['@test transitionTo supports query params (multiple)']() { this.add( 'controller:index', Controller.extend({ @@ -1101,35 +1108,33 @@ moduleFor( }) ); - return this.visitAndAssert('/').then(() => { - this.transitionTo({ queryParams: { foo: 'borf' } }); - this.assertCurrentPath('/?foo=borf', 'shorthand supported'); + await this.visitAndAssert('/'); + await this.transitionTo({ queryParams: { foo: 'borf' } }); + this.assertCurrentPath('/?foo=borf', 'shorthand supported'); - this.transitionTo({ queryParams: { 'index:foo': 'blaf' } }); - this.assertCurrentPath('/?foo=blaf', 'longform supported'); + await this.transitionTo({ queryParams: { 'index:foo': 'blaf' } }); + this.assertCurrentPath('/?foo=blaf', 'longform supported'); - this.transitionTo({ queryParams: { 'index:foo': false } }); - this.assertCurrentPath('/?foo=false', 'longform supported (bool)'); + await this.transitionTo({ queryParams: { 'index:foo': false } }); + this.assertCurrentPath('/?foo=false', 'longform supported (bool)'); - this.transitionTo({ queryParams: { foo: false } }); - this.assertCurrentPath('/?foo=false', 'shorhand supported (bool)'); - }); + await this.transitionTo({ queryParams: { foo: false } }); + this.assertCurrentPath('/?foo=false', 'shorhand supported (bool)'); } - ["@test setting controller QP to empty string doesn't generate null in URL"](assert) { + async ["@test setting controller QP to empty string doesn't generate null in URL"](assert) { assert.expect(1); this.setSingleQPController('index', 'foo', '123'); - return this.visit('/').then(() => { - let controller = this.getController('index'); + await this.visit('/'); + let controller = this.getController('index'); - this.expectedPushURL = '/?foo='; - this.setAndFlush(controller, 'foo', ''); - }); + this.expectedPushURL = '/?foo='; + await this.setAndFlush(controller, 'foo', ''); } - ["@test setting QP to empty string doesn't generate null in URL"](assert) { + async ["@test setting QP to empty string doesn't generate null in URL"](assert) { assert.expect(1); this.add( @@ -1143,12 +1148,11 @@ moduleFor( }) ); - return this.visit('/').then(() => { - let controller = this.getController('index'); + await this.visit('/'); + let controller = this.getController('index'); - this.expectedPushURL = '/?foo='; - this.setAndFlush(controller, 'foo', ''); - }); + this.expectedPushURL = '/?foo='; + await this.setAndFlush(controller, 'foo', ''); } ['@test A default boolean value deserializes QPs as booleans rather than strings'](assert) { @@ -1191,7 +1195,7 @@ moduleFor( }); } - ['@test Array query params can be set'](assert) { + async ['@test Array query params can be set'](assert) { assert.expect(2); this.router.map(function() { @@ -1200,30 +1204,28 @@ moduleFor( this.setSingleQPController('home', 'foo', []); - return this.visit('/').then(() => { - let controller = this.getController('home'); + await this.visit('/'); + let controller = this.getController('home'); - this.setAndFlush(controller, 'foo', [1, 2]); - this.assertCurrentPath('/?foo=%5B1%2C2%5D'); + await this.setAndFlush(controller, 'foo', [1, 2]); + this.assertCurrentPath('/?foo=%5B1%2C2%5D'); - this.setAndFlush(controller, 'foo', [3, 4]); - this.assertCurrentPath('/?foo=%5B3%2C4%5D'); - }); + await this.setAndFlush(controller, 'foo', [3, 4]); + this.assertCurrentPath('/?foo=%5B3%2C4%5D'); } - ['@test (de)serialization: arrays'](assert) { + async ['@test (de)serialization: arrays'](assert) { assert.expect(4); this.setSingleQPController('index', 'foo', [1]); - return this.visitAndAssert('/').then(() => { - this.transitionTo({ queryParams: { foo: [2, 3] } }); - this.assertCurrentPath('/?foo=%5B2%2C3%5D', 'shorthand supported'); - this.transitionTo({ queryParams: { 'index:foo': [4, 5] } }); - this.assertCurrentPath('/?foo=%5B4%2C5%5D', 'longform supported'); - this.transitionTo({ queryParams: { foo: [] } }); - this.assertCurrentPath('/?foo=%5B%5D', 'longform supported'); - }); + await this.visitAndAssert('/'); + await this.transitionTo({ queryParams: { foo: [2, 3] } }); + this.assertCurrentPath('/?foo=%5B2%2C3%5D', 'shorthand supported'); + await this.transitionTo({ queryParams: { 'index:foo': [4, 5] } }); + this.assertCurrentPath('/?foo=%5B4%2C5%5D', 'longform supported'); + await this.transitionTo({ queryParams: { foo: [] } }); + this.assertCurrentPath('/?foo=%5B%5D', 'longform supported'); } ['@test Url with array query param sets controller property to array'](assert) { @@ -1237,7 +1239,7 @@ moduleFor( }); } - ['@test Array query params can be pushed/popped'](assert) { + async ['@test Array query params can be pushed/popped'](assert) { assert.expect(17); this.router.map(function() { @@ -1246,44 +1248,51 @@ moduleFor( this.setSingleQPController('home', 'foo', emberA()); - return this.visitAndAssert('/').then(() => { - let controller = this.getController('home'); - - run(controller.foo, 'pushObject', 1); - this.assertCurrentPath('/?foo=%5B1%5D'); - assert.deepEqual(controller.foo, [1]); - - run(controller.foo, 'popObject'); - this.assertCurrentPath('/'); - assert.deepEqual(controller.foo, []); - - run(controller.foo, 'pushObject', 1); - this.assertCurrentPath('/?foo=%5B1%5D'); - assert.deepEqual(controller.foo, [1]); - - run(controller.foo, 'popObject'); - this.assertCurrentPath('/'); - assert.deepEqual(controller.foo, []); - - run(controller.foo, 'pushObject', 1); - this.assertCurrentPath('/?foo=%5B1%5D'); - assert.deepEqual(controller.foo, [1]); - - run(controller.foo, 'pushObject', 2); - this.assertCurrentPath('/?foo=%5B1%2C2%5D'); - assert.deepEqual(controller.foo, [1, 2]); - - run(controller.foo, 'popObject'); - this.assertCurrentPath('/?foo=%5B1%5D'); - assert.deepEqual(controller.foo, [1]); - - run(controller.foo, 'unshiftObject', 'lol'); - this.assertCurrentPath('/?foo=%5B%22lol%22%2C1%5D'); - assert.deepEqual(controller.foo, ['lol', 1]); - }); + await this.visitAndAssert('/'); + let controller = this.getController('home'); + + controller.foo.pushObject(1); + await runLoopSettled(); + this.assertCurrentPath('/?foo=%5B1%5D'); + assert.deepEqual(controller.foo, [1]); + + controller.foo.popObject(); + await runLoopSettled(); + this.assertCurrentPath('/'); + assert.deepEqual(controller.foo, []); + + controller.foo.pushObject(1); + await runLoopSettled(); + this.assertCurrentPath('/?foo=%5B1%5D'); + assert.deepEqual(controller.foo, [1]); + + controller.foo.popObject(); + await runLoopSettled(); + this.assertCurrentPath('/'); + assert.deepEqual(controller.foo, []); + + controller.foo.pushObject(1); + await runLoopSettled(); + this.assertCurrentPath('/?foo=%5B1%5D'); + assert.deepEqual(controller.foo, [1]); + + controller.foo.pushObject(2); + await runLoopSettled(); + this.assertCurrentPath('/?foo=%5B1%2C2%5D'); + assert.deepEqual(controller.foo, [1, 2]); + + controller.foo.popObject(); + await runLoopSettled(); + this.assertCurrentPath('/?foo=%5B1%5D'); + assert.deepEqual(controller.foo, [1]); + + controller.foo.unshiftObject('lol'); + await runLoopSettled(); + this.assertCurrentPath('/?foo=%5B%22lol%22%2C1%5D'); + assert.deepEqual(controller.foo, ['lol', 1]); } - ["@test Overwriting with array with same content shouldn't refire update"](assert) { + async ["@test Overwriting with array with same content shouldn't refire update"](assert) { assert.expect(4); this.router.map(function() { @@ -1302,15 +1311,14 @@ moduleFor( this.setSingleQPController('home', 'foo', emberA([1])); - return this.visitAndAssert('/').then(() => { - assert.equal(modelCount, 1); + await this.visitAndAssert('/'); + assert.equal(modelCount, 1); - let controller = this.getController('home'); - this.setAndFlush(controller, 'model', emberA([1])); + let controller = this.getController('home'); + await this.setAndFlush(controller, 'model', emberA([1])); - assert.equal(modelCount, 1); - this.assertCurrentPath('/'); - }); + assert.equal(modelCount, 1); + this.assertCurrentPath('/'); } ['@test Defaulting to params hash as the model should not result in that params object being watched']( @@ -1343,7 +1351,7 @@ moduleFor( }); } - ['@test Setting bound query param property to null or undefined does not serialize to url']( + async ['@test Setting bound query param property to null or undefined does not serialize to url']( assert ) { assert.expect(9); @@ -1354,29 +1362,28 @@ moduleFor( this.setSingleQPController('home', 'foo', [1, 2]); - return this.visitAndAssert('/home').then(() => { - var controller = this.getController('home'); + await this.visitAndAssert('/home'); + var controller = this.getController('home'); - assert.deepEqual(controller.get('foo'), [1, 2]); - this.assertCurrentPath('/home'); + assert.deepEqual(controller.get('foo'), [1, 2]); + this.assertCurrentPath('/home'); - this.setAndFlush(controller, 'foo', emberA([1, 3])); - this.assertCurrentPath('/home?foo=%5B1%2C3%5D'); + await this.setAndFlush(controller, 'foo', emberA([1, 3])); + this.assertCurrentPath('/home?foo=%5B1%2C3%5D'); - return this.transitionTo('/home').then(() => { - assert.deepEqual(controller.get('foo'), [1, 2]); - this.assertCurrentPath('/home'); + await this.transitionTo('/home'); - this.setAndFlush(controller, 'foo', null); - this.assertCurrentPath('/home', 'Setting property to null'); + assert.deepEqual(controller.get('foo'), [1, 2]); + this.assertCurrentPath('/home'); - this.setAndFlush(controller, 'foo', emberA([1, 3])); - this.assertCurrentPath('/home?foo=%5B1%2C3%5D'); + await this.setAndFlush(controller, 'foo', null); + this.assertCurrentPath('/home', 'Setting property to null'); - this.setAndFlush(controller, 'foo', undefined); - this.assertCurrentPath('/home', 'Setting property to undefined'); - }); - }); + await this.setAndFlush(controller, 'foo', emberA([1, 3])); + this.assertCurrentPath('/home?foo=%5B1%2C3%5D'); + + await this.setAndFlush(controller, 'foo', undefined); + this.assertCurrentPath('/home', 'Setting property to undefined'); } ['@test {{link-to}} with null or undefined QPs does not get serialized into url'](assert) { @@ -1436,7 +1443,7 @@ moduleFor( return this.visitAndAssert('/'); } - ['@test opting into replace does not affect transitions between routes'](assert) { + async ['@test opting into replace does not affect transitions between routes'](assert) { assert.expect(5); this.addTemplate( @@ -1462,24 +1469,23 @@ moduleFor( }) ); - return this.visit('/').then(() => { - let controller = this.getController('bar'); + await this.visit('/'); + let controller = this.getController('bar'); - this.expectedPushURL = '/foo'; - run(document.getElementById('foo-link'), 'click'); + this.expectedPushURL = '/foo'; + run(document.getElementById('foo-link'), 'click'); - this.expectedPushURL = '/bar'; - run(document.getElementById('bar-no-qp-link'), 'click'); + this.expectedPushURL = '/bar'; + run(document.getElementById('bar-no-qp-link'), 'click'); - this.expectedReplaceURL = '/bar?raytiley=woot'; - this.setAndFlush(controller, 'raytiley', 'woot'); + this.expectedReplaceURL = '/bar?raytiley=woot'; + await this.setAndFlush(controller, 'raytiley', 'woot'); - this.expectedPushURL = '/foo'; - run(document.getElementById('foo-link'), 'click'); + this.expectedPushURL = '/foo'; + run(document.getElementById('foo-link'), 'click'); - this.expectedPushURL = '/bar?raytiley=isthebest'; - run(document.getElementById('bar-link'), 'click'); - }); + this.expectedPushURL = '/bar?raytiley=isthebest'; + run(document.getElementById('bar-link'), 'click'); } ["@test undefined isn't serialized or deserialized into a string"](assert) { @@ -1552,7 +1558,7 @@ moduleFor( ); } - ['@test handle route names that clash with Object.prototype properties'](assert) { + async ['@test handle route names that clash with Object.prototype properties'](assert) { assert.expect(1); this.router.map(function() { @@ -1570,11 +1576,10 @@ moduleFor( }) ); - return this.visit('/').then(() => { - this.transitionTo('constructor', { queryParams: { foo: '999' } }); - let controller = this.getController('constructor'); - assert.equal(get(controller, 'foo'), '999'); - }); + await this.visit('/'); + await this.transitionTo('constructor', { queryParams: { foo: '999' } }); + let controller = this.getController('constructor'); + assert.equal(get(controller, 'foo'), '999'); } } ); diff --git a/packages/ember/tests/routing/query_params_test/model_dependent_state_with_query_params_test.js b/packages/ember/tests/routing/query_params_test/model_dependent_state_with_query_params_test.js index c8cc7c8fec1..33469df564b 100644 --- a/packages/ember/tests/routing/query_params_test/model_dependent_state_with_query_params_test.js +++ b/packages/ember/tests/routing/query_params_test/model_dependent_state_with_query_params_test.js @@ -1,9 +1,8 @@ import Controller from '@ember/controller'; import { A as emberA } from '@ember/-internals/runtime'; import { Route } from '@ember/-internals/routing'; -import { run } from '@ember/runloop'; import { computed } from '@ember/-internals/metal'; -import { QueryParamTestCase, moduleFor } from 'internal-test-helpers'; +import { QueryParamTestCase, moduleFor, runLoopSettled } from 'internal-test-helpers'; class ModelDependentQPTestCase extends QueryParamTestCase { boot() { @@ -27,74 +26,78 @@ class ModelDependentQPTestCase extends QueryParamTestCase { this.application.resolveRegistration(`route:${name}`).reopen(options); } - queryParamsStickyTest1(urlPrefix) { + async queryParamsStickyTest1(urlPrefix) { let assert = this.assert; assert.expect(14); - return this.boot().then(() => { - run(this.$link1, 'click'); - this.assertCurrentPath(`${urlPrefix}/a-1`); + await this.boot(); + this.$link1.click(); + await runLoopSettled(); - this.setAndFlush(this.controller, 'q', 'lol'); + this.assertCurrentPath(`${urlPrefix}/a-1`); - assert.equal(this.$link1.getAttribute('href'), `${urlPrefix}/a-1?q=lol`); - assert.equal(this.$link2.getAttribute('href'), `${urlPrefix}/a-2`); - assert.equal(this.$link3.getAttribute('href'), `${urlPrefix}/a-3`); + await this.setAndFlush(this.controller, 'q', 'lol'); - run(this.$link2, 'click'); + assert.equal(this.$link1.getAttribute('href'), `${urlPrefix}/a-1?q=lol`); + assert.equal(this.$link2.getAttribute('href'), `${urlPrefix}/a-2`); + assert.equal(this.$link3.getAttribute('href'), `${urlPrefix}/a-3`); - assert.equal(this.controller.get('q'), 'wat'); - assert.equal(this.controller.get('z'), 0); - assert.deepEqual(this.controller.get('model'), { id: 'a-2' }); - assert.equal(this.$link1.getAttribute('href'), `${urlPrefix}/a-1?q=lol`); - assert.equal(this.$link2.getAttribute('href'), `${urlPrefix}/a-2`); - assert.equal(this.$link3.getAttribute('href'), `${urlPrefix}/a-3`); - }); + this.$link2.click(); + await runLoopSettled(); + + assert.equal(this.controller.get('q'), 'wat'); + assert.equal(this.controller.get('z'), 0); + assert.deepEqual(this.controller.get('model'), { id: 'a-2' }); + assert.equal(this.$link1.getAttribute('href'), `${urlPrefix}/a-1?q=lol`); + assert.equal(this.$link2.getAttribute('href'), `${urlPrefix}/a-2`); + assert.equal(this.$link3.getAttribute('href'), `${urlPrefix}/a-3`); } - queryParamsStickyTest2(urlPrefix) { + async queryParamsStickyTest2(urlPrefix) { let assert = this.assert; assert.expect(24); - return this.boot().then(() => { - this.expectedModelHookParams = { id: 'a-1', q: 'lol', z: 0 }; - this.transitionTo(`${urlPrefix}/a-1?q=lol`); + await this.boot(); + this.expectedModelHookParams = { id: 'a-1', q: 'lol', z: 0 }; - assert.deepEqual(this.controller.get('model'), { id: 'a-1' }); - assert.equal(this.controller.get('q'), 'lol'); - assert.equal(this.controller.get('z'), 0); - assert.equal(this.$link1.getAttribute('href'), `${urlPrefix}/a-1?q=lol`); - assert.equal(this.$link2.getAttribute('href'), `${urlPrefix}/a-2`); - assert.equal(this.$link3.getAttribute('href'), `${urlPrefix}/a-3`); + await this.transitionTo(`${urlPrefix}/a-1?q=lol`); - this.expectedModelHookParams = { id: 'a-2', q: 'lol', z: 0 }; - this.transitionTo(`${urlPrefix}/a-2?q=lol`); + assert.deepEqual(this.controller.get('model'), { id: 'a-1' }); + assert.equal(this.controller.get('q'), 'lol'); + assert.equal(this.controller.get('z'), 0); + assert.equal(this.$link1.getAttribute('href'), `${urlPrefix}/a-1?q=lol`); + assert.equal(this.$link2.getAttribute('href'), `${urlPrefix}/a-2`); + assert.equal(this.$link3.getAttribute('href'), `${urlPrefix}/a-3`); - assert.deepEqual( - this.controller.get('model'), - { id: 'a-2' }, - "controller's model changed to a-2" - ); - assert.equal(this.controller.get('q'), 'lol'); - assert.equal(this.controller.get('z'), 0); - assert.equal(this.$link1.getAttribute('href'), `${urlPrefix}/a-1?q=lol`); - assert.equal(this.$link2.getAttribute('href'), `${urlPrefix}/a-2?q=lol`); - assert.equal(this.$link3.getAttribute('href'), `${urlPrefix}/a-3`); - - this.expectedModelHookParams = { id: 'a-3', q: 'lol', z: 123 }; - this.transitionTo(`${urlPrefix}/a-3?q=lol&z=123`); - - assert.equal(this.controller.get('q'), 'lol'); - assert.equal(this.controller.get('z'), 123); - assert.equal(this.$link1.getAttribute('href'), `${urlPrefix}/a-1?q=lol`); - assert.equal(this.$link2.getAttribute('href'), `${urlPrefix}/a-2?q=lol`); - assert.equal(this.$link3.getAttribute('href'), `${urlPrefix}/a-3?q=lol&z=123`); - }); + this.expectedModelHookParams = { id: 'a-2', q: 'lol', z: 0 }; + + await this.transitionTo(`${urlPrefix}/a-2?q=lol`); + + assert.deepEqual( + this.controller.get('model'), + { id: 'a-2' }, + "controller's model changed to a-2" + ); + assert.equal(this.controller.get('q'), 'lol'); + assert.equal(this.controller.get('z'), 0); + assert.equal(this.$link1.getAttribute('href'), `${urlPrefix}/a-1?q=lol`); + assert.equal(this.$link2.getAttribute('href'), `${urlPrefix}/a-2?q=lol`); + assert.equal(this.$link3.getAttribute('href'), `${urlPrefix}/a-3`); + + this.expectedModelHookParams = { id: 'a-3', q: 'lol', z: 123 }; + + await this.transitionTo(`${urlPrefix}/a-3?q=lol&z=123`); + + assert.equal(this.controller.get('q'), 'lol'); + assert.equal(this.controller.get('z'), 123); + assert.equal(this.$link1.getAttribute('href'), `${urlPrefix}/a-1?q=lol`); + assert.equal(this.$link2.getAttribute('href'), `${urlPrefix}/a-2?q=lol`); + assert.equal(this.$link3.getAttribute('href'), `${urlPrefix}/a-3?q=lol&z=123`); } - queryParamsStickyTest3(urlPrefix, articleLookup) { + async queryParamsStickyTest3(urlPrefix, articleLookup) { let assert = this.assert; assert.expect(32); @@ -104,50 +107,49 @@ class ModelDependentQPTestCase extends QueryParamTestCase { `{{#each articles as |a|}} {{link-to 'Article' '${articleLookup}' a.id id=a.id}} {{/each}}` ); - return this.boot().then(() => { - this.expectedModelHookParams = { id: 'a-1', q: 'wat', z: 0 }; - this.transitionTo(articleLookup, 'a-1'); - - assert.deepEqual(this.controller.get('model'), { id: 'a-1' }); - assert.equal(this.controller.get('q'), 'wat'); - assert.equal(this.controller.get('z'), 0); - assert.equal(this.$link1.getAttribute('href'), `${urlPrefix}/a-1`); - assert.equal(this.$link2.getAttribute('href'), `${urlPrefix}/a-2`); - assert.equal(this.$link3.getAttribute('href'), `${urlPrefix}/a-3`); - - this.expectedModelHookParams = { id: 'a-2', q: 'lol', z: 0 }; - this.transitionTo(articleLookup, 'a-2', { queryParams: { q: 'lol' } }); - - assert.deepEqual(this.controller.get('model'), { id: 'a-2' }); - assert.equal(this.controller.get('q'), 'lol'); - assert.equal(this.controller.get('z'), 0); - assert.equal(this.$link1.getAttribute('href'), `${urlPrefix}/a-1`); - assert.equal(this.$link2.getAttribute('href'), `${urlPrefix}/a-2?q=lol`); - assert.equal(this.$link3.getAttribute('href'), `${urlPrefix}/a-3`); - - this.expectedModelHookParams = { id: 'a-3', q: 'hay', z: 0 }; - this.transitionTo(articleLookup, 'a-3', { queryParams: { q: 'hay' } }); - - assert.deepEqual(this.controller.get('model'), { id: 'a-3' }); - assert.equal(this.controller.get('q'), 'hay'); - assert.equal(this.controller.get('z'), 0); - assert.equal(this.$link1.getAttribute('href'), `${urlPrefix}/a-1`); - assert.equal(this.$link2.getAttribute('href'), `${urlPrefix}/a-2?q=lol`); - assert.equal(this.$link3.getAttribute('href'), `${urlPrefix}/a-3?q=hay`); - - this.expectedModelHookParams = { id: 'a-2', q: 'lol', z: 1 }; - this.transitionTo(articleLookup, 'a-2', { queryParams: { z: 1 } }); - - assert.deepEqual(this.controller.get('model'), { id: 'a-2' }); - assert.equal(this.controller.get('q'), 'lol'); - assert.equal(this.controller.get('z'), 1); - assert.equal(this.$link1.getAttribute('href'), `${urlPrefix}/a-1`); - assert.equal(this.$link2.getAttribute('href'), `${urlPrefix}/a-2?q=lol&z=1`); - assert.equal(this.$link3.getAttribute('href'), `${urlPrefix}/a-3?q=hay`); - }); + await this.boot(); + this.expectedModelHookParams = { id: 'a-1', q: 'wat', z: 0 }; + await this.transitionTo(articleLookup, 'a-1'); + + assert.deepEqual(this.controller.get('model'), { id: 'a-1' }); + assert.equal(this.controller.get('q'), 'wat'); + assert.equal(this.controller.get('z'), 0); + assert.equal(this.$link1.getAttribute('href'), `${urlPrefix}/a-1`); + assert.equal(this.$link2.getAttribute('href'), `${urlPrefix}/a-2`); + assert.equal(this.$link3.getAttribute('href'), `${urlPrefix}/a-3`); + + this.expectedModelHookParams = { id: 'a-2', q: 'lol', z: 0 }; + await this.transitionTo(articleLookup, 'a-2', { queryParams: { q: 'lol' } }); + + assert.deepEqual(this.controller.get('model'), { id: 'a-2' }); + assert.equal(this.controller.get('q'), 'lol'); + assert.equal(this.controller.get('z'), 0); + assert.equal(this.$link1.getAttribute('href'), `${urlPrefix}/a-1`); + assert.equal(this.$link2.getAttribute('href'), `${urlPrefix}/a-2?q=lol`); + assert.equal(this.$link3.getAttribute('href'), `${urlPrefix}/a-3`); + + this.expectedModelHookParams = { id: 'a-3', q: 'hay', z: 0 }; + await this.transitionTo(articleLookup, 'a-3', { queryParams: { q: 'hay' } }); + + assert.deepEqual(this.controller.get('model'), { id: 'a-3' }); + assert.equal(this.controller.get('q'), 'hay'); + assert.equal(this.controller.get('z'), 0); + assert.equal(this.$link1.getAttribute('href'), `${urlPrefix}/a-1`); + assert.equal(this.$link2.getAttribute('href'), `${urlPrefix}/a-2?q=lol`); + assert.equal(this.$link3.getAttribute('href'), `${urlPrefix}/a-3?q=hay`); + + this.expectedModelHookParams = { id: 'a-2', q: 'lol', z: 1 }; + await this.transitionTo(articleLookup, 'a-2', { queryParams: { z: 1 } }); + + assert.deepEqual(this.controller.get('model'), { id: 'a-2' }); + assert.equal(this.controller.get('q'), 'lol'); + assert.equal(this.controller.get('z'), 1); + assert.equal(this.$link1.getAttribute('href'), `${urlPrefix}/a-1`); + assert.equal(this.$link2.getAttribute('href'), `${urlPrefix}/a-2?q=lol&z=1`); + assert.equal(this.$link3.getAttribute('href'), `${urlPrefix}/a-3?q=hay`); } - queryParamsStickyTest4(urlPrefix, articleLookup) { + async queryParamsStickyTest4(urlPrefix, articleLookup) { let assert = this.assert; assert.expect(24); @@ -158,74 +160,75 @@ class ModelDependentQPTestCase extends QueryParamTestCase { queryParams: { q: { scope: 'controller' } }, }); - return this.visitApplication().then(() => { - run(this.$link1, 'click'); - this.assertCurrentPath(`${urlPrefix}/a-1`); + await this.visitApplication(); + this.$link1.click(); + await runLoopSettled(); - this.setAndFlush(this.controller, 'q', 'lol'); + this.assertCurrentPath(`${urlPrefix}/a-1`); - assert.equal(this.$link1.getAttribute('href'), `${urlPrefix}/a-1?q=lol`); - assert.equal(this.$link2.getAttribute('href'), `${urlPrefix}/a-2?q=lol`); - assert.equal(this.$link3.getAttribute('href'), `${urlPrefix}/a-3?q=lol`); + await this.setAndFlush(this.controller, 'q', 'lol'); - run(this.$link2, 'click'); + assert.equal(this.$link1.getAttribute('href'), `${urlPrefix}/a-1?q=lol`); + assert.equal(this.$link2.getAttribute('href'), `${urlPrefix}/a-2?q=lol`); + assert.equal(this.$link3.getAttribute('href'), `${urlPrefix}/a-3?q=lol`); - assert.equal(this.controller.get('q'), 'lol'); - assert.equal(this.controller.get('z'), 0); - assert.deepEqual(this.controller.get('model'), { id: 'a-2' }); + this.$link2.click(); + await runLoopSettled(); - assert.equal(this.$link1.getAttribute('href'), `${urlPrefix}/a-1?q=lol`); - assert.equal(this.$link2.getAttribute('href'), `${urlPrefix}/a-2?q=lol`); - assert.equal(this.$link3.getAttribute('href'), `${urlPrefix}/a-3?q=lol`); + assert.equal(this.controller.get('q'), 'lol'); + assert.equal(this.controller.get('z'), 0); + assert.deepEqual(this.controller.get('model'), { id: 'a-2' }); - this.expectedModelHookParams = { id: 'a-3', q: 'haha', z: 123 }; - this.transitionTo(`${urlPrefix}/a-3?q=haha&z=123`); + assert.equal(this.$link1.getAttribute('href'), `${urlPrefix}/a-1?q=lol`); + assert.equal(this.$link2.getAttribute('href'), `${urlPrefix}/a-2?q=lol`); + assert.equal(this.$link3.getAttribute('href'), `${urlPrefix}/a-3?q=lol`); - assert.deepEqual(this.controller.get('model'), { id: 'a-3' }); - assert.equal(this.controller.get('q'), 'haha'); - assert.equal(this.controller.get('z'), 123); + this.expectedModelHookParams = { id: 'a-3', q: 'haha', z: 123 }; + await this.transitionTo(`${urlPrefix}/a-3?q=haha&z=123`); - assert.equal(this.$link1.getAttribute('href'), `${urlPrefix}/a-1?q=haha`); - assert.equal(this.$link2.getAttribute('href'), `${urlPrefix}/a-2?q=haha`); - assert.equal(this.$link3.getAttribute('href'), `${urlPrefix}/a-3?q=haha&z=123`); + assert.deepEqual(this.controller.get('model'), { id: 'a-3' }); + assert.equal(this.controller.get('q'), 'haha'); + assert.equal(this.controller.get('z'), 123); - this.setAndFlush(this.controller, 'q', 'woot'); + assert.equal(this.$link1.getAttribute('href'), `${urlPrefix}/a-1?q=haha`); + assert.equal(this.$link2.getAttribute('href'), `${urlPrefix}/a-2?q=haha`); + assert.equal(this.$link3.getAttribute('href'), `${urlPrefix}/a-3?q=haha&z=123`); - assert.equal(this.$link1.getAttribute('href'), `${urlPrefix}/a-1?q=woot`); - assert.equal(this.$link2.getAttribute('href'), `${urlPrefix}/a-2?q=woot`); - assert.equal(this.$link3.getAttribute('href'), `${urlPrefix}/a-3?q=woot&z=123`); - }); + await this.setAndFlush(this.controller, 'q', 'woot'); + + assert.equal(this.$link1.getAttribute('href'), `${urlPrefix}/a-1?q=woot`); + assert.equal(this.$link2.getAttribute('href'), `${urlPrefix}/a-2?q=woot`); + assert.equal(this.$link3.getAttribute('href'), `${urlPrefix}/a-3?q=woot&z=123`); } - queryParamsStickyTest5(urlPrefix, commentsLookupKey) { + async queryParamsStickyTest5(urlPrefix, commentsLookupKey) { let assert = this.assert; assert.expect(12); - return this.boot().then(() => { - this.transitionTo(commentsLookupKey, 'a-1'); + await this.boot(); + await this.transitionTo(commentsLookupKey, 'a-1'); - let commentsCtrl = this.getController(commentsLookupKey); - assert.equal(commentsCtrl.get('page'), 1); - this.assertCurrentPath(`${urlPrefix}/a-1/comments`); + let commentsCtrl = this.getController(commentsLookupKey); + assert.equal(commentsCtrl.get('page'), 1); + this.assertCurrentPath(`${urlPrefix}/a-1/comments`); - this.setAndFlush(commentsCtrl, 'page', 2); - this.assertCurrentPath(`${urlPrefix}/a-1/comments?page=2`); + await this.setAndFlush(commentsCtrl, 'page', 2); + this.assertCurrentPath(`${urlPrefix}/a-1/comments?page=2`); - this.setAndFlush(commentsCtrl, 'page', 3); - this.assertCurrentPath(`${urlPrefix}/a-1/comments?page=3`); + await this.setAndFlush(commentsCtrl, 'page', 3); + this.assertCurrentPath(`${urlPrefix}/a-1/comments?page=3`); - this.transitionTo(commentsLookupKey, 'a-2'); - assert.equal(commentsCtrl.get('page'), 1); - this.assertCurrentPath(`${urlPrefix}/a-2/comments`); + await this.transitionTo(commentsLookupKey, 'a-2'); + assert.equal(commentsCtrl.get('page'), 1); + this.assertCurrentPath(`${urlPrefix}/a-2/comments`); - this.transitionTo(commentsLookupKey, 'a-1'); - assert.equal(commentsCtrl.get('page'), 3); - this.assertCurrentPath(`${urlPrefix}/a-1/comments?page=3`); - }); + await this.transitionTo(commentsLookupKey, 'a-1'); + assert.equal(commentsCtrl.get('page'), 3); + this.assertCurrentPath(`${urlPrefix}/a-1/comments?page=3`); } - queryParamsStickyTest6(urlPrefix, articleLookup, commentsLookup) { + async queryParamsStickyTest6(urlPrefix, articleLookup, commentsLookup) { let assert = this.assert; assert.expect(13); @@ -246,35 +249,30 @@ class ModelDependentQPTestCase extends QueryParamTestCase { `{{link-to 'A' '${commentsLookup}' 'a-1' id='one'}} {{link-to 'B' '${commentsLookup}' 'a-2' id='two'}}` ); - return this.visitApplication().then(() => { - this.transitionTo(commentsLookup, 'a-1'); - - let commentsCtrl = this.getController(commentsLookup); - assert.equal(commentsCtrl.get('page'), 1); - this.assertCurrentPath(`${urlPrefix}/a-1/comments`); + await this.visitApplication(); + await this.transitionTo(commentsLookup, 'a-1'); - this.setAndFlush(commentsCtrl, 'page', 2); - this.assertCurrentPath(`${urlPrefix}/a-1/comments?page=2`); + let commentsCtrl = this.getController(commentsLookup); + assert.equal(commentsCtrl.get('page'), 1); + this.assertCurrentPath(`${urlPrefix}/a-1/comments`); - this.transitionTo(commentsLookup, 'a-2'); - assert.equal(commentsCtrl.get('page'), 1); - assert.equal(this.controller.get('q'), 'wat'); + await this.setAndFlush(commentsCtrl, 'page', 2); + this.assertCurrentPath(`${urlPrefix}/a-1/comments?page=2`); - this.transitionTo(commentsLookup, 'a-1'); + await this.transitionTo(commentsLookup, 'a-2'); + assert.equal(commentsCtrl.get('page'), 1); + assert.equal(this.controller.get('q'), 'wat'); - this.assertCurrentPath(`${urlPrefix}/a-1/comments`); - assert.equal(commentsCtrl.get('page'), 1); + await this.transitionTo(commentsLookup, 'a-1'); + this.assertCurrentPath(`${urlPrefix}/a-1/comments`); + assert.equal(commentsCtrl.get('page'), 1); - this.transitionTo('about'); - assert.equal( - document.getElementById('one').getAttribute('href'), - `${urlPrefix}/a-1/comments?q=imdone` - ); - assert.equal( - document.getElementById('two').getAttribute('href'), - `${urlPrefix}/a-2/comments` - ); - }); + await this.transitionTo('about'); + assert.equal( + document.getElementById('one').getAttribute('href'), + `${urlPrefix}/a-1/comments?q=imdone` + ); + assert.equal(document.getElementById('two').getAttribute('href'), `${urlPrefix}/a-2/comments`); } } @@ -627,491 +625,442 @@ moduleFor( }); } - ["@test query params have 'model' stickiness by default"](assert) { + async ["@test query params have 'model' stickiness by default"](assert) { assert.expect(59); - return this.boot().then(() => { - run(this.links['s-1-a-1'], 'click'); - assert.deepEqual(this.site_controller.get('model'), { id: 's-1' }); - assert.deepEqual(this.article_controller.get('model'), { id: 'a-1' }); - this.assertCurrentPath('/site/s-1/a/a-1'); - - this.setAndFlush(this.article_controller, 'q', 'lol'); - - assert.equal(this.links['s-1-a-1'].getAttribute('href'), '/site/s-1/a/a-1?q=lol'); - assert.equal(this.links['s-1-a-2'].getAttribute('href'), '/site/s-1/a/a-2'); - assert.equal(this.links['s-1-a-3'].getAttribute('href'), '/site/s-1/a/a-3'); - assert.equal(this.links['s-2-a-1'].getAttribute('href'), '/site/s-2/a/a-1?q=lol'); - assert.equal(this.links['s-2-a-2'].getAttribute('href'), '/site/s-2/a/a-2'); - assert.equal(this.links['s-2-a-3'].getAttribute('href'), '/site/s-2/a/a-3'); - assert.equal(this.links['s-3-a-1'].getAttribute('href'), '/site/s-3/a/a-1?q=lol'); - assert.equal(this.links['s-3-a-2'].getAttribute('href'), '/site/s-3/a/a-2'); - assert.equal(this.links['s-3-a-3'].getAttribute('href'), '/site/s-3/a/a-3'); - - this.setAndFlush(this.site_controller, 'country', 'us'); - - assert.equal( - this.links['s-1-a-1'].getAttribute('href'), - '/site/s-1/a/a-1?country=us&q=lol' - ); - assert.equal(this.links['s-1-a-2'].getAttribute('href'), '/site/s-1/a/a-2?country=us'); - assert.equal(this.links['s-1-a-3'].getAttribute('href'), '/site/s-1/a/a-3?country=us'); - assert.equal(this.links['s-2-a-1'].getAttribute('href'), '/site/s-2/a/a-1?q=lol'); - assert.equal(this.links['s-2-a-2'].getAttribute('href'), '/site/s-2/a/a-2'); - assert.equal(this.links['s-2-a-3'].getAttribute('href'), '/site/s-2/a/a-3'); - assert.equal(this.links['s-3-a-1'].getAttribute('href'), '/site/s-3/a/a-1?q=lol'); - assert.equal(this.links['s-3-a-2'].getAttribute('href'), '/site/s-3/a/a-2'); - assert.equal(this.links['s-3-a-3'].getAttribute('href'), '/site/s-3/a/a-3'); - - run(this.links['s-1-a-2'], 'click'); - - assert.equal(this.site_controller.get('country'), 'us'); - assert.equal(this.article_controller.get('q'), 'wat'); - assert.equal(this.article_controller.get('z'), 0); - assert.deepEqual(this.site_controller.get('model'), { id: 's-1' }); - assert.deepEqual(this.article_controller.get('model'), { id: 'a-2' }); - assert.equal( - this.links['s-1-a-1'].getAttribute('href'), - '/site/s-1/a/a-1?country=us&q=lol' - ); - assert.equal(this.links['s-1-a-2'].getAttribute('href'), '/site/s-1/a/a-2?country=us'); - assert.equal(this.links['s-1-a-3'].getAttribute('href'), '/site/s-1/a/a-3?country=us'); - assert.equal(this.links['s-2-a-1'].getAttribute('href'), '/site/s-2/a/a-1?q=lol'); - assert.equal(this.links['s-2-a-2'].getAttribute('href'), '/site/s-2/a/a-2'); - assert.equal(this.links['s-2-a-3'].getAttribute('href'), '/site/s-2/a/a-3'); - assert.equal(this.links['s-3-a-1'].getAttribute('href'), '/site/s-3/a/a-1?q=lol'); - assert.equal(this.links['s-3-a-2'].getAttribute('href'), '/site/s-3/a/a-2'); - assert.equal(this.links['s-3-a-3'].getAttribute('href'), '/site/s-3/a/a-3'); - - run(this.links['s-2-a-2'], 'click'); - - assert.equal(this.site_controller.get('country'), 'au'); - assert.equal(this.article_controller.get('q'), 'wat'); - assert.equal(this.article_controller.get('z'), 0); - assert.deepEqual(this.site_controller.get('model'), { id: 's-2' }); - assert.deepEqual(this.article_controller.get('model'), { id: 'a-2' }); - assert.equal( - this.links['s-1-a-1'].getAttribute('href'), - '/site/s-1/a/a-1?country=us&q=lol' - ); - assert.equal(this.links['s-1-a-2'].getAttribute('href'), '/site/s-1/a/a-2?country=us'); - assert.equal(this.links['s-1-a-3'].getAttribute('href'), '/site/s-1/a/a-3?country=us'); - assert.equal(this.links['s-2-a-1'].getAttribute('href'), '/site/s-2/a/a-1?q=lol'); - assert.equal(this.links['s-2-a-2'].getAttribute('href'), '/site/s-2/a/a-2'); - assert.equal(this.links['s-2-a-3'].getAttribute('href'), '/site/s-2/a/a-3'); - assert.equal(this.links['s-3-a-1'].getAttribute('href'), '/site/s-3/a/a-1?q=lol'); - assert.equal(this.links['s-3-a-2'].getAttribute('href'), '/site/s-3/a/a-2'); - assert.equal(this.links['s-3-a-3'].getAttribute('href'), '/site/s-3/a/a-3'); - }); + await this.boot(); + this.links['s-1-a-1'].click(); + await runLoopSettled(); + assert.deepEqual(this.site_controller.get('model'), { id: 's-1' }); + assert.deepEqual(this.article_controller.get('model'), { id: 'a-1' }); + this.assertCurrentPath('/site/s-1/a/a-1'); + + await this.setAndFlush(this.article_controller, 'q', 'lol'); + + assert.equal(this.links['s-1-a-1'].getAttribute('href'), '/site/s-1/a/a-1?q=lol'); + assert.equal(this.links['s-1-a-2'].getAttribute('href'), '/site/s-1/a/a-2'); + assert.equal(this.links['s-1-a-3'].getAttribute('href'), '/site/s-1/a/a-3'); + assert.equal(this.links['s-2-a-1'].getAttribute('href'), '/site/s-2/a/a-1?q=lol'); + assert.equal(this.links['s-2-a-2'].getAttribute('href'), '/site/s-2/a/a-2'); + assert.equal(this.links['s-2-a-3'].getAttribute('href'), '/site/s-2/a/a-3'); + assert.equal(this.links['s-3-a-1'].getAttribute('href'), '/site/s-3/a/a-1?q=lol'); + assert.equal(this.links['s-3-a-2'].getAttribute('href'), '/site/s-3/a/a-2'); + assert.equal(this.links['s-3-a-3'].getAttribute('href'), '/site/s-3/a/a-3'); + + await this.setAndFlush(this.site_controller, 'country', 'us'); + + assert.equal(this.links['s-1-a-1'].getAttribute('href'), '/site/s-1/a/a-1?country=us&q=lol'); + assert.equal(this.links['s-1-a-2'].getAttribute('href'), '/site/s-1/a/a-2?country=us'); + assert.equal(this.links['s-1-a-3'].getAttribute('href'), '/site/s-1/a/a-3?country=us'); + assert.equal(this.links['s-2-a-1'].getAttribute('href'), '/site/s-2/a/a-1?q=lol'); + assert.equal(this.links['s-2-a-2'].getAttribute('href'), '/site/s-2/a/a-2'); + assert.equal(this.links['s-2-a-3'].getAttribute('href'), '/site/s-2/a/a-3'); + assert.equal(this.links['s-3-a-1'].getAttribute('href'), '/site/s-3/a/a-1?q=lol'); + assert.equal(this.links['s-3-a-2'].getAttribute('href'), '/site/s-3/a/a-2'); + assert.equal(this.links['s-3-a-3'].getAttribute('href'), '/site/s-3/a/a-3'); + + this.links['s-1-a-2'].click(); + await runLoopSettled(); + + assert.equal(this.site_controller.get('country'), 'us'); + assert.equal(this.article_controller.get('q'), 'wat'); + assert.equal(this.article_controller.get('z'), 0); + assert.deepEqual(this.site_controller.get('model'), { id: 's-1' }); + assert.deepEqual(this.article_controller.get('model'), { id: 'a-2' }); + assert.equal(this.links['s-1-a-1'].getAttribute('href'), '/site/s-1/a/a-1?country=us&q=lol'); + assert.equal(this.links['s-1-a-2'].getAttribute('href'), '/site/s-1/a/a-2?country=us'); + assert.equal(this.links['s-1-a-3'].getAttribute('href'), '/site/s-1/a/a-3?country=us'); + assert.equal(this.links['s-2-a-1'].getAttribute('href'), '/site/s-2/a/a-1?q=lol'); + assert.equal(this.links['s-2-a-2'].getAttribute('href'), '/site/s-2/a/a-2'); + assert.equal(this.links['s-2-a-3'].getAttribute('href'), '/site/s-2/a/a-3'); + assert.equal(this.links['s-3-a-1'].getAttribute('href'), '/site/s-3/a/a-1?q=lol'); + assert.equal(this.links['s-3-a-2'].getAttribute('href'), '/site/s-3/a/a-2'); + assert.equal(this.links['s-3-a-3'].getAttribute('href'), '/site/s-3/a/a-3'); + + this.links['s-2-a-2'].click(); + await runLoopSettled(); + + assert.equal(this.site_controller.get('country'), 'au'); + assert.equal(this.article_controller.get('q'), 'wat'); + assert.equal(this.article_controller.get('z'), 0); + assert.deepEqual(this.site_controller.get('model'), { id: 's-2' }); + assert.deepEqual(this.article_controller.get('model'), { id: 'a-2' }); + assert.equal(this.links['s-1-a-1'].getAttribute('href'), '/site/s-1/a/a-1?country=us&q=lol'); + assert.equal(this.links['s-1-a-2'].getAttribute('href'), '/site/s-1/a/a-2?country=us'); + assert.equal(this.links['s-1-a-3'].getAttribute('href'), '/site/s-1/a/a-3?country=us'); + assert.equal(this.links['s-2-a-1'].getAttribute('href'), '/site/s-2/a/a-1?q=lol'); + assert.equal(this.links['s-2-a-2'].getAttribute('href'), '/site/s-2/a/a-2'); + assert.equal(this.links['s-2-a-3'].getAttribute('href'), '/site/s-2/a/a-3'); + assert.equal(this.links['s-3-a-1'].getAttribute('href'), '/site/s-3/a/a-1?q=lol'); + assert.equal(this.links['s-3-a-2'].getAttribute('href'), '/site/s-3/a/a-2'); + assert.equal(this.links['s-3-a-3'].getAttribute('href'), '/site/s-3/a/a-3'); } - ["@test query params have 'model' stickiness by default (url changes)"](assert) { + async ["@test query params have 'model' stickiness by default (url changes)"](assert) { assert.expect(88); - return this.boot().then(() => { - this.expectedSiteModelHookParams = { site_id: 's-1', country: 'au' }; - this.expectedArticleModelHookParams = { - article_id: 'a-1', - q: 'lol', - z: 0, - }; - this.transitionTo('/site/s-1/a/a-1?q=lol'); - - assert.deepEqual( - this.site_controller.get('model'), - { id: 's-1' }, - "site controller's model is s-1" - ); - assert.deepEqual( - this.article_controller.get('model'), - { id: 'a-1' }, - "article controller's model is a-1" - ); - assert.equal(this.site_controller.get('country'), 'au'); - assert.equal(this.article_controller.get('q'), 'lol'); - assert.equal(this.article_controller.get('z'), 0); - assert.equal(this.links['s-1-a-1'].getAttribute('href'), '/site/s-1/a/a-1?q=lol'); - assert.equal(this.links['s-1-a-2'].getAttribute('href'), '/site/s-1/a/a-2'); - assert.equal(this.links['s-1-a-3'].getAttribute('href'), '/site/s-1/a/a-3'); - assert.equal(this.links['s-2-a-1'].getAttribute('href'), '/site/s-2/a/a-1?q=lol'); - assert.equal(this.links['s-2-a-2'].getAttribute('href'), '/site/s-2/a/a-2'); - assert.equal(this.links['s-2-a-3'].getAttribute('href'), '/site/s-2/a/a-3'); - assert.equal(this.links['s-3-a-1'].getAttribute('href'), '/site/s-3/a/a-1?q=lol'); - assert.equal(this.links['s-3-a-2'].getAttribute('href'), '/site/s-3/a/a-2'); - assert.equal(this.links['s-3-a-3'].getAttribute('href'), '/site/s-3/a/a-3'); + await this.boot(); + this.expectedSiteModelHookParams = { site_id: 's-1', country: 'au' }; + this.expectedArticleModelHookParams = { + article_id: 'a-1', + q: 'lol', + z: 0, + }; + await this.transitionTo('/site/s-1/a/a-1?q=lol'); - this.expectedSiteModelHookParams = { site_id: 's-2', country: 'us' }; - this.expectedArticleModelHookParams = { - article_id: 'a-1', - q: 'lol', - z: 0, - }; - this.transitionTo('/site/s-2/a/a-1?country=us&q=lol'); - - assert.deepEqual( - this.site_controller.get('model'), - { id: 's-2' }, - "site controller's model is s-2" - ); - assert.deepEqual( - this.article_controller.get('model'), - { id: 'a-1' }, - "article controller's model is a-1" - ); - assert.equal(this.site_controller.get('country'), 'us'); - assert.equal(this.article_controller.get('q'), 'lol'); - assert.equal(this.article_controller.get('z'), 0); - assert.equal(this.links['s-1-a-1'].getAttribute('href'), '/site/s-1/a/a-1?q=lol'); - assert.equal(this.links['s-1-a-2'].getAttribute('href'), '/site/s-1/a/a-2'); - assert.equal(this.links['s-1-a-3'].getAttribute('href'), '/site/s-1/a/a-3'); - assert.equal( - this.links['s-2-a-1'].getAttribute('href'), - '/site/s-2/a/a-1?country=us&q=lol' - ); - assert.equal(this.links['s-2-a-2'].getAttribute('href'), '/site/s-2/a/a-2?country=us'); - assert.equal(this.links['s-2-a-3'].getAttribute('href'), '/site/s-2/a/a-3?country=us'); - assert.equal(this.links['s-3-a-1'].getAttribute('href'), '/site/s-3/a/a-1?q=lol'); - assert.equal(this.links['s-3-a-2'].getAttribute('href'), '/site/s-3/a/a-2'); - assert.equal(this.links['s-3-a-3'].getAttribute('href'), '/site/s-3/a/a-3'); - - this.expectedSiteModelHookParams = { site_id: 's-2', country: 'us' }; - this.expectedArticleModelHookParams = { - article_id: 'a-2', - q: 'lol', - z: 0, - }; - this.transitionTo('/site/s-2/a/a-2?country=us&q=lol'); - - assert.deepEqual( - this.site_controller.get('model'), - { id: 's-2' }, - "site controller's model is s-2" - ); - assert.deepEqual( - this.article_controller.get('model'), - { id: 'a-2' }, - "article controller's model is a-2" - ); - assert.equal(this.site_controller.get('country'), 'us'); - assert.equal(this.article_controller.get('q'), 'lol'); - assert.equal(this.article_controller.get('z'), 0); - assert.equal(this.links['s-1-a-1'].getAttribute('href'), '/site/s-1/a/a-1?q=lol'); - assert.equal(this.links['s-1-a-2'].getAttribute('href'), '/site/s-1/a/a-2?q=lol'); - assert.equal(this.links['s-1-a-3'].getAttribute('href'), '/site/s-1/a/a-3'); - assert.equal( - this.links['s-2-a-1'].getAttribute('href'), - '/site/s-2/a/a-1?country=us&q=lol' - ); - assert.equal( - this.links['s-2-a-2'].getAttribute('href'), - '/site/s-2/a/a-2?country=us&q=lol' - ); - assert.equal(this.links['s-2-a-3'].getAttribute('href'), '/site/s-2/a/a-3?country=us'); - assert.equal(this.links['s-3-a-1'].getAttribute('href'), '/site/s-3/a/a-1?q=lol'); - assert.equal(this.links['s-3-a-2'].getAttribute('href'), '/site/s-3/a/a-2?q=lol'); - assert.equal(this.links['s-3-a-3'].getAttribute('href'), '/site/s-3/a/a-3'); - - this.expectedSiteModelHookParams = { site_id: 's-2', country: 'us' }; - this.expectedArticleModelHookParams = { - article_id: 'a-3', - q: 'lol', - z: 123, - }; - this.transitionTo('/site/s-2/a/a-3?country=us&q=lol&z=123'); - - assert.deepEqual( - this.site_controller.get('model'), - { id: 's-2' }, - "site controller's model is s-2" - ); - assert.deepEqual( - this.article_controller.get('model'), - { id: 'a-3' }, - "article controller's model is a-3" - ); - assert.equal(this.site_controller.get('country'), 'us'); - assert.equal(this.article_controller.get('q'), 'lol'); - assert.equal(this.article_controller.get('z'), 123); - assert.equal(this.links['s-1-a-1'].getAttribute('href'), '/site/s-1/a/a-1?q=lol'); - assert.equal(this.links['s-1-a-2'].getAttribute('href'), '/site/s-1/a/a-2?q=lol'); - assert.equal(this.links['s-1-a-3'].getAttribute('href'), '/site/s-1/a/a-3?q=lol&z=123'); - assert.equal( - this.links['s-2-a-1'].getAttribute('href'), - '/site/s-2/a/a-1?country=us&q=lol' - ); - assert.equal( - this.links['s-2-a-2'].getAttribute('href'), - '/site/s-2/a/a-2?country=us&q=lol' - ); - assert.equal( - this.links['s-2-a-3'].getAttribute('href'), - '/site/s-2/a/a-3?country=us&q=lol&z=123' - ); - assert.equal(this.links['s-3-a-1'].getAttribute('href'), '/site/s-3/a/a-1?q=lol'); - assert.equal(this.links['s-3-a-2'].getAttribute('href'), '/site/s-3/a/a-2?q=lol'); - assert.equal(this.links['s-3-a-3'].getAttribute('href'), '/site/s-3/a/a-3?q=lol&z=123'); - - this.expectedSiteModelHookParams = { site_id: 's-3', country: 'nz' }; - this.expectedArticleModelHookParams = { - article_id: 'a-3', - q: 'lol', - z: 123, - }; - this.transitionTo('/site/s-3/a/a-3?country=nz&q=lol&z=123'); - - assert.deepEqual( - this.site_controller.get('model'), - { id: 's-3' }, - "site controller's model is s-3" - ); - assert.deepEqual( - this.article_controller.get('model'), - { id: 'a-3' }, - "article controller's model is a-3" - ); - assert.equal(this.site_controller.get('country'), 'nz'); - assert.equal(this.article_controller.get('q'), 'lol'); - assert.equal(this.article_controller.get('z'), 123); - assert.equal(this.links['s-1-a-1'].getAttribute('href'), '/site/s-1/a/a-1?q=lol'); - assert.equal(this.links['s-1-a-2'].getAttribute('href'), '/site/s-1/a/a-2?q=lol'); - assert.equal(this.links['s-1-a-3'].getAttribute('href'), '/site/s-1/a/a-3?q=lol&z=123'); - assert.equal( - this.links['s-2-a-1'].getAttribute('href'), - '/site/s-2/a/a-1?country=us&q=lol' - ); - assert.equal( - this.links['s-2-a-2'].getAttribute('href'), - '/site/s-2/a/a-2?country=us&q=lol' - ); - assert.equal( - this.links['s-2-a-3'].getAttribute('href'), - '/site/s-2/a/a-3?country=us&q=lol&z=123' - ); - assert.equal( - this.links['s-3-a-1'].getAttribute('href'), - '/site/s-3/a/a-1?country=nz&q=lol' - ); - assert.equal( - this.links['s-3-a-2'].getAttribute('href'), - '/site/s-3/a/a-2?country=nz&q=lol' - ); - assert.equal( - this.links['s-3-a-3'].getAttribute('href'), - '/site/s-3/a/a-3?country=nz&q=lol&z=123' - ); - }); - } + assert.deepEqual( + this.site_controller.get('model'), + { id: 's-1' }, + "site controller's model is s-1" + ); + assert.deepEqual( + this.article_controller.get('model'), + { id: 'a-1' }, + "article controller's model is a-1" + ); + assert.equal(this.site_controller.get('country'), 'au'); + assert.equal(this.article_controller.get('q'), 'lol'); + assert.equal(this.article_controller.get('z'), 0); + assert.equal(this.links['s-1-a-1'].getAttribute('href'), '/site/s-1/a/a-1?q=lol'); + assert.equal(this.links['s-1-a-2'].getAttribute('href'), '/site/s-1/a/a-2'); + assert.equal(this.links['s-1-a-3'].getAttribute('href'), '/site/s-1/a/a-3'); + assert.equal(this.links['s-2-a-1'].getAttribute('href'), '/site/s-2/a/a-1?q=lol'); + assert.equal(this.links['s-2-a-2'].getAttribute('href'), '/site/s-2/a/a-2'); + assert.equal(this.links['s-2-a-3'].getAttribute('href'), '/site/s-2/a/a-3'); + assert.equal(this.links['s-3-a-1'].getAttribute('href'), '/site/s-3/a/a-1?q=lol'); + assert.equal(this.links['s-3-a-2'].getAttribute('href'), '/site/s-3/a/a-2'); + assert.equal(this.links['s-3-a-3'].getAttribute('href'), '/site/s-3/a/a-3'); + + this.expectedSiteModelHookParams = { site_id: 's-2', country: 'us' }; + this.expectedArticleModelHookParams = { + article_id: 'a-1', + q: 'lol', + z: 0, + }; + await this.transitionTo('/site/s-2/a/a-1?country=us&q=lol'); - ["@test query params have 'model' stickiness by default (params-based transitions)"](assert) { - assert.expect(118); + assert.deepEqual( + this.site_controller.get('model'), + { id: 's-2' }, + "site controller's model is s-2" + ); + assert.deepEqual( + this.article_controller.get('model'), + { id: 'a-1' }, + "article controller's model is a-1" + ); + assert.equal(this.site_controller.get('country'), 'us'); + assert.equal(this.article_controller.get('q'), 'lol'); + assert.equal(this.article_controller.get('z'), 0); + assert.equal(this.links['s-1-a-1'].getAttribute('href'), '/site/s-1/a/a-1?q=lol'); + assert.equal(this.links['s-1-a-2'].getAttribute('href'), '/site/s-1/a/a-2'); + assert.equal(this.links['s-1-a-3'].getAttribute('href'), '/site/s-1/a/a-3'); + assert.equal(this.links['s-2-a-1'].getAttribute('href'), '/site/s-2/a/a-1?country=us&q=lol'); + assert.equal(this.links['s-2-a-2'].getAttribute('href'), '/site/s-2/a/a-2?country=us'); + assert.equal(this.links['s-2-a-3'].getAttribute('href'), '/site/s-2/a/a-3?country=us'); + assert.equal(this.links['s-3-a-1'].getAttribute('href'), '/site/s-3/a/a-1?q=lol'); + assert.equal(this.links['s-3-a-2'].getAttribute('href'), '/site/s-3/a/a-2'); + assert.equal(this.links['s-3-a-3'].getAttribute('href'), '/site/s-3/a/a-3'); + + this.expectedSiteModelHookParams = { site_id: 's-2', country: 'us' }; + this.expectedArticleModelHookParams = { + article_id: 'a-2', + q: 'lol', + z: 0, + }; + await this.transitionTo('/site/s-2/a/a-2?country=us&q=lol'); - return this.boot().then(() => { - this.expectedSiteModelHookParams = { site_id: 's-1', country: 'au' }; - this.expectedArticleModelHookParams = { - article_id: 'a-1', - q: 'wat', - z: 0, - }; - this.transitionTo('site.article', 's-1', 'a-1'); - - assert.deepEqual(this.site_controller.get('model'), { id: 's-1' }); - assert.deepEqual(this.article_controller.get('model'), { id: 'a-1' }); - assert.equal(this.site_controller.get('country'), 'au'); - assert.equal(this.article_controller.get('q'), 'wat'); - assert.equal(this.article_controller.get('z'), 0); - assert.equal(this.links['s-1-a-1'].getAttribute('href'), '/site/s-1/a/a-1'); - assert.equal(this.links['s-1-a-2'].getAttribute('href'), '/site/s-1/a/a-2'); - assert.equal(this.links['s-1-a-3'].getAttribute('href'), '/site/s-1/a/a-3'); - assert.equal(this.links['s-2-a-1'].getAttribute('href'), '/site/s-2/a/a-1'); - assert.equal(this.links['s-2-a-2'].getAttribute('href'), '/site/s-2/a/a-2'); - assert.equal(this.links['s-2-a-3'].getAttribute('href'), '/site/s-2/a/a-3'); - assert.equal(this.links['s-3-a-1'].getAttribute('href'), '/site/s-3/a/a-1'); - assert.equal(this.links['s-3-a-2'].getAttribute('href'), '/site/s-3/a/a-2'); - assert.equal(this.links['s-3-a-3'].getAttribute('href'), '/site/s-3/a/a-3'); + assert.deepEqual( + this.site_controller.get('model'), + { id: 's-2' }, + "site controller's model is s-2" + ); + assert.deepEqual( + this.article_controller.get('model'), + { id: 'a-2' }, + "article controller's model is a-2" + ); + assert.equal(this.site_controller.get('country'), 'us'); + assert.equal(this.article_controller.get('q'), 'lol'); + assert.equal(this.article_controller.get('z'), 0); + assert.equal(this.links['s-1-a-1'].getAttribute('href'), '/site/s-1/a/a-1?q=lol'); + assert.equal(this.links['s-1-a-2'].getAttribute('href'), '/site/s-1/a/a-2?q=lol'); + assert.equal(this.links['s-1-a-3'].getAttribute('href'), '/site/s-1/a/a-3'); + assert.equal(this.links['s-2-a-1'].getAttribute('href'), '/site/s-2/a/a-1?country=us&q=lol'); + assert.equal(this.links['s-2-a-2'].getAttribute('href'), '/site/s-2/a/a-2?country=us&q=lol'); + assert.equal(this.links['s-2-a-3'].getAttribute('href'), '/site/s-2/a/a-3?country=us'); + assert.equal(this.links['s-3-a-1'].getAttribute('href'), '/site/s-3/a/a-1?q=lol'); + assert.equal(this.links['s-3-a-2'].getAttribute('href'), '/site/s-3/a/a-2?q=lol'); + assert.equal(this.links['s-3-a-3'].getAttribute('href'), '/site/s-3/a/a-3'); + + this.expectedSiteModelHookParams = { site_id: 's-2', country: 'us' }; + this.expectedArticleModelHookParams = { + article_id: 'a-3', + q: 'lol', + z: 123, + }; + await this.transitionTo('/site/s-2/a/a-3?country=us&q=lol&z=123'); - this.expectedSiteModelHookParams = { site_id: 's-1', country: 'au' }; - this.expectedArticleModelHookParams = { - article_id: 'a-2', - q: 'lol', - z: 0, - }; - this.transitionTo('site.article', 's-1', 'a-2', { - queryParams: { q: 'lol' }, - }); + assert.deepEqual( + this.site_controller.get('model'), + { id: 's-2' }, + "site controller's model is s-2" + ); + assert.deepEqual( + this.article_controller.get('model'), + { id: 'a-3' }, + "article controller's model is a-3" + ); + assert.equal(this.site_controller.get('country'), 'us'); + assert.equal(this.article_controller.get('q'), 'lol'); + assert.equal(this.article_controller.get('z'), 123); + assert.equal(this.links['s-1-a-1'].getAttribute('href'), '/site/s-1/a/a-1?q=lol'); + assert.equal(this.links['s-1-a-2'].getAttribute('href'), '/site/s-1/a/a-2?q=lol'); + assert.equal(this.links['s-1-a-3'].getAttribute('href'), '/site/s-1/a/a-3?q=lol&z=123'); + assert.equal(this.links['s-2-a-1'].getAttribute('href'), '/site/s-2/a/a-1?country=us&q=lol'); + assert.equal(this.links['s-2-a-2'].getAttribute('href'), '/site/s-2/a/a-2?country=us&q=lol'); + assert.equal( + this.links['s-2-a-3'].getAttribute('href'), + '/site/s-2/a/a-3?country=us&q=lol&z=123' + ); + assert.equal(this.links['s-3-a-1'].getAttribute('href'), '/site/s-3/a/a-1?q=lol'); + assert.equal(this.links['s-3-a-2'].getAttribute('href'), '/site/s-3/a/a-2?q=lol'); + assert.equal(this.links['s-3-a-3'].getAttribute('href'), '/site/s-3/a/a-3?q=lol&z=123'); + + this.expectedSiteModelHookParams = { site_id: 's-3', country: 'nz' }; + this.expectedArticleModelHookParams = { + article_id: 'a-3', + q: 'lol', + z: 123, + }; + await this.transitionTo('/site/s-3/a/a-3?country=nz&q=lol&z=123'); - assert.deepEqual(this.site_controller.get('model'), { id: 's-1' }); - assert.deepEqual(this.article_controller.get('model'), { id: 'a-2' }); - assert.equal(this.site_controller.get('country'), 'au'); - assert.equal(this.article_controller.get('q'), 'lol'); - assert.equal(this.article_controller.get('z'), 0); - assert.equal(this.links['s-1-a-1'].getAttribute('href'), '/site/s-1/a/a-1'); - assert.equal(this.links['s-1-a-2'].getAttribute('href'), '/site/s-1/a/a-2?q=lol'); - assert.equal(this.links['s-1-a-3'].getAttribute('href'), '/site/s-1/a/a-3'); - assert.equal(this.links['s-2-a-1'].getAttribute('href'), '/site/s-2/a/a-1'); - assert.equal(this.links['s-2-a-2'].getAttribute('href'), '/site/s-2/a/a-2?q=lol'); - assert.equal(this.links['s-2-a-3'].getAttribute('href'), '/site/s-2/a/a-3'); - assert.equal(this.links['s-3-a-1'].getAttribute('href'), '/site/s-3/a/a-1'); - assert.equal(this.links['s-3-a-2'].getAttribute('href'), '/site/s-3/a/a-2?q=lol'); - assert.equal(this.links['s-3-a-3'].getAttribute('href'), '/site/s-3/a/a-3'); + assert.deepEqual( + this.site_controller.get('model'), + { id: 's-3' }, + "site controller's model is s-3" + ); + assert.deepEqual( + this.article_controller.get('model'), + { id: 'a-3' }, + "article controller's model is a-3" + ); + assert.equal(this.site_controller.get('country'), 'nz'); + assert.equal(this.article_controller.get('q'), 'lol'); + assert.equal(this.article_controller.get('z'), 123); + assert.equal(this.links['s-1-a-1'].getAttribute('href'), '/site/s-1/a/a-1?q=lol'); + assert.equal(this.links['s-1-a-2'].getAttribute('href'), '/site/s-1/a/a-2?q=lol'); + assert.equal(this.links['s-1-a-3'].getAttribute('href'), '/site/s-1/a/a-3?q=lol&z=123'); + assert.equal(this.links['s-2-a-1'].getAttribute('href'), '/site/s-2/a/a-1?country=us&q=lol'); + assert.equal(this.links['s-2-a-2'].getAttribute('href'), '/site/s-2/a/a-2?country=us&q=lol'); + assert.equal( + this.links['s-2-a-3'].getAttribute('href'), + '/site/s-2/a/a-3?country=us&q=lol&z=123' + ); + assert.equal(this.links['s-3-a-1'].getAttribute('href'), '/site/s-3/a/a-1?country=nz&q=lol'); + assert.equal(this.links['s-3-a-2'].getAttribute('href'), '/site/s-3/a/a-2?country=nz&q=lol'); + assert.equal( + this.links['s-3-a-3'].getAttribute('href'), + '/site/s-3/a/a-3?country=nz&q=lol&z=123' + ); + } - this.expectedSiteModelHookParams = { site_id: 's-1', country: 'au' }; - this.expectedArticleModelHookParams = { - article_id: 'a-3', - q: 'hay', - z: 0, - }; - this.transitionTo('site.article', 's-1', 'a-3', { - queryParams: { q: 'hay' }, - }); + async ["@test query params have 'model' stickiness by default (params-based transitions)"]( + assert + ) { + assert.expect(118); - assert.deepEqual(this.site_controller.get('model'), { id: 's-1' }); - assert.deepEqual(this.article_controller.get('model'), { id: 'a-3' }); - assert.equal(this.site_controller.get('country'), 'au'); - assert.equal(this.article_controller.get('q'), 'hay'); - assert.equal(this.article_controller.get('z'), 0); - assert.equal(this.links['s-1-a-1'].getAttribute('href'), '/site/s-1/a/a-1'); - assert.equal(this.links['s-1-a-2'].getAttribute('href'), '/site/s-1/a/a-2?q=lol'); - assert.equal(this.links['s-1-a-3'].getAttribute('href'), '/site/s-1/a/a-3?q=hay'); - assert.equal(this.links['s-2-a-1'].getAttribute('href'), '/site/s-2/a/a-1'); - assert.equal(this.links['s-2-a-2'].getAttribute('href'), '/site/s-2/a/a-2?q=lol'); - assert.equal(this.links['s-2-a-3'].getAttribute('href'), '/site/s-2/a/a-3?q=hay'); - assert.equal(this.links['s-3-a-1'].getAttribute('href'), '/site/s-3/a/a-1'); - assert.equal(this.links['s-3-a-2'].getAttribute('href'), '/site/s-3/a/a-2?q=lol'); - assert.equal(this.links['s-3-a-3'].getAttribute('href'), '/site/s-3/a/a-3?q=hay'); - - this.expectedSiteModelHookParams = { site_id: 's-1', country: 'au' }; - this.expectedArticleModelHookParams = { - article_id: 'a-2', - q: 'lol', - z: 1, - }; - this.transitionTo('site.article', 's-1', 'a-2', { - queryParams: { z: 1 }, - }); + await this.boot(); + this.expectedSiteModelHookParams = { site_id: 's-1', country: 'au' }; + this.expectedArticleModelHookParams = { + article_id: 'a-1', + q: 'wat', + z: 0, + }; + await this.transitionTo('site.article', 's-1', 'a-1'); + + assert.deepEqual(this.site_controller.get('model'), { id: 's-1' }); + assert.deepEqual(this.article_controller.get('model'), { id: 'a-1' }); + assert.equal(this.site_controller.get('country'), 'au'); + assert.equal(this.article_controller.get('q'), 'wat'); + assert.equal(this.article_controller.get('z'), 0); + assert.equal(this.links['s-1-a-1'].getAttribute('href'), '/site/s-1/a/a-1'); + assert.equal(this.links['s-1-a-2'].getAttribute('href'), '/site/s-1/a/a-2'); + assert.equal(this.links['s-1-a-3'].getAttribute('href'), '/site/s-1/a/a-3'); + assert.equal(this.links['s-2-a-1'].getAttribute('href'), '/site/s-2/a/a-1'); + assert.equal(this.links['s-2-a-2'].getAttribute('href'), '/site/s-2/a/a-2'); + assert.equal(this.links['s-2-a-3'].getAttribute('href'), '/site/s-2/a/a-3'); + assert.equal(this.links['s-3-a-1'].getAttribute('href'), '/site/s-3/a/a-1'); + assert.equal(this.links['s-3-a-2'].getAttribute('href'), '/site/s-3/a/a-2'); + assert.equal(this.links['s-3-a-3'].getAttribute('href'), '/site/s-3/a/a-3'); + + this.expectedSiteModelHookParams = { site_id: 's-1', country: 'au' }; + this.expectedArticleModelHookParams = { + article_id: 'a-2', + q: 'lol', + z: 0, + }; + await this.transitionTo('site.article', 's-1', 'a-2', { + queryParams: { q: 'lol' }, + }); - assert.deepEqual(this.site_controller.get('model'), { id: 's-1' }); - assert.deepEqual(this.article_controller.get('model'), { id: 'a-2' }); - assert.equal(this.site_controller.get('country'), 'au'); - assert.equal(this.article_controller.get('q'), 'lol'); - assert.equal(this.article_controller.get('z'), 1); - assert.equal(this.links['s-1-a-1'].getAttribute('href'), '/site/s-1/a/a-1'); - assert.equal(this.links['s-1-a-2'].getAttribute('href'), '/site/s-1/a/a-2?q=lol&z=1'); - assert.equal(this.links['s-1-a-3'].getAttribute('href'), '/site/s-1/a/a-3?q=hay'); - assert.equal(this.links['s-2-a-1'].getAttribute('href'), '/site/s-2/a/a-1'); - assert.equal(this.links['s-2-a-2'].getAttribute('href'), '/site/s-2/a/a-2?q=lol&z=1'); - assert.equal(this.links['s-2-a-3'].getAttribute('href'), '/site/s-2/a/a-3?q=hay'); - assert.equal(this.links['s-3-a-1'].getAttribute('href'), '/site/s-3/a/a-1'); - assert.equal(this.links['s-3-a-2'].getAttribute('href'), '/site/s-3/a/a-2?q=lol&z=1'); - assert.equal(this.links['s-3-a-3'].getAttribute('href'), '/site/s-3/a/a-3?q=hay'); - - this.expectedSiteModelHookParams = { site_id: 's-2', country: 'us' }; - this.expectedArticleModelHookParams = { - article_id: 'a-2', - q: 'lol', - z: 1, - }; - this.transitionTo('site.article', 's-2', 'a-2', { - queryParams: { country: 'us' }, - }); + assert.deepEqual(this.site_controller.get('model'), { id: 's-1' }); + assert.deepEqual(this.article_controller.get('model'), { id: 'a-2' }); + assert.equal(this.site_controller.get('country'), 'au'); + assert.equal(this.article_controller.get('q'), 'lol'); + assert.equal(this.article_controller.get('z'), 0); + assert.equal(this.links['s-1-a-1'].getAttribute('href'), '/site/s-1/a/a-1'); + assert.equal(this.links['s-1-a-2'].getAttribute('href'), '/site/s-1/a/a-2?q=lol'); + assert.equal(this.links['s-1-a-3'].getAttribute('href'), '/site/s-1/a/a-3'); + assert.equal(this.links['s-2-a-1'].getAttribute('href'), '/site/s-2/a/a-1'); + assert.equal(this.links['s-2-a-2'].getAttribute('href'), '/site/s-2/a/a-2?q=lol'); + assert.equal(this.links['s-2-a-3'].getAttribute('href'), '/site/s-2/a/a-3'); + assert.equal(this.links['s-3-a-1'].getAttribute('href'), '/site/s-3/a/a-1'); + assert.equal(this.links['s-3-a-2'].getAttribute('href'), '/site/s-3/a/a-2?q=lol'); + assert.equal(this.links['s-3-a-3'].getAttribute('href'), '/site/s-3/a/a-3'); + + this.expectedSiteModelHookParams = { site_id: 's-1', country: 'au' }; + this.expectedArticleModelHookParams = { + article_id: 'a-3', + q: 'hay', + z: 0, + }; + await this.transitionTo('site.article', 's-1', 'a-3', { + queryParams: { q: 'hay' }, + }); - assert.deepEqual(this.site_controller.get('model'), { id: 's-2' }); - assert.deepEqual(this.article_controller.get('model'), { id: 'a-2' }); - assert.equal(this.site_controller.get('country'), 'us'); - assert.equal(this.article_controller.get('q'), 'lol'); - assert.equal(this.article_controller.get('z'), 1); - assert.equal(this.links['s-1-a-1'].getAttribute('href'), '/site/s-1/a/a-1'); - assert.equal(this.links['s-1-a-2'].getAttribute('href'), '/site/s-1/a/a-2?q=lol&z=1'); - assert.equal(this.links['s-1-a-3'].getAttribute('href'), '/site/s-1/a/a-3?q=hay'); - assert.equal(this.links['s-2-a-1'].getAttribute('href'), '/site/s-2/a/a-1?country=us'); - assert.equal( - this.links['s-2-a-2'].getAttribute('href'), - '/site/s-2/a/a-2?country=us&q=lol&z=1' - ); - assert.equal( - this.links['s-2-a-3'].getAttribute('href'), - '/site/s-2/a/a-3?country=us&q=hay' - ); - assert.equal(this.links['s-3-a-1'].getAttribute('href'), '/site/s-3/a/a-1'); - assert.equal(this.links['s-3-a-2'].getAttribute('href'), '/site/s-3/a/a-2?q=lol&z=1'); - assert.equal(this.links['s-3-a-3'].getAttribute('href'), '/site/s-3/a/a-3?q=hay'); + assert.deepEqual(this.site_controller.get('model'), { id: 's-1' }); + assert.deepEqual(this.article_controller.get('model'), { id: 'a-3' }); + assert.equal(this.site_controller.get('country'), 'au'); + assert.equal(this.article_controller.get('q'), 'hay'); + assert.equal(this.article_controller.get('z'), 0); + assert.equal(this.links['s-1-a-1'].getAttribute('href'), '/site/s-1/a/a-1'); + assert.equal(this.links['s-1-a-2'].getAttribute('href'), '/site/s-1/a/a-2?q=lol'); + assert.equal(this.links['s-1-a-3'].getAttribute('href'), '/site/s-1/a/a-3?q=hay'); + assert.equal(this.links['s-2-a-1'].getAttribute('href'), '/site/s-2/a/a-1'); + assert.equal(this.links['s-2-a-2'].getAttribute('href'), '/site/s-2/a/a-2?q=lol'); + assert.equal(this.links['s-2-a-3'].getAttribute('href'), '/site/s-2/a/a-3?q=hay'); + assert.equal(this.links['s-3-a-1'].getAttribute('href'), '/site/s-3/a/a-1'); + assert.equal(this.links['s-3-a-2'].getAttribute('href'), '/site/s-3/a/a-2?q=lol'); + assert.equal(this.links['s-3-a-3'].getAttribute('href'), '/site/s-3/a/a-3?q=hay'); + + this.expectedSiteModelHookParams = { site_id: 's-1', country: 'au' }; + this.expectedArticleModelHookParams = { + article_id: 'a-2', + q: 'lol', + z: 1, + }; + await this.transitionTo('site.article', 's-1', 'a-2', { + queryParams: { z: 1 }, + }); - this.expectedSiteModelHookParams = { site_id: 's-2', country: 'us' }; - this.expectedArticleModelHookParams = { - article_id: 'a-1', - q: 'yeah', - z: 0, - }; - this.transitionTo('site.article', 's-2', 'a-1', { - queryParams: { q: 'yeah' }, - }); + assert.deepEqual(this.site_controller.get('model'), { id: 's-1' }); + assert.deepEqual(this.article_controller.get('model'), { id: 'a-2' }); + assert.equal(this.site_controller.get('country'), 'au'); + assert.equal(this.article_controller.get('q'), 'lol'); + assert.equal(this.article_controller.get('z'), 1); + assert.equal(this.links['s-1-a-1'].getAttribute('href'), '/site/s-1/a/a-1'); + assert.equal(this.links['s-1-a-2'].getAttribute('href'), '/site/s-1/a/a-2?q=lol&z=1'); + assert.equal(this.links['s-1-a-3'].getAttribute('href'), '/site/s-1/a/a-3?q=hay'); + assert.equal(this.links['s-2-a-1'].getAttribute('href'), '/site/s-2/a/a-1'); + assert.equal(this.links['s-2-a-2'].getAttribute('href'), '/site/s-2/a/a-2?q=lol&z=1'); + assert.equal(this.links['s-2-a-3'].getAttribute('href'), '/site/s-2/a/a-3?q=hay'); + assert.equal(this.links['s-3-a-1'].getAttribute('href'), '/site/s-3/a/a-1'); + assert.equal(this.links['s-3-a-2'].getAttribute('href'), '/site/s-3/a/a-2?q=lol&z=1'); + assert.equal(this.links['s-3-a-3'].getAttribute('href'), '/site/s-3/a/a-3?q=hay'); + + this.expectedSiteModelHookParams = { site_id: 's-2', country: 'us' }; + this.expectedArticleModelHookParams = { + article_id: 'a-2', + q: 'lol', + z: 1, + }; + await this.transitionTo('site.article', 's-2', 'a-2', { + queryParams: { country: 'us' }, + }); - assert.deepEqual(this.site_controller.get('model'), { id: 's-2' }); - assert.deepEqual(this.article_controller.get('model'), { id: 'a-1' }); - assert.equal(this.site_controller.get('country'), 'us'); - assert.equal(this.article_controller.get('q'), 'yeah'); - assert.equal(this.article_controller.get('z'), 0); - assert.equal(this.links['s-1-a-1'].getAttribute('href'), '/site/s-1/a/a-1?q=yeah'); - assert.equal(this.links['s-1-a-2'].getAttribute('href'), '/site/s-1/a/a-2?q=lol&z=1'); - assert.equal(this.links['s-1-a-3'].getAttribute('href'), '/site/s-1/a/a-3?q=hay'); - assert.equal( - this.links['s-2-a-1'].getAttribute('href'), - '/site/s-2/a/a-1?country=us&q=yeah' - ); - assert.equal( - this.links['s-2-a-2'].getAttribute('href'), - '/site/s-2/a/a-2?country=us&q=lol&z=1' - ); - assert.equal( - this.links['s-2-a-3'].getAttribute('href'), - '/site/s-2/a/a-3?country=us&q=hay' - ); - assert.equal(this.links['s-3-a-1'].getAttribute('href'), '/site/s-3/a/a-1?q=yeah'); - assert.equal(this.links['s-3-a-2'].getAttribute('href'), '/site/s-3/a/a-2?q=lol&z=1'); - assert.equal(this.links['s-3-a-3'].getAttribute('href'), '/site/s-3/a/a-3?q=hay'); - - this.expectedSiteModelHookParams = { site_id: 's-3', country: 'nz' }; - this.expectedArticleModelHookParams = { - article_id: 'a-3', - q: 'hay', - z: 3, - }; - this.transitionTo('site.article', 's-3', 'a-3', { - queryParams: { country: 'nz', z: 3 }, - }); + assert.deepEqual(this.site_controller.get('model'), { id: 's-2' }); + assert.deepEqual(this.article_controller.get('model'), { id: 'a-2' }); + assert.equal(this.site_controller.get('country'), 'us'); + assert.equal(this.article_controller.get('q'), 'lol'); + assert.equal(this.article_controller.get('z'), 1); + assert.equal(this.links['s-1-a-1'].getAttribute('href'), '/site/s-1/a/a-1'); + assert.equal(this.links['s-1-a-2'].getAttribute('href'), '/site/s-1/a/a-2?q=lol&z=1'); + assert.equal(this.links['s-1-a-3'].getAttribute('href'), '/site/s-1/a/a-3?q=hay'); + assert.equal(this.links['s-2-a-1'].getAttribute('href'), '/site/s-2/a/a-1?country=us'); + assert.equal( + this.links['s-2-a-2'].getAttribute('href'), + '/site/s-2/a/a-2?country=us&q=lol&z=1' + ); + assert.equal(this.links['s-2-a-3'].getAttribute('href'), '/site/s-2/a/a-3?country=us&q=hay'); + assert.equal(this.links['s-3-a-1'].getAttribute('href'), '/site/s-3/a/a-1'); + assert.equal(this.links['s-3-a-2'].getAttribute('href'), '/site/s-3/a/a-2?q=lol&z=1'); + assert.equal(this.links['s-3-a-3'].getAttribute('href'), '/site/s-3/a/a-3?q=hay'); + + this.expectedSiteModelHookParams = { site_id: 's-2', country: 'us' }; + this.expectedArticleModelHookParams = { + article_id: 'a-1', + q: 'yeah', + z: 0, + }; + await this.transitionTo('site.article', 's-2', 'a-1', { + queryParams: { q: 'yeah' }, + }); - assert.deepEqual(this.site_controller.get('model'), { id: 's-3' }); - assert.deepEqual(this.article_controller.get('model'), { id: 'a-3' }); - assert.equal(this.site_controller.get('country'), 'nz'); - assert.equal(this.article_controller.get('q'), 'hay'); - assert.equal(this.article_controller.get('z'), 3); - assert.equal(this.links['s-1-a-1'].getAttribute('href'), '/site/s-1/a/a-1?q=yeah'); - assert.equal(this.links['s-1-a-2'].getAttribute('href'), '/site/s-1/a/a-2?q=lol&z=1'); - assert.equal(this.links['s-1-a-3'].getAttribute('href'), '/site/s-1/a/a-3?q=hay&z=3'); - assert.equal( - this.links['s-2-a-1'].getAttribute('href'), - '/site/s-2/a/a-1?country=us&q=yeah' - ); - assert.equal( - this.links['s-2-a-2'].getAttribute('href'), - '/site/s-2/a/a-2?country=us&q=lol&z=1' - ); - assert.equal( - this.links['s-2-a-3'].getAttribute('href'), - '/site/s-2/a/a-3?country=us&q=hay&z=3' - ); - assert.equal( - this.links['s-3-a-1'].getAttribute('href'), - '/site/s-3/a/a-1?country=nz&q=yeah' - ); - assert.equal( - this.links['s-3-a-2'].getAttribute('href'), - '/site/s-3/a/a-2?country=nz&q=lol&z=1' - ); - assert.equal( - this.links['s-3-a-3'].getAttribute('href'), - '/site/s-3/a/a-3?country=nz&q=hay&z=3' - ); + assert.deepEqual(this.site_controller.get('model'), { id: 's-2' }); + assert.deepEqual(this.article_controller.get('model'), { id: 'a-1' }); + assert.equal(this.site_controller.get('country'), 'us'); + assert.equal(this.article_controller.get('q'), 'yeah'); + assert.equal(this.article_controller.get('z'), 0); + assert.equal(this.links['s-1-a-1'].getAttribute('href'), '/site/s-1/a/a-1?q=yeah'); + assert.equal(this.links['s-1-a-2'].getAttribute('href'), '/site/s-1/a/a-2?q=lol&z=1'); + assert.equal(this.links['s-1-a-3'].getAttribute('href'), '/site/s-1/a/a-3?q=hay'); + assert.equal(this.links['s-2-a-1'].getAttribute('href'), '/site/s-2/a/a-1?country=us&q=yeah'); + assert.equal( + this.links['s-2-a-2'].getAttribute('href'), + '/site/s-2/a/a-2?country=us&q=lol&z=1' + ); + assert.equal(this.links['s-2-a-3'].getAttribute('href'), '/site/s-2/a/a-3?country=us&q=hay'); + assert.equal(this.links['s-3-a-1'].getAttribute('href'), '/site/s-3/a/a-1?q=yeah'); + assert.equal(this.links['s-3-a-2'].getAttribute('href'), '/site/s-3/a/a-2?q=lol&z=1'); + assert.equal(this.links['s-3-a-3'].getAttribute('href'), '/site/s-3/a/a-3?q=hay'); + + this.expectedSiteModelHookParams = { site_id: 's-3', country: 'nz' }; + this.expectedArticleModelHookParams = { + article_id: 'a-3', + q: 'hay', + z: 3, + }; + await this.transitionTo('site.article', 's-3', 'a-3', { + queryParams: { country: 'nz', z: 3 }, }); + + assert.deepEqual(this.site_controller.get('model'), { id: 's-3' }); + assert.deepEqual(this.article_controller.get('model'), { id: 'a-3' }); + assert.equal(this.site_controller.get('country'), 'nz'); + assert.equal(this.article_controller.get('q'), 'hay'); + assert.equal(this.article_controller.get('z'), 3); + assert.equal(this.links['s-1-a-1'].getAttribute('href'), '/site/s-1/a/a-1?q=yeah'); + assert.equal(this.links['s-1-a-2'].getAttribute('href'), '/site/s-1/a/a-2?q=lol&z=1'); + assert.equal(this.links['s-1-a-3'].getAttribute('href'), '/site/s-1/a/a-3?q=hay&z=3'); + assert.equal(this.links['s-2-a-1'].getAttribute('href'), '/site/s-2/a/a-1?country=us&q=yeah'); + assert.equal( + this.links['s-2-a-2'].getAttribute('href'), + '/site/s-2/a/a-2?country=us&q=lol&z=1' + ); + assert.equal( + this.links['s-2-a-3'].getAttribute('href'), + '/site/s-2/a/a-3?country=us&q=hay&z=3' + ); + assert.equal(this.links['s-3-a-1'].getAttribute('href'), '/site/s-3/a/a-1?country=nz&q=yeah'); + assert.equal( + this.links['s-3-a-2'].getAttribute('href'), + '/site/s-3/a/a-2?country=nz&q=lol&z=1' + ); + assert.equal( + this.links['s-3-a-3'].getAttribute('href'), + '/site/s-3/a/a-3?country=nz&q=hay&z=3' + ); } } ); diff --git a/packages/ember/tests/routing/query_params_test/overlapping_query_params_test.js b/packages/ember/tests/routing/query_params_test/overlapping_query_params_test.js index 3feb1f5739c..62dcdbf028e 100644 --- a/packages/ember/tests/routing/query_params_test/overlapping_query_params_test.js +++ b/packages/ember/tests/routing/query_params_test/overlapping_query_params_test.js @@ -1,8 +1,7 @@ import Controller from '@ember/controller'; import { Route } from '@ember/-internals/routing'; -import { run } from '@ember/runloop'; import { Mixin } from '@ember/-internals/metal'; -import { QueryParamTestCase, moduleFor } from 'internal-test-helpers'; +import { QueryParamTestCase, moduleFor, runLoopSettled } from 'internal-test-helpers'; moduleFor( 'Query Params - overlapping query param property names', @@ -17,81 +16,76 @@ moduleFor( return this.visit('/parent/child'); } - ['@test can remap same-named qp props'](assert) { + async ['@test can remap same-named qp props'](assert) { assert.expect(7); this.setMappedQPController('parent'); this.setMappedQPController('parent.child', 'page', 'childPage'); - return this.setupBase().then(() => { - this.assertCurrentPath('/parent/child'); + await this.setupBase(); + this.assertCurrentPath('/parent/child'); - let parentController = this.getController('parent'); - let parentChildController = this.getController('parent.child'); + let parentController = this.getController('parent'); + let parentChildController = this.getController('parent.child'); - this.setAndFlush(parentController, 'page', 2); - this.assertCurrentPath('/parent/child?parentPage=2'); - this.setAndFlush(parentController, 'page', 1); - this.assertCurrentPath('/parent/child'); + await this.setAndFlush(parentController, 'page', 2); + this.assertCurrentPath('/parent/child?parentPage=2'); + await this.setAndFlush(parentController, 'page', 1); + this.assertCurrentPath('/parent/child'); - this.setAndFlush(parentChildController, 'page', 2); - this.assertCurrentPath('/parent/child?childPage=2'); - this.setAndFlush(parentChildController, 'page', 1); - this.assertCurrentPath('/parent/child'); + await this.setAndFlush(parentChildController, 'page', 2); + this.assertCurrentPath('/parent/child?childPage=2'); + await this.setAndFlush(parentChildController, 'page', 1); + this.assertCurrentPath('/parent/child'); - run(() => { - parentController.set('page', 2); - parentChildController.set('page', 2); - }); + parentController.set('page', 2); + parentChildController.set('page', 2); + await runLoopSettled(); - this.assertCurrentPath('/parent/child?childPage=2&parentPage=2'); + this.assertCurrentPath('/parent/child?childPage=2&parentPage=2'); - run(() => { - parentController.set('page', 1); - parentChildController.set('page', 1); - }); + parentController.set('page', 1); + parentChildController.set('page', 1); + await runLoopSettled(); - this.assertCurrentPath('/parent/child'); - }); + this.assertCurrentPath('/parent/child'); } - ['@test query params can be either controller property or url key'](assert) { + async ['@test query params can be either controller property or url key'](assert) { assert.expect(3); this.setMappedQPController('parent'); - return this.setupBase().then(() => { - this.assertCurrentPath('/parent/child'); + await this.setupBase(); + this.assertCurrentPath('/parent/child'); - this.transitionTo('parent.child', { queryParams: { page: 2 } }); - this.assertCurrentPath('/parent/child?parentPage=2'); + await this.transitionTo('parent.child', { queryParams: { page: 2 } }); + this.assertCurrentPath('/parent/child?parentPage=2'); - this.transitionTo('parent.child', { queryParams: { parentPage: 3 } }); - this.assertCurrentPath('/parent/child?parentPage=3'); - }); + await this.transitionTo('parent.child', { queryParams: { parentPage: 3 } }); + this.assertCurrentPath('/parent/child?parentPage=3'); } - ['@test query param matching a url key and controller property'](assert) { + async ['@test query param matching a url key and controller property'](assert) { assert.expect(3); this.setMappedQPController('parent', 'page', 'parentPage'); this.setMappedQPController('parent.child', 'index', 'page'); - return this.setupBase().then(() => { - this.transitionTo('parent.child', { queryParams: { page: 2 } }); - this.assertCurrentPath('/parent/child?parentPage=2'); + await this.setupBase(); + await this.transitionTo('parent.child', { queryParams: { page: 2 } }); + this.assertCurrentPath('/parent/child?parentPage=2'); - this.transitionTo('parent.child', { queryParams: { parentPage: 3 } }); - this.assertCurrentPath('/parent/child?parentPage=3'); + await this.transitionTo('parent.child', { queryParams: { parentPage: 3 } }); + this.assertCurrentPath('/parent/child?parentPage=3'); - this.transitionTo('parent.child', { - queryParams: { index: 2, page: 2 }, - }); - this.assertCurrentPath('/parent/child?page=2&parentPage=2'); + await this.transitionTo('parent.child', { + queryParams: { index: 2, page: 2 }, }); + this.assertCurrentPath('/parent/child?page=2&parentPage=2'); } - ['@test query param matching same property on two controllers use the urlKey higher in the chain']( + async ['@test query param matching same property on two controllers use the urlKey higher in the chain']( assert ) { assert.expect(4); @@ -99,26 +93,25 @@ moduleFor( this.setMappedQPController('parent', 'page', 'parentPage'); this.setMappedQPController('parent.child', 'page', 'childPage'); - return this.setupBase().then(() => { - this.transitionTo('parent.child', { queryParams: { page: 2 } }); - this.assertCurrentPath('/parent/child?parentPage=2'); + await this.setupBase(); + await this.transitionTo('parent.child', { queryParams: { page: 2 } }); + this.assertCurrentPath('/parent/child?parentPage=2'); - this.transitionTo('parent.child', { queryParams: { parentPage: 3 } }); - this.assertCurrentPath('/parent/child?parentPage=3'); + await this.transitionTo('parent.child', { queryParams: { parentPage: 3 } }); + this.assertCurrentPath('/parent/child?parentPage=3'); - this.transitionTo('parent.child', { - queryParams: { childPage: 2, page: 2 }, - }); - this.assertCurrentPath('/parent/child?childPage=2&parentPage=2'); + await this.transitionTo('parent.child', { + queryParams: { childPage: 2, page: 2 }, + }); + this.assertCurrentPath('/parent/child?childPage=2&parentPage=2'); - this.transitionTo('parent.child', { - queryParams: { childPage: 3, parentPage: 4 }, - }); - this.assertCurrentPath('/parent/child?childPage=3&parentPage=4'); + await this.transitionTo('parent.child', { + queryParams: { childPage: 3, parentPage: 4 }, }); + this.assertCurrentPath('/parent/child?childPage=3&parentPage=4'); } - ['@test query params does not error when a query parameter exists for route instances that share a controller']( + async ['@test query params does not error when a query parameter exists for route instances that share a controller']( assert ) { assert.expect(1); @@ -129,10 +122,10 @@ moduleFor( this.add('controller:parent', parentController); this.add('route:parent.child', Route.extend({ controllerName: 'parent' })); - return this.setupBase('/parent').then(() => { - this.transitionTo('parent.child', { queryParams: { page: 2 } }); - this.assertCurrentPath('/parent/child?page=2'); - }); + await this.setupBase('/parent'); + await this.transitionTo('parent.child', { queryParams: { page: 2 } }); + + this.assertCurrentPath('/parent/child?page=2'); } async ['@test query params in the same route hierarchy with the same url key get auto-scoped']( @@ -149,7 +142,7 @@ moduleFor( ); } - ['@test Support shared but overridable mixin pattern'](assert) { + async ['@test Support shared but overridable mixin pattern'](assert) { assert.expect(7); let HasPage = Mixin.create({ @@ -166,22 +159,21 @@ moduleFor( this.add('controller:parent.child', Controller.extend(HasPage)); - return this.setupBase().then(() => { - this.assertCurrentPath('/parent/child'); + await this.setupBase(); + this.assertCurrentPath('/parent/child'); - let parentController = this.getController('parent'); - let parentChildController = this.getController('parent.child'); + let parentController = this.getController('parent'); + let parentChildController = this.getController('parent.child'); - this.setAndFlush(parentChildController, 'page', 2); - this.assertCurrentPath('/parent/child?page=2'); - assert.equal(parentController.get('page'), 1); - assert.equal(parentChildController.get('page'), 2); + await this.setAndFlush(parentChildController, 'page', 2); + this.assertCurrentPath('/parent/child?page=2'); + assert.equal(parentController.get('page'), 1); + assert.equal(parentChildController.get('page'), 2); - this.setAndFlush(parentController, 'page', 2); - this.assertCurrentPath('/parent/child?page=2&yespage=2'); - assert.equal(parentController.get('page'), 2); - assert.equal(parentChildController.get('page'), 2); - }); + await this.setAndFlush(parentController, 'page', 2); + this.assertCurrentPath('/parent/child?page=2&yespage=2'); + assert.equal(parentController.get('page'), 2); + assert.equal(parentChildController.get('page'), 2); } } ); diff --git a/packages/ember/tests/routing/router_service_test/events_test.js b/packages/ember/tests/routing/router_service_test/events_test.js index 8ad646c96f0..d969fa28fbd 100644 --- a/packages/ember/tests/routing/router_service_test/events_test.js +++ b/packages/ember/tests/routing/router_service_test/events_test.js @@ -692,7 +692,7 @@ moduleFor( }); } - '@test willTransition events are deprecated on routes'() { + async '@test willTransition events are deprecated on routes'() { this.add( 'route:application', Route.extend({ @@ -702,12 +702,13 @@ moduleFor( }, }) ); - expectDeprecation(() => { - return this.visit('/'); - }, 'You attempted to listen to the "willTransition" event which is deprecated. Please inject the router service and listen to the "routeWillChange" event.'); + await expectDeprecationAsync( + () => this.visit('/'), + 'You attempted to listen to the "willTransition" event which is deprecated. Please inject the router service and listen to the "routeWillChange" event.' + ); } - '@test didTransition events are deprecated on routes'() { + async '@test didTransition events are deprecated on routes'() { this.add( 'route:application', Route.extend({ @@ -717,9 +718,10 @@ moduleFor( }, }) ); - expectDeprecation(() => { - return this.visit('/'); - }, 'You attempted to listen to the "didTransition" event which is deprecated. Please inject the router service and listen to the "routeDidChange" event.'); + await expectDeprecationAsync( + () => this.visit('/'), + 'You attempted to listen to the "didTransition" event which is deprecated. Please inject the router service and listen to the "routeDidChange" event.' + ); } '@test other events are not deprecated on routes'() { @@ -767,10 +769,11 @@ moduleFor( }; } - '@test willTransition hook is deprecated'() { - expectDeprecation(() => { - return this.visit('/'); - }, 'You attempted to override the "willTransition" method which is deprecated. Please inject the router service and listen to the "routeWillChange" event.'); + async '@test willTransition hook is deprecated'() { + await expectDeprecationAsync( + () => this.visit('/'), + 'You attempted to override the "willTransition" method which is deprecated. Please inject the router service and listen to the "routeWillChange" event.' + ); } } ); @@ -786,10 +789,11 @@ moduleFor( }; } - '@test didTransition hook is deprecated'() { - expectDeprecation(() => { - return this.visit('/'); - }, 'You attempted to override the "didTransition" method which is deprecated. Please inject the router service and listen to the "routeDidChange" event.'); + async '@test didTransition hook is deprecated'() { + await expectDeprecationAsync( + () => this.visit('/'), + 'You attempted to override the "didTransition" method which is deprecated. Please inject the router service and listen to the "routeDidChange" event.' + ); } } ); diff --git a/packages/ember/tests/routing/substates_test.js b/packages/ember/tests/routing/substates_test.js index 43831b45672..303025e751e 100644 --- a/packages/ember/tests/routing/substates_test.js +++ b/packages/ember/tests/routing/substates_test.js @@ -20,6 +20,10 @@ moduleFor( this.addTemplate('index', 'INDEX'); } + visit(...args) { + return runTask(() => super.visit(...args)); + } + getController(name) { return this.applicationInstance.lookup(`controller:${name}`); } @@ -591,7 +595,7 @@ moduleFor( }) ); - let promise = this.visit('/grandma/mom').then(() => { + let promise = runTask(() => this.visit('/grandma/mom')).then(() => { text = this.$('#app').text(); assert.equal(text, 'GRANDMA MOM', `Grandma.mom loaded text is displayed`); @@ -935,7 +939,7 @@ moduleFor( }) ); - let promise = this.visit('/grandma/mom/sally').then(() => { + let promise = runTask(() => this.visit('/grandma/mom/sally')).then(() => { text = this.$('#app').text(); assert.equal(text, 'GRANDMA MOM SALLY', `Sally template displayed`); @@ -986,18 +990,17 @@ moduleFor( }) ); - return this.visit('/grandma/mom/sally').then(() => { - assert.equal(this.currentPath, 'grandma.mom.sally', 'Initial route fully loaded'); + await this.visit('/grandma/mom/sally'); + assert.equal(this.currentPath, 'grandma.mom.sally', 'Initial route fully loaded'); - let promise = this.visit('/grandma/puppies').then(() => { - assert.equal(this.currentPath, 'grandma.puppies', 'Finished transition'); - }); + let promise = runTask(() => this.visit('/grandma/puppies')).then(() => { + assert.equal(this.currentPath, 'grandma.puppies', 'Finished transition'); + }); - assert.equal(this.currentPath, 'grandma.loading', `in pivot route's child loading state`); - deferred.resolve(); + assert.equal(this.currentPath, 'grandma.loading', `in pivot route's child loading state`); + deferred.resolve(); - return promise; - }); + return promise; } async [`@test Error events that aren't bubbled don't throw application assertions`](assert) { @@ -1100,7 +1103,7 @@ moduleFor( }) ); - let promise = this.visit('/grandma').then(() => { + let promise = runTask(() => this.visit('/grandma')).then(() => { assert.equal(this.currentPath, 'memere.index', 'Transition should be complete'); }); let memereController = this.getController('memere'); diff --git a/packages/internal-test-helpers/lib/ember-dev/setup-qunit.ts b/packages/internal-test-helpers/lib/ember-dev/setup-qunit.ts index 460a19bad7c..55fd8b8f387 100644 --- a/packages/internal-test-helpers/lib/ember-dev/setup-qunit.ts +++ b/packages/internal-test-helpers/lib/ember-dev/setup-qunit.ts @@ -10,6 +10,8 @@ import { DebugEnv } from './utils'; import { setupWarningHelpers } from './warning'; declare global { + var Ember: any; + interface Assert { rejects(promise: Promise, expected?: string | RegExp, message?: string): Promise; @@ -49,25 +51,30 @@ export default function setupQUnit({ runningProdBuild }: { runningProdBuild: boo expected?: RegExp | string, message?: string ) { - let threw = false; + let error: Error; + let prevOnError = Ember.onerror; + + Ember.onerror = (e: Error) => { + error = e; + }; try { await promise; } catch (e) { - threw = true; - - QUnit.assert.throws( - () => { - throw e; - }, - expected, - message - ); + error = e; } - if (!threw) { - QUnit.assert.ok(false, `expected an error to be thrown: ${expected}`); - } + QUnit.assert.throws( + () => { + if (error) { + throw error; + } + }, + expected, + message + ); + + Ember.onerror = prevOnError; }; QUnit.assert.throwsAssertion = function( diff --git a/packages/internal-test-helpers/lib/test-cases/abstract-application.js b/packages/internal-test-helpers/lib/test-cases/abstract-application.js index 113397cabb3..0bfbdea3dda 100644 --- a/packages/internal-test-helpers/lib/test-cases/abstract-application.js +++ b/packages/internal-test-helpers/lib/test-cases/abstract-application.js @@ -18,9 +18,7 @@ export default class AbstractApplicationTestCase extends AbstractTestCase { async visit(url, options) { // Create the instance - let instance = await runTask(() => - this._ensureInstance(options).then(instance => instance.visit(url)) - ); + let instance = await this._ensureInstance(options).then(instance => instance.visit(url)); // Await all asynchronous actions await runLoopSettled(); diff --git a/packages/internal-test-helpers/lib/test-cases/application.js b/packages/internal-test-helpers/lib/test-cases/application.js index 941f0d2d724..3dcc5b20e42 100644 --- a/packages/internal-test-helpers/lib/test-cases/application.js +++ b/packages/internal-test-helpers/lib/test-cases/application.js @@ -3,7 +3,7 @@ import Application from '@ember/application'; import { Router } from '@ember/-internals/routing'; import { assign } from '@ember/polyfills'; -import { runTask } from '../run'; +import { runTask, runLoopSettled } from '../run'; export default class ApplicationTestCase extends TestResolverApplicationTestCase { constructor() { @@ -33,9 +33,9 @@ export default class ApplicationTestCase extends TestResolverApplicationTestCase return this.applicationInstance.lookup('router:main'); } - transitionTo() { - return runTask(() => { - return this.appRouter.transitionTo(...arguments); - }); + async transitionTo() { + await this.appRouter.transitionTo(...arguments); + + await runLoopSettled(); } } diff --git a/packages/internal-test-helpers/lib/test-cases/query-param.js b/packages/internal-test-helpers/lib/test-cases/query-param.js index 0d21c0c57bc..808c2f7729a 100644 --- a/packages/internal-test-helpers/lib/test-cases/query-param.js +++ b/packages/internal-test-helpers/lib/test-cases/query-param.js @@ -1,8 +1,8 @@ import Controller from '@ember/controller'; import { NoneLocation } from '@ember/-internals/routing'; -import { run } from '@ember/runloop'; import ApplicationTestCase from './application'; +import { runLoopSettled } from '../run'; export default class QueryParamTestCase extends ApplicationTestCase { constructor() { @@ -68,8 +68,14 @@ export default class QueryParamTestCase extends ApplicationTestCase { }; } - setAndFlush(obj, prop, value) { - return run(obj, 'set', prop, value); + async setAndFlush(obj, prop, value) { + if (typeof prop === 'object') { + obj.setProperties(prop); + } else { + obj.set(prop, value); + } + + await runLoopSettled(); } assertCurrentPath(path, message = `current path equals '${path}'`) {