diff --git a/package.json b/package.json index 1044aec77c7..878ef7162a7 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,7 @@ "express": "^4.5.0", "finalhandler": "^0.4.0", "github": "^0.2.3", - "glimmer-engine": "tildeio/glimmer#591a188", + "glimmer-engine": "tildeio/glimmer#2c22999", "glob": "^5.0.13", "htmlbars": "0.14.16", "mocha": "^2.4.5", diff --git a/packages/ember-glimmer/lib/components/curly-component.js b/packages/ember-glimmer/lib/components/curly-component.js index 5c487fcbb32..68bf85dd6b0 100644 --- a/packages/ember-glimmer/lib/components/curly-component.js +++ b/packages/ember-glimmer/lib/components/curly-component.js @@ -1,5 +1,6 @@ import { StatementSyntax, ValueReference } from 'glimmer-runtime'; import { AttributeBindingReference, RootReference, applyClassNameBinding } from '../utils/references'; +import { DIRTY_TAG } from '../ember-views/component'; import EmptyObject from 'ember-metal/empty_object'; export class CurlyComponentSyntax extends StatementSyntax { @@ -19,7 +20,7 @@ export class CurlyComponentSyntax extends StatementSyntax { function attrsToProps(keys, attrs) { let merged = new EmptyObject(); - merged.attrs = merged; + merged.attrs = attrs; for (let i = 0, l = keys.length; i < l; i++) { let name = keys[i]; @@ -32,9 +33,10 @@ function attrsToProps(keys, attrs) { } class ComponentStateBucket { - constructor(component) { + constructor(component, args) { this.component = component; this.classRef = null; + this.argsRevision = args.tag.value(); } } @@ -46,17 +48,19 @@ class CurlyComponentManager { let attrs = args.named.value(); let props = attrsToProps(args.named.keys, attrs); + props.renderer = parentView.renderer; + let component = klass.create(props); dynamicScope.view = component; parentView.appendChild(component); - // component.trigger('didInitAttrs', { attrs }); - // component.trigger('didReceiveAttrs', { newAttrs: attrs }); - // component.trigger('willInsertElement'); - // component.trigger('willRender'); + component.trigger('didInitAttrs', { attrs }); + component.trigger('didReceiveAttrs', { newAttrs: attrs }); + component.trigger('willInsertElement'); + component.trigger('willRender'); - let bucket = new ComponentStateBucket(component); + let bucket = new ComponentStateBucket(component, args); if (args.named.has('class')) { bucket.classRef = args.named.get('class'); @@ -99,30 +103,41 @@ class CurlyComponentManager { component._transitionTo('hasElement'); } + getTag({ component }) { + return component[DIRTY_TAG]; + } + didCreate({ component }) { - // component.trigger('didInsertElement'); - // component.trigger('didRender'); + component.trigger('didInsertElement'); + component.trigger('didRender'); component._transitionTo('inDOM'); } - update({ component }, args, dynamicScope) { - let attrs = args.named.value(); - let props = attrsToProps(args.named.keys, attrs); + update(bucket, args, dynamicScope) { + let { component, argsRevision } = bucket; - // let oldAttrs = component.attrs; - // let newAttrs = attrs; + if (!args.tag.validate(argsRevision)) { + bucket.argsRevision = args.tag.value(); - component.setProperties(props); + let attrs = args.named.value(); + let props = attrsToProps(args.named.keys, attrs); + + let oldAttrs = component.attrs; + let newAttrs = attrs; + + component.setProperties(props); + + component.trigger('didUpdateAttrs', { oldAttrs, newAttrs }); + component.trigger('didReceiveAttrs', { oldAttrs, newAttrs }); + } - // component.trigger('didUpdateAttrs', { oldAttrs, newAttrs }); - // component.trigger('didReceiveAttrs', { oldAttrs, newAttrs }); - // component.trigger('willUpdate'); - // component.trigger('willRender'); + component.trigger('willUpdate'); + component.trigger('willRender'); } didUpdate({ component }) { - // component.trigger('didUpdate'); - // component.trigger('didRender'); + component.trigger('didUpdate'); + component.trigger('didRender'); } getDestructor({ component }) { diff --git a/packages/ember-glimmer/lib/components/outlet.js b/packages/ember-glimmer/lib/components/outlet.js index e75124def09..0d22ce8905f 100644 --- a/packages/ember-glimmer/lib/components/outlet.js +++ b/packages/ember-glimmer/lib/components/outlet.js @@ -100,6 +100,10 @@ class AbstractOutletComponentManager { return new RootReference(state.render.controller); } + getTag(state) { + return null; + } + getDestructor(state) { return null; } diff --git a/packages/ember-glimmer/lib/ember-metal-views/index.js b/packages/ember-glimmer/lib/ember-metal-views/index.js index cc770c80e74..59832b7377c 100644 --- a/packages/ember-glimmer/lib/ember-metal-views/index.js +++ b/packages/ember-glimmer/lib/ember-metal-views/index.js @@ -61,8 +61,15 @@ class Renderer { } remove(view) { + view.trigger('willDestroyElement'); view._transitionTo('destroying'); - view['_renderResult'].destroy(); + + let { _renderResult } = view; + + if (_renderResult) { + _renderResult.destroy(); + } + view.destroy(); } diff --git a/packages/ember-glimmer/lib/ember-views/component.js b/packages/ember-glimmer/lib/ember-views/component.js index 12e78f068d1..2766d0458e4 100644 --- a/packages/ember-glimmer/lib/ember-views/component.js +++ b/packages/ember-glimmer/lib/ember-views/component.js @@ -6,6 +6,10 @@ import InstrumentationSupport from 'ember-views/mixins/instrumentation_support'; import AriaRoleSupport from 'ember-views/mixins/aria_role_support'; import ViewMixin from 'ember-views/mixins/view_support'; import EmberView from 'ember-views/views/view'; +import symbol from 'ember-metal/symbol'; +import { DirtyableTag } from 'glimmer-reference'; + +export const DIRTY_TAG = symbol('DIRTY_TAG'); export default CoreView.extend( ChildViewsSupport, @@ -22,6 +26,12 @@ export default CoreView.extend( init() { this._super(...arguments); this._viewRegistry = this._viewRegistry || EmberView.views; + this[DIRTY_TAG] = new DirtyableTag(); + }, + + rerender() { + this[DIRTY_TAG].dirty(); + this._super(); }, __defineNonEnumerable(property) { diff --git a/packages/ember-glimmer/lib/environment.js b/packages/ember-glimmer/lib/environment.js index cc74bea468d..57508277265 100644 --- a/packages/ember-glimmer/lib/environment.js +++ b/packages/ember-glimmer/lib/environment.js @@ -26,8 +26,9 @@ import { default as get } from './helpers/get'; import { default as hash } from './helpers/hash'; import { default as loc } from './helpers/loc'; import { default as log } from './helpers/log'; -import { default as classHelper } from './helpers/-class'; +import { default as readonly } from './helpers/readonly'; import { default as unbound } from './helpers/unbound'; +import { default as classHelper } from './helpers/-class'; import { OWNER } from 'container/owner'; const builtInHelpers = { @@ -38,6 +39,7 @@ const builtInHelpers = { hash, loc, log, + readonly, unbound, '-class': classHelper }; @@ -162,4 +164,13 @@ export default class Environment extends GlimmerEnvironment { let keyPath = args.named.get('key').value(); return createIterable(ref, keyPath); } + + didCreate(component, manager) { + this.createdComponents.unshift(component); + this.createdManagers.unshift(manager); + } + + didDestroy(destroyable) { + destroyable.destroy(); + } } diff --git a/packages/ember-glimmer/lib/helpers/readonly.js b/packages/ember-glimmer/lib/helpers/readonly.js new file mode 100644 index 00000000000..dca2cdbf426 --- /dev/null +++ b/packages/ember-glimmer/lib/helpers/readonly.js @@ -0,0 +1,7 @@ +import { helper } from '../helper'; + +function readonly(args) { + return args[0]; +} + +export default helper(readonly); diff --git a/packages/ember-glimmer/tests/integration/components/life-cycle-test.js b/packages/ember-glimmer/tests/integration/components/life-cycle-test.js new file mode 100644 index 00000000000..1e5177b8859 --- /dev/null +++ b/packages/ember-glimmer/tests/integration/components/life-cycle-test.js @@ -0,0 +1,543 @@ +import { set } from 'ember-metal/property_set'; +import { Component } from '../../utils/helpers'; +import { strip } from '../../utils/abstract-test-case'; +import { moduleFor, RenderingTest } from '../../utils/test-case'; + +class LifeCycleHooksTest extends RenderingTest { + constructor() { + super(); + this.hooks = []; + this.components = {}; + } + + /* abstract */ + get ComponentClass() { + throw new Error('Not implemented: `ComponentClass`'); + } + + /* abstract */ + invocationFor(name, namedArgs = {}) { + throw new Error('Not implemented: `invocationFor`'); + } + + /* abstract */ + attrFor(name) { + throw new Error('Not implemented: `attrFor`'); + } + + get boundHelpers() { + return { + invoke: bind(this.invocationFor, this), + attr: bind(this.attrFor, this) + }; + } + + registerComponent(name, { template = null }) { + let pushComponent = (instance) => { + this.components[name] = instance; + }; + + let pushHook = (hookName, args) => { + this.hooks.push(hook(name, hookName, args)); + }; + + let ComponentClass = this.ComponentClass.extend({ + init() { + expectDeprecation(() => { this._super(...arguments); }, + /didInitAttrs called/); + + pushHook('init'); + pushComponent(this); + }, + + didInitAttrs(options) { + pushHook('didInitAttrs', options); + }, + + didUpdateAttrs(options) { + pushHook('didUpdateAttrs', options); + }, + + willUpdate(options) { + pushHook('willUpdate', options); + }, + + didReceiveAttrs(options) { + pushHook('didReceiveAttrs', options); + }, + + willRender() { + pushHook('willRender'); + }, + + didRender() { + pushHook('didRender'); + }, + + didInsertElement() { + pushHook('didInsertElement'); + }, + + didUpdate(options) { + pushHook('didUpdate', options); + } + }); + + super.registerComponent(name, { ComponentClass, template }); + } + + assertHooks(label, ...rawHooks) { + let hooks = rawHooks.map(raw => hook(...raw)); + this.assert.deepEqual(json(this.hooks), json(hooks), label); + this.hooks = []; + } + + ['@test lifecycle hooks are invoked in a predictable order']() { + let { attr, invoke } = this.boundHelpers; + + this.registerComponent('the-top', { template: strip` +
+ Twitter: {{${attr('twitter')}}}| + ${invoke('the-middle', { name: string('Tom Dale') })} +
` + }); + + this.registerComponent('the-middle', { template: strip` +
+ Name: {{${attr('name')}}}| + ${invoke('the-bottom', { website: string('tomdale.net') })} +
` + }); + + this.registerComponent('the-bottom', { template: strip` +
+ Website: {{${attr('website')}}} +
` + }); + + this.render(invoke('the-top', { twitter: expr('twitter') }), { twitter: '@tomdale' }); + + this.assertText('Twitter: @tomdale|Name: Tom Dale|Website: tomdale.net'); + + let topAttrs = { twitter: '@tomdale' }; + let middleAttrs = { name: 'Tom Dale' }; + let bottomAttrs = { website: 'tomdale.net' }; + + this.assertHooks( + + 'after initial render', + + // Sync hooks + + ['the-top', 'init'], + ['the-top', 'didInitAttrs', { attrs: topAttrs }], + ['the-top', 'didReceiveAttrs', { newAttrs: topAttrs }], + ['the-top', 'willRender'], + + ['the-middle', 'init'], + ['the-middle', 'didInitAttrs', { attrs: middleAttrs }], + ['the-middle', 'didReceiveAttrs', { newAttrs: middleAttrs }], + ['the-middle', 'willRender'], + + ['the-bottom', 'init'], + ['the-bottom', 'didInitAttrs', { attrs: bottomAttrs }], + ['the-bottom', 'didReceiveAttrs', { newAttrs: bottomAttrs }], + ['the-bottom', 'willRender'], + + // Async hooks + + ['the-bottom', 'didInsertElement'], + ['the-bottom', 'didRender'], + + ['the-middle', 'didInsertElement'], + ['the-middle', 'didRender'], + + ['the-top', 'didInsertElement'], + ['the-top', 'didRender'] + + ); + + this.runTask(() => this.components['the-bottom'].rerender()); + + this.assertText('Twitter: @tomdale|Name: Tom Dale|Website: tomdale.net'); + + this.assertHooks( + + 'after no-op rerender (bottom)', + + // Sync hooks + + ['the-bottom', 'willUpdate'], + ['the-bottom', 'willRender'], + + // Async hooks + + ['the-bottom', 'didUpdate'], + ['the-bottom', 'didRender'] + + ); + + this.runTask(() => this.components['the-middle'].rerender()); + + this.assertText('Twitter: @tomdale|Name: Tom Dale|Website: tomdale.net'); + + bottomAttrs = { oldAttrs: { website: 'tomdale.net' }, newAttrs: { website: 'tomdale.net' } }; + + // The original implementation of the hooks in HTMLBars does a + // deeper walk than necessary (using the AlwaysDirty validator), + // resulting in executing the experimental "new hooks" too often. + // + // In particular, hooks were executed downstream from the original + // call to `rerender()` even if the rerendering component did not + // use `this.set()` to update the arguments of downstream components. + // + // Because Glimmer uses a pull-based model instead of a blunt + // push-based model, we can avoid a deeper traversal than is + // necessary. + + if (this.isHTMLBars) { + this.assertHooks( + + 'after no-op rerender (middle)', + + // Sync hooks + + ['the-middle', 'willUpdate'], + ['the-middle', 'willRender'], + + ['the-bottom', 'didUpdateAttrs', bottomAttrs], + ['the-bottom', 'didReceiveAttrs', bottomAttrs], + + ['the-bottom', 'willUpdate'], + ['the-bottom', 'willRender'], + + // Async hooks + + ['the-bottom', 'didUpdate'], + ['the-bottom', 'didRender'], + + ['the-middle', 'didUpdate'], + ['the-middle', 'didRender'] + + ); + } else { + this.assertHooks( + + 'after no-op rerender (middle)', + + // Sync hooks + + ['the-middle', 'willUpdate'], + ['the-middle', 'willRender'], + + // Async hooks + + ['the-middle', 'didUpdate'], + ['the-middle', 'didRender'] + + ); + } + + this.runTask(() => this.components['the-top'].rerender()); + + this.assertText('Twitter: @tomdale|Name: Tom Dale|Website: tomdale.net'); + + middleAttrs = { oldAttrs: { name: 'Tom Dale' }, newAttrs: { name: 'Tom Dale' } }; + + + if (this.isHTMLBars) { + this.assertHooks( + + 'after no-op rerender (top)', + + // Sync hooks + + ['the-top', 'willUpdate'], + ['the-top', 'willRender'], + + ['the-middle', 'didUpdateAttrs', middleAttrs], + ['the-middle', 'didReceiveAttrs', middleAttrs], + + ['the-middle', 'willUpdate'], + ['the-middle', 'willRender'], + + ['the-bottom', 'didUpdateAttrs', bottomAttrs], + ['the-bottom', 'didReceiveAttrs', bottomAttrs], + + ['the-bottom', 'willUpdate'], + ['the-bottom', 'willRender'], + + // Async hooks + + ['the-bottom', 'didUpdate'], + ['the-bottom', 'didRender'], + + ['the-middle', 'didUpdate'], + ['the-middle', 'didRender'], + + ['the-top', 'didUpdate'], + ['the-top', 'didRender'] + + ); + } else { + this.assertHooks( + + 'after no-op rerender (top)', + + // Sync hooks + + ['the-top', 'willUpdate'], + ['the-top', 'willRender'], + + // Async hooks + + ['the-top', 'didUpdate'], + ['the-top', 'didRender'] + + ); + } + + this.runTask(() => set(this.context, 'twitter', '@horsetomdale')); + + this.assertText('Twitter: @horsetomdale|Name: Tom Dale|Website: tomdale.net'); + + // Because the `twitter` attr is only used by the topmost component, + // and not passed down, we do not expect to see lifecycle hooks + // called for child components. If the `didReceiveAttrs` hook used + // the new attribute to rerender itself imperatively, that would result + // in lifecycle hooks being invoked for the child. + + topAttrs = { oldAttrs: { twitter: '@tomdale' }, newAttrs: { twitter: '@horsetomdale' } }; + + this.assertHooks( + + 'after update', + + // Sync hooks + + ['the-top', 'didUpdateAttrs', topAttrs], + ['the-top', 'didReceiveAttrs', topAttrs], + + ['the-top', 'willUpdate'], + ['the-top', 'willRender'], + + // Async hooks + + ['the-top', 'didUpdate'], + ['the-top', 'didRender'] + + ); + } + + ['@test passing values through attrs causes lifecycle hooks to fire if the attribute values have changed']() { + let { attr, invoke } = this.boundHelpers; + + this.registerComponent('the-top', { template: strip` +
+ Top: ${invoke('the-middle', { twitterTop: expr(attr('twitter')) })} +
` + }); + + this.registerComponent('the-middle', { template: strip` +
+ Middle: ${invoke('the-bottom', { twitterMiddle: expr(attr('twitterTop')) })} +
` + }); + + this.registerComponent('the-bottom', { template: strip` +
+ Bottom: {{${attr('twitterMiddle')}}} +
` + }); + + this.render(invoke('the-top', { twitter: expr('twitter') }), { twitter: '@tomdale' }); + + this.assertText('Top: Middle: Bottom: @tomdale'); + + let topAttrs = { twitter: '@tomdale' }; + let middleAttrs = { twitterTop: '@tomdale' }; + let bottomAttrs = { twitterMiddle: '@tomdale' }; + + this.assertHooks( + + 'after initial render', + + // Sync hooks + + ['the-top', 'init'], + ['the-top', 'didInitAttrs', { attrs: topAttrs }], + ['the-top', 'didReceiveAttrs', { newAttrs: topAttrs }], + ['the-top', 'willRender'], + + ['the-middle', 'init'], + ['the-middle', 'didInitAttrs', { attrs: middleAttrs }], + ['the-middle', 'didReceiveAttrs', { newAttrs: middleAttrs }], + ['the-middle', 'willRender'], + + ['the-bottom', 'init'], + ['the-bottom', 'didInitAttrs', { attrs: bottomAttrs }], + ['the-bottom', 'didReceiveAttrs', { newAttrs: bottomAttrs }], + ['the-bottom', 'willRender'], + + // Async hooks + + ['the-bottom', 'didInsertElement'], + ['the-bottom', 'didRender'], + + ['the-middle', 'didInsertElement'], + ['the-middle', 'didRender'], + + ['the-top', 'didInsertElement'], + ['the-top', 'didRender'] + + ); + + this.runTask(() => set(this.context, 'twitter', '@horsetomdale')); + + this.assertText('Top: Middle: Bottom: @horsetomdale'); + + // Because the `twitter` attr is used by the all of the components, + // the lifecycle hooks are invoked for all components. + + topAttrs = { oldAttrs: { twitter: '@tomdale' }, newAttrs: { twitter: '@horsetomdale' } }; + middleAttrs = { oldAttrs: { twitterTop: '@tomdale' }, newAttrs: { twitterTop: '@horsetomdale' } }; + bottomAttrs = { oldAttrs: { twitterMiddle: '@tomdale' }, newAttrs: { twitterMiddle: '@horsetomdale' } }; + + this.assertHooks( + + 'after updating (root)', + + // Sync hooks + + ['the-top', 'didUpdateAttrs', topAttrs], + ['the-top', 'didReceiveAttrs', topAttrs], + + ['the-top', 'willUpdate'], + ['the-top', 'willRender'], + + ['the-middle', 'didUpdateAttrs', middleAttrs], + ['the-middle', 'didReceiveAttrs', middleAttrs], + + ['the-middle', 'willUpdate'], + ['the-middle', 'willRender'], + + ['the-bottom', 'didUpdateAttrs', bottomAttrs], + ['the-bottom', 'didReceiveAttrs', bottomAttrs], + + ['the-bottom', 'willUpdate'], + ['the-bottom', 'willRender'], + + // Async hooks + + ['the-bottom', 'didUpdate'], + ['the-bottom', 'didRender'], + + ['the-middle', 'didUpdate'], + ['the-middle', 'didRender'], + + ['the-top', 'didUpdate'], + ['the-top', 'didRender'] + + ); + + this.runTask(() => this.rerender()); + + this.assertText('Top: Middle: Bottom: @horsetomdale'); + + // In this case, because the attrs are passed down, all child components are invoked. + + topAttrs = { oldAttrs: { twitter: '@horsetomdale' }, newAttrs: { twitter: '@horsetomdale' } }; + middleAttrs = { oldAttrs: { twitterTop: '@horsetomdale' }, newAttrs: { twitterTop: '@horsetomdale' } }; + bottomAttrs = { oldAttrs: { twitterMiddle: '@horsetomdale' }, newAttrs: { twitterMiddle: '@horsetomdale' } }; + + if (this.isHTMLBars) { + this.assertHooks( + + 'after no-op rernder (root)', + + // Sync hooks + + ['the-top', 'didUpdateAttrs', topAttrs], + ['the-top', 'didReceiveAttrs', topAttrs], + + ['the-top', 'willUpdate'], + ['the-top', 'willRender'], + + ['the-middle', 'didUpdateAttrs', middleAttrs], + ['the-middle', 'didReceiveAttrs', middleAttrs], + + ['the-middle', 'willUpdate'], + ['the-middle', 'willRender'], + + ['the-bottom', 'didUpdateAttrs', bottomAttrs], + ['the-bottom', 'didReceiveAttrs', bottomAttrs], + + ['the-bottom', 'willUpdate'], + ['the-bottom', 'willRender'], + + // Async hooks + + ['the-bottom', 'didUpdate'], + ['the-bottom', 'didRender'], + + ['the-middle', 'didUpdate'], + ['the-middle', 'didRender'], + + ['the-top', 'didUpdate'], + ['the-top', 'didRender'] + + ); + } else { + this.assertHooks('after no-op rernder (root)'); + } + } + +} + +moduleFor('Components test: lifecycle hooks (curly components)', class extends LifeCycleHooksTest { + + get ComponentClass() { + return Component; + } + + invocationFor(name, namedArgs = {}) { + let attrs = Object.keys(namedArgs).map(k => `${k}=${this.val(namedArgs[k])}`).join(' '); + return `{{${name} ${attrs}}}`; + } + + attrFor(name) { + return `${name}`; + } + + /* private */ + val(value) { + if (value.isString) { + return JSON.stringify(value.value); + } else if (value.isExpr) { + return `(readonly ${value.value})`; + } else { + throw new Error(`Unknown value: ${value}`); + } + } + +}); + +function bind(func, thisArg) { + return (...args) => func.apply(thisArg, args); +} + +function string(value) { + return { isString: true, value }; +} + +function expr(value) { + return { isExpr: true, value }; +} + +function hook(name, hook, args) { + return { name, hook, args }; +} + +function json(serializable) { + return JSON.parse(JSON.stringify(serializable)); +} diff --git a/packages/ember-glimmer/tests/integration/components/will-destroy-element-hook-test.js b/packages/ember-glimmer/tests/integration/components/will-destroy-element-hook-test.js new file mode 100644 index 00000000000..85268e06298 --- /dev/null +++ b/packages/ember-glimmer/tests/integration/components/will-destroy-element-hook-test.js @@ -0,0 +1,34 @@ +import { set } from 'ember-metal/property_set'; +import { Component } from '../../utils/helpers'; +import { moduleFor, RenderingTest } from '../../utils/test-case'; + +moduleFor('Component willDestroyElement hook', class extends RenderingTest { + ['@test it calls willDestroyElement when removed by if'](assert) { + let didInsertElementCount = 0; + let willDestroyElementCount = 0; + let FooBarComponent = Component.extend({ + didInsertElement() { + didInsertElementCount++; + assert.notEqual(this.element.parentNode, null, 'precond component is in DOM'); + }, + willDestroyElement() { + willDestroyElementCount++; + assert.notEqual(this.element.parentNode, null, 'has not been removed from DOM yet'); + } + }); + + this.registerComponent('foo-bar', { ComponentClass: FooBarComponent, template: 'hello' }); + + this.render('{{#if switch}}{{foo-bar}}{{/if}}', { switch: true }); + + assert.equal(didInsertElementCount, 1, 'didInsertElement was called once'); + + this.assertComponentElement(this.firstChild, { content: 'hello' }); + + this.runTask(() => set(this.context, 'switch', false)); + + assert.equal(willDestroyElementCount, 1, 'willDestroyElement was called once'); + + this.assertText(''); + } +}); diff --git a/packages/ember-glimmer/tests/utils/abstract-test-case.js b/packages/ember-glimmer/tests/utils/abstract-test-case.js index a5bc4ea1d1c..e2251e38812 100644 --- a/packages/ember-glimmer/tests/utils/abstract-test-case.js +++ b/packages/ember-glimmer/tests/utils/abstract-test-case.js @@ -98,6 +98,14 @@ export class TestCase { this.assert = QUnit.config.current.assert; } + get isHTMLBars() { + return packageName === 'htmlbars'; + } + + get isGlimmer() { + return packageName === 'glimmer'; + } + teardown() {} // The following methods require `this.element` to work @@ -390,6 +398,10 @@ export class RenderingTest extends TestCase { } } -export function strip([str]) { +export function strip([...strings], ...values) { + let str = strings.map((string, index) => { + let interpolated = values[index]; + return string + (interpolated !== undefined ? interpolated : ''); + }).join(''); return str.split('\n').map(s => s.trim()).join(''); } diff --git a/packages/ember-glimmer/tests/utils/test-case.js b/packages/ember-glimmer/tests/utils/test-case.js index 44429eb99ef..52e27840805 100644 --- a/packages/ember-glimmer/tests/utils/test-case.js +++ b/packages/ember-glimmer/tests/utils/test-case.js @@ -26,6 +26,11 @@ export class RenderingTest extends AbstractRenderingTest { owner.registerOptionsForType('component', { singleton: false }); } + render(...args) { + super.render(...args); + this.renderer._root = this.component; + } + runTask(callback) { super.runTask(() => { callback(); diff --git a/packages/ember-htmlbars/tests/integration/component_lifecycle_test.js b/packages/ember-htmlbars/tests/integration/component_lifecycle_test.js index c8a0734ba35..4b8c6e38df2 100644 --- a/packages/ember-htmlbars/tests/integration/component_lifecycle_test.js +++ b/packages/ember-htmlbars/tests/integration/component_lifecycle_test.js @@ -316,7 +316,6 @@ styles.forEach(style => { // Because the `twitter` attr is used by the all of the components, // the lifecycle hooks are invoked for all components. - topAttrs = { oldAttrs: { twitter: '@tomdale' }, newAttrs: { twitter: '@hipstertomdale' } }; middleAttrs = { oldAttrs: { twitterTop: '@tomdale' }, newAttrs: { twitterTop: '@hipstertomdale' } }; bottomAttrs = { oldAttrs: { twitterMiddle: '@tomdale' }, newAttrs: { twitterMiddle: '@hipstertomdale' } }; diff --git a/packages/ember-htmlbars/tests/integration/will-destroy-element-hook-test.js b/packages/ember-htmlbars/tests/integration/will-destroy-element-hook-test.js deleted file mode 100644 index 6c720a712e3..00000000000 --- a/packages/ember-htmlbars/tests/integration/will-destroy-element-hook-test.js +++ /dev/null @@ -1,70 +0,0 @@ -import run from 'ember-metal/run_loop'; -import Component from 'ember-views/components/component'; -import compile from 'ember-template-compiler/system/compile'; -import { runAppend, runDestroy } from 'ember-runtime/tests/utils'; - -import { set } from 'ember-metal/property_set'; - -import { registerKeyword, resetKeyword } from 'ember-htmlbars/tests/utils'; -import viewKeyword from 'ember-htmlbars/keywords/view'; - -var component, originalViewKeyword; - -import isEnabled from 'ember-metal/features'; -if (!isEnabled('ember-glimmer')) { - // jscs:disable - -QUnit.module('ember-htmlbars: destroy-element-hook tests', { - setup() { - originalViewKeyword = registerKeyword('view', viewKeyword); - }, - teardown() { - runDestroy(component); - resetKeyword('view', originalViewKeyword); - } -}); - -QUnit.test('willDestroyElement is only called once when a component leaves scope', function(assert) { - var innerChild, innerChildDestroyed; - - component = Component.create({ - switch: true, - - layout: compile(` - {{~#if switch~}} - {{~#view innerChild}}Truthy{{/view~}} - {{~/if~}} - `), - - innerChild: Component.extend({ - init() { - this._super(...arguments); - innerChild = this; - }, - - willDestroyElement() { - if (innerChildDestroyed) { - throw new Error('willDestroyElement has already been called!!'); - } else { - innerChildDestroyed = true; - } - } - }) - }); - - runAppend(component); - - assert.equal(component.$().text(), 'Truthy', 'precond - truthy template is displayed'); - assert.equal(component.get('childViews.length'), 1); - - run(function() { - set(component, 'switch', false); - }); - - run(function() { - assert.equal(innerChild.get('isDestroyed'), true, 'the innerChild has been destroyed'); - assert.equal(component.$().text(), '', 'truthy template is removed'); - }); -}); - -}