diff --git a/.eslintignore b/.eslintignore
index 68b8560b4a8..3956d17380d 100644
--- a/.eslintignore
+++ b/.eslintignore
@@ -12,7 +12,7 @@ lib/highlight_js/
 plugins/**/lib/javascripts/locale
 public/
 vendor/
-app/assets/javascripts/discourse/tests/test_helper.js
+app/assets/javascripts/discourse/tests/test-boot-rails.js
 app/assets/javascripts/discourse/tests/fixtures
 node_modules/
 dist/
diff --git a/.prettierignore b/.prettierignore
index 3884169675c..217c164e17c 100644
--- a/.prettierignore
+++ b/.prettierignore
@@ -20,7 +20,7 @@ lib/highlight_js/
 plugins/**/lib/javascripts/locale
 public/
 vendor/
-app/assets/javascripts/discourse/tests/test_helper.js
+app/assets/javascripts/discourse/tests/test-boot-rails.js
 app/assets/javascripts/discourse/tests/fixtures
 node_modules/
 dist/
diff --git a/app/assets/javascripts/discourse-common/addon/lib/debounce.js b/app/assets/javascripts/discourse-common/addon/lib/debounce.js
index 5e30864574f..dcd2693b78c 100644
--- a/app/assets/javascripts/discourse-common/addon/lib/debounce.js
+++ b/app/assets/javascripts/discourse-common/addon/lib/debounce.js
@@ -11,7 +11,9 @@ let testingFunc = isLegacyEmber() ? run : next;
 
 export default function () {
   if (isTesting()) {
-    return testingFunc(...arguments);
+    // Don't include the time argument (in ms)
+    let args = [].slice.call(arguments, 0, -1);
+    return testingFunc.apply(void 0, args);
   } else {
     return debounce(...arguments);
   }
diff --git a/app/assets/javascripts/discourse/ember-cli-build.js b/app/assets/javascripts/discourse/ember-cli-build.js
index 5125e7e08e5..602f51f189b 100644
--- a/app/assets/javascripts/discourse/ember-cli-build.js
+++ b/app/assets/javascripts/discourse/ember-cli-build.js
@@ -37,8 +37,66 @@ module.exports = function (defaults) {
       // We don't use SRI in Rails. Disable here to match:
       enabled: false,
     },
+
+    "ember-cli-terser": {
+      enabled: true,
+      exclude: [
+        "**/test-*.js",
+        "**/core-tests*.js",
+        "**/highlightjs/*",
+        "**/javascripts/*",
+      ],
+    },
+
+    // We need to build tests in prod for theme tests
+    tests: true,
   });
 
+  // Patching a private method is not great, but there's no other way for us to tell
+  // Ember CLI that we want the tests alone in a package without helpers/fixtures, since
+  // we re-use those in the theme tests.
+  app._defaultPackager.packageApplicationTests = function (tree) {
+    let appTestTrees = []
+      .concat(
+        this.packageEmberCliInternalFiles(),
+        this.packageTestApplicationConfig(),
+        tree
+      )
+      .filter(Boolean);
+
+    appTestTrees = mergeTrees(appTestTrees, {
+      overwrite: true,
+      annotation: "TreeMerger (appTestTrees)",
+    });
+
+    let tests = concat(appTestTrees, {
+      inputFiles: [
+        "**/tests/acceptance/*.js",
+        "**/tests/integration/*.js",
+        "**tests/unit/*.js",
+      ],
+      headerFiles: ["vendor/ember-cli/tests-prefix.js"],
+      footerFiles: ["vendor/ember-cli/app-config.js"],
+      outputFile: "/assets/core-tests.js",
+      annotation: "Concat: Core Tests",
+      sourceMapConfig: false,
+    });
+
+    let testHelpers = concat(appTestTrees, {
+      inputFiles: [
+        "**/tests/test-boot-ember-cli.js",
+        "**/tests/helpers/**/*.js",
+        "**/tests/fixtures/**/*.js",
+        "**/tests/setup-tests.js",
+      ],
+      outputFile: "/assets/test-helpers.js",
+      annotation: "Concat: Test Helpers",
+      sourceMapConfig: false,
+    });
+
+    return mergeTrees([tests, testHelpers]);
+  };
+
   // WARNING: We should only import scripts here if they are not in NPM.
   // For example: our very specific version of bootstrap-modal.
   app.import(vendorJs + "bootbox.js");
