Repository: ember-cli/ember-cli
Pull request #7656 (2018-03-01 08:32:40)

Extract `contentFor` into `ember-app-utils`
Main motivation behind this change is on-going extraction into default
packager. Because, we previously bound `contentFor` to application's
context, it was harder to extract the way we assemble certain trees
(config tree, for example).

This change does the following:

+ removes that binding and replaces it with explicit contract (we pass
now pass in additional `options` object) which makes it easier to test
(no need to set up `EmberApp`) and easier to reason about
+ adds missing tests for both "app-boot" and "test-body-footer"
types
+ removes `contentFor` method from `EmberApp`: not a breaking change
since it is a private method


Patch:
diff --git a/blueprints/app/files/package.json b/blueprints/app/files/package.json
index 4e50d04f6..ad64a76e6 100644
--- a/blueprints/app/files/package.json
+++ b/blueprints/app/files/package.json
@@ -39,8 +39,7 @@
     "ember-source": "~3.1.0-beta.1<% if (welcome) { %>",
     "ember-welcome-page": "^3.0.0<% } %>",
     "eslint-plugin-ember": "^5.0.0",
-    "loader.js": "^4.2.3",
-    "qunit-dom": "^0.5.0"
+    "loader.js": "^4.2.3"
   },
   "engines": {
     "node": "^4.5 || 6.* || >= 7.*"
diff --git a/lib/broccoli/default-packager.js b/lib/broccoli/default-packager.js
index 9a2dba8c5..4c4e67d06 100644
--- a/lib/broccoli/default-packager.js
+++ b/lib/broccoli/default-packager.js
@@ -3,7 +3,6 @@
 const Funnel = require('broccoli-funnel');
 
 const DEFAULT_BOWER_PATH = 'bower_components';
-const DEFAULT_VENDOR_PATH = 'vendor';
 
 /**
  * Responsible for packaging Ember.js application.
@@ -14,7 +13,6 @@ const DEFAULT_VENDOR_PATH = 'vendor';
 module.exports = class DefaultPackager {
   constructor() {
     this._cachedBower = null;
-    this._cachedVendor = null;
   }
 
   /*
@@ -46,6 +44,7 @@ module.exports = class DefaultPackager {
   packageBower(tree, bowerDirectory) {
     if (this._cachedBower === null) {
       this._cachedBower = new Funnel(tree, {
+        srcDir: '/',
         destDir: bowerDirectory || DEFAULT_BOWER_PATH,
         annotation: 'Packaged Bower',
       });
@@ -53,48 +52,4 @@ module.exports = class DefaultPackager {
 
     return this._cachedBower;
   }
-
-  /*
-   * Given an input tree, returns a properly assembled Broccoli tree with vendor
-   * files.
-   *
-   * Given a tree:
-   *
-   * ```
-   * ├── babel-polyfill/
-   * ├── ember-cli-shims/
-   * ├── ember-load-initializers/
-   * ├── ember-qunit/
-   * ├── ember-resolver/
-   * ├── sinon/
-   * └── tether/
-   * ```
-   *
-   * Returns:
-   *
-   * ```
-   * vendor/
-   * ├── babel-polyfill/
-   * ├── ember-cli-shims/
-   * ├── ember-load-initializers/
-   * ├── ember-qunit/
-   * ├── ember-resolver/
-   * ├── sinon/
-   * └── tether/
-   * ```
-   *
-   * @private
-   * @method packageVendor
-   * @param {BroccoliTree} tree
-  */
-  packageVendor(tree) {
-    if (this._cachedVendor === null) {
-      this._cachedVendor = new Funnel(tree, {
-        destDir: DEFAULT_VENDOR_PATH,
-        annotation: 'Packaged Vendor',
-      });
-    }
-
-    return this._cachedVendor;
-  }
 };
diff --git a/lib/broccoli/ember-app.js b/lib/broccoli/ember-app.js
index 1ccb67593..bd5f4f2a8 100644
--- a/lib/broccoli/ember-app.js
+++ b/lib/broccoli/ember-app.js
@@ -4,7 +4,6 @@
 /**
 @module ember-cli
 */
-const fs = require('fs');
 const existsSync = require('exists-sync');
 const path = require('path');
 const p = require('ember-cli-preprocess-registry/preprocessors');
