From aedf4572f6673e1aa2e4e0263ed41c6e729b9a0c Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Tue, 6 Jan 2026 20:46:14 -0500 Subject: [PATCH 1/2] Copy tests from ember-element-helper --- index.html | 4 + tests/browser/helpers/element-test.js | 393 ++++++++++++++++++++++++++ 2 files changed, 397 insertions(+) create mode 100644 tests/browser/helpers/element-test.js diff --git a/index.html b/index.html index c6988aa7d69..08360c3aa0e 100644 --- a/index.html +++ b/index.html @@ -80,6 +80,10 @@ import.meta.glob('./packages/{@glimmer,@glimmer-workspace}/*/test/**/*-test.{js,ts}', { eager: true, }); + + // High-level feature tests using regular testing apis, + // similar to how tests would be in real apps + import.meta.glob('./tests/browser/**/*.{js,ts,gjs,gts}', { eager: true });
diff --git a/tests/browser/helpers/element-test.js b/tests/browser/helpers/element-test.js new file mode 100644 index 00000000000..2166584f612 --- /dev/null +++ b/tests/browser/helpers/element-test.js @@ -0,0 +1,393 @@ +/* eslint-disable disable-features/disable-async-await */ +import { module, test } from 'qunit'; +import { hbs } from 'ember-cli-htmlbars'; +import { setupRenderingTest } from 'ember-qunit'; +import { click, render, settled } from '@ember/test-helpers'; +import { helper } from '@ember/component/helper'; +import Ember from 'ember'; +import { macroCondition, dependencySatisfies } from '@embroider/macros'; + +module('Integration | Helper | element', function (hooks) { + let originalOnerror; + let expectEmberError; + let expectedEmberErrors; + + setupRenderingTest(hooks); + + hooks.beforeEach((assert) => { + originalOnerror = Ember.onerror; + + expectEmberError = function (expectation) { + let _onerror = Ember.onerror; + + expectedEmberErrors.push(expectation); + + Ember.onerror = function (error) { + assert.throws(() => { + throw error; + }, expectedEmberErrors.pop()); + Ember.onerror = _onerror; + }; + }; + + expectedEmberErrors = []; + }); + + hooks.afterEach((assert) => { + Ember.onerror = originalOnerror; + + expectedEmberErrors.forEach((expected) => { + assert.strictEqual(undefined, expected); + }); + }); + + test('it renders a tag with the given tag name', async function (assert) { + await render(hbs` + {{#let (element "h1") as |Tag|}} + hello world! + {{/let}} + `); + + assert.dom('h1#content').hasText('hello world!'); + }); + + test('it does not render any tags when passed an empty string', async function (assert) { + await render(hbs` + {{#let (element "") as |Tag|}} + hello world! + {{/let}} + `); + + assert.strictEqual(this.element.innerHTML.trim(), 'hello world!'); + }); + + test('it does not render anything when passed null', async function (assert) { + await render(hbs` + {{#let (element null) as |Tag|}} + hello world! + {{/let}} + `); + + assert.strictEqual(this.element.innerHTML.trim(), ''); + }); + + test('it does not render anything when passed undefined', async function (assert) { + await render(hbs` + {{#let (element undefined) as |Tag|}} + hello world! + {{/let}} + `); + + assert.strictEqual(this.element.innerHTML.trim(), ''); + }); + + test('it works with element modifiers', async function (assert) { + let clicked = 0; + + this.set('didClick', () => clicked++); + + // https://github.com/ember-cli/babel-plugin-htmlbars-inline-precompile/issues/103 + await render( + hbs( + '\ + {{#let (element "button") as |Tag|}}\ + hello world!\ + {{/let}}\ + ', + { insertRuntimeErrors: true } + ) + ); + + assert.dom('button#action').hasAttribute('type', 'button').hasText('hello world!'); + assert.strictEqual(clicked, 0, 'never clicked'); + + await click('button#action'); + + assert.strictEqual(clicked, 1, 'clicked once'); + + await click('button#action'); + + assert.strictEqual(clicked, 2, 'clicked twice'); + }); + + test('it can be rendered multiple times', async function (assert) { + await render(hbs` + {{#let (element "h1") as |Tag|}} + hello + world + !!!!! + {{/let}} + `); + + assert.dom('h1#content-1').hasText('hello'); + assert.dom('h1#content-2').hasText('world'); + assert.dom('h1#content-3').hasText('!!!!!'); + }); + + test('it can be passed to the component helper', async function (assert) { + await render(hbs` + {{#let (component (ensure-safe-component (element "h1"))) as |Tag|}} + hello + {{/let}} + + {{#let (element "h2") as |h2|}} + {{#let (ensure-safe-component h2) as |Tag|}} + world + {{/let}} + {{/let}} + + {{#let (element "h3") as |h3|}} + {{#component (ensure-safe-component h3) id="content-3"}}!!!!!{{/component}} + {{/let}} + `); + + assert.dom('h1#content-1').hasText('hello'); + assert.dom('h2#content-2').hasText('world'); + assert.dom('h3#content-3').hasText('!!!!!'); + }); + + test('it renders when the tag name changes', async function (assert) { + let count = 0; + + this.owner.register( + 'helper:counter', + helper(() => ++count) + ); + + this.set('tagName', 'h1'); + + await render(hbs` + {{#let (element this.tagName) as |Tag|}} + rendered {{counter}} time(s) + {{/let}} + `); + + assert.dom('h1#content').hasText('rendered 1 time(s)'); + assert.dom('h2#content').doesNotExist(); + assert.dom('h3#content').doesNotExist(); + + this.set('tagName', 'h2'); + + await settled(); + + assert.dom('h1#content').doesNotExist(); + assert.dom('h2#content').hasText('rendered 2 time(s)'); + assert.dom('h3#content').doesNotExist(); + + this.set('tagName', 'h2'); + + await settled(); + + assert.dom('h1#content').doesNotExist(); + assert.dom('h2#content').hasText('rendered 2 time(s)'); + assert.dom('h3#content').doesNotExist(); + + this.set('tagName', 'h3'); + + await settled(); + + assert.dom('h1#content').doesNotExist(); + assert.dom('h2#content').doesNotExist(); + assert.dom('h3#content').hasText('rendered 3 time(s)'); + + this.set('tagName', ''); + + await settled(); + + assert.dom('h1#content').doesNotExist(); + assert.dom('h2#content').doesNotExist(); + assert.dom('h3#content').doesNotExist(); + + assert.strictEqual(this.element.innerHTML.trim(), 'rendered 4 time(s)'); + + this.set('tagName', 'h1'); + + await settled(); + + assert.dom('h1#content').hasText('rendered 5 time(s)'); + assert.dom('h2#content').doesNotExist(); + assert.dom('h3#content').doesNotExist(); + }); + + test('it can be passed as argument and works with ...attributes', async function (assert) { + this.set('tagName', 'p'); + + await render(hbs` + Test + `); + + assert.dom('p#content').hasText('Test').hasClass('extra'); + + this.set('tagName', 'div'); + + await settled(); + + assert.dom('div#content').hasText('Test').hasClass('extra'); + + this.set('tagName', ''); + + await settled(); + + assert.strictEqual(this.element.innerText.trim(), 'Test'); + + this.set('tagName', 'p'); + + await settled(); + + assert.dom('p#content').hasText('Test').hasClass('extra'); + }); + + test.skip('it can be invoked inline', async function (assert) { + this.set('tagName', 'p'); + + await render(hbs`{{element this.tagName}}`); + + assert.dom('p').exists(); + + this.set('tagName', 'br'); + + await settled(); + + assert.dom('br').exists(); + + this.set('tagName', ''); + + assert.strictEqual(this.element.innerHTML.trim(), ''); + + this.set('tagName', 'p'); + + await settled(); + + assert.dom('p').exists(); + }); + + module('invalid usages', function () { + test('it requires at least one argument', async function () { + expectEmberError( + new Error('Assertion Failed: The `element` helper takes a single positional argument') + ); + + await render(hbs` +
+ {{#let (element) as |Tag|}} + hello world! + {{/let}} +
+ `); + }); + + test('it requires no more than one argument', async function () { + expectEmberError( + new Error('Assertion Failed: The `element` helper takes a single positional argument') + ); + + await render(hbs` +
+ {{#let (element "h1" "h2") as |Tag|}} + hello world! + {{/let}} +
+ `); + }); + + test('it does not take any named arguments', async function () { + expectEmberError( + new Error('Assertion Failed: The `element` helper does not take any named arguments') + ); + + await render(hbs` +
+ {{#let (element "h1" id="content") as |Tag|}} + hello world! + {{/let}} +
+ `); + }); + + test('it does not take a block', async function (assert) { + // Before the EMBER_GLIMMER_ANGLE_BRACKET_BUILT_INS feature was enabled + // in 3.10, the "dash rule" short-circuited this assertion by accident, + // so this was just a no-op but no error was thrown. + if (macroCondition(dependencySatisfies('ember-source', '>=3.25.0-beta.0'))) { + expectEmberError( + new Error( + 'Attempted to resolve `element`, which was expected to be a component, but nothing was found.' + ) + ); + } else if (macroCondition(dependencySatisfies('ember-source', '>=3.10.0-beta.0'))) { + expectEmberError( + new Error( + 'Assertion Failed: Helpers may not be used in the block form, for example {{#element}}{{/element}}. Please use a component, or alternatively use the helper in combination with a built-in Ember helper, for example {{#if (element)}}{{/if}}.' + ) + ); + } + + // Due to https://github.com/glimmerjs/glimmer-vm/pull/1073, we need to + // wrap the invalid block in a conditional to ensure the initial render + // complete without errors. This is fixed in Ember 3.16+. + this.set('showBlock', false); + + await render(hbs` +
+ {{#if this.showBlock}} + {{#element "h1"}}hello world!{{/element}} + {{/if}} +
+ `); + + assert.dom('h1').doesNotExist(); + + this.set('showBlock', true); + + await settled(); + + assert.dom('h1').doesNotExist(); + }); + + test('it throws when passed a number', async function () { + expectEmberError( + new Error( + 'Assertion Failed: The argument passed to the `element` helper must be a string (you passed `123`)' + ) + ); + + await render(hbs` +
+ {{#let (element 123) as |Tag|}} + hello world! + {{/let}} +
+ `); + }); + + test('it throws when passed a boolean', async function () { + expectEmberError( + new Error( + 'Assertion Failed: The argument passed to the `element` helper must be a string (you passed `false`)' + ) + ); + + await render(hbs` +
+ {{#let (element false) as |Tag|}} + hello world! + {{/let}} +
+ `); + }); + + test('it throws when passed an object', async function () { + expectEmberError( + new Error('Assertion Failed: The argument passed to the `element` helper must be a string') + ); + + await render(hbs` +
+ {{#let (element (hash)) as |Tag|}} + hello world! + {{/let}} +
+ `); + }); + }); +}); From 3d0d2aa31d30667b17777a1356958c309e635f61 Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Tue, 6 Jan 2026 20:49:17 -0500 Subject: [PATCH 2/2] Convert to gjs --- .../{element-test.js => element-test.gjs} | 84 +++++++++---------- 1 file changed, 40 insertions(+), 44 deletions(-) rename tests/browser/helpers/{element-test.js => element-test.gjs} (90%) diff --git a/tests/browser/helpers/element-test.js b/tests/browser/helpers/element-test.gjs similarity index 90% rename from tests/browser/helpers/element-test.js rename to tests/browser/helpers/element-test.gjs index 2166584f612..a5396c9f1d8 100644 --- a/tests/browser/helpers/element-test.js +++ b/tests/browser/helpers/element-test.gjs @@ -1,6 +1,5 @@ /* eslint-disable disable-features/disable-async-await */ import { module, test } from 'qunit'; -import { hbs } from 'ember-cli-htmlbars'; import { setupRenderingTest } from 'ember-qunit'; import { click, render, settled } from '@ember/test-helpers'; import { helper } from '@ember/component/helper'; @@ -42,41 +41,41 @@ module('Integration | Helper | element', function (hooks) { }); test('it renders a tag with the given tag name', async function (assert) { - await render(hbs` + await render(); assert.dom('h1#content').hasText('hello world!'); }); test('it does not render any tags when passed an empty string', async function (assert) { - await render(hbs` + await render(); assert.strictEqual(this.element.innerHTML.trim(), 'hello world!'); }); test('it does not render anything when passed null', async function (assert) { - await render(hbs` + await render(); assert.strictEqual(this.element.innerHTML.trim(), ''); }); test('it does not render anything when passed undefined', async function (assert) { - await render(hbs` + await render(); assert.strictEqual(this.element.innerHTML.trim(), ''); }); @@ -86,19 +85,16 @@ module('Integration | Helper | element', function (hooks) { this.set('didClick', () => clicked++); - // https://github.com/ember-cli/babel-plugin-htmlbars-inline-precompile/issues/103 - await render( - hbs( - '\ - {{#let (element "button") as |Tag|}}\ - hello world!\ - {{/let}}\ - ', - { insertRuntimeErrors: true } - ) - ); + await render(); - assert.dom('button#action').hasAttribute('type', 'button').hasText('hello world!'); + assert + .dom('button#action') + .hasAttribute('type', 'button') + .hasText('hello world!'); assert.strictEqual(clicked, 0, 'never clicked'); await click('button#action'); @@ -111,13 +107,13 @@ module('Integration | Helper | element', function (hooks) { }); test('it can be rendered multiple times', async function (assert) { - await render(hbs` + await render(); assert.dom('h1#content-1').hasText('hello'); assert.dom('h1#content-2').hasText('world'); @@ -125,7 +121,7 @@ module('Integration | Helper | element', function (hooks) { }); test('it can be passed to the component helper', async function (assert) { - await render(hbs` + await render(); assert.dom('h1#content-1').hasText('hello'); assert.dom('h2#content-2').hasText('world'); @@ -156,11 +152,11 @@ module('Integration | Helper | element', function (hooks) { this.set('tagName', 'h1'); - await render(hbs` + await render(); assert.dom('h1#content').hasText('rendered 1 time(s)'); assert.dom('h2#content').doesNotExist(); @@ -212,9 +208,9 @@ module('Integration | Helper | element', function (hooks) { test('it can be passed as argument and works with ...attributes', async function (assert) { this.set('tagName', 'p'); - await render(hbs` + await render(); assert.dom('p#content').hasText('Test').hasClass('extra'); @@ -240,7 +236,7 @@ module('Integration | Helper | element', function (hooks) { test.skip('it can be invoked inline', async function (assert) { this.set('tagName', 'p'); - await render(hbs`{{element this.tagName}}`); + await render(); assert.dom('p').exists(); @@ -267,13 +263,13 @@ module('Integration | Helper | element', function (hooks) { new Error('Assertion Failed: The `element` helper takes a single positional argument') ); - await render(hbs` + await render(); }); test('it requires no more than one argument', async function () { @@ -281,13 +277,13 @@ module('Integration | Helper | element', function (hooks) { new Error('Assertion Failed: The `element` helper takes a single positional argument') ); - await render(hbs` + await render(); }); test('it does not take any named arguments', async function () { @@ -295,13 +291,13 @@ module('Integration | Helper | element', function (hooks) { new Error('Assertion Failed: The `element` helper does not take any named arguments') ); - await render(hbs` + await render(); }); test('it does not take a block', async function (assert) { @@ -327,13 +323,13 @@ module('Integration | Helper | element', function (hooks) { // complete without errors. This is fixed in Ember 3.16+. this.set('showBlock', false); - await render(hbs` + await render(); assert.dom('h1').doesNotExist(); @@ -351,13 +347,13 @@ module('Integration | Helper | element', function (hooks) { ) ); - await render(hbs` + await render(); }); test('it throws when passed a boolean', async function () { @@ -367,13 +363,13 @@ module('Integration | Helper | element', function (hooks) { ) ); - await render(hbs` + await render(); }); test('it throws when passed an object', async function () { @@ -381,13 +377,13 @@ module('Integration | Helper | element', function (hooks) { new Error('Assertion Failed: The argument passed to the `element` helper must be a string') ); - await render(hbs` + await render(); }); }); });