diff --git a/app/assets/javascripts/discourse/package.json b/app/assets/javascripts/discourse/package.json
index 32b109253a9..056a33e0880 100644
--- a/app/assets/javascripts/discourse/package.json
+++ b/app/assets/javascripts/discourse/package.json
@@ -34,7 +34,7 @@
     "discourse-common": "^1.0.0",
     "discourse-hbr": "^1.0.0",
     "discourse-widget-hbs": "^1.0.0",
-    "ember-auto-import": "^1.10.1",
+    "ember-auto-import": "^1.12.0",
     "ember-buffered-proxy": "^2.0.0-beta.0",
     "ember-cli": "~3.25.3",
     "ember-cli-app-version": "^4.0.0",
diff --git a/app/assets/javascripts/discourse/public/assets/scripts/discourse-test-listen-boot.js b/app/assets/javascripts/discourse/public/assets/scripts/discourse-test-listen-boot.js
new file mode 100644
index 00000000000..8f46890eb42
--- /dev/null
+++ b/app/assets/javascripts/discourse/public/assets/scripts/discourse-test-listen-boot.js
@@ -0,0 +1,4 @@
+document.write(
+  "<style>#ember-testing-container { position: fixed; background: white; bottom: 0; right: 0; width: 640px; height: 384px; overflow: auto; z-index: 9999; border: 1px solid #ccc; transform: translateZ(0)} #ember-testing { width: 200%; height: 200%; transform: scale(0.5); transform-origin: top left; }</style>"
+);
+require('discourse/tests/test-boot-ember-cli');
diff --git a/app/assets/javascripts/discourse/tests/plugin_tests.js.erb b/app/assets/javascripts/discourse/tests/active-plugins.js.erb
similarity index 53%
rename from app/assets/javascripts/discourse/tests/plugin_tests.js.erb
rename to app/assets/javascripts/discourse/tests/active-plugins.js.erb
index 0bb267961df..6b2958541e9 100644
--- a/app/assets/javascripts/discourse/tests/plugin_tests.js.erb
+++ b/app/assets/javascripts/discourse/tests/active-plugins.js.erb
@@ -8,13 +8,4 @@
       require_asset(f)
     end
   end
-
-  Discourse.plugins.each do |p|
-    root_path = "#{File.dirname(p.path)}/test/javascripts"
-
-    to_glob = [root_path + '/**/**.es6']
-    to_glob << (root_path + '/**/**.js') if p.transpile_js
-
-    Dir.glob(to_glob) { |f| require_asset(f) }
-  end
 %>
diff --git a/app/assets/javascripts/discourse/tests/core_plugins_tests.js b/app/assets/javascripts/discourse/tests/core-tests.js
similarity index 75%
rename from app/assets/javascripts/discourse/tests/core_plugins_tests.js
rename to app/assets/javascripts/discourse/tests/core-tests.js
index f9c99ce034e..1890b65888e 100644
--- a/app/assets/javascripts/discourse/tests/core_plugins_tests.js
+++ b/app/assets/javascripts/discourse/tests/core-tests.js
@@ -1,4 +1,3 @@
 //= require_tree ./acceptance
 //= require_tree ./integration
 //= require_tree ./unit
-//= require ./plugin_tests
diff --git a/app/assets/javascripts/discourse/tests/index.html b/app/assets/javascripts/discourse/tests/index.html
index 34c33c48e40..1c9ac2bb559 100644
--- a/app/assets/javascripts/discourse/tests/index.html
+++ b/app/assets/javascripts/discourse/tests/index.html
@@ -50,9 +50,14 @@
     <script src="{{rootURL}}assets/test-support.js"></script>
     <script src="{{rootURL}}assets/discourse.js"></script>
     <script src="{{rootURL}}assets/discourse-markdown.js"></script>
-    <script src="{{rootURL}}assets/discourse/tests/core_plugins_tests.js"></script>
+    <script src="{{rootURL}}assets/discourse/tests/active-plugins.js"></script>
     <script src="{{rootURL}}assets/admin.js"></script>
-    <script src="{{rootURL}}assets/tests.js"></script>
+    <script src="{{rootURL}}assets/test-helpers.js"></script>
+    <script src="{{rootURL}}assets/core-tests.js"></script>
+    <script src="{{rootURL}}assets/discourse/tests/plugin-tests.js"></script>
+    <script>
+      require('discourse/tests/test-boot-ember-cli');
+    </script>
     <script src="{{rootURL}}assets/scripts/discourse-boot.js"></script>
 
     {{content-for "body-footer"}}
