import { module, test } from "qunit"; import { exists, query } from "discourse/tests/helpers/qunit-helpers"; import { setupRenderingTest } from "discourse/tests/helpers/component-test"; import { click, fillIn, render } from "@ember/test-helpers"; import { hbs } from "ember-cli-htmlbars"; import widgetHbs from "discourse/widgets/hbs-compiler"; import Widget from "discourse/widgets/widget"; import ClassicComponent from "@ember/component"; import RenderGlimmer from "discourse/widgets/render-glimmer"; import { bind } from "discourse-common/utils/decorators"; class DemoWidget extends Widget { static actionTriggered = false; tagName = "div.my-widget"; buildKey() { return "abc"; } defaultState() { return { actionTriggered: false, }; } html(attrs, state) { return [ this.attach("button", { label: "rerender", className: "triggerRerender", action: "dummyAction", }), new RenderGlimmer( this, "div.glimmer-wrapper", hbs`
arg1={{@data.arg1}} dynamicArg={{@data.dynamicArg}}
`, { ...attrs, actionForComponentToTrigger: this.actionForComponentToTrigger, widgetActionTriggered: state.actionTriggered, } ), ]; } dummyAction() {} @bind actionForComponentToTrigger() { this.state.actionTriggered = true; DemoWidget.actionTriggered = true; this.scheduleRerender(); } } class DemoComponent extends ClassicComponent { static eventLog = []; classNames = ["demo-component"]; layout = hbs`

{{@widgetActionTriggered}}

`; init() { DemoComponent.eventLog.push("init"); super.init(...arguments); } didInsertElement() { DemoComponent.eventLog.push("didInsertElement"); } willDestroyElement() { DemoComponent.eventLog.push("willDestroyElement"); } didReceiveAttrs() { DemoComponent.eventLog.push("didReceiveAttrs"); } willDestroy() { DemoComponent.eventLog.push("willDestroy"); } } class ToggleDemoWidget extends Widget { static actionTriggered = false; tagName = "div.my-widget"; buildKey() { return "abc"; } defaultState() { return { showOne: true, }; } html(attrs, state) { const output = [ this.attach("button", { label: "toggle", className: "toggleButton", action: "toggleComponent", }), ]; if (state.showOne) { output.push(new RenderGlimmer(this, "div.glimmer-wrapper", hbs`One`, {})); } else { output.push(new RenderGlimmer(this, "div.glimmer-wrapper", hbs`Two`, {})); } return output; } toggleComponent() { this.state.showOne = !this.state.showOne; } } module("Integration | Component | Widget | render-glimmer", function (hooks) { setupRenderingTest(hooks); hooks.beforeEach(function () { DemoComponent.eventLog = []; DemoWidget.actionTriggered = false; this.registry.register("widget:demo-widget", DemoWidget); this.registry.register("widget:toggle-demo-widget", ToggleDemoWidget); this.registry.register("component:demo-component", DemoComponent); }); hooks.afterEach(function () { this.registry.unregister("widget:demo-widget"); this.registry.unregister("widget:toggle-demo-widget"); this.registry.unregister("component:demo-component"); }); test("argument handling", async function (assert) { await render( hbs` ` ); assert.true(exists("div.my-widget"), "widget is rendered"); assert.true(exists("div.glimmer-content"), "glimmer content is rendered"); assert.strictEqual( query("div.glimmer-content").innerText, "arg1=val1 dynamicArg=", "arguments are passed through" ); await fillIn("input.dynamic-value-input", "somedynamicvalue"); assert.strictEqual( query("div.glimmer-content").innerText, "arg1=val1 dynamicArg=", "changed arguments do not change before rerender" ); await click(".my-widget button"); assert.strictEqual( query("div.glimmer-content").innerText, "arg1=val1 dynamicArg=somedynamicvalue", "changed arguments are applied after rerender" ); }); test("child component lifecycle", async function (assert) { assert.deepEqual( DemoComponent.eventLog, [], "component event log starts empty" ); await render( hbs` {{#unless (eq this.dynamicValue 'hidden')}} {{/unless}}` ); assert.true(exists("div.my-widget"), "widget is rendered"); assert.true(exists("div.glimmer-content"), "glimmer content is rendered"); assert.true(exists("div.demo-component"), "demo component is rendered"); assert.deepEqual( DemoComponent.eventLog, ["init", "didReceiveAttrs", "didInsertElement"], "component is initialized correctly" ); DemoComponent.eventLog = []; await fillIn("input.dynamic-value-input", "somedynamicvalue"); assert.deepEqual( DemoComponent.eventLog, [], "component is not notified of attr change before widget rerender" ); await click(".my-widget button"); assert.deepEqual( DemoComponent.eventLog, ["didReceiveAttrs"], "component is notified of attr change during widget rerender" ); DemoComponent.eventLog = []; await fillIn("input.dynamic-value-input", "hidden"); assert.deepEqual( DemoComponent.eventLog, ["willDestroyElement", "willDestroy"], "destroy hooks are run correctly" ); DemoComponent.eventLog = []; await fillIn("input.dynamic-value-input", "visibleAgain"); assert.deepEqual( DemoComponent.eventLog, ["init", "didReceiveAttrs", "didInsertElement"], "component can be reinitialized" ); }); test("trigger widget actions from component", async function (assert) { assert.false( DemoWidget.actionTriggered, "widget event has not been triggered yet" ); await render( hbs` {{#unless (eq this.dynamicValue 'hidden')}} {{/unless}}` ); assert.true( exists("div.demo-component button"), "component button is rendered" ); await click("div.demo-component button"); assert.true(DemoWidget.actionTriggered, "widget event is triggered"); }); test("modify widget state with component action", async function (assert) { await render( hbs`` ); assert.false( DemoWidget.actionTriggered, "widget event has not been triggered yet" ); assert.strictEqual( query(".action-state").innerText, "false", "eventTriggered is false in nested component" ); assert.true( exists("div.demo-component button"), "component button is rendered" ); await click("div.demo-component button"); assert.true(DemoWidget.actionTriggered, "widget event is triggered"); assert.strictEqual( query(".action-state").innerText, "true", "eventTriggered is true in nested component" ); }); test("developer ergonomics", function (assert) { assert.throws( () => { // eslint-disable-next-line no-new new RenderGlimmer(this, "div", ``); }, /`template` should be a template compiled via `ember-cli-htmlbars`/, "it raises a useful error when passed a string instead of a template" ); assert.throws( () => { // eslint-disable-next-line no-new new RenderGlimmer(this, "div", widgetHbs`{{using-the-wrong-compiler}}`); }, /`template` should be a template compiled via `ember-cli-htmlbars`/, "it raises a useful error when passed a widget-hbs-compiler template" ); // eslint-disable-next-line no-new new RenderGlimmer(this, "div", hbs``); assert.true(true, "it doesn't raise an error for correct params"); }); test("multiple adjacent components", async function (assert) { await render(hbs``); assert.strictEqual(query("div.glimmer-wrapper").innerText, "One"); await click(".toggleButton"); assert.strictEqual(query("div.glimmer-wrapper").innerText, "Two"); await click(".toggleButton"); assert.strictEqual(query("div.glimmer-wrapper").innerText, "One"); }); });