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}'`) {