diff --git a/app/assets/javascripts/discourse/tests/plugin-tests.js.erb b/app/assets/javascripts/discourse/tests/plugin-tests.js.erb
new file mode 100644
index 00000000000..f2b38319a4a
--- /dev/null
+++ b/app/assets/javascripts/discourse/tests/plugin-tests.js.erb
@@ -0,0 +1,10 @@
+<%
+  Discourse.plugins.each do |p|
+    root_path = "#{File.dirname(p.path)}/test/javascripts"
+
+    to_glob = [root_path + '/**/**.es6']
+    to_glob << (root_path + '/**/**.js') if p.transpile_js
+
+    Dir.glob(to_glob) { |f| require_asset(f) }
+  end
+%>
diff --git a/app/assets/javascripts/discourse/tests/test-helper.js b/app/assets/javascripts/discourse/tests/test-boot-ember-cli.js
similarity index 96%
rename from app/assets/javascripts/discourse/tests/test-helper.js
rename to app/assets/javascripts/discourse/tests/test-boot-ember-cli.js
index ffb266b9724..cfbc0b9ff8c 100644
--- a/app/assets/javascripts/discourse/tests/test-helper.js
+++ b/app/assets/javascripts/discourse/tests/test-boot-ember-cli.js
@@ -34,3 +34,4 @@ document.addEventListener("discourse-booted", () => {
     setupEmberOnerrorValidation: !skippingCore,
   });
 });
+window.EmberENV.TESTS_FILE_LOADED = true;
diff --git a/app/assets/javascripts/discourse/tests/theme_qunit_helper.js b/app/assets/javascripts/discourse/tests/test-helpers-rails.js
similarity index 82%
rename from app/assets/javascripts/discourse/tests/theme_qunit_helper.js
rename to app/assets/javascripts/discourse/tests/test-helpers-rails.js
index a9c60c90f9e..71bb2c44530 100644
--- a/app/assets/javascripts/discourse/tests/theme_qunit_helper.js
+++ b/app/assets/javascripts/discourse/tests/test-helpers-rails.js
@@ -3,4 +3,3 @@
 //= require_tree ./helpers
 //= require_tree ./fixtures
 //= require ./setup-tests
-//= require test-shims
diff --git a/app/assets/javascripts/discourse/tests/theme_qunit_tests_vendor.js b/app/assets/javascripts/discourse/tests/test-support-rails.js
similarity index 69%
rename from app/assets/javascripts/discourse/tests/theme_qunit_tests_vendor.js
rename to app/assets/javascripts/discourse/tests/test-support-rails.js
index 3c6865b1e04..4f916572efc 100644
--- a/app/assets/javascripts/discourse/tests/theme_qunit_tests_vendor.js
+++ b/app/assets/javascripts/discourse/tests/test-support-rails.js
@@ -5,11 +5,10 @@
 //= require fake_xml_http_request
 //= require route-recognizer
 //= require pretender
-
-// These are not loaded in prod or development
-// But we need them for testing handlebars templates in qunit
-//= require handlebars
-//= require ember-template-compiler
-
 //= require sinon
 //= require break_string
