}
- ): RendererRoot {
- const root = new ComponentRootState(this.state, component, {
- args: options.args,
- into: intoTarget(options.into),
+ registerDestructor(glimmerResult, () => {
+ if (LAST_RENDER_INTO.get(element)?.glimmerResult === glimmerResult) {
+ LAST_RENDER_INTO.delete(element);
+ }
});
- return this.state.renderRoot(root, this);
}
- rerender(): void {
- this.state.scheduleRevalidate(this);
- }
+ return result;
}
diff --git a/packages/@ember/-internals/glimmer/lib/renderer.ts b/packages/@ember/-internals/glimmer/lib/renderer.ts
index 88166168d10..3a9310e9c2c 100644
--- a/packages/@ember/-internals/glimmer/lib/renderer.ts
+++ b/packages/@ember/-internals/glimmer/lib/renderer.ts
@@ -235,7 +235,7 @@ export class Renderer extends BaseRenderer {
dynamicScope,
this.state.builder
);
- this.state.renderRoot(rootState, this);
+ this.state.renderRoot(rootState);
}
cleanupRootFor(component: ClassicComponent): void {
diff --git a/packages/@ember/-internals/glimmer/tests/integration/application/debug-render-tree-test.ts b/packages/@ember/-internals/glimmer/tests/integration/application/debug-render-tree-test.ts
index 0bbf0471c5d..017b56bf9f0 100644
--- a/packages/@ember/-internals/glimmer/tests/integration/application/debug-render-tree-test.ts
+++ b/packages/@ember/-internals/glimmer/tests/integration/application/debug-render-tree-test.ts
@@ -16,7 +16,7 @@ import type { EngineInstanceOptions } from '@ember/engine/instance';
import type EngineInstance from '@ember/engine/instance';
import type { CapturedRenderNode } from '@glimmer/interfaces';
import { componentCapabilities, setComponentTemplate } from '@glimmer/manager';
-import { templateOnlyComponent } from '@glimmer/runtime';
+import templateOnlyComponent from '@ember/component/template-only';
import type { SimpleElement, SimpleNode } from '@simple-dom/interface';
import { precompileTemplate } from '@ember/template-compilation';
import { runTask } from 'internal-test-helpers/lib/run';
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 409ba12fe02..53e2a12d0db 100644
--- a/packages/@ember/-internals/glimmer/tests/integration/application/engine-test.js
+++ b/packages/@ember/-internals/glimmer/tests/integration/application/engine-test.js
@@ -14,7 +14,7 @@ import { next } from '@ember/runloop';
import { precompileTemplate } from '@ember/template-compilation';
import { setComponentTemplate } from '@glimmer/manager';
-import { templateOnlyComponent } from '@glimmer/runtime';
+import templateOnlyComponent from '@ember/component/template-only';
moduleFor(
'Application test: engine rendering',
diff --git a/packages/@ember/-internals/glimmer/tests/integration/components/angle-bracket-invocation-test.js b/packages/@ember/-internals/glimmer/tests/integration/components/angle-bracket-invocation-test.js
index 8ef0e73300a..cfd7fc0898a 100644
--- a/packages/@ember/-internals/glimmer/tests/integration/components/angle-bracket-invocation-test.js
+++ b/packages/@ember/-internals/glimmer/tests/integration/components/angle-bracket-invocation-test.js
@@ -6,7 +6,7 @@ import { set, setProperties } from '@ember/object';
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
-import { Component as EmberComponent } from '../../utils/helpers';
+import PositionalComponent from '../../utils/positional-component';
import { template } from '@ember/template-compiler/runtime';
import templateOnly from '@ember/component/template-only';
import { precompileTemplate } from '@ember/template-compilation';
@@ -142,55 +142,47 @@ moduleFor(
this.assertElement(this.firstChild, { tagName: 'div', content: '' });
}
- '@test it can have a custom id and it is not bound'() {
+ '@test it renders passed named arguments'() {
this.owner.register(
'component:foo-bar',
- setComponentTemplate(
- precompileTemplate('{{this.id}} {{this.elementId}}'),
- class extends EmberComponent {}
- )
+ setComponentTemplate(precompileTemplate('{{@foo}}'), class extends Component {})
);
- this.render(' ', {
- customId: 'bizz',
+ this.render(' ', {
+ model: {
+ bar: 'Hola',
+ },
});
- this.assertComponentElement(this.firstChild, {
- tagName: 'div',
- attrs: { id: 'bizz' },
- content: 'bizz bizz',
- });
+ this.assertText('Hola');
- this.assertStableRerender();
+ runTask(() => this.rerender());
- runTask(() => set(this.context, 'customId', 'bar'));
+ this.assertText('Hola');
- this.assertComponentElement(this.firstChild, {
- tagName: 'div',
- attrs: { id: 'bizz' },
- content: 'bar bizz',
- });
+ runTask(() => this.context.set('model.bar', 'Hello'));
- runTask(() => set(this.context, 'customId', 'bizz'));
+ this.assertText('Hello');
- this.assertComponentElement(this.firstChild, {
- tagName: 'div',
- attrs: { id: 'bizz' },
- content: 'bizz bizz',
- });
+ runTask(() => this.context.set('model', { bar: 'Hola' }));
+
+ this.assertText('Hola');
}
'@test it can have a custom id attribute and it is bound'() {
this.owner.register(
'component:foo-bar',
- setComponentTemplate(precompileTemplate('hello'), class extends EmberComponent {})
+ setComponentTemplate(
+ precompileTemplate('hello
'),
+ class extends Component {}
+ )
);
this.render(' ', {
customId: 'bizz',
});
- this.assertComponentElement(this.firstChild, {
+ this.assertElement(this.firstChild, {
tagName: 'div',
attrs: { id: 'bizz' },
content: 'hello',
@@ -200,7 +192,7 @@ moduleFor(
runTask(() => set(this.context, 'customId', 'bar'));
- this.assertComponentElement(this.firstChild, {
+ this.assertElement(this.firstChild, {
tagName: 'div',
attrs: { id: 'bar' },
content: 'hello',
@@ -208,282 +200,17 @@ moduleFor(
runTask(() => set(this.context, 'customId', 'bizz'));
- this.assertComponentElement(this.firstChild, {
+ this.assertElement(this.firstChild, {
tagName: 'div',
attrs: { id: 'bizz' },
content: 'hello',
});
}
- '@test it can have a custom tagName'() {
- this.owner.register(
- 'component:foo-bar',
- setComponentTemplate(
- precompileTemplate('hello'),
- class extends EmberComponent {
- tagName = 'foo-bar';
- }
- )
- );
-
- this.render(' ');
-
- this.assertComponentElement(this.firstChild, {
- tagName: 'foo-bar',
- content: 'hello',
- });
-
- runTask(() => this.rerender());
-
- this.assertComponentElement(this.firstChild, {
- tagName: 'foo-bar',
- content: 'hello',
- });
- }
-
- '@test it can have a custom tagName from the invocation'() {
- this.owner.register(
- 'component:foo-bar',
- setComponentTemplate(precompileTemplate('hello'), class extends EmberComponent {})
- );
-
- this.render(' ');
-
- this.assertComponentElement(this.firstChild, {
- tagName: 'foo-bar',
- content: 'hello',
- });
-
- runTask(() => this.rerender());
-
- this.assertComponentElement(this.firstChild, {
- tagName: 'foo-bar',
- content: 'hello',
- });
- }
-
- '@test it can have custom classNames'() {
- this.owner.register(
- 'component:foo-bar',
- setComponentTemplate(
- precompileTemplate('hello'),
- class extends EmberComponent {
- classNames = ['foo', 'bar'];
- }
- )
- );
-
- this.render(' ');
-
- this.assertComponentElement(this.firstChild, {
- tagName: 'div',
- attrs: { class: classes('ember-view foo bar') },
- content: 'hello',
- });
-
- runTask(() => this.rerender());
-
- this.assertComponentElement(this.firstChild, {
- tagName: 'div',
- attrs: { class: classes('ember-view foo bar') },
- content: 'hello',
- });
- }
-
- '@test class property on components can be dynamic'() {
- this.owner.register(
- 'component:foo-bar',
- setComponentTemplate(precompileTemplate('hello'), class extends EmberComponent {})
- );
-
- this.render(' ', {
- fooBar: true,
- });
-
- this.assertComponentElement(this.firstChild, {
- content: 'hello',
- attrs: { class: classes('ember-view foo-bar') },
- });
-
- runTask(() => this.rerender());
-
- this.assertComponentElement(this.firstChild, {
- content: 'hello',
- attrs: { class: classes('ember-view foo-bar') },
- });
-
- runTask(() => set(this.context, 'fooBar', false));
-
- this.assertComponentElement(this.firstChild, {
- content: 'hello',
- attrs: { class: classes('ember-view') },
- });
-
- runTask(() => set(this.context, 'fooBar', true));
-
- this.assertComponentElement(this.firstChild, {
- content: 'hello',
- attrs: { class: classes('ember-view foo-bar') },
- });
- }
-
- '@test it can set custom classNames from the invocation'() {
- let FooBarComponent = class extends EmberComponent {
- classNames = ['foo'];
- };
-
- this.owner.register(
- 'component:foo-bar',
- setComponentTemplate(precompileTemplate('hello'), FooBarComponent)
- );
-
- this.render(strip`
-
-
-
- `);
-
- this.assertComponentElement(this.nthChild(0), {
- tagName: 'div',
- attrs: { class: classes('ember-view foo bar baz') },
- content: 'hello',
- });
- this.assertComponentElement(this.nthChild(1), {
- tagName: 'div',
- attrs: { class: classes('ember-view foo bar baz') },
- content: 'hello',
- });
- this.assertComponentElement(this.nthChild(2), {
- tagName: 'div',
- attrs: { class: classes('ember-view foo') },
- content: 'hello',
- });
-
- runTask(() => this.rerender());
-
- this.assertComponentElement(this.nthChild(0), {
- tagName: 'div',
- attrs: { class: classes('ember-view foo bar baz') },
- content: 'hello',
- });
- this.assertComponentElement(this.nthChild(1), {
- tagName: 'div',
- attrs: { class: classes('ember-view foo bar baz') },
- content: 'hello',
- });
- this.assertComponentElement(this.nthChild(2), {
- tagName: 'div',
- attrs: { class: classes('ember-view foo') },
- content: 'hello',
- });
- }
-
- '@test it has an element'() {
- let instance;
-
- let FooBarComponent = class extends EmberComponent {
- init() {
- super.init();
- instance = this;
- }
- };
-
- this.owner.register(
- 'component:foo-bar',
- setComponentTemplate(precompileTemplate('hello'), FooBarComponent)
- );
-
- this.render(' ');
-
- let element1 = instance.element;
-
- this.assertComponentElement(element1, { content: 'hello' });
-
- runTask(() => this.rerender());
-
- let element2 = instance.element;
-
- this.assertComponentElement(element2, { content: 'hello' });
-
- this.assertSameNode(element2, element1);
- }
-
- '@test it has the right parentView and childViews'(assert) {
- let fooBarInstance, fooBarBazInstance;
-
- let FooBarComponent = class extends EmberComponent {
- init() {
- super.init();
- fooBarInstance = this;
- }
- };
-
- let FooBarBazComponent = class extends EmberComponent {
- init() {
- super.init();
- fooBarBazInstance = this;
- }
- };
-
- this.owner.register(
- 'component:foo-bar',
- setComponentTemplate(precompileTemplate('foo-bar {{foo-bar-baz}}'), FooBarComponent)
- );
- this.owner.register(
- 'component:foo-bar-baz',
- setComponentTemplate(precompileTemplate('foo-bar-baz'), FooBarBazComponent)
- );
-
- this.render(' ');
- this.assertText('foo-bar foo-bar-baz');
-
- assert.equal(fooBarInstance.parentView, this.component);
- assert.equal(fooBarBazInstance.parentView, fooBarInstance);
-
- assert.deepEqual(this.component.childViews, [fooBarInstance]);
- assert.deepEqual(fooBarInstance.childViews, [fooBarBazInstance]);
-
- runTask(() => this.rerender());
- this.assertText('foo-bar foo-bar-baz');
-
- assert.equal(fooBarInstance.parentView, this.component);
- assert.equal(fooBarBazInstance.parentView, fooBarInstance);
-
- assert.deepEqual(this.component.childViews, [fooBarInstance]);
- assert.deepEqual(fooBarInstance.childViews, [fooBarBazInstance]);
- }
-
- '@test it renders passed named arguments'() {
- this.owner.register(
- 'component:foo-bar',
- setComponentTemplate(precompileTemplate('{{@foo}}'), class extends Component {})
- );
-
- this.render(' ', {
- model: {
- bar: 'Hola',
- },
- });
-
- this.assertText('Hola');
-
- runTask(() => this.rerender());
-
- this.assertText('Hola');
-
- runTask(() => this.context.set('model.bar', 'Hello'));
-
- this.assertText('Hello');
-
- runTask(() => this.context.set('model', { bar: 'Hola' }));
-
- this.assertText('Hola');
- }
-
- '@test it reflects named arguments as properties'() {
+ '@test it reflects named arguments on `this.args`'() {
this.owner.register(
'component:foo-bar',
- setComponentTemplate(precompileTemplate('{{this.foo}}'), class extends EmberComponent {})
+ setComponentTemplate(precompileTemplate('{{this.args.foo}}'), class extends Component {})
);
this.render(' ', {
@@ -585,7 +312,7 @@ moduleFor(
}
'@test positional parameters are not allowed'() {
- let TestComponent = class extends EmberComponent {
+ let TestComponent = class extends PositionalComponent {
static positionalParams = ['first', 'second'];
};
@@ -704,54 +431,6 @@ moduleFor(
this.assertStableRerender();
}
- '@test includes invocation specified attributes in root element ("splattributes")'() {
- this.owner.register(
- 'component:foo-bar',
- setComponentTemplate(precompileTemplate('hello'), class extends EmberComponent {})
- );
-
- this.render(' ', {
- foo: 'foo',
- bar: 'bar',
- });
-
- this.assertComponentElement(this.firstChild, {
- tagName: 'div',
- attrs: { 'data-foo': 'foo', 'data-bar': 'bar' },
- content: 'hello',
- });
-
- runTask(() => this.rerender());
-
- this.assertComponentElement(this.firstChild, {
- tagName: 'div',
- attrs: { 'data-foo': 'foo', 'data-bar': 'bar' },
- content: 'hello',
- });
-
- runTask(() => {
- set(this.context, 'foo', 'FOO');
- set(this.context, 'bar', undefined);
- });
-
- this.assertComponentElement(this.firstChild, {
- tagName: 'div',
- attrs: { 'data-foo': 'FOO' },
- content: 'hello',
- });
-
- runTask(() => {
- set(this.context, 'foo', 'foo');
- set(this.context, 'bar', 'bar');
- });
-
- this.assertComponentElement(this.firstChild, {
- tagName: 'div',
- attrs: { 'data-foo': 'foo', 'data-bar': 'bar' },
- content: 'hello',
- });
- }
-
'@test attributes without values passed at invocation are included in `...attributes` ("splattributes")'() {
this.owner.register(
'component:foo-bar',
diff --git a/packages/@ember/-internals/glimmer/tests/integration/components/attribute-bindings-test.js b/packages/@ember/-internals/glimmer/tests/integration/components/attribute-bindings-test.js
index 137d31497f6..93eb48a58fb 100644
--- a/packages/@ember/-internals/glimmer/tests/integration/components/attribute-bindings-test.js
+++ b/packages/@ember/-internals/glimmer/tests/integration/components/attribute-bindings-test.js
@@ -994,5 +994,79 @@ moduleFor(
this.assert.ok(this.nthChild(0).id.match(/ember\d+/), 'a valid `id` was used');
this.assert.ok(this.nthChild(1).id.match(/ember\d+/), 'a valid `id` was used');
}
+
+ '@test setting a value attributeBinding to null results in an empty string value'(assert) {
+ this.owner.register(
+ 'component:input-component',
+ setComponentTemplate(
+ precompileTemplate('input component'),
+ class extends Component {
+ tagName = 'input';
+ attributeBindings = ['value'];
+ }
+ )
+ );
+
+ this.render('{{input-component value=this.someProp}}', { someProp: null });
+
+ let element = this.firstChild;
+ assert.strictEqual(element.value, '');
+
+ runTask(() => set(this.context, 'someProp', 'wycats'));
+ assert.strictEqual(element.value, 'wycats');
+
+ runTask(() => set(this.context, 'someProp', null));
+ assert.strictEqual(element.value, '');
+ }
+
+ // https://github.com/emberjs/ember.js/issues/15675
+ renderRange(value) {
+ this.owner.register(
+ 'component:range-input',
+ setComponentTemplate(
+ precompileTemplate(''),
+ class extends Component {
+ tagName = 'input';
+ type = 'range';
+ attributeBindings = ['type', 'value', 'min', 'max'];
+ }
+ )
+ );
+
+ this.render('{{range-input max=this.max min=this.min value=this.value}}', {
+ max: 50,
+ min: -5,
+ value,
+ });
+ }
+
+ '@test [emberjs/ember.js#15675] range input: value over the default max but below the set max is kept'(
+ assert
+ ) {
+ this.renderRange(25);
+ assert.strictEqual(this.firstChild.value, '25');
+ }
+
+ '@test [emberjs/ember.js#15675] range input: value below the default min but above the set min is kept'(
+ assert
+ ) {
+ this.renderRange(-2);
+ assert.strictEqual(this.firstChild.value, '-2');
+ }
+
+ '@test [emberjs/ember.js#15675] range input: value in the valid default range is kept'(assert) {
+ this.renderRange(5);
+ assert.strictEqual(this.firstChild.value, '5');
+ }
+
+ '@test [emberjs/ember.js#15675] range input: value above max is reset to max'(assert) {
+ this.renderRange(55);
+ assert.strictEqual(this.firstChild.value, '50');
+ }
+
+ '@test [emberjs/ember.js#15675] range input: value below min is reset to min'(assert) {
+ this.renderRange(-10);
+ assert.strictEqual(this.firstChild.value, '-5');
+ }
}
);
diff --git a/packages/@ember/-internals/glimmer/tests/integration/components/attrs-lookup-test.js b/packages/@ember/-internals/glimmer/tests/integration/components/attrs-lookup-test.js
index 631b532e336..451f7734ecd 100644
--- a/packages/@ember/-internals/glimmer/tests/integration/components/attrs-lookup-test.js
+++ b/packages/@ember/-internals/glimmer/tests/integration/components/attrs-lookup-test.js
@@ -171,9 +171,7 @@ moduleFor(
}
};
- FooBarComponent.reopenClass({
- positionalParams: ['firstPositional'],
- });
+ FooBarComponent.positionalParams = ['firstPositional'];
this.owner.register('component:foo-bar', FooBarComponent);
diff --git a/packages/@ember/-internals/glimmer/tests/integration/components/classic/angle-bracket-invocation-test.js b/packages/@ember/-internals/glimmer/tests/integration/components/classic/angle-bracket-invocation-test.js
new file mode 100644
index 00000000000..772f2ae6d8d
--- /dev/null
+++ b/packages/@ember/-internals/glimmer/tests/integration/components/classic/angle-bracket-invocation-test.js
@@ -0,0 +1,543 @@
+import { moduleFor, RenderingTestCase, strip, classes, runTask } from 'internal-test-helpers';
+import { setComponentTemplate } from '@glimmer/manager';
+import { set } from '@ember/object';
+import { Component as EmberComponent } from '../../../utils/helpers';
+import { precompileTemplate } from '@ember/template-compilation';
+
+moduleFor(
+ 'AngleBracket Invocation (classic component)',
+ class extends RenderingTestCase {
+ '@test it can have a custom id and it is not bound'() {
+ this.owner.register(
+ 'component:foo-bar',
+ setComponentTemplate(
+ precompileTemplate('{{this.id}} {{this.elementId}}'),
+ class extends EmberComponent {}
+ )
+ );
+
+ this.render(' ', {
+ customId: 'bizz',
+ });
+
+ this.assertComponentElement(this.firstChild, {
+ tagName: 'div',
+ attrs: { id: 'bizz' },
+ content: 'bizz bizz',
+ });
+
+ this.assertStableRerender();
+
+ runTask(() => set(this.context, 'customId', 'bar'));
+
+ this.assertComponentElement(this.firstChild, {
+ tagName: 'div',
+ attrs: { id: 'bizz' },
+ content: 'bar bizz',
+ });
+
+ runTask(() => set(this.context, 'customId', 'bizz'));
+
+ this.assertComponentElement(this.firstChild, {
+ tagName: 'div',
+ attrs: { id: 'bizz' },
+ content: 'bizz bizz',
+ });
+ }
+
+ '@test it can have a custom id attribute and it is bound'() {
+ this.owner.register(
+ 'component:foo-bar',
+ setComponentTemplate(precompileTemplate('hello'), class extends EmberComponent {})
+ );
+
+ this.render(' ', {
+ customId: 'bizz',
+ });
+
+ this.assertComponentElement(this.firstChild, {
+ tagName: 'div',
+ attrs: { id: 'bizz' },
+ content: 'hello',
+ });
+
+ this.assertStableRerender();
+
+ runTask(() => set(this.context, 'customId', 'bar'));
+
+ this.assertComponentElement(this.firstChild, {
+ tagName: 'div',
+ attrs: { id: 'bar' },
+ content: 'hello',
+ });
+
+ runTask(() => set(this.context, 'customId', 'bizz'));
+
+ this.assertComponentElement(this.firstChild, {
+ tagName: 'div',
+ attrs: { id: 'bizz' },
+ content: 'hello',
+ });
+ }
+
+ '@test it can have a custom tagName'() {
+ this.owner.register(
+ 'component:foo-bar',
+ setComponentTemplate(
+ precompileTemplate('hello'),
+ class extends EmberComponent {
+ tagName = 'foo-bar';
+ }
+ )
+ );
+
+ this.render(' ');
+
+ this.assertComponentElement(this.firstChild, {
+ tagName: 'foo-bar',
+ content: 'hello',
+ });
+
+ runTask(() => this.rerender());
+
+ this.assertComponentElement(this.firstChild, {
+ tagName: 'foo-bar',
+ content: 'hello',
+ });
+ }
+
+ '@test it can have a custom tagName from the invocation'() {
+ this.owner.register(
+ 'component:foo-bar',
+ setComponentTemplate(precompileTemplate('hello'), class extends EmberComponent {})
+ );
+
+ this.render(' ');
+
+ this.assertComponentElement(this.firstChild, {
+ tagName: 'foo-bar',
+ content: 'hello',
+ });
+
+ runTask(() => this.rerender());
+
+ this.assertComponentElement(this.firstChild, {
+ tagName: 'foo-bar',
+ content: 'hello',
+ });
+ }
+
+ '@test it can have custom classNames'() {
+ this.owner.register(
+ 'component:foo-bar',
+ setComponentTemplate(
+ precompileTemplate('hello'),
+ class extends EmberComponent {
+ classNames = ['foo', 'bar'];
+ }
+ )
+ );
+
+ this.render(' ');
+
+ this.assertComponentElement(this.firstChild, {
+ tagName: 'div',
+ attrs: { class: classes('ember-view foo bar') },
+ content: 'hello',
+ });
+
+ runTask(() => this.rerender());
+
+ this.assertComponentElement(this.firstChild, {
+ tagName: 'div',
+ attrs: { class: classes('ember-view foo bar') },
+ content: 'hello',
+ });
+ }
+
+ '@test class property on components can be dynamic'() {
+ this.owner.register(
+ 'component:foo-bar',
+ setComponentTemplate(precompileTemplate('hello'), class extends EmberComponent {})
+ );
+
+ this.render(' ', {
+ fooBar: true,
+ });
+
+ this.assertComponentElement(this.firstChild, {
+ content: 'hello',
+ attrs: { class: classes('ember-view foo-bar') },
+ });
+
+ runTask(() => this.rerender());
+
+ this.assertComponentElement(this.firstChild, {
+ content: 'hello',
+ attrs: { class: classes('ember-view foo-bar') },
+ });
+
+ runTask(() => set(this.context, 'fooBar', false));
+
+ this.assertComponentElement(this.firstChild, {
+ content: 'hello',
+ attrs: { class: classes('ember-view') },
+ });
+
+ runTask(() => set(this.context, 'fooBar', true));
+
+ this.assertComponentElement(this.firstChild, {
+ content: 'hello',
+ attrs: { class: classes('ember-view foo-bar') },
+ });
+ }
+
+ '@test it can set custom classNames from the invocation'() {
+ let FooBarComponent = class extends EmberComponent {
+ classNames = ['foo'];
+ };
+
+ this.owner.register(
+ 'component:foo-bar',
+ setComponentTemplate(precompileTemplate('hello'), FooBarComponent)
+ );
+
+ this.render(strip`
+
+
+
+ `);
+
+ this.assertComponentElement(this.nthChild(0), {
+ tagName: 'div',
+ attrs: { class: classes('ember-view foo bar baz') },
+ content: 'hello',
+ });
+ this.assertComponentElement(this.nthChild(1), {
+ tagName: 'div',
+ attrs: { class: classes('ember-view foo bar baz') },
+ content: 'hello',
+ });
+ this.assertComponentElement(this.nthChild(2), {
+ tagName: 'div',
+ attrs: { class: classes('ember-view foo') },
+ content: 'hello',
+ });
+
+ runTask(() => this.rerender());
+
+ this.assertComponentElement(this.nthChild(0), {
+ tagName: 'div',
+ attrs: { class: classes('ember-view foo bar baz') },
+ content: 'hello',
+ });
+ this.assertComponentElement(this.nthChild(1), {
+ tagName: 'div',
+ attrs: { class: classes('ember-view foo bar baz') },
+ content: 'hello',
+ });
+ this.assertComponentElement(this.nthChild(2), {
+ tagName: 'div',
+ attrs: { class: classes('ember-view foo') },
+ content: 'hello',
+ });
+ }
+
+ '@test it has an element'() {
+ let instance;
+
+ let FooBarComponent = class extends EmberComponent {
+ init() {
+ super.init();
+ instance = this;
+ }
+ };
+
+ this.owner.register(
+ 'component:foo-bar',
+ setComponentTemplate(precompileTemplate('hello'), FooBarComponent)
+ );
+
+ this.render(' ');
+
+ let element1 = instance.element;
+
+ this.assertComponentElement(element1, { content: 'hello' });
+
+ runTask(() => this.rerender());
+
+ let element2 = instance.element;
+
+ this.assertComponentElement(element2, { content: 'hello' });
+
+ this.assertSameNode(element2, element1);
+ }
+
+ '@test it has the right parentView and childViews'(assert) {
+ let fooBarInstance, fooBarBazInstance;
+
+ let FooBarComponent = class extends EmberComponent {
+ init() {
+ super.init();
+ fooBarInstance = this;
+ }
+ };
+
+ let FooBarBazComponent = class extends EmberComponent {
+ init() {
+ super.init();
+ fooBarBazInstance = this;
+ }
+ };
+
+ this.owner.register(
+ 'component:foo-bar',
+ setComponentTemplate(precompileTemplate('foo-bar {{foo-bar-baz}}'), FooBarComponent)
+ );
+ this.owner.register(
+ 'component:foo-bar-baz',
+ setComponentTemplate(precompileTemplate('foo-bar-baz'), FooBarBazComponent)
+ );
+
+ this.render(' ');
+ this.assertText('foo-bar foo-bar-baz');
+
+ assert.equal(fooBarInstance.parentView, this.component);
+ assert.equal(fooBarBazInstance.parentView, fooBarInstance);
+
+ assert.deepEqual(this.component.childViews, [fooBarInstance]);
+ assert.deepEqual(fooBarInstance.childViews, [fooBarBazInstance]);
+
+ runTask(() => this.rerender());
+ this.assertText('foo-bar foo-bar-baz');
+
+ assert.equal(fooBarInstance.parentView, this.component);
+ assert.equal(fooBarBazInstance.parentView, fooBarInstance);
+
+ assert.deepEqual(this.component.childViews, [fooBarInstance]);
+ assert.deepEqual(fooBarInstance.childViews, [fooBarBazInstance]);
+ }
+
+ '@test it reflects named arguments as properties'() {
+ this.owner.register(
+ 'component:foo-bar',
+ setComponentTemplate(precompileTemplate('{{this.foo}}'), class extends EmberComponent {})
+ );
+
+ this.render(' ', {
+ model: {
+ bar: 'Hola',
+ },
+ });
+
+ this.assertText('Hola');
+
+ runTask(() => this.rerender());
+
+ this.assertText('Hola');
+
+ runTask(() => this.context.set('model.bar', 'Hello'));
+
+ this.assertText('Hello');
+
+ runTask(() => this.context.set('model', { bar: 'Hola' }));
+
+ this.assertText('Hola');
+ }
+
+ '@test positional parameters are not allowed'() {
+ let TestComponent = class extends EmberComponent {
+ static positionalParams = ['first', 'second'];
+ };
+
+ this.owner.register(
+ 'component:sample-component',
+ setComponentTemplate(precompileTemplate('{{this.first}}{{this.second}}'), TestComponent)
+ );
+
+ // this is somewhat silly as the browser "corrects" for these as
+ // attribute names, but regardless the thing we care about here is that
+ // they are **not** used as positional params
+ this.render(' ');
+
+ this.assertText('');
+ }
+
+ '@test includes invocation specified attributes in root element ("splattributes")'() {
+ this.owner.register(
+ 'component:foo-bar',
+ setComponentTemplate(precompileTemplate('hello'), class extends EmberComponent {})
+ );
+
+ this.render(' ', {
+ foo: 'foo',
+ bar: 'bar',
+ });
+
+ this.assertComponentElement(this.firstChild, {
+ tagName: 'div',
+ attrs: { 'data-foo': 'foo', 'data-bar': 'bar' },
+ content: 'hello',
+ });
+
+ runTask(() => this.rerender());
+
+ this.assertComponentElement(this.firstChild, {
+ tagName: 'div',
+ attrs: { 'data-foo': 'foo', 'data-bar': 'bar' },
+ content: 'hello',
+ });
+
+ runTask(() => {
+ set(this.context, 'foo', 'FOO');
+ set(this.context, 'bar', undefined);
+ });
+
+ this.assertComponentElement(this.firstChild, {
+ tagName: 'div',
+ attrs: { 'data-foo': 'FOO' },
+ content: 'hello',
+ });
+
+ runTask(() => {
+ set(this.context, 'foo', 'foo');
+ set(this.context, 'bar', 'bar');
+ });
+
+ this.assertComponentElement(this.firstChild, {
+ tagName: 'div',
+ attrs: { 'data-foo': 'foo', 'data-bar': 'bar' },
+ content: 'hello',
+ });
+ }
+
+ '@test invocation attributes are merged with the classes of the component'() {
+ this.owner.register(
+ 'component:foo-bar',
+ setComponentTemplate(
+ precompileTemplate('Hello world!'),
+ class extends EmberComponent {
+ classNames = ['inner'];
+ }
+ )
+ );
+
+ this.render(' ');
+
+ this.assertComponentElement(this.firstChild, {
+ tagName: 'div',
+ attrs: { class: classes('ember-view inner outer') },
+ content: 'Hello world!',
+ });
+
+ this.assertStableRerender();
+ }
+
+ '@test invocation attributes win over attributeBindings'() {
+ this.owner.register(
+ 'component:foo-bar',
+ setComponentTemplate(
+ precompileTemplate('Hello world!'),
+ class extends EmberComponent {
+ attributeBindings = ['data-foo'];
+ 'data-foo' = 'inner';
+ }
+ )
+ );
+
+ this.render(' ');
+
+ this.assertComponentElement(this.firstChild, {
+ tagName: 'div',
+ attrs: { 'data-foo': 'outer' },
+ content: 'Hello world!',
+ });
+
+ this.assertStableRerender();
+ }
+
+ '@test explicit ...attributes in the layout receive the invocation attributes'() {
+ this.owner.register(
+ 'component:foo-bar',
+ setComponentTemplate(
+ precompileTemplate('Hello world! '),
+ class extends EmberComponent {}
+ )
+ );
+
+ this.render(' ');
+
+ this.assertComponentElement(this.firstChild, {
+ tagName: 'div',
+ attrs: { 'data-foo': 'bar' },
+ content: 'Hello world! ',
+ });
+
+ this.assertStableRerender();
+ }
+
+ '@test it can yield to a named block'() {
+ this.owner.register(
+ 'component:foo-bar',
+ setComponentTemplate(
+ precompileTemplate('Hello{{yield to="baz"}}world!'),
+ class extends EmberComponent {}
+ )
+ );
+
+ this.render('<:baz> my ');
+
+ this.assertComponentElement(this.firstChild, {
+ tagName: 'div',
+ content: 'Hello my world!',
+ });
+
+ this.assertStableRerender();
+ }
+
+ '@test it can yield to an explicit default named block'() {
+ this.owner.register(
+ 'component:foo-bar',
+ setComponentTemplate(
+ precompileTemplate('Hello{{yield to="default"}}world!'),
+ class extends EmberComponent {}
+ )
+ );
+
+ this.render('<:default> my ');
+
+ this.assertComponentElement(this.firstChild, {
+ tagName: 'div',
+ content: 'Hello my world!',
+ });
+
+ this.assertStableRerender();
+ }
+
+ '@test it can yield a value to the else and inverse named blocks'() {
+ this.owner.register(
+ 'component:foo-bar',
+ setComponentTemplate(
+ precompileTemplate('Hello{{yield "my" to="inverse"}}world!'),
+ class extends EmberComponent {}
+ )
+ );
+
+ this.render(strip`
+ <:else as |value|> {{value}}
+ <:inverse as |value|> {{value}}
+ `);
+
+ this.assertComponentElement(this.nthChild(0), {
+ tagName: 'div',
+ content: 'Hello my world!',
+ });
+ this.assertComponentElement(this.nthChild(1), {
+ tagName: 'div',
+ content: 'Hello my world!',
+ });
+
+ this.assertStableRerender();
+ }
+ }
+);
diff --git a/packages/@ember/-internals/glimmer/tests/integration/components/append-test.js b/packages/@ember/-internals/glimmer/tests/integration/components/classic/append-test.js
similarity index 99%
rename from packages/@ember/-internals/glimmer/tests/integration/components/append-test.js
rename to packages/@ember/-internals/glimmer/tests/integration/components/classic/append-test.js
index fd5a5030aa4..5a59e63b4d0 100644
--- a/packages/@ember/-internals/glimmer/tests/integration/components/append-test.js
+++ b/packages/@ember/-internals/glimmer/tests/integration/components/classic/append-test.js
@@ -5,13 +5,13 @@ import {
runTask,
expectDeprecation,
} from 'internal-test-helpers';
-import { DEPRECATIONS } from '../../../../deprecations';
+import { DEPRECATIONS } from '../../../../../deprecations';
import { set } from '@ember/object';
import { setComponentTemplate } from '@glimmer/manager';
import { precompileTemplate } from '@ember/template-compilation';
-import { Component } from '../../utils/helpers';
+import { Component } from '../../../utils/helpers';
class AbstractAppendTest extends RenderingTestCase {
constructor() {
diff --git a/packages/@ember/-internals/glimmer/tests/integration/components/classic/application-lifecycle-test.js b/packages/@ember/-internals/glimmer/tests/integration/components/classic/application-lifecycle-test.js
new file mode 100644
index 00000000000..679e2401aa1
--- /dev/null
+++ b/packages/@ember/-internals/glimmer/tests/integration/components/classic/application-lifecycle-test.js
@@ -0,0 +1,91 @@
+import { moduleFor, AutobootApplicationTestCase, runTask } from 'internal-test-helpers';
+import Application from '@ember/application';
+import Router from '@ember/routing/router';
+import { Component } from '@ember/-internals/glimmer';
+import { precompileTemplate } from '@ember/template-compilation';
+import { setComponentTemplate } from '@glimmer/manager';
+
+moduleFor(
+ 'Application Lifecycle (classic component)',
+ class extends AutobootApplicationTestCase {
+ createApplication() {
+ let application = super.createApplication(...arguments);
+ this.add(
+ 'router:main',
+ class extends Router {
+ location = 'none';
+ }
+ );
+ return application;
+ }
+
+ [`@test initializers can augment an applications customEvents hash`](assert) {
+ assert.expect(1);
+
+ let MyApplication = class extends Application {};
+
+ MyApplication.initializer({
+ name: 'customize-things',
+ initialize(application) {
+ application.customEvents = {
+ wowza: 'wowza',
+ };
+ },
+ });
+
+ runTask(() => {
+ this.createApplication({}, MyApplication);
+
+ this.add(
+ 'component:foo-bar',
+ setComponentTemplate(
+ precompileTemplate(`
`),
+ class extends Component {
+ wowza() {
+ assert.ok(true, 'fired the event!');
+ }
+ }
+ )
+ );
+
+ this.add('template:application', precompileTemplate(`{{foo-bar}}`));
+ });
+
+ this.$('#wowza-thingy').trigger('wowza');
+ }
+
+ [`@test instanceInitializers can augment an the customEvents hash`](assert) {
+ assert.expect(1);
+
+ let MyApplication = class extends Application {};
+
+ MyApplication.instanceInitializer({
+ name: 'customize-things',
+ initialize(application) {
+ application.customEvents = {
+ herky: 'jerky',
+ };
+ },
+ });
+ runTask(() => {
+ this.createApplication({}, MyApplication);
+
+ this.add(
+ 'component:foo-bar',
+ setComponentTemplate(
+ precompileTemplate(`
`),
+ class extends Component {
+ jerky() {
+ assert.ok(true, 'fired the event!');
+ }
+ }
+ )
+ );
+
+ this.add('template:application', precompileTemplate(`{{foo-bar}}`));
+ });
+
+ this.$('#herky-thingy').trigger('herky');
+ }
+ }
+);
diff --git a/packages/@ember/-internals/glimmer/tests/integration/components/classic/component-context-test.js b/packages/@ember/-internals/glimmer/tests/integration/components/classic/component-context-test.js
new file mode 100644
index 00000000000..8f73184b48e
--- /dev/null
+++ b/packages/@ember/-internals/glimmer/tests/integration/components/classic/component-context-test.js
@@ -0,0 +1,107 @@
+import Controller from '@ember/controller';
+import { Component as EmberComponent } from '@ember/-internals/glimmer';
+import { precompileTemplate } from '@ember/template-compilation';
+import { moduleFor, ApplicationTestCase, getTextOf } from 'internal-test-helpers';
+
+moduleFor(
+ 'Application Lifecycle - Component Context (classic component)',
+ class extends ApplicationTestCase {
+ ['@test Components without a block should have the proper content'](assert) {
+ this.add(
+ 'template:application',
+ precompileTemplate(
+ `
+ {{my-component}}
+ `
+ )
+ );
+
+ this.add(
+ 'controller:application',
+ class extends Controller {
+ text = 'outer';
+ }
+ );
+ this.add(
+ 'component:my-component',
+ class extends EmberComponent {
+ didInsertElement() {
+ this.element.innerHTML = 'Some text inserted';
+ }
+ }
+ );
+
+ return this.visit('/').then(() => {
+ let text = getTextOf(this.element.querySelector('#wrapper'));
+ assert.equal(text, 'Some text inserted', 'The component is composed correctly');
+ });
+ }
+
+ ['@test properties of a component without a template should not collide with internal structures [DEPRECATED]'](
+ assert
+ ) {
+ this.add(
+ 'template:application',
+ precompileTemplate(
+ `
+ {{my-component data=this.foo}}
`
+ )
+ );
+
+ this.add(
+ 'controller:application',
+ class extends Controller {
+ text = 'outer';
+ foo = 'Some text inserted';
+ }
+ );
+ this.add(
+ 'component:my-component',
+ class extends EmberComponent {
+ didInsertElement() {
+ this.element.innerHTML = this.get('data');
+ }
+ }
+ );
+
+ return this.visit('/').then(() => {
+ let text = getTextOf(this.element.querySelector('#wrapper'));
+ assert.equal(text, 'Some text inserted', 'The component is composed correctly');
+ });
+ }
+
+ ['@test attrs property of a component without a template should not collide with internal structures'](
+ assert
+ ) {
+ this.add(
+ 'template:application',
+ precompileTemplate(
+ `
+ {{my-component attrs=this.foo}}
+ `
+ )
+ );
+
+ this.add(
+ 'controller:application',
+ class extends Controller {
+ text = 'outer';
+ foo = 'Some text inserted';
+ }
+ );
+ this.add(
+ 'component:my-component',
+ class extends EmberComponent {
+ didInsertElement() {
+ this.element.innerHTML = this.get('attrs.attrs.value');
+ }
+ }
+ );
+
+ return this.visit('/').then(() => {
+ let text = getTextOf(this.element.querySelector('#wrapper'));
+ assert.equal(text, 'Some text inserted', 'The component is composed correctly');
+ });
+ }
+ }
+);
diff --git a/packages/@ember/-internals/glimmer/tests/integration/components/classic/component-registration-test.js b/packages/@ember/-internals/glimmer/tests/integration/components/classic/component-registration-test.js
new file mode 100644
index 00000000000..e1afc0f8a18
--- /dev/null
+++ b/packages/@ember/-internals/glimmer/tests/integration/components/classic/component-registration-test.js
@@ -0,0 +1,127 @@
+import Application from '@ember/application';
+import Controller from '@ember/controller';
+import { Component } from '@ember/-internals/glimmer';
+import { precompileTemplate } from '@ember/template-compilation';
+import { moduleFor, ApplicationTestCase } from 'internal-test-helpers';
+
+moduleFor(
+ 'Application Lifecycle - Component Registration (classic component)',
+ class extends ApplicationTestCase {
+ // This is necessary for this.application.instanceInitializer to not leak between tests
+ createApplication(options) {
+ return super.createApplication(options, class extends Application {});
+ }
+
+ ['@test Late-registered components can be rendered with custom `layout` property'](assert) {
+ this.add(
+ 'template:application',
+ precompileTemplate(`there goes {{my-hero}}
`)
+ );
+
+ this.application.instanceInitializer({
+ name: 'my-hero-component',
+ initialize(applicationInstance) {
+ applicationInstance.register(
+ 'component:my-hero',
+ class extends Component {
+ classNames = ['testing123'];
+ layout = precompileTemplate('watch him as he GOES');
+ }
+ );
+ },
+ });
+
+ return this.visit('/').then(() => {
+ let text = this.$('#wrapper').text().trim();
+ assert.equal(
+ text,
+ 'there goes watch him as he GOES',
+ 'The component is composed correctly'
+ );
+ });
+ }
+
+ ['@test Assigning layoutName to a component should setup the template as a layout'](assert) {
+ assert.expect(1);
+
+ this.add(
+ 'template:application',
+ precompileTemplate(
+ `{{#my-component}}{{this.text}}{{/my-component}}
`
+ )
+ );
+ this.add('template:foo-bar-baz', precompileTemplate('{{this.text}}-{{yield}}'));
+
+ this.application.instanceInitializer({
+ name: 'application-controller',
+ initialize(applicationInstance) {
+ applicationInstance.register(
+ 'controller:application',
+ class extends Controller {
+ text = 'outer';
+ }
+ );
+ },
+ });
+ this.application.instanceInitializer({
+ name: 'my-component-component',
+ initialize(applicationInstance) {
+ applicationInstance.register(
+ 'component:my-component',
+ class extends Component {
+ text = 'inner';
+ layoutName = 'foo-bar-baz';
+ }
+ );
+ },
+ });
+
+ return this.visit('/').then(() => {
+ let text = this.$('#wrapper').text().trim();
+ assert.equal(text, 'inner-outer', 'The component is composed correctly');
+ });
+ }
+
+ ['@test Assigning layoutName and layout to a component should use the `layout` value'](assert) {
+ assert.expect(1);
+
+ this.add(
+ 'template:application',
+ precompileTemplate(
+ `{{#my-component}}{{this.text}}{{/my-component}}
`
+ )
+ );
+ this.add('template:foo-bar-baz', precompileTemplate('No way!'));
+
+ this.application.instanceInitializer({
+ name: 'application-controller-layout',
+ initialize(applicationInstance) {
+ applicationInstance.register(
+ 'controller:application',
+ class extends Controller {
+ text = 'outer';
+ }
+ );
+ },
+ });
+ this.application.instanceInitializer({
+ name: 'my-component-component-layout',
+ initialize(applicationInstance) {
+ applicationInstance.register(
+ 'component:my-component',
+ class extends Component {
+ text = 'inner';
+ layoutName = 'foo-bar-baz';
+ layout = precompileTemplate('{{this.text}}-{{yield}}');
+ }
+ );
+ },
+ });
+
+ return this.visit('/').then(() => {
+ let text = this.$('#wrapper').text().trim();
+ assert.equal(text, 'inner-outer', 'The component is composed correctly');
+ });
+ }
+ }
+);
diff --git a/packages/@ember/-internals/glimmer/tests/integration/components/classic/component-template-test.js b/packages/@ember/-internals/glimmer/tests/integration/components/classic/component-template-test.js
new file mode 100644
index 00000000000..6d6ccf6cbbf
--- /dev/null
+++ b/packages/@ember/-internals/glimmer/tests/integration/components/classic/component-template-test.js
@@ -0,0 +1,26 @@
+import { moduleFor, RenderingTestCase, runTask } from 'internal-test-helpers';
+import { setComponentTemplate } from '@glimmer/manager';
+import { precompileTemplate } from '@ember/template-compilation';
+import { Component as EmberComponent } from '../../../utils/helpers';
+
+moduleFor(
+ 'Components test: setComponentTemplate (classic component)',
+ class extends RenderingTestCase {
+ '@test templates set with setComponentTemplate are inherited (EmberObject.extend())'() {
+ let Parent = setComponentTemplate(
+ precompileTemplate('hello'),
+ class extends EmberComponent {}
+ );
+
+ this.owner.register('component:foo-bar', class extends Parent {});
+
+ this.render(' ');
+
+ this.assertComponentElement(this.firstChild, { content: 'hello' });
+
+ runTask(() => this.rerender());
+
+ this.assertComponentElement(this.firstChild, { content: 'hello' });
+ }
+ }
+);
diff --git a/packages/@ember/-internals/glimmer/tests/integration/components/classic/content-test.js b/packages/@ember/-internals/glimmer/tests/integration/components/classic/content-test.js
new file mode 100644
index 00000000000..02db6b6d981
--- /dev/null
+++ b/packages/@ember/-internals/glimmer/tests/integration/components/classic/content-test.js
@@ -0,0 +1,56 @@
+import { DEBUG } from '@glimmer/env';
+
+import { RenderingTestCase, moduleFor } from 'internal-test-helpers';
+
+import { getDebugFunction, setDebugFunction } from '@ember/debug';
+import { constructStyleDeprecationMessage } from '@ember/-internals/views';
+import { precompileTemplate } from '@ember/template-compilation';
+import { setComponentTemplate } from '@glimmer/manager';
+import { Component } from '../../../utils/helpers';
+
+let warnings, originalWarn;
+class StyleTest extends RenderingTestCase {
+ constructor() {
+ super(...arguments);
+ warnings = [];
+ originalWarn = getDebugFunction('warn');
+ setDebugFunction('warn', function (message, test) {
+ if (!test) {
+ warnings.push(message);
+ }
+ });
+ }
+
+ teardown() {
+ super.teardown(...arguments);
+ setDebugFunction('warn', originalWarn);
+ }
+
+ assertStyleWarning(style) {
+ this.assert.deepEqual(warnings, [constructStyleDeprecationMessage(style)]);
+ }
+}
+
+if (DEBUG) {
+ moduleFor(
+ 'Inline style tests - warnings (classic component)',
+ class extends StyleTest {
+ ['@test specifying `attributeBindings: ["style"]` generates a warning']() {
+ let FooBarComponent = class extends Component {
+ attributeBindings = ['style'];
+ };
+
+ this.owner.register(
+ 'component:foo-bar',
+ setComponentTemplate(precompileTemplate('hello'), FooBarComponent)
+ );
+ let userValue = 'width: 42px';
+ this.render('{{foo-bar style=this.userValue}}', {
+ userValue,
+ });
+
+ this.assertStyleWarning(userValue);
+ }
+ }
+ );
+}
diff --git a/packages/@ember/-internals/glimmer/tests/integration/components/classic/contextual-components-test.js b/packages/@ember/-internals/glimmer/tests/integration/components/classic/contextual-components-test.js
new file mode 100644
index 00000000000..cae118daefc
--- /dev/null
+++ b/packages/@ember/-internals/glimmer/tests/integration/components/classic/contextual-components-test.js
@@ -0,0 +1,458 @@
+import { moduleFor, RenderingTestCase, applyMixins, strip, runTask } from 'internal-test-helpers';
+import { action } from '@ember/object';
+import { trackedArray } from '@ember/reactive/collections';
+import Component from '@glimmer/component';
+import { tracked } from '@glimmer/tracking';
+import { Component as EmberComponent } from '../../../utils/helpers';
+import { precompileTemplate } from '@ember/template-compilation';
+import { setComponentTemplate } from '@glimmer/manager';
+
+moduleFor(
+ 'Components test: contextual components (classic component)',
+ class extends RenderingTestCase {
+ ['@test conflicting positional and hash parameters does not raise an assertion if rerendered']() {
+ // In some cases, rerendering with a positional param used to cause an
+ // assertion. This test checks it does not.
+ this.owner.register(
+ 'component:-looked-up',
+ setComponentTemplate(
+ precompileTemplate('{{this.greeting}} {{this.name}}'),
+ class extends EmberComponent {
+ static positionalParams = ['name'];
+ }
+ )
+ );
+
+ this.render('{{component (component "-looked-up" this.model.name greeting="Hodi")}}', {
+ model: {
+ name: 'Hodari',
+ },
+ });
+
+ this.assertText('Hodi Hodari');
+
+ runTask(() => this.rerender());
+
+ this.assertText('Hodi Hodari');
+
+ runTask(() => this.context.set('model.name', 'Sergio'));
+
+ this.assertText('Hodi Sergio');
+
+ runTask(() => this.context.set('model', { name: 'Hodari' }));
+
+ this.assertText('Hodi Hodari');
+ }
+
+ ['@test renders with dot path and rest parameter does not leak'](assert) {
+ // In the original implementation, positional parameters were not handled
+ // correctly causing the first positional parameter to be the contextual
+ // component itself.
+ let value = false;
+
+ this.owner.register(
+ 'component:my-component',
+ class extends EmberComponent {
+ static positionalParams = ['value'];
+
+ didReceiveAttrs() {
+ value = this.getAttr('value');
+ }
+ }
+ );
+
+ this.render(
+ strip`
+ {{#let (hash my-component=(component 'my-component' this.first)) as |c|}}
+ {{c.my-component}}
+ {{/let}}`,
+ { first: 'first' }
+ );
+
+ assert.equal(value, 'first', 'value is the expected parameter');
+ }
+
+ ['@test renders with dot path and updates attributes'](assert) {
+ this.owner.register(
+ 'component:my-nested-component',
+ setComponentTemplate(
+ precompileTemplate('{{this.myProp}} '),
+ class extends EmberComponent {
+ static positionalParams = ['my-parent-attr'];
+
+ didReceiveAttrs() {
+ this.set('myProp', this.getAttr('my-parent-attr'));
+ }
+ }
+ )
+ );
+
+ this.owner.register(
+ 'component:my-component',
+ setComponentTemplate(
+ precompileTemplate(
+ '{{yield (hash my-nested-component=(component "my-nested-component" my-parent-attr=@my-attr))}}'
+ ),
+ class extends Component {}
+ )
+ );
+
+ this.owner.register(
+ 'component:my-action-component',
+ setComponentTemplate(
+ precompileTemplate(
+ '{{#my-component my-attr=this.myProp as |api|}}{{api.my-nested-component}}{{/my-component}}Change value '
+ ),
+ class extends EmberComponent {
+ @action
+ changeValue() {
+ this.incrementProperty('myProp');
+ }
+ }
+ )
+ );
+
+ this.render('{{my-action-component myProp=this.model.myProp}}', {
+ model: {
+ myProp: 1,
+ },
+ });
+
+ assert.equal(this.$('#nested-prop').text(), '1');
+
+ runTask(() => this.rerender());
+
+ assert.equal(this.$('#nested-prop').text(), '1');
+
+ runTask(() => this.$('button').click());
+
+ assert.equal(this.$('#nested-prop').text(), '2');
+
+ runTask(() => this.$('button').click());
+
+ assert.equal(this.$('#nested-prop').text(), '3');
+
+ runTask(() => this.context.set('model', { myProp: 1 }));
+
+ assert.equal(this.$('#nested-prop').text(), '1');
+ }
+
+ ['@test parameters in a contextual component are mutable when value is a param'](assert) {
+ // This checks that a `(mut)` is added to parameters and attributes to
+ // contextual components when it is a param.
+ this.owner.register(
+ 'component:change-button',
+ setComponentTemplate(
+ precompileTemplate(
+ 'Change to 10 '
+ ),
+ class extends EmberComponent {
+ static positionalParams = ['val'];
+ }
+ )
+ );
+
+ this.render(
+ strip`
+ {{component (component "change-button" this.model.val2)}}
+ {{this.model.val2}} `,
+ {
+ model: {
+ val2: 8,
+ },
+ }
+ );
+
+ assert.equal(this.$('.value').text(), '8');
+
+ runTask(() => this.rerender());
+
+ assert.equal(this.$('.value').text(), '8');
+
+ runTask(() => this.$('.my-button').click());
+
+ assert.equal(this.$('.value').text(), '10');
+
+ runTask(() => this.context.set('model', { val2: 8 }));
+
+ assert.equal(this.$('.value').text(), '8');
+ }
+
+ ['@test tagless blockless components render'](assert) {
+ this.owner.register(
+ 'component:my-comp',
+ class extends EmberComponent {
+ tagName = '';
+ }
+ );
+
+ this.render(`{{my-comp}}`);
+
+ runTask(() => this.rerender());
+
+ assert.equal(this.$().text(), '');
+ }
+
+ ['@test GH#13494 tagless blockless component with property binding'](assert) {
+ this.owner.register(
+ 'component:outer-component',
+ setComponentTemplate(
+ precompileTemplate(
+ 'message: {{this.message}}{{inner-component message=this.message}} '
+ ),
+ class extends Component {
+ @tracked message = 'hello';
+ @action
+ change() {
+ this.message = 'goodbye';
+ }
+ }
+ )
+ );
+
+ this.owner.register(
+ 'component:inner-component',
+ class extends EmberComponent {
+ tagName = '';
+ }
+ );
+
+ this.render(`{{outer-component}}`);
+
+ assert.equal(this.$().text(), 'message: hello');
+
+ runTask(() => this.rerender());
+
+ assert.equal(this.$().text(), 'message: hello');
+
+ runTask(() => this.$('button').click());
+
+ assert.equal(this.$().text(), 'message: goodbye');
+
+ runTask(() => this.rerender());
+
+ assert.equal(this.$().text(), 'message: goodbye');
+ }
+
+ ['@test GH#14508 rest positional params are received when passed as named parameter']() {
+ this.owner.register(
+ 'component:my-link',
+ setComponentTemplate(
+ precompileTemplate('{{#each this.params as |p|}}{{p}}{{/each}}'),
+ class extends EmberComponent {
+ positionalParams = 'params';
+ }
+ )
+ );
+
+ this.render('{{component (component "my-link") params=this.allParams}}', {
+ allParams: trackedArray(['a', 'b']),
+ });
+
+ this.assertText('ab');
+
+ runTask(() => this.rerender());
+
+ this.assertText('ab');
+
+ runTask(() => this.context.get('allParams').push('c'));
+
+ this.assertText('abc');
+
+ runTask(() => this.context.get('allParams').pop());
+
+ this.assertText('ab');
+
+ runTask(() => this.context.get('allParams').splice(0));
+
+ this.assertText('');
+
+ runTask(() => this.context.set('allParams', ['1', '2']));
+
+ this.assertText('12');
+
+ runTask(() => this.context.set('allParams', ['a', 'b']));
+
+ this.assertText('ab');
+ }
+
+ ['@test GH#14508 rest positional params are received when passed as named parameter with dot notation']() {
+ this.owner.register(
+ 'component:my-link',
+ setComponentTemplate(
+ precompileTemplate('{{#each this.params as |p|}}{{p}}{{/each}}'),
+ class extends EmberComponent {
+ positionalParams = 'params';
+ }
+ )
+ );
+
+ this.render(
+ '{{#let (hash link=(component "my-link")) as |c|}}{{c.link params=this.allParams}}{{/let}}',
+ {
+ allParams: trackedArray(['a', 'b']),
+ }
+ );
+
+ this.assertText('ab');
+
+ runTask(() => this.rerender());
+
+ this.assertText('ab');
+
+ runTask(() => this.context.get('allParams').push('c'));
+
+ this.assertText('abc');
+
+ runTask(() => this.context.get('allParams').pop());
+
+ this.assertText('ab');
+
+ runTask(() => this.context.get('allParams').splice(0));
+
+ this.assertText('');
+
+ runTask(() => this.context.set('allParams', ['1', '2']));
+
+ this.assertText('12');
+
+ runTask(() => this.context.set('allParams', ['a', 'b']));
+
+ this.assertText('ab');
+ }
+
+ '@test component helper can curry arguments'() {
+ this.owner.register(
+ 'component:foo-bar',
+ setComponentTemplate(
+ precompileTemplate(
+ '1. [{{this.one}}]2. [{{this.two}}]3. [{{this.three}}]4. [{{this.four}}]5. [{{this.five}}]6. [{{this.six}}]{{yield}}a. [{{this.a}}]b. [{{this.b}}]c. [{{this.c}}]d. [{{this.d}}]e. [{{this.e}}]f. [{{this.f}}]'
+ ),
+ class extends EmberComponent {
+ static positionalParams = ['one', 'two', 'three', 'four', 'five', 'six'];
+ }
+ )
+ );
+
+ this.render(strip`
+ {{#let (component "foo-bar" "outer 1" "outer 2" a="outer a" b="outer b" c="outer c" e="outer e") as |outer|}}
+ {{#let (component outer "inner 1" a="inner a" d="inner d" e="inner e") as |inner|}}
+ {{#component inner "invocation 1" "invocation 2" a="invocation a" b="invocation b"}}---{{/component}}
+ {{/let}}
+ {{/let}}
+ `);
+
+ this.assertText(
+ '1. [outer 1]2. [outer 2]3. [inner 1]4. [invocation 1]5. [invocation 2]6. []---a. [invocation a]b. [invocation b]c. [outer c]d. [inner d]e. [inner e]f. []'
+ );
+ }
+
+ '@test component helper: currying works inline'() {
+ this.owner.register(
+ 'component:foo-bar',
+ setComponentTemplate(
+ precompileTemplate(
+ '1. [{{this.one}}]2. [{{this.two}}]3. [{{this.three}}]4. [{{this.four}}]5. [{{this.five}}]6. [{{this.six}}]'
+ ),
+ class extends EmberComponent {
+ static positionalParams = ['one', 'two', 'three', 'four', 'five', 'six'];
+ }
+ )
+ );
+
+ this.render(
+ strip`
+ {{component (component (component 'foo-bar' this.foo.first this.foo.second) 'inner 1') 'invocation 1' 'invocation 2'}}
+ `,
+ { foo: { first: 'outer 1', second: 'outer 2' } }
+ );
+
+ this.assertText(
+ '1. [outer 1]2. [outer 2]3. [inner 1]4. [invocation 1]5. [invocation 2]6. []'
+ );
+ }
+ }
+);
+
+class ContextualComponentMutableParamsTest extends RenderingTestCase {
+ render(templateStr, context = {}) {
+ super.render(
+ `${templateStr}{{this.model.val2}} `,
+ Object.assign(context, { model: { val2: 8 } })
+ );
+ }
+}
+
+class MutableParamTestGenerator {
+ constructor(cases) {
+ this.cases = cases;
+ }
+
+ generate({ title, setup }) {
+ return {
+ [`@test parameters in a contextual component are mutable when value is a ${title}`](assert) {
+ this.owner.register(
+ 'component:change-button',
+ setComponentTemplate(
+ precompileTemplate(
+ 'Change to 10 '
+ ),
+ class extends EmberComponent {
+ static positionalParams = ['val'];
+ }
+ )
+ );
+
+ setup.call(this, assert);
+
+ assert.equal(this.$('.value').text(), '8');
+
+ runTask(() => this.rerender());
+
+ assert.equal(this.$('.value').text(), '8');
+
+ runTask(() => this.$('.my-button').click());
+
+ assert.equal(this.$('.value').text(), '10');
+
+ runTask(() => this.context.set('model', { val2: 8 }));
+
+ assert.equal(this.$('.value').text(), '8');
+ },
+ };
+ }
+}
+
+applyMixins(
+ ContextualComponentMutableParamsTest,
+ new MutableParamTestGenerator([
+ {
+ title: 'param',
+ setup() {
+ this.render('{{component (component "change-button" this.model.val2)}}');
+ },
+ },
+
+ {
+ title: 'nested param',
+ setup() {
+ this.owner.register(
+ 'component:my-comp',
+ setComponentTemplate(
+ precompileTemplate('{{component this.components.comp}}'),
+ class extends EmberComponent {
+ static positionalParams = ['components'];
+ }
+ )
+ );
+
+ this.render('{{my-comp (hash comp=(component "change-button" this.model.val2))}}');
+ },
+ },
+ ])
+);
+
+moduleFor(
+ 'Components test: contextual components -- mutable params (classic component)',
+ ContextualComponentMutableParamsTest
+);
diff --git a/packages/@ember/-internals/glimmer/tests/integration/components/classic/curly-components-test.js b/packages/@ember/-internals/glimmer/tests/integration/components/classic/curly-components-test.js
new file mode 100644
index 00000000000..5b4ac8e7fad
--- /dev/null
+++ b/packages/@ember/-internals/glimmer/tests/integration/components/classic/curly-components-test.js
@@ -0,0 +1,2477 @@
+import {
+ moduleFor,
+ RenderingTestCase,
+ strip,
+ classes,
+ runTask,
+ runLoopSettled,
+ expectDeprecation,
+ testUnless,
+} from 'internal-test-helpers';
+import { DEPRECATIONS } from '../../../../../deprecations';
+import { action } from '@ember/object';
+import { run } from '@ember/runloop';
+import { DEBUG } from '@glimmer/env';
+import { tracked } from '@ember/-internals/metal';
+import { on } from '@ember/object/evented';
+import { service } from '@ember/service';
+import { set, get, computed, observer } from '@ember/object';
+import { trackedArray } from '@ember/reactive/collections';
+import { Component } from '../../../utils/helpers';
+import { template } from '@ember/template-compiler/runtime';
+import { setComponentTemplate } from '@glimmer/manager';
+import { precompileTemplate } from '@ember/template-compilation';
+
+moduleFor(
+ 'Components test: curly components (classic component)',
+ class extends RenderingTestCase {
+ ['@test it can have a custom id and it is not bound']() {
+ this.owner.register(
+ 'component:foo-bar',
+ setComponentTemplate(
+ precompileTemplate('{{this.id}} {{this.elementId}}'),
+ class extends Component {}
+ )
+ );
+
+ this.render('{{foo-bar id=this.customId}}', {
+ customId: 'bizz',
+ });
+
+ this.assertComponentElement(this.firstChild, {
+ tagName: 'div',
+ attrs: { id: 'bizz' },
+ content: 'bizz bizz',
+ });
+
+ runTask(() => this.rerender());
+
+ this.assertComponentElement(this.firstChild, {
+ tagName: 'div',
+ attrs: { id: 'bizz' },
+ content: 'bizz bizz',
+ });
+
+ runTask(() => set(this.context, 'customId', 'bar'));
+
+ this.assertComponentElement(this.firstChild, {
+ tagName: 'div',
+ attrs: { id: 'bizz' },
+ content: 'bar bizz',
+ });
+
+ runTask(() => set(this.context, 'customId', 'bizz'));
+
+ this.assertComponentElement(this.firstChild, {
+ tagName: 'div',
+ attrs: { id: 'bizz' },
+ content: 'bizz bizz',
+ });
+ }
+
+ ['@test elementId cannot change'](assert) {
+ let component;
+ let FooBarComponent = class extends Component {
+ elementId = 'blahzorz';
+ init() {
+ super.init(...arguments);
+ component = this;
+ }
+ };
+
+ this.owner.register(
+ 'component:foo-bar',
+ setComponentTemplate(precompileTemplate('{{this.elementId}}'), FooBarComponent)
+ );
+
+ this.render('{{foo-bar}}');
+
+ this.assertComponentElement(this.firstChild, {
+ tagName: 'div',
+ attrs: { id: 'blahzorz' },
+ content: 'blahzorz',
+ });
+
+ if (DEBUG) {
+ let willThrow = () => run(null, set, component, 'elementId', 'herpyderpy');
+
+ assert.throws(willThrow, /Changing a view's elementId after creation is not allowed/);
+
+ this.assertComponentElement(this.firstChild, {
+ tagName: 'div',
+ attrs: { id: 'blahzorz' },
+ content: 'blahzorz',
+ });
+ }
+ }
+
+ ['@test elementId is stable when other values change']() {
+ let changingArg = 'arbitrary value';
+ let parentInstance;
+ this.owner.register(
+ 'component:foo-bar',
+ setComponentTemplate(
+ precompileTemplate('{{quux-baz elementId="stable-id" changingArg=this.changingArg}}'),
+ class extends Component {
+ init() {
+ super.init(...arguments);
+ parentInstance = this;
+ }
+ changingArg = changingArg;
+ }
+ )
+ );
+
+ this.owner.register(
+ 'component:quux-baz',
+ setComponentTemplate(precompileTemplate('{{this.changingArg}}'), class extends Component {})
+ );
+
+ this.render('{{foo-bar}}');
+ this.assertComponentElement(this.firstChild.firstChild, {
+ attrs: { id: 'stable-id' },
+ content: 'arbitrary value',
+ });
+
+ changingArg = 'a different value';
+ runTask(() => set(parentInstance, 'changingArg', changingArg));
+ this.assertComponentElement(this.firstChild.firstChild, {
+ attrs: { id: 'stable-id' },
+ content: changingArg,
+ });
+ }
+
+ ['@test can specify template with `layoutName` property']() {
+ let FooBarComponent = class extends Component {
+ elementId = 'blahzorz';
+ layoutName = 'fizz-bar';
+ init() {
+ super.init(...arguments);
+ this.local = 'hey';
+ }
+ };
+
+ this.registerTemplate('fizz-bar', `FIZZ BAR {{this.local}}`);
+
+ this.owner.register('component:foo-bar', FooBarComponent);
+
+ this.render('{{foo-bar}}');
+
+ this.assertText('FIZZ BAR hey');
+ }
+
+ ['@test layout supports computed property']() {
+ let FooBarComponent = class extends Component {
+ elementId = 'blahzorz';
+ @computed
+ get layout() {
+ return precompileTemplate('so much layout wat {{this.lulz}}');
+ }
+ init() {
+ super.init(...arguments);
+ this.lulz = 'heyo';
+ }
+ };
+
+ this.owner.register('component:foo-bar', FooBarComponent);
+
+ this.render('{{foo-bar}}');
+
+ this.assertText('so much layout wat heyo');
+ }
+
+ ['@test passing undefined elementId results in a default elementId'](assert) {
+ let FooBarComponent = class extends Component {
+ tagName = 'h1';
+ };
+
+ this.owner.register(
+ 'component:foo-bar',
+ setComponentTemplate(precompileTemplate('something'), FooBarComponent)
+ );
+
+ this.render('{{foo-bar id=this.somethingUndefined}}');
+
+ let foundId = this.$('h1').attr('id');
+ assert.ok(
+ /^ember/.test(foundId),
+ 'Has a reasonable id attribute (found id=' + foundId + ').'
+ );
+
+ runTask(() => this.rerender());
+
+ let newFoundId = this.$('h1').attr('id');
+ assert.ok(
+ /^ember/.test(newFoundId),
+ 'Has a reasonable id attribute (found id=' + newFoundId + ').'
+ );
+
+ assert.equal(foundId, newFoundId);
+ }
+
+ ['@test id is an alias for elementId'](assert) {
+ let FooBarComponent = class extends Component {
+ tagName = 'h1';
+ };
+
+ this.owner.register(
+ 'component:foo-bar',
+ setComponentTemplate(precompileTemplate('something'), FooBarComponent)
+ );
+
+ this.render('{{foo-bar id="custom-id"}}');
+
+ let foundId = this.$('h1').attr('id');
+ assert.equal(foundId, 'custom-id');
+
+ runTask(() => this.rerender());
+
+ let newFoundId = this.$('h1').attr('id');
+ assert.equal(newFoundId, 'custom-id');
+
+ assert.equal(foundId, newFoundId);
+ }
+
+ ['@test cannot pass both id and elementId at the same time']() {
+ this.owner.register(
+ 'component:foo-bar',
+ setComponentTemplate(precompileTemplate(''), class extends Component {})
+ );
+
+ expectAssertion(() => {
+ this.render('{{foo-bar id="zomg" elementId="lol"}}');
+ }, /You cannot invoke a component with both 'id' and 'elementId' at the same time./);
+ }
+
+ ['@test it can have a custom tagName']() {
+ let FooBarComponent = class extends Component {
+ tagName = 'foo-bar';
+ };
+
+ this.owner.register(
+ 'component:foo-bar',
+ setComponentTemplate(precompileTemplate('hello'), FooBarComponent)
+ );
+
+ this.render('{{foo-bar}}');
+
+ this.assertComponentElement(this.firstChild, {
+ tagName: 'foo-bar',
+ content: 'hello',
+ });
+
+ runTask(() => this.rerender());
+
+ this.assertComponentElement(this.firstChild, {
+ tagName: 'foo-bar',
+ content: 'hello',
+ });
+ }
+
+ ['@test it can have a custom tagName set in the constructor']() {
+ let FooBarComponent = class extends Component {
+ init() {
+ super.init(...arguments);
+ this.tagName = 'foo-bar';
+ }
+ };
+
+ this.owner.register(
+ 'component:foo-bar',
+ setComponentTemplate(precompileTemplate('hello'), FooBarComponent)
+ );
+
+ this.render('{{foo-bar}}');
+
+ this.assertComponentElement(this.firstChild, {
+ tagName: 'foo-bar',
+ content: 'hello',
+ });
+
+ runTask(() => this.rerender());
+
+ this.assertComponentElement(this.firstChild, {
+ tagName: 'foo-bar',
+ content: 'hello',
+ });
+ }
+
+ ['@test it can have a custom tagName from the invocation']() {
+ this.owner.register(
+ 'component:foo-bar',
+ setComponentTemplate(precompileTemplate('hello'), class extends Component {})
+ );
+
+ this.render('{{foo-bar tagName="foo-bar"}}');
+
+ this.assertComponentElement(this.firstChild, {
+ tagName: 'foo-bar',
+ content: 'hello',
+ });
+
+ runTask(() => this.rerender());
+
+ this.assertComponentElement(this.firstChild, {
+ tagName: 'foo-bar',
+ content: 'hello',
+ });
+ }
+
+ ['@test tagName can not be a computed property']() {
+ let FooBarComponent = class extends Component {
+ @computed
+ get tagName() {
+ return 'foo-bar';
+ }
+ };
+
+ this.owner.register(
+ 'component:foo-bar',
+ setComponentTemplate(precompileTemplate('hello'), FooBarComponent)
+ );
+
+ expectAssertion(() => {
+ this.render('{{foo-bar}}');
+ }, /You cannot use a computed property for the component's `tagName` \(<.+?>\)\./);
+ }
+
+ ['@test class is applied before didInsertElement'](assert) {
+ let componentClass;
+ let FooBarComponent = class extends Component {
+ didInsertElement() {
+ componentClass = this.element.className;
+ }
+ };
+
+ this.owner.register(
+ 'component:foo-bar',
+ setComponentTemplate(precompileTemplate('hello'), FooBarComponent)
+ );
+
+ this.render('{{foo-bar class="foo-bar"}}');
+
+ assert.equal(componentClass, 'foo-bar ember-view');
+ }
+
+ ['@test it can have custom classNames']() {
+ let FooBarComponent = class extends Component {
+ classNames = ['foo', 'bar'];
+ };
+
+ this.owner.register(
+ 'component:foo-bar',
+ setComponentTemplate(precompileTemplate('hello'), FooBarComponent)
+ );
+
+ this.render('{{foo-bar}}');
+
+ this.assertComponentElement(this.firstChild, {
+ tagName: 'div',
+ attrs: { class: classes('ember-view foo bar') },
+ content: 'hello',
+ });
+
+ runTask(() => this.rerender());
+
+ this.assertComponentElement(this.firstChild, {
+ tagName: 'div',
+ attrs: { class: classes('ember-view foo bar') },
+ content: 'hello',
+ });
+ }
+
+ ['@test should not apply falsy class name']() {
+ this.owner.register(
+ 'component:foo-bar',
+ setComponentTemplate(precompileTemplate('hello'), class extends Component {})
+ );
+
+ this.render('{{foo-bar class=this.somethingFalsy}}', {
+ somethingFalsy: false,
+ });
+
+ this.assertComponentElement(this.firstChild, {
+ tagName: 'div',
+ attrs: { class: 'ember-view' },
+ content: 'hello',
+ });
+
+ runTask(() => this.rerender());
+
+ this.assertComponentElement(this.firstChild, {
+ tagName: 'div',
+ attrs: { class: 'ember-view' },
+ content: 'hello',
+ });
+ }
+
+ ['@test should update class using inline if, initially false, no alternate']() {
+ this.owner.register(
+ 'component:foo-bar',
+ setComponentTemplate(precompileTemplate('hello'), class extends Component {})
+ );
+
+ this.render('{{foo-bar class=(if this.predicate "thing") }}', {
+ predicate: false,
+ });
+
+ this.assertComponentElement(this.firstChild, {
+ tagName: 'div',
+ attrs: { class: 'ember-view' },
+ content: 'hello',
+ });
+
+ runTask(() => set(this.context, 'predicate', true));
+ runTask(() => this.rerender());
+
+ this.assertComponentElement(this.firstChild, {
+ tagName: 'div',
+ attrs: { class: classes('ember-view thing') },
+ content: 'hello',
+ });
+
+ runTask(() => set(this.context, 'predicate', false));
+ runTask(() => this.rerender());
+
+ this.assertComponentElement(this.firstChild, {
+ tagName: 'div',
+ attrs: { class: 'ember-view' },
+ content: 'hello',
+ });
+ }
+
+ ['@test should update class using inline if, initially true, no alternate']() {
+ this.owner.register(
+ 'component:foo-bar',
+ setComponentTemplate(precompileTemplate('hello'), class extends Component {})
+ );
+
+ this.render('{{foo-bar class=(if this.predicate "thing") }}', {
+ predicate: true,
+ });
+
+ this.assertComponentElement(this.firstChild, {
+ tagName: 'div',
+ attrs: { class: classes('ember-view thing') },
+ content: 'hello',
+ });
+
+ runTask(() => set(this.context, 'predicate', false));
+ runTask(() => this.rerender());
+
+ this.assertComponentElement(this.firstChild, {
+ tagName: 'div',
+ attrs: { class: 'ember-view' },
+ content: 'hello',
+ });
+
+ runTask(() => set(this.context, 'predicate', true));
+ runTask(() => this.rerender());
+
+ this.assertComponentElement(this.firstChild, {
+ tagName: 'div',
+ attrs: { class: classes('ember-view thing') },
+ content: 'hello',
+ });
+ }
+
+ ['@test class property on components can be dynamic']() {
+ this.owner.register(
+ 'component:foo-bar',
+ setComponentTemplate(precompileTemplate('hello'), class extends Component {})
+ );
+
+ this.render('{{foo-bar class=(if this.fooBar "foo-bar")}}', {
+ fooBar: true,
+ });
+
+ this.assertComponentElement(this.firstChild, {
+ content: 'hello',
+ attrs: { class: classes('ember-view foo-bar') },
+ });
+
+ runTask(() => this.rerender());
+
+ this.assertComponentElement(this.firstChild, {
+ content: 'hello',
+ attrs: { class: classes('ember-view foo-bar') },
+ });
+
+ runTask(() => set(this.context, 'fooBar', false));
+
+ this.assertComponentElement(this.firstChild, {
+ content: 'hello',
+ attrs: { class: classes('ember-view') },
+ });
+
+ runTask(() => set(this.context, 'fooBar', true));
+
+ this.assertComponentElement(this.firstChild, {
+ content: 'hello',
+ attrs: { class: classes('ember-view foo-bar') },
+ });
+ }
+
+ ['@test it can have custom classNames from constructor']() {
+ let FooBarComponent = class extends Component {
+ init() {
+ super.init(...arguments);
+ this.classNames = this.classNames.slice();
+ this.classNames.push('foo', 'bar', `outside-${this.get('extraClass')}`);
+ }
+ };
+
+ this.owner.register(
+ 'component:foo-bar',
+ setComponentTemplate(precompileTemplate('hello'), FooBarComponent)
+ );
+
+ this.render('{{foo-bar extraClass="baz"}}');
+
+ this.assertComponentElement(this.firstChild, {
+ tagName: 'div',
+ attrs: { class: classes('ember-view foo bar outside-baz') },
+ content: 'hello',
+ });
+
+ runTask(() => this.rerender());
+
+ this.assertComponentElement(this.firstChild, {
+ tagName: 'div',
+ attrs: { class: classes('ember-view foo bar outside-baz') },
+ content: 'hello',
+ });
+ }
+
+ ['@test it can set custom classNames from the invocation']() {
+ let FooBarComponent = class extends Component {
+ classNames = ['foo'];
+ };
+
+ this.owner.register(
+ 'component:foo-bar',
+ setComponentTemplate(precompileTemplate('hello'), FooBarComponent)
+ );
+
+ this.render(strip`
+ {{foo-bar class="bar baz"}}
+ {{foo-bar classNames="bar baz"}}
+ {{foo-bar}}
+ `);
+
+ this.assertComponentElement(this.nthChild(0), {
+ tagName: 'div',
+ attrs: { class: classes('ember-view foo bar baz') },
+ content: 'hello',
+ });
+ this.assertComponentElement(this.nthChild(1), {
+ tagName: 'div',
+ attrs: { class: classes('ember-view foo bar baz') },
+ content: 'hello',
+ });
+ this.assertComponentElement(this.nthChild(2), {
+ tagName: 'div',
+ attrs: { class: classes('ember-view foo') },
+ content: 'hello',
+ });
+
+ runTask(() => this.rerender());
+
+ this.assertComponentElement(this.nthChild(0), {
+ tagName: 'div',
+ attrs: { class: classes('ember-view foo bar baz') },
+ content: 'hello',
+ });
+ this.assertComponentElement(this.nthChild(1), {
+ tagName: 'div',
+ attrs: { class: classes('ember-view foo bar baz') },
+ content: 'hello',
+ });
+ this.assertComponentElement(this.nthChild(2), {
+ tagName: 'div',
+ attrs: { class: classes('ember-view foo') },
+ content: 'hello',
+ });
+ }
+
+ ['@test it has an element']() {
+ let instance;
+
+ let FooBarComponent = class extends Component {
+ init() {
+ super.init(...arguments);
+ instance = this;
+ }
+ };
+
+ this.owner.register(
+ 'component:foo-bar',
+ setComponentTemplate(precompileTemplate('hello'), FooBarComponent)
+ );
+
+ this.render('{{foo-bar}}');
+
+ let element1 = instance.element;
+
+ this.assertComponentElement(element1, { content: 'hello' });
+
+ runTask(() => this.rerender());
+
+ let element2 = instance.element;
+
+ this.assertComponentElement(element2, { content: 'hello' });
+
+ this.assertSameNode(element2, element1);
+ }
+
+ ['@test an empty component does not have childNodes'](assert) {
+ let fooBarInstance;
+ let FooBarComponent = class extends Component {
+ tagName = 'input';
+ init() {
+ super.init(...arguments);
+ fooBarInstance = this;
+ }
+ };
+
+ this.owner.register(
+ 'component:foo-bar',
+ setComponentTemplate(precompileTemplate(''), FooBarComponent)
+ );
+
+ this.render('{{foo-bar}}');
+
+ this.assertComponentElement(this.firstChild, { tagName: 'input' });
+
+ assert.strictEqual(fooBarInstance.element.childNodes.length, 0);
+
+ runTask(() => this.rerender());
+
+ this.assertComponentElement(this.firstChild, { tagName: 'input' });
+
+ assert.strictEqual(fooBarInstance.element.childNodes.length, 0);
+ }
+
+ ['@test it has the right parentView and childViews'](assert) {
+ let fooBarInstance, fooBarBazInstance;
+
+ let FooBarComponent = class extends Component {
+ init() {
+ super.init(...arguments);
+ fooBarInstance = this;
+ }
+ };
+
+ let FooBarBazComponent = class extends Component {
+ init() {
+ super.init(...arguments);
+ fooBarBazInstance = this;
+ }
+ };
+
+ this.owner.register(
+ 'component:foo-bar',
+ setComponentTemplate(precompileTemplate('foo-bar {{foo-bar-baz}}'), FooBarComponent)
+ );
+ this.owner.register(
+ 'component:foo-bar-baz',
+ setComponentTemplate(precompileTemplate('foo-bar-baz'), FooBarBazComponent)
+ );
+
+ this.render('{{foo-bar}}');
+ this.assertText('foo-bar foo-bar-baz');
+
+ assert.equal(fooBarInstance.parentView, this.component);
+ assert.equal(fooBarBazInstance.parentView, fooBarInstance);
+
+ assert.deepEqual(this.component.childViews, [fooBarInstance]);
+ assert.deepEqual(fooBarInstance.childViews, [fooBarBazInstance]);
+
+ runTask(() => this.rerender());
+ this.assertText('foo-bar foo-bar-baz');
+
+ assert.equal(fooBarInstance.parentView, this.component);
+ assert.equal(fooBarBazInstance.parentView, fooBarInstance);
+
+ assert.deepEqual(this.component.childViews, [fooBarInstance]);
+ assert.deepEqual(fooBarInstance.childViews, [fooBarBazInstance]);
+ }
+
+ ['@test it reflects named arguments as properties']() {
+ this.owner.register(
+ 'component:foo-bar',
+ setComponentTemplate(precompileTemplate('{{this.foo}}'), class extends Component {})
+ );
+
+ this.render('{{foo-bar foo=this.model.bar}}', {
+ model: {
+ bar: 'Hola',
+ },
+ });
+
+ this.assertText('Hola');
+
+ runTask(() => this.rerender());
+
+ this.assertText('Hola');
+
+ runTask(() => this.context.set('model.bar', 'Hello'));
+
+ this.assertText('Hello');
+
+ runTask(() => this.context.set('model', { bar: 'Hola' }));
+
+ this.assertText('Hola');
+ }
+
+ ['@test late bound layouts return the same definition'](assert) {
+ let templateIds = [];
+
+ // This is testing the scenario where you import a template and
+ // set it to the layout property:
+ //
+ // import Component from '@ember/component';
+ // import layout from './template';
+ //
+ // export default Component.extend({
+ // layout
+ // });
+ let hello = precompileTemplate('Hello');
+ let bye = precompileTemplate('Bye');
+
+ let FooBarComponent = class extends Component {
+ init() {
+ super.init(...arguments);
+ this.layout = this.cond ? hello : bye;
+ templateIds.push(this.layout.id);
+ }
+ };
+
+ this.owner.register('component:foo-bar', FooBarComponent);
+
+ this.render(
+ '{{foo-bar cond=true}}{{foo-bar cond=false}}{{foo-bar cond=true}}{{foo-bar cond=false}}'
+ );
+
+ let [t1, t2, t3, t4] = templateIds;
+ assert.equal(t1, t3);
+ assert.equal(t2, t4);
+ }
+
+ // Perhaps change this test to `{{this.attrs.someProp.value}}` when removing the deprecation?
+ ['@test non-block with properties on this.attrs']() {
+ expectDeprecation(() => {
+ this.owner.register(
+ 'component:non-block',
+ template('In layout - someProp: {{this.attrs.someProp}}', {
+ component: class extends Component {},
+ strictMode: false,
+ })
+ );
+ }, /Using {{this.attrs}} to reference named arguments has been deprecated. {{this.attrs.someProp}} should be updated to {{@someProp}}./);
+
+ this.render('{{non-block someProp=this.prop}}', {
+ prop: 'something here',
+ });
+
+ this.assertText('In layout - someProp: something here');
+
+ runTask(() => this.rerender());
+
+ this.assertText('In layout - someProp: something here');
+
+ runTask(() => this.context.set('prop', 'other thing there'));
+
+ this.assertText('In layout - someProp: other thing there');
+
+ runTask(() => this.context.set('prop', 'something here'));
+
+ this.assertText('In layout - someProp: something here');
+ }
+
+ ['@test non-block with properties overridden in init']() {
+ let instance;
+ this.owner.register(
+ 'component:non-block',
+ setComponentTemplate(
+ precompileTemplate('In layout - someProp: {{this.someProp}}'),
+ class extends Component {
+ init() {
+ super.init(...arguments);
+ instance = this;
+ this.someProp = 'value set in instance';
+ }
+ }
+ )
+ );
+
+ this.render('{{non-block someProp=this.prop}}', {
+ prop: 'something passed when invoked',
+ });
+
+ this.assertText('In layout - someProp: value set in instance');
+
+ runTask(() => this.rerender());
+
+ this.assertText('In layout - someProp: value set in instance');
+
+ runTask(() => this.context.set('prop', 'updated something passed when invoked'));
+
+ this.assertText('In layout - someProp: updated something passed when invoked');
+
+ runTask(() => instance.set('someProp', 'update value set in instance'));
+
+ this.assertText('In layout - someProp: update value set in instance');
+
+ runTask(() => this.context.set('prop', 'something passed when invoked'));
+ runTask(() => instance.set('someProp', 'value set in instance'));
+
+ this.assertText('In layout - someProp: value set in instance');
+ }
+
+ ['@test rerendering component with attrs from parent'](assert) {
+ let willUpdateCount = 0;
+ let didReceiveAttrsCount = 0;
+
+ function expectHooks({ willUpdate, didReceiveAttrs }, callback) {
+ willUpdateCount = 0;
+ didReceiveAttrsCount = 0;
+
+ callback();
+
+ if (willUpdate) {
+ assert.strictEqual(willUpdateCount, 1, 'The willUpdate hook was fired');
+ } else {
+ assert.strictEqual(willUpdateCount, 0, 'The willUpdate hook was not fired');
+ }
+
+ if (didReceiveAttrs) {
+ assert.strictEqual(didReceiveAttrsCount, 1, 'The didReceiveAttrs hook was fired');
+ } else {
+ assert.strictEqual(didReceiveAttrsCount, 0, 'The didReceiveAttrs hook was not fired');
+ }
+ }
+
+ this.owner.register(
+ 'component:non-block',
+ setComponentTemplate(
+ precompileTemplate('In layout - someProp: {{this.someProp}}'),
+ class extends Component {
+ didReceiveAttrs() {
+ didReceiveAttrsCount++;
+ }
+
+ willUpdate() {
+ willUpdateCount++;
+ }
+ }
+ )
+ );
+
+ expectHooks({ willUpdate: false, didReceiveAttrs: true }, () => {
+ this.render('{{non-block someProp=this.someProp}}', {
+ someProp: 'wycats',
+ });
+ });
+
+ this.assertText('In layout - someProp: wycats');
+
+ // Note: Hooks are not fired in Glimmer for idempotent re-renders
+ expectHooks({ willUpdate: false, didReceiveAttrs: false }, () => {
+ runTask(() => this.rerender());
+ });
+
+ this.assertText('In layout - someProp: wycats');
+
+ expectHooks({ willUpdate: true, didReceiveAttrs: true }, () => {
+ runTask(() => this.context.set('someProp', 'tomdale'));
+ });
+
+ this.assertText('In layout - someProp: tomdale');
+
+ // Note: Hooks are not fired in Glimmer for idempotent re-renders
+ expectHooks({ willUpdate: false, didReceiveAttrs: false }, () => {
+ runTask(() => this.rerender());
+ });
+
+ this.assertText('In layout - someProp: tomdale');
+
+ expectHooks({ willUpdate: true, didReceiveAttrs: true }, () => {
+ runTask(() => this.context.set('someProp', 'wycats'));
+ });
+
+ this.assertText('In layout - someProp: wycats');
+ }
+
+ ['@test setting a value for a computed property then later getting the value for that property works'](
+ assert
+ ) {
+ let componentInstance = null;
+
+ this.owner.register(
+ 'component:non-block',
+ setComponentTemplate(
+ precompileTemplate('foobar '),
+ class extends Component {
+ counter = computed({
+ set(key, value) {
+ return value;
+ },
+ });
+
+ init() {
+ super.init(...arguments);
+ componentInstance = this;
+ }
+
+ @action
+ myClick() {
+ let currentCounter = this.get('counter');
+
+ assert.equal(currentCounter, 0, 'the current `counter` value is correct');
+
+ let newCounter = currentCounter + 1;
+ this.set('counter', newCounter);
+
+ assert.equal(
+ this.get('counter'),
+ newCounter,
+ "getting the newly set `counter` property works; it's equal to the value we just set and not `undefined`"
+ );
+ }
+ }
+ )
+ );
+
+ this.render(`{{non-block counter=this.counter}}`, {
+ counter: 0,
+ });
+
+ runTask(() => this.$('button').click());
+
+ assert.equal(
+ componentInstance.get('counter'),
+ 1,
+ '`counter` incremented on click on the component and is not `undefined`'
+ );
+ }
+
+ // Perhaps change this test to `{{this.attrs.foo.value}}` when removing the deprecation?
+ ['@test this.attrs.foo === @foo === foo']() {
+ expectDeprecation(() => {
+ this.owner.register(
+ 'component:foo-bar',
+ template(
+ strip`
+ Args: {{this.attrs.value}} | {{@value}} | {{this.value}}
+ {{#each this.attrs.items as |item|}}
+ {{item}}
+ {{/each}}
+ {{#each @items as |item|}}
+ {{item}}
+ {{/each}}
+ {{#each this.items as |item|}}
+ {{item}}
+ {{/each}}
+ `,
+ { component: class extends Component {}, strictMode: false }
+ )
+ );
+ }, /Using {{this.attrs}} to reference named arguments has been deprecated. {{this.attrs..+}} should be updated to {{@.+}}./);
+
+ this.render('{{foo-bar value=this.model.value items=this.model.items}}', {
+ model: {
+ value: 'wat',
+ items: [1, 2, 3],
+ },
+ });
+
+ this.assertStableRerender();
+
+ runTask(() => {
+ this.context.set('model.value', 'lul');
+ this.context.set('model.items', [1]);
+ });
+
+ this.assertText(strip`Args: lul | lul | lul111`);
+
+ runTask(() => this.context.set('model', { value: 'wat', items: [1, 2, 3] }));
+
+ this.assertText('Args: wat | wat | wat123123123');
+ }
+
+ ['@test non-block with properties on self']() {
+ this.owner.register(
+ 'component:non-block',
+ setComponentTemplate(
+ precompileTemplate('In layout - someProp: {{this.someProp}}'),
+ class extends Component {}
+ )
+ );
+
+ this.render('{{non-block someProp=this.prop}}', {
+ prop: 'something here',
+ });
+
+ this.assertText('In layout - someProp: something here');
+
+ runTask(() => this.rerender());
+
+ this.assertText('In layout - someProp: something here');
+
+ runTask(() => this.context.set('prop', 'something else'));
+
+ this.assertText('In layout - someProp: something else');
+
+ runTask(() => this.context.set('prop', 'something here'));
+
+ this.assertText('In layout - someProp: something here');
+ }
+
+ ['@test block with properties on self']() {
+ this.owner.register(
+ 'component:with-block',
+ setComponentTemplate(
+ precompileTemplate('In layout - someProp: {{this.someProp}} - {{yield}}'),
+ class extends Component {}
+ )
+ );
+
+ this.render(
+ strip`
+ {{#with-block someProp=this.prop}}
+ In template
+ {{/with-block}}`,
+ {
+ prop: 'something here',
+ }
+ );
+
+ this.assertText('In layout - someProp: something here - In template');
+
+ runTask(() => this.rerender());
+
+ this.assertText('In layout - someProp: something here - In template');
+
+ runTask(() => this.context.set('prop', 'something else'));
+
+ this.assertText('In layout - someProp: something else - In template');
+
+ runTask(() => this.context.set('prop', 'something here'));
+
+ this.assertText('In layout - someProp: something here - In template');
+ }
+
+ // Perhaps change this test to `{{this.attrs.someProp.value}}` when removing the deprecation?
+ ['@test block with properties on this.attrs']() {
+ expectDeprecation(() => {
+ this.owner.register(
+ 'component:with-block',
+ template('In layout - someProp: {{this.attrs.someProp}} - {{yield}}', {
+ component: class extends Component {},
+ strictMode: false,
+ })
+ );
+ }, /Using {{this.attrs}} to reference named arguments has been deprecated. {{this.attrs.someProp}} should be updated to {{@someProp}}./);
+
+ this.render(
+ strip`
+ {{#with-block someProp=this.prop}}
+ In template
+ {{/with-block}}`,
+ {
+ prop: 'something here',
+ }
+ );
+
+ this.assertText('In layout - someProp: something here - In template');
+
+ runTask(() => this.rerender());
+
+ this.assertText('In layout - someProp: something here - In template');
+
+ runTask(() => this.context.set('prop', 'something else'));
+
+ this.assertText('In layout - someProp: something else - In template');
+
+ runTask(() => this.context.set('prop', 'something here'));
+
+ this.assertText('In layout - someProp: something here - In template');
+ }
+
+ ['@test arbitrary positional parameter conflict with hash parameter is reported']() {
+ this.owner.register(
+ 'component:sample-component',
+ template(
+ strip`
+ {{#each this.names as |name|}}
+ {{name}}
+ {{/each}}
+ `,
+ {
+ component: class extends Component {
+ static positionalParams = 'names';
+ },
+ strictMode: false,
+ }
+ )
+ );
+
+ expectAssertion(() => {
+ this.render(`{{sample-component "Foo" 4 "Bar" names=this.numbers id="args-3"}}`, {
+ numbers: [1, 2, 3],
+ });
+ }, 'You cannot specify positional parameters and the hash argument `names`.');
+ }
+
+ ['@test can use hash parameter instead of arbitrary positional param [GH #12444]']() {
+ this.owner.register(
+ 'component:sample-component',
+ template(
+ strip`
+ {{#each this.names as |name|}}
+ {{name}}
+ {{/each}}
+ `,
+ {
+ component: class extends Component {
+ static positionalParams = 'names';
+ },
+ strictMode: false,
+ }
+ )
+ );
+
+ this.render('{{sample-component names=this.things}}', {
+ things: trackedArray(['Foo', 4, 'Bar']),
+ });
+
+ this.assertText('Foo4Bar');
+
+ runTask(() => this.rerender());
+
+ this.assertText('Foo4Bar');
+
+ runTask(() => this.context.get('things').push(5));
+
+ this.assertText('Foo4Bar5');
+
+ runTask(() => this.context.get('things').shift());
+
+ this.assertText('4Bar5');
+
+ runTask(() => this.context.get('things').splice(0));
+
+ this.assertText('');
+
+ runTask(() => this.context.set('things', ['Foo', 4, 'Bar']));
+
+ this.assertText('Foo4Bar');
+ }
+
+ ['@test can use hash parameter instead of positional param'](assert) {
+ this.owner.register(
+ 'component:sample-component',
+ setComponentTemplate(
+ precompileTemplate('{{this.first}} - {{this.second}}'),
+ class extends Component {
+ static positionalParams = ['first', 'second'];
+ }
+ )
+ );
+
+ // TODO: Fix when id is implemented
+ this.render(strip`
+ {{sample-component "one" "two" elementId="two-positional"}}
+ {{sample-component "one" second="two" elementId="one-positional"}}
+ {{sample-component first="one" second="two" elementId="no-positional"}}`);
+
+ assert.equal(this.$('#two-positional').text(), 'one - two');
+ assert.equal(this.$('#one-positional').text(), 'one - two');
+ assert.equal(this.$('#no-positional').text(), 'one - two');
+
+ runTask(() => this.rerender());
+
+ assert.equal(this.$('#two-positional').text(), 'one - two');
+ assert.equal(this.$('#one-positional').text(), 'one - two');
+ assert.equal(this.$('#no-positional').text(), 'one - two');
+ }
+
+ ['@test with ariaRole specified']() {
+ this.owner.register(
+ 'component:aria-test',
+ setComponentTemplate(precompileTemplate('Here!'), class extends Component {})
+ );
+
+ this.render('{{aria-test ariaRole=this.role}}', {
+ role: 'main',
+ });
+
+ this.assertComponentElement(this.firstChild, { attrs: { role: 'main' } });
+
+ runTask(() => this.rerender());
+
+ this.assertComponentElement(this.firstChild, { attrs: { role: 'main' } });
+
+ runTask(() => this.context.set('role', 'input'));
+
+ this.assertComponentElement(this.firstChild, {
+ attrs: { role: 'input' },
+ });
+
+ runTask(() => this.context.set('role', 'main'));
+
+ this.assertComponentElement(this.firstChild, { attrs: { role: 'main' } });
+ }
+
+ ['@test with ariaRole defined but initially falsey GH#16379']() {
+ this.owner.register(
+ 'component:aria-test',
+ setComponentTemplate(precompileTemplate('Here!'), class extends Component {})
+ );
+
+ this.render('{{aria-test ariaRole=this.role}}', {
+ role: undefined,
+ });
+
+ this.assertComponentElement(this.firstChild, { attrs: {} });
+
+ runTask(() => this.rerender());
+
+ this.assertComponentElement(this.firstChild, { attrs: {} });
+
+ runTask(() => this.context.set('role', 'input'));
+
+ this.assertComponentElement(this.firstChild, {
+ attrs: { role: 'input' },
+ });
+
+ runTask(() => this.context.set('role', undefined));
+
+ this.assertComponentElement(this.firstChild, { attrs: {} });
+ }
+
+ ['@test without ariaRole defined initially']() {
+ // we are using the ability to lazily add a role as a sign that we are
+ // doing extra work
+ let instance;
+ this.owner.register(
+ 'component:aria-test',
+ setComponentTemplate(
+ precompileTemplate('Here!'),
+ class extends Component {
+ init() {
+ super.init(...arguments);
+ instance = this;
+ }
+ }
+ )
+ );
+
+ this.render('{{aria-test}}');
+
+ this.assertComponentElement(this.firstChild, { attrs: {} });
+
+ runTask(() => this.rerender());
+
+ this.assertComponentElement(this.firstChild, { attrs: {} });
+
+ runTask(() => instance.set('ariaRole', 'input'));
+
+ this.assertComponentElement(this.firstChild, { attrs: {} });
+ }
+
+ ['@test `template` specified in component is overridden by block']() {
+ this.owner.register(
+ 'component:with-template',
+ setComponentTemplate(
+ precompileTemplate('[In layout - {{this.name}}] {{yield}}'),
+ class extends Component {
+ template = 'Should not be used';
+ }
+ )
+ );
+
+ this.render(
+ strip`
+ {{#with-template name="with-block"}}
+ [In block - {{this.name}}]
+ {{/with-template}}
+ {{with-template name="without-block"}}`,
+ {
+ name: 'Whoop, whoop!',
+ }
+ );
+
+ this.assertText(
+ '[In layout - with-block] [In block - Whoop, whoop!][In layout - without-block] '
+ );
+
+ runTask(() => this.rerender());
+
+ this.assertText(
+ '[In layout - with-block] [In block - Whoop, whoop!][In layout - without-block] '
+ );
+
+ runTask(() => this.context.set('name', 'Ole, ole'));
+
+ this.assertText('[In layout - with-block] [In block - Ole, ole][In layout - without-block] ');
+
+ runTask(() => this.context.set('name', 'Whoop, whoop!'));
+
+ this.assertText(
+ '[In layout - with-block] [In block - Whoop, whoop!][In layout - without-block] '
+ );
+ }
+
+ ['@test if a value is passed as a non-positional parameter, it raises an assertion']() {
+ this.owner.register(
+ 'component:sample-component',
+ setComponentTemplate(
+ precompileTemplate('{{this.name}}'),
+ class extends Component {
+ static positionalParams = ['name'];
+ }
+ )
+ );
+
+ expectAssertion(() => {
+ this.render('{{sample-component this.notMyName name=this.myName}}', {
+ myName: 'Quint',
+ notMyName: 'Sergio',
+ });
+ }, 'You cannot specify both a positional param (at position 0) and the hash argument `name`.');
+ }
+
+ ['@test component in template of a yielding component should have the proper parentView'](
+ assert
+ ) {
+ let outer, innerTemplate, innerLayout;
+
+ this.owner.register(
+ 'component:x-outer',
+ setComponentTemplate(
+ precompileTemplate('{{x-inner-in-layout}}{{yield}}'),
+ class extends Component {
+ init() {
+ super.init(...arguments);
+ outer = this;
+ }
+ }
+ )
+ );
+
+ this.owner.register(
+ 'component:x-inner-in-template',
+ class extends Component {
+ init() {
+ super.init(...arguments);
+ innerTemplate = this;
+ }
+ }
+ );
+
+ this.owner.register(
+ 'component:x-inner-in-layout',
+ class extends Component {
+ init() {
+ super.init(...arguments);
+ innerLayout = this;
+ }
+ }
+ );
+
+ this.render('{{#x-outer}}{{x-inner-in-template}}{{/x-outer}}');
+
+ assert.equal(
+ innerTemplate.parentView,
+ outer,
+ 'receives the wrapping component as its parentView in template blocks'
+ );
+ assert.equal(
+ innerLayout.parentView,
+ outer,
+ 'receives the wrapping component as its parentView in layout'
+ );
+ assert.equal(
+ outer.parentView,
+ this.context,
+ 'x-outer receives the ambient scope as its parentView'
+ );
+
+ runTask(() => this.rerender());
+
+ assert.equal(
+ innerTemplate.parentView,
+ outer,
+ 'receives the wrapping component as its parentView in template blocks'
+ );
+ assert.equal(
+ innerLayout.parentView,
+ outer,
+ 'receives the wrapping component as its parentView in layout'
+ );
+ assert.equal(
+ outer.parentView,
+ this.context,
+ 'x-outer receives the ambient scope as its parentView'
+ );
+ }
+
+ ['@test newly-added sub-components get correct parentView'](assert) {
+ let outer, inner;
+
+ this.owner.register(
+ 'component:x-outer',
+ class extends Component {
+ init() {
+ super.init(...arguments);
+ outer = this;
+ }
+ }
+ );
+
+ this.owner.register(
+ 'component:x-inner',
+ class extends Component {
+ init() {
+ super.init(...arguments);
+ inner = this;
+ }
+ }
+ );
+
+ this.render(
+ strip`
+ {{#x-outer}}
+ {{#if this.showInner}}
+ {{x-inner}}
+ {{/if}}
+ {{/x-outer}}`,
+ {
+ showInner: false,
+ }
+ );
+
+ assert.equal(
+ outer.parentView,
+ this.context,
+ 'x-outer receives the ambient scope as its parentView'
+ );
+
+ runTask(() => this.rerender());
+
+ assert.equal(
+ outer.parentView,
+ this.context,
+ 'x-outer receives the ambient scope as its parentView (after rerender)'
+ );
+
+ runTask(() => this.context.set('showInner', true));
+
+ assert.equal(
+ outer.parentView,
+ this.context,
+ 'x-outer receives the ambient scope as its parentView'
+ );
+ assert.equal(
+ inner.parentView,
+ outer,
+ 'receives the wrapping component as its parentView in template blocks'
+ );
+
+ runTask(() => this.context.set('showInner', false));
+
+ assert.equal(
+ outer.parentView,
+ this.context,
+ 'x-outer receives the ambient scope as its parentView'
+ );
+ }
+
+ ['@test specifying classNames results in correct class'](assert) {
+ this.owner.register(
+ 'component:some-clicky-thing',
+ class extends Component {
+ tagName = 'button';
+ classNames = ['foo', 'bar'];
+ }
+ );
+
+ this.render(strip`
+ {{#some-clicky-thing classNames="baz"}}
+ Click Me
+ {{/some-clicky-thing}}`);
+
+ // TODO: ember-view is no longer viewable in the classNames array. Bug or
+ // feature?
+ let expectedClassNames = ['ember-view', 'foo', 'bar', 'baz'];
+
+ assert.ok(
+ this.$('button').is('.foo.bar.baz.ember-view'),
+ `the element has the correct classes: ${this.$('button').attr('class')}`
+ );
+ // `ember-view` is no longer in classNames.
+ // assert.deepEqual(clickyThing.get('classNames'), expectedClassNames, 'classNames are properly combined');
+ this.assertComponentElement(this.firstChild, {
+ tagName: 'button',
+ attrs: { class: classes(expectedClassNames.join(' ')) },
+ });
+
+ runTask(() => this.rerender());
+
+ assert.ok(
+ this.$('button').is('.foo.bar.baz.ember-view'),
+ `the element has the correct classes: ${this.$('button').attr('class')} (rerender)`
+ );
+ // `ember-view` is no longer in classNames.
+ // assert.deepEqual(clickyThing.get('classNames'), expectedClassNames, 'classNames are properly combined (rerender)');
+ this.assertComponentElement(this.firstChild, {
+ tagName: 'button',
+ attrs: { class: classes(expectedClassNames.join(' ')) },
+ });
+ }
+
+ ['@test specifying custom concatenatedProperties avoids clobbering']() {
+ this.owner.register(
+ 'component:some-clicky-thing',
+ template(
+ strip`
+ {{#each this.blahzz as |p|}}
+ {{p}}
+ {{/each}}
+ - {{yield}}
+ `,
+ {
+ component: class extends Component {
+ concatenatedProperties = ['blahzz'];
+ blahzz = ['blark', 'pory'];
+ },
+ strictMode: false,
+ }
+ )
+ );
+
+ this.render(strip`
+ {{#some-clicky-thing blahzz="baz"}}
+ Click Me
+ {{/some-clicky-thing}}`);
+
+ this.assertText('blarkporybaz- Click Me');
+
+ runTask(() => this.rerender());
+
+ this.assertText('blarkporybaz- Click Me');
+ }
+
+ ['@test a two way binding flows upstream when consumed in the template']() {
+ let component;
+ let FooBarComponent = class extends Component {
+ init() {
+ super.init(...arguments);
+ component = this;
+ }
+ };
+
+ this.owner.register(
+ 'component:foo-bar',
+ setComponentTemplate(precompileTemplate('{{this.bar}}'), FooBarComponent)
+ );
+
+ this.render('{{this.localBar}} - {{foo-bar bar=this.localBar}}', {
+ localBar: 'initial value',
+ });
+
+ this.assertText('initial value - initial value');
+
+ runTask(() => this.rerender());
+
+ this.assertText('initial value - initial value');
+
+ runTask(() => {
+ component.set('bar', 'updated value');
+ });
+
+ this.assertText('updated value - updated value');
+
+ runTask(() => {
+ component.set('bar', undefined);
+ });
+
+ this.assertText(' - ');
+
+ runTask(() => {
+ this.component.set('localBar', 'initial value');
+ });
+
+ this.assertText('initial value - initial value');
+ }
+
+ ['@test a two way binding flows upstream through a CP when consumed in the template']() {
+ let component;
+ let FooBarComponent = class extends Component {
+ init() {
+ super.init(...arguments);
+ component = this;
+ }
+
+ @computed
+ get bar() {
+ return this._bar;
+ }
+ set bar(value) {
+ this._bar = value;
+ }
+ };
+
+ this.owner.register(
+ 'component:foo-bar',
+ setComponentTemplate(precompileTemplate('{{this.bar}}'), FooBarComponent)
+ );
+
+ this.render('{{this.localBar}} - {{foo-bar bar=this.localBar}}', {
+ localBar: 'initial value',
+ });
+
+ this.assertText('initial value - initial value');
+
+ runTask(() => this.rerender());
+
+ this.assertText('initial value - initial value');
+
+ runTask(() => {
+ component.set('bar', 'updated value');
+ });
+
+ this.assertText('updated value - updated value');
+
+ runTask(() => {
+ this.component.set('localBar', 'initial value');
+ });
+
+ this.assertText('initial value - initial value');
+ }
+
+ ['@test a two way binding flows upstream through a CP without template consumption']() {
+ let component;
+ let FooBarComponent = class extends Component {
+ init() {
+ super.init(...arguments);
+ component = this;
+ }
+
+ @computed
+ get bar() {
+ return this._bar;
+ }
+
+ set bar(value) {
+ this._bar = value;
+ }
+ };
+
+ this.owner.register(
+ 'component:foo-bar',
+ setComponentTemplate(precompileTemplate(''), FooBarComponent)
+ );
+
+ this.render('{{this.localBar}}{{foo-bar bar=this.localBar}}', {
+ localBar: 'initial value',
+ });
+
+ this.assertText('initial value');
+
+ runTask(() => this.rerender());
+
+ this.assertText('initial value');
+
+ runTask(() => {
+ component.set('bar', 'updated value');
+ });
+
+ this.assertText('updated value');
+
+ runTask(() => {
+ this.component.set('localBar', 'initial value');
+ });
+
+ this.assertText('initial value');
+ }
+
+ ['@test GH#18417 - a two way binding flows upstream to a parent component through a CP']() {
+ let parent, child;
+ let ParentComponent = class extends Component {
+ init() {
+ super.init(...arguments);
+ parent = this;
+ }
+
+ @tracked string = 'Hello|World';
+ };
+
+ this.owner.register(
+ 'component:parent',
+ setComponentTemplate(
+ precompileTemplate(
+ '{{child value=this.string}}Parent String={{this.string}} '
+ ),
+ ParentComponent
+ )
+ );
+
+ let ChildComponent = class extends Component {
+ init() {
+ super.init(...arguments);
+ child = this;
+ }
+
+ a = null; // computed based on passed in value of `string`
+ b = null; // computed based on passed in value of `string`
+
+ @computed('a', 'b')
+ get value() {
+ return this.a + '|' + this.b;
+ }
+
+ set value(value) {
+ let vals = value.split('|');
+ this.set('a', vals[0]);
+ this.set('b', vals[1]);
+ }
+ };
+
+ this.owner.register(
+ 'component:child',
+ setComponentTemplate(precompileTemplate('{{this.value}}'), ChildComponent)
+ );
+
+ this.render('{{parent}}');
+
+ this.assert.equal(parent.string, 'Hello|World', 'precond - parent value');
+ this.assert.equal(
+ this.element.querySelector('[data-test-parent-value]').textContent.trim(),
+ 'Hello|World',
+ 'precond - parent rendered value'
+ );
+
+ runTask(() => {
+ child.set('a', 'Foo');
+ });
+
+ this.assert.equal(parent.string, 'Foo|World', 'parent value updated');
+
+ this.assert.equal(
+ this.element.querySelector('[data-test-parent-value]').textContent.trim(),
+ 'Foo|World',
+ 'parent updated value rendered'
+ );
+
+ runTask(() => {
+ child.set('a', 'Hello');
+ });
+
+ this.assert.equal(parent.string, 'Hello|World', 'parent value reset');
+ this.assert.equal(
+ this.element.querySelector('[data-test-parent-value]').textContent.trim(),
+ 'Hello|World',
+ 'parent template reset'
+ );
+ }
+
+ ['@test injecting an unknown service raises an exception']() {
+ this.owner.register(
+ 'component:foo-bar',
+ class extends Component {
+ @service
+ missingService;
+ }
+ );
+
+ expectAssertion(() => {
+ this.render('{{foo-bar}}');
+ }, "Attempting to inject an unknown injection: 'service:missingService'");
+ }
+
+ ['@test throws if `super.init` is not called from `init`']() {
+ this.owner.register(
+ 'component:foo-bar',
+ class extends Component {
+ init() {}
+ }
+ );
+
+ expectAssertion(() => {
+ this.render('{{foo-bar}}');
+ }, /You must call `super.init\(...arguments\);` or `this._super\(...arguments\)` when overriding `init` on a framework object. Please update .*/);
+ }
+
+ ['@test it can use readDOMAttr to read input value']() {
+ let component;
+ let assertElement = (expectedValue) => {
+ // value is a property, not an attribute
+ this.assertHTML(` `);
+ this.assert.equal(this.firstChild.value, expectedValue, 'value property is correct');
+ this.assert.equal(
+ get(component, 'value'),
+ expectedValue,
+ 'component.get("value") is correct'
+ );
+ };
+
+ this.owner.register(
+ 'component:one-way-input',
+ class extends Component {
+ tagName = 'input';
+ attributeBindings = ['value'];
+
+ init() {
+ super.init(...arguments);
+ component = this;
+ }
+
+ change() {
+ let value = this.readDOMAttr('value');
+ this.set('value', value);
+ }
+ }
+ );
+
+ this.render('{{one-way-input value=this.value}}', {
+ value: 'foo',
+ });
+
+ assertElement('foo');
+
+ this.assertStableRerender();
+
+ runTask(() => {
+ this.firstChild.value = 'bar';
+ this.$('input').trigger('change');
+ });
+
+ assertElement('bar');
+
+ runTask(() => {
+ this.firstChild.value = 'foo';
+ this.$('input').trigger('change');
+ });
+
+ assertElement('foo');
+
+ runTask(() => {
+ set(component, 'value', 'bar');
+ });
+
+ assertElement('bar');
+
+ runTask(() => {
+ this.firstChild.value = 'foo';
+ this.$('input').trigger('change');
+ });
+
+ assertElement('foo');
+ }
+
+ ['@test child triggers revalidate during parent destruction (GH#13846)']() {
+ this.owner.register(
+ 'component:x-select',
+ setComponentTemplate(
+ precompileTemplate('{{yield this}}'),
+ class extends Component {
+ tagName = 'select';
+
+ init() {
+ super.init(...arguments);
+ this.options = [];
+ this.value = null;
+ }
+
+ updateValue() {
+ let newValue = this.options.at(-1)?.value;
+
+ this.set('value', newValue);
+ }
+
+ registerOption(option) {
+ this.options.push(option);
+ }
+
+ unregisterOption(option) {
+ this.options.splice(this.options.indexOf(option), 1);
+
+ this.updateValue();
+ }
+ }
+ )
+ );
+
+ this.owner.register(
+ 'component:x-option',
+ class extends Component {
+ tagName = 'option';
+ attributeBindings = ['selected'];
+
+ didInsertElement() {
+ super.didInsertElement(...arguments);
+
+ this.get('select').registerOption(this);
+ }
+
+ @computed('select.value')
+ get selected() {
+ return this.get('value') === this.get('select.value');
+ }
+
+ willDestroyElement() {
+ super.willDestroyElement(...arguments);
+ this.get('select').unregisterOption(this);
+ }
+ }
+ );
+
+ this.render(strip`
+ {{#x-select value=this.value as |select|}}
+ {{#x-option value="1" select=select}}1{{/x-option}}
+ {{#x-option value="2" select=select}}2{{/x-option}}
+ {{/x-select}}
+ `);
+
+ this.teardown();
+
+ this.assert.ok(true, 'no errors during teardown');
+ }
+
+ ['@test setting a property in willDestroyElement does not assert (GH#14273)'](assert) {
+ assert.expect(2);
+
+ this.owner.register(
+ 'component:foo-bar',
+ setComponentTemplate(
+ precompileTemplate(`{{#if this.showFoo}}things{{/if}}`),
+ class extends Component {
+ init() {
+ super.init(...arguments);
+ this.showFoo = true;
+ }
+
+ willDestroyElement() {
+ this.set('showFoo', false);
+ assert.ok(true, 'willDestroyElement was fired');
+ super.willDestroyElement(...arguments);
+ }
+ }
+ )
+ );
+
+ this.render(`{{foo-bar}}`);
+
+ this.assertText('things');
+ }
+
+ async ['@test didReceiveAttrs fires after .init() but before observers become active'](assert) {
+ let barCopyDidChangeCount = 0;
+
+ this.owner.register(
+ 'component:foo-bar',
+ setComponentTemplate(
+ precompileTemplate('{{this.bar}}-{{this.barCopy}}'),
+ Component.extend({
+ init() {
+ this._super(...arguments);
+ this.didInit = true;
+ },
+
+ didReceiveAttrs() {
+ assert.ok(this.didInit, 'expected init to have run before didReceiveAttrs');
+ this.set('barCopy', this.attrs.bar.value + 1);
+ },
+
+ barCopyDidChange: observer('barCopy', () => {
+ barCopyDidChangeCount++;
+ }),
+ })
+ )
+ );
+
+ await this.render(`{{foo-bar bar=this.bar}}`, { bar: 3 });
+
+ this.assertText('3-4');
+
+ assert.strictEqual(barCopyDidChangeCount, 1, 'expected observer firing for: barCopy');
+
+ set(this.context, 'bar', 7);
+
+ await runLoopSettled();
+
+ this.assertText('7-8');
+
+ assert.strictEqual(barCopyDidChangeCount, 2, 'expected observer firing for: barCopy');
+ }
+
+ ['@test overriding didReceiveAttrs does not trigger deprecation'](assert) {
+ this.owner.register(
+ 'component:foo-bar',
+ setComponentTemplate(
+ precompileTemplate('{{this.foo}}-{{this.fooCopy}}-{{this.bar}}-{{this.barCopy}}'),
+ class extends Component {
+ didReceiveAttrs() {
+ assert.equal(1, this.get('foo'), 'expected attrs to have correct value');
+ }
+ }
+ )
+ );
+
+ this.render(`{{foo-bar foo=this.foo bar=this.bar}}`, { foo: 1, bar: 3 });
+ }
+
+ ['@test overriding didUpdateAttrs does not trigger deprecation'](assert) {
+ this.owner.register(
+ 'component:foo-bar',
+ setComponentTemplate(
+ precompileTemplate('{{this.foo}}-{{this.fooCopy}}-{{this.bar}}-{{this.barCopy}}'),
+ class extends Component {
+ didUpdateAttrs() {
+ assert.equal(5, this.get('foo'), 'expected newAttrs to have new value');
+ }
+ }
+ )
+ );
+
+ this.render(`{{foo-bar foo=this.foo bar=this.bar}}`, { foo: 1, bar: 3 });
+
+ runTask(() => set(this.context, 'foo', 5));
+ }
+
+ [`${testUnless(DEPRECATIONS.DEPRECATE_EVENTED.isRemoved)} @test triggering an event only attempts to invoke an identically named method, if it actually is a function (GH#15228)`](
+ assert
+ ) {
+ assert.expect(5);
+
+ let payload = ['arbitrary', 'event', 'data'];
+
+ expectDeprecation(
+ () => {
+ this.owner.register(
+ 'component:evented-component',
+ Component.extend({
+ someTruthyProperty: true,
+
+ init() {
+ this._super(...arguments);
+ expectDeprecation(
+ () => {
+ this.trigger('someMethod', ...payload);
+ this.trigger('someTruthyProperty', ...payload);
+ },
+ /Evented#trigger` is deprecated/,
+ DEPRECATIONS.DEPRECATE_EVENTED.isEnabled
+ );
+ },
+
+ someMethod(...data) {
+ assert.deepEqual(
+ data,
+ payload,
+ 'the method `someMethod` should be called, when `someMethod` is triggered'
+ );
+ },
+
+ listenerForSomeMethod: on('someMethod', function (...data) {
+ assert.deepEqual(
+ data,
+ payload,
+ 'the listener `listenerForSomeMethod` should be called, when `someMethod` is triggered'
+ );
+ }),
+
+ listenerForSomeTruthyProperty: on('someTruthyProperty', function (...data) {
+ assert.deepEqual(
+ data,
+ payload,
+ 'the listener `listenerForSomeTruthyProperty` should be called, when `someTruthyProperty` is triggered'
+ );
+ }),
+ })
+ );
+ },
+ /`on\(\)` event decorator is deprecated/,
+ DEPRECATIONS.DEPRECATE_EVENTED.isEnabled
+ );
+
+ this.render(`{{evented-component}}`);
+ }
+
+ ['@test unimplimented positionalParams do not cause an error GH#14416']() {
+ this.owner.register(
+ 'component:foo-bar',
+ setComponentTemplate(precompileTemplate('hello'), class extends Component {})
+ );
+
+ this.render('{{foo-bar this.wat}}');
+ this.assertText('hello');
+ }
+
+ ['@test using attrs for positional params is asserted against']() {
+ let MyComponent = class extends Component {};
+
+ expectAssertion(() => {
+ this.owner.register(
+ 'component:foo-bar',
+ template(
+ 'MyVar1: {{attrs.myVar}} {{this.myVar}} MyVar2: {{this.myVar2}} {{attrs.myVar2}}',
+ {
+ component: MyComponent.reopenClass({ positionalParams: ['myVar'] }),
+ strictMode: false,
+ }
+ )
+ );
+ }, 'Using {{attrs}} to reference named arguments is not supported. {{attrs.myVar}} should be updated to {{@myVar}}. (L1:C10) ');
+ }
+
+ // Perhaps change this test to `{{this.attrs.myVar.value}}` when removing the deprecation?
+ ['@test using this.attrs for positional params']() {
+ let MyComponent = class extends Component {};
+
+ expectDeprecation(() => {
+ this.owner.register(
+ 'component:foo-bar',
+ template(
+ 'MyVar1: {{this.attrs.myVar}} {{this.myVar}} MyVar2: {{this.myVar2}} {{this.attrs.myVar2}}',
+ {
+ component: MyComponent.reopenClass({ positionalParams: ['myVar'] }),
+ strictMode: false,
+ }
+ )
+ );
+ }, /Using {{this.attrs}} to reference named arguments has been deprecated. {{this.attrs.myVar2?}} should be updated to {{@myVar2?}}./);
+
+ this.render('{{foo-bar 1 myVar2=2}}');
+
+ this.assertText('MyVar1: 1 1 MyVar2: 2 2');
+ }
+
+ ['@test using named arguments for positional params']() {
+ let MyComponent = class extends Component {};
+
+ this.owner.register(
+ 'component:foo-bar',
+ setComponentTemplate(
+ precompileTemplate(
+ 'MyVar1: {{@myVar}} {{this.myVar}} MyVar2: {{this.myVar2}} {{@myVar2}}'
+ ),
+ MyComponent.reopenClass({
+ positionalParams: ['myVar'],
+ })
+ )
+ );
+
+ this.render('{{foo-bar 1 myVar2=2}}');
+
+ this.assertText('MyVar1: 1 1 MyVar2: 2 2');
+ }
+
+ ['@test has attrs by didReceiveAttrs with native classes'](assert) {
+ class FooBarComponent extends Component {
+ constructor(injections) {
+ super(injections);
+ // analogous to class field defaults
+ this.foo = 'bar';
+ }
+
+ didReceiveAttrs() {
+ assert.equal(this.foo, 'bar', 'received default attrs correctly');
+ }
+ }
+
+ this.owner.register('component:foo-bar', FooBarComponent);
+
+ this.render('{{foo-bar}}');
+ }
+
+ '@test lifecycle hooks are not tracked'() {
+ this.owner.register(
+ 'component:foo-bar',
+ setComponentTemplate(
+ precompileTemplate('{{this.baz}}'),
+ class extends Component {
+ @tracked foo;
+
+ willInsertElement() {
+ this.foo;
+ this.foo = 123;
+ }
+
+ willRender() {
+ this.foo;
+ this.foo = 123;
+ }
+
+ didRender() {
+ this.foo;
+ this.foo = 123;
+ }
+
+ didReceiveAttrs() {
+ this.foo;
+ this.foo = 123;
+ }
+
+ didUpdate() {
+ this.foo;
+ this.foo = 123;
+ }
+
+ didUpdateAttrs() {
+ this.foo;
+ this.foo = 123;
+ }
+
+ didInsertElement() {
+ this.foo;
+ this.foo = 123;
+ }
+
+ willClearRender() {
+ this.foo;
+ this.foo = 123;
+ }
+
+ willDestroyElement() {
+ this.foo;
+ this.foo = 123;
+ }
+
+ didDestroyElement() {
+ this.foo;
+ this.foo = 123;
+ }
+ }
+ )
+ );
+
+ this.render('{{#if this.cond}}{{foo-bar baz=this.value}}{{/if}}', {
+ cond: true,
+ value: 'hello',
+ });
+
+ this.assertComponentElement(this.firstChild, { content: 'hello' });
+
+ runTask(() => set(this.context, 'value', 'world'));
+
+ this.assertComponentElement(this.firstChild, { content: 'world' });
+
+ runTask(() => set(this.context, 'cond', false));
+ }
+
+ '@test tracked property mutation in init does not error'() {
+ // TODO: this should issue a deprecation, but since the curly manager
+ // uses an untracked frame for construction we don't (yet)
+ this.owner.register(
+ 'component:foo-bar',
+ setComponentTemplate(
+ precompileTemplate(`{{this.itemCount}}`),
+ class extends Component {
+ @tracked itemCount = 0;
+
+ init() {
+ super.init(...arguments);
+
+ // first read the tracked property
+ let { itemCount } = this;
+
+ // then attempt to update the tracked property
+ this.itemCount = itemCount + 1;
+ }
+ }
+ )
+ );
+
+ this.render(' ');
+
+ this.assertComponentElement(this.firstChild, { content: '1' });
+ }
+
+ '@test a block yielded by a tagless component sees the caller scope, the layout sees its own'() {
+ this.owner.register(
+ 'component:foo-bar',
+ setComponentTemplate(
+ precompileTemplate(
+ '[Layout: {{this.zomg}}][Layout: {{this.lol}}][Layout: {{this.foo}}]{{yield}}'
+ ),
+ class extends Component {
+ tagName = '';
+ }
+ )
+ );
+
+ this.render(
+ strip`
+
+ [Outside: {{this.zomg}}]
+ {{#let this.zomg as |lol|}}
+ [Inside: {{this.zomg}}]
+ [Inside: {{lol}}]
+ {{#foo-bar foo=this.zomg}}
+ [Block: {{this.zomg}}]
+ [Block: {{lol}}]
+ {{/foo-bar}}
+ {{/let}}
+
`,
+ { zomg: 'zomg' }
+ );
+
+ this.assertText(
+ '[Outside: zomg][Inside: zomg][Inside: zomg][Layout: ][Layout: ][Layout: zomg][Block: zomg][Block: zomg]'
+ );
+
+ this.assertStableRerender();
+ }
+
+ '@test a component with a slashed name can be invoked with curlies'() {
+ this.owner.register(
+ 'component:fizz-bar/baz-bar',
+ setComponentTemplate(precompileTemplate('{{this.hey}}'), class extends Component {})
+ );
+
+ this.render('{{fizz-bar/baz-bar hey="hello"}}');
+
+ this.assertComponentElement(this.firstChild, { tagName: 'div', content: 'hello' });
+
+ this.assertStableRerender();
+ }
+
+ '@test static arbitrary number of positional parameters'() {
+ this.owner.register(
+ 'component:sample-component',
+ setComponentTemplate(
+ precompileTemplate('{{#each this.names key="@index" as |name|}}{{name}}{{/each}}'),
+ class extends Component {
+ static positionalParams = 'names';
+ }
+ )
+ );
+
+ this.render(strip`
+ {{sample-component "Foo" 4 "Bar"}}
+ {{sample-component "Foo" 4 "Bar" 5 "Baz"}}
+ `);
+
+ this.assertComponentElement(this.nthChild(0), { tagName: 'div', content: 'Foo4Bar' });
+ this.assertComponentElement(this.nthChild(1), { tagName: 'div', content: 'Foo4Bar5Baz' });
+ }
+
+ '@test dynamic arbitrary number of positional parameters'() {
+ this.owner.register(
+ 'component:sample-component',
+ setComponentTemplate(
+ precompileTemplate('{{#each this.n key="@index" as |name|}}{{name}}{{/each}}'),
+ class extends Component {
+ static positionalParams = 'n';
+ }
+ )
+ );
+
+ this.render('{{sample-component this.user1 this.user2}}', { user1: 'Foo', user2: 4 });
+
+ this.assertComponentElement(this.firstChild, { tagName: 'div', content: 'Foo4' });
+
+ runTask(() => {
+ set(this.context, 'user1', 'Bar');
+ set(this.context, 'user2', '5');
+ });
+
+ this.assertComponentElement(this.firstChild, { tagName: 'div', content: 'Bar5' });
+
+ runTask(() => set(this.context, 'user2', '6'));
+
+ this.assertComponentElement(this.firstChild, { tagName: 'div', content: 'Bar6' });
+ }
+
+ '@test the {{component}} helper works with positional params'() {
+ this.owner.register(
+ 'component:sample-component',
+ setComponentTemplate(
+ precompileTemplate('{{this.name}}{{this.age}}'),
+ class extends Component {
+ static positionalParams = ['name', 'age'];
+ }
+ )
+ );
+
+ this.render('{{component "sample-component" this.myName this.myAge}}', {
+ myName: 'Quint',
+ myAge: 4,
+ });
+
+ this.assertComponentElement(this.firstChild, { tagName: 'div', content: 'Quint4' });
+
+ runTask(() => {
+ set(this.context, 'myName', 'Edward');
+ set(this.context, 'myAge', '5');
+ });
+
+ this.assertComponentElement(this.firstChild, { tagName: 'div', content: 'Edward5' });
+
+ runTask(() => {
+ set(this.context, 'myName', 'Quint');
+ set(this.context, 'myAge', '4');
+ });
+
+ this.assertComponentElement(this.firstChild, { tagName: 'div', content: 'Quint4' });
+ }
+
+ '@test every component instance gets a unique element id'(assert) {
+ this.owner.register(
+ 'component:x-curly',
+ setComponentTemplate(precompileTemplate(''), class extends Component {})
+ );
+
+ this.render(strip`
+ {{x-curly}}
+ {{x-curly}}
+ {{x-curly}}
+ `);
+
+ let ids = [0, 1, 2].map((i) => {
+ let element = this.nthChild(i);
+ assert.ok(/^ember\d+$/.test(element.id), `element ${i} has a generated id`);
+ return element.id;
+ });
+
+ assert.strictEqual(new Set(ids).size, 3, 'the components each have a unique id');
+ }
+
+ '@test a classic component removed by an `if` is destroyed once'(assert) {
+ let willDestroyElement = 0;
+ let destroyed = 0;
+
+ this.owner.register(
+ 'component:destroy-me',
+ setComponentTemplate(
+ precompileTemplate('destroy me!'),
+ class extends Component {
+ willDestroyElement() {
+ super.willDestroyElement(...arguments);
+ willDestroyElement++;
+ }
+
+ destroy() {
+ super.destroy(...arguments);
+ destroyed++;
+ }
+ }
+ )
+ );
+
+ this.render('{{#if this.cond}}{{destroy-me}}{{/if}}', { cond: true });
+
+ assert.strictEqual(willDestroyElement, 0, 'willDestroyElement is not called initially');
+ assert.strictEqual(destroyed, 0, 'destroy is not called initially');
+
+ runTask(() => set(this.context, 'cond', false));
+
+ assert.strictEqual(willDestroyElement, 1, 'willDestroyElement is called exactly once');
+ assert.strictEqual(destroyed, 1, 'destroy is called exactly once');
+ }
+ }
+);
diff --git a/packages/@ember/-internals/glimmer/tests/integration/components/classic/dynamic-components-test.js b/packages/@ember/-internals/glimmer/tests/integration/components/classic/dynamic-components-test.js
new file mode 100644
index 00000000000..1e4bfe85f42
--- /dev/null
+++ b/packages/@ember/-internals/glimmer/tests/integration/components/classic/dynamic-components-test.js
@@ -0,0 +1,84 @@
+import { moduleFor, RenderingTestCase, runTask } from 'internal-test-helpers';
+import { precompileTemplate } from '@ember/template-compilation';
+import { setComponentTemplate } from '@glimmer/manager';
+import { Component as EmberComponent } from '../../../utils/helpers';
+
+moduleFor(
+ 'Components test: dynamic components (classic component)',
+ class extends RenderingTestCase {
+ ['@test it has an element']() {
+ let instance;
+
+ let FooBarComponent = class extends EmberComponent {
+ init() {
+ super.init();
+ instance = this;
+ }
+ };
+
+ this.owner.register(
+ 'component:foo-bar',
+ setComponentTemplate(precompileTemplate('hello'), FooBarComponent)
+ );
+
+ this.render('{{component "foo-bar"}}');
+
+ let element1 = instance.element;
+
+ this.assertComponentElement(element1, { content: 'hello' });
+
+ runTask(() => this.rerender());
+
+ let element2 = instance.element;
+
+ this.assertComponentElement(element2, { content: 'hello' });
+
+ this.assertSameNode(element2, element1);
+ }
+
+ ['@test it has the right parentView and childViews'](assert) {
+ let fooBarInstance, fooBarBazInstance;
+
+ let FooBarComponent = class extends EmberComponent {
+ init() {
+ super.init();
+ fooBarInstance = this;
+ }
+ };
+
+ let FooBarBazComponent = class extends EmberComponent {
+ init() {
+ super.init();
+ fooBarBazInstance = this;
+ }
+ };
+
+ this.owner.register(
+ 'component:foo-bar',
+ setComponentTemplate(precompileTemplate('foo-bar {{foo-bar-baz}}'), FooBarComponent)
+ );
+ this.owner.register(
+ 'component:foo-bar-baz',
+ setComponentTemplate(precompileTemplate('foo-bar-baz'), FooBarBazComponent)
+ );
+
+ this.render('{{component "foo-bar"}}');
+ this.assertText('foo-bar foo-bar-baz');
+
+ assert.equal(fooBarInstance.parentView, this.component);
+ assert.equal(fooBarBazInstance.parentView, fooBarInstance);
+
+ assert.deepEqual(this.component.childViews, [fooBarInstance]);
+ assert.deepEqual(fooBarInstance.childViews, [fooBarBazInstance]);
+
+ runTask(() => this.rerender());
+ this.assertText('foo-bar foo-bar-baz');
+
+ assert.equal(fooBarInstance.parentView, this.component);
+ assert.equal(fooBarBazInstance.parentView, fooBarInstance);
+
+ assert.deepEqual(this.component.childViews, [fooBarInstance]);
+ assert.deepEqual(fooBarInstance.childViews, [fooBarBazInstance]);
+ }
+ }
+);
diff --git a/packages/@ember/-internals/glimmer/tests/integration/components/classic/each-test.js b/packages/@ember/-internals/glimmer/tests/integration/components/classic/each-test.js
new file mode 100644
index 00000000000..305407051a1
--- /dev/null
+++ b/packages/@ember/-internals/glimmer/tests/integration/components/classic/each-test.js
@@ -0,0 +1,78 @@
+import { moduleFor, RenderingTestCase, strip, runTask } from 'internal-test-helpers';
+
+import { set } from '@ember/object';
+import { precompileTemplate } from '@ember/template-compilation';
+import { setComponentTemplate } from '@glimmer/manager';
+
+import { Component as EmberComponent } from '../../../utils/helpers';
+
+moduleFor(
+ 'Syntax test: {{#each}} with native arrays (classic component)',
+ class extends RenderingTestCase {
+ makeList(items) {
+ this.list = items;
+ }
+
+ replaceList(list) {
+ runTask(() => set(this.context, 'list', list));
+ }
+
+ objectAt(idx) {
+ return this.list[idx];
+ }
+
+ render(template, context = {}) {
+ context.list = this.list;
+
+ return super.render(template, context);
+ }
+
+ [`@test updating and setting within #each`]() {
+ this.makeList([{ value: 1 }, { value: 2 }, { value: 3 }]);
+
+ let FooBarComponent = class extends EmberComponent {
+ init() {
+ super.init(...arguments);
+ this.isEven = true;
+ this.tagName = 'li';
+ }
+
+ _isEven() {
+ this.set('isEven', this.get('item.value') % 2 === 0);
+ }
+
+ didUpdate() {
+ this._isEven();
+ }
+ };
+
+ this.owner.register(
+ 'component:foo-bar',
+ setComponentTemplate(
+ precompileTemplate('{{#if this.isEven}}{{this.item.value}}{{/if}}'),
+ FooBarComponent
+ )
+ );
+
+ this.render(strip`
+ {{#each this.list as |item|}}
+ Prev
+ {{foo-bar item=item}}
+ Next
+ {{/each}}
+ `);
+
+ this.assertText('Prev1NextPrev2NextPrev3Next');
+
+ this.assertStableRerender();
+
+ runTask(() => set(this.objectAt(0), 'value', 3));
+
+ this.assertText('PrevNextPrev2NextPrev3Next');
+
+ this.replaceList([{ value: 1 }, { value: 2 }, { value: 3 }]);
+
+ this.assertText('Prev1NextPrev2NextPrev3Next');
+ }
+ }
+);
diff --git a/packages/@ember/-internals/glimmer/tests/integration/components/classic/error-handling-test.js b/packages/@ember/-internals/glimmer/tests/integration/components/classic/error-handling-test.js
new file mode 100644
index 00000000000..4c0ebfc319e
--- /dev/null
+++ b/packages/@ember/-internals/glimmer/tests/integration/components/classic/error-handling-test.js
@@ -0,0 +1,81 @@
+import { moduleFor, RenderingTestCase, runTask } from 'internal-test-helpers';
+import { set } from '@ember/object';
+import { precompileTemplate } from '@ember/template-compilation';
+import { setComponentTemplate } from '@glimmer/manager';
+import { Component as EmberComponent } from '../../../utils/helpers';
+
+moduleFor(
+ 'Errors thrown during render (classic component)',
+ class extends RenderingTestCase {
+ ['@test it can recover resets the transaction when an error is thrown during didInsertElement'](
+ assert
+ ) {
+ let shouldThrow = true;
+ let FooBarComponent = class extends EmberComponent {
+ didInsertElement() {
+ super.didInsertElement(...arguments);
+ if (shouldThrow) {
+ throw new Error('silly mistake!');
+ }
+ }
+ };
+
+ this.owner.register(
+ 'component:foo-bar',
+ setComponentTemplate(precompileTemplate('hello'), FooBarComponent)
+ );
+
+ assert.throws(() => {
+ this.render('{{#if this.switch}}{{#foo-bar}}{{foo-bar}}{{/foo-bar}}{{/if}}', {
+ switch: true,
+ });
+ }, /silly mistake/);
+
+ assert.equal(
+ this.renderer._inRenderTransaction,
+ false,
+ 'should not be in a transaction even though an error was thrown'
+ );
+
+ this.assertText('hello');
+
+ runTask(() => set(this.context, 'switch', false));
+
+ this.assertText('');
+ }
+
+ ['@test it can recover resets the transaction when an error is thrown during destroy'](assert) {
+ let shouldThrow = true;
+ let FooBarComponent = class extends EmberComponent {
+ destroy() {
+ super.destroy(...arguments);
+ if (shouldThrow) {
+ throw new Error('silly mistake!');
+ }
+ }
+ };
+
+ this.owner.register(
+ 'component:foo-bar',
+ setComponentTemplate(precompileTemplate('hello'), FooBarComponent)
+ );
+
+ this.render('{{#if this.switch}}{{#foo-bar}}{{foo-bar}}{{/foo-bar}}{{/if}}', {
+ switch: true,
+ });
+
+ this.assertText('hello');
+
+ assert.throws(() => {
+ runTask(() => set(this.context, 'switch', false));
+ }, /silly mistake/);
+
+ this.assertText('');
+
+ shouldThrow = false;
+ runTask(() => set(this.context, 'switch', true));
+
+ this.assertText('hello');
+ }
+ }
+);
diff --git a/packages/@ember/-internals/glimmer/tests/integration/components/classic/event-dispatcher-test.js b/packages/@ember/-internals/glimmer/tests/integration/components/classic/event-dispatcher-test.js
new file mode 100644
index 00000000000..99b63c7e8f5
--- /dev/null
+++ b/packages/@ember/-internals/glimmer/tests/integration/components/classic/event-dispatcher-test.js
@@ -0,0 +1,422 @@
+import {
+ expectDeprecation,
+ moduleFor,
+ RenderingTestCase,
+ runTask,
+ testUnless,
+} from 'internal-test-helpers';
+import { DEPRECATIONS } from '../../../../../deprecations';
+import { Component as EmberComponent } from '../../../utils/helpers';
+import { _getCurrentRunLoop } from '@ember/runloop';
+import { precompileTemplate } from '@ember/template-compilation';
+import { setComponentTemplate } from '@glimmer/manager';
+
+function fireNativeWithDataTransfer(node, type, dataTransfer) {
+ let event = new Event(type, { bubbles: true, cancelable: true });
+ event.dataTransfer = dataTransfer;
+ node.dispatchEvent(event);
+}
+
+function triggerEvent(node, event) {
+ switch (event) {
+ case 'focusin':
+ return node.focus();
+ case 'focusout':
+ return node.blur();
+ default:
+ return node.trigger(event);
+ }
+}
+
+const SUPPORTED_EMBER_EVENTS = {
+ touchstart: 'touchStart',
+ touchmove: 'touchMove',
+ touchend: 'touchEnd',
+ touchcancel: 'touchCancel',
+ keydown: 'keyDown',
+ keyup: 'keyUp',
+ keypress: 'keyPress',
+ mousedown: 'mouseDown',
+ mouseup: 'mouseUp',
+ contextmenu: 'contextMenu',
+ click: 'click',
+ dblclick: 'doubleClick',
+ focusin: 'focusIn',
+ focusout: 'focusOut',
+ submit: 'submit',
+ input: 'input',
+ change: 'change',
+ dragstart: 'dragStart',
+ drag: 'drag',
+ dragenter: 'dragEnter',
+ dragleave: 'dragLeave',
+ dragover: 'dragOver',
+ drop: 'drop',
+ dragend: 'dragEnd',
+};
+
+moduleFor(
+ 'EventDispatcher (classic component)',
+ class extends RenderingTestCase {
+ ['@test event handler methods are called when event is triggered'](assert) {
+ let receivedEvent;
+ let browserEvent;
+
+ this.owner.register(
+ 'component:x-button',
+ class extends EmberComponent {
+ tagName = 'button';
+
+ touchMove(event) {
+ receivedEvent = event;
+ }
+ touchStart(event) {
+ receivedEvent = event;
+ }
+ touchEnd(event) {
+ receivedEvent = event;
+ }
+ touchCancel(event) {
+ receivedEvent = event;
+ }
+ keyDown(event) {
+ receivedEvent = event;
+ }
+ keyUp(event) {
+ receivedEvent = event;
+ }
+ keyPress(event) {
+ receivedEvent = event;
+ }
+ mouseDown(event) {
+ receivedEvent = event;
+ }
+ mouseUp(event) {
+ receivedEvent = event;
+ }
+ contextMenu(event) {
+ receivedEvent = event;
+ }
+ click(event) {
+ receivedEvent = event;
+ }
+ doubleClick(event) {
+ receivedEvent = event;
+ }
+ focusIn(event) {
+ receivedEvent = event;
+ }
+ focusOut(event) {
+ receivedEvent = event;
+ }
+ submit(event) {
+ receivedEvent = event;
+ }
+ input(event) {
+ receivedEvent = event;
+ }
+ change(event) {
+ receivedEvent = event;
+ }
+ dragStart(event) {
+ receivedEvent = event;
+ }
+ drag(event) {
+ receivedEvent = event;
+ }
+ dragEnter(event) {
+ receivedEvent = event;
+ }
+ dragLeave(event) {
+ receivedEvent = event;
+ }
+ dragOver(event) {
+ receivedEvent = event;
+ }
+ drop(event) {
+ receivedEvent = event;
+ }
+ dragEnd(event) {
+ receivedEvent = event;
+ }
+ }
+ );
+
+ this.render(`{{x-button}}`);
+
+ let elementNode = this.$('button');
+ let element = elementNode[0];
+
+ for (browserEvent in SUPPORTED_EMBER_EVENTS) {
+ receivedEvent = null;
+ runTask(() => triggerEvent(elementNode, browserEvent));
+ assert.ok(receivedEvent, `${browserEvent} event was triggered`);
+ assert.strictEqual(receivedEvent.target, element);
+ }
+ }
+
+ [`${testUnless(DEPRECATIONS.DEPRECATE_EVENTED.isRemoved)} @test event listeners are called when event is triggered`](
+ assert
+ ) {
+ let receivedEvent;
+ let browserEvent;
+
+ this.owner.register(
+ 'component:x-button',
+ class extends EmberComponent {
+ tagName = 'button';
+ init() {
+ super.init();
+ Object.keys(SUPPORTED_EMBER_EVENTS).forEach((browserEvent) => {
+ expectDeprecation(
+ () => {
+ this.on(SUPPORTED_EMBER_EVENTS[browserEvent], (event) => (receivedEvent = event));
+ },
+ /Evented#on` is deprecated/,
+ DEPRECATIONS.DEPRECATE_EVENTED.isEnabled
+ );
+ });
+ }
+ }
+ );
+
+ this.render(`{{x-button}}`);
+
+ let elementNode = this.$('button');
+ let element = elementNode[0];
+
+ for (browserEvent in SUPPORTED_EMBER_EVENTS) {
+ receivedEvent = null;
+ runTask(() => triggerEvent(elementNode, browserEvent));
+ assert.ok(receivedEvent, `${browserEvent} event was triggered`);
+ assert.strictEqual(receivedEvent.target, element);
+ }
+ }
+
+ ['@test events bubble view hierarchy for form elements'](assert) {
+ let receivedEvent;
+
+ this.owner.register(
+ 'component:x-foo',
+ setComponentTemplate(
+ precompileTemplate(` `),
+ class extends EmberComponent {
+ change(event) {
+ receivedEvent = event;
+ }
+ }
+ )
+ );
+
+ this.render(`{{x-foo}}`);
+
+ runTask(() => this.$('#is-done').trigger('change'));
+ assert.ok(receivedEvent, 'change event was triggered');
+ assert.strictEqual(receivedEvent.target, this.$('#is-done')[0]);
+ }
+
+ ['@test events bubble to parent view'](assert) {
+ let receivedEvent;
+
+ this.owner.register(
+ 'component:x-foo',
+ setComponentTemplate(
+ precompileTemplate(`{{yield}}`),
+ class extends EmberComponent {
+ change(event) {
+ receivedEvent = event;
+ }
+ }
+ )
+ );
+
+ this.owner.register(
+ 'component:x-bar',
+ setComponentTemplate(
+ precompileTemplate(` `),
+ class extends EmberComponent {
+ change() {}
+ }
+ )
+ );
+
+ this.render(`{{#x-foo}}{{x-bar}}{{/x-foo}}`);
+
+ runTask(() => this.$('#is-done').trigger('change'));
+ assert.ok(receivedEvent, 'change event was triggered');
+ assert.strictEqual(receivedEvent.target, this.$('#is-done')[0]);
+ }
+
+ ['@test events bubbling up can be prevented by returning false'](assert) {
+ let hasReceivedEvent;
+
+ this.owner.register(
+ 'component:x-foo',
+ setComponentTemplate(
+ precompileTemplate(`{{yield}}`),
+ class extends EmberComponent {
+ change() {
+ hasReceivedEvent = true;
+ }
+ }
+ )
+ );
+
+ this.owner.register(
+ 'component:x-bar',
+ setComponentTemplate(
+ precompileTemplate(` `),
+ class extends EmberComponent {
+ change() {
+ return false;
+ }
+ }
+ )
+ );
+
+ this.render(`{{#x-foo}}{{x-bar}}{{/x-foo}}`);
+
+ runTask(() => this.$('#is-done').trigger('change'));
+ assert.notOk(hasReceivedEvent, 'change event has not been received');
+ }
+
+ ['@test events bubbling up can be prevented by calling stopPropagation()'](assert) {
+ let hasReceivedEvent;
+
+ this.owner.register(
+ 'component:x-foo',
+ setComponentTemplate(
+ precompileTemplate(`{{yield}}`),
+ class extends EmberComponent {
+ change() {
+ hasReceivedEvent = true;
+ }
+ }
+ )
+ );
+
+ this.owner.register(
+ 'component:x-bar',
+ setComponentTemplate(
+ precompileTemplate(` `),
+ class extends EmberComponent {
+ change(e) {
+ e.stopPropagation();
+ }
+ }
+ )
+ );
+
+ this.render(`{{#x-foo}}{{x-bar}}{{/x-foo}}`);
+
+ runTask(() => this.$('#is-done').trigger('change'));
+ assert.notOk(hasReceivedEvent, 'change event has not been received');
+ }
+
+ ['@test event handlers are wrapped in a run loop'](assert) {
+ this.owner.register(
+ 'component:x-foo',
+ setComponentTemplate(
+ precompileTemplate(` `),
+ class extends EmberComponent {
+ change() {
+ assert.ok(_getCurrentRunLoop(), 'a run loop should have started');
+ }
+ }
+ )
+ );
+
+ this.render(`{{x-foo}}`);
+
+ this.$('#is-done').trigger('click');
+ }
+ }
+);
+
+moduleFor(
+ 'EventDispatcher#setup (classic component)',
+ class extends RenderingTestCase {
+ constructor() {
+ super(...arguments);
+
+ this.dispatcher = this.owner.lookup('event_dispatcher:main');
+ }
+
+ getBootOptions() {
+ return {
+ skipEventDispatcher: true,
+ };
+ }
+
+ ['@test additional events can be specified'](assert) {
+ this.dispatcher.setup({ myevent: 'myEvent' });
+
+ this.owner.register(
+ 'component:x-foo',
+ setComponentTemplate(
+ precompileTemplate(`Hello!
`),
+ class extends EmberComponent {
+ myEvent() {
+ assert.ok(true, 'custom event was triggered');
+ }
+ }
+ )
+ );
+
+ this.render(`{{x-foo}}`);
+
+ this.$('div').trigger('myevent');
+ }
+
+ ['@test default events can be disabled via `customEvents`'](assert) {
+ this.dispatcher.setup({ click: null });
+
+ this.owner.register(
+ 'component:x-foo',
+ setComponentTemplate(
+ precompileTemplate(`Hello!
`),
+ class extends EmberComponent {
+ click() {
+ assert.ok(false, 'click method was called');
+ }
+
+ null() {
+ assert.ok(false, 'null method was called');
+ }
+
+ doubleClick() {
+ assert.ok(true, 'a non-disabled event is still handled properly');
+ }
+ }
+ )
+ );
+
+ this.render(`{{x-foo}}`);
+
+ this.$('div').trigger('click');
+ this.$('div').trigger('dblclick');
+ }
+ }
+);
+
+moduleFor(
+ 'EventDispatcher - Event Properties (classic component)',
+ class extends RenderingTestCase {
+ ['@test dataTransfer property is added to drop event'](assert) {
+ let receivedEvent;
+ this.owner.register(
+ 'component:x-foo',
+ class extends EmberComponent {
+ drop(event) {
+ receivedEvent = event;
+ }
+ }
+ );
+
+ this.render(`{{x-foo}}`);
+
+ fireNativeWithDataTransfer(this.$('div')[0], 'drop', 'success');
+ assert.equal(receivedEvent.dataTransfer, 'success');
+ }
+ }
+);
diff --git a/packages/@ember/-internals/glimmer/tests/integration/components/instrumentation-test.js b/packages/@ember/-internals/glimmer/tests/integration/components/classic/instrumentation-test.js
similarity index 98%
rename from packages/@ember/-internals/glimmer/tests/integration/components/instrumentation-test.js
rename to packages/@ember/-internals/glimmer/tests/integration/components/classic/instrumentation-test.js
index df8770ca99f..ff766a814d8 100644
--- a/packages/@ember/-internals/glimmer/tests/integration/components/instrumentation-test.js
+++ b/packages/@ember/-internals/glimmer/tests/integration/components/classic/instrumentation-test.js
@@ -8,7 +8,7 @@ import {
import { precompileTemplate } from '@ember/template-compilation';
import { setComponentTemplate } from '@glimmer/manager';
-import { Component } from '../../utils/helpers';
+import { Component } from '../../../utils/helpers';
moduleFor(
'Components instrumentation',
diff --git a/packages/@ember/-internals/glimmer/tests/integration/components/classic/life-cycle-test.js b/packages/@ember/-internals/glimmer/tests/integration/components/classic/life-cycle-test.js
new file mode 100644
index 00000000000..d94d4715974
--- /dev/null
+++ b/packages/@ember/-internals/glimmer/tests/integration/components/classic/life-cycle-test.js
@@ -0,0 +1,1640 @@
+import { classes, moduleFor, RenderingTestCase, runTask, strip } from 'internal-test-helpers';
+
+import { schedule } from '@ember/runloop';
+import { set, setProperties } from '@ember/object';
+import { trackedArray } from '@ember/reactive/collections';
+import { getViewElement, getViewId } from '@ember/-internals/views';
+import { precompileTemplate } from '@ember/template-compilation';
+import { setComponentTemplate } from '@glimmer/manager';
+
+import { Component } from '../../../utils/helpers';
+import { addListener } from '@ember/-internals/metal';
+
+class LifeCycleHooksTest extends RenderingTestCase {
+ constructor() {
+ super(...arguments);
+ this.hooks = [];
+ this.components = {};
+ this.componentRegistry = [];
+ this.teardownAssertions = [];
+ this.viewRegistry = this.owner.lookup('-view-registry:main');
+ }
+
+ afterEach() {
+ super.afterEach();
+
+ for (let i = 0; i < this.teardownAssertions.length; i++) {
+ this.teardownAssertions[i]();
+ }
+ }
+
+ get isInteractive() {
+ return true;
+ }
+
+ getBootOptions() {
+ return {
+ isInteractive: this.isInteractive,
+ };
+ }
+
+ /* 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),
+ };
+ }
+
+ assertRegisteredViews(label) {
+ let viewRegistry = this.viewRegistry;
+ let topLevelId = getViewId(this.component);
+ let actual = Object.keys(viewRegistry)
+ .sort()
+ .filter((id) => id !== topLevelId);
+
+ if (this.isInteractive) {
+ let expected = this.componentRegistry.sort();
+
+ this.assert.deepEqual(actual, expected, 'registered views - ' + label);
+ } else {
+ this.assert.deepEqual(actual, [], 'no views should be registered for non-interactive mode');
+ }
+ }
+
+ registerComponent(name, { template = null }) {
+ let pushComponent = (instance) => {
+ this.components[name] = instance;
+ this.componentRegistry.push(getViewId(instance));
+ };
+
+ let removeComponent = (instance) => {
+ let index = this.componentRegistry.indexOf(getViewId(instance));
+ this.componentRegistry.splice(index, 1);
+
+ delete this.components[name];
+ };
+
+ let pushHook = (hookName, args) => {
+ this.hooks.push(hook(name, hookName, args));
+ };
+
+ let assertParentView = (hookName, instance) => {
+ this.assert.ok(instance.parentView, `parentView should be present in ${hookName}`);
+
+ if (hookName === 'willDestroyElement') {
+ this.assert.ok(
+ instance.parentView.childViews.indexOf(instance) !== -1,
+ `view is still connected to parentView in ${hookName}`
+ );
+ }
+ };
+
+ let assertElement = (hookName, instance, inDOM = true) => {
+ if (instance.tagName === '') {
+ return;
+ }
+
+ this.assert.ok(
+ getViewElement(instance),
+ `element should be present on ${instance} during ${hookName}`
+ );
+
+ if (this.isInteractive) {
+ this.assert.ok(
+ instance.element,
+ `this.element should be present on ${instance} during ${hookName}`
+ );
+ this.assert.equal(
+ document.body.contains(instance.element),
+ inDOM,
+ `element for ${instance} ${
+ inDOM ? 'should' : 'should not'
+ } be in the DOM during ${hookName}`
+ );
+ } else {
+ this.assert.throws(
+ () => instance.element,
+ /Accessing `this.element` is not allowed in non-interactive environments/
+ );
+ }
+ };
+
+ let assertNoElement = (hookName, instance) => {
+ this.assert.strictEqual(
+ getViewElement(instance),
+ null,
+ `element should not be present in ${hookName}`
+ );
+
+ if (this.isInteractive) {
+ this.assert.strictEqual(
+ instance.element,
+ null,
+ `this.element should not be present in ${hookName}`
+ );
+ } else {
+ this.assert.throws(
+ () => instance.element,
+ /Accessing `this.element` is not allowed in non-interactive environments/
+ );
+ }
+ };
+
+ let assertState = (hookName, expectedState, instance) => {
+ this.assert.equal(
+ instance._state,
+ expectedState,
+ `within ${hookName} the expected _state is ${expectedState}`
+ );
+ };
+
+ let { isInteractive } = this;
+
+ let ComponentClass = class extends this.ComponentClass {
+ init() {
+ super.init(...arguments);
+
+ this.isInitialRender = true;
+ this.componentName = name;
+ pushHook('init');
+ pushComponent(this);
+ assertParentView('init', this);
+ assertNoElement('init', this);
+ assertState('init', 'preRender', this);
+
+ addListener(this, 'init', () => pushHook('on(init)'));
+
+ schedule('afterRender', () => {
+ this.isInitialRender = false;
+ });
+ }
+
+ didReceiveAttrs(options) {
+ pushHook('didReceiveAttrs', options);
+ assertParentView('didReceiveAttrs', this);
+
+ if (this.isInitialRender) {
+ assertNoElement('didReceiveAttrs', this);
+ assertState('didReceiveAttrs', 'preRender', this);
+ } else {
+ assertElement('didReceiveAttrs', this);
+
+ if (isInteractive) {
+ assertState('didReceiveAttrs', 'inDOM', this);
+ } else {
+ assertState('didReceiveAttrs', 'hasElement', this);
+ }
+ }
+ }
+
+ willInsertElement() {
+ pushHook('willInsertElement');
+ assertParentView('willInsertElement', this);
+ assertElement('willInsertElement', this, false);
+ assertState('willInsertElement', 'hasElement', this);
+ }
+
+ willRender() {
+ pushHook('willRender');
+ assertParentView('willRender', this);
+
+ if (this.isInitialRender) {
+ assertNoElement('willRender', this, false);
+ assertState('willRender', 'preRender', this);
+ } else {
+ assertElement('willRender', this);
+ assertState('willRender', 'inDOM', this);
+ }
+ }
+
+ didInsertElement() {
+ pushHook('didInsertElement');
+ assertParentView('didInsertElement', this);
+ assertElement('didInsertElement', this);
+ assertState('didInsertElement', 'inDOM', this);
+ }
+
+ didRender() {
+ pushHook('didRender');
+ assertParentView('didRender', this);
+ assertElement('didRender', this);
+ assertState('didRender', 'inDOM', this);
+ }
+
+ didUpdateAttrs(options) {
+ pushHook('didUpdateAttrs', options);
+ assertParentView('didUpdateAttrs', this);
+
+ if (isInteractive) {
+ assertState('didUpdateAttrs', 'inDOM', this);
+ } else {
+ assertState('didUpdateAttrs', 'hasElement', this);
+ }
+ }
+
+ willUpdate(options) {
+ pushHook('willUpdate', options);
+ assertParentView('willUpdate', this);
+ assertElement('willUpdate', this);
+ assertState('willUpdate', 'inDOM', this);
+ }
+
+ didUpdate(options) {
+ pushHook('didUpdate', options);
+ assertParentView('didUpdate', this);
+ assertElement('didUpdate', this);
+ assertState('didUpdate', 'inDOM', this);
+ }
+
+ willDestroyElement() {
+ pushHook('willDestroyElement');
+ assertParentView('willDestroyElement', this);
+ assertElement('willDestroyElement', this);
+ assertState('willDestroyElement', 'inDOM', this);
+ }
+
+ willClearRender() {
+ pushHook('willClearRender');
+ assertParentView('willClearRender', this);
+ assertElement('willClearRender', this);
+ assertState('willClearRender', 'inDOM', this);
+ }
+
+ didDestroyElement() {
+ pushHook('didDestroyElement');
+ assertNoElement('didDestroyElement', this);
+ assertState('didDestroyElement', 'destroying', this);
+ }
+
+ willDestroy() {
+ pushHook('willDestroy');
+ removeComponent(this);
+
+ this._super(...arguments);
+ }
+ };
+
+ if (typeof template === 'string') {
+ setComponentTemplate(this.compile(template), ComponentClass);
+ }
+ this.owner.register(`component:${name}`, ComponentClass);
+ }
+
+ assertHooks({ label, interactive, nonInteractive }) {
+ let rawHooks = this.isInteractive ? interactive : nonInteractive;
+ 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(attr('twitter')) }), {
+ twitter: '@tomdale',
+ });
+
+ this.assertText('Twitter: @tomdale|Name: Tom Dale|Website: tomdale.net');
+ this.assertRegisteredViews('intial render');
+
+ this.assertHooks({
+ label: 'after initial render',
+
+ interactive: [
+ // Sync hooks
+
+ ['the-top', 'init'],
+ ['the-top', 'on(init)'],
+ ['the-top', 'didReceiveAttrs'],
+ ['the-top', 'willRender'],
+ ['the-top', 'willInsertElement'],
+
+ ['the-middle', 'init'],
+ ['the-middle', 'on(init)'],
+ ['the-middle', 'didReceiveAttrs'],
+ ['the-middle', 'willRender'],
+ ['the-middle', 'willInsertElement'],
+
+ ['the-bottom', 'init'],
+ ['the-bottom', 'on(init)'],
+ ['the-bottom', 'didReceiveAttrs'],
+ ['the-bottom', 'willRender'],
+ ['the-bottom', 'willInsertElement'],
+
+ // Async hooks
+
+ ['the-bottom', 'didInsertElement'],
+ ['the-bottom', 'didRender'],
+
+ ['the-middle', 'didInsertElement'],
+ ['the-middle', 'didRender'],
+
+ ['the-top', 'didInsertElement'],
+ ['the-top', 'didRender'],
+ ],
+
+ nonInteractive: [
+ // Sync hooks
+ ['the-top', 'init'],
+ ['the-top', 'on(init)'],
+ ['the-top', 'didReceiveAttrs'],
+
+ ['the-middle', 'init'],
+ ['the-middle', 'on(init)'],
+ ['the-middle', 'didReceiveAttrs'],
+
+ ['the-bottom', 'init'],
+ ['the-bottom', 'on(init)'],
+ ['the-bottom', 'didReceiveAttrs'],
+ ],
+ });
+
+ runTask(() => this.components['the-bottom'].rerender());
+
+ this.assertText('Twitter: @tomdale|Name: Tom Dale|Website: tomdale.net');
+
+ this.assertHooks({
+ label: 'after no-op rerender (bottom)',
+
+ interactive: [
+ // Sync hooks
+ ['the-top', 'willUpdate'],
+ ['the-top', 'willRender'],
+
+ ['the-middle', 'willUpdate'],
+ ['the-middle', 'willRender'],
+
+ ['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'],
+ ],
+
+ nonInteractive: [],
+ });
+
+ runTask(() => this.components['the-middle'].rerender());
+
+ this.assertText('Twitter: @tomdale|Name: Tom Dale|Website: tomdale.net');
+
+ this.assertHooks({
+ label: 'after no-op rerender (middle)',
+
+ interactive: [
+ // Sync hooks
+
+ ['the-top', 'willUpdate'],
+ ['the-top', 'willRender'],
+
+ ['the-middle', 'willUpdate'],
+ ['the-middle', 'willRender'],
+
+ // Async hooks
+
+ ['the-middle', 'didUpdate'],
+ ['the-middle', 'didRender'],
+
+ ['the-top', 'didUpdate'],
+ ['the-top', 'didRender'],
+ ],
+
+ nonInteractive: [],
+ });
+
+ runTask(() => this.components['the-top'].rerender());
+
+ this.assertText('Twitter: @tomdale|Name: Tom Dale|Website: tomdale.net');
+
+ this.assertHooks({
+ label: 'after no-op rerender (top)',
+
+ interactive: [
+ // Sync hooks
+
+ ['the-top', 'willUpdate'],
+ ['the-top', 'willRender'],
+
+ // Async hooks
+
+ ['the-top', 'didUpdate'],
+ ['the-top', 'didRender'],
+ ],
+
+ nonInteractive: [],
+ });
+
+ 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.
+
+ this.assertHooks({
+ label: 'after update',
+
+ interactive: [
+ // Sync hooks
+
+ ['the-top', 'didUpdateAttrs'],
+ ['the-top', 'didReceiveAttrs'],
+
+ ['the-top', 'willUpdate'],
+ ['the-top', 'willRender'],
+
+ // Async hooks
+
+ ['the-top', 'didUpdate'],
+ ['the-top', 'didRender'],
+ ],
+
+ nonInteractive: [
+ // Sync hooks
+ ['the-top', 'didUpdateAttrs'],
+ ['the-top', 'didReceiveAttrs'],
+ ],
+ });
+
+ this.teardownAssertions.push(() => {
+ this.assertHooks({
+ label: 'destroy',
+
+ interactive: [
+ ['the-top', 'willDestroyElement'],
+ ['the-top', 'willClearRender'],
+ ['the-middle', 'willDestroyElement'],
+ ['the-middle', 'willClearRender'],
+ ['the-bottom', 'willDestroyElement'],
+ ['the-bottom', 'willClearRender'],
+ ['the-top', 'didDestroyElement'],
+ ['the-middle', 'didDestroyElement'],
+ ['the-bottom', 'didDestroyElement'],
+ ['the-top', 'willDestroy'],
+ ['the-middle', 'willDestroy'],
+ ['the-bottom', 'willDestroy'],
+ ],
+
+ nonInteractive: [
+ ['the-top', 'willDestroy'],
+ ['the-middle', 'willDestroy'],
+ ['the-bottom', 'willDestroy'],
+ ],
+ });
+
+ this.assertRegisteredViews('after destroy');
+ });
+ }
+
+ ['@test lifecycle hooks are invoked in a correct sibling order']() {
+ let { attr, invoke } = this.boundHelpers;
+
+ this.registerComponent('the-parent', {
+ template: strip`
+
+ ${invoke('the-first-child', { twitter: expr(attr('twitter')) })}|
+ ${invoke('the-second-child', { name: expr(attr('name')) })}|
+ ${invoke('the-last-child', { website: expr(attr('website')) })}
+
`,
+ });
+
+ this.registerComponent('the-first-child', {
+ template: `Twitter: {{${attr('twitter')}}}`,
+ });
+
+ this.registerComponent('the-second-child', {
+ template: `Name: {{${attr('name')}}}`,
+ });
+
+ this.registerComponent('the-last-child', {
+ template: `Website: {{${attr('website')}}}`,
+ });
+
+ this.render(
+ invoke('the-parent', {
+ twitter: expr(attr('twitter')),
+ name: expr(attr('name')),
+ website: expr(attr('website')),
+ }),
+ {
+ twitter: '@tomdale',
+ name: 'Tom Dale',
+ website: 'tomdale.net',
+ }
+ );
+
+ this.assertText('Twitter: @tomdale|Name: Tom Dale|Website: tomdale.net');
+ this.assertRegisteredViews('intial render');
+
+ this.assertHooks({
+ label: 'after initial render',
+
+ interactive: [
+ // Sync hooks
+
+ ['the-parent', 'init'],
+ ['the-parent', 'on(init)'],
+ ['the-parent', 'didReceiveAttrs'],
+ ['the-parent', 'willRender'],
+ ['the-parent', 'willInsertElement'],
+
+ ['the-first-child', 'init'],
+ ['the-first-child', 'on(init)'],
+ ['the-first-child', 'didReceiveAttrs'],
+ ['the-first-child', 'willRender'],
+ ['the-first-child', 'willInsertElement'],
+
+ ['the-second-child', 'init'],
+ ['the-second-child', 'on(init)'],
+ ['the-second-child', 'didReceiveAttrs'],
+ ['the-second-child', 'willRender'],
+ ['the-second-child', 'willInsertElement'],
+
+ ['the-last-child', 'init'],
+ ['the-last-child', 'on(init)'],
+ ['the-last-child', 'didReceiveAttrs'],
+ ['the-last-child', 'willRender'],
+ ['the-last-child', 'willInsertElement'],
+
+ // Async hooks
+
+ ['the-first-child', 'didInsertElement'],
+ ['the-first-child', 'didRender'],
+
+ ['the-second-child', 'didInsertElement'],
+ ['the-second-child', 'didRender'],
+
+ ['the-last-child', 'didInsertElement'],
+ ['the-last-child', 'didRender'],
+
+ ['the-parent', 'didInsertElement'],
+ ['the-parent', 'didRender'],
+ ],
+
+ nonInteractive: [
+ // Sync hooks
+
+ ['the-parent', 'init'],
+ ['the-parent', 'on(init)'],
+ ['the-parent', 'didReceiveAttrs'],
+
+ ['the-first-child', 'init'],
+ ['the-first-child', 'on(init)'],
+ ['the-first-child', 'didReceiveAttrs'],
+
+ ['the-second-child', 'init'],
+ ['the-second-child', 'on(init)'],
+ ['the-second-child', 'didReceiveAttrs'],
+
+ ['the-last-child', 'init'],
+ ['the-last-child', 'on(init)'],
+ ['the-last-child', 'didReceiveAttrs'],
+ ],
+ });
+
+ runTask(() => this.components['the-first-child'].rerender());
+
+ this.assertText('Twitter: @tomdale|Name: Tom Dale|Website: tomdale.net');
+
+ this.assertHooks({
+ label: 'after no-op rerender (first child)',
+
+ interactive: [
+ // Sync hooks
+
+ ['the-parent', 'willUpdate'],
+ ['the-parent', 'willRender'],
+
+ ['the-first-child', 'willUpdate'],
+ ['the-first-child', 'willRender'],
+
+ // Async hooks
+
+ ['the-first-child', 'didUpdate'],
+ ['the-first-child', 'didRender'],
+
+ ['the-parent', 'didUpdate'],
+ ['the-parent', 'didRender'],
+ ],
+
+ nonInteractive: [],
+ });
+
+ runTask(() => this.components['the-second-child'].rerender());
+
+ this.assertText('Twitter: @tomdale|Name: Tom Dale|Website: tomdale.net');
+
+ this.assertHooks({
+ label: 'after no-op rerender (second child)',
+
+ interactive: [
+ // Sync hooks
+
+ ['the-parent', 'willUpdate'],
+ ['the-parent', 'willRender'],
+
+ ['the-second-child', 'willUpdate'],
+ ['the-second-child', 'willRender'],
+
+ // Async hooks
+
+ ['the-second-child', 'didUpdate'],
+ ['the-second-child', 'didRender'],
+
+ ['the-parent', 'didUpdate'],
+ ['the-parent', 'didRender'],
+ ],
+
+ nonInteractive: [],
+ });
+
+ runTask(() => this.components['the-last-child'].rerender());
+
+ this.assertText('Twitter: @tomdale|Name: Tom Dale|Website: tomdale.net');
+
+ this.assertHooks({
+ label: 'after no-op rerender (last child)',
+
+ interactive: [
+ // Sync hooks
+
+ ['the-parent', 'willUpdate'],
+ ['the-parent', 'willRender'],
+
+ ['the-last-child', 'willUpdate'],
+ ['the-last-child', 'willRender'],
+
+ // Async hooks
+
+ ['the-last-child', 'didUpdate'],
+ ['the-last-child', 'didRender'],
+
+ ['the-parent', 'didUpdate'],
+ ['the-parent', 'didRender'],
+ ],
+
+ nonInteractive: [],
+ });
+
+ runTask(() => this.components['the-parent'].rerender());
+
+ this.assertText('Twitter: @tomdale|Name: Tom Dale|Website: tomdale.net');
+
+ this.assertHooks({
+ label: 'after no-op rerender (parent)',
+
+ interactive: [
+ // Sync hooks
+
+ ['the-parent', 'willUpdate'],
+ ['the-parent', 'willRender'],
+
+ // Async hooks
+
+ ['the-parent', 'didUpdate'],
+ ['the-parent', 'didRender'],
+ ],
+
+ nonInteractive: [],
+ });
+
+ runTask(() =>
+ setProperties(this.context, {
+ twitter: '@horsetomdale',
+ name: 'Horse Tom Dale',
+ website: 'horsetomdale.net',
+ })
+ );
+
+ this.assertText('Twitter: @horsetomdale|Name: Horse Tom Dale|Website: horsetomdale.net');
+
+ this.assertHooks({
+ label: 'after update',
+
+ interactive: [
+ // Sync hooks
+
+ ['the-parent', 'didUpdateAttrs'],
+ ['the-parent', 'didReceiveAttrs'],
+
+ ['the-parent', 'willUpdate'],
+ ['the-parent', 'willRender'],
+
+ ['the-first-child', 'didUpdateAttrs'],
+ ['the-first-child', 'didReceiveAttrs'],
+
+ ['the-first-child', 'willUpdate'],
+ ['the-first-child', 'willRender'],
+
+ ['the-second-child', 'didUpdateAttrs'],
+ ['the-second-child', 'didReceiveAttrs'],
+
+ ['the-second-child', 'willUpdate'],
+ ['the-second-child', 'willRender'],
+
+ ['the-last-child', 'didUpdateAttrs'],
+ ['the-last-child', 'didReceiveAttrs'],
+
+ ['the-last-child', 'willUpdate'],
+ ['the-last-child', 'willRender'],
+
+ // Async hooks
+
+ ['the-first-child', 'didUpdate'],
+ ['the-first-child', 'didRender'],
+
+ ['the-second-child', 'didUpdate'],
+ ['the-second-child', 'didRender'],
+
+ ['the-last-child', 'didUpdate'],
+ ['the-last-child', 'didRender'],
+
+ ['the-parent', 'didUpdate'],
+ ['the-parent', 'didRender'],
+ ],
+
+ nonInteractive: [
+ // Sync hooks
+
+ ['the-parent', 'didUpdateAttrs'],
+ ['the-parent', 'didReceiveAttrs'],
+
+ ['the-first-child', 'didUpdateAttrs'],
+ ['the-first-child', 'didReceiveAttrs'],
+
+ ['the-second-child', 'didUpdateAttrs'],
+ ['the-second-child', 'didReceiveAttrs'],
+
+ ['the-last-child', 'didUpdateAttrs'],
+ ['the-last-child', 'didReceiveAttrs'],
+ ],
+ });
+
+ this.teardownAssertions.push(() => {
+ this.assertHooks({
+ label: 'destroy',
+
+ interactive: [
+ ['the-parent', 'willDestroyElement'],
+ ['the-parent', 'willClearRender'],
+ ['the-first-child', 'willDestroyElement'],
+ ['the-first-child', 'willClearRender'],
+ ['the-second-child', 'willDestroyElement'],
+ ['the-second-child', 'willClearRender'],
+ ['the-last-child', 'willDestroyElement'],
+ ['the-last-child', 'willClearRender'],
+
+ ['the-parent', 'didDestroyElement'],
+ ['the-first-child', 'didDestroyElement'],
+ ['the-second-child', 'didDestroyElement'],
+ ['the-last-child', 'didDestroyElement'],
+
+ ['the-parent', 'willDestroy'],
+ ['the-first-child', 'willDestroy'],
+ ['the-second-child', 'willDestroy'],
+ ['the-last-child', 'willDestroy'],
+ ],
+
+ nonInteractive: [
+ ['the-parent', 'willDestroy'],
+ ['the-first-child', 'willDestroy'],
+ ['the-second-child', 'willDestroy'],
+ ['the-last-child', 'willDestroy'],
+ ],
+ });
+
+ this.assertRegisteredViews('after destroy');
+ });
+ }
+
+ ['@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(attr('twitter')) }), {
+ twitter: '@tomdale',
+ });
+
+ this.assertText('Top: Middle: Bottom: @tomdale');
+ this.assertRegisteredViews('intial render');
+
+ this.assertHooks({
+ label: 'after initial render',
+
+ interactive: [
+ // Sync hooks
+
+ ['the-top', 'init'],
+ ['the-top', 'on(init)'],
+ ['the-top', 'didReceiveAttrs'],
+ ['the-top', 'willRender'],
+ ['the-top', 'willInsertElement'],
+
+ ['the-middle', 'init'],
+ ['the-middle', 'on(init)'],
+ ['the-middle', 'didReceiveAttrs'],
+ ['the-middle', 'willRender'],
+ ['the-middle', 'willInsertElement'],
+
+ ['the-bottom', 'init'],
+ ['the-bottom', 'on(init)'],
+ ['the-bottom', 'didReceiveAttrs'],
+ ['the-bottom', 'willRender'],
+ ['the-bottom', 'willInsertElement'],
+
+ // Async hooks
+
+ ['the-bottom', 'didInsertElement'],
+ ['the-bottom', 'didRender'],
+
+ ['the-middle', 'didInsertElement'],
+ ['the-middle', 'didRender'],
+
+ ['the-top', 'didInsertElement'],
+ ['the-top', 'didRender'],
+ ],
+
+ nonInteractive: [
+ // Sync hooks
+
+ ['the-top', 'init'],
+ ['the-top', 'on(init)'],
+ ['the-top', 'didReceiveAttrs'],
+
+ ['the-middle', 'init'],
+ ['the-middle', 'on(init)'],
+ ['the-middle', 'didReceiveAttrs'],
+
+ ['the-bottom', 'init'],
+ ['the-bottom', 'on(init)'],
+ ['the-bottom', 'didReceiveAttrs'],
+ ],
+ });
+
+ 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.
+
+ this.assertHooks({
+ label: 'after updating (root)',
+
+ interactive: [
+ // Sync hooks
+
+ ['the-top', 'didUpdateAttrs'],
+ ['the-top', 'didReceiveAttrs'],
+
+ ['the-top', 'willUpdate'],
+ ['the-top', 'willRender'],
+
+ ['the-middle', 'didUpdateAttrs'],
+ ['the-middle', 'didReceiveAttrs'],
+
+ ['the-middle', 'willUpdate'],
+ ['the-middle', 'willRender'],
+
+ ['the-bottom', 'didUpdateAttrs'],
+ ['the-bottom', 'didReceiveAttrs'],
+
+ ['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'],
+ ],
+
+ nonInteractive: [
+ // Sync hooks
+
+ ['the-top', 'didUpdateAttrs'],
+ ['the-top', 'didReceiveAttrs'],
+
+ ['the-middle', 'didUpdateAttrs'],
+ ['the-middle', 'didReceiveAttrs'],
+
+ ['the-bottom', 'didUpdateAttrs'],
+ ['the-bottom', 'didReceiveAttrs'],
+ ],
+ });
+
+ runTask(() => this.rerender());
+
+ this.assertText('Top: Middle: Bottom: @horsetomdale');
+
+ // In this case, because the attrs are passed down, all child components are invoked.
+
+ this.assertHooks({
+ label: 'after no-op rernder (root)',
+ interactive: [],
+ nonInteractive: [],
+ });
+
+ this.teardownAssertions.push(() => {
+ this.assertHooks({
+ label: 'destroy',
+
+ interactive: [
+ ['the-top', 'willDestroyElement'],
+ ['the-top', 'willClearRender'],
+ ['the-middle', 'willDestroyElement'],
+ ['the-middle', 'willClearRender'],
+ ['the-bottom', 'willDestroyElement'],
+ ['the-bottom', 'willClearRender'],
+ ['the-top', 'didDestroyElement'],
+ ['the-middle', 'didDestroyElement'],
+ ['the-bottom', 'didDestroyElement'],
+ ['the-top', 'willDestroy'],
+ ['the-middle', 'willDestroy'],
+ ['the-bottom', 'willDestroy'],
+ ],
+
+ nonInteractive: [
+ ['the-top', 'willDestroy'],
+ ['the-middle', 'willDestroy'],
+ ['the-bottom', 'willDestroy'],
+ ],
+ });
+
+ this.assertRegisteredViews('after destroy');
+ });
+ }
+
+ ['@test components rendered from `{{each}}` have correct life-cycle hooks to be called']() {
+ let { invoke } = this.boundHelpers;
+
+ this.registerComponent('nested-item', { template: `{{yield}}` });
+
+ this.registerComponent('an-item', {
+ template: strip`
+ {{#nested-item}}Item: {{this.count}}{{/nested-item}}
+ `,
+ });
+
+ this.registerComponent('no-items', {
+ template: strip`
+ {{#nested-item}}Nothing to see here{{/nested-item}}
+ `,
+ });
+
+ this.render(
+ strip`
+ {{#each this.items as |item|}}
+ ${invoke('an-item', { count: expr('item') })}
+ {{else}}
+ ${invoke('no-items')}
+ {{/each}}
+ `,
+ {
+ items: [1, 2, 3, 4, 5],
+ }
+ );
+
+ this.assertText('Item: 1Item: 2Item: 3Item: 4Item: 5');
+ this.assertRegisteredViews('intial render');
+
+ let initialHooks = () => {
+ let ret = [
+ ['an-item', 'init'],
+ ['an-item', 'on(init)'],
+ ['an-item', 'didReceiveAttrs'],
+ ];
+ if (this.isInteractive) {
+ ret.push(['an-item', 'willRender'], ['an-item', 'willInsertElement']);
+ }
+ ret.push(
+ ['nested-item', 'init'],
+ ['nested-item', 'on(init)'],
+ ['nested-item', 'didReceiveAttrs']
+ );
+ if (this.isInteractive) {
+ ret.push(['nested-item', 'willRender'], ['nested-item', 'willInsertElement']);
+ }
+ return ret;
+ };
+
+ let initialAfterRenderHooks = () => {
+ if (this.isInteractive) {
+ return [
+ ['nested-item', 'didInsertElement'],
+ ['nested-item', 'didRender'],
+ ['an-item', 'didInsertElement'],
+ ['an-item', 'didRender'],
+ ];
+ } else {
+ return [];
+ }
+ };
+
+ this.assertHooks({
+ label: 'after initial render',
+
+ interactive: [
+ // Sync hooks
+ ...initialHooks(1),
+ ...initialHooks(2),
+ ...initialHooks(3),
+ ...initialHooks(4),
+ ...initialHooks(5),
+
+ // Async hooks
+ ...initialAfterRenderHooks(5),
+ ...initialAfterRenderHooks(4),
+ ...initialAfterRenderHooks(3),
+ ...initialAfterRenderHooks(2),
+ ...initialAfterRenderHooks(1),
+ ],
+
+ nonInteractive: [
+ // Sync hooks
+ ...initialHooks(1),
+ ...initialHooks(2),
+ ...initialHooks(3),
+ ...initialHooks(4),
+ ...initialHooks(5),
+
+ // Async hooks
+ ...initialAfterRenderHooks(5),
+ ...initialAfterRenderHooks(4),
+ ...initialAfterRenderHooks(3),
+ ...initialAfterRenderHooks(2),
+ ...initialAfterRenderHooks(1),
+ ],
+ });
+
+ // TODO: Is this correct? Should childViews be populated in non-interactive mode?
+ if (this.isInteractive) {
+ this.assert.equal(this.component.childViews.length, 5, 'childViews precond');
+ }
+
+ runTask(() => set(this.context, 'items', []));
+
+ // TODO: Is this correct? Should childViews be populated in non-interactive mode?
+ if (this.isInteractive) {
+ this.assert.equal(this.component.childViews.length, 1, 'childViews updated');
+ }
+
+ this.assertText('Nothing to see here');
+
+ this.assertHooks({
+ label: 'reset to empty array',
+
+ interactive: [
+ ['an-item', 'willDestroyElement'],
+ ['an-item', 'willClearRender'],
+ ['nested-item', 'willDestroyElement'],
+ ['nested-item', 'willClearRender'],
+ ['an-item', 'willDestroyElement'],
+ ['an-item', 'willClearRender'],
+ ['nested-item', 'willDestroyElement'],
+ ['nested-item', 'willClearRender'],
+ ['an-item', 'willDestroyElement'],
+ ['an-item', 'willClearRender'],
+ ['nested-item', 'willDestroyElement'],
+ ['nested-item', 'willClearRender'],
+ ['an-item', 'willDestroyElement'],
+ ['an-item', 'willClearRender'],
+ ['nested-item', 'willDestroyElement'],
+ ['nested-item', 'willClearRender'],
+ ['an-item', 'willDestroyElement'],
+ ['an-item', 'willClearRender'],
+ ['nested-item', 'willDestroyElement'],
+ ['nested-item', 'willClearRender'],
+
+ ['no-items', 'init'],
+ ['no-items', 'on(init)'],
+ ['no-items', 'didReceiveAttrs'],
+ ['no-items', 'willRender'],
+ ['no-items', 'willInsertElement'],
+
+ ['nested-item', 'init'],
+ ['nested-item', 'on(init)'],
+ ['nested-item', 'didReceiveAttrs'],
+ ['nested-item', 'willRender'],
+ ['nested-item', 'willInsertElement'],
+
+ ['nested-item', 'didInsertElement'],
+ ['nested-item', 'didRender'],
+ ['no-items', 'didInsertElement'],
+ ['no-items', 'didRender'],
+
+ ['an-item', 'didDestroyElement'],
+ ['nested-item', 'didDestroyElement'],
+ ['an-item', 'didDestroyElement'],
+ ['nested-item', 'didDestroyElement'],
+ ['an-item', 'didDestroyElement'],
+ ['nested-item', 'didDestroyElement'],
+ ['an-item', 'didDestroyElement'],
+ ['nested-item', 'didDestroyElement'],
+ ['an-item', 'didDestroyElement'],
+ ['nested-item', 'didDestroyElement'],
+
+ ['an-item', 'willDestroy'],
+ ['nested-item', 'willDestroy'],
+ ['an-item', 'willDestroy'],
+ ['nested-item', 'willDestroy'],
+ ['an-item', 'willDestroy'],
+ ['nested-item', 'willDestroy'],
+ ['an-item', 'willDestroy'],
+ ['nested-item', 'willDestroy'],
+ ['an-item', 'willDestroy'],
+ ['nested-item', 'willDestroy'],
+ ],
+
+ nonInteractive: [
+ ['no-items', 'init'],
+ ['no-items', 'on(init)'],
+ ['no-items', 'didReceiveAttrs'],
+
+ ['nested-item', 'init'],
+ ['nested-item', 'on(init)'],
+ ['nested-item', 'didReceiveAttrs'],
+
+ ['an-item', 'willDestroy'],
+ ['nested-item', 'willDestroy'],
+ ['an-item', 'willDestroy'],
+ ['nested-item', 'willDestroy'],
+ ['an-item', 'willDestroy'],
+ ['nested-item', 'willDestroy'],
+ ['an-item', 'willDestroy'],
+ ['nested-item', 'willDestroy'],
+ ['an-item', 'willDestroy'],
+ ['nested-item', 'willDestroy'],
+ ],
+ });
+
+ this.teardownAssertions.push(() => {
+ this.assertHooks({
+ label: 'destroy',
+
+ interactive: [
+ ['no-items', 'willDestroyElement'],
+ ['no-items', 'willClearRender'],
+ ['nested-item', 'willDestroyElement'],
+ ['nested-item', 'willClearRender'],
+
+ ['no-items', 'didDestroyElement'],
+ ['nested-item', 'didDestroyElement'],
+
+ ['no-items', 'willDestroy'],
+ ['nested-item', 'willDestroy'],
+ ],
+
+ nonInteractive: [
+ ['no-items', 'willDestroy'],
+ ['nested-item', 'willDestroy'],
+ ],
+ });
+
+ this.assertRegisteredViews('after destroy');
+ });
+ }
+}
+
+class CurlyComponentsTest 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 `this.${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}`);
+ }
+ }
+}
+
+moduleFor(
+ 'Components test: interactive lifecycle hooks (curly components)',
+ class extends CurlyComponentsTest {
+ get isInteractive() {
+ return true;
+ }
+ }
+);
+
+moduleFor(
+ 'Components test: non-interactive lifecycle hooks (curly components)',
+ class extends CurlyComponentsTest {
+ get isInteractive() {
+ return false;
+ }
+ }
+);
+
+moduleFor(
+ 'Components test: interactive lifecycle hooks (tagless curly components)',
+ class extends CurlyComponentsTest {
+ get ComponentClass() {
+ return class extends Component {
+ tagName = '';
+ };
+ }
+
+ get isInteractive() {
+ return true;
+ }
+ }
+);
+
+moduleFor(
+ 'Components test: non-interactive lifecycle hooks (tagless curly components)',
+ class extends CurlyComponentsTest {
+ get ComponentClass() {
+ return class extends Component {
+ tagName = '';
+ };
+ }
+
+ get isInteractive() {
+ return false;
+ }
+ }
+);
+
+moduleFor(
+ 'Run loop and lifecycle hooks (classic component)',
+ class extends RenderingTestCase {
+ ['@test afterRender set']() {
+ let ComponentClass = class extends Component {
+ width = '5';
+ didInsertElement() {
+ schedule('afterRender', () => {
+ this.set('width', '10');
+ });
+ }
+ };
+
+ this.owner.register(
+ 'component:foo-bar',
+ setComponentTemplate(precompileTemplate('{{this.width}}'), ComponentClass)
+ );
+
+ this.render('{{foo-bar}}');
+
+ this.assertText('10');
+
+ runTask(() => this.rerender());
+
+ this.assertText('10');
+ }
+
+ ['@test afterRender set on parent']() {
+ let ComponentClass = class extends Component {
+ didInsertElement() {
+ schedule('afterRender', () => {
+ let parent = this.get('parent');
+ parent.set('foo', 'wat');
+ });
+ }
+ };
+
+ this.owner.register(
+ 'component:foo-bar',
+ setComponentTemplate(precompileTemplate('{{this.foo}}'), ComponentClass)
+ );
+
+ this.render('{{foo-bar parent=this foo=this.foo}}');
+
+ this.assertText('wat');
+
+ runTask(() => this.rerender());
+
+ this.assertText('wat');
+ }
+
+ ['@test `willRender` can set before render (GH#14458)']() {
+ let ComponentClass = class extends Component {
+ tagName = 'a';
+ customHref = 'http://google.com';
+ attributeBindings = ['customHref:href'];
+ willRender() {
+ this.set('customHref', 'http://willRender.com');
+ }
+ };
+
+ this.owner.register(
+ 'component:foo-bar',
+ setComponentTemplate(precompileTemplate('Hello World'), ComponentClass)
+ );
+
+ this.render(`{{foo-bar id="foo"}}`);
+
+ this.assertElement(this.firstChild, {
+ tagName: 'a',
+ attrs: {
+ id: 'foo',
+ href: 'http://willRender.com',
+ class: classes('ember-view'),
+ },
+ });
+ }
+
+ ['@test that thing about destroying'](assert) {
+ let ParentDestroyedElements = [];
+ let ChildDestroyedElements = [];
+
+ let ParentComponent = class extends Component {
+ willDestroyElement() {
+ ParentDestroyedElements.push({
+ id: this.itemId,
+ name: 'parent-component',
+ hasParent: Boolean(this.element.parentNode),
+ nextSibling: Boolean(this.element.nextSibling),
+ previousSibling: Boolean(this.element.previousSibling),
+ });
+ }
+ };
+
+ let NestedComponent = class extends Component {
+ willDestroyElement() {
+ ChildDestroyedElements.push({
+ id: this.nestedId,
+ name: 'nested-component',
+ hasParent: Boolean(this.element.parentNode),
+ nextSibling: Boolean(this.element.nextSibling),
+ previousSibling: Boolean(this.element.previousSibling),
+ });
+ }
+ };
+
+ this.owner.register(
+ 'component:parent-component',
+ setComponentTemplate(
+ precompileTemplate(
+ "{{yield}}{{#nested-component nestedId=(concat this.itemId '-A')}}A{{/nested-component}}{{#nested-component nestedId=(concat this.itemId '-B')}}B{{/nested-component}} "
+ ),
+ ParentComponent
+ )
+ );
+
+ this.owner.register(
+ 'component:nested-component',
+ setComponentTemplate(precompileTemplate('{{yield}}'), NestedComponent)
+ );
+
+ let array = trackedArray([{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }, { id: 5 }]);
+
+ this.render(
+ strip`
+ {{#each this.items as |item|}}
+ {{#parent-component itemId=item.id}}{{item.id}}{{/parent-component}}
+ {{/each}}
+ {{#if this.model.shouldShow}}
+ {{#parent-component itemId=6}}6{{/parent-component}}
+ {{/if}}
+ {{#if this.model.shouldShow}}
+ {{#parent-component itemId=7}}7{{/parent-component}}
+ {{/if}}
+ `,
+ {
+ items: array,
+ model: { shouldShow: true },
+ }
+ );
+
+ this.assertText('1AB2AB3AB4AB5AB6AB7AB');
+
+ runTask(() => {
+ array.splice(2, 2);
+ set(this.context, 'model.shouldShow', false);
+ });
+
+ this.assertText('1AB2AB5AB');
+
+ assertDestroyHooks(
+ assert,
+ [...ParentDestroyedElements],
+ [
+ {
+ id: 3,
+ hasParent: true,
+ nextSibling: true,
+ previousSibling: true,
+ },
+ {
+ id: 4,
+ hasParent: true,
+ nextSibling: true,
+ previousSibling: true,
+ },
+ {
+ id: 6,
+ hasParent: true,
+ nextSibling: true,
+ previousSibling: true,
+ },
+ {
+ id: 7,
+ hasParent: true,
+ nextSibling: false,
+ previousSibling: true,
+ },
+ ]
+ );
+
+ assertDestroyHooks(
+ assert,
+ [...ChildDestroyedElements],
+ [
+ {
+ id: '3-A',
+ hasParent: true,
+ nextSibling: true,
+ previousSibling: false,
+ },
+ {
+ id: '3-B',
+ hasParent: true,
+ nextSibling: false,
+ previousSibling: true,
+ },
+ {
+ id: '4-A',
+ hasParent: true,
+ nextSibling: true,
+ previousSibling: false,
+ },
+ {
+ id: '4-B',
+ hasParent: true,
+ nextSibling: false,
+ previousSibling: true,
+ },
+ {
+ id: '6-A',
+ hasParent: true,
+ nextSibling: true,
+ previousSibling: false,
+ },
+ {
+ id: '6-B',
+ hasParent: true,
+ nextSibling: false,
+ previousSibling: true,
+ },
+ {
+ id: '7-A',
+ hasParent: true,
+ nextSibling: true,
+ previousSibling: false,
+ },
+ {
+ id: '7-B',
+ hasParent: true,
+ nextSibling: false,
+ previousSibling: true,
+ },
+ ]
+ );
+ }
+
+ ['@test lifecycle hooks exist on the base class'](assert) {
+ // Make sure we get the finalized component prototype
+ let prototype = Component.proto();
+
+ assert.equal(typeof prototype.didDestroyElement, 'function', 'didDestroyElement exists');
+ assert.equal(typeof prototype.didInsertElement, 'function', 'didInsertElement exists');
+ assert.equal(typeof prototype.didReceiveAttrs, 'function', 'didReceiveAttrs exists');
+ assert.equal(typeof prototype.didRender, 'function', 'didRender exists');
+ assert.equal(typeof prototype.didUpdate, 'function', 'didUpdate exists');
+ assert.equal(typeof prototype.didUpdateAttrs, 'function', 'didUpdateAttrs exists');
+ assert.equal(typeof prototype.willClearRender, 'function', 'willClearRender exists');
+ assert.equal(typeof prototype.willDestroy, 'function', 'willDestroy exists');
+ assert.equal(typeof prototype.willDestroyElement, 'function', 'willDestroyElement exists');
+ assert.equal(typeof prototype.willInsertElement, 'function', 'willInsertElement exists');
+ assert.equal(typeof prototype.willRender, 'function', 'willRender exists');
+ assert.equal(typeof prototype.willUpdate, 'function', 'willUpdate exists');
+ }
+ }
+);
+
+function assertDestroyHooks(assert, _actual, _expected) {
+ _expected.forEach((expected, i) => {
+ let name = expected.name;
+ assert.equal(expected.id, _actual[i].id, `${name} id is the same`);
+ assert.equal(expected.hasParent, _actual[i].hasParent, `${name} has parent node`);
+ assert.equal(expected.nextSibling, _actual[i].nextSibling, `${name} has next sibling node`);
+ assert.equal(
+ expected.previousSibling,
+ _actual[i].previousSibling,
+ `${name} has previous sibling node`
+ );
+ });
+}
+
+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, { attrs, oldAttrs, newAttrs } = {}) {
+ return { name, hook, args: { attrs, oldAttrs, newAttrs } };
+}
+
+function json(serializable) {
+ return JSON.parse(JSON.stringify(serializable));
+}
diff --git a/packages/@ember/-internals/glimmer/tests/integration/components/classic/mut-test.js b/packages/@ember/-internals/glimmer/tests/integration/components/classic/mut-test.js
new file mode 100644
index 00000000000..8c0f11a9795
--- /dev/null
+++ b/packages/@ember/-internals/glimmer/tests/integration/components/classic/mut-test.js
@@ -0,0 +1,613 @@
+import { RenderingTestCase, moduleFor, styles, runTask } from 'internal-test-helpers';
+import { set, get, computed } from '@ember/object';
+import { precompileTemplate } from '@ember/template-compilation';
+import { setComponentTemplate } from '@glimmer/manager';
+import { Component as EmberComponent, htmlSafe } from '../../../utils/helpers';
+
+moduleFor(
+ 'Helpers test: {{mut}} (classic component)',
+ class extends RenderingTestCase {
+ ['@test a simple mutable binding using `mut` propagates properly']() {
+ let bottom;
+
+ this.owner.register(
+ 'component:bottom-mut',
+ setComponentTemplate(
+ precompileTemplate('{{this.setMe}}'),
+ class extends EmberComponent {
+ didInsertElement() {
+ bottom = this;
+ }
+ }
+ )
+ );
+
+ this.owner.register(
+ 'component:middle-mut',
+ setComponentTemplate(
+ precompileTemplate('{{bottom-mut setMe=this.value}}'),
+ class extends EmberComponent {}
+ )
+ );
+
+ this.render('{{middle-mut value=(mut this.val)}}', {
+ val: 12,
+ });
+
+ this.assertText('12', 'the data propagated downwards');
+
+ this.assertStableRerender();
+
+ runTask(() => bottom.attrs.setMe.update(13));
+
+ this.assertText('13', 'the set took effect');
+ this.assert.strictEqual(get(bottom, 'setMe'), 13, "the set took effect on bottom's prop");
+ this.assert.strictEqual(bottom.attrs.setMe.value, 13, "the set took effect on bottom's attr");
+ this.assert.strictEqual(get(this.context, 'val'), 13, 'the set propagated back up');
+
+ runTask(() => set(bottom, 'setMe', 14));
+
+ this.assertText('14', 'the set took effect');
+ this.assert.strictEqual(get(bottom, 'setMe'), 14, "the set took effect on bottom's prop");
+ this.assert.strictEqual(bottom.attrs.setMe.value, 14, "the set took effect on bottom's attr");
+ this.assert.strictEqual(get(this.context, 'val'), 14, 'the set propagated back up');
+
+ runTask(() => set(this.context, 'val', 12));
+
+ this.assertText('12');
+ }
+
+ ['@test a simple mutable binding using `mut` inserts into the DOM']() {
+ let bottom, middle;
+
+ this.owner.register(
+ 'component:bottom-mut',
+ setComponentTemplate(
+ precompileTemplate('{{this.setMe}}'),
+ class extends EmberComponent {
+ didInsertElement() {
+ bottom = this;
+ }
+ }
+ )
+ );
+
+ this.owner.register(
+ 'component:middle-mut',
+ setComponentTemplate(
+ precompileTemplate('{{bottom-mut setMe=(mut this.value)}}'),
+ class extends EmberComponent {
+ didInsertElement() {
+ middle = this;
+ }
+ }
+ )
+ );
+
+ this.render('{{middle-mut value=(mut this.val)}}', {
+ val: 12,
+ });
+
+ this.assertText('12', 'the data propagated downwards');
+
+ this.assertStableRerender();
+
+ runTask(() => bottom.attrs.setMe.update(13));
+
+ this.assertText('13', 'the set took effect');
+ this.assert.strictEqual(get(bottom, 'setMe'), 13, "the set took effect on bottom's prop");
+ this.assert.strictEqual(bottom.attrs.setMe.value, 13, "the set took effect on bottom's attr");
+ this.assert.strictEqual(get(middle, 'value'), 13, "the set propagated to middle's prop");
+ this.assert.strictEqual(middle.attrs.value.value, 13, "the set propagated to middle's attr");
+ this.assert.strictEqual(get(this.context, 'val'), 13, 'the set propagated back up');
+
+ runTask(() => set(bottom, 'setMe', 14));
+
+ this.assertText('14', 'the set took effect');
+ this.assert.strictEqual(get(bottom, 'setMe'), 14, "the set took effect on bottom's prop");
+ this.assert.strictEqual(bottom.attrs.setMe.value, 14, "the set took effect on bottom's attr");
+ this.assert.strictEqual(get(middle, 'value'), 14, "the set propagated to middle's prop");
+ this.assert.strictEqual(middle.attrs.value.value, 14, "the set propagated to middle's attr");
+ this.assert.strictEqual(get(this.context, 'val'), 14, 'the set propagated back up');
+
+ runTask(() => set(this.context, 'val', 12));
+
+ this.assertText('12');
+ }
+
+ // See https://github.com/emberjs/ember.js/commit/807a0cd for an explanation of this test
+ ['@test using a string value through middle tier does not trigger assertion (due to the auto-mut transform)']() {
+ let bottom;
+
+ this.owner.register(
+ 'component:bottom-mut',
+ setComponentTemplate(
+ precompileTemplate('{{this.stuff}}'),
+ class extends EmberComponent {
+ didInsertElement() {
+ bottom = this;
+ }
+ }
+ )
+ );
+
+ this.owner.register(
+ 'component:middle-mut',
+ setComponentTemplate(
+ precompileTemplate('{{bottom-mut stuff=this.value}}'),
+ class extends EmberComponent {}
+ )
+ );
+
+ this.render('{{middle-mut value="foo"}}');
+
+ this.assert.equal(get(bottom, 'stuff'), 'foo', 'the data propagated');
+ this.assertText('foo');
+
+ this.assertStableRerender();
+
+ // No U-R for this test
+ }
+
+ ['@test {{readonly}} of a {{mut}} is converted into an immutable binding']() {
+ let middle, bottom;
+
+ this.owner.register(
+ 'component:bottom-mut',
+ setComponentTemplate(
+ precompileTemplate('{{this.setMe}}'),
+ class extends EmberComponent {
+ didInsertElement() {
+ bottom = this;
+ }
+ }
+ )
+ );
+
+ this.owner.register(
+ 'component:middle-mut',
+ setComponentTemplate(
+ precompileTemplate('{{bottom-mut setMe=(readonly this.value)}}'),
+ class extends EmberComponent {
+ didInsertElement() {
+ middle = this;
+ }
+ }
+ )
+ );
+
+ this.render('{{middle-mut value=(mut this.val)}}', {
+ val: 12,
+ });
+
+ this.assertText('12');
+
+ this.assertStableRerender();
+
+ runTask(() => middle.attrs.value.update(13));
+
+ this.assert.strictEqual(get(middle, 'value'), 13, "the set took effect on middle's prop");
+ this.assert.strictEqual(middle.attrs.value.value, 13, "the set took effect on middle's attr");
+
+ runTask(() => set(middle, 'value', 14));
+
+ this.assert.strictEqual(get(middle, 'value'), 14, "the set took effect on middle's prop");
+ this.assert.strictEqual(middle.attrs.value.value, 14, "the set took effect on middle's attr");
+ this.assert.strictEqual(
+ bottom.attrs.setMe,
+ 14,
+ 'the mutable binding has been converted to an immutable cell'
+ );
+ this.assertText('14');
+ this.assert.strictEqual(get(this.context, 'val'), 14, 'the set propagated back up');
+
+ runTask(() => set(this.context, 'val', 12));
+
+ this.assertText('12');
+ }
+
+ ['@test a simple mutable binding using {{mut}} is available in hooks']() {
+ let bottom;
+ let willRender = [];
+ let didInsert = [];
+
+ this.owner.register(
+ 'component:bottom-mut',
+ setComponentTemplate(
+ precompileTemplate('{{this.setMe}}'),
+ class extends EmberComponent {
+ willRender() {
+ willRender.push(get(this, 'setMe'));
+ }
+ didInsertElement() {
+ didInsert.push(get(this, 'setMe'));
+ bottom = this;
+ }
+ }
+ )
+ );
+
+ this.owner.register(
+ 'component:middle-mut',
+ setComponentTemplate(
+ precompileTemplate('{{bottom-mut setMe=(mut this.value)}}'),
+ class extends EmberComponent {}
+ )
+ );
+
+ this.render('{{middle-mut value=(mut this.val)}}', {
+ val: 12,
+ });
+
+ this.assert.deepEqual(willRender, [12], 'willReceive is [12]');
+ this.assert.deepEqual(didInsert, [12], 'didInsert is [12]');
+ this.assertText('12');
+
+ this.assertStableRerender();
+
+ this.assert.deepEqual(willRender, [12], 'willReceive is [12]');
+ this.assert.deepEqual(didInsert, [12], 'didInsert is [12]');
+ this.assert.strictEqual(get(bottom, 'setMe'), 12, 'the data propagated');
+
+ runTask(() => bottom.attrs.setMe.update(13));
+
+ this.assert.strictEqual(get(bottom, 'setMe'), 13, "the set took effect on bottom's prop");
+ this.assert.strictEqual(bottom.attrs.setMe.value, 13, "the set took effect on bottom's attr");
+ this.assert.strictEqual(get(this.context, 'val'), 13, 'the set propagated back up');
+
+ runTask(() => set(bottom, 'setMe', 14));
+
+ this.assert.strictEqual(get(bottom, 'setMe'), 14, "the set took effect on bottom's prop");
+ this.assert.strictEqual(bottom.attrs.setMe.value, 14, "the set took effect on bottom's attr");
+ this.assert.strictEqual(get(this.context, 'val'), 14, 'the set propagated back up');
+
+ runTask(() => set(this.context, 'val', 12));
+
+ this.assertText('12');
+ }
+
+ ['@test a mutable binding with a backing computed property and attribute present in the root of the component is updated when the upstream property invalidates #11023']() {
+ let bottom, middle;
+
+ this.owner.register(
+ 'component:bottom-mut',
+ setComponentTemplate(
+ precompileTemplate('{{this.thingy}}'),
+ class extends EmberComponent {
+ thingy = null;
+ didInsertElement() {
+ bottom = this;
+ }
+ }
+ )
+ );
+
+ this.owner.register(
+ 'component:middle-mut',
+ setComponentTemplate(
+ precompileTemplate('{{bottom-mut thingy=(mut this.val)}}'),
+ class extends EmberComponent {
+ baseValue = 12;
+ @computed('baseValue')
+ get val() {
+ return this.baseValue;
+ }
+ didInsertElement() {
+ middle = this;
+ }
+ }
+ )
+ );
+
+ this.render('{{middle-mut}}');
+
+ this.assert.strictEqual(get(bottom, 'thingy'), 12, 'data propagated');
+ this.assertText('12');
+
+ this.assertStableRerender();
+
+ runTask(() => set(middle, 'baseValue', 13));
+
+ this.assert.strictEqual(get(middle, 'val'), 13, 'the set took effect');
+ this.assert.strictEqual(
+ bottom.attrs.thingy.value,
+ 13,
+ "the set propagated down to bottom's attrs"
+ );
+ this.assert.strictEqual(
+ get(bottom, 'thingy'),
+ 13,
+ "the set propagated down to bottom's prop"
+ );
+ this.assertText('13');
+
+ runTask(() => set(middle, 'baseValue', 12));
+
+ this.assertText('12');
+ }
+
+ ['@test automatic mutable bindings exposes a mut cell in attrs']() {
+ let inner;
+
+ this.owner.register(
+ 'component:x-inner',
+ setComponentTemplate(
+ precompileTemplate('{{this.foo}}'),
+ class extends EmberComponent {
+ didInsertElement() {
+ inner = this;
+ }
+ }
+ )
+ );
+
+ this.owner.register(
+ 'component:x-outer',
+ setComponentTemplate(
+ precompileTemplate('{{x-inner foo=this.bar}}'),
+ class extends EmberComponent {}
+ )
+ );
+
+ this.render('{{x-outer bar=this.baz}}', { baz: 'foo' });
+
+ this.assertText('foo');
+
+ this.assertStableRerender();
+
+ runTask(() => inner.attrs.foo.update('bar'));
+
+ this.assert.equal(inner.attrs.foo.value, 'bar');
+ this.assert.equal(get(inner, 'foo'), 'bar');
+ this.assertText('bar');
+
+ runTask(() => inner.attrs.foo.update('foo'));
+
+ this.assertText('foo');
+ }
+
+ ['@test automatic mutable bindings tolerate undefined non-stream inputs and attempts to set them']() {
+ let inner;
+
+ this.owner.register(
+ 'component:x-inner',
+ setComponentTemplate(
+ precompileTemplate('{{@model}}'),
+ class extends EmberComponent {
+ didInsertElement() {
+ inner = this;
+ }
+ }
+ )
+ );
+
+ this.owner.register(
+ 'component:x-outer',
+ setComponentTemplate(
+ precompileTemplate('{{x-inner model=this.nonexistent}}'),
+ class extends EmberComponent {}
+ )
+ );
+
+ this.render('{{x-outer}}');
+
+ this.assertText('');
+
+ this.assertStableRerender();
+
+ runTask(() => inner.attrs.model.update(42));
+
+ this.assert.equal(inner.attrs.model.value, 42);
+ this.assert.equal(get(inner, 'model'), 42);
+ this.assertText('42');
+
+ runTask(() => inner.attrs.model.update(undefined));
+
+ this.assertText('');
+ }
+
+ ['@test automatic mutable bindings tolerate constant non-stream inputs and attempts to set them']() {
+ let inner;
+
+ this.owner.register(
+ 'component:x-inner',
+ setComponentTemplate(
+ precompileTemplate('hello{{@model}}'),
+ class extends EmberComponent {
+ didInsertElement() {
+ inner = this;
+ }
+ }
+ )
+ );
+
+ this.owner.register(
+ 'component:x-outer',
+ setComponentTemplate(
+ precompileTemplate(
+ // Use `this.x` here instead of `@x` to let `x-inner` mutate `this.x`.
+ // `@x` points to the literal binding from `x-outer`, which is of
+ // course immutable.
+ '{{x-inner model=this.x}}'
+ ),
+ class extends EmberComponent {}
+ )
+ );
+
+ this.render('{{x-outer x="foo"}}');
+
+ this.assertText('hellofoo');
+
+ this.assertStableRerender();
+
+ runTask(() => inner.attrs.model.update(42));
+
+ this.assert.equal(inner.attrs.model.value, 42);
+ this.assert.equal(get(inner, 'model'), 42);
+ this.assertText('hello42');
+
+ runTask(() => inner.attrs.model.update('foo'));
+
+ this.assertText('hellofoo');
+ }
+ }
+);
+
+moduleFor(
+ 'Mutable Bindings used in Computed Properties that are bound as attributeBindings (classic component)',
+ class extends RenderingTestCase {
+ ['@test an attribute binding of a computed property of a 2-way bound attr recomputes when the attr changes']() {
+ let input, output;
+
+ this.owner.register(
+ 'component:x-input',
+ class extends EmberComponent {
+ didInsertElement() {
+ input = this;
+ }
+ }
+ );
+
+ this.owner.register(
+ 'component:x-output',
+ setComponentTemplate(
+ precompileTemplate('{{this.height}}'),
+ class extends EmberComponent {
+ attributeBindings = ['style'];
+ didInsertElement() {
+ output = this;
+ }
+ @computed('height')
+ get style() {
+ let height = this.get('height');
+ return htmlSafe(`height: ${height}px;`);
+ }
+ height = 20;
+ }
+ )
+ );
+
+ this.render('{{x-output height=this.height}}{{x-input height=(mut this.height)}}', {
+ height: 60,
+ });
+
+ this.assertComponentElement(this.firstChild, {
+ tagName: 'div',
+ attrs: { style: styles('height: 60px;') },
+ content: '60',
+ });
+
+ this.assertStableRerender();
+
+ runTask(() => input.attrs.height.update(35));
+
+ this.assert.strictEqual(get(output, 'height'), 35, 'the set took effect');
+ this.assert.strictEqual(get(this.context, 'height'), 35, 'the set propagated back up');
+ this.assertComponentElement(this.firstChild, {
+ tagName: 'div',
+ attrs: { style: styles('height: 35px;') },
+ content: '35',
+ });
+
+ runTask(() => set(input, 'height', 36));
+
+ this.assert.strictEqual(get(output, 'height'), 36, 'the set took effect');
+ this.assert.strictEqual(get(this.context, 'height'), 36, 'the set propagated back up');
+ this.assertComponentElement(this.firstChild, {
+ tagName: 'div',
+ attrs: { style: styles('height: 36px;') },
+ content: '36',
+ });
+
+ runTask(() => set(this.context, 'height', 60));
+
+ this.assertComponentElement(this.firstChild, {
+ tagName: 'div',
+ attrs: { style: styles('height: 60px;') },
+ content: '60',
+ });
+ this.assert.strictEqual(get(input, 'height'), 60);
+ }
+
+ ['@test an attribute binding of a computed property with a setter of a 2-way bound attr recomputes when the attr changes']() {
+ let input, output;
+
+ this.owner.register(
+ 'component:x-input',
+ class extends EmberComponent {
+ didInsertElement() {
+ input = this;
+ }
+ }
+ );
+
+ this.owner.register(
+ 'component:x-output',
+ setComponentTemplate(
+ precompileTemplate('{{this.width}}x{{this.height}}'),
+ class extends EmberComponent {
+ attributeBindings = ['style'];
+ didInsertElement() {
+ output = this;
+ }
+ @computed('height', 'width')
+ get style() {
+ let height = this.get('height');
+ let width = this.get('width');
+ return htmlSafe(`height: ${height}px; width: ${width}px;`);
+ }
+ height = 20;
+ @computed('height')
+ get width() {
+ return this.get('height') * 2;
+ }
+ set width(width) {
+ this.set('height', width / 2);
+ }
+ }
+ )
+ );
+
+ this.render('{{x-output width=this.width}}{{x-input width=(mut this.width)}}', {
+ width: 70,
+ });
+
+ this.assertComponentElement(this.firstChild, {
+ tagName: 'div',
+ attrs: { style: styles('height: 35px; width: 70px;') },
+ content: '70x35',
+ });
+
+ this.assertStableRerender();
+
+ runTask(() => set(input, 'width', 80));
+
+ this.assert.strictEqual(get(output, 'width'), 80, 'the set took effect');
+ this.assert.strictEqual(get(this.context, 'width'), 80, 'the set propagated back up');
+ this.assertComponentElement(this.firstChild, {
+ tagName: 'div',
+ attrs: { style: styles('height: 40px; width: 80px;') },
+ content: '80x40',
+ });
+
+ runTask(() => input.attrs.width.update(90));
+
+ this.assert.strictEqual(get(output, 'width'), 90, 'the set took effect');
+ this.assert.strictEqual(get(this.context, 'width'), 90, 'the set propagated back up');
+ this.assertComponentElement(this.firstChild, {
+ tagName: 'div',
+ attrs: { style: styles('height: 45px; width: 90px;') },
+ content: '90x45',
+ });
+
+ runTask(() => set(this.context, 'width', 70));
+
+ this.assertComponentElement(this.firstChild, {
+ tagName: 'div',
+ attrs: { style: styles('height: 35px; width: 70px;') },
+ content: '70x35',
+ });
+ this.assert.strictEqual(get(input, 'width'), 70);
+ }
+ }
+);
diff --git a/packages/@ember/-internals/glimmer/tests/integration/components/classic/readonly-test.js b/packages/@ember/-internals/glimmer/tests/integration/components/classic/readonly-test.js
new file mode 100644
index 00000000000..fcd3671cccb
--- /dev/null
+++ b/packages/@ember/-internals/glimmer/tests/integration/components/classic/readonly-test.js
@@ -0,0 +1,271 @@
+import { RenderingTestCase, moduleFor, runTask } from 'internal-test-helpers';
+import { precompileTemplate } from '@ember/template-compilation';
+import { setComponentTemplate } from '@glimmer/manager';
+
+import { set, get } from '@ember/object';
+
+import { Component } from '../../../utils/helpers';
+
+moduleFor(
+ 'Helpers test: {{readonly}} (classic component)',
+ class extends RenderingTestCase {
+ ['@test {{readonly}} of a path should work']() {
+ let component;
+
+ this.owner.register(
+ 'component:foo-bar',
+ setComponentTemplate(
+ precompileTemplate('{{this.value}}'),
+ class extends Component {
+ didInsertElement() {
+ component = this;
+ }
+ }
+ )
+ );
+
+ this.render('{{foo-bar value=(readonly this.val)}}', {
+ val: 12,
+ });
+
+ this.assertText('12');
+
+ this.assertStableRerender();
+
+ runTask(() => set(component, 'value', 13));
+ this.assert.notOk(component.attrs.value.update);
+
+ this.assertText('13', 'local property is updated');
+ this.assert.equal(get(this.context, 'val'), 12, 'upstream attribute is not updated');
+
+ // No U-R
+ }
+
+ '@test updating a {{readonly}} property from above works'(assert) {
+ let component;
+
+ this.owner.register(
+ 'component:foo-bar',
+ setComponentTemplate(
+ precompileTemplate('{{this.value}}'),
+ class extends Component {
+ init() {
+ super.init(...arguments);
+ component = this;
+ }
+ }
+ )
+ );
+
+ this.render('{{foo-bar value=(readonly this.thing)}}', {
+ thing: 'initial',
+ });
+
+ this.assertText('initial');
+
+ this.assertStableRerender();
+
+ assert.strictEqual(component.attrs.value, 'initial', 'no mutable cell');
+ assert.strictEqual(get(component, 'value'), 'initial', 'no mutable cell');
+ assert.strictEqual(this.context.thing, 'initial');
+
+ runTask(() => set(this.context, 'thing', 'updated!'));
+
+ this.assertText('updated!');
+ assert.strictEqual(component.attrs.value, 'updated!', 'passed down value was set in attrs');
+ assert.strictEqual(get(component, 'value'), 'updated!', 'passed down value was set');
+
+ runTask(() => set(this.context, 'thing', 'initial'));
+
+ this.assertText('initial');
+ }
+
+ '@test updating a nested path of a {{readonly}}'(assert) {
+ let component;
+
+ this.owner.register(
+ 'component:foo-bar',
+ setComponentTemplate(
+ precompileTemplate('{{this.value.prop}}'),
+ class extends Component {
+ didInsertElement() {
+ component = this;
+ }
+ }
+ )
+ );
+
+ this.render('{{foo-bar value=(readonly this.thing)}}', {
+ thing: {
+ prop: 'initial',
+ },
+ });
+
+ this.assertText('initial');
+
+ this.assertStableRerender();
+
+ assert.notOk(component.attrs.value.update, 'no update available');
+ assert.deepEqual(get(component, 'value'), { prop: 'initial' });
+ assert.deepEqual(this.context.thing, { prop: 'initial' });
+
+ runTask(() => set(component, 'value.prop', 'updated!'));
+
+ this.assertText('updated!', 'nested path is updated');
+ assert.deepEqual(get(component, 'value'), { prop: 'updated!' });
+ assert.deepEqual(this.context.thing, { prop: 'updated!' });
+
+ runTask(() => set(component, 'value.prop', 'initial'));
+
+ this.assertText('initial');
+ }
+
+ ['@test {{readonly}} of a string renders correctly']() {
+ let component;
+
+ this.owner.register(
+ 'component:foo-bar',
+ setComponentTemplate(
+ precompileTemplate('{{this.value}}'),
+ class extends Component {
+ didInsertElement() {
+ component = this;
+ }
+ }
+ )
+ );
+
+ this.render('{{foo-bar value=(readonly "12")}}');
+
+ this.assertText('12');
+
+ this.assertStableRerender();
+
+ this.assert.notOk(component.attrs.value.update);
+ this.assert.strictEqual(get(component, 'value'), '12');
+
+ runTask(() => set(component, 'value', '13'));
+
+ this.assertText('13', 'local property is updated');
+ this.assert.strictEqual(get(component, 'value'), '13');
+
+ runTask(() => set(component, 'value', '12'));
+
+ this.assertText('12');
+ }
+
+ ['@test {{mut}} of a {{readonly}} mutates only the middle and bottom tiers']() {
+ let middle, bottom;
+
+ this.owner.register(
+ 'component:x-bottom',
+ setComponentTemplate(
+ precompileTemplate('{{this.bar}}'),
+ class extends Component {
+ didInsertElement() {
+ bottom = this;
+ }
+ }
+ )
+ );
+
+ this.owner.register(
+ 'component:x-middle',
+ setComponentTemplate(
+ precompileTemplate('{{this.foo}} {{x-bottom bar=(mut this.foo)}}'),
+ class extends Component {
+ didInsertElement() {
+ middle = this;
+ }
+ }
+ )
+ );
+
+ this.render('{{x-middle foo=(readonly this.val)}}', {
+ val: 12,
+ });
+
+ this.assertText('12 12');
+
+ this.assertStableRerender();
+
+ this.assert.equal(get(bottom, 'bar'), 12, "bottom's local bar received the value");
+ this.assert.equal(get(middle, 'foo'), 12, "middle's local foo received the value");
+
+ // updating the mut-cell directly
+ runTask(() => bottom.attrs.bar.update(13));
+
+ this.assert.equal(
+ get(bottom, 'bar'),
+ 13,
+ "bottom's local bar was updated after set of bottom's bar"
+ );
+ this.assert.equal(
+ get(middle, 'foo'),
+ 13,
+ "middle's local foo was updated after set of bottom's bar"
+ );
+ this.assertText('13 13');
+ this.assert.equal(get(this.context, 'val'), 12, 'But context val is not updated');
+
+ runTask(() => set(bottom, 'bar', 14));
+
+ this.assert.equal(
+ get(bottom, 'bar'),
+ 14,
+ "bottom's local bar was updated after set of bottom's bar"
+ );
+ this.assert.equal(
+ get(middle, 'foo'),
+ 14,
+ "middle's local foo was updated after set of bottom's bar"
+ );
+ this.assertText('14 14');
+ this.assert.equal(get(this.context, 'val'), 12, 'But context val is not updated');
+
+ this.assert.notOk(middle.attrs.foo.update, "middle's foo attr is not a mutable cell");
+ runTask(() => set(middle, 'foo', 15));
+
+ this.assertText('15 15');
+ this.assert.equal(get(middle, 'foo'), 15, "set of middle's foo took effect");
+ this.assert.equal(
+ get(bottom, 'bar'),
+ 15,
+ "bottom's local bar was updated after set of middle's foo"
+ );
+ this.assert.equal(get(this.context, 'val'), 12, 'Context val remains unchanged');
+
+ runTask(() => set(this.context, 'val', 10));
+
+ this.assertText('10 10');
+ this.assert.equal(
+ get(bottom, 'bar'),
+ 10,
+ "bottom's local bar was updated after set of context's val"
+ );
+ this.assert.equal(
+ get(middle, 'foo'),
+ 10,
+ "middle's local foo was updated after set of context's val"
+ );
+
+ // setting as a normal property
+ runTask(() => set(bottom, 'bar', undefined));
+
+ this.assertText(' ');
+ this.assert.equal(
+ get(bottom, 'bar'),
+ undefined,
+ "bottom's local bar was updated to a falsy value"
+ );
+ this.assert.equal(
+ get(middle, 'foo'),
+ undefined,
+ "middle's local foo was updated to a falsy value"
+ );
+
+ runTask(() => set(this.context, 'val', 12));
+ this.assertText('12 12', 'bottom and middle were both reset');
+ }
+ }
+);
diff --git a/packages/@ember/-internals/glimmer/tests/integration/components/classic/template-rendering-test.js b/packages/@ember/-internals/glimmer/tests/integration/components/classic/template-rendering-test.js
new file mode 100644
index 00000000000..f0dfb6178e0
--- /dev/null
+++ b/packages/@ember/-internals/glimmer/tests/integration/components/classic/template-rendering-test.js
@@ -0,0 +1,90 @@
+import Route from '@ember/routing/route';
+import Controller from '@ember/controller';
+import { moduleFor, ApplicationTestCase } from 'internal-test-helpers';
+import { run } from '@ember/runloop';
+import { Component as EmberComponent } from '@ember/-internals/glimmer';
+import { precompileTemplate } from '@ember/template-compilation';
+
+moduleFor(
+ 'Route - template rendering (classic component)',
+ class extends ApplicationTestCase {
+ ['@test Components inside an outlet have their didInsertElement hook invoked when the route is displayed'](
+ assert
+ ) {
+ this.add(
+ 'template:index',
+ precompileTemplate(
+ '{{#if this.showFirst}}{{my-component}}{{else}}{{other-component}}{{/if}}'
+ )
+ );
+
+ let myComponentCounter = 0;
+ let otherComponentCounter = 0;
+ let indexController;
+
+ this.router.map(function () {
+ this.route('index', { path: '/' });
+ });
+
+ this.add(
+ 'controller:index',
+ class extends Controller {
+ showFirst = true;
+ }
+ );
+
+ this.add(
+ 'route:index',
+ class extends Route {
+ setupController(controller) {
+ indexController = controller;
+ }
+ }
+ );
+
+ this.add(
+ 'component:my-component',
+ class extends EmberComponent {
+ didInsertElement() {
+ myComponentCounter++;
+ }
+ }
+ );
+
+ this.add(
+ 'component:other-component',
+ class extends EmberComponent {
+ didInsertElement() {
+ otherComponentCounter++;
+ }
+ }
+ );
+
+ return this.visit('/').then(() => {
+ assert.strictEqual(
+ myComponentCounter,
+ 1,
+ 'didInsertElement invoked on displayed component'
+ );
+ assert.strictEqual(
+ otherComponentCounter,
+ 0,
+ 'didInsertElement not invoked on displayed component'
+ );
+
+ run(() => indexController.set('showFirst', false));
+
+ assert.strictEqual(
+ myComponentCounter,
+ 1,
+ 'didInsertElement not invoked on displayed component'
+ );
+ assert.strictEqual(
+ otherComponentCounter,
+ 1,
+ 'didInsertElement invoked on displayed component'
+ );
+ });
+ }
+ }
+);
diff --git a/packages/@ember/-internals/glimmer/tests/integration/components/classic/tracked-test.js b/packages/@ember/-internals/glimmer/tests/integration/components/classic/tracked-test.js
new file mode 100644
index 00000000000..c580ec85c3f
--- /dev/null
+++ b/packages/@ember/-internals/glimmer/tests/integration/components/classic/tracked-test.js
@@ -0,0 +1,51 @@
+import { tracked } from '@ember/-internals/metal';
+import { moduleFor, RenderingTestCase, runTask } from 'internal-test-helpers';
+import { Component as EmberComponent } from '../../../utils/helpers';
+import { precompileTemplate } from '@ember/template-compilation';
+import { setComponentTemplate } from '@glimmer/manager';
+
+moduleFor(
+ 'Component Tracked Properties (classic component)',
+ class extends RenderingTestCase {
+ '@test simple test using classic component'() {
+ let personId = 0;
+ class Person {
+ @tracked first;
+ @tracked last;
+
+ constructor(first, last) {
+ this.id = personId++;
+ this.first = first;
+ this.last = last;
+ }
+ }
+
+ class PersonComponent extends EmberComponent {
+ @tracked first;
+ @tracked last;
+
+ get person() {
+ return new Person(this.first, this.last);
+ }
+ }
+
+ this.owner.register(
+ 'component:person-wrapper',
+ setComponentTemplate(
+ precompileTemplate('{{@first}} {{@last}} | {{this.person.first}} {{this.person.last}}'),
+ PersonComponent
+ )
+ );
+
+ this.render(' ', {
+ first: 'robert',
+ last: 'jackson',
+ });
+
+ this.assertText('robert jackson | robert jackson');
+
+ runTask(() => this.context.set('first', 'max'));
+ this.assertText('max jackson | max jackson');
+ }
+ }
+);
diff --git a/packages/@ember/-internals/glimmer/tests/integration/components/utils-test.js b/packages/@ember/-internals/glimmer/tests/integration/components/classic/utils-test.js
similarity index 99%
rename from packages/@ember/-internals/glimmer/tests/integration/components/utils-test.js
rename to packages/@ember/-internals/glimmer/tests/integration/components/classic/utils-test.js
index 790d09862ab..fd0c7fe77aa 100644
--- a/packages/@ember/-internals/glimmer/tests/integration/components/utils-test.js
+++ b/packages/@ember/-internals/glimmer/tests/integration/components/classic/utils-test.js
@@ -12,7 +12,7 @@ import {
import { precompileTemplate } from '@ember/template-compilation';
import { setComponentTemplate } from '@glimmer/manager';
-import { Component } from '../../utils/helpers';
+import { Component } from '../../../utils/helpers';
moduleFor(
'View tree tests',
diff --git a/packages/@ember/-internals/glimmer/tests/integration/components/classic/yield-test.js b/packages/@ember/-internals/glimmer/tests/integration/components/classic/yield-test.js
new file mode 100644
index 00000000000..6c2fb909f5e
--- /dev/null
+++ b/packages/@ember/-internals/glimmer/tests/integration/components/classic/yield-test.js
@@ -0,0 +1,33 @@
+import { RenderingTestCase, moduleFor } from 'internal-test-helpers';
+import { precompileTemplate } from '@ember/template-compilation';
+import { setComponentTemplate } from '@glimmer/manager';
+import { Component as EmberComponent } from '../../../utils/helpers';
+
+moduleFor(
+ 'Helpers test: {{yield}} helper (classic component)',
+ class extends RenderingTestCase {
+ // INUR not need with no data update
+ ['@test yield should not introduce a view'](assert) {
+ let ParentCompComponent = class extends EmberComponent {
+ isParentComponent = true;
+ };
+
+ let ChildCompComponent = class extends EmberComponent {
+ didReceiveAttrs() {
+ super.didReceiveAttrs();
+ let parentView = this.get('parentView');
+
+ assert.ok(parentView.get('isParentComponent'));
+ }
+ };
+
+ this.owner.register(
+ 'component:parent-comp',
+ setComponentTemplate(precompileTemplate('{{yield}}'), ParentCompComponent)
+ );
+ this.owner.register('component:child-comp', ChildCompComponent);
+
+ this.render('{{#parent-comp}}{{child-comp}}{{/parent-comp}}');
+ }
+ }
+);
diff --git a/packages/@ember/-internals/glimmer/tests/integration/components/component-template-test.js b/packages/@ember/-internals/glimmer/tests/integration/components/component-template-test.js
index 0571b381e22..8a5085e2a53 100644
--- a/packages/@ember/-internals/glimmer/tests/integration/components/component-template-test.js
+++ b/packages/@ember/-internals/glimmer/tests/integration/components/component-template-test.js
@@ -4,7 +4,6 @@ import { moduleFor, RenderingTestCase, runTask } from 'internal-test-helpers';
import { setComponentTemplate, getComponentTemplate } from '@glimmer/manager';
import { precompileTemplate } from '@ember/template-compilation';
import Component from '@glimmer/component';
-import { Component as EmberComponent } from '../../utils/helpers';
moduleFor(
'Components test: setComponentTemplate',
@@ -74,23 +73,6 @@ moduleFor(
}, /Cannot call `setComponentTemplate` multiple times on the same class \(`Thing`\)/);
}
- '@test templates set with setComponentTemplate are inherited (EmberObject.extend())'() {
- let Parent = setComponentTemplate(
- precompileTemplate('hello'),
- class extends EmberComponent {}
- );
-
- this.owner.register('component:foo-bar', class extends Parent {});
-
- this.render(' ');
-
- this.assertComponentElement(this.firstChild, { content: 'hello' });
-
- runTask(() => this.rerender());
-
- this.assertComponentElement(this.firstChild, { content: 'hello' });
- }
-
'@test templates set with setComponentTemplate are inherited (native ES class extends)'() {
let Parent = setComponentTemplate(precompileTemplate('hello'), class extends Component {});
diff --git a/packages/@ember/-internals/glimmer/tests/integration/components/contextual-components-test.js b/packages/@ember/-internals/glimmer/tests/integration/components/contextual-components-test.js
index 9c1566896a8..ecabc49d499 100644
--- a/packages/@ember/-internals/glimmer/tests/integration/components/contextual-components-test.js
+++ b/packages/@ember/-internals/glimmer/tests/integration/components/contextual-components-test.js
@@ -2,11 +2,9 @@ import { DEBUG } from '@glimmer/env';
import { moduleFor, RenderingTestCase, applyMixins, strip, runTask } from 'internal-test-helpers';
import { action } from '@ember/object';
-import { A as emberA } from '@ember/array';
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
-import { Component as EmberComponent } from '../../utils/helpers';
import PositionalComponent from '../../utils/positional-component';
import { precompileTemplate } from '@ember/template-compilation';
import { setComponentTemplate } from '@glimmer/manager';
@@ -507,40 +505,6 @@ moduleFor(
this.assertText('Inner 28');
}
- ['@test conflicting positional and hash parameters does not raise an assertion if rerendered']() {
- // In some cases, rerendering with a positional param used to cause an
- // assertion. This test checks it does not.
- this.owner.register(
- 'component:-looked-up',
- setComponentTemplate(
- precompileTemplate('{{this.greeting}} {{this.name}}'),
- class extends EmberComponent {
- static positionalParams = ['name'];
- }
- )
- );
-
- this.render('{{component (component "-looked-up" this.model.name greeting="Hodi")}}', {
- model: {
- name: 'Hodari',
- },
- });
-
- this.assertText('Hodi Hodari');
-
- runTask(() => this.rerender());
-
- this.assertText('Hodi Hodari');
-
- runTask(() => this.context.set('model.name', 'Sergio'));
-
- this.assertText('Hodi Sergio');
-
- runTask(() => this.context.set('model', { name: 'Hodari' }));
-
- this.assertText('Hodi Hodari');
- }
-
['@test component with dynamic component name resolving to undefined, then an existing component']() {
this.owner.register(
'component:foo-bar',
@@ -804,13 +768,17 @@ moduleFor(
this.owner.register(
'component:my-component',
- class extends EmberComponent {
- static positionalParams = ['value'];
+ setComponentTemplate(
+ precompileTemplate(''),
+ class extends PositionalComponent {
+ static positionalParams = ['value'];
- didReceiveAttrs() {
- value = this.getAttr('value');
+ constructor(owner, args) {
+ super(owner, args);
+ value = this.value;
+ }
}
- }
+ )
);
this.render(
@@ -828,14 +796,8 @@ moduleFor(
this.owner.register(
'component:my-nested-component',
setComponentTemplate(
- precompileTemplate('{{this.myProp}} '),
- class extends EmberComponent {
- static positionalParams = ['my-parent-attr'];
-
- didReceiveAttrs() {
- this.set('myProp', this.getAttr('my-parent-attr'));
- }
- }
+ precompileTemplate('{{@my-parent-attr}} '),
+ class extends Component {}
)
);
@@ -853,21 +815,17 @@ moduleFor(
'component:my-action-component',
setComponentTemplate(
precompileTemplate(
- '{{#my-component my-attr=this.myProp as |api|}}{{api.my-nested-component}}{{/my-component}}Change value '
+ '{{#my-component my-attr=@myProp as |api|}}{{api.my-nested-component}}{{/my-component}}Change value '
),
- class extends EmberComponent {
- @action
- changeValue() {
- this.incrementProperty('myProp');
- }
- }
+ class extends Component {}
)
);
- this.render('{{my-action-component myProp=this.model.myProp}}', {
+ this.render('{{my-action-component myProp=this.model.myProp changeValue=this.changeValue}}', {
model: {
myProp: 1,
},
+ changeValue: () => this.context.incrementProperty('model.myProp'),
});
assert.equal(this.$('#nested-prop').text(), '1');
@@ -918,103 +876,6 @@ moduleFor(
this.assertText('Foo');
}
- ['@test parameters in a contextual component are mutable when value is a param'](assert) {
- // This checks that a `(mut)` is added to parameters and attributes to
- // contextual components when it is a param.
- this.owner.register(
- 'component:change-button',
- setComponentTemplate(
- precompileTemplate(
- 'Change to 10 '
- ),
- class extends EmberComponent {
- static positionalParams = ['val'];
- }
- )
- );
-
- this.render(
- strip`
- {{component (component "change-button" this.model.val2)}}
- {{this.model.val2}} `,
- {
- model: {
- val2: 8,
- },
- }
- );
-
- assert.equal(this.$('.value').text(), '8');
-
- runTask(() => this.rerender());
-
- assert.equal(this.$('.value').text(), '8');
-
- runTask(() => this.$('.my-button').click());
-
- assert.equal(this.$('.value').text(), '10');
-
- runTask(() => this.context.set('model', { val2: 8 }));
-
- assert.equal(this.$('.value').text(), '8');
- }
-
- ['@test tagless blockless components render'](assert) {
- this.owner.register(
- 'component:my-comp',
- class extends EmberComponent {
- tagName = '';
- }
- );
-
- this.render(`{{my-comp}}`);
-
- runTask(() => this.rerender());
-
- assert.equal(this.$().text(), '');
- }
-
- ['@test GH#13494 tagless blockless component with property binding'](assert) {
- this.owner.register(
- 'component:outer-component',
- setComponentTemplate(
- precompileTemplate(
- 'message: {{this.message}}{{inner-component message=this.message}} '
- ),
- class extends Component {
- @tracked message = 'hello';
- @action
- change() {
- this.message = 'goodbye';
- }
- }
- )
- );
-
- this.owner.register(
- 'component:inner-component',
- class extends EmberComponent {
- tagName = '';
- }
- );
-
- this.render(`{{outer-component}}`);
-
- assert.equal(this.$().text(), 'message: hello');
-
- runTask(() => this.rerender());
-
- assert.equal(this.$().text(), 'message: hello');
-
- runTask(() => this.$('button').click());
-
- assert.equal(this.$().text(), 'message: goodbye');
-
- runTask(() => this.rerender());
-
- assert.equal(this.$().text(), 'message: goodbye');
- }
-
['@test GH#13982 contextual component ref is stable even when bound params change'](assert) {
let instance, previousInstance;
let initCount = 0;
@@ -1243,93 +1104,6 @@ moduleFor(
assert.equal(this.$().text(), 'my-comp: open');
}
- ['@test GH#14508 rest positional params are received when passed as named parameter']() {
- this.owner.register(
- 'component:my-link',
- setComponentTemplate(
- precompileTemplate('{{#each this.params as |p|}}{{p}}{{/each}}'),
- class extends EmberComponent {
- positionalParams = 'params';
- }
- )
- );
-
- this.render('{{component (component "my-link") params=this.allParams}}', {
- allParams: emberA(['a', 'b']),
- });
-
- this.assertText('ab');
-
- runTask(() => this.rerender());
-
- this.assertText('ab');
-
- runTask(() => this.context.get('allParams').pushObject('c'));
-
- this.assertText('abc');
-
- runTask(() => this.context.get('allParams').popObject());
-
- this.assertText('ab');
-
- runTask(() => this.context.get('allParams').clear());
-
- this.assertText('');
-
- runTask(() => this.context.set('allParams', emberA(['1', '2'])));
-
- this.assertText('12');
-
- runTask(() => this.context.set('allParams', emberA(['a', 'b'])));
-
- this.assertText('ab');
- }
-
- ['@test GH#14508 rest positional params are received when passed as named parameter with dot notation']() {
- this.owner.register(
- 'component:my-link',
- setComponentTemplate(
- precompileTemplate('{{#each this.params as |p|}}{{p}}{{/each}}'),
- class extends EmberComponent {
- positionalParams = 'params';
- }
- )
- );
-
- this.render(
- '{{#let (hash link=(component "my-link")) as |c|}}{{c.link params=this.allParams}}{{/let}}',
- {
- allParams: emberA(['a', 'b']),
- }
- );
-
- this.assertText('ab');
-
- runTask(() => this.rerender());
-
- this.assertText('ab');
-
- runTask(() => this.context.get('allParams').pushObject('c'));
-
- this.assertText('abc');
-
- runTask(() => this.context.get('allParams').popObject());
-
- this.assertText('ab');
-
- runTask(() => this.context.get('allParams').clear());
-
- this.assertText('');
-
- runTask(() => this.context.set('allParams', emberA(['1', '2'])));
-
- this.assertText('12');
-
- runTask(() => this.context.set('allParams', emberA(['a', 'b'])));
-
- this.assertText('ab');
- }
-
['@test it can invoke input component']() {
this.render('{{component (component "input" type="text" value=this.value)}}', {
value: 'foo',
@@ -1381,6 +1155,45 @@ moduleFor(
this.assert.strictEqual('foo', this.firstChild.value);
}
+ ['@test GH#13494 component without content with an argument binding'](assert) {
+ this.owner.register(
+ 'component:outer-component',
+ setComponentTemplate(
+ precompileTemplate(
+ 'message: {{this.message}}{{inner-component message=this.message}} '
+ ),
+ class extends Component {
+ @tracked message = 'hello';
+ @action
+ change() {
+ this.message = 'goodbye';
+ }
+ }
+ )
+ );
+
+ this.owner.register(
+ 'component:inner-component',
+ setComponentTemplate(precompileTemplate(''), class extends Component {})
+ );
+
+ this.render(`{{outer-component}}`);
+
+ assert.equal(this.$().text(), 'message: hello');
+
+ runTask(() => this.rerender());
+
+ assert.equal(this.$().text(), 'message: hello');
+
+ runTask(() => this.$('button').click());
+
+ assert.equal(this.$().text(), 'message: goodbye');
+
+ runTask(() => this.rerender());
+
+ assert.equal(this.$().text(), 'message: goodbye');
+ }
+
['@test GH#17121 local variable should win over helper (without arguments)']() {
this.registerHelper('foo', () => 'foo helper');
@@ -1598,32 +1411,18 @@ class MutableParamTestGenerator {
this.cases = cases;
}
- generate({ title, positional, setup }) {
+ generate({ title, setup }) {
return {
[`@test parameters in a contextual component are mutable when value is a ${title}`](assert) {
- if (positional) {
- this.owner.register(
- 'component:change-button',
- setComponentTemplate(
- precompileTemplate(
- 'Change to 10 '
- ),
- class extends EmberComponent {
- static positionalParams = ['val'];
- }
- )
- );
- } else {
- this.owner.register(
- 'component:change-button',
- setComponentTemplate(
- precompileTemplate(
- 'Change to 10 '
- ),
- class extends Component {}
- )
- );
- }
+ this.owner.register(
+ 'component:change-button',
+ setComponentTemplate(
+ precompileTemplate(
+ 'Change to 10 '
+ ),
+ class extends Component {}
+ )
+ );
setup.call(this, assert);
@@ -1648,32 +1447,6 @@ class MutableParamTestGenerator {
applyMixins(
ContextualComponentMutableParamsTest,
new MutableParamTestGenerator([
- {
- title: 'param',
- positional: true,
- setup() {
- this.render('{{component (component "change-button" this.model.val2)}}');
- },
- },
-
- {
- title: 'nested param',
- positional: true,
- setup() {
- this.owner.register(
- 'component:my-comp',
- setComponentTemplate(
- precompileTemplate('{{component this.components.comp}}'),
- class extends EmberComponent {
- static positionalParams = ['components'];
- }
- )
- );
-
- this.render('{{my-comp (hash comp=(component "change-button" this.model.val2))}}');
- },
- },
-
{
title: 'hash value',
setup() {
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 aa8574fba41..fb5dd168b0b 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
@@ -2,27 +2,19 @@ import {
moduleFor,
RenderingTestCase,
strip,
- classes,
equalTokens,
equalsElement,
runTask,
- runLoopSettled,
- expectDeprecation,
- testUnless,
+ defineSimpleModifier,
} from 'internal-test-helpers';
-import { DEPRECATIONS } from '../../../../deprecations';
-import { action } from '@ember/object';
-import { run } from '@ember/runloop';
-import { DEBUG } from '@glimmer/env';
import { tracked } from '@ember/-internals/metal';
import { alias } from '@ember/object/computed';
-import { on } from '@ember/object/evented';
import Service, { service } from '@ember/service';
-import EmberObject, { set, get, computed, observer } from '@ember/object';
-import { A as emberA } from '@ember/array';
+import EmberObject, { set, computed } from '@ember/object';
+import { trackedArray } from '@ember/reactive/collections';
-import { Component, htmlSafe } from '../../utils/helpers';
+import { htmlSafe } from '../../utils/helpers';
import PositionalComponent from '../../utils/positional-component';
import GlimmerComponent from '@glimmer/component';
import { template } from '@ember/template-compiler/runtime';
@@ -49,3434 +41,1400 @@ moduleFor(
this.assertHTML('hello');
}
- ['@test it can have a custom id and it is not bound']() {
+ ['@test it renders passed named arguments']() {
this.owner.register(
'component:foo-bar',
- setComponentTemplate(
- precompileTemplate('{{this.id}} {{this.elementId}}'),
- class extends Component {}
- )
+ setComponentTemplate(precompileTemplate('{{@foo}}'), templateOnly())
);
- this.render('{{foo-bar id=this.customId}}', {
- customId: 'bizz',
+ this.render('{{foo-bar foo=this.model.bar}}', {
+ model: {
+ bar: 'Hola',
+ },
});
- this.assertComponentElement(this.firstChild, {
- tagName: 'div',
- attrs: { id: 'bizz' },
- content: 'bizz bizz',
- });
+ this.assertText('Hola');
runTask(() => this.rerender());
- this.assertComponentElement(this.firstChild, {
- tagName: 'div',
- attrs: { id: 'bizz' },
- content: 'bizz bizz',
- });
+ this.assertText('Hola');
- runTask(() => set(this.context, 'customId', 'bar'));
+ runTask(() => this.context.set('model.bar', 'Hello'));
- this.assertComponentElement(this.firstChild, {
- tagName: 'div',
- attrs: { id: 'bizz' },
- content: 'bar bizz',
- });
+ this.assertText('Hello');
- runTask(() => set(this.context, 'customId', 'bizz'));
+ runTask(() => this.context.set('model', { bar: 'Hola' }));
- this.assertComponentElement(this.firstChild, {
- tagName: 'div',
- attrs: { id: 'bizz' },
- content: 'bizz bizz',
- });
+ this.assertText('Hola');
}
- ['@test elementId cannot change'](assert) {
- let component;
- let FooBarComponent = class extends Component {
- elementId = 'blahzorz';
- init() {
- super.init(...arguments);
- component = this;
- }
- };
-
+ ['@test it can render a basic component with a block']() {
this.owner.register(
'component:foo-bar',
- setComponentTemplate(precompileTemplate('{{this.elementId}}'), FooBarComponent)
+ setComponentTemplate(precompileTemplate('{{yield}} - In component'), templateOnly())
);
- this.render('{{foo-bar}}');
-
- this.assertComponentElement(this.firstChild, {
- tagName: 'div',
- attrs: { id: 'blahzorz' },
- content: 'blahzorz',
- });
+ this.render('{{#foo-bar}}hello{{/foo-bar}}');
- if (DEBUG) {
- let willThrow = () => run(null, set, component, 'elementId', 'herpyderpy');
+ this.assertHTML('hello - In component');
- assert.throws(willThrow, /Changing a view's elementId after creation is not allowed/);
+ runTask(() => this.rerender());
- this.assertComponentElement(this.firstChild, {
- tagName: 'div',
- attrs: { id: 'blahzorz' },
- content: 'blahzorz',
- });
- }
+ this.assertHTML('hello - In component');
}
- ['@test elementId is stable when other values change']() {
- let changingArg = 'arbitrary value';
- let parentInstance;
- this.owner.register(
- 'component:foo-bar',
- setComponentTemplate(
- precompileTemplate('{{quux-baz elementId="stable-id" changingArg=this.changingArg}}'),
- class extends Component {
- init() {
- super.init(...arguments);
- parentInstance = this;
- }
- changingArg = changingArg;
- }
- )
- );
-
- this.owner.register(
- 'component:quux-baz',
- setComponentTemplate(precompileTemplate('{{this.changingArg}}'), class extends Component {})
- );
-
- this.render('{{foo-bar}}');
- this.assertComponentElement(this.firstChild.firstChild, {
- attrs: { id: 'stable-id' },
- content: 'arbitrary value',
- });
+ ['@test it renders the layout with the component instance as the context']() {
+ let instance;
- changingArg = 'a different value';
- runTask(() => set(parentInstance, 'changingArg', changingArg));
- this.assertComponentElement(this.firstChild.firstChild, {
- attrs: { id: 'stable-id' },
- content: changingArg,
- });
- }
+ let FooBarComponent = class extends GlimmerComponent {
+ @tracked message = 'hello';
- ['@test can specify template with `layoutName` property']() {
- let FooBarComponent = class extends Component {
- elementId = 'blahzorz';
- layoutName = 'fizz-bar';
- init() {
- super.init(...arguments);
- this.local = 'hey';
+ constructor(owner, args) {
+ super(owner, args);
+ instance = this;
}
};
- this.registerTemplate('fizz-bar', `FIZZ BAR {{this.local}}`);
-
- this.owner.register('component:foo-bar', FooBarComponent);
+ this.owner.register(
+ 'component:foo-bar',
+ setComponentTemplate(precompileTemplate('{{this.message}}'), FooBarComponent)
+ );
this.render('{{foo-bar}}');
- this.assertText('FIZZ BAR hey');
- }
+ this.assertHTML('hello');
- ['@test layout supports computed property']() {
- let FooBarComponent = class extends Component {
- elementId = 'blahzorz';
- @computed
- get layout() {
- return precompileTemplate('so much layout wat {{this.lulz}}');
- }
- init() {
- super.init(...arguments);
- this.lulz = 'heyo';
- }
- };
+ runTask(() => this.rerender());
- this.owner.register('component:foo-bar', FooBarComponent);
+ this.assertHTML('hello');
- this.render('{{foo-bar}}');
+ runTask(() => set(instance, 'message', 'goodbye'));
- this.assertText('so much layout wat heyo');
- }
+ this.assertHTML('goodbye');
- ['@test passing undefined elementId results in a default elementId'](assert) {
- let FooBarComponent = class extends Component {
- tagName = 'h1';
- };
+ runTask(() => set(instance, 'message', 'hello'));
+
+ this.assertHTML('hello');
+ }
+ ['@test it preserves the outer context when yielding']() {
this.owner.register(
'component:foo-bar',
- setComponentTemplate(precompileTemplate('something'), FooBarComponent)
+ setComponentTemplate(precompileTemplate('{{yield}}'), templateOnly())
);
- this.render('{{foo-bar id=this.somethingUndefined}}');
+ this.render('{{#foo-bar}}{{this.message}}{{/foo-bar}}', { message: 'hello' });
- let foundId = this.$('h1').attr('id');
- assert.ok(
- /^ember/.test(foundId),
- 'Has a reasonable id attribute (found id=' + foundId + ').'
- );
+ this.assertHTML('hello');
runTask(() => this.rerender());
- let newFoundId = this.$('h1').attr('id');
- assert.ok(
- /^ember/.test(newFoundId),
- 'Has a reasonable id attribute (found id=' + newFoundId + ').'
- );
-
- assert.equal(foundId, newFoundId);
- }
+ this.assertHTML('hello');
- ['@test id is an alias for elementId'](assert) {
- let FooBarComponent = class extends Component {
- tagName = 'h1';
- };
+ runTask(() => set(this.context, 'message', 'goodbye'));
- this.owner.register(
- 'component:foo-bar',
- setComponentTemplate(precompileTemplate('something'), FooBarComponent)
- );
+ this.assertHTML('goodbye');
- this.render('{{foo-bar id="custom-id"}}');
+ runTask(() => set(this.context, 'message', 'hello'));
- let foundId = this.$('h1').attr('id');
- assert.equal(foundId, 'custom-id');
+ this.assertHTML('hello');
+ }
- runTask(() => this.rerender());
+ ['@test it can yield a block param named for reserved words [GH#14096]']() {
+ let instance;
- let newFoundId = this.$('h1').attr('id');
- assert.equal(newFoundId, 'custom-id');
+ let FooBarComponent = class extends GlimmerComponent {
+ constructor(owner, args) {
+ super(owner, args);
+ instance = this;
+ }
- assert.equal(foundId, newFoundId);
- }
+ @tracked name = 'foo-bar';
+ };
- ['@test cannot pass both id and elementId at the same time']() {
this.owner.register(
'component:foo-bar',
- setComponentTemplate(precompileTemplate(''), class extends Component {})
+ setComponentTemplate(precompileTemplate('{{yield this}}'), FooBarComponent)
);
- expectAssertion(() => {
- this.render('{{foo-bar id="zomg" elementId="lol"}}');
- }, /You cannot invoke a component with both 'id' and 'elementId' at the same time./);
- }
+ this.render('{{#foo-bar as |component|}}{{component.name}}{{/foo-bar}}');
- ['@test it can have a custom tagName']() {
- let FooBarComponent = class extends Component {
- tagName = 'foo-bar';
- };
+ this.assertHTML('foo-bar');
- this.owner.register(
- 'component:foo-bar',
- setComponentTemplate(precompileTemplate('hello'), FooBarComponent)
- );
+ this.assertStableRerender();
- this.render('{{foo-bar}}');
+ runTask(() => set(instance, 'name', 'derp-qux'));
- this.assertComponentElement(this.firstChild, {
- tagName: 'foo-bar',
- content: 'hello',
- });
+ this.assertHTML('derp-qux');
- runTask(() => this.rerender());
+ runTask(() => set(instance, 'name', 'foo-bar'));
- this.assertComponentElement(this.firstChild, {
- tagName: 'foo-bar',
- content: 'hello',
- });
+ this.assertHTML('foo-bar');
}
- ['@test it can have a custom tagName set in the constructor']() {
- let FooBarComponent = class extends Component {
- init() {
- super.init(...arguments);
- this.tagName = 'foo-bar';
+ ['@test it can yield internal and external properties positionally']() {
+ let instance;
+
+ let FooBarComponent = class extends GlimmerComponent {
+ constructor(owner, args) {
+ super(owner, args);
+ instance = this;
}
+
+ @tracked greeting = 'hello';
};
this.owner.register(
'component:foo-bar',
- setComponentTemplate(precompileTemplate('hello'), FooBarComponent)
+ setComponentTemplate(
+ precompileTemplate('{{yield this.greeting @greetee.firstName}}'),
+ FooBarComponent
+ )
);
- this.render('{{foo-bar}}');
+ this.render(
+ '{{#foo-bar greetee=this.person as |greeting name|}}{{name}} {{this.person.lastName}}, {{greeting}}{{/foo-bar}}',
+ {
+ person: {
+ firstName: 'Joel',
+ lastName: 'Kang',
+ },
+ }
+ );
- this.assertComponentElement(this.firstChild, {
- tagName: 'foo-bar',
- content: 'hello',
- });
+ this.assertHTML('Joel Kang, hello');
runTask(() => this.rerender());
- this.assertComponentElement(this.firstChild, {
- tagName: 'foo-bar',
- content: 'hello',
- });
- }
+ this.assertHTML('Joel Kang, hello');
- ['@test it can have a custom tagName from the invocation']() {
- this.owner.register(
- 'component:foo-bar',
- setComponentTemplate(precompileTemplate('hello'), class extends Component {})
+ runTask(() =>
+ set(this.context, 'person', {
+ firstName: 'Dora',
+ lastName: 'the Explorer',
+ })
);
- this.render('{{foo-bar tagName="foo-bar"}}');
+ this.assertHTML('Dora the Explorer, hello');
- this.assertComponentElement(this.firstChild, {
- tagName: 'foo-bar',
- content: 'hello',
- });
+ runTask(() => set(instance, 'greeting', 'hola'));
- runTask(() => this.rerender());
+ this.assertHTML('Dora the Explorer, hola');
- this.assertComponentElement(this.firstChild, {
- tagName: 'foo-bar',
- content: 'hello',
+ runTask(() => {
+ set(instance, 'greeting', 'hello');
+ set(this.context, 'person', {
+ firstName: 'Joel',
+ lastName: 'Kang',
+ });
});
- }
-
- ['@test tagName can not be a computed property']() {
- let FooBarComponent = class extends Component {
- @computed
- get tagName() {
- return 'foo-bar';
- }
- };
-
- this.owner.register(
- 'component:foo-bar',
- setComponentTemplate(precompileTemplate('hello'), FooBarComponent)
- );
- expectAssertion(() => {
- this.render('{{foo-bar}}');
- }, /You cannot use a computed property for the component's `tagName` \(<.+?>\)\./);
+ this.assertHTML('Joel Kang, hello');
}
- ['@test class is applied before didInsertElement'](assert) {
- let componentClass;
- let FooBarComponent = class extends Component {
- didInsertElement() {
- componentClass = this.element.className;
+ ['@test #11519 - block param infinite loop']() {
+ let instance;
+ let FooBarComponent = class extends GlimmerComponent {
+ constructor(owner, args) {
+ super(owner, args);
+ instance = this;
}
- };
-
- this.owner.register(
- 'component:foo-bar',
- setComponentTemplate(precompileTemplate('hello'), FooBarComponent)
- );
-
- this.render('{{foo-bar class="foo-bar"}}');
-
- assert.equal(componentClass, 'foo-bar ember-view');
- }
- ['@test it can have custom classNames']() {
- let FooBarComponent = class extends Component {
- classNames = ['foo', 'bar'];
+ @tracked danger = 0;
};
this.owner.register(
'component:foo-bar',
- setComponentTemplate(precompileTemplate('hello'), FooBarComponent)
+ setComponentTemplate(
+ precompileTemplate('{{this.danger}}{{yield this.danger}}'),
+ FooBarComponent
+ )
);
- this.render('{{foo-bar}}');
+ // On initial render, create streams. The bug will not have manifested yet, but at this point
+ // we have created streams that create a circular invalidation.
+ this.render(`{{#foo-bar as |dangerBlockParam|}}{{/foo-bar}}`);
- this.assertComponentElement(this.firstChild, {
- tagName: 'div',
- attrs: { class: classes('ember-view foo bar') },
- content: 'hello',
- });
+ this.assertText('0');
+ // Trigger a non-revalidating re-render. The yielded block will not be dirtied
+ // nor will block param streams, and thus no infinite loop will occur.
runTask(() => this.rerender());
- this.assertComponentElement(this.firstChild, {
- tagName: 'div',
- attrs: { class: classes('ember-view foo bar') },
- content: 'hello',
- });
- }
-
- ['@test should not apply falsy class name']() {
- this.owner.register(
- 'component:foo-bar',
- setComponentTemplate(precompileTemplate('hello'), class extends Component {})
- );
+ this.assertText('0');
- this.render('{{foo-bar class=this.somethingFalsy}}', {
- somethingFalsy: false,
- });
+ // Trigger a revalidation, which will cause an infinite loop without the fix
+ // in place. Note that we do not see the infinite loop is in testing mode,
+ // because a deprecation warning about re-renders is issued, which Ember
+ // treats as an exception.
+ runTask(() => set(instance, 'danger', 1));
- this.assertComponentElement(this.firstChild, {
- tagName: 'div',
- attrs: { class: 'ember-view' },
- content: 'hello',
- });
+ this.assertText('1');
- runTask(() => this.rerender());
+ runTask(() => set(instance, 'danger', 0));
- this.assertComponentElement(this.firstChild, {
- tagName: 'div',
- attrs: { class: 'ember-view' },
- content: 'hello',
- });
+ this.assertText('0');
}
- ['@test should update class using inline if, initially false, no alternate']() {
- this.owner.register(
- 'component:foo-bar',
- setComponentTemplate(precompileTemplate('hello'), class extends Component {})
- );
-
- this.render('{{foo-bar class=(if this.predicate "thing") }}', {
- predicate: false,
- });
-
- this.assertComponentElement(this.firstChild, {
- tagName: 'div',
- attrs: { class: 'ember-view' },
- content: 'hello',
- });
-
- runTask(() => set(this.context, 'predicate', true));
- runTask(() => this.rerender());
-
- this.assertComponentElement(this.firstChild, {
- tagName: 'div',
- attrs: { class: classes('ember-view thing') },
- content: 'hello',
- });
-
- runTask(() => set(this.context, 'predicate', false));
- runTask(() => this.rerender());
-
- this.assertComponentElement(this.firstChild, {
- tagName: 'div',
- attrs: { class: 'ember-view' },
- content: 'hello',
- });
- }
+ ['@test the component and its child components are destroyed'](assert) {
+ let destroyed = { 1: 0, 2: 0, 3: 0, 4: 0, 5: 0, 6: 0, 7: 0, 8: 0 };
- ['@test should update class using inline if, initially true, no alternate']() {
this.owner.register(
'component:foo-bar',
- setComponentTemplate(precompileTemplate('hello'), class extends Component {})
+ setComponentTemplate(
+ precompileTemplate('{{@id}} {{yield}}'),
+ class extends GlimmerComponent {
+ willDestroy() {
+ super.willDestroy();
+ destroyed[this.args.id]++;
+ }
+ }
+ )
);
- this.render('{{foo-bar class=(if this.predicate "thing") }}', {
- predicate: true,
- });
-
- this.assertComponentElement(this.firstChild, {
- tagName: 'div',
- attrs: { class: classes('ember-view thing') },
- content: 'hello',
- });
-
- runTask(() => set(this.context, 'predicate', false));
- runTask(() => this.rerender());
+ this.render(
+ strip`
+ {{#if this.cond1}}
+ {{#foo-bar id=1}}
+ {{#if this.cond2}}
+ {{#foo-bar id=2}}{{/foo-bar}}
+ {{#if this.cond3}}
+ {{#foo-bar id=3}}
+ {{#if this.cond4}}
+ {{#foo-bar id=4}}
+ {{#if this.cond5}}
+ {{#foo-bar id=5}}{{/foo-bar}}
+ {{#foo-bar id=6}}{{/foo-bar}}
+ {{#foo-bar id=7}}{{/foo-bar}}
+ {{/if}}
+ {{#foo-bar id=8}}{{/foo-bar}}
+ {{/foo-bar}}
+ {{/if}}
+ {{/foo-bar}}
+ {{/if}}
+ {{/if}}
+ {{/foo-bar}}
+ {{/if}}`,
+ {
+ cond1: true,
+ cond2: true,
+ cond3: true,
+ cond4: true,
+ cond5: true,
+ }
+ );
- this.assertComponentElement(this.firstChild, {
- tagName: 'div',
- attrs: { class: 'ember-view' },
- content: 'hello',
- });
+ this.assertText('1 2 3 4 5 6 7 8 ');
- runTask(() => set(this.context, 'predicate', true));
runTask(() => this.rerender());
- this.assertComponentElement(this.firstChild, {
- tagName: 'div',
- attrs: { class: classes('ember-view thing') },
- content: 'hello',
+ assert.deepEqual(destroyed, {
+ 1: 0,
+ 2: 0,
+ 3: 0,
+ 4: 0,
+ 5: 0,
+ 6: 0,
+ 7: 0,
+ 8: 0,
});
- }
- ['@test class property on components can be dynamic']() {
- this.owner.register(
- 'component:foo-bar',
- setComponentTemplate(precompileTemplate('hello'), class extends Component {})
- );
+ runTask(() => set(this.context, 'cond5', false));
- this.render('{{foo-bar class=(if this.fooBar "foo-bar")}}', {
- fooBar: true,
- });
+ this.assertText('1 2 3 4 8 ');
- this.assertComponentElement(this.firstChild, {
- content: 'hello',
- attrs: { class: classes('ember-view foo-bar') },
+ assert.deepEqual(destroyed, {
+ 1: 0,
+ 2: 0,
+ 3: 0,
+ 4: 0,
+ 5: 1,
+ 6: 1,
+ 7: 1,
+ 8: 0,
});
- runTask(() => this.rerender());
-
- this.assertComponentElement(this.firstChild, {
- content: 'hello',
- attrs: { class: classes('ember-view foo-bar') },
+ runTask(() => {
+ set(this.context, 'cond3', false);
+ set(this.context, 'cond5', true);
+ set(this.context, 'cond4', false);
});
- runTask(() => set(this.context, 'fooBar', false));
-
- this.assertComponentElement(this.firstChild, {
- content: 'hello',
- attrs: { class: classes('ember-view') },
+ assert.deepEqual(destroyed, {
+ 1: 0,
+ 2: 0,
+ 3: 1,
+ 4: 1,
+ 5: 1,
+ 6: 1,
+ 7: 1,
+ 8: 1,
});
- runTask(() => set(this.context, 'fooBar', true));
+ runTask(() => {
+ set(this.context, 'cond2', false);
+ set(this.context, 'cond1', false);
+ });
- this.assertComponentElement(this.firstChild, {
- content: 'hello',
- attrs: { class: classes('ember-view foo-bar') },
+ assert.deepEqual(destroyed, {
+ 1: 1,
+ 2: 1,
+ 3: 1,
+ 4: 1,
+ 5: 1,
+ 6: 1,
+ 7: 1,
+ 8: 1,
});
}
- ['@test it can have custom classNames from constructor']() {
- let FooBarComponent = class extends Component {
- init() {
- super.init(...arguments);
- this.classNames = this.classNames.slice();
- this.classNames.push('foo', 'bar', `outside-${this.get('extraClass')}`);
+ ['@test should escape HTML in normal mustaches']() {
+ let component;
+ let FooBarComponent = class extends GlimmerComponent {
+ constructor(owner, args) {
+ super(owner, args);
+ component = this;
}
+
+ @tracked output = 'you need to be more bold ';
};
this.owner.register(
'component:foo-bar',
- setComponentTemplate(precompileTemplate('hello'), FooBarComponent)
+ setComponentTemplate(precompileTemplate('{{this.output}}'), FooBarComponent)
);
- this.render('{{foo-bar extraClass="baz"}}');
+ this.render('{{foo-bar}}');
- this.assertComponentElement(this.firstChild, {
- tagName: 'div',
- attrs: { class: classes('ember-view foo bar outside-baz') },
- content: 'hello',
- });
+ this.assertText('you need to be more bold ');
runTask(() => this.rerender());
- this.assertComponentElement(this.firstChild, {
- tagName: 'div',
- attrs: { class: classes('ember-view foo bar outside-baz') },
- content: 'hello',
- });
- }
-
- ['@test it can set custom classNames from the invocation']() {
- let FooBarComponent = class extends Component {
- classNames = ['foo'];
- };
-
- this.owner.register(
- 'component:foo-bar',
- setComponentTemplate(precompileTemplate('hello'), FooBarComponent)
- );
-
- this.render(strip`
- {{foo-bar class="bar baz"}}
- {{foo-bar classNames="bar baz"}}
- {{foo-bar}}
- `);
+ this.assertText('you need to be more bold ');
- this.assertComponentElement(this.nthChild(0), {
- tagName: 'div',
- attrs: { class: classes('ember-view foo bar baz') },
- content: 'hello',
- });
- this.assertComponentElement(this.nthChild(1), {
- tagName: 'div',
- attrs: { class: classes('ember-view foo bar baz') },
- content: 'hello',
- });
- this.assertComponentElement(this.nthChild(2), {
- tagName: 'div',
- attrs: { class: classes('ember-view foo') },
- content: 'hello',
- });
+ runTask(() => set(component, 'output', 'you are so super '));
- runTask(() => this.rerender());
+ this.assertText('you are so super ');
- this.assertComponentElement(this.nthChild(0), {
- tagName: 'div',
- attrs: { class: classes('ember-view foo bar baz') },
- content: 'hello',
- });
- this.assertComponentElement(this.nthChild(1), {
- tagName: 'div',
- attrs: { class: classes('ember-view foo bar baz') },
- content: 'hello',
- });
- this.assertComponentElement(this.nthChild(2), {
- tagName: 'div',
- attrs: { class: classes('ember-view foo') },
- content: 'hello',
- });
+ runTask(() => set(component, 'output', 'you need to be more bold '));
}
- ['@test it has an element']() {
- let instance;
-
- let FooBarComponent = class extends Component {
- init() {
- super.init(...arguments);
- instance = this;
+ ['@test should not escape HTML in triple mustaches']() {
+ let expectedHtmlBold = 'you need to be more bold ';
+ let expectedHtmlItalic = 'you are so super ';
+ let component;
+ let FooBarComponent = class extends GlimmerComponent {
+ constructor(owner, args) {
+ super(owner, args);
+ component = this;
}
+
+ @tracked output = expectedHtmlBold;
};
this.owner.register(
'component:foo-bar',
- setComponentTemplate(precompileTemplate('hello'), FooBarComponent)
+ setComponentTemplate(precompileTemplate('{{{this.output}}}'), FooBarComponent)
);
this.render('{{foo-bar}}');
- let element1 = instance.element;
-
- this.assertComponentElement(element1, { content: 'hello' });
+ equalTokens(this.element, expectedHtmlBold);
runTask(() => this.rerender());
- let element2 = instance.element;
+ equalTokens(this.element, expectedHtmlBold);
+
+ runTask(() => set(component, 'output', expectedHtmlItalic));
+
+ equalTokens(this.element, expectedHtmlItalic);
- this.assertComponentElement(element2, { content: 'hello' });
+ runTask(() => set(component, 'output', expectedHtmlBold));
- this.assertSameNode(element2, element1);
+ equalTokens(this.element, expectedHtmlBold);
}
- ['@test an empty component does not have childNodes'](assert) {
- let fooBarInstance;
- let FooBarComponent = class extends Component {
- tagName = 'input';
- init() {
- super.init(...arguments);
- fooBarInstance = this;
+ ['@test should not escape HTML if string is a htmlSafe']() {
+ let expectedHtmlBold = 'you need to be more bold ';
+ let expectedHtmlItalic = 'you are so super ';
+ let component;
+ let FooBarComponent = class extends GlimmerComponent {
+ constructor(owner, args) {
+ super(owner, args);
+ component = this;
}
+
+ @tracked output = htmlSafe(expectedHtmlBold);
};
this.owner.register(
'component:foo-bar',
- setComponentTemplate(precompileTemplate(''), FooBarComponent)
+ setComponentTemplate(precompileTemplate('{{this.output}}'), FooBarComponent)
);
this.render('{{foo-bar}}');
- this.assertComponentElement(this.firstChild, { tagName: 'input' });
-
- assert.strictEqual(fooBarInstance.element.childNodes.length, 0);
+ equalTokens(this.element, expectedHtmlBold);
runTask(() => this.rerender());
- this.assertComponentElement(this.firstChild, { tagName: 'input' });
+ equalTokens(this.element, expectedHtmlBold);
- assert.strictEqual(fooBarInstance.element.childNodes.length, 0);
- }
+ runTask(() => set(component, 'output', htmlSafe(expectedHtmlItalic)));
+
+ equalTokens(this.element, expectedHtmlItalic);
- ['@test it has the right parentView and childViews'](assert) {
- let fooBarInstance, fooBarBazInstance;
+ runTask(() => set(component, 'output', htmlSafe(expectedHtmlBold)));
- let FooBarComponent = class extends Component {
- init() {
- super.init(...arguments);
- fooBarInstance = this;
- }
- };
+ equalTokens(this.element, expectedHtmlBold);
+ }
+
+ ['@test can use isStream property without conflict (#13271)']() {
+ let component;
+ let FooBarComponent = class extends GlimmerComponent {
+ @tracked isStream = true;
- let FooBarBazComponent = class extends Component {
- init() {
- super.init(...arguments);
- fooBarBazInstance = this;
+ constructor(owner, args) {
+ super(owner, args);
+ component = this;
}
};
this.owner.register(
'component:foo-bar',
- setComponentTemplate(precompileTemplate('foo-bar {{foo-bar-baz}}'), FooBarComponent)
- );
- this.owner.register(
- 'component:foo-bar-baz',
- setComponentTemplate(precompileTemplate('foo-bar-baz'), FooBarBazComponent)
+ template(
+ strip`
+ {{#if this.isStream}}
+ true
+ {{else}}
+ false
+ {{/if}}
+ `,
+ { component: FooBarComponent, strictMode: false }
+ )
);
this.render('{{foo-bar}}');
- this.assertText('foo-bar foo-bar-baz');
- assert.equal(fooBarInstance.parentView, this.component);
- assert.equal(fooBarBazInstance.parentView, fooBarInstance);
-
- assert.deepEqual(this.component.childViews, [fooBarInstance]);
- assert.deepEqual(fooBarInstance.childViews, [fooBarBazInstance]);
+ this.assertHTML('true');
runTask(() => this.rerender());
- this.assertText('foo-bar foo-bar-baz');
- assert.equal(fooBarInstance.parentView, this.component);
- assert.equal(fooBarBazInstance.parentView, fooBarInstance);
+ this.assertHTML('true');
- assert.deepEqual(this.component.childViews, [fooBarInstance]);
- assert.deepEqual(fooBarInstance.childViews, [fooBarBazInstance]);
- }
+ runTask(() => set(component, 'isStream', false));
- ['@test it renders passed named arguments']() {
+ this.assertHTML('false');
+
+ runTask(() => set(component, 'isStream', true));
+
+ this.assertHTML('true');
+ }
+ ['@test lookup of component takes priority over property']() {
this.owner.register(
- 'component:foo-bar',
- setComponentTemplate(precompileTemplate('{{@foo}}'), templateOnly())
+ 'component:some-component',
+ setComponentTemplate(precompileTemplate('some-component'), templateOnly())
);
- this.render('{{foo-bar foo=this.model.bar}}', {
- model: {
- bar: 'Hola',
- },
+ this.render('{{this.some-prop}} {{some-component}}', {
+ 'some-component': 'not-some-component',
+ 'some-prop': 'some-prop',
});
- this.assertText('Hola');
+ this.assertText('some-prop some-component');
runTask(() => this.rerender());
- this.assertText('Hola');
-
- runTask(() => this.context.set('model.bar', 'Hello'));
-
- this.assertText('Hello');
-
- runTask(() => this.context.set('model', { bar: 'Hola' }));
-
- this.assertText('Hola');
+ this.assertText('some-prop some-component');
}
- ['@test it reflects named arguments as properties']() {
+ ['@test component without dash is looked up']() {
this.owner.register(
- 'component:foo-bar',
- setComponentTemplate(precompileTemplate('{{this.foo}}'), class extends Component {})
+ 'component:somecomponent',
+ setComponentTemplate(precompileTemplate('somecomponent'), templateOnly())
);
- this.render('{{foo-bar foo=this.model.bar}}', {
- model: {
- bar: 'Hola',
- },
+ this.render('{{somecomponent}}', {
+ somecomponent: 'notsomecomponent',
});
- this.assertText('Hola');
-
- runTask(() => this.rerender());
+ this.assertText('somecomponent');
- this.assertText('Hola');
+ this.assertStableRerender();
- runTask(() => this.context.set('model.bar', 'Hello'));
+ runTask(() => this.context.set('somecomponent', 'not not notsomecomponent'));
- this.assertText('Hello');
+ this.assertText('somecomponent');
- runTask(() => this.context.set('model', { bar: 'Hola' }));
+ runTask(() => this.context.set('somecomponent', 'notsomecomponent'));
- this.assertText('Hola');
+ this.assertText('somecomponent');
}
- ['@test it can render a basic component with a block']() {
- this.owner.register(
- 'component:foo-bar',
- setComponentTemplate(precompileTemplate('{{yield}} - In component'), templateOnly())
- );
-
- this.render('{{#foo-bar}}hello{{/foo-bar}}');
-
- this.assertHTML('hello - In component');
-
- runTask(() => this.rerender());
-
- this.assertHTML('hello - In component');
- }
-
- ['@test it renders the layout with the component instance as the context']() {
- let instance;
-
- let FooBarComponent = class extends GlimmerComponent {
- @tracked message = 'hello';
-
- constructor(owner, args) {
- super(owner, args);
- instance = this;
- }
- };
-
- this.owner.register(
- 'component:foo-bar',
- setComponentTemplate(precompileTemplate('{{this.message}}'), FooBarComponent)
- );
-
- this.render('{{foo-bar}}');
-
- this.assertHTML('hello');
-
- runTask(() => this.rerender());
-
- this.assertHTML('hello');
-
- runTask(() => set(instance, 'message', 'goodbye'));
-
- this.assertHTML('goodbye');
-
- runTask(() => set(instance, 'message', 'hello'));
-
- this.assertHTML('hello');
- }
-
- ['@test it preserves the outer context when yielding']() {
- this.owner.register(
- 'component:foo-bar',
- setComponentTemplate(precompileTemplate('{{yield}}'), templateOnly())
- );
-
- this.render('{{#foo-bar}}{{this.message}}{{/foo-bar}}', { message: 'hello' });
-
- this.assertHTML('hello');
-
- runTask(() => this.rerender());
-
- this.assertHTML('hello');
-
- runTask(() => set(this.context, 'message', 'goodbye'));
-
- this.assertHTML('goodbye');
-
- runTask(() => set(this.context, 'message', 'hello'));
-
- this.assertHTML('hello');
- }
-
- ['@test it can yield a block param named for reserved words [GH#14096]']() {
- let instance;
-
- let FooBarComponent = class extends GlimmerComponent {
- constructor(owner, args) {
- super(owner, args);
- instance = this;
- }
-
- @tracked name = 'foo-bar';
- };
-
- this.owner.register(
- 'component:foo-bar',
- setComponentTemplate(precompileTemplate('{{yield this}}'), FooBarComponent)
- );
-
- this.render('{{#foo-bar as |component|}}{{component.name}}{{/foo-bar}}');
-
- this.assertHTML('foo-bar');
-
- this.assertStableRerender();
-
- runTask(() => set(instance, 'name', 'derp-qux'));
-
- this.assertHTML('derp-qux');
-
- runTask(() => set(instance, 'name', 'foo-bar'));
-
- this.assertHTML('foo-bar');
+ ['@test non-block with properties access via attrs is asserted against']() {
+ expectAssertion(() => {
+ this.owner.register(
+ 'component:non-block',
+ template('In layout - someProp: {{attrs.someProp}}', {
+ component: templateOnly(),
+ strictMode: false,
+ })
+ );
+ }, 'Using {{attrs}} to reference named arguments is not supported. {{attrs.someProp}} should be updated to {{@someProp}}. (L1:C24) ');
}
- ['@test it can yield internal and external properties positionally']() {
- let instance;
-
- let FooBarComponent = class extends GlimmerComponent {
- constructor(owner, args) {
- super(owner, args);
- instance = this;
- }
-
- @tracked greeting = 'hello';
- };
-
+ ['@test non-block with named argument']() {
this.owner.register(
- 'component:foo-bar',
+ 'component:non-block',
setComponentTemplate(
- precompileTemplate('{{yield this.greeting @greetee.firstName}}'),
- FooBarComponent
+ precompileTemplate('In layout - someProp: {{@someProp}}'),
+ templateOnly()
)
);
- this.render(
- '{{#foo-bar greetee=this.person as |greeting name|}}{{name}} {{this.person.lastName}}, {{greeting}}{{/foo-bar}}',
- {
- person: {
- firstName: 'Joel',
- lastName: 'Kang',
- },
- }
- );
-
- this.assertHTML('Joel Kang, hello');
-
- runTask(() => this.rerender());
-
- this.assertHTML('Joel Kang, hello');
-
- runTask(() =>
- set(this.context, 'person', {
- firstName: 'Dora',
- lastName: 'the Explorer',
- })
- );
-
- this.assertHTML('Dora the Explorer, hello');
-
- runTask(() => set(instance, 'greeting', 'hola'));
-
- this.assertHTML('Dora the Explorer, hola');
-
- runTask(() => {
- set(instance, 'greeting', 'hello');
- set(this.context, 'person', {
- firstName: 'Joel',
- lastName: 'Kang',
- });
+ this.render('{{non-block someProp=this.prop}}', {
+ prop: 'something here',
});
- this.assertHTML('Joel Kang, hello');
- }
-
- ['@test #11519 - block param infinite loop']() {
- let instance;
- let FooBarComponent = class extends GlimmerComponent {
- constructor(owner, args) {
- super(owner, args);
- instance = this;
- }
-
- @tracked danger = 0;
- };
-
- this.owner.register(
- 'component:foo-bar',
- setComponentTemplate(
- precompileTemplate('{{this.danger}}{{yield this.danger}}'),
- FooBarComponent
- )
- );
-
- // On initial render, create streams. The bug will not have manifested yet, but at this point
- // we have created streams that create a circular invalidation.
- this.render(`{{#foo-bar as |dangerBlockParam|}}{{/foo-bar}}`);
-
- this.assertText('0');
+ this.assertText('In layout - someProp: something here');
- // Trigger a non-revalidating re-render. The yielded block will not be dirtied
- // nor will block param streams, and thus no infinite loop will occur.
runTask(() => this.rerender());
- this.assertText('0');
-
- // Trigger a revalidation, which will cause an infinite loop without the fix
- // in place. Note that we do not see the infinite loop is in testing mode,
- // because a deprecation warning about re-renders is issued, which Ember
- // treats as an exception.
- runTask(() => set(instance, 'danger', 1));
-
- this.assertText('1');
-
- runTask(() => set(instance, 'danger', 0));
-
- this.assertText('0');
- }
-
- ['@test the component and its child components are destroyed'](assert) {
- let destroyed = { 1: 0, 2: 0, 3: 0, 4: 0, 5: 0, 6: 0, 7: 0, 8: 0 };
-
- this.owner.register(
- 'component:foo-bar',
- setComponentTemplate(
- precompileTemplate('{{@id}} {{yield}}'),
- class extends GlimmerComponent {
- willDestroy() {
- super.willDestroy();
- destroyed[this.args.id]++;
- }
- }
- )
- );
-
- this.render(
- strip`
- {{#if this.cond1}}
- {{#foo-bar id=1}}
- {{#if this.cond2}}
- {{#foo-bar id=2}}{{/foo-bar}}
- {{#if this.cond3}}
- {{#foo-bar id=3}}
- {{#if this.cond4}}
- {{#foo-bar id=4}}
- {{#if this.cond5}}
- {{#foo-bar id=5}}{{/foo-bar}}
- {{#foo-bar id=6}}{{/foo-bar}}
- {{#foo-bar id=7}}{{/foo-bar}}
- {{/if}}
- {{#foo-bar id=8}}{{/foo-bar}}
- {{/foo-bar}}
- {{/if}}
- {{/foo-bar}}
- {{/if}}
- {{/if}}
- {{/foo-bar}}
- {{/if}}`,
- {
- cond1: true,
- cond2: true,
- cond3: true,
- cond4: true,
- cond5: true,
- }
- );
+ this.assertText('In layout - someProp: something here');
- this.assertText('1 2 3 4 5 6 7 8 ');
-
- runTask(() => this.rerender());
-
- assert.deepEqual(destroyed, {
- 1: 0,
- 2: 0,
- 3: 0,
- 4: 0,
- 5: 0,
- 6: 0,
- 7: 0,
- 8: 0,
- });
-
- runTask(() => set(this.context, 'cond5', false));
-
- this.assertText('1 2 3 4 8 ');
-
- assert.deepEqual(destroyed, {
- 1: 0,
- 2: 0,
- 3: 0,
- 4: 0,
- 5: 1,
- 6: 1,
- 7: 1,
- 8: 0,
- });
-
- runTask(() => {
- set(this.context, 'cond3', false);
- set(this.context, 'cond5', true);
- set(this.context, 'cond4', false);
- });
-
- assert.deepEqual(destroyed, {
- 1: 0,
- 2: 0,
- 3: 1,
- 4: 1,
- 5: 1,
- 6: 1,
- 7: 1,
- 8: 1,
- });
-
- runTask(() => {
- set(this.context, 'cond2', false);
- set(this.context, 'cond1', false);
- });
-
- assert.deepEqual(destroyed, {
- 1: 1,
- 2: 1,
- 3: 1,
- 4: 1,
- 5: 1,
- 6: 1,
- 7: 1,
- 8: 1,
- });
- }
-
- ['@test should escape HTML in normal mustaches']() {
- let component;
- let FooBarComponent = class extends GlimmerComponent {
- constructor(owner, args) {
- super(owner, args);
- component = this;
- }
-
- @tracked output = 'you need to be more bold ';
- };
-
- this.owner.register(
- 'component:foo-bar',
- setComponentTemplate(precompileTemplate('{{this.output}}'), FooBarComponent)
- );
-
- this.render('{{foo-bar}}');
-
- this.assertText('you need to be more bold ');
-
- runTask(() => this.rerender());
-
- this.assertText('you need to be more bold ');
-
- runTask(() => set(component, 'output', 'you are so super '));
-
- this.assertText('you are so super ');
-
- runTask(() => set(component, 'output', 'you need to be more bold '));
- }
-
- ['@test should not escape HTML in triple mustaches']() {
- let expectedHtmlBold = 'you need to be more bold ';
- let expectedHtmlItalic = 'you are so super ';
- let component;
- let FooBarComponent = class extends GlimmerComponent {
- constructor(owner, args) {
- super(owner, args);
- component = this;
- }
-
- @tracked output = expectedHtmlBold;
- };
-
- this.owner.register(
- 'component:foo-bar',
- setComponentTemplate(precompileTemplate('{{{this.output}}}'), FooBarComponent)
- );
-
- this.render('{{foo-bar}}');
-
- equalTokens(this.element, expectedHtmlBold);
-
- runTask(() => this.rerender());
-
- equalTokens(this.element, expectedHtmlBold);
-
- runTask(() => set(component, 'output', expectedHtmlItalic));
-
- equalTokens(this.element, expectedHtmlItalic);
-
- runTask(() => set(component, 'output', expectedHtmlBold));
-
- equalTokens(this.element, expectedHtmlBold);
- }
-
- ['@test should not escape HTML if string is a htmlSafe']() {
- let expectedHtmlBold = 'you need to be more bold ';
- let expectedHtmlItalic = 'you are so super ';
- let component;
- let FooBarComponent = class extends GlimmerComponent {
- constructor(owner, args) {
- super(owner, args);
- component = this;
- }
-
- @tracked output = htmlSafe(expectedHtmlBold);
- };
-
- this.owner.register(
- 'component:foo-bar',
- setComponentTemplate(precompileTemplate('{{this.output}}'), FooBarComponent)
- );
-
- this.render('{{foo-bar}}');
-
- equalTokens(this.element, expectedHtmlBold);
-
- runTask(() => this.rerender());
-
- equalTokens(this.element, expectedHtmlBold);
-
- runTask(() => set(component, 'output', htmlSafe(expectedHtmlItalic)));
-
- equalTokens(this.element, expectedHtmlItalic);
-
- runTask(() => set(component, 'output', htmlSafe(expectedHtmlBold)));
-
- equalTokens(this.element, expectedHtmlBold);
- }
-
- ['@test late bound layouts return the same definition'](assert) {
- let templateIds = [];
-
- // This is testing the scenario where you import a template and
- // set it to the layout property:
- //
- // import Component from '@ember/component';
- // import layout from './template';
- //
- // export default Component.extend({
- // layout
- // });
- let hello = precompileTemplate('Hello');
- let bye = precompileTemplate('Bye');
-
- let FooBarComponent = class extends Component {
- init() {
- super.init(...arguments);
- this.layout = this.cond ? hello : bye;
- templateIds.push(this.layout.id);
- }
- };
-
- this.owner.register('component:foo-bar', FooBarComponent);
-
- this.render(
- '{{foo-bar cond=true}}{{foo-bar cond=false}}{{foo-bar cond=true}}{{foo-bar cond=false}}'
- );
-
- let [t1, t2, t3, t4] = templateIds;
- assert.equal(t1, t3);
- assert.equal(t2, t4);
- }
-
- ['@test can use isStream property without conflict (#13271)']() {
- let component;
- let FooBarComponent = class extends GlimmerComponent {
- @tracked isStream = true;
-
- constructor(owner, args) {
- super(owner, args);
- component = this;
- }
- };
-
- this.owner.register(
- 'component:foo-bar',
- template(
- strip`
- {{#if this.isStream}}
- true
- {{else}}
- false
- {{/if}}
- `,
- { component: FooBarComponent, strictMode: false }
- )
- );
-
- this.render('{{foo-bar}}');
-
- this.assertHTML('true');
-
- runTask(() => this.rerender());
-
- this.assertHTML('true');
-
- runTask(() => set(component, 'isStream', false));
-
- this.assertHTML('false');
-
- runTask(() => set(component, 'isStream', true));
-
- this.assertHTML('true');
- }
- ['@test lookup of component takes priority over property']() {
- this.owner.register(
- 'component:some-component',
- setComponentTemplate(precompileTemplate('some-component'), templateOnly())
- );
-
- this.render('{{this.some-prop}} {{some-component}}', {
- 'some-component': 'not-some-component',
- 'some-prop': 'some-prop',
- });
-
- this.assertText('some-prop some-component');
-
- runTask(() => this.rerender());
-
- this.assertText('some-prop some-component');
- }
-
- ['@test component without dash is looked up']() {
- this.owner.register(
- 'component:somecomponent',
- setComponentTemplate(precompileTemplate('somecomponent'), templateOnly())
- );
-
- this.render('{{somecomponent}}', {
- somecomponent: 'notsomecomponent',
- });
-
- this.assertText('somecomponent');
-
- this.assertStableRerender();
-
- runTask(() => this.context.set('somecomponent', 'not not notsomecomponent'));
-
- this.assertText('somecomponent');
-
- runTask(() => this.context.set('somecomponent', 'notsomecomponent'));
-
- this.assertText('somecomponent');
- }
-
- ['@test non-block with properties access via attrs is asserted against']() {
- expectAssertion(() => {
- this.owner.register(
- 'component:non-block',
- template('In layout - someProp: {{attrs.someProp}}', {
- component: templateOnly(),
- strictMode: false,
- })
- );
- }, 'Using {{attrs}} to reference named arguments is not supported. {{attrs.someProp}} should be updated to {{@someProp}}. (L1:C24) ');
- }
-
- // Perhaps change this test to `{{this.attrs.someProp.value}}` when removing the deprecation?
- ['@test non-block with properties on this.attrs']() {
- expectDeprecation(() => {
- this.owner.register(
- 'component:non-block',
- template('In layout - someProp: {{this.attrs.someProp}}', {
- component: class extends Component {},
- strictMode: false,
- })
- );
- }, /Using {{this.attrs}} to reference named arguments has been deprecated. {{this.attrs.someProp}} should be updated to {{@someProp}}./);
-
- this.render('{{non-block someProp=this.prop}}', {
- prop: 'something here',
- });
-
- this.assertText('In layout - someProp: something here');
-
- runTask(() => this.rerender());
-
- this.assertText('In layout - someProp: something here');
-
- runTask(() => this.context.set('prop', 'other thing there'));
-
- this.assertText('In layout - someProp: other thing there');
-
- runTask(() => this.context.set('prop', 'something here'));
-
- this.assertText('In layout - someProp: something here');
- }
-
- ['@test non-block with named argument']() {
- this.owner.register(
- 'component:non-block',
- setComponentTemplate(
- precompileTemplate('In layout - someProp: {{@someProp}}'),
- templateOnly()
- )
- );
-
- this.render('{{non-block someProp=this.prop}}', {
- prop: 'something here',
- });
-
- this.assertText('In layout - someProp: something here');
-
- runTask(() => this.rerender());
-
- this.assertText('In layout - someProp: something here');
-
- runTask(() => this.context.set('prop', 'other thing there'));
-
- this.assertText('In layout - someProp: other thing there');
-
- runTask(() => this.context.set('prop', 'something here'));
-
- this.assertText('In layout - someProp: something here');
- }
-
- ['@test non-block with properties overridden in init']() {
- let instance;
- this.owner.register(
- 'component:non-block',
- setComponentTemplate(
- precompileTemplate('In layout - someProp: {{this.someProp}}'),
- class extends Component {
- init() {
- super.init(...arguments);
- instance = this;
- this.someProp = 'value set in instance';
- }
- }
- )
- );
-
- this.render('{{non-block someProp=this.prop}}', {
- prop: 'something passed when invoked',
- });
-
- this.assertText('In layout - someProp: value set in instance');
-
- runTask(() => this.rerender());
-
- this.assertText('In layout - someProp: value set in instance');
-
- runTask(() => this.context.set('prop', 'updated something passed when invoked'));
-
- this.assertText('In layout - someProp: updated something passed when invoked');
-
- runTask(() => instance.set('someProp', 'update value set in instance'));
-
- this.assertText('In layout - someProp: update value set in instance');
-
- runTask(() => this.context.set('prop', 'something passed when invoked'));
- runTask(() => instance.set('someProp', 'value set in instance'));
-
- this.assertText('In layout - someProp: value set in instance');
- }
-
- ['@test rerendering component with attrs from parent'](assert) {
- let willUpdateCount = 0;
- let didReceiveAttrsCount = 0;
-
- function expectHooks({ willUpdate, didReceiveAttrs }, callback) {
- willUpdateCount = 0;
- didReceiveAttrsCount = 0;
-
- callback();
-
- if (willUpdate) {
- assert.strictEqual(willUpdateCount, 1, 'The willUpdate hook was fired');
- } else {
- assert.strictEqual(willUpdateCount, 0, 'The willUpdate hook was not fired');
- }
-
- if (didReceiveAttrs) {
- assert.strictEqual(didReceiveAttrsCount, 1, 'The didReceiveAttrs hook was fired');
- } else {
- assert.strictEqual(didReceiveAttrsCount, 0, 'The didReceiveAttrs hook was not fired');
- }
- }
-
- this.owner.register(
- 'component:non-block',
- setComponentTemplate(
- precompileTemplate('In layout - someProp: {{this.someProp}}'),
- class extends Component {
- didReceiveAttrs() {
- didReceiveAttrsCount++;
- }
-
- willUpdate() {
- willUpdateCount++;
- }
- }
- )
- );
-
- expectHooks({ willUpdate: false, didReceiveAttrs: true }, () => {
- this.render('{{non-block someProp=this.someProp}}', {
- someProp: 'wycats',
- });
- });
-
- this.assertText('In layout - someProp: wycats');
-
- // Note: Hooks are not fired in Glimmer for idempotent re-renders
- expectHooks({ willUpdate: false, didReceiveAttrs: false }, () => {
- runTask(() => this.rerender());
- });
-
- this.assertText('In layout - someProp: wycats');
-
- expectHooks({ willUpdate: true, didReceiveAttrs: true }, () => {
- runTask(() => this.context.set('someProp', 'tomdale'));
- });
-
- this.assertText('In layout - someProp: tomdale');
-
- // Note: Hooks are not fired in Glimmer for idempotent re-renders
- expectHooks({ willUpdate: false, didReceiveAttrs: false }, () => {
- runTask(() => this.rerender());
- });
-
- this.assertText('In layout - someProp: tomdale');
-
- expectHooks({ willUpdate: true, didReceiveAttrs: true }, () => {
- runTask(() => this.context.set('someProp', 'wycats'));
- });
-
- this.assertText('In layout - someProp: wycats');
- }
-
- ['@test setting a value for a computed property then later getting the value for that property works'](
- assert
- ) {
- let componentInstance = null;
-
- this.owner.register(
- 'component:non-block',
- setComponentTemplate(
- precompileTemplate('foobar '),
- class extends Component {
- counter = computed({
- set(key, value) {
- return value;
- },
- });
-
- init() {
- super.init(...arguments);
- componentInstance = this;
- }
-
- @action
- myClick() {
- let currentCounter = this.get('counter');
-
- assert.equal(currentCounter, 0, 'the current `counter` value is correct');
-
- let newCounter = currentCounter + 1;
- this.set('counter', newCounter);
-
- assert.equal(
- this.get('counter'),
- newCounter,
- "getting the newly set `counter` property works; it's equal to the value we just set and not `undefined`"
- );
- }
- }
- )
- );
-
- this.render(`{{non-block counter=this.counter}}`, {
- counter: 0,
- });
-
- runTask(() => this.$('button').click());
-
- assert.equal(
- componentInstance.get('counter'),
- 1,
- '`counter` incremented on click on the component and is not `undefined`'
- );
- }
-
- // Perhaps change this test to `{{this.attrs.foo.value}}` when removing the deprecation?
- ['@test this.attrs.foo === @foo === foo']() {
- expectDeprecation(() => {
- this.owner.register(
- 'component:foo-bar',
- template(
- strip`
- Args: {{this.attrs.value}} | {{@value}} | {{this.value}}
- {{#each this.attrs.items as |item|}}
- {{item}}
- {{/each}}
- {{#each @items as |item|}}
- {{item}}
- {{/each}}
- {{#each this.items as |item|}}
- {{item}}
- {{/each}}
- `,
- { component: class extends Component {}, strictMode: false }
- )
- );
- }, /Using {{this.attrs}} to reference named arguments has been deprecated. {{this.attrs..+}} should be updated to {{@.+}}./);
-
- this.render('{{foo-bar value=this.model.value items=this.model.items}}', {
- model: {
- value: 'wat',
- items: [1, 2, 3],
- },
- });
-
- this.assertStableRerender();
-
- runTask(() => {
- this.context.set('model.value', 'lul');
- this.context.set('model.items', [1]);
- });
-
- this.assertText(strip`Args: lul | lul | lul111`);
-
- runTask(() => this.context.set('model', { value: 'wat', items: [1, 2, 3] }));
-
- this.assertText('Args: wat | wat | wat123123123');
- }
-
- ['@test non-block with properties on self']() {
- this.owner.register(
- 'component:non-block',
- setComponentTemplate(
- precompileTemplate('In layout - someProp: {{this.someProp}}'),
- class extends Component {}
- )
- );
-
- this.render('{{non-block someProp=this.prop}}', {
- prop: 'something here',
- });
-
- this.assertText('In layout - someProp: something here');
-
- runTask(() => this.rerender());
-
- this.assertText('In layout - someProp: something here');
-
- runTask(() => this.context.set('prop', 'something else'));
-
- this.assertText('In layout - someProp: something else');
-
- runTask(() => this.context.set('prop', 'something here'));
-
- this.assertText('In layout - someProp: something here');
- }
-
- ['@test block with properties on self']() {
- this.owner.register(
- 'component:with-block',
- setComponentTemplate(
- precompileTemplate('In layout - someProp: {{this.someProp}} - {{yield}}'),
- class extends Component {}
- )
- );
-
- this.render(
- strip`
- {{#with-block someProp=this.prop}}
- In template
- {{/with-block}}`,
- {
- prop: 'something here',
- }
- );
-
- this.assertText('In layout - someProp: something here - In template');
-
- runTask(() => this.rerender());
-
- this.assertText('In layout - someProp: something here - In template');
-
- runTask(() => this.context.set('prop', 'something else'));
-
- this.assertText('In layout - someProp: something else - In template');
-
- runTask(() => this.context.set('prop', 'something here'));
-
- this.assertText('In layout - someProp: something here - In template');
- }
-
- ['@test block with properties on attrs is asserted against']() {
- expectAssertion(() => {
- this.owner.register(
- 'component:with-block',
- template('In layout - someProp: {{attrs.someProp}} - {{yield}}', {
- component: templateOnly(),
- strictMode: false,
- })
- );
- }, 'Using {{attrs}} to reference named arguments is not supported. {{attrs.someProp}} should be updated to {{@someProp}}. (L1:C24) ');
- }
-
- // Perhaps change this test to `{{this.attrs.someProp.value}}` when removing the deprecation?
- ['@test block with properties on this.attrs']() {
- expectDeprecation(() => {
- this.owner.register(
- 'component:with-block',
- template('In layout - someProp: {{this.attrs.someProp}} - {{yield}}', {
- component: class extends Component {},
- strictMode: false,
- })
- );
- }, /Using {{this.attrs}} to reference named arguments has been deprecated. {{this.attrs.someProp}} should be updated to {{@someProp}}./);
-
- this.render(
- strip`
- {{#with-block someProp=this.prop}}
- In template
- {{/with-block}}`,
- {
- prop: 'something here',
- }
- );
-
- this.assertText('In layout - someProp: something here - In template');
-
- runTask(() => this.rerender());
-
- this.assertText('In layout - someProp: something here - In template');
-
- runTask(() => this.context.set('prop', 'something else'));
-
- this.assertText('In layout - someProp: something else - In template');
-
- runTask(() => this.context.set('prop', 'something here'));
-
- this.assertText('In layout - someProp: something here - In template');
- }
-
- ['@test block with named argument']() {
- this.owner.register(
- 'component:with-block',
- setComponentTemplate(
- precompileTemplate('In layout - someProp: {{@someProp}} - {{yield}}'),
- templateOnly()
- )
- );
-
- this.render(
- strip`
- {{#with-block someProp=this.prop}}
- In template
- {{/with-block}}`,
- {
- prop: 'something here',
- }
- );
-
- this.assertText('In layout - someProp: something here - In template');
-
- runTask(() => this.rerender());
-
- this.assertText('In layout - someProp: something here - In template');
-
- runTask(() => this.context.set('prop', 'something else'));
-
- this.assertText('In layout - someProp: something else - In template');
-
- runTask(() => this.context.set('prop', 'something here'));
-
- this.assertText('In layout - someProp: something here - In template');
- }
-
- ['@test static arbitrary number of positional parameters'](assert) {
- this.owner.register(
- 'component:sample-component',
- template(
- strip`
- {{#each this.names as |name|}}
- {{name}}
- {{/each}}
- `,
- {
- component: class extends PositionalComponent {
- static positionalParams = 'names';
- },
- strictMode: false,
- }
- )
- );
-
- this.render(strip`
- {{sample-component "Foo" 4 "Bar"}}
- {{sample-component "Foo" 4 "Bar" 5 "Baz"}}
`);
-
- assert.equal(this.$('#args-3').text(), 'Foo4Bar');
- assert.equal(this.$('#args-5').text(), 'Foo4Bar5Baz');
-
- runTask(() => this.rerender());
-
- assert.equal(this.$('#args-3').text(), 'Foo4Bar');
- assert.equal(this.$('#args-5').text(), 'Foo4Bar5Baz');
- }
-
- ['@test arbitrary positional parameter conflict with hash parameter is reported']() {
- this.owner.register(
- 'component:sample-component',
- template(
- strip`
- {{#each this.names as |name|}}
- {{name}}
- {{/each}}
- `,
- {
- component: class extends Component {
- static positionalParams = 'names';
- },
- strictMode: false,
- }
- )
- );
-
- expectAssertion(() => {
- this.render(`{{sample-component "Foo" 4 "Bar" names=this.numbers id="args-3"}}`, {
- numbers: [1, 2, 3],
- });
- }, 'You cannot specify positional parameters and the hash argument `names`.');
- }
-
- ['@test can use hash parameter instead of arbitrary positional param [GH #12444]']() {
- this.owner.register(
- 'component:sample-component',
- template(
- strip`
- {{#each this.names as |name|}}
- {{name}}
- {{/each}}
- `,
- {
- component: class extends Component {
- static positionalParams = 'names';
- },
- strictMode: false,
- }
- )
- );
-
- this.render('{{sample-component names=this.things}}', {
- things: emberA(['Foo', 4, 'Bar']),
- });
-
- this.assertText('Foo4Bar');
-
- runTask(() => this.rerender());
-
- this.assertText('Foo4Bar');
-
- runTask(() => this.context.get('things').pushObject(5));
-
- this.assertText('Foo4Bar5');
-
- runTask(() => this.context.get('things').shiftObject());
-
- this.assertText('4Bar5');
-
- runTask(() => this.context.get('things').clear());
-
- this.assertText('');
-
- runTask(() => this.context.set('things', emberA(['Foo', 4, 'Bar'])));
-
- this.assertText('Foo4Bar');
- }
-
- ['@test can use hash parameter instead of positional param'](assert) {
- this.owner.register(
- 'component:sample-component',
- setComponentTemplate(
- precompileTemplate('{{this.first}} - {{this.second}}'),
- class extends Component {
- static positionalParams = ['first', 'second'];
- }
- )
- );
-
- // TODO: Fix when id is implemented
- this.render(strip`
- {{sample-component "one" "two" elementId="two-positional"}}
- {{sample-component "one" second="two" elementId="one-positional"}}
- {{sample-component first="one" second="two" elementId="no-positional"}}`);
-
- assert.equal(this.$('#two-positional').text(), 'one - two');
- assert.equal(this.$('#one-positional').text(), 'one - two');
- assert.equal(this.$('#no-positional').text(), 'one - two');
-
- runTask(() => this.rerender());
-
- assert.equal(this.$('#two-positional').text(), 'one - two');
- assert.equal(this.$('#one-positional').text(), 'one - two');
- assert.equal(this.$('#no-positional').text(), 'one - two');
- }
-
- ['@test dynamic arbitrary number of positional parameters']() {
- this.owner.register(
- 'component:sample-component',
- template(
- strip`
- {{#each this.n as |name|}}
- {{name}}
- {{/each}}
- `,
- {
- component: class extends PositionalComponent {
- static positionalParams = 'n';
- },
- strictMode: false,
- }
- )
- );
-
- this.render(`{{sample-component this.user1 this.user2}}`, {
- user1: 'Foo',
- user2: 4,
- });
-
- this.assertText('Foo4');
-
- runTask(() => this.rerender());
-
- this.assertText('Foo4');
-
- runTask(() => this.context.set('user1', 'Bar'));
-
- this.assertText('Bar4');
-
- runTask(() => this.context.set('user2', '5'));
-
- this.assertText('Bar5');
-
- runTask(() => {
- this.context.set('user1', 'Foo');
- this.context.set('user2', 4);
- });
-
- this.assertText('Foo4');
- }
-
- ['@test with ariaRole specified']() {
- this.owner.register(
- 'component:aria-test',
- setComponentTemplate(precompileTemplate('Here!'), class extends Component {})
- );
-
- this.render('{{aria-test ariaRole=this.role}}', {
- role: 'main',
- });
-
- this.assertComponentElement(this.firstChild, { attrs: { role: 'main' } });
-
- runTask(() => this.rerender());
-
- this.assertComponentElement(this.firstChild, { attrs: { role: 'main' } });
-
- runTask(() => this.context.set('role', 'input'));
-
- this.assertComponentElement(this.firstChild, {
- attrs: { role: 'input' },
- });
-
- runTask(() => this.context.set('role', 'main'));
-
- this.assertComponentElement(this.firstChild, { attrs: { role: 'main' } });
- }
-
- ['@test with ariaRole defined but initially falsey GH#16379']() {
- this.owner.register(
- 'component:aria-test',
- setComponentTemplate(precompileTemplate('Here!'), class extends Component {})
- );
-
- this.render('{{aria-test ariaRole=this.role}}', {
- role: undefined,
- });
-
- this.assertComponentElement(this.firstChild, { attrs: {} });
-
- runTask(() => this.rerender());
-
- this.assertComponentElement(this.firstChild, { attrs: {} });
-
- runTask(() => this.context.set('role', 'input'));
-
- this.assertComponentElement(this.firstChild, {
- attrs: { role: 'input' },
- });
-
- runTask(() => this.context.set('role', undefined));
-
- this.assertComponentElement(this.firstChild, { attrs: {} });
- }
-
- ['@test without ariaRole defined initially']() {
- // we are using the ability to lazily add a role as a sign that we are
- // doing extra work
- let instance;
- this.owner.register(
- 'component:aria-test',
- setComponentTemplate(
- precompileTemplate('Here!'),
- class extends Component {
- init() {
- super.init(...arguments);
- instance = this;
- }
- }
- )
- );
-
- this.render('{{aria-test}}');
-
- this.assertComponentElement(this.firstChild, { attrs: {} });
-
- runTask(() => this.rerender());
-
- this.assertComponentElement(this.firstChild, { attrs: {} });
-
- runTask(() => instance.set('ariaRole', 'input'));
-
- this.assertComponentElement(this.firstChild, { attrs: {} });
- }
-
- ['@test `template` specified in component is overridden by block']() {
- this.owner.register(
- 'component:with-template',
- setComponentTemplate(
- precompileTemplate('[In layout - {{this.name}}] {{yield}}'),
- class extends Component {
- template = 'Should not be used';
- }
- )
- );
-
- this.render(
- strip`
- {{#with-template name="with-block"}}
- [In block - {{this.name}}]
- {{/with-template}}
- {{with-template name="without-block"}}`,
- {
- name: 'Whoop, whoop!',
- }
- );
-
- this.assertText(
- '[In layout - with-block] [In block - Whoop, whoop!][In layout - without-block] '
- );
-
- runTask(() => this.rerender());
-
- this.assertText(
- '[In layout - with-block] [In block - Whoop, whoop!][In layout - without-block] '
- );
-
- runTask(() => this.context.set('name', 'Ole, ole'));
-
- this.assertText('[In layout - with-block] [In block - Ole, ole][In layout - without-block] ');
-
- runTask(() => this.context.set('name', 'Whoop, whoop!'));
-
- this.assertText(
- '[In layout - with-block] [In block - Whoop, whoop!][In layout - without-block] '
- );
- }
-
- ['@test (has-block) is true when block supplied']() {
- this.owner.register(
- 'component:with-block',
- template(
- strip`
- {{#if (has-block)}}
- {{yield}}
- {{else}}
- No Block!
- {{/if}}
- `,
- { component: templateOnly(), strictMode: false }
- )
- );
-
- this.render(strip`
- {{#with-block}}
- In template
- {{/with-block}}`);
-
- this.assertText('In template');
-
- runTask(() => this.rerender());
-
- this.assertText('In template');
- }
-
- ['@test (has-block) is false when no block supplied']() {
- this.owner.register(
- 'component:with-block',
- template(
- strip`
- {{#if (has-block)}}
- {{yield}}
- {{else}}
- No Block!
- {{/if}}
- `,
- { component: templateOnly(), strictMode: false }
- )
- );
-
- this.render('{{with-block}}');
-
- this.assertText('No Block!');
-
- runTask(() => this.rerender());
-
- this.assertText('No Block!');
- }
-
- ['@test (has-block-params) is true when block param supplied']() {
- this.owner.register(
- 'component:with-block',
- template(
- strip`
- {{#if (has-block-params)}}
- {{yield this}} - In Component
- {{else}}
- {{yield}} No Block!
- {{/if}}
- `,
- { component: class extends GlimmerComponent {}, strictMode: false }
- )
- );
-
- this.render(strip`
- {{#with-block as |something|}}
- In template
- {{/with-block}}`);
-
- this.assertText('In template - In Component');
-
- runTask(() => this.rerender());
-
- this.assertText('In template - In Component');
- }
-
- ['@test (has-block-params) is false when no block param supplied']() {
- this.owner.register(
- 'component:with-block',
- template(
- strip`
- {{#if (has-block-params)}}
- {{yield this}}
- {{else}}
- {{yield}} No Block Param!
- {{/if}}
- `,
- { component: class extends GlimmerComponent {}, strictMode: false }
- )
- );
-
- this.render(strip`
- {{#with-block}}
- In block
- {{/with-block}}`);
-
- this.assertText('In block No Block Param!');
-
- runTask(() => this.rerender());
-
- this.assertText('In block No Block Param!');
- }
-
- ['@test static named positional parameters']() {
- this.owner.register(
- 'component:sample-component',
- setComponentTemplate(
- precompileTemplate('{{this.name}}{{this.age}}'),
- class extends PositionalComponent {
- static positionalParams = ['name', 'age'];
- }
- )
- );
-
- this.render('{{sample-component "Quint" 4}}');
-
- this.assertText('Quint4');
-
- runTask(() => this.rerender());
-
- this.assertText('Quint4');
- }
-
- ['@test dynamic named positional parameters']() {
- this.owner.register(
- 'component:sample-component',
- setComponentTemplate(
- precompileTemplate('{{this.name}}{{this.age}}'),
- class extends PositionalComponent {
- static positionalParams = ['name', 'age'];
- }
- )
- );
-
- this.render('{{sample-component this.myName this.myAge}}', {
- myName: 'Quint',
- myAge: 4,
- });
-
- this.assertText('Quint4');
-
- runTask(() => this.rerender());
-
- this.assertText('Quint4');
-
- runTask(() => this.context.set('myName', 'Sergio'));
-
- this.assertText('Sergio4');
-
- runTask(() => this.context.set('myAge', 2));
-
- this.assertText('Sergio2');
-
- runTask(() => {
- this.context.set('myName', 'Quint');
- this.context.set('myAge', 4);
- });
-
- this.assertText('Quint4');
- }
-
- ['@test if a value is passed as a non-positional parameter, it raises an assertion']() {
- this.owner.register(
- 'component:sample-component',
- setComponentTemplate(
- precompileTemplate('{{this.name}}'),
- class extends Component {
- static positionalParams = ['name'];
- }
- )
- );
-
- expectAssertion(() => {
- this.render('{{sample-component this.notMyName name=this.myName}}', {
- myName: 'Quint',
- notMyName: 'Sergio',
- });
- }, 'You cannot specify both a positional param (at position 0) and the hash argument `name`.');
- }
-
- ['@test yield to inverse']() {
- this.owner.register(
- 'component:my-if',
- template(
- strip`
- {{#if @predicate}}
- Yes:{{yield @someValue}}
- {{else}}
- No:{{yield to="inverse"}}
- {{/if}}
- `,
- { component: templateOnly(), strictMode: false }
- )
- );
-
- this.render(
- strip`
- {{#my-if predicate=this.activated someValue=42 as |result|}}
- Hello{{result}}
- {{else}}
- Goodbye
- {{/my-if}}`,
- {
- activated: true,
- }
- );
-
- this.assertText('Yes:Hello42');
-
- runTask(() => this.rerender());
-
- this.assertText('Yes:Hello42');
-
- runTask(() => this.context.set('activated', false));
-
- this.assertText('No:Goodbye');
-
- runTask(() => this.context.set('activated', true));
-
- this.assertText('Yes:Hello42');
- }
-
- ['@test expression (has-block) inverse']() {
- this.owner.register(
- 'component:check-inverse',
- template(
- strip`
- {{#if (has-block "inverse")}}
- Yes
- {{else}}
- No
- {{/if}}
- `,
- { component: templateOnly(), strictMode: false }
- )
- );
-
- this.render(strip`
- {{#check-inverse}}{{/check-inverse}}
- {{#check-inverse}}{{else}}{{/check-inverse}}`);
-
- this.assertHTML('NoYes');
-
- this.assertStableRerender();
- }
-
- ['@test expression (has-block) default']() {
- this.owner.register(
- 'component:check-block',
- template(
- strip`
- {{#if (has-block)}}
- Yes
- {{else}}
- No
- {{/if}}
- `,
- { component: templateOnly(), strictMode: false }
- )
- );
-
- this.render(strip`
- {{check-block}}
- {{#check-block}}{{/check-block}}`);
-
- this.assertHTML('NoYes');
-
- this.assertStableRerender();
- }
-
- ['@test expression (has-block-params) inverse']() {
- this.owner.register(
- 'component:check-inverse',
- template(
- strip`
- {{#if (has-block-params "inverse")}}
- Yes
- {{else}}
- No
- {{/if}}
- `,
- { component: templateOnly(), strictMode: false }
- )
- );
-
- this.render(strip`
- {{#check-inverse}}{{/check-inverse}}
- {{#check-inverse as |something|}}{{/check-inverse}}`);
-
- this.assertHTML('NoNo');
-
- this.assertStableRerender();
- }
-
- ['@test expression (has-block-params) default']() {
- this.owner.register(
- 'component:check-block',
- template(
- strip`
- {{#if (has-block-params)}}
- Yes
- {{else}}
- No
- {{/if}}
- `,
- { component: templateOnly(), strictMode: false }
- )
- );
-
- this.render(strip`
- {{#check-block}}{{/check-block}}
- {{#check-block as |something|}}{{/check-block}}`);
-
- this.assertHTML('NoYes');
-
- this.assertStableRerender();
- }
-
- ['@test (has-block) expression in an attribute'](assert) {
- this.owner.register(
- 'component:check-attr',
- setComponentTemplate(
- precompileTemplate(' '),
- templateOnly()
- )
- );
-
- this.render(strip`
- {{check-attr}}
- {{#check-attr}}{{/check-attr}}`);
-
- equalsElement(assert, this.$('button')[0], 'button', { name: 'false' }, '');
- equalsElement(assert, this.$('button')[1], 'button', { name: 'true' }, '');
-
- this.assertStableRerender();
- }
-
- ['@test (has-block) inverse expression in an attribute'](assert) {
- this.owner.register(
- 'component:check-attr',
- setComponentTemplate(
- precompileTemplate(' '),
- templateOnly()
- )
- );
-
- this.render(strip`
- {{#check-attr}}{{/check-attr}}
- {{#check-attr}}{{else}}{{/check-attr}}`);
-
- equalsElement(assert, this.$('button')[0], 'button', { name: 'false' }, '');
- equalsElement(assert, this.$('button')[1], 'button', { name: 'true' }, '');
-
- this.assertStableRerender();
- }
-
- ['@test (has-block-params) expression in an attribute'](assert) {
- this.owner.register(
- 'component:check-attr',
- setComponentTemplate(
- precompileTemplate(' '),
- templateOnly()
- )
- );
-
- this.render(strip`
- {{#check-attr}}{{/check-attr}}
- {{#check-attr as |something|}}{{/check-attr}}`);
-
- equalsElement(assert, this.$('button')[0], 'button', { name: 'false' }, '');
- equalsElement(assert, this.$('button')[1], 'button', { name: 'true' }, '');
-
- this.assertStableRerender();
- }
-
- ['@test (has-block-params) inverse expression in an attribute'](assert) {
- this.owner.register(
- 'component:check-attr',
- setComponentTemplate(
- precompileTemplate(' '),
- templateOnly()
- )
- );
-
- this.render(strip`
- {{#check-attr}}{{/check-attr}}
- {{#check-attr as |something|}}{{/check-attr}}`);
-
- equalsElement(assert, this.$('button')[0], 'button', { name: 'false' }, '');
- equalsElement(assert, this.$('button')[1], 'button', { name: 'false' }, '');
-
- this.assertStableRerender();
- }
-
- ['@test (has-block) as a param to a helper']() {
- this.owner.register(
- 'component:check-helper',
- setComponentTemplate(
- precompileTemplate('{{if (has-block) "true" "false"}}'),
- templateOnly()
- )
- );
-
- this.render(strip`
- {{check-helper}}
- {{#check-helper}}{{/check-helper}}`);
-
- this.assertHTML('falsetrue');
-
- this.assertStableRerender();
- }
-
- ['@test (has-block) inverse as a param to a helper']() {
- this.owner.register(
- 'component:check-helper',
- setComponentTemplate(
- precompileTemplate('{{if (has-block "inverse") "true" "false"}}'),
- templateOnly()
- )
- );
-
- this.render(strip`
- {{#check-helper}}{{/check-helper}}
- {{#check-helper}}{{else}}{{/check-helper}}`);
-
- this.assertHTML('falsetrue');
-
- this.assertStableRerender();
- }
-
- ['@test (has-block-params) as a param to a helper']() {
- this.owner.register(
- 'component:check-helper',
- setComponentTemplate(
- precompileTemplate('{{if (has-block-params) "true" "false"}}'),
- templateOnly()
- )
- );
-
- this.render(strip`
- {{#check-helper}}{{/check-helper}}
- {{#check-helper as |something|}}{{/check-helper}}`);
-
- this.assertHTML('falsetrue');
-
- this.assertStableRerender();
- }
-
- ['@test (has-block-params) inverse as a param to a helper']() {
- this.owner.register(
- 'component:check-helper',
- setComponentTemplate(
- precompileTemplate('{{if (has-block-params "inverse") "true" "false"}}'),
- templateOnly()
- )
- );
-
- this.render(strip`
- {{#check-helper}}{{/check-helper}}
- {{#check-helper as |something|}}{{/check-helper}}`);
-
- this.assertHTML('falsefalse');
-
- this.assertStableRerender();
- }
-
- ['@test component in template of a yielding component should have the proper parentView'](
- assert
- ) {
- let outer, innerTemplate, innerLayout;
-
- this.owner.register(
- 'component:x-outer',
- setComponentTemplate(
- precompileTemplate('{{x-inner-in-layout}}{{yield}}'),
- class extends Component {
- init() {
- super.init(...arguments);
- outer = this;
- }
- }
- )
- );
-
- this.owner.register(
- 'component:x-inner-in-template',
- class extends Component {
- init() {
- super.init(...arguments);
- innerTemplate = this;
- }
- }
- );
-
- this.owner.register(
- 'component:x-inner-in-layout',
- class extends Component {
- init() {
- super.init(...arguments);
- innerLayout = this;
- }
- }
- );
-
- this.render('{{#x-outer}}{{x-inner-in-template}}{{/x-outer}}');
+ runTask(() => this.context.set('prop', 'other thing there'));
- assert.equal(
- innerTemplate.parentView,
- outer,
- 'receives the wrapping component as its parentView in template blocks'
- );
- assert.equal(
- innerLayout.parentView,
- outer,
- 'receives the wrapping component as its parentView in layout'
- );
- assert.equal(
- outer.parentView,
- this.context,
- 'x-outer receives the ambient scope as its parentView'
- );
+ this.assertText('In layout - someProp: other thing there');
- runTask(() => this.rerender());
+ runTask(() => this.context.set('prop', 'something here'));
- assert.equal(
- innerTemplate.parentView,
- outer,
- 'receives the wrapping component as its parentView in template blocks'
- );
- assert.equal(
- innerLayout.parentView,
- outer,
- 'receives the wrapping component as its parentView in layout'
- );
- assert.equal(
- outer.parentView,
- this.context,
- 'x-outer receives the ambient scope as its parentView'
- );
+ this.assertText('In layout - someProp: something here');
}
- ['@test newly-added sub-components get correct parentView'](assert) {
- let outer, inner;
-
- this.owner.register(
- 'component:x-outer',
- class extends Component {
- init() {
- super.init(...arguments);
- outer = this;
- }
- }
- );
+ ['@test block with properties on attrs is asserted against']() {
+ expectAssertion(() => {
+ this.owner.register(
+ 'component:with-block',
+ template('In layout - someProp: {{attrs.someProp}} - {{yield}}', {
+ component: templateOnly(),
+ strictMode: false,
+ })
+ );
+ }, 'Using {{attrs}} to reference named arguments is not supported. {{attrs.someProp}} should be updated to {{@someProp}}. (L1:C24) ');
+ }
+ ['@test block with named argument']() {
this.owner.register(
- 'component:x-inner',
- class extends Component {
- init() {
- super.init(...arguments);
- inner = this;
- }
- }
+ 'component:with-block',
+ setComponentTemplate(
+ precompileTemplate('In layout - someProp: {{@someProp}} - {{yield}}'),
+ templateOnly()
+ )
);
this.render(
strip`
- {{#x-outer}}
- {{#if this.showInner}}
- {{x-inner}}
- {{/if}}
- {{/x-outer}}`,
+ {{#with-block someProp=this.prop}}
+ In template
+ {{/with-block}}`,
{
- showInner: false,
+ prop: 'something here',
}
);
- assert.equal(
- outer.parentView,
- this.context,
- 'x-outer receives the ambient scope as its parentView'
- );
+ this.assertText('In layout - someProp: something here - In template');
runTask(() => this.rerender());
- assert.equal(
- outer.parentView,
- this.context,
- 'x-outer receives the ambient scope as its parentView (after rerender)'
- );
+ this.assertText('In layout - someProp: something here - In template');
- runTask(() => this.context.set('showInner', true));
+ runTask(() => this.context.set('prop', 'something else'));
- assert.equal(
- outer.parentView,
- this.context,
- 'x-outer receives the ambient scope as its parentView'
- );
- assert.equal(
- inner.parentView,
- outer,
- 'receives the wrapping component as its parentView in template blocks'
- );
+ this.assertText('In layout - someProp: something else - In template');
- runTask(() => this.context.set('showInner', false));
+ runTask(() => this.context.set('prop', 'something here'));
- assert.equal(
- outer.parentView,
- this.context,
- 'x-outer receives the ambient scope as its parentView'
- );
+ this.assertText('In layout - someProp: something here - In template');
}
- ["@test when a property is changed during children's rendering"]() {
- let middle;
-
+ ['@test static arbitrary number of positional parameters'](assert) {
this.owner.register(
- 'component:x-outer',
- setComponentTemplate(
- precompileTemplate('{{#x-middle}}{{x-inner value=this.value}}{{/x-middle}}'),
- class extends GlimmerComponent {
- value = 1;
+ 'component:sample-component',
+ template(
+ strip`
+ {{#each this.names as |name|}}
+ {{name}}
+ {{/each}}
+ `,
+ {
+ component: class extends PositionalComponent {
+ static positionalParams = 'names';
+ },
+ strictMode: false,
}
)
);
- class MiddleComponent extends GlimmerComponent {
- constructor(owner, args) {
- super(owner, args);
- middle = this;
- }
- @tracked value = null;
- }
-
- this.owner.register(
- 'component:x-middle',
- setComponentTemplate(
- precompileTemplate('{{this.value}}
{{yield}}'),
- MiddleComponent
- )
- );
+ this.render(strip`
+ {{sample-component "Foo" 4 "Bar"}}
+ {{sample-component "Foo" 4 "Bar" 5 "Baz"}}
`);
- this.owner.register(
- 'component:x-inner',
- setComponentTemplate(
- precompileTemplate('{{@value}}
'),
- class extends GlimmerComponent {
- constructor(owner, args) {
- super(owner, args);
- middle.value = this.args.value;
- }
- }
- )
- );
+ assert.equal(this.$('#args-3').text(), 'Foo4Bar');
+ assert.equal(this.$('#args-5').text(), 'Foo4Bar5Baz');
- let expectedBacktrackingMessage = backtrackingMessageFor('value', MiddleComponent.name, {
- renderTree: ['x-outer', 'x-middle', 'this.value'],
- });
+ runTask(() => this.rerender());
- expectAssertion(() => {
- this.render('{{x-outer}}');
- }, expectedBacktrackingMessage);
+ assert.equal(this.$('#args-3').text(), 'Foo4Bar');
+ assert.equal(this.$('#args-5').text(), 'Foo4Bar5Baz');
}
- ["@test when a shared dependency is changed during children's rendering"]() {
- this.owner.register(
- 'component:x-outer',
- setComponentTemplate(
- precompileTemplate(
- '{{this.wrapper.content}}
{{x-inner value=this.value wrapper=this.wrapper}}'
- ),
- class extends GlimmerComponent {
- value = 1;
- wrapper = EmberObject.create({ content: null });
- }
- )
- );
-
+ ['@test dynamic arbitrary number of positional parameters']() {
this.owner.register(
- 'component:x-inner',
- setComponentTemplate(
- precompileTemplate('{{@wrapper.content}}
'),
- class extends GlimmerComponent {
- constructor(owner, args) {
- super(owner, args);
- this.args.wrapper.set('content', this.args.value);
- }
+ 'component:sample-component',
+ template(
+ strip`
+ {{#each this.n as |name|}}
+ {{name}}
+ {{/each}}
+ `,
+ {
+ component: class extends PositionalComponent {
+ static positionalParams = 'n';
+ },
+ strictMode: false,
}
)
);
- let expectedBacktrackingMessage = backtrackingMessageFor('content', '<.+?>', {
- renderTree: ['x-outer', 'this.wrapper.content'],
+ this.render(`{{sample-component this.user1 this.user2}}`, {
+ user1: 'Foo',
+ user2: 4,
});
- expectAssertion(() => {
- this.render('{{x-outer}}');
- }, expectedBacktrackingMessage);
- }
+ this.assertText('Foo4');
- ["@test when a shared dependency is changed during children's rendering (tracked)"]() {
- class Wrapper {
- @tracked content = null;
- }
+ runTask(() => this.rerender());
- this.owner.register(
- 'component:x-outer',
- setComponentTemplate(
- precompileTemplate(
- '{{this.wrapper.content}}
{{x-inner value=this.value wrapper=this.wrapper}}'
- ),
- class extends GlimmerComponent {
- value = 1;
- wrapper = new Wrapper();
- }
- )
- );
+ this.assertText('Foo4');
- this.owner.register(
- 'component:x-inner',
- setComponentTemplate(
- precompileTemplate('{{@wrapper.content}}
'),
- class extends GlimmerComponent {
- constructor(owner, args) {
- super(owner, args);
- this.args.wrapper.content = this.args.value;
- }
- }
- )
- );
+ runTask(() => this.context.set('user1', 'Bar'));
- let expectedBacktrackingMessage = backtrackingMessageFor('content', Wrapper.name, {
- renderTree: ['x-outer', 'this.wrapper.content'],
+ this.assertText('Bar4');
+
+ runTask(() => this.context.set('user2', '5'));
+
+ this.assertText('Bar5');
+
+ runTask(() => {
+ this.context.set('user1', 'Foo');
+ this.context.set('user2', 4);
});
- expectAssertion(() => {
- this.render('{{x-outer}}');
- }, expectedBacktrackingMessage);
+ this.assertText('Foo4');
}
- ['@test non-block with each rendering child components']() {
+ ['@test (has-block) is true when block supplied']() {
this.owner.register(
- 'component:non-block',
+ 'component:with-block',
template(
strip`
- In layout. {{#each @items as |item|}}
- [{{child-non-block item=item}}]
- {{/each}}
+ {{#if (has-block)}}
+ {{yield}}
+ {{else}}
+ No Block!
+ {{/if}}
`,
{ component: templateOnly(), strictMode: false }
)
);
- this.owner.register(
- 'component:child-non-block',
- setComponentTemplate(precompileTemplate('Child: {{@item}}.'), templateOnly())
- );
-
- let items = emberA(['Tom', 'Dick', 'Harry']);
-
- this.render('{{non-block items=this.items}}', { items });
+ this.render(strip`
+ {{#with-block}}
+ In template
+ {{/with-block}}`);
- this.assertText('In layout. [Child: Tom.][Child: Dick.][Child: Harry.]');
+ this.assertText('In template');
runTask(() => this.rerender());
- this.assertText('In layout. [Child: Tom.][Child: Dick.][Child: Harry.]');
-
- runTask(() => this.context.get('items').pushObject('Sergio'));
+ this.assertText('In template');
+ }
- this.assertText('In layout. [Child: Tom.][Child: Dick.][Child: Harry.][Child: Sergio.]');
+ ['@test (has-block) is false when no block supplied']() {
+ this.owner.register(
+ 'component:with-block',
+ template(
+ strip`
+ {{#if (has-block)}}
+ {{yield}}
+ {{else}}
+ No Block!
+ {{/if}}
+ `,
+ { component: templateOnly(), strictMode: false }
+ )
+ );
- runTask(() => this.context.get('items').shiftObject());
+ this.render('{{with-block}}');
- this.assertText('In layout. [Child: Dick.][Child: Harry.][Child: Sergio.]');
+ this.assertText('No Block!');
- runTask(() => this.context.set('items', emberA(['Tom', 'Dick', 'Harry'])));
+ runTask(() => this.rerender());
- this.assertText('In layout. [Child: Tom.][Child: Dick.][Child: Harry.]');
+ this.assertText('No Block!');
}
- ['@test specifying classNames results in correct class'](assert) {
+ ['@test (has-block-params) is true when block param supplied']() {
this.owner.register(
- 'component:some-clicky-thing',
- class extends Component {
- tagName = 'button';
- classNames = ['foo', 'bar'];
- }
+ 'component:with-block',
+ template(
+ strip`
+ {{#if (has-block-params)}}
+ {{yield this}} - In Component
+ {{else}}
+ {{yield}} No Block!
+ {{/if}}
+ `,
+ { component: class extends GlimmerComponent {}, strictMode: false }
+ )
);
this.render(strip`
- {{#some-clicky-thing classNames="baz"}}
- Click Me
- {{/some-clicky-thing}}`);
-
- // TODO: ember-view is no longer viewable in the classNames array. Bug or
- // feature?
- let expectedClassNames = ['ember-view', 'foo', 'bar', 'baz'];
-
- assert.ok(
- this.$('button').is('.foo.bar.baz.ember-view'),
- `the element has the correct classes: ${this.$('button').attr('class')}`
- );
- // `ember-view` is no longer in classNames.
- // assert.deepEqual(clickyThing.get('classNames'), expectedClassNames, 'classNames are properly combined');
- this.assertComponentElement(this.firstChild, {
- tagName: 'button',
- attrs: { class: classes(expectedClassNames.join(' ')) },
- });
+ {{#with-block as |something|}}
+ In template
+ {{/with-block}}`);
+
+ this.assertText('In template - In Component');
runTask(() => this.rerender());
- assert.ok(
- this.$('button').is('.foo.bar.baz.ember-view'),
- `the element has the correct classes: ${this.$('button').attr('class')} (rerender)`
- );
- // `ember-view` is no longer in classNames.
- // assert.deepEqual(clickyThing.get('classNames'), expectedClassNames, 'classNames are properly combined (rerender)');
- this.assertComponentElement(this.firstChild, {
- tagName: 'button',
- attrs: { class: classes(expectedClassNames.join(' ')) },
- });
+ this.assertText('In template - In Component');
}
- ['@test specifying custom concatenatedProperties avoids clobbering']() {
+ ['@test (has-block-params) is false when no block param supplied']() {
this.owner.register(
- 'component:some-clicky-thing',
+ 'component:with-block',
template(
strip`
- {{#each this.blahzz as |p|}}
- {{p}}
- {{/each}}
- - {{yield}}
+ {{#if (has-block-params)}}
+ {{yield this}}
+ {{else}}
+ {{yield}} No Block Param!
+ {{/if}}
`,
- {
- component: class extends Component {
- concatenatedProperties = ['blahzz'];
- blahzz = ['blark', 'pory'];
- },
- strictMode: false,
- }
+ { component: class extends GlimmerComponent {}, strictMode: false }
)
);
this.render(strip`
- {{#some-clicky-thing blahzz="baz"}}
- Click Me
- {{/some-clicky-thing}}`);
+ {{#with-block}}
+ In block
+ {{/with-block}}`);
- this.assertText('blarkporybaz- Click Me');
+ this.assertText('In block No Block Param!');
runTask(() => this.rerender());
- this.assertText('blarkporybaz- Click Me');
+ this.assertText('In block No Block Param!');
}
- ['@test a two way binding flows upstream when consumed in the template']() {
- let component;
- let FooBarComponent = class extends Component {
- init() {
- super.init(...arguments);
- component = this;
- }
- };
+ ['@test static named positional parameters']() {
+ this.owner.register(
+ 'component:sample-component',
+ setComponentTemplate(
+ precompileTemplate('{{this.name}}{{this.age}}'),
+ class extends PositionalComponent {
+ static positionalParams = ['name', 'age'];
+ }
+ )
+ );
+
+ this.render('{{sample-component "Quint" 4}}');
+
+ this.assertText('Quint4');
+
+ runTask(() => this.rerender());
+
+ this.assertText('Quint4');
+ }
+ ['@test dynamic named positional parameters']() {
this.owner.register(
- 'component:foo-bar',
- setComponentTemplate(precompileTemplate('{{this.bar}}'), FooBarComponent)
+ 'component:sample-component',
+ setComponentTemplate(
+ precompileTemplate('{{this.name}}{{this.age}}'),
+ class extends PositionalComponent {
+ static positionalParams = ['name', 'age'];
+ }
+ )
);
- this.render('{{this.localBar}} - {{foo-bar bar=this.localBar}}', {
- localBar: 'initial value',
+ this.render('{{sample-component this.myName this.myAge}}', {
+ myName: 'Quint',
+ myAge: 4,
});
- this.assertText('initial value - initial value');
+ this.assertText('Quint4');
runTask(() => this.rerender());
- this.assertText('initial value - initial value');
+ this.assertText('Quint4');
- runTask(() => {
- component.set('bar', 'updated value');
- });
+ runTask(() => this.context.set('myName', 'Sergio'));
- this.assertText('updated value - updated value');
+ this.assertText('Sergio4');
- runTask(() => {
- component.set('bar', undefined);
- });
+ runTask(() => this.context.set('myAge', 2));
- this.assertText(' - ');
+ this.assertText('Sergio2');
runTask(() => {
- this.component.set('localBar', 'initial value');
+ this.context.set('myName', 'Quint');
+ this.context.set('myAge', 4);
});
- this.assertText('initial value - initial value');
+ this.assertText('Quint4');
}
- ['@test a two way binding flows upstream through a CP when consumed in the template']() {
- let component;
- let FooBarComponent = class extends Component {
- init() {
- super.init(...arguments);
- component = this;
- }
+ ['@test yield to inverse']() {
+ this.owner.register(
+ 'component:my-if',
+ template(
+ strip`
+ {{#if @predicate}}
+ Yes:{{yield @someValue}}
+ {{else}}
+ No:{{yield to="inverse"}}
+ {{/if}}
+ `,
+ { component: templateOnly(), strictMode: false }
+ )
+ );
- @computed
- get bar() {
- return this._bar;
- }
- set bar(value) {
- this._bar = value;
+ this.render(
+ strip`
+ {{#my-if predicate=this.activated someValue=42 as |result|}}
+ Hello{{result}}
+ {{else}}
+ Goodbye
+ {{/my-if}}`,
+ {
+ activated: true,
}
- };
+ );
+
+ this.assertText('Yes:Hello42');
+
+ runTask(() => this.rerender());
+
+ this.assertText('Yes:Hello42');
+
+ runTask(() => this.context.set('activated', false));
+
+ this.assertText('No:Goodbye');
+
+ runTask(() => this.context.set('activated', true));
+
+ this.assertText('Yes:Hello42');
+ }
+ ['@test yield to else']() {
this.owner.register(
- 'component:foo-bar',
- setComponentTemplate(precompileTemplate('{{this.bar}}'), FooBarComponent)
+ 'component:my-if',
+ template(
+ strip`
+ {{#if @predicate}}
+ Yes:{{yield @someValue}}
+ {{else}}
+ No:{{yield to="else"}}
+ {{/if}}
+ `,
+ { component: templateOnly(), strictMode: false }
+ )
);
- this.render('{{this.localBar}} - {{foo-bar bar=this.localBar}}', {
- localBar: 'initial value',
- });
+ this.render(
+ strip`
+ {{#my-if predicate=this.activated someValue=42 as |result|}}
+ Hello{{result}}
+ {{else}}
+ Goodbye
+ {{/my-if}}`,
+ {
+ activated: true,
+ }
+ );
- this.assertText('initial value - initial value');
+ this.assertText('Yes:Hello42');
runTask(() => this.rerender());
- this.assertText('initial value - initial value');
+ this.assertText('Yes:Hello42');
- runTask(() => {
- component.set('bar', 'updated value');
- });
+ runTask(() => this.context.set('activated', false));
- this.assertText('updated value - updated value');
+ this.assertText('No:Goodbye');
- runTask(() => {
- this.component.set('localBar', 'initial value');
- });
+ runTask(() => this.context.set('activated', true));
- this.assertText('initial value - initial value');
+ this.assertText('Yes:Hello42');
}
- ['@test a two way binding flows upstream through a CP without template consumption']() {
- let component;
- let FooBarComponent = class extends Component {
- init() {
- super.init(...arguments);
- component = this;
- }
+ ['@test expression (has-block) inverse']() {
+ this.owner.register(
+ 'component:check-inverse',
+ template(
+ strip`
+ {{#if (has-block "inverse")}}
+ Yes
+ {{else}}
+ No
+ {{/if}}
+ `,
+ { component: templateOnly(), strictMode: false }
+ )
+ );
- @computed
- get bar() {
- return this._bar;
- }
+ this.render(strip`
+ {{#check-inverse}}{{/check-inverse}}
+ {{#check-inverse}}{{else}}{{/check-inverse}}`);
- set bar(value) {
- this._bar = value;
- }
- };
+ this.assertHTML('NoYes');
+
+ this.assertStableRerender();
+ }
+ ['@test expression (has-block) default']() {
this.owner.register(
- 'component:foo-bar',
- setComponentTemplate(precompileTemplate(''), FooBarComponent)
+ 'component:check-block',
+ template(
+ strip`
+ {{#if (has-block)}}
+ Yes
+ {{else}}
+ No
+ {{/if}}
+ `,
+ { component: templateOnly(), strictMode: false }
+ )
);
- this.render('{{this.localBar}}{{foo-bar bar=this.localBar}}', {
- localBar: 'initial value',
- });
+ this.render(strip`
+ {{check-block}}
+ {{#check-block}}{{/check-block}}`);
- this.assertText('initial value');
+ this.assertHTML('NoYes');
- runTask(() => this.rerender());
+ this.assertStableRerender();
+ }
+
+ ['@test expression (has-block-params) inverse']() {
+ this.owner.register(
+ 'component:check-inverse',
+ template(
+ strip`
+ {{#if (has-block-params "inverse")}}
+ Yes
+ {{else}}
+ No
+ {{/if}}
+ `,
+ { component: templateOnly(), strictMode: false }
+ )
+ );
+
+ this.render(strip`
+ {{#check-inverse}}{{/check-inverse}}
+ {{#check-inverse as |something|}}{{/check-inverse}}`);
- this.assertText('initial value');
+ this.assertHTML('NoNo');
- runTask(() => {
- component.set('bar', 'updated value');
- });
+ this.assertStableRerender();
+ }
+
+ ['@test expression (has-block-params) default']() {
+ this.owner.register(
+ 'component:check-block',
+ template(
+ strip`
+ {{#if (has-block-params)}}
+ Yes
+ {{else}}
+ No
+ {{/if}}
+ `,
+ { component: templateOnly(), strictMode: false }
+ )
+ );
- this.assertText('updated value');
+ this.render(strip`
+ {{#check-block}}{{/check-block}}
+ {{#check-block as |something|}}{{/check-block}}`);
- runTask(() => {
- this.component.set('localBar', 'initial value');
- });
+ this.assertHTML('NoYes');
- this.assertText('initial value');
+ this.assertStableRerender();
}
- ['@test GH#18417 - a two way binding flows upstream to a parent component through a CP']() {
- let parent, child;
- let ParentComponent = class extends Component {
- init() {
- super.init(...arguments);
- parent = this;
- }
-
- @tracked string = 'Hello|World';
- };
-
+ ['@test (has-block) expression in an attribute'](assert) {
this.owner.register(
- 'component:parent',
+ 'component:check-attr',
setComponentTemplate(
- precompileTemplate(
- '{{child value=this.string}}Parent String={{this.string}} '
- ),
- ParentComponent
+ precompileTemplate(' '),
+ templateOnly()
)
);
- let ChildComponent = class extends Component {
- init() {
- super.init(...arguments);
- child = this;
- }
-
- a = null; // computed based on passed in value of `string`
- b = null; // computed based on passed in value of `string`
+ this.render(strip`
+ {{check-attr}}
+ {{#check-attr}}{{/check-attr}}`);
- @computed('a', 'b')
- get value() {
- return this.a + '|' + this.b;
- }
+ equalsElement(assert, this.$('button')[0], 'button', { name: 'false' }, '');
+ equalsElement(assert, this.$('button')[1], 'button', { name: 'true' }, '');
- set value(value) {
- let vals = value.split('|');
- this.set('a', vals[0]);
- this.set('b', vals[1]);
- }
- };
+ this.assertStableRerender();
+ }
+ ['@test (has-block) inverse expression in an attribute'](assert) {
this.owner.register(
- 'component:child',
- setComponentTemplate(precompileTemplate('{{this.value}}'), ChildComponent)
- );
-
- this.render('{{parent}}');
-
- this.assert.equal(parent.string, 'Hello|World', 'precond - parent value');
- this.assert.equal(
- this.element.querySelector('[data-test-parent-value]').textContent.trim(),
- 'Hello|World',
- 'precond - parent rendered value'
+ 'component:check-attr',
+ setComponentTemplate(
+ precompileTemplate(' '),
+ templateOnly()
+ )
);
- runTask(() => {
- child.set('a', 'Foo');
- });
-
- this.assert.equal(parent.string, 'Foo|World', 'parent value updated');
-
- this.assert.equal(
- this.element.querySelector('[data-test-parent-value]').textContent.trim(),
- 'Foo|World',
- 'parent updated value rendered'
- );
+ this.render(strip`
+ {{#check-attr}}{{/check-attr}}
+ {{#check-attr}}{{else}}{{/check-attr}}`);
- runTask(() => {
- child.set('a', 'Hello');
- });
+ equalsElement(assert, this.$('button')[0], 'button', { name: 'false' }, '');
+ equalsElement(assert, this.$('button')[1], 'button', { name: 'true' }, '');
- this.assert.equal(parent.string, 'Hello|World', 'parent value reset');
- this.assert.equal(
- this.element.querySelector('[data-test-parent-value]').textContent.trim(),
- 'Hello|World',
- 'parent template reset'
- );
+ this.assertStableRerender();
}
- ['@test services can be injected into components']() {
- let serviceInstance;
- this.registerService(
- 'name',
- class extends Service {
- init() {
- super.init(...arguments);
- serviceInstance = this;
- }
- last = 'Jackson';
- }
- );
-
+ ['@test (has-block-params) expression in an attribute'](assert) {
this.owner.register(
- 'component:foo-bar',
+ 'component:check-attr',
setComponentTemplate(
- precompileTemplate('{{this.name.last}}'),
- class extends GlimmerComponent {
- @service
- name;
- }
+ precompileTemplate(' '),
+ templateOnly()
)
);
- this.render('{{foo-bar}}');
-
- this.assertText('Jackson');
+ this.render(strip`
+ {{#check-attr}}{{/check-attr}}
+ {{#check-attr as |something|}}{{/check-attr}}`);
- runTask(() => this.rerender());
+ equalsElement(assert, this.$('button')[0], 'button', { name: 'false' }, '');
+ equalsElement(assert, this.$('button')[1], 'button', { name: 'true' }, '');
- this.assertText('Jackson');
+ this.assertStableRerender();
+ }
- runTask(() => {
- serviceInstance.set('last', 'McGuffey');
- });
+ ['@test (has-block-params) inverse expression in an attribute'](assert) {
+ this.owner.register(
+ 'component:check-attr',
+ setComponentTemplate(
+ precompileTemplate(' '),
+ templateOnly()
+ )
+ );
- this.assertText('McGuffey');
+ this.render(strip`
+ {{#check-attr}}{{/check-attr}}
+ {{#check-attr as |something|}}{{/check-attr}}`);
- runTask(() => {
- serviceInstance.set('last', 'Jackson');
- });
+ equalsElement(assert, this.$('button')[0], 'button', { name: 'false' }, '');
+ equalsElement(assert, this.$('button')[1], 'button', { name: 'false' }, '');
- this.assertText('Jackson');
+ this.assertStableRerender();
}
- ['@test injecting an unknown service raises an exception']() {
+ ['@test (has-block) as a param to a helper']() {
this.owner.register(
- 'component:foo-bar',
- class extends Component {
- @service
- missingService;
- }
+ 'component:check-helper',
+ setComponentTemplate(
+ precompileTemplate('{{if (has-block) "true" "false"}}'),
+ templateOnly()
+ )
);
- expectAssertion(() => {
- this.render('{{foo-bar}}');
- }, "Attempting to inject an unknown injection: 'service:missingService'");
- }
+ this.render(strip`
+ {{check-helper}}
+ {{#check-helper}}{{/check-helper}}`);
- ['@test throws if `super.init` is not called from `init`']() {
- this.owner.register(
- 'component:foo-bar',
- class extends Component {
- init() {}
- }
- );
+ this.assertHTML('falsetrue');
- expectAssertion(() => {
- this.render('{{foo-bar}}');
- }, /You must call `super.init\(...arguments\);` or `this._super\(...arguments\)` when overriding `init` on a framework object. Please update .*/);
+ this.assertStableRerender();
}
- ['@test it can use readDOMAttr to read input value']() {
- let component;
- let assertElement = (expectedValue) => {
- // value is a property, not an attribute
- this.assertHTML(` `);
- this.assert.equal(this.firstChild.value, expectedValue, 'value property is correct');
- this.assert.equal(
- get(component, 'value'),
- expectedValue,
- 'component.get("value") is correct'
- );
- };
-
+ ['@test (has-block) inverse as a param to a helper']() {
this.owner.register(
- 'component:one-way-input',
- class extends Component {
- tagName = 'input';
- attributeBindings = ['value'];
-
- init() {
- super.init(...arguments);
- component = this;
- }
-
- change() {
- let value = this.readDOMAttr('value');
- this.set('value', value);
- }
- }
+ 'component:check-helper',
+ setComponentTemplate(
+ precompileTemplate('{{if (has-block "inverse") "true" "false"}}'),
+ templateOnly()
+ )
);
- this.render('{{one-way-input value=this.value}}', {
- value: 'foo',
- });
+ this.render(strip`
+ {{#check-helper}}{{/check-helper}}
+ {{#check-helper}}{{else}}{{/check-helper}}`);
- assertElement('foo');
+ this.assertHTML('falsetrue');
this.assertStableRerender();
+ }
- runTask(() => {
- this.firstChild.value = 'bar';
- this.$('input').trigger('change');
- });
+ ['@test (has-block-params) as a param to a helper']() {
+ this.owner.register(
+ 'component:check-helper',
+ setComponentTemplate(
+ precompileTemplate('{{if (has-block-params) "true" "false"}}'),
+ templateOnly()
+ )
+ );
- assertElement('bar');
+ this.render(strip`
+ {{#check-helper}}{{/check-helper}}
+ {{#check-helper as |something|}}{{/check-helper}}`);
- runTask(() => {
- this.firstChild.value = 'foo';
- this.$('input').trigger('change');
- });
+ this.assertHTML('falsetrue');
- assertElement('foo');
+ this.assertStableRerender();
+ }
- runTask(() => {
- set(component, 'value', 'bar');
- });
+ ['@test (has-block-params) inverse as a param to a helper']() {
+ this.owner.register(
+ 'component:check-helper',
+ setComponentTemplate(
+ precompileTemplate('{{if (has-block-params "inverse") "true" "false"}}'),
+ templateOnly()
+ )
+ );
- assertElement('bar');
+ this.render(strip`
+ {{#check-helper}}{{/check-helper}}
+ {{#check-helper as |something|}}{{/check-helper}}`);
- runTask(() => {
- this.firstChild.value = 'foo';
- this.$('input').trigger('change');
- });
+ this.assertHTML('falsefalse');
- assertElement('foo');
+ this.assertStableRerender();
}
- ['@test child triggers revalidate during parent destruction (GH#13846)']() {
+ ["@test when a property is changed during children's rendering"]() {
+ let middle;
+
this.owner.register(
- 'component:x-select',
+ 'component:x-outer',
setComponentTemplate(
- precompileTemplate('{{yield this}}'),
- class extends Component {
- tagName = 'select';
-
- init() {
- super.init(...arguments);
- this.options = emberA([]);
- this.value = null;
- }
-
- updateValue() {
- let newValue = this.get('options.lastObject.value');
-
- this.set('value', newValue);
- }
+ precompileTemplate('{{#x-middle}}{{x-inner value=this.value}}{{/x-middle}}'),
+ class extends GlimmerComponent {
+ value = 1;
+ }
+ )
+ );
- registerOption(option) {
- this.get('options').addObject(option);
- }
+ class MiddleComponent extends GlimmerComponent {
+ constructor(owner, args) {
+ super(owner, args);
+ middle = this;
+ }
+ @tracked value = null;
+ }
- unregisterOption(option) {
- this.get('options').removeObject(option);
+ this.owner.register(
+ 'component:x-middle',
+ setComponentTemplate(
+ precompileTemplate('{{this.value}}
{{yield}}'),
+ MiddleComponent
+ )
+ );
- this.updateValue();
+ this.owner.register(
+ 'component:x-inner',
+ setComponentTemplate(
+ precompileTemplate('{{@value}}
'),
+ class extends GlimmerComponent {
+ constructor(owner, args) {
+ super(owner, args);
+ middle.value = this.args.value;
}
}
)
);
- this.owner.register(
- 'component:x-option',
- class extends Component {
- tagName = 'option';
- attributeBindings = ['selected'];
-
- didInsertElement() {
- super.didInsertElement(...arguments);
+ let expectedBacktrackingMessage = backtrackingMessageFor('value', MiddleComponent.name, {
+ renderTree: ['x-outer', 'x-middle', 'this.value'],
+ });
- this.get('select').registerOption(this);
- }
+ expectAssertion(() => {
+ this.render('{{x-outer}}');
+ }, expectedBacktrackingMessage);
+ }
- @computed('select.value')
- get selected() {
- return this.get('value') === this.get('select.value');
+ ["@test when a shared dependency is changed during children's rendering"]() {
+ this.owner.register(
+ 'component:x-outer',
+ setComponentTemplate(
+ precompileTemplate(
+ '{{this.wrapper.content}}
{{x-inner value=this.value wrapper=this.wrapper}}'
+ ),
+ class extends GlimmerComponent {
+ value = 1;
+ wrapper = EmberObject.create({ content: null });
}
+ )
+ );
- willDestroyElement() {
- super.willDestroyElement(...arguments);
- this.get('select').unregisterOption(this);
+ this.owner.register(
+ 'component:x-inner',
+ setComponentTemplate(
+ precompileTemplate('{{@wrapper.content}}
'),
+ class extends GlimmerComponent {
+ constructor(owner, args) {
+ super(owner, args);
+ this.args.wrapper.set('content', this.args.value);
+ }
}
- }
+ )
);
- this.render(strip`
- {{#x-select value=this.value as |select|}}
- {{#x-option value="1" select=select}}1{{/x-option}}
- {{#x-option value="2" select=select}}2{{/x-option}}
- {{/x-select}}
- `);
-
- this.teardown();
+ let expectedBacktrackingMessage = backtrackingMessageFor('content', '<.+?>', {
+ renderTree: ['x-outer', 'this.wrapper.content'],
+ });
- this.assert.ok(true, 'no errors during teardown');
+ expectAssertion(() => {
+ this.render('{{x-outer}}');
+ }, expectedBacktrackingMessage);
}
- ['@test setting a property in willDestroyElement does not assert (GH#14273)'](assert) {
- assert.expect(2);
+ ["@test when a shared dependency is changed during children's rendering (tracked)"]() {
+ class Wrapper {
+ @tracked content = null;
+ }
this.owner.register(
- 'component:foo-bar',
+ 'component:x-outer',
setComponentTemplate(
- precompileTemplate(`{{#if this.showFoo}}things{{/if}}`),
- class extends Component {
- init() {
- super.init(...arguments);
- this.showFoo = true;
- }
+ precompileTemplate(
+ '{{this.wrapper.content}}
{{x-inner value=this.value wrapper=this.wrapper}}'
+ ),
+ class extends GlimmerComponent {
+ value = 1;
+ wrapper = new Wrapper();
+ }
+ )
+ );
- willDestroyElement() {
- this.set('showFoo', false);
- assert.ok(true, 'willDestroyElement was fired');
- super.willDestroyElement(...arguments);
+ this.owner.register(
+ 'component:x-inner',
+ setComponentTemplate(
+ precompileTemplate('{{@wrapper.content}}
'),
+ class extends GlimmerComponent {
+ constructor(owner, args) {
+ super(owner, args);
+ this.args.wrapper.content = this.args.value;
}
}
)
);
- this.render(`{{foo-bar}}`);
+ let expectedBacktrackingMessage = backtrackingMessageFor('content', Wrapper.name, {
+ renderTree: ['x-outer', 'this.wrapper.content'],
+ });
- this.assertText('things');
+ expectAssertion(() => {
+ this.render('{{x-outer}}');
+ }, expectedBacktrackingMessage);
}
- async ['@test didReceiveAttrs fires after .init() but before observers become active'](assert) {
- let barCopyDidChangeCount = 0;
+ ['@test non-block with each rendering child components']() {
+ this.owner.register(
+ 'component:non-block',
+ template(
+ strip`
+ In layout. {{#each @items as |item|}}
+ [{{child-non-block item=item}}]
+ {{/each}}
+ `,
+ { component: templateOnly(), strictMode: false }
+ )
+ );
this.owner.register(
- 'component:foo-bar',
- setComponentTemplate(
- precompileTemplate('{{this.bar}}-{{this.barCopy}}'),
- Component.extend({
- init() {
- this._super(...arguments);
- this.didInit = true;
- },
+ 'component:child-non-block',
+ setComponentTemplate(precompileTemplate('Child: {{@item}}.'), templateOnly())
+ );
- didReceiveAttrs() {
- assert.ok(this.didInit, 'expected init to have run before didReceiveAttrs');
- this.set('barCopy', this.attrs.bar.value + 1);
- },
+ let items = trackedArray(['Tom', 'Dick', 'Harry']);
- barCopyDidChange: observer('barCopy', () => {
- barCopyDidChangeCount++;
- }),
- })
- )
- );
+ this.render('{{non-block items=this.items}}', { items });
- await this.render(`{{foo-bar bar=this.bar}}`, { bar: 3 });
+ this.assertText('In layout. [Child: Tom.][Child: Dick.][Child: Harry.]');
+
+ runTask(() => this.rerender());
+
+ this.assertText('In layout. [Child: Tom.][Child: Dick.][Child: Harry.]');
- this.assertText('3-4');
+ runTask(() => this.context.get('items').push('Sergio'));
- assert.strictEqual(barCopyDidChangeCount, 1, 'expected observer firing for: barCopy');
+ this.assertText('In layout. [Child: Tom.][Child: Dick.][Child: Harry.][Child: Sergio.]');
- set(this.context, 'bar', 7);
+ runTask(() => this.context.get('items').shift());
- await runLoopSettled();
+ this.assertText('In layout. [Child: Dick.][Child: Harry.][Child: Sergio.]');
- this.assertText('7-8');
+ runTask(() => this.context.set('items', ['Tom', 'Dick', 'Harry']));
- assert.strictEqual(barCopyDidChangeCount, 2, 'expected observer firing for: barCopy');
+ this.assertText('In layout. [Child: Tom.][Child: Dick.][Child: Harry.]');
}
- ['@test overriding didReceiveAttrs does not trigger deprecation'](assert) {
- this.owner.register(
- 'component:foo-bar',
- setComponentTemplate(
- precompileTemplate('{{this.foo}}-{{this.fooCopy}}-{{this.bar}}-{{this.barCopy}}'),
- class extends Component {
- didReceiveAttrs() {
- assert.equal(1, this.get('foo'), 'expected attrs to have correct value');
- }
+ ['@test services can be injected into components']() {
+ let serviceInstance;
+ this.registerService(
+ 'name',
+ class extends Service {
+ init() {
+ super.init(...arguments);
+ serviceInstance = this;
}
- )
+ last = 'Jackson';
+ }
);
- this.render(`{{foo-bar foo=this.foo bar=this.bar}}`, { foo: 1, bar: 3 });
- }
-
- ['@test overriding didUpdateAttrs does not trigger deprecation'](assert) {
this.owner.register(
'component:foo-bar',
setComponentTemplate(
- precompileTemplate('{{this.foo}}-{{this.fooCopy}}-{{this.bar}}-{{this.barCopy}}'),
- class extends Component {
- didUpdateAttrs() {
- assert.equal(5, this.get('foo'), 'expected newAttrs to have new value');
- }
+ precompileTemplate('{{this.name.last}}'),
+ class extends GlimmerComponent {
+ @service
+ name;
}
)
);
- this.render(`{{foo-bar foo=this.foo bar=this.bar}}`, { foo: 1, bar: 3 });
+ this.render('{{foo-bar}}');
- runTask(() => set(this.context, 'foo', 5));
- }
+ this.assertText('Jackson');
- [`${testUnless(DEPRECATIONS.DEPRECATE_EVENTED.isRemoved)} @test triggering an event only attempts to invoke an identically named method, if it actually is a function (GH#15228)`](
- assert
- ) {
- assert.expect(5);
-
- let payload = ['arbitrary', 'event', 'data'];
-
- expectDeprecation(
- () => {
- this.owner.register(
- 'component:evented-component',
- Component.extend({
- someTruthyProperty: true,
-
- init() {
- this._super(...arguments);
- expectDeprecation(
- () => {
- this.trigger('someMethod', ...payload);
- this.trigger('someTruthyProperty', ...payload);
- },
- /Evented#trigger` is deprecated/,
- DEPRECATIONS.DEPRECATE_EVENTED.isEnabled
- );
- },
+ runTask(() => this.rerender());
- someMethod(...data) {
- assert.deepEqual(
- data,
- payload,
- 'the method `someMethod` should be called, when `someMethod` is triggered'
- );
- },
+ this.assertText('Jackson');
- listenerForSomeMethod: on('someMethod', function (...data) {
- assert.deepEqual(
- data,
- payload,
- 'the listener `listenerForSomeMethod` should be called, when `someMethod` is triggered'
- );
- }),
-
- listenerForSomeTruthyProperty: on('someTruthyProperty', function (...data) {
- assert.deepEqual(
- data,
- payload,
- 'the listener `listenerForSomeTruthyProperty` should be called, when `someTruthyProperty` is triggered'
- );
- }),
- })
- );
- },
- /`on\(\)` event decorator is deprecated/,
- DEPRECATIONS.DEPRECATE_EVENTED.isEnabled
- );
+ runTask(() => {
+ serviceInstance.set('last', 'McGuffey');
+ });
+
+ this.assertText('McGuffey');
+
+ runTask(() => {
+ serviceInstance.set('last', 'Jackson');
+ });
- this.render(`{{evented-component}}`);
+ this.assertText('Jackson');
}
['@test component yielding in an {{#each}} has correct block values after rerendering (GH#14284)']() {
@@ -3517,75 +1475,6 @@ moduleFor(
this.assertText('|foo||bar||qux||baz|');
}
- ['@test unimplimented positionalParams do not cause an error GH#14416']() {
- this.owner.register(
- 'component:foo-bar',
- setComponentTemplate(precompileTemplate('hello'), class extends Component {})
- );
-
- this.render('{{foo-bar this.wat}}');
- this.assertText('hello');
- }
-
- ['@test using attrs for positional params is asserted against']() {
- let MyComponent = class extends Component {};
-
- expectAssertion(() => {
- this.owner.register(
- 'component:foo-bar',
- template(
- 'MyVar1: {{attrs.myVar}} {{this.myVar}} MyVar2: {{this.myVar2}} {{attrs.myVar2}}',
- {
- component: MyComponent.reopenClass({ positionalParams: ['myVar'] }),
- strictMode: false,
- }
- )
- );
- }, 'Using {{attrs}} to reference named arguments is not supported. {{attrs.myVar}} should be updated to {{@myVar}}. (L1:C10) ');
- }
-
- // Perhaps change this test to `{{this.attrs.myVar.value}}` when removing the deprecation?
- ['@test using this.attrs for positional params']() {
- let MyComponent = class extends Component {};
-
- expectDeprecation(() => {
- this.owner.register(
- 'component:foo-bar',
- template(
- 'MyVar1: {{this.attrs.myVar}} {{this.myVar}} MyVar2: {{this.myVar2}} {{this.attrs.myVar2}}',
- {
- component: MyComponent.reopenClass({ positionalParams: ['myVar'] }),
- strictMode: false,
- }
- )
- );
- }, /Using {{this.attrs}} to reference named arguments has been deprecated. {{this.attrs.myVar2?}} should be updated to {{@myVar2?}}./);
-
- this.render('{{foo-bar 1 myVar2=2}}');
-
- this.assertText('MyVar1: 1 1 MyVar2: 2 2');
- }
-
- ['@test using named arguments for positional params']() {
- let MyComponent = class extends Component {};
-
- this.owner.register(
- 'component:foo-bar',
- setComponentTemplate(
- precompileTemplate(
- 'MyVar1: {{@myVar}} {{this.myVar}} MyVar2: {{this.myVar2}} {{@myVar2}}'
- ),
- MyComponent.reopenClass({
- positionalParams: ['myVar'],
- })
- )
- );
-
- this.render('{{foo-bar 1 myVar2=2}}');
-
- this.assertText('MyVar1: 1 1 MyVar2: 2 2');
- }
-
["@test can use `{{this}}` to emit the component's toString value [GH#14581]"]() {
this.owner.register(
'component:foo-bar',
@@ -3682,24 +1571,6 @@ moduleFor(
this.assertText('3');
}
- ['@test has attrs by didReceiveAttrs with native classes'](assert) {
- class FooBarComponent extends Component {
- constructor(injections) {
- super(injections);
- // analogous to class field defaults
- this.foo = 'bar';
- }
-
- didReceiveAttrs() {
- assert.equal(this.foo, 'bar', 'received default attrs correctly');
- }
- }
-
- this.owner.register('component:foo-bar', FooBarComponent);
-
- this.render('{{foo-bar}}');
- }
-
['@test ensure aliases are watched properly [GH#17243]']() {
let fooInstance, barInstance;
@@ -3805,122 +1676,127 @@ moduleFor(
this.assertHTML('bar');
}
- '@test lifecycle hooks are not tracked'() {
+ '@test `{{#let blah as |foo|}}` does not affect ` ` resolution'() {
+ this.owner.register(
+ 'component:foo',
+ setComponentTemplate(precompileTemplate('foo component'), templateOnly())
+ );
+
+ this.render('{{#let "foo block param" as |foo|}} {{/let}}');
+
+ this.assertText('foo component');
+
+ runTask(() => this.rerender());
+
+ this.assertText('foo component');
+ }
+
+ ['@test class is applied before modifiers are installed'](assert) {
+ assert.expect(1);
+
this.owner.register(
'component:foo-bar',
setComponentTemplate(
- precompileTemplate('{{this.baz}}'),
- class extends Component {
- @tracked foo;
+ precompileTemplate('
'),
+ class extends GlimmerComponent {
+ inserted = defineSimpleModifier((element) => {
+ assert.ok(element.classList.contains('foo-bar'), 'the class is on the element');
+ });
+ }
+ )
+ );
+
+ this.render('{{foo-bar class="foo-bar"}}');
+ }
+
+ ['@test child triggers revalidate during parent destruction (GH#13846)']() {
+ this.owner.register(
+ 'component:x-select',
+ setComponentTemplate(
+ precompileTemplate('{{yield this}} '),
+ class extends GlimmerComponent {
+ @tracked value = null;
- willInsertElement() {
- this.foo;
- this.foo = 123;
- }
+ options = [];
- willRender() {
- this.foo;
- this.foo = 123;
- }
+ updateValue() {
+ let last = this.options[this.options.length - 1];
- didRender() {
- this.foo;
- this.foo = 123;
+ this.value = last ? last.args.value : null;
}
- didReceiveAttrs() {
- this.foo;
- this.foo = 123;
+ registerOption(option) {
+ this.options.push(option);
}
- didUpdate() {
- this.foo;
- this.foo = 123;
- }
+ unregisterOption(option) {
+ this.options.splice(this.options.indexOf(option), 1);
- didUpdateAttrs() {
- this.foo;
- this.foo = 123;
+ this.updateValue();
}
+ }
+ )
+ );
- didInsertElement() {
- this.foo;
- this.foo = 123;
- }
+ this.owner.register(
+ 'component:x-option',
+ setComponentTemplate(
+ precompileTemplate('{{yield}} '),
+ class extends GlimmerComponent {
+ constructor(owner, args) {
+ super(owner, args);
- willClearRender() {
- this.foo;
- this.foo = 123;
+ this.args.select.registerOption(this);
}
- willDestroyElement() {
- this.foo;
- this.foo = 123;
+ get selected() {
+ return this.args.value === this.args.select.value;
}
- didDestroyElement() {
- this.foo;
- this.foo = 123;
+ willDestroy() {
+ super.willDestroy();
+
+ this.args.select.unregisterOption(this);
}
}
)
);
- this.render('{{#if this.cond}}{{foo-bar baz=this.value}}{{/if}}', {
- cond: true,
- value: 'hello',
- });
-
- this.assertComponentElement(this.firstChild, { content: 'hello' });
-
- runTask(() => set(this.context, 'value', 'world'));
+ this.render(strip`
+ {{#x-select as |select|}}
+ {{#x-option value="1" select=select}}1{{/x-option}}
+ {{#x-option value="2" select=select}}2{{/x-option}}
+ {{/x-select}}
+ `);
- this.assertComponentElement(this.firstChild, { content: 'world' });
+ this.teardown();
- runTask(() => set(this.context, 'cond', false));
+ this.assert.ok(true, 'no errors during teardown');
}
- '@test tracked property mutation in init does not error'() {
- // TODO: this should issue a deprecation, but since the curly manager
- // uses an untracked frame for construction we don't (yet)
+ ['@test setting a tracked property in willDestroy does not assert (GH#14273)'](assert) {
+ assert.expect(2);
+
this.owner.register(
'component:foo-bar',
setComponentTemplate(
- precompileTemplate(`{{this.itemCount}}`),
- class extends Component {
- @tracked itemCount = 0;
-
- init() {
- super.init(...arguments);
+ precompileTemplate(`{{#if this.showFoo}}things{{/if}}`),
+ class extends GlimmerComponent {
+ @tracked showFoo = true;
- // first read the tracked property
- let { itemCount } = this;
+ willDestroy() {
+ super.willDestroy();
- // then attempt to update the tracked property
- this.itemCount = itemCount + 1;
+ this.showFoo = false;
+ assert.ok(true, 'willDestroy was fired');
}
}
)
);
- this.render(' ');
-
- this.assertComponentElement(this.firstChild, { content: '1' });
- }
-
- '@test `{{#let blah as |foo|}}` does not affect ` ` resolution'() {
- this.owner.register(
- 'component:foo',
- setComponentTemplate(precompileTemplate('foo component'), templateOnly())
- );
-
- this.render('{{#let "foo block param" as |foo|}} {{/let}}');
-
- this.assertText('foo component');
-
- runTask(() => this.rerender());
+ this.render(`{{foo-bar}}`);
- this.assertText('foo component');
+ this.assertText('things');
}
}
);
diff --git a/packages/@ember/-internals/glimmer/tests/integration/components/dynamic-components-test.js b/packages/@ember/-internals/glimmer/tests/integration/components/dynamic-components-test.js
index 787403ff838..a69a87e8e56 100644
--- a/packages/@ember/-internals/glimmer/tests/integration/components/dynamic-components-test.js
+++ b/packages/@ember/-internals/glimmer/tests/integration/components/dynamic-components-test.js
@@ -7,10 +7,8 @@ import { precompileTemplate } from '@ember/template-compilation';
import { setComponentTemplate } from '@glimmer/manager';
import Component from '@glimmer/component';
-import { Component as EmberComponent } from '../../utils/helpers';
import PositionalComponent from '../../utils/positional-component';
import { backtrackingMessageFor } from '../../utils/debug-stack';
-import GlimmerishComponent from '../../utils/glimmerish-component';
moduleFor(
'Components test: dynamic components',
@@ -108,7 +106,7 @@ moduleFor(
precompileTemplate('{{component this.componentName}}', {
strictMode: true,
}),
- class extends GlimmerishComponent {
+ class extends Component {
componentName = { name: 'not-a-component' };
}
);
@@ -120,81 +118,6 @@ moduleFor(
}, /The `{{component}}` helper received an invalid value\. In strict mode, it expects a component definition\./);
}
- ['@test it has an element']() {
- let instance;
-
- let FooBarComponent = class extends EmberComponent {
- init() {
- super.init();
- instance = this;
- }
- };
-
- this.owner.register(
- 'component:foo-bar',
- setComponentTemplate(precompileTemplate('hello'), FooBarComponent)
- );
-
- this.render('{{component "foo-bar"}}');
-
- let element1 = instance.element;
-
- this.assertComponentElement(element1, { content: 'hello' });
-
- runTask(() => this.rerender());
-
- let element2 = instance.element;
-
- this.assertComponentElement(element2, { content: 'hello' });
-
- this.assertSameNode(element2, element1);
- }
-
- ['@test it has the right parentView and childViews'](assert) {
- let fooBarInstance, fooBarBazInstance;
-
- let FooBarComponent = class extends EmberComponent {
- init() {
- super.init();
- fooBarInstance = this;
- }
- };
-
- let FooBarBazComponent = class extends EmberComponent {
- init() {
- super.init();
- fooBarBazInstance = this;
- }
- };
-
- this.owner.register(
- 'component:foo-bar',
- setComponentTemplate(precompileTemplate('foo-bar {{foo-bar-baz}}'), FooBarComponent)
- );
- this.owner.register(
- 'component:foo-bar-baz',
- setComponentTemplate(precompileTemplate('foo-bar-baz'), FooBarBazComponent)
- );
-
- this.render('{{component "foo-bar"}}');
- this.assertText('foo-bar foo-bar-baz');
-
- assert.equal(fooBarInstance.parentView, this.component);
- assert.equal(fooBarBazInstance.parentView, fooBarInstance);
-
- assert.deepEqual(this.component.childViews, [fooBarInstance]);
- assert.deepEqual(fooBarInstance.childViews, [fooBarBazInstance]);
-
- runTask(() => this.rerender());
- this.assertText('foo-bar foo-bar-baz');
-
- assert.equal(fooBarInstance.parentView, this.component);
- assert.equal(fooBarBazInstance.parentView, fooBarInstance);
-
- assert.deepEqual(this.component.childViews, [fooBarInstance]);
- assert.deepEqual(fooBarInstance.childViews, [fooBarBazInstance]);
- }
-
['@test it can render a basic component with a block']() {
this.owner.register(
'component:foo-bar',
diff --git a/packages/@ember/-internals/glimmer/tests/integration/components/error-handling-test.js b/packages/@ember/-internals/glimmer/tests/integration/components/error-handling-test.js
index d16bac7f652..80811b35ec6 100644
--- a/packages/@ember/-internals/glimmer/tests/integration/components/error-handling-test.js
+++ b/packages/@ember/-internals/glimmer/tests/integration/components/error-handling-test.js
@@ -1,13 +1,12 @@
import { DEBUG } from '@glimmer/env';
-import { moduleFor, RenderingTestCase, runTask } from 'internal-test-helpers';
+import { moduleFor, RenderingTestCase, runTask, defineSimpleModifier } from 'internal-test-helpers';
import { set } from '@ember/object';
import { precompileTemplate } from '@ember/template-compilation';
import { setComponentTemplate } from '@glimmer/manager';
import Component from '@glimmer/component';
-import { Component as EmberComponent } from '../../utils/helpers';
moduleFor(
'Errors thrown during render',
@@ -109,22 +108,24 @@ moduleFor(
}
}
- ['@test it can recover resets the transaction when an error is thrown during didInsertElement'](
+ ['@test it can recover resets the transaction when an error is thrown during modifier installation'](
assert
) {
let shouldThrow = true;
- let FooBarComponent = class extends EmberComponent {
- didInsertElement() {
- super.didInsertElement(...arguments);
+ let FooBarComponent = class extends Component {
+ inserted = defineSimpleModifier(() => {
if (shouldThrow) {
throw new Error('silly mistake!');
}
- }
+ });
};
this.owner.register(
'component:foo-bar',
- setComponentTemplate(precompileTemplate('hello'), FooBarComponent)
+ setComponentTemplate(
+ precompileTemplate('hello
'),
+ FooBarComponent
+ )
);
assert.throws(() => {
@@ -148,9 +149,9 @@ moduleFor(
['@test it can recover resets the transaction when an error is thrown during destroy'](assert) {
let shouldThrow = true;
- let FooBarComponent = class extends EmberComponent {
- destroy() {
- super.destroy(...arguments);
+ let FooBarComponent = class extends Component {
+ willDestroy() {
+ super.willDestroy(...arguments);
if (shouldThrow) {
throw new Error('silly mistake!');
}
diff --git a/packages/@ember/-internals/glimmer/tests/integration/components/glimmer-component-test.gjs b/packages/@ember/-internals/glimmer/tests/integration/components/glimmer-component-test.gjs
index ce561ae678a..383a6f1bb78 100644
--- a/packages/@ember/-internals/glimmer/tests/integration/components/glimmer-component-test.gjs
+++ b/packages/@ember/-internals/glimmer/tests/integration/components/glimmer-component-test.gjs
@@ -1,4 +1,6 @@
-import { moduleFor, RenderingTestCase, runTask } from 'internal-test-helpers';
+import { module, test } from 'qunit';
+import { setupRenderingTest } from 'ember-qunit';
+import { render, settled } from '@ember/test-helpers';
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
@@ -11,126 +13,128 @@ class Label {
@tracked text = 'first';
}
-moduleFor(
- 'Components test: @glimmer/component',
- class extends RenderingTestCase {
- '@test isDestroying and isDestroyed transition as the component is torn down'(assert) {
- let toggle = new Toggle();
- let instance;
-
- class Subject extends Component {
- constructor() {
- super(...arguments);
- instance = this;
- assert.step(
- `constructor: isDestroying=${this.isDestroying} isDestroyed=${this.isDestroyed}`
- );
- }
-
- willDestroy() {
- super.willDestroy();
- assert.step(
- `willDestroy: isDestroying=${this.isDestroying} isDestroyed=${this.isDestroyed}`
- );
- }
-
- subject
+module('Components test: @glimmer/component', function (hooks) {
+ setupRenderingTest(hooks);
+
+ test('isDestroying and isDestroyed transition as the component is torn down', async function (assert) {
+ let toggle = new Toggle();
+ let instance;
+
+ class Subject extends Component {
+ constructor() {
+ super(...arguments);
+ instance = this;
+ assert.step(
+ `constructor: isDestroying=${this.isDestroying} isDestroyed=${this.isDestroyed}`
+ );
}
- const Root = {{#if toggle.isShowing}} {{/if}} ;
+ willDestroy() {
+ super.willDestroy();
+ assert.step(
+ `willDestroy: isDestroying=${this.isDestroying} isDestroyed=${this.isDestroyed}`
+ );
+ }
- this.renderComponent(Root, { expect: 'subject' });
+ subject
+ }
- assert.verifySteps(['constructor: isDestroying=false isDestroyed=false']);
- assert.false(instance.isDestroying, 'isDestroying is false while rendered');
- assert.false(instance.isDestroyed, 'isDestroyed is false while rendered');
+ await render({{#if toggle.isShowing}} {{/if}} );
- runTask(() => (toggle.isShowing = false));
+ await assert.stableRender('subject');
+ assert.verifySteps(['constructor: isDestroying=false isDestroyed=false']);
+ assert.false(instance.isDestroying, 'isDestroying is false while rendered');
+ assert.false(instance.isDestroyed, 'isDestroyed is false while rendered');
- this.assertText('');
- assert.verifySteps(['willDestroy: isDestroying=true isDestroyed=false']);
- assert.true(instance.isDestroying, 'isDestroying is true after teardown');
- assert.true(instance.isDestroyed, 'isDestroyed is true after teardown');
- }
+ toggle.isShowing = false;
+ await settled();
- '@test tracked state and args are reactive without re-constructing the component'(assert) {
- let label = new Label();
- let instance;
+ assert.dom().hasNoText();
+ assert.verifySteps(['willDestroy: isDestroying=true isDestroyed=false']);
+ assert.true(instance.isDestroying, 'isDestroying is true after teardown');
+ assert.true(instance.isDestroyed, 'isDestroyed is true after teardown');
+ });
- class Subject extends Component {
- @tracked count = 0;
+ test('tracked state and args are reactive without re-constructing the component', async function (assert) {
+ let label = new Label();
+ let instance;
- constructor() {
- super(...arguments);
- instance = this;
- assert.step('constructor');
- }
+ class Subject extends Component {
+ @tracked count = 0;
- {{@text}}-{{this.count}}
+ constructor() {
+ super(...arguments);
+ instance = this;
+ assert.step('constructor');
}
- const Root = ;
-
- this.renderComponent(Root, { expect: 'first-0' });
+ {{@text}}-{{this.count}}
+ }
- assert.verifySteps(['constructor']);
+ await render( );
- runTask(() => instance.count++);
+ await assert.stableRender('first-0');
+ assert.verifySteps(['constructor']);
- this.assertText('first-1');
+ instance.count++;
+ await settled();
- runTask(() => (label.text = 'second'));
+ assert.dom().hasText('first-1');
- this.assertText('second-1');
- assert.verifySteps([], 'the component updated in place rather than being re-constructed');
- }
+ label.text = 'second';
+ await settled();
- '@test re-mounting constructs a new instance and leaves the old one destroyed'(assert) {
- let toggle = new Toggle();
- let instances = [];
+ assert.dom().hasText('second-1');
+ assert.verifySteps([], 'the component updated in place rather than being re-constructed');
+ });
- class Subject extends Component {
- constructor() {
- super(...arguments);
- instances.push(this);
- assert.step(`constructor: ${instances.length}`);
- }
+ test('re-mounting constructs a new instance and leaves the old one destroyed', async function (assert) {
+ let toggle = new Toggle();
+ let instances = [];
- willDestroy() {
- super.willDestroy();
- assert.step(`willDestroy: ${instances.indexOf(this) + 1}`);
- }
+ class Subject extends Component {
+ constructor() {
+ super(...arguments);
+ instances.push(this);
+ assert.step(`constructor: ${instances.length}`);
+ }
- subject
+ willDestroy() {
+ super.willDestroy();
+ assert.step(`willDestroy: ${instances.indexOf(this) + 1}`);
}
- const Root = {{#if toggle.isShowing}} {{/if}} ;
+ subject
+ }
- this.renderComponent(Root, { expect: 'subject' });
+ await render({{#if toggle.isShowing}} {{/if}} );
- assert.verifySteps(['constructor: 1']);
+ await assert.stableRender('subject');
+ assert.verifySteps(['constructor: 1']);
- runTask(() => (toggle.isShowing = false));
+ toggle.isShowing = false;
+ await settled();
- this.assertText('');
- assert.verifySteps(['willDestroy: 1']);
+ assert.dom().hasNoText();
+ assert.verifySteps(['willDestroy: 1']);
- runTask(() => (toggle.isShowing = true));
+ toggle.isShowing = true;
+ await settled();
- this.assertText('subject');
- assert.verifySteps(['constructor: 2']);
+ assert.dom().hasText('subject');
+ assert.verifySteps(['constructor: 2']);
- assert.strictEqual(instances.length, 2, 'a second instance was constructed');
- assert.notStrictEqual(instances[0], instances[1], 'the instances are distinct');
- assert.true(instances[0].isDestroyed, 'the first instance stayed destroyed');
- assert.false(instances[1].isDestroying, 'the second instance is not destroying');
- assert.false(instances[1].isDestroyed, 'the second instance is not destroyed');
+ assert.strictEqual(instances.length, 2, 'a second instance was constructed');
+ assert.notStrictEqual(instances[0], instances[1], 'the instances are distinct');
+ assert.true(instances[0].isDestroyed, 'the first instance stayed destroyed');
+ assert.false(instances[1].isDestroying, 'the second instance is not destroying');
+ assert.false(instances[1].isDestroyed, 'the second instance is not destroyed');
- runTask(() => (toggle.isShowing = false));
+ toggle.isShowing = false;
+ await settled();
- this.assertText('');
- assert.verifySteps(['willDestroy: 2']);
- assert.true(instances[1].isDestroyed, 'the second instance was destroyed in turn');
- }
- }
-);
+ assert.dom().hasNoText();
+ assert.verifySteps(['willDestroy: 2']);
+ assert.true(instances[1].isDestroyed, 'the second instance was destroyed in turn');
+ });
+});
diff --git a/packages/@ember/-internals/glimmer/tests/integration/components/life-cycle-test.js b/packages/@ember/-internals/glimmer/tests/integration/components/life-cycle-test.js
index b989a47b24a..891e26d4a66 100644
--- a/packages/@ember/-internals/glimmer/tests/integration/components/life-cycle-test.js
+++ b/packages/@ember/-internals/glimmer/tests/integration/components/life-cycle-test.js
@@ -1,1369 +1,33 @@
-import { classes, moduleFor, RenderingTestCase, runTask, strip } from 'internal-test-helpers';
+import { moduleFor, RenderingTestCase, runTask, defineSimpleModifier } from 'internal-test-helpers';
import { schedule } from '@ember/runloop';
-import { set, setProperties } from '@ember/object';
-import { A as emberA } from '@ember/array';
-import { getViewElement, getViewId } from '@ember/-internals/views';
+import { set } from '@ember/object';
+import { tracked } from '@glimmer/tracking';
import { precompileTemplate } from '@ember/template-compilation';
import { setComponentTemplate } from '@glimmer/manager';
-import { Component } from '../../utils/helpers';
-import { addListener } from '@ember/-internals/metal';
-
-class LifeCycleHooksTest extends RenderingTestCase {
- constructor() {
- super(...arguments);
- this.hooks = [];
- this.components = {};
- this.componentRegistry = [];
- this.teardownAssertions = [];
- this.viewRegistry = this.owner.lookup('-view-registry:main');
- }
-
- afterEach() {
- super.afterEach();
-
- for (let i = 0; i < this.teardownAssertions.length; i++) {
- this.teardownAssertions[i]();
- }
- }
-
- get isInteractive() {
- return true;
- }
-
- getBootOptions() {
- return {
- isInteractive: this.isInteractive,
- };
- }
-
- /* 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),
- };
- }
-
- assertRegisteredViews(label) {
- let viewRegistry = this.viewRegistry;
- let topLevelId = getViewId(this.component);
- let actual = Object.keys(viewRegistry)
- .sort()
- .filter((id) => id !== topLevelId);
-
- if (this.isInteractive) {
- let expected = this.componentRegistry.sort();
-
- this.assert.deepEqual(actual, expected, 'registered views - ' + label);
- } else {
- this.assert.deepEqual(actual, [], 'no views should be registered for non-interactive mode');
- }
- }
-
- registerComponent(name, { template = null }) {
- let pushComponent = (instance) => {
- this.components[name] = instance;
- this.componentRegistry.push(getViewId(instance));
- };
-
- let removeComponent = (instance) => {
- let index = this.componentRegistry.indexOf(getViewId(instance));
- this.componentRegistry.splice(index, 1);
-
- delete this.components[name];
- };
-
- let pushHook = (hookName, args) => {
- this.hooks.push(hook(name, hookName, args));
- };
-
- let assertParentView = (hookName, instance) => {
- this.assert.ok(instance.parentView, `parentView should be present in ${hookName}`);
-
- if (hookName === 'willDestroyElement') {
- this.assert.ok(
- instance.parentView.childViews.indexOf(instance) !== -1,
- `view is still connected to parentView in ${hookName}`
- );
- }
- };
-
- let assertElement = (hookName, instance, inDOM = true) => {
- if (instance.tagName === '') {
- return;
- }
-
- this.assert.ok(
- getViewElement(instance),
- `element should be present on ${instance} during ${hookName}`
- );
-
- if (this.isInteractive) {
- this.assert.ok(
- instance.element,
- `this.element should be present on ${instance} during ${hookName}`
- );
- this.assert.equal(
- document.body.contains(instance.element),
- inDOM,
- `element for ${instance} ${
- inDOM ? 'should' : 'should not'
- } be in the DOM during ${hookName}`
- );
- } else {
- this.assert.throws(
- () => instance.element,
- /Accessing `this.element` is not allowed in non-interactive environments/
- );
- }
- };
-
- let assertNoElement = (hookName, instance) => {
- this.assert.strictEqual(
- getViewElement(instance),
- null,
- `element should not be present in ${hookName}`
- );
-
- if (this.isInteractive) {
- this.assert.strictEqual(
- instance.element,
- null,
- `this.element should not be present in ${hookName}`
- );
- } else {
- this.assert.throws(
- () => instance.element,
- /Accessing `this.element` is not allowed in non-interactive environments/
- );
- }
- };
-
- let assertState = (hookName, expectedState, instance) => {
- this.assert.equal(
- instance._state,
- expectedState,
- `within ${hookName} the expected _state is ${expectedState}`
- );
- };
-
- let { isInteractive } = this;
-
- let ComponentClass = class extends this.ComponentClass {
- init() {
- super.init(...arguments);
-
- this.isInitialRender = true;
- this.componentName = name;
- pushHook('init');
- pushComponent(this);
- assertParentView('init', this);
- assertNoElement('init', this);
- assertState('init', 'preRender', this);
-
- addListener(this, 'init', () => pushHook('on(init)'));
-
- schedule('afterRender', () => {
- this.isInitialRender = false;
- });
- }
-
- didReceiveAttrs(options) {
- pushHook('didReceiveAttrs', options);
- assertParentView('didReceiveAttrs', this);
-
- if (this.isInitialRender) {
- assertNoElement('didReceiveAttrs', this);
- assertState('didReceiveAttrs', 'preRender', this);
- } else {
- assertElement('didReceiveAttrs', this);
-
- if (isInteractive) {
- assertState('didReceiveAttrs', 'inDOM', this);
- } else {
- assertState('didReceiveAttrs', 'hasElement', this);
- }
- }
- }
-
- willInsertElement() {
- pushHook('willInsertElement');
- assertParentView('willInsertElement', this);
- assertElement('willInsertElement', this, false);
- assertState('willInsertElement', 'hasElement', this);
- }
-
- willRender() {
- pushHook('willRender');
- assertParentView('willRender', this);
-
- if (this.isInitialRender) {
- assertNoElement('willRender', this, false);
- assertState('willRender', 'preRender', this);
- } else {
- assertElement('willRender', this);
- assertState('willRender', 'inDOM', this);
- }
- }
-
- didInsertElement() {
- pushHook('didInsertElement');
- assertParentView('didInsertElement', this);
- assertElement('didInsertElement', this);
- assertState('didInsertElement', 'inDOM', this);
- }
-
- didRender() {
- pushHook('didRender');
- assertParentView('didRender', this);
- assertElement('didRender', this);
- assertState('didRender', 'inDOM', this);
- }
-
- didUpdateAttrs(options) {
- pushHook('didUpdateAttrs', options);
- assertParentView('didUpdateAttrs', this);
-
- if (isInteractive) {
- assertState('didUpdateAttrs', 'inDOM', this);
- } else {
- assertState('didUpdateAttrs', 'hasElement', this);
- }
- }
-
- willUpdate(options) {
- pushHook('willUpdate', options);
- assertParentView('willUpdate', this);
- assertElement('willUpdate', this);
- assertState('willUpdate', 'inDOM', this);
- }
-
- didUpdate(options) {
- pushHook('didUpdate', options);
- assertParentView('didUpdate', this);
- assertElement('didUpdate', this);
- assertState('didUpdate', 'inDOM', this);
- }
-
- willDestroyElement() {
- pushHook('willDestroyElement');
- assertParentView('willDestroyElement', this);
- assertElement('willDestroyElement', this);
- assertState('willDestroyElement', 'inDOM', this);
- }
-
- willClearRender() {
- pushHook('willClearRender');
- assertParentView('willClearRender', this);
- assertElement('willClearRender', this);
- assertState('willClearRender', 'inDOM', this);
- }
-
- didDestroyElement() {
- pushHook('didDestroyElement');
- assertNoElement('didDestroyElement', this);
- assertState('didDestroyElement', 'destroying', this);
- }
-
- willDestroy() {
- pushHook('willDestroy');
- removeComponent(this);
-
- this._super(...arguments);
- }
- };
-
- if (typeof template === 'string') {
- setComponentTemplate(this.compile(template), ComponentClass);
- }
- this.owner.register(`component:${name}`, ComponentClass);
- }
-
- assertHooks({ label, interactive, nonInteractive }) {
- let rawHooks = this.isInteractive ? interactive : nonInteractive;
- 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(attr('twitter')) }), {
- twitter: '@tomdale',
- });
-
- this.assertText('Twitter: @tomdale|Name: Tom Dale|Website: tomdale.net');
- this.assertRegisteredViews('intial render');
-
- this.assertHooks({
- label: 'after initial render',
-
- interactive: [
- // Sync hooks
-
- ['the-top', 'init'],
- ['the-top', 'on(init)'],
- ['the-top', 'didReceiveAttrs'],
- ['the-top', 'willRender'],
- ['the-top', 'willInsertElement'],
-
- ['the-middle', 'init'],
- ['the-middle', 'on(init)'],
- ['the-middle', 'didReceiveAttrs'],
- ['the-middle', 'willRender'],
- ['the-middle', 'willInsertElement'],
-
- ['the-bottom', 'init'],
- ['the-bottom', 'on(init)'],
- ['the-bottom', 'didReceiveAttrs'],
- ['the-bottom', 'willRender'],
- ['the-bottom', 'willInsertElement'],
-
- // Async hooks
-
- ['the-bottom', 'didInsertElement'],
- ['the-bottom', 'didRender'],
-
- ['the-middle', 'didInsertElement'],
- ['the-middle', 'didRender'],
-
- ['the-top', 'didInsertElement'],
- ['the-top', 'didRender'],
- ],
-
- nonInteractive: [
- // Sync hooks
- ['the-top', 'init'],
- ['the-top', 'on(init)'],
- ['the-top', 'didReceiveAttrs'],
-
- ['the-middle', 'init'],
- ['the-middle', 'on(init)'],
- ['the-middle', 'didReceiveAttrs'],
-
- ['the-bottom', 'init'],
- ['the-bottom', 'on(init)'],
- ['the-bottom', 'didReceiveAttrs'],
- ],
- });
-
- runTask(() => this.components['the-bottom'].rerender());
-
- this.assertText('Twitter: @tomdale|Name: Tom Dale|Website: tomdale.net');
-
- this.assertHooks({
- label: 'after no-op rerender (bottom)',
-
- interactive: [
- // Sync hooks
- ['the-top', 'willUpdate'],
- ['the-top', 'willRender'],
-
- ['the-middle', 'willUpdate'],
- ['the-middle', 'willRender'],
-
- ['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'],
- ],
-
- nonInteractive: [],
- });
-
- runTask(() => this.components['the-middle'].rerender());
-
- this.assertText('Twitter: @tomdale|Name: Tom Dale|Website: tomdale.net');
-
- this.assertHooks({
- label: 'after no-op rerender (middle)',
-
- interactive: [
- // Sync hooks
-
- ['the-top', 'willUpdate'],
- ['the-top', 'willRender'],
-
- ['the-middle', 'willUpdate'],
- ['the-middle', 'willRender'],
-
- // Async hooks
-
- ['the-middle', 'didUpdate'],
- ['the-middle', 'didRender'],
-
- ['the-top', 'didUpdate'],
- ['the-top', 'didRender'],
- ],
-
- nonInteractive: [],
- });
-
- runTask(() => this.components['the-top'].rerender());
-
- this.assertText('Twitter: @tomdale|Name: Tom Dale|Website: tomdale.net');
-
- this.assertHooks({
- label: 'after no-op rerender (top)',
-
- interactive: [
- // Sync hooks
-
- ['the-top', 'willUpdate'],
- ['the-top', 'willRender'],
-
- // Async hooks
-
- ['the-top', 'didUpdate'],
- ['the-top', 'didRender'],
- ],
-
- nonInteractive: [],
- });
-
- 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.
-
- this.assertHooks({
- label: 'after update',
-
- interactive: [
- // Sync hooks
-
- ['the-top', 'didUpdateAttrs'],
- ['the-top', 'didReceiveAttrs'],
-
- ['the-top', 'willUpdate'],
- ['the-top', 'willRender'],
-
- // Async hooks
-
- ['the-top', 'didUpdate'],
- ['the-top', 'didRender'],
- ],
-
- nonInteractive: [
- // Sync hooks
- ['the-top', 'didUpdateAttrs'],
- ['the-top', 'didReceiveAttrs'],
- ],
- });
-
- this.teardownAssertions.push(() => {
- this.assertHooks({
- label: 'destroy',
-
- interactive: [
- ['the-top', 'willDestroyElement'],
- ['the-top', 'willClearRender'],
- ['the-middle', 'willDestroyElement'],
- ['the-middle', 'willClearRender'],
- ['the-bottom', 'willDestroyElement'],
- ['the-bottom', 'willClearRender'],
- ['the-top', 'didDestroyElement'],
- ['the-middle', 'didDestroyElement'],
- ['the-bottom', 'didDestroyElement'],
- ['the-top', 'willDestroy'],
- ['the-middle', 'willDestroy'],
- ['the-bottom', 'willDestroy'],
- ],
-
- nonInteractive: [
- ['the-top', 'willDestroy'],
- ['the-middle', 'willDestroy'],
- ['the-bottom', 'willDestroy'],
- ],
- });
-
- this.assertRegisteredViews('after destroy');
- });
- }
-
- ['@test lifecycle hooks are invoked in a correct sibling order']() {
- let { attr, invoke } = this.boundHelpers;
-
- this.registerComponent('the-parent', {
- template: strip`
-
- ${invoke('the-first-child', { twitter: expr(attr('twitter')) })}|
- ${invoke('the-second-child', { name: expr(attr('name')) })}|
- ${invoke('the-last-child', { website: expr(attr('website')) })}
-
`,
- });
-
- this.registerComponent('the-first-child', {
- template: `Twitter: {{${attr('twitter')}}}`,
- });
-
- this.registerComponent('the-second-child', {
- template: `Name: {{${attr('name')}}}`,
- });
-
- this.registerComponent('the-last-child', {
- template: `Website: {{${attr('website')}}}`,
- });
-
- this.render(
- invoke('the-parent', {
- twitter: expr(attr('twitter')),
- name: expr(attr('name')),
- website: expr(attr('website')),
- }),
- {
- twitter: '@tomdale',
- name: 'Tom Dale',
- website: 'tomdale.net',
- }
- );
-
- this.assertText('Twitter: @tomdale|Name: Tom Dale|Website: tomdale.net');
- this.assertRegisteredViews('intial render');
-
- this.assertHooks({
- label: 'after initial render',
-
- interactive: [
- // Sync hooks
-
- ['the-parent', 'init'],
- ['the-parent', 'on(init)'],
- ['the-parent', 'didReceiveAttrs'],
- ['the-parent', 'willRender'],
- ['the-parent', 'willInsertElement'],
-
- ['the-first-child', 'init'],
- ['the-first-child', 'on(init)'],
- ['the-first-child', 'didReceiveAttrs'],
- ['the-first-child', 'willRender'],
- ['the-first-child', 'willInsertElement'],
-
- ['the-second-child', 'init'],
- ['the-second-child', 'on(init)'],
- ['the-second-child', 'didReceiveAttrs'],
- ['the-second-child', 'willRender'],
- ['the-second-child', 'willInsertElement'],
-
- ['the-last-child', 'init'],
- ['the-last-child', 'on(init)'],
- ['the-last-child', 'didReceiveAttrs'],
- ['the-last-child', 'willRender'],
- ['the-last-child', 'willInsertElement'],
-
- // Async hooks
-
- ['the-first-child', 'didInsertElement'],
- ['the-first-child', 'didRender'],
-
- ['the-second-child', 'didInsertElement'],
- ['the-second-child', 'didRender'],
-
- ['the-last-child', 'didInsertElement'],
- ['the-last-child', 'didRender'],
-
- ['the-parent', 'didInsertElement'],
- ['the-parent', 'didRender'],
- ],
-
- nonInteractive: [
- // Sync hooks
-
- ['the-parent', 'init'],
- ['the-parent', 'on(init)'],
- ['the-parent', 'didReceiveAttrs'],
-
- ['the-first-child', 'init'],
- ['the-first-child', 'on(init)'],
- ['the-first-child', 'didReceiveAttrs'],
-
- ['the-second-child', 'init'],
- ['the-second-child', 'on(init)'],
- ['the-second-child', 'didReceiveAttrs'],
-
- ['the-last-child', 'init'],
- ['the-last-child', 'on(init)'],
- ['the-last-child', 'didReceiveAttrs'],
- ],
- });
-
- runTask(() => this.components['the-first-child'].rerender());
-
- this.assertText('Twitter: @tomdale|Name: Tom Dale|Website: tomdale.net');
-
- this.assertHooks({
- label: 'after no-op rerender (first child)',
-
- interactive: [
- // Sync hooks
-
- ['the-parent', 'willUpdate'],
- ['the-parent', 'willRender'],
-
- ['the-first-child', 'willUpdate'],
- ['the-first-child', 'willRender'],
-
- // Async hooks
-
- ['the-first-child', 'didUpdate'],
- ['the-first-child', 'didRender'],
-
- ['the-parent', 'didUpdate'],
- ['the-parent', 'didRender'],
- ],
-
- nonInteractive: [],
- });
-
- runTask(() => this.components['the-second-child'].rerender());
-
- this.assertText('Twitter: @tomdale|Name: Tom Dale|Website: tomdale.net');
-
- this.assertHooks({
- label: 'after no-op rerender (second child)',
-
- interactive: [
- // Sync hooks
-
- ['the-parent', 'willUpdate'],
- ['the-parent', 'willRender'],
-
- ['the-second-child', 'willUpdate'],
- ['the-second-child', 'willRender'],
-
- // Async hooks
-
- ['the-second-child', 'didUpdate'],
- ['the-second-child', 'didRender'],
-
- ['the-parent', 'didUpdate'],
- ['the-parent', 'didRender'],
- ],
-
- nonInteractive: [],
- });
-
- runTask(() => this.components['the-last-child'].rerender());
-
- this.assertText('Twitter: @tomdale|Name: Tom Dale|Website: tomdale.net');
-
- this.assertHooks({
- label: 'after no-op rerender (last child)',
-
- interactive: [
- // Sync hooks
-
- ['the-parent', 'willUpdate'],
- ['the-parent', 'willRender'],
-
- ['the-last-child', 'willUpdate'],
- ['the-last-child', 'willRender'],
-
- // Async hooks
-
- ['the-last-child', 'didUpdate'],
- ['the-last-child', 'didRender'],
-
- ['the-parent', 'didUpdate'],
- ['the-parent', 'didRender'],
- ],
-
- nonInteractive: [],
- });
-
- runTask(() => this.components['the-parent'].rerender());
-
- this.assertText('Twitter: @tomdale|Name: Tom Dale|Website: tomdale.net');
-
- this.assertHooks({
- label: 'after no-op rerender (parent)',
-
- interactive: [
- // Sync hooks
-
- ['the-parent', 'willUpdate'],
- ['the-parent', 'willRender'],
-
- // Async hooks
-
- ['the-parent', 'didUpdate'],
- ['the-parent', 'didRender'],
- ],
-
- nonInteractive: [],
- });
-
- runTask(() =>
- setProperties(this.context, {
- twitter: '@horsetomdale',
- name: 'Horse Tom Dale',
- website: 'horsetomdale.net',
- })
- );
-
- this.assertText('Twitter: @horsetomdale|Name: Horse Tom Dale|Website: horsetomdale.net');
-
- this.assertHooks({
- label: 'after update',
-
- interactive: [
- // Sync hooks
-
- ['the-parent', 'didUpdateAttrs'],
- ['the-parent', 'didReceiveAttrs'],
-
- ['the-parent', 'willUpdate'],
- ['the-parent', 'willRender'],
-
- ['the-first-child', 'didUpdateAttrs'],
- ['the-first-child', 'didReceiveAttrs'],
-
- ['the-first-child', 'willUpdate'],
- ['the-first-child', 'willRender'],
-
- ['the-second-child', 'didUpdateAttrs'],
- ['the-second-child', 'didReceiveAttrs'],
-
- ['the-second-child', 'willUpdate'],
- ['the-second-child', 'willRender'],
-
- ['the-last-child', 'didUpdateAttrs'],
- ['the-last-child', 'didReceiveAttrs'],
-
- ['the-last-child', 'willUpdate'],
- ['the-last-child', 'willRender'],
-
- // Async hooks
-
- ['the-first-child', 'didUpdate'],
- ['the-first-child', 'didRender'],
-
- ['the-second-child', 'didUpdate'],
- ['the-second-child', 'didRender'],
-
- ['the-last-child', 'didUpdate'],
- ['the-last-child', 'didRender'],
-
- ['the-parent', 'didUpdate'],
- ['the-parent', 'didRender'],
- ],
-
- nonInteractive: [
- // Sync hooks
-
- ['the-parent', 'didUpdateAttrs'],
- ['the-parent', 'didReceiveAttrs'],
-
- ['the-first-child', 'didUpdateAttrs'],
- ['the-first-child', 'didReceiveAttrs'],
-
- ['the-second-child', 'didUpdateAttrs'],
- ['the-second-child', 'didReceiveAttrs'],
-
- ['the-last-child', 'didUpdateAttrs'],
- ['the-last-child', 'didReceiveAttrs'],
- ],
- });
-
- this.teardownAssertions.push(() => {
- this.assertHooks({
- label: 'destroy',
-
- interactive: [
- ['the-parent', 'willDestroyElement'],
- ['the-parent', 'willClearRender'],
- ['the-first-child', 'willDestroyElement'],
- ['the-first-child', 'willClearRender'],
- ['the-second-child', 'willDestroyElement'],
- ['the-second-child', 'willClearRender'],
- ['the-last-child', 'willDestroyElement'],
- ['the-last-child', 'willClearRender'],
-
- ['the-parent', 'didDestroyElement'],
- ['the-first-child', 'didDestroyElement'],
- ['the-second-child', 'didDestroyElement'],
- ['the-last-child', 'didDestroyElement'],
-
- ['the-parent', 'willDestroy'],
- ['the-first-child', 'willDestroy'],
- ['the-second-child', 'willDestroy'],
- ['the-last-child', 'willDestroy'],
- ],
-
- nonInteractive: [
- ['the-parent', 'willDestroy'],
- ['the-first-child', 'willDestroy'],
- ['the-second-child', 'willDestroy'],
- ['the-last-child', 'willDestroy'],
- ],
- });
-
- this.assertRegisteredViews('after destroy');
- });
- }
-
- ['@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(attr('twitter')) }), {
- twitter: '@tomdale',
- });
-
- this.assertText('Top: Middle: Bottom: @tomdale');
- this.assertRegisteredViews('intial render');
-
- this.assertHooks({
- label: 'after initial render',
-
- interactive: [
- // Sync hooks
-
- ['the-top', 'init'],
- ['the-top', 'on(init)'],
- ['the-top', 'didReceiveAttrs'],
- ['the-top', 'willRender'],
- ['the-top', 'willInsertElement'],
-
- ['the-middle', 'init'],
- ['the-middle', 'on(init)'],
- ['the-middle', 'didReceiveAttrs'],
- ['the-middle', 'willRender'],
- ['the-middle', 'willInsertElement'],
-
- ['the-bottom', 'init'],
- ['the-bottom', 'on(init)'],
- ['the-bottom', 'didReceiveAttrs'],
- ['the-bottom', 'willRender'],
- ['the-bottom', 'willInsertElement'],
-
- // Async hooks
-
- ['the-bottom', 'didInsertElement'],
- ['the-bottom', 'didRender'],
-
- ['the-middle', 'didInsertElement'],
- ['the-middle', 'didRender'],
-
- ['the-top', 'didInsertElement'],
- ['the-top', 'didRender'],
- ],
-
- nonInteractive: [
- // Sync hooks
-
- ['the-top', 'init'],
- ['the-top', 'on(init)'],
- ['the-top', 'didReceiveAttrs'],
-
- ['the-middle', 'init'],
- ['the-middle', 'on(init)'],
- ['the-middle', 'didReceiveAttrs'],
-
- ['the-bottom', 'init'],
- ['the-bottom', 'on(init)'],
- ['the-bottom', 'didReceiveAttrs'],
- ],
- });
-
- 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.
-
- this.assertHooks({
- label: 'after updating (root)',
-
- interactive: [
- // Sync hooks
-
- ['the-top', 'didUpdateAttrs'],
- ['the-top', 'didReceiveAttrs'],
-
- ['the-top', 'willUpdate'],
- ['the-top', 'willRender'],
-
- ['the-middle', 'didUpdateAttrs'],
- ['the-middle', 'didReceiveAttrs'],
-
- ['the-middle', 'willUpdate'],
- ['the-middle', 'willRender'],
-
- ['the-bottom', 'didUpdateAttrs'],
- ['the-bottom', 'didReceiveAttrs'],
-
- ['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'],
- ],
-
- nonInteractive: [
- // Sync hooks
-
- ['the-top', 'didUpdateAttrs'],
- ['the-top', 'didReceiveAttrs'],
-
- ['the-middle', 'didUpdateAttrs'],
- ['the-middle', 'didReceiveAttrs'],
-
- ['the-bottom', 'didUpdateAttrs'],
- ['the-bottom', 'didReceiveAttrs'],
- ],
- });
-
- runTask(() => this.rerender());
-
- this.assertText('Top: Middle: Bottom: @horsetomdale');
-
- // In this case, because the attrs are passed down, all child components are invoked.
-
- this.assertHooks({
- label: 'after no-op rernder (root)',
- interactive: [],
- nonInteractive: [],
- });
-
- this.teardownAssertions.push(() => {
- this.assertHooks({
- label: 'destroy',
-
- interactive: [
- ['the-top', 'willDestroyElement'],
- ['the-top', 'willClearRender'],
- ['the-middle', 'willDestroyElement'],
- ['the-middle', 'willClearRender'],
- ['the-bottom', 'willDestroyElement'],
- ['the-bottom', 'willClearRender'],
- ['the-top', 'didDestroyElement'],
- ['the-middle', 'didDestroyElement'],
- ['the-bottom', 'didDestroyElement'],
- ['the-top', 'willDestroy'],
- ['the-middle', 'willDestroy'],
- ['the-bottom', 'willDestroy'],
- ],
-
- nonInteractive: [
- ['the-top', 'willDestroy'],
- ['the-middle', 'willDestroy'],
- ['the-bottom', 'willDestroy'],
- ],
- });
-
- this.assertRegisteredViews('after destroy');
- });
- }
-
- ['@test components rendered from `{{each}}` have correct life-cycle hooks to be called']() {
- let { invoke } = this.boundHelpers;
-
- this.registerComponent('nested-item', { template: `{{yield}}` });
-
- this.registerComponent('an-item', {
- template: strip`
- {{#nested-item}}Item: {{this.count}}{{/nested-item}}
- `,
- });
-
- this.registerComponent('no-items', {
- template: strip`
- {{#nested-item}}Nothing to see here{{/nested-item}}
- `,
- });
-
- this.render(
- strip`
- {{#each this.items as |item|}}
- ${invoke('an-item', { count: expr('item') })}
- {{else}}
- ${invoke('no-items')}
- {{/each}}
- `,
- {
- items: [1, 2, 3, 4, 5],
- }
- );
-
- this.assertText('Item: 1Item: 2Item: 3Item: 4Item: 5');
- this.assertRegisteredViews('intial render');
-
- let initialHooks = () => {
- let ret = [
- ['an-item', 'init'],
- ['an-item', 'on(init)'],
- ['an-item', 'didReceiveAttrs'],
- ];
- if (this.isInteractive) {
- ret.push(['an-item', 'willRender'], ['an-item', 'willInsertElement']);
- }
- ret.push(
- ['nested-item', 'init'],
- ['nested-item', 'on(init)'],
- ['nested-item', 'didReceiveAttrs']
- );
- if (this.isInteractive) {
- ret.push(['nested-item', 'willRender'], ['nested-item', 'willInsertElement']);
- }
- return ret;
- };
-
- let initialAfterRenderHooks = () => {
- if (this.isInteractive) {
- return [
- ['nested-item', 'didInsertElement'],
- ['nested-item', 'didRender'],
- ['an-item', 'didInsertElement'],
- ['an-item', 'didRender'],
- ];
- } else {
- return [];
- }
- };
-
- this.assertHooks({
- label: 'after initial render',
-
- interactive: [
- // Sync hooks
- ...initialHooks(1),
- ...initialHooks(2),
- ...initialHooks(3),
- ...initialHooks(4),
- ...initialHooks(5),
-
- // Async hooks
- ...initialAfterRenderHooks(5),
- ...initialAfterRenderHooks(4),
- ...initialAfterRenderHooks(3),
- ...initialAfterRenderHooks(2),
- ...initialAfterRenderHooks(1),
- ],
-
- nonInteractive: [
- // Sync hooks
- ...initialHooks(1),
- ...initialHooks(2),
- ...initialHooks(3),
- ...initialHooks(4),
- ...initialHooks(5),
-
- // Async hooks
- ...initialAfterRenderHooks(5),
- ...initialAfterRenderHooks(4),
- ...initialAfterRenderHooks(3),
- ...initialAfterRenderHooks(2),
- ...initialAfterRenderHooks(1),
- ],
- });
-
- // TODO: Is this correct? Should childViews be populated in non-interactive mode?
- if (this.isInteractive) {
- this.assert.equal(this.component.childViews.length, 5, 'childViews precond');
- }
-
- runTask(() => set(this.context, 'items', []));
-
- // TODO: Is this correct? Should childViews be populated in non-interactive mode?
- if (this.isInteractive) {
- this.assert.equal(this.component.childViews.length, 1, 'childViews updated');
- }
-
- this.assertText('Nothing to see here');
-
- this.assertHooks({
- label: 'reset to empty array',
-
- interactive: [
- ['an-item', 'willDestroyElement'],
- ['an-item', 'willClearRender'],
- ['nested-item', 'willDestroyElement'],
- ['nested-item', 'willClearRender'],
- ['an-item', 'willDestroyElement'],
- ['an-item', 'willClearRender'],
- ['nested-item', 'willDestroyElement'],
- ['nested-item', 'willClearRender'],
- ['an-item', 'willDestroyElement'],
- ['an-item', 'willClearRender'],
- ['nested-item', 'willDestroyElement'],
- ['nested-item', 'willClearRender'],
- ['an-item', 'willDestroyElement'],
- ['an-item', 'willClearRender'],
- ['nested-item', 'willDestroyElement'],
- ['nested-item', 'willClearRender'],
- ['an-item', 'willDestroyElement'],
- ['an-item', 'willClearRender'],
- ['nested-item', 'willDestroyElement'],
- ['nested-item', 'willClearRender'],
-
- ['no-items', 'init'],
- ['no-items', 'on(init)'],
- ['no-items', 'didReceiveAttrs'],
- ['no-items', 'willRender'],
- ['no-items', 'willInsertElement'],
-
- ['nested-item', 'init'],
- ['nested-item', 'on(init)'],
- ['nested-item', 'didReceiveAttrs'],
- ['nested-item', 'willRender'],
- ['nested-item', 'willInsertElement'],
-
- ['nested-item', 'didInsertElement'],
- ['nested-item', 'didRender'],
- ['no-items', 'didInsertElement'],
- ['no-items', 'didRender'],
-
- ['an-item', 'didDestroyElement'],
- ['nested-item', 'didDestroyElement'],
- ['an-item', 'didDestroyElement'],
- ['nested-item', 'didDestroyElement'],
- ['an-item', 'didDestroyElement'],
- ['nested-item', 'didDestroyElement'],
- ['an-item', 'didDestroyElement'],
- ['nested-item', 'didDestroyElement'],
- ['an-item', 'didDestroyElement'],
- ['nested-item', 'didDestroyElement'],
-
- ['an-item', 'willDestroy'],
- ['nested-item', 'willDestroy'],
- ['an-item', 'willDestroy'],
- ['nested-item', 'willDestroy'],
- ['an-item', 'willDestroy'],
- ['nested-item', 'willDestroy'],
- ['an-item', 'willDestroy'],
- ['nested-item', 'willDestroy'],
- ['an-item', 'willDestroy'],
- ['nested-item', 'willDestroy'],
- ],
-
- nonInteractive: [
- ['no-items', 'init'],
- ['no-items', 'on(init)'],
- ['no-items', 'didReceiveAttrs'],
-
- ['nested-item', 'init'],
- ['nested-item', 'on(init)'],
- ['nested-item', 'didReceiveAttrs'],
-
- ['an-item', 'willDestroy'],
- ['nested-item', 'willDestroy'],
- ['an-item', 'willDestroy'],
- ['nested-item', 'willDestroy'],
- ['an-item', 'willDestroy'],
- ['nested-item', 'willDestroy'],
- ['an-item', 'willDestroy'],
- ['nested-item', 'willDestroy'],
- ['an-item', 'willDestroy'],
- ['nested-item', 'willDestroy'],
- ],
- });
-
- this.teardownAssertions.push(() => {
- this.assertHooks({
- label: 'destroy',
-
- interactive: [
- ['no-items', 'willDestroyElement'],
- ['no-items', 'willClearRender'],
- ['nested-item', 'willDestroyElement'],
- ['nested-item', 'willClearRender'],
-
- ['no-items', 'didDestroyElement'],
- ['nested-item', 'didDestroyElement'],
-
- ['no-items', 'willDestroy'],
- ['nested-item', 'willDestroy'],
- ],
-
- nonInteractive: [
- ['no-items', 'willDestroy'],
- ['nested-item', 'willDestroy'],
- ],
- });
-
- this.assertRegisteredViews('after destroy');
- });
- }
-}
-
-class CurlyComponentsTest 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 `this.${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}`);
- }
- }
-}
-
-moduleFor(
- 'Components test: interactive lifecycle hooks (curly components)',
- class extends CurlyComponentsTest {
- get isInteractive() {
- return true;
- }
- }
-);
-
-moduleFor(
- 'Components test: non-interactive lifecycle hooks (curly components)',
- class extends CurlyComponentsTest {
- get isInteractive() {
- return false;
- }
- }
-);
-
-moduleFor(
- 'Components test: interactive lifecycle hooks (tagless curly components)',
- class extends CurlyComponentsTest {
- get ComponentClass() {
- return class extends Component {
- tagName = '';
- };
- }
-
- get isInteractive() {
- return true;
- }
- }
-);
-
-moduleFor(
- 'Components test: non-interactive lifecycle hooks (tagless curly components)',
- class extends CurlyComponentsTest {
- get ComponentClass() {
- return class extends Component {
- tagName = '';
- };
- }
-
- get isInteractive() {
- return false;
- }
- }
-);
+import Component from '@glimmer/component';
moduleFor(
'Run loop and lifecycle hooks',
class extends RenderingTestCase {
['@test afterRender set']() {
let ComponentClass = class extends Component {
- width = '5';
- didInsertElement() {
+ @tracked width = '5';
+
+ inserted = defineSimpleModifier(() => {
schedule('afterRender', () => {
- this.set('width', '10');
+ this.width = '10';
});
- }
+ });
};
this.owner.register(
'component:foo-bar',
- setComponentTemplate(precompileTemplate('{{this.width}}'), ComponentClass)
+ setComponentTemplate(
+ precompileTemplate('{{this.width}}
'),
+ ComponentClass
+ )
);
this.render('{{foo-bar}}');
@@ -1377,17 +41,19 @@ moduleFor(
['@test afterRender set on parent']() {
let ComponentClass = class extends Component {
- didInsertElement() {
+ inserted = defineSimpleModifier(() => {
schedule('afterRender', () => {
- let parent = this.get('parent');
- parent.set('foo', 'wat');
+ set(this.args.parent, 'foo', 'wat');
});
- }
+ });
};
this.owner.register(
'component:foo-bar',
- setComponentTemplate(precompileTemplate('{{this.foo}}'), ComponentClass)
+ setComponentTemplate(
+ precompileTemplate('{{@foo}}
'),
+ ComponentClass
+ )
);
this.render('{{foo-bar parent=this foo=this.foo}}');
@@ -1399,243 +65,114 @@ moduleFor(
this.assertText('wat');
}
- ['@test `willRender` can set before render (GH#14458)']() {
- let ComponentClass = class extends Component {
- tagName = 'a';
- customHref = 'http://google.com';
- attributeBindings = ['customHref:href'];
- willRender() {
- this.set('customHref', 'http://willRender.com');
- }
- };
+ ['@test lifecycle hooks are invoked in a predictable order'](assert) {
+ let hooks = [];
- this.owner.register(
- 'component:foo-bar',
- setComponentTemplate(precompileTemplate('Hello World'), ComponentClass)
- );
-
- this.render(`{{foo-bar id="foo"}}`);
-
- this.assertElement(this.firstChild, {
- tagName: 'a',
- attrs: {
- id: 'foo',
- href: 'http://willRender.com',
- class: classes('ember-view'),
- },
- });
- }
+ let componentClass = (name) =>
+ class extends Component {
+ constructor(owner, args) {
+ super(owner, args);
+ hooks.push(`${name}: constructor`);
+ }
- ['@test that thing about destroying'](assert) {
- let ParentDestroyedElements = [];
- let ChildDestroyedElements = [];
+ modifier = defineSimpleModifier(() => {
+ hooks.push(`${name}: modifier installed`);
- let ParentComponent = class extends Component {
- willDestroyElement() {
- ParentDestroyedElements.push({
- id: this.itemId,
- name: 'parent-component',
- hasParent: Boolean(this.element.parentNode),
- nextSibling: Boolean(this.element.nextSibling),
- previousSibling: Boolean(this.element.previousSibling),
+ return () => hooks.push(`${name}: modifier destroyed`);
});
- }
- };
- let NestedComponent = class extends Component {
- willDestroyElement() {
- ChildDestroyedElements.push({
- id: this.nestedId,
- name: 'nested-component',
- hasParent: Boolean(this.element.parentNode),
- nextSibling: Boolean(this.element.nextSibling),
- previousSibling: Boolean(this.element.previousSibling),
- });
- }
- };
+ willDestroy() {
+ super.willDestroy();
+ hooks.push(`${name}: willDestroy`);
+ }
+ };
this.owner.register(
- 'component:parent-component',
+ 'component:the-top',
setComponentTemplate(
precompileTemplate(
- "{{yield}}{{#nested-component nestedId=(concat this.itemId '-A')}}A{{/nested-component}}{{#nested-component nestedId=(concat this.itemId '-B')}}B{{/nested-component}} "
+ '{{the-middle value=@value}}{{the-sibling}}
'
),
- ParentComponent
+ componentClass('the-top')
)
);
this.owner.register(
- 'component:nested-component',
- setComponentTemplate(precompileTemplate('{{yield}}'), NestedComponent)
+ 'component:the-middle',
+ setComponentTemplate(
+ precompileTemplate('{{the-bottom value=@value}}
'),
+ componentClass('the-middle')
+ )
);
- let array = emberA([{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }, { id: 5 }]);
-
- this.render(
- strip`
- {{#each this.items as |item|}}
- {{#parent-component itemId=item.id}}{{item.id}}{{/parent-component}}
- {{/each}}
- {{#if this.model.shouldShow}}
- {{#parent-component itemId=6}}6{{/parent-component}}
- {{/if}}
- {{#if this.model.shouldShow}}
- {{#parent-component itemId=7}}7{{/parent-component}}
- {{/if}}
- `,
- {
- items: array,
- model: { shouldShow: true },
- }
+ this.owner.register(
+ 'component:the-bottom',
+ setComponentTemplate(
+ precompileTemplate('{{@value}}
'),
+ componentClass('the-bottom')
+ )
);
- this.assertText('1AB2AB3AB4AB5AB6AB7AB');
+ this.owner.register(
+ 'component:the-sibling',
+ setComponentTemplate(
+ precompileTemplate('
'),
+ componentClass('the-sibling')
+ )
+ );
- runTask(() => {
- array.removeAt(2);
- array.removeAt(2);
- set(this.context, 'model.shouldShow', false);
+ this.render('{{#if this.show}}{{the-top value=this.value}}{{/if}}', {
+ show: true,
+ value: 1,
});
- this.assertText('1AB2AB5AB');
+ this.assertText('1');
- assertDestroyHooks(
- assert,
- [...ParentDestroyedElements],
+ assert.deepEqual(
+ hooks,
[
- {
- id: 3,
- hasParent: true,
- nextSibling: true,
- previousSibling: true,
- },
- {
- id: 4,
- hasParent: true,
- nextSibling: true,
- previousSibling: true,
- },
- {
- id: 6,
- hasParent: true,
- nextSibling: true,
- previousSibling: true,
- },
- {
- id: 7,
- hasParent: true,
- nextSibling: false,
- previousSibling: true,
- },
- ]
+ 'the-top: constructor',
+ 'the-middle: constructor',
+ 'the-bottom: constructor',
+ 'the-sibling: constructor',
+ 'the-bottom: modifier installed',
+ 'the-middle: modifier installed',
+ 'the-sibling: modifier installed',
+ 'the-top: modifier installed',
+ ],
+ 'initial render'
);
- assertDestroyHooks(
- assert,
- [...ChildDestroyedElements],
- [
- {
- id: '3-A',
- hasParent: true,
- nextSibling: true,
- previousSibling: false,
- },
- {
- id: '3-B',
- hasParent: true,
- nextSibling: false,
- previousSibling: true,
- },
- {
- id: '4-A',
- hasParent: true,
- nextSibling: true,
- previousSibling: false,
- },
- {
- id: '4-B',
- hasParent: true,
- nextSibling: false,
- previousSibling: true,
- },
- {
- id: '6-A',
- hasParent: true,
- nextSibling: true,
- previousSibling: false,
- },
- {
- id: '6-B',
- hasParent: true,
- nextSibling: false,
- previousSibling: true,
- },
- {
- id: '7-A',
- hasParent: true,
- nextSibling: true,
- previousSibling: false,
- },
- {
- id: '7-B',
- hasParent: true,
- nextSibling: false,
- previousSibling: true,
- },
- ]
- );
- }
+ hooks = [];
- ['@test lifecycle hooks exist on the base class'](assert) {
- // Make sure we get the finalized component prototype
- let prototype = Component.proto();
+ runTask(() => this.rerender());
- assert.equal(typeof prototype.didDestroyElement, 'function', 'didDestroyElement exists');
- assert.equal(typeof prototype.didInsertElement, 'function', 'didInsertElement exists');
- assert.equal(typeof prototype.didReceiveAttrs, 'function', 'didReceiveAttrs exists');
- assert.equal(typeof prototype.didRender, 'function', 'didRender exists');
- assert.equal(typeof prototype.didUpdate, 'function', 'didUpdate exists');
- assert.equal(typeof prototype.didUpdateAttrs, 'function', 'didUpdateAttrs exists');
- assert.equal(typeof prototype.willClearRender, 'function', 'willClearRender exists');
- assert.equal(typeof prototype.willDestroy, 'function', 'willDestroy exists');
- assert.equal(typeof prototype.willDestroyElement, 'function', 'willDestroyElement exists');
- assert.equal(typeof prototype.willInsertElement, 'function', 'willInsertElement exists');
- assert.equal(typeof prototype.willRender, 'function', 'willRender exists');
- assert.equal(typeof prototype.willUpdate, 'function', 'willUpdate exists');
- }
- }
-);
+ assert.deepEqual(hooks, [], 'a rerender without changes runs no hook');
-function assertDestroyHooks(assert, _actual, _expected) {
- _expected.forEach((expected, i) => {
- let name = expected.name;
- assert.equal(expected.id, _actual[i].id, `${name} id is the same`);
- assert.equal(expected.hasParent, _actual[i].hasParent, `${name} has parent node`);
- assert.equal(expected.nextSibling, _actual[i].nextSibling, `${name} has next sibling node`);
- assert.equal(
- expected.previousSibling,
- _actual[i].previousSibling,
- `${name} has previous sibling node`
- );
- });
-}
+ runTask(() => set(this.context, 'value', 2));
-function bind(func, thisArg) {
- return (...args) => func.apply(thisArg, args);
-}
+ this.assertText('2');
-function string(value) {
- return { isString: true, value };
-}
+ assert.deepEqual(hooks, [], 'an argument update runs no hook');
-function expr(value) {
- return { isExpr: true, value };
-}
+ runTask(() => set(this.context, 'show', false));
-function hook(name, hook, { attrs, oldAttrs, newAttrs } = {}) {
- return { name, hook, args: { attrs, oldAttrs, newAttrs } };
-}
+ this.assertText('');
-function json(serializable) {
- return JSON.parse(JSON.stringify(serializable));
-}
+ assert.deepEqual(
+ hooks,
+ [
+ 'the-bottom: modifier destroyed',
+ 'the-middle: modifier destroyed',
+ 'the-sibling: modifier destroyed',
+ 'the-top: modifier destroyed',
+ 'the-top: willDestroy',
+ 'the-middle: willDestroy',
+ 'the-bottom: willDestroy',
+ 'the-sibling: willDestroy',
+ ],
+ 'destroy'
+ );
+ }
+ }
+);
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 2a4d84bedd1..46c8c7db092 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
@@ -6,7 +6,7 @@ import {
runTask,
} from 'internal-test-helpers';
import Controller, { inject as injectController } from '@ember/controller';
-import { A as emberA } from '@ember/array';
+import { trackedArray } from '@ember/reactive/collections';
import { RSVP } from '@ember/-internals/runtime';
import Route from '@ember/routing/route';
import NoneLocation from '@ember/routing/none-location';
@@ -1607,7 +1607,7 @@ moduleFor(
controller = this;
}
- routeNames = emberA(['foo', 'bar', 'rar']);
+ routeNames = trackedArray(['foo', 'bar', 'rar']);
route1 = 'bar';
route2 = 'foo';
}
@@ -1649,7 +1649,7 @@ moduleFor(
linksEqual(this.$('a'), ['/foo', '/bar', '/rar', '/foo', '/bar', '/rar', '/rar', '/foo']);
- runTask(() => controller.routeNames.shiftObject());
+ runTask(() => controller.routeNames.shift());
linksEqual(this.$('a'), ['/bar', '/rar', '/bar', '/rar', '/rar', '/foo']);
}
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 e1313862bba..72c318843fa 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
@@ -5,7 +5,7 @@ import {
runTask,
} from 'internal-test-helpers';
import Controller, { inject as injectController } from '@ember/controller';
-import { A as emberA } from '@ember/array';
+import { trackedArray } from '@ember/reactive/collections';
import { RSVP } from '@ember/-internals/runtime';
import Route from '@ember/routing/route';
import NoneLocation from '@ember/routing/none-location';
@@ -1523,7 +1523,7 @@ moduleFor(
controller = this;
}
- routeNames = emberA(['foo', 'bar', 'rar']);
+ routeNames = trackedArray(['foo', 'bar', 'rar']);
route1 = 'bar';
route2 = 'foo';
}
@@ -1567,7 +1567,7 @@ moduleFor(
linksEqual(this.$('a'), ['/foo', '/bar', '/rar', '/foo', '/bar', '/rar', '/rar', '/foo']);
- runTask(() => controller.routeNames.shiftObject());
+ runTask(() => controller.routeNames.shift());
linksEqual(this.$('a'), ['/bar', '/rar', '/bar', '/rar', '/rar', '/foo']);
}
diff --git a/packages/@ember/-internals/glimmer/tests/integration/components/render-component-test.ts b/packages/@ember/-internals/glimmer/tests/integration/components/render-component-test.ts
index 479cafe6b6e..4f3b9c91f87 100644
--- a/packages/@ember/-internals/glimmer/tests/integration/components/render-component-test.ts
+++ b/packages/@ember/-internals/glimmer/tests/integration/components/render-component-test.ts
@@ -8,7 +8,6 @@ import {
defineSimpleModifier,
moduleFor,
type ClassicComponentShape,
- runDestroy,
} from 'internal-test-helpers';
import { Input, Textarea } from '@ember/component';
@@ -17,12 +16,13 @@ import { template } from '@ember/template-compiler/runtime';
import { template as compileTimeTemplate } from '@ember/template-compiler';
import { setComponentTemplate } from '@glimmer/manager';
import templateOnly from '@ember/component/template-only';
-import { array, concat, fn, get, hash, on } from '@glimmer/runtime';
-import GlimmerishComponent from '../../utils/glimmerish-component';
+import { array, concat, fn, get, hash } from '@ember/helper';
+import { on } from '@ember/modifier';
+import GlimmerComponent from '@glimmer/component';
-import { run } from '@ember/runloop';
import { destroy, associateDestroyableChild, registerDestructor } from '@glimmer/destroyable';
-import { renderComponent, type RenderResult } from '../../../lib/renderer';
+import { renderComponent, renderSettled, type RenderResult } from '../../../lib/renderer';
+import { renderers } from '../../../lib/renderers';
import { trackedObject } from '@ember/reactive/collections';
import { cached, tracked } from '@glimmer/tracking';
import Service, { service } from '@ember/service';
@@ -46,35 +46,34 @@ class RenderComponentTestCase extends AbstractStrictTestCase {
return document.querySelector('#qunit-fixture')!;
}
- assertChange({ change, expect }: { change: () => void; expect: string }) {
- run(() => change());
+ async assertChange({ change, expect }: { change: () => void; expect: string }) {
+ change();
+ await renderSettled();
assertHTML(expect);
- this.assertStableRerender();
+ await this.assertStableRerender();
}
- renderComponent(
+ async renderComponent(
component: object,
options: { args?: Record; expect: string } | { classic: ClassicComponentShape }
) {
let { owner } = this;
- run(() => {
- const result = renderComponent(component, {
- owner,
- args: 'args' in options ? options.args : {},
- env: { document: document, isInteractive: true, hasDOM: true },
- into: this.element,
- });
- this.component = {
- ...result,
- rerender() {
- // unused, but asserted against
- },
- };
- registerDestructor(this, () => result.destroy());
+ const result = renderComponent(component, {
+ owner,
+ args: 'args' in options ? options.args : {},
+ env: { document: document, isInteractive: true, hasDOM: true },
+ into: this.element,
});
+ this.component = {
+ ...result,
+ rerender() {
+ // unused, but asserted against
+ },
+ };
+ registerDestructor(this, () => result.destroy());
if ('expect' in options) {
assertHTML(options.expect);
@@ -82,62 +81,67 @@ class RenderComponentTestCase extends AbstractStrictTestCase {
assertClassicComponentElement(options.classic);
}
- this.assertStableRerender();
+ await this.assertStableRerender();
}
}
moduleFor(
'Strict Mode - RenderComponentTestCase',
class extends RenderComponentTestCase {
- afterEach() {
+ async afterEach() {
if (this.component) {
- runDestroy(this);
+ destroy(this);
+ await renderSettled();
}
}
- '@test destroy cleans up dom via destrying the test context'() {
+ async '@test destroy cleans up dom via destrying the test context'() {
let Foo = setComponentTemplate(precompileTemplate('Hello, world!'), templateOnly());
let Root = setComponentTemplate(
precompileTemplate(' ', { strictMode: true, scope: () => ({ Foo }) }),
templateOnly()
);
- this.renderComponent(Root, { expect: 'Hello, world!' });
+ await this.renderComponent(Root, { expect: 'Hello, world!' });
- run(() => destroy(this));
+ destroy(this);
+ await renderSettled();
assertHTML('');
}
- '@test destroy of the owner cleans up dom via destrying the test context'() {
+ async '@test destroy of the owner cleans up dom via destrying the test context'() {
let Foo = setComponentTemplate(precompileTemplate('Hello, world!'), templateOnly());
let Root = setComponentTemplate(
precompileTemplate(' ', { strictMode: true, scope: () => ({ Foo }) }),
templateOnly()
);
- this.renderComponent(Root, { expect: 'Hello, world!' });
+ await this.renderComponent(Root, { expect: 'Hello, world!' });
- run(() => destroy(this.owner));
+ destroy(this.owner);
+ await renderSettled();
assertHTML('');
}
- '@test captureRenderTree includes the rendered components'(assert: QUnit['assert']) {
+ async '@test captureRenderTree includes the rendered components'(assert: QUnit['assert']) {
let HelloWorld = setComponentTemplate(precompileTemplate('Hello, world!'), templateOnly());
let Root = setComponentTemplate(
precompileTemplate(' ', { strictMode: true, scope: () => ({ HelloWorld }) }),
templateOnly()
);
- this.renderComponent(Root, { expect: 'Hello, world!' });
+ await this.renderComponent(Root, { expect: 'Hello, world!' });
if (!ENV._DEBUG_RENDER_TREE) return;
assert.deepEqual(renderTreeNames(this.owner), ['{ROOT}', 'HelloWorld']);
}
- '@test captureRenderTree includes components rendered with any owner'(assert: QUnit['assert']) {
+ async '@test captureRenderTree includes components rendered with any owner'(
+ assert: QUnit['assert']
+ ) {
let Owned = setComponentTemplate(precompileTemplate('owned'), templateOnly());
let Ownerless = setComponentTemplate(precompileTemplate('ownerless'), templateOnly());
let OwnedRoot = setComponentTemplate(
@@ -153,10 +157,10 @@ moduleFor(
let ownerlessElement = document.createElement('div');
this.element.append(ownedElement, ownerlessElement);
- let results = run(() => [
+ let results = [
renderComponent(OwnedRoot, { owner: this.owner, into: ownedElement }),
renderComponent(OwnerlessRoot, { into: ownerlessElement }),
- ]);
+ ];
assertHTML('owned
ownerless
');
@@ -164,17 +168,17 @@ moduleFor(
assert.deepEqual(renderTreeNames(this.owner), ['{ROOT}', 'Owned', '{ROOT}', 'Ownerless']);
}
- run(() => {
- for (let result of results) {
- result.destroy();
- }
- });
+ for (let result of results) {
+ result.destroy();
+ }
+ await renderSettled();
if (ENV._DEBUG_RENDER_TREE) {
assert.deepEqual(renderTreeNames(this.owner), [], 'destroyed renders are gone');
}
- run(() => destroy(this));
+ destroy(this);
+ await renderSettled();
}
}
);
@@ -198,62 +202,59 @@ moduleFor(
return document.querySelector('#qunit-fixture')!;
}
- '@test manually calling destroy cleans up the DOM'() {
+ async '@test manually calling destroy cleans up the DOM'() {
let Foo = setComponentTemplate(precompileTemplate('Hello, world!'), templateOnly());
let owner = buildOwner({});
- let manualDestroy: () => void;
- run(() => {
- let result = renderComponent(Foo, {
- owner,
- into: this.element,
- });
- manualDestroy = result.destroy;
- this.component = {
- ...result,
- rerender() {
- // unused, but asserted against
- },
- };
+ let result = renderComponent(Foo, {
+ owner,
+ into: this.element,
});
+ this.component = {
+ ...result,
+ rerender() {
+ // unused, but asserted against
+ },
+ };
assertHTML('Hello, world!');
- this.assertStableRerender();
+ await this.assertStableRerender();
- run(() => manualDestroy());
+ result.destroy();
+ await renderSettled();
assertHTML('');
- this.assertStableRerender();
+ await this.assertStableRerender();
- run(() => destroy(owner));
+ destroy(owner);
+ await renderSettled();
}
- '@test destroying the owner cleans up the DOM'() {
+ async '@test destroying the owner cleans up the DOM'() {
let Foo = setComponentTemplate(precompileTemplate('Hello, world!'), templateOnly());
let owner = buildOwner({});
- run(() => {
- let result = renderComponent(Foo, {
- owner,
- into: this.element,
- });
- this.component = {
- ...result,
- rerender() {
- // unused, but asserted against
- },
- };
+ let result = renderComponent(Foo, {
+ owner,
+ into: this.element,
});
+ this.component = {
+ ...result,
+ rerender() {
+ // unused, but asserted against
+ },
+ };
assertHTML('Hello, world!');
- this.assertStableRerender();
+ await this.assertStableRerender();
- run(() => destroy(owner));
+ destroy(owner);
+ await renderSettled();
assertHTML('');
- this.assertStableRerender();
+ await this.assertStableRerender();
}
}
);
@@ -261,75 +262,77 @@ moduleFor(
moduleFor(
'Strict Mode - renderComponent',
class extends RenderComponentTestCase {
- afterEach() {
+ async afterEach() {
if (this.component) {
- runDestroy(this);
+ destroy(this);
+ await renderSettled();
}
}
- '@test destroy cleans up dom via destroying the owner'() {
+ async '@test destroy cleans up dom via destroying the owner'() {
let Foo = setComponentTemplate(precompileTemplate('Hello, world!'), templateOnly());
let Root = setComponentTemplate(
precompileTemplate(' ', { strictMode: true, scope: () => ({ Foo }) }),
templateOnly()
);
- this.renderComponent(Root, { expect: 'Hello, world!' });
+ await this.renderComponent(Root, { expect: 'Hello, world!' });
- run(() => destroy(this.owner));
+ destroy(this.owner);
+ await renderSettled();
assertHTML('');
}
- '@test Can use a component in scope'() {
+ async '@test Can use a component in scope'() {
let Foo = setComponentTemplate(precompileTemplate('Hello, world!'), templateOnly());
let Root = setComponentTemplate(
precompileTemplate(' ', { strictMode: true, scope: () => ({ Foo }) }),
templateOnly()
);
- this.renderComponent(Root, { expect: 'Hello, world!' });
+ await this.renderComponent(Root, { expect: 'Hello, world!' });
}
- '@test Can use a custom helper in scope (in append position)'() {
+ async '@test Can use a custom helper in scope (in append position)'() {
let foo = defineSimpleHelper(() => 'Hello, world!');
let Root = setComponentTemplate(
precompileTemplate('{{foo}}', { strictMode: true, scope: () => ({ foo }) }),
templateOnly()
);
- this.renderComponent(Root, { expect: 'Hello, world!' });
+ await this.renderComponent(Root, { expect: 'Hello, world!' });
}
- '@test Can use a custom modifier in scope'() {
+ async '@test Can use a custom modifier in scope'() {
let foo = defineSimpleModifier((element) => (element.innerHTML = 'Hello, world!'));
let Root = setComponentTemplate(
precompileTemplate('
', { strictMode: true, scope: () => ({ foo }) }),
templateOnly()
);
- this.renderComponent(Root, { expect: 'Hello, world!
' });
+ await this.renderComponent(Root, { expect: 'Hello, world!
' });
}
- '@test Can shadow keywords (runtime)'() {
+ async '@test Can shadow keywords (runtime)'() {
let each = setComponentTemplate(precompileTemplate('Hello, world!'), templateOnly());
let Bar = template('{{#each}}{{/each}}', {
scope: () => ({ each }),
});
- this.renderComponent(Bar, { expect: 'Hello, world!' });
+ await this.renderComponent(Bar, { expect: 'Hello, world!' });
}
- '@test Can shadow keywords (compile-time)'() {
+ async '@test Can shadow keywords (compile-time)'() {
let each = setComponentTemplate(precompileTemplate('Hello, world!'), templateOnly());
let Bar = compileTimeTemplate('{{#each}}{{/each}}', {
scope: () => ({ each }),
});
- this.renderComponent(Bar, { expect: 'Hello, world!' });
+ await this.renderComponent(Bar, { expect: 'Hello, world!' });
}
- '@test Can use constant values in ambiguous helper/component position'() {
+ async '@test Can use constant values in ambiguous helper/component position'() {
let value = 'Hello, world!';
let Root = setComponentTemplate(
@@ -337,19 +340,19 @@ moduleFor(
templateOnly()
);
- this.renderComponent(Root, { expect: 'Hello, world!' });
+ await this.renderComponent(Root, { expect: 'Hello, world!' });
}
- '@test Can use inline if and unless in strict mode templates'() {
+ async '@test Can use inline if and unless in strict mode templates'() {
let Root = setComponentTemplate(
precompileTemplate('{{if true "foo" "bar"}}{{unless true "foo" "bar"}}'),
templateOnly()
);
- this.renderComponent(Root, { expect: 'foobar' });
+ await this.renderComponent(Root, { expect: 'foobar' });
}
- '@test multiple components have independent lifetimes'() {
+ async '@test multiple components have independent lifetimes'() {
class State {
@tracked showSecond = true;
}
@@ -363,51 +366,51 @@ moduleFor(
templateOnly()
);
- this.renderComponent(Root, { expect: 'Hello, world!Hello, world!' });
+ await this.renderComponent(Root, { expect: 'Hello, world!Hello, world!' });
- this.assertChange({
+ await this.assertChange({
change: () => (state.showSecond = false),
expect: 'Hello, world!',
});
}
- '@test Can use a dynamic component definition'() {
+ async '@test Can use a dynamic component definition'() {
let Foo = setComponentTemplate(precompileTemplate('Hello, world!'), templateOnly());
let Root = setComponentTemplate(
precompileTemplate(' '),
- class extends GlimmerishComponent {
+ class extends GlimmerComponent {
Foo = Foo;
}
);
- this.renderComponent(Root, { expect: 'Hello, world!' });
+ await this.renderComponent(Root, { expect: 'Hello, world!' });
}
- '@test Can use a dynamic component definition (curly)'() {
+ async '@test Can use a dynamic component definition (curly)'() {
let Foo = setComponentTemplate(precompileTemplate('Hello, world!'), templateOnly());
let Root = setComponentTemplate(
precompileTemplate('{{this.Foo}}'),
- class extends GlimmerishComponent {
+ class extends GlimmerComponent {
Foo = Foo;
}
);
- this.renderComponent(Root, { expect: 'Hello, world!' });
+ await this.renderComponent(Root, { expect: 'Hello, world!' });
}
- '@test Can use a dynamic helper definition'() {
+ async '@test Can use a dynamic helper definition'() {
let foo = defineSimpleHelper(() => 'Hello, world!');
let Root = setComponentTemplate(
precompileTemplate('{{this.foo}}'),
- class extends GlimmerishComponent {
+ class extends GlimmerComponent {
foo = foo;
}
);
- this.renderComponent(Root, { expect: 'Hello, world!' });
+ await this.renderComponent(Root, { expect: 'Hello, world!' });
}
- '@test Can use a curried dynamic helper'() {
+ async '@test Can use a curried dynamic helper'() {
let foo = defineSimpleHelper((value) => value);
let Foo = setComponentTemplate(precompileTemplate('{{@value}}'), templateOnly());
let Root = setComponentTemplate(
@@ -418,10 +421,10 @@ moduleFor(
templateOnly()
);
- this.renderComponent(Root, { expect: 'Hello, world!' });
+ await this.renderComponent(Root, { expect: 'Hello, world!' });
}
- '@test Can use a curried dynamic modifier'() {
+ async '@test Can use a curried dynamic modifier'() {
let foo = defineSimpleModifier((element, [text]) => (element.innerHTML = text));
let Foo = setComponentTemplate(precompileTemplate('
'), templateOnly());
let Root = setComponentTemplate(
@@ -432,22 +435,22 @@ moduleFor(
templateOnly()
);
- this.renderComponent(Root, { expect: 'Hello, world!
' });
+ await this.renderComponent(Root, { expect: 'Hello, world!
' });
}
- '@test when args are trackedObject, the rendered component response appropriately'() {
+ async '@test when args are trackedObject, the rendered component response appropriately'() {
let args = trackedObject({ foo: 2 });
let Root = setComponentTemplate(precompileTemplate('{{@foo}}'), templateOnly());
- this.renderComponent(Root, { args, expect: '2' });
+ await this.renderComponent(Root, { args, expect: '2' });
- this.assertChange({
+ await this.assertChange({
change: () => args.foo++,
expect: '3',
});
}
- '@skip when args are a custom tracked class, the rendered component response appropriately'() {
+ async '@skip when args are a custom tracked class, the rendered component response appropriately'() {
class Args {
@tracked foo = 2;
}
@@ -455,15 +458,15 @@ moduleFor(
let Root = setComponentTemplate(precompileTemplate('{{@foo}}'), templateOnly());
// @ts-expect-error SAFETY: custom class is not currently supported as args, but would be nice to support?
- this.renderComponent(Root, { args, expect: '2' });
+ await this.renderComponent(Root, { args, expect: '2' });
- this.assertChange({
+ await this.assertChange({
change: () => args.foo++,
expect: '3',
});
}
- '@test a modifier can call renderComponent'() {
+ async '@test a modifier can call renderComponent'() {
let render = defineSimpleModifier((element, [comp]) => {
let result = renderComponent(comp, { into: element });
@@ -479,15 +482,87 @@ moduleFor(
templateOnly()
);
- this.renderComponent(Root, { expect: 'hi there
' });
+ await this.renderComponent(Root, { expect: 'hi there
' });
+ }
+
+ async '@test destroying the result releases its root and renderer'(assert: QUnit['assert']) {
+ let render = defineSimpleModifier((element, [comp]) => {
+ let result = renderComponent(comp, { into: element });
+
+ return () => result.destroy();
+ });
+
+ let Inner = setComponentTemplate(precompileTemplate('hi there'), templateOnly());
+
+ class State {
+ @tracked show = true;
+ }
+ let state = new State();
+
+ let Root = setComponentTemplate(
+ precompileTemplate(`{{#if state.show}}
{{/if}}`, {
+ strictMode: true,
+ scope: () => ({ render, Inner, state }),
+ }),
+ templateOnly()
+ );
+
+ await this.renderComponent(Root, { expect: 'hi there
' });
+
+ let baseline = renderers.length;
+
+ for (let i = 0; i < 5; i++) {
+ state.show = false;
+ await renderSettled();
+ assertHTML('');
+ state.show = true;
+ await renderSettled();
+ assertHTML('hi there
');
+ }
+
+ assert.strictEqual(
+ renderers.length,
+ baseline,
+ 'renderers for destroyed renderComponent results are not retained'
+ );
+ }
+
+ async '@test destroying the result removes its root from a shared renderer'(
+ assert: QUnit['assert']
+ ) {
+ let Inner = setComponentTemplate(precompileTemplate('hi there'), templateOnly());
+ let { owner } = this;
+ let element = document.createElement('div');
+
+ let first = renderComponent(Inner, { owner, into: element });
+ let renderer = renderers[renderers.length - 1]!;
+
+ assert.strictEqual(renderer.state.roots.length, 1);
+
+ for (let i = 0; i < 5; i++) {
+ let other = document.createElement('div');
+ let result = renderComponent(Inner, { owner, into: other });
+ result.destroy();
+ await renderSettled();
+ }
+
+ assert.strictEqual(renderer.state.roots.length, 1, 'destroyed roots are not retained');
+
+ first.destroy();
+ await renderSettled();
+
+ assert.false(renderers.includes(renderer), 'renderer with no roots is deregistered');
+
+ destroy(this);
+ await renderSettled();
}
- '@test can render in to a detached element'() {
+ async '@test can render in to a detached element'() {
let Inner = setComponentTemplate(precompileTemplate('hello there'), templateOnly());
let element = document.createElement('div');
let attach: () => void;
- class _Root extends GlimmerishComponent {
+ class _Root extends GlimmerComponent {
@tracked attached: Element | undefined;
constructor(owner: any, args: any) {
@@ -503,9 +578,9 @@ moduleFor(
let Root = setComponentTemplate(precompileTemplate(`{{this.attached}}`), _Root);
- this.renderComponent(Root, { expect: '' });
+ await this.renderComponent(Root, { expect: '' });
- this.assertChange({
+ await this.assertChange({
change: () => attach(),
expect: `hello there
`,
});
@@ -515,11 +590,11 @@ moduleFor(
* Test skipped because when an error occurs,
* we mess up the cache used by renderComponent.
*/
- '@skip can *not* render in to a TextNode'(assert: Assert) {
+ async '@skip can *not* render in to a TextNode'(assert: Assert) {
let Inner = setComponentTemplate(precompileTemplate('hello there'), templateOnly());
let element = document.createTextNode('');
- class _Root extends GlimmerishComponent {
+ class _Root extends GlimmerComponent {
@tracked attached: Element | undefined;
constructor(owner: any, args: any) {
@@ -539,18 +614,18 @@ moduleFor(
let Root = setComponentTemplate(precompileTemplate(``), _Root);
- this.renderComponent(Root, { expect: '' });
+ await this.renderComponent(Root, { expect: '' });
assert.verifySteps(['throw']);
}
- '@test replaces existing contents within the target element'() {
+ async '@test replaces existing contents within the target element'() {
let Inner = setComponentTemplate(precompileTemplate('hello there'), templateOnly());
let element = document.createElement('div');
element.innerHTML = 'general kenobi';
let render: () => void;
- class _Root extends GlimmerishComponent {
+ class _Root extends GlimmerComponent {
constructor(owner: any, args: any) {
super(owner, args);
@@ -567,15 +642,15 @@ moduleFor(
_Root
);
- this.renderComponent(Root, { expect: 'general kenobi
' });
+ await this.renderComponent(Root, { expect: 'general kenobi
' });
- this.assertChange({
+ await this.assertChange({
change: () => render(),
expect: `hello there
`,
});
}
- [`@test renderComponent is eager, so it tracks with its parent`](assert: Assert) {
+ async [`@test renderComponent is eager, so it tracks with its parent`](assert: Assert) {
let step = (...x: unknown[]) => assert.step(x.join(':'));
let Inner = setComponentTemplate(
@@ -584,7 +659,7 @@ moduleFor(
);
let element = document.createElement('div');
- class _Root extends GlimmerishComponent {
+ class _Root extends GlimmerComponent {
@tracked foo = 2;
increment = () => this.foo++;
@@ -616,10 +691,10 @@ moduleFor(
_Root
);
- this.renderComponent(Root, { expect: '2 ++
' });
+ await this.renderComponent(Root, { expect: '2 ++
' });
assert.verifySteps(['render:root', 'foo:2']);
- this.assertChange({
+ await this.assertChange({
change: () => {
this.element.querySelector('button')?.click();
},
@@ -641,14 +716,13 @@ moduleFor(
assert.strictEqual(this.element.innerHTML, '3 ++
');
- run(() => {
- destroy(this.owner);
- });
+ destroy(this.owner);
+ await renderSettled();
assert.strictEqual(this.element.innerHTML, '');
}
- '@test multiple renderComponents share reactivity'() {
+ async '@test multiple renderComponents share reactivity'() {
let args = trackedObject({ foo: 2 });
let InnerOne = setComponentTemplate(precompileTemplate('{{@foo}}'), templateOnly());
@@ -660,7 +734,7 @@ moduleFor(
element1.setAttribute('data-one', '');
element2.setAttribute('data-two', '');
- class _Root extends GlimmerishComponent {
+ class _Root extends GlimmerComponent {
constructor(owner: any, _args: any) {
super(owner, _args);
@@ -682,25 +756,27 @@ moduleFor(
_Root
);
- this.renderComponent(Root, { expect: '2
2
' });
+ await this.renderComponent(Root, {
+ expect: '2
2
',
+ });
- this.assertChange({
+ await this.assertChange({
change: () => args.foo++,
expect: '3
3
',
});
}
- '@test multiple renderComponents share service injection'() {
+ async '@test multiple renderComponents share service injection'() {
class State extends Service {
@tracked foo = 2;
}
this.owner.register('service:state', State);
- class _One extends GlimmerishComponent {
+ class _One extends GlimmerComponent {
@service state!: State;
}
- class _Two extends GlimmerishComponent {
+ class _Two extends GlimmerComponent {
@service state!: State;
}
let InnerOne = setComponentTemplate(precompileTemplate('{{this.state.foo}}'), _One);
@@ -712,7 +788,7 @@ moduleFor(
element1.setAttribute('data-one', '');
element2.setAttribute('data-two', '');
- class _Root extends GlimmerishComponent {
+ class _Root extends GlimmerComponent {
constructor(owner: any, _args: any) {
super(owner, _args);
@@ -734,17 +810,19 @@ moduleFor(
_Root
);
- this.renderComponent(Root, { expect: '2
2
' });
+ await this.renderComponent(Root, {
+ expect: '2
2
',
+ });
let x = this.owner.lookup('service:state') as State;
- this.assertChange({
+ await this.assertChange({
change: () => x.foo++,
expect: '3
3
',
});
}
- '@test rendering multiple times to adjacent elements'() {
+ async '@test rendering multiple times to adjacent elements'() {
let aHelper = (str: string) => str.toUpperCase();
let Child = setComponentTemplate(
precompileTemplate(`Hi: {{aHelper "there"}}`, {
@@ -754,7 +832,7 @@ moduleFor(
templateOnly()
);
let get = (id: string) => this.element.querySelector(id);
- function render(Comp: GlimmerishComponent, id: string, owner: Owner) {
+ function render(Comp: GlimmerComponent, id: string, owner: Owner) {
renderComponent(Comp, {
into: get(`#${id}`)!,
owner,
@@ -777,18 +855,19 @@ moduleFor(
templateOnly()
);
- this.renderComponent(Root, {
+ await this.renderComponent(Root, {
expect: [`a:Hi: THERE
`, `b:Hi: THERE
`, ``, ``].join(
'\n'
),
});
- run(() => destroy(this));
+ destroy(this);
+ await renderSettled();
assertHTML('');
}
- '@test multiple calls to render in to the same element appear as siblings'() {
+ async '@test multiple calls to render in to the same element appear as siblings'() {
let aHelper = (str: string) => str.toUpperCase();
let Child = setComponentTemplate(
precompileTemplate(`Hi: {{aHelper "there"}}`, {
@@ -798,7 +877,7 @@ moduleFor(
templateOnly()
);
let get = (id: string) => this.element.querySelector(id);
- function render(Comp: GlimmerishComponent, id: string, owner: Owner) {
+ function render(Comp: GlimmerComponent, id: string, owner: Owner) {
renderComponent(Comp, {
into: get(`#${id}`)!,
owner,
@@ -817,10 +896,11 @@ moduleFor(
templateOnly()
);
- this.renderComponent(Root, {
+ await this.renderComponent(Root, {
expect: `a:Hi: THEREa:Hi: THERE
\n\n`,
});
- run(() => destroy(this));
+ destroy(this);
+ await renderSettled();
assertHTML('');
}
@@ -828,7 +908,7 @@ moduleFor(
/**
* NOTE: subsequent renders to the same element are prepended to the element's children
*/
- '@test multiple calls to render in to the same element appear as siblings and can be updated'() {
+ async '@test multiple calls to render in to the same element appear as siblings and can be updated'() {
let aHelper = (str: string) => str.toUpperCase();
let dataA = trackedObject({ count: 1 });
let dataB = trackedObject({ count: -1 });
@@ -841,7 +921,7 @@ moduleFor(
);
let get = (id: string) => this.element.querySelector(id);
- function render(Comp: GlimmerishComponent, id: string, owner: Owner) {
+ function render(Comp: GlimmerComponent, id: string, owner: Owner) {
renderComponent(Comp, {
into: get(`#${id}`)!,
owner,
@@ -871,7 +951,7 @@ moduleFor(
templateOnly()
);
- this.renderComponent(Root, {
+ await this.renderComponent(Root, {
expect: [
``,
`
b:Hi: THERE:-1 `,
@@ -882,7 +962,7 @@ moduleFor(
].join('\n'),
});
- this.assertChange({
+ await this.assertChange({
change: () => dataA.count++,
expect: [
`
`,
@@ -898,7 +978,7 @@ moduleFor(
* ERROR in conflict with the NOTE on this test.
* the elementns flip locations... which is kinda bonkers
*/
- this.assertChange({
+ await this.assertChange({
change: () => dataB.count--,
expect: [
`
`,
@@ -910,7 +990,8 @@ moduleFor(
].join('\n'),
});
- run(() => destroy(this));
+ destroy(this);
+ await renderSettled();
assertHTML('');
}
@@ -920,7 +1001,7 @@ moduleFor(
let get = (id: string) => this.element.querySelector(id);
let promises: Promise
[] = [];
- function render(Comp: GlimmerishComponent, id: string, owner: Owner) {
+ function render(Comp: GlimmerComponent, id: string, owner: Owner) {
let promise = (async () => {
await Promise.resolve();
let element = get(`#${id}`);
@@ -952,7 +1033,7 @@ moduleFor(
templateOnly()
);
- this.renderComponent(Root, {
+ await this.renderComponent(Root, {
expect: [`
`, `
`, ``, ``].join('\n'),
});
@@ -960,7 +1041,8 @@ moduleFor(
assertHTML([`a:Hi
`, `b:Hi
`, ``, ``].join('\n'));
- run(() => destroy(this));
+ destroy(this);
+ await renderSettled();
assertHTML('');
}
@@ -970,7 +1052,7 @@ moduleFor(
moduleFor(
'Strict Mode <-> Loose Mode - renderComponent',
class extends RenderComponentTestCase {
- '@test incidentally invoked loose-mode components can still resolve helpers'() {
+ async '@test incidentally invoked loose-mode components can still resolve helpers'() {
this.owner.register('helper:a-helper', (str: string) => str.toUpperCase());
let Loose = setComponentTemplate(
precompileTemplate(`Hi: {{a-helper "there"}}`),
@@ -981,9 +1063,10 @@ moduleFor(
templateOnly()
);
- this.renderComponent(Root, { expect: 'Hi: THERE' });
+ await this.renderComponent(Root, { expect: 'Hi: THERE' });
- run(() => destroy(this));
+ destroy(this);
+ await renderSettled();
assertHTML('');
}
@@ -999,14 +1082,14 @@ moduleFor(
}, /but that value was not in scope: a-helper/);
}
- '@test rendering multiple times to adjacent elements'() {
+ async '@test rendering multiple times to adjacent elements'() {
this.owner.register('helper:a-helper', (str: string) => str.toUpperCase());
let Loose = setComponentTemplate(
precompileTemplate(`Hi: {{a-helper "there"}}`),
templateOnly()
);
let get = (id: string) => this.element.querySelector(id);
- function render(Comp: GlimmerishComponent, id: string, owner: Owner) {
+ function render(Comp: GlimmerComponent, id: string, owner: Owner) {
renderComponent(Comp, {
into: get(`#${id}`)!,
owner,
@@ -1029,13 +1112,14 @@ moduleFor(
templateOnly()
);
- this.renderComponent(Root, {
+ await this.renderComponent(Root, {
expect: [`a:Hi: THERE
`, `b:Hi: THERE
`, ``, ``].join(
'\n'
),
});
- run(() => destroy(this));
+ destroy(this);
+ await renderSettled();
assertHTML('');
}
@@ -1045,13 +1129,13 @@ moduleFor(
moduleFor(
'Strict Mode - renderComponent - built ins',
class extends RenderComponentTestCase {
- '@test Can use Input'() {
+ async '@test Can use Input'() {
let Root = setComponentTemplate(
precompileTemplate(' ', { strictMode: true, scope: () => ({ Input }) }),
templateOnly()
);
- this.renderComponent(Root, {
+ await this.renderComponent(Root, {
classic: {
tagName: 'input',
attrs: {
@@ -1062,13 +1146,13 @@ moduleFor(
});
}
- '@test Can use Textarea'() {
+ async '@test Can use Textarea'() {
let Root = setComponentTemplate(
precompileTemplate('', { strictMode: true, scope: () => ({ Textarea }) }),
templateOnly()
);
- this.renderComponent(Root, {
+ await this.renderComponent(Root, {
classic: {
tagName: 'textarea',
attrs: {
@@ -1078,7 +1162,7 @@ moduleFor(
});
}
- '@test Can use hash'() {
+ async '@test Can use hash'() {
let Root = setComponentTemplate(
precompileTemplate(
'{{#let (hash value="Hello, world!") as |hash|}}{{hash.value}}{{/let}}',
@@ -1087,10 +1171,10 @@ moduleFor(
templateOnly()
);
- this.renderComponent(Root, { expect: 'Hello, world!' });
+ await this.renderComponent(Root, { expect: 'Hello, world!' });
}
- '@test Can use array'() {
+ async '@test Can use array'() {
let Root = setComponentTemplate(
precompileTemplate('{{#each (array "Hello, world!") as |value|}}{{value}}{{/each}}', {
strictMode: true,
@@ -1099,10 +1183,10 @@ moduleFor(
templateOnly()
);
- this.renderComponent(Root, { expect: 'Hello, world!' });
+ await this.renderComponent(Root, { expect: 'Hello, world!' });
}
- '@test Can use concat'() {
+ async '@test Can use concat'() {
let Root = setComponentTemplate(
precompileTemplate('{{(concat "Hello" ", " "world!")}}', {
strictMode: true,
@@ -1111,10 +1195,10 @@ moduleFor(
templateOnly()
);
- this.renderComponent(Root, { expect: 'Hello, world!' });
+ await this.renderComponent(Root, { expect: 'Hello, world!' });
}
- '@test Can use get'() {
+ async '@test Can use get'() {
let Root = setComponentTemplate(
precompileTemplate(
'{{#let (hash value="Hello, world!") as |hash|}}{{(get hash "value")}}{{/let}}',
@@ -1123,10 +1207,10 @@ moduleFor(
templateOnly()
);
- this.renderComponent(Root, { expect: 'Hello, world!' });
+ await this.renderComponent(Root, { expect: 'Hello, world!' });
}
- '@test Can use on and fn'(assert: Assert) {
+ async '@test Can use on and fn'(assert: Assert) {
let handleClick = (value: unknown) => {
assert.step('handleClick');
assert.equal(value, 123);
@@ -1140,7 +1224,7 @@ moduleFor(
templateOnly()
);
- this.renderComponent(Root, { expect: 'Click ' });
+ await this.renderComponent(Root, { expect: 'Click ' });
clickElement('button');
@@ -1159,7 +1243,7 @@ moduleFor(
// both strict and resolution modes, and these things would be implicitly
// covered elsewhere, but until then, these coverage are essential.
- '@test Can use each-in'() {
+ async '@test Can use each-in'() {
let obj = {
foo: 'FOO',
bar: 'BAR',
@@ -1173,10 +1257,10 @@ moduleFor(
templateOnly()
);
- this.renderComponent(Root, { expect: '[foo:FOO][bar:BAR]' });
+ await this.renderComponent(Root, { expect: '[foo:FOO][bar:BAR]' });
}
- '@test Can use in-element'() {
+ async '@test Can use in-element'() {
let getElement = (id: string) => document.getElementById(id);
let Foo = setComponentTemplate(
@@ -1194,7 +1278,7 @@ moduleFor(
templateOnly()
);
- this.renderComponent(Root, {
+ await this.renderComponent(Root, {
expect: '[before
][after]',
});
}
diff --git a/packages/@ember/-internals/glimmer/tests/integration/components/runtime-template-compiler-explicit-test.ts b/packages/@ember/-internals/glimmer/tests/integration/components/runtime-template-compiler-explicit-test.ts
index 1bed33f5d01..57d3b97bdae 100644
--- a/packages/@ember/-internals/glimmer/tests/integration/components/runtime-template-compiler-explicit-test.ts
+++ b/packages/@ember/-internals/glimmer/tests/integration/components/runtime-template-compiler-explicit-test.ts
@@ -1,7 +1,7 @@
import { template } from '@ember/template-compiler/runtime';
import { ALLOWED_GLOBALS } from '@ember/template-compiler';
import { RenderingTestCase, defineSimpleModifier, moduleFor } from 'internal-test-helpers';
-import GlimmerishComponent from '../../utils/glimmerish-component';
+import GlimmerComponent from '@glimmer/component';
import { on } from '@ember/modifier/on';
import { fn } from '@ember/helper';
@@ -98,7 +98,7 @@ moduleFor(
await this.renderComponentModule(() => {
let Foo = template('Hello, world!');
- return class extends GlimmerishComponent {
+ return class extends GlimmerComponent {
static {
template(' ', {
component: this,
@@ -117,7 +117,7 @@ moduleFor(
await this.renderComponentModule(() => {
let Foo = template('Hello, world!');
- return class extends GlimmerishComponent {
+ return class extends GlimmerComponent {
static {
template('{{this.Foo}}', {
component: this,
@@ -136,7 +136,7 @@ moduleFor(
await this.renderComponentModule(() => {
let foo = () => 'Hello, world!';
- return class extends GlimmerishComponent {
+ return class extends GlimmerComponent {
static {
template('{{this.foo}}', {
component: this,
@@ -228,7 +228,7 @@ moduleFor(
}
async '@test Can use hash'() {
- const { hash } = await import('@glimmer/runtime');
+ const { hash } = await import('@ember/helper');
await this.renderComponentModule(() => {
return template('{{#let (hash value="Hello, world!") as |hash|}}{{hash.value}}{{/let}}', {
@@ -241,7 +241,7 @@ moduleFor(
}
async '@test Can use array'() {
- const { array } = await import('@glimmer/runtime');
+ const { array } = await import('@ember/helper');
await this.renderComponentModule(() => {
return template('{{#each (array "Hello, world!") as |value|}}{{value}}{{/each}}', {
@@ -253,7 +253,7 @@ moduleFor(
}
async '@test Can use concat'() {
- const { concat } = await import('@glimmer/runtime');
+ const { concat } = await import('@ember/helper');
await this.renderComponentModule(() => {
return template('{{(concat "Hello" ", " "world!")}}', {
@@ -266,7 +266,7 @@ moduleFor(
}
async '@test Can use get'() {
- const { hash, get } = await import('@glimmer/runtime');
+ const { hash, get } = await import('@ember/helper');
await this.renderComponentModule(() => {
return template(
diff --git a/packages/@ember/-internals/glimmer/tests/integration/components/runtime-template-compiler-implicit-test.ts b/packages/@ember/-internals/glimmer/tests/integration/components/runtime-template-compiler-implicit-test.ts
index 0d5e73bd544..79f7bad4199 100644
--- a/packages/@ember/-internals/glimmer/tests/integration/components/runtime-template-compiler-implicit-test.ts
+++ b/packages/@ember/-internals/glimmer/tests/integration/components/runtime-template-compiler-implicit-test.ts
@@ -1,7 +1,7 @@
import { template } from '@ember/template-compiler/runtime';
import { ALLOWED_GLOBALS } from '@ember/template-compiler';
import { RenderingTestCase, defineSimpleModifier, moduleFor } from 'internal-test-helpers';
-import GlimmerishComponent from '../../utils/glimmerish-component';
+import GlimmerComponent from '@glimmer/component';
import { on } from '@ember/modifier/on';
import { fn } from '@ember/helper';
@@ -145,7 +145,7 @@ moduleFor(
},
});
- return class extends GlimmerishComponent {
+ return class extends GlimmerComponent {
static {
template(' ', {
component: this,
@@ -171,7 +171,7 @@ moduleFor(
},
});
- return class extends GlimmerishComponent {
+ return class extends GlimmerComponent {
static {
template('{{this.Foo}}', {
component: this,
@@ -193,7 +193,7 @@ moduleFor(
await this.renderComponentModule(() => {
let foo = () => 'Hello, world!';
- return class extends GlimmerishComponent {
+ return class extends GlimmerComponent {
static {
template('{{this.foo}}', {
component: this,
@@ -312,7 +312,7 @@ moduleFor(
}
async '@test Can use hash'() {
- const { hash } = await import('@glimmer/runtime');
+ const { hash } = await import('@ember/helper');
hide(hash);
@@ -329,7 +329,7 @@ moduleFor(
}
async '@test Can use array'() {
- const { array } = await import('@glimmer/runtime');
+ const { array } = await import('@ember/helper');
hide(array);
@@ -345,7 +345,7 @@ moduleFor(
}
async '@test Can use concat'() {
- const { concat } = await import('@glimmer/runtime');
+ const { concat } = await import('@ember/helper');
hide(concat);
@@ -362,7 +362,7 @@ moduleFor(
}
async '@test Can use get'() {
- const { hash, get } = await import('@glimmer/runtime');
+ const { hash, get } = await import('@ember/helper');
hide(hash);
hide(get);
diff --git a/packages/@ember/-internals/glimmer/tests/integration/components/strict-mode-test.js b/packages/@ember/-internals/glimmer/tests/integration/components/strict-mode-test.js
index d624f3f6ca8..85214215889 100644
--- a/packages/@ember/-internals/glimmer/tests/integration/components/strict-mode-test.js
+++ b/packages/@ember/-internals/glimmer/tests/integration/components/strict-mode-test.js
@@ -13,8 +13,9 @@ import { template } from '@ember/template-compiler/runtime';
import { template as compileTimeTemplate } from '@ember/template-compiler';
import { setComponentTemplate } from '@glimmer/manager';
import templateOnly from '@ember/component/template-only';
-import { hash, array, concat, get, on, fn } from '@glimmer/runtime';
-import GlimmerishComponent from '../../utils/glimmerish-component';
+import { hash, array, concat, get, fn } from '@ember/helper';
+import { on } from '@ember/modifier';
+import GlimmerComponent from '@glimmer/component';
moduleFor(
'Strict Mode',
@@ -119,7 +120,7 @@ moduleFor(
let Foo = setComponentTemplate(precompileTemplate('Hello, world!'), templateOnly());
let Bar = setComponentTemplate(
precompileTemplate(' '),
- class extends GlimmerishComponent {
+ class extends GlimmerComponent {
Foo = Foo;
}
);
@@ -135,7 +136,7 @@ moduleFor(
let Foo = setComponentTemplate(precompileTemplate('Hello, world!'), templateOnly());
let Bar = setComponentTemplate(
precompileTemplate('{{this.Foo}}'),
- class extends GlimmerishComponent {
+ class extends GlimmerComponent {
Foo = Foo;
}
);
@@ -151,7 +152,7 @@ moduleFor(
let foo = defineSimpleHelper(() => 'Hello, world!');
let Bar = setComponentTemplate(
precompileTemplate('{{this.foo}}'),
- class extends GlimmerishComponent {
+ class extends GlimmerComponent {
foo = foo;
}
);
diff --git a/packages/@ember/-internals/glimmer/tests/integration/components/template-only-components-test.js b/packages/@ember/-internals/glimmer/tests/integration/components/template-only-components-test.js
index 546f6648d44..46635b0ebc8 100644
--- a/packages/@ember/-internals/glimmer/tests/integration/components/template-only-components-test.js
+++ b/packages/@ember/-internals/glimmer/tests/integration/components/template-only-components-test.js
@@ -1,6 +1,6 @@
import { moduleFor, RenderingTestCase, runTask } from 'internal-test-helpers';
import { setComponentTemplate } from '@glimmer/manager';
-import { templateOnlyComponent } from '@glimmer/runtime';
+import templateOnlyComponent from '@ember/component/template-only';
import { precompileTemplate } from '@ember/template-compilation';
import templateOnly from '@ember/component/template-only';
import EmberObject from '@ember/object';
diff --git a/packages/@ember/-internals/glimmer/tests/integration/components/to-string-test.js b/packages/@ember/-internals/glimmer/tests/integration/components/to-string-test.js
index 9606a6e9752..56d5bdcd864 100644
--- a/packages/@ember/-internals/glimmer/tests/integration/components/to-string-test.js
+++ b/packages/@ember/-internals/glimmer/tests/integration/components/to-string-test.js
@@ -1,12 +1,8 @@
-import { moduleFor, RenderingTestCase } from 'internal-test-helpers';
+import { module, test } from 'qunit';
+import Component from '@ember/component';
-import { Component } from '@ember/-internals/glimmer';
-
-moduleFor(
- 'built-in component toString',
- class AbstractAppendTest extends RenderingTestCase {
- '@test component has the correct toString value'(assert) {
- assert.strictEqual(Component.toString(), '@ember/component');
- }
- }
-);
+module('built-in component toString', function () {
+ test('component has the correct toString value', function (assert) {
+ assert.strictEqual(Component.toString(), '@ember/component');
+ });
+});
diff --git a/packages/@ember/-internals/glimmer/tests/integration/components/tracked-test.js b/packages/@ember/-internals/glimmer/tests/integration/components/tracked-test.js
index 4064e6668a3..7a33c7dbd17 100644
--- a/packages/@ember/-internals/glimmer/tests/integration/components/tracked-test.js
+++ b/packages/@ember/-internals/glimmer/tests/integration/components/tracked-test.js
@@ -16,56 +16,13 @@ import {
testUnless,
} from 'internal-test-helpers';
import { DEPRECATIONS } from '@ember/-internals/deprecations';
-import GlimmerishComponent from '../../utils/glimmerish-component';
import Component from '@glimmer/component';
-import { Component as EmberComponent } from '../../utils/helpers';
import { precompileTemplate } from '@ember/template-compilation';
import { setComponentTemplate } from '@glimmer/manager';
moduleFor(
'Component Tracked Properties',
class extends RenderingTestCase {
- '@test simple test using classic component'() {
- let personId = 0;
- class Person {
- @tracked first;
- @tracked last;
-
- constructor(first, last) {
- this.id = personId++;
- this.first = first;
- this.last = last;
- }
- }
-
- class PersonComponent extends EmberComponent {
- @tracked first;
- @tracked last;
-
- get person() {
- return new Person(this.first, this.last);
- }
- }
-
- this.owner.register(
- 'component:person-wrapper',
- setComponentTemplate(
- precompileTemplate('{{@first}} {{@last}} | {{this.person.first}} {{this.person.last}}'),
- PersonComponent
- )
- );
-
- this.render(' ', {
- first: 'robert',
- last: 'jackson',
- });
-
- this.assertText('robert jackson | robert jackson');
-
- runTask(() => this.context.set('first', 'max'));
- this.assertText('max jackson | max jackson');
- }
-
'@test simple test using glimmerish component'() {
let personId = 0;
class Person {
@@ -79,7 +36,7 @@ moduleFor(
}
}
- class PersonComponent extends GlimmerishComponent {
+ class PersonComponent extends Component {
get person() {
return new Person(this.args.first, this.args.last);
}
@@ -115,7 +72,7 @@ moduleFor(
let PromiseArray = ArrayProxy.extend(PromiseProxyMixin);
- class LoaderComponent extends GlimmerishComponent {
+ class LoaderComponent extends Component {
get data() {
if (!this._data) {
this._data = PromiseArray.create({
@@ -145,7 +102,7 @@ moduleFor(
)} @test creating an array proxy inside a tracking context and immediately updating its content before usage does not trigger backtracking assertion`]() {
expectDeprecation(/`ArrayProxy` is deprecated/, DEPRECATIONS.DEPRECATE_ARRAY_PROXY.isEnabled);
- class LoaderComponent extends GlimmerishComponent {
+ class LoaderComponent extends Component {
get data() {
if (!this._data) {
this._data = ArrayProxy.create({
@@ -320,7 +277,7 @@ moduleFor(
'{{#let (tracked 0) as |count|}}{{count.value}} {{/let}}',
{ strictMode: true, scope: () => ({ tracked, on, fn, increment }) }
),
- class extends GlimmerishComponent {}
+ class extends Component {}
)
);
@@ -667,7 +624,7 @@ moduleFor(
}
}
- class PersonComponent extends GlimmerishComponent {
+ class PersonComponent extends Component {
person = new Person(this.args.first, this.args.last);
}
@@ -693,10 +650,7 @@ moduleFor(
'@test works when EmberObject created during render'() {
this.owner.register(
'component:test',
- setComponentTemplate(
- precompileTemplate('{{@data.length}}'),
- class extends GlimmerishComponent {}
- )
+ setComponentTemplate(precompileTemplate('{{@data.length}}'), class extends Component {})
);
let RecordMeta = new WeakMap();
@@ -748,14 +702,14 @@ moduleFor(
let outerRenderCount = 0;
let innerRenderCount = 0;
- class OuterComponent extends GlimmerishComponent {
+ class OuterComponent extends Component {
get count() {
outerRenderCount++;
return this.args.count;
}
}
- class InnerComponent extends GlimmerishComponent {
+ class InnerComponent extends Component {
@tracked count = 0;
get combinedCounts() {
@@ -816,7 +770,7 @@ moduleFor(
}
'@test computed properties can depend on args'() {
- class TestComponent extends GlimmerishComponent {
+ class TestComponent extends Component {
@computed('args.text')
get text() {
return this.args.text;
@@ -846,7 +800,7 @@ moduleFor(
text: 'hello!',
});
- class TestComponent extends GlimmerishComponent {
+ class TestComponent extends Component {
@computed('args.foo.text')
get text() {
return this.args.foo.text;
@@ -872,7 +826,7 @@ moduleFor(
}
'@test args can be accessed with get()'() {
- class TestComponent extends GlimmerishComponent {
+ class TestComponent extends Component {
get text() {
return get(this, 'args.text');
}
@@ -897,7 +851,7 @@ moduleFor(
}
'@test args can be accessed with get() if no value is passed'() {
- class TestComponent extends GlimmerishComponent {
+ class TestComponent extends Component {
get text() {
return get(this, 'args.text') || 'hello!';
}
@@ -916,7 +870,7 @@ moduleFor(
}
'@test named args are enumerable'() {
- class TestComponent extends GlimmerishComponent {
+ class TestComponent extends Component {
get objectKeys() {
return Object.keys(this.args).join('');
}
@@ -946,7 +900,7 @@ moduleFor(
'component:test',
setComponentTemplate(
precompileTemplate('{{#each-in this.args as |key value|}}{{key}}:{{value}}{{/each-in}}'),
- class extends GlimmerishComponent {}
+ class extends Component {}
)
);
diff --git a/packages/@ember/-internals/glimmer/tests/integration/components/web-component-fallback-test.gjs b/packages/@ember/-internals/glimmer/tests/integration/components/web-component-fallback-test.gjs
new file mode 100644
index 00000000000..df49b4968b4
--- /dev/null
+++ b/packages/@ember/-internals/glimmer/tests/integration/components/web-component-fallback-test.gjs
@@ -0,0 +1,44 @@
+import { tracked } from '@glimmer/tracking';
+import { module, test } from 'qunit';
+import { setupRenderingTest } from 'ember-qunit';
+import { render, settled } from '@ember/test-helpers';
+
+class State {
+ @tracked name = 'Robert';
+}
+
+module('Components test: web component fallback', function (hooks) {
+ setupRenderingTest(hooks);
+
+ test('custom elements are rendered', async function (assert) {
+ await render(
+
+ hello
+
+ );
+
+ await assert.stableRender('hello ');
+ });
+
+ test('custom elements can have bound attributes', async function (assert) {
+ let state = new State();
+
+ await render(
+
+ hello
+
+ );
+
+ await assert.stableRender('hello ');
+
+ state.name = 'Kris';
+ await settled();
+
+ assert.dom('foo-bar').hasAttribute('some-attr', 'Kris').hasText('hello');
+
+ state.name = 'Robert';
+ await settled();
+
+ assert.dom('foo-bar').hasAttribute('some-attr', 'Robert').hasText('hello');
+ });
+});
diff --git a/packages/@ember/-internals/glimmer/tests/integration/components/web-component-fallback-test.js b/packages/@ember/-internals/glimmer/tests/integration/components/web-component-fallback-test.js
deleted file mode 100644
index 09721bf365a..00000000000
--- a/packages/@ember/-internals/glimmer/tests/integration/components/web-component-fallback-test.js
+++ /dev/null
@@ -1,36 +0,0 @@
-import { moduleFor, RenderingTestCase, runTask } from 'internal-test-helpers';
-
-import { set } from '@ember/object';
-
-moduleFor(
- 'Components test: web component fallback',
- class extends RenderingTestCase {
- ['@test custom elements are rendered']() {
- let template = `hello `;
-
- this.render(template);
-
- this.assertHTML(template);
-
- this.assertStableRerender();
- }
-
- ['@test custom elements can have bound attributes']() {
- let template = `hello `;
-
- this.render(template, { name: 'Robert' });
-
- this.assertHTML(`hello `);
-
- this.assertStableRerender();
-
- runTask(() => set(this.context, 'name', 'Kris'));
-
- this.assertHTML(`hello `);
-
- runTask(() => set(this.context, 'name', 'Robert'));
-
- this.assertHTML(`hello `);
- }
- }
-);
diff --git a/packages/@ember/-internals/glimmer/tests/integration/content-test.js b/packages/@ember/-internals/glimmer/tests/integration/content-test.js
index 467e7967c4d..ba92af7a624 100644
--- a/packages/@ember/-internals/glimmer/tests/integration/content-test.js
+++ b/packages/@ember/-internals/glimmer/tests/integration/content-test.js
@@ -19,7 +19,8 @@ import ObjectProxy from '@ember/object/proxy';
import { constructStyleDeprecationMessage } from '@ember/-internals/views';
import { precompileTemplate } from '@ember/template-compilation';
import { setComponentTemplate } from '@glimmer/manager';
-import { Component, SafeString, htmlSafe } from '../utils/helpers';
+import Component from '@glimmer/component';
+import { SafeString, htmlSafe } from '../utils/helpers';
const EMPTY = Object.freeze({});
@@ -1734,17 +1735,16 @@ if (DEBUG) {
this.assertStyleWarning(userValue);
}
- ['@test specifying `attributeBindings: ["style"]` generates a warning']() {
- let FooBarComponent = class extends Component {
- attributeBindings = ['style'];
- };
-
+ ['@test specifying `style` on a component with `...attributes` generates a warning']() {
this.owner.register(
'component:foo-bar',
- setComponentTemplate(precompileTemplate('hello'), FooBarComponent)
+ setComponentTemplate(
+ precompileTemplate('hello
'),
+ class extends Component {}
+ )
);
let userValue = 'width: 42px';
- this.render('{{foo-bar style=this.userValue}}', {
+ this.render(' ', {
userValue,
});
diff --git a/packages/@ember/-internals/glimmer/tests/integration/custom-helper-test.gjs b/packages/@ember/-internals/glimmer/tests/integration/custom-helper-test.gjs
index 60c29bef92b..830105dd390 100644
--- a/packages/@ember/-internals/glimmer/tests/integration/custom-helper-test.gjs
+++ b/packages/@ember/-internals/glimmer/tests/integration/custom-helper-test.gjs
@@ -1,20 +1,20 @@
-import { RenderingTestCase, moduleFor } from 'internal-test-helpers';
-import { Helper } from '../../index';
+import { module, test } from 'qunit';
+import { setupRenderingTest } from 'ember-qunit';
+import { render } from '@ember/test-helpers';
+import Helper from '@ember/component/helper';
-moduleFor(
- 'Custom Helper test',
- class extends RenderingTestCase {
- ['@test works with strict-mode']() {
- class Custom extends Helper {
- compute([value]) {
- return `${value}-custom`;
- }
- }
+module('Custom Helper test', function (hooks) {
+ setupRenderingTest(hooks);
- let TestComponent = {{ (Custom 'my-test') }}
- this.render(` `, { TestComponent });
- this.assertText('my-test-custom');
- this.assertStableRerender();
+ test('works with strict-mode', async function (assert) {
+ class Custom extends Helper {
+ compute([value]) {
+ return `${value}-custom`;
+ }
}
- }
-);
+
+ await render({{(Custom "my-test")}} );
+
+ await assert.stableRender('my-test-custom');
+ });
+});
diff --git a/packages/@ember/-internals/glimmer/tests/integration/event-dispatcher-test.js b/packages/@ember/-internals/glimmer/tests/integration/event-dispatcher-test.js
index 497be58e772..2352bdf344a 100644
--- a/packages/@ember/-internals/glimmer/tests/integration/event-dispatcher-test.js
+++ b/packages/@ember/-internals/glimmer/tests/integration/event-dispatcher-test.js
@@ -1,222 +1,12 @@
-import {
- expectDeprecation,
- moduleFor,
- RenderingTestCase,
- runTask,
- testUnless,
-} from 'internal-test-helpers';
-import { DEPRECATIONS } from '../../../deprecations';
+import { moduleFor, RenderingTestCase, runTask } from 'internal-test-helpers';
import Component from '@glimmer/component';
-import { Component as EmberComponent } from '../utils/helpers';
-import { _getCurrentRunLoop } from '@ember/runloop';
import { precompileTemplate } from '@ember/template-compilation';
import { setComponentTemplate } from '@glimmer/manager';
-function fireNativeWithDataTransfer(node, type, dataTransfer) {
- let event = new Event(type, { bubbles: true, cancelable: true });
- event.dataTransfer = dataTransfer;
- node.dispatchEvent(event);
-}
-
-function triggerEvent(node, event) {
- switch (event) {
- case 'focusin':
- return node.focus();
- case 'focusout':
- return node.blur();
- default:
- return node.trigger(event);
- }
-}
-
-const SUPPORTED_EMBER_EVENTS = {
- touchstart: 'touchStart',
- touchmove: 'touchMove',
- touchend: 'touchEnd',
- touchcancel: 'touchCancel',
- keydown: 'keyDown',
- keyup: 'keyUp',
- keypress: 'keyPress',
- mousedown: 'mouseDown',
- mouseup: 'mouseUp',
- contextmenu: 'contextMenu',
- click: 'click',
- dblclick: 'doubleClick',
- focusin: 'focusIn',
- focusout: 'focusOut',
- submit: 'submit',
- input: 'input',
- change: 'change',
- dragstart: 'dragStart',
- drag: 'drag',
- dragenter: 'dragEnter',
- dragleave: 'dragLeave',
- dragover: 'dragOver',
- drop: 'drop',
- dragend: 'dragEnd',
-};
-
moduleFor(
'EventDispatcher',
class extends RenderingTestCase {
- ['@test event handler methods are called when event is triggered'](assert) {
- let receivedEvent;
- let browserEvent;
-
- this.owner.register(
- 'component:x-button',
- class extends EmberComponent {
- tagName = 'button';
-
- touchMove(event) {
- receivedEvent = event;
- }
- touchStart(event) {
- receivedEvent = event;
- }
- touchEnd(event) {
- receivedEvent = event;
- }
- touchCancel(event) {
- receivedEvent = event;
- }
- keyDown(event) {
- receivedEvent = event;
- }
- keyUp(event) {
- receivedEvent = event;
- }
- keyPress(event) {
- receivedEvent = event;
- }
- mouseDown(event) {
- receivedEvent = event;
- }
- mouseUp(event) {
- receivedEvent = event;
- }
- contextMenu(event) {
- receivedEvent = event;
- }
- click(event) {
- receivedEvent = event;
- }
- doubleClick(event) {
- receivedEvent = event;
- }
- focusIn(event) {
- receivedEvent = event;
- }
- focusOut(event) {
- receivedEvent = event;
- }
- submit(event) {
- receivedEvent = event;
- }
- input(event) {
- receivedEvent = event;
- }
- change(event) {
- receivedEvent = event;
- }
- dragStart(event) {
- receivedEvent = event;
- }
- drag(event) {
- receivedEvent = event;
- }
- dragEnter(event) {
- receivedEvent = event;
- }
- dragLeave(event) {
- receivedEvent = event;
- }
- dragOver(event) {
- receivedEvent = event;
- }
- drop(event) {
- receivedEvent = event;
- }
- dragEnd(event) {
- receivedEvent = event;
- }
- }
- );
-
- this.render(`{{x-button}}`);
-
- let elementNode = this.$('button');
- let element = elementNode[0];
-
- for (browserEvent in SUPPORTED_EMBER_EVENTS) {
- receivedEvent = null;
- runTask(() => triggerEvent(elementNode, browserEvent));
- assert.ok(receivedEvent, `${browserEvent} event was triggered`);
- assert.strictEqual(receivedEvent.target, element);
- }
- }
-
- [`${testUnless(DEPRECATIONS.DEPRECATE_EVENTED.isRemoved)} @test event listeners are called when event is triggered`](
- assert
- ) {
- let receivedEvent;
- let browserEvent;
-
- this.owner.register(
- 'component:x-button',
- class extends EmberComponent {
- tagName = 'button';
- init() {
- super.init();
- Object.keys(SUPPORTED_EMBER_EVENTS).forEach((browserEvent) => {
- expectDeprecation(
- () => {
- this.on(SUPPORTED_EMBER_EVENTS[browserEvent], (event) => (receivedEvent = event));
- },
- /Evented#on` is deprecated/,
- DEPRECATIONS.DEPRECATE_EVENTED.isEnabled
- );
- });
- }
- }
- );
-
- this.render(`{{x-button}}`);
-
- let elementNode = this.$('button');
- let element = elementNode[0];
-
- for (browserEvent in SUPPORTED_EMBER_EVENTS) {
- receivedEvent = null;
- runTask(() => triggerEvent(elementNode, browserEvent));
- assert.ok(receivedEvent, `${browserEvent} event was triggered`);
- assert.strictEqual(receivedEvent.target, element);
- }
- }
-
- ['@test events bubble view hierarchy for form elements'](assert) {
- let receivedEvent;
-
- this.owner.register(
- 'component:x-foo',
- setComponentTemplate(
- precompileTemplate(` `),
- class extends EmberComponent {
- change(event) {
- receivedEvent = event;
- }
- }
- )
- );
-
- this.render(`{{x-foo}}`);
-
- runTask(() => this.$('#is-done').trigger('change'));
- assert.ok(receivedEvent, 'change event was triggered');
- assert.strictEqual(receivedEvent.target, this.$('#is-done')[0]);
- }
-
['@test case insensitive events'](assert) {
let receivedEvent;
@@ -260,122 +50,6 @@ moduleFor(
assert.ok(receivedEvent, 'change event was triggered');
assert.strictEqual(receivedEvent.target, this.$('#is-done')[0]);
}
-
- ['@test events bubble to parent view'](assert) {
- let receivedEvent;
-
- this.owner.register(
- 'component:x-foo',
- setComponentTemplate(
- precompileTemplate(`{{yield}}`),
- class extends EmberComponent {
- change(event) {
- receivedEvent = event;
- }
- }
- )
- );
-
- this.owner.register(
- 'component:x-bar',
- setComponentTemplate(
- precompileTemplate(` `),
- class extends EmberComponent {
- change() {}
- }
- )
- );
-
- this.render(`{{#x-foo}}{{x-bar}}{{/x-foo}}`);
-
- runTask(() => this.$('#is-done').trigger('change'));
- assert.ok(receivedEvent, 'change event was triggered');
- assert.strictEqual(receivedEvent.target, this.$('#is-done')[0]);
- }
-
- ['@test events bubbling up can be prevented by returning false'](assert) {
- let hasReceivedEvent;
-
- this.owner.register(
- 'component:x-foo',
- setComponentTemplate(
- precompileTemplate(`{{yield}}`),
- class extends EmberComponent {
- change() {
- hasReceivedEvent = true;
- }
- }
- )
- );
-
- this.owner.register(
- 'component:x-bar',
- setComponentTemplate(
- precompileTemplate(` `),
- class extends EmberComponent {
- change() {
- return false;
- }
- }
- )
- );
-
- this.render(`{{#x-foo}}{{x-bar}}{{/x-foo}}`);
-
- runTask(() => this.$('#is-done').trigger('change'));
- assert.notOk(hasReceivedEvent, 'change event has not been received');
- }
-
- ['@test events bubbling up can be prevented by calling stopPropagation()'](assert) {
- let hasReceivedEvent;
-
- this.owner.register(
- 'component:x-foo',
- setComponentTemplate(
- precompileTemplate(`{{yield}}`),
- class extends EmberComponent {
- change() {
- hasReceivedEvent = true;
- }
- }
- )
- );
-
- this.owner.register(
- 'component:x-bar',
- setComponentTemplate(
- precompileTemplate(` `),
- class extends EmberComponent {
- change(e) {
- e.stopPropagation();
- }
- }
- )
- );
-
- this.render(`{{#x-foo}}{{x-bar}}{{/x-foo}}`);
-
- runTask(() => this.$('#is-done').trigger('change'));
- assert.notOk(hasReceivedEvent, 'change event has not been received');
- }
-
- ['@test event handlers are wrapped in a run loop'](assert) {
- this.owner.register(
- 'component:x-foo',
- setComponentTemplate(
- precompileTemplate(` `),
- class extends EmberComponent {
- change() {
- assert.ok(_getCurrentRunLoop(), 'a run loop should have started');
- }
- }
- )
- );
-
- this.render(`{{x-foo}}`);
-
- this.$('#is-done').trigger('click');
- }
}
);
@@ -394,26 +68,6 @@ moduleFor(
};
}
- ['@test additional events can be specified'](assert) {
- this.dispatcher.setup({ myevent: 'myEvent' });
-
- this.owner.register(
- 'component:x-foo',
- setComponentTemplate(
- precompileTemplate(`Hello!
`),
- class extends EmberComponent {
- myEvent() {
- assert.ok(true, 'custom event was triggered');
- }
- }
- )
- );
-
- this.render(`{{x-foo}}`);
-
- this.$('div').trigger('myevent');
- }
-
['@test a rootElement can be specified'](assert) {
this.element.innerHTML = '
';
// this.$().append('
');
@@ -423,35 +77,6 @@ moduleFor(
assert.equal(this.dispatcher.rootElement, '#app', 'the dispatchers rootElement was updated');
}
- ['@test default events can be disabled via `customEvents`'](assert) {
- this.dispatcher.setup({ click: null });
-
- this.owner.register(
- 'component:x-foo',
- setComponentTemplate(
- precompileTemplate(`Hello!
`),
- class extends EmberComponent {
- click() {
- assert.ok(false, 'click method was called');
- }
-
- null() {
- assert.ok(false, 'null method was called');
- }
-
- doubleClick() {
- assert.ok(true, 'a non-disabled event is still handled properly');
- }
- }
- )
- );
-
- this.render(`{{x-foo}}`);
-
- this.$('div').trigger('click');
- this.$('div').trigger('dblclick');
- }
-
['@test throws if specified rootElement does not exist'](assert) {
assert.throws(() => {
this.dispatcher.setup({ myevent: 'myEvent' }, '#app');
@@ -459,25 +84,3 @@ moduleFor(
}
}
);
-
-moduleFor(
- 'EventDispatcher - Event Properties',
- class extends RenderingTestCase {
- ['@test dataTransfer property is added to drop event'](assert) {
- let receivedEvent;
- this.owner.register(
- 'component:x-foo',
- class extends EmberComponent {
- drop(event) {
- receivedEvent = event;
- }
- }
- );
-
- this.render(`{{x-foo}}`);
-
- fireNativeWithDataTransfer(this.$('div')[0], 'drop', 'success');
- assert.equal(receivedEvent.dataTransfer, 'success');
- }
- }
-);
diff --git a/packages/@ember/-internals/glimmer/tests/integration/helpers/array-test.gjs b/packages/@ember/-internals/glimmer/tests/integration/helpers/array-test.gjs
new file mode 100644
index 00000000000..d45b969edd5
--- /dev/null
+++ b/packages/@ember/-internals/glimmer/tests/integration/helpers/array-test.gjs
@@ -0,0 +1,331 @@
+import Component from '@glimmer/component';
+import { tracked } from '@glimmer/tracking';
+import { module, test } from 'qunit';
+import { setupRenderingTest } from 'ember-qunit';
+import { render, settled } from '@ember/test-helpers';
+import { array, hash } from '@ember/helper';
+
+class People {
+ @tracked personOne;
+ @tracked personTwo;
+
+ constructor({ personOne, personTwo }) {
+ this.personOne = personOne;
+ this.personTwo = personTwo;
+ }
+}
+
+class State {
+ @tracked model;
+
+ constructor(model) {
+ this.model = new People(model);
+ }
+}
+
+module('Helpers test: {{array}}', function (hooks) {
+ setupRenderingTest(hooks);
+
+ test('returns an array', async function (assert) {
+ await render(
+
+ {{#let (array 'Sergio') as |people|}}
+ {{#each people as |personName|}}
+ {{personName}}
+ {{/each}}
+ {{/let}}
+
+ );
+
+ assert.dom().hasText('Sergio');
+ await assert.stableRender();
+ });
+
+ test('the array helper can be shadowed', async function (assert) {
+ function array(...list) {
+ return list.map((n) => n * 2);
+ }
+
+ let shadowArray = array;
+ let First = {{#each (array 1 2 3) as |n|}}[{{n}}]{{/each}} ;
+
+ await render(
+
+ {{#let shadowArray as |array|}}
+ {{#each (array 5 10 15) as |n|}}[{{n}}]{{/each}}
+ {{/let}}
+
+
+ );
+
+ assert.dom().hasText('[10][20][30] [2][4][6]');
+ await assert.stableRender();
+ });
+
+ test('can have more than one value', async function (assert) {
+ await render(
+
+ {{#let (array 'Sergio' 'Robert') as |people|}}
+ {{#each people as |personName|}}
+ {{personName}},
+ {{/each}}
+ {{/let}}
+
+ );
+
+ assert.dom().hasText('Sergio, Robert,');
+ await assert.stableRender();
+ });
+
+ test('binds values when variables are used', async function (assert) {
+ let state = new People({ personOne: 'Tom' });
+
+ await render(
+
+ {{#let (array state.personOne) as |people|}}
+ {{#each people as |personName|}}
+ {{personName}}
+ {{/each}}
+ {{/let}}
+
+ );
+
+ assert.dom().hasText('Tom');
+ await assert.stableRender();
+
+ state.personOne = 'Yehuda';
+ await settled();
+
+ assert.dom().hasText('Yehuda');
+
+ state.personOne = 'Tom';
+ await settled();
+
+ assert.dom().hasText('Tom');
+ });
+
+ test('binds multiple values when variables are used', async function (assert) {
+ let state = new People({ personOne: 'Tom', personTwo: 'Yehuda' });
+
+ await render(
+
+ {{#let (array state.personOne state.personTwo) as |people|}}
+ {{#each people as |personName|}}
+ {{personName}},
+ {{/each}}
+ {{/let}}
+
+ );
+
+ assert.dom().hasText('Tom, Yehuda,');
+ await assert.stableRender();
+
+ state.personOne = 'Sergio';
+ await settled();
+
+ assert.dom().hasText('Sergio, Yehuda,');
+
+ state.personTwo = 'Tom';
+ await settled();
+
+ assert.dom().hasText('Sergio, Tom,');
+
+ state.personOne = 'Tom';
+ state.personTwo = 'Yehuda';
+ await settled();
+
+ assert.dom().hasText('Tom, Yehuda,');
+ });
+
+ test('array helpers can be nested', async function (assert) {
+ let state = new People({ personOne: 'Tom', personTwo: 'Yehuda' });
+
+ await render(
+
+ {{#let (array (array state.personOne state.personTwo)) as |listOfPeople|}}
+ {{#each listOfPeople as |people|}}
+ List:
+ {{#each people as |personName|}}
+ {{personName}},
+ {{/each}}
+ {{/each}}
+ {{/let}}
+
+ );
+
+ assert.dom().hasText('List: Tom, Yehuda,');
+ await assert.stableRender();
+
+ state.personOne = 'Chad';
+ await settled();
+
+ assert.dom().hasText('List: Chad, Yehuda,');
+
+ state.personTwo = 'Balint';
+ await settled();
+
+ assert.dom().hasText('List: Chad, Balint,');
+
+ state.personOne = 'Tom';
+ state.personTwo = 'Yehuda';
+ await settled();
+
+ assert.dom().hasText('List: Tom, Yehuda,');
+ });
+
+ test('should yield hash of an array of internal properties', async function (assert) {
+ let fooBarInstance;
+
+ class FooBar extends Component {
+ @tracked model = new People({ personOne: 'Chad' });
+
+ constructor(owner, args) {
+ super(owner, args);
+ fooBarInstance = this;
+ }
+
+ {{yield (hash people=(array this.model.personOne))}}
+ }
+
+ await render(
+
+ {{#each values.people as |personName|}}{{personName}}{{/each}}
+
+ );
+
+ await assert.stableRender('Chad');
+
+ fooBarInstance.model.personOne = 'Godfrey';
+ await settled();
+
+ assert.dom().hasText('Godfrey');
+
+ fooBarInstance.model = new People({ personOne: 'Chad' });
+ await settled();
+
+ assert.dom().hasText('Chad');
+
+ fooBarInstance.model.personOne = 'Godfrey';
+ await settled();
+
+ assert.dom().hasText('Godfrey');
+ });
+
+ test('should yield hash of an array of internal and external properties', async function (assert) {
+ let state = new State({ personTwo: 'Tom' });
+ let fooBarInstance;
+
+ class FooBar extends Component {
+ @tracked model = new People({ personOne: 'Chad' });
+
+ constructor(owner, args) {
+ super(owner, args);
+ fooBarInstance = this;
+ }
+
+ {{yield (hash people=(array this.model.personOne @personTwo))}}
+ }
+
+ await render(
+
+
+ {{#each values.people as |personName|}}
+ {{personName}},
+ {{/each}}
+
+
+ );
+
+ assert.dom().hasText('Chad, Tom,');
+ await assert.stableRender();
+
+ fooBarInstance.model.personOne = 'Godfrey';
+ state.model.personTwo = 'Yehuda';
+ await settled();
+
+ assert.dom().hasText('Godfrey, Yehuda,');
+
+ fooBarInstance.model = new People({ personOne: 'Chad' });
+ state.model = new People({ personTwo: 'Tom' });
+ await settled();
+
+ assert.dom().hasText('Chad, Tom,');
+ });
+
+ test('should render when passing as argument to a component invocation', async function (assert) {
+ let state = new People({ personTwo: 'Chad' });
+ let FooBar = {{#each @people as |personName|}}{{personName}},{{/each}} ;
+
+ await render( );
+
+ await assert.stableRender('Tom,Chad,');
+
+ state.personTwo = 'Godfrey';
+ await settled();
+
+ assert.dom().hasText('Tom,Godfrey,');
+
+ state.personTwo = 'Chad';
+ await settled();
+
+ assert.dom().hasText('Tom,Chad,');
+ });
+
+ test('should return an entirely new array when any argument change', async function (assert) {
+ let state = new People({ personTwo: 'Chad' });
+ let fooBarInstance;
+
+ class FooBar extends Component {
+ constructor(owner, args) {
+ super(owner, args);
+ fooBarInstance = this;
+ }
+
+ {{#each @people as |personName|}}{{personName}},{{/each}}
+ }
+
+ await render( );
+
+ let firstArray = fooBarInstance.args.people;
+
+ state.personTwo = 'Godfrey';
+ await settled();
+
+ assert.ok(
+ firstArray !== fooBarInstance.args.people,
+ 'should have created an entirely new array'
+ );
+ });
+
+ test('capture array values in JS to assert deep equal', async function (assert) {
+ let state = new People({ personTwo: 'Godfrey' });
+ let captured;
+
+ function capture(list) {
+ captured = list;
+ return 'captured';
+ }
+
+ await render({{capture (array 'Tom' state.personTwo)}} );
+
+ assert.deepEqual(captured, ['Tom', 'Godfrey']);
+
+ state.personTwo = 'Robert';
+ await settled();
+
+ assert.deepEqual(captured, ['Tom', 'Robert']);
+
+ state.personTwo = 'Godfrey';
+ await settled();
+
+ assert.deepEqual(captured, ['Tom', 'Godfrey']);
+ });
+
+ test('GH18693 properties in hash can be accessed from the array', async function (assert) {
+ await render(
+ {{#each (array (hash some='thing')) as |item|}}{{item.some}}{{/each}}
+ );
+
+ assert.dom().hasText('thing');
+ });
+});
diff --git a/packages/@ember/-internals/glimmer/tests/integration/helpers/array-test.js b/packages/@ember/-internals/glimmer/tests/integration/helpers/array-test.js
deleted file mode 100644
index 0cb71acd6b4..00000000000
--- a/packages/@ember/-internals/glimmer/tests/integration/helpers/array-test.js
+++ /dev/null
@@ -1,331 +0,0 @@
-import { RenderingTestCase, moduleFor, runTask, strip } from 'internal-test-helpers';
-import { setComponentTemplate } from '@glimmer/manager';
-import { precompileTemplate } from '@ember/template-compilation';
-import templateOnly from '@ember/component/template-only';
-
-import { set } from '@ember/object';
-
-import Component from '@glimmer/component';
-
-moduleFor(
- 'Helpers test: {{array}}',
- class extends RenderingTestCase {
- ['@test returns an array']() {
- this.render(strip`
- {{#let (array "Sergio") as |people|}}
- {{#each people as |personName|}}
- {{personName}}
- {{/each}}
- {{/let}}`);
-
- this.assertText('Sergio');
-
- this.assertStableRerender();
- }
-
- ['@test the array helper can be shadowed']() {
- function array(...list) {
- return list.map((n) => n * 2);
- }
-
- let First = setComponentTemplate(
- precompileTemplate(`{{#each (array 1 2 3) as |n|}}[{{n}}]{{/each}}`, {
- strictMode: true,
- scope: () => ({ array }),
- }),
- templateOnly()
- );
-
- let Root = setComponentTemplate(
- precompileTemplate(
- `{{#let shadowArray as |array|}}{{#each (array 5 10 15) as |n|}}[{{n}}]{{/each}}{{/let}} `,
- { strictMode: true, scope: () => ({ shadowArray: array, First }) }
- ),
- templateOnly()
- );
-
- this.renderComponent(Root, { expect: '[10][20][30][2][4][6]' });
- }
-
- ['@test can have more than one value']() {
- this.render(strip`
- {{#let (array "Sergio" "Robert") as |people|}}
- {{#each people as |personName|}}
- {{personName}},
- {{/each}}
- {{/let}}`);
-
- this.assertText('Sergio,Robert,');
-
- this.assertStableRerender();
- }
-
- ['@test binds values when variables are used']() {
- this.render(
- strip`{{#let (array this.personOne) as |people|}}
- {{#each people as |personName|}}
- {{personName}}
- {{/each}}
- {{/let}}`,
- {
- personOne: 'Tom',
- }
- );
-
- this.assertText('Tom');
-
- this.assertStableRerender();
-
- runTask(() => set(this.context, 'personOne', 'Yehuda'));
- this.assertText('Yehuda');
-
- runTask(() => set(this.context, 'personOne', 'Tom'));
- this.assertText('Tom');
- }
-
- ['@test binds multiple values when variables are used']() {
- this.render(
- strip`{{#let (array this.personOne this.personTwo) as |people|}}
- {{#each people as |personName|}}
- {{personName}},
- {{/each}}
- {{/let}}`,
- {
- personOne: 'Tom',
- personTwo: 'Yehuda',
- }
- );
-
- this.assertText('Tom,Yehuda,');
-
- this.assertStableRerender();
-
- runTask(() => set(this.context, 'personOne', 'Sergio'));
-
- this.assertText('Sergio,Yehuda,');
-
- runTask(() => set(this.context, 'personTwo', 'Tom'));
-
- this.assertText('Sergio,Tom,');
-
- runTask(() => {
- set(this.context, 'personOne', 'Tom');
- set(this.context, 'personTwo', 'Yehuda');
- });
-
- this.assertText('Tom,Yehuda,');
- }
-
- ['@test array helpers can be nested']() {
- this.render(
- strip`{{#let (array (array this.personOne this.personTwo)) as |listOfPeople|}}
- {{#each listOfPeople as |people|}}
- List:
- {{#each people as |personName|}}
- {{personName}},
- {{/each}}
- {{/each}}
- {{/let}}`,
- {
- personOne: 'Tom',
- personTwo: 'Yehuda',
- }
- );
-
- this.assertText('List:Tom,Yehuda,');
-
- this.assertStableRerender();
-
- runTask(() => set(this.context, 'personOne', 'Chad'));
-
- this.assertText('List:Chad,Yehuda,');
-
- runTask(() => set(this.context, 'personTwo', 'Balint'));
-
- this.assertText('List:Chad,Balint,');
-
- runTask(() => {
- set(this.context, 'personOne', 'Tom');
- set(this.context, 'personTwo', 'Yehuda');
- });
-
- this.assertText('List:Tom,Yehuda,');
- }
-
- ['@test should yield hash of an array of internal properties']() {
- let fooBarInstance;
- let FooBarComponent = class extends Component {
- constructor(owner, args) {
- super(owner, args);
- fooBarInstance = this;
- this.model = { personOne: 'Chad' };
- }
- };
-
- this.owner.register(
- 'component:foo-bar',
- setComponentTemplate(
- precompileTemplate(`{{yield (hash people=(array this.model.personOne))}}`),
- FooBarComponent
- )
- );
-
- this.render(strip`
- {{#foo-bar as |values|}}
- {{#each values.people as |personName|}}
- {{personName}}
- {{/each}}
- {{/foo-bar}}`);
-
- this.assertText('Chad');
-
- this.assertStableRerender();
-
- runTask(() => set(fooBarInstance, 'model.personOne', 'Godfrey'));
-
- this.assertText('Godfrey');
-
- runTask(() => set(fooBarInstance, 'model', { personOne: 'Chad' }));
-
- this.assertText('Chad');
-
- runTask(() => set(fooBarInstance, 'model.personOne', 'Godfrey'));
-
- this.assertText('Godfrey');
- }
-
- ['@test should yield hash of an array of internal and external properties']() {
- let fooBarInstance;
- let FooBarComponent = class extends Component {
- constructor(owner, args) {
- super(owner, args);
- fooBarInstance = this;
- this.model = { personOne: 'Chad' };
- }
- };
-
- this.owner.register(
- 'component:foo-bar',
- setComponentTemplate(
- precompileTemplate(`{{yield (hash people=(array this.model.personOne @personTwo))}}`),
- FooBarComponent
- )
- );
-
- this.render(
- strip`
- {{#foo-bar personTwo=this.model.personTwo as |values|}}
- {{#each values.people as |personName|}}
- {{personName}},
- {{/each}}
- {{/foo-bar}}`,
- {
- model: { personTwo: 'Tom' },
- }
- );
-
- this.assertText('Chad,Tom,');
-
- this.assertStableRerender();
-
- runTask(() => {
- set(fooBarInstance, 'model.personOne', 'Godfrey');
- set(this.context, 'model.personTwo', 'Yehuda');
- });
-
- this.assertText('Godfrey,Yehuda,');
-
- runTask(() => {
- set(fooBarInstance, 'model', { personOne: 'Chad' });
- set(this.context, 'model', { personTwo: 'Tom' });
- });
-
- this.assertText('Chad,Tom,');
- }
-
- ['@test should render when passing as argument to a component invocation']() {
- let FooBarComponent = class extends Component {};
-
- this.owner.register(
- 'component:foo-bar',
- setComponentTemplate(
- precompileTemplate('{{#each @people as |personName|}}{{personName}},{{/each}}'),
- FooBarComponent
- )
- );
-
- this.render(strip`{{foo-bar people=(array "Tom" this.personTwo)}}`, { personTwo: 'Chad' });
-
- this.assertText('Tom,Chad,');
-
- this.assertStableRerender();
-
- runTask(() => set(this.context, 'personTwo', 'Godfrey'));
-
- this.assertText('Tom,Godfrey,');
-
- runTask(() => set(this.context, 'personTwo', 'Chad'));
-
- this.assertText('Tom,Chad,');
- }
-
- ['@test should return an entirely new array when any argument change']() {
- let fooBarInstance;
- let FooBarComponent = class extends Component {
- constructor(owner, args) {
- super(owner, args);
- fooBarInstance = this;
- }
- };
-
- this.owner.register(
- 'component:foo-bar',
- setComponentTemplate(
- precompileTemplate('{{#each @people as |personName|}}{{personName}},{{/each}}'),
- FooBarComponent
- )
- );
-
- this.render(strip`{{foo-bar people=(array "Tom" this.personTwo)}}`, { personTwo: 'Chad' });
-
- let firstArray = fooBarInstance.args.people;
-
- runTask(() => set(this.context, 'personTwo', 'Godfrey'));
-
- this.assert.ok(
- firstArray !== fooBarInstance.args.people,
- 'should have created an entirely new array'
- );
- }
-
- ['@test capture array values in JS to assert deep equal']() {
- let captured;
-
- this.registerHelper('capture', function ([array]) {
- captured = array;
- return 'captured';
- });
-
- this.render(`{{capture (array 'Tom' this.personTwo)}}`, { personTwo: 'Godfrey' });
-
- this.assert.deepEqual(captured, ['Tom', 'Godfrey']);
-
- runTask(() => set(this.context, 'personTwo', 'Robert'));
-
- this.assert.deepEqual(captured, ['Tom', 'Robert']);
-
- runTask(() => set(this.context, 'personTwo', 'Godfrey'));
-
- this.assert.deepEqual(captured, ['Tom', 'Godfrey']);
- }
-
- ['@test GH18693 properties in hash can be accessed from the array']() {
- this.render(strip`
- {{#each (array (hash some="thing")) as |item|}}
- {{item.some}}
- {{/each}}`);
-
- this.assertText('thing');
- }
- }
-);
diff --git a/packages/@ember/-internals/glimmer/tests/integration/helpers/concat-test.gjs b/packages/@ember/-internals/glimmer/tests/integration/helpers/concat-test.gjs
new file mode 100644
index 00000000000..9fd9f045854
--- /dev/null
+++ b/packages/@ember/-internals/glimmer/tests/integration/helpers/concat-test.gjs
@@ -0,0 +1,107 @@
+import { tracked } from '@glimmer/tracking';
+import { module, test } from 'qunit';
+import { setupRenderingTest } from 'ember-qunit';
+import { render, settled } from '@ember/test-helpers';
+import { concat } from '@ember/helper';
+
+class Model {
+ @tracked first;
+ @tracked second;
+ @tracked third;
+ @tracked fourth;
+
+ constructor({ first, second, third, fourth }) {
+ this.first = first;
+ this.second = second;
+ this.third = third;
+ this.fourth = fourth;
+ }
+}
+
+class State {
+ @tracked model;
+
+ constructor(model) {
+ this.model = new Model(model);
+ }
+}
+
+module('Helpers test: {{concat}}', function (hooks) {
+ setupRenderingTest(hooks);
+
+ test('it concats static arguments', async function (assert) {
+ await render({{concat "foo" " " "bar" " " "baz"}} );
+
+ await assert.stableRender('foo bar baz');
+ });
+
+ test('it updates for bound arguments', async function (assert) {
+ let state = new State({ first: 'one', second: 'two' });
+
+ await render({{concat state.model.first state.model.second}} );
+
+ await assert.stableRender('onetwo');
+
+ state.model.first = 'three';
+ await settled();
+
+ assert.dom().hasText('threetwo');
+
+ state.model.second = 'four';
+ await settled();
+
+ assert.dom().hasText('threefour');
+
+ state.model = new Model({ first: 'one', second: 'two' });
+ await settled();
+
+ assert.dom().hasText('onetwo');
+ });
+
+ test('it can be used as a sub-expression', async function (assert) {
+ let state = new State({ first: 'one', second: 'two', third: 'three', fourth: 'four' });
+
+ await render(
+ {{concat (concat state.model.first state.model.second) (concat state.model.third state.model.fourth)}}
+ );
+
+ await assert.stableRender('onetwothreefour');
+
+ state.model.first = 'five';
+ await settled();
+
+ assert.dom().hasText('fivetwothreefour');
+
+ state.model.second = 'six';
+ state.model.third = 'seven';
+ await settled();
+
+ assert.dom().hasText('fivesixsevenfour');
+
+ state.model = new Model({ first: 'one', second: 'two', third: 'three', fourth: 'four' });
+ await settled();
+
+ assert.dom().hasText('onetwothreefour');
+ });
+
+ test('it can be used as input for other helpers', async function (assert) {
+ let state = new State({ first: 'one', second: 'two' });
+ let eq = (actual, expected) => actual === expected;
+
+ await render(
+ {{#if (eq (concat state.model.first state.model.second) "onetwo")}}Truthy!{{else}}False{{/if}}
+ );
+
+ await assert.stableRender('Truthy!');
+
+ state.model.first = 'three';
+ await settled();
+
+ assert.dom().hasText('False');
+
+ state.model = new Model({ first: 'one', second: 'two' });
+ await settled();
+
+ assert.dom().hasText('Truthy!');
+ });
+});
diff --git a/packages/@ember/-internals/glimmer/tests/integration/helpers/concat-test.js b/packages/@ember/-internals/glimmer/tests/integration/helpers/concat-test.js
deleted file mode 100644
index 7f1565e6220..00000000000
--- a/packages/@ember/-internals/glimmer/tests/integration/helpers/concat-test.js
+++ /dev/null
@@ -1,107 +0,0 @@
-import { RenderingTestCase, moduleFor, runTask } from 'internal-test-helpers';
-
-import { set } from '@ember/object';
-
-moduleFor(
- 'Helpers test: {{concat}}',
- class extends RenderingTestCase {
- ['@test it concats static arguments']() {
- this.render(`{{concat "foo" " " "bar" " " "baz"}}`);
- this.assertText('foo bar baz');
- }
-
- ['@test it updates for bound arguments']() {
- this.render(`{{concat this.model.first this.model.second}}`, {
- model: { first: 'one', second: 'two' },
- });
-
- this.assertText('onetwo');
-
- runTask(() => this.rerender());
-
- this.assertText('onetwo');
-
- runTask(() => set(this.context, 'model.first', 'three'));
-
- this.assertText('threetwo');
-
- runTask(() => set(this.context, 'model.second', 'four'));
-
- this.assertText('threefour');
-
- runTask(() => set(this.context, 'model', { first: 'one', second: 'two' }));
-
- this.assertText('onetwo');
- }
-
- ['@test it can be used as a sub-expression']() {
- this.render(
- `{{concat (concat this.model.first this.model.second) (concat this.model.third this.model.fourth)}}`,
- {
- model: {
- first: 'one',
- second: 'two',
- third: 'three',
- fourth: 'four',
- },
- }
- );
-
- this.assertText('onetwothreefour');
-
- runTask(() => this.rerender());
-
- this.assertText('onetwothreefour');
-
- runTask(() => set(this.context, 'model.first', 'five'));
-
- this.assertText('fivetwothreefour');
-
- runTask(() => {
- set(this.context, 'model.second', 'six');
- set(this.context, 'model.third', 'seven');
- });
-
- this.assertText('fivesixsevenfour');
-
- runTask(() => {
- set(this.context, 'model', {
- first: 'one',
- second: 'two',
- third: 'three',
- fourth: 'four',
- });
- });
-
- this.assertText('onetwothreefour');
- }
-
- ['@test it can be used as input for other helpers']() {
- this.registerHelper('x-eq', ([actual, expected]) => actual === expected);
-
- this.render(
- `{{#if (x-eq (concat this.model.first this.model.second) "onetwo")}}Truthy!{{else}}False{{/if}}`,
- {
- model: {
- first: 'one',
- second: 'two',
- },
- }
- );
-
- this.assertText('Truthy!');
-
- runTask(() => this.rerender());
-
- this.assertText('Truthy!');
-
- runTask(() => set(this.context, 'model.first', 'three'));
-
- this.assertText('False');
-
- runTask(() => set(this.context, 'model', { first: 'one', second: 'two' }));
-
- this.assertText('Truthy!');
- }
- }
-);
diff --git a/packages/@ember/-internals/glimmer/tests/integration/helpers/default-helper-manager-test.gjs b/packages/@ember/-internals/glimmer/tests/integration/helpers/default-helper-manager-test.gjs
new file mode 100644
index 00000000000..bcf3771e213
--- /dev/null
+++ b/packages/@ember/-internals/glimmer/tests/integration/helpers/default-helper-manager-test.gjs
@@ -0,0 +1,148 @@
+import Component from '@glimmer/component';
+import { tracked } from '@glimmer/tracking';
+import { module, test } from 'qunit';
+import { setupRenderingTest } from 'ember-qunit';
+import { render, settled } from '@ember/test-helpers';
+import { action } from '@ember/object';
+
+class State {
+ @tracked foo = 123;
+}
+
+module('Helpers test: default helper manager', function (hooks) {
+ setupRenderingTest(hooks);
+
+ test('plain functions can be used as helpers', async function (assert) {
+ function hello() {
+ return 'hello';
+ }
+
+ await render({{(hello)}} );
+
+ await assert.stableRender('hello');
+ });
+
+ test('positional arguments are passed as function arguments', async function (assert) {
+ function hello(...args) {
+ assert.deepEqual(args, [1, 2, 3]);
+ return args.length;
+ }
+
+ await render({{(hello 1 2 3)}} );
+
+ assert.dom().hasText('3');
+ });
+
+ test('tracks changes to positional arguments', async function (assert) {
+ let state = new State();
+ let count = 0;
+
+ function hello(firstArgument) {
+ count++;
+ return firstArgument;
+ }
+
+ await render({{(hello state.foo)}} );
+
+ assert.strictEqual(count, 1, 'rendered once');
+ await assert.stableRender('123');
+ assert.strictEqual(count, 1, 'rendered once');
+
+ state.foo = 456;
+ await settled();
+
+ assert.strictEqual(count, 2, 'rendered twice');
+ assert.dom().hasText('456');
+ });
+
+ test('named arguments are passed as the last function argument', async function (assert) {
+ function hello(positional, named) {
+ assert.strictEqual(positional, 'foo');
+
+ return named.foo;
+ }
+
+ await render({{(hello 'foo' foo='bar')}} );
+
+ assert.dom().hasText('bar');
+ });
+
+ test('tracks changes to named arguments', async function (assert) {
+ let state = new State();
+ let count = 0;
+
+ function hello(named) {
+ count++;
+ return named.foo;
+ }
+
+ await render({{(hello foo=state.foo)}} );
+
+ assert.strictEqual(count, 1, 'rendered once');
+ await assert.stableRender('123');
+ assert.strictEqual(count, 1, 'rendered once');
+
+ state.foo = 456;
+ await settled();
+
+ assert.strictEqual(count, 2, 'rendered twice');
+ assert.dom().hasText('456');
+ });
+
+ test('plain functions passed as component arguments can be used as helpers', async function (assert) {
+ function hello() {
+ return 'hello';
+ }
+
+ let FooBar = {{(@hello)}} ;
+
+ await render( );
+
+ assert.dom().hasText('hello');
+ });
+
+ test('plain functions stored as class properties can be used as helpers', async function (assert) {
+ class FooBar extends Component {
+ hello = () => {
+ return 'hello';
+ };
+
+ {{(this.hello)}}
+ }
+
+ await render( );
+
+ assert.dom().hasText('hello');
+ });
+
+ test('class methods can be used as helpers', async function (assert) {
+ class FooBar extends Component {
+ hello() {
+ return 'hello';
+ }
+
+ {{(this.hello)}}
+ }
+
+ await render( );
+
+ assert.dom().hasText('hello');
+ });
+
+ test('actions can be used as helpers', async function (assert) {
+ class FooBar extends Component {
+ someProperty = 'hello';
+
+ @action
+ hello() {
+ return this.someProperty;
+ }
+
+ {{(this.hello)}}
+ }
+
+ await render( );
+
+ assert.dom().hasText('hello');
+ });
+});
diff --git a/packages/@ember/-internals/glimmer/tests/integration/helpers/default-helper-manager-test.js b/packages/@ember/-internals/glimmer/tests/integration/helpers/default-helper-manager-test.js
deleted file mode 100644
index 2c7f457659b..00000000000
--- a/packages/@ember/-internals/glimmer/tests/integration/helpers/default-helper-manager-test.js
+++ /dev/null
@@ -1,172 +0,0 @@
-import { RenderingTestCase, moduleFor, runTask } from 'internal-test-helpers';
-import { setComponentTemplate } from '@glimmer/manager';
-import Component from '@glimmer/component';
-import { precompileTemplate } from '@ember/template-compilation';
-import { set } from '@ember/object';
-import { action } from '@ember/object';
-
-moduleFor(
- 'Helpers test: default helper manager',
- class extends RenderingTestCase {
- '@test plain functions can be used as helpers'() {
- function hello() {
- return 'hello';
- }
-
- this.render('{{(this.hello)}}', {
- hello,
- });
- this.assertText('hello');
-
- runTask(() => this.rerender());
- this.assertText('hello');
- }
-
- '@test positional arguments are passed as function arguments'(assert) {
- function hello(...args) {
- assert.deepEqual(args, [1, 2, 3]);
- return args.length;
- }
-
- this.render('{{(this.hello 1 2 3)}}', {
- hello,
- });
- this.assertText('3');
- }
-
- '@test tracks changes to positional arguments'(assert) {
- let count = 0;
-
- function hello(firstArgument) {
- count++;
- return firstArgument;
- }
-
- this.render('{{(this.hello this.foo)}}', {
- hello,
- foo: 123,
- });
-
- assert.strictEqual(count, 1, 'rendered once');
- this.assertText('123');
-
- runTask(() => this.rerender());
- assert.equal(count, 1, 'rendered once');
- this.assertText('123');
-
- runTask(() => set(this.context, 'foo', 456));
-
- assert.equal(count, 2, 'rendered twice');
- this.assertText('456');
- }
-
- '@test named arguments are passed as the last function argument'(assert) {
- function hello(positional, named) {
- assert.strictEqual(positional, 'foo');
-
- return named.foo;
- }
-
- this.render('{{(this.hello "foo" foo="bar")}}', {
- hello,
- });
- this.assertText('bar');
- }
-
- '@test tracks changes to named arguments'(assert) {
- let count = 0;
-
- function hello(named) {
- count++;
- return named.foo;
- }
-
- this.render('{{(this.hello foo=this.foo)}}', {
- hello,
- foo: 123,
- });
-
- assert.strictEqual(count, 1, 'rendered once');
- this.assertText('123');
-
- runTask(() => this.rerender());
- assert.equal(count, 1, 'rendered once');
- this.assertText('123');
-
- runTask(() => set(this.context, 'foo', 456));
-
- assert.equal(count, 2, 'rendered twice');
- this.assertText('456');
- }
-
- '@test plain functions passed as component arguments can be used as helpers'() {
- function hello() {
- return 'hello';
- }
-
- this.owner.register(
- 'component:foo-bar',
- setComponentTemplate(precompileTemplate('{{(@hello)}}'), class extends Component {})
- );
-
- this.render(` `, {
- hello,
- });
- this.assertText('hello');
- }
-
- '@test plain functions stored as class properties can be used as helpers'() {
- this.owner.register(
- 'component:foo-bar',
- setComponentTemplate(
- precompileTemplate('{{(this.hello)}}'),
- class extends Component {
- hello = () => {
- return 'hello';
- };
- }
- )
- );
-
- this.render(` `);
- this.assertText('hello');
- }
-
- '@test class methods can be used as helpers'() {
- this.owner.register(
- 'component:foo-bar',
- setComponentTemplate(
- precompileTemplate('{{(this.hello)}}'),
- class extends Component {
- hello() {
- return 'hello';
- }
- }
- )
- );
-
- this.render(` `);
- this.assertText('hello');
- }
-
- '@test actions can be used as helpers'() {
- this.owner.register(
- 'component:foo-bar',
- setComponentTemplate(
- precompileTemplate('{{(this.hello)}}'),
- class extends Component {
- someProperty = 'hello';
-
- @action
- hello() {
- return this.someProperty;
- }
- }
- )
- );
-
- this.render(` `);
- this.assertText('hello');
- }
- }
-);
diff --git a/packages/@ember/-internals/glimmer/tests/integration/helpers/element-test.gjs b/packages/@ember/-internals/glimmer/tests/integration/helpers/element-test.gjs
new file mode 100644
index 00000000000..eccc8bc6e6c
--- /dev/null
+++ b/packages/@ember/-internals/glimmer/tests/integration/helpers/element-test.gjs
@@ -0,0 +1,253 @@
+import { DEBUG } from '@glimmer/env';
+import { tracked } from '@glimmer/tracking';
+import { module, test } from 'qunit';
+import { setupRenderingTest } from 'ember-qunit';
+import { render, settled } from '@ember/test-helpers';
+import { element, hash } from '@ember/helper';
+import { on } from '@ember/modifier';
+
+module('Helpers test: {{element}}', function (hooks) {
+ setupRenderingTest(hooks);
+
+ test('it renders a tag with the given tag name', async function (assert) {
+ await render(
+
+ {{#let (element 'h1') as |Tag|}}
+ hello world!
+ {{/let}}
+
+ );
+
+ assert.dom().hasHtml('hello world! ');
+ });
+
+ test('it does not render any tags when passed an empty string', async function (assert) {
+ await render(
+
+ {{#let (element '') as |Tag|}}
+ hello world!
+ {{/let}}
+
+ );
+
+ assert.dom().hasHtml('hello world!');
+ });
+
+ test('it throws when passed null', async function (assert) {
+ if (!DEBUG) {
+ assert.expect(0);
+ return;
+ }
+
+ let nil = null;
+
+ await assert.rejects(
+ render(
+
+ {{#let (element nil) as |Tag|}}hello {{/let}}
+
+ ),
+ /The argument passed to the `element` helper must be a string/
+ );
+ });
+
+ test('it throws when passed undefined', async function (assert) {
+ if (!DEBUG) {
+ assert.expect(0);
+ return;
+ }
+
+ let undef = undefined;
+
+ await assert.rejects(
+ render(
+
+ {{#let (element undef) as |Tag|}}hello {{/let}}
+
+ ),
+ /The argument passed to the `element` helper must be a string/
+ );
+ });
+
+ test('it works with element modifiers', async function (assert) {
+ let didClick = () => {};
+
+ await render(
+
+ {{#let (element 'button') as |Tag|}}
+ hello world!
+ {{/let}}
+
+ );
+
+ assert.dom().hasHtml('hello world! ');
+ });
+
+ test('it can be rendered multiple times', async function (assert) {
+ await render(
+
+ {{#let (element 'h1') as |Tag|}}
+ hello
+ world
+ !!!!!
+ {{/let}}
+
+ );
+
+ assert
+ .dom()
+ .hasHtml(
+ 'hello world !!!!! '
+ );
+ });
+
+ test('it renders when the tag name changes', async function (assert) {
+ class State {
+ @tracked htmlTag = 'h1';
+ }
+
+ let state = new State();
+
+ await render(
+
+ {{#let (element state.htmlTag) as |Tag|}}
+ hello
+ {{/let}}
+
+ );
+
+ assert.dom().hasHtml('hello ');
+
+ state.htmlTag = 'h2';
+ await settled();
+ assert.dom().hasHtml('hello ');
+
+ state.htmlTag = 'h3';
+ await settled();
+ assert.dom().hasHtml('hello ');
+
+ state.htmlTag = '';
+ await settled();
+ assert.dom().hasText('hello');
+
+ state.htmlTag = 'h1';
+ await settled();
+ assert.dom().hasHtml('hello ');
+ });
+
+ test('it can be passed as argument and works with ...attributes', async function (assert) {
+ let Inner =
+ {{#let @tag as |Tag|}}
+ {{yield}}
+ {{/let}}
+ ;
+
+ await render(
+
+
+
+ );
+
+ assert.dom().hasHtml('');
+ });
+
+ test('it requires at least one argument', async function (assert) {
+ if (!DEBUG) {
+ assert.expect(0);
+ return;
+ }
+
+ await assert.rejects(
+ render(
+
+ {{#let (element) as |Tag|}}hello {{/let}}
+
+ ),
+ /The `element` helper takes a single positional argument/
+ );
+ });
+
+ test('it requires no more than one argument', async function (assert) {
+ if (!DEBUG) {
+ assert.expect(0);
+ return;
+ }
+
+ await assert.rejects(
+ render(
+
+ {{#let (element 'h1' 'h2') as |Tag|}}hello {{/let}}
+
+ ),
+ /The `element` helper takes a single positional argument/
+ );
+ });
+
+ test('it does not take any named arguments', async function (assert) {
+ if (!DEBUG) {
+ assert.expect(0);
+ return;
+ }
+
+ await assert.rejects(
+ render(
+
+ {{#let (element 'h1' id='content') as |Tag|}}hello {{/let}}
+
+ ),
+ /The `element` helper does not take any named arguments/
+ );
+ });
+
+ test('it throws when passed a number', async function (assert) {
+ if (!DEBUG) {
+ assert.expect(0);
+ return;
+ }
+
+ let num = 123;
+
+ await assert.rejects(
+ render(
+
+ {{#let (element num) as |Tag|}}hello {{/let}}
+
+ ),
+ /The argument passed to the `element` helper must be a string \(you passed `123`\)/
+ );
+ });
+
+ test('it throws when passed a boolean', async function (assert) {
+ if (!DEBUG) {
+ assert.expect(0);
+ return;
+ }
+
+ let bool = false;
+
+ await assert.rejects(
+ render(
+
+ {{#let (element bool) as |Tag|}}hello {{/let}}
+
+ ),
+ /The argument passed to the `element` helper must be a string \(you passed `false`\)/
+ );
+ });
+
+ test('it throws when passed an object', async function (assert) {
+ if (!DEBUG) {
+ assert.expect(0);
+ return;
+ }
+
+ await assert.rejects(
+ render(
+
+ {{#let (element (hash)) as |Tag|}}hello {{/let}}
+
+ ),
+ /The argument passed to the `element` helper must be a string/
+ );
+ });
+});
diff --git a/packages/@ember/-internals/glimmer/tests/integration/helpers/element-test.js b/packages/@ember/-internals/glimmer/tests/integration/helpers/element-test.js
deleted file mode 100644
index 32618bf1869..00000000000
--- a/packages/@ember/-internals/glimmer/tests/integration/helpers/element-test.js
+++ /dev/null
@@ -1,207 +0,0 @@
-import { DEBUG } from '@glimmer/env';
-import { tracked } from '@glimmer/tracking';
-import { RenderingTestCase, moduleFor, runTask } from 'internal-test-helpers';
-import { element as elementHelper, hash } from '@ember/helper';
-import { on } from '@ember/modifier';
-import { template } from '@ember/template-compiler/runtime';
-
-moduleFor(
- 'Helpers test: {{element}}',
- class extends RenderingTestCase {
- '@test it renders a tag with the given tag name'() {
- let AComponent = template(
- `{{#let (element "h1") as |Tag|}}hello world! {{/let}}`,
- { scope: () => ({ element: elementHelper }) }
- );
- this.renderComponent(AComponent, { expect: 'hello world! ' });
- }
-
- '@test it does not render any tags when passed an empty string'() {
- let AComponent = template(
- `{{#let (element "") as |Tag|}}hello world! {{/let}}`,
- { scope: () => ({ element: elementHelper }) }
- );
- this.renderComponent(AComponent, { expect: 'hello world!' });
- }
-
- ['@test it throws when passed null']() {
- if (!DEBUG) {
- this.assert.expect(0);
- return;
- }
-
- let nil = null;
- this.assert.throws(() => {
- let AComponent = template(`{{#let (element nil) as |Tag|}}hello {{/let}}`, {
- scope: () => ({ element: elementHelper, nil }),
- });
- this.renderComponent(AComponent, { expect: '' });
- }, /The argument passed to the `element` helper must be a string/);
- }
-
- ['@test it throws when passed undefined']() {
- if (!DEBUG) {
- this.assert.expect(0);
- return;
- }
-
- let undef = undefined;
- this.assert.throws(() => {
- let AComponent = template(`{{#let (element undef) as |Tag|}}hello {{/let}}`, {
- scope: () => ({ element: elementHelper, undef }),
- });
- this.renderComponent(AComponent, { expect: '' });
- }, /The argument passed to the `element` helper must be a string/);
- }
-
- '@test it works with element modifiers'() {
- let didClick = () => {};
- let AComponent = template(
- `{{#let (element "button") as |Tag|}}hello world! {{/let}}`,
- { scope: () => ({ element: elementHelper, on, didClick }) }
- );
- this.renderComponent(AComponent, {
- expect: 'hello world! ',
- });
- }
-
- '@test it can be rendered multiple times'() {
- let AComponent = template(
- `{{#let (element "h1") as |Tag|}}hello world !!!!! {{/let}}`,
- { scope: () => ({ element: elementHelper }) }
- );
- this.renderComponent(AComponent, {
- expect:
- 'hello world !!!!! ',
- });
- }
-
- '@test it renders when the tag name changes'() {
- class State {
- @tracked htmlTag = 'h1';
- }
-
- let state = new State();
-
- let AComponent = template(
- `{{#let (element state.htmlTag) as |Tag|}}hello {{/let}}`,
- { scope: () => ({ element: elementHelper, state }) }
- );
- this.renderComponent(AComponent, {
- expect: 'hello ',
- });
-
- runTask(() => (state.htmlTag = 'h2'));
- this.assertHTML('hello ');
-
- runTask(() => (state.htmlTag = 'h3'));
- this.assertHTML('hello ');
-
- runTask(() => (state.htmlTag = ''));
- this.assertText('hello');
-
- runTask(() => (state.htmlTag = 'h1'));
- this.assertHTML('hello ');
- }
-
- '@test it can be passed as argument and works with ...attributes'() {
- let Inner = template(
- `{{#let @tag as |Tag|}}{{yield}} {{/let}}`,
- { scope: () => ({ element: elementHelper }) }
- );
-
- let Outer = template(``, {
- scope: () => ({ Inner, element: elementHelper }),
- });
-
- this.renderComponent(Outer, { expect: '' });
- }
-
- ['@test it requires at least one argument']() {
- if (!DEBUG) {
- this.assert.expect(0);
- return;
- }
-
- this.assert.throws(() => {
- let AComponent = template(`{{#let (element) as |Tag|}}hello {{/let}}`, {
- scope: () => ({ element: elementHelper }),
- });
- this.renderComponent(AComponent, { expect: '' });
- }, /The `element` helper takes a single positional argument/);
- }
-
- ['@test it requires no more than one argument']() {
- if (!DEBUG) {
- this.assert.expect(0);
- return;
- }
-
- this.assert.throws(() => {
- let AComponent = template(`{{#let (element "h1" "h2") as |Tag|}}hello {{/let}}`, {
- scope: () => ({ element: elementHelper }),
- });
- this.renderComponent(AComponent, { expect: '' });
- }, /The `element` helper takes a single positional argument/);
- }
-
- ['@test it does not take any named arguments']() {
- if (!DEBUG) {
- this.assert.expect(0);
- return;
- }
-
- this.assert.throws(() => {
- let AComponent = template(
- `{{#let (element "h1" id="content") as |Tag|}}hello {{/let}}`,
- { scope: () => ({ element: elementHelper }) }
- );
- this.renderComponent(AComponent, { expect: '' });
- }, /The `element` helper does not take any named arguments/);
- }
-
- ['@test it throws when passed a number']() {
- if (!DEBUG) {
- this.assert.expect(0);
- return;
- }
-
- let num = 123;
- this.assert.throws(() => {
- let AComponent = template(`{{#let (element num) as |Tag|}}hello {{/let}}`, {
- scope: () => ({ element: elementHelper, num }),
- });
- this.renderComponent(AComponent, { expect: '' });
- }, /The argument passed to the `element` helper must be a string \(you passed `123`\)/);
- }
-
- ['@test it throws when passed a boolean']() {
- if (!DEBUG) {
- this.assert.expect(0);
- return;
- }
-
- let bool = false;
- this.assert.throws(() => {
- let AComponent = template(`{{#let (element bool) as |Tag|}}hello {{/let}}`, {
- scope: () => ({ element: elementHelper, bool }),
- });
- this.renderComponent(AComponent, { expect: '' });
- }, /The argument passed to the `element` helper must be a string \(you passed `false`\)/);
- }
-
- ['@test it throws when passed an object']() {
- if (!DEBUG) {
- this.assert.expect(0);
- return;
- }
-
- this.assert.throws(() => {
- let AComponent = template(`{{#let (element (hash)) as |Tag|}}hello {{/let}}`, {
- scope: () => ({ element: elementHelper, hash }),
- });
- this.renderComponent(AComponent, { expect: '' });
- }, /The argument passed to the `element` helper must be a string/);
- }
- }
-);
diff --git a/packages/@ember/-internals/glimmer/tests/integration/helpers/fn-test.gjs b/packages/@ember/-internals/glimmer/tests/integration/helpers/fn-test.gjs
new file mode 100644
index 00000000000..d9a8625fe6f
--- /dev/null
+++ b/packages/@ember/-internals/glimmer/tests/integration/helpers/fn-test.gjs
@@ -0,0 +1,273 @@
+import Component from '@glimmer/component';
+import { DEBUG } from '@glimmer/env';
+import { tracked } from '@glimmer/tracking';
+import { module, test } from 'qunit';
+import { setupRenderingTest } from 'ember-qunit';
+import { render, settled } from '@ember/test-helpers';
+import { fn } from '@ember/helper';
+
+class State {
+ @tracked myFunc;
+ @tracked arg1;
+ @tracked arg2;
+ @tracked arg3;
+
+ constructor({ myFunc, arg1, arg2, arg3 }) {
+ this.myFunc = myFunc;
+ this.arg1 = arg1;
+ this.arg2 = arg2;
+ this.arg3 = arg3;
+ }
+}
+
+let stashedFn;
+
+class Stash extends Component {
+ constructor(owner, args) {
+ super(owner, args);
+ stashedFn = this.args.stashedFn;
+ }
+
+
+}
+
+function invoke(fn) {
+ return fn();
+}
+
+function boundFn(fn, ...args) {
+ return () => fn(...args.map((arg) => `bound:${arg}`));
+}
+
+let id = (arg) => arg;
+
+module('Helpers test: {{fn}}', function (hooks) {
+ setupRenderingTest(hooks);
+
+ hooks.beforeEach(function () {
+ stashedFn = undefined;
+ });
+
+ test('fn can be shadowed', async function (assert) {
+ let fn = boundFn;
+
+ await render(
+
+ [{{invoke (fn id 1)}}]{{#let boundFn as |fn|}}[{{invoke (fn id 2)}}{{/let}}]
+
+ );
+
+ await assert.stableRender('[bound:1][bound:2]');
+ });
+
+ test('updates when arguments change', async function (assert) {
+ let state = new State({
+ myFunc(arg1, arg2) {
+ return `arg1: ${arg1}, arg2: ${arg2}`;
+ },
+
+ arg1: 'foo',
+ arg2: 'bar',
+ });
+
+ await render({{invoke (fn state.myFunc state.arg1 state.arg2)}} );
+
+ await assert.stableRender('arg1: foo, arg2: bar');
+
+ state.arg1 = 'qux';
+ await settled();
+
+ assert.dom().hasText('arg1: qux, arg2: bar');
+
+ state.arg2 = 'derp';
+ await settled();
+
+ assert.dom().hasText('arg1: qux, arg2: derp');
+
+ state.arg1 = 'foo';
+ state.arg2 = 'bar';
+ await settled();
+
+ assert.dom().hasText('arg1: foo, arg2: bar');
+ });
+
+ test('updates when the function changes', async function (assert) {
+ let func1 = (arg1, arg2) => `arg1: ${arg1}, arg2: ${arg2}`;
+ let func2 = (arg1, arg2) => `arg2: ${arg2}, arg1: ${arg1}`;
+ let state = new State({ myFunc: func1, arg1: 'foo', arg2: 'bar' });
+
+ await render({{invoke (fn state.myFunc state.arg1 state.arg2)}} );
+
+ await assert.stableRender('arg1: foo, arg2: bar');
+
+ state.myFunc = func2;
+ await settled();
+
+ assert.dom().hasText('arg2: bar, arg1: foo');
+
+ state.myFunc = func1;
+ await settled();
+
+ assert.dom().hasText('arg1: foo, arg2: bar');
+ });
+
+ test('a stashed fn result update arguments when invoked', async function (assert) {
+ let state = new State({
+ myFunc(arg1, arg2) {
+ return `arg1: ${arg1}, arg2: ${arg2}`;
+ },
+
+ arg1: 'foo',
+ arg2: 'bar',
+ });
+
+ await render(
+
+ );
+
+ assert.strictEqual(stashedFn(), 'arg1: foo, arg2: bar');
+
+ state.arg1 = 'qux';
+ await settled();
+
+ assert.strictEqual(stashedFn(), 'arg1: qux, arg2: bar');
+
+ state.arg2 = 'derp';
+ await settled();
+
+ assert.strictEqual(stashedFn(), 'arg1: qux, arg2: derp');
+
+ state.arg1 = 'foo';
+ state.arg2 = 'bar';
+ await settled();
+
+ assert.strictEqual(stashedFn(), 'arg1: foo, arg2: bar');
+ });
+
+ test('a stashed fn result invokes the correct function when the bound function changes', async function (assert) {
+ let func1 = (arg1, arg2) => `arg1: ${arg1}, arg2: ${arg2}`;
+ let func2 = (arg1, arg2) => `arg2: ${arg2}, arg1: ${arg1}`;
+ let state = new State({ myFunc: func1, arg1: 'foo', arg2: 'bar' });
+
+ await render(
+
+ );
+
+ assert.strictEqual(stashedFn(), 'arg1: foo, arg2: bar');
+
+ state.myFunc = func2;
+ await settled();
+
+ assert.strictEqual(stashedFn(), 'arg2: bar, arg1: foo');
+
+ state.myFunc = func1;
+ await settled();
+
+ assert.strictEqual(stashedFn(), 'arg1: foo, arg2: bar');
+ });
+
+ test('there is no `this` context within the callback', async function (assert) {
+ if (DEBUG) {
+ assert.expect(0);
+ return;
+ }
+
+ let state = new State({
+ myFunc() {
+ assert.strictEqual(this, null, 'this is bound to null in production builds');
+ },
+ });
+
+ await render( );
+
+ stashedFn();
+ });
+
+ test('can use `this` if bound prior to passing to fn', async function (assert) {
+ let context = {
+ myFunc(arg1) {
+ return `arg1: ${arg1}, arg2: ${this.arg2}`;
+ },
+ get myFunc2() {
+ return this.myFunc.bind(this);
+ },
+
+ arg1: 'foo',
+ arg2: 'bar',
+ };
+
+ await render( );
+
+ assert.strictEqual(stashedFn(), 'arg1: foo, arg2: bar');
+ });
+
+ test('partially applies each layer when nested [GH#17959]', async function (assert) {
+ let state = new State({
+ myFunc(arg1, arg2, arg3) {
+ return `arg1: ${arg1}, arg2: ${arg2}, arg3: ${arg3}`;
+ },
+
+ arg1: 'foo',
+ arg2: 'bar',
+ arg3: 'qux',
+ });
+
+ await render(
+ {{invoke (fn (fn (fn state.myFunc state.arg1) state.arg2) state.arg3)}}
+ );
+
+ await assert.stableRender('arg1: foo, arg2: bar, arg3: qux');
+
+ state.arg1 = 'qux';
+ await settled();
+
+ assert.dom().hasText('arg1: qux, arg2: bar, arg3: qux');
+
+ state.arg2 = 'derp';
+ await settled();
+
+ assert.dom().hasText('arg1: qux, arg2: derp, arg3: qux');
+
+ state.arg3 = 'huzzah';
+ await settled();
+
+ assert.dom().hasText('arg1: qux, arg2: derp, arg3: huzzah');
+
+ state.arg1 = 'foo';
+ state.arg2 = 'bar';
+ state.arg3 = 'qux';
+ await settled();
+
+ assert.dom().hasText('arg1: foo, arg2: bar, arg3: qux');
+ });
+
+ test('can be used on the result of `mut`', async function (assert) {
+ let state = new State({ arg1: 'foo', arg2: 'bar' });
+
+ await render(
+ {{state.arg1}}
+ );
+
+ assert.dom().hasText('foo');
+
+ stashedFn();
+ await settled();
+
+ assert.dom().hasText('bar');
+ });
+
+ test('can be used on the result of `mut` with a falsy value', async function (assert) {
+ let state = new State({ arg1: 'foo', arg2: false });
+
+ await render(
+ {{state.arg1}}
+ );
+
+ assert.dom().hasText('foo');
+
+ stashedFn();
+ await settled();
+
+ assert.dom().hasText('false');
+ });
+});
diff --git a/packages/@ember/-internals/glimmer/tests/integration/helpers/fn-test.js b/packages/@ember/-internals/glimmer/tests/integration/helpers/fn-test.js
deleted file mode 100644
index 7818d763eda..00000000000
--- a/packages/@ember/-internals/glimmer/tests/integration/helpers/fn-test.js
+++ /dev/null
@@ -1,231 +0,0 @@
-import { set } from '@ember/object';
-import { DEBUG } from '@glimmer/env';
-import { RenderingTestCase, moduleFor, runTask } from 'internal-test-helpers';
-import { template } from '@ember/template-compiler/runtime';
-import Component from '@glimmer/component';
-
-moduleFor(
- 'Helpers test: {{fn}}',
- class extends RenderingTestCase {
- beforeEach() {
- this.registerHelper('invoke', function ([fn]) {
- return fn();
- });
-
- let testContext = this;
- this.owner.register(
- 'component:stash',
- class extends Component {
- constructor(owner, args) {
- super(owner, args);
- testContext.stashedFn = this.args.stashedFn;
- }
- }
- );
- }
-
- '@test fn can be shadowed'() {
- let First = template(
- `[{{invoke (fn id 1)}}]{{#let boundFn as |fn|}}[{{invoke (fn id 2)}}{{/let}}]`,
- { scope: () => ({ fn: boundFn, boundFn, id, invoke }) }
- );
- this.renderComponent(First, { expect: `[bound:1][bound:2]` });
- }
-
- '@test updates when arguments change'() {
- this.render(`{{invoke (fn this.myFunc this.arg1 this.arg2)}}`, {
- myFunc(arg1, arg2) {
- return `arg1: ${arg1}, arg2: ${arg2}`;
- },
-
- arg1: 'foo',
- arg2: 'bar',
- });
-
- this.assertText('arg1: foo, arg2: bar');
-
- this.assertStableRerender();
-
- runTask(() => set(this.context, 'arg1', 'qux'));
- this.assertText('arg1: qux, arg2: bar');
-
- runTask(() => set(this.context, 'arg2', 'derp'));
- this.assertText('arg1: qux, arg2: derp');
-
- runTask(() => {
- set(this.context, 'arg1', 'foo');
- set(this.context, 'arg2', 'bar');
- });
-
- this.assertText('arg1: foo, arg2: bar');
- }
-
- '@test updates when the function changes'() {
- let func1 = (arg1, arg2) => `arg1: ${arg1}, arg2: ${arg2}`;
- let func2 = (arg1, arg2) => `arg2: ${arg2}, arg1: ${arg1}`;
-
- this.render(`{{invoke (fn this.myFunc this.arg1 this.arg2)}}`, {
- myFunc: func1,
-
- arg1: 'foo',
- arg2: 'bar',
- });
-
- this.assertText('arg1: foo, arg2: bar');
- this.assertStableRerender();
-
- runTask(() => set(this.context, 'myFunc', func2));
- this.assertText('arg2: bar, arg1: foo');
-
- runTask(() => set(this.context, 'myFunc', func1));
- this.assertText('arg1: foo, arg2: bar');
- }
-
- '@test a stashed fn result update arguments when invoked'(assert) {
- this.render(`{{stash stashedFn=(fn this.myFunc this.arg1 this.arg2)}}`, {
- myFunc(arg1, arg2) {
- return `arg1: ${arg1}, arg2: ${arg2}`;
- },
-
- arg1: 'foo',
- arg2: 'bar',
- });
-
- assert.equal(this.stashedFn(), 'arg1: foo, arg2: bar');
-
- runTask(() => set(this.context, 'arg1', 'qux'));
- assert.equal(this.stashedFn(), 'arg1: qux, arg2: bar');
-
- runTask(() => set(this.context, 'arg2', 'derp'));
- assert.equal(this.stashedFn(), 'arg1: qux, arg2: derp');
-
- runTask(() => {
- set(this.context, 'arg1', 'foo');
- set(this.context, 'arg2', 'bar');
- });
-
- assert.equal(this.stashedFn(), 'arg1: foo, arg2: bar');
- }
-
- '@test a stashed fn result invokes the correct function when the bound function changes'(
- assert
- ) {
- let func1 = (arg1, arg2) => `arg1: ${arg1}, arg2: ${arg2}`;
- let func2 = (arg1, arg2) => `arg2: ${arg2}, arg1: ${arg1}`;
-
- this.render(`{{stash stashedFn=(fn this.myFunc this.arg1 this.arg2)}}`, {
- myFunc: func1,
-
- arg1: 'foo',
- arg2: 'bar',
- });
-
- assert.equal(this.stashedFn(), 'arg1: foo, arg2: bar');
-
- runTask(() => set(this.context, 'myFunc', func2));
- assert.equal(this.stashedFn(), 'arg2: bar, arg1: foo');
-
- runTask(() => set(this.context, 'myFunc', func1));
- assert.equal(this.stashedFn(), 'arg1: foo, arg2: bar');
- }
-
- '@test there is no `this` context within the callback'(assert) {
- if (DEBUG) {
- assert.expect(0);
- return;
- }
-
- this.render(`{{stash stashedFn=(fn this.myFunc this.arg1)}}`, {
- myFunc() {
- assert.strictEqual(this, null, 'this is bound to null in production builds');
- },
- });
-
- this.stashedFn();
- }
-
- '@test can use `this` if bound prior to passing to fn'(assert) {
- this.render(`{{stash stashedFn=(fn this.myFunc2 this.arg1)}}`, {
- myFunc(arg1) {
- return `arg1: ${arg1}, arg2: ${this.arg2}`;
- },
- get myFunc2() {
- return this.myFunc.bind(this);
- },
-
- arg1: 'foo',
- arg2: 'bar',
- });
-
- assert.equal(this.stashedFn(), 'arg1: foo, arg2: bar');
- }
-
- '@test partially applies each layer when nested [GH#17959]'() {
- this.render(`{{invoke (fn (fn (fn this.myFunc this.arg1) this.arg2) this.arg3)}}`, {
- myFunc(arg1, arg2, arg3) {
- return `arg1: ${arg1}, arg2: ${arg2}, arg3: ${arg3}`;
- },
-
- arg1: 'foo',
- arg2: 'bar',
- arg3: 'qux',
- });
-
- this.assertText('arg1: foo, arg2: bar, arg3: qux');
- this.assertStableRerender();
-
- runTask(() => set(this.context, 'arg1', 'qux'));
- this.assertText('arg1: qux, arg2: bar, arg3: qux');
-
- runTask(() => set(this.context, 'arg2', 'derp'));
- this.assertText('arg1: qux, arg2: derp, arg3: qux');
-
- runTask(() => set(this.context, 'arg3', 'huzzah'));
- this.assertText('arg1: qux, arg2: derp, arg3: huzzah');
-
- runTask(() => {
- set(this.context, 'arg1', 'foo');
- set(this.context, 'arg2', 'bar');
- set(this.context, 'arg3', 'qux');
- });
-
- this.assertText('arg1: foo, arg2: bar, arg3: qux');
- }
-
- '@test can be used on the result of `mut`'() {
- this.render(`{{this.arg1}}{{stash stashedFn=(fn (mut this.arg1) this.arg2)}}`, {
- arg1: 'foo',
- arg2: 'bar',
- });
-
- this.assertText('foo');
-
- runTask(() => this.stashedFn());
-
- this.assertText('bar');
- }
-
- '@test can be used on the result of `mut` with a falsy value'() {
- this.render(`{{this.arg1}}{{stash stashedFn=(fn (mut this.arg1) this.arg2)}}`, {
- arg1: 'foo',
- arg2: false,
- });
-
- this.assertText('foo');
-
- runTask(() => this.stashedFn());
-
- this.assertText('false');
- }
- }
-);
-
-function invoke(fn) {
- return fn();
-}
-
-function boundFn(fn, ...args) {
- return () => fn(...args.map((arg) => `bound:${arg}`));
-}
-
-let id = (arg) => arg;
diff --git a/packages/@ember/-internals/glimmer/tests/integration/helpers/get-test.js b/packages/@ember/-internals/glimmer/tests/integration/helpers/get-test.js
index f9224eb1412..33271c8f141 100644
--- a/packages/@ember/-internals/glimmer/tests/integration/helpers/get-test.js
+++ b/packages/@ember/-internals/glimmer/tests/integration/helpers/get-test.js
@@ -6,7 +6,6 @@ import { precompileTemplate } from '@ember/template-compilation';
import { setComponentTemplate } from '@glimmer/manager';
import Component from '@glimmer/component';
-import GlimmerishComponent from '../../utils/glimmerish-component';
moduleFor(
'Helpers test: {{get}}',
@@ -624,7 +623,7 @@ moduleFor(
}
'@test should be able to get an object value with a path from this.args in a glimmer component'() {
- class PersonComponent extends GlimmerishComponent {
+ class PersonComponent extends Component {
options = ['first', 'last', 'age'];
}
diff --git a/packages/@ember/-internals/glimmer/tests/integration/helpers/hash-test.gjs b/packages/@ember/-internals/glimmer/tests/integration/helpers/hash-test.gjs
new file mode 100644
index 00000000000..a94de2881c8
--- /dev/null
+++ b/packages/@ember/-internals/glimmer/tests/integration/helpers/hash-test.gjs
@@ -0,0 +1,250 @@
+import Component from '@glimmer/component';
+import { tracked } from '@glimmer/tracking';
+import { module, test } from 'qunit';
+import { setupRenderingTest } from 'ember-qunit';
+import { render, settled } from '@ember/test-helpers';
+import { hash } from '@ember/helper';
+import { computed } from '@ember/object';
+
+class Model {
+ @tracked firstName;
+ @tracked lastName;
+
+ constructor({ firstName, lastName }) {
+ this.firstName = firstName;
+ this.lastName = lastName;
+ }
+}
+
+class State {
+ @tracked model;
+
+ constructor(model) {
+ this.model = new Model(model);
+ }
+}
+
+module('Helpers test: {{hash}}', function (hooks) {
+ setupRenderingTest(hooks);
+
+ test('returns a hash with the right key-value', async function (assert) {
+ await render(
+ {{#let (hash name='Sergio') as |person|}}{{person.name}}{{/let}}
+ );
+
+ await assert.stableRender('Sergio');
+ });
+
+ test('can be shadowed', async function (assert) {
+ let hash = (obj) =>
+ Object.entries(obj)
+ .map(([key, value]) => `hash:${key}=${value}`)
+ .join(',');
+ let shadowHash = hash;
+
+ await render(
+
+ ({{hash apple='red' banana='yellow'}})
+ {{#let shadowHash as |hash|}}
+ ({{hash apple='green'}})
+ {{/let}}
+
+ );
+
+ assert.dom().hasText('(hash:apple=red,hash:banana=yellow) (hash:apple=green)');
+ await assert.stableRender();
+ });
+
+ test('can have more than one key-value', async function (assert) {
+ await render(
+
+ {{#let (hash name='Sergio' lastName='Arbeo') as |person|}}
+ {{person.name}}
+ {{person.lastName}}
+ {{/let}}
+
+ );
+
+ assert.dom().hasText('Sergio Arbeo');
+ await assert.stableRender();
+ });
+
+ test('binds values when variables are used', async function (assert) {
+ let state = new State({ firstName: 'Marisa' });
+
+ await render(
+
+ {{#let (hash name=state.model.firstName lastName='Arbeo') as |person|}}
+ {{person.name}}
+ {{person.lastName}}
+ {{/let}}
+
+ );
+
+ assert.dom().hasText('Marisa Arbeo');
+ await assert.stableRender();
+
+ state.model.firstName = 'Sergio';
+ await settled();
+
+ assert.dom().hasText('Sergio Arbeo');
+
+ state.model = new Model({ firstName: 'Marisa' });
+ await settled();
+
+ assert.dom().hasText('Marisa Arbeo');
+ });
+
+ test('binds multiple values when variables are used', async function (assert) {
+ let state = new State({ firstName: 'Marisa', lastName: 'Arbeo' });
+
+ await render(
+
+ {{#let (hash name=state.model.firstName lastName=state.model.lastName) as |person|}}
+ {{person.name}}
+ {{person.lastName}}
+ {{/let}}
+
+ );
+
+ assert.dom().hasText('Marisa Arbeo');
+ await assert.stableRender();
+
+ state.model.firstName = 'Sergio';
+ await settled();
+
+ assert.dom().hasText('Sergio Arbeo');
+
+ state.model.lastName = 'Smith';
+ await settled();
+
+ assert.dom().hasText('Sergio Smith');
+
+ state.model = new Model({ firstName: 'Marisa', lastName: 'Arbeo' });
+ await settled();
+
+ assert.dom().hasText('Marisa Arbeo');
+ });
+
+ test('hash helpers can be nested', async function (assert) {
+ let state = new State({ firstName: 'Balint' });
+
+ await render(
+
+ {{#let (hash person=(hash name=state.model.firstName)) as |ctx|}}{{ctx.person.name}}{{/let}}
+
+ );
+
+ await assert.stableRender('Balint');
+
+ state.model.firstName = 'Chad';
+ await settled();
+
+ assert.dom().hasText('Chad');
+
+ state.model = new Model({ firstName: 'Balint' });
+ await settled();
+
+ assert.dom().hasText('Balint');
+ });
+
+ test('should yield hash of internal properties', async function (assert) {
+ let fooBarInstance;
+
+ class FooBar extends Component {
+ @tracked model = new Model({ firstName: 'Chad' });
+
+ constructor(owner, args) {
+ super(owner, args);
+ fooBarInstance = this;
+ }
+
+ {{yield (hash firstName=this.model.firstName)}}
+ }
+
+ await render(
+
+ {{values.firstName}}
+
+ );
+
+ await assert.stableRender('Chad');
+
+ fooBarInstance.model.firstName = 'Godfrey';
+ await settled();
+
+ assert.dom().hasText('Godfrey');
+
+ fooBarInstance.model = new Model({ firstName: 'Chad' });
+ await settled();
+
+ assert.dom().hasText('Chad');
+ });
+
+ test('should yield hash of internal and external properties', async function (assert) {
+ let state = new State({ lastName: 'Hietala' });
+ let fooBarInstance;
+
+ class FooBar extends Component {
+ @tracked model = new Model({ firstName: 'Chad' });
+
+ constructor(owner, args) {
+ super(owner, args);
+ fooBarInstance = this;
+ }
+
+ {{yield (hash firstName=this.model.firstName lastName=@lastName)}}
+ }
+
+ await render(
+
+
+ {{values.firstName}}
+ {{values.lastName}}
+
+
+ );
+
+ assert.dom().hasText('Chad Hietala');
+ await assert.stableRender();
+
+ fooBarInstance.model.firstName = 'Godfrey';
+ state.model.lastName = 'Chan';
+ await settled();
+
+ assert.dom().hasText('Godfrey Chan');
+
+ fooBarInstance.model = new Model({ firstName: 'Chad' });
+ state.model = new Model({ lastName: 'Hietala' });
+ await settled();
+
+ assert.dom().hasText('Chad Hietala');
+ });
+
+ test('works with computeds', async function (assert) {
+ let state = new Model({ firstName: 'Chad', lastName: 'Hietala' });
+
+ class FooBar extends Component {
+ @computed('args.hash.firstName', 'args.hash.lastName')
+ get fullName() {
+ return `${this.args.hash.firstName} ${this.args.hash.lastName}`;
+ }
+
+ {{this.fullName}}
+ }
+
+ await render(
+
+
+
+ );
+
+ await assert.stableRender('Chad Hietala');
+
+ state.firstName = 'Godfrey';
+ state.lastName = 'Chan';
+ await settled();
+
+ assert.dom().hasText('Godfrey Chan');
+ });
+});
diff --git a/packages/@ember/-internals/glimmer/tests/integration/helpers/hash-test.js b/packages/@ember/-internals/glimmer/tests/integration/helpers/hash-test.js
deleted file mode 100644
index a44b638cc9c..00000000000
--- a/packages/@ember/-internals/glimmer/tests/integration/helpers/hash-test.js
+++ /dev/null
@@ -1,246 +0,0 @@
-import { RenderingTestCase, moduleFor, runTask } from 'internal-test-helpers';
-import { setComponentTemplate } from '@glimmer/manager';
-import { precompileTemplate } from '@ember/template-compilation';
-import { template } from '@ember/template-compiler/runtime';
-
-import Component from '@glimmer/component';
-
-import { set, computed } from '@ember/object';
-
-moduleFor(
- 'Helpers test: {{hash}}',
- class extends RenderingTestCase {
- ['@test returns a hash with the right key-value']() {
- this.render(`{{#let (hash name="Sergio") as |person|}}{{person.name}}{{/let}}`);
-
- this.assertText('Sergio');
-
- runTask(() => this.rerender());
-
- this.assertText('Sergio');
- }
-
- ['@test can be shadowed']() {
- let hash = (obj) =>
- Object.entries(obj)
- .map(([key, value]) => `hash:${key}=${value}`)
- .join(',');
- let Root = template(
- `({{hash apple='red' banana='yellow'}}) ({{#let shadowHash as |hash|}}{{hash apple='green'}}{{/let}})`,
- { scope: () => ({ hash, shadowHash: hash }) }
- );
-
- this.renderComponent(Root, {
- expect: '(hash:apple=red,hash:banana=yellow) (hash:apple=green)',
- });
- }
-
- ['@test can have more than one key-value']() {
- this.render(
- `{{#let (hash name="Sergio" lastName="Arbeo") as |person|}}{{person.name}} {{person.lastName}}{{/let}}`
- );
-
- this.assertText('Sergio Arbeo');
-
- runTask(() => this.rerender());
-
- this.assertText('Sergio Arbeo');
- }
-
- ['@test binds values when variables are used']() {
- this.render(
- `{{#let (hash name=this.model.firstName lastName="Arbeo") as |person|}}{{person.name}} {{person.lastName}}{{/let}}`,
- {
- model: {
- firstName: 'Marisa',
- },
- }
- );
-
- this.assertText('Marisa Arbeo');
-
- runTask(() => this.rerender());
-
- this.assertText('Marisa Arbeo');
-
- runTask(() => set(this.context, 'model.firstName', 'Sergio'));
-
- this.assertText('Sergio Arbeo');
-
- runTask(() => set(this.context, 'model', { firstName: 'Marisa' }));
-
- this.assertText('Marisa Arbeo');
- }
-
- ['@test binds multiple values when variables are used']() {
- this.render(
- `{{#let (hash name=this.model.firstName lastName=this.model.lastName) as |person|}}{{person.name}} {{person.lastName}}{{/let}}`,
- {
- model: {
- firstName: 'Marisa',
- lastName: 'Arbeo',
- },
- }
- );
-
- this.assertText('Marisa Arbeo');
-
- runTask(() => this.rerender());
-
- this.assertText('Marisa Arbeo');
-
- runTask(() => set(this.context, 'model.firstName', 'Sergio'));
-
- this.assertText('Sergio Arbeo');
-
- runTask(() => set(this.context, 'model.lastName', 'Smith'));
-
- this.assertText('Sergio Smith');
-
- runTask(() =>
- set(this.context, 'model', {
- firstName: 'Marisa',
- lastName: 'Arbeo',
- })
- );
-
- this.assertText('Marisa Arbeo');
- }
-
- ['@test hash helpers can be nested']() {
- this.render(
- `{{#let (hash person=(hash name=this.model.firstName)) as |ctx|}}{{ctx.person.name}}{{/let}}`,
- {
- model: { firstName: 'Balint' },
- }
- );
-
- this.assertText('Balint');
-
- runTask(() => this.rerender());
-
- this.assertText('Balint');
-
- runTask(() => set(this.context, 'model.firstName', 'Chad'));
-
- this.assertText('Chad');
-
- runTask(() => set(this.context, 'model', { firstName: 'Balint' }));
-
- this.assertText('Balint');
- }
-
- ['@test should yield hash of internal properties']() {
- let fooBarInstance;
- let FooBarComponent = class extends Component {
- constructor(owner, args) {
- super(owner, args);
- fooBarInstance = this;
- this.model = { firstName: 'Chad' };
- }
- };
-
- this.owner.register(
- 'component:foo-bar',
- setComponentTemplate(
- precompileTemplate(`{{yield (hash firstName=this.model.firstName)}}`),
- FooBarComponent
- )
- );
-
- this.render(`{{#foo-bar as |values|}}{{values.firstName}}{{/foo-bar}}`);
-
- this.assertText('Chad');
-
- runTask(() => this.rerender());
-
- this.assertText('Chad');
-
- runTask(() => set(fooBarInstance, 'model.firstName', 'Godfrey'));
-
- this.assertText('Godfrey');
-
- runTask(() => set(fooBarInstance, 'model', { firstName: 'Chad' }));
-
- this.assertText('Chad');
- }
-
- ['@test should yield hash of internal and external properties']() {
- let fooBarInstance;
- let FooBarComponent = class extends Component {
- constructor(owner, args) {
- super(owner, args);
- fooBarInstance = this;
- this.model = { firstName: 'Chad' };
- }
- };
-
- this.owner.register(
- 'component:foo-bar',
- setComponentTemplate(
- precompileTemplate(`{{yield (hash firstName=this.model.firstName lastName=@lastName)}}`),
- FooBarComponent
- )
- );
-
- this.render(
- `{{#foo-bar lastName=this.model.lastName as |values|}}{{values.firstName}} {{values.lastName}}{{/foo-bar}}`,
- {
- model: { lastName: 'Hietala' },
- }
- );
-
- this.assertText('Chad Hietala');
-
- runTask(() => this.rerender());
-
- this.assertText('Chad Hietala');
-
- runTask(() => {
- set(fooBarInstance, 'model.firstName', 'Godfrey');
- set(this.context, 'model.lastName', 'Chan');
- });
-
- this.assertText('Godfrey Chan');
-
- runTask(() => {
- set(fooBarInstance, 'model', { firstName: 'Chad' });
- set(this.context, 'model', { lastName: 'Hietala' });
- });
-
- this.assertText('Chad Hietala');
- }
-
- ['@test works with computeds']() {
- let FooBarComponent = class extends Component {
- @computed('args.hash.firstName', 'args.hash.lastName')
- get fullName() {
- return `${this.args.hash.firstName} ${this.args.hash.lastName}`;
- }
- };
-
- this.owner.register(
- 'component:foo-bar',
- setComponentTemplate(precompileTemplate(`{{this.fullName}}`), FooBarComponent)
- );
-
- this.render(`{{foo-bar hash=(hash firstName=this.firstName lastName=this.lastName)}}`, {
- firstName: 'Chad',
- lastName: 'Hietala',
- });
-
- this.assertText('Chad Hietala');
-
- runTask(() => this.rerender());
-
- this.assertText('Chad Hietala');
-
- runTask(() => {
- set(this.context, 'firstName', 'Godfrey');
- set(this.context, 'lastName', 'Chan');
- });
-
- this.assertText('Godfrey Chan');
- }
- }
-);
diff --git a/packages/@ember/-internals/glimmer/tests/integration/helpers/if-laziness-test.gjs b/packages/@ember/-internals/glimmer/tests/integration/helpers/if-laziness-test.gjs
index 3c96a349375..cc717606c38 100644
--- a/packages/@ember/-internals/glimmer/tests/integration/helpers/if-laziness-test.gjs
+++ b/packages/@ember/-internals/glimmer/tests/integration/helpers/if-laziness-test.gjs
@@ -1,26 +1,34 @@
-import { moduleFor, RenderingTestCase } from 'internal-test-helpers';
+import { tracked } from '@glimmer/tracking';
+import { module, test } from 'qunit';
+import { setupRenderingTest } from 'ember-qunit';
+import { render, settled } from '@ember/test-helpers';
-moduleFor(
- 'Helpers test: inline {{if}} subexpression laziness',
- class extends RenderingTestCase {
- '@test it does not evaluate a subexpression with only const arguments when the branch is not taken (GH#21030)'(
- assert
- ) {
- let called = 0;
+module('Helpers test: inline {{if}} subexpression laziness', function (hooks) {
+ setupRenderingTest(hooks);
- function sideEffect() {
- called++;
- return 'value';
- }
+ test('it does not evaluate a subexpression with only const arguments when the branch is not taken (GH#21030)', async function (assert) {
+ let called = 0;
- let TestComponent = {{if false (sideEffect)}} ;
-
- this.render(' ', { TestComponent });
+ function sideEffect() {
+ called++;
+ return 'value';
+ }
- assert.strictEqual(called, 0, 'helper was not evaluated for the untaken branch');
- this.assertText('');
- this.assertStableRerender();
- assert.strictEqual(called, 0, 'helper was not evaluated on rerender');
+ class State {
+ @tracked text = '';
}
- }
-);
+
+ let state = new State();
+
+ await render({{if false (sideEffect)}}{{state.text}} );
+
+ assert.strictEqual(called, 0, 'helper was not evaluated for the untaken branch');
+ await assert.stableRender('');
+
+ state.text = 'updated';
+ await settled();
+
+ assert.dom().hasText('updated');
+ assert.strictEqual(called, 0, 'helper was not evaluated on rerender');
+ });
+});
diff --git a/packages/@ember/-internals/glimmer/tests/integration/helpers/invoke-helper-test.js b/packages/@ember/-internals/glimmer/tests/integration/helpers/invoke-helper-test.js
index 8b4fc637c16..6c299ee49b9 100644
--- a/packages/@ember/-internals/glimmer/tests/integration/helpers/invoke-helper-test.js
+++ b/packages/@ember/-internals/glimmer/tests/integration/helpers/invoke-helper-test.js
@@ -10,7 +10,7 @@ import Service, { service } from '@ember/service';
import { DEBUG } from '@glimmer/env';
import { getValue } from '@glimmer/validator';
import { destroy, isDestroyed, registerDestructor } from '@glimmer/destroyable';
-import { invokeHelper } from '@glimmer/runtime';
+import { invokeHelper } from '@ember/helper';
moduleFor(
'Helpers test: invokeHelper',
diff --git a/packages/@ember/-internals/glimmer/tests/integration/helpers/log-test.gjs b/packages/@ember/-internals/glimmer/tests/integration/helpers/log-test.gjs
new file mode 100644
index 00000000000..db37bcb00a0
--- /dev/null
+++ b/packages/@ember/-internals/glimmer/tests/integration/helpers/log-test.gjs
@@ -0,0 +1,83 @@
+import Component from '@glimmer/component';
+import { module, test } from 'qunit';
+import { setupRenderingTest } from 'ember-qunit';
+import { render } from '@ember/test-helpers';
+
+module('Helpers test: {{log}}', function (hooks) {
+ setupRenderingTest(hooks);
+
+ let originalLog;
+ let logCalls;
+
+ hooks.beforeEach(function () {
+ /* eslint-disable no-console */
+ originalLog = console.log;
+ logCalls = [];
+ console.log = (...args) => {
+ logCalls.push(...args);
+ /* eslint-enable no-console */
+ };
+ });
+
+ hooks.afterEach(function () {
+ /* eslint-disable no-console */
+ console.log = originalLog;
+ /* eslint-enable no-console */
+ });
+
+ function assertLog(assert, values) {
+ assert.dom().hasNoText();
+ assert.strictEqual(logCalls.length, values.length);
+
+ for (let i = 0, len = values.length; i < len; i++) {
+ assert.strictEqual(logCalls[i], values[i]);
+ }
+ }
+
+ test('correctly logs primitives', async function (assert) {
+ await render({{log "one" 1 true}} );
+
+ assertLog(assert, ['one', 1, true]);
+ });
+
+ test('correctly logs a property', async function (assert) {
+ class Subject extends Component {
+ value = 'one';
+
+ {{log this.value}}
+ }
+
+ await render( );
+
+ assertLog(assert, ['one']);
+ });
+
+ test('correctly logs multiple arguments', async function (assert) {
+ class Subject extends Component {
+ value = 'one';
+
+ {{log "my variable:" this.value}}
+ }
+
+ await render( );
+
+ assertLog(assert, ['my variable:', 'one']);
+ });
+
+ test('correctly logs `this`', async function (assert) {
+ let instance;
+
+ class Subject extends Component {
+ constructor() {
+ super(...arguments);
+ instance = this;
+ }
+
+ {{log this}}
+ }
+
+ await render( );
+
+ assertLog(assert, [instance]);
+ });
+});
diff --git a/packages/@ember/-internals/glimmer/tests/integration/helpers/log-test.js b/packages/@ember/-internals/glimmer/tests/integration/helpers/log-test.js
deleted file mode 100644
index 4f665af7baf..00000000000
--- a/packages/@ember/-internals/glimmer/tests/integration/helpers/log-test.js
+++ /dev/null
@@ -1,61 +0,0 @@
-import { RenderingTestCase, moduleFor } from 'internal-test-helpers';
-
-moduleFor(
- 'Helpers test: {{log}}',
- class extends RenderingTestCase {
- constructor() {
- super(...arguments);
- /* eslint-disable no-console */
- this.originalLog = console.log;
- this.logCalls = [];
- console.log = (...args) => {
- this.logCalls.push(...args);
- /* eslint-enable no-console */
- };
- }
-
- teardown() {
- super.teardown();
- /* eslint-disable no-console */
- console.log = this.originalLog;
- /* eslint-enable no-console */
- }
-
- assertLog(values) {
- this.assertText('');
- this.assert.strictEqual(this.logCalls.length, values.length);
-
- for (let i = 0, len = values.length; i < len; i++) {
- this.assert.strictEqual(this.logCalls[i], values[i]);
- }
- }
-
- ['@test correctly logs primitives']() {
- this.render(`{{log "one" 1 true}}`);
-
- this.assertLog(['one', 1, true]);
- }
-
- ['@test correctly logs a property']() {
- this.render(`{{log this.value}}`, {
- value: 'one',
- });
-
- this.assertLog(['one']);
- }
-
- ['@test correctly logs multiple arguments']() {
- this.render(`{{log "my variable:" this.value}}`, {
- value: 'one',
- });
-
- this.assertLog(['my variable:', 'one']);
- }
-
- ['@test correctly logs `this`']() {
- this.render(`{{log this}}`);
-
- this.assertLog([this.context]);
- }
- }
-);
diff --git a/packages/@ember/-internals/glimmer/tests/integration/helpers/mut-test.js b/packages/@ember/-internals/glimmer/tests/integration/helpers/mut-test.js
index 17ee1a41b47..990b1a8f0e2 100644
--- a/packages/@ember/-internals/glimmer/tests/integration/helpers/mut-test.js
+++ b/packages/@ember/-internals/glimmer/tests/integration/helpers/mut-test.js
@@ -1,35 +1,29 @@
-import { RenderingTestCase, moduleFor, styles, runTask } from 'internal-test-helpers';
+import { RenderingTestCase, moduleFor, runTask } from 'internal-test-helpers';
-import { set, get, computed } from '@ember/object';
+import { set, get } from '@ember/object';
import { precompileTemplate } from '@ember/template-compilation';
import { setComponentTemplate } from '@glimmer/manager';
import Component from '@glimmer/component';
-import { Component as EmberComponent, htmlSafe } from '../../utils/helpers';
+import { tracked } from '@glimmer/tracking';
moduleFor(
'Helpers test: {{mut}}',
class extends RenderingTestCase {
['@test a simple mutable binding using `mut` propagates properly']() {
- let bottom;
-
this.owner.register(
'component:bottom-mut',
setComponentTemplate(
- precompileTemplate('{{this.setMe}}'),
- class extends EmberComponent {
- didInsertElement() {
- bottom = this;
- }
- }
+ precompileTemplate('{{@setMe}} '),
+ class extends Component {}
)
);
this.owner.register(
'component:middle-mut',
setComponentTemplate(
- precompileTemplate('{{bottom-mut setMe=this.value}}'),
- class extends EmberComponent {}
+ precompileTemplate('{{bottom-mut setMe=@value}}'),
+ class extends Component {}
)
);
@@ -41,49 +35,30 @@ moduleFor(
this.assertStableRerender();
- runTask(() => bottom.attrs.setMe.update(13));
+ runTask(() => this.$('button').click());
this.assertText('13', 'the set took effect');
- this.assert.strictEqual(get(bottom, 'setMe'), 13, "the set took effect on bottom's prop");
- this.assert.strictEqual(bottom.attrs.setMe.value, 13, "the set took effect on bottom's attr");
this.assert.strictEqual(get(this.context, 'val'), 13, 'the set propagated back up');
- runTask(() => set(bottom, 'setMe', 14));
-
- this.assertText('14', 'the set took effect');
- this.assert.strictEqual(get(bottom, 'setMe'), 14, "the set took effect on bottom's prop");
- this.assert.strictEqual(bottom.attrs.setMe.value, 14, "the set took effect on bottom's attr");
- this.assert.strictEqual(get(this.context, 'val'), 14, 'the set propagated back up');
-
runTask(() => set(this.context, 'val', 12));
this.assertText('12');
}
['@test a simple mutable binding using `mut` inserts into the DOM']() {
- let bottom, middle;
-
this.owner.register(
'component:bottom-mut',
setComponentTemplate(
- precompileTemplate('{{this.setMe}}'),
- class extends EmberComponent {
- didInsertElement() {
- bottom = this;
- }
- }
+ precompileTemplate('{{@setMe}} '),
+ class extends Component {}
)
);
this.owner.register(
'component:middle-mut',
setComponentTemplate(
- precompileTemplate('{{bottom-mut setMe=(mut this.value)}}'),
- class extends EmberComponent {
- didInsertElement() {
- middle = this;
- }
- }
+ precompileTemplate('[{{@value}}]{{bottom-mut setMe=(mut @value)}}'),
+ class extends Component {}
)
);
@@ -91,31 +66,18 @@ moduleFor(
val: 12,
});
- this.assertText('12', 'the data propagated downwards');
+ this.assertText('[12]12', 'the data propagated downwards');
this.assertStableRerender();
- runTask(() => bottom.attrs.setMe.update(13));
+ runTask(() => this.$('button').click());
- this.assertText('13', 'the set took effect');
- this.assert.strictEqual(get(bottom, 'setMe'), 13, "the set took effect on bottom's prop");
- this.assert.strictEqual(bottom.attrs.setMe.value, 13, "the set took effect on bottom's attr");
- this.assert.strictEqual(get(middle, 'value'), 13, "the set propagated to middle's prop");
- this.assert.strictEqual(middle.attrs.value.value, 13, "the set propagated to middle's attr");
+ this.assertText('[13]13', 'the set took effect in the middle and the bottom');
this.assert.strictEqual(get(this.context, 'val'), 13, 'the set propagated back up');
- runTask(() => set(bottom, 'setMe', 14));
-
- this.assertText('14', 'the set took effect');
- this.assert.strictEqual(get(bottom, 'setMe'), 14, "the set took effect on bottom's prop");
- this.assert.strictEqual(bottom.attrs.setMe.value, 14, "the set took effect on bottom's attr");
- this.assert.strictEqual(get(middle, 'value'), 14, "the set propagated to middle's prop");
- this.assert.strictEqual(middle.attrs.value.value, 14, "the set propagated to middle's attr");
- this.assert.strictEqual(get(this.context, 'val'), 14, 'the set propagated back up');
-
runTask(() => set(this.context, 'val', 12));
- this.assertText('12');
+ this.assertText('[12]12');
}
['@test passing a literal results in a assertion']() {
@@ -140,97 +102,6 @@ moduleFor(
}, 'You can only pass a path to mut');
}
- // See https://github.com/emberjs/ember.js/commit/807a0cd for an explanation of this test
- ['@test using a string value through middle tier does not trigger assertion (due to the auto-mut transform)']() {
- let bottom;
-
- this.owner.register(
- 'component:bottom-mut',
- setComponentTemplate(
- precompileTemplate('{{this.stuff}}'),
- class extends EmberComponent {
- didInsertElement() {
- bottom = this;
- }
- }
- )
- );
-
- this.owner.register(
- 'component:middle-mut',
- setComponentTemplate(
- precompileTemplate('{{bottom-mut stuff=this.value}}'),
- class extends EmberComponent {}
- )
- );
-
- this.render('{{middle-mut value="foo"}}');
-
- this.assert.equal(get(bottom, 'stuff'), 'foo', 'the data propagated');
- this.assertText('foo');
-
- this.assertStableRerender();
-
- // No U-R for this test
- }
-
- ['@test {{readonly}} of a {{mut}} is converted into an immutable binding']() {
- let middle, bottom;
-
- this.owner.register(
- 'component:bottom-mut',
- setComponentTemplate(
- precompileTemplate('{{this.setMe}}'),
- class extends EmberComponent {
- didInsertElement() {
- bottom = this;
- }
- }
- )
- );
-
- this.owner.register(
- 'component:middle-mut',
- setComponentTemplate(
- precompileTemplate('{{bottom-mut setMe=(readonly this.value)}}'),
- class extends EmberComponent {
- didInsertElement() {
- middle = this;
- }
- }
- )
- );
-
- this.render('{{middle-mut value=(mut this.val)}}', {
- val: 12,
- });
-
- this.assertText('12');
-
- this.assertStableRerender();
-
- runTask(() => middle.attrs.value.update(13));
-
- this.assert.strictEqual(get(middle, 'value'), 13, "the set took effect on middle's prop");
- this.assert.strictEqual(middle.attrs.value.value, 13, "the set took effect on middle's attr");
-
- runTask(() => set(middle, 'value', 14));
-
- this.assert.strictEqual(get(middle, 'value'), 14, "the set took effect on middle's prop");
- this.assert.strictEqual(middle.attrs.value.value, 14, "the set took effect on middle's attr");
- this.assert.strictEqual(
- bottom.attrs.setMe,
- 14,
- 'the mutable binding has been converted to an immutable cell'
- );
- this.assertText('14');
- this.assert.strictEqual(get(this.context, 'val'), 14, 'the set propagated back up');
-
- runTask(() => set(this.context, 'val', 12));
-
- this.assertText('12');
- }
-
['@test mutable bindings work inside of yielded content']() {
this.owner.register(
'component:bottom-mut',
@@ -262,93 +133,31 @@ moduleFor(
this.assertText('Matthew Beale');
}
- ['@test a simple mutable binding using {{mut}} is available in hooks']() {
- let bottom;
- let willRender = [];
- let didInsert = [];
+ ['@test a mutable binding with a backing getter is updated when the upstream property invalidates #11023']() {
+ let middle;
this.owner.register(
'component:bottom-mut',
- setComponentTemplate(
- precompileTemplate('{{this.setMe}}'),
- class extends EmberComponent {
- willRender() {
- willRender.push(get(this, 'setMe'));
- }
- didInsertElement() {
- didInsert.push(get(this, 'setMe'));
- bottom = this;
- }
- }
- )
- );
-
- this.owner.register(
- 'component:middle-mut',
- setComponentTemplate(
- precompileTemplate('{{bottom-mut setMe=(mut this.value)}}'),
- class extends EmberComponent {}
- )
- );
-
- this.render('{{middle-mut value=(mut this.val)}}', {
- val: 12,
- });
-
- this.assert.deepEqual(willRender, [12], 'willReceive is [12]');
- this.assert.deepEqual(didInsert, [12], 'didInsert is [12]');
- this.assertText('12');
-
- this.assertStableRerender();
-
- this.assert.deepEqual(willRender, [12], 'willReceive is [12]');
- this.assert.deepEqual(didInsert, [12], 'didInsert is [12]');
- this.assert.strictEqual(get(bottom, 'setMe'), 12, 'the data propagated');
-
- runTask(() => bottom.attrs.setMe.update(13));
-
- this.assert.strictEqual(get(bottom, 'setMe'), 13, "the set took effect on bottom's prop");
- this.assert.strictEqual(bottom.attrs.setMe.value, 13, "the set took effect on bottom's attr");
- this.assert.strictEqual(get(this.context, 'val'), 13, 'the set propagated back up');
-
- runTask(() => set(bottom, 'setMe', 14));
-
- this.assert.strictEqual(get(bottom, 'setMe'), 14, "the set took effect on bottom's prop");
- this.assert.strictEqual(bottom.attrs.setMe.value, 14, "the set took effect on bottom's attr");
- this.assert.strictEqual(get(this.context, 'val'), 14, 'the set propagated back up');
-
- runTask(() => set(this.context, 'val', 12));
-
- this.assertText('12');
- }
-
- ['@test a mutable binding with a backing computed property and attribute present in the root of the component is updated when the upstream property invalidates #11023']() {
- let bottom, middle;
-
- this.owner.register(
- 'component:bottom-mut',
- setComponentTemplate(
- precompileTemplate('{{this.thingy}}'),
- class extends EmberComponent {
- thingy = null;
- didInsertElement() {
- bottom = this;
- }
- }
- )
+ setComponentTemplate(precompileTemplate('{{@thingy}}'), class extends Component {})
);
this.owner.register(
'component:middle-mut',
setComponentTemplate(
precompileTemplate('{{bottom-mut thingy=(mut this.val)}}'),
- class extends EmberComponent {
- baseValue = 12;
- @computed('baseValue')
+ class extends Component {
+ @tracked baseValue = 12;
+
get val() {
return this.baseValue;
}
- didInsertElement() {
+
+ set val(value) {
+ this.baseValue = value;
+ }
+
+ constructor(owner, args) {
+ super(owner, args);
middle = this;
}
}
@@ -357,313 +166,17 @@ moduleFor(
this.render('{{middle-mut}}');
- this.assert.strictEqual(get(bottom, 'thingy'), 12, 'data propagated');
this.assertText('12');
this.assertStableRerender();
- runTask(() => set(middle, 'baseValue', 13));
+ runTask(() => (middle.baseValue = 13));
- this.assert.strictEqual(get(middle, 'val'), 13, 'the set took effect');
- this.assert.strictEqual(
- bottom.attrs.thingy.value,
- 13,
- "the set propagated down to bottom's attrs"
- );
- this.assert.strictEqual(
- get(bottom, 'thingy'),
- 13,
- "the set propagated down to bottom's prop"
- );
this.assertText('13');
- runTask(() => set(middle, 'baseValue', 12));
+ runTask(() => (middle.baseValue = 12));
this.assertText('12');
}
-
- ['@test automatic mutable bindings exposes a mut cell in attrs']() {
- let inner;
-
- this.owner.register(
- 'component:x-inner',
- setComponentTemplate(
- precompileTemplate('{{this.foo}}'),
- class extends EmberComponent {
- didInsertElement() {
- inner = this;
- }
- }
- )
- );
-
- this.owner.register(
- 'component:x-outer',
- setComponentTemplate(
- precompileTemplate('{{x-inner foo=this.bar}}'),
- class extends EmberComponent {}
- )
- );
-
- this.render('{{x-outer bar=this.baz}}', { baz: 'foo' });
-
- this.assertText('foo');
-
- this.assertStableRerender();
-
- runTask(() => inner.attrs.foo.update('bar'));
-
- this.assert.equal(inner.attrs.foo.value, 'bar');
- this.assert.equal(get(inner, 'foo'), 'bar');
- this.assertText('bar');
-
- runTask(() => inner.attrs.foo.update('foo'));
-
- this.assertText('foo');
- }
-
- ['@test automatic mutable bindings tolerate undefined non-stream inputs and attempts to set them']() {
- let inner;
-
- this.owner.register(
- 'component:x-inner',
- setComponentTemplate(
- precompileTemplate('{{@model}}'),
- class extends EmberComponent {
- didInsertElement() {
- inner = this;
- }
- }
- )
- );
-
- this.owner.register(
- 'component:x-outer',
- setComponentTemplate(
- precompileTemplate('{{x-inner model=this.nonexistent}}'),
- class extends EmberComponent {}
- )
- );
-
- this.render('{{x-outer}}');
-
- this.assertText('');
-
- this.assertStableRerender();
-
- runTask(() => inner.attrs.model.update(42));
-
- this.assert.equal(inner.attrs.model.value, 42);
- this.assert.equal(get(inner, 'model'), 42);
- this.assertText('42');
-
- runTask(() => inner.attrs.model.update(undefined));
-
- this.assertText('');
- }
-
- ['@test automatic mutable bindings tolerate constant non-stream inputs and attempts to set them']() {
- let inner;
-
- this.owner.register(
- 'component:x-inner',
- setComponentTemplate(
- precompileTemplate('hello{{@model}}'),
- class extends EmberComponent {
- didInsertElement() {
- inner = this;
- }
- }
- )
- );
-
- this.owner.register(
- 'component:x-outer',
- setComponentTemplate(
- precompileTemplate(
- // Use `this.x` here instead of `@x` to let `x-inner` mutate `this.x`.
- // `@x` points to the literal binding from `x-outer`, which is of
- // course immutable.
- '{{x-inner model=this.x}}'
- ),
- class extends EmberComponent {}
- )
- );
-
- this.render('{{x-outer x="foo"}}');
-
- this.assertText('hellofoo');
-
- this.assertStableRerender();
-
- runTask(() => inner.attrs.model.update(42));
-
- this.assert.equal(inner.attrs.model.value, 42);
- this.assert.equal(get(inner, 'model'), 42);
- this.assertText('hello42');
-
- runTask(() => inner.attrs.model.update('foo'));
-
- this.assertText('hellofoo');
- }
- }
-);
-
-moduleFor(
- 'Mutable Bindings used in Computed Properties that are bound as attributeBindings',
- class extends RenderingTestCase {
- ['@test an attribute binding of a computed property of a 2-way bound attr recomputes when the attr changes']() {
- let input, output;
-
- this.owner.register(
- 'component:x-input',
- class extends EmberComponent {
- didInsertElement() {
- input = this;
- }
- }
- );
-
- this.owner.register(
- 'component:x-output',
- setComponentTemplate(
- precompileTemplate('{{this.height}}'),
- class extends EmberComponent {
- attributeBindings = ['style'];
- didInsertElement() {
- output = this;
- }
- @computed('height')
- get style() {
- let height = this.get('height');
- return htmlSafe(`height: ${height}px;`);
- }
- height = 20;
- }
- )
- );
-
- this.render('{{x-output height=this.height}}{{x-input height=(mut this.height)}}', {
- height: 60,
- });
-
- this.assertComponentElement(this.firstChild, {
- tagName: 'div',
- attrs: { style: styles('height: 60px;') },
- content: '60',
- });
-
- this.assertStableRerender();
-
- runTask(() => input.attrs.height.update(35));
-
- this.assert.strictEqual(get(output, 'height'), 35, 'the set took effect');
- this.assert.strictEqual(get(this.context, 'height'), 35, 'the set propagated back up');
- this.assertComponentElement(this.firstChild, {
- tagName: 'div',
- attrs: { style: styles('height: 35px;') },
- content: '35',
- });
-
- runTask(() => set(input, 'height', 36));
-
- this.assert.strictEqual(get(output, 'height'), 36, 'the set took effect');
- this.assert.strictEqual(get(this.context, 'height'), 36, 'the set propagated back up');
- this.assertComponentElement(this.firstChild, {
- tagName: 'div',
- attrs: { style: styles('height: 36px;') },
- content: '36',
- });
-
- runTask(() => set(this.context, 'height', 60));
-
- this.assertComponentElement(this.firstChild, {
- tagName: 'div',
- attrs: { style: styles('height: 60px;') },
- content: '60',
- });
- this.assert.strictEqual(get(input, 'height'), 60);
- }
-
- ['@test an attribute binding of a computed property with a setter of a 2-way bound attr recomputes when the attr changes']() {
- let input, output;
-
- this.owner.register(
- 'component:x-input',
- class extends EmberComponent {
- didInsertElement() {
- input = this;
- }
- }
- );
-
- this.owner.register(
- 'component:x-output',
- setComponentTemplate(
- precompileTemplate('{{this.width}}x{{this.height}}'),
- class extends EmberComponent {
- attributeBindings = ['style'];
- didInsertElement() {
- output = this;
- }
- @computed('height', 'width')
- get style() {
- let height = this.get('height');
- let width = this.get('width');
- return htmlSafe(`height: ${height}px; width: ${width}px;`);
- }
- height = 20;
- @computed('height')
- get width() {
- return this.get('height') * 2;
- }
- set width(width) {
- this.set('height', width / 2);
- }
- }
- )
- );
-
- this.render('{{x-output width=this.width}}{{x-input width=(mut this.width)}}', {
- width: 70,
- });
-
- this.assertComponentElement(this.firstChild, {
- tagName: 'div',
- attrs: { style: styles('height: 35px; width: 70px;') },
- content: '70x35',
- });
-
- this.assertStableRerender();
-
- runTask(() => set(input, 'width', 80));
-
- this.assert.strictEqual(get(output, 'width'), 80, 'the set took effect');
- this.assert.strictEqual(get(this.context, 'width'), 80, 'the set propagated back up');
- this.assertComponentElement(this.firstChild, {
- tagName: 'div',
- attrs: { style: styles('height: 40px; width: 80px;') },
- content: '80x40',
- });
-
- runTask(() => input.attrs.width.update(90));
-
- this.assert.strictEqual(get(output, 'width'), 90, 'the set took effect');
- this.assert.strictEqual(get(this.context, 'width'), 90, 'the set propagated back up');
- this.assertComponentElement(this.firstChild, {
- tagName: 'div',
- attrs: { style: styles('height: 45px; width: 90px;') },
- content: '90x45',
- });
-
- runTask(() => set(this.context, 'width', 70));
-
- this.assertComponentElement(this.firstChild, {
- tagName: 'div',
- attrs: { style: styles('height: 35px; width: 70px;') },
- content: '70x35',
- });
- this.assert.strictEqual(get(input, 'width'), 70);
- }
}
);
diff --git a/packages/@ember/-internals/glimmer/tests/integration/helpers/readonly-test.js b/packages/@ember/-internals/glimmer/tests/integration/helpers/readonly-test.js
index 6dc5b3960a5..ec8949e9c22 100644
--- a/packages/@ember/-internals/glimmer/tests/integration/helpers/readonly-test.js
+++ b/packages/@ember/-internals/glimmer/tests/integration/helpers/readonly-test.js
@@ -2,59 +2,17 @@ import { RenderingTestCase, moduleFor, runTask } from 'internal-test-helpers';
import { precompileTemplate } from '@ember/template-compilation';
import { setComponentTemplate } from '@glimmer/manager';
-import { set, get } from '@ember/object';
+import { set } from '@ember/object';
-import { Component } from '../../utils/helpers';
+import Component from '@glimmer/component';
moduleFor(
'Helpers test: {{readonly}}',
class extends RenderingTestCase {
- ['@test {{readonly}} of a path should work']() {
- let component;
-
- this.owner.register(
- 'component:foo-bar',
- setComponentTemplate(
- precompileTemplate('{{this.value}}'),
- class extends Component {
- didInsertElement() {
- component = this;
- }
- }
- )
- );
-
- this.render('{{foo-bar value=(readonly this.val)}}', {
- val: 12,
- });
-
- this.assertText('12');
-
- this.assertStableRerender();
-
- runTask(() => set(component, 'value', 13));
- this.assert.notOk(component.attrs.value.update);
-
- this.assertText('13', 'local property is updated');
- this.assert.equal(get(this.context, 'val'), 12, 'upstream attribute is not updated');
-
- // No U-R
- }
-
- '@test updating a {{readonly}} property from above works'(assert) {
- let component;
-
+ '@test updating a {{readonly}} property from above works'() {
this.owner.register(
'component:foo-bar',
- setComponentTemplate(
- precompileTemplate('{{this.value}}'),
- class extends Component {
- init() {
- super.init(...arguments);
- component = this;
- }
- }
- )
+ setComponentTemplate(precompileTemplate('{{@value}}'), class extends Component {})
);
this.render('{{foo-bar value=(readonly this.thing)}}', {
@@ -65,15 +23,9 @@ moduleFor(
this.assertStableRerender();
- assert.strictEqual(component.attrs.value, 'initial', 'no mutable cell');
- assert.strictEqual(get(component, 'value'), 'initial', 'no mutable cell');
- assert.strictEqual(this.context.thing, 'initial');
-
runTask(() => set(this.context, 'thing', 'updated!'));
this.assertText('updated!');
- assert.strictEqual(component.attrs.value, 'updated!', 'passed down value was set in attrs');
- assert.strictEqual(get(component, 'value'), 'updated!', 'passed down value was set');
runTask(() => set(this.context, 'thing', 'initial'));
@@ -86,9 +38,10 @@ moduleFor(
this.owner.register(
'component:foo-bar',
setComponentTemplate(
- precompileTemplate('{{this.value.prop}}'),
+ precompileTemplate('{{@value.prop}}'),
class extends Component {
- didInsertElement() {
+ constructor(owner, args) {
+ super(owner, args);
component = this;
}
}
@@ -105,34 +58,20 @@ moduleFor(
this.assertStableRerender();
- assert.notOk(component.attrs.value.update, 'no update available');
- assert.deepEqual(get(component, 'value'), { prop: 'initial' });
- assert.deepEqual(this.context.thing, { prop: 'initial' });
-
- runTask(() => set(component, 'value.prop', 'updated!'));
+ runTask(() => set(component.args.value, 'prop', 'updated!'));
this.assertText('updated!', 'nested path is updated');
- assert.deepEqual(get(component, 'value'), { prop: 'updated!' });
assert.deepEqual(this.context.thing, { prop: 'updated!' });
- runTask(() => set(component, 'value.prop', 'initial'));
+ runTask(() => set(component.args.value, 'prop', 'initial'));
this.assertText('initial');
}
['@test {{readonly}} of a string renders correctly']() {
- let component;
-
this.owner.register(
'component:foo-bar',
- setComponentTemplate(
- precompileTemplate('{{this.value}}'),
- class extends Component {
- didInsertElement() {
- component = this;
- }
- }
- )
+ setComponentTemplate(precompileTemplate('{{@value}}'), class extends Component {})
);
this.render('{{foo-bar value=(readonly "12")}}');
@@ -140,132 +79,27 @@ moduleFor(
this.assertText('12');
this.assertStableRerender();
-
- this.assert.notOk(component.attrs.value.update);
- this.assert.strictEqual(get(component, 'value'), '12');
-
- runTask(() => set(component, 'value', '13'));
-
- this.assertText('13', 'local property is updated');
- this.assert.strictEqual(get(component, 'value'), '13');
-
- runTask(() => set(component, 'value', '12'));
-
- this.assertText('12');
}
- ['@test {{mut}} of a {{readonly}} mutates only the middle and bottom tiers']() {
- let middle, bottom;
-
+ ['@test {{mut}} of a {{readonly}} argument is not allowed']() {
this.owner.register(
'component:x-bottom',
- setComponentTemplate(
- precompileTemplate('{{this.bar}}'),
- class extends Component {
- didInsertElement() {
- bottom = this;
- }
- }
- )
+ setComponentTemplate(precompileTemplate('{{@bar}}'), class extends Component {})
);
this.owner.register(
'component:x-middle',
setComponentTemplate(
- precompileTemplate('{{this.foo}} {{x-bottom bar=(mut this.foo)}}'),
- class extends Component {
- didInsertElement() {
- middle = this;
- }
- }
+ precompileTemplate('{{@foo}} {{x-bottom bar=(mut @foo)}}'),
+ class extends Component {}
)
);
- this.render('{{x-middle foo=(readonly this.val)}}', {
- val: 12,
- });
-
- this.assertText('12 12');
-
- this.assertStableRerender();
-
- this.assert.equal(get(bottom, 'bar'), 12, "bottom's local bar received the value");
- this.assert.equal(get(middle, 'foo'), 12, "middle's local foo received the value");
-
- // updating the mut-cell directly
- runTask(() => bottom.attrs.bar.update(13));
-
- this.assert.equal(
- get(bottom, 'bar'),
- 13,
- "bottom's local bar was updated after set of bottom's bar"
- );
- this.assert.equal(
- get(middle, 'foo'),
- 13,
- "middle's local foo was updated after set of bottom's bar"
- );
- this.assertText('13 13');
- this.assert.equal(get(this.context, 'val'), 12, 'But context val is not updated');
-
- runTask(() => set(bottom, 'bar', 14));
-
- this.assert.equal(
- get(bottom, 'bar'),
- 14,
- "bottom's local bar was updated after set of bottom's bar"
- );
- this.assert.equal(
- get(middle, 'foo'),
- 14,
- "middle's local foo was updated after set of bottom's bar"
- );
- this.assertText('14 14');
- this.assert.equal(get(this.context, 'val'), 12, 'But context val is not updated');
-
- this.assert.notOk(middle.attrs.foo.update, "middle's foo attr is not a mutable cell");
- runTask(() => set(middle, 'foo', 15));
-
- this.assertText('15 15');
- this.assert.equal(get(middle, 'foo'), 15, "set of middle's foo took effect");
- this.assert.equal(
- get(bottom, 'bar'),
- 15,
- "bottom's local bar was updated after set of middle's foo"
- );
- this.assert.equal(get(this.context, 'val'), 12, 'Context val remains unchanged');
-
- runTask(() => set(this.context, 'val', 10));
-
- this.assertText('10 10');
- this.assert.equal(
- get(bottom, 'bar'),
- 10,
- "bottom's local bar was updated after set of context's val"
- );
- this.assert.equal(
- get(middle, 'foo'),
- 10,
- "middle's local foo was updated after set of context's val"
- );
-
- // setting as a normal property
- runTask(() => set(bottom, 'bar', undefined));
-
- this.assertText(' ');
- this.assert.equal(
- get(bottom, 'bar'),
- undefined,
- "bottom's local bar was updated to a falsy value"
- );
- this.assert.equal(
- get(middle, 'foo'),
- undefined,
- "middle's local foo was updated to a falsy value"
- );
-
- runTask(() => set(this.context, 'val', 12));
- this.assertText('12 12', 'bottom and middle were both reset');
+ expectAssertion(() => {
+ this.render('{{x-middle foo=(readonly this.val)}}', {
+ val: 12,
+ });
+ }, 'You can only pass a path to mut');
}
}
);
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 a37ede590f3..bf687667e9a 100644
--- a/packages/@ember/-internals/glimmer/tests/integration/helpers/unbound-test.js
+++ b/packages/@ember/-internals/glimmer/tests/integration/helpers/unbound-test.js
@@ -7,7 +7,6 @@ import {
} from 'internal-test-helpers';
import { set, get, setProperties } from '@ember/object';
-import { A as emberA } from '@ember/array';
import { precompileTemplate } from '@ember/template-compilation';
import { setComponentTemplate } from '@glimmer/manager';
@@ -44,7 +43,7 @@ moduleFor(
['@test should be able to use unbound helper in #each helper']() {
this.render(`{{#each this.items as |item|}}{{unbound item}} {{/each}} `, {
- items: emberA(['a', 'b', 'c', 1, 2, 3]),
+ items: ['a', 'b', 'c', 1, 2, 3],
});
this.assertText('abc123');
@@ -58,7 +57,7 @@ moduleFor(
this.render(
`{{#each this.items as |item|}}{{unbound item.wham}} {{/each}} `,
{
- items: emberA([{ wham: 'bam' }, { wham: 1 }]),
+ items: [{ wham: 'bam' }, { wham: 1 }],
}
);
@@ -68,11 +67,11 @@ moduleFor(
this.assertText('bam1');
- runTask(() => this.context.items.setEach('wham', 'HEY'));
+ runTask(() => this.context.items.forEach((item) => set(item, 'wham', 'HEY')));
this.assertText('bam1');
- runTask(() => set(this.context, 'items', emberA([{ wham: 'bam' }, { wham: 1 }])));
+ runTask(() => set(this.context, 'items', [{ wham: 'bam' }, { wham: 1 }]));
this.assertText('bam1');
}
@@ -112,7 +111,7 @@ moduleFor(
}
['@test should property escape unsafe hrefs']() {
- let unsafeUrls = emberA([
+ let unsafeUrls = [
{
name: 'Bob',
url: 'javascript:bob-is-cool',
@@ -125,7 +124,7 @@ moduleFor(
name: 'Richard',
url: 'javascript:richard-is-cool',
},
- ]);
+ ];
this.render(
``,
@@ -154,7 +153,9 @@ moduleFor(
this.assertHTML(escapedHtml);
- runTask(() => this.context.people.setEach('url', 'http://google.com'));
+ runTask(() =>
+ this.context.people.forEach((person) => set(person, 'url', 'http://google.com'))
+ );
this.assertHTML(escapedHtml);
@@ -447,7 +448,7 @@ moduleFor(
this.render(
`{{#each this.people as |person|}}{{capitalize person.firstName}} {{unbound (capitalize person.firstName)}}{{/each}}`,
{
- people: emberA([
+ people: [
{
firstName: 'shooby',
lastName: 'taylor',
@@ -456,7 +457,7 @@ moduleFor(
firstName: 'cindy',
lastName: 'taylor',
},
- ]),
+ ],
}
);
@@ -466,25 +467,21 @@ moduleFor(
this.assertText('SHOOBY SHOOBYCINDY CINDY');
- runTask(() => this.context.people.setEach('firstName', 'chad'));
+ runTask(() => this.context.people.forEach((person) => set(person, 'firstName', 'chad')));
this.assertText('CHAD SHOOBYCHAD CINDY');
runTask(() =>
- set(
- this.context,
- 'people',
- emberA([
- {
- firstName: 'shooby',
- lastName: 'taylor',
- },
- {
- firstName: 'cindy',
- lastName: 'taylor',
- },
- ])
- )
+ set(this.context, 'people', [
+ {
+ firstName: 'shooby',
+ lastName: 'taylor',
+ },
+ {
+ firstName: 'cindy',
+ lastName: 'taylor',
+ },
+ ])
);
this.assertText('SHOOBY SHOOBYCINDY CINDY');
diff --git a/packages/@ember/-internals/glimmer/tests/integration/helpers/unique-id-test.gjs b/packages/@ember/-internals/glimmer/tests/integration/helpers/unique-id-test.gjs
new file mode 100644
index 00000000000..0a6f5f82c81
--- /dev/null
+++ b/packages/@ember/-internals/glimmer/tests/integration/helpers/unique-id-test.gjs
@@ -0,0 +1,144 @@
+import { tracked } from '@glimmer/tracking';
+import { getValue } from '@glimmer/validator';
+import { module, test } from 'qunit';
+import { setupRenderingTest } from 'ember-qunit';
+import { find, findAll, render, settled } from '@ember/test-helpers';
+import { uniqueId, invokeHelper } from '@ember/helper';
+
+class State {
+ @tracked prefix = 'app';
+}
+
+module('Helpers test: {{unique-id}} JS', function () {
+ test('it can be invoked as a JS function', function (assert) {
+ let first = uniqueId();
+ let second = uniqueId();
+
+ assert.notStrictEqual(
+ first,
+ second,
+ `different invocations of uniqueId should produce different values`
+ );
+ });
+
+ test('it can be invoked via invokeHelper', function (assert) {
+ let first = getValue(invokeHelper({}, uniqueId));
+ let second = getValue(invokeHelper({}, uniqueId));
+
+ assert.notStrictEqual(
+ first,
+ second,
+ `different invocations of uniqueId should produce different values`
+ );
+ });
+});
+
+module('Helpers test: {{unique-id}}', function (hooks) {
+ setupRenderingTest(hooks);
+
+ test('it generates a unique id (string) each time', async function (assert) {
+ await render(
+
+ {{(uniqueId)}}
{{(uniqueId)}}
+
+ );
+
+ assert.dom('p').exists({ count: 2 });
+ assert.dom('p:first-of-type').hasAnyText();
+ assert.dom('p:last-of-type').hasAnyText();
+
+ let first = find('p:first-of-type').textContent;
+ let second = find('p:last-of-type').textContent;
+
+ await assert.stableRender();
+
+ assert.notStrictEqual(
+ first,
+ second,
+ `different invocations of {{unique-id}} should produce different values`
+ );
+ });
+
+ test(`when unique-id is used with #let, it remains stable when it's used`, async function (assert) {
+ await render(
+
+ {{#let (uniqueId) as |id|}}{{id}}
{{id}}
{{/let}}
+
+ );
+
+ assert.dom('p').exists({ count: 2 });
+ assert.dom('p:first-of-type').hasAnyText();
+ assert.dom('p:last-of-type').hasAnyText();
+
+ let first = find('p:first-of-type').textContent;
+ let second = find('p:last-of-type').textContent;
+
+ await assert.stableRender();
+
+ assert.strictEqual(first, second, `when unique-id is used as a variable, it remains the same`);
+ });
+
+ test(`unique-id doesn't change if it's concatenated with a value that does change`, async function (assert) {
+ function uniquePart(prefix) {
+ let forAttr = find('label').getAttribute('for');
+
+ assert
+ .dom('input')
+ .hasAttribute(
+ 'id',
+ forAttr,
+ `the label's 'for' attribute should be the same as the input's 'id' attribute`
+ );
+ assert
+ .dom('label')
+ .hasAttribute('for', new RegExp(`^${prefix}-.+$`), 'the id starts with the prefix');
+
+ return forAttr.slice(prefix.length + 1);
+ }
+
+ let state = new State();
+
+ await render(
+
+ {{#let (uniqueId) as |id|}}
+ Enable Feature
+
+ {{/let}}
+
+ );
+
+ let id = uniquePart('app');
+
+ await assert.stableRender();
+
+ state.prefix = 'melanie';
+ await settled();
+
+ let newId = uniquePart('melanie');
+
+ assert.strictEqual(
+ id,
+ newId,
+ `the unique-id part of a concatenated attribute shouldn't change just because a dynamic part of it changed`
+ );
+
+ await assert.stableRender();
+ });
+
+ test('it only generates valid selectors', async function (assert) {
+ let iterations = 1000;
+ let items = new Array(iterations).fill(null);
+
+ await render(
+
+ {{#each items}}{{(uniqueId)}}
{{/each}}
+
+ );
+
+ assert.dom('p').exists({ count: iterations });
+
+ for (let paragraph of findAll('p')) {
+ assert.dom(paragraph).hasText(/^\D/, '{{unique-id}} should produce valid selectors');
+ }
+ });
+});
diff --git a/packages/@ember/-internals/glimmer/tests/integration/helpers/unique-id-test.js b/packages/@ember/-internals/glimmer/tests/integration/helpers/unique-id-test.js
deleted file mode 100644
index 85d1d68df8e..00000000000
--- a/packages/@ember/-internals/glimmer/tests/integration/helpers/unique-id-test.js
+++ /dev/null
@@ -1,199 +0,0 @@
-import {
- AbstractTestCase,
- RenderingTestCase,
- strip,
- moduleFor,
- runTask,
-} from 'internal-test-helpers';
-import { setProperties } from '@ember/object';
-import { uniqueId, invokeHelper } from '@ember/helper';
-import { getValue } from '@glimmer/validator';
-
-moduleFor(
- 'Helpers test: {{unique-id}} JS',
- class extends AbstractTestCase {
- constructor(assert) {
- super(assert);
- this.assert = assert;
- }
- ['@test it can be invoked as a JS function']() {
- let first = uniqueId();
- let second = uniqueId();
-
- this.assert.notStrictEqual(
- first,
- second,
- `different invocations of uniqueId should produce different values`
- );
- }
- ['@test it can be invoked via invokeHelper']() {
- let first = getValue(invokeHelper({}, uniqueId));
- let second = getValue(invokeHelper({}, uniqueId));
-
- this.assert.notStrictEqual(
- first,
- second,
- `different invocations of uniqueId should produce different values`
- );
- }
- }
-);
-moduleFor(
- 'Helpers test: {{unique-id}}',
- class extends RenderingTestCase {
- ['@test it generates a unique id (string) each time']() {
- let { first, second } = this.render(`{{unique-id}}
{{unique-id}}
`, () => {
- let first = this.asElement(this.firstChild);
- let second = this.asElement(this.nthChild(1));
-
- return {
- first: this.asTextContent(first.firstChild),
- second: this.asTextContent(second.firstChild),
- };
- });
-
- this.assert.notStrictEqual(
- first,
- second,
- `different invocations of {{unique-id}} should produce different values`
- );
- }
-
- [`@test when unique-id is used with #let, it remains stable when it's used`]() {
- let { first, second } = this.render(
- strip`
- {{#let (unique-id) as |id|}}
- {{id}}
{{id}}
- {{/let}}
- `,
- () => {
- let first = this.asElement(this.firstChild);
- let second = this.asElement(this.nthChild(1));
-
- return {
- first: this.asTextContent(first.firstChild),
- second: this.asTextContent(second.firstChild),
- };
- }
- );
-
- this.assert.strictEqual(
- first,
- second,
- `when unique-id is used as a variable, it remains the same`
- );
- }
-
- [`@test unique-id doesn't change if it's concatenated with a value that does change`]() {
- class Elements {
- constructor(label, input, assert) {
- this.label = label;
- this.input = input;
- this.assert = assert;
- }
-
- id(regex) {
- let forAttr = this.label.getAttribute('for');
-
- this.assert.strictEqual(
- forAttr,
- this.input.getAttribute('id'),
- `the label's 'for' attribute should be the same as the input's 'id' attribute`
- );
-
- let match = forAttr.match(regex);
-
- this.assert.ok(match, 'the id starts with the prefix');
-
- return match[1];
- }
- }
-
- let { elements, id } = this.render(
- strip`
- {{#let (unique-id) as |id|}}
- Enable Feature
-
- {{/let}}`,
- { prefix: 'app' },
- () => {
- let label = this.asElement(this.firstChild, 'label');
- let input = this.asElement(this.nthChild(1), 'input');
-
- let elements = new Elements(label, input, this.assert);
-
- return { elements, id: elements.id(/^app-(.*)$/) };
- }
- );
-
- this.update({ prefix: 'melanie' }, () => {
- let newId = elements.id(/^melanie-(.*)$/);
-
- this.assert.strictEqual(
- id,
- newId,
- `the unique-id part of a concatenated attribute shouldn't change just because a dynamic part of it changed`
- );
- });
- }
-
- ['@test it only generates valid selectors']() {
- let iterations = 1000;
- let reNumericStart = /^\d/;
-
- let template = '{{unique-id}}
'.repeat(iterations);
- super.render(template);
-
- for (let i = 0; i < iterations; i++) {
- let textNode = this.nthChild(i).firstChild;
- let text = textNode.data;
-
- this.assert.false(
- reNumericStart.test(text),
- `{{unique-id}} should produce valid selectors` + text
- );
- }
- }
-
- render(template, ...rest) {
- // If there are three parameters to `render`, the second parameter is the
- // template's arguments.
- let args = rest.length === 2 ? rest[0] : {};
- // If there are two parameters to `render`, the second parameter is the
- // postcondition. Otherwise, the third parameter is the postcondition.
- let postcondition = rest.length === 2 ? rest[1] : rest[0];
-
- super.render(template, args);
- let result = postcondition();
- this.assertStableRerender();
- return result;
- }
-
- update(args, postcondition) {
- runTask(() => setProperties(this.context, args));
- postcondition();
- this.assertStableRerender();
- }
-
- asElement(node, tag) {
- this.assert.ok(node !== null && node.nodeType === 1);
-
- if (tag) {
- this.assert.strictEqual(node.tagName.toLowerCase(), tag, `Element is <${tag}>`);
- }
-
- return node;
- }
-
- asTextNode(node) {
- this.assert.ok(node !== null && node.nodeType === 3);
- return node;
- }
-
- asTextContent(node) {
- let data = this.asTextNode(node).data;
- this.assert.ok(data.trim().length > 0, `The text node has content`);
- return data;
- }
- }
-);
diff --git a/packages/@ember/-internals/glimmer/tests/integration/helpers/yield-test.js b/packages/@ember/-internals/glimmer/tests/integration/helpers/yield-test.js
index 7831bc442bb..54f004f60fe 100644
--- a/packages/@ember/-internals/glimmer/tests/integration/helpers/yield-test.js
+++ b/packages/@ember/-internals/glimmer/tests/integration/helpers/yield-test.js
@@ -5,7 +5,6 @@ import { precompileTemplate } from '@ember/template-compilation';
import { setComponentTemplate } from '@glimmer/manager';
import Component from '@glimmer/component';
-import { Component as EmberComponent } from '../../utils/helpers';
moduleFor(
'Helpers test: {{yield}} helper',
@@ -344,30 +343,6 @@ moduleFor(
this.assertText('OuterOuter');
}
- // INUR not need with no data update
- ['@test yield should not introduce a view'](assert) {
- let ParentCompComponent = class extends EmberComponent {
- isParentComponent = true;
- };
-
- let ChildCompComponent = class extends EmberComponent {
- didReceiveAttrs() {
- super.didReceiveAttrs();
- let parentView = this.get('parentView');
-
- assert.ok(parentView.get('isParentComponent'));
- }
- };
-
- this.owner.register(
- 'component:parent-comp',
- setComponentTemplate(precompileTemplate('{{yield}}'), ParentCompComponent)
- );
- this.owner.register('component:child-comp', ChildCompComponent);
-
- this.render('{{#parent-comp}}{{child-comp}}{{/parent-comp}}');
- }
-
['@test yield with nested components (#3220)']() {
this.owner.register(
'component:inner-component',
diff --git a/packages/@ember/-internals/glimmer/tests/integration/modifiers/on-test.js b/packages/@ember/-internals/glimmer/tests/integration/modifiers/on-test.js
index 7673295ac58..a4db22b153e 100644
--- a/packages/@ember/-internals/glimmer/tests/integration/modifiers/on-test.js
+++ b/packages/@ember/-internals/glimmer/tests/integration/modifiers/on-test.js
@@ -1,6 +1,6 @@
import { moduleFor, RenderingTestCase, runTask } from 'internal-test-helpers';
import { getInternalModifierManager, setComponentTemplate } from '@glimmer/manager';
-import { on } from '@glimmer/runtime';
+import { on } from '@ember/modifier';
import { precompileTemplate } from '@ember/template-compilation';
import { DEBUG } from '@glimmer/env';
@@ -296,6 +296,84 @@ moduleFor(
this.assertStableRerender();
this.assertCounts({ adds: 2, removes: 0 });
}
+
+ "@test listeners from the invocation run before the element's own listeners, wherever ...attributes is (GH#17877)"(
+ assert
+ ) {
+ let calls = [];
+
+ this.owner.register(
+ 'component:before-splat',
+ setComponentTemplate(
+ precompileTemplate(`Click `),
+ class extends Component {
+ inner = () => calls.push('inner');
+ }
+ )
+ );
+
+ this.owner.register(
+ 'component:after-splat',
+ setComponentTemplate(
+ precompileTemplate(`Click `),
+ class extends Component {
+ inner = () => calls.push('inner');
+ }
+ )
+ );
+
+ this.render(
+ ` `,
+ { outer: () => calls.push('outer') }
+ );
+
+ runTask(() => this.$('#before').click());
+ assert.deepEqual(calls, ['outer', 'inner'], 'own modifier before ...attributes');
+
+ calls = [];
+ runTask(() => this.$('#after').click());
+ assert.deepEqual(calls, ['outer', 'inner'], 'own modifier after ...attributes');
+ }
+
+ "@test listeners on ancestors in a component run after the caller's listeners, unless propagation is stopped (GH#17877)"(
+ assert
+ ) {
+ let calls = [];
+
+ this.owner.register(
+ 'component:wrapped-button',
+ setComponentTemplate(
+ precompileTemplate(
+ `Click
`
+ ),
+ class extends Component {
+ inner = () => calls.push('inner');
+ wrapper = () => calls.push('wrapper');
+ }
+ )
+ );
+
+ let stop = false;
+
+ this.render(` `, {
+ outer: (event) => {
+ calls.push('outer');
+ if (stop) event.stopPropagation();
+ },
+ });
+
+ runTask(() => this.$('button').click());
+ assert.deepEqual(calls, ['outer', 'inner', 'wrapper'], 'the ancestor listener runs last');
+
+ calls = [];
+ stop = true;
+ runTask(() => this.$('button').click());
+ assert.deepEqual(
+ calls,
+ ['outer', 'inner'],
+ "stopping propagation in the caller's listener prevents the ancestor listener"
+ );
+ }
}
);
diff --git a/packages/@ember/-internals/glimmer/tests/integration/mount-test.js b/packages/@ember/-internals/glimmer/tests/integration/mount-test.js
index 3d192ff85c2..1ace0bd0a89 100644
--- a/packages/@ember/-internals/glimmer/tests/integration/mount-test.js
+++ b/packages/@ember/-internals/glimmer/tests/integration/mount-test.js
@@ -434,3 +434,71 @@ moduleFor(
}
}
);
+
+moduleFor(
+ '{{mount}} owner tests',
+ class extends ApplicationTestCase {
+ constructor() {
+ super(...arguments);
+
+ this.owners = {};
+ let owners = this.owners;
+ let capture = (name) =>
+ class extends Component {
+ constructor(owner, args) {
+ super(owner, args);
+ owners[name] = getOwner(this);
+ }
+ };
+
+ this.router.map(function () {
+ this.route('owner-check');
+ });
+
+ this.add(
+ 'component:app-check',
+ setComponentTemplate(precompileTemplate('app-check'), class extends Component {})
+ );
+
+ this.add(
+ 'engine:owner-engine',
+ class extends Engine {
+ router = null;
+ Resolver = ModuleBasedTestResolver;
+
+ init() {
+ super.init(...arguments);
+ this.register(
+ 'template:application',
+ precompileTemplate(' {{@model.foo}}')
+ );
+ this.register(
+ 'component:engine-outer',
+ setComponentTemplate(precompileTemplate(' '), capture('outer'))
+ );
+ this.register(
+ 'component:engine-inner',
+ setComponentTemplate(precompileTemplate('engine-inner'), capture('inner'))
+ );
+ }
+ }
+ );
+ }
+
+ ['@test components rendered inside an engine are created with the engine as their owner']() {
+ this.add(
+ 'template:owner-check',
+ precompileTemplate('{{mount "owner-engine" model=(hash foo=(component "app-check"))}}')
+ );
+
+ return this.visit('/owner-check').then(() => {
+ let { outer, inner } = this.owners;
+
+ this.assert.ok(outer !== this.applicationInstance, 'engine owner is not the app');
+ this.assert.ok(getEngineParent(outer) === this.applicationInstance, 'engine parent');
+ this.assert.ok(inner === outer, 'nested components share the engine owner');
+ this.assertText('engine-innerapp-check');
+ });
+ }
+ }
+);
diff --git a/packages/@ember/-internals/glimmer/tests/integration/refinements-test.gjs b/packages/@ember/-internals/glimmer/tests/integration/refinements-test.gjs
new file mode 100644
index 00000000000..3e24af538fc
--- /dev/null
+++ b/packages/@ember/-internals/glimmer/tests/integration/refinements-test.gjs
@@ -0,0 +1,78 @@
+import { tracked } from '@glimmer/tracking';
+import { module, test } from 'qunit';
+import { setupRenderingTest } from 'ember-qunit';
+import { render, settled } from '@ember/test-helpers';
+
+class State {
+ @tracked var = 'var';
+}
+
+module('syntax refinements', function (hooks) {
+ setupRenderingTest(hooks);
+
+ test('block params should not be refined', async function (assert) {
+ let state = new State();
+ let foo = () => 'bar helper';
+
+ await render(
+
+ {{#let state.var as |foo|}}
+ {{foo}}
+ {{/let}}
+
+ ---
+
+ {{#let state.var as |outlet|}}
+ {{outlet}}
+ {{/let}}
+
+ ---
+
+ {{#let state.var as |mount|}}
+ {{mount}}
+ {{/let}}
+
+ ---
+
+ {{#let state.var as |component|}}
+ {{component}}
+ {{/let}}
+
+ ---
+
+ {{#let state.var as |input|}}
+ {{input}}
+ {{/let}}
+
+ ---
+
+ {{#let state.var as |-with-dynamic-vars|}}
+ {{-with-dynamic-vars}}
+ {{/let}}
+
+ ---
+
+ {{#let state.var as |-in-element|}}
+ {{-in-element}}
+ {{/let}}
+
+ );
+
+ assert.dom().hasText('var --- var --- var --- var --- var --- var --- var');
+ await assert.stableRender();
+
+ state.var = 'RARRR!!!';
+ await settled();
+
+ assert
+ .dom()
+ .hasText(
+ 'RARRR!!! --- RARRR!!! --- RARRR!!! --- RARRR!!! --- RARRR!!! --- RARRR!!! --- RARRR!!!'
+ );
+
+ state.var = 'var';
+ await settled();
+
+ assert.dom().hasText('var --- var --- var --- var --- var --- var --- var');
+ });
+});
diff --git a/packages/@ember/-internals/glimmer/tests/integration/refinements-test.js b/packages/@ember/-internals/glimmer/tests/integration/refinements-test.js
deleted file mode 100644
index 9ea183396f0..00000000000
--- a/packages/@ember/-internals/glimmer/tests/integration/refinements-test.js
+++ /dev/null
@@ -1,66 +0,0 @@
-import { RenderingTestCase, moduleFor, strip, runTask } from 'internal-test-helpers';
-
-import { set } from '@ember/object';
-
-moduleFor(
- 'syntax refinements',
- class extends RenderingTestCase {
- ['@test block params should not be refined']() {
- this.registerHelper('foo', () => 'bar helper');
-
- this.render(
- strip`
- {{#let this.var as |foo|}}
- {{foo}}
- {{/let}}
-
- ---
-
- {{#let this.var as |outlet|}}
- {{outlet}}
- {{/let}}
-
- ---
-
- {{#let this.var as |mount|}}
- {{mount}}
- {{/let}}
-
- ---
-
- {{#let this.var as |component|}}
- {{component}}
- {{/let}}
-
- ---
-
- {{#let this.var as |input|}}
- {{input}}
- {{/let}}
-
- ---
-
- {{#let this.var as |-with-dynamic-vars|}}
- {{-with-dynamic-vars}}
- {{/let}}
-
- ---
-
- {{#let this.var as |-in-element|}}
- {{-in-element}}
- {{/let}}`,
- { var: 'var' }
- );
-
- this.assertText('var---var---var---var---var---var---var');
-
- runTask(() => set(this.context, 'var', 'RARRR!!!'));
-
- this.assertText('RARRR!!!---RARRR!!!---RARRR!!!---RARRR!!!---RARRR!!!---RARRR!!!---RARRR!!!');
-
- runTask(() => set(this.context, 'var', 'var'));
-
- this.assertText('var---var---var---var---var---var---var');
- }
- }
-);
diff --git a/packages/@ember/-internals/glimmer/tests/integration/render-settled-test.gjs b/packages/@ember/-internals/glimmer/tests/integration/render-settled-test.gjs
new file mode 100644
index 00000000000..b6b72fd38c6
--- /dev/null
+++ b/packages/@ember/-internals/glimmer/tests/integration/render-settled-test.gjs
@@ -0,0 +1,84 @@
+import { tracked } from '@glimmer/tracking';
+import { module, test } from 'qunit';
+import { setupRenderingTest } from 'ember-qunit';
+import { render } from '@ember/test-helpers';
+import { renderSettled } from '@ember/renderer';
+import { run, schedule } from '@ember/runloop';
+
+class State {
+ @tracked foo = 'bar';
+}
+
+module('renderSettled', function (hooks) {
+ setupRenderingTest(hooks);
+
+ test('resolves when no rendering is happening', async function (assert) {
+ await renderSettled();
+
+ assert.ok(true, 'resolved even without rendering');
+ });
+
+ test('resolves renderers exist but no runloops are triggered', async function (assert) {
+ let state = new State();
+
+ await render({{state.foo}} );
+
+ await renderSettled();
+
+ assert.ok(true, 'resolved even without runloops');
+ });
+
+ test('does not create extraneous promises', async function (assert) {
+ let first = renderSettled();
+ let second = renderSettled();
+
+ assert.strictEqual(first, second);
+
+ await Promise.all([first, second]);
+ });
+
+ test('resolves when rendering has completed (after property update)', async function (assert) {
+ let state = new State();
+
+ await render({{state.foo}} );
+
+ assert.dom().hasText('bar');
+ state.foo = 'baz';
+ assert.dom().hasText('bar');
+
+ await renderSettled();
+
+ assert.dom().hasText('baz');
+ });
+
+ test('resolves in run loop when renderer has settled', async function (assert) {
+ assert.expect(3);
+
+ let state = new State();
+
+ await render({{state.foo}} );
+
+ assert.dom().hasText('bar');
+
+ let settledInRunLoop;
+
+ run(() => {
+ schedule('actions', null, () => {
+ state.foo = 'set in actions';
+
+ settledInRunLoop = renderSettled();
+
+ schedule('afterRender', null, () => {
+ state.foo = 'set in afterRender';
+ });
+ });
+
+ // still not updated here
+ assert.dom().hasText('bar');
+ });
+
+ await settledInRunLoop;
+
+ assert.dom().hasText('set in afterRender');
+ });
+});
diff --git a/packages/@ember/-internals/glimmer/tests/integration/render-settled-test.js b/packages/@ember/-internals/glimmer/tests/integration/render-settled-test.js
deleted file mode 100644
index f17d201dc5b..00000000000
--- a/packages/@ember/-internals/glimmer/tests/integration/render-settled-test.js
+++ /dev/null
@@ -1,74 +0,0 @@
-import { RenderingTestCase, moduleFor, strip } from 'internal-test-helpers';
-
-import { renderSettled } from '@ember/-internals/glimmer';
-import { run, schedule } from '@ember/runloop';
-
-import { all } from 'rsvp';
-
-moduleFor(
- 'renderSettled',
- class extends RenderingTestCase {
- ['@test resolves when no rendering is happening'](assert) {
- return renderSettled().then(() => {
- assert.ok(true, 'resolved even without rendering');
- });
- }
-
- ['@test resolves renderers exist but no runloops are triggered'](assert) {
- this.render(strip`{{this.foo}}`, { foo: 'bar' });
-
- return renderSettled().then(() => {
- assert.ok(true, 'resolved even without runloops');
- });
- }
-
- ['@test does not create extraneous promises'](assert) {
- let first = renderSettled();
- let second = renderSettled();
-
- assert.strictEqual(first, second);
-
- return all([first, second]);
- }
-
- ['@test resolves when rendering has completed (after property update)']() {
- this.render(strip`{{this.foo}}`, { foo: 'bar' });
-
- this.assertText('bar');
- this.component.set('foo', 'baz');
- this.assertText('bar');
-
- return renderSettled().then(() => {
- this.assertText('baz');
- });
- }
-
- ['@test resolves in run loop when renderer has settled'](assert) {
- assert.expect(3);
-
- this.render(strip`{{this.foo}}`, { foo: 'bar' });
-
- this.assertText('bar');
- let promise;
-
- return run(() => {
- schedule('actions', null, () => {
- this.component.set('foo', 'set in actions');
-
- promise = renderSettled().then(() => {
- this.assertText('set in afterRender');
- });
-
- schedule('afterRender', null, () => {
- this.component.set('foo', 'set in afterRender');
- });
- });
-
- // still not updated here
- this.assertText('bar');
-
- return promise;
- });
- }
- }
-);
diff --git a/packages/@ember/-internals/glimmer/tests/integration/svg-test.gjs b/packages/@ember/-internals/glimmer/tests/integration/svg-test.gjs
new file mode 100644
index 00000000000..210fdfcae5a
--- /dev/null
+++ b/packages/@ember/-internals/glimmer/tests/integration/svg-test.gjs
@@ -0,0 +1,117 @@
+import { tracked } from '@glimmer/tracking';
+import { module, test } from 'qunit';
+import { setupRenderingTest } from 'ember-qunit';
+import { render, settled } from '@ember/test-helpers';
+
+class Model {
+ @tracked viewBoxString;
+ @tracked color;
+
+ constructor({ viewBoxString, color }) {
+ this.viewBoxString = viewBoxString;
+ this.color = color;
+ }
+}
+
+class State {
+ @tracked model;
+
+ constructor(model) {
+ this.model = new Model(model);
+ }
+}
+
+module('SVG element tests', function (hooks) {
+ setupRenderingTest(hooks);
+
+ test('unquoted viewBox property is output', async function (assert) {
+ let viewBoxString = '0 0 100 100';
+ let state = new State({ viewBoxString });
+
+ await render(
+
+
+
+ );
+
+ await assert.stableRender('
');
+
+ state.model.viewBoxString = null;
+ await settled();
+
+ assert.dom('div').doesNotHaveAttribute('svg');
+
+ state.model = new Model({ viewBoxString });
+ await settled();
+
+ assert.dom('svg').hasAttribute('viewBox', '0 0 100 100');
+ });
+
+ test('quoted viewBox property is output', async function (assert) {
+ let viewBoxString = '0 0 100 100';
+ let state = new State({ viewBoxString });
+
+ await render(
+
+
+
+ );
+
+ await assert.stableRender('
');
+
+ state.model.viewBoxString = null;
+ await settled();
+
+ assert.dom('div').doesNotHaveAttribute('svg');
+
+ state.model = new Model({ viewBoxString });
+ await settled();
+
+ assert.dom('svg').hasAttribute('viewBox', '0 0 100 100');
+ });
+
+ test('quoted viewBox property is concat', async function (assert) {
+ let viewBoxString = '100 100';
+ let state = new State({ viewBoxString });
+
+ await render(
+
+
+
+ );
+
+ await assert.stableRender('
');
+
+ state.model.viewBoxString = '200 200';
+ await settled();
+
+ assert.dom('svg').hasAttribute('viewBox', '0 0 200 200');
+
+ state.model = new Model({ viewBoxString });
+ await settled();
+
+ assert.dom('svg').hasAttribute('viewBox', '0 0 100 100');
+ });
+
+ test('class is output', async function (assert) {
+ let state = new State({ color: 'blue' });
+
+ await render(
+
+
+
+ );
+
+ await assert.stableRender('
');
+
+ state.model.color = 'yellow';
+ await settled();
+
+ assert.dom('svg').hasAttribute('class', 'yellow tall');
+
+ state.model = new Model({ color: 'blue' });
+ await settled();
+
+ assert.dom('svg').hasAttribute('class', 'blue tall');
+ });
+});
diff --git a/packages/@ember/-internals/glimmer/tests/integration/svg-test.js b/packages/@ember/-internals/glimmer/tests/integration/svg-test.js
deleted file mode 100644
index 4c1fe44016a..00000000000
--- a/packages/@ember/-internals/glimmer/tests/integration/svg-test.js
+++ /dev/null
@@ -1,158 +0,0 @@
-import { RenderingTestCase, moduleFor, strip, runTask } from 'internal-test-helpers';
-
-import { set } from '@ember/object';
-
-moduleFor(
- 'SVG element tests',
- class extends RenderingTestCase {
- ['@test unquoted viewBox property is output'](assert) {
- let viewBoxString = '0 0 100 100';
-
- this.render('
', {
- model: {
- viewBoxString,
- },
- });
-
- this.assertInnerHTML(strip`
-
-
-
- `);
-
- runTask(() => this.rerender());
-
- this.assertInnerHTML(strip`
-
-
-
- `);
-
- runTask(() => set(this.context, 'model.viewBoxString', null));
-
- assert.equal(this.firstChild.getAttribute('svg'), null);
-
- runTask(() => set(this.context, 'model', { viewBoxString }));
-
- this.assertInnerHTML(strip`
-
-
-
- `);
- }
-
- ['@test quoted viewBox property is output'](assert) {
- let viewBoxString = '0 0 100 100';
-
- this.render('
', {
- model: {
- viewBoxString,
- },
- });
-
- this.assertInnerHTML(strip`
-
-
-
- `);
-
- runTask(() => this.rerender());
-
- this.assertInnerHTML(strip`
-
-
-
- `);
-
- runTask(() => set(this.context, 'model.viewBoxString', null));
-
- assert.equal(this.firstChild.getAttribute('svg'), null);
-
- runTask(() => set(this.context, 'model', { viewBoxString }));
-
- this.assertInnerHTML(strip`
-
-
-
- `);
- }
-
- ['@test quoted viewBox property is concat']() {
- let viewBoxString = '100 100';
-
- this.render('
', {
- model: {
- viewBoxString,
- },
- });
-
- this.assertInnerHTML(strip`
-
-
-
- `);
-
- runTask(() => this.rerender());
-
- this.assertInnerHTML(strip`
-
-
-
- `);
-
- runTask(() => set(this.context, 'model.viewBoxString', '200 200'));
-
- this.assertInnerHTML(strip`
-
-
-
- `);
-
- runTask(() => set(this.context, 'model', { viewBoxString }));
-
- this.assertInnerHTML(strip`
-
-
-
- `);
- }
-
- ['@test class is output']() {
- this.render("
", {
- model: {
- color: 'blue',
- },
- });
-
- this.assertInnerHTML(strip`
-
-
-
- `);
-
- runTask(() => this.rerender());
-
- this.assertInnerHTML(strip`
-
-
-
- `);
-
- runTask(() => set(this.context, 'model.color', 'yellow'));
-
- this.assertInnerHTML(strip`
-
-
-
- `);
-
- runTask(() => set(this.context, 'model', { color: 'blue' }));
-
- this.assertInnerHTML(strip`
-
-
-
- `);
- }
- }
-);
diff --git a/packages/@ember/-internals/glimmer/tests/integration/syntax/each-test.js b/packages/@ember/-internals/glimmer/tests/integration/syntax/each-test.js
index 83216933002..db550e99be9 100644
--- a/packages/@ember/-internals/glimmer/tests/integration/syntax/each-test.js
+++ b/packages/@ember/-internals/glimmer/tests/integration/syntax/each-test.js
@@ -5,6 +5,7 @@ import {
strip,
runTask,
ignoreDeprecation,
+ defineSimpleModifier,
} from 'internal-test-helpers';
import { DEPRECATIONS } from '@ember/-internals/deprecations';
@@ -17,7 +18,8 @@ import { precompileTemplate } from '@ember/template-compilation';
import { setComponentTemplate } from '@glimmer/manager';
import Component from '@glimmer/component';
-import { Component as EmberComponent, htmlSafe } from '../../utils/helpers';
+import { tracked } from '@glimmer/tracking';
+import { htmlSafe } from '../../utils/helpers';
import {
TogglingSyntaxConditionalsTest,
TruthyGenerator,
@@ -525,26 +527,27 @@ class EachTest extends AbstractEachTest {
[`@test updating and setting within #each`]() {
this.makeList([{ value: 1 }, { value: 2 }, { value: 3 }]);
- let FooBarComponent = class extends EmberComponent {
- init() {
- super.init(...arguments);
- this.isEven = true;
- this.tagName = 'li';
- }
+ let FooBarComponent = class extends Component {
+ @tracked isEven = true;
- _isEven() {
- this.set('isEven', this.get('item.value') % 2 === 0);
- }
+ isInstalled = false;
- didUpdate() {
- this._isEven();
- }
+ // Like the classic `didUpdate` hook, this sets state only after an update.
+ updateIsEven = defineSimpleModifier((element, [value]) => {
+ if (this.isInstalled) {
+ this.isEven = value % 2 === 0;
+ }
+
+ this.isInstalled = true;
+ });
};
this.owner.register(
'component:foo-bar',
setComponentTemplate(
- precompileTemplate('{{#if this.isEven}}{{this.item.value}}{{/if}}'),
+ precompileTemplate(
+ '{{#if this.isEven}}{{@item.value}}{{/if}} '
+ ),
FooBarComponent
)
);
@@ -1141,7 +1144,7 @@ if (!ARRAY_PROXY_REMOVED) {
init: function () {
this._super(...arguments);
- this.set('content', emberA(wrapped));
+ this.set('content', wrapped);
},
}).create()
);
diff --git a/packages/@ember/-internals/glimmer/tests/integration/syntax/if-unless-test.js b/packages/@ember/-internals/glimmer/tests/integration/syntax/if-unless-test.js
index 19b999faf7b..b8e6cc37a58 100644
--- a/packages/@ember/-internals/glimmer/tests/integration/syntax/if-unless-test.js
+++ b/packages/@ember/-internals/glimmer/tests/integration/syntax/if-unless-test.js
@@ -1,6 +1,5 @@
import { RenderingTestCase, moduleFor, strip, runTask } from 'internal-test-helpers';
-import { A as emberA } from '@ember/array';
import { set } from '@ember/object';
import { precompileTemplate } from '@ember/template-compilation';
import { setComponentTemplate } from '@glimmer/manager';
@@ -65,7 +64,7 @@ moduleFor(
{{else}}
Nothing Here!
{{/if}}`,
- { cond: true, numbers: emberA([1, 2, 3]) }
+ { cond: true, numbers: [1, 2, 3] }
);
this.assertText('123');
diff --git a/packages/@ember/-internals/glimmer/tests/integration/syntax/let-test.js b/packages/@ember/-internals/glimmer/tests/integration/syntax/let-test.js
index d9f71186d8a..f1270479267 100644
--- a/packages/@ember/-internals/glimmer/tests/integration/syntax/let-test.js
+++ b/packages/@ember/-internals/glimmer/tests/integration/syntax/let-test.js
@@ -9,7 +9,7 @@ import {
import { DEPRECATIONS } from '@ember/-internals/deprecations';
import { get, set } from '@ember/object';
-import { A as emberA, removeAt } from '@ember/array';
+import { trackedArray } from '@ember/reactive/collections';
import ObjectProxy from '@ember/object/proxy';
moduleFor(
@@ -173,7 +173,7 @@ moduleFor(
this.render(
`{{#let this.arrayThing as |words|}}{{#each words as |word|}}{{word}}{{/each}}{{/let}}`,
{
- arrayThing: emberA(['Hello', ' ', 'world']),
+ arrayThing: trackedArray(['Hello', ' ', 'world']),
}
);
@@ -185,10 +185,10 @@ moduleFor(
runTask(() => {
let array = get(this.context, 'arrayThing');
- array.replace(0, 1, ['Goodbye']);
- removeAt(array, 1);
- array.insertAt(1, ', ');
- array.pushObject('!');
+ array.splice(0, 1, 'Goodbye');
+ array.splice(1, 1);
+ array.splice(1, 0, ', ');
+ array.push('!');
});
this.assertText('Goodbye, world!');
diff --git a/packages/@ember/-internals/glimmer/tests/utils/glimmerish-component.js b/packages/@ember/-internals/glimmer/tests/utils/glimmerish-component.js
deleted file mode 100644
index 6c9a4aca5fb..00000000000
--- a/packages/@ember/-internals/glimmer/tests/utils/glimmerish-component.js
+++ /dev/null
@@ -1,29 +0,0 @@
-import { componentCapabilities } from '@glimmer/manager';
-import { setComponentManager } from '@ember/-internals/glimmer';
-import { setOwner } from '@ember/-internals/owner';
-
-class GlimmerishComponentManager {
- constructor(owner) {
- this.capabilities = componentCapabilities('3.13', { updateHook: false });
- this.owner = owner;
- }
-
- createComponent(Factory, args) {
- return new Factory(this.owner, args.named);
- }
-
- getContext(component) {
- return component;
- }
-}
-
-class GlimmerishComponent {
- constructor(owner, args) {
- setOwner(this, owner);
- this.args = args;
- }
-}
-
-setComponentManager((owner) => new GlimmerishComponentManager(owner), GlimmerishComponent);
-
-export default GlimmerishComponent;
diff --git a/packages/@ember/-internals/glimmer/tests/utils/shared-conditional-tests.js b/packages/@ember/-internals/glimmer/tests/utils/shared-conditional-tests.js
index cce81d78f39..07ac2d0ecc4 100644
--- a/packages/@ember/-internals/glimmer/tests/utils/shared-conditional-tests.js
+++ b/packages/@ember/-internals/glimmer/tests/utils/shared-conditional-tests.js
@@ -315,6 +315,34 @@ export const ObjectTestCases = {
// Testing behaviors related to arrays and array proxies
export const ArrayTestCases = {
['@test it considers empty arrays falsy']() {
+ this.renderValues(['hello'], []);
+
+ this.assertText('T1F2');
+
+ runTask(() => this.rerender());
+
+ this.assertText('T1F2');
+
+ runTask(() => set(this.context, 'cond1', []));
+
+ this.assertText('F1F2');
+
+ runTask(() => {
+ set(this.context, 'cond1', ['hello']);
+ set(this.context, 'cond2', [1]);
+ });
+
+ this.assertText('T1T2');
+
+ runTask(() => {
+ set(this.context, 'cond1', ['hello']);
+ set(this.context, 'cond2', []);
+ });
+
+ this.assertText('T1F2');
+ },
+
+ ['@test it considers empty Ember arrays falsy']() {
this.renderValues(emberA(['hello']), emberA());
this.assertText('T1F2');
@@ -335,15 +363,15 @@ export const ArrayTestCases = {
this.assertText('T1T2');
runTask(() => {
- set(this.context, 'cond1', emberA(['hello']));
- set(this.context, 'cond2', emberA());
+ set(this.context, 'cond1', ['hello']);
+ set(this.context, 'cond2', []);
});
this.assertText('T1F2');
},
[`${testUnless(ARRAY_PROXY_REMOVED)} @test it considers array proxies without content falsy`]() {
- this.renderValues(arrayProxy({ content: emberA(['hello']) }), arrayProxy({ content: null }));
+ this.renderValues(arrayProxy({ content: ['hello'] }), arrayProxy({ content: null }));
this.assertText('T1F2');
@@ -359,14 +387,14 @@ export const ArrayTestCases = {
this.assertText('F1F2');
runTask(() => {
- set(this.context, 'cond1.content', emberA(['hello']));
- set(this.context, 'cond2.content', emberA([1]));
+ set(this.context, 'cond1.content', ['hello']);
+ set(this.context, 'cond2.content', [1]);
});
this.assertText('T1T2');
runTask(() => {
- set(this.context, 'cond1', arrayProxy({ content: emberA(['hello']) }));
+ set(this.context, 'cond1', arrayProxy({ content: ['hello'] }));
set(this.context, 'cond2', arrayProxy({ content: null }));
});
@@ -397,8 +425,8 @@ export const ArrayTestCases = {
this.assertText('T1T2');
runTask(() => {
- set(this.context, 'cond1', arrayProxy({ content: emberA(['hello']) }));
- set(this.context, 'cond2', arrayProxy({ content: emberA() }));
+ set(this.context, 'cond1', arrayProxy({ content: ['hello'] }));
+ set(this.context, 'cond2', arrayProxy({ content: [] }));
});
this.assertText('T1F2');
diff --git a/packages/@ember/-internals/runtime/tests/core/is_array_test.js b/packages/@ember/-internals/runtime/tests/core/is_array_test.js
index a11de641cb0..0e83d6cadb2 100644
--- a/packages/@ember/-internals/runtime/tests/core/is_array_test.js
+++ b/packages/@ember/-internals/runtime/tests/core/is_array_test.js
@@ -1,4 +1,4 @@
-import { A as emberA, isArray } from '@ember/array';
+import { isArray } from '@ember/array';
import ArrayProxy from '@ember/array/proxy';
import EmberObject from '@ember/object';
import { window } from '@ember/-internals/browser-environment';
@@ -38,7 +38,7 @@ moduleFor(
)} @test Ember.isArray with an ArrayProxy`](assert) {
expectDeprecation(/`ArrayProxy` is deprecated/, DEPRECATIONS.DEPRECATE_ARRAY_PROXY.isEnabled);
- let arrayProxy = ArrayProxy.create({ content: emberA() });
+ let arrayProxy = ArrayProxy.create({ content: [] });
assert.equal(isArray(arrayProxy), true, 'ArrayProxy');
}
diff --git a/packages/@ember/-internals/runtime/tests/core/is_empty_test.js b/packages/@ember/-internals/runtime/tests/core/is_empty_test.js
index f339fa3e28a..a6ee37c8b15 100644
--- a/packages/@ember/-internals/runtime/tests/core/is_empty_test.js
+++ b/packages/@ember/-internals/runtime/tests/core/is_empty_test.js
@@ -1,7 +1,6 @@
import { isEmpty } from '@ember/utils';
import ArrayProxy from '@ember/array/proxy';
import ObjectProxy from '@ember/object/proxy';
-import { A as emberA } from '@ember/array';
import { moduleFor, AbstractTestCase, expectDeprecation, testUnless } from 'internal-test-helpers';
import { DEPRECATIONS } from '@ember/-internals/deprecations';
@@ -17,7 +16,7 @@ moduleFor(
expectDeprecation(/`ArrayProxy` is deprecated/, DEPRECATIONS.DEPRECATE_ARRAY_PROXY.isEnabled);
- let arrayProxy = ArrayProxy.create({ content: emberA() });
+ let arrayProxy = ArrayProxy.create({ content: [] });
assert.equal(true, isEmpty(arrayProxy), 'for an ArrayProxy that has empty content');
}
@@ -33,7 +32,7 @@ moduleFor(
DEPRECATIONS.DEPRECATE_OBJECT_PROXY.isEnabled
);
- let arrayProxy = ArrayProxy.create({ content: emberA([]) });
+ let arrayProxy = ArrayProxy.create({ content: [] });
let objectProxy = ObjectProxy.create({ content: arrayProxy });
assert.equal(
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 9dac1d8fb36..6c5fcb01d5c 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
@@ -1,6 +1,5 @@
import { peekMeta } from '@ember/-internals/meta';
import ArrayProxy from '@ember/array/proxy';
-import { A } from '@ember/array';
import { moduleFor, AbstractTestCase, expectDeprecation, testUnless } from 'internal-test-helpers';
import { DEPRECATIONS } from '@ember/-internals/deprecations';
@@ -24,7 +23,7 @@ moduleFor(
[`${testUnless(DEPRECATIONS.DEPRECATE_ARRAY_PROXY.isRemoved)} @test setting 'content' adds listeners correctly`](
assert
) {
- let content = A();
+ let content = [];
let proxy = ArrayProxy.create();
assert.deepEqual(sortedListenersFor(content, '@array:before'), []);
@@ -43,8 +42,8 @@ moduleFor(
[`${testUnless(DEPRECATIONS.DEPRECATE_ARRAY_PROXY.isRemoved)} @test changing 'content' adds and removes listeners correctly`](
assert
) {
- let content1 = A();
- let content2 = A();
+ let content1 = [];
+ let content2 = [];
let proxy = ArrayProxy.create({ content: content1 });
assert.deepEqual(sortedListenersFor(content1, '@array:before'), []);
diff --git a/packages/@ember/helper/index.ts b/packages/@ember/helper/index.ts
index 945dcd1c469..c50fc30e417 100644
--- a/packages/@ember/helper/index.ts
+++ b/packages/@ember/helper/index.ts
@@ -477,8 +477,35 @@ export interface ConcatHelper extends Opaque<'helper:concat'> {}
*
* ```
*
- * This will display the result of `@foo.item1` when `index` is `1`, and
- * `this.foo.item2` when `index` is `2`, etc.
+ * This will display the result of `@address.street` when `@field` is
+ * `"street"`, and `@address.city` when `@field` is `"city"`, etc.
+ *
+ * The key is treated as a path, so a key containing a period is split on each
+ * period and looked up one segment at a time:
+ *
+ * ```gjs
+ * import { get } from '@ember/helper';
+ *
+ * const person = { address: { city: 'Portland' } };
+ *
+ *
+ * {{get person "address.city"}} {{! Portland }}
+ *
+ * ```
+ *
+ * This also means that `{{get}}` cannot read a property whose name itself
+ * contains a period. `{{get obj "some.key"}}` looks up `obj.some.key`, not
+ * `obj["some.key"]`, and returns `undefined` when `obj.some` does not exist.
+ * To read such a key, use a plain function instead:
+ *
+ * ```gjs
+ * const obj = { 'some.key': 'some value' };
+ * const lookup = (object, key) => object[key];
+ *
+ *
+ * {{lookup obj "some.key"}} {{! some value }}
+ *
+ * ```
*
* @method get
* @for @ember/helper
diff --git a/packages/@ember/modifier/index.ts b/packages/@ember/modifier/index.ts
index 428bc4f71b5..88a0ddf4842 100644
--- a/packages/@ember/modifier/index.ts
+++ b/packages/@ember/modifier/index.ts
@@ -1,3 +1,8 @@
+/**
+@module @ember/modifier
+@public
+*/
+
import { setModifierManager as glimmerSetModifierManager } from '@glimmer/manager/lib/public/api';
import type Owner from '@ember/owner';
@@ -5,6 +10,132 @@ import type { ModifierManager } from '@glimmer/interfaces';
export { on, type OnModifier } from './on';
+/**
+ Sets the modifier manager for an object or function.
+
+ ```js
+ import { setModifierManager } from '@ember/modifier';
+
+ export default setModifierManager(
+ (owner) => new MyModifierManager(owner),
+ class MyModifier {}
+ );
+ ```
+
+ When a value is used as a modifier in a template, the modifier manager is
+ looked up on the object by walking up its prototype chain and finding the
+ first modifier manager. This manager then receives the value and can create
+ and manage an instance of a modifier from it. This lets addons such as
+ `ember-modifier` design their own high-level modifier APIs on top of a small,
+ stable primitive.
+
+ Modifier managers were introduced in
+ [RFC #373, Element Modifier Managers](https://rfcs.emberjs.com/id/0373-element-modifier-managers),
+ which describes the design and motivation in more detail.
+
+ `setModifierManager` receives two arguments:
+
+ 1. A factory function, which receives the `owner` and returns an instance of a
+ modifier manager.
+ 2. A modifier definition, which is the object or function to associate the
+ factory function with.
+
+ The first time the definition is looked up, the factory function will be
+ called to create the modifier manager. It will be cached per `owner`, and in
+ subsequent lookups the cached modifier manager will be used instead. Because
+ one manager is shared by every usage of the definition, managers should
+ generally be stateless, keeping per-modifier state in the value returned from
+ `createModifier`.
+
+ Modifier managers must fulfill the following interface (This example uses
+ [TypeScript interfaces](https://www.typescriptlang.org/docs/handbook/interfaces.html)
+ for precision, you do not need to write modifier managers using TypeScript):
+
+ ```ts
+ interface ModifierManager {
+ capabilities: ModifierCapabilities;
+
+ createModifier(definition: ModifierDefinition, args: TemplateArgs): ModifierStateBucket;
+
+ installModifier(bucket: ModifierStateBucket, element: Element, args: TemplateArgs): void;
+
+ updateModifier(bucket: ModifierStateBucket, args: TemplateArgs): void;
+
+ destroyModifier(bucket: ModifierStateBucket, args: TemplateArgs): void;
+ }
+
+ interface TemplateArgs {
+ positional: unknown[];
+ named: Record;
+ }
+ ```
+
+ The `capabilities` property _must_ be set to the result of calling the
+ `capabilities` function exported from `@ember/modifier`.
+
+ #### `createModifier`
+
+ Receives the modifier definition and the arguments, and returns a state
+ bucket that is passed to the other hooks. It is called before the element is
+ available, so it should not do any DOM work.
+
+ #### `installModifier`
+
+ Called once the element the modifier is attached to has been inserted into
+ the DOM. Any args consumed while this hook runs are tracked, unless the
+ `disableAutoTracking` capability is enabled.
+
+ #### `updateModifier`
+
+ Called when any of the tracked args consumed during the previous
+ `installModifier` or `updateModifier` call have changed.
+
+ #### `destroyModifier`
+
+ Called when the modifier is torn down, either because the element is being
+ removed or because the modifier is no longer applied to it.
+
+ An example manager for a function-based modifier:
+
+ ```js
+ import { setModifierManager, capabilities } from '@ember/modifier';
+
+ class FunctionModifierManager {
+ capabilities = capabilities('3.22');
+
+ createModifier(fn) {
+ return { fn, element: null, teardown: null };
+ }
+
+ installModifier(state, element, args) {
+ state.element = element;
+ state.teardown = state.fn(element, args.positional, args.named);
+ }
+
+ updateModifier(state, args) {
+ state.teardown?.();
+ state.teardown = state.fn(state.element, args.positional, args.named);
+ }
+
+ destroyModifier(state) {
+ state.teardown?.();
+ }
+ }
+
+ export function modifier(fn) {
+ return setModifierManager(() => new FunctionModifierManager(), fn);
+ }
+ ```
+
+ @method setModifierManager
+ @for @ember/modifier
+ @static
+ @param {Function} factory A factory function which receives the owner, and returns a modifier manager
+ @param {object} definition The definition to associate the manager factory with
+ @return {object} The definition passed into setModifierManager
+ @since 3.8.0
+ @public
+*/
// NOTE: this uses assignment to *require* that the `glimmerSetModifierManager`
// is legally assignable to this type, i.e. that variance is properly upheld.
export const setModifierManager: (
@@ -15,4 +146,42 @@ export const setModifierManager: (
export type { ModifierManager };
export type { ModifierCapabilities } from '@glimmer/interfaces';
+/**
+ `capabilities` returns a capabilities configuration which can be used to modify
+ the behavior of a modifier manager. Manager capabilities _must_ be provided
+ using the `capabilities` function, as the underlying implementation can change
+ over time.
+
+ The first argument is a version string, which is the version of Ember that the
+ capabilities were defined in. Currently the only supported version is `'3.22'`.
+
+ ```js
+ capabilities('3.22');
+ ```
+
+ The second argument is an object of capabilities and boolean values indicating
+ whether they are enabled or disabled.
+
+ ```js
+ capabilities('3.22', { disableAutoTracking: true });
+ ```
+
+ ### `3.22` capabilities
+
+ #### `disableAutoTracking`
+
+ - Default value: false
+
+ When enabled, args consumed during `installModifier` and `updateModifier` are
+ not tracked, so changes to them will not cause `updateModifier` to be called.
+
+ @method capabilities
+ @static
+ @for @ember/modifier
+ @param {String} managerApiVersion The version of capabilities that are being used
+ @param options The capabilities values
+ @return {Capabilities} The capabilities object instance
+ @since 3.22.0
+ @public
+*/
export { modifierCapabilities as capabilities } from '@glimmer/manager/lib/public/modifier';
diff --git a/packages/@ember/object/tests/observable_test.js b/packages/@ember/object/tests/observable_test.js
index 33f63a40743..a6c3374cf73 100644
--- a/packages/@ember/object/tests/observable_test.js
+++ b/packages/@ember/object/tests/observable_test.js
@@ -641,7 +641,7 @@ moduleFor(
toggleVal: true,
observedProperty: 'beingWatched',
testRemove: 'observerToBeRemoved',
- normalArray: emberA([1, 2, 3, 4, 5]),
+ normalArray: [1, 2, 3, 4, 5],
});
}
@@ -719,6 +719,10 @@ moduleFor(
}
async ['@test should notify array observer when array changes'](assert) {
+ object.set('normalArray', emberA([1, 2, 3, 4, 5]));
+ await runLoopSettled();
+ object.abnormal = 'zeroValue';
+
get(object, 'normalArray').replace(0, 0, [6]);
await runLoopSettled();
diff --git a/packages/@ember/routing/lib/utils.ts b/packages/@ember/routing/lib/utils.ts
index fd68488fde2..7175a5e8322 100644
--- a/packages/@ember/routing/lib/utils.ts
+++ b/packages/@ember/routing/lib/utils.ts
@@ -290,6 +290,24 @@ export function shallowEqual(a: A, b: B): bo
return aCount === bCount;
}
+// Apps receive this as `defaultValueType` in `serializeQueryParam` and
+// `deserializeQueryParam`, so it keeps the type names of the common defaults.
+export function queryParamType(value: unknown): string {
+ if (value === null) {
+ return 'null';
+ }
+
+ if (Array.isArray(value)) {
+ return 'array';
+ }
+
+ if (value instanceof Date) {
+ return 'date';
+ }
+
+ return typeof value;
+}
+
function isRouteOptions(value: unknown): value is RouteOptions {
if (value && typeof value === 'object') {
let qps = (value as RouteOptions).queryParams;
diff --git a/packages/@ember/routing/route.ts b/packages/@ember/routing/route.ts
index e6ecb50922f..3390ca0e0de 100644
--- a/packages/@ember/routing/route.ts
+++ b/packages/@ember/routing/route.ts
@@ -45,6 +45,7 @@ import {
calculateCacheKey,
normalizeControllerQueryParams,
prefixRouteNameArg,
+ queryParamType,
stashParamNames,
} from './lib/utils';
@@ -1958,24 +1959,6 @@ function addQueryParamsObservers(controller: any, propNames: string[]) {
});
}
-// Apps receive this as `defaultValueType` in `serializeQueryParam` and
-// `deserializeQueryParam`, so it keeps the type names of the common defaults.
-function queryParamType(value: unknown): string {
- if (value === null) {
- return 'null';
- }
-
- if (Array.isArray(value)) {
- return 'array';
- }
-
- if (value instanceof Date) {
- return 'date';
- }
-
- return typeof value;
-}
-
function getEngineRouteName(engine: EngineInstance, routeName: string) {
if (engine.routable) {
let prefix = engine.mountPoint;
diff --git a/packages/@ember/routing/router.ts b/packages/@ember/routing/router.ts
index d39fb1385de..3f6a356eaa1 100644
--- a/packages/@ember/routing/router.ts
+++ b/packages/@ember/routing/router.ts
@@ -18,6 +18,7 @@ import {
calculateCacheKey,
extractRouteArgs,
getActiveTargetName,
+ queryParamType,
resemblesURL,
} from './lib/utils';
import type { RouteArgs, RouteOptions } from './lib/utils';
@@ -1099,10 +1100,7 @@ class EmberRouter extends EmberObject {
} else if (value === undefined) {
return; // We don't serialize undefined values
} else {
- queryParams[key] = this._serializeQueryParam(
- value,
- Array.isArray(value) ? 'array' : typeof value
- );
+ queryParams[key] = this._serializeQueryParam(value, queryParamType(value));
}
}
);
diff --git a/packages/@ember/routing/tests/location/history_location_test.js b/packages/@ember/routing/tests/location/history_location_test.js
index 68505a5a3ec..e5b992957fe 100644
--- a/packages/@ember/routing/tests/location/history_location_test.js
+++ b/packages/@ember/routing/tests/location/history_location_test.js
@@ -68,15 +68,16 @@ moduleFor(
['@test HistoryLocation initState does not get fired on init'](assert) {
assert.expect(1);
- HistoryTestLocation.reopen({
+ HistoryTestLocation = class extends HistoryTestLocation {
init() {
assert.ok(true, 'init was called');
- this._super(...arguments);
- },
+ super.init(...arguments);
+ }
+
initState() {
assert.ok(false, 'initState() should not be called automatically');
- },
- });
+ }
+ };
createLocation();
}
@@ -93,7 +94,7 @@ moduleFor(
createLocation();
location.initState();
- assert.strictEqual(location.get('baseURL'), '/foo/');
+ assert.strictEqual(location.baseURL, '/foo/');
} finally {
document.head.removeChild(base);
}
@@ -111,7 +112,7 @@ moduleFor(
createLocation();
location.initState();
- assert.strictEqual(location.get('baseURL'), '');
+ assert.strictEqual(location.baseURL, '');
} finally {
document.head.removeChild(base);
}
@@ -120,20 +121,20 @@ moduleFor(
['@test base URL is removed when retrieving the current pathname'](assert) {
assert.expect(1);
- HistoryTestLocation.reopen({
+ HistoryTestLocation = class extends HistoryTestLocation {
init() {
- this._super(...arguments);
+ super.init(...arguments);
set(this, 'location', mockBrowserLocation('/base/foo/bar'));
set(this, 'baseURL', '/base/');
- },
+ }
initState() {
- this._super(...arguments);
+ super.initState(...arguments);
assert.equal(this.getURL(), '/foo/bar');
- },
- });
+ }
+ };
createLocation();
location.initState();
@@ -142,14 +143,14 @@ moduleFor(
['@test base URL is preserved when moving around'](assert) {
assert.expect(2);
- HistoryTestLocation.reopen({
+ HistoryTestLocation = class extends HistoryTestLocation {
init() {
- this._super(...arguments);
+ super.init(...arguments);
set(this, 'location', mockBrowserLocation('/base/foo/bar'));
set(this, 'baseURL', '/base/');
- },
- });
+ }
+ };
createLocation();
location.initState();
@@ -184,15 +185,15 @@ moduleFor(
['@test HistoryLocation.getURL() returns the current url, excluding both rootURL and baseURL'](
assert
) {
- HistoryTestLocation.reopen({
+ HistoryTestLocation = class extends HistoryTestLocation {
init() {
- this._super(...arguments);
+ super.init(...arguments);
set(this, 'location', mockBrowserLocation('/base/foo/bar'));
set(this, 'rootURL', '/app/');
set(this, 'baseURL', '/base/');
- },
- });
+ }
+ };
createLocation();
@@ -202,14 +203,14 @@ moduleFor(
['@test HistoryLocation.getURL() returns the current url, does not remove rootURL if its not at start of url'](
assert
) {
- HistoryTestLocation.reopen({
+ HistoryTestLocation = class extends HistoryTestLocation {
init() {
- this._super(...arguments);
+ super.init(...arguments);
set(this, 'location', mockBrowserLocation('/foo/bar/baz'));
set(this, 'rootURL', '/bar/');
- },
- });
+ }
+ };
createLocation();
@@ -219,13 +220,13 @@ moduleFor(
['@test HistoryLocation.getURL() will not remove the rootURL when only a partial match'](
assert
) {
- HistoryTestLocation.reopen({
+ HistoryTestLocation = class extends HistoryTestLocation {
init() {
- this._super(...arguments);
+ super.init(...arguments);
set(this, 'location', mockBrowserLocation('/bars/baz'));
set(this, 'rootURL', '/bar/');
- },
- });
+ }
+ };
createLocation();
@@ -235,14 +236,14 @@ moduleFor(
['@test HistoryLocation.getURL() returns the current url, does not remove baseURL if its not at start of url'](
assert
) {
- HistoryTestLocation.reopen({
+ HistoryTestLocation = class extends HistoryTestLocation {
init() {
- this._super(...arguments);
+ super.init(...arguments);
set(this, 'location', mockBrowserLocation('/foo/bar/baz'));
set(this, 'baseURL', '/bar/');
- },
- });
+ }
+ };
createLocation();
@@ -252,13 +253,13 @@ moduleFor(
['@test HistoryLocation.getURL() will not remove the baseURL when only a partial match'](
assert
) {
- HistoryTestLocation.reopen({
+ HistoryTestLocation = class extends HistoryTestLocation {
init() {
- this._super(...arguments);
+ super.init(...arguments);
set(this, 'location', mockBrowserLocation('/bars/baz'));
set(this, 'baseURL', '/bar/');
- },
- });
+ }
+ };
createLocation();
@@ -266,12 +267,12 @@ moduleFor(
}
['@test HistoryLocation.getURL() includes location.search'](assert) {
- HistoryTestLocation.reopen({
+ HistoryTestLocation = class extends HistoryTestLocation {
init() {
- this._super(...arguments);
+ super.init(...arguments);
set(this, 'location', mockBrowserLocation('/foo/bar?time=morphin'));
- },
- });
+ }
+ };
createLocation();
@@ -279,12 +280,12 @@ moduleFor(
}
['@test HistoryLocation.getURL() includes location.hash'](assert) {
- HistoryTestLocation.reopen({
+ HistoryTestLocation = class extends HistoryTestLocation {
init() {
- this._super(...arguments);
+ super.init(...arguments);
set(this, 'location', mockBrowserLocation('/foo/bar#pink-power-ranger'));
- },
- });
+ }
+ };
createLocation();
@@ -292,12 +293,12 @@ moduleFor(
}
['@test HistoryLocation.getURL() includes location.hash and location.search'](assert) {
- HistoryTestLocation.reopen({
+ HistoryTestLocation = class extends HistoryTestLocation {
init() {
- this._super(...arguments);
+ super.init(...arguments);
set(this, 'location', mockBrowserLocation('/foo/bar?time=morphin#pink-power-ranger'));
- },
- });
+ }
+ };
createLocation();
@@ -305,14 +306,14 @@ moduleFor(
}
['@test HistoryLocation.getURL() drops duplicate slashes'](assert) {
- HistoryTestLocation.reopen({
+ HistoryTestLocation = class extends HistoryTestLocation {
init() {
- this._super(...arguments);
+ super.init(...arguments);
let location = mockBrowserLocation('//admin//profile//');
location.pathname = '//admin//profile//'; // mockBrowserLocation does not allow for `//`, so force it
set(this, 'location', location);
- },
- });
+ }
+ };
createLocation();
@@ -322,14 +323,14 @@ moduleFor(
['@test HistoryLocation.getURL() treats regex metacharacters in rootURL and baseURL literally'](
assert
) {
- HistoryTestLocation.reopen({
+ HistoryTestLocation = class extends HistoryTestLocation {
init() {
- this._super(...arguments);
+ super.init(...arguments);
set(this, 'location', mockBrowserLocation('/axc/secret'));
set(this, 'rootURL', '/a.c/');
set(this, 'baseURL', '/a.c/');
- },
- });
+ }
+ };
createLocation();
@@ -344,12 +345,12 @@ moduleFor(
FakeHistory.state = existingState;
- HistoryTestLocation.reopen({
+ HistoryTestLocation = class extends HistoryTestLocation {
init() {
- this._super(...arguments);
+ super.init(...arguments);
set(this, 'location', mockBrowserLocation('/route/path'));
- },
- });
+ }
+ };
createLocation();
location.initState();
diff --git a/packages/@ember/routing/tests/location/none_location_test.js b/packages/@ember/routing/tests/location/none_location_test.js
index 04f77db057b..ca1091f6d89 100644
--- a/packages/@ember/routing/tests/location/none_location_test.js
+++ b/packages/@ember/routing/tests/location/none_location_test.js
@@ -29,12 +29,12 @@ moduleFor(
}
['@test NoneLocation.formatURL() returns the current url always appending rootURL'](assert) {
- NoneTestLocation.reopen({
+ NoneTestLocation = class extends NoneTestLocation {
init() {
- this._super(...arguments);
+ super.init(...arguments);
set(this, 'rootURL', '/en/');
- },
- });
+ }
+ };
createLocation();
@@ -42,13 +42,13 @@ moduleFor(
}
['@test NoneLocation.getURL() returns the current path minus rootURL'](assert) {
- NoneTestLocation.reopen({
+ NoneTestLocation = class extends NoneTestLocation {
init() {
- this._super(...arguments);
+ super.init(...arguments);
set(this, 'rootURL', '/foo/');
set(this, 'path', '/foo/bar');
- },
- });
+ }
+ };
createLocation();
@@ -58,13 +58,13 @@ moduleFor(
['@test NoneLocation.getURL() will remove the rootURL only from the beginning of a url'](
assert
) {
- NoneTestLocation.reopen({
+ NoneTestLocation = class extends NoneTestLocation {
init() {
- this._super(...arguments);
+ super.init(...arguments);
set(this, 'rootURL', '/bar/');
set(this, 'path', '/foo/bar/baz');
- },
- });
+ }
+ };
createLocation();
@@ -72,13 +72,13 @@ moduleFor(
}
['@test NoneLocation.getURL() will not remove the rootURL when only a partial match'](assert) {
- NoneTestLocation.reopen({
+ NoneTestLocation = class extends NoneTestLocation {
init() {
- this._super(...arguments);
+ super.init(...arguments);
set(this, 'rootURL', '/bar/');
set(this, 'path', '/bars/baz');
- },
- });
+ }
+ };
createLocation();
@@ -86,13 +86,13 @@ moduleFor(
}
['@test NoneLocation.getURL() treats regex metacharacters in rootURL literally'](assert) {
- NoneTestLocation.reopen({
+ NoneTestLocation = class extends NoneTestLocation {
init() {
- this._super(...arguments);
+ super.init(...arguments);
set(this, 'rootURL', '/a.c/');
set(this, 'path', '/axc/secret');
- },
- });
+ }
+ };
createLocation();
@@ -100,13 +100,13 @@ moduleFor(
}
['@test NoneLocation.getURL() strips the rootURL when it has an extra trailing slash'](assert) {
- NoneTestLocation.reopen({
+ NoneTestLocation = class extends NoneTestLocation {
init() {
- this._super(...arguments);
+ super.init(...arguments);
set(this, 'rootURL', '/foo//');
set(this, 'path', '/foo//bar');
- },
- });
+ }
+ };
createLocation();
diff --git a/packages/@ember/routing/tests/system/dsl_test.js b/packages/@ember/routing/tests/system/dsl_test.js
index 5b7d63df119..1d502629f3c 100644
--- a/packages/@ember/routing/tests/system/dsl_test.js
+++ b/packages/@ember/routing/tests/system/dsl_test.js
@@ -87,11 +87,7 @@ moduleFor(
this.route('blork');
});
- this.routerInstance.reopen({
- _hasModuleBasedResolver() {
- return true;
- },
- });
+ this.routerInstance._hasModuleBasedResolver = () => true;
let router = this.routerInstance;
router._initRouterJs();
@@ -133,11 +129,7 @@ moduleFor(
});
});
- this.routerInstance.reopen({
- _hasModuleBasedResolver() {
- return true;
- },
- });
+ this.routerInstance._hasModuleBasedResolver = () => true;
let router = this.routerInstance;
router._initRouterJs();
@@ -285,11 +277,7 @@ moduleFor(
this.mount('chat');
});
- this.routerInstance.reopen({
- _hasModuleBasedResolver() {
- return true;
- },
- });
+ this.routerInstance._hasModuleBasedResolver = () => true;
this.routerInstance._initRouterJs();
let router = this.routerInstance;
assert.ok(router._routerMicrolib.recognizer.names['chat'], 'main route was created');
@@ -304,11 +292,7 @@ moduleFor(
this.mount('chat', { as: 'shoutbox' });
});
- this.routerInstance.reopen({
- _hasModuleBasedResolver() {
- return true;
- },
- });
+ this.routerInstance._hasModuleBasedResolver = () => true;
this.routerInstance._initRouterJs();
let router = this.routerInstance;
@@ -351,11 +335,7 @@ moduleFor(
});
});
- this.routerInstance.reopen({
- _hasModuleBasedResolver() {
- return true;
- },
- });
+ this.routerInstance._hasModuleBasedResolver = () => true;
this.routerInstance._initRouterJs();
let router = this.routerInstance;
diff --git a/packages/@ember/utils/tests/compare_test.js b/packages/@ember/utils/tests/compare_test.js
index 36c107bd826..9d1ef5be7b7 100644
--- a/packages/@ember/utils/tests/compare_test.js
+++ b/packages/@ember/utils/tests/compare_test.js
@@ -4,13 +4,11 @@ import { moduleFor, AbstractTestCase, expectDeprecation, testUnless } from 'inte
import { DEPRECATIONS } from '@ember/-internals/deprecations';
let data = [];
-let Comp = EmberObject.extend();
-
-Comp.reopenClass({
- compare(obj) {
+class Comp extends EmberObject {
+ static compare(obj) {
return obj.get('val');
- },
-});
+ }
+}
const UTILS = DEPRECATIONS.DEPRECATE_EMBER_UTILS;
diff --git a/packages/@ember/utils/tests/type_of_test.js b/packages/@ember/utils/tests/type_of_test.js
index c1739a7ae48..7f778b2f7dc 100644
--- a/packages/@ember/utils/tests/type_of_test.js
+++ b/packages/@ember/utils/tests/type_of_test.js
@@ -46,6 +46,7 @@ moduleFor(
assert.equal(typeOf(instance.method), 'function', 'item of type function');
assert.equal(typeOf(instance.asyncMethod), 'function', 'item of type async function');
assert.equal(typeOf(EmberObject.extend()), 'class', 'item of type class');
+ assert.equal(typeOf(class extends EmberObject {}), 'class', 'item of type native class');
assert.equal(typeOf(new Error()), 'error', 'item of type error');
}
diff --git a/packages/@glimmer-workspace/integration-tests/index.ts b/packages/@glimmer-workspace/integration-tests/index.ts
index 1199fdaa1db..4d3c7a01b46 100644
--- a/packages/@glimmer-workspace/integration-tests/index.ts
+++ b/packages/@glimmer-workspace/integration-tests/index.ts
@@ -1,4 +1,3 @@
-export * from './lib/base-env';
export * from './lib/compile';
export * from './lib/components';
export * from './lib/dom/assertions';
@@ -8,7 +7,6 @@ export * from './lib/markers';
export * from './lib/modes/env';
export * from './lib/modes/jit/delegate';
export * from './lib/modes/jit/register';
-export * from './lib/modes/jit/resolver';
export * from './lib/modes/node/env';
export * from './lib/modes/rehydration/delegate';
export * from './lib/modes/rehydration/partial-rehydration-delegate';
@@ -21,6 +19,5 @@ export * from './lib/test-decorator';
export * from './lib/test-helpers/define';
export * from './lib/test-helpers/module';
export * from './lib/test-helpers/strings';
-export * from './lib/test-helpers/tracked';
export * from './lib/test-helpers/tracked-object';
export { syntaxErrorFor } from '@glimmer-workspace/test-utils';
diff --git a/packages/@glimmer-workspace/integration-tests/lib/base-env.ts b/packages/@glimmer-workspace/integration-tests/lib/base-env.ts
deleted file mode 100644
index 24dc0280253..00000000000
--- a/packages/@glimmer-workspace/integration-tests/lib/base-env.ts
+++ /dev/null
@@ -1,37 +0,0 @@
-import type { Destroyable, Destructor } from '@glimmer/interfaces';
-import type { EnvironmentDelegate } from '@glimmer/runtime';
-
-interface Scheduled {
- destroyable: Destroyable;
- destructor: Destructor;
-}
-
-let scheduled: Scheduled[] = [];
-let scheduledFinishDestruction: (() => void)[] = [];
-
-export function scheduleWillDestroy(task: Scheduled) {
- scheduled.push(task);
-}
-
-export function scheduleDidDestroy(fn: () => void) {
- scheduledFinishDestruction.push(fn);
-}
-
-export const BaseEnv: EnvironmentDelegate = {
- isInteractive: true,
-
- enableDebugTooling: false,
-
- onTransactionCommit() {
- for (const { destroyable, destructor } of scheduled) {
- destructor(destroyable);
- }
-
- scheduledFinishDestruction.forEach((fn) => fn());
-
- scheduled = [];
- scheduledFinishDestruction = [];
- },
-};
-
-export {};
diff --git a/packages/@glimmer-workspace/integration-tests/lib/compile.ts b/packages/@glimmer-workspace/integration-tests/lib/compile.ts
index 1a312e3ec81..5f0e245c35c 100644
--- a/packages/@glimmer-workspace/integration-tests/lib/compile.ts
+++ b/packages/@glimmer-workspace/integration-tests/lib/compile.ts
@@ -11,8 +11,12 @@ import { templateFactory } from '@glimmer/opcode-compiler';
// TODO: This fundamentally has little to do with testing and
// most tests should just use a more generic preprocess, extracted
// out of the test environment.
-export function preprocess(templateSource: string, options?: PrecompileOptions): Template {
- return createTemplate(templateSource, options)({});
+export function preprocess(
+ templateSource: string,
+ options?: PrecompileOptions,
+ owner: object = {}
+): Template {
+ return createTemplate(templateSource, options)(owner);
}
let templateId = 0;
diff --git a/packages/@glimmer-workspace/integration-tests/lib/components.ts b/packages/@glimmer-workspace/integration-tests/lib/components.ts
index d7eeb0bfa44..636425fd893 100644
--- a/packages/@glimmer-workspace/integration-tests/lib/components.ts
+++ b/packages/@glimmer-workspace/integration-tests/lib/components.ts
@@ -1,3 +1 @@
-export * from './components/emberish-curly';
-export * from './components/emberish-glimmer';
export * from './components/types';
diff --git a/packages/@glimmer-workspace/integration-tests/lib/components/emberish-curly.ts b/packages/@glimmer-workspace/integration-tests/lib/components/emberish-curly.ts
deleted file mode 100644
index 61560bb3931..00000000000
--- a/packages/@glimmer-workspace/integration-tests/lib/components/emberish-curly.ts
+++ /dev/null
@@ -1,325 +0,0 @@
-import type {
- Bounds,
- CapturedNamedArguments,
- CompilableProgram,
- Destroyable,
- Dict,
- DynamicScope,
- ElementOperations,
- Environment,
- InternalComponentCapabilities,
- Nullable,
- Owner,
- PreparedArguments,
- Reference,
- Template,
- VMArguments,
- WithCreateInstance,
- WithDynamicLayout,
- WithDynamicTagName,
-} from '@glimmer/interfaces';
-import type { DirtyableTag } from '@glimmer/validator';
-import { unwrapTemplate } from '@glimmer/debug-util';
-import { registerDestructor } from '@glimmer/destroyable';
-import { setInternalComponentManager } from '@glimmer/manager';
-import {
- childRefFor,
- createComputeRef,
- createConstRef,
- createPrimitiveRef,
- valueForRef,
-} from '@glimmer/reference';
-import { reifyNamed, reifyPositional } from '@glimmer/runtime';
-import { assign, EMPTY_ARRAY, keys } from '@glimmer/util';
-import { consumeTag, createTag, dirtyTag, dirtyTagFor } from '@glimmer/validator';
-
-import type { TestJitRuntimeResolver } from '../modes/jit/resolver';
-import type { TestComponentConstructor } from './types';
-
-export type Attrs = Dict;
-export type AttrsDiff = { oldAttrs: Nullable; newAttrs: Attrs };
-
-export interface EmberishCurlyComponentFactory extends TestComponentConstructor {
- fromDynamicScope?: string[];
- positionalParams: string | string[];
- create(options: { attrs: Attrs; targetObject: any }): EmberishCurlyComponent;
- new (...args: unknown[]): this;
-}
-
-let GUID = 1;
-
-export class EmberishCurlyComponent {
- public static positionalParams: string[] | string = [];
-
- public dirtinessTag: DirtyableTag = createTag();
- declare public layout: Template;
- declare public name: string;
- public tagName: Nullable = null;
- public attributeBindings: Nullable = null;
- declare public attrs: Attrs;
- declare public element: Element;
- declare public bounds: Bounds;
- public parentView: Nullable = null;
- declare public args: CapturedNamedArguments;
-
- public _guid: string;
-
- // create(options: { attrs: Attrs; targetObject: any }): EmberishCurlyComponent
-
- static create(args: { attrs: Attrs; targetObject: any }): EmberishCurlyComponent {
- let c = new this();
-
- for (let key of keys(args)) {
- (c as any)[key] = args[key];
- }
-
- return c;
- }
-
- init() {}
-
- constructor() {
- this._guid = `${GUID++}`;
- this.init();
- }
-
- set(key: string, value: unknown) {
- (this as any)[key] = value;
- dirtyTagFor(this, key);
- }
-
- setProperties(dict: Dict) {
- for (let key of keys(dict)) {
- this.set(key, dict[key]);
- }
- }
-
- recompute() {
- dirtyTag(this.dirtinessTag);
- }
-
- destroy() {}
-
- didInitAttrs(_options: { attrs: Attrs }) {}
- didUpdateAttrs(_diff: AttrsDiff) {}
- didReceiveAttrs(_diff: AttrsDiff) {}
- willInsertElement() {}
- willDestroyElement() {}
- willUpdate() {}
- willRender() {}
- didInsertElement() {}
- didUpdate() {}
- didRender() {}
-}
-
-export interface EmberishCurlyComponentState {
- component: EmberishCurlyComponent;
- selfRef: Reference;
-}
-
-const EMBERISH_CURLY_CAPABILITIES: InternalComponentCapabilities = {
- dynamicLayout: true,
- dynamicTag: true,
- prepareArgs: true,
- createArgs: true,
- attributeHook: true,
- elementHook: true,
- dynamicScope: true,
- createCaller: true,
- updateHook: true,
- createInstance: true,
- wrapped: true,
- willDestroy: true,
- hasSubOwner: false,
-};
-
-export class EmberishCurlyComponentManager
- implements
- WithCreateInstance,
- WithDynamicTagName,
- WithDynamicLayout
-{
- getDebugName(state: EmberishCurlyComponentFactory) {
- return state.name;
- }
-
- getCapabilities(): InternalComponentCapabilities {
- return EMBERISH_CURLY_CAPABILITIES;
- }
-
- getDynamicLayout({
- component: { layout },
- }: EmberishCurlyComponentState): CompilableProgram | null {
- if (layout) {
- return unwrapTemplate(layout).asWrappedLayout();
- }
-
- return null;
- }
-
- prepareArgs(
- definition: EmberishCurlyComponentFactory,
- args: VMArguments
- ): Nullable {
- const { positionalParams } = definition || EmberishCurlyComponent;
- if (typeof positionalParams === 'string') {
- if (args.named.has(positionalParams)) {
- if (args.positional.length === 0) {
- return null;
- } else {
- throw new Error(
- `You cannot specify positional parameters and the hash argument \`${positionalParams}\`.`
- );
- }
- }
-
- let named = args.named.capture();
- let positional = args.positional.capture();
- named[positionalParams] = createComputeRef(() => reifyPositional(positional));
-
- return { positional: EMPTY_ARRAY, named } as PreparedArguments;
- } else if (Array.isArray(positionalParams)) {
- let named = assign({}, args.named.capture());
- let count = Math.min(positionalParams.length, args.positional.length);
-
- for (let i = 0; i < count; i++) {
- let name = positionalParams[i] as string;
-
- if (named[name]) {
- throw new Error(
- `You cannot specify both a positional param (at position ${i}) and the hash argument \`${name}\`.`
- );
- }
-
- named[name] = args.positional.at(i);
- }
-
- return { positional: EMPTY_ARRAY, named } as PreparedArguments;
- } else {
- return null;
- }
- }
-
- create(
- _owner: Owner,
- definition: EmberishCurlyComponentFactory,
- _args: VMArguments,
- _env: Environment,
- dynamicScope: DynamicScope,
- callerSelf: Reference,
- hasDefaultBlock: boolean
- ): EmberishCurlyComponentState {
- let klass = definition || EmberishCurlyComponent;
- let self = valueForRef(callerSelf);
- let args = _args.named.capture();
- let attrs = reifyNamed(args);
- let merged = assign(
- {},
- attrs,
- { attrs },
- { args },
- { targetObject: self },
- { HAS_BLOCK: hasDefaultBlock }
- );
- let component = klass.create(merged);
-
- component.args = args;
-
- let dyn: Nullable = klass.fromDynamicScope || null;
-
- if (dyn) {
- for (let i = 0; i < dyn.length; i++) {
- let name = dyn[i] as string;
- component.set(name, valueForRef(dynamicScope.get(name)));
- }
- }
-
- consumeTag(component.dirtinessTag);
-
- component.didInitAttrs({ attrs });
- component.didReceiveAttrs({ oldAttrs: null, newAttrs: attrs });
- component.willInsertElement();
- component.willRender();
-
- registerDestructor(component, () => component.destroy());
-
- const selfRef = createConstRef(component, 'this');
-
- return { component, selfRef };
- }
-
- getSelf({ selfRef }: EmberishCurlyComponentState): Reference {
- return selfRef;
- }
-
- getTagName({ component: { tagName } }: EmberishCurlyComponentState): Nullable {
- if (tagName) {
- return tagName;
- } else if (tagName === null) {
- return 'div';
- } else {
- return null;
- }
- }
-
- didCreateElement(
- { component, selfRef }: EmberishCurlyComponentState,
- element: Element,
- operations: ElementOperations
- ): void {
- component.element = element;
-
- operations.setAttribute('id', createPrimitiveRef(`ember${component._guid}`), false, null);
- operations.setAttribute('class', createPrimitiveRef('ember-view'), false, null);
-
- let bindings = component.attributeBindings;
-
- if (bindings) {
- for (const attribute of bindings) {
- let reference = childRefFor(selfRef, attribute);
-
- operations.setAttribute(attribute, reference, false, null);
- }
- }
- }
-
- didRenderLayout({ component }: EmberishCurlyComponentState, bounds: Bounds): void {
- component.bounds = bounds;
- }
-
- didCreate({ component }: EmberishCurlyComponentState): void {
- component.didInsertElement();
- registerDestructor(component, () => component.willDestroyElement(), true);
-
- component.didRender();
- }
-
- update({ component }: EmberishCurlyComponentState): void {
- let oldAttrs = component.attrs;
- let newAttrs = reifyNamed(component.args);
- let merged = assign({}, newAttrs, { attrs: newAttrs });
-
- consumeTag(component.dirtinessTag);
-
- component.setProperties(merged);
- component.didUpdateAttrs({ oldAttrs, newAttrs });
- component.didReceiveAttrs({ oldAttrs, newAttrs });
- component.willUpdate();
- component.willRender();
- }
-
- didUpdateLayout(): void {}
-
- didUpdate({ component }: EmberishCurlyComponentState): void {
- component.didUpdate();
- component.didRender();
- }
-
- getDestroyable({ component }: EmberishCurlyComponentState): Destroyable {
- return component;
- }
-}
-
-const EMBERISH_CURLY_COMPONENT_MANAGER = new EmberishCurlyComponentManager();
-
-setInternalComponentManager(EMBERISH_CURLY_COMPONENT_MANAGER, EmberishCurlyComponent);
diff --git a/packages/@glimmer-workspace/integration-tests/lib/components/emberish-glimmer.ts b/packages/@glimmer-workspace/integration-tests/lib/components/emberish-glimmer.ts
deleted file mode 100644
index 42a5f2f9e30..00000000000
--- a/packages/@glimmer-workspace/integration-tests/lib/components/emberish-glimmer.ts
+++ /dev/null
@@ -1,65 +0,0 @@
-import type { Arguments, ComponentManager, Dict, Owner } from '@glimmer/interfaces';
-import { destroy, isDestroyed, isDestroying, registerDestructor } from '@glimmer/destroyable';
-import { componentCapabilities, setComponentManager } from '@glimmer/manager';
-import { setOwner } from '@glimmer/owner';
-
-export type Args = Dict;
-
-export class GlimmerishComponent {
- public args: Dict;
-
- constructor(owner: Owner, args: Dict) {
- setOwner(this, owner);
- this.args = args;
- }
-
- get isDestroying() {
- return isDestroying(this);
- }
-
- get isDestroyed() {
- return isDestroyed(this);
- }
-
- willDestroy() {}
-}
-
-export interface Constructor {
- // eslint-disable-next-line @typescript-eslint/no-empty-object-type
- new (owner: unknown, args: {}): T;
-}
-
-const CAPABILITIES = componentCapabilities('3.13', {
- destructor: true,
-});
-
-class GlimmerComponentManager implements ComponentManager {
- capabilities = CAPABILITIES;
-
- private owner: unknown;
-
- constructor(owner: unknown) {
- this.owner = owner;
- }
-
- createComponent(
- ComponentClass: Constructor,
- args: Arguments
- ): GlimmerishComponent {
- let component = new ComponentClass(this.owner, args.named);
-
- registerDestructor(component, () => component.willDestroy());
-
- return component;
- }
-
- getContext(component: GlimmerishComponent): GlimmerishComponent {
- return component;
- }
-
- destroyComponent(component: GlimmerishComponent): void {
- destroy(component);
- }
-}
-
-setComponentManager((owner) => new GlimmerComponentManager(owner), GlimmerishComponent);
diff --git a/packages/@glimmer-workspace/integration-tests/lib/components/types.ts b/packages/@glimmer-workspace/integration-tests/lib/components/types.ts
index 15a1d61a289..f3036f8abf1 100644
--- a/packages/@glimmer-workspace/integration-tests/lib/components/types.ts
+++ b/packages/@glimmer-workspace/integration-tests/lib/components/types.ts
@@ -1,19 +1,15 @@
+import type GlimmerComponent from '@glimmer/component';
import type { Dict } from '@glimmer/interfaces';
-import type { TemplateOnlyComponent } from '@glimmer/runtime';
-
-import type { EmberishCurlyComponent } from './emberish-curly';
-import type { GlimmerishComponent } from './emberish-glimmer';
+import type { TemplateOnlyComponent } from '@ember/component/template-only';
export type ComponentKind = 'Glimmer' | 'Curly' | 'Dynamic' | 'TemplateOnly' | 'Custom' | 'unknown';
-export interface TestComponentConstructor {
- new (): T;
-}
-
export interface ComponentTypes {
- Glimmer: typeof GlimmerishComponent;
- Curly: TestComponentConstructor;
- Dynamic: TestComponentConstructor;
+ Glimmer: typeof GlimmerComponent;
+ /** A Glimmer component invoked with curly syntax (`{{#test-component}}`). */
+ Curly: typeof GlimmerComponent;
+ /** A Glimmer component invoked through `{{component this.componentName}}`. */
+ Dynamic: typeof GlimmerComponent;
TemplateOnly: TemplateOnlyComponent;
Custom: unknown;
unknown: unknown;
diff --git a/packages/@glimmer-workspace/integration-tests/lib/dom/assertions.ts b/packages/@glimmer-workspace/integration-tests/lib/dom/assertions.ts
index f064182840c..92787a8f53d 100644
--- a/packages/@glimmer-workspace/integration-tests/lib/dom/assertions.ts
+++ b/packages/@glimmer-workspace/integration-tests/lib/dom/assertions.ts
@@ -1,7 +1,7 @@
/* eslint-disable @typescript-eslint/no-unsafe-enum-comparison */
import type { Dict, SimpleElement, SimpleNode } from '@glimmer/interfaces';
import { isSimpleElement } from '@glimmer/debug-util';
-import { assign, dict } from '@glimmer/util';
+import { dict } from '@glimmer/util';
export interface DebugElement {
element: SimpleElement | null | undefined;
@@ -155,27 +155,6 @@ export function equalsAttr(expected: any): Matcher {
};
}
-export function assertEmberishElement(
- element: SimpleElement,
- tagName: string,
- attrs: object,
- contents: string
-): void;
-export function assertEmberishElement(element: SimpleElement, tagName: string, attrs: object): void;
-export function assertEmberishElement(
- element: SimpleElement,
- tagName: string,
- contents: string
-): void;
-export function assertEmberishElement(element: SimpleElement, tagName: string): void;
-export function assertEmberishElement(...args: any[]): void {
- let [element, tagName, attrs, contents] = processAssertComponentArgs(args);
-
- let fullAttrs = assign({ class: classes('ember-view'), id: regex(/^ember\d*$/u) }, attrs);
-
- equalsElement(element, tagName, fullAttrs, contents);
-}
-
export function assertSerializedInElement(result: string, expected: string, message?: string) {
let matched = /