├── app └── .gitkeep ├── addon └── .gitkeep ├── tests ├── unit │ └── .gitkeep ├── integration │ └── .gitkeep ├── dummy │ ├── app │ │ ├── helpers │ │ │ └── .gitkeep │ │ ├── pods │ │ │ └── examples │ │ │ │ ├── index │ │ │ │ └── template.hbs │ │ │ │ ├── network │ │ │ │ ├── other │ │ │ │ │ ├── echo │ │ │ │ │ │ ├── template.hbs │ │ │ │ │ │ ├── controller.js │ │ │ │ │ │ └── route.js │ │ │ │ │ ├── headers │ │ │ │ │ │ ├── template.hbs │ │ │ │ │ │ └── route.js │ │ │ │ │ ├── get-request │ │ │ │ │ │ ├── template.hbs │ │ │ │ │ │ └── route.js │ │ │ │ │ ├── origin-override │ │ │ │ │ │ ├── template.hbs │ │ │ │ │ │ └── route.js │ │ │ │ │ └── post-request │ │ │ │ │ │ ├── template.hbs │ │ │ │ │ │ └── route.js │ │ │ │ └── notes │ │ │ │ │ ├── note │ │ │ │ │ ├── template.hbs │ │ │ │ │ └── route.js │ │ │ │ │ └── index │ │ │ │ │ ├── template.hbs │ │ │ │ │ └── route.js │ │ │ │ ├── query-parameters │ │ │ │ ├── template.hbs │ │ │ │ └── controller.js │ │ │ │ ├── global-config │ │ │ │ ├── template.hbs │ │ │ │ └── route.js │ │ │ │ ├── visit-customization │ │ │ │ ├── template.hbs │ │ │ │ └── route.js │ │ │ │ ├── errors │ │ │ │ ├── access-document │ │ │ │ │ └── route.js │ │ │ │ ├── throw-message │ │ │ │ │ └── route.js │ │ │ │ └── throw-error-object │ │ │ │ │ └── route.js │ │ │ │ ├── not-found │ │ │ │ └── template.hbs │ │ │ │ ├── redirects │ │ │ │ ├── replace-with │ │ │ │ │ └── route.js │ │ │ │ └── transition-to │ │ │ │ │ └── route.js │ │ │ │ └── request-object │ │ │ │ ├── controller.js │ │ │ │ └── template.hbs │ │ ├── templates │ │ │ ├── application.hbs │ │ │ ├── head.hbs │ │ │ ├── docs │ │ │ │ ├── html.md │ │ │ │ ├── fastboot-configuration.md │ │ │ │ ├── debugging.md │ │ │ │ ├── videos.md │ │ │ │ ├── quickstart.md │ │ │ │ ├── document.md │ │ │ │ ├── status-code.md │ │ │ │ ├── index.md │ │ │ │ ├── visit.md │ │ │ │ └── network-mocking.md │ │ │ ├── index.hbs │ │ │ └── docs.hbs │ │ ├── serializers │ │ │ └── application.js │ │ ├── models │ │ │ └── note.js │ │ ├── adapters │ │ │ └── application.js │ │ ├── routes │ │ │ └── application.js │ │ ├── app.js │ │ ├── services │ │ │ └── store.js │ │ ├── styles │ │ │ └── app.css │ │ ├── index.html │ │ └── router.js │ ├── public │ │ └── robots.txt │ ├── mirage │ │ ├── serializers │ │ │ └── application.js │ │ ├── scenarios │ │ │ └── default.js │ │ └── config.js │ └── config │ │ ├── optional-features.json │ │ ├── targets.js │ │ ├── addon-docs.js │ │ ├── ember-cli-update.json │ │ ├── deploy.js │ │ ├── fastboot-testing.js │ │ ├── environment.js │ │ └── ember-try.js ├── test-helper.js ├── fastboot │ ├── global-config-test.js │ ├── visit-customization-test.js │ ├── errors-test.js │ ├── mirage-interceptors-test.js │ ├── redirects-test.js │ ├── basic-test.js │ ├── generic-interceptors-test.js │ ├── request-object-test.js │ └── network-mocking-test.js ├── index.html └── helpers │ └── index.js ├── .watchmanconfig ├── .template-lintrc.js ├── .stylelintrc.js ├── .stylelintignore ├── .prettierignore ├── .prettierrc.js ├── .eslintignore ├── blueprints ├── fastboot-test │ ├── index.js │ └── files │ │ └── tests │ │ └── fastboot │ │ └── __name__-test.js └── fastboot-test-config │ ├── files │ └── __config__ │ │ └── fastboot-testing.js │ └── index.js ├── .ember-cli ├── .github ├── dependabot.yml └── workflows │ ├── publish.yml │ ├── plan-release.yml │ └── ci.yml ├── config ├── addon-docs.js └── deploy.js ├── .gitignore ├── .editorconfig ├── testem.js ├── .npmignore ├── ember-cli-build.js ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── .eslintrc.js ├── addon-test-support ├── -private │ └── mock-server.js └── index.js ├── RELEASE.md ├── CHANGELOG.md ├── .release-plan.json ├── lib └── helpers.js ├── index.js └── package.json /app/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /addon/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/unit/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/integration/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/dummy/app/helpers/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.watchmanconfig: -------------------------------------------------------------------------------- 1 | { 2 | "ignore_dirs": ["dist"] 3 | } 4 | -------------------------------------------------------------------------------- /tests/dummy/app/pods/examples/index/template.hbs: -------------------------------------------------------------------------------- 1 |