+//= require test-shims
+//= require jquery.magnific-popup.min.js
+//= require handlebars
+//= require ember-template-compiler
+//= require markdown-it-bundle
diff --git a/app/assets/javascripts/discourse/tests/test_helper.js b/app/assets/javascripts/discourse/tests/test_helper.js
deleted file mode 100644
index 028f3a5167f..00000000000
--- a/app/assets/javascripts/discourse/tests/test_helper.js
+++ /dev/null
@@ -1,38 +0,0 @@
-// discourse-skip-module
-
-//= require env
-//= require jquery.debug
-//= require jquery.ui.widget
-//= require ember.debug
-//= require message-bus
-//= require qunit
-//= require ember-qunit
-//= require fake_xml_http_request
-//= require route-recognizer
-//= require pretender
-//= require locales/i18n
-//= require locales/en
-//= require discourse-loader
-
-// Our base application
-//= require vendor
-//= require discourse-shims
-//= require markdown-it-bundle
-//= require application
-//= require admin
-
-// These are not loaded in prod or development
-// But we need them for testing handlebars templates in qunit
-//= require handlebars
-//= require ember-template-compiler
-
-// Test helpers
-//= require sinon
-//= require_tree ./helpers
-//= require break_string
-
-//= require_tree ./fixtures
-
-//= require ./setup-tests
-//= require test-shims
-//= require jquery.magnific-popup.min.js
diff --git a/app/assets/javascripts/discourse/tests/theme_qunit_vendor.js b/app/assets/javascripts/discourse/tests/theme_qunit_vendor.js
deleted file mode 100644
index 9eb492b917d..00000000000
--- a/app/assets/javascripts/discourse/tests/theme_qunit_vendor.js
+++ /dev/null
@@ -1,30 +0,0 @@
-// This bundle contains the same dependencies as app/assets/javascripts/vendor.js
-// minus ember_jquery.
-// ember_jquery doesn't work with theme tests in production because it
-// contains production builds of Ember and jQuery, so we have a separate bundle
-// caled theme_qunit_ember_jquery which contains a debug build for Ember and jQuery.
-// We don't put theme_qunit_ember_jquery in this bundle because it would make the
-// bundle too big and cause OOM exceptions during rebuilds for self-hosters on
-// low-end machines.
-
-//= require logster
-
-//= require template_include.js
-
-//= require message-bus
-//= require jquery.ui.widget.js
-//= require Markdown.Converter.js
-//= require bootbox.js
-//= require popper.js
-//= require bootstrap-modal.js
-//= require caret_position
-//= require jquery.sortable.js
-//= require lodash.js
-//= require itsatrap.js
-//= require rsvp.js
-//= require uppy.js
-//= require buffered-proxy
-//= require virtual-dom
-//= require virtual-dom-amd
-//= require discourse-shims
-//= require pretty-text-bundle
diff --git a/app/assets/javascripts/ember_include.js.erb b/app/assets/javascripts/ember_include.js.erb
index df7a68817ec..9bb1c0c59ae 100644
--- a/app/assets/javascripts/ember_include.js.erb
+++ b/app/assets/javascripts/ember_include.js.erb
@@ -1,5 +1,5 @@
 <%
-if Rails.env.development? || Rails.env.test?
+if @force_ember_debug || Rails.env.development? || Rails.env.test?
   require_asset ("ember.debug.js")
 else
   require_asset ("ember.prod.js")
diff --git a/app/assets/javascripts/vendor-common.js b/app/assets/javascripts/vendor-common.js
new file mode 100644
index 00000000000..9ce43717d4c
--- /dev/null
+++ b/app/assets/javascripts/vendor-common.js
@@ -0,0 +1,21 @@
+//= require logster
+
+//= require template_include.js
+
+//= require message-bus
+//= require jquery.ui.widget.js
+//= require Markdown.Converter.js
+//= require bootbox.js
+//= require popper.js
+//= require bootstrap-modal.js
+//= require caret_position
+//= require jquery.sortable.js
+//= require lodash.js
+//= require itsatrap.js
+//= require rsvp.js
+//= require uppy.js
+//= require buffered-proxy
+//= require virtual-dom
+//= require virtual-dom-amd
+//= require discourse-shims
+//= require pretty-text-bundle
diff --git a/app/assets/javascripts/vendor-theme-tests.js b/app/assets/javascripts/vendor-theme-tests.js
new file mode 100644
index 00000000000..0c7aa8c1364
--- /dev/null
+++ b/app/assets/javascripts/vendor-theme-tests.js
@@ -0,0 +1,6 @@
+//= require set-prototype-polyfill
+//= require env
+//= require jquery
+//= require ember.debug.js
+//= require discourse-loader
+//= require vendor-common
diff --git a/app/assets/javascripts/vendor.js b/app/assets/javascripts/vendor.js
index fb49d750d58..7ec26b1ca4f 100644
--- a/app/assets/javascripts/vendor.js
+++ b/app/assets/javascripts/vendor.js
@@ -1,23 +1,2 @@
 //= require ember_jquery
-
-//= require logster
-
-//= require template_include.js
-
-//= require message-bus
-//= require jquery.ui.widget.js
-//= require Markdown.Converter.js
-//= require bootbox.js
-//= require popper.js
-//= require bootstrap-modal.js
-//= require caret_position
-//= require jquery.sortable.js
-//= require lodash.js
-//= require itsatrap.js
-//= require rsvp.js
-//= require uppy.js
-//= require buffered-proxy
-//= require virtual-dom
-//= require virtual-dom-amd
-//= require discourse-shims
-//= require pretty-text-bundle
+//= require vendor-common
diff --git a/app/assets/javascripts/yarn.lock b/app/assets/javascripts/yarn.lock
index 691b5eea85e..80a80588c24 100644
--- a/app/assets/javascripts/yarn.lock
+++ b/app/assets/javascripts/yarn.lock
@@ -4810,6 +4810,41 @@ ember-auto-import@^1.10.1, ember-auto-import@^1.5.3:
     walk-sync "^0.3.3"
     webpack "^4.43.0"
 
