From bdc582852d1ab2c9e3c73cf5b982872450c4ad55 Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Wed, 30 Sep 2026 22:35:59 -0400 Subject: [PATCH 01/46] Move classic component tests into components/classic Twelve test files check features that stay after the classic `Component` is removed (RFC 1216). These files no longer import the classic class. Each test that uses the classic class moves, unchanged, to a file of the same name in `tests/integration/components/classic/`. These tests check classic-only API: the wrapper element, `elementId`, lifecycle hooks, `this.attrs`, `positionalParams`, two-way bindings, `appendTo`, the view utilities, and `render.component` instrumentation. - `append-test.js`, `instrumentation-test.js`, `life-cycle-test.js` and `utils-test.js` move whole. Every test in them is classic-only. - The other eight files split. The new modules have the old name plus "(classic component)". - Six curly tests that only extend the classic class, and never create an instance, move too. No test is deleted and no test body changes. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../angle-bracket-invocation-test.js | 405 -- .../classic/angle-bracket-invocation-test.js | 414 ++ .../components/{ => classic}/append-test.js | 4 +- .../classic/component-template-test.js | 26 + .../classic/contextual-components-test.js | 407 ++ .../classic/curly-components-test.js | 2294 ++++++++++ .../classic/dynamic-components-test.js | 84 + .../components/classic/error-handling-test.js | 81 + .../{ => classic}/instrumentation-test.js | 2 +- .../{ => classic}/life-cycle-test.js | 2 +- .../components/classic/mut-test.js | 613 +++ .../components/classic/tracked-test.js | 51 + .../components/{ => classic}/utils-test.js | 2 +- .../components/component-template-test.js | 18 - .../components/contextual-components-test.js | 376 +- .../components/curly-components-test.js | 4047 ++++------------- .../components/dynamic-components-test.js | 76 - .../components/error-handling-test.js | 72 - .../integration/components/tracked-test.js | 42 - .../tests/integration/helpers/mut-test.js | 608 +-- 20 files changed, 4873 insertions(+), 4751 deletions(-) create mode 100644 packages/@ember/-internals/glimmer/tests/integration/components/classic/angle-bracket-invocation-test.js rename packages/@ember/-internals/glimmer/tests/integration/components/{ => classic}/append-test.js (99%) create mode 100644 packages/@ember/-internals/glimmer/tests/integration/components/classic/component-template-test.js create mode 100644 packages/@ember/-internals/glimmer/tests/integration/components/classic/contextual-components-test.js create mode 100644 packages/@ember/-internals/glimmer/tests/integration/components/classic/curly-components-test.js create mode 100644 packages/@ember/-internals/glimmer/tests/integration/components/classic/dynamic-components-test.js create mode 100644 packages/@ember/-internals/glimmer/tests/integration/components/classic/error-handling-test.js rename packages/@ember/-internals/glimmer/tests/integration/components/{ => classic}/instrumentation-test.js (98%) rename packages/@ember/-internals/glimmer/tests/integration/components/{ => classic}/life-cycle-test.js (99%) create mode 100644 packages/@ember/-internals/glimmer/tests/integration/components/classic/mut-test.js create mode 100644 packages/@ember/-internals/glimmer/tests/integration/components/classic/tracked-test.js rename packages/@ember/-internals/glimmer/tests/integration/components/{ => classic}/utils-test.js (99%) 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..89929ec9653 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,6 @@ import { set, setProperties } from '@ember/object'; import Component from '@glimmer/component'; import { tracked } from '@glimmer/tracking'; -import { Component as EmberComponent } from '../../utils/helpers'; import { template } from '@ember/template-compiler/runtime'; import templateOnly from '@ember/component/template-only'; import { precompileTemplate } from '@ember/template-compilation'; @@ -142,317 +141,6 @@ moduleFor( this.assertElement(this.firstChild, { tagName: 'div', content: '' }); } - '@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 renders passed named arguments'() { this.owner.register( 'component:foo-bar', @@ -480,33 +168,6 @@ moduleFor( this.assertText('Hola'); } - '@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 it can render a basic component with a block'() { this.owner.register( 'component:foo-bar', @@ -584,24 +245,6 @@ moduleFor( this.assertText('Joel Kang, hello'); } - '@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 can invoke curried components with capitalized block param names'() { this.owner.register( 'component:foo-bar', @@ -704,54 +347,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/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..440daa3e98c --- /dev/null +++ b/packages/@ember/-internals/glimmer/tests/integration/components/classic/angle-bracket-invocation-test.js @@ -0,0 +1,414 @@ +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', + }); + } + } +); 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/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/contextual-components-test.js b/packages/@ember/-internals/glimmer/tests/integration/components/classic/contextual-components-test.js new file mode 100644 index 00000000000..1a94d373c60 --- /dev/null +++ b/packages/@ember/-internals/glimmer/tests/integration/components/classic/contextual-components-test.js @@ -0,0 +1,407 @@ +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 { 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}}
' + ), + 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( + '' + ), + 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 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..54acc8be5a5 --- /dev/null +++ b/packages/@ember/-internals/glimmer/tests/integration/components/classic/curly-components-test.js @@ -0,0 +1,2294 @@ +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 { A as emberA } from '@ember/array'; +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(''), + 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: 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 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 = emberA([]); + this.value = null; + } + + updateValue() { + let newValue = this.get('options.lastObject.value'); + + this.set('value', newValue); + } + + registerOption(option) { + this.get('options').addObject(option); + } + + unregisterOption(option) { + this.get('options').removeObject(option); + + 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' }); + } + } +); 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/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/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/life-cycle-test.js b/packages/@ember/-internals/glimmer/tests/integration/components/classic/life-cycle-test.js similarity index 99% rename from packages/@ember/-internals/glimmer/tests/integration/components/life-cycle-test.js rename to packages/@ember/-internals/glimmer/tests/integration/components/classic/life-cycle-test.js index b989a47b24a..3bb2e497c28 100644 --- a/packages/@ember/-internals/glimmer/tests/integration/components/life-cycle-test.js +++ b/packages/@ember/-internals/glimmer/tests/integration/components/classic/life-cycle-test.js @@ -7,7 +7,7 @@ import { getViewElement, getViewId } from '@ember/-internals/views'; import { precompileTemplate } from '@ember/template-compilation'; import { setComponentTemplate } from '@glimmer/manager'; -import { Component } from '../../utils/helpers'; +import { Component } from '../../../utils/helpers'; import { addListener } from '@ember/-internals/metal'; class LifeCycleHooksTest extends RenderingTestCase { 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/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/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..883d9883c91 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 @@ -1,12 +1,7 @@ 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 +502,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', @@ -796,99 +757,6 @@ moduleFor( this.assertText(`${expectedText},Hola`); } - ['@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}}
' - ), - 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 adding parameters to a contextual component's instance does not add it to other instances"]() { // If parameters and attributes are not handled correctly, setting a value // in an invokation can leak to others invocation. @@ -918,103 +786,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( - '' - ), - 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 EmberComponent { - static positionalParams = ['val']; - } - ) - ); - } else { - this.owner.register( - 'component:change-button', - setComponentTemplate( - precompileTemplate( - '' - ), - class extends Component {} - ) - ); - } + this.owner.register( + 'component:change-button', + setComponentTemplate( + precompileTemplate( + '' + ), + class extends Component {} + ) + ); setup.call(this, assert); @@ -1648,32 +1318,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..a33813e5dbc 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,18 @@ import { moduleFor, RenderingTestCase, strip, - classes, equalTokens, equalsElement, 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 { 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 EmberObject, { set, computed } from '@ember/object'; import { A as emberA } from '@ember/array'; -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 +40,1358 @@ 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.assertText('you need to be more bold'); - 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(() => 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))); - ['@test it has the right parentView and childViews'](assert) { - let fooBarInstance, fooBarBazInstance; + equalTokens(this.element, expectedHtmlItalic); - let FooBarComponent = class extends Component { - init() { - super.init(...arguments); - fooBarInstance = this; - } - }; + runTask(() => set(component, 'output', htmlSafe(expectedHtmlBold))); + + 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 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('{{yield this.greeting @greetee.firstName}}'), - FooBarComponent - ) - ); - - 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.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'); - - // 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('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(''), - 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']; - } - ) - ); - + ['@test non-block with properties access via attrs is asserted against']() { 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(); + 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 (has-block-params) as a param to a helper']() { + ['@test non-block with named argument']() { this.owner.register( - 'component:check-helper', + 'component:non-block', setComponentTemplate( - precompileTemplate('{{if (has-block-params) "true" "false"}}'), + precompileTemplate('In layout - someProp: {{@someProp}}'), templateOnly() ) ); - this.render(strip` - {{#check-helper}}{{/check-helper}} - {{#check-helper as |something|}}{{/check-helper}}`); + this.render('{{non-block someProp=this.prop}}', { + prop: 'something here', + }); - this.assertHTML('falsetrue'); + this.assertText('In layout - someProp: something here'); - this.assertStableRerender(); - } + runTask(() => this.rerender()); - ['@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.assertText('In layout - someProp: something here'); - this.render(strip` - {{#check-helper}}{{/check-helper}} - {{#check-helper as |something|}}{{/check-helper}}`); + runTask(() => this.context.set('prop', 'other thing there')); - this.assertHTML('falsefalse'); + this.assertText('In layout - someProp: other thing there'); - this.assertStableRerender(); + runTask(() => this.context.set('prop', 'something here')); + + this.assertText('In layout - someProp: something here'); } - ['@test component in template of a yielding component should have the proper parentView']( - assert - ) { - let outer, innerTemplate, innerLayout; + ['@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-outer', + 'component:with-block', setComponentTemplate( - precompileTemplate('{{x-inner-in-layout}}{{yield}}'), - class extends Component { - init() { - super.init(...arguments); - outer = this; - } - } + precompileTemplate('In layout - someProp: {{@someProp}} - {{yield}}'), + templateOnly() ) ); - 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}}`, + {{#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; - - this.owner.register( - 'component:x-outer', - setComponentTemplate( - precompileTemplate('{{#x-middle}}{{x-inner value=this.value}}{{/x-middle}}'), - class extends GlimmerComponent { - value = 1; - } - ) - ); - - 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 - ) - ); - + ['@test static arbitrary number of positional parameters'](assert) { this.owner.register( - 'component:x-inner', - setComponentTemplate( - precompileTemplate('
{{@value}}
'), - class extends GlimmerComponent { - constructor(owner, args) { - super(owner, args); - middle.value = this.args.value; - } + 'component:sample-component', + template( + strip` + {{#each this.names as |name|}} + {{name}} + {{/each}} + `, + { + component: class extends PositionalComponent { + static positionalParams = 'names'; + }, + strictMode: false, } ) ); - let expectedBacktrackingMessage = backtrackingMessageFor('value', MiddleComponent.name, { - renderTree: ['x-outer', 'x-middle', 'this.value'], - }); - - expectAssertion(() => { - this.render('{{x-outer}}'); - }, expectedBacktrackingMessage); - } - - ["@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 }); - } - ) - ); + this.render(strip` +
{{sample-component "Foo" 4 "Bar"}}
+
{{sample-component "Foo" 4 "Bar" 5 "Baz"}}
`); - 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); - } - } - ) - ); + assert.equal(this.$('#args-3').text(), 'Foo4Bar'); + assert.equal(this.$('#args-5').text(), 'Foo4Bar5Baz'); - let expectedBacktrackingMessage = backtrackingMessageFor('content', '<.+?>', { - renderTree: ['x-outer', 'this.wrapper.content'], - }); + 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 (tracked)"]() { - class Wrapper { - @tracked content = null; - } - - 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(); - } - ) - ); - + ['@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.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', Wrapper.name, { - 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'); + + 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 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; - } - - @computed - get bar() { - return this._bar; - } - set bar(value) { - this._bar = value; - } - }; - + ['@test yield to inverse']() { 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="inverse"}} + {{/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 } + ) + ); + + 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 } + ) + ); - @computed - get bar() { - return this._bar; - } + this.render(strip` + {{check-block}} + {{#check-block}}{{/check-block}}`); - set bar(value) { - this._bar = value; - } - }; + this.assertHTML('NoYes'); + + this.assertStableRerender(); + } + ['@test expression (has-block-params) inverse']() { this.owner.register( - 'component:foo-bar', - setComponentTemplate(precompileTemplate(''), FooBarComponent) + 'component:check-inverse', + template( + strip` + {{#if (has-block-params "inverse")}} + Yes + {{else}} + No + {{/if}} + `, + { component: templateOnly(), strictMode: false } + ) ); - this.render('{{this.localBar}}{{foo-bar bar=this.localBar}}', { - localBar: 'initial value', - }); - - this.assertText('initial value'); + this.render(strip` + {{#check-inverse}}{{/check-inverse}} + {{#check-inverse as |something|}}{{/check-inverse}}`); - runTask(() => this.rerender()); + this.assertHTML('NoNo'); - this.assertText('initial value'); + this.assertStableRerender(); + } - runTask(() => { - component.set('bar', 'updated value'); - }); + ['@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) + 'component:check-attr', + setComponentTemplate( + precompileTemplate(''), + templateOnly() + ) ); - 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' - ); + this.render(strip` + {{#check-attr}}{{/check-attr}} + {{#check-attr}}{{else}}{{/check-attr}}`); - runTask(() => { - child.set('a', 'Foo'); - }); + equalsElement(assert, this.$('button')[0], 'button', { name: 'false' }, ''); + equalsElement(assert, this.$('button')[1], 'button', { name: 'true' }, ''); - this.assert.equal(parent.string, 'Foo|World', 'parent value updated'); + this.assertStableRerender(); + } - this.assert.equal( - this.element.querySelector('[data-test-parent-value]').textContent.trim(), - 'Foo|World', - 'parent updated value rendered' + ['@test (has-block-params) expression in an attribute'](assert) { + this.owner.register( + 'component:check-attr', + setComponentTemplate( + precompileTemplate(''), + templateOnly() + ) ); - runTask(() => { - child.set('a', 'Hello'); - }); + this.render(strip` + {{#check-attr}}{{/check-attr}} + {{#check-attr as |something|}}{{/check-attr}}`); - 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' - ); - } + equalsElement(assert, this.$('button')[0], 'button', { name: 'false' }, ''); + equalsElement(assert, this.$('button')[1], 'button', { name: 'true' }, ''); - ['@test services can be injected into components']() { - let serviceInstance; - this.registerService( - 'name', - class extends Service { - init() { - super.init(...arguments); - serviceInstance = this; - } - last = 'Jackson'; - } - ); + this.assertStableRerender(); + } + ['@test (has-block-params) inverse 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: 'false' }, ''); - this.assertText('Jackson'); + this.assertStableRerender(); + } - runTask(() => { - serviceInstance.set('last', 'McGuffey'); - }); + ['@test (has-block) as a param to a helper']() { + this.owner.register( + 'component:check-helper', + setComponentTemplate( + precompileTemplate('{{if (has-block) "true" "false"}}'), + templateOnly() + ) + ); - this.assertText('McGuffey'); + this.render(strip` + {{check-helper}} + {{#check-helper}}{{/check-helper}}`); - runTask(() => { - serviceInstance.set('last', 'Jackson'); - }); + this.assertHTML('falsetrue'); - this.assertText('Jackson'); + this.assertStableRerender(); } - ['@test injecting an unknown service raises an exception']() { + ['@test (has-block) inverse 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 "inverse") "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}}{{else}}{{/check-helper}}`); + + this.assertHTML('falsetrue'); + + this.assertStableRerender(); } - ['@test throws if `super.init` is not called from `init`']() { + ['@test (has-block-params) as a param to a helper']() { this.owner.register( - 'component:foo-bar', - class extends Component { - init() {} - } + 'component:check-helper', + setComponentTemplate( + precompileTemplate('{{if (has-block-params) "true" "false"}}'), + templateOnly() + ) ); - 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.render(strip` + {{#check-helper}}{{/check-helper}} + {{#check-helper as |something|}}{{/check-helper}}`); - this.owner.register( - 'component:one-way-input', - class extends Component { - tagName = 'input'; - attributeBindings = ['value']; + this.assertHTML('falsetrue'); - init() { - super.init(...arguments); - component = this; - } + this.assertStableRerender(); + } - change() { - let value = this.readDOMAttr('value'); - this.set('value', value); - } - } + ['@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('{{one-way-input value=this.value}}', { - value: 'foo', - }); + this.render(strip` + {{#check-helper}}{{/check-helper}} + {{#check-helper as |something|}}{{/check-helper}}`); - assertElement('foo'); + this.assertHTML('falsefalse'); this.assertStableRerender(); + } - runTask(() => { - this.firstChild.value = 'bar'; - this.$('input').trigger('change'); - }); - - assertElement('bar'); + ["@test when a property is changed during children's rendering"]() { + let middle; - runTask(() => { - this.firstChild.value = 'foo'; - this.$('input').trigger('change'); - }); + this.owner.register( + 'component:x-outer', + setComponentTemplate( + precompileTemplate('{{#x-middle}}{{x-inner value=this.value}}{{/x-middle}}'), + class extends GlimmerComponent { + value = 1; + } + ) + ); - assertElement('foo'); + class MiddleComponent extends GlimmerComponent { + constructor(owner, args) { + super(owner, args); + middle = this; + } + @tracked value = null; + } - runTask(() => { - set(component, 'value', 'bar'); - }); + this.owner.register( + 'component:x-middle', + setComponentTemplate( + precompileTemplate('
{{this.value}}
{{yield}}'), + MiddleComponent + ) + ); - assertElement('bar'); + this.owner.register( + 'component:x-inner', + setComponentTemplate( + precompileTemplate('
{{@value}}
'), + class extends GlimmerComponent { + constructor(owner, args) { + super(owner, args); + middle.value = this.args.value; + } + } + ) + ); - runTask(() => { - this.firstChild.value = 'foo'; - this.$('input').trigger('change'); + let expectedBacktrackingMessage = backtrackingMessageFor('value', MiddleComponent.name, { + renderTree: ['x-outer', 'x-middle', 'this.value'], }); - assertElement('foo'); + expectAssertion(() => { + this.render('{{x-outer}}'); + }, expectedBacktrackingMessage); } - ['@test child triggers revalidate during parent destruction (GH#13846)']() { + ["@test when a shared dependency is changed during children's rendering"]() { 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); - } - - registerOption(option) { - this.get('options').addObject(option); - } - - unregisterOption(option) { - this.get('options').removeObject(option); - - this.updateValue(); - } + precompileTemplate( + '
{{this.wrapper.content}}
{{x-inner value=this.value wrapper=this.wrapper}}' + ), + class extends GlimmerComponent { + value = 1; + wrapper = EmberObject.create({ content: null }); } ) ); 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); + '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 = emberA(['Tom', 'Dick', 'Harry']); - barCopyDidChange: observer('barCopy', () => { - barCopyDidChangeCount++; - }), - }) - ) - ); + this.render('{{non-block items=this.items}}', { items }); + + this.assertText('In layout. [Child: Tom.][Child: Dick.][Child: Harry.]'); + + runTask(() => this.rerender()); - await this.render(`{{foo-bar bar=this.bar}}`, { bar: 3 }); + this.assertText('In layout. [Child: Tom.][Child: Dick.][Child: Harry.]'); - this.assertText('3-4'); + runTask(() => this.context.get('items').pushObject('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').shiftObject()); - await runLoopSettled(); + this.assertText('In layout. [Child: Dick.][Child: Harry.][Child: Sergio.]'); - this.assertText('7-8'); + runTask(() => this.context.set('items', emberA(['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'); - this.render(`{{evented-component}}`); + runTask(() => { + serviceInstance.set('last', 'Jackson'); + }); + + this.assertText('Jackson'); } ['@test component yielding in an {{#each}} has correct block values after rerendering (GH#14284)']() { @@ -3517,75 +1432,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 +1528,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,109 +1633,6 @@ moduleFor( this.assertHTML('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 `{{#let blah as |foo|}}` does not affect `` resolution'() { this.owner.register( 'component:foo', 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..1051086601b 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,7 +7,6 @@ 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'; @@ -120,81 +119,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..fcb19125a64 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 @@ -7,7 +7,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( 'Errors thrown during render', @@ -108,76 +107,5 @@ moduleFor( this.assertText('hello', 'it does rerender after error in production'); } } - - ['@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/tracked-test.js b/packages/@ember/-internals/glimmer/tests/integration/components/tracked-test.js index 4064e6668a3..2d206d9db05 100644 --- a/packages/@ember/-internals/glimmer/tests/integration/components/tracked-test.js +++ b/packages/@ember/-internals/glimmer/tests/integration/components/tracked-test.js @@ -18,54 +18,12 @@ import { 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 { 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..d1c10dcd92b 100644 --- a/packages/@ember/-internals/glimmer/tests/integration/helpers/mut-test.js +++ b/packages/@ember/-internals/glimmer/tests/integration/helpers/mut-test.js @@ -1,123 +1,14 @@ -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 } 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'; 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; - } - } - ) - ); - - 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'); - } - ['@test passing a literal results in a assertion']() { this.owner.register( 'component:bottom-mut', @@ -140,97 +31,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', @@ -261,409 +61,5 @@ moduleFor( this.assertText('Matthew Beale'); } - - ['@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', - 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); - } } ); From fb5c5dce4fd951b77ca696c0226ba1a01a39c967 Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Wed, 30 Sep 2026 22:36:14 -0400 Subject: [PATCH 02/46] Add Glimmer versions of nine tests that moved to components/classic The subject of these tests stays after the classic `Component` is removed. Each one now also has a version without the classic class, in the original file. - angle bracket: positional parameters are not allowed (`PositionalComponent`). - contextual: a curried positional argument does not leak with a dot path (`PositionalComponent`), and a dot path updates attributes. - error handling: an error during destroy (`willDestroy`), and an error while a modifier installs. The modifier takes the place of `didInsertElement`. - life cycle: "afterRender set" and "afterRender set on parent". A modifier schedules the `afterRender` work. - `mut`: a binding propagates through one and two `mut` layers. The bottom component sets the value with `(fn (mut @setMe) 13)`. The classic module "Run loop and lifecycle hooks" gets the "(classic component)" suffix, because the original file has that module again. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../angle-bracket-invocation-test.js | 19 ++++ .../components/classic/life-cycle-test.js | 2 +- .../components/contextual-components-test.js | 87 +++++++++++++++++++ .../components/error-handling-test.js | 75 +++++++++++++++- .../integration/components/life-cycle-test.js | 68 +++++++++++++++ .../tests/integration/helpers/mut-test.js | 72 ++++++++++++++- 6 files changed, 320 insertions(+), 3 deletions(-) create mode 100644 packages/@ember/-internals/glimmer/tests/integration/components/life-cycle-test.js 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 89929ec9653..6381b2e2b67 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,6 +6,7 @@ import { set, setProperties } from '@ember/object'; import Component from '@glimmer/component'; import { tracked } from '@glimmer/tracking'; +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'; @@ -245,6 +246,24 @@ moduleFor( this.assertText('Joel Kang, hello'); } + '@test positional parameters are not allowed'() { + let TestComponent = class extends PositionalComponent { + 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 can invoke curried components with capitalized block param names'() { this.owner.register( 'component:foo-bar', 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 index 3bb2e497c28..7515d22048c 100644 --- 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 @@ -1349,7 +1349,7 @@ moduleFor( ); moduleFor( - 'Run loop and lifecycle hooks', + 'Run loop and lifecycle hooks (classic component)', class extends RenderingTestCase { ['@test afterRender set']() { let ComponentClass = 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 883d9883c91..dac7fef42a7 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 @@ -757,6 +757,93 @@ moduleFor( this.assertText(`${expectedText},Hola`); } + ['@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', + setComponentTemplate( + precompileTemplate(''), + class extends PositionalComponent { + static positionalParams = ['value']; + + constructor(owner, args) { + super(owner, args); + value = this.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('{{@my-parent-attr}}'), + class extends Component {} + ) + ); + + 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=@myProp as |api|}}{{api.my-nested-component}}{{/my-component}}
' + ), + class extends Component {} + ) + ); + + 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'); + + 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 adding parameters to a contextual component's instance does not add it to other instances"]() { // If parameters and attributes are not handled correctly, setting a value // in an invokation can leak to others invocation. 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 fcb19125a64..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,6 +1,6 @@ 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'; @@ -107,5 +107,78 @@ moduleFor( this.assertText('hello', 'it does rerender after error in production'); } } + + ['@test it can recover resets the transaction when an error is thrown during modifier installation']( + assert + ) { + let shouldThrow = true; + let FooBarComponent = class extends Component { + inserted = defineSimpleModifier(() => { + 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 Component { + willDestroy() { + super.willDestroy(...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/life-cycle-test.js b/packages/@ember/-internals/glimmer/tests/integration/components/life-cycle-test.js new file mode 100644 index 00000000000..0300dd5284f --- /dev/null +++ b/packages/@ember/-internals/glimmer/tests/integration/components/life-cycle-test.js @@ -0,0 +1,68 @@ +import { moduleFor, RenderingTestCase, runTask, defineSimpleModifier } from 'internal-test-helpers'; + +import { schedule } from '@ember/runloop'; +import { set } from '@ember/object'; +import { tracked } from '@glimmer/tracking'; +import { precompileTemplate } from '@ember/template-compilation'; +import { setComponentTemplate } from '@glimmer/manager'; + +import Component from '@glimmer/component'; + +moduleFor( + 'Run loop and lifecycle hooks', + class extends RenderingTestCase { + ['@test afterRender set']() { + let ComponentClass = class extends Component { + @tracked width = '5'; + + inserted = defineSimpleModifier(() => { + schedule('afterRender', () => { + this.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 { + inserted = defineSimpleModifier(() => { + schedule('afterRender', () => { + set(this.args.parent, 'foo', 'wat'); + }); + }); + }; + + this.owner.register( + 'component:foo-bar', + setComponentTemplate( + precompileTemplate('
{{@foo}}
'), + ComponentClass + ) + ); + + this.render('{{foo-bar parent=this foo=this.foo}}'); + + this.assertText('wat'); + + runTask(() => this.rerender()); + + this.assertText('wat'); + } + } +); 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 d1c10dcd92b..73447e41a61 100644 --- a/packages/@ember/-internals/glimmer/tests/integration/helpers/mut-test.js +++ b/packages/@ember/-internals/glimmer/tests/integration/helpers/mut-test.js @@ -1,6 +1,6 @@ import { RenderingTestCase, moduleFor, runTask } from 'internal-test-helpers'; -import { set } from '@ember/object'; +import { set, get } from '@ember/object'; import { precompileTemplate } from '@ember/template-compilation'; import { setComponentTemplate } from '@glimmer/manager'; @@ -9,6 +9,76 @@ import Component from '@glimmer/component'; moduleFor( 'Helpers test: {{mut}}', class extends RenderingTestCase { + ['@test a simple mutable binding using `mut` propagates properly']() { + this.owner.register( + 'component:bottom-mut', + setComponentTemplate( + precompileTemplate('{{@setMe}}'), + class extends Component {} + ) + ); + + this.owner.register( + 'component:middle-mut', + setComponentTemplate( + precompileTemplate('{{bottom-mut setMe=@value}}'), + class extends Component {} + ) + ); + + this.render('{{middle-mut value=(mut this.val)}}', { + val: 12, + }); + + this.assertText('12', 'the data propagated downwards'); + + this.assertStableRerender(); + + runTask(() => this.$('button').click()); + + this.assertText('13', 'the set took effect'); + this.assert.strictEqual(get(this.context, 'val'), 13, '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']() { + this.owner.register( + 'component:bottom-mut', + setComponentTemplate( + precompileTemplate('{{@setMe}}'), + class extends Component {} + ) + ); + + this.owner.register( + 'component:middle-mut', + setComponentTemplate( + precompileTemplate('[{{@value}}]{{bottom-mut setMe=(mut @value)}}'), + class extends Component {} + ) + ); + + this.render('{{middle-mut value=(mut this.val)}}', { + val: 12, + }); + + this.assertText('[12]12', 'the data propagated downwards'); + + this.assertStableRerender(); + + runTask(() => this.$('button').click()); + + 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(this.context, 'val', 12)); + + this.assertText('[12]12'); + } + ['@test passing a literal results in a assertion']() { this.owner.register( 'component:bottom-mut', From a3cc15a9427acf5ad3b7ccf9e2432ef6db6131b2 Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Thu, 1 Oct 2026 03:46:55 -0400 Subject: [PATCH 03/46] Move more classic component tests into components/classic Seven more test files no longer import the classic `Component`. Their classic-only tests move, unchanged, to `tests/integration/components/classic/`. - `helpers/readonly-test.js` moves whole. Each test checks two-way binding or `this.attrs`. - `event-dispatcher-test.js`: the ten tests of event handler methods and `customEvents`. - `helpers/yield-test.js`: the `parentView` test. - `content-test.js`: the `attributeBindings: ["style"]` warning test. - `packages/ember/tests`: the `customEvents` tests of the application lifecycle, the tests of a component without a template, and the `layout` and `layoutName` tests. The new modules have the old name plus "(classic component)". A module that needs setup code gets a copy of it. No test is deleted and no test body changes. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../classic/application-lifecycle-test.js | 91 ++++ .../classic/component-context-test.js | 107 +++++ .../classic/component-registration-test.js | 127 ++++++ .../components/classic/content-test.js | 56 +++ .../classic/event-dispatcher-test.js | 422 ++++++++++++++++++ .../classic}/readonly-test.js | 4 +- .../components/classic/yield-test.js | 33 ++ .../glimmer/tests/integration/content-test.js | 21 +- .../integration/event-dispatcher-test.js | 399 +---------------- .../tests/integration/helpers/yield-test.js | 25 -- .../ember/tests/application_lifecycle_test.js | 72 --- .../ember/tests/component_context_test.js | 99 ---- .../tests/component_registration_test.js | 114 ----- 13 files changed, 840 insertions(+), 730 deletions(-) create mode 100644 packages/@ember/-internals/glimmer/tests/integration/components/classic/application-lifecycle-test.js create mode 100644 packages/@ember/-internals/glimmer/tests/integration/components/classic/component-context-test.js create mode 100644 packages/@ember/-internals/glimmer/tests/integration/components/classic/component-registration-test.js create mode 100644 packages/@ember/-internals/glimmer/tests/integration/components/classic/content-test.js create mode 100644 packages/@ember/-internals/glimmer/tests/integration/components/classic/event-dispatcher-test.js rename packages/@ember/-internals/glimmer/tests/integration/{helpers => components/classic}/readonly-test.js (98%) create mode 100644 packages/@ember/-internals/glimmer/tests/integration/components/classic/yield-test.js 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/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/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/helpers/readonly-test.js b/packages/@ember/-internals/glimmer/tests/integration/components/classic/readonly-test.js similarity index 98% rename from packages/@ember/-internals/glimmer/tests/integration/helpers/readonly-test.js rename to packages/@ember/-internals/glimmer/tests/integration/components/classic/readonly-test.js index 6dc5b3960a5..fcd3671cccb 100644 --- a/packages/@ember/-internals/glimmer/tests/integration/helpers/readonly-test.js +++ b/packages/@ember/-internals/glimmer/tests/integration/components/classic/readonly-test.js @@ -4,10 +4,10 @@ import { setComponentTemplate } from '@glimmer/manager'; import { set, get } from '@ember/object'; -import { Component } from '../../utils/helpers'; +import { Component } from '../../../utils/helpers'; moduleFor( - 'Helpers test: {{readonly}}', + 'Helpers test: {{readonly}} (classic component)', class extends RenderingTestCase { ['@test {{readonly}} of a path should work']() { let component; 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/content-test.js b/packages/@ember/-internals/glimmer/tests/integration/content-test.js index 467e7967c4d..16dc9f9d9c7 100644 --- a/packages/@ember/-internals/glimmer/tests/integration/content-test.js +++ b/packages/@ember/-internals/glimmer/tests/integration/content-test.js @@ -17,9 +17,7 @@ import EmberObject from '@ember/object'; import { readOnly } from '@ember/object/computed'; 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 { SafeString, htmlSafe } from '../utils/helpers'; const EMPTY = Object.freeze({}); @@ -1734,23 +1732,6 @@ if (DEBUG) { this.assertStyleWarning(userValue); } - ['@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); - } - ['@test specifying `
` works properly without a warning']() { this.render('
', { userValue: 'width: 42px', 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/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/tests/application_lifecycle_test.js b/packages/ember/tests/application_lifecycle_test.js index bd2698ab7df..d02cda03245 100644 --- a/packages/ember/tests/application_lifecycle_test.js +++ b/packages/ember/tests/application_lifecycle_test.js @@ -1,10 +1,7 @@ import { moduleFor, AutobootApplicationTestCase, runTask } from 'internal-test-helpers'; -import Application from '@ember/application'; import Route from '@ember/routing/route'; import Router from '@ember/routing/router'; -import { Component } from '@ember/-internals/glimmer'; import { precompileTemplate } from '@ember/template-compilation'; -import { setComponentTemplate } from '@glimmer/manager'; import { getDebugFunction, setDebugFunction } from '@ember/debug'; const originalDebug = getDebugFunction('debug'); @@ -120,74 +117,5 @@ moduleFor( runTask(() => this.application.destroy()); assert.equal(router._updatableRootOutletState, null, 'the root outlet was not re-rendered'); } - - [`@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/tests/component_context_test.js b/packages/ember/tests/component_context_test.js index 87f819e1677..58405af20e3 100644 --- a/packages/ember/tests/component_context_test.js +++ b/packages/ember/tests/component_context_test.js @@ -1,6 +1,5 @@ import Controller from '@ember/controller'; import Component from '@glimmer/component'; -import { Component as EmberComponent } from '@ember/-internals/glimmer'; import { precompileTemplate } from '@ember/template-compilation'; import { setComponentTemplate } from '@glimmer/manager'; import { moduleFor, ApplicationTestCase, getTextOf } from 'internal-test-helpers'; @@ -109,103 +108,5 @@ moduleFor( assert.equal(text, 'inner', 'The component is composed correctly'); }); } - - ['@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/tests/component_registration_test.js b/packages/ember/tests/component_registration_test.js index b1145a007be..f251208cc9d 100644 --- a/packages/ember/tests/component_registration_test.js +++ b/packages/ember/tests/component_registration_test.js @@ -1,6 +1,4 @@ import Application from '@ember/application'; -import Controller from '@ember/controller'; -import { Component } from '@ember/-internals/glimmer'; import { setComponentTemplate } from '@glimmer/manager'; import { precompileTemplate } from '@ember/template-compilation'; import { moduleFor, ApplicationTestCase } from 'internal-test-helpers'; @@ -55,118 +53,6 @@ moduleFor( }); } - ['@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'); - }); - } - async ['@test Using name of component that does not exist'](assert) { this.add( 'template:application', From 679def23a6d39d76de3c605f0108906949dc7272 Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Thu, 1 Oct 2026 03:46:55 -0400 Subject: [PATCH 04/46] Use Glimmer components in three more test files - `each-test.js`: "updating and setting within #each" sets tracked state from a modifier update. The modifier takes the place of `didUpdate`. - `template_rendering_test.js`: the outlet test counts modifier installs. The modifier takes the place of `didInsertElement`, so the test name now says "modifiers installed". - `helpers/readonly-test.js`: three new tests check `(readonly)` with Glimmer components. The classic tests of this helper are in `components/classic/readonly-test.js`. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../integration/helpers/readonly-test.js | 84 +++++++++++++++++++ .../tests/integration/syntax/each-test.js | 31 +++---- .../tests/routing/template_rendering_test.js | 44 +++++----- 3 files changed, 126 insertions(+), 33 deletions(-) create mode 100644 packages/@ember/-internals/glimmer/tests/integration/helpers/readonly-test.js diff --git a/packages/@ember/-internals/glimmer/tests/integration/helpers/readonly-test.js b/packages/@ember/-internals/glimmer/tests/integration/helpers/readonly-test.js new file mode 100644 index 00000000000..45433449afc --- /dev/null +++ b/packages/@ember/-internals/glimmer/tests/integration/helpers/readonly-test.js @@ -0,0 +1,84 @@ +import { RenderingTestCase, moduleFor, runTask } from 'internal-test-helpers'; +import { precompileTemplate } from '@ember/template-compilation'; +import { setComponentTemplate } from '@glimmer/manager'; + +import { set } from '@ember/object'; + +import Component from '@glimmer/component'; + +moduleFor( + 'Helpers test: {{readonly}}', + class extends RenderingTestCase { + '@test updating a {{readonly}} property from above works'() { + this.owner.register( + 'component:foo-bar', + setComponentTemplate(precompileTemplate('{{@value}}'), class extends Component {}) + ); + + this.render('{{foo-bar value=(readonly this.thing)}}', { + thing: 'initial', + }); + + this.assertText('initial'); + + this.assertStableRerender(); + + runTask(() => set(this.context, 'thing', 'updated!')); + + this.assertText('updated!'); + + 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('{{@value.prop}}'), + class extends Component { + constructor(owner, args) { + super(owner, args); + component = this; + } + } + ) + ); + + this.render('{{foo-bar value=(readonly this.thing)}}', { + thing: { + prop: 'initial', + }, + }); + + this.assertText('initial'); + + this.assertStableRerender(); + + runTask(() => set(component.args.value, 'prop', 'updated!')); + + this.assertText('updated!', 'nested path is updated'); + assert.deepEqual(this.context.thing, { prop: 'updated!' }); + + runTask(() => set(component.args.value, 'prop', 'initial')); + + this.assertText('initial'); + } + + ['@test {{readonly}} of a string renders correctly']() { + this.owner.register( + 'component:foo-bar', + setComponentTemplate(precompileTemplate('{{@value}}'), class extends Component {}) + ); + + this.render('{{foo-bar value=(readonly "12")}}'); + + this.assertText('12'); + + this.assertStableRerender(); + } + } +); 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..45dfd2be3d5 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 ) ); diff --git a/packages/ember/tests/routing/template_rendering_test.js b/packages/ember/tests/routing/template_rendering_test.js index 902437b94c7..00bd20b818d 100644 --- a/packages/ember/tests/routing/template_rendering_test.js +++ b/packages/ember/tests/routing/template_rendering_test.js @@ -3,10 +3,14 @@ import Route from '@ember/routing/route'; import Controller from '@ember/controller'; import EmberObject from '@ember/object'; import { A as emberA } from '@ember/array'; -import { moduleFor, ApplicationTestCase, getTextOf } from 'internal-test-helpers'; +import { + moduleFor, + ApplicationTestCase, + getTextOf, + defineSimpleModifier, +} from 'internal-test-helpers'; import { run } from '@ember/runloop'; import Component from '@glimmer/component'; -import { Component as EmberComponent } from '@ember/-internals/glimmer'; import { setComponentTemplate } from '@glimmer/manager'; import { service } from '@ember/service'; import { precompileTemplate } from '@ember/template-compilation'; @@ -302,7 +306,7 @@ moduleFor( }); } - ['@test Components inside an outlet have their didInsertElement hook invoked when the route is displayed']( + ['@test Components inside an outlet have their modifiers installed when the route is displayed']( assert ) { this.add( @@ -338,32 +342,34 @@ moduleFor( this.add( 'component:my-component', - class extends EmberComponent { - didInsertElement() { - myComponentCounter++; + setComponentTemplate( + precompileTemplate('
    '), + class extends Component { + inserted = defineSimpleModifier(() => { + myComponentCounter++; + }); } - } + ) ); this.add( 'component:other-component', - class extends EmberComponent { - didInsertElement() { - otherComponentCounter++; + setComponentTemplate( + precompileTemplate('
    '), + class extends Component { + inserted = defineSimpleModifier(() => { + otherComponentCounter++; + }); } - } + ) ); return this.visit('/').then(() => { - assert.strictEqual( - myComponentCounter, - 1, - 'didInsertElement invoked on displayed component' - ); + assert.strictEqual(myComponentCounter, 1, 'the displayed component installs its modifier'); assert.strictEqual( otherComponentCounter, 0, - 'didInsertElement not invoked on displayed component' + 'the other component does not install its modifier' ); run(() => indexController.set('showFirst', false)); @@ -371,12 +377,12 @@ moduleFor( assert.strictEqual( myComponentCounter, 1, - 'didInsertElement not invoked on displayed component' + 'the removed component does not install its modifier again' ); assert.strictEqual( otherComponentCounter, 1, - 'didInsertElement invoked on displayed component' + 'the newly displayed component installs its modifier' ); }); } From 46ce689cd85c3cedd593a1f6cf31af4ea5125906 Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Thu, 1 Oct 2026 04:05:01 -0400 Subject: [PATCH 05/46] Keep classic and Glimmer coverage for tests that can use both A test that can use a Glimmer component now has two versions: the classic one in `components/classic/`, and a Glimmer one in the original file. When the classic `Component` is removed, only classic coverage goes away. Classic versions that the last commit dropped come back: - `classic/each-test.js`: "updating and setting within #each", with `didUpdate`. - `classic/template-rendering-test.js`: the outlet test, with `didInsertElement`. New Glimmer versions of moved tests: - `readonly`: `(mut @foo)` of a `(readonly)` argument is not allowed. - content: `style` on a component with `...attributes` gives the style warning. - component context: arguments called `data` and `attrs` do not collide with internal structures. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../components/classic/each-test.js | 78 ++++++++++++++++ .../classic/template-rendering-test.js | 90 +++++++++++++++++++ .../glimmer/tests/integration/content-test.js | 19 ++++ .../integration/helpers/readonly-test.js | 21 +++++ .../ember/tests/component_context_test.js | 48 ++++++++++ 5 files changed, 256 insertions(+) create mode 100644 packages/@ember/-internals/glimmer/tests/integration/components/classic/each-test.js create mode 100644 packages/@ember/-internals/glimmer/tests/integration/components/classic/template-rendering-test.js 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/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/content-test.js b/packages/@ember/-internals/glimmer/tests/integration/content-test.js index 16dc9f9d9c7..ba92af7a624 100644 --- a/packages/@ember/-internals/glimmer/tests/integration/content-test.js +++ b/packages/@ember/-internals/glimmer/tests/integration/content-test.js @@ -17,6 +17,9 @@ import EmberObject from '@ember/object'; import { readOnly } from '@ember/object/computed'; 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 from '@glimmer/component'; import { SafeString, htmlSafe } from '../utils/helpers'; const EMPTY = Object.freeze({}); @@ -1732,6 +1735,22 @@ if (DEBUG) { this.assertStyleWarning(userValue); } + ['@test specifying `style` on a component with `...attributes` generates a warning']() { + this.owner.register( + 'component:foo-bar', + setComponentTemplate( + precompileTemplate('
    hello
    '), + class extends Component {} + ) + ); + let userValue = 'width: 42px'; + this.render('', { + userValue, + }); + + this.assertStyleWarning(userValue); + } + ['@test specifying `
    ` works properly without a warning']() { this.render('
    ', { userValue: 'width: 42px', 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 45433449afc..ec8949e9c22 100644 --- a/packages/@ember/-internals/glimmer/tests/integration/helpers/readonly-test.js +++ b/packages/@ember/-internals/glimmer/tests/integration/helpers/readonly-test.js @@ -80,5 +80,26 @@ moduleFor( this.assertStableRerender(); } + + ['@test {{mut}} of a {{readonly}} argument is not allowed']() { + this.owner.register( + 'component:x-bottom', + setComponentTemplate(precompileTemplate('{{@bar}}'), class extends Component {}) + ); + + this.owner.register( + 'component:x-middle', + setComponentTemplate( + precompileTemplate('{{@foo}} {{x-bottom bar=(mut @foo)}}'), + class extends Component {} + ) + ); + + expectAssertion(() => { + this.render('{{x-middle foo=(readonly this.val)}}', { + val: 12, + }); + }, 'You can only pass a path to mut'); + } } ); diff --git a/packages/ember/tests/component_context_test.js b/packages/ember/tests/component_context_test.js index 58405af20e3..d9b114ca548 100644 --- a/packages/ember/tests/component_context_test.js +++ b/packages/ember/tests/component_context_test.js @@ -108,5 +108,53 @@ moduleFor( assert.equal(text, 'inner', 'The component is composed correctly'); }); } + + ['@test an argument called `data` should not collide with internal structures'](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', + setComponentTemplate(precompileTemplate('{{@data}}'), class extends Component {}) + ); + + return this.visit('/').then(() => { + let text = getTextOf(this.element.querySelector('#wrapper')); + assert.equal(text, 'Some text inserted', 'The component is composed correctly'); + }); + } + + ['@test an argument called `attrs` 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', + setComponentTemplate(precompileTemplate('{{@attrs}}'), class extends Component {}) + ); + + return this.visit('/').then(() => { + let text = getTextOf(this.element.querySelector('#wrapper')); + assert.equal(text, 'Some text inserted', 'The component is composed correctly'); + }); + } } ); From 1a46798aa09fb8c8546235a9626b657e6e6a60d8 Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Thu, 1 Oct 2026 04:17:04 -0400 Subject: [PATCH 06/46] Add Glimmer versions of eight classic component tests The classic tests in `tests/integration/components/classic/` go away together with the classic `Component`. These eight subjects also exist for Glimmer components, and had no Glimmer test: - angle bracket: an `id` attribute is bound, and named arguments are on `this.args`. - contextual: GH#13494, a component without content that gets a bound argument. - curly: a class is on the element before a modifier installs, GH#13846 (a child sets state on its parent during destruction), and GH#14273 (a tracked property set in `willDestroy`). - life cycle: the order of constructors, modifier installs, modifier destructors and `willDestroy` in a component tree. - `mut`: #11023, `(mut this.val)` on a getter that reads tracked state. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../angle-bracket-invocation-test.js | 65 +++++++++++ .../components/contextual-components-test.js | 42 +++++++ .../components/curly-components-test.js | 109 +++++++++++++++++ .../integration/components/life-cycle-test.js | 110 ++++++++++++++++++ .../tests/integration/helpers/mut-test.js | 47 ++++++++ 5 files changed, 373 insertions(+) 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 6381b2e2b67..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 @@ -169,6 +169,71 @@ moduleFor( 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 Component {} + ) + ); + + this.render('', { + customId: 'bizz', + }); + + this.assertElement(this.firstChild, { + tagName: 'div', + attrs: { id: 'bizz' }, + content: 'hello', + }); + + this.assertStableRerender(); + + runTask(() => set(this.context, 'customId', 'bar')); + + this.assertElement(this.firstChild, { + tagName: 'div', + attrs: { id: 'bar' }, + content: 'hello', + }); + + runTask(() => set(this.context, 'customId', 'bizz')); + + this.assertElement(this.firstChild, { + tagName: 'div', + attrs: { id: 'bizz' }, + content: 'hello', + }); + } + + '@test it reflects named arguments on `this.args`'() { + this.owner.register( + 'component:foo-bar', + setComponentTemplate(precompileTemplate('{{this.args.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 can render a basic component with a block'() { this.owner.register( 'component:foo-bar', 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 dac7fef42a7..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 @@ -1,7 +1,10 @@ import { DEBUG } from '@glimmer/env'; import { moduleFor, RenderingTestCase, applyMixins, strip, runTask } from 'internal-test-helpers'; +import { action } from '@ember/object'; + import Component from '@glimmer/component'; +import { tracked } from '@glimmer/tracking'; import PositionalComponent from '../../utils/positional-component'; import { precompileTemplate } from '@ember/template-compilation'; import { setComponentTemplate } from '@glimmer/manager'; @@ -1152,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 { + inner = () => calls.push('inner'); + } + ) + ); + + this.owner.register( + 'component:after-splat', + setComponentTemplate( + precompileTemplate(``), + 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 {{on}} listeners run before classic component event handler methods (GH#17877)'(assert) { + let calls = []; + + this.owner.register( + 'component:classic-clicker', + setComponentTemplate( + precompileTemplate(`Click`), + class extends ClassicComponent { + inner = () => calls.push('inner'); + + click() { + calls.push('click()'); + } + } + ) + ); + + this.render(``, { + outer: () => calls.push('outer'), + }); + + runTask(() => this.$('span').click()); + assert.deepEqual(calls, ['inner', 'outer', 'click()'], 'click() runs last'); + + calls = []; + runTask(() => + this.context.set('outer', (event) => { + calls.push('outer'); + event.stopPropagation(); + }) + ); + runTask(() => this.$('span').click()); + assert.deepEqual( + calls, + ['inner', 'outer'], + 'stopping propagation in an {{on}} listener prevents click()' + ); + } } ); diff --git a/packages/@glimmer/runtime/lib/modifiers/on.ts b/packages/@glimmer/runtime/lib/modifiers/on.ts index 51b4f48d00a..0ca3da5320a 100644 --- a/packages/@glimmer/runtime/lib/modifiers/on.ts +++ b/packages/@glimmer/runtime/lib/modifiers/on.ts @@ -313,6 +313,45 @@ function addEventListener( In this case, the `saveLike` function will receive two arguments: the click event and the value of `@post`. + ### Listener Order + + Listeners added with `{{on}}` are regular DOM event listeners, so they run in + the order they were added to an element, and an event reaches listeners on the + target element before it bubbles up to listeners on its ancestors. + + When a component forwards modifiers to an element with `...attributes`, the + `{{on}}` listeners passed in by the caller run before the listeners that the + component puts on that element itself. This is true wherever `...attributes` + appears in the element's attribute list: + + ```gjs {data-filename="app/components/my-button.gjs"} + + ``` + + ```gjs + + ``` + + If a component needs its own handler to run first, for example to decide + whether the caller's handler should run, have the caller pass the handler as + an argument and call it from the component's handler instead of passing it + with `{{on}}`. + + Event handler methods on classic components, such as `click()`, are run by + Ember's event dispatcher, which listens on the application's root element. + They run after any `{{on}}` listeners on the component's element and its + descendants, and calling `event.stopPropagation()` in one of those listeners + prevents them from running. + ### Function Context In the example above, we used `@action` to ensure that `likePost` is From 3ee8cb304bd7c6608429fcdd6c472e45afb2f435 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 2 Oct 2026 20:16:54 +0000 Subject: [PATCH 12/46] Link RFC #373 from the setModifierManager docs Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_013JirFwqPVyTTFj5zPbonSh --- packages/@ember/modifier/index.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/@ember/modifier/index.ts b/packages/@ember/modifier/index.ts index e8b9364c2ce..88a0ddf4842 100644 --- a/packages/@ember/modifier/index.ts +++ b/packages/@ember/modifier/index.ts @@ -29,6 +29,10 @@ export { on, type OnModifier } from './on'; `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 @@ -129,6 +133,7 @@ export { on, type OnModifier } from './on'; @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` From f4cbcdcbb976827a642503e49d56202758329eca Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 2 Oct 2026 20:21:15 +0000 Subject: [PATCH 13/46] Use a Glimmer component for the {{on}} ordering test Replace the classic component `click()` test with a @glimmer/component that has a listener on a wrapping element, and drop the classic handler section from the docs. Also document that changing an {{on}}'s arguments re-adds the listener, which moves it after the others. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_013JirFwqPVyTTFj5zPbonSh --- .../tests/integration/modifiers/on-test.js | 44 +++++++++---------- packages/@glimmer/runtime/lib/modifiers/on.ts | 14 +++--- 2 files changed, 31 insertions(+), 27 deletions(-) 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 6812a5c4f4b..094b0ff991b 100644 --- a/packages/@ember/-internals/glimmer/tests/integration/modifiers/on-test.js +++ b/packages/@ember/-internals/glimmer/tests/integration/modifiers/on-test.js @@ -6,7 +6,6 @@ import { precompileTemplate } from '@ember/template-compilation'; import { DEBUG } from '@glimmer/env'; import Component from '@glimmer/component'; -import ClassicComponent from '@ember/component'; moduleFor( '{{on}} Modifier', @@ -336,42 +335,43 @@ moduleFor( assert.deepEqual(calls, ['outer', 'inner'], 'own modifier after ...attributes'); } - '@test {{on}} listeners run before classic component event handler methods (GH#17877)'(assert) { + "@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:classic-clicker', + 'component:wrapped-button', setComponentTemplate( - precompileTemplate(`Click`), - class extends ClassicComponent { + precompileTemplate( + `
    ` + ), + class extends Component { inner = () => calls.push('inner'); - - click() { - calls.push('click()'); - } + wrapper = () => calls.push('wrapper'); } ) ); - this.render(``, { - outer: () => calls.push('outer'), + let stop = false; + + this.render(``, { + outer: (event) => { + calls.push('outer'); + if (stop) event.stopPropagation(); + }, }); - runTask(() => this.$('span').click()); - assert.deepEqual(calls, ['inner', 'outer', 'click()'], 'click() runs last'); + runTask(() => this.$('button').click()); + assert.deepEqual(calls, ['outer', 'inner', 'wrapper'], 'the ancestor listener runs last'); calls = []; - runTask(() => - this.context.set('outer', (event) => { - calls.push('outer'); - event.stopPropagation(); - }) - ); - runTask(() => this.$('span').click()); + stop = true; + runTask(() => this.$('button').click()); assert.deepEqual( calls, - ['inner', 'outer'], - 'stopping propagation in an {{on}} listener prevents click()' + ['outer', 'inner'], + "stopping propagation in the caller's listener prevents the ancestor listener" ); } } diff --git a/packages/@glimmer/runtime/lib/modifiers/on.ts b/packages/@glimmer/runtime/lib/modifiers/on.ts index 0ca3da5320a..e8c38554895 100644 --- a/packages/@glimmer/runtime/lib/modifiers/on.ts +++ b/packages/@glimmer/runtime/lib/modifiers/on.ts @@ -341,16 +341,20 @@ function addEventListener( ``` + This order is set when the listeners are first added. When the arguments to an + `{{on}}` change, for example because a different callback is passed in, that + listener is removed and added again, so it moves after the other listeners on + the element. + If a component needs its own handler to run first, for example to decide whether the caller's handler should run, have the caller pass the handler as an argument and call it from the component's handler instead of passing it with `{{on}}`. - Event handler methods on classic components, such as `click()`, are run by - Ember's event dispatcher, which listens on the application's root element. - They run after any `{{on}}` listeners on the component's element and its - descendants, and calling `event.stopPropagation()` in one of those listeners - prevents them from running. + Calling `event.stopPropagation()` in any of these listeners stops the event + from reaching listeners on ancestor elements, including ones the component + put on a wrapping element in its own template. It does not stop the other + listeners on the same element. ### Function Context From d692ad880e54c01d92ee48a3f7ddfa5d4c45e758 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 2 Oct 2026 22:22:20 +0000 Subject: [PATCH 14/46] Collect garbage between the phases of pnpm bench Garbage from one phase (render10000Items, for example) was collected inside a later phase, wherever V8 decided to. A change that allocates more or less moved those collections between phases, so phase times shifted for reasons unrelated to the phase itself. Chrome now runs with --expose-gc, and waitForIdle() calls gc() before the next phase starts, outside the measured marks. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01JqJMavyH9FWrQWG3PAGzsT --- bin/benchmark/run.mjs | 4 ++++ smoke-tests/benchmark-app/app/run-benchmark.js | 11 ++++++++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/bin/benchmark/run.mjs b/bin/benchmark/run.mjs index 3a1cf6bcc0a..454376b7386 100644 --- a/bin/benchmark/run.mjs +++ b/bin/benchmark/run.mjs @@ -177,6 +177,10 @@ async function bootAndRun({ headless = true } = {}) { // as the contradictory flags cause use-after-free crashes on macOS '--disable-gpu', '--disable-gpu-compositing', + // Exposes gc(), which the benchmark app calls between phases (see + // waitForIdle in smoke-tests/benchmark-app/app/run-benchmark.js), so + // the garbage of one phase is not collected inside a later one. + '--js-flags=--expose-gc', ].join(','), ]; diff --git a/smoke-tests/benchmark-app/app/run-benchmark.js b/smoke-tests/benchmark-app/app/run-benchmark.js index 23c79e915c1..5083d3b49d3 100644 --- a/smoke-tests/benchmark-app/app/run-benchmark.js +++ b/smoke-tests/benchmark-app/app/run-benchmark.js @@ -29,7 +29,16 @@ export function emitDomClickEvent(selector) { export function waitForIdle() { return new Promise((resolve) => { - requestIdleCallback(resolve); + requestIdleCallback(() => { + // Collect the previous phase's garbage outside of any measured phase. + // Chrome only exposes gc() with --js-flags=--expose-gc. + if (typeof globalThis.gc === 'function') { + globalThis.gc(); + requestIdleCallback(resolve); + } else { + resolve(); + } + }); }); } From 20c63b05c243ed40e8701b18feb1a8cdbe734ea0 Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Sun, 4 Oct 2026 04:14:21 -0400 Subject: [PATCH 15/46] Use settled from @ember/test-helpers in the transitionTo tests Four RouterService#transitionTo tests started a transition inside `run(() => ...)`. They now start the transition directly and wait with `await settled()`, as an application test does. `@ember/test-helpers` was not available in the main test build. This commit adds it: - `@ember/test-helpers` is a devDependency of the root package. - `testDependencies` in rollup.config.mjs lets the test build resolve `@ember/test-helpers`, `@ember/test-waiters` and `dom-element-descriptors` from node_modules. - babel.test.config.mjs runs the `@embroider/macros` plugin, because those two addons use macros. `buildMacros()` reads NODE_ENV, but the test build selects its mode with EMBER_ENV. The `configure` hook enables development mode from EMBER_ENV, so `isDevelopingApp()` is the same with and without NODE_ENV. The published build does not use babel.test.config.mjs. Co-Authored-By: Claude Opus 5.5 (1M context) --- babel.test.config.mjs | 14 ++++++- package.json | 1 + .../router_service_test/transitionTo_test.js | 38 +++++++++---------- pnpm-lock.yaml | 3 ++ rollup.config.mjs | 3 ++ 5 files changed, 37 insertions(+), 22 deletions(-) diff --git a/babel.test.config.mjs b/babel.test.config.mjs index f83bb403a1f..dad707aeaf6 100644 --- a/babel.test.config.mjs +++ b/babel.test.config.mjs @@ -9,12 +9,24 @@ import { createRequire } from 'node:module'; import baseConfig from './babel.config.mjs'; +import { buildMacros } from '@embroider/macros/babel'; // eslint-disable-next-line no-redeclare const require = createRequire(import.meta.url); const buildDebugMacroPlugin = require('./broccoli/build-debug-macro-plugin.cjs'); const isProduction = process.env.EMBER_ENV === 'production'; +// @ember/test-helpers and @ember/test-waiters use @embroider/macros. +// +// buildMacros() reads NODE_ENV, but our builds select the mode with EMBER_ENV. +const macros = buildMacros({ + configure(config) { + if (!isProduction) { + config.enablePackageDevelopment(process.cwd()); + } + }, +}); + export default { ...baseConfig, @@ -27,5 +39,5 @@ export default { ], ], - plugins: [...baseConfig.plugins, ...buildDebugMacroPlugin(!isProduction)], + plugins: [...baseConfig.plugins, ...macros.babelMacros, ...buildDebugMacroPlugin(!isProduction)], }; diff --git a/package.json b/package.json index dfac084f3da..26b3917add8 100644 --- a/package.json +++ b/package.json @@ -106,6 +106,7 @@ "@babel/plugin-transform-typescript": "^7.26.8", "@babel/preset-env": "^7.16.11", "@babel/types": "^7.22.5", + "@ember/test-helpers": "^5.5.0", "@embroider/macros": "^1.20.2", "@embroider/shared-internals": "^3.0.2", "@embroider/vite": "^1.7.2", diff --git a/packages/ember/tests/routing/router_service_test/transitionTo_test.js b/packages/ember/tests/routing/router_service_test/transitionTo_test.js index 1416976c3ee..2378177d65d 100644 --- a/packages/ember/tests/routing/router_service_test/transitionTo_test.js +++ b/packages/ember/tests/routing/router_service_test/transitionTo_test.js @@ -3,11 +3,11 @@ import Component from '@glimmer/component'; import Route from '@ember/routing/route'; import NoneLocation from '@ember/routing/none-location'; import Controller from '@ember/controller'; -import { run } from '@ember/runloop'; import { action, get } from '@ember/object'; import { precompileTemplate } from '@ember/template-compilation'; import { setComponentTemplate } from '@glimmer/manager'; import { RouterTestCase, moduleFor } from 'internal-test-helpers'; +import { settled } from '@ember/test-helpers'; import { InternalTransition as Transition } from 'router_js'; moduleFor( @@ -96,7 +96,7 @@ moduleFor( }); } - ['@test RouterService#transitionTo with basic route'](assert) { + async ['@test RouterService#transitionTo with basic route'](assert) { assert.expect(1); let componentInstance; @@ -122,16 +122,15 @@ moduleFor( ) ); - return this.visit('/').then(() => { - run(function () { - componentInstance.transitionToSister(); - }); + await this.visit('/'); - assert.equal(this.routerService.get('currentRouteName'), 'parent.sister'); - }); + componentInstance.transitionToSister(); + await settled(); + + assert.equal(this.routerService.get('currentRouteName'), 'parent.sister'); } - ['@test RouterService#transitionTo with basic route using URL'](assert) { + async ['@test RouterService#transitionTo with basic route using URL'](assert) { assert.expect(1); let componentInstance; @@ -157,13 +156,12 @@ moduleFor( ) ); - return this.visit('/').then(() => { - run(function () { - componentInstance.transitionToSister(); - }); + await this.visit('/'); - assert.equal(this.routerService.get('currentRouteName'), 'parent.sister'); - }); + componentInstance.transitionToSister(); + await settled(); + + assert.equal(this.routerService.get('currentRouteName'), 'parent.sister'); } async ['@test RouterService#transitionTo with dynamic segment'](assert) { @@ -196,9 +194,8 @@ moduleFor( await this.visit('/'); - run(function () { - componentInstance.transitionToDynamic(); - }); + componentInstance.transitionToDynamic(); + await settled(); assert.equal(this.routerService.get('currentRouteName'), 'dynamic'); assert.equal(this.routerService.get('currentURL'), '/dynamic/1'); @@ -244,9 +241,8 @@ moduleFor( await this.visit('/'); - run(function () { - componentInstance.transitionToDynamic(); - }); + componentInstance.transitionToDynamic(); + await settled(); assert.equal(this.routerService.get('currentRouteName'), 'dynamic'); assert.equal(this.routerService.get('currentURL'), '/dynamic/1'); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 73af5a19048..2f53d3c07d5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -91,6 +91,9 @@ importers: '@babel/types': specifier: ^7.22.5 version: 7.29.7 + '@ember/test-helpers': + specifier: ^5.5.0 + version: 5.5.0(@babel/core@7.29.7) '@embroider/macros': specifier: ^1.20.2 version: 1.20.3(@babel/core@7.29.7) diff --git a/rollup.config.mjs b/rollup.config.mjs index 4491f00f19c..95266380d4d 100644 --- a/rollup.config.mjs +++ b/rollup.config.mjs @@ -16,6 +16,9 @@ const buildDebugMacroPlugin = require('./broccoli/build-debug-macro-plugin.cjs') const canaryFeatures = require('./broccoli/canary-features.cjs'); const testDependencies = [ + '@ember/test-helpers', + '@ember/test-waiters', + 'dom-element-descriptors', 'qunit', 'vite', 'js-reporters', From 860195b62e370858887ad6577e8c55aebb533bd2 Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Mon, 5 Oct 2026 22:18:52 -0400 Subject: [PATCH 16/46] Pool the trackers of tracking frames Each tracking frame made one Tracker, one Set, and one array from that Set. Frames are strictly nested, so beginTrackFrame now takes the tracker for its depth from a pool. A tracker keeps its tags in an array. A tag keeps the index at which a tracker took it last, in a new field `slot`. A tracker has the tag if its entry at that index is the tag, so a tag that the frame consumes again costs one comparison. resetTracking clears the pooled trackers. An untrack frame takes a depth but no tracker, so the pool can have holes. Split out of #21650. Co-Authored-By: Claude Opus 5.5 (1M context) --- packages/@glimmer/validator/lib/tracking.ts | 96 ++++++++++++++++--- packages/@glimmer/validator/lib/validators.ts | 7 ++ .../@glimmer/validator/test/tracking-test.ts | 86 +++++++++++++++++ 3 files changed, 175 insertions(+), 14 deletions(-) diff --git a/packages/@glimmer/validator/lib/tracking.ts b/packages/@glimmer/validator/lib/tracking.ts index d94ae456e6b..4596f340f5d 100644 --- a/packages/@glimmer/validator/lib/tracking.ts +++ b/packages/@glimmer/validator/lib/tracking.ts @@ -8,34 +8,80 @@ import { unwrap } from './utils'; import { combine, CONSTANT_TAG, isConstTag, validateTag, valueForTag } from './validators'; /** - * An object that that tracks @tracked properties that were consumed. + * A tag of this package. Each tag class declares `slot`: + * the index of the tag in the tracker that took it last. */ -class Tracker { - private tags = new Set(); - private last: Tag | null = null; +interface ConsumedTag extends Tag { + slot: number; +} +/** + * An object that tracks @tracked properties that were consumed. + * + * Trackers are pooled by frame depth (see `beginTrackFrame`), + * so a tracker holds no tag after its frame ends. + */ +class Tracker { + /** + * The consumed tags. `size` counts the live entries. + * + * The array never shrinks, because a write to `length` is a slow path in V8, + * and this code runs for every frame. + */ + private tags: (Tag | null)[] = []; + private size = 0; + + /** + * A tag keeps the index at which a tracker took it last. + * If this tracker has the tag at that index, the frame consumed the tag before, + * so a tag that the frame consumes again costs one comparison. + * + * The index can come from another tracker. + * The entry at that index is then another tag or no tag, and this tracker takes the tag. + * So a tag that a nested frame takes at another index, between two consumptions + * of this frame, is taken two times. A combined tag with a duplicate has the same revision. + */ add(tag: Tag) { if (tag === CONSTANT_TAG) return; - this.tags.add(tag); - if (DEBUG) { unwrap(debug.markTagAsConsumed)(tag); } - this.last = tag; + let { tags, size } = this; + + if (tags[(tag as ConsumedTag).slot] === tag) return; + + (tag as ConsumedTag).slot = size; + tags[size] = tag; + this.size = size + 1; } combine(): Tag { - let { tags } = this; + let { tags, size } = this; + let result: Tag; - if (tags.size === 0) { - return CONSTANT_TAG; - } else if (tags.size === 1) { - return this.last as Tag; + if (size === 0) { + result = CONSTANT_TAG; + } else if (size === 1) { + result = tags[0] as Tag; } else { - return combine(Array.from(this.tags)); + result = combine(tags.slice(0, size) as Tag[]); } + + this.clear(); + + return result; + } + + clear(): void { + let { tags, size } = this; + + for (let i = 0; i < size; i++) { + tags[i] = null; + } + + this.size = 0; } } @@ -56,10 +102,26 @@ let CURRENT_TRACKER: Tracker | null = null; const OPEN_TRACK_FRAMES: (Tracker | null)[] = []; +/** + * Frames are strictly nested, so the tracker of a frame at depth `n` is free + * when that frame ends. One tracker for each depth is enough. + * + * An untrack frame takes a depth but no tracker, so the pool can have holes. + */ +const TRACKER_POOL: (Tracker | undefined)[] = []; + export function beginTrackFrame(debuggingContext?: string | false): void { + let depth = OPEN_TRACK_FRAMES.length; + OPEN_TRACK_FRAMES.push(CURRENT_TRACKER); - CURRENT_TRACKER = new Tracker(); + let tracker = TRACKER_POOL[depth]; + + if (tracker === undefined) { + tracker = TRACKER_POOL[depth] = new Tracker(); + } + + CURRENT_TRACKER = tracker; if (DEBUG) { unwrap(debug.beginTrackingTransaction)(debuggingContext); @@ -101,6 +163,12 @@ export function resetTracking(): string | void { OPEN_TRACK_FRAMES.pop(); } + for (let tracker of TRACKER_POOL) { + if (tracker !== undefined) { + tracker.clear(); + } + } + CURRENT_TRACKER = null; if (DEBUG) { diff --git a/packages/@glimmer/validator/lib/validators.ts b/packages/@glimmer/validator/lib/validators.ts index 66c7f1ac1cb..7de985abf97 100644 --- a/packages/@glimmer/validator/lib/validators.ts +++ b/packages/@glimmer/validator/lib/validators.ts @@ -114,6 +114,11 @@ class MonomorphicTagImpl { public subtag: Tag | Tag[] | null = null; private subtagBufferCache: Revision | null = null; + /** + * The index of this tag in the tracker that took it last. + */ + public slot = 0; + declare [TYPE]: T; constructor(type: T) { @@ -254,6 +259,7 @@ const VOLATILE_TAG_ID: IVOLATILE_TAG_ID = 100; export class VolatileTag implements Tag { readonly [TYPE] = VOLATILE_TAG_ID; + slot = 0; [COMPUTE](): Revision { return VOLATILE; } @@ -267,6 +273,7 @@ const CURRENT_TAG_ID: ICURRENT_TAG_ID = 101; export class CurrentTag implements Tag { readonly [TYPE] = CURRENT_TAG_ID; + slot = 0; [COMPUTE](): Revision { return $REVISION; } diff --git a/packages/@glimmer/validator/test/tracking-test.ts b/packages/@glimmer/validator/test/tracking-test.ts index 1a0fc4c1dc0..40d8d57f18a 100644 --- a/packages/@glimmer/validator/test/tracking-test.ts +++ b/packages/@glimmer/validator/test/tracking-test.ts @@ -1,6 +1,7 @@ import { DEBUG } from '@glimmer/env'; import { beginTrackFrame, + beginUntrackFrame, consumeTag, createCache, createTag, @@ -10,6 +11,7 @@ import { getValue, isConst, isTracking, + resetTracking, track, trackedData, untrack, @@ -252,6 +254,90 @@ module('@glimmer/validator: tracking', () => { assert.notOk(validateTag(combined, snapshot)); }); + test('it returns the tag itself if the frame consumed one tag many times', (assert) => { + let tag = createTag(); + + beginTrackFrame(); + + consumeTag(tag); + consumeTag(tag); + + assert.strictEqual(endTrackFrame(), tag); + }); + + test('it keeps a tag that a nested frame consumed between two consumptions', (assert) => { + let tag1 = createTag(); + let tag2 = createTag(); + + beginTrackFrame(); + + consumeTag(tag1); + consumeTag(tag2); + + beginTrackFrame(); + consumeTag(tag1); + let inner = endTrackFrame(); + + consumeTag(tag1); + + let outer = endTrackFrame(); + + assert.strictEqual(inner, tag1); + + let snapshot = valueForTag(outer); + dirtyTag(tag1); + assert.notOk(validateTag(outer, snapshot)); + + snapshot = valueForTag(outer); + dirtyTag(tag2); + assert.notOk(validateTag(outer, snapshot)); + }); + + test('it takes a tag one time if nested frames consume it at the same index', (assert) => { + let tag = createTag(); + + beginTrackFrame(); + + for (let i = 0; i < 3; i++) { + beginTrackFrame(); + consumeTag(tag); + consumeTag(endTrackFrame()); + } + + assert.strictEqual(endTrackFrame(), tag); + }); + + test('it does not keep the tags of a frame that did not end', (assert) => { + let tag1 = createTag(); + let tag2 = createTag(); + + beginTrackFrame(); + consumeTag(tag1); + + resetTracking(); + + beginTrackFrame(); + consumeTag(tag2); + + assert.strictEqual(endTrackFrame(), tag2); + }); + + test('it resets after a frame that began inside untrack frames', (assert) => { + /** + * Deeper than any other test goes, + * so these depths have no tracker yet. + */ + for (let i = 0; i < 100; i++) { + beginUntrackFrame(); + } + + beginTrackFrame(); + + resetTracking(); + + assert.notOk(isTracking()); + }); + test('isTracking works within a track', (assert) => { assert.notOk(isTracking()); From 8bb3a5b481f9a4288d433f3b2df12dfa29123b8f Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Mon, 5 Oct 2026 22:18:52 -0400 Subject: [PATCH 17/46] Make the functions of a TrackedValue on their first use Each TrackedValue made four arrow functions and one options object. `get`, `set`, `update` and `freeze` are now accessors that make the bound function on its first read, and keep it. Each one has a setter, so code can still assign to them. The default options are one frozen object. Split out of #21650. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../@glimmer/validator/lib/tracked-value.ts | 99 ++++++++++++++----- .../validator/test/tracked-value-test.ts | 37 +++++++ 2 files changed, 113 insertions(+), 23 deletions(-) diff --git a/packages/@glimmer/validator/lib/tracked-value.ts b/packages/@glimmer/validator/lib/tracked-value.ts index b08c964c6c7..87e8718cdb8 100644 --- a/packages/@glimmer/validator/lib/tracked-value.ts +++ b/packages/@glimmer/validator/lib/tracked-value.ts @@ -28,6 +28,20 @@ export class TrackedValue implements Reactive { readonly #options: ReactiveOptions; readonly #tag: UpdatableTag; + /** + * `get`, `set`, `update` and `freeze` are bound to the instance, + * so that they can be detached from it. + * + * Each one is made the first time that it is read, + * so an instance that only uses `value` makes no function. + * + * Each one can be assigned, for example by a test that replaces it. + */ + #get: (() => Value) | undefined; + #set: ((value: Value) => boolean) | undefined; + #update: ((updater: (value: Value) => Value) => void) | undefined; + #freeze: (() => void) | undefined; + constructor(value: Value, options: ReactiveOptions) { this.#value = value; this.#options = options; @@ -48,15 +62,25 @@ export class TrackedValue implements Reactive { } set value(value: Value) { - this.set(value); + let set = this.#set; + + if (set === undefined) { + this.#write(value); + } else { + set(value); + } } /** * Function short-hand for reading `value`. */ - get = (): Value => { - return this.value; - }; + get get(): () => Value { + return (this.#get ??= () => this.value); + } + + set get(fn: () => Value) { + this.#get = fn; + } /** * Function short-hand for assigning `value`. @@ -64,7 +88,42 @@ export class TrackedValue implements Reactive { * Returns `true` if the value changed (and consumers were notified), * `false` if the new value was equal to the current one. */ - set = (value: Value): boolean => { + get set(): (value: Value) => boolean { + return (this.#set ??= (value) => this.#write(value)); + } + + set set(fn: (value: Value) => boolean) { + this.#set = fn; + } + + /** + * Update the value based on the current value, without consuming it. + */ + get update(): (updater: (value: Value) => Value) => void { + return (this.#update ??= (updater) => { + this.#write(updater(this.#value)); + }); + } + + set update(fn: (updater: (value: Value) => Value) => void) { + this.#update = fn; + } + + /** + * Prevents further updates, making the TrackedValue behave as a + * ReadOnlyReactive. + */ + get freeze(): () => void { + return (this.#freeze ??= () => { + this.#isFrozen = true; + }); + } + + set freeze(fn: () => void) { + this.#freeze = fn; + } + + #write(value: Value): boolean { if (this.#isFrozen) { throw new Error( `Cannot update a frozen TrackedValue${ @@ -82,30 +141,24 @@ export class TrackedValue implements Reactive { DIRTY_TAG(this.#tag); return true; - }; - - /** - * Update the value based on the current value, without consuming it. - */ - update = (updater: (value: Value) => Value): void => { - this.set(updater(this.#value)); - }; - - /** - * Prevents further updates, making the TrackedValue behave as a - * ReadOnlyReactive. - */ - freeze = (): void => { - this.#isFrozen = true; - }; + } } +const DEFAULT_OPTIONS: ReactiveOptions = Object.freeze({ + equals: Object.is, + description: undefined, +}); + export function trackedValue( value: Value, options?: { equals?: (a: Value, b: Value) => boolean; description?: string } ): TrackedValue { + if (options === undefined) { + return new TrackedValue(value, DEFAULT_OPTIONS); + } + return new TrackedValue(value, { - equals: options?.equals ?? Object.is, - description: options?.description, + equals: options.equals ?? Object.is, + description: options.description, }); } diff --git a/packages/@glimmer/validator/test/tracked-value-test.ts b/packages/@glimmer/validator/test/tracked-value-test.ts index abcd6c6b63e..833f6b59cfa 100644 --- a/packages/@glimmer/validator/test/tracked-value-test.ts +++ b/packages/@glimmer/validator/test/tracked-value-test.ts @@ -101,6 +101,43 @@ module('@glimmer/validator: trackedValue()', () => { assert.true(validateTag(tag, snapshot), 'update() did not entangle with the value'); }); + test('each method is the same function on each read', (assert) => { + const x = trackedValue(0); + + assert.strictEqual(x.get, x.get); + assert.strictEqual(x.set, x.set); + assert.strictEqual(x.update, x.update); + assert.strictEqual(x.freeze, x.freeze); + }); + + test('each method can be replaced', (assert) => { + const x = trackedValue(0); + const calls: number[] = []; + + x.set = (value) => { + calls.push(value); + + return false; + }; + + assert.false(x.set(1)); + + x.value = 2; + + assert.deepEqual(calls, [1, 2], 'a write through `value` uses the replacement'); + assert.strictEqual(x.value, 0, 'the replacement did not write the value'); + }); + + test('a detached freeze() prevents updates', (assert) => { + const x = trackedValue(0); + const { freeze, set } = x; + + freeze(); + + assert.throws(() => set(1), /frozen/u); + assert.throws(() => (x.value = 1), /frozen/u); + }); + test('methods can be detached from the instance', (assert) => { const x = trackedValue(0); const { get, set, update } = x; From b76e9ad795a62ed003b1f937e23fcac74393fa8a Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Mon, 5 Oct 2026 22:18:52 -0400 Subject: [PATCH 18/46] Make the value field of a TrackedValue general from the first instance All instances have one hidden class, and V8 records which kind of value the `#value` field held so far. If no instance held a number yet, the first number makes V8 throw away the optimized code of each function that reads a TrackedValue. The constructor now stores a number before the value, so the field is general from the first instance. Split out of #21650. Co-Authored-By: Claude Opus 5.5 (1M context) --- packages/@glimmer/validator/lib/tracked-value.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/packages/@glimmer/validator/lib/tracked-value.ts b/packages/@glimmer/validator/lib/tracked-value.ts index b08c964c6c7..52e2124e7e0 100644 --- a/packages/@glimmer/validator/lib/tracked-value.ts +++ b/packages/@glimmer/validator/lib/tracked-value.ts @@ -29,6 +29,17 @@ export class TrackedValue implements Reactive { readonly #tag: UpdatableTag; constructor(value: Value, options: ReactiveOptions) { + /** + * All instances have one hidden class, + * and V8 records which kind of value `#value` held so far. + * + * The first value of another kind makes V8 throw away + * the optimized code that reads the field. + * + * The field starts as `undefined`, + * so a number here makes it general from the first instance. + */ + this.#value = 0 as Value; this.#value = value; this.#options = options; this.#tag = createUpdatableTag(); From bc6dc98d893bfd8ae53af846a955a0029fa3e935 Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Mon, 5 Oct 2026 22:18:52 -0400 Subject: [PATCH 19/46] Use an index in the loop over the subtags of a combined tag The `for...of` loop made one iterator for each computation of a combined tag. The loop keeps `Math.max`. A `>` comparison ignores the NaN revision of the volatile tag. Split out of #21650. Co-Authored-By: Claude Opus 5.5 (1M context) --- packages/@glimmer/validator/lib/validators.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/@glimmer/validator/lib/validators.ts b/packages/@glimmer/validator/lib/validators.ts index 66c7f1ac1cb..0bd9cf1a7ce 100644 --- a/packages/@glimmer/validator/lib/validators.ts +++ b/packages/@glimmer/validator/lib/validators.ts @@ -138,9 +138,8 @@ class MonomorphicTagImpl { if (subtag !== null) { if (Array.isArray(subtag)) { - for (const tag of subtag) { - let value = tag[COMPUTE](); - revision = Math.max(value, revision); + for (let i = 0; i < subtag.length; i++) { + revision = Math.max((subtag[i] as Tag)[COMPUTE](), revision); } } else { let subtagValue = subtag[COMPUTE](); From d7f60e2cb4955d6c8be57671e1f7d75b04ec4091 Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Mon, 5 Oct 2026 23:35:26 -0400 Subject: [PATCH 20/46] Explain the tracker pool with diagrams The comment blocks now show the tags array of a tracker, how `slot` finds a tag that the frame has already, the nested frame that gives a duplicate, and which tracker of the pool each frame depth uses. Comments only. Co-Authored-By: Claude Opus 5.5 (1M context) --- packages/@glimmer/validator/lib/tracking.ts | 104 +++++++++++++++--- packages/@glimmer/validator/lib/validators.ts | 16 ++- 2 files changed, 102 insertions(+), 18 deletions(-) diff --git a/packages/@glimmer/validator/lib/tracking.ts b/packages/@glimmer/validator/lib/tracking.ts index 4596f340f5d..1ae354a32c6 100644 --- a/packages/@glimmer/validator/lib/tracking.ts +++ b/packages/@glimmer/validator/lib/tracking.ts @@ -8,8 +8,10 @@ import { unwrap } from './utils'; import { combine, CONSTANT_TAG, isConstTag, validateTag, valueForTag } from './validators'; /** - * A tag of this package. Each tag class declares `slot`: - * the index of the tag in the tracker that took it last. + * A tag with the `slot` field that `Tracker#add` reads and writes. + * + * Each tag class of this package declares the field, + * so a tag has it from its construction. */ interface ConsumedTag extends Tag { slot: number; @@ -18,28 +20,61 @@ interface ConsumedTag extends Tag { /** * An object that tracks @tracked properties that were consumed. * - * Trackers are pooled by frame depth (see `beginTrackFrame`), - * so a tracker holds no tag after its frame ends. + * A tracker collects the tags that one tracking frame consumes. + * When the frame ends, `combine()` makes one tag from them. + * + * A tracker is not made for each frame. + * `beginTrackFrame` takes it from `TRACKER_POOL`, so the next frame + * at the same depth uses the same tracker again. */ class Tracker { /** - * The consumed tags. `size` counts the live entries. + * The tags that the current frame consumed. + * Each tag is at the position of its first consumption. + * `size` is the number of tags. + * + * tags: [ a, b, c, null, null ] + * size: 3 * - * The array never shrinks, because a write to `length` is a slow path in V8, - * and this code runs for every frame. + * The entries from `size` up are `null`. + * They are left from an earlier frame that consumed more tags. + * + * The array never shrinks. + * A write to `length` is a slow path in V8, and this code runs for every frame. */ private tags: (Tag | null)[] = []; private size = 0; /** - * A tag keeps the index at which a tracker took it last. - * If this tracker has the tag at that index, the frame consumed the tag before, - * so a tag that the frame consumes again costs one comparison. + * Adds a tag to the frame, one time. + * + * A frame can consume one tag many times, + * so the tracker must find out if it has the tag already. + * It does that with no `Set`: the tag keeps the index + * at which a tracker put it last, in `tag.slot`. + * + * consume a tags: [ a ] a.slot = 0 + * consume b tags: [ a, b ] b.slot = 1 + * consume a tags[a.slot] is a, so the frame has it. No change. * - * The index can come from another tracker. - * The entry at that index is then another tag or no tag, and this tracker takes the tag. - * So a tag that a nested frame takes at another index, between two consumptions - * of this frame, is taken two times. A combined tag with a duplicate has the same revision. + * The check is `tags[tag.slot] === tag`. + * It compares the entry with the tag, so a `slot` that is out of date + * cannot hide a tag. The worst case is a tag that is in the array two times. + * + * That case needs a nested frame. + * The nested frame has its own tracker, and that tracker also writes `slot`: + * + * 1. outer frame consumes a, b outer tags: [ a, b ] b.slot = 1 + * 2. inner frame consumes b inner tags: [ b ] b.slot = 0 + * 3. outer frame consumes b outer tags[0] is a, not b. + * outer tags: [ a, b, b ] b.slot = 2 + * + * The duplicate does no harm. + * The revision of a combined tag is the highest revision of its tags, + * and a tag that is there two times does not change the highest. + * + * If the inner frame puts the tag at the index that the outer frame used, + * the check of the outer frame still passes, and there is no duplicate. */ add(tag: Tag) { if (tag === CONSTANT_TAG) return; @@ -74,6 +109,18 @@ class Tracker { return result; } + /** + * Empties the tracker for the next frame at the same depth. + * + * before: tags: [ a, b, c ] size: 3 + * after: tags: [ null, null, null ] size: 0 + * + * The entries must be `null`, and not only ignored: + * + * - an entry that stays is a match for `tags[tag.slot] === tag`, + * so the next frame would not add that tag + * - an entry that stays keeps its tag alive after the frame + */ clear(): void { let { tags, size } = this; @@ -103,10 +150,27 @@ let CURRENT_TRACKER: Tracker | null = null; const OPEN_TRACK_FRAMES: (Tracker | null)[] = []; /** - * Frames are strictly nested, so the tracker of a frame at depth `n` is free - * when that frame ends. One tracker for each depth is enough. + * The trackers, by the depth of the frame that uses them. + * The depth of a frame is the number of frames that are open around it. + * + * Frames are strictly nested: a frame ends before the frame around it ends. + * So two frames at the same depth are never open at the same time, + * and one tracker for each depth is enough. + * + * frame A depth 0 TRACKER_POOL[0] + * |- frame B depth 1 TRACKER_POOL[1] + * | `- frame C depth 2 TRACKER_POOL[2] + * `- frame D depth 1 TRACKER_POOL[1], which B used before + * + * A tracker is made the first time that a frame opens at its depth. + * After that, a frame at that depth allocates no tracker. * - * An untrack frame takes a depth but no tracker, so the pool can have holes. + * An untrack frame takes a depth but no tracker, so the pool can have holes: + * + * untrack frame depth 0 no tracker + * `- frame E depth 1 TRACKER_POOL[1] + * + * TRACKER_POOL: [ , tracker ] */ const TRACKER_POOL: (Tracker | undefined)[] = []; @@ -163,6 +227,12 @@ export function resetTracking(): string | void { OPEN_TRACK_FRAMES.pop(); } + /** + * A frame that did not end left its tags in its tracker. + * The next frame at that depth must start with no tag. + * + * The pool can have holes, see `TRACKER_POOL`. + */ for (let tracker of TRACKER_POOL) { if (tracker !== undefined) { tracker.clear(); diff --git a/packages/@glimmer/validator/lib/validators.ts b/packages/@glimmer/validator/lib/validators.ts index 7de985abf97..01fafbfd19f 100644 --- a/packages/@glimmer/validator/lib/validators.ts +++ b/packages/@glimmer/validator/lib/validators.ts @@ -115,7 +115,11 @@ class MonomorphicTagImpl { private subtagBufferCache: Revision | null = null; /** - * The index of this tag in the tracker that took it last. + * The index at which a tracker put this tag last. + * `Tracker#add` uses it to find out if a tracking frame has the tag already. + * + * The start value 0 is safe: + * the tracker compares its entry at that index with the tag. */ public slot = 0; @@ -259,7 +263,12 @@ const VOLATILE_TAG_ID: IVOLATILE_TAG_ID = 100; export class VolatileTag implements Tag { readonly [TYPE] = VOLATILE_TAG_ID; + + /** + * See `slot` of `MonomorphicTagImpl`. + */ slot = 0; + [COMPUTE](): Revision { return VOLATILE; } @@ -273,7 +282,12 @@ const CURRENT_TAG_ID: ICURRENT_TAG_ID = 101; export class CurrentTag implements Tag { readonly [TYPE] = CURRENT_TAG_ID; + + /** + * See `slot` of `MonomorphicTagImpl`. + */ slot = 0; + [COMPUTE](): Revision { return $REVISION; } From 379a7ca59f8a432c06efbd6da8af038dc4a23874 Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Sat, 3 Oct 2026 16:51:03 -0400 Subject: [PATCH 21/46] Wait with renderSettled in the renderComponent tests, not run() The renderComponent tests wrapped each render, update and destroy in `run(() => ...)`. The tests now call the API directly and wait with `await renderSettled()`, as an application does. - `renderComponent()` renders synchronously. The test helper asserts the HTML immediately after the call, with no wait. - Each tracked update and each destroy is followed by `await renderSettled()`. - `AbstractStrictTestCase` no longer uses `runTask` and `runDestroy`. Only this test file extends that class. No test was added, removed or renamed. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../components/render-component-test.ts | 379 +++++++++--------- .../lib/test-cases/abstract.ts | 15 +- 2 files changed, 207 insertions(+), 187 deletions(-) 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 0ffbd17155b..95679af45be 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'; @@ -20,9 +19,8 @@ import templateOnly from '@ember/component/template-only'; import { array, concat, fn, get, hash, on } from '@glimmer/runtime'; import GlimmerishComponent from '../../utils/glimmerish-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'; @@ -47,35 +45,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); @@ -83,62 +80,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( @@ -154,10 +156,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
    '); @@ -165,17 +167,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(); } } ); @@ -199,62 +201,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(); } } ); @@ -262,75 +261,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( @@ -338,19 +339,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; } @@ -364,15 +365,15 @@ 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(''), @@ -381,10 +382,10 @@ moduleFor( } ); - 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}}'), @@ -393,10 +394,10 @@ moduleFor( } ); - 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}}'), @@ -405,10 +406,10 @@ moduleFor( } ); - 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( @@ -419,10 +420,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( @@ -433,22 +434,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; } @@ -456,15 +457,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 }); @@ -480,10 +481,10 @@ moduleFor( templateOnly() ); - this.renderComponent(Root, { expect: '
    hi there
    ' }); + await this.renderComponent(Root, { expect: '
    hi there
    ' }); } - '@test destroying the result releases its root and renderer'(assert: QUnit['assert']) { + async '@test destroying the result releases its root and renderer'(assert: QUnit['assert']) { let render = defineSimpleModifier((element, [comp]) => { let result = renderComponent(comp, { into: element }); @@ -505,14 +506,16 @@ moduleFor( templateOnly() ); - this.renderComponent(Root, { expect: '
    hi there
    ' }); + await this.renderComponent(Root, { expect: '
    hi there
    ' }); let baseline = renderers.length; for (let i = 0; i < 5; i++) { - run(() => (state.show = false)); + state.show = false; + await renderSettled(); assertHTML(''); - run(() => (state.show = true)); + state.show = true; + await renderSettled(); assertHTML('
    hi there
    '); } @@ -523,32 +526,37 @@ moduleFor( ); } - '@test destroying the result removes its root from a shared renderer'(assert: QUnit['assert']) { + 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 = run(() => renderComponent(Inner, { owner, into: element })); + 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 = run(() => renderComponent(Inner, { owner, into: other })); - run(() => result.destroy()); + let result = renderComponent(Inner, { owner, into: other }); + result.destroy(); + await renderSettled(); } assert.strictEqual(renderer.state.roots.length, 1, 'destroyed roots are not retained'); - run(() => first.destroy()); + first.destroy(); + await renderSettled(); assert.false(renderers.includes(renderer), 'renderer with no roots is deregistered'); - run(() => destroy(this)); + 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; @@ -569,9 +577,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
    `, }); @@ -581,7 +589,7 @@ 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(''); @@ -605,11 +613,11 @@ 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'; @@ -633,15 +641,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( @@ -682,10 +690,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(); }, @@ -707,14 +715,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()); @@ -748,15 +755,17 @@ 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; } @@ -800,17 +809,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"}}`, { @@ -843,18 +854,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"}}`, { @@ -883,10 +895,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(''); } @@ -894,7 +907,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 }); @@ -937,7 +950,7 @@ moduleFor( templateOnly() ); - this.renderComponent(Root, { + await this.renderComponent(Root, { expect: [ `
    `, `b:Hi: THERE:-1`, @@ -948,7 +961,7 @@ moduleFor( ].join('\n'), }); - this.assertChange({ + await this.assertChange({ change: () => dataA.count++, expect: [ `
    `, @@ -964,7 +977,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: [ `
    `, @@ -976,7 +989,8 @@ moduleFor( ].join('\n'), }); - run(() => destroy(this)); + destroy(this); + await renderSettled(); assertHTML(''); } @@ -1018,7 +1032,7 @@ moduleFor( templateOnly() ); - this.renderComponent(Root, { + await this.renderComponent(Root, { expect: [`

    `, `
    `, ``, ``].join('\n'), }); @@ -1026,7 +1040,8 @@ moduleFor( assertHTML([`
    a:Hi

    `, `
    b:Hi
    `, ``, ``].join('\n')); - run(() => destroy(this)); + destroy(this); + await renderSettled(); assertHTML(''); } @@ -1036,7 +1051,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"}}`), @@ -1047,9 +1062,10 @@ moduleFor( templateOnly() ); - this.renderComponent(Root, { expect: 'Hi: THERE' }); + await this.renderComponent(Root, { expect: 'Hi: THERE' }); - run(() => destroy(this)); + destroy(this); + await renderSettled(); assertHTML(''); } @@ -1065,7 +1081,7 @@ 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"}}`), @@ -1095,13 +1111,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(''); } @@ -1111,13 +1128,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: { @@ -1128,13 +1145,13 @@ moduleFor( }); } - '@test Can use Textarea'() { + async '@test Can use Textarea'() { let Root = setComponentTemplate( precompileTemplate('