FastbootTesting

-------------------------------------------------------------------------------- /tests/dummy/app/templates/application.hbs: -------------------------------------------------------------------------------- 1 | 2 | 3 | {{outlet}} 4 | -------------------------------------------------------------------------------- /tests/dummy/public/robots.txt: -------------------------------------------------------------------------------- 1 | # http://www.robotstxt.org 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /.template-lintrc.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | extends: 'recommended', 5 | }; 6 | -------------------------------------------------------------------------------- /tests/dummy/app/pods/examples/network/other/echo/template.hbs: -------------------------------------------------------------------------------- 1 |
{{@model}}
2 | -------------------------------------------------------------------------------- /tests/dummy/app/pods/examples/query-parameters/template.hbs: -------------------------------------------------------------------------------- 1 |

{{this.first}} {{this.second}} {{this.third}}

2 | -------------------------------------------------------------------------------- /tests/dummy/app/pods/examples/global-config/template.hbs: -------------------------------------------------------------------------------- 1 |

{{@model.SampleGlobal}}

2 | -------------------------------------------------------------------------------- /tests/dummy/app/pods/examples/network/other/headers/template.hbs: -------------------------------------------------------------------------------- 1 |
2 | {{@model}} 3 |
-------------------------------------------------------------------------------- /.stylelintrc.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | extends: ['stylelint-config-standard', 'stylelint-prettier/recommended'], 5 | }; 6 | -------------------------------------------------------------------------------- /tests/dummy/mirage/serializers/application.js: -------------------------------------------------------------------------------- 1 | import { JSONAPISerializer } from 'miragejs'; 2 | 3 | export default JSONAPISerializer.extend({}); 4 | -------------------------------------------------------------------------------- /tests/dummy/app/templates/head.hbs: -------------------------------------------------------------------------------- 1 | {{!-- template-lint-disable no-forbidden-elements --}} 2 | 3 | -------------------------------------------------------------------------------- /tests/dummy/app/pods/examples/network/notes/note/template.hbs: -------------------------------------------------------------------------------- 1 | The data loaded from the server is: 2 | 3 |
4 | {{@model.title}} 5 |
6 | -------------------------------------------------------------------------------- /tests/dummy/app/serializers/application.js: -------------------------------------------------------------------------------- 1 | import JSONAPISerializer from '@ember-data/serializer/json-api'; 2 | 3 | export default class extends JSONAPISerializer {} 4 | -------------------------------------------------------------------------------- /.stylelintignore: -------------------------------------------------------------------------------- 1 | # unconventional files 2 | /blueprints/*/files/ 3 | /vendor/ 4 | 5 | # compiled output 6 | /dist/ 7 | 8 | # addons 9 | /.node_modules.ember-try/ 10 | -------------------------------------------------------------------------------- /tests/dummy/app/models/note.js: -------------------------------------------------------------------------------- 1 | import Model, { attr } from '@ember-data/model'; 2 | 3 | export default class NoteModel extends Model { 4 | @attr('string') title; 5 | } 6 | -------------------------------------------------------------------------------- /tests/dummy/app/adapters/application.js: -------------------------------------------------------------------------------- 1 | import JSONAPIAdapter from '@ember-data/adapter/json-api'; 2 | 3 | export default class extends JSONAPIAdapter { 4 | namespace = 'api'; 5 | } 6 | -------------------------------------------------------------------------------- /tests/dummy/app/pods/examples/network/other/echo/controller.js: -------------------------------------------------------------------------------- 1 | import Controller from '@ember/controller'; 2 | 3 | export default class extends Controller { 4 | queryParams = ['message']; 5 | } 6 | -------------------------------------------------------------------------------- /tests/dummy/app/pods/examples/visit-customization/template.hbs: -------------------------------------------------------------------------------- 1 |
2 | {{@model.data}} 3 |
4 | 5 |
6 | {{@model.add}} 7 |
8 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # unconventional js 2 | /blueprints/*/files/ 3 | 4 | # compiled output 5 | /dist/ 6 | 7 | # misc 8 | /coverage/ 9 | !.* 10 | .*/ 11 | 12 | # ember-try 13 | /.node_modules.ember-try/ 14 | -------------------------------------------------------------------------------- /tests/dummy/app/pods/examples/errors/access-document/route.js: -------------------------------------------------------------------------------- 1 | import Route from '@ember/routing/route'; 2 | 3 | export default class extends Route { 4 | model() { 5 | return document.title; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /tests/dummy/app/pods/examples/errors/throw-message/route.js: -------------------------------------------------------------------------------- 1 | import Route from '@ember/routing/route'; 2 | 3 | export default class extends Route { 4 | model() { 5 | throw 'This errors in FastBoot!'; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /tests/dummy/config/optional-features.json: -------------------------------------------------------------------------------- 1 | { 2 | "application-template-wrapper": false, 3 | "default-async-observers": true, 4 | "jquery-integration": false, 5 | "template-only-glimmer-components": true 6 | } 7 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | overrides: [ 5 | { 6 | files: '*.{js,ts}', 7 | options: { 8 | singleQuote: true, 9 | }, 10 | }, 11 | ], 12 | }; 13 | -------------------------------------------------------------------------------- /tests/dummy/app/pods/examples/not-found/template.hbs: -------------------------------------------------------------------------------- 1 |
2 |

