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';