Skip to content

Commit 392e649

Browse files
committed
Migrate Node tests to ESM
1 parent cdb86a4 commit 392e649

33 files changed

Lines changed: 2343 additions & 2301 deletions

pnpm-lock.yaml

Lines changed: 1054 additions & 177 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"name": "ember-test-node-template",
3+
"version": "0.0.0",
4+
"private": true,
5+
"type": "module",
6+
"description": "Node-focused smoke test template for ember-source",
7+
"scripts": {
8+
"test:node": "node --import ./tests/node/helpers/register-loader.js --test tests/node/*.test.js"
9+
},
10+
"dependencies": {
11+
"simple-dom": "^1.4.0",
12+
"html-differ": "^1.4.0"
13+
},
14+
"engines": {
15+
"node": ">= 20.6.0"
16+
}
17+
}
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
import { describe, test, beforeEach, afterEach } from 'node:test';
2+
import assert from 'node:assert/strict';
3+
import { createAppContext } from './helpers/setup-app.js';
4+
import { assertHtmlMatches } from './helpers/assert-html-matches.js';
5+
6+
describe('App Boot', () => {
7+
let ctx;
8+
9+
beforeEach(async () => {
10+
ctx = await createAppContext();
11+
});
12+
13+
afterEach(() => {
14+
ctx.destroy();
15+
});
16+
17+
test('App boots and routes to a URL', async () => {
18+
await ctx.visit('/');
19+
assert.ok(ctx.app);
20+
});
21+
22+
test('nested {{component}}', async () => {
23+
ctx.template('index', '{{root-component}}');
24+
25+
ctx.component(
26+
'root-component',
27+
{
28+
location: 'World',
29+
hasExistence: true,
30+
},
31+
'<h1>Hello {{#if this.hasExistence}}{{this.location}}{{/if}}</h1><div>{{component "foo-bar"}}</div>'
32+
);
33+
34+
ctx.component(
35+
'foo-bar',
36+
undefined,
37+
'<p>The files are *inside* the computer?!</p>'
38+
);
39+
40+
const html = await ctx.renderToHTML('/');
41+
assert.ok(
42+
assertHtmlMatches(
43+
html,
44+
'<body><div id="EMBER_ID" class="ember-view"><h1>Hello World</h1><div><p>The files are *inside* the computer?!</p></div></div></body>'
45+
),
46+
`HTML should match, got: ${html}`
47+
);
48+
});
49+
50+
test('<LinkTo>', async () => {
51+
ctx.template('application', "<h1><LinkTo @route='photos'>Go to photos</LinkTo></h1>");
52+
ctx.routes(function () {
53+
this.route('photos');
54+
});
55+
56+
const html = await ctx.renderToHTML('/');
57+
assert.ok(
58+
assertHtmlMatches(
59+
html,
60+
'<body><h1><a id="EMBER_ID" href="/photos" class="ember-view">Go to photos</a></h1></body>'
61+
),
62+
`HTML should match, got: ${html}`
63+
);
64+
});
65+
66+
test('{{link-to}}', async () => {
67+
ctx.template('application', "<h1>{{#link-to route='photos'}}Go to photos{{/link-to}}</h1>");
68+
ctx.routes(function () {
69+
this.route('photos');
70+
});
71+
72+
const html = await ctx.renderToHTML('/');
73+
assert.ok(
74+
assertHtmlMatches(
75+
html,
76+
'<body><h1><a id="EMBER_ID" href="/photos" class="ember-view">Go to photos</a></h1></body>'
77+
),
78+
`HTML should match, got: ${html}`
79+
);
80+
});
81+
82+
test('non-escaped content', async () => {
83+
ctx.routes(function () {
84+
this.route('photos');
85+
});
86+
87+
ctx.template('application', '<h1>{{{this.title}}}</h1>');
88+
ctx.controller('application', {
89+
title: '<b>Hello world</b>',
90+
});
91+
92+
const html = await ctx.renderToHTML('/');
93+
assert.ok(
94+
assertHtmlMatches(html, '<body><h1><b>Hello world</b></h1></body>'),
95+
`HTML should match, got: ${html}`
96+
);
97+
});
98+
99+
test('outlets', async () => {
100+
ctx.routes(function () {
101+
this.route('photos');
102+
});
103+
104+
ctx.template('application', '<p>{{outlet}}</p>');
105+
ctx.template('index', '<span>index</span>');
106+
ctx.template('photos', '<em>photos</em>');
107+
108+
const [indexHtml, photosHtml] = await ctx.all([
109+
ctx.renderToHTML('/'),
110+
ctx.renderToHTML('/photos'),
111+
]);
112+
113+
assert.ok(
114+
assertHtmlMatches(indexHtml, '<body><p><span>index</span></p></body>'),
115+
`index HTML should match, got: ${indexHtml}`
116+
);
117+
assert.ok(
118+
assertHtmlMatches(photosHtml, '<body><p><em>photos</em></p></body>'),
119+
`photos HTML should match, got: ${photosHtml}`
120+
);
121+
});
122+
123+
test('lifecycle hooks disabled', async () => {
124+
let didReceiveAttrsCalled = false;
125+
let willRenderCalled = false;
126+
let didRenderCalled = false;
127+
let willInsertElementCalled = false;
128+
let didInsertElementCalled = false;
129+
130+
ctx.template('application', "{{my-component foo='bar'}}{{outlet}}");
131+
132+
ctx.component('my-component', {
133+
didReceiveAttrs() {
134+
didReceiveAttrsCalled = true;
135+
},
136+
willRender() {
137+
willRenderCalled = true;
138+
},
139+
didRender() {
140+
didRenderCalled = true;
141+
},
142+
willInsertElement() {
143+
willInsertElementCalled = true;
144+
},
145+
didInsertElement() {
146+
didInsertElementCalled = true;
147+
},
148+
});
149+
150+
await ctx.renderToHTML('/');
151+
152+
assert.ok(didReceiveAttrsCalled, 'should trigger didReceiveAttrs hook');
153+
assert.ok(!willRenderCalled, 'should not trigger willRender hook');
154+
assert.ok(!didRenderCalled, 'should not trigger didRender hook');
155+
assert.ok(!willInsertElementCalled, 'should not trigger willInsertElement hook');
156+
assert.ok(!didInsertElementCalled, 'should not trigger didInsertElement hook');
157+
});
158+
});
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { describe, test, beforeEach, afterEach } from 'node:test';
2+
import assert from 'node:assert/strict';
3+
import { createComponentContext } from './helpers/setup-component.js';
4+
5+
describe('Components can be rendered without a DOM dependency', () => {
6+
let ctx;
7+
8+
beforeEach(async () => {
9+
ctx = await createComponentContext();
10+
});
11+
12+
afterEach(() => {
13+
ctx.destroy();
14+
});
15+
16+
test('Simple component', () => {
17+
let html = ctx.render('<h1>Hello</h1>');
18+
assert.ok(html.match(/<h1>Hello<\/h1>/), `expected <h1>Hello</h1> in: ${html}`);
19+
});
20+
21+
test('Component with dynamic value', () => {
22+
ctx.set('location', 'World');
23+
24+
let html = ctx.render('<h1>Hello {{this.location}}</h1>');
25+
assert.ok(html.match(/<h1>Hello World<\/h1>/), `expected <h1>Hello World</h1> in: ${html}`);
26+
});
27+
28+
test('Ensure undefined attributes requiring protocol sanitization do not error', () => {
29+
ctx.owner.register(
30+
'component:fake-link',
31+
class extends ctx.Component {
32+
tagName = 'link';
33+
attributeBindings = ['href', 'rel'];
34+
rel = 'canonical';
35+
}
36+
);
37+
38+
let html = ctx.render('{{fake-link}}');
39+
assert.ok(html.match(/rel="canonical"/), `expected rel="canonical" in: ${html}`);
40+
});
41+
42+
test('attributes requiring protocol sanitization do not error', () => {
43+
ctx.set('someHref', 'https://foo.com/');
44+
45+
let html = ctx.render('<a href={{this.someHref}}>Some Link</a>');
46+
assert.ok(
47+
html.match(/<a href="https:\/\/foo.com\/">Some Link<\/a>/),
48+
`expected link in: ${html}`
49+
);
50+
});
51+
});
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
/**
2+
* ESM port of tests/node/fastboot-sandbox-test.js
3+
*
4+
* The original test loaded Ember via vm.Script in a sandbox, simulating
5+
* FastBoot's execution model with the AMD bundle. In the ESM world,
6+
* we test that Ember can be dynamically imported and used to boot an app
7+
* with SimpleDOM, which is the ESM equivalent of the FastBoot sandbox pattern.
8+
*/
9+
import { describe, test } from 'node:test';
10+
import assert from 'node:assert/strict';
11+
import SimpleDOM from 'simple-dom';
12+
import Application from 'ember-source/@ember/application/index.js';
13+
import EmberObject from 'ember-source/@ember/object/index.js';
14+
import EmberRouter from 'ember-source/@ember/routing/router.js';
15+
import { run } from 'ember-source/@ember/runloop/index.js';
16+
import { precompile } from 'ember-source/ember-template-compiler/index.js';
17+
import { createTemplateFactory } from 'ember-source/@ember/template-factory/index.js';
18+
19+
const HTMLSerializer = new SimpleDOM.HTMLSerializer(SimpleDOM.voidMap);
20+
21+
function compile(templateString, options) {
22+
let templateSpec = precompile(templateString, options);
23+
return createTemplateFactory(JSON.parse(templateSpec));
24+
}
25+
26+
async function fastbootVisit(app, url) {
27+
let doc = new SimpleDOM.Document();
28+
let rootElement = doc.body;
29+
let options = { isBrowser: false, document: doc, rootElement: rootElement };
30+
31+
await app.boot();
32+
33+
let instance = await app.buildInstance();
34+
35+
try {
36+
await instance.boot(options);
37+
await instance.visit(url, options);
38+
39+
return {
40+
url: instance.getURL(),
41+
title: doc.title,
42+
body: HTMLSerializer.serialize(rootElement),
43+
};
44+
} finally {
45+
instance.destroy();
46+
}
47+
}
48+
49+
describe('FastBoot sandbox ESM equivalent', () => {
50+
test('FastBoot: basic', async () => {
51+
let Router = class extends EmberRouter {};
52+
Router.map(function () {
53+
this.route('a');
54+
this.route('b');
55+
});
56+
57+
let registry = {
58+
'router:main': Router,
59+
'template:application': compile('<h1>Hello world!</h1>\n{{outlet}}'),
60+
};
61+
62+
class Resolver extends EmberObject {
63+
resolve(specifier) {
64+
return registry[specifier];
65+
}
66+
}
67+
68+
let app = class extends Application {}.create({
69+
autoboot: false,
70+
Resolver,
71+
});
72+
73+
let result = await fastbootVisit(app, '/');
74+
75+
assert.equal(result.url, '/', 'landed on correct url');
76+
assert.equal(
77+
result.body,
78+
'<body><h1>Hello world!</h1>\n<!----></body>',
79+
'results in expected HTML'
80+
);
81+
82+
run(app, 'destroy');
83+
});
84+
});
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
/**
2+
* ESM port of tests/node/helpers/assert-html-matches.js
3+
*
4+
* Tests whether two fragments of HTML 'appear' to match.
5+
* Ignores whitespace and ID attributes (which Ember auto-generates).
6+
*/
7+
import { HtmlDiffer } from 'html-differ';
8+
9+
const htmlDiffer = new HtmlDiffer({
10+
ignoreAttributes: ['id'],
11+
ignoreWhitespaces: true,
12+
});
13+
14+
export function assertHtmlMatches(actual, expected) {
15+
return htmlDiffer.isEqual(actual, expected);
16+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/**
2+
* ESM port of tests/node/helpers/build-owner.js
3+
*/
4+
import EmberObject from 'ember-source/@ember/object/index.js';
5+
import Application from 'ember-source/@ember/application/index.js';
6+
import ApplicationInstance from 'ember-source/@ember/application/instance.js';
7+
import { Registry } from 'ember-source/@ember/-internals/container/index.js';
8+
import RegistryProxyMixin from 'ember-source/@ember/-internals/runtime/lib/mixins/registry_proxy.js';
9+
import ContainerProxyMixin from 'ember-source/@ember/-internals/runtime/lib/mixins/container_proxy.js';
10+
11+
export function buildOwner(resolver) {
12+
let Owner = EmberObject.extend(RegistryProxyMixin, ContainerProxyMixin);
13+
14+
let namespace = EmberObject.create({
15+
Resolver: {
16+
create: function () {
17+
return resolver;
18+
},
19+
},
20+
});
21+
22+
let fallbackRegistry = Application.buildRegistry(namespace);
23+
let registry = new Registry({
24+
fallback: fallbackRegistry,
25+
});
26+
27+
ApplicationInstance.setupRegistry(registry);
28+
29+
let owner = Owner.create({
30+
__registry__: registry,
31+
__container__: null,
32+
});
33+
34+
let container = registry.container({ owner: owner });
35+
owner.__container__ = container;
36+
37+
return owner;
38+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export async function loadTemplateCompiler() {
2+
return import('ember-source/ember-template-compiler/index.js');
3+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
/**
2+
* @embroider/macros shim for Node.js ESM loading.
3+
*
4+
* The Ember ESM packages in dist/packages/ contain unresolved imports from
5+
* '@embroider/macros' (specifically isDevelopingApp). In a real app build,
6+
* these are resolved at compile time by a Babel plugin. For Node.js testing
7+
* we provide a runtime shim.
8+
*/
9+
export function isDevelopingApp() {
10+
return true;
11+
}

0 commit comments

Comments
 (0)