Not found

3 |

4 | This page doesn't exist. 5 | 6 | Head home? 7 |

8 |
9 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | # unconventional js 2 | /blueprints/*/files/ 3 | 4 | # compiled output 5 | /declarations/ 6 | /dist/ 7 | 8 | # misc 9 | /coverage/ 10 | !.* 11 | .*/ 12 | 13 | # ember-try 14 | /.node_modules.ember-try/ 15 | -------------------------------------------------------------------------------- /tests/dummy/app/pods/examples/network/notes/index/template.hbs: -------------------------------------------------------------------------------- 1 | The data loaded from the server is: 2 | 3 | {{#each @model as |note|}} 4 |
5 | {{note.title}} 6 |
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 |
11 |
12 | 21 |
22 | 23 |
24 | 32 | Get started → 33 | 34 |
35 | 36 |
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 | [![npm version](https://img.shields.io/npm/v/ember-cli-fastboot-testing.svg)](http://badge.fury.io/js/ember-cli-fastboot-testing) 4 | [![CI](https://github.com/embermap/ember-cli-fastboot-testing/actions/workflows/ci.yml/badge.svg)](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 | 10 | 13 | 14 | 15 | 16 | 19 | 22 | 23 | 24 | 25 | 28 | 35 | 36 | 37 | 38 | 41 | 48 | 49 | 50 | 51 | 54 | 57 | 58 | 59 | 60 | 63 | 66 | 67 | 68 | 69 | 72 | 79 | 80 | 81 |
8 | Host 9 | 11 | {{this.request.host}} 12 |
17 | Protocol 18 | 20 | {{this.request.protocol}} 21 |
26 | Headers 27 | 29 | {{#each-in this.request.headers.headers as |header value|}} 30 |
31 | {{header}}: {{value}} 32 |
33 | {{/each-in}} 34 |
39 | Query params 40 | 42 | {{#each-in this.request.queryParams as |param value|}} 43 |
44 | {{param}}: {{value}} 45 |
46 | {{/each-in}} 47 |
52 | Path 53 | 55 | {{this.request.path}} 56 |
61 | Method 62 | 64 | {{this.request.method}} 65 |
70 | Cookies 71 | 73 | {{#each-in this.request.cookies as |cookie value|}} 74 |
75 | {{cookie}}: {{value}} 76 |
77 | {{/each-in}} 78 |
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 `
${err.stack}
`; 147 | }; 148 | --------------------------------------------------------------------------------