diff --git a/packages/@ember/-internals/glimmer/tests/integration/content-test.js b/packages/@ember/-internals/glimmer/tests/integration/content-test.js index 4eeeea40536..f2645b9f0e5 100644 --- a/packages/@ember/-internals/glimmer/tests/integration/content-test.js +++ b/packages/@ember/-internals/glimmer/tests/integration/content-test.js @@ -1802,3 +1802,50 @@ if (DEBUG) { } ); } + +// The bug this documents is the backtracking assertion, which only exists in +// debug builds; in production the listener succeeds and the todo would pass. +if (DEBUG) { + moduleFor( + 'Dynamic content tests: disabling a focused element', + class extends RenderingTestCase { + async '@todo a {{on "blur"}} listener may update rendered state when the element becomes disabled (GH#19287)'( + assert + ) { + let error; + + this.render( + '{{this.isFocused}}', + { + isDisabled: false, + isFocused: false, + onFocus: () => set(this.context, 'isFocused', true), + onBlur: () => { + try { + set(this.context, 'isFocused', false); + } catch (e) { + error = e; + } + }, + } + ); + + let textarea = this.element.querySelector('textarea'); + + runTask(() => textarea.focus()); + assert.strictEqual(this.context.isFocused, true, 'precond - focus listener ran'); + + runTask(() => set(this.context, 'isDisabled', true)); + + await new Promise((resolve) => setTimeout(resolve, 50)); + + assert.strictEqual( + error, + undefined, + 'no backtracking assertion was thrown from the blur listener' + ); + assert.strictEqual(this.context.isFocused, false, 'blur listener updated the state'); + } + } + ); +} 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 182d2322102..c9c12f8aa92 100644 --- a/packages/@ember/-internals/glimmer/tests/integration/modifiers/on-test.js +++ b/packages/@ember/-internals/glimmer/tests/integration/modifiers/on-test.js @@ -1,4 +1,5 @@ import { moduleFor, RenderingTestCase, runTask } from 'internal-test-helpers'; +import { setTesting } from '@ember/debug'; import { getInternalModifierManager, setComponentTemplate } from '@glimmer/manager'; import { on } from '@glimmer/runtime'; import { precompileTemplate } from '@ember/template-compilation'; @@ -296,6 +297,53 @@ moduleFor( this.assertStableRerender(); this.assertCounts({ adds: 2, removes: 0 }); } + + async '@todo all listeners for an event run even when an earlier one removes the element (GH#19344)'( + assert + ) { + let sequence = []; + + this.render( + '{{#if this.showBox}}
{{/if}}', + { + showBox: true, + first: () => { + sequence.push('first'); + this.context.set('showBox', false); + }, + second: () => sequence.push('second'), + } + ); + + let box = this.element.querySelector('#scrollbox'); + + // Testing mode disables the autorun that makes this reproducible in + // apps: with autorun enabled, the flush is scheduled as a microtask, + // and browser-dispatched events (unlike synthetic el.click()) run a + // microtask checkpoint between listener callbacks. The flush tears + // down the element's modifiers mid-dispatch, and the removed second + // listener is never invoked. + setTesting(false); + try { + box.scrollTop = 30; + + await new Promise((resolve) => { + let attempts = 0; + (function check() { + if (sequence.length >= 2 || ++attempts > 50) { + resolve(); + } else { + setTimeout(check, 10); + } + })(); + }); + } finally { + setTesting(true); + } + + assert.strictEqual(this.element.querySelector('#scrollbox'), null, 'the element was removed'); + assert.deepEqual(sequence, ['first', 'second'], 'both listeners ran, in order'); + } } ); diff --git a/packages/internal-test-helpers/lib/module-for.ts b/packages/internal-test-helpers/lib/module-for.ts index b091ef50b98..e6f3ab1b076 100644 --- a/packages/internal-test-helpers/lib/module-for.ts +++ b/packages/internal-test-helpers/lib/module-for.ts @@ -163,6 +163,10 @@ export function setupTestClass( QUnit.skip(name.slice(5), function (this: TestContext, assert) { return (this.instance![name] as any)(assert); }); + } else if (name.indexOf('@todo ') === 0) { + QUnit.todo(name.slice(5), function (this: TestContext, assert) { + return (this.instance![name] as any)(assert); + }); } else { let match = /^@feature\(([A-Z_a-z-! ,]+)\) /.exec(name);