@@ -54,7 +53,7 @@ const createApplicationJsStrategy = strategies.createApplicationJsStrategy;
 
 const normalizeUrl = emberAppUtils.normalizeUrl;
 const convertObjectToString = emberAppUtils.convertObjectToString;
-const calculateBaseTag = emberAppUtils.calculateBaseTag;
+const contentFor = emberAppUtils.contentFor;
 
 let DEFAULT_CONFIG = {
   storeConfigInMeta: true,
@@ -895,6 +894,13 @@ class EmberApp {
     @return
   */
   _configReplacePatterns() {
+    let contentForOptions = {
+      addons: this.project.addons,
+      autoRun: this.options.autoRun,
+      storeConfigInMeta: this.options.storeConfigInMeta,
+      isModuleUnification: experiments.MODULE_UNIFICATION && !!this.trees.src,
+    };
+
     return [{
       match: /{{rootURL}}/g,
       replacement(config) {
@@ -907,7 +913,9 @@ class EmberApp {
       },
     }, {
       match: /{{content-for ['"](.+)["']}}/g,
-      replacement: this.contentFor.bind(this),
+      replacement(config, match, type) {
+        return contentFor(config, match, type, contentForOptions);
+      },
     }, {
       match: /{{MODULE_PREFIX}}/g,
       replacement(config) {
@@ -1189,22 +1197,40 @@ class EmberApp {
 
   /**
     @private
-    @method _processedExternalTree
+    @method _processedVendorTree
     @return
   */
-  _processedExternalTree() {
-    if (!this._cachedExternalTree) {
-      let vendorTrees = this.addonTreesFor('vendor');
+  _processedVendorTree() {
+    if (!this._cachedVendorTree) {
+      let trees = this.addonTreesFor('vendor');
 
       if (this.trees.vendor) {
-        vendorTrees.push(this.trees.vendor);
+        trees.push(this.trees.vendor);
       }
 
-      let vendor = this._defaultPackager.packageVendor(mergeTrees(vendorTrees, {
+      let mergedVendor = mergeTrees(trees, {
         overwrite: true,
         annotation: 'TreeMerger (vendor)',
-      }));
+      });
 
+      this._cachedVendorTree = new Funnel(mergedVendor, {
+        srcDir: '/',
+        destDir: 'vendor/',
+        annotation: 'Funnel (vendor)',
+      });
+    }
+
+    return this._cachedVendorTree;
+  }
+
+  /**
+    @private
+    @method _processedExternalTree
+    @return
+  */
+  _processedExternalTree() {
+    if (!this._cachedExternalTree) {
+      let vendor = this._processedVendorTree();
       let addons = this.addonTree();
 
       let trees = [vendor].concat(addons);
@@ -1900,111 +1926,6 @@ class EmberApp {
 
     return this.addonPostprocessTree('all', tree);
   }
-
-  /**
-    Returns the content for a specific type (section) for index.html.
-
-    Currently supported types:
-    - 'head'
-    - 'config-module'
-    - 'head-footer'
-    - 'test-header-footer'
-    - 'body-footer'
-    - 'test-body-footer'
-
-    Addons can also implement this method and could also define additional
-    types (eg. 'some-addon-section').
-
-    @private
-    @method contentFor
-    @param  {Object} config Application configuration
-    @param  {RegExp} match  Regular expression to match against
-    @param  {String} type   Type of content
-    @return {String}        The content.
-   */
-  contentFor(config, match, type) {
-    let content = [];
-
-    // This normalizes `rootURL` to the value which we use everywhere inside of Ember CLI.
-    // This makes sure that the user doesn't have to account for it in application code.
-    if ('rootURL' in config) {
-      config.rootURL = normalizeUrl(config.rootURL);
-    }
-
-    switch (type) {
-      case 'head': this._contentForHead(content, config); break;
-      case 'config-module': this._contentForConfigModule(content, config); break;
-      case 'app-boot': this._contentForAppBoot(content, config); break;
-      case 'test-body-footer': this._contentForTestBodyFooter(content); break;
-    }
-
-    content = this.project.addons.reduce((content, addon) => {
-      let addonContent = addon.contentFor ? addon.contentFor(type, config, content) : null;
-      if (addonContent) {
-        return content.concat(addonContent);
-      }
-
-      return content;
-    }, content);
-
-    return content.join('\n');
-  }
-
-  /**
-    @private
-    @method _contentForTestBodyFooter
-    @param {Array} content
-  */
-  _contentForTestBodyFooter(content) {
-    content.push('<script>Ember.assert(\'The tests file was not loaded. Make sure your tests index.html includes "assets/tests.js".\', EmberENV.TESTS_FILE_LOADED);</script>');
-  }
-
-  /**
-    @private
-    @method _contentForHead
-    @param {Array} content
-    @param {Object} config
-  */
-  _contentForHead(content, config) {
-    content.push(calculateBaseTag(config.baseURL, config.locationType));
-
-    if (this.options.storeConfigInMeta) {
-      content.push(`<meta name="${config.modulePrefix}/config/environment" content="${escape(JSON.stringify(config))}" />`);
-    }
-  }
-
-  /**
-    @private
-    @method _contentForConfigModule
-    @param {Array} content
-    @param {Object} config
-  */
-  _contentForConfigModule(content, config) {
-    if (this.options.storeConfigInMeta) {
-      content.push(`var prefix = '${config.modulePrefix}';`);
-      content.push(fs.readFileSync(path.join(__dirname, 'app-config-from-meta.js')));
-    } else {
-      content.push(`var exports = {'default': ${JSON.stringify(config)}};` +
-        `Object.defineProperty(exports, '__esModule', {value: true});` +
-        `return exports;`);
-    }
-  }
-
-  /**
-    @private
-    @method _contentForAppBoot
-    @param {Array} content
-    @param {Object} config
-  */
-  _contentForAppBoot(content, config) {
-    if (this.options.autoRun) {
-      let shouldUseSrc = experiments.MODULE_UNIFICATION && !!this.trees.src;
-      let moduleToRequire = `${config.modulePrefix}/${shouldUseSrc ? 'src/main' : 'app'}`;
-      content.push('if (!runningTests) {');
-      content.push(`  require("${moduleToRequire}")["default"].create(${convertObjectToString(config.APP)});`);
-      content.push('}');
-    }
-  }
 }
 
 module.exports = EmberApp;
diff --git a/lib/utilities/ember-app-utils.js b/lib/utilities/ember-app-utils.js
index c807c9346..d3d0c6f5a 100644
--- a/lib/utilities/ember-app-utils.js
+++ b/lib/utilities/ember-app-utils.js
@@ -1,5 +1,7 @@
 'use strict';
 
+const fs = require('fs');
+const path = require('path');
 const cleanBaseURL = require('clean-base-url');
 
 /**
@@ -50,4 +52,95 @@ function calculateBaseTag(baseURL, locationType) {
   return normalizedBaseUrl ? `<base href="${normalizedBaseUrl}" />` : '';
 }
 
-module.exports = { normalizeUrl, convertObjectToString, calculateBaseTag };
+/**
+ * Returns the content for a specific type (section) for index.html.
+ *
+ * ```
+ * {{content-for "[type]"}}
+ * ```
+ *
+ * Supported types:
+ *
+ * - 'head'
+ * - 'config-module'
+ * - 'head-footer'
+ * - 'test-header-footer'
+ * - 'body-footer'
+ * - 'test-body-footer'
+ *
+ * @method contentFor
+ * @param {Object} config Ember.js application configuration
+ * @param {RegExp} mathch Regular expression to match against
+ * @param {String} type Type of content
+ * @param {Object} options Settings that control the default content
+ * @param {Boolean} options.autoRun Controls whether to bootstrap the
+                    application or not
+ * @param {Boolean} options.storeConfigInMeta Controls whether to include the
+                    contents of config
+ * @param {Boolean} options.isModuleUnification Signifies if the application
+                    supports module unification or not
+ * @return {String} The content.
+*/
+function contentFor(config, match, type, options) {
+  let content = [];
+
+  // This normalizes `rootURL` to the value which we use everywhere inside of Ember CLI.
+  // This makes sure that the user doesn't have to account for it in application code.
+  if ('rootURL' in config) {
+    config.rootURL = normalizeUrl(config.rootURL);
+  }
+
+  switch (type) {
+    case 'head':
+      content.push(calculateBaseTag(config.baseURL, config.locationType));
+
+      if (options.storeConfigInMeta) {
+        content.push(`<meta name="${config.modulePrefix}/config/environment" content="${escape(JSON.stringify(config))}" />`);
+      }
+
+      break;
+    case 'config-module':
+      if (options.storeConfigInMeta) {
+        content.push(`var prefix = '${config.modulePrefix}';`);
+        content.push(fs.readFileSync(path.join(__dirname, '../broccoli/app-config-from-meta.js')));
+      } else {
+        content.push(`
+          var exports = {
+            'default': ${JSON.stringify(config)}
+          };
+          Object.defineProperty(exports, '__esModule', {value: true});
+          return exports;
+        `);
+      }
+
+      break;
+    case 'app-boot':
+      if (options.autoRun) {
+        let moduleToRequire = `${config.modulePrefix}/${options.isModuleUnification ? 'src/main' : 'app'}`;
+        content.push(`
+          if (!runningTests) {
+            require("${moduleToRequire}")["default"].create(${convertObjectToString(config.APP)});
+          }
+        `);
+      }
+
+      break;
+    case 'test-body-footer':
+      content.push(`<script>Ember.assert('The tests file was not loaded. Make sure your tests index.html includes "assets/tests.js".', EmberENV.TESTS_FILE_LOADED);</script>`);
+
+      break;
+  }
+
+  content = options.addons.reduce((content, addon) => {
+    let addonContent = addon.contentFor ? addon.contentFor(type, config, content) : null;
+    if (addonContent) {
+      return content.concat(addonContent);
+    }
+
+    return content;
+  }, content);
+
+  return content.join('\n');
+}
+
+module.exports = { normalizeUrl, convertObjectToString, calculateBaseTag, contentFor };


Test patch:
diff --git a/tests/fixtures/addon/npm/package.json b/tests/fixtures/addon/npm/package.json
index 5d8c31e8d..655a2558f 100644
--- a/tests/fixtures/addon/npm/package.json
+++ b/tests/fixtures/addon/npm/package.json
@@ -45,8 +45,7 @@
     "ember-try": "^0.2.23",
     "eslint-plugin-ember": "^5.0.0",
     "eslint-plugin-node": "^5.2.1",
-    "loader.js": "^4.2.3",
-    "qunit-dom": "^0.5.0"
+    "loader.js": "^4.2.3"
   },
   "engines": {
     "node": "^4.5 || 6.* || >= 7.*"
diff --git a/tests/fixtures/addon/yarn/package.json b/tests/fixtures/addon/yarn/package.json
index e23cd0ba6..e076e9da6 100644
--- a/tests/fixtures/addon/yarn/package.json
+++ b/tests/fixtures/addon/yarn/package.json
@@ -46,8 +46,7 @@
     "ember-welcome-page": "^3.0.0",
     "eslint-plugin-ember": "^5.0.0",
     "eslint-plugin-node": "^5.2.1",
-    "loader.js": "^4.2.3",
-    "qunit-dom": "^0.5.0"
+    "loader.js": "^4.2.3"
   },
   "engines": {
     "node": "^4.5 || 6.* || >= 7.*"
diff --git a/tests/fixtures/app/npm/package.json b/tests/fixtures/app/npm/package.json
index d96f914b4..62a5e5add 100644
--- a/tests/fixtures/app/npm/package.json
+++ b/tests/fixtures/app/npm/package.json
@@ -38,8 +38,7 @@
     "ember-resolver": "^4.0.0",
     "ember-source": "~3.1.0-beta.1",
     "eslint-plugin-ember": "^5.0.0",
-    "loader.js": "^4.2.3",
-    "qunit-dom": "^0.5.0"
+    "loader.js": "^4.2.3"
   },
   "engines": {
     "node": "^4.5 || 6.* || >= 7.*"
diff --git a/tests/fixtures/app/yarn/package.json b/tests/fixtures/app/yarn/package.json
index 16abec4a5..149ecd981 100644
--- a/tests/fixtures/app/yarn/package.json
+++ b/tests/fixtures/app/yarn/package.json
@@ -39,8 +39,7 @@
     "ember-source": "~3.1.0-beta.1",
     "ember-welcome-page": "^3.0.0",
     "eslint-plugin-ember": "^5.0.0",
-    "loader.js": "^4.2.3",
-    "qunit-dom": "^0.5.0"
+    "loader.js": "^4.2.3"
   },
   "engines": {
     "node": "^4.5 || 6.* || >= 7.*"
diff --git a/tests/unit/broccoli/default-packager/vendor-test.js b/tests/unit/broccoli/default-packager/vendor-test.js
deleted file mode 100644
index 47d45a256..000000000
--- a/tests/unit/broccoli/default-packager/vendor-test.js
+++ /dev/null
@@ -1,82 +0,0 @@
-'use strict';
-
-const co = require('co');
-const expect = require('chai').expect;
-const DefaultPackager = require('../../../../lib/broccoli/default-packager');
-const broccoliTestHelper = require('broccoli-test-helper');
-
-const buildOutput = broccoliTestHelper.buildOutput;
-const createTempDir = broccoliTestHelper.createTempDir;
-
-describe('Default Packager: Vendor', function() {
-  let input, output;
-
-  let VENDOR_PACKAGES = {
-    'babel-polyfill': {
-      'polyfill.js': 'polyfill',
-      'polyfill.min.js': 'polyfill',
-    },
-    'ember-fetch.js': 'ember fetch',
-    'ember-weakmap-passthrough.js': 'ember weakmap',
-    'ember-weakmap-polyfill.js': 'ember weakmap',
-    'install-getowner-polyfill.js': 'install getowner',
-    'ember-cli-mirage': {
-      'prentender-shim.js': 'pretender',
-    },
-    'ember-cli-shims': {
-      'app-shims.js': 'app shims',
-      'deprecations.js': 'deprecations',
-    },
-    'ember': {
-      'ember.min.js': 'ember',
-    },
-    loader: {
-      'loader.js': 'loader',
-    },
-    'ember-resolver': {
-      'legacy-shims.js': 'legacy shims',
-    },
-    tether: {
-      js: {
-        'tether.js': 'tether',
-      },
-    },
-  };
-
-  before(co.wrap(function *() {
-    input = yield createTempDir();
-
-    input.write(VENDOR_PACKAGES);
-  }));
-
-  after(co.wrap(function *() {
-    yield input.dispose();
-  }));
-
-  afterEach(co.wrap(function *() {
-    yield output.dispose();
-  }));
-
-  it('caches packaged vendor tree', co.wrap(function *() {
-    let defaultPackager = new DefaultPackager();
-
-    expect(defaultPackager._cachedVendor).to.equal(null);
-
-    output = yield buildOutput(defaultPackager.packageVendor(input.path()));
-
-    expect(defaultPackager._cachedVendor).to.not.equal(null);
-    expect(defaultPackager._cachedVendor._annotation).to.equal('Packaged Vendor');
-  }));
-
-  it('packages vendor files', co.wrap(function *() {
-    let defaultPackager = new DefaultPackager();
-
-    output = yield buildOutput(defaultPackager.packageVendor(input.path()));
-
-    let outputFiles = output.read();
-
-    expect(outputFiles).to.deep.equal({
-      vendor: VENDOR_PACKAGES,
-    });
-  }));
-});
diff --git a/tests/unit/broccoli/ember-app-test.js b/tests/unit/broccoli/ember-app-test.js
index c2597a44b..1178750be 100644
--- a/tests/unit/broccoli/ember-app-test.js
+++ b/tests/unit/broccoli/ember-app-test.js
@@ -2,7 +2,6 @@
 
 'use strict';
 
-const fs = require('fs');
 const path = require('path');
 const Project = require('../../../lib/models/project');
 const expect = require('chai').expect;
@@ -276,173 +275,6 @@ describe('EmberApp', function() {
     });
   });
 
-  describe('contentFor', function() {
-    let config, defaultMatch;
-
-    beforeEach(function() {
-      project._addonsInitialized = true;
-      project.addons = [];
-
-      app = new EmberApp({
-        project,
-      });
-
-      config = {
-        modulePrefix: 'cool-foo',
-      };
-
-      defaultMatch = '{{content-for \'head\'}}';
-    });
-
-    describe('contentFor from addons', function() {
-      it('calls `contentFor` on addon', function() {
-        let calledConfig, calledType;
-
-        project.addons.push({
-          contentFor(type, config) {
-            calledType = type;
-            calledConfig = config;
-
-            return 'blammo';
-          },
-        });
-
-        let actual = app.contentFor(config, defaultMatch, 'foo');
-
-        expect(calledConfig).to.deep.equal(config);
-        expect(calledType).to.equal('foo');
-        expect(actual).to.equal('blammo');
-      });
-
-      it('calls `contentFor` on each addon', function() {
-        project.addons.push({
-          contentFor() {
-            return 'blammo';
-          },
-        });
-
-        project.addons.push({
-          contentFor() {
-            return 'blahzorz';
-          },
-        });
-
-        let actual = app.contentFor(config, defaultMatch, 'foo');
-
-        expect(actual).to.equal('blammo\nblahzorz');
-      });
-
-      it('allows later addons to inspect previous content', function() {
-        let calledContent;
-
-        project.addons.push({
-          contentFor() {
-            return 'zero';
-          },
-        });
-
-        project.addons.push({
-          contentFor() {
-            return 'one';
-          },
-        });
-
-        project.addons.push({
-          contentFor(type, config, content) {
-            calledContent = content.slice();
-            content.pop();
-            return 'two';
-          },
-        });
-
-        let actual = app.contentFor(config, defaultMatch, 'foo');
-
-        expect(calledContent).to.deep.equal(['zero', 'one']);
-        expect(actual).to.equal('zero\ntwo');
-      });
-    });
-
-    describe('contentFor("head")', function() {
-      it('includes the `meta` tag in `head` by default', function() {
-        let escapedConfig = escape(JSON.stringify(config));
-        let metaExpected = `<meta name="cool-foo/config/environment" content="${escapedConfig}" />`;
-        let actual = app.contentFor(config, defaultMatch, 'head');
-
-        expect(actual).to.contain(metaExpected);
-      });
-
-      it('does not include the `meta` tag in `head` if storeConfigInMeta is false', function() {
-        app.options.storeConfigInMeta = false;
-
-        let escapedConfig = escape(JSON.stringify(config));
-        let metaExpected = `<meta name="cool-foo/config/environment" content="${escapedConfig}" />`;
-        let actual = app.contentFor(config, defaultMatch, 'head');
-
-        expect(actual).to.not.contain(metaExpected);
-      });
-
-      it('includes the `base` tag in `head` if locationType is auto', function() {
-        config.locationType = 'auto';
-        config.baseURL = '/';
-        let expected = '<base href="/" />';
-        let actual = app.contentFor(config, defaultMatch, 'head');
-
-        expect(actual).to.contain(expected);
-      });
-
-      it('includes the `base` tag in `head` if locationType is none (testem requirement)', function() {
-        config.locationType = 'none';
-        config.baseURL = '/';
-        let expected = '<base href="/" />';
-        let actual = app.contentFor(config, defaultMatch, 'head');
-
-        expect(actual).to.contain(expected);
-      });
-
-      it('does not include the `base` tag in `head` if locationType is hash', function() {
-        config.locationType = 'hash';
-        config.baseURL = '/foo/bar';
-        let expected = '<base href="/foo/bar/" />';
-        let actual = app.contentFor(config, defaultMatch, 'head');
-
-        expect(actual).to.not.contain(expected);
-      });
-
-      it('does not include the `base` tag in `head` if baseURL is undefined', function() {
-        let expected = '<base href=';
-        let actual = app.contentFor(config, defaultMatch, 'head');
-
-        expect(actual).to.not.contain(expected);
-      });
-    });
-
-    describe('contentFor("config-module")', function() {
-      it('includes the meta gathering snippet by default', function() {
-        let metaSnippetPath = path.join(__dirname, '..', '..', '..', 'lib', 'broccoli', 'app-config-from-meta.js');
-        let expected = fs.readFileSync(metaSnippetPath, { encoding: 'utf8' });
-
-        let actual = app.contentFor(config, defaultMatch, 'config-module');
-
-        expect(actual).to.contain(expected);
-      });
-
-      it('includes the raw config if storeConfigInMeta is false', function() {
-        app.options.storeConfigInMeta = false;
-
-        let expected = JSON.stringify(config);
-        let actual = app.contentFor(config, defaultMatch, 'config-module');
-
-        expect(actual).to.contain(expected);
-      });
-    });
-
-    it('has no default value other than `head`', function() {
-      expect(app.contentFor(config, defaultMatch, 'foo')).to.equal('');
-      expect(app.contentFor(config, defaultMatch, 'body')).to.equal('');
-      expect(app.contentFor(config, defaultMatch, 'blah')).to.equal('');
-    });
-  });
-
   describe('addons', function() {
     describe('included hook', function() {
       it('included hook is called properly on instantiation', function() {
@@ -537,6 +369,14 @@ describe('EmberApp', function() {
           td.when(app.addonTreesFor(), { ignoreExtraArgs: true }).thenReturn(['batman']);
         });
 
+        it('_processedVendorTree calls addonTreesFor', function() {
+          app._processedVendorTree();
+
+          let args = td.explain(app.addonTreesFor).calls.map(function(call) { return call.args[0]; });
+
+          expect(args).to.deep.equal(['vendor']);
+        });
+
         it('_processedAppTree calls addonTreesFor', function() {
           app._processedAppTree();
 
diff --git a/tests/unit/utilities/ember-app-utils-test.js b/tests/unit/utilities/ember-app-utils-test.js
index 9af731abc..7f2a9ff65 100644
--- a/tests/unit/utilities/ember-app-utils-test.js
+++ b/tests/unit/utilities/ember-app-utils-test.js
@@ -1,14 +1,224 @@
 'use strict';
 
+const fs = require('fs');
+const path = require('path');
 const expect = require('chai').expect;
 
 const emberAppUtils = require('../../../lib/utilities/ember-app-utils');
 
+const contentFor = emberAppUtils.contentFor;
 const normalizeUrl = emberAppUtils.normalizeUrl;
 const calculateBaseTag = emberAppUtils.calculateBaseTag;
 const convertObjectToString = emberAppUtils.convertObjectToString;
 
 describe('ember-app-utils', function() {
+  describe(`contentFor`, function() {
+    let config = {
+      modulePrefix: 'cool-foo',
+    };
+
+    let defaultMatch = '{{content-for \'head\'}}';
+    let escapedConfig = escape(JSON.stringify(config));
+    let defaultOptions = {
+      storeConfigInMeta: true,
+      autoRun: true,
+      addons: [],
+      isModuleUnification: false,
+    };
+
+    it('returns an empty string if invalid type is specified', function() {
+      expect(contentFor(config, defaultMatch, 'foo', defaultOptions)).to.equal('');
+      expect(contentFor(config, defaultMatch, 'body', defaultOptions)).to.equal('');
+      expect(contentFor(config, defaultMatch, 'blah', defaultOptions)).to.equal('');
+    });
+
+    describe('"head"', function() {
+      it('returns `<meta>` tag by default', function() {
+        let actual = contentFor(config, defaultMatch, 'head', defaultOptions);
+        let expected = `<meta name="cool-foo/config/environment" content="${escapedConfig}" />`;
+
+        expect(
+          actual,
+          '`<meta>` tag was included by default'
+        ).to.contain(expected);
+      });
+
+      it('omits `<meta>` tag if `storeConfigInMeta` is false', function() {
+        let options = Object.assign({}, defaultOptions, { storeConfigInMeta: false });
+
+        let output = contentFor(config, defaultMatch, 'head', options);
+        let expected = `<meta name="cool-foo/config/environment" content="${escapedConfig}" />`;
+
+        expect(
+          output,
+          '`<meta>` tag was not included'
+        ).not.to.contain(expected);
+      });
+
+      it('returns `<base>` tag if `locationType` is "auto"', function() {
+        config.locationType = 'auto';
+        config.baseURL = '/';
+
+        let expected = '<base href="/" />';
+        let output = contentFor(config, defaultMatch, 'head', defaultOptions);
+
+        expect(
+          output,
+          '`<base>` tag was included'
+        ).to.contain(expected);
+      });
+
+      // this is required by testem
+      it('returns `<base>` tag if `locationType` is "none"', function() {
+        config.locationType = 'none';
+        config.baseURL = '/';
+
+        let output = contentFor(config, defaultMatch, 'head', defaultOptions);
+        let expected = '<base href="/" />';
+
+        expect(
+          output,
+          '`<base>` tag was included'
+        ).to.contain(expected);
+      });
+
+      it('omits `<base>` tag if `locationType` is "hash"', function() {
+        config.locationType = 'hash';
+        config.baseURL = '/foo/bar';
+
+        let expected = '<base href="/foo/bar/" />';
+        let output = contentFor(config, defaultMatch, 'head', defaultOptions);
+
+        expect(
+          output,
+          '`<base>` tag was not included'
+        ).to.not.contain(expected);
+      });
+
+      it('omits `<base>` tag if `baseURL` is `undefined`', function() {
+        let expected = '<base href=';
+        let output = contentFor(config, defaultMatch, 'head', defaultOptions);
+
+        expect(
+          output,
+          '`<base>` tag was not included'
+        ).to.not.contain(expected);
+      });
+    });
+
+    describe('"config-module"', function() {
+      it('returns `<meta>` tag gathering snippet by default', function() {
+        let metaSnippetPath = path.join(__dirname, '..', '..', '..', 'lib', 'broccoli', 'app-config-from-meta.js');
+        let expected = fs.readFileSync(metaSnippetPath, { encoding: 'utf8' });
+
+        let output = contentFor(config, defaultMatch, 'config-module', defaultOptions);
+
+        expect(
+          output,
+          'includes `<meta>` tag snippet'
+        ).to.contain(expected);
+      });
+
+      it('returns "raw" config if `storeConfigInMeta` is false', function() {
+        let options = Object.assign({}, defaultOptions, { storeConfigInMeta: false });
+        let expected = JSON.stringify(config);
+        let output = contentFor(config, defaultMatch, 'config-module', options);
+
+        expect(
+          output,
+          'includes "raw" config'
+        ).to.contain(expected);
+      });
+    });
+
+    describe('"app-boot"', function() {
+      it('returns application bootstrap snippet by default', function() {
+        let output = contentFor(config, defaultMatch, 'app-boot', defaultOptions);
+
+        expect(
+          output,
+          'includes applicaton bootstrap snippet'
+        ).to.contain('require("cool-foo/app")["default"].create({});');
+      });
+
+      it('returns application bootstrap snippet with MU module name if `isModuleUnification` is true', function() {
+        let options = Object.assign({}, defaultOptions, { isModuleUnification: true });
+        let output = contentFor(config, defaultMatch, 'app-boot', options);
+
+        expect(
+          output,
+          'includes applicaton bootstrap snippet'
+        ).to.contain('require("cool-foo/src/main")["default"].create({});');
+      });
+
+      it('omits application bootstrap snippet if `autoRun` is false', function() {
+        let options = Object.assign({}, defaultOptions, { autoRun: false });
+        let output = contentFor(config, defaultMatch, 'app-boot', options);
+
+        expect(
+          output,
+          'includes applicaton bootstrap snippet'
+        ).to.equal('');
+      });
+    });
+
+    describe('"test-body-footer"', function() {
+      it('returns `<script> tag with a failed test load assertion`', function() {
+        let output = contentFor(config, defaultMatch, 'test-body-footer', defaultOptions);
+
+        expect(
+          output,
+          'includes `<script>` tag'
+        ).to.equal(`<script>Ember.assert('The tests file was not loaded. Make sure your tests index.html includes "assets/tests.js".', EmberENV.TESTS_FILE_LOADED);</script>`);
+      });
+    });
+
+    describe(`for addons`, function() {
+      it('allows later addons to inspect previous content', function() {
+        let calledContent;
+        let addons = [{
+          contentFor() {
+            return 'zero';
+          },
+        }, {
+          contentFor() {
+            return 'one';
+          },
+        }, {
+          contentFor(type, config, content) {
+            calledContent = content.slice();
+            content.pop();
+
+            return 'two';
+          },
+        }];
+
+        let options = Object.assign({}, defaultOptions, { addons });
+        let output = contentFor(config, defaultMatch, 'foo', options);
+
+        expect(calledContent).to.deep.equal(['zero', 'one']);
+        expect(output).to.equal('zero\ntwo');
+      });
+
+      it('calls `contentFor` on addons', function() {
+        let addons = [{
+          contentFor() {
+            return 'blammo';
+          },
+        }, {
+          contentFor() {
+            return 'blahzorz';
+          },
+        }];
+
+        let options = Object.assign({}, defaultOptions, { addons });
+        let output = contentFor(config, defaultMatch, 'foo', options);
+
+        expect(output).to.equal('blammo\nblahzorz');
+      });
+    });
+  });
+
   describe(`calculateBaseTag`, function() {
     ['auto', 'history'].forEach(locationType => {
       it(`generates a base tag correctly for location: ${locationType}`, function() {