+ember-auto-import@^1.12.0:
+  version "1.12.0"
+  resolved "https://registry.yarnpkg.com/ember-auto-import/-/ember-auto-import-1.12.0.tgz#52246b04891090e2608244e65c4c6af7710df12b"
+  integrity sha512-fzMGnyHGfUNFHchpLbJ98Vs/c5H2wZBMR9r/XwW+WOWPisZDGLUPPyhJQsSREPoUQ+o8GvyLaD/rkrKqW8bmgw==
+  dependencies:
+    "@babel/core" "^7.1.6"
+    "@babel/preset-env" "^7.10.2"
+    "@babel/traverse" "^7.1.6"
+    "@babel/types" "^7.1.6"
+    "@embroider/core" "^0.33.0"
+    babel-core "^6.26.3"
+    babel-loader "^8.0.6"
+    babel-plugin-syntax-dynamic-import "^6.18.0"
+    babylon "^6.18.0"
+    broccoli-debug "^0.6.4"
+    broccoli-node-api "^1.7.0"
+    broccoli-plugin "^4.0.0"
+    broccoli-source "^3.0.0"
+    debug "^3.1.0"
+    ember-cli-babel "^7.0.0"
+    enhanced-resolve "^4.0.0"
+    fs-extra "^6.0.1"
+    fs-tree-diff "^2.0.0"
+    handlebars "^4.3.1"
+    js-string-escape "^1.0.1"
+    lodash "^4.17.19"
+    mkdirp "^0.5.1"
+    resolve-package-path "^3.1.0"
+    rimraf "^2.6.2"
+    semver "^7.3.4"
+    symlink-or-copy "^1.2.0"
+    typescript-memoize "^1.0.0-alpha.3"
+    walk-sync "^0.3.3"
+    webpack "^4.43.0"
+
 ember-buffered-proxy@^2.0.0-beta.0:
   version "2.0.0-beta.0"
   resolved "https://registry.yarnpkg.com/ember-buffered-proxy/-/ember-buffered-proxy-2.0.0-beta.0.tgz#65be4e2d0dcf40a5a2dab548c84a21aa332555a2"
diff --git a/app/controllers/bootstrap_controller.rb b/app/controllers/bootstrap_controller.rb
index 811058d7526..af3919e8285 100644
--- a/app/controllers/bootstrap_controller.rb
+++ b/app/controllers/bootstrap_controller.rb
@@ -69,7 +69,7 @@ class BootstrapController < ApplicationController
       locale_script: locale,
       stylesheets: @stylesheets,
       plugin_js: plugin_js,
-      plugin_test_js: [script_asset_path("plugin_tests")],
+      plugin_test_js: [script_asset_path("plugin-tests")],
       setup_data: client_side_setup_data,
       preloaded: @preloaded,
       html: create_html,
diff --git a/app/controllers/qunit_controller.rb b/app/controllers/qunit_controller.rb
index 309d2af8b20..003b3430245 100644
--- a/app/controllers/qunit_controller.rb
+++ b/app/controllers/qunit_controller.rb
@@ -8,15 +8,32 @@ class QunitController < ApplicationController
   }
   layout false
 
+  def is_ember_cli_proxy?
+    request.headers["HTTP_X_DISCOURSE_EMBER_CLI"] == "true"
+  end
+
   # only used in test / dev
   def index
-    raise Discourse::NotFound.new if request.headers["HTTP_X_DISCOURSE_EMBER_CLI"] == "true"
+    raise Discourse::NotFound.new if is_ember_cli_proxy?
     raise Discourse::InvalidAccess.new if Rails.env.production?
   end
 
   def theme
     raise Discourse::NotFound.new if !can_see_theme_qunit?
 
