FEATURE: Allow theme tests to be run in production (#12815)
This commit allows site admins to run theme tests in production via a new `/theme-qunit` route. When you visit `/theme-qunit`, you'll see a list of the themes/components installed on your site that have tests, and from there you can select a theme or component that you run its tests. We also have a new rake task `themes:install_and_test` that can be used to install a list of themes/components on a temporary database and run the tests of the themes/components that are installed. This rake task can be useful when upgrading/deploying a Discourse instance to make sure that the installed themes/components are compatible with the new Discourse version being deployed, and if the tests fail you can abort the build/deploy process so you don't end up with a broken site.
This commit is contained in:
parent
7c3268e0c5
commit
7217dcb67a
|
@ -0,0 +1,4 @@
|
|||
//= require_tree ./acceptance
|
||||
//= require_tree ./integration
|
||||
//= require_tree ./unit
|
||||
//= require ./plugin_tests
|
|
@ -253,7 +253,6 @@ function setupTestsCommon(application, container, config) {
|
|||
let pluginPath = getUrlParameter("qunit_single_plugin")
|
||||
? "/" + getUrlParameter("qunit_single_plugin") + "/"
|
||||
: "/plugins/";
|
||||
let themeOnly = getUrlParameter("theme_name") || getUrlParameter("theme_url");
|
||||
|
||||
if (getUrlParameter("qunit_disable_auto_start") === "1") {
|
||||
QUnit.config.autostart = false;
|
||||
|
@ -263,19 +262,11 @@ function setupTestsCommon(application, container, config) {
|
|||
let isTest = /\-test/.test(entry);
|
||||
let regex = new RegExp(pluginPath);
|
||||
let isPlugin = regex.test(entry);
|
||||
let isTheme = /^discourse\/theme\-\d+\/.+/.test(entry);
|
||||
|
||||
if (!isTest) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (themeOnly) {
|
||||
if (isTheme) {
|
||||
require(entry, null, null, true);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!skipCore || isPlugin) {
|
||||
require(entry, null, null, true);
|
||||
}
|
||||
|
|
|
@ -5,11 +5,11 @@
|
|||
//= require jquery.ui.widget
|
||||
//= require ember.debug
|
||||
//= require message-bus
|
||||
//= require qunit/qunit/qunit
|
||||
//= require qunit
|
||||
//= require ember-qunit
|
||||
//= require fake_xml_http_request
|
||||
//= require route-recognizer
|
||||
//= require pretender/pretender
|
||||
//= require pretender
|
||||
//= require locales/i18n
|
||||
//= require locales/en
|
||||
//= require discourse-loader
|
||||
|
@ -28,16 +28,12 @@
|
|||
//= require ember-template-compiler
|
||||
|
||||
// Test helpers
|
||||
//= require sinon/pkg/sinon
|
||||
//= require sinon
|
||||
//= require_tree ./helpers
|
||||
//= require break_string
|
||||
|
||||
// Finally, the tests themselves
|
||||
//= require_tree ./fixtures
|
||||
//= require_tree ./acceptance
|
||||
//= require_tree ./integration
|
||||
//= require_tree ./unit
|
||||
//= require plugin_tests
|
||||
//= require setup-tests
|
||||
|
||||
//= require ./setup-tests
|
||||
//= require test-shims
|
||||
//= require jquery.magnific-popup.min.js
|
||||
|
|
|
@ -6,10 +6,10 @@
|
|||
//= require ember.debug
|
||||
//= require locales/i18n
|
||||
//= require locales/en
|
||||
//= require route-recognizer/dist/route-recognizer
|
||||
//= require route-recognizer
|
||||
//= require fake_xml_http_request
|
||||
//= require pretender/pretender
|
||||
//= require qunit/qunit/qunit
|
||||
//= require pretender
|
||||
//= require qunit
|
||||
//= require ember-qunit
|
||||
//= require discourse-loader
|
||||
//= require jquery.debug
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
@import '/stylesheets/desktop.css';
|
||||
@import 'qunit/qunit/qunit.css';
|
||||
@import "vendor/qunit";
|
||||
|
||||
.modal-backdrop {
|
||||
display: none;
|
|
@ -0,0 +1,436 @@
|
|||
/*!
|
||||
* QUnit 2.8.0
|
||||
* https://qunitjs.com/
|
||||
*
|
||||
* Copyright jQuery Foundation and other contributors
|
||||
* Released under the MIT license
|
||||
* https://jquery.org/license
|
||||
*
|
||||
* Date: 2018-11-02T16:17Z
|
||||
*/
|
||||
|
||||
/** Font Family and Sizes */
|
||||
|
||||
#qunit-tests, #qunit-header, #qunit-banner, #qunit-testrunner-toolbar, #qunit-filteredTest, #qunit-userAgent, #qunit-testresult {
|
||||
font-family: "Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
#qunit-testrunner-toolbar, #qunit-filteredTest, #qunit-userAgent, #qunit-testresult, #qunit-tests li { font-size: small; }
|
||||
#qunit-tests { font-size: smaller; }
|
||||
|
||||
|
||||
/** Resets */
|
||||
|
||||
#qunit-tests, #qunit-header, #qunit-banner, #qunit-filteredTest, #qunit-userAgent, #qunit-testresult, #qunit-modulefilter {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
|
||||
/** Header (excluding toolbar) */
|
||||
|
||||
#qunit-header {
|
||||
padding: 0.5em 0 0.5em 1em;
|
||||
|
||||
color: #8699A4;
|
||||
background-color: #0D3349;
|
||||
|
||||
font-size: 1.5em;
|
||||
line-height: 1em;
|
||||
font-weight: 400;
|
||||
|
||||
border-radius: 5px 5px 0 0;
|
||||
}
|
||||
|
||||
#qunit-header a {
|
||||
text-decoration: none;
|
||||
color: #C2CCD1;
|
||||
}
|
||||
|
||||
#qunit-header a:hover,
|
||||
#qunit-header a:focus {
|
||||
color: #FFF;
|
||||
}
|
||||
|
||||
#qunit-banner {
|
||||
height: 5px;
|
||||
}
|
||||
|
||||
#qunit-filteredTest {
|
||||
padding: 0.5em 1em 0.5em 1em;
|
||||
color: #366097;
|
||||
background-color: #F4FF77;
|
||||
}
|
||||
|
||||
#qunit-userAgent {
|
||||
padding: 0.5em 1em 0.5em 1em;
|
||||
color: #FFF;
|
||||
background-color: #2B81AF;
|
||||
text-shadow: rgba(0, 0, 0, 0.5) 2px 2px 1px;
|
||||
}
|
||||
|
||||
|
||||
/** Toolbar */
|
||||
|
||||
#qunit-testrunner-toolbar {
|
||||
padding: 0.5em 1em 0.5em 1em;
|
||||
color: #5E740B;
|
||||
background-color: #EEE;
|
||||
}
|
||||
|
||||
#qunit-testrunner-toolbar .clearfix {
|
||||
height: 0;
|
||||
clear: both;
|
||||
}
|
||||
|
||||
#qunit-testrunner-toolbar label {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
#qunit-testrunner-toolbar input[type=checkbox],
|
||||
#qunit-testrunner-toolbar input[type=radio] {
|
||||
margin: 3px;
|
||||
vertical-align: -2px;
|
||||
}
|
||||
|
||||
#qunit-testrunner-toolbar input[type=text] {
|
||||
box-sizing: border-box;
|
||||
height: 1.6em;
|
||||
}
|
||||
|
||||
.qunit-url-config,
|
||||
.qunit-filter,
|
||||
#qunit-modulefilter {
|
||||
display: inline-block;
|
||||
line-height: 2.1em;
|
||||
}
|
||||
|
||||
.qunit-filter,
|
||||
#qunit-modulefilter {
|
||||
float: right;
|
||||
position: relative;
|
||||
margin-left: 1em;
|
||||
}
|
||||
|
||||
.qunit-url-config label {
|
||||
margin-right: 0.5em;
|
||||
}
|
||||
|
||||
#qunit-modulefilter-search {
|
||||
box-sizing: border-box;
|
||||
width: 400px;
|
||||
}
|
||||
|
||||
#qunit-modulefilter-search-container:after {
|
||||
position: absolute;
|
||||
right: 0.3em;
|
||||
content: "\25bc";
|
||||
color: black;
|
||||
}
|
||||
|
||||
#qunit-modulefilter-dropdown {
|
||||
/* align with #qunit-modulefilter-search */
|
||||
box-sizing: border-box;
|
||||
width: 400px;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 50%;
|
||||
margin-top: 0.8em;
|
||||
|
||||
border: 1px solid #D3D3D3;
|
||||
border-top: none;
|
||||
border-radius: 0 0 .25em .25em;
|
||||
color: #000;
|
||||
background-color: #F5F5F5;
|
||||
z-index: 99;
|
||||
}
|
||||
|
||||
#qunit-modulefilter-dropdown a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
#qunit-modulefilter-dropdown .clickable.checked {
|
||||
font-weight: bold;
|
||||
color: #000;
|
||||
background-color: #D2E0E6;
|
||||
}
|
||||
|
||||
#qunit-modulefilter-dropdown .clickable:hover {
|
||||
color: #FFF;
|
||||
background-color: #0D3349;
|
||||
}
|
||||
|
||||
#qunit-modulefilter-actions {
|
||||
display: block;
|
||||
overflow: auto;
|
||||
|
||||
/* align with #qunit-modulefilter-dropdown-list */
|
||||
font: smaller/1.5em sans-serif;
|
||||
}
|
||||
|
||||
#qunit-modulefilter-dropdown #qunit-modulefilter-actions > * {
|
||||
box-sizing: border-box;
|
||||
max-height: 2.8em;
|
||||
display: block;
|
||||
padding: 0.4em;
|
||||
}
|
||||
|
||||
#qunit-modulefilter-dropdown #qunit-modulefilter-actions > button {
|
||||
float: right;
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
#qunit-modulefilter-dropdown #qunit-modulefilter-actions > :last-child {
|
||||
/* insert padding to align with checkbox margins */
|
||||
padding-left: 3px;
|
||||
}
|
||||
|
||||
#qunit-modulefilter-dropdown-list {
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
margin: 0;
|
||||
border-top: 2px groove threedhighlight;
|
||||
padding: 0.4em 0 0;
|
||||
font: smaller/1.5em sans-serif;
|
||||
}
|
||||
|
||||
#qunit-modulefilter-dropdown-list li {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
#qunit-modulefilter-dropdown-list .clickable {
|
||||
display: block;
|
||||
padding-left: 0.15em;
|
||||
}
|
||||
|
||||
|
||||
/** Tests: Pass/Fail */
|
||||
|
||||
#qunit-tests {
|
||||
list-style-position: inside;
|
||||
}
|
||||
|
||||
#qunit-tests li {
|
||||
padding: 0.4em 1em 0.4em 1em;
|
||||
border-bottom: 1px solid #FFF;
|
||||
list-style-position: inside;
|
||||
}
|
||||
|
||||
#qunit-tests > li {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#qunit-tests li.running,
|
||||
#qunit-tests li.pass,
|
||||
#qunit-tests li.fail,
|
||||
#qunit-tests li.skipped,
|
||||
#qunit-tests li.aborted {
|
||||
display: list-item;
|
||||
}
|
||||
|
||||
#qunit-tests.hidepass {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#qunit-tests.hidepass li.running,
|
||||
#qunit-tests.hidepass li.pass:not(.todo) {
|
||||
visibility: hidden;
|
||||
position: absolute;
|
||||
width: 0;
|
||||
height: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#qunit-tests li strong {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#qunit-tests li.skipped strong {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
#qunit-tests li a {
|
||||
padding: 0.5em;
|
||||
color: #C2CCD1;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
#qunit-tests li p a {
|
||||
padding: 0.25em;
|
||||
color: #6B6464;
|
||||
}
|
||||
#qunit-tests li a:hover,
|
||||
#qunit-tests li a:focus {
|
||||
color: #000;
|
||||
}
|
||||
|
||||
#qunit-tests li .runtime {
|
||||
float: right;
|
||||
font-size: smaller;
|
||||
}
|
||||
|
||||
.qunit-assert-list {
|
||||
margin-top: 0.5em;
|
||||
padding: 0.5em;
|
||||
|
||||
background-color: #FFF;
|
||||
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.qunit-source {
|
||||
margin: 0.6em 0 0.3em;
|
||||
}
|
||||
|
||||
.qunit-collapsed {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#qunit-tests table {
|
||||
border-collapse: collapse;
|
||||
margin-top: 0.2em;
|
||||
}
|
||||
|
||||
#qunit-tests th {
|
||||
text-align: right;
|
||||
vertical-align: top;
|
||||
padding: 0 0.5em 0 0;
|
||||
}
|
||||
|
||||
#qunit-tests td {
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
#qunit-tests pre {
|
||||
margin: 0;
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
#qunit-tests del {
|
||||
color: #374E0C;
|
||||
background-color: #E0F2BE;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
#qunit-tests ins {
|
||||
color: #500;
|
||||
background-color: #FFCACA;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
/*** Test Counts */
|
||||
|
||||
#qunit-tests b.counts { color: #000; }
|
||||
#qunit-tests b.passed { color: #5E740B; }
|
||||
#qunit-tests b.failed { color: #710909; }
|
||||
|
||||
#qunit-tests li li {
|
||||
padding: 5px;
|
||||
background-color: #FFF;
|
||||
border-bottom: none;
|
||||
list-style-position: inside;
|
||||
}
|
||||
|
||||
/*** Passing Styles */
|
||||
|
||||
#qunit-tests li li.pass {
|
||||
color: #3C510C;
|
||||
background-color: #FFF;
|
||||
border-left: 10px solid #C6E746;
|
||||
}
|
||||
|
||||
#qunit-tests .pass { color: #528CE0; background-color: #D2E0E6; }
|
||||
#qunit-tests .pass .test-name { color: #366097; }
|
||||
|
||||
#qunit-tests .pass .test-actual,
|
||||
#qunit-tests .pass .test-expected { color: #999; }
|
||||
|
||||
#qunit-banner.qunit-pass { background-color: #C6E746; }
|
||||
|
||||
/*** Failing Styles */
|
||||
|
||||
#qunit-tests li li.fail {
|
||||
color: #710909;
|
||||
background-color: #FFF;
|
||||
border-left: 10px solid #EE5757;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
#qunit-tests > li:last-child {
|
||||
border-radius: 0 0 5px 5px;
|
||||
}
|
||||
|
||||
#qunit-tests .fail { color: #000; background-color: #EE5757; }
|
||||
#qunit-tests .fail .test-name,
|
||||
#qunit-tests .fail .module-name { color: #000; }
|
||||
|
||||
#qunit-tests .fail .test-actual { color: #EE5757; }
|
||||
#qunit-tests .fail .test-expected { color: #008000; }
|
||||
|
||||
#qunit-banner.qunit-fail { background-color: #EE5757; }
|
||||
|
||||
|
||||
/*** Aborted tests */
|
||||
#qunit-tests .aborted { color: #000; background-color: orange; }
|
||||
/*** Skipped tests */
|
||||
|
||||
#qunit-tests .skipped {
|
||||
background-color: #EBECE9;
|
||||
}
|
||||
|
||||
#qunit-tests .qunit-todo-label,
|
||||
#qunit-tests .qunit-skipped-label {
|
||||
background-color: #F4FF77;
|
||||
display: inline-block;
|
||||
font-style: normal;
|
||||
color: #366097;
|
||||
line-height: 1.8em;
|
||||
padding: 0 0.5em;
|
||||
margin: -0.4em 0.4em -0.4em 0;
|
||||
}
|
||||
|
||||
#qunit-tests .qunit-todo-label {
|
||||
background-color: #EEE;
|
||||
}
|
||||
|
||||
/** Result */
|
||||
|
||||
#qunit-testresult {
|
||||
color: #2B81AF;
|
||||
background-color: #D2E0E6;
|
||||
|
||||
border-bottom: 1px solid #FFF;
|
||||
}
|
||||
#qunit-testresult .clearfix {
|
||||
height: 0;
|
||||
clear: both;
|
||||
}
|
||||
#qunit-testresult .module-name {
|
||||
font-weight: 700;
|
||||
}
|
||||
#qunit-testresult-display {
|
||||
padding: 0.5em 1em 0.5em 1em;
|
||||
width: 85%;
|
||||
float:left;
|
||||
}
|
||||
#qunit-testresult-controls {
|
||||
padding: 0.5em 1em 0.5em 1em;
|
||||
width: 10%;
|
||||
float:left;
|
||||
}
|
||||
|
||||
/** Fixture */
|
||||
|
||||
#qunit-fixture {
|
||||
position: absolute;
|
||||
top: -10000px;
|
||||
left: -10000px;
|
||||
width: 1000px;
|
||||
height: 1000px;
|
||||
}
|
|
@ -11,16 +11,52 @@ class QunitController < ApplicationController
|
|||
# only used in test / dev
|
||||
def index
|
||||
raise Discourse::InvalidAccess.new if Rails.env.production?
|
||||
if (theme_name = params[:theme_name]).present?
|
||||
theme = Theme.find_by(name: theme_name)
|
||||
raise Discourse::NotFound if theme.blank?
|
||||
elsif (theme_url = params[:theme_url]).present?
|
||||
theme = RemoteTheme.find_by(remote_url: theme_url)
|
||||
raise Discourse::NotFound if theme.blank?
|
||||
end
|
||||
|
||||
def theme
|
||||
raise Discourse::NotFound.new if !can_see_theme_qunit?
|
||||
|
||||
param_key = nil
|
||||
@suggested_themes = nil
|
||||
if (id = get_param(:id)).present?
|
||||
theme = Theme.find_by(id: id.to_i)
|
||||
param_key = :id
|
||||
elsif (name = get_param(:name)).present?
|
||||
theme = Theme.find_by(name: name)
|
||||
param_key = :name
|
||||
elsif (url = get_param(:url)).present?
|
||||
theme = RemoteTheme.find_by(remote_url: url)&.theme
|
||||
param_key = :url
|
||||
end
|
||||
if theme.present?
|
||||
request.env[:resolved_theme_ids] = [theme.id]
|
||||
request.env[:skip_theme_ids_transformation] = true
|
||||
|
||||
if param_key && theme.blank?
|
||||
return render plain: "Can't find theme with #{param_key} #{params[param_key].inspect}", status: :not_found
|
||||
end
|
||||
|
||||
if !param_key
|
||||
@suggested_themes = Theme
|
||||
.where(
|
||||
id: ThemeField.where(target_id: Theme.targets[:tests_js]).distinct.pluck(:theme_id)
|
||||
)
|
||||
.order(updated_at: :desc)
|
||||
.pluck(:id, :name)
|
||||
return
|
||||
end
|
||||
|
||||
request.env[:resolved_theme_ids] = [theme.id]
|
||||
request.env[:skip_theme_ids_transformation] = true
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def can_see_theme_qunit?
|
||||
return true if !Rails.env.production?
|
||||
current_user&.admin?
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def get_param(key)
|
||||
params[:"theme_#{key}"] || params[key]
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
class ThemeJavascriptsController < ApplicationController
|
||||
DISK_CACHE_PATH = "#{Rails.root}/tmp/javascript-cache"
|
||||
TESTS_DISK_CACHE_PATH = "#{Rails.root}/tmp/javascript-cache/tests"
|
||||
|
||||
skip_before_action(
|
||||
:check_xhr,
|
||||
|
@ -11,7 +12,7 @@ class ThemeJavascriptsController < ApplicationController
|
|||
only: [:show, :show_tests]
|
||||
)
|
||||
|
||||
before_action :is_asset_path, :no_cookies, :apply_cdn_headers, only: [:show]
|
||||
before_action :is_asset_path, :no_cookies, :apply_cdn_headers, only: [:show, :show_tests]
|
||||
|
||||
def show
|
||||
raise Discourse::NotFound unless last_modified.present?
|
||||
|
@ -35,26 +36,26 @@ class ThemeJavascriptsController < ApplicationController
|
|||
end
|
||||
|
||||
def show_tests
|
||||
raise Discourse::NotFound if Rails.env.production?
|
||||
digest = params[:digest]
|
||||
raise Discourse::NotFound if !digest.match?(/^\h{40}$/)
|
||||
|
||||
theme_id = params.require(:theme_id)
|
||||
theme = Theme.find(theme_id)
|
||||
content = ThemeField
|
||||
.where(
|
||||
theme_id: theme_id,
|
||||
target_id: Theme.targets[:tests_js]
|
||||
)
|
||||
.each(&:ensure_baked!)
|
||||
.map(&:value_baked)
|
||||
.join("\n")
|
||||
theme = Theme.find_by(id: params[:theme_id])
|
||||
raise Discourse::NotFound if theme.blank?
|
||||
|
||||
ThemeJavascriptCompiler.force_default_settings(content, theme)
|
||||
content, content_digest = theme.baked_js_tests_with_digest
|
||||
raise Discourse::NotFound if content.blank? || content_digest != digest
|
||||
|
||||
response.headers["Content-Length"] = content.size.to_s
|
||||
response.headers["Last-Modified"] = Time.zone.now.httpdate
|
||||
immutable_for(1.second)
|
||||
@cache_file = "#{TESTS_DISK_CACHE_PATH}/#{digest}.js"
|
||||
return render body: nil, status: 304 if not_modified?
|
||||
|
||||
send_data content, filename: "js-tests-theme-#{theme_id}.js", disposition: :inline
|
||||
if !File.exist?(@cache_file)
|
||||
FileUtils.mkdir_p(TESTS_DISK_CACHE_PATH)
|
||||
File.write(@cache_file, content)
|
||||
end
|
||||
|
||||
response.headers["Content-Length"] = File.size(@cache_file).to_s
|
||||
set_cache_control_headers
|
||||
send_file(@cache_file, disposition: :inline)
|
||||
end
|
||||
|
||||
private
|
||||
|
@ -64,7 +65,13 @@ class ThemeJavascriptsController < ApplicationController
|
|||
end
|
||||
|
||||
def last_modified
|
||||
@last_modified ||= query.pluck_first(:updated_at)
|
||||
@last_modified ||= begin
|
||||
if params[:action].to_s == "show_tests"
|
||||
File.exist?(@cache_file) ? File.ctime(@cache_file) : nil
|
||||
else
|
||||
query.pluck_first(:updated_at)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def not_modified?
|
||||
|
|
|
@ -2,23 +2,14 @@
|
|||
|
||||
module QunitHelper
|
||||
def theme_tests
|
||||
theme_ids = request.env[:resolved_theme_ids]
|
||||
return "" if theme_ids.blank?
|
||||
theme = Theme.find_by(id: request.env[:resolved_theme_ids]&.first)
|
||||
return "" if theme.blank?
|
||||
|
||||
skip_transformation = request.env[:skip_theme_ids_transformation]
|
||||
query = ThemeField
|
||||
.joins(:theme)
|
||||
.where(
|
||||
target_id: Theme.targets[:tests_js],
|
||||
theme_id: skip_transformation ? theme_ids : Theme.transform_ids(theme_ids)
|
||||
)
|
||||
.pluck(:theme_id)
|
||||
.uniq
|
||||
.map do |theme_id|
|
||||
src = "#{GlobalSetting.cdn_url}#{Discourse.base_path}/theme-javascripts/tests/#{theme_id}.js"
|
||||
"<script src='#{src}'></script>"
|
||||
end
|
||||
.join("\n")
|
||||
.html_safe
|
||||
_, digest = theme.baked_js_tests_with_digest
|
||||
src = "#{GlobalSetting.cdn_url}" \
|
||||
"#{Discourse.base_path}" \
|
||||
"/theme-javascripts/tests/#{theme.id}-#{digest}.js" \
|
||||
"?__ws=#{Discourse.current_hostname}"
|
||||
"<script src='#{src}'></script>".html_safe
|
||||
end
|
||||
end
|
||||
|
|
|
@ -485,6 +485,16 @@ class Theme < ActiveRecord::Base
|
|||
end
|
||||
end
|
||||
|
||||
def cached_default_settings
|
||||
Discourse.cache.fetch("default_settings_for_theme_#{self.id}", expires_in: 30.minutes) do
|
||||
settings_hash = {}
|
||||
self.settings.each do |setting|
|
||||
settings_hash[setting.name] = setting.default
|
||||
end
|
||||
settings_hash
|
||||
end
|
||||
end
|
||||
|
||||
def build_settings_hash
|
||||
hash = {}
|
||||
self.settings.each do |setting|
|
||||
|
@ -503,6 +513,7 @@ class Theme < ActiveRecord::Base
|
|||
def clear_cached_settings!
|
||||
DB.after_commit do
|
||||
Discourse.cache.delete("settings_for_theme_#{self.id}")
|
||||
Discourse.cache.delete("default_settings_for_theme_#{self.id}")
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -662,6 +673,23 @@ class Theme < ActiveRecord::Base
|
|||
setting_row.save!
|
||||
end
|
||||
|
||||
def baked_js_tests_with_digest
|
||||
content = theme_fields
|
||||
.where(target_id: Theme.targets[:tests_js])
|
||||
.each(&:ensure_baked!)
|
||||
.map(&:value_baked)
|
||||
.join("\n")
|
||||
|
||||
return [nil, nil] if content.blank?
|
||||
|
||||
content = <<~JS + content
|
||||
(function() {
|
||||
require("discourse/lib/theme-settings-store").registerSettings(#{self.id}, #{cached_default_settings.to_json}, { force: true });
|
||||
})();
|
||||
JS
|
||||
[content, Digest::SHA1.hexdigest(content)]
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def to_scss_variable(name, value)
|
||||
|
|
|
@ -3,13 +3,11 @@
|
|||
<head>
|
||||
<title>QUnit Test Runner</title>
|
||||
<%= discourse_color_scheme_stylesheets %>
|
||||
<%= stylesheet_link_tag "test_helper" %>
|
||||
<%= javascript_include_tag "test_helper" %>
|
||||
<%= theme_tests %>
|
||||
<%= theme_translations_lookup %>
|
||||
<%= theme_js_lookup %>
|
||||
<%= theme_lookup("head_tag") %>
|
||||
<%= javascript_include_tag "test_starter" %>
|
||||
<%= discourse_stylesheet_link_tag(:desktop, theme_ids: nil) %>
|
||||
<%= discourse_stylesheet_link_tag(:test_helper, theme_ids: nil) %>
|
||||
<%= preload_script "discourse/tests/test_helper" %>
|
||||
<%= preload_script "discourse/tests/core_plugins_tests" %>
|
||||
<%= preload_script "discourse/tests/test_starter" %>
|
||||
<%= csrf_meta_tags %>
|
||||
<script src="<%= ExtraLocalesController.url('admin') %>"></script>
|
||||
<meta property="og:title" content="">
|
||||
|
|
|
@ -0,0 +1,42 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Theme QUnit Test Runner</title>
|
||||
<%= discourse_color_scheme_stylesheets %>
|
||||
<%- if !@suggested_themes %>
|
||||
<%= discourse_stylesheet_link_tag(:desktop, theme_ids: nil) %>
|
||||
<%= discourse_stylesheet_link_tag(:test_helper, theme_ids: nil) %>
|
||||
<%= preload_script "discourse/tests/test_helper" %>
|
||||
<%= theme_translations_lookup %>
|
||||
<%= theme_js_lookup %>
|
||||
<%= theme_lookup("head_tag") %>
|
||||
<%= theme_tests %>
|
||||
<%= preload_script "discourse/tests/test_starter" %>
|
||||
<%= csrf_meta_tags %>
|
||||
<%= preload_script_url ExtraLocalesController.url('admin') %>
|
||||
<meta property="og:title" content="">
|
||||
<meta property="og:url" content="">
|
||||
<%- else %>
|
||||
<style>
|
||||
html {
|
||||
font-family: Arial;
|
||||
}
|
||||
a {
|
||||
display: block;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
</style>
|
||||
<%- end %>
|
||||
</head>
|
||||
<body>
|
||||
<%- if !@suggested_themes %>
|
||||
<div id="qunit"></div>
|
||||
<div id="qunit-fixture"></div>
|
||||
<%- else %>
|
||||
<h3>Select a theme/component:</h3>
|
||||
<%- @suggested_themes.each do |(id, name)| %>
|
||||
<%= link_to name, theme_qunit_url(id: id) %>
|
||||
<%- end %>
|
||||
<%- end %>
|
||||
</body>
|
||||
</html>
|
|
@ -2,9 +2,9 @@
|
|||
<html>
|
||||
<head>
|
||||
<title>QUnit Test Runner</title>
|
||||
<%= stylesheet_link_tag "test_helper" %>
|
||||
<%= discourse_stylesheet_link_tag(:test_helper, theme_ids: nil) %>
|
||||
<%= discourse_stylesheet_link_tag :wizard, theme_ids: nil %>
|
||||
<%= javascript_include_tag "wizard/test/test_helper" %>
|
||||
<%= preload_script "wizard/test/test_helper" %>
|
||||
<%= csrf_meta_tags %>
|
||||
<script src="<%= ExtraLocalesController.url("wizard") %>"></script>
|
||||
</head>
|
||||
|
|
|
@ -126,11 +126,6 @@ module Discourse
|
|||
|
||||
config.assets.paths += %W(#{config.root}/config/locales #{config.root}/public/javascripts)
|
||||
|
||||
if Rails.env == "development" || Rails.env == "test"
|
||||
config.assets.paths << "#{config.root}/app/assets/javascripts/discourse/tests"
|
||||
config.assets.paths << "#{config.root}/node_modules"
|
||||
end
|
||||
|
||||
# Allows us to skip minifincation on some files
|
||||
config.assets.skip_minification = []
|
||||
|
||||
|
@ -166,6 +161,8 @@ module Discourse
|
|||
confirm-new-email/bootstrap.js
|
||||
onpopstate-handler.js
|
||||
embed-application.js
|
||||
discourse/tests/test_helper.js
|
||||
discourse/tests/test_starter.js
|
||||
}
|
||||
|
||||
# Precompile all available locales
|
||||
|
|
|
@ -528,7 +528,7 @@ Discourse::Application.routes.draw do
|
|||
get "stylesheets/:name.css" => "stylesheets#show", constraints: { name: /[-a-z0-9_]+/ }
|
||||
get "color-scheme-stylesheet/:id(/:theme_id)" => "stylesheets#color_scheme", constraints: { format: :json }
|
||||
get "theme-javascripts/:digest.js" => "theme_javascripts#show", constraints: { digest: /\h{40}/ }
|
||||
get "theme-javascripts/tests/:theme_id.js" => "theme_javascripts#show_tests"
|
||||
get "theme-javascripts/tests/:theme_id-:digest.js" => "theme_javascripts#show_tests"
|
||||
|
||||
post "uploads/lookup-metadata" => "uploads#metadata"
|
||||
post "uploads" => "uploads#create"
|
||||
|
@ -960,6 +960,7 @@ Discourse::Application.routes.draw do
|
|||
get "/qunit" => "qunit#index"
|
||||
get "/wizard/qunit" => "wizard#qunit"
|
||||
end
|
||||
get "/theme-qunit" => "qunit#theme"
|
||||
|
||||
post "/push_notifications/subscribe" => "push_notification#subscribe"
|
||||
post "/push_notifications/unsubscribe" => "push_notification#unsubscribe"
|
||||
|
|
|
@ -329,26 +329,7 @@ task 'db:validate_indexes', [:arg] => ['db:ensure_post_migrations', 'environment
|
|||
|
||||
db = TemporaryDb.new
|
||||
db.start
|
||||
|
||||
ActiveRecord::Base.establish_connection(
|
||||
adapter: 'postgresql',
|
||||
database: 'discourse',
|
||||
port: db.pg_port,
|
||||
host: 'localhost'
|
||||
)
|
||||
|
||||
puts "Running migrations on blank database!"
|
||||
|
||||
old_stdout = $stdout.clone
|
||||
old_stderr = $stderr.clone
|
||||
$stdout.reopen(File.new('/dev/null', 'w'))
|
||||
$stderr.reopen(File.new('/dev/null', 'w'))
|
||||
|
||||
SeedFu.quiet = true
|
||||
Rake::Task["db:migrate"].invoke
|
||||
|
||||
$stdout.reopen(old_stdout)
|
||||
$stderr.reopen(old_stderr)
|
||||
db.migrate
|
||||
|
||||
ActiveRecord::Base.establish_connection(
|
||||
adapter: 'postgresql',
|
||||
|
|
|
@ -185,6 +185,15 @@ def dependencies
|
|||
source: 'route-recognizer/dist/route-recognizer.js.map',
|
||||
public_root: true
|
||||
},
|
||||
{
|
||||
source: 'qunit/qunit/qunit.js'
|
||||
},
|
||||
{
|
||||
source: 'pretender/pretender.js'
|
||||
},
|
||||
{
|
||||
source: 'sinon/pkg/sinon.js'
|
||||
},
|
||||
|
||||
]
|
||||
end
|
||||
|
|
|
@ -45,13 +45,14 @@ task "qunit:test", [:timeout, :qunit_path] do |_, args|
|
|||
|
||||
pid = Process.spawn(
|
||||
{
|
||||
"RAILS_ENV" => "test",
|
||||
"RAILS_ENV" => ENV["QUNIT_RAILS_ENV"] || "test",
|
||||
"SKIP_ENFORCE_HOSTNAME" => "1",
|
||||
"UNICORN_PID_PATH" => "#{Rails.root}/tmp/pids/unicorn_test_#{port}.pid", # So this can run alongside development
|
||||
"UNICORN_PORT" => port.to_s,
|
||||
"UNICORN_SIDEKIQS" => "0"
|
||||
},
|
||||
"#{Rails.root}/bin/unicorn -c config/unicorn.conf.rb"
|
||||
"#{Rails.root}/bin/unicorn -c config/unicorn.conf.rb",
|
||||
pgroup: true
|
||||
)
|
||||
|
||||
begin
|
||||
|
@ -61,7 +62,7 @@ task "qunit:test", [:timeout, :qunit_path] do |_, args|
|
|||
cmd = "node #{test_path}/run-qunit.js http://localhost:#{port}#{qunit_path}"
|
||||
options = { seed: (ENV["QUNIT_SEED"] || Random.new.seed), hidepassed: 1 }
|
||||
|
||||
%w{module filter qunit_skip_core qunit_single_plugin theme_name theme_url}.each do |arg|
|
||||
%w{module filter qunit_skip_core qunit_single_plugin theme_name theme_url theme_id}.each do |arg|
|
||||
options[arg] = ENV[arg.upcase] if ENV[arg.upcase].present?
|
||||
end
|
||||
|
||||
|
@ -108,7 +109,7 @@ task "qunit:test", [:timeout, :qunit_path] do |_, args|
|
|||
|
||||
ensure
|
||||
# was having issues with HUP
|
||||
Process.kill "KILL", pid
|
||||
Process.kill "-KILL", pid
|
||||
FileUtils.rm("#{Rails.root}/tmp/pids/unicorn_test_#{port}.pid")
|
||||
end
|
||||
|
||||
|
|
|
@ -98,22 +98,59 @@ task "themes:audit" => :environment do
|
|||
end
|
||||
|
||||
desc "Run QUnit tests of a theme/component"
|
||||
task "themes:qunit", :theme_name_or_url do |t, args|
|
||||
name_or_url = args[:theme_name_or_url]
|
||||
if name_or_url.blank?
|
||||
raise "A theme name or URL must be provided."
|
||||
end
|
||||
if name_or_url =~ /^(url|name)=(.+)/
|
||||
cmd = "THEME_#{Regexp.last_match(1).upcase}=#{Regexp.last_match(2)} "
|
||||
cmd += `which rake`.strip + " qunit:test"
|
||||
sh cmd
|
||||
else
|
||||
task "themes:qunit", :type, :value do |t, args|
|
||||
type = args[:type]
|
||||
value = args[:value]
|
||||
if !%w(name url id).include?(type) || value.blank?
|
||||
raise <<~MSG
|
||||
Cannot parse passed argument #{name_or_url.inspect}.
|
||||
Wrong arguments type:#{type.inspect}, value:#{value.inspect}"
|
||||
Usage:
|
||||
`bundle exec rake themes:unit[url=<theme_url>]`
|
||||
`bundle exec rake themes:unit[url,<theme_url>]`
|
||||
OR
|
||||
`bundle exec rake themes:unit[name=<theme_name>]`
|
||||
`bundle exec rake themes:unit[name,<theme_name>]`
|
||||
OR
|
||||
`bundle exec rake themes:unit[id,<theme_id>]`
|
||||
MSG
|
||||
end
|
||||
ENV["THEME_#{type.upcase}"] = value.to_s
|
||||
Rake::Task["qunit:test"].reenable
|
||||
Rake::Task["qunit:test"].invoke(1200000, "/theme-qunit")
|
||||
end
|
||||
|
||||
desc "Install a theme/component on a temporary DB and run QUnit tests"
|
||||
task "themes:install_and_test" => :environment do |t, args|
|
||||
db = TemporaryDb.new
|
||||
db.start
|
||||
db.migrate
|
||||
ActiveRecord::Base.establish_connection(
|
||||
adapter: 'postgresql',
|
||||
database: 'discourse',
|
||||
port: db.pg_port,
|
||||
host: 'localhost'
|
||||
)
|
||||
|
||||
seeded_themes = Theme.pluck(:id)
|
||||
Rake::Task["themes:install"].invoke
|
||||
themes = Theme.pluck(:name, :id)
|
||||
|
||||
ENV["PGPORT"] = db.pg_port.to_s
|
||||
ENV["PGHOST"] = "localhost"
|
||||
ENV["QUNIT_RAILS_ENV"] = "development"
|
||||
ENV["DISCOURSE_DEV_DB"] = "discourse"
|
||||
|
||||
count = 0
|
||||
themes.each do |(name, id)|
|
||||
if seeded_themes.include?(id)
|
||||
puts "Skipping seeded theme #{name} (id: #{id})"
|
||||
next
|
||||
end
|
||||
puts "Running tests for theme #{name} (id: #{id})..."
|
||||
Rake::Task["themes:qunit"].reenable
|
||||
Rake::Task["themes:qunit"].invoke("id", id)
|
||||
count += 1
|
||||
end
|
||||
raise "Error: No themes were installed" if count == 0
|
||||
ensure
|
||||
db&.stop
|
||||
db&.remove
|
||||
end
|
||||
|
|
|
@ -86,6 +86,7 @@ class TemporaryDb
|
|||
while !`#{pg_ctl_path} -D '#{PG_TEMP_PATH}' status`.include?('server is running')
|
||||
sleep 0.1
|
||||
end
|
||||
@started = true
|
||||
|
||||
`createuser -h localhost -p #{pg_port} -s -D -w discourse 2> /dev/null`
|
||||
`createdb -h localhost -p #{pg_port} discourse`
|
||||
|
@ -94,7 +95,37 @@ class TemporaryDb
|
|||
end
|
||||
|
||||
def stop
|
||||
@started = false
|
||||
`#{pg_ctl_path} -D '#{PG_TEMP_PATH}' stop`
|
||||
end
|
||||
|
||||
def remove
|
||||
raise "Error: the database must be stopped before it can be removed" if @started
|
||||
FileUtils.rm_rf PG_TEMP_PATH
|
||||
end
|
||||
|
||||
def migrate
|
||||
if !@started
|
||||
raise "Error: the database must be started before it can be migrated."
|
||||
end
|
||||
ActiveRecord::Base.establish_connection(
|
||||
adapter: 'postgresql',
|
||||
database: 'discourse',
|
||||
port: pg_port,
|
||||
host: 'localhost'
|
||||
)
|
||||
|
||||
puts "Running migrations on blank database!"
|
||||
|
||||
old_stdout = $stdout.clone
|
||||
old_stderr = $stderr.clone
|
||||
$stdout.reopen(File.new('/dev/null', 'w'))
|
||||
$stderr.reopen(File.new('/dev/null', 'w'))
|
||||
|
||||
SeedFu.quiet = true
|
||||
Rake::Task["db:migrate"].invoke
|
||||
ensure
|
||||
$stdout.reopen(old_stdout) if old_stdout
|
||||
$stderr.reopen(old_stderr) if old_stderr
|
||||
end
|
||||
end
|
||||
|
|
|
@ -151,18 +151,6 @@ class ThemeJavascriptCompiler
|
|||
class CompileError < StandardError
|
||||
end
|
||||
|
||||
def self.force_default_settings(content, theme)
|
||||
settings_hash = {}
|
||||
theme.settings.each do |setting|
|
||||
settings_hash[setting.name] = setting.default
|
||||
end
|
||||
content.prepend <<~JS
|
||||
(function() {
|
||||
require("discourse/lib/theme-settings-store").registerSettings(#{theme.id}, #{settings_hash.to_json}, { force: true });
|
||||
})();
|
||||
JS
|
||||
end
|
||||
|
||||
attr_accessor :content
|
||||
|
||||
def initialize(theme_id, theme_name)
|
||||
|
|
|
@ -865,4 +865,41 @@ HTML
|
|||
end
|
||||
end
|
||||
|
||||
describe "#baked_js_tests_with_digest" do
|
||||
before do
|
||||
ThemeField.create!(
|
||||
theme_id: theme.id,
|
||||
target_id: Theme.targets[:settings],
|
||||
name: "yaml",
|
||||
value: "some_number: 1"
|
||||
)
|
||||
theme.set_field(
|
||||
target: :tests_js,
|
||||
type: :js,
|
||||
name: "acceptance/some-test.js",
|
||||
value: "assert.ok(true);"
|
||||
)
|
||||
theme.save!
|
||||
end
|
||||
|
||||
it 'returns nil for content and digest if theme does not have tests' do
|
||||
ThemeField.destroy_all
|
||||
expect(theme.baked_js_tests_with_digest).to eq([nil, nil])
|
||||
end
|
||||
|
||||
it 'digest does not change when settings are changed' do
|
||||
content, digest = theme.baked_js_tests_with_digest
|
||||
expect(content).to be_present
|
||||
expect(digest).to be_present
|
||||
expect(content).to include("assert.ok(true);")
|
||||
|
||||
theme.update_setting(:some_number, 55)
|
||||
theme.save!
|
||||
expect(theme.build_settings_hash[:some_number]).to eq(55)
|
||||
|
||||
new_content, new_digest = theme.baked_js_tests_with_digest
|
||||
expect(new_content).to eq(content)
|
||||
expect(new_digest).to eq(digest)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -3,74 +3,101 @@
|
|||
require 'rails_helper'
|
||||
|
||||
describe QunitController do
|
||||
let(:theme) { Fabricate(:theme, name: 'main-theme') }
|
||||
let(:component) { Fabricate(:theme, component: true, name: 'enabled-component') }
|
||||
let(:disabled_component) { Fabricate(:theme, component: true, enabled: false, name: 'disabled-component') }
|
||||
describe "#theme" do
|
||||
let(:theme) { Fabricate(:theme, name: 'main-theme') }
|
||||
let(:component) { Fabricate(:theme, component: true, name: 'enabled-component') }
|
||||
let(:disabled_component) { Fabricate(:theme, component: true, enabled: false, name: 'disabled-component') }
|
||||
let(:theme_without_tests) { Fabricate(:theme, name: 'no-tests-guy') }
|
||||
|
||||
before do
|
||||
Theme.destroy_all
|
||||
theme.set_default!
|
||||
component.add_relative_theme!(:parent, theme)
|
||||
disabled_component.add_relative_theme!(:parent, theme)
|
||||
[theme, component, disabled_component].each do |t|
|
||||
t.set_field(
|
||||
target: :extra_js,
|
||||
type: :js,
|
||||
name: "discourse/initializers/my-#{t.id}-initializer.js",
|
||||
value: "console.log(#{t.id});"
|
||||
)
|
||||
t.set_field(
|
||||
target: :tests_js,
|
||||
type: :js,
|
||||
name: "acceptance/some-test-#{t.id}.js",
|
||||
value: "assert.ok(#{t.id});"
|
||||
)
|
||||
t.save!
|
||||
before do
|
||||
Theme.destroy_all
|
||||
theme.set_default!
|
||||
component.add_relative_theme!(:parent, theme)
|
||||
disabled_component.add_relative_theme!(:parent, theme)
|
||||
[theme, component, disabled_component].each do |t|
|
||||
t.set_field(
|
||||
target: :extra_js,
|
||||
type: :js,
|
||||
name: "discourse/initializers/my-#{t.id}-initializer.js",
|
||||
value: "console.log(#{t.id});"
|
||||
)
|
||||
t.set_field(
|
||||
target: :tests_js,
|
||||
type: :js,
|
||||
name: "acceptance/some-test-#{t.id}.js",
|
||||
value: "assert.ok(#{t.id});"
|
||||
)
|
||||
t.save!
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "when no theme is specified" do
|
||||
it "includes tests of enabled theme + components" do
|
||||
get '/qunit'
|
||||
js_urls = JavascriptCache.where(theme_id: [theme.id, component.id]).map(&:url)
|
||||
expect(js_urls.size).to eq(2)
|
||||
js_urls.each do |url|
|
||||
expect(response.body).to include(url)
|
||||
end
|
||||
[theme, component].each do |t|
|
||||
expect(response.body).to include("/theme-javascripts/tests/#{t.id}.js")
|
||||
context "non-admin users on production" do
|
||||
before do
|
||||
Rails.env.stubs(:production?).returns(true)
|
||||
end
|
||||
|
||||
js_urls = JavascriptCache.where(theme_id: disabled_component).map(&:url)
|
||||
expect(js_urls.size).to eq(1)
|
||||
js_urls.each do |url|
|
||||
expect(response.body).not_to include(url)
|
||||
it "anons cannot see the page" do
|
||||
get '/theme-qunit'
|
||||
expect(response.status).to eq(404)
|
||||
end
|
||||
|
||||
it "regular users cannot see the page" do
|
||||
sign_in(Fabricate(:user))
|
||||
get '/theme-qunit'
|
||||
expect(response.status).to eq(404)
|
||||
end
|
||||
expect(response.body).not_to include("/theme-javascripts/tests/#{disabled_component.id}.js")
|
||||
end
|
||||
end
|
||||
|
||||
context "when a theme is specified" do
|
||||
it "includes tests of the specified theme only" do
|
||||
[theme, disabled_component].each do |t|
|
||||
get "/qunit?theme_name=#{t.name}"
|
||||
js_urls = JavascriptCache.where(theme_id: t.id).map(&:url)
|
||||
expect(js_urls.size).to eq(1)
|
||||
js_urls.each do |url|
|
||||
expect(response.body).to include(url)
|
||||
end
|
||||
expect(response.body).to include("/theme-javascripts/tests/#{t.id}.js")
|
||||
context "admin users" do
|
||||
before do
|
||||
sign_in(Fabricate(:admin))
|
||||
end
|
||||
|
||||
excluded = Theme.pluck(:id) - [t.id]
|
||||
js_urls = JavascriptCache.where(theme_id: excluded).map(&:url)
|
||||
expect(js_urls.size).to eq(2)
|
||||
js_urls.each do |url|
|
||||
expect(response.body).not_to include(url)
|
||||
end
|
||||
excluded.each do |id|
|
||||
expect(response.body).not_to include("/theme-javascripts/tests/#{id}.js")
|
||||
context "when no theme is specified" do
|
||||
it "renders a list of themes and components that have tests" do
|
||||
get '/theme-qunit'
|
||||
expect(response.status).to eq(200)
|
||||
[theme, component, disabled_component].each do |t|
|
||||
expect(response.body).to include(t.name)
|
||||
expect(response.body).to include("/theme-qunit?id=#{t.id}")
|
||||
end
|
||||
expect(response.body).not_to include(theme_without_tests.name)
|
||||
expect(response.body).not_to include("/theme-qunit?id=#{theme_without_tests.id}")
|
||||
end
|
||||
end
|
||||
|
||||
it "can specify theme by id" do
|
||||
get "/theme-qunit?id=#{theme.id}"
|
||||
expect(response.status).to eq(200)
|
||||
expect(response.body).to include("/theme-javascripts/tests/#{theme.id}-")
|
||||
end
|
||||
|
||||
it "can specify theme by name" do
|
||||
get "/theme-qunit?name=#{theme.name}"
|
||||
expect(response.status).to eq(200)
|
||||
expect(response.body).to include("/theme-javascripts/tests/#{theme.id}-")
|
||||
end
|
||||
|
||||
it "can specify theme by url" do
|
||||
theme.build_remote_theme(remote_url: "git@github.com:discourse/discourse.git").save!
|
||||
theme.save!
|
||||
get "/theme-qunit?url=#{theme.remote_theme.remote_url}"
|
||||
expect(response.status).to eq(200)
|
||||
expect(response.body).to include("/theme-javascripts/tests/#{theme.id}-")
|
||||
end
|
||||
|
||||
it "themes qunit page includes all the JS/CSS it needs" do
|
||||
get "/theme-qunit?id=#{theme.id}"
|
||||
expect(response.status).to eq(200)
|
||||
expect(response.body).to include("/stylesheets/color_definitions_base_")
|
||||
expect(response.body).to include("/stylesheets/desktop_")
|
||||
expect(response.body).to include("/stylesheets/test_helper_")
|
||||
expect(response.body).to include("/assets/discourse/tests/test_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")
|
||||
expect(response.body).to include("/extra-locales/admin")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -2,9 +2,19 @@
|
|||
require 'rails_helper'
|
||||
|
||||
describe ThemeJavascriptsController do
|
||||
include ActiveSupport::Testing::TimeHelpers
|
||||
|
||||
def clear_disk_cache
|
||||
if Dir.exist?(ThemeJavascriptsController::DISK_CACHE_PATH)
|
||||
`rm -rf #{ThemeJavascriptsController::DISK_CACHE_PATH}`
|
||||
end
|
||||
end
|
||||
|
||||
let!(:theme) { Fabricate(:theme) }
|
||||
let(:theme_field) { ThemeField.create!(theme: theme, target_id: 0, name: "header", value: "<a>html</a>") }
|
||||
let(:javascript_cache) { JavascriptCache.create!(content: 'console.log("hello");', theme_field: theme_field) }
|
||||
before { clear_disk_cache }
|
||||
after { clear_disk_cache }
|
||||
|
||||
describe '#show' do
|
||||
def update_digest_and_get(digest)
|
||||
|
@ -47,31 +57,83 @@ describe ThemeJavascriptsController do
|
|||
get "/theme-javascripts/#{javascript_cache.digest}.js"
|
||||
expect(response.status).to eq(404)
|
||||
end
|
||||
|
||||
def clear_disk_cache
|
||||
if Dir.exist?(ThemeJavascriptsController::DISK_CACHE_PATH)
|
||||
`rm #{ThemeJavascriptsController::DISK_CACHE_PATH}/*`
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "#show_tests" do
|
||||
context "theme settings" do
|
||||
let(:component) { Fabricate(:theme, component: true, name: 'enabled-component') }
|
||||
let(:component) { Fabricate(:theme, component: true, name: 'enabled-component') }
|
||||
let!(:tests_field) do
|
||||
field = component.set_field(
|
||||
target: :tests_js,
|
||||
type: :js,
|
||||
name: "acceptance/some-test.js",
|
||||
value: "assert.ok(true);"
|
||||
)
|
||||
component.save!
|
||||
field
|
||||
end
|
||||
|
||||
it "forces default values" do
|
||||
ThemeField.create!(
|
||||
theme: component,
|
||||
target_id: Theme.targets[:settings],
|
||||
name: "yaml",
|
||||
value: "num_setting: 5"
|
||||
)
|
||||
component.reload
|
||||
component.update_setting(:num_setting, 643)
|
||||
before do
|
||||
ThemeField.create!(
|
||||
theme: component,
|
||||
target_id: Theme.targets[:settings],
|
||||
name: "yaml",
|
||||
value: "num_setting: 5"
|
||||
)
|
||||
component.save!
|
||||
end
|
||||
|
||||
get "/theme-javascripts/tests/#{component.id}.js"
|
||||
expect(response.body).to include("require(\"discourse/lib/theme-settings-store\").registerSettings(#{component.id}, {\"num_setting\":5}, { force: true });")
|
||||
end
|
||||
it "forces theme settings default values" do
|
||||
component.update_setting(:num_setting, 643)
|
||||
_, digest = component.baked_js_tests_with_digest
|
||||
|
||||
get "/theme-javascripts/tests/#{component.id}-#{digest}.js"
|
||||
expect(response.body).to include("require(\"discourse/lib/theme-settings-store\").registerSettings(#{component.id}, {\"num_setting\":5}, { force: true });")
|
||||
expect(response.body).to include("assert.ok(true);")
|
||||
end
|
||||
|
||||
it "responds with 404 if digest is not a 40 chars hex" do
|
||||
digest = Rack::Utils.escape('../../../../../../../../../../etc/passwd').gsub('.', '%2E')
|
||||
get "/theme-javascripts/tests/#{component.id}-#{digest}.js"
|
||||
expect(response.status).to eq(404)
|
||||
|
||||
get "/theme-javascripts/tests/#{component.id}-abc123.js"
|
||||
expect(response.status).to eq(404)
|
||||
end
|
||||
|
||||
it "responds with 404 if theme does not exist" do
|
||||
get "/theme-javascripts/tests/#{Theme.maximum(:id) + 1}-#{SecureRandom.hex(20)}.js"
|
||||
expect(response.status).to eq(404)
|
||||
end
|
||||
|
||||
it "responds with 304 if tests digest has not changed" do
|
||||
content, digest = component.baked_js_tests_with_digest
|
||||
get "/theme-javascripts/tests/#{component.id}-#{digest}.js"
|
||||
last_modified = Time.rfc2822(response.headers["Last-Modified"])
|
||||
expect(response.status).to eq(200)
|
||||
expect(response.headers["Content-Length"].to_i).to eq(content.size)
|
||||
|
||||
get "/theme-javascripts/tests/#{component.id}-#{digest}.js",
|
||||
headers: { "If-Modified-Since" => (last_modified + 10.seconds).rfc2822 }
|
||||
expect(response.status).to eq(304)
|
||||
end
|
||||
|
||||
it "responds with 404 to requests with old digests" do
|
||||
_, old_digest = component.baked_js_tests_with_digest
|
||||
get "/theme-javascripts/tests/#{component.id}-#{old_digest}.js"
|
||||
expect(response.status).to eq(200)
|
||||
expect(response.body).to include("assert.ok(true);")
|
||||
|
||||
tests_field.update!(value: "assert.ok(343434);")
|
||||
tests_field.invalidate_baked!
|
||||
_, digest = component.baked_js_tests_with_digest
|
||||
expect(old_digest).not_to eq(digest)
|
||||
|
||||
get "/theme-javascripts/tests/#{component.id}-#{old_digest}.js"
|
||||
expect(response.status).to eq(404)
|
||||
|
||||
get "/theme-javascripts/tests/#{component.id}-#{digest}.js"
|
||||
expect(response.status).to eq(200)
|
||||
expect(response.body).to include("assert.ok(343434);")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,487 @@
|
|||
(function(self) {
|
||||
'use strict';
|
||||
|
||||
var appearsBrowserified = typeof self !== 'undefined' &&
|
||||
typeof process !== 'undefined' &&
|
||||
Object.prototype.toString.call(process) === '[object Object]';
|
||||
|
||||
var RouteRecognizer = appearsBrowserified ? require('route-recognizer') : self.RouteRecognizer;
|
||||
var FakeXMLHttpRequest = appearsBrowserified ? require('fake-xml-http-request') : self.FakeXMLHttpRequest;
|
||||
|
||||
/**
|
||||
* parseURL - decompose a URL into its parts
|
||||
* @param {String} url a URL
|
||||
* @return {Object} parts of the URL, including the following
|
||||
*
|
||||
* 'https://www.yahoo.com:1234/mypage?test=yes#abc'
|
||||
*
|
||||
* {
|
||||
* host: 'www.yahoo.com:1234',
|
||||
* protocol: 'https:',
|
||||
* search: '?test=yes',
|
||||
* hash: '#abc',
|
||||
* href: 'https://www.yahoo.com:1234/mypage?test=yes#abc',
|
||||
* pathname: '/mypage',
|
||||
* fullpath: '/mypage?test=yes'
|
||||
* }
|
||||
*/
|
||||
function parseURL(url) {
|
||||
// TODO: something for when document isn't present... #yolo
|
||||
var anchor = document.createElement('a');
|
||||
anchor.href = url;
|
||||
|
||||
if (!anchor.host) {
|
||||
anchor.href = anchor.href; // IE: load the host and protocol
|
||||
}
|
||||
|
||||
var pathname = anchor.pathname;
|
||||
if (pathname.charAt(0) !== '/') {
|
||||
pathname = '/' + pathname; // IE: prepend leading slash
|
||||
}
|
||||
|
||||
var host = anchor.host;
|
||||
if (anchor.port === '80' || anchor.port === '443') {
|
||||
host = anchor.hostname; // IE: remove default port
|
||||
}
|
||||
|
||||
return {
|
||||
host: host,
|
||||
protocol: anchor.protocol,
|
||||
search: anchor.search,
|
||||
hash: anchor.hash,
|
||||
href: anchor.href,
|
||||
pathname: pathname,
|
||||
fullpath: pathname + (anchor.search || '') + (anchor.hash || '')
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Registry
|
||||
*
|
||||
* A registry is a map of HTTP verbs to route recognizers.
|
||||
*/
|
||||
|
||||
function Registry(/* host */) {
|
||||
// Herein we keep track of RouteRecognizer instances
|
||||
// keyed by HTTP method. Feel free to add more as needed.
|
||||
this.verbs = {
|
||||
GET: new RouteRecognizer(),
|
||||
PUT: new RouteRecognizer(),
|
||||
POST: new RouteRecognizer(),
|
||||
DELETE: new RouteRecognizer(),
|
||||
PATCH: new RouteRecognizer(),
|
||||
HEAD: new RouteRecognizer(),
|
||||
OPTIONS: new RouteRecognizer()
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Hosts
|
||||
*
|
||||
* a map of hosts to Registries, ultimately allowing
|
||||
* a per-host-and-port, per HTTP verb lookup of RouteRecognizers
|
||||
*/
|
||||
function Hosts() {
|
||||
this._registries = {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Hosts#forURL - retrieve a map of HTTP verbs to RouteRecognizers
|
||||
* for a given URL
|
||||
*
|
||||
* @param {String} url a URL
|
||||
* @return {Registry} a map of HTTP verbs to RouteRecognizers
|
||||
* corresponding to the provided URL's
|
||||
* hostname and port
|
||||
*/
|
||||
Hosts.prototype.forURL = function(url) {
|
||||
var host = parseURL(url).host;
|
||||
var registry = this._registries[host];
|
||||
|
||||
if (registry === undefined) {
|
||||
registry = (this._registries[host] = new Registry(host));
|
||||
}
|
||||
|
||||
return registry.verbs;
|
||||
};
|
||||
|
||||
|
||||
function Pretender(/* routeMap1, routeMap2, ..., options*/) {
|
||||
this.hosts = new Hosts();
|
||||
|
||||
var lastArg = arguments[arguments.length - 1];
|
||||
var options = typeof lastArg === 'object' ? lastArg : null;
|
||||
var shouldNotTrack = options && (options.trackRequests === false);
|
||||
var noopArray = { push: function() {}, length: 0 };
|
||||
|
||||
this.handlers = [];
|
||||
this.handledRequests = shouldNotTrack ? noopArray: [];
|
||||
this.passthroughRequests = shouldNotTrack ? noopArray: [];
|
||||
this.unhandledRequests = shouldNotTrack ? noopArray: [];
|
||||
this.requestReferences = [];
|
||||
this.forcePassthrough = options && (options.forcePassthrough === true);
|
||||
this.disableUnhandled = options && (options.disableUnhandled === true);
|
||||
|
||||
// reference the native XMLHttpRequest object so
|
||||
// it can be restored later
|
||||
this._nativeXMLHttpRequest = self.XMLHttpRequest;
|
||||
this.running = false;
|
||||
var ctx = { pretender: this };
|
||||
this.ctx = ctx;
|
||||
|
||||
// capture xhr requests, channeling them into
|
||||
// the route map.
|
||||
self.XMLHttpRequest = interceptor(ctx);
|
||||
|
||||
// 'start' the server
|
||||
this.running = true;
|
||||
|
||||
// trigger the route map DSL.
|
||||
var argLength = options ? arguments.length - 1 : arguments.length;
|
||||
for (var i = 0; i < argLength; i++) {
|
||||
this.map(arguments[i]);
|
||||
}
|
||||
}
|
||||
|
||||
function interceptor(ctx) {
|
||||
function FakeRequest() {
|
||||
// super()
|
||||
FakeXMLHttpRequest.call(this);
|
||||
}
|
||||
FakeRequest.prototype = Object.create(FakeXMLHttpRequest.prototype);
|
||||
FakeRequest.prototype.constructor = FakeRequest;
|
||||
|
||||
// extend
|
||||
FakeRequest.prototype.send = function send() {
|
||||
if (!ctx.pretender.running) {
|
||||
throw new Error('You shut down a Pretender instance while there was a pending request. ' +
|
||||
'That request just tried to complete. Check to see if you accidentally shut down ' +
|
||||
'a pretender earlier than you intended to');
|
||||
}
|
||||
|
||||
FakeXMLHttpRequest.prototype.send.apply(this, arguments);
|
||||
|
||||
if (ctx.pretender.checkPassthrough(this)) {
|
||||
var xhr = createPassthrough(this);
|
||||
xhr.send.apply(xhr, arguments);
|
||||
} else {
|
||||
ctx.pretender.handleRequest(this);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
function createPassthrough(fakeXHR) {
|
||||
// event types to handle on the xhr
|
||||
var evts = ['error', 'timeout', 'abort', 'readystatechange'];
|
||||
|
||||
// event types to handle on the xhr.upload
|
||||
var uploadEvents = [];
|
||||
|
||||
// properties to copy from the native xhr to fake xhr
|
||||
var lifecycleProps = ['readyState', 'responseText', 'responseXML', 'status', 'statusText'];
|
||||
|
||||
var xhr = fakeXHR._passthroughRequest = new ctx.pretender._nativeXMLHttpRequest();
|
||||
xhr.open(fakeXHR.method, fakeXHR.url, fakeXHR.async, fakeXHR.username, fakeXHR.password);
|
||||
|
||||
if (fakeXHR.responseType === 'arraybuffer') {
|
||||
lifecycleProps = ['readyState', 'response', 'status', 'statusText'];
|
||||
xhr.responseType = fakeXHR.responseType;
|
||||
}
|
||||
|
||||
// use onload if the browser supports it
|
||||
if ('onload' in xhr) {
|
||||
evts.push('load');
|
||||
}
|
||||
|
||||
// add progress event for async calls
|
||||
// avoid using progress events for sync calls, they will hang https://bugs.webkit.org/show_bug.cgi?id=40996.
|
||||
if (fakeXHR.async && fakeXHR.responseType !== 'arraybuffer') {
|
||||
evts.push('progress');
|
||||
uploadEvents.push('progress');
|
||||
}
|
||||
|
||||
// update `propertyNames` properties from `fromXHR` to `toXHR`
|
||||
function copyLifecycleProperties(propertyNames, fromXHR, toXHR) {
|
||||
for (var i = 0; i < propertyNames.length; i++) {
|
||||
var prop = propertyNames[i];
|
||||
if (prop in fromXHR) {
|
||||
toXHR[prop] = fromXHR[prop];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// fire fake event on `eventable`
|
||||
function dispatchEvent(eventable, eventType, event) {
|
||||
eventable.dispatchEvent(event);
|
||||
if (eventable['on' + eventType]) {
|
||||
eventable['on' + eventType](event);
|
||||
}
|
||||
}
|
||||
|
||||
// set the on- handler on the native xhr for the given eventType
|
||||
function createHandler(eventType) {
|
||||
xhr['on' + eventType] = function(event) {
|
||||
copyLifecycleProperties(lifecycleProps, xhr, fakeXHR);
|
||||
dispatchEvent(fakeXHR, eventType, event);
|
||||
};
|
||||
}
|
||||
|
||||
// set the on- handler on the native xhr's `upload` property for
|
||||
// the given eventType
|
||||
function createUploadHandler(eventType) {
|
||||
if (xhr.upload) {
|
||||
xhr.upload['on' + eventType] = function(event) {
|
||||
dispatchEvent(fakeXHR.upload, eventType, event);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
var i;
|
||||
for (i = 0; i < evts.length; i++) {
|
||||
createHandler(evts[i]);
|
||||
}
|
||||
for (i = 0; i < uploadEvents.length; i++) {
|
||||
createUploadHandler(uploadEvents[i]);
|
||||
}
|
||||
|
||||
if (fakeXHR.async) {
|
||||
xhr.timeout = fakeXHR.timeout;
|
||||
xhr.withCredentials = fakeXHR.withCredentials;
|
||||
}
|
||||
for (var h in fakeXHR.requestHeaders) {
|
||||
xhr.setRequestHeader(h, fakeXHR.requestHeaders[h]);
|
||||
}
|
||||
return xhr;
|
||||
}
|
||||
|
||||
FakeRequest.prototype._passthroughCheck = function(method, args) {
|
||||
if (this._passthroughRequest) {
|
||||
return this._passthroughRequest[method].apply(this._passthroughRequest, args);
|
||||
}
|
||||
return FakeXMLHttpRequest.prototype[method].apply(this, args);
|
||||
};
|
||||
|
||||
FakeRequest.prototype.abort = function abort() {
|
||||
return this._passthroughCheck('abort', arguments);
|
||||
};
|
||||
|
||||
FakeRequest.prototype.getResponseHeader = function getResponseHeader() {
|
||||
return this._passthroughCheck('getResponseHeader', arguments);
|
||||
};
|
||||
|
||||
FakeRequest.prototype.getAllResponseHeaders = function getAllResponseHeaders() {
|
||||
return this._passthroughCheck('getAllResponseHeaders', arguments);
|
||||
};
|
||||
|
||||
if (ctx.pretender._nativeXMLHttpRequest.prototype._passthroughCheck) {
|
||||
console.warn('You created a second Pretender instance while there was already one running. ' +
|
||||
'Running two Pretender servers at once will lead to unexpected results and will ' +
|
||||
'be removed entirely in a future major version.' +
|
||||
'Please call .shutdown() on your instances when you no longer need them to respond.');
|
||||
}
|
||||
return FakeRequest;
|
||||
}
|
||||
|
||||
function verbify(verb) {
|
||||
return function(path, handler, async) {
|
||||
return this.register(verb, path, handler, async);
|
||||
};
|
||||
}
|
||||
|
||||
function scheduleProgressEvent(request, startTime, totalTime) {
|
||||
setTimeout(function() {
|
||||
if (!request.aborted && !request.status) {
|
||||
var ellapsedTime = new Date().getTime() - startTime.getTime();
|
||||
request.upload._progress(true, ellapsedTime, totalTime);
|
||||
request._progress(true, ellapsedTime, totalTime);
|
||||
scheduleProgressEvent(request, startTime, totalTime);
|
||||
}
|
||||
}, 50);
|
||||
}
|
||||
|
||||
function isArray(array) {
|
||||
return Object.prototype.toString.call(array) === '[object Array]';
|
||||
}
|
||||
|
||||
var PASSTHROUGH = {};
|
||||
|
||||
Pretender.prototype = {
|
||||
get: verbify('GET'),
|
||||
post: verbify('POST'),
|
||||
put: verbify('PUT'),
|
||||
'delete': verbify('DELETE'),
|
||||
patch: verbify('PATCH'),
|
||||
head: verbify('HEAD'),
|
||||
options: verbify('OPTIONS'),
|
||||
map: function(maps) {
|
||||
maps.call(this);
|
||||
},
|
||||
register: function register(verb, url, handler, async) {
|
||||
if (!handler) {
|
||||
throw new Error('The function you tried passing to Pretender to handle ' +
|
||||
verb + ' ' + url + ' is undefined or missing.');
|
||||
}
|
||||
|
||||
handler.numberOfCalls = 0;
|
||||
handler.async = async;
|
||||
this.handlers.push(handler);
|
||||
|
||||
var registry = this.hosts.forURL(url)[verb];
|
||||
|
||||
registry.add([{
|
||||
path: parseURL(url).fullpath,
|
||||
handler: handler
|
||||
}]);
|
||||
|
||||
return handler;
|
||||
},
|
||||
passthrough: PASSTHROUGH,
|
||||
checkPassthrough: function checkPassthrough(request) {
|
||||
var verb = request.method.toUpperCase();
|
||||
var path = parseURL(request.url).fullpath;
|
||||
var recognized = this.hosts.forURL(request.url)[verb].recognize(path);
|
||||
var match = recognized && recognized[0];
|
||||
|
||||
if ((match && match.handler === PASSTHROUGH) || this.forcePassthrough) {
|
||||
this.passthroughRequests.push(request);
|
||||
this.passthroughRequest(verb, path, request);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
handleRequest: function handleRequest(request) {
|
||||
var verb = request.method.toUpperCase();
|
||||
var path = request.url;
|
||||
|
||||
var handler = this._handlerFor(verb, path, request);
|
||||
|
||||
if (handler) {
|
||||
handler.handler.numberOfCalls++;
|
||||
var async = handler.handler.async;
|
||||
this.handledRequests.push(request);
|
||||
|
||||
var pretender = this;
|
||||
|
||||
var _handleRequest = function(statusHeadersAndBody) {
|
||||
if (!isArray(statusHeadersAndBody)) {
|
||||
var note = 'Remember to `return [status, headers, body];` in your route handler.';
|
||||
throw new Error('Nothing returned by handler for ' + path + '. ' + note);
|
||||
}
|
||||
|
||||
var status = statusHeadersAndBody[0],
|
||||
headers = pretender.prepareHeaders(statusHeadersAndBody[1]),
|
||||
body = pretender.prepareBody(statusHeadersAndBody[2], headers);
|
||||
|
||||
pretender.handleResponse(request, async, function() {
|
||||
request.respond(status, headers, body);
|
||||
pretender.handledRequest(verb, path, request);
|
||||
});
|
||||
};
|
||||
|
||||
try {
|
||||
var result = handler.handler(request);
|
||||
if (result && typeof result.then === 'function') {
|
||||
// `result` is a promise, resolve it
|
||||
result.then(function(resolvedResult) {
|
||||
_handleRequest(resolvedResult);
|
||||
});
|
||||
} else {
|
||||
_handleRequest(result);
|
||||
}
|
||||
} catch (error) {
|
||||
this.erroredRequest(verb, path, request, error);
|
||||
this.resolve(request);
|
||||
}
|
||||
} else {
|
||||
if (!this.disableUnhandled) {
|
||||
this.unhandledRequests.push(request);
|
||||
this.unhandledRequest(verb, path, request);
|
||||
}
|
||||
}
|
||||
},
|
||||
handleResponse: function handleResponse(request, strategy, callback) {
|
||||
var delay = typeof strategy === 'function' ? strategy() : strategy;
|
||||
delay = typeof delay === 'boolean' || typeof delay === 'number' ? delay : 0;
|
||||
|
||||
if (delay === false) {
|
||||
callback();
|
||||
} else {
|
||||
var pretender = this;
|
||||
pretender.requestReferences.push({
|
||||
request: request,
|
||||
callback: callback
|
||||
});
|
||||
|
||||
if (delay !== true) {
|
||||
scheduleProgressEvent(request, new Date(), delay);
|
||||
setTimeout(function() {
|
||||
pretender.resolve(request);
|
||||
}, delay);
|
||||
}
|
||||
}
|
||||
},
|
||||
resolve: function resolve(request) {
|
||||
for (var i = 0, len = this.requestReferences.length; i < len; i++) {
|
||||
var res = this.requestReferences[i];
|
||||
if (res.request === request) {
|
||||
res.callback();
|
||||
this.requestReferences.splice(i, 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
requiresManualResolution: function(verb, path) {
|
||||
var handler = this._handlerFor(verb.toUpperCase(), path, {});
|
||||
if (!handler) { return false; }
|
||||
|
||||
var async = handler.handler.async;
|
||||
return typeof async === 'function' ? async() === true : async === true;
|
||||
},
|
||||
prepareBody: function(body) { return body; },
|
||||
prepareHeaders: function(headers) { return headers; },
|
||||
handledRequest: function(/* verb, path, request */) { /* no-op */},
|
||||
passthroughRequest: function(/* verb, path, request */) { /* no-op */},
|
||||
unhandledRequest: function(verb, path/*, request */) {
|
||||
throw new Error('Pretender intercepted ' + verb + ' ' +
|
||||
path + ' but no handler was defined for this type of request');
|
||||
},
|
||||
erroredRequest: function(verb, path, request, error) {
|
||||
error.message = 'Pretender intercepted ' + verb + ' ' +
|
||||
path + ' but encountered an error: ' + error.message;
|
||||
throw error;
|
||||
},
|
||||
_handlerFor: function(verb, url, request) {
|
||||
var registry = this.hosts.forURL(url)[verb];
|
||||
var matches = registry.recognize(parseURL(url).fullpath);
|
||||
|
||||
var match = matches ? matches[0] : null;
|
||||
if (match) {
|
||||
request.params = match.params;
|
||||
request.queryParams = matches.queryParams;
|
||||
}
|
||||
|
||||
return match;
|
||||
},
|
||||
shutdown: function shutdown() {
|
||||
self.XMLHttpRequest = this._nativeXMLHttpRequest;
|
||||
this.ctx.pretender = undefined;
|
||||
// 'stop' the server
|
||||
this.running = false;
|
||||
}
|
||||
};
|
||||
|
||||
Pretender.parseURL = parseURL;
|
||||
Pretender.Hosts = Hosts;
|
||||
Pretender.Registry = Registry;
|
||||
|
||||
if (typeof module === 'object') {
|
||||
module.exports = Pretender;
|
||||
} else if (typeof define !== 'undefined') {
|
||||
define('pretender', [], function() {
|
||||
return Pretender;
|
||||
});
|
||||
}
|
||||
self.Pretender = Pretender;
|
||||
}(self));
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue