This repository has been archived on 2023-03-18. You can view files and clone it, but cannot push or open issues or pull requests.
osr-discourse-src/app/assets/javascripts/discourse/tests/unit/lib/ember-events-test.js
Dan Gebhardt 0221855ba7
DEV: Normalize event handling to improve Glimmer + Classic component compat (Take 2) (#18742)
Classic Ember components (i.e. "@ember/component") rely upon "event
delegation" to listen for events at the application root and then dispatch
those events to any event handlers defined on individual Classic components.
This coordination is handled by Ember's EventDispatcher.

In contrast, Glimmer components (i.e. "@glimmer/component") expect event
listeners to be added to elements using modifiers (such as `{{on "click"}}`).
These event listeners are added directly to DOM elements using
`addEventListener`. There is no need for an event dispatcher.

Issues may arise when using Classic and Glimmer components together, since it
requires reconciling the two event handling approaches. For instance, event
propagation may not work as expected when a Classic component is nested
inside a Glimmer component.

`normalizeEmberEventHandling` helps an application standardize upon the
Glimmer event handling approach by eliminating usage of event delegation and
instead rewiring Classic components to directly use `addEventListener`.

Specifically, it performs the following:

- Invokes `eliminateClassicEventDelegation()` to remove all events associated
  with Ember's EventDispatcher to reduce its runtime overhead and ensure that
  it is effectively not in use.

- Invokes `rewireClassicComponentEvents(app)` to rewire each Classic
  component to add its own event listeners for standard event handlers (e.g.
  `click`, `mouseDown`, `submit`, etc.).

- Configures an instance initializer that invokes
  `rewireActionModifier(appInstance)` to redefine the `action` modifier with
    a substitute that uses `addEventListener`.

Additional changes include:
* d-button: only preventDefault / stopPropagation for handled actions
   This allows unhandled events to propagate as expected.
* d-editor: avoid adding duplicate event listener for tests
   This extra event listener causes duplicate paste events in tests.
* group-manage-email-settings: Monitor `input` instead of `change` event for checkboxes
2022-10-26 14:44:12 +01:00

301 lines
9.3 KiB
JavaScript

import { module, test } from "qunit";
import { setupRenderingTest } from "ember-qunit";
import { click, render } from "@ember/test-helpers";
import { hbs } from "ember-cli-htmlbars";
/* eslint-disable ember/require-tagless-components */
/* eslint-disable ember/no-classic-classes */
/* eslint-disable ember/no-classic-components */
import { default as ClassicComponent } from "@ember/component";
import { default as GlimmerComponent } from "@glimmer/component";
import { action } from "@ember/object";
// Configure test-local Classic and Glimmer components that
// will be immune from upgrades to actual Discourse components.
const ExampleClassicButton = ClassicComponent.extend({
tagName: "button",
type: "button",
preventEventPropagation: false,
onClick: null,
onMouseDown: null,
click(event) {
event.preventDefault();
if (this.preventEventPropagation) {
event.stopPropagation();
}
this.onClick?.(event);
},
});
const exampleClassicButtonTemplate = hbs`{{yield}}`;
class ExampleGlimmerButton extends GlimmerComponent {
@action
click(event) {
event.preventDefault();
if (this.args.preventEventPropagation) {
event.stopPropagation();
}
this.args.onClick?.(event);
}
}
const exampleGlimmerButtonTemplate = hbs`
<button {{on 'click' this.click}} type='button' ...attributes>
{{yield}}
</button>
`;
module("Unit | Lib | ember-events", function (hooks) {
setupRenderingTest(hooks);
hooks.beforeEach(function () {
this.owner.register(
"component:example-classic-button",
ExampleClassicButton
);
this.owner.register(
"template:components/example-classic-button",
exampleClassicButtonTemplate
);
this.owner.register(
"component:example-glimmer-button",
ExampleGlimmerButton
);
this.owner.register(
"template:components/example-glimmer-button",
exampleGlimmerButtonTemplate
);
});
module("classic component event configuration", function () {
test("it adds listeners for standard event handlers on the component prototype or the component itself", async function (assert) {
let i = 0;
this.setProperties({
onOneClick: () => this.set("oneClicked", i++),
onTwoClick: () => this.set("twoClicked", i++),
oneClicked: undefined,
twoClicked: undefined,
});
await render(hbs`
<ExampleClassicButton id="buttonOne" @onClick={{this.onOneClick}} />
<ExampleClassicButton id="buttonTwo" @click={{this.onTwoClick}} />
`);
await click("#buttonOne");
await click("#buttonTwo");
assert.strictEqual(this.oneClicked, 0);
assert.strictEqual(this.twoClicked, 1);
});
test("it adds listeners for standard event handlers on the component itself or the component prototype (order reversed)", async function (assert) {
let i = 0;
this.setProperties({
onOneClick: () => this.set("oneClicked", i++),
onTwoClick: () => this.set("twoClicked", i++),
oneClicked: undefined,
twoClicked: undefined,
});
await render(hbs`
<ExampleClassicButton id="buttonOne" @click={{this.onOneClick}} />
<ExampleClassicButton id="buttonTwo" @onClick={{this.onTwoClick}} />
`);
await click("#buttonOne");
await click("#buttonTwo");
assert.strictEqual(this.oneClicked, 0);
assert.strictEqual(this.twoClicked, 1);
});
});
module("nested glimmer inside classic", function () {
test("it handles click events and allows propagation by default", async function (assert) {
let i = 0;
this.setProperties({
onParentClick: () => this.set("parentClicked", i++),
onChildClick: () => this.set("childClicked", i++),
parentClicked: undefined,
childClicked: undefined,
});
await render(hbs`
<ExampleClassicButton id="parentButton" @onClick={{this.onParentClick}}>
<ExampleGlimmerButton id="childButton" @onClick={{this.onChildClick}} />
</ExampleClassicButton>
`);
await click("#childButton");
assert.strictEqual(this.childClicked, 0);
assert.strictEqual(this.parentClicked, 1);
});
test("it handles click events and can prevent event propagation", async function (assert) {
let i = 0;
this.setProperties({
onParentClick: () => this.set("parentClicked", i++),
onChildClick: () => this.set("childClicked", i++),
parentClicked: undefined,
childClicked: undefined,
});
await render(hbs`
<ExampleClassicButton id="parentButton" @onClick={{this.onParentClick}}>
<ExampleGlimmerButton id="childButton" @preventEventPropagation={{true}} @onClick={{this.onChildClick}} />
</ExampleClassicButton>
`);
await click("#childButton");
assert.strictEqual(this.childClicked, 0);
assert.strictEqual(this.parentClicked, undefined);
});
});
module("nested classic inside glimmer", function () {
test("it handles click events and allows propagation by default", async function (assert) {
let i = 0;
this.setProperties({
onParentClick: () => this.set("parentClicked", i++),
onChildClick: () => this.set("childClicked", i++),
parentClicked: undefined,
childClicked: undefined,
});
await render(hbs`
<ExampleGlimmerButton id="parentButton" @onClick={{this.onParentClick}}>
<ExampleClassicButton id="childButton" @onClick={{this.onChildClick}} />
</ExampleGlimmerButton>
`);
await click("#childButton");
assert.strictEqual(this.childClicked, 0);
assert.strictEqual(this.parentClicked, 1);
});
test("it handles click events and can prevent event propagation", async function (assert) {
let i = 0;
this.setProperties({
onParentClick: () => this.set("parentClicked", i++),
onChildClick: () => this.set("childClicked", i++),
parentClicked: undefined,
childClicked: undefined,
});
await render(hbs`
<ExampleGlimmerButton id="parentButton" @onClick={{this.onParentClick}}>
<ExampleClassicButton id="childButton" @preventEventPropagation={{true}} @onClick={{this.onChildClick}} />
</ExampleGlimmerButton>
`);
await click("#childButton");
assert.strictEqual(this.childClicked, 0);
assert.strictEqual(this.parentClicked, undefined);
});
});
module("nested `{{action}}` usage inside classic", function () {
test("it handles click events and allows propagation by default", async function (assert) {
let i = 0;
this.setProperties({
onParentClick: () => this.set("parentClicked", i++),
onChildClick: () => this.set("childClicked", i++),
parentClicked: undefined,
childClicked: undefined,
});
await render(hbs`
<ExampleClassicButton id="parentButton" @onClick={{this.onParentClick}}>
<button id="childButton" {{action this.onChildClick}} />
</ExampleClassicButton>
`);
await click("#childButton");
assert.strictEqual(this.childClicked, 0);
assert.strictEqual(this.parentClicked, 1);
});
test("it handles click events and can prevent event propagation", async function (assert) {
let i = 0;
this.setProperties({
onParentClick: () => this.set("parentClicked", i++),
onChildClick: () => this.set("childClicked", i++),
parentClicked: undefined,
childClicked: undefined,
});
await render(hbs`
<ExampleClassicButton id="parentButton" @onClick={{this.onParentClick}}>
<button id="childButton" {{action this.onChildClick bubbles=false}} />
</ExampleClassicButton>
`);
await click("#childButton");
assert.strictEqual(this.childClicked, 0);
assert.strictEqual(this.parentClicked, undefined);
});
});
module("nested `{{action}}` usage inside glimmer", function () {
test("it handles click events and allows propagation by default", async function (assert) {
let i = 0;
this.setProperties({
onParentClick: () => this.set("parentClicked", i++),
onChildClick: () => this.set("childClicked", i++),
parentClicked: undefined,
childClicked: undefined,
});
await render(hbs`
<ExampleGlimmerButton id="parentButton" @onClick={{this.onParentClick}}>
<button id="childButton" {{action this.onChildClick}} />
</ExampleGlimmerButton>
`);
await click("#childButton");
assert.strictEqual(this.childClicked, 0);
assert.strictEqual(this.parentClicked, 1);
});
test("it handles click events and can prevent event propagation", async function (assert) {
let i = 0;
this.setProperties({
onParentClick: () => this.set("parentClicked", i++),
onChildClick: () => this.set("childClicked", i++),
parentClicked: undefined,
childClicked: undefined,
});
await render(hbs`
<ExampleGlimmerButton id="parentButton" @onClick={{this.onParentClick}}>
<button id="childButton" {{action this.onChildClick bubbles=false}} />
</ExampleGlimmerButton>
`);
await click("#childButton");
assert.strictEqual(this.childClicked, 0);
assert.strictEqual(this.parentClicked, undefined);
});
});
});