+    @is_proxied = is_ember_cli_proxy?
+    @legacy_ember = if Rails.env.production?
+      ENV['EMBER_CLI_PROD_ASSETS'] != "1"
+    else
+      !@is_proxied
+    end
+
+    # In production mode all bundles use `application`
+    @app_bundle = "application"
+    if Rails.env.development? && @is_proxied
+      @app_bundle = "discourse"
+    end
+
     param_key = nil
     @suggested_themes = nil
     if (id = get_param(:id)).present?
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index c5a96271231..f232bd5b7e5 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -13,9 +13,11 @@ module ApplicationHelper
     @extra_body_classes ||= Set.new
   end
 
-  def discourse_config_environment
+  def discourse_config_environment(testing: false)
+
     # TODO: Can this come from Ember CLI somehow?
-    { modulePrefix: "discourse",
+    config = {
+      modulePrefix: "discourse",
       environment: Rails.env,
       rootURL: Discourse.base_path,
       locationType: "auto",
@@ -32,7 +34,16 @@ module ApplicationHelper
         version: "#{Discourse::VERSION::STRING} #{Discourse.git_version}",
         exportApplicationGlobal: true
       }
-    }.to_json
+    }
+
+    if testing
+      config[:environment] = "test"
+      config[:locationType] = "none"
+      config[:APP][:autoboot] = false
+      config[:APP][:rootElement] = '#ember-testing'
+    end
+
+    config.to_json
   end
 
   def google_universal_analytics_json(ua_domain_name = nil)
diff --git a/app/helpers/qunit_helper.rb b/app/helpers/qunit_helper.rb
index e0376a1ad26..9dec03aa213 100644
--- a/app/helpers/qunit_helper.rb
+++ b/app/helpers/qunit_helper.rb
@@ -1,6 +1,38 @@
 # frozen_string_literal: true
 
 module QunitHelper
+
+  def vendor_theme_tests
+    return preload_script("vendor-theme-tests") if @legacy_ember
+    preload_script("vendor")
+  end
+
+  def support_bundles
+    result = []
+    if Rails.env.production? || @legacy_ember
+      result << preload_script("discourse/tests/test-support-rails")
+      result << preload_script("discourse/tests/test-helpers-rails")
+    else
+      result << preload_script("test-support")
+      result << preload_script("test-helpers")
+    end
+    result.join("\n").html_safe
+  end
+
+  def boot_bundles
+    result = []
+    if @legacy_ember
+      result << preload_script("discourse/tests/test_starter")
+    elsif @is_proxied
+      result << preload_script("scripts/discourse-test-listen-boot")
+      result << preload_script("scripts/discourse-boot")
+    else
+      result << preload_script("discourse-test-listen-boot")
+      result << preload_script("discourse-boot")
+    end
+    result.join("\n").html_safe
+  end
+
   def theme_tests
     theme = Theme.find_by(id: request.env[:resolved_theme_id])
     return "" if theme.blank?
diff --git a/app/views/qunit/index.html.erb b/app/views/qunit/index.html.erb
index 830e3d8bb50..9912387a2d7 100644
--- a/app/views/qunit/index.html.erb
+++ b/app/views/qunit/index.html.erb
@@ -5,8 +5,15 @@
     <%= discourse_color_scheme_stylesheets %>
     <%= discourse_stylesheet_link_tag(:desktop, theme_id: nil) %>
     <%= discourse_stylesheet_link_tag(:test_helper, theme_id: nil) %>
-    <%= preload_script "discourse/tests/test_helper" %>
-    <%= preload_script "discourse/tests/core_plugins_tests" %>
+    <%= preload_script "locales/#{I18n.locale}" %>
+    <%= preload_script "vendor" %>
+    <%= preload_script "application" %>
+    <%= preload_script "admin" %>
+    <%= preload_script "discourse/tests/test-support-rails" %>
+    <%= preload_script "discourse/tests/test-helpers-rails" %>
+    <%= preload_script "discourse/tests/active-plugins" %>
+    <%= preload_script "discourse/tests/core-tests" %>
+    <%= preload_script "discourse/tests/plugin-tests" %>
     <%= preload_script "discourse/tests/test_starter" %>
     <%= csrf_meta_tags %>
     <meta property="og:title" content="">
diff --git a/app/views/qunit/theme.html.erb b/app/views/qunit/theme.html.erb
index 53752cc9a29..30b9f1260f5 100644
--- a/app/views/qunit/theme.html.erb
+++ b/app/views/qunit/theme.html.erb
@@ -6,17 +6,12 @@
     <%- if !@suggested_themes %>
       <%= discourse_stylesheet_link_tag(:desktop, theme_id: nil) %>
       <%= discourse_stylesheet_link_tag(:test_helper, theme_id: nil) %>
