Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 23 additions & 1 deletion babel.test.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,29 @@

import { createRequire } from 'node:module';
import baseConfig from './babel.config.mjs';
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, @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());
}
},
});

export default {
...baseConfig,

Expand All @@ -27,5 +44,10 @@ export default {
],
],

plugins: [...baseConfig.plugins, ...buildDebugMacroPlugin(!isProduction)],
plugins: [
...baseConfig.plugins,
appEmberSatisfiesPlugin,
...macros.babelMacros,
...buildDebugMacroPlugin(!isProduction),
],
};
32 changes: 32 additions & 0 deletions broccoli/app-ember-satisfies-plugin.cjs
Original file line number Diff line number Diff line change
@@ -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));
},
});
},
},
};
};
20 changes: 15 additions & 5 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,14 @@
</head>
<body>
<script type="module">
import 'qunit/qunit/qunit.js';
// By package name, as in the tests. The dev server then gives this page
// and the tests the same copy of QUnit.
import 'qunit';
import 'qunit/qunit/qunit.css';

// The last script of this page starts QUnit, after all tests are loaded.
QUnit.config.autostart = false;

window.EmberENV = {};

// Test under octane defaults
Expand Down Expand Up @@ -46,13 +51,11 @@

<script type="module">
import setupQUnit from './packages/internal-test-helpers/lib/ember-dev/setup-qunit.ts';
import setupTestHelpers from './packages/internal-test-helpers/lib/ember-dev/setup-test-helpers.js';
import { setTesting } from './packages/@ember/debug/index.ts';
setupQUnit();
setupTestHelpers();
setTesting(true);

let s = document.createElement('script');
s.src = '/testem.js';
document.body.append(s);
</script>

<script type="module">
Expand All @@ -79,6 +82,13 @@
eager: true,
}
);

// ember-qunit calls the testem hook when a test imports it, and that call
// throws if testem is already hooked. So testem loads after the tests.
let testem = document.createElement('script');
testem.src = '/testem.js';
testem.onload = testem.onerror = () => QUnit.start();
document.body.append(testem);
</script>

<div id="qunit"></div>
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@
"@babel/plugin-transform-typescript": "^7.26.8",
"@babel/preset-env": "^7.16.11",
"@babel/types": "^7.22.5",
"@ember/test-helpers": "^5.5.0",
"@embroider/macros": "^1.20.2",
"@embroider/shared-internals": "^3.0.2",
"@embroider/vite": "^1.7.2",
Expand All @@ -131,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",
Expand Down
Original file line number Diff line number Diff line change
@@ -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(
<template>{{#let (element "h1") as |Tag|}}<Tag id="content">hello world!</Tag>{{/let}}</template>
);

assert.strictEqual(this.element.innerHTML, '<h1 id="content">hello world!</h1>');
});

test('it does not render any tags when passed an empty string', async function (assert) {
await render(
<template>{{#let (element "") as |Tag|}}<Tag id="content">hello world!</Tag>{{/let}}</template>
);

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(<template>{{#let (element nil) as |Tag|}}<Tag>hello</Tag>{{/let}}</template>),
/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(<template>{{#let (element undef) as |Tag|}}<Tag>hello</Tag>{{/let}}</template>),
/The argument passed to the `element` helper must be a string/
);
});

test('it works with element modifiers', async function (assert) {
let didClick = () => {};

await render(
<template>{{#let (element "button") as |Tag|}}<Tag type="button" id="action" {{on "click" didClick}}>hello world!</Tag>{{/let}}</template>
);

assert.strictEqual(
this.element.innerHTML,
'<button id="action" type="button">hello world!</button>'
);
});

test('it can be rendered multiple times', async function (assert) {
await render(
<template>{{#let (element "h1") as |Tag|}}<Tag id="content-1">hello</Tag><Tag id="content-2">world</Tag><Tag id="content-3">!!!!!</Tag>{{/let}}</template>
);

assert.strictEqual(
this.element.innerHTML,
'<h1 id="content-1">hello</h1><h1 id="content-2">world</h1><h1 id="content-3">!!!!!</h1>'
);
});

test('it renders when the tag name changes', async function (assert) {
class State {
@tracked htmlTag = 'h1';
}

let state = new State();

await render(
<template>{{#let (element state.htmlTag) as |Tag|}}<Tag id="content">hello</Tag>{{/let}}</template>
);

assert.strictEqual(this.element.innerHTML, '<h1 id="content">hello</h1>');

state.htmlTag = 'h2';
await settled();
assert.strictEqual(this.element.innerHTML, '<h2 id="content">hello</h2>');

state.htmlTag = 'h3';
await settled();
assert.strictEqual(this.element.innerHTML, '<h3 id="content">hello</h3>');

state.htmlTag = '';
await settled();
assert.strictEqual(this.element.textContent, 'hello');

state.htmlTag = 'h1';
await settled();
assert.strictEqual(this.element.innerHTML, '<h1 id="content">hello</h1>');
});

test('it can be passed as argument and works with ...attributes', async function (assert) {
let Inner = <template>{{#let @tag as |Tag|}}<Tag id="content" ...attributes>{{yield}}</Tag>{{/let}}</template>;

await render(<template><Inner @tag={{element "p"}} class="extra">Test</Inner></template>);

assert.strictEqual(this.element.innerHTML, '<p id="content" class="extra">Test</p>');
});

test('it requires at least one argument', async function (assert) {
if (!DEBUG) {
assert.expect(0);
return;
}

await assert.rejects(
render(<template>{{#let (element) as |Tag|}}<Tag>hello</Tag>{{/let}}</template>),
/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(<template>{{#let (element "h1" "h2") as |Tag|}}<Tag>hello</Tag>{{/let}}</template>),
/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(
<template>{{#let (element "h1" id="content") as |Tag|}}<Tag>hello</Tag>{{/let}}</template>
),
/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(<template>{{#let (element num) as |Tag|}}<Tag>hello</Tag>{{/let}}</template>),
/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(<template>{{#let (element bool) as |Tag|}}<Tag>hello</Tag>{{/let}}</template>),
/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(<template>{{#let (element (hash)) as |Tag|}}<Tag>hello</Tag>{{/let}}</template>),
/The argument passed to the `element` helper must be a string/
);
});
});
Loading
Loading