7 | {{/each}}
8 |
--------------------------------------------------------------------------------
/blueprints/fastboot-test/index.js:
--------------------------------------------------------------------------------
1 | /* eslint-env node */
2 | module.exports = {
3 | description: 'A test that runs against fastboot generated html',
4 |
5 | normalizeEntityName(entityName) {
6 | return entityName;
7 | },
8 | };
9 |
--------------------------------------------------------------------------------
/tests/dummy/app/pods/examples/errors/throw-error-object/route.js:
--------------------------------------------------------------------------------
1 | import Route from '@ember/routing/route';
2 |
3 | export default class extends Route {
4 | model() {
5 | throw new Error('This errors in FastBoot!');
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/tests/dummy/app/pods/examples/network/other/get-request/template.hbs:
--------------------------------------------------------------------------------
1 | The data loaded from the server is:
2 |
3 | {{#each @model as |note|}}
4 |
5 | {{note.title}}
6 |
7 | {{/each}}
8 |
--------------------------------------------------------------------------------
/tests/dummy/app/pods/examples/network/other/origin-override/template.hbs:
--------------------------------------------------------------------------------
1 | The data loaded from the server is:
2 |
3 | {{#each @model as |note|}}
4 |
5 | {{note.title}}
6 |
7 | {{/each}}
8 |
--------------------------------------------------------------------------------
/tests/dummy/app/pods/examples/network/other/post-request/template.hbs:
--------------------------------------------------------------------------------
1 | The data loaded from the server is:
2 |
3 | {{#each @model as |note|}}
4 |
5 | {{note.title}}
6 |
7 | {{/each}}
8 |
--------------------------------------------------------------------------------
/tests/dummy/app/pods/examples/global-config/route.js:
--------------------------------------------------------------------------------
1 | import Route from '@ember/routing/route';
2 |
3 | export default class extends Route {
4 | model() {
5 | return {
6 | SampleGlobal: window.SampleGlobal,
7 | };
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/tests/dummy/mirage/scenarios/default.js:
--------------------------------------------------------------------------------
1 | export default function (/* server */) {
2 | /*
3 | Seed your development database using your factories.
4 | This data will not be loaded in your tests.
5 | */
6 | // server.createList('note', 10);
7 | }
8 |
--------------------------------------------------------------------------------
/tests/dummy/config/targets.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const browsers = [
4 | 'last 1 Chrome versions',
5 | 'last 1 Firefox versions',
6 | 'last 1 Safari versions',
7 | ];
8 |
9 | module.exports = {
10 | browsers,
11 | node: 'current',
12 | };
13 |
--------------------------------------------------------------------------------
/tests/dummy/app/pods/examples/query-parameters/controller.js:
--------------------------------------------------------------------------------
1 | import Controller from '@ember/controller';
2 |
3 | export default class extends Controller {
4 | queryParams = ['first', 'second', 'third'];
5 | first = null;
6 | second = null;
7 | third = null;
8 | }
9 |
--------------------------------------------------------------------------------
/.ember-cli:
--------------------------------------------------------------------------------
1 | {
2 | /**
3 | Setting `isTypeScriptProject` to true will force the blueprint generators to generate TypeScript
4 | rather than JavaScript by default, when a TypeScript version of a given blueprint is available.
5 | */
6 | "isTypeScriptProject": false
7 | }
8 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: npm
4 | directory: "/"
5 | schedule:
6 | interval: daily
7 | open-pull-requests-limit: 10
8 | ignore:
9 | - dependency-name: ember-cli
10 | versions:
11 | - ">= 0"
12 |
--------------------------------------------------------------------------------
/tests/dummy/app/pods/examples/network/notes/index/route.js:
--------------------------------------------------------------------------------
1 | import Route from '@ember/routing/route';
2 | import { inject as service } from '@ember/service';
3 |
4 | export default class extends Route {
5 | @service store;
6 |
7 | model() {
8 | return this.store.findAll('note');
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/tests/dummy/app/routes/application.js:
--------------------------------------------------------------------------------
1 | import Route from '@ember/routing/route';
2 | import { inject as service } from '@ember/service';
3 |
4 | export default class extends Route {
5 | @service headData;
6 |
7 | afterModel() {
8 | this.headData.set('title', 'Fastboot testing');
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/tests/dummy/app/pods/examples/network/other/get-request/route.js:
--------------------------------------------------------------------------------
1 | import Route from '@ember/routing/route';
2 | import fetch from 'fetch';
3 |
4 | export default class extends Route {
5 | async model() {
6 | let response = await fetch('/api/notes');
7 | return await response.json();
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/tests/dummy/app/pods/examples/redirects/replace-with/route.js:
--------------------------------------------------------------------------------
1 | import Route from '@ember/routing/route';
2 | import { inject as service } from '@ember/service';
3 |
4 | export default class extends Route {
5 | @service router;
6 |
7 | redirect() {
8 | this.router.replaceWith('/examples');
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/tests/dummy/app/pods/examples/request-object/controller.js:
--------------------------------------------------------------------------------
1 | import Controller from '@ember/controller';
2 | import { inject as service } from '@ember/service';
3 |
4 | export default class extends Controller {
5 | @service fastboot;
6 |
7 | get request() {
8 | return this.fastboot.request;
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/tests/dummy/app/pods/examples/redirects/transition-to/route.js:
--------------------------------------------------------------------------------
1 | import Route from '@ember/routing/route';
2 | import { inject as service } from '@ember/service';
3 |
4 | export default class extends Route {
5 | @service router;
6 |
7 | redirect() {
8 | this.router.transitionTo('/examples');
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/tests/dummy/app/pods/examples/network/other/post-request/route.js:
--------------------------------------------------------------------------------
1 | import Route from '@ember/routing/route';
2 | import fetch from 'fetch';
3 |
4 | export default class extends Route {
5 | async model() {
6 | let response = await fetch('/api/notes', { method: 'post' });
7 | return await response.json();
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/tests/dummy/app/pods/examples/network/notes/note/route.js:
--------------------------------------------------------------------------------
1 | import Route from '@ember/routing/route';
2 | import { inject as service } from '@ember/service';
3 |
4 | export default class extends Route {
5 | @service store;
6 |
7 | model(params) {
8 | return this.store.findRecord('note', params['note_id']);
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/tests/dummy/app/pods/examples/network/other/headers/route.js:
--------------------------------------------------------------------------------
1 | import Route from '@ember/routing/route';
2 | import fetch from 'fetch';
3 |
4 | export default class extends Route {
5 | async model() {
6 | let response = await fetch('/api/notes');
7 | const headers = await response.headers;
8 | return headers.get('x-test');
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/tests/dummy/config/addon-docs.js:
--------------------------------------------------------------------------------
1 | /* eslint-env node */
2 | 'use strict';
3 |
4 | const AddonDocsConfig = require('ember-cli-addon-docs/lib/config');
5 |
6 | module.exports = class extends AddonDocsConfig {
7 | // See https://ember-learn.github.io/ember-cli-addon-docs/docs/deploying
8 | // for details on configuration you can override here.
9 | };
10 |
--------------------------------------------------------------------------------
/config/addon-docs.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable n/no-unpublished-require */
2 | 'use strict';
3 |
4 | const AddonDocsConfig = require('ember-cli-addon-docs/lib/config');
5 |
6 | module.exports = class extends AddonDocsConfig {
7 | // See https://ember-learn.github.io/ember-cli-addon-docs/docs/deploying
8 | // for details on configuration you can override here.
9 | };
10 |
--------------------------------------------------------------------------------
/tests/test-helper.js:
--------------------------------------------------------------------------------
1 | import Application from 'dummy/app';
2 | import config from 'dummy/config/environment';
3 | import * as QUnit from 'qunit';
4 | import { setApplication } from '@ember/test-helpers';
5 | import { setup } from 'qunit-dom';
6 | import { start } from 'ember-qunit';
7 |
8 | setApplication(Application.create(config.APP));
9 |
10 | setup(QUnit.assert);
11 |
12 | start();
13 |
--------------------------------------------------------------------------------
/tests/dummy/app/pods/examples/visit-customization/route.js:
--------------------------------------------------------------------------------
1 | import Route from '@ember/routing/route';
2 | import { inject as service } from '@ember/service';
3 |
4 | export default class extends Route {
5 | @service fastboot;
6 |
7 | model() {
8 | let { data, add } = this.fastboot.get('metadata');
9 |
10 | return {
11 | data,
12 | add: add(1, 2),
13 | };
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # compiled output
2 | /dist/
3 | /declarations/
4 |
5 | # dependencies
6 | /node_modules/
7 |
8 | # misc
9 | /.env*
10 | /.pnp*
11 | /.eslintcache
12 | /coverage/
13 | /npm-debug.log*
14 | /testem.log
15 | /yarn-error.log
16 |
17 | # ember-try
18 | /.node_modules.ember-try/
19 | /npm-shrinkwrap.json.ember-try
20 | /package.json.ember-try
21 | /package-lock.json.ember-try
22 | /yarn.lock.ember-try
23 |
24 | # broccoli-debug
25 | /DEBUG/
26 |
--------------------------------------------------------------------------------
/tests/dummy/app/app.js:
--------------------------------------------------------------------------------
1 | import Application from '@ember/application';
2 | import Resolver from 'ember-resolver';
3 | import loadInitializers from 'ember-load-initializers';
4 | import config from 'dummy/config/environment';
5 |
6 | export default class App extends Application {
7 | modulePrefix = config.modulePrefix;
8 | podModulePrefix = config.podModulePrefix;
9 | Resolver = Resolver;
10 | }
11 |
12 | loadInitializers(App, config.modulePrefix);
13 |
--------------------------------------------------------------------------------
/tests/fastboot/global-config-test.js:
--------------------------------------------------------------------------------
1 | import { module, test } from 'qunit';
2 | import { setup, visit } from 'ember-cli-fastboot-testing/test-support';
3 |
4 | module('Fastboot | global config', function (hooks) {
5 | setup(hooks);
6 |
7 | test('it renders the correct global', async function (assert) {
8 | await visit('/examples/global-config');
9 | assert.dom('[data-test-id="sandboxGlobals"]').hasText('TestSampleGlobal');
10 | });
11 | });
12 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # EditorConfig helps developers define and maintain consistent
2 | # coding styles between different editors and IDEs
3 | # editorconfig.org
4 |
5 | root = true
6 |
7 | [*]
8 | end_of_line = lf
9 | charset = utf-8
10 | trim_trailing_whitespace = true
11 | insert_final_newline = true
12 | indent_style = space
13 | indent_size = 2
14 |
15 | [*.hbs]
16 | insert_final_newline = false
17 |
18 | [*.{diff,md}]
19 | trim_trailing_whitespace = false
20 |
--------------------------------------------------------------------------------
/blueprints/fastboot-test-config/files/__config__/fastboot-testing.js:
--------------------------------------------------------------------------------
1 | module.exports = () => {
2 | return {
3 | resilient: false,
4 | buildSandboxGlobals(defaultGlobals) {
5 | return {
6 | ...defaultGlobals
7 | // here you can add globals to the Fastboot renderer
8 | };
9 | },
10 | setupFastboot(fastbootInstance) {
11 | // here you can access the fastboot instance which runs the tests
12 | },
13 | };
14 | };
15 |
--------------------------------------------------------------------------------
/tests/dummy/app/pods/examples/network/other/origin-override/route.js:
--------------------------------------------------------------------------------
1 | import Route from '@ember/routing/route';
2 | import fetch from 'fetch';
3 | import config from 'dummy/config/environment';
4 |
5 | export default class extends Route {
6 | async model() {
7 | const { originForOverride } = config;
8 | let response = await fetch(`${originForOverride}/api/notes`, {
9 | method: 'get',
10 | });
11 | return await response.json();
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/blueprints/fastboot-test/files/tests/fastboot/__name__-test.js:
--------------------------------------------------------------------------------
1 | import { module, test } from 'qunit';
2 | import { setup, visit, /* mockServer */ } from 'ember-cli-fastboot-testing/test-support';
3 |
4 | module('FastBoot | <%= dasherizedModuleName %>', function(hooks) {
5 | setup(hooks);
6 |
7 | test('it renders a page...', async function(assert) {
8 | await visit('/');
9 |
10 | // replace this line with a real assertion!
11 | assert.ok(true);
12 | });
13 |
14 | });
15 |
--------------------------------------------------------------------------------
/tests/dummy/app/services/store.js:
--------------------------------------------------------------------------------
1 | import { LegacyNetworkHandler } from '@ember-data/legacy-compat';
2 | import RequestManager from '@ember-data/request';
3 | import Fetch from '@ember-data/request/fetch';
4 | import BaseStore, { CacheHandler } from '@ember-data/store';
5 |
6 | export default class StoreService extends BaseStore {
7 | constructor(owner) {
8 | super(owner);
9 | this.requestManager = new RequestManager();
10 | this.requestManager.use([LegacyNetworkHandler, Fetch]);
11 | this.requestManager.useCache(CacheHandler);
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/tests/dummy/config/ember-cli-update.json:
--------------------------------------------------------------------------------
1 | {
2 | "schemaVersion": "1.0.0",
3 | "packages": [
4 | {
5 | "name": "ember-cli",
6 | "version": "5.11.0",
7 | "blueprints": [
8 | {
9 | "name": "addon",
10 | "outputRepo": "https://github.com/ember-cli/ember-addon-output",
11 | "codemodsSource": "ember-addon-codemods-manifest@1",
12 | "isBaseBlueprint": true,
13 | "options": [
14 | "--yarn",
15 | "--no-welcome"
16 | ]
17 | }
18 | ]
19 | }
20 | ]
21 | }
22 |
--------------------------------------------------------------------------------
/tests/dummy/app/styles/app.css:
--------------------------------------------------------------------------------
1 | /* Ember supports plain CSS out of the box. More info: https://cli.emberjs.com/release/advanced-use/stylesheets/ */
2 | :root {
3 | --brand-primary: #7972ce;
4 | }
5 |
6 | .bg-brand {
7 | background-color: var(--brand-primary);
8 | }
9 |
10 | .text-brand {
11 | color: var(--brand-primary);
12 | }
13 |
14 | .container {
15 | padding-left: 1rem;
16 | padding-right: 1rem;
17 | max-width: 1400px;
18 | margin-left: auto;
19 | margin-right: auto;
20 | }
21 |
22 | @media (width >= 1768px) {
23 | .container {
24 | padding-left: 1.5rem;
25 | padding-right: 1.5rem;
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/tests/dummy/app/pods/examples/network/other/echo/route.js:
--------------------------------------------------------------------------------
1 | import Route from '@ember/routing/route';
2 | import fetch from 'fetch';
3 |
4 | export default class extends Route {
5 | // this endpoint is defined in index.js, it's used to represent
6 | // a url that ember-cli might already have in it's express router.
7 |
8 | // we should be able to post a body to this echo endpoint and
9 | // get a reply back.
10 | model(params) {
11 | return fetch('/fastboot-testing/echo', {
12 | method: 'POST',
13 | body: params.message,
14 | }).then((response) => {
15 | return response.text();
16 | });
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/blueprints/fastboot-test-config/index.js:
--------------------------------------------------------------------------------
1 | /* eslint-env node */
2 | module.exports = {
3 | description:
4 | 'Generates test config for adding custom Fastboot sandbox globals',
5 |
6 | normalizeEntityName(entityName) {
7 | return entityName;
8 | },
9 |
10 | fileMapTokens(/* options */) {
11 | let configPath = 'config';
12 | let pkg = this.project.pkg;
13 |
14 | if (pkg['ember-addon'] && pkg['ember-addon']['configPath']) {
15 | configPath = pkg['ember-addon']['configPath'];
16 | }
17 | return {
18 | __config__(/* options */) {
19 | return configPath;
20 | },
21 | };
22 | },
23 | };
24 |
--------------------------------------------------------------------------------
/tests/dummy/app/templates/docs/html.md:
--------------------------------------------------------------------------------
1 | # HTML tests
2 |
3 | Testing FastBoot rendered HTML is the most common use case for this library. Any HTML rendered by FastBoot can be tested the same way you would test an Ember application with `qunit-dom`.
4 |
5 | ```js
6 | import { module, test } from 'qunit';
7 | import { setup, visit } from 'ember-cli-fastboot-testing/test-support';
8 |
9 | module('FastBoot | testing html', function(hooks) {
10 | setup(hooks);
11 |
12 | test('it renders the correct h1 title', async function(assert) {
13 | await visit('/');
14 |
15 | assert.dom('h1').hasText('My page title!');
16 | });
17 | });
18 | ```
19 |
--------------------------------------------------------------------------------
/testem.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | module.exports = {
4 | test_page: 'tests/index.html?hidepassed',
5 | disable_watching: true,
6 | launch_in_ci: ['Chrome'],
7 | launch_in_dev: ['Chrome'],
8 | browser_start_timeout: 120,
9 | browser_args: {
10 | Chrome: {
11 | ci: [
12 | // --no-sandbox is needed when running Chrome inside a container
13 | process.env.CI ? '--no-sandbox' : null,
14 | '--headless',
15 | '--disable-dev-shm-usage',
16 | '--disable-software-rasterizer',
17 | '--mute-audio',
18 | '--remote-debugging-port=0',
19 | '--window-size=1440,900',
20 | ].filter(Boolean),
21 | },
22 | },
23 | };
24 |
--------------------------------------------------------------------------------
/tests/fastboot/visit-customization-test.js:
--------------------------------------------------------------------------------
1 | import { module, skip } from 'qunit';
2 | import { setup, visit } from 'ember-cli-fastboot-testing/test-support';
3 |
4 | module('Fastboot | visit customization', function (hooks) {
5 | setup(hooks);
6 |
7 | // this test doesnt work in ember 2.x
8 | skip('it renders the correct metadata', async function (assert) {
9 | await visit('/examples/visit-customization', {
10 | metadata: {
11 | data: 'data in metadata',
12 | add: (a, b) => a + b,
13 | },
14 | });
15 |
16 | assert.dom('[data-test-id="metadata-data"]').hasText('data in metadata');
17 | assert.dom('[data-test-id="metadata-fn"]').hasText('3');
18 | });
19 | });
20 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | # compiled output
2 | /dist/
3 | /tmp/
4 |
5 | # misc
6 | /.editorconfig
7 | /.ember-cli
8 | /.env*
9 | /.eslintcache
10 | /.eslintignore
11 | /.eslintrc.js
12 | /.git/
13 | /.github/
14 | /.gitignore
15 | /.prettierignore
16 | /.prettierrc.js
17 | /.stylelintignore
18 | /.stylelintrc.js
19 | /.template-lintrc.js
20 | /.travis.yml
21 | /.watchmanconfig
22 | /CONTRIBUTING.md
23 | /ember-cli-build.js
24 | /testem.js
25 | /tests/
26 | /tsconfig.declarations.json
27 | /tsconfig.json
28 | /yarn-error.log
29 | /yarn.lock
30 | .gitkeep
31 |
32 | # ember-try
33 | /.node_modules.ember-try/
34 | /npm-shrinkwrap.json.ember-try
35 | /package.json.ember-try
36 | /package-lock.json.ember-try
37 | /yarn.lock.ember-try
38 |
--------------------------------------------------------------------------------
/ember-cli-build.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const EmberAddon = require('ember-cli/lib/broccoli/ember-addon');
4 |
5 | module.exports = function (defaults) {
6 | const app = new EmberAddon(defaults, {
7 | // Add options here
8 | '@embroider/macros': {
9 | setConfig: {
10 | '@ember-data/store': {
11 | polyfillUUID: true,
12 | },
13 | },
14 | },
15 | autoImport: {
16 | forbidEval: true,
17 | },
18 | });
19 |
20 | app.import('vendor/tailwind.min.css');
21 |
22 | const { maybeEmbroider } = require('@embroider/test-setup');
23 | return maybeEmbroider(app, {
24 | skipBabel: [
25 | {
26 | package: 'qunit',
27 | },
28 | ],
29 | });
30 | };
31 |
--------------------------------------------------------------------------------
/tests/dummy/app/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Fastboot Testing
6 |
7 |
8 |
9 | {{content-for "head"}}
10 |
11 |
12 |
13 |
14 | {{content-for "head-footer"}}
15 |
16 |
17 | {{content-for "body"}}
18 |
19 |
20 |
21 |
22 | {{content-for "body-footer"}}
23 |
24 |
25 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # How To Contribute
2 |
3 | ## Installation
4 |
5 | - `git clone https://github.com/embermap/ember-cli-fastboot-testing.git`
6 | - `cd ember-cli-fastboot-testing`
7 | - `yarn install`
8 |
9 | ## Linting
10 |
11 | - `yarn lint`
12 | - `yarn lint:fix`
13 |
14 | ## Running tests
15 |
16 | - `yarn test` – Runs the test suite on the current Ember version
17 | - `yarn test:ember --server` – Runs the test suite in "watch mode"
18 | - `yarn test:ember-compatibility` – Runs the test suite against multiple Ember versions
19 |
20 | ## Running the dummy application
21 |
22 | - `yarn start`
23 | - Visit the dummy application at [http://localhost:4200](http://localhost:4200).
24 |
25 | For more information on using ember-cli, visit [https://cli.emberjs.com/release/](https://cli.emberjs.com/release/).
26 |
--------------------------------------------------------------------------------
/tests/dummy/app/templates/docs/fastboot-configuration.md:
--------------------------------------------------------------------------------
1 | # FastBoot configuration
2 |
3 | When using this addon a new FastBoot instance will be created specifically for the testing environment. The options provided to the instance can be customized through a config file.
4 |
5 | ```js
6 | // my-app/config/fastboot-testing.js
7 |
8 | module.exports = () => {
9 | return {
10 | resilient: true,
11 | buildSandboxGlobals(defaultGlobals) {
12 | return {
13 | ...defaultGlobals,
14 | SampleGlobal: `TestSampleGlobal`,
15 | najax,
16 | };
17 | },
18 | };
19 | };
20 | ```
21 |
22 | If no configuration file is present, then FastBoot testing will use sane defaults that make sense when testing FastBoot applications. For most applications not providing a customized configuration file is the best approach.
23 |
--------------------------------------------------------------------------------
/tests/fastboot/errors-test.js:
--------------------------------------------------------------------------------
1 | import { module, test } from 'qunit';
2 | import { setup, visit } from 'ember-cli-fastboot-testing/test-support';
3 |
4 | module('Fastboot | errors', function (hooks) {
5 | setup(hooks);
6 |
7 | test('it displays fastboot errors', async function (assert) {
8 | await visit('/examples/errors/throw-message');
9 |
10 | assert.dom().includesText('This errors in FastBoot!');
11 | });
12 |
13 | test('it displays fastboot error objects', async function (assert) {
14 | await visit('/examples/errors/throw-error-object');
15 |
16 | assert.dom().includesText('This errors in FastBoot!');
17 | });
18 |
19 | test('it displays fastboot errors like trying to access document', async function (assert) {
20 | await visit('/examples/errors/access-document');
21 |
22 | assert.dom().includesText('ReferenceError: document is not defined');
23 | });
24 | });
25 |
--------------------------------------------------------------------------------
/tests/dummy/app/templates/index.hbs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
10 |
37 |
--------------------------------------------------------------------------------
/config/deploy.js:
--------------------------------------------------------------------------------
1 | /* eslint-env node */
2 | 'use strict';
3 |
4 | module.exports = function (deployTarget) {
5 | let ENV = {
6 | build: {},
7 | // include other plugin configuration that applies to all deploy targets here
8 | };
9 |
10 | if (deployTarget === 'development') {
11 | ENV.build.environment = 'development';
12 | // configure other plugins for development deploy target here
13 | }
14 |
15 | if (deployTarget === 'staging') {
16 | ENV.build.environment = 'production';
17 | // configure other plugins for staging deploy target here
18 | }
19 |
20 | if (deployTarget === 'production') {
21 | ENV.build.environment = 'production';
22 | // configure other plugins for production deploy target here
23 | }
24 |
25 | // Note: if you need to build some configuration asynchronously, you can return
26 | // a promise that resolves with the ENV object instead of returning the
27 | // ENV object synchronously.
28 | return ENV;
29 | };
30 |
--------------------------------------------------------------------------------
/tests/dummy/app/templates/docs/debugging.md:
--------------------------------------------------------------------------------
1 | # Debugging
2 |
3 | ## Pause test
4 |
5 | The `pauseTest` helper can be used to inspect FastBoot rendered HTML.
6 |
7 | ```js
8 | import { module, test, skip } from 'qunit';
9 | import { setup, visit } from 'ember-cli-fastboot-testing/test-support';
10 |
11 | module('FastBoot | debugging test', function(hooks) {
12 | setup(hooks);
13 |
14 | test('it renders the correct html', async function(assert) {
15 | await visit('/');
16 |
17 | await this.pauseTest();
18 |
19 | // ...
20 | });
21 | });
22 | ```
23 |
24 | ## Node debugging
25 |
26 | The following video shows how to use Node's built in debugging tools to inspect a FastBoot application.
27 |
28 |
29 |
36 |
37 |
--------------------------------------------------------------------------------
/tests/dummy/config/deploy.js:
--------------------------------------------------------------------------------
1 | /* eslint-env node */
2 | 'use strict';
3 |
4 | module.exports = function (deployTarget) {
5 | let ENV = {
6 | build: {},
7 | // include other plugin configuration that applies to all deploy targets here
8 | };
9 |
10 | if (deployTarget === 'development') {
11 | ENV.build.environment = 'development';
12 | // configure other plugins for development deploy target here
13 | }
14 |
15 | if (deployTarget === 'staging') {
16 | ENV.build.environment = 'production';
17 | // configure other plugins for staging deploy target here
18 | }
19 |
20 | if (deployTarget === 'production') {
21 | ENV.build.environment = 'production';
22 | // configure other plugins for production deploy target here
23 | }
24 |
25 | // Note: if you need to build some configuration asynchronously, you can return
26 | // a promise that resolves with the ENV object instead of returning the
27 | // ENV object synchronously.
28 | return ENV;
29 | };
30 |
--------------------------------------------------------------------------------
/tests/fastboot/mirage-interceptors-test.js:
--------------------------------------------------------------------------------
1 | import { module, test } from 'qunit';
2 | import { setup, visit } from 'ember-cli-fastboot-testing/test-support';
3 | import { setupMirage } from 'ember-cli-mirage/test-support';
4 |
5 | /*
6 | This test is setup to emulate the following scenario: Someone is using
7 | FastBoot testing as well as a local mirage server. When FastBoot testing
8 | asks ember-cli to render a page, mirage will intercept the http request and
9 | then say it didnt know how to handle the request.
10 |
11 | We want to provide a better error message when that happens.
12 | */
13 | module('Fastboot | mirage interceptor', function (hooks) {
14 | setup(hooks);
15 | setupMirage(hooks);
16 |
17 | test('it doesnt work if mirage blocks our http request to ember-cli', async function (assert) {
18 | assert.rejects(
19 | visit('/'),
20 | /It looks like Mirage is intercepting ember-cli-fastboot-testing's attempt to render \//,
21 | );
22 | });
23 | });
24 |
--------------------------------------------------------------------------------
/tests/fastboot/redirects-test.js:
--------------------------------------------------------------------------------
1 | import { module, test } from 'qunit';
2 | import { setup, visit } from 'ember-cli-fastboot-testing/test-support';
3 |
4 | module('FastBoot | redirects test', function (hooks) {
5 | setup(hooks);
6 |
7 | test('redirects with a transition to', async function (assert) {
8 | let { headers, statusCode, url } = await visit(
9 | '/examples/redirects/transition-to',
10 | );
11 |
12 | assert.strictEqual(statusCode, 307);
13 | assert.strictEqual(url, '/examples');
14 | assert.deepEqual(headers.location, [`//${window.location.host}/examples`]);
15 | });
16 |
17 | test('redirects with a replace with', async function (assert) {
18 | let { headers, statusCode, url } = await visit(
19 | '/examples/redirects/replace-with',
20 | );
21 |
22 | assert.strictEqual(statusCode, 307);
23 | assert.strictEqual(url, '/examples');
24 | assert.deepEqual(headers.location, [`//${window.location.host}/examples`]);
25 | });
26 | });
27 |
--------------------------------------------------------------------------------
/tests/dummy/app/templates/docs/videos.md:
--------------------------------------------------------------------------------
1 | # Videos
2 |
3 | Here are a number of videos using this addon with the [EmberMap](https://embermap.com) code base.
4 |
5 | ## Getting started
6 |
7 |
8 |
15 |
16 |
17 | ## Network mocking
18 |
19 |
20 |
27 |
28 |
29 | ## Debugging
30 |
31 |
32 |
39 |
40 |
--------------------------------------------------------------------------------
/tests/dummy/app/templates/docs/quickstart.md:
--------------------------------------------------------------------------------
1 | # Quickstart
2 |
3 | Install the addon.
4 |
5 | ```bash
6 | ember install ember-cli-fastboot-testing
7 | ```
8 |
9 | Use the generator to create your first fastboot test.
10 |
11 | ```bash
12 | ember g fastboot-test home-page
13 | ```
14 |
15 | Open the test file, `tests/fastboot/home-page-test.js`.
16 |
17 | ```js
18 | import { module, test } from 'qunit';
19 | import { setup, visit } from 'ember-cli-fastboot-testing/test-support';
20 |
21 | module('FastBoot | home-page test', function(hooks) {
22 | setup(hooks);
23 |
24 | test('it renders a page...', async function(assert) {
25 | await visit('/');
26 |
27 | // replace this line with a real assertion!
28 | assert.ok(true);
29 | });
30 |
31 | });
32 | ```
33 |
34 | Replace the assertion with one that verifies the title on your home page.
35 |
36 | ```diff
37 | - assert.ok(true);
38 | + assert.dom('h1').hasText('Welcome!');
39 | ```
40 |
41 | Now run your test suite and visit the testem URL.
42 |
43 | ```bash
44 | ember test --server
45 | ```
46 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2018
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6 |
7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8 |
9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
10 |
--------------------------------------------------------------------------------
/tests/fastboot/basic-test.js:
--------------------------------------------------------------------------------
1 | import { module, test } from 'qunit';
2 | import { setup, visit } from 'ember-cli-fastboot-testing/test-support';
3 |
4 | module('Fastboot | basic', function (hooks) {
5 | setup(hooks);
6 |
7 | test('it renders the correct h1 title', async function (assert) {
8 | await visit('/examples');
9 |
10 | assert.dom('h1').includesText('FastbootTesting');
11 | });
12 |
13 | test('it renders the correct og:title', async function (assert) {
14 | let { htmlDocument } = await visit('/examples');
15 |
16 | assert
17 | .dom('meta[property="og:title"]', htmlDocument)
18 | .hasAttribute('content', 'Fastboot testing');
19 | });
20 |
21 | test('it gets a success response code', async function (assert) {
22 | let { statusCode } = await visit('/examples');
23 |
24 | assert.strictEqual(statusCode, 200);
25 | });
26 |
27 | test('it preserves all query parameters', async function (assert) {
28 | await visit('/examples/query-parameters?first=1&second=2&third=3');
29 |
30 | assert.dom('h1').hasText('1 2 3');
31 | });
32 | });
33 |
--------------------------------------------------------------------------------
/tests/dummy/mirage/config.js:
--------------------------------------------------------------------------------
1 | import { createServer } from 'miragejs';
2 |
3 | export default function (config) {
4 | let finalConfig = {
5 | ...config,
6 | routes() {
7 | // These comments are here to help you get started. Feel free to delete them.
8 | /*
9 | Config (with defaults).
10 |
11 | Note: these only affect routes defined *after* them!
12 | */
13 | // this.urlPrefix = ''; // make this `http://localhost:8080`, for example, if your API is on a different server
14 | // this.namespace = ''; // make this `/api`, for example, if your API is namespaced
15 | // this.timing = 400; // delay for each request, automatically set to 0 during testing
16 | /*
17 | Shorthand cheatsheet:
18 |
19 | this.get('/notes');
20 | this.post('/notes');
21 | this.get('/notes/:id');
22 | this.put('/notes/:id'); // or this.patch
23 | this.del('/notes/:id');
24 |
25 | this.resource('user')
26 |
27 | http://www.ember-cli-mirage.com/docs/v0.4.x/shorthands/
28 | */
29 | },
30 | };
31 |
32 | return createServer(finalConfig);
33 | }
34 |
--------------------------------------------------------------------------------
/tests/dummy/config/fastboot-testing.js:
--------------------------------------------------------------------------------
1 | const najax = require('najax');
2 | const semver = require('semver');
3 | const { URL } = require('node:url');
4 |
5 | const version = require(require.resolve('fastboot/package.json')).version;
6 |
7 | if (semver.lt(version, '3.0.0')) {
8 | module.exports = {
9 | resilient: false,
10 | sandboxGlobals: {
11 | SampleGlobal: 'TestSampleGlobal',
12 | },
13 | };
14 | } else {
15 | module.exports = () => {
16 | return {
17 | resilient: false,
18 | buildSandboxGlobals(defaultGlobals) {
19 | return Object.assign({}, defaultGlobals, {
20 | SampleGlobal: 'TestSampleGlobal',
21 | najax,
22 | /**
23 | * Use WHATWG URL API.
24 | *
25 | * @see {@link https://github.com/glimmerjs/glimmer-vm/blob/master/packages/%40glimmer/runtime/lib/dom/sanitized-values.ts#L62}
26 | */
27 | URL,
28 | AbortController,
29 |
30 | // This is being actively used throughout codebase in many places.
31 | URLSearchParams,
32 | fetch: require('node-fetch'),
33 | });
34 | },
35 | };
36 | };
37 | }
38 |
--------------------------------------------------------------------------------
/tests/dummy/app/templates/docs.hbs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 | {{outlet}}
29 |
30 |
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/tests/dummy/app/templates/docs/document.md:
--------------------------------------------------------------------------------
1 | # Document tests
2 |
3 | There are some HTML elements generated by FastBoot that cannot be rendered inside of Ember's testing container. For example, rendering a FastBoot generated `` tag inside of the testing container would result in an invalid HTML document. A `` tag inside of a `
`, that's nonsense!
4 |
5 | However, a common use case for FastBoot is to generate `head`, `title`, and `meta` tags for crawlers and bots, so it would make sense to want to test that these tags are rendered correctly.
6 |
7 | To write these sort of tests, use the `htmlDocument` returned from `visit` to get a `Document` object that you can assert against with `qunit-dom`.
8 |
9 | ```js
10 | import { module, test, skip } from 'qunit';
11 | import { setup, visit } from 'ember-cli-fastboot-testing/test-support';
12 |
13 | module('FastBoot | document test', function(hooks) {
14 | setup(hooks);
15 |
16 | test('it renders the correct og:title', async function(assert) {
17 | let { htmlDocument } = await visit('/');
18 |
19 | assert
20 | .dom('head meta[property="og:title"]', htmlDocument)
21 | .hasAttribute('content', 'Fastboot testing');
22 | });
23 | });
24 | ```
25 |
--------------------------------------------------------------------------------
/.github/workflows/publish.yml:
--------------------------------------------------------------------------------
1 | # For every push to the primary branch with .release-plan.json modified,
2 | # runs release-plan.
3 |
4 | name: Publish Stable
5 |
6 | on:
7 | workflow_dispatch:
8 | push:
9 | branches:
10 | - main
11 | - master
12 | paths:
13 | - '.release-plan.json'
14 |
15 | concurrency:
16 | group: publish-${{ github.head_ref || github.ref }}
17 | cancel-in-progress: true
18 |
19 | jobs:
20 | publish:
21 | name: "NPM Publish"
22 | runs-on: ubuntu-latest
23 | permissions:
24 | contents: write
25 | id-token: write
26 | attestations: write
27 |
28 | steps:
29 | - uses: actions/checkout@v5
30 | - uses: pnpm/action-setup@v4
31 | with:
32 | version: 10
33 | - uses: actions/setup-node@v6
34 | with:
35 | node-version: 22
36 | registry-url: 'https://registry.npmjs.org'
37 | cache: pnpm
38 | - run: pnpm install --frozen-lockfile
39 | - run: npm install -g npm@latest # ensure that the globally installed npm is new enough to support OIDC
40 | - name: Publish to NPM
41 | run: NPM_CONFIG_PROVENANCE=true pnpm release-plan publish
42 | env:
43 | GITHUB_AUTH: ${{ secrets.GITHUB_TOKEN }}
44 |
--------------------------------------------------------------------------------
/tests/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Dummy Tests
6 |
7 |
8 |
9 | {{content-for "head"}}
10 | {{content-for "test-head"}}
11 |
12 |
13 |
14 |
15 |
16 | {{content-for "head-footer"}}
17 | {{content-for "test-head-footer"}}
18 |
19 |
20 | {{content-for "body"}}
21 | {{content-for "test-body"}}
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 | {{content-for "body-footer"}}
37 | {{content-for "test-body-footer"}}
38 |
39 |
40 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Ember CLI Fastboot Testing
2 |
3 | [](http://badge.fury.io/js/ember-cli-fastboot-testing)
4 | [](https://github.com/embermap/ember-cli-fastboot-testing/actions/workflows/ci.yml)
5 |
6 | A collection of APIs for testing Fastboot rendered applications.
7 |
8 | [View the docs here](https://embermap.github.io/ember-cli-fastboot-testing/).
9 |
10 | ## Feature requests
11 |
12 | Please open an issue and add a :+1: emoji reaction. We will use the number of reactions as votes to indicate community interest, which will in turn help us prioritize feature development.
13 |
14 | You can view the most-upvoted feature requests with [this link](https://github.com/embermap/ember-cli-fastboot-testing/issues?utf8=✓&q=is%3Aissue+is%3Aopen+label%3A%22Feature+%2F+Enhancement%22+sort%3Areactions-%2B1-desc).
15 |
16 | ## About
17 |
18 | This library is developed and maintained by [EmberMap](https://embermap.com/). If your company is looking to see how it can get the most out of Ember, please [get in touch](mailto:info@embermap.com)!
19 |
20 | ## Contributing
21 |
22 | See the [Contributing](CONTRIBUTING.md) guide for details.
23 |
24 | ## License
25 |
26 | This project is licensed under the [MIT License](LICENSE.md).
27 |
--------------------------------------------------------------------------------
/tests/helpers/index.js:
--------------------------------------------------------------------------------
1 | import {
2 | setupApplicationTest as upstreamSetupApplicationTest,
3 | setupRenderingTest as upstreamSetupRenderingTest,
4 | setupTest as upstreamSetupTest,
5 | } from 'ember-qunit';
6 |
7 | // This file exists to provide wrappers around ember-qunit's
8 | // test setup functions. This way, you can easily extend the setup that is
9 | // needed per test type.
10 |
11 | function setupApplicationTest(hooks, options) {
12 | upstreamSetupApplicationTest(hooks, options);
13 |
14 | // Additional setup for application tests can be done here.
15 | //
16 | // For example, if you need an authenticated session for each
17 | // application test, you could do:
18 | //
19 | // hooks.beforeEach(async function () {
20 | // await authenticateSession(); // ember-simple-auth
21 | // });
22 | //
23 | // This is also a good place to call test setup functions coming
24 | // from other addons:
25 | //
26 | // setupIntl(hooks, 'en-us'); // ember-intl
27 | // setupMirage(hooks); // ember-cli-mirage
28 | }
29 |
30 | function setupRenderingTest(hooks, options) {
31 | upstreamSetupRenderingTest(hooks, options);
32 |
33 | // Additional setup for rendering tests can be done here.
34 | }
35 |
36 | function setupTest(hooks, options) {
37 | upstreamSetupTest(hooks, options);
38 |
39 | // Additional setup for unit tests can be done here.
40 | }
41 |
42 | export { setupApplicationTest, setupRenderingTest, setupTest };
43 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | module.exports = {
4 | globals: {
5 | server: true,
6 | },
7 | root: true,
8 | parser: '@babel/eslint-parser',
9 | parserOptions: {
10 | ecmaVersion: 'latest',
11 | sourceType: 'module',
12 | requireConfigFile: false,
13 | babelOptions: {
14 | plugins: [
15 | ['@babel/plugin-proposal-decorators', { decoratorsBeforeExport: true }],
16 | ],
17 | },
18 | },
19 | plugins: ['ember'],
20 | extends: [
21 | 'eslint:recommended',
22 | 'plugin:ember/recommended',
23 | 'plugin:prettier/recommended',
24 | ],
25 | env: {
26 | browser: true,
27 | },
28 | rules: {},
29 | overrides: [
30 | // node files
31 | {
32 | files: [
33 | './.eslintrc.js',
34 | './.prettierrc.js',
35 | './.stylelintrc.js',
36 | './.template-lintrc.js',
37 | './ember-cli-build.js',
38 | './index.js',
39 | './testem.js',
40 | './blueprints/*/index.js',
41 | './config/**/*.js',
42 | './lib/helpers.js',
43 | './tests/dummy/config/**/*.js',
44 | ],
45 | parserOptions: {
46 | sourceType: 'script',
47 | },
48 | env: {
49 | browser: false,
50 | node: true,
51 | },
52 | extends: ['plugin:n/recommended'],
53 | },
54 | {
55 | // test files
56 | files: ['tests/**/*-test.{js,ts}'],
57 | extends: ['plugin:qunit/recommended'],
58 | },
59 | ],
60 | };
61 |
--------------------------------------------------------------------------------
/tests/dummy/app/templates/docs/status-code.md:
--------------------------------------------------------------------------------
1 | # Status code tests
2 |
3 | In an Ember application the `index.html` file acts as the application's bootstrap file. It loads all of the JavaScript and then initializes the application.
4 |
5 | If we think about this, that means that every Ember application's initial render starts off as an HTTP 200. Once the application is fully initialized we can then decide on the client if we should redirect to another route, or render one of Ember's error templates.
6 |
7 | However, once we introduce FastBoot and have our application running on the server, we get the ability to use other HTTP status codes.
8 |
9 | We can use 300 status codes for routes that redirect, and 400/500 status codes when there is a rendering error.
10 |
11 | FastBoot testing provides support for testing status codes. The `visit` helper will return a `statusCode` with every request it makes.
12 |
13 |
14 | ```js
15 | import { module, test, skip } from 'qunit';
16 | import { setup, visit } from 'ember-cli-fastboot-testing/test-support';
17 |
18 | module('FastBoot | status code test', function(hooks) {
19 | setup(hooks);
20 |
21 | test('it gets a success response code', async function(assert) {
22 | let { statusCode } = await visit('/');
23 |
24 | assert.equal(statusCode, 200);
25 | });
26 |
27 | test('it gets a 404 response code', async function(assert) {
28 | let { statusCode } = await visit('/file-not-found');
29 |
30 | assert.equal(statusCode, 404);
31 | });
32 | });
33 | ```
34 |
--------------------------------------------------------------------------------
/addon-test-support/-private/mock-server.js:
--------------------------------------------------------------------------------
1 | import { fetch } from 'whatwg-fetch';
2 |
3 | let createMock = function (
4 | path,
5 | method,
6 | statusCode,
7 | response,
8 | responseHeaders,
9 | ) {
10 | let origin = false;
11 |
12 | if (path.startsWith('http')) {
13 | const url = new URL(path);
14 | origin = url.origin;
15 | path = `${url.pathname}${url.search}`;
16 | }
17 |
18 | return fetch('/__mock-request', {
19 | method: 'post',
20 | headers: {
21 | 'Content-Type': 'application/json',
22 | },
23 | body: JSON.stringify({
24 | path,
25 | method,
26 | statusCode,
27 | response,
28 | responseHeaders,
29 | origin,
30 | }),
31 | });
32 | };
33 |
34 | export let mockServer = {
35 | get(path, response, status = 200, responseHeaders) {
36 | return createMock(path, 'GET', status, response, responseHeaders);
37 | },
38 |
39 | post(path, response, status = 200, responseHeaders) {
40 | return createMock(path, 'POST', status, response, responseHeaders);
41 | },
42 |
43 | patch(path, response, status = 200, responseHeaders) {
44 | return createMock(path, 'PATCH', status, response, responseHeaders);
45 | },
46 |
47 | put(path, response, status = 200, responseHeaders) {
48 | return createMock(path, 'PUT', status, response, responseHeaders);
49 | },
50 |
51 | delete(path, response, status = 200, responseHeaders) {
52 | return createMock(path, 'DELETE', status, response, responseHeaders);
53 | },
54 |
55 | cleanUp() {
56 | return fetch('/__cleanup-mocks');
57 | },
58 | };
59 |
--------------------------------------------------------------------------------
/RELEASE.md:
--------------------------------------------------------------------------------
1 | # Release Process
2 |
3 | Releases in this repo are mostly automated using [release-plan](https://github.com/embroider-build/release-plan/). Once you label all your PRs correctly (see below) you will have an automatically generated PR that updates your CHANGELOG.md file and a `.release-plan.json` that is used to prepare the release once the PR is merged.
4 |
5 | ## Preparation
6 |
7 | Since the majority of the actual release process is automated, the remaining tasks before releasing are:
8 |
9 | - correctly labeling **all** pull requests that have been merged since the last release
10 | - updating pull request titles so they make sense to our users
11 |
12 | Some great information on why this is important can be found at [keepachangelog.com](https://keepachangelog.com/en/1.1.0/), but the overall
13 | guiding principle here is that changelogs are for humans, not machines.
14 |
15 | When reviewing merged PR's the labels to be used are:
16 |
17 | - breaking - Used when the PR is considered a breaking change.
18 | - enhancement - Used when the PR adds a new feature or enhancement.
19 | - bug - Used when the PR fixes a bug included in a previous release.
20 | - documentation - Used when the PR adds or updates documentation.
21 | - internal - Internal changes or things that don't fit in any other category.
22 |
23 | **Note:** `release-plan` requires that **all** PRs are labeled. If a PR doesn't fit in a category it's fine to label it as `internal`
24 |
25 | ## Release
26 |
27 | Once the prep work is completed, the actual release is straight forward: you just need to merge the open [Plan Release](https://github.com/embermap/ember-cli-fastboot-testing/pulls?q=is%3Apr+is%3Aopen+%22Prepare+Release%22+in%3Atitle) PR
28 |
--------------------------------------------------------------------------------
/tests/dummy/app/router.js:
--------------------------------------------------------------------------------
1 | import AddonDocsRouter, { docsRoute } from 'ember-cli-addon-docs/router';
2 | import config from 'dummy/config/environment';
3 |
4 | export default class Router extends AddonDocsRouter {
5 | location = config.locationType;
6 | rootURL = config.rootURL;
7 | }
8 |
9 | Router.map(function () {
10 | docsRoute(this, function () {
11 | this.route('quickstart');
12 | this.route('debugging');
13 | this.route('videos');
14 | this.route('html');
15 | this.route('document');
16 | this.route('status-code');
17 | this.route('visit');
18 | this.route('network-mocking');
19 | this.route('fastboot-configuration');
20 | });
21 |
22 | this.route('examples', function () {
23 | this.route('redirects', function () {
24 | this.route('transition-to');
25 | this.route('replace-with');
26 | });
27 |
28 | this.route('query-parameters');
29 | this.route('visit-customization');
30 | this.route('global-config');
31 |
32 | this.route('request-object');
33 |
34 | this.route('network', function () {
35 | this.route('notes', function () {
36 | this.route('note', { path: ':note_id' });
37 | });
38 |
39 | this.route('other', function () {
40 | this.route('get-request');
41 | this.route('headers');
42 | this.route('post-request');
43 | this.route('origin-override');
44 | this.route('echo');
45 | });
46 | });
47 |
48 | this.route('errors', function () {
49 | this.route('throw-message');
50 | this.route('throw-error-object');
51 | this.route('access-document');
52 | });
53 | });
54 |
55 | this.route('not-found', { path: '/*path' });
56 | });
57 |
--------------------------------------------------------------------------------
/tests/dummy/config/environment.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | module.exports = function (environment) {
4 | const ENV = {
5 | modulePrefix: 'dummy',
6 | podModulePrefix: 'dummy/pods',
7 | environment,
8 | rootURL: '/',
9 | locationType: 'history',
10 | originForOverride: 'http://localhost:3000',
11 | EmberENV: {
12 | EXTEND_PROTOTYPES: false,
13 | FEATURES: {
14 | // Here you can enable experimental features on an ember canary build
15 | // e.g. EMBER_NATIVE_DECORATOR_SUPPORT: true
16 | },
17 | },
18 |
19 | APP: {
20 | // Here you can pass flags/options to your application instance
21 | // when it is created
22 | },
23 | fastboot: {
24 | hostWhitelist: [/^localhost:\d+$/],
25 | },
26 | 'ember-cli-mirage': {
27 | enabled: false,
28 | },
29 | };
30 |
31 | if (environment === 'development') {
32 | // ENV.APP.LOG_RESOLVER = true;
33 | // ENV.APP.LOG_ACTIVE_GENERATION = true;
34 | // ENV.APP.LOG_TRANSITIONS = true;
35 | // ENV.APP.LOG_TRANSITIONS_INTERNAL = true;
36 | // ENV.APP.LOG_VIEW_LOOKUPS = true;
37 | }
38 |
39 | if (environment === 'test') {
40 | // Testem prefers this...
41 | ENV.locationType = 'none';
42 |
43 | // keep test console output quieter
44 | ENV.APP.LOG_ACTIVE_GENERATION = false;
45 | ENV.APP.LOG_VIEW_LOOKUPS = false;
46 |
47 | ENV.APP.rootElement = '#ember-testing';
48 | ENV.APP.autoboot = false;
49 | }
50 |
51 | if (environment === 'production') {
52 | // Allow ember-cli-addon-docs to update the rootURL in compiled assets
53 | ENV.rootURL = 'ADDON_DOCS_ROOT_URL';
54 | // here you can enable a production-specific feature
55 | }
56 |
57 | return ENV;
58 | };
59 |
--------------------------------------------------------------------------------
/tests/dummy/app/pods/examples/request-object/template.hbs:
--------------------------------------------------------------------------------
1 |
The request object
2 |
3 |
4 |
5 |
6 |
7 |
8 | Host
9 |
10 |
11 | {{this.request.host}}
12 |
13 |
14 |
15 |
16 |
17 | Protocol
18 |
19 |
20 | {{this.request.protocol}}
21 |
22 |
23 |
24 |
25 |
26 | Headers
27 |
28 |
29 | {{#each-in this.request.headers.headers as |header value|}}
30 |
31 | {{header}}: {{value}}
32 |
33 | {{/each-in}}
34 |
35 |
36 |
37 |
38 |
39 | Query params
40 |
41 |
42 | {{#each-in this.request.queryParams as |param value|}}
43 |
44 | {{param}}: {{value}}
45 |
46 | {{/each-in}}
47 |
48 |
49 |
50 |
51 |
52 | Path
53 |
54 |
55 | {{this.request.path}}
56 |
57 |
58 |
59 |
60 |
61 | Method
62 |
63 |
64 | {{this.request.method}}
65 |
66 |
67 |
68 |
69 |
70 | Cookies
71 |
72 |
73 | {{#each-in this.request.cookies as |cookie value|}}
74 |
75 | {{cookie}}: {{value}}
76 |
77 | {{/each-in}}
78 |
79 |
80 |
81 |
82 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | ## Release (2025-12-03)
4 |
5 | * ember-cli-fastboot-testing 0.7.0 (major)
6 |
7 | #### :boom: Breaking Change
8 | * `ember-cli-fastboot-testing`
9 | * [#874](https://github.com/embermap/ember-cli-fastboot-testing/pull/874) Drop support for Node.js v16 and below ([@SergeAstapov](https://github.com/SergeAstapov))
10 |
11 | #### :rocket: Enhancement
12 | * `ember-cli-fastboot-testing`
13 | * [#875](https://github.com/embermap/ember-cli-fastboot-testing/pull/875) Sync with latest addon blueprint via ember-cli-update; widen ember-source in peer deps ([@SergeAstapov](https://github.com/SergeAstapov))
14 |
15 | #### :bug: Bug Fix
16 | * `ember-cli-fastboot-testing`
17 | * [#923](https://github.com/embermap/ember-cli-fastboot-testing/pull/923) Avoid eagerly importing nock outside of testing ([@sergey-panov](https://github.com/sergey-panov))
18 |
19 | #### :house: Internal
20 | * `ember-cli-fastboot-testing`
21 | * [#926](https://github.com/embermap/ember-cli-fastboot-testing/pull/926) switch to pnpm ([@SergeAstapov](https://github.com/SergeAstapov))
22 | * [#925](https://github.com/embermap/ember-cli-fastboot-testing/pull/925) Prepare Release v0.7.0 ([@github-actions[bot]](https://github.com/apps/github-actions))
23 | * [#924](https://github.com/embermap/ember-cli-fastboot-testing/pull/924) Setup release-plan ([@SergeAstapov](https://github.com/SergeAstapov))
24 | * [#876](https://github.com/embermap/ember-cli-fastboot-testing/pull/876) Drop fastboot@1 and fastboot@2 from testing matrix ([@SergeAstapov](https://github.com/SergeAstapov))
25 |
26 | #### Committers: 3
27 | - Sergey Astapov ([@SergeAstapov](https://github.com/SergeAstapov))
28 | - Sergey Panov ([@sergey-panov](https://github.com/sergey-panov))
29 | - [@github-actions[bot]](https://github.com/apps/github-actions)
30 |
--------------------------------------------------------------------------------
/tests/fastboot/generic-interceptors-test.js:
--------------------------------------------------------------------------------
1 | import { module, test } from 'qunit';
2 | import { setup, visit } from 'ember-cli-fastboot-testing/test-support';
3 | import Pretender from 'pretender';
4 |
5 | /*
6 | This test is setup to emulate the following scenario: Someone is using
7 | FastBoot testing as well as an http interceptor for mocking.
8 | When FastBoot testing asks ember-cli to render a page, the interceptor
9 | will block the http request and attempt to return a mock.
10 |
11 | We'll simulate this by overriding fetch.
12 |
13 | We want to provide a better error message when that happens.
14 | */
15 | module('Fastboot | generic interceptor', function (hooks) {
16 | setup(hooks);
17 |
18 | test('it doesnt work if an interceptor blocks our request to ember-cli', async function (assert) {
19 | let server = new Pretender(function () {
20 | this.post('/__fastboot-testing', () => {
21 | throw new Error('Blocked!');
22 | });
23 | });
24 |
25 | assert.rejects(
26 | visit('/'),
27 | /We were unable to render \/. Is your test suite blocking or intercepting HTTP requests\? Error: Pretender intercepted POST \/__fastboot-testing but encountered an error: Blocked/,
28 | );
29 |
30 | server.shutdown();
31 | });
32 |
33 | test('mocked response', function (assert) {
34 | let server = new Pretender(function () {
35 | this.get('/__fastboot-testing', () => {
36 | return [
37 | 200,
38 | { 'Content-Type': 'application/json' },
39 | JSON.stringify({ mocked: true }),
40 | ];
41 | });
42 | });
43 |
44 | assert.rejects(
45 | visit('/'),
46 | /We were unable to render \/. Is your test suite blocking or intercepting HTTP requests\?/,
47 | );
48 |
49 | server.shutdown();
50 | });
51 | });
52 |
--------------------------------------------------------------------------------
/tests/dummy/app/templates/docs/index.md:
--------------------------------------------------------------------------------
1 | # Why test FastBoot?
2 |
3 | FastBoot applications run on the server inside of a node process that does not support the full DOM specification. The server's minimal DOM implementation is optimized for Ember's view layer, but missing APIs often lead to surprising and unexpected bugs.
4 |
5 | Ember has an amazing suite of testing tools built into the framework. As Ember developers, we've come to rely on a passing test suite as a sign the application is ready to be deployed.
6 |
7 | However, there's a catch. The test suite runs the application in the browser, not FastBoot. On one hand, this makes sense since most of our tests are focused on user behavior and interactions, not initial page renders. On the other hand, we're asking for trouble if we haven't run any tests against FastBoot or its DOM implementation.
8 |
9 | Take this code example. It's a button component that renders with a loading spinner inside of it. Whenever this component re-renders itself, we need to slightly adjust the width of the button so that it can fit the spinner.
10 |
11 | ```js
12 | Component.extend({
13 |
14 | didRender() {
15 | this._super(...arguments);
16 |
17 | // When the component is rendered with a loading spinner we
18 | // need to figure out how to adjust the left margin so we can
19 | // make room for the spinner
20 | let spinnerWidth = this.element
21 | .querySelector('[data-loading-spinner]')
22 | .offsetWidth();
23 | let offset = spinnerWidth / 2;
24 |
25 | this.set('spinnerOffset', `margin-left: -${offset}px`);
26 | }
27 |
28 | })
29 | ```
30 |
31 | This code relies on the `offsetWidth` of an element in our template preform this calculation. We've got this code tested in our application and it's passing, but when we go to render a page using this component in FastBoot we get a 500 error.
32 |
33 | That's because there's there's no `document` API in FastBoot, so this code crashes our application even though our test suite is passing.
34 |
35 | Wouldn't it be great if we could run our test suite in FastBoot? That way, we know there's no code in our application that's incompatible with FastBoot's DOM API.
36 |
37 | Well, that's exactly what this addon aims to do!
38 |
--------------------------------------------------------------------------------
/.github/workflows/plan-release.yml:
--------------------------------------------------------------------------------
1 | name: Plan Release
2 | on:
3 | workflow_dispatch:
4 | push:
5 | branches:
6 | - main
7 | - master
8 | pull_request_target: # This workflow has permissions on the repo, do NOT run code from PRs in this workflow. See https://securitylab.github.com/research/github-actions-preventing-pwn-requests/
9 | types:
10 | - labeled
11 | - unlabeled
12 |
13 | concurrency:
14 | group: plan-release # only the latest one of these should ever be running
15 | cancel-in-progress: true
16 |
17 | jobs:
18 | should-run-release-plan-prepare:
19 | name: Should we run release-plan prepare?
20 | runs-on: ubuntu-latest
21 | outputs:
22 | should-prepare: ${{ steps.should-prepare.outputs.should-prepare }}
23 | steps:
24 | - uses: release-plan/actions/should-prepare-release@v1
25 | with:
26 | ref: 'master'
27 | id: should-prepare
28 |
29 | create-prepare-release-pr:
30 | name: Create Prepare Release PR
31 | runs-on: ubuntu-latest
32 | timeout-minutes: 5
33 | needs: should-run-release-plan-prepare
34 | permissions:
35 | contents: write
36 | issues: read
37 | pull-requests: write
38 | if: needs.should-run-release-plan-prepare.outputs.should-prepare == 'true'
39 | steps:
40 | - uses: release-plan/actions/prepare@v1
41 | name: Run release-plan prepare
42 | with:
43 | ref: 'master'
44 | env:
45 | GITHUB_AUTH: ${{ secrets.GITHUB_TOKEN }}
46 | id: explanation
47 |
48 | - uses: peter-evans/create-pull-request@v7
49 | name: Create Prepare Release PR
50 | with:
51 | commit-message: "Prepare Release ${{ steps.explanation.outputs.new-version}} using 'release-plan'"
52 | labels: "internal"
53 | sign-commits: true
54 | branch: release-preview
55 | title: Prepare Release ${{ steps.explanation.outputs.new-version }}
56 | body: |
57 | This PR is a preview of the release that [release-plan](https://github.com/embroider-build/release-plan) has prepared. To release you should just merge this PR 👍
58 |
59 | -----------------------------------------
60 |
61 | ${{ steps.explanation.outputs.text }}
62 |
--------------------------------------------------------------------------------
/.release-plan.json:
--------------------------------------------------------------------------------
1 | {
2 | "solution": {
3 | "ember-cli-fastboot-testing": {
4 | "impact": "major",
5 | "oldVersion": "0.6.2",
6 | "newVersion": "0.7.0",
7 | "tagName": "latest",
8 | "constraints": [
9 | {
10 | "impact": "major",
11 | "reason": "Appears in changelog section :boom: Breaking Change"
12 | },
13 | {
14 | "impact": "minor",
15 | "reason": "Appears in changelog section :rocket: Enhancement"
16 | },
17 | {
18 | "impact": "patch",
19 | "reason": "Appears in changelog section :bug: Bug Fix"
20 | },
21 | {
22 | "impact": "patch",
23 | "reason": "Appears in changelog section :house: Internal"
24 | }
25 | ],
26 | "pkgJSONPath": "./package.json"
27 | }
28 | },
29 | "description": "## Release (2025-12-03)\n\n* ember-cli-fastboot-testing 0.7.0 (major)\n\n#### :boom: Breaking Change\n* `ember-cli-fastboot-testing`\n * [#874](https://github.com/embermap/ember-cli-fastboot-testing/pull/874) Drop support for Node.js v16 and below ([@SergeAstapov](https://github.com/SergeAstapov))\n\n#### :rocket: Enhancement\n* `ember-cli-fastboot-testing`\n * [#875](https://github.com/embermap/ember-cli-fastboot-testing/pull/875) Sync with latest addon blueprint via ember-cli-update; widen ember-source in peer deps ([@SergeAstapov](https://github.com/SergeAstapov))\n\n#### :bug: Bug Fix\n* `ember-cli-fastboot-testing`\n * [#923](https://github.com/embermap/ember-cli-fastboot-testing/pull/923) Avoid eagerly importing nock outside of testing ([@sergey-panov](https://github.com/sergey-panov))\n\n#### :house: Internal\n* `ember-cli-fastboot-testing`\n * [#926](https://github.com/embermap/ember-cli-fastboot-testing/pull/926) switch to pnpm ([@SergeAstapov](https://github.com/SergeAstapov))\n * [#925](https://github.com/embermap/ember-cli-fastboot-testing/pull/925) Prepare Release v0.7.0 ([@github-actions[bot]](https://github.com/apps/github-actions))\n * [#924](https://github.com/embermap/ember-cli-fastboot-testing/pull/924) Setup release-plan ([@SergeAstapov](https://github.com/SergeAstapov))\n * [#876](https://github.com/embermap/ember-cli-fastboot-testing/pull/876) Drop fastboot@1 and fastboot@2 from testing matrix ([@SergeAstapov](https://github.com/SergeAstapov))\n\n#### Committers: 3\n- Sergey Astapov ([@SergeAstapov](https://github.com/SergeAstapov))\n- Sergey Panov ([@sergey-panov](https://github.com/sergey-panov))\n- [@github-actions[bot]](https://github.com/apps/github-actions)\n"
30 | }
31 |
--------------------------------------------------------------------------------
/tests/dummy/app/templates/docs/visit.md:
--------------------------------------------------------------------------------
1 | # Visit helper
2 |
3 | The `visit` helper is the main API behind this addon. This function allows you to render a page generated by FastBoot into Ember's testing container.
4 |
5 | ```js
6 | import { module, test } from 'qunit';
7 | import { setup, visit } from 'ember-cli-fastboot-testing/test-support';
8 |
9 | module('FastBoot | home-page test', function(hooks) {
10 | setup(hooks);
11 |
12 | test('it renders a page...', async function(assert) {
13 | await visit('/');
14 |
15 | // ...
16 | });
17 |
18 | });
19 | ```
20 |
21 | ## Options
22 |
23 | Visit can take an optional second parameter that lets you control the request sent to FastBoot. Many FastBoot applications will have conditional logic based on the incoming HTTP request. For example, you may want to render a different template based on the requesting browser's user agent.
24 |
25 | ### HTTP Headers
26 |
27 | We can generate a FastBoot request with different HTTP headers using `visit`'s options.
28 |
29 | ```js
30 | import { module, test } from 'qunit';
31 | import { setup, visit } from 'ember-cli-fastboot-testing/test-support';
32 |
33 | module('FastBoot | home-page test', function(hooks) {
34 | setup(hooks);
35 |
36 | test('it renders a page when given a specific user-agent', async function(assert) {
37 | await visit('/', {
38 | headers: {
39 | 'user-agent': 'A crawling bot!'
40 | }
41 | });
42 |
43 | // ...
44 | });
45 |
46 | });
47 | ```
48 |
49 | The headers object can be configured with any HTTP header. You can change the hostname or set a logged in user cookie.
50 |
51 | ```js
52 | await visit('/', {
53 | headers: {
54 | 'cookie': 'user_id=12345;',
55 | 'host': 'example.com'
56 | }
57 | });
58 | ```
59 |
60 | ### Customizing App.visit
61 |
62 | FastBoot's main entry point is its `App.visit` API. This method normally takes a URL and an options object that contains request and response data. However, heavily customized FastBoot applications will sometimes require additional data to be present in the options object.
63 |
64 | FastBoot testing will pass through any options given to the visit helper when rendering pages in FastBoot. For example, here's how the following test code will translate to a FastBoot render.
65 |
66 | ```js
67 | test('It can pass custom options to fastboot', async function(assert) {
68 | await visit('/', {
69 | meta: {
70 | version: '1.2.3'
71 | }
72 | });
73 | });
74 |
75 | // This will render FastBoot using the following
76 |
77 | fastbootApp.visit('/', { meta: { version: '1.2.3' }});
78 | ```
79 |
--------------------------------------------------------------------------------
/tests/dummy/config/ember-try.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const getChannelURL = require('ember-source-channel-url');
4 | const { embroiderSafe, embroiderOptimized } = require('@embroider/test-setup');
5 |
6 | module.exports = async function () {
7 | return {
8 | useYarn: true,
9 | scenarios: [
10 | {
11 | name: 'ember-lts-3.28',
12 | npm: {
13 | devDependencies: {
14 | 'ember-cli': '~4.12.0',
15 | 'ember-source': '~3.28.0',
16 | },
17 | },
18 | },
19 | {
20 | name: 'ember-lts-4.4',
21 | npm: {
22 | devDependencies: {
23 | 'ember-source': '~4.4.0',
24 | },
25 | },
26 | },
27 | {
28 | name: 'ember-lts-4.8',
29 | npm: {
30 | devDependencies: {
31 | 'ember-source': '~4.8.0',
32 | },
33 | },
34 | },
35 | {
36 | name: 'ember-lts-4.12',
37 | npm: {
38 | devDependencies: {
39 | 'ember-source': '~4.12.0',
40 | },
41 | },
42 | },
43 | {
44 | name: 'ember-lts-5.4',
45 | npm: {
46 | devDependencies: {
47 | 'ember-source': '~5.4.0',
48 | },
49 | },
50 | },
51 | {
52 | name: 'ember-lts-5.8',
53 | npm: {
54 | devDependencies: {
55 | 'ember-source': '~5.8.0',
56 | },
57 | },
58 | },
59 | {
60 | name: 'fastboot-3.0',
61 | npm: {
62 | dependencies: {
63 | fastboot: '^3.0.0',
64 | },
65 | devDependencies: {
66 | 'ember-cli-fastboot': '^3.0.0',
67 | 'ember-source': '~4.12.0',
68 | },
69 | // ember-cli-fastboot v3 incorrectly passed args to json-stable-stringify
70 | // which results in TypeError in latest versions of json-stable-stringify,
71 | // hence pinning.
72 | resolutions: {
73 | 'json-stable-stringify': '1.0.2',
74 | },
75 | },
76 | },
77 | {
78 | name: 'ember-release',
79 | npm: {
80 | devDependencies: {
81 | 'ember-source': await getChannelURL('release'),
82 | },
83 | },
84 | },
85 | {
86 | name: 'ember-beta',
87 | npm: {
88 | devDependencies: {
89 | 'ember-source': await getChannelURL('beta'),
90 | },
91 | },
92 | },
93 | {
94 | name: 'ember-canary',
95 | npm: {
96 | devDependencies: {
97 | 'ember-source': await getChannelURL('canary'),
98 | },
99 | },
100 | },
101 | embroiderSafe(),
102 | embroiderOptimized(),
103 | ],
104 | };
105 | };
106 |
--------------------------------------------------------------------------------
/tests/fastboot/request-object-test.js:
--------------------------------------------------------------------------------
1 | import { module, test } from 'qunit';
2 | import { setup, visit } from 'ember-cli-fastboot-testing/test-support';
3 |
4 | module('FastBoot | request object test', function (hooks) {
5 | setup(hooks);
6 |
7 | test('it has a host', async function (assert) {
8 | await visit('/examples/request-object');
9 |
10 | assert.dom('[data-test-id=host]').includesText(window.location.host);
11 | });
12 |
13 | test('it has a protocol', async function (assert) {
14 | await visit('/examples/request-object');
15 |
16 | assert.dom('[data-test-id=protocol]').includesText('http:');
17 | });
18 |
19 | test('it has headers', async function (assert) {
20 | await visit('/examples/request-object');
21 |
22 | assert.dom('[data-test-id=headers]').includesText('host: localhost');
23 | });
24 |
25 | test('it has user agent in headers', async function (assert) {
26 | await visit('/examples/request-object');
27 |
28 | assert
29 | .dom('[data-test-id=headers] [data-header-name=user-agent]')
30 | .includesText('user-agent: ');
31 | });
32 |
33 | test('it can override header in visit request', async function (assert) {
34 | await visit('/examples/request-object', {
35 | headers: {
36 | 'user-agent': 'ember-cli-fastboot-testing',
37 | },
38 | });
39 |
40 | assert
41 | .dom('[data-test-id=headers] [data-header-name=user-agent]')
42 | .includesText('user-agent: ember-cli-fastboot-testing');
43 | });
44 |
45 | test('it includes cookies in request headers', async function (assert) {
46 | // Make sure at least *some* cookie is present otherwise cookie header does not exist.
47 | document.cookie = 'foo=bar; path=/';
48 |
49 | await visit('/examples/request-object');
50 |
51 | assert
52 | .dom('[data-test-id=headers] [data-header-name=cookie]')
53 | .includesText('cookie: ');
54 |
55 | // Delete dummy cookie
56 | document.cookie = 'foo=bar; path=/; expires=Thu, 01 Jan 1970 00:00:01 GMT';
57 | });
58 |
59 | test('it can specify cookies header in visit request', async function (assert) {
60 | await visit('/examples/request-object', {
61 | headers: {
62 | cookie: 'test_cookie=test; user_id=what-user005;',
63 | },
64 | });
65 |
66 | assert
67 | .dom('[data-test-id=headers] [data-header-name=cookie]')
68 | .includesText('cookie: test_cookie=test; user_id=what-user005;');
69 | });
70 |
71 | test('it has query params', async function (assert) {
72 | await visit('/examples/request-object?testing=true');
73 |
74 | assert.dom('[data-test-id=query-params]').includesText('testing: true');
75 | });
76 |
77 | test('it has a path', async function (assert) {
78 | await visit('/examples/request-object');
79 |
80 | assert.dom('[data-test-id=path]').includesText('/examples/request-object');
81 | });
82 |
83 | test('it has a method', async function (assert) {
84 | await visit('/examples/request-object');
85 |
86 | assert.dom('[data-test-id=method]').includesText('GET');
87 | });
88 | });
89 |
--------------------------------------------------------------------------------
/tests/dummy/app/templates/docs/network-mocking.md:
--------------------------------------------------------------------------------
1 | # Network mocking
2 |
3 | Just about every Ember application ends up having pages that depend on data from an external API. It's common to use tools like [Mirage](https://www.ember-cli-mirage.com/) to mock the network when testing these applications.
4 |
5 | It makes sense that we would also want to mock the network while running FastBoot tests. However, since FastBoot runs inside of Node.js, we'll need to use a network mocking library that is written for node.
6 |
7 | FastBoot Testing exposes an API for intercepting requests made from your FastBoot app.
8 |
9 | The following example shows how to mock a test that fetches note data.
10 |
11 | ```js
12 | import { module, test } from 'qunit';
13 | import { setup, visit, mockServer } from 'ember-cli-fastboot-testing/test-support';
14 |
15 | module('Fastboot | Note page', function(hooks) {
16 | setup(hooks);
17 |
18 | test('it can render a note', async function(assert) {
19 | await mockServer.get('/api/notes/1', {
20 | data: {
21 | type: 'note',
22 | id: '1',
23 | attributes: {
24 | title: 'Hello world!'
25 | }
26 | }
27 | });
28 |
29 | await visit('/notes/1');
30 |
31 | assert.dom('[data-test-id="title"]').hasText("Hello world!");
32 | });
33 | });
34 | ```
35 |
36 | The `mockServer#get` method maps a URL to a response for the lifecycle of the test. At the end of each test the `mockServer` is reset.
37 |
38 | By default, the `mockServer` will use a status code of 200. However, an optional status code can be passed in as the third parameter.
39 |
40 | ```js
41 | test('it renders the 404 page when it cannot fetch a note', async function(assert) {
42 | await mockServer.get(
43 | '/api/notes/1',
44 | { error: 'Note not found' },
45 | 404
46 | );
47 |
48 | await visit('/notes/1');
49 |
50 | assert.dom('[data-test-id="page-not-found"]').exists();
51 | });
52 | ```
53 |
54 | By default, passing just a path to `mockServer` will use the current host running your app.
55 |
56 | Some implementations require a different origin for `mockServer` calls. In that case, you can override the default host by including the hostname with the path.
57 |
58 | ```js
59 | test('it makes a call to a different host', async function(assert) {
60 | await mockServer.get('http://localhost:3000/api/notes/1', {
61 | data: {
62 | type: 'note',
63 | id: '1',
64 | attributes: {
65 | title: 'Hello world!'
66 | }
67 | }
68 | });
69 |
70 | await visit('/notes/1');
71 |
72 | assert.dom('[data-test-id="page-not-found"]').exists();
73 | });
74 | ```
75 |
76 | The `mockServer` also exposes `post`, `patch`, `put`, and `delete` mocking methods.
77 |
78 | ## Video
79 |
80 | This following video shows how [EmberMap](https://embermap.com) uses FastBoot testing's networking mocking.
81 |
82 |
83 |
90 |
91 |
--------------------------------------------------------------------------------
/tests/fastboot/network-mocking-test.js:
--------------------------------------------------------------------------------
1 | import { module, test } from 'qunit';
2 | import {
3 | setup,
4 | visit,
5 | mockServer,
6 | } from 'ember-cli-fastboot-testing/test-support';
7 | import config from 'dummy/config/environment';
8 |
9 | module('Fastboot | network mocking', function (hooks) {
10 | setup(hooks);
11 |
12 | test('it will not change an endpoint that already exists', async function (assert) {
13 | await visit('/examples/network/other/echo?message=hello%20world');
14 | assert.dom('[data-test-id="echo"]').hasText('hello world');
15 | });
16 |
17 | test('it can mock an array of models', async function (assert) {
18 | await mockServer.get('/api/notes', {
19 | data: [
20 | {
21 | type: 'note',
22 | id: '1',
23 | attributes: {
24 | title: 'test note',
25 | },
26 | },
27 | {
28 | type: 'note',
29 | id: '2',
30 | attributes: {
31 | title: 'test 2',
32 | },
33 | },
34 | ],
35 | });
36 |
37 | await visit('/examples/network/notes');
38 |
39 | assert.dom('[data-test-id="title-1"]').hasText('test note');
40 | assert.dom('[data-test-id="title-2"]').hasText('test 2');
41 | });
42 |
43 | test('it can mock a single model', async function (assert) {
44 | await mockServer.get('/api/notes/1', {
45 | data: {
46 | type: 'note',
47 | id: '1',
48 | attributes: {
49 | title: 'test note',
50 | },
51 | },
52 | });
53 |
54 | await visit('/examples/network/notes/1');
55 |
56 | assert.dom('[data-test-id="title"]').hasText('test note');
57 | });
58 |
59 | test('it can mock 404s', async function (assert) {
60 | await mockServer.get(
61 | '/api/notes/1',
62 | {
63 | errors: [{ title: 'Not found' }],
64 | },
65 | 404,
66 | );
67 |
68 | await visit('/examples/network/notes/1');
69 |
70 | assert
71 | .dom()
72 | .includesText('Ember Data Request GET /api/notes/1 returned a 404');
73 | });
74 |
75 | test('it can mock a get request', async function (assert) {
76 | await mockServer.get('/api/notes', [{ id: 1, title: 'get note' }]);
77 |
78 | await visit('/examples/network/other/get-request');
79 |
80 | assert.dom('[data-test-id="title-1"]').hasText('get note');
81 | });
82 |
83 | test('it can mock a post request', async function (assert) {
84 | await mockServer.post('/api/notes', [{ id: 1, title: 'post note' }]);
85 |
86 | await visit('/examples/network/other/post-request');
87 |
88 | assert.dom('[data-test-id="title-1"]').hasText('post note');
89 | });
90 |
91 | test('it can mock a large response', async function (assert) {
92 | let title = 'a'.repeat(1024 * 1024 * 5); // 5 MB
93 |
94 | await mockServer.get('/api/notes', [{ id: 1, title }]);
95 |
96 | await visit('/examples/network/other/get-request');
97 |
98 | assert.dom('[data-test-id="title-1"]').exists();
99 | });
100 |
101 | test('it can override the origin on a mock response', async function (assert) {
102 | const { originForOverride } = config;
103 |
104 | await mockServer.get(`${originForOverride}/api/notes`, [
105 | { id: 1, title: 'get note' },
106 | ]);
107 |
108 | await visit('/examples/network/other/origin-override');
109 |
110 | assert.dom('[data-test-id="title-1"]').hasText('get note');
111 | });
112 |
113 | test('it can add headers a mock response', async function (assert) {
114 | await mockServer.get('/api/notes', { data: [] }, 200, { 'x-test': 'foo' });
115 |
116 | await visit('/examples/network/other/headers');
117 |
118 | assert.dom('[data-test-id="headers"]').hasText('foo');
119 | });
120 | });
121 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | - master
8 | - 'v*'
9 | pull_request: {}
10 |
11 | concurrency:
12 | group: ci-${{ github.head_ref || github.ref }}
13 | cancel-in-progress: true
14 |
15 | jobs:
16 | test:
17 | name: "Tests"
18 | runs-on: ubuntu-latest
19 | timeout-minutes: 10
20 |
21 | steps:
22 | - uses: actions/checkout@v5
23 | - uses: pnpm/action-setup@v4
24 | with:
25 | version: 10
26 | - uses: actions/setup-node@v6
27 | with:
28 | node-version: 18
29 | registry-url: 'https://registry.npmjs.org'
30 | cache: pnpm
31 |
32 | - run: pnpm install --frozen-lockfile
33 |
34 | - name: Lint
35 | run: pnpm lint
36 |
37 | - name: Run Tests
38 | run: pnpm test:ember
39 |
40 | floating:
41 | name: "Floating Dependencies"
42 | runs-on: ubuntu-latest
43 | timeout-minutes: 10
44 |
45 | steps:
46 | - uses: actions/checkout@v5
47 | - uses: pnpm/action-setup@v4
48 | with:
49 | version: 10
50 | - uses: actions/setup-node@v6
51 | with:
52 | node-version: 18
53 | registry-url: 'https://registry.npmjs.org'
54 | cache: pnpm
55 |
56 | - name: Install Dependencies
57 | run: pnpm install --no-lockfile
58 |
59 | - name: Run Tests
60 | run: pnpm test:ember
61 |
62 | separate_build:
63 | name: "Tests with separate build"
64 | runs-on: ubuntu-latest
65 |
66 | steps:
67 | - uses: actions/checkout@v5
68 | - uses: pnpm/action-setup@v4
69 | with:
70 | version: 10
71 | - uses: actions/setup-node@v6
72 | with:
73 | node-version: 18
74 | registry-url: 'https://registry.npmjs.org'
75 | cache: pnpm
76 |
77 | - name: Install Dependencies
78 | run: pnpm install --frozen-lockfile
79 |
80 | - name: Run Tests
81 | run: pnpm test:separate-build
82 |
83 | try-scenarios:
84 | name: ${{ matrix.try-scenario }}
85 | runs-on: ubuntu-latest
86 | needs: "test"
87 | timeout-minutes: 10
88 |
89 | strategy:
90 | fail-fast: false
91 | matrix:
92 | try-scenario:
93 | - ember-lts-3.28
94 | - ember-lts-4.4
95 | - ember-lts-4.8
96 | - ember-lts-4.12
97 | - ember-lts-5.4
98 | - ember-lts-5.8
99 | - fastboot-3.0
100 | - ember-release
101 | - ember-beta
102 | # - ember-canary
103 | - embroider-safe
104 | - embroider-optimized
105 |
106 | steps:
107 | - uses: actions/checkout@v5
108 | - uses: pnpm/action-setup@v4
109 | with:
110 | version: 10
111 | - uses: actions/setup-node@v6
112 | with:
113 | node-version: 18
114 | registry-url: 'https://registry.npmjs.org'
115 | cache: pnpm
116 |
117 | - name: Install Dependencies
118 | run: pnpm install --frozen-lockfile
119 |
120 | - name: Run Tests
121 | run: ./node_modules/.bin/ember try:one ${{ matrix.try-scenario }}
122 |
123 | deploy:
124 | name: "Deploy"
125 | runs-on: ubuntu-latest
126 | if: startsWith(github.ref, 'refs/tags/v') && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master')
127 | needs: [ test, try-scenarios ]
128 |
129 | steps:
130 | - uses: actions/checkout@v5
131 | - uses: pnpm/action-setup@v4
132 | with:
133 | version: 10
134 | - uses: actions/setup-node@v6
135 | with:
136 | node-version: 18
137 | registry-url: 'https://registry.npmjs.org'
138 | cache: pnpm
139 |
140 | - name: Install Dependencies
141 | run: pnpm install --frozen-lockfile
142 |
143 | - name: Ember Deploy
144 | run: node_modules/.bin/ember deploy production
145 |
--------------------------------------------------------------------------------
/lib/helpers.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const fs = require('fs');
4 | const path = require('path');
5 | const { URL } = require('url');
6 | const JSONfn = require('json-fn');
7 | const FastBoot = require('fastboot');
8 | const bodyParser = require('body-parser');
9 | const { deprecate } = require('util');
10 |
11 | let _nock;
12 | function getNock() {
13 | if (!_nock) {
14 | _nock = require('nock');
15 | }
16 | return _nock;
17 | }
18 |
19 | function createMockRequest(app) {
20 | app.post(
21 | '/__mock-request',
22 | bodyParser.json({ limit: '50mb' }),
23 | (req, res) => {
24 | const requestOrigin = req.body.origin || req.headers.origin;
25 | let mock = getNock()(requestOrigin)
26 | .persist()
27 | .intercept(req.body.path, req.body.method)
28 | .reply(
29 | req.body.statusCode,
30 | req.body.response,
31 | req.body.responseHeaders,
32 | );
33 |
34 | res.json({ mocks: mock.pendingMocks() });
35 | },
36 | );
37 | }
38 |
39 | function createCleanUpMocks(app) {
40 | app.use('/__cleanup-mocks', (req, res) => {
41 | getNock().cleanAll();
42 |
43 | res.json({ ok: true });
44 | });
45 | }
46 |
47 | function createFastbootTest(app, callback) {
48 | app.post('/__fastboot-testing', bodyParser.json(), (req, res) => {
49 | const urlToVisit = decodeURIComponent(req.body.url);
50 | const origin = req.headers.origin
51 | ? req.headers.origin // Available when tests are run with `--server`
52 | : new URL(req.headers.referer).origin; // Otherwise extract origin from referer
53 | const parsed = new URL(urlToVisit, origin);
54 |
55 | const headers = Object.assign(
56 | {},
57 | req.headers,
58 | JSONfn.parse(req.body.options).headers || {},
59 | );
60 |
61 | const defaultOptions = {
62 | request: {
63 | method: 'GET',
64 | protocol: 'http',
65 | url: parsed.pathname,
66 | query: Object.fromEntries(parsed.searchParams),
67 | headers,
68 | },
69 | response: {},
70 | };
71 |
72 | const options = Object.assign(
73 | defaultOptions,
74 | JSONfn.parse(req.body.options),
75 | );
76 |
77 | res.set('x-fastboot-testing', true);
78 |
79 | callback({ req, res, options, urlToVisit });
80 | });
81 | }
82 |
83 | function createFastbootEcho(app) {
84 | app.post('/fastboot-testing/echo', bodyParser.text(), (req, res) => {
85 | res.send(req.body);
86 | });
87 | }
88 |
89 | function reloadServer(fastboot, distPath) {
90 | fastboot.reload({ distPath });
91 | }
92 |
93 | function createServer(distPath, pkg) {
94 | const options = makeFastbootTestingConfig({ distPath }, pkg);
95 | return new FastBoot(options);
96 | }
97 |
98 | function makeFastbootTestingConfig(config, pkg) {
99 | const defaults = {
100 | setupFastboot() {},
101 | };
102 |
103 | let configPath = 'config';
104 |
105 | if (pkg['ember-addon'] && pkg['ember-addon']['configPath']) {
106 | configPath = pkg['ember-addon']['configPath'];
107 | }
108 |
109 | const fastbootTestConfigPath = path.resolve(
110 | configPath,
111 | 'fastboot-testing.js',
112 | );
113 |
114 | let customized = {};
115 |
116 | if (fs.existsSync(fastbootTestConfigPath)) {
117 | const fastbootTestConfig = require(fastbootTestConfigPath);
118 |
119 | if (typeof fastbootTestConfig === 'function') {
120 | customized = fastbootTestConfig();
121 | } else {
122 | deprecate(
123 | () => Object.assign(customized, fastbootTestConfig),
124 | `Exporting an object from ${fastbootTestConfigPath} has been deprecated. Please export a function which returns an object instead.`,
125 | )();
126 | }
127 | }
128 |
129 | return Object.assign({}, config, defaults, customized);
130 | }
131 |
132 | module.exports = {
133 | createMockRequest,
134 | createCleanUpMocks,
135 | createFastbootTest,
136 | createFastbootEcho,
137 | reloadServer,
138 | createServer,
139 | makeFastbootTestingConfig,
140 | };
141 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const resolve = require('resolve');
4 | const minimist = require('minimist');
5 |
6 | module.exports = {
7 | name: require('./package').name,
8 |
9 | isDevelopingAddon() {
10 | return false;
11 | },
12 |
13 | included() {
14 | this._super.included.apply(this, arguments);
15 |
16 | const isEnabled =
17 | this.app.name === 'dummy' || this.app.env !== 'production';
18 |
19 | if (!isEnabled) return;
20 |
21 | try {
22 | resolve.sync('ember-cli-fastboot/package.json', {
23 | basedir: this.project.root,
24 | });
25 | } catch (err) {
26 | throw new Error(
27 | `Unable to find FastBoot. Did you forget to add ember-cli-fastboot to your app? ${err}`,
28 | );
29 | }
30 | },
31 |
32 | serverMiddleware(options) {
33 | this._fastbootRenderingMiddleware(options.app);
34 | },
35 |
36 | testemMiddleware(app) {
37 | this._fastbootRenderingMiddleware(app);
38 | },
39 |
40 | // we have to use the outputReady hook to ensure that ember-cli has finished copying the contents to the outputPath directory
41 | outputReady(result) {
42 | const { reloadServer, createServer } = require('./lib/helpers');
43 |
44 | const isEnabled =
45 | this.app.name === 'dummy' || this.app.env !== 'production';
46 |
47 | if (isEnabled) {
48 | // NOTE: result.directory is not updated and still references the same path as postBuild hook (this might be a bug in ember-cli)
49 | // Set the distPath to the "final" outputPath, where EMBER_CLI_TEST_OUTPUT is the path passed to testem (set by ember-cli).
50 | // We fall back to result.directory if EMBER_CLI_TEST_OUTPUT is not set.
51 | let distPath = process.env.EMBER_CLI_TEST_OUTPUT || result.directory;
52 | let { pkg } = this.project;
53 |
54 | if (this.fastboot) {
55 | reloadServer(this.fastboot, distPath);
56 | } else {
57 | this.fastboot = createServer(distPath, pkg);
58 | }
59 | }
60 |
61 | return result;
62 | },
63 |
64 | _fastbootRenderingMiddleware(app) {
65 | const {
66 | createCleanUpMocks,
67 | createFastbootEcho,
68 | createFastbootTest,
69 | createMockRequest,
70 | createServer,
71 | } = require('./lib/helpers');
72 | createMockRequest(app);
73 | createCleanUpMocks(app);
74 | createFastbootTest(app, ({ res, options, urlToVisit }) => {
75 | if (!this.fastboot) {
76 | const path = minimist(process.argv.slice(2)).path;
77 | if (path) {
78 | this.fastboot = createServer(path, this.project.pkg);
79 | } else {
80 | return res.json({ err: 'no path found' });
81 | }
82 | }
83 |
84 | this.fastboot
85 | .visit(urlToVisit, options)
86 | .then((page) => {
87 | page.html().then((html) => {
88 | res.json({
89 | finalized: page.finalized,
90 | url: page.url,
91 | statusCode: page.statusCode,
92 | headers: page.headers.headers,
93 | html: html,
94 | });
95 | });
96 | })
97 | .catch((err) => {
98 | let errorObject;
99 | let jsonError = {};
100 |
101 | errorObject = typeof err === 'string' ? new Error(err) : err;
102 |
103 | // we need to copy these properties off the error
104 | // object into a pojo that can be serialized and
105 | // sent over the wire to the test runner.
106 | let errorProps = [
107 | 'description',
108 | 'fileName',
109 | 'lineNumber',
110 | 'message',
111 | 'name',
112 | 'number',
113 | 'stack',
114 | ];
115 |
116 | errorProps.forEach((key) => (jsonError[key] = errorObject[key]));
117 |
118 | res.json({ err: jsonError });
119 | });
120 | });
121 |
122 | if (this.app && this.app.name === 'dummy') {
123 | // our dummy app has an echo endpoint!
124 | createFastbootEcho(app);
125 | }
126 | },
127 | };
128 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ember-cli-fastboot-testing",
3 | "version": "0.7.0",
4 | "description": "Test your FastBoot-rendered HTML alongside your application's tests.",
5 | "keywords": [
6 | "ember-addon"
7 | ],
8 | "homepage": "https://embermap.github.io/ember-cli-fastboot-testing",
9 | "repository": "https://github.com/embermap/ember-cli-fastboot-testing",
10 | "license": "MIT",
11 | "author": "Ryan Toronto (https://embermap.com)",
12 | "directories": {
13 | "doc": "doc",
14 | "test": "tests"
15 | },
16 | "scripts": {
17 | "build": "ember build --environment=production",
18 | "lint": "concurrently \"yarn:lint:*(!fix)\" --names \"lint:\"",
19 | "lint:css": "stylelint \"**/*.css\"",
20 | "lint:css:fix": "concurrently \"yarn:lint:css -- --fix\"",
21 | "lint:fix": "concurrently \"yarn:lint:*:fix\" --names \"fix:\"",
22 | "lint:hbs": "ember-template-lint .",
23 | "lint:hbs:fix": "ember-template-lint . --fix",
24 | "lint:js": "eslint . --cache",
25 | "lint:js:fix": "eslint . --fix",
26 | "start": "ember serve",
27 | "test": "concurrently \"yarn:lint\" \"yarn:test:*\" --names \"lint,test:\"",
28 | "test:ember": "ember test",
29 | "test:ember-compatibility": "ember try:each",
30 | "test:separate-build": "yarn build --environment test && yarn test:ember --path=dist --filter 'Fastboot | basic'"
31 | },
32 | "dependencies": {
33 | "body-parser": "^1.20.3 || ^2.2.1",
34 | "ember-auto-import": "^2.8.1",
35 | "ember-cli-babel": "^8.2.0",
36 | "fastboot": "^3.0.3 || ^4.1.5",
37 | "json-fn": "^1.1.1",
38 | "minimist": "^1.2.6",
39 | "nock": "^13.5.5",
40 | "resolve": "^1.22.8",
41 | "whatwg-fetch": "^3.6.20"
42 | },
43 | "devDependencies": {
44 | "@babel/core": "^7.25.2",
45 | "@babel/eslint-parser": "^7.25.1",
46 | "@babel/plugin-proposal-decorators": "^7.24.7",
47 | "@ember/optional-features": "^2.1.0",
48 | "@ember/string": "^3.1.1",
49 | "@ember/test-helpers": "^3.3.1",
50 | "@embroider/test-setup": "^3.0.3",
51 | "@glimmer/component": "^1.1.2",
52 | "@glimmer/tracking": "^1.1.2",
53 | "broccoli-asset-rev": "^3.0.0",
54 | "broccoli-plugin": "^4.0.7",
55 | "broccoli-stew": "^3.0.0",
56 | "concurrently": "^8.2.2",
57 | "ember-cli": "~5.11.0",
58 | "ember-cli-addon-docs": "^7.0.1",
59 | "ember-cli-addon-docs-yuidoc": "^1.1.0",
60 | "ember-cli-clean-css": "^3.0.0",
61 | "ember-cli-dependency-checker": "^3.3.2",
62 | "ember-cli-deploy": "^2.0.0",
63 | "ember-cli-deploy-build": "^3.0.0",
64 | "ember-cli-deploy-git": "^1.3.3",
65 | "ember-cli-deploy-git-ci": "^1.0.1",
66 | "ember-cli-fastboot": "^4.1.5",
67 | "ember-cli-head": "^2.0.0",
68 | "ember-cli-htmlbars": "^6.3.0",
69 | "ember-cli-inject-live-reload": "^2.1.0",
70 | "ember-cli-mirage": "^3.0.4",
71 | "ember-cli-sri": "^2.1.1",
72 | "ember-cli-terser": "^4.0.2",
73 | "ember-data": "~4.12.8",
74 | "ember-fetch": "^8.1.1",
75 | "ember-load-initializers": "^2.1.2",
76 | "ember-qunit": "^8.1.0",
77 | "ember-resolver": "^11.0.1",
78 | "ember-source": "~5.12.0",
79 | "ember-source-channel-url": "^3.0.0",
80 | "ember-template-lint": "^6.0.0",
81 | "ember-try": "^3.0.0",
82 | "eslint": "^8.57.0",
83 | "eslint-config-prettier": "^9.1.0",
84 | "eslint-plugin-ember": "^12.2.0",
85 | "eslint-plugin-n": "^17.10.3",
86 | "eslint-plugin-prettier": "^5.2.1",
87 | "eslint-plugin-qunit": "^8.1.1",
88 | "loader.js": "^4.7.0",
89 | "miragejs": "^0.1.48",
90 | "najax": "^1.0.7",
91 | "node-fetch": "^2.7.0",
92 | "pretender": "~3.4.7",
93 | "prettier": "^3.3.3",
94 | "qunit": "^2.22.0",
95 | "qunit-dom": "^3.2.1",
96 | "release-plan": "^0.17.0",
97 | "semver": "^7.6.3",
98 | "stylelint": "^15.11.0",
99 | "stylelint-config-standard": "^34.0.0",
100 | "stylelint-prettier": "^4.1.0",
101 | "webpack": "^5.93.0"
102 | },
103 | "peerDependencies": {
104 | "@ember/test-helpers": ">= 3.0.0",
105 | "ember-source": "^3.28.0 || >= 4.0.0"
106 | },
107 | "engines": {
108 | "node": "18.* || 20.* || >= 22"
109 | },
110 | "volta": {
111 | "node": "18.20.4",
112 | "yarn": "1.22.22",
113 | "pnpm": "10.24.0"
114 | },
115 | "ember": {
116 | "edition": "octane"
117 | },
118 | "ember-addon": {
119 | "configPath": "tests/dummy/config",
120 | "before": [
121 | "ember-cli-fastboot"
122 | ],
123 | "after": []
124 | },
125 | "fastbootDependencies": [
126 | "crypto"
127 | ],
128 | "release-plan": {
129 | "semverIncrementAs": {
130 | "major": "minor"
131 | }
132 | }
133 | }
134 |
--------------------------------------------------------------------------------
/addon-test-support/index.js:
--------------------------------------------------------------------------------
1 | import { fetch } from 'whatwg-fetch';
2 | import { setupContext, teardownContext } from '@ember/test-helpers';
3 | import { mockServer } from './-private/mock-server';
4 | import JSONfn from 'json-fn';
5 |
6 | export function setup(hooks) {
7 | hooks.beforeEach(async function () {
8 | await mockServer.cleanUp();
9 | return setupContext(this);
10 | });
11 |
12 | hooks.afterEach(async function () {
13 | await mockServer.cleanUp();
14 | return teardownContext(this);
15 | });
16 | }
17 |
18 | /**
19 | * @todo
20 | *
21 | * @example
22 | * ```
23 | * // TODO
24 | * ```
25 | *
26 | * @param {string} url the URL path to render, like `/photos/1`
27 | * @param {Object} options
28 | * @param {string} [options.html] the HTML document to insert the rendered app into
29 | * @param {Object} [options.metadata] Per request specific data used in the app.
30 | * @param {Boolean} [options.shouldRender] whether the app should do rendering or not. If set to false, it puts the app in routing-only.
31 | * @param {Boolean} [options.disableShoebox] whether we should send the API data in the shoebox. If set to false, it will not send the API data used for rendering the app on server side in the index.html.
32 | * @param {Integer} [options.destroyAppInstanceInMs] whether to destroy the instance in the given number of ms. This is a failure mechanism to not wedge the Node process (See: https://github.com/ember-fastboot/fastboot/issues/90)
33 | * @param {ClientRequest} [options.request] Node's `ClientRequest` object is provided to the Ember application via the FastBoot service.
34 | * @param {ClientResponse} [options.response] Node's `ServerResponse` object is provided to the Ember application via the FastBoot service.
35 | * @return {Promise} result
36 | */
37 | export async function fastboot(url, options = {}) {
38 | let response = await fetchFromEmberCli(url, options);
39 | let result = await response.json();
40 |
41 | let body = result.err ? formatError(result.err) : extractBody(result.html);
42 |
43 | result.htmlDocument = parseHtml(result.html);
44 | result.body = body;
45 |
46 | return result;
47 | }
48 |
49 | /**
50 | * @todo
51 | *
52 | * @example
53 | * ```
54 | * // TODO
55 | * ```
56 | *
57 | * @param {string} url the URL path to render, like `/photos/1`
58 | * @param {Object} options
59 | * @param {string} [options.html] the HTML document to insert the rendered app into
60 | * @param {Object} [options.metadata] Per request specific data used in the app.
61 | * @param {Boolean} [options.shouldRender] whether the app should do rendering or not. If set to false, it puts the app in routing-only.
62 | * @param {Boolean} [options.disableShoebox] whether we should send the API data in the shoebox. If set to false, it will not send the API data used for rendering the app on server side in the index.html.
63 | * @param {Integer} [options.destroyAppInstanceInMs] whether to destroy the instance in the given number of ms. This is a failure mechanism to not wedge the Node process (See: https://github.com/ember-fastboot/fastboot/issues/90)
64 | * @param {ClientRequest} [options.request] Node's `ClientRequest` object is provided to the Ember application via the FastBoot service.
65 | * @param {ClientResponse} [options.response] Node's `ServerResponse` object is provided to the Ember application via the FastBoot service.
66 | * @return {Promise} result
67 | */
68 | export async function visit(url, options = {}) {
69 | let result = await fastboot(url, options);
70 |
71 | document.querySelector('#ember-testing').innerHTML = result.body;
72 |
73 | return result;
74 | }
75 |
76 | export { mockServer };
77 |
78 | // private
79 |
80 | let fetchFromEmberCli = async function (url, options) {
81 | let response;
82 | let error;
83 |
84 | try {
85 | response = await fetch('/__fastboot-testing', {
86 | method: 'POST',
87 | headers: {
88 | 'Content-Type': 'application/json',
89 | },
90 | body: JSON.stringify({
91 | url,
92 | options: JSONfn.stringify(options),
93 | }),
94 | });
95 | } catch (e) {
96 | if (e.message && e.message.match(/^Mirage:/)) {
97 | error = `Ember CLI FastBoot Testing: It looks like Mirage is intercepting ember-cli-fastboot-testing's attempt to render ${url}. Please disable Mirage when running FastBoot tests.`;
98 | } else {
99 | error = `Ember CLI FastBoot Testing: We were unable to render ${url}. Is your test suite blocking or intercepting HTTP requests? Error: ${
100 | e.message ? e.message : e
101 | }.`;
102 | }
103 | }
104 |
105 | if (
106 | response &&
107 | response.headers &&
108 | response.headers.get &&
109 | response.headers.get('x-fastboot-testing') !== 'true'
110 | ) {
111 | error = `Ember CLI FastBoot Testing: We were unable to render ${url}. Is your test suite blocking or intercepting HTTP requests?`;
112 | }
113 |
114 | if (error) {
115 | // eslint-disable-next-line no-console
116 | console.error(error);
117 | throw new Error(error);
118 | }
119 |
120 | return response;
121 | };
122 |
123 | let parseHtml = function (str) {
124 | let parser = new DOMParser();
125 | return parser.parseFromString(str, 'text/html');
126 | };
127 |
128 | let extractBody = function (html) {
129 | let start = '';
130 | let end = '';
131 |
132 | let startPosition = html.indexOf(start);
133 | let endPosition = html.indexOf(end);
134 |
135 | if (!startPosition || !endPosition) {
136 | throw 'Could not find fastboot boundary';
137 | }
138 |
139 | let startAt = startPosition + start.length;
140 | let endAt = endPosition - startAt;
141 |
142 | return html.substr(startAt, endAt);
143 | };
144 |
145 | let formatError = function (err) {
146 | return `