-      <%= preload_script "locales/en" %>
-      <%= preload_script "discourse/tests/theme_qunit_ember_jquery" %>
-      <%= preload_script "discourse/tests/theme_qunit_vendor" %>
-      <%= preload_script "discourse/tests/theme_qunit_tests_vendor" %>
-      <%= preload_script "markdown-it-bundle" %>
-      <%= preload_script "application" %>
-      <%- Discourse.find_plugin_js_assets(include_official: allow_plugins?, include_unofficial: allow_third_party_plugins?, request: request).each do |file| %>
-        <%= preload_script file %>
-      <%- end %>
+      <%= preload_script "locales/#{I18n.locale}" %>
+      <%= vendor_theme_tests %>
+      <%= preload_script @app_bundle %>
       <%= preload_script "admin" %>
-      <%= preload_script "discourse/tests/theme_qunit_helper" %>
+      <%= preload_script "discourse/tests/active-plugins" %>
+      <%= support_bundles %>
       <%= theme_translations_lookup %>
       <%= theme_js_lookup %>
       <%= theme_lookup("head_tag") %>
@@ -24,7 +19,7 @@
       <%= tag.meta id: 'data-discourse-setup', data: client_side_setup_data %>
       <meta property="og:title" content="">
       <meta property="og:url" content="">
-      <%= preload_script "discourse/tests/test_starter" %>
+      <meta name="discourse/config/environment" content="<%=u discourse_config_environment(testing: true) %>" />
     <%- else %>
       <style>
         html {
@@ -35,8 +30,10 @@
   </head>
   <body>
     <%- if !@suggested_themes %>
-      <div id="qunit"></div>
-      <div id="qunit-fixture"></div>
+      <%- if @legacy_ember %>
+        <div id="qunit"></div>
+        <div id="qunit-fixture"></div>
+      <%- end %>
     <%- else %>
       <h2>Theme QUnit Test Runner</h2>
       <%- if @suggested_themes.size == 0 %>
@@ -49,4 +46,7 @@
       <%- end %>
     <%- end %>
   </body>
+  <%- if !@suggested_themes %>
+    <%= boot_bundles %>
+  <%- end %>
 </html>
diff --git a/config/application.rb b/config/application.rb
index 59394707820..1c4a0bb51b4 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -176,13 +176,18 @@ module Discourse
       confirm-new-email/bootstrap.js
       onpopstate-handler.js
       embed-application.js
-      discourse/tests/theme_qunit_ember_jquery.js
-      discourse/tests/theme_qunit_vendor.js
-      discourse/tests/theme_qunit_tests_vendor.js
-      discourse/tests/theme_qunit_helper.js
+      discourse/tests/active-plugins.js
       discourse/tests/test_starter.js
     }
 
+    if ENV['EMBER_CLI_PROD_ASSETS'] != "1"
+      config.assets.precompile += %w{
+        discourse/tests/test-support-rails.js
+        discourse/tests/test-helpers-rails.js
+        vendor-theme-tests.js
+      }
+    end
+
     # Precompile all available locales
     unless GlobalSetting.try(:omit_base_locales)
       Dir.glob("#{config.root}/app/assets/javascripts/locales/*.js.erb").each do |file|
@@ -370,7 +375,7 @@ module Discourse
             %w{qunit.js
               qunit.css
               test_helper.css
-              discourse/tests/test_helper.js
+              discourse/tests/test-boot-rails.js
               wizard/test/test_helper.js
             }.include?(logical_path) ||
             logical_path =~ /\/node_modules/ ||
diff --git a/lib/autospec/qunit_runner.rb b/lib/autospec/qunit_runner.rb
index a50ed7593a3..ace0fbef386 100644
--- a/lib/autospec/qunit_runner.rb
+++ b/lib/autospec/qunit_runner.rb
@@ -32,7 +32,7 @@ module Autospec
     # Discourse specific
     reload(%r{^discourse/tests/javascripts/fixtures/.+_fixtures\.js(\.es6)?$})
     reload(%r{^discourse/tests/javascripts/(helpers|mixins)/.+\.js(\.es6)?$})
-    reload("app/assets/javascripts/discoruse/tests/javascripts/test_helper.js")
+    reload("app/assets/javascripts/discoruse/tests/javascripts/test-boot-rails.js")
 
     watch(%r{^plugins/.*/test/.+\.js(\.es6)?$})
 
