diff --git a/babel.test.config.mjs b/babel.test.config.mjs
index dad707aeaf6..b2705501e39 100644
--- a/babel.test.config.mjs
+++ b/babel.test.config.mjs
@@ -14,12 +14,17 @@ import { buildMacros } from '@embroider/macros/babel';
// eslint-disable-next-line no-redeclare
const require = createRequire(import.meta.url);
const buildDebugMacroPlugin = require('./broccoli/build-debug-macro-plugin.cjs');
+const appEmberSatisfiesPlugin = require('./broccoli/app-ember-satisfies-plugin.cjs');
const isProduction = process.env.EMBER_ENV === 'production';
-// @ember/test-helpers and @ember/test-waiters use @embroider/macros.
+// @ember/test-helpers, @ember/test-waiters and ember-qunit use @embroider/macros.
//
// buildMacros() reads NODE_ENV, but our builds select the mode with EMBER_ENV.
const macros = buildMacros({
+ setConfig: {
+ // index.html loads the QUnit styles, and tests render into #qunit-fixture.
+ 'ember-qunit': { theme: 'none', disableContainerStyles: true },
+ },
configure(config) {
if (!isProduction) {
config.enablePackageDevelopment(process.cwd());
@@ -39,5 +44,10 @@ export default {
],
],
- plugins: [...baseConfig.plugins, ...macros.babelMacros, ...buildDebugMacroPlugin(!isProduction)],
+ plugins: [
+ ...baseConfig.plugins,
+ appEmberSatisfiesPlugin,
+ ...macros.babelMacros,
+ ...buildDebugMacroPlugin(!isProduction),
+ ],
};
diff --git a/broccoli/app-ember-satisfies-plugin.cjs b/broccoli/app-ember-satisfies-plugin.cjs
new file mode 100644
index 00000000000..c1217c19000
--- /dev/null
+++ b/broccoli/app-ember-satisfies-plugin.cjs
@@ -0,0 +1,32 @@
+const semver = require('semver');
+const { version } = require('../package.json');
+
+/*
+ `appEmberSatisfies` from @embroider/macros reads the ember-source version
+ from the dependencies of the app.
+
+ In this repo the app is ember-source, and a package cannot depend on itself.
+ So this plugin answers the macro with the version of this repo, before
+ @embroider/macros sees it.
+*/
+module.exports = function appEmberSatisfiesPlugin({ types: t }) {
+ return {
+ name: 'app-ember-satisfies',
+ visitor: {
+ Program(program) {
+ program.traverse({
+ CallExpression(call) {
+ if (!call.get('callee').referencesImport('@embroider/macros', 'appEmberSatisfies')) {
+ return;
+ }
+
+ let [range] = call.node.arguments;
+ let satisfied = semver.satisfies(version, range.value, { includePrerelease: true });
+
+ call.replaceWith(t.booleanLiteral(satisfied));
+ },
+ });
+ },
+ },
+ };
+};
diff --git a/index.html b/index.html
index ba2b0bcf2fe..d67e07a7834 100644
--- a/index.html
+++ b/index.html
@@ -7,9 +7,14 @@
diff --git a/package.json b/package.json
index 26b3917add8..ee5d718b1b7 100644
--- a/package.json
+++ b/package.json
@@ -132,6 +132,7 @@
"ember-cli-browserstack": "^4.0.0",
"ember-cli-dependency-checker": "^3.3.1",
"ember-cli-yuidoc": "^0.9.1",
+ "ember-qunit": "^9.1.0",
"eslint": "^9.21.0",
"eslint-import-resolver-node": "^0.3.10",
"eslint-plugin-disable-features": "^0.1.3",
diff --git a/packages/@ember/-internals/glimmer/tests/integration/helpers/element-test.gjs b/packages/@ember/-internals/glimmer/tests/integration/helpers/element-test.gjs
new file mode 100644
index 00000000000..75294e27315
--- /dev/null
+++ b/packages/@ember/-internals/glimmer/tests/integration/helpers/element-test.gjs
@@ -0,0 +1,195 @@
+import { DEBUG } from '@glimmer/env';
+import { tracked } from '@glimmer/tracking';
+import { module, test } from 'qunit';
+import { setupRenderingTest } from 'ember-qunit';
+import { render, settled } from '@ember/test-helpers';
+import { element, hash } from '@ember/helper';
+import { on } from '@ember/modifier';
+
+module('Helpers test: {{element}}', function (hooks) {
+ setupRenderingTest(hooks);
+
+ test('it renders a tag with the given tag name', async function (assert) {
+ await render(
+ {{#let (element "h1") as |Tag|}}hello world!{{/let}}
+ );
+
+ assert.strictEqual(this.element.innerHTML, '
hello world!
');
+ });
+
+ test('it does not render any tags when passed an empty string', async function (assert) {
+ await render(
+ {{#let (element "") as |Tag|}}hello world!{{/let}}
+ );
+
+ assert.strictEqual(this.element.innerHTML, 'hello world!');
+ });
+
+ test('it throws when passed null', async function (assert) {
+ if (!DEBUG) {
+ assert.expect(0);
+ return;
+ }
+
+ let nil = null;
+
+ await assert.rejects(
+ render({{#let (element nil) as |Tag|}}hello{{/let}}),
+ /The argument passed to the `element` helper must be a string/
+ );
+ });
+
+ test('it throws when passed undefined', async function (assert) {
+ if (!DEBUG) {
+ assert.expect(0);
+ return;
+ }
+
+ let undef = undefined;
+
+ await assert.rejects(
+ render({{#let (element undef) as |Tag|}}hello{{/let}}),
+ /The argument passed to the `element` helper must be a string/
+ );
+ });
+
+ test('it works with element modifiers', async function (assert) {
+ let didClick = () => {};
+
+ await render(
+ {{#let (element "button") as |Tag|}}hello world!{{/let}}
+ );
+
+ assert.strictEqual(
+ this.element.innerHTML,
+ ''
+ );
+ });
+
+ test('it can be rendered multiple times', async function (assert) {
+ await render(
+ {{#let (element "h1") as |Tag|}}helloworld!!!!!{{/let}}
+ );
+
+ assert.strictEqual(
+ this.element.innerHTML,
+ '
hello
world
!!!!!
'
+ );
+ });
+
+ test('it renders when the tag name changes', async function (assert) {
+ class State {
+ @tracked htmlTag = 'h1';
+ }
+
+ let state = new State();
+
+ await render(
+ {{#let (element state.htmlTag) as |Tag|}}hello{{/let}}
+ );
+
+ assert.strictEqual(this.element.innerHTML, '
');
+ });
+
+ test('it can be passed as argument and works with ...attributes', async function (assert) {
+ let Inner = {{#let @tag as |Tag|}}{{yield}}{{/let}};
+
+ await render(Test);
+
+ assert.strictEqual(this.element.innerHTML, '
Test
');
+ });
+
+ test('it requires at least one argument', async function (assert) {
+ if (!DEBUG) {
+ assert.expect(0);
+ return;
+ }
+
+ await assert.rejects(
+ render({{#let (element) as |Tag|}}hello{{/let}}),
+ /The `element` helper takes a single positional argument/
+ );
+ });
+
+ test('it requires no more than one argument', async function (assert) {
+ if (!DEBUG) {
+ assert.expect(0);
+ return;
+ }
+
+ await assert.rejects(
+ render({{#let (element "h1" "h2") as |Tag|}}hello{{/let}}),
+ /The `element` helper takes a single positional argument/
+ );
+ });
+
+ test('it does not take any named arguments', async function (assert) {
+ if (!DEBUG) {
+ assert.expect(0);
+ return;
+ }
+
+ await assert.rejects(
+ render(
+ {{#let (element "h1" id="content") as |Tag|}}hello{{/let}}
+ ),
+ /The `element` helper does not take any named arguments/
+ );
+ });
+
+ test('it throws when passed a number', async function (assert) {
+ if (!DEBUG) {
+ assert.expect(0);
+ return;
+ }
+
+ let num = 123;
+
+ await assert.rejects(
+ render({{#let (element num) as |Tag|}}hello{{/let}}),
+ /The argument passed to the `element` helper must be a string \(you passed `123`\)/
+ );
+ });
+
+ test('it throws when passed a boolean', async function (assert) {
+ if (!DEBUG) {
+ assert.expect(0);
+ return;
+ }
+
+ let bool = false;
+
+ await assert.rejects(
+ render({{#let (element bool) as |Tag|}}hello{{/let}}),
+ /The argument passed to the `element` helper must be a string \(you passed `false`\)/
+ );
+ });
+
+ test('it throws when passed an object', async function (assert) {
+ if (!DEBUG) {
+ assert.expect(0);
+ return;
+ }
+
+ await assert.rejects(
+ render({{#let (element (hash)) as |Tag|}}hello{{/let}}),
+ /The argument passed to the `element` helper must be a string/
+ );
+ });
+});
diff --git a/packages/@ember/-internals/glimmer/tests/integration/helpers/element-test.js b/packages/@ember/-internals/glimmer/tests/integration/helpers/element-test.js
deleted file mode 100644
index 32618bf1869..00000000000
--- a/packages/@ember/-internals/glimmer/tests/integration/helpers/element-test.js
+++ /dev/null
@@ -1,207 +0,0 @@
-import { DEBUG } from '@glimmer/env';
-import { tracked } from '@glimmer/tracking';
-import { RenderingTestCase, moduleFor, runTask } from 'internal-test-helpers';
-import { element as elementHelper, hash } from '@ember/helper';
-import { on } from '@ember/modifier';
-import { template } from '@ember/template-compiler/runtime';
-
-moduleFor(
- 'Helpers test: {{element}}',
- class extends RenderingTestCase {
- '@test it renders a tag with the given tag name'() {
- let AComponent = template(
- `{{#let (element "h1") as |Tag|}}hello world!{{/let}}`,
- { scope: () => ({ element: elementHelper }) }
- );
- this.renderComponent(AComponent, { expect: '
hello world!
' });
- }
-
- '@test it does not render any tags when passed an empty string'() {
- let AComponent = template(
- `{{#let (element "") as |Tag|}}hello world!{{/let}}`,
- { scope: () => ({ element: elementHelper }) }
- );
- this.renderComponent(AComponent, { expect: 'hello world!' });
- }
-
- ['@test it throws when passed null']() {
- if (!DEBUG) {
- this.assert.expect(0);
- return;
- }
-
- let nil = null;
- this.assert.throws(() => {
- let AComponent = template(`{{#let (element nil) as |Tag|}}hello{{/let}}`, {
- scope: () => ({ element: elementHelper, nil }),
- });
- this.renderComponent(AComponent, { expect: '' });
- }, /The argument passed to the `element` helper must be a string/);
- }
-
- ['@test it throws when passed undefined']() {
- if (!DEBUG) {
- this.assert.expect(0);
- return;
- }
-
- let undef = undefined;
- this.assert.throws(() => {
- let AComponent = template(`{{#let (element undef) as |Tag|}}hello{{/let}}`, {
- scope: () => ({ element: elementHelper, undef }),
- });
- this.renderComponent(AComponent, { expect: '' });
- }, /The argument passed to the `element` helper must be a string/);
- }
-
- '@test it works with element modifiers'() {
- let didClick = () => {};
- let AComponent = template(
- `{{#let (element "button") as |Tag|}}hello world!{{/let}}`,
- { scope: () => ({ element: elementHelper, on, didClick }) }
- );
- this.renderComponent(AComponent, {
- expect: '',
- });
- }
-
- '@test it can be rendered multiple times'() {
- let AComponent = template(
- `{{#let (element "h1") as |Tag|}}helloworld!!!!!{{/let}}`,
- { scope: () => ({ element: elementHelper }) }
- );
- this.renderComponent(AComponent, {
- expect:
- '
hello
world
!!!!!
',
- });
- }
-
- '@test it renders when the tag name changes'() {
- class State {
- @tracked htmlTag = 'h1';
- }
-
- let state = new State();
-
- let AComponent = template(
- `{{#let (element state.htmlTag) as |Tag|}}hello{{/let}}`,
- { scope: () => ({ element: elementHelper, state }) }
- );
- this.renderComponent(AComponent, {
- expect: '