diff --git a/.gitignore b/.gitignore index 156c78353e8..8c6d07e3109 100644 --- a/.gitignore +++ b/.gitignore @@ -28,6 +28,7 @@ test/tmp test/version_tmp test_*.html /tests/ember-tests.js +/smoke-tests/scenarios/output/ tmp tmp*.gem tmp.bpm diff --git a/packages/@ember/-internals/glimmer/lib/renderer.ts b/packages/@ember/-internals/glimmer/lib/renderer.ts index a1a177fd924..a1b65103013 100644 --- a/packages/@ember/-internals/glimmer/lib/renderer.ts +++ b/packages/@ember/-internals/glimmer/lib/renderer.ts @@ -227,6 +227,8 @@ class ClassicRootState { let result = (this.result = iterator.sync()); + associateDestroyableChild(owner, result); + // override .render function after initial render this.render = errorLoopTransaction(() => result.rerender({ alwaysRevalidate: false })); }); @@ -648,6 +650,10 @@ export function renderComponent( let innerResult = renderer.render(component, { into, args }).result; + if (innerResult) { + associateDestroyableChild(owner, innerResult); + } + let result = { destroy() { if (innerResult) { 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 1bac4bea6aa..abd3279d51f 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 @@ -18,7 +18,7 @@ import { array, concat, fn, get, hash, on } from '@glimmer/runtime'; import GlimmerishComponent from '../../utils/glimmerish-component'; import { run } from '@ember/runloop'; -import { associateDestroyableChild, registerDestructor } from '@glimmer/destroyable'; +import { destroy, associateDestroyableChild, registerDestructor } from '@glimmer/destroyable'; import { renderComponent, type RenderResult } from '../../../lib/renderer'; import { trackedObject } from '@ember/reactive/collections'; import { cached, tracked } from '@glimmer/tracking'; @@ -80,17 +80,126 @@ class RenderComponentTestCase extends AbstractStrictTestCase { } } +moduleFor( + 'Strict Mode - RenderComponentTestCase', + class extends RenderComponentTestCase { + afterEach() { + if (this.component) { + runDestroy(this); + } + } + + '@test destroy cleans up dom via destrying the test context'() { + let Foo = defComponent('Hello, world!'); + let Root = defComponent('', { scope: { Foo } }); + + this.renderComponent(Root, { expect: 'Hello, world!' }); + + run(() => destroy(this)); + + assertHTML(''); + } + + '@test destroy of the owner cleans up dom via destrying the test context'() { + let Foo = defComponent('Hello, world!'); + let Root = defComponent('', { scope: { Foo } }); + + this.renderComponent(Root, { expect: 'Hello, world!' }); + + run(() => destroy(this.owner)); + + assertHTML(''); + } + } +); + +moduleFor( + 'Strict Mode - renderComponent (direct)', + class extends AbstractStrictTestCase { + get element() { + return document.querySelector('#qunit-fixture')!; + } + + '@test manually calling destroy cleans up the DOM'() { + let Foo = defComponent('Hello, world!'); + + 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 + }, + }; + }); + + assertHTML('Hello, world!'); + this.assertStableRerender(); + + run(() => manualDestroy()); + + assertHTML(''); + this.assertStableRerender(); + + run(() => destroy(owner)); + } + + '@test destroying the owner cleans up the DOM'() { + let Foo = defComponent('Hello, world!'); + + let owner = buildOwner({}); + + run(() => { + let result = renderComponent(Foo, { + owner, + into: this.element, + }); + this.component = { + ...result, + rerender() { + // unused, but asserted against + }, + }; + }); + + assertHTML('Hello, world!'); + this.assertStableRerender(); + + run(() => destroy(owner)); + + assertHTML(''); + this.assertStableRerender(); + } + } +); + moduleFor( 'Strict Mode - renderComponent', class extends RenderComponentTestCase { afterEach() { if (this.component) { - // runDestroy(this.component); - // runDestroy(this.owner); runDestroy(this); } } + '@test destroy cleans up dom via destroying the owner'() { + let Foo = defComponent('Hello, world!'); + let Root = defComponent('', { scope: { Foo } }); + + this.renderComponent(Root, { expect: 'Hello, world!' }); + + run(() => destroy(this.owner)); + + assertHTML(''); + } + '@test Can use a component in scope'() { let Foo = defComponent('Hello, world!'); let Root = defComponent('', { scope: { Foo } }); @@ -133,6 +242,24 @@ moduleFor( this.renderComponent(Root, { expect: 'foobar' }); } + '@test multiple components have independent lifetimes'() { + class State { + @tracked showSecond = true; + } + let state = new State(); + let Foo = defComponent('Hello, world!'); + let Root = defComponent('{{#if state.showSecond}}{{/if}}', { + scope: { state, Foo }, + }); + + this.renderComponent(Root, { expect: 'Hello, world!Hello, world!' }); + + this.assertChange({ + change: () => (state.showSecond = false), + expect: 'Hello, world!', + }); + } + '@test Can use a dynamic component definition'() { let Foo = defComponent('Hello, world!'); let Root = defComponent('', { diff --git a/smoke-tests/scenarios/basic-test.ts b/smoke-tests/scenarios/basic-test.ts index eecf94d563a..eee87411303 100644 --- a/smoke-tests/scenarios/basic-test.ts +++ b/smoke-tests/scenarios/basic-test.ts @@ -106,6 +106,84 @@ function basicTest(scenarios: Scenarios, appName: string) { `, }, integration: { + 'destruction-test.gjs': ` + import { module, test } from 'qunit'; + import { clearRender, render } from '@ember/test-helpers'; + import { setupRenderingTest } from 'ember-qunit'; + import { destroy, registerDestructor } from '@ember/destroyable'; + + import Component from '@glimmer/component'; + + class WillDestroy extends Component { + willDestroy() { + super.willDestroy(); + this.args.onDestroy(); + } + } + + class Destructor extends Component { + constructor(...args) { + super(...args); + + let onDestroy = this.args.onDestroy; + registerDestructor(this, () => onDestroy()); + } + } + + module('@glimmer/component Destruction', function (hooks) { + setupRenderingTest(hooks); + + module('after', function (hooks) { + hooks.after(function (assert) { + assert.verifySteps(['WillDestroy destroyed']); + }); + + test('it calls "@onDestroy"', async function (assert) { + const onDestroy = () => assert.step('WillDestroy destroyed'); + + await render( + + ); + }); + }); + + module('afterEach', function (hooks) { + hooks.afterEach(function (assert) { + assert.verifySteps(['WillDestroy destroyed']); + }); + + test('it calls "@onDestroy"', async function (assert) { + const onDestroy = () => assert.step('WillDestroy destroyed'); + + await render( + + ); + + destroy(this.owner); + }); + }); + + test('it calls "@onDestroy"', async function (assert) { + const onDestroy = () => assert.step('destroyed'); + + await render(); + + await clearRender(); + + assert.verifySteps(['destroyed']); + }); + + test('it calls "registerDestructor"', async function (assert) { + const onDestroy = () => assert.step('destroyed'); + + await render(); + + await clearRender(); + + assert.verifySteps(['destroyed']); + }); + }); + `, 'interactive-example-test.js': ` import { module, test } from 'qunit'; import { setupRenderingTest } from 'ember-qunit';