diff --git a/lib/tasks/assets.rake b/lib/tasks/assets.rake
index c8025a92cea..f57971cee9b 100644
--- a/lib/tasks/assets.rake
+++ b/lib/tasks/assets.rake
@@ -39,8 +39,7 @@ task 'assets:precompile:before' do
     # Remove the assets that Ember CLI will handle for us
     Rails.configuration.assets.precompile.reject! do |asset|
       asset.is_a?(String) &&
-        (%w(application.js admin.js ember_jquery.js pretty-text-bundle.js start-discourse.js vendor.js).include?(asset) ||
-          asset.start_with?("discourse/tests"))
+        (%w(application.js admin.js ember_jquery.js pretty-text-bundle.js start-discourse.js vendor.js).include?(asset))
     end
   end
 end
@@ -250,9 +249,14 @@ def copy_ember_cli_assets
   # Copy assets and generate manifest data
   log_task_duration('Copy assets and generate manifest data') {
     Dir["#{ember_cli_assets}**/*"].each do |f|
-      if f !~ /test/ && File.file?(f)
+      if File.file?(f)
         rel_file = f.sub(ember_cli_assets, "")
-        digest = f.scan(/\-([a-f0-9]+)\./)[0][0]
+        file_digest = Digest::SHA384.digest(File.read(f))
+        digest = if f =~ /\-([a-f0-9]+)\./
+          Regexp.last_match[1]
+        else
+          Digest.hexencode(file_digest)[0...32]
+        end
 
         dest = "public/assets"
         dest_sub = dest
@@ -263,10 +267,17 @@ def copy_ember_cli_assets
         FileUtils.mkdir_p(dest_sub) unless Dir.exist?(dest_sub)
         log_file = File.basename(rel_file).sub("-#{digest}", "")
 
-        # It's simpler to serve the file as `application.js`
-        if log_file == "discourse.js"
+        # We need a few hacks here to move what Ember uses to what Rails wants
+        case log_file
+        when "discourse.js"
           log_file = "application.js"
           rel_file.sub!(/^discourse/, "application")
+        when "test-support.js"
+          log_file = "discourse/tests/test-support-rails.js"
+          rel_file = "discourse/tests/test-support-rails-#{digest}.js"
+        when "test-helpers.js"
+          log_file = "discourse/tests/test-helpers-rails.js"
+          rel_file = "discourse/tests/test-helpers-rails-#{digest}.js"
         end
 
         res = FileUtils.cp(f, "#{dest}/#{rel_file}")
@@ -277,7 +288,7 @@ def copy_ember_cli_assets
           "mtime" => File.mtime(f).iso8601(9),
           "size" => File.size(f),
           "digest" => digest,
-          "integrity" => "sha384-#{Base64.encode64(Digest::SHA384.digest(File.read(f))).chomp}"
+          "integrity" => "sha384-#{Base64.encode64(file_digest).chomp}"
         }
       end
     end
diff --git a/spec/requests/qunit_controller_spec.rb b/spec/requests/qunit_controller_spec.rb
index bf06a012a85..ed58be10e2d 100644
--- a/spec/requests/qunit_controller_spec.rb
+++ b/spec/requests/qunit_controller_spec.rb
@@ -98,16 +98,13 @@ describe QunitController do
         expect(response.body).to include("/stylesheets/desktop_")
         expect(response.body).to include("/stylesheets/test_helper_")
         expect(response.body).to include("/assets/locales/en.js")
-        expect(response.body).to include("/assets/discourse/tests/theme_qunit_ember_jquery.js")
-        expect(response.body).to include("/assets/discourse/tests/theme_qunit_vendor.js")
-        expect(response.body).to include("/assets/discourse/tests/theme_qunit_tests_vendor.js")
+        expect(response.body).to include("/test-support")
+        expect(response.body).to include("/test-helpers")
         expect(response.body).to include("/assets/markdown-it-bundle.js")
         expect(response.body).to include("/assets/application.js")
         expect(response.body).to include("/assets/admin.js")
-        expect(response.body).to include("/assets/discourse/tests/theme_qunit_helper.js")
         expect(response.body).to match(/\/theme-javascripts\/\h{40}\.js/)
         expect(response.body).to include("/theme-javascripts/tests/#{theme.id}-")
-        expect(response.body).to include("/assets/discourse/tests/test_starter.js")
       end
     end
   end