FEATURE: Allow theme tests to be run in production (take 2) (#12845)
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
e832088edf
commit
4f88f2eb15
|
@ -0,0 +1,4 @@
|
||||||
|
//= require_tree ./acceptance
|
||||||
|
//= require_tree ./integration
|
||||||
|
//= require_tree ./unit
|
||||||
|
//= require ./plugin_tests
|
|
@ -1,5 +1,6 @@
|
||||||
import Pretender from "pretender";
|
import Pretender from "pretender";
|
||||||
import User from "discourse/models/user";
|
import User from "discourse/models/user";
|
||||||
|
import getURL from "discourse-common/lib/get-url";
|
||||||
|
|
||||||
export function parsePostData(query) {
|
export function parsePostData(query) {
|
||||||
const result = {};
|
const result = {};
|
||||||
|
@ -39,7 +40,15 @@ const helpers = { response, success, parsePostData };
|
||||||
|
|
||||||
export let fixturesByUrl;
|
export let fixturesByUrl;
|
||||||
|
|
||||||
export default new Pretender();
|
const instance = new Pretender();
|
||||||
|
|
||||||
|
const oldRegister = instance.register;
|
||||||
|
instance.register = (...args) => {
|
||||||
|
args[1] = getURL(args[1]);
|
||||||
|
return oldRegister.call(instance, ...args);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default instance;
|
||||||
|
|
||||||
export function pretenderHelpers() {
|
export function pretenderHelpers() {
|
||||||
return { parsePostData, response, success };
|
return { parsePostData, response, success };
|
||||||
|
|
|
@ -152,6 +152,12 @@ function setupTestsCommon(application, container, config) {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let setupData;
|
||||||
|
const setupDataElement = document.getElementById("data-discourse-setup");
|
||||||
|
if (setupDataElement) {
|
||||||
|
setupData = setupDataElement.dataset;
|
||||||
|
setupDataElement.remove();
|
||||||
|
}
|
||||||
QUnit.testStart(function (ctx) {
|
QUnit.testStart(function (ctx) {
|
||||||
bootbox.$body = $("#ember-testing");
|
bootbox.$body = $("#ember-testing");
|
||||||
let settings = resetSettings();
|
let settings = resetSettings();
|
||||||
|
@ -162,6 +168,15 @@ function setupTestsCommon(application, container, config) {
|
||||||
app = createApplication(config, settings);
|
app = createApplication(config, settings);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const cdn = setupData ? setupData.cdn : null;
|
||||||
|
const baseUri = setupData ? setupData.baseUri : "";
|
||||||
|
setupURL(cdn, "http://localhost:3000", baseUri);
|
||||||
|
if (setupData && setupData.s3BaseUrl) {
|
||||||
|
setupS3CDN(setupData.s3BaseUrl, setupData.s3Cdn);
|
||||||
|
} else {
|
||||||
|
setupS3CDN(null, null);
|
||||||
|
}
|
||||||
|
|
||||||
server = createPretender;
|
server = createPretender;
|
||||||
server.handlers = [];
|
server.handlers = [];
|
||||||
applyDefaultHandlers(server);
|
applyDefaultHandlers(server);
|
||||||
|
@ -199,10 +214,12 @@ function setupTestsCommon(application, container, config) {
|
||||||
|
|
||||||
applyPretender(ctx.module, server, pretenderHelpers());
|
applyPretender(ctx.module, server, pretenderHelpers());
|
||||||
|
|
||||||
setupURL(null, "http://localhost:3000", "");
|
|
||||||
setupS3CDN(null, null);
|
|
||||||
|
|
||||||
Session.resetCurrent();
|
Session.resetCurrent();
|
||||||
|
if (setupData) {
|
||||||
|
const session = Session.current();
|
||||||
|
session.markdownItURL = setupData.markdownItUrl;
|
||||||
|
session.highlightJsPath = setupData.highlightJsPath;
|
||||||
|
}
|
||||||
User.resetCurrent();
|
User.resetCurrent();
|
||||||
let site = resetSite(settings);
|
let site = resetSite(settings);
|
||||||
createHelperContext({
|
createHelperContext({
|
||||||
|
@ -253,7 +270,6 @@ function setupTestsCommon(application, container, config) {
|
||||||
let pluginPath = getUrlParameter("qunit_single_plugin")
|
let pluginPath = getUrlParameter("qunit_single_plugin")
|
||||||
? "/" + getUrlParameter("qunit_single_plugin") + "/"
|
? "/" + getUrlParameter("qunit_single_plugin") + "/"
|
||||||
: "/plugins/";
|
: "/plugins/";
|
||||||
let themeOnly = getUrlParameter("theme_name") || getUrlParameter("theme_url");
|
|
||||||
|
|
||||||
if (getUrlParameter("qunit_disable_auto_start") === "1") {
|
if (getUrlParameter("qunit_disable_auto_start") === "1") {
|
||||||
QUnit.config.autostart = false;
|
QUnit.config.autostart = false;
|
||||||
|
@ -263,19 +279,11 @@ function setupTestsCommon(application, container, config) {
|
||||||
let isTest = /\-test/.test(entry);
|
let isTest = /\-test/.test(entry);
|
||||||
let regex = new RegExp(pluginPath);
|
let regex = new RegExp(pluginPath);
|
||||||
let isPlugin = regex.test(entry);
|
let isPlugin = regex.test(entry);
|
||||||
let isTheme = /^discourse\/theme\-\d+\/.+/.test(entry);
|
|
||||||
|
|
||||||
if (!isTest) {
|
if (!isTest) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (themeOnly) {
|
|
||||||
if (isTheme) {
|
|
||||||
require(entry, null, null, true);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!skipCore || isPlugin) {
|
if (!skipCore || isPlugin) {
|
||||||
require(entry, null, null, true);
|
require(entry, null, null, true);
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,11 +5,11 @@
|
||||||
//= require jquery.ui.widget
|
//= require jquery.ui.widget
|
||||||
//= require ember.debug
|
//= require ember.debug
|
||||||
//= require message-bus
|
//= require message-bus
|
||||||
//= require qunit/qunit/qunit
|
//= require qunit
|
||||||
//= require ember-qunit
|
//= require ember-qunit
|
||||||
//= require fake_xml_http_request
|
//= require fake_xml_http_request
|
||||||
//= require route-recognizer
|
//= require route-recognizer
|
||||||
//= require pretender/pretender
|
//= require pretender
|
||||||
//= require locales/i18n
|
//= require locales/i18n
|
||||||
//= require locales/en
|
//= require locales/en
|
||||||
//= require discourse-loader
|
//= require discourse-loader
|
||||||
|
@ -28,16 +28,12 @@
|
||||||
//= require ember-template-compiler
|
//= require ember-template-compiler
|
||||||
|
|
||||||
// Test helpers
|
// Test helpers
|
||||||
//= require sinon/pkg/sinon
|
//= require sinon
|
||||||
//= require_tree ./helpers
|
//= require_tree ./helpers
|
||||||
//= require break_string
|
//= require break_string
|
||||||
|
|
||||||
// Finally, the tests themselves
|
|
||||||
//= require_tree ./fixtures
|
//= require_tree ./fixtures
|
||||||
//= require_tree ./acceptance
|
|
||||||
//= require_tree ./integration
|
//= require ./setup-tests
|
||||||
//= require_tree ./unit
|
|
||||||
//= require plugin_tests
|
|
||||||
//= require setup-tests
|
|
||||||
//= require test-shims
|
//= require test-shims
|
||||||
//= require jquery.magnific-popup.min.js
|
//= require jquery.magnific-popup.min.js
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
// discourse-skip-module
|
||||||
|
|
||||||
|
//= require_tree ./helpers
|
||||||
|
//= require_tree ./fixtures
|
||||||
|
//= require ./setup-tests
|
||||||
|
//= require test-shims
|
|
@ -0,0 +1,19 @@
|
||||||
|
// discourse-skip-module
|
||||||
|
|
||||||
|
//= require env
|
||||||
|
//= require jquery.debug
|
||||||
|
//= require ember.debug
|
||||||
|
//= require qunit
|
||||||
|
//= require ember-qunit
|
||||||
|
//= require fake_xml_http_request
|
||||||
|
//= require route-recognizer
|
||||||
|
//= require pretender
|
||||||
|
//= require discourse-loader
|
||||||
|
|
||||||
|
// 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
|
|
@ -6,10 +6,10 @@
|
||||||
//= require ember.debug
|
//= require ember.debug
|
||||||
//= require locales/i18n
|
//= require locales/i18n
|
||||||
//= require locales/en
|
//= require locales/en
|
||||||
//= require route-recognizer/dist/route-recognizer
|
//= require route-recognizer
|
||||||
//= require fake_xml_http_request
|
//= require fake_xml_http_request
|
||||||
//= require pretender/pretender
|
//= require pretender
|
||||||
//= require qunit/qunit/qunit
|
//= require qunit
|
||||||
//= require ember-qunit
|
//= require ember-qunit
|
||||||
//= require discourse-loader
|
//= require discourse-loader
|
||||||
//= require jquery.debug
|
//= require jquery.debug
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
@import '/stylesheets/desktop.css';
|
@import "vendor/qunit";
|
||||||
@import 'qunit/qunit/qunit.css';
|
|
||||||
|
|
||||||
.modal-backdrop {
|
.modal-backdrop {
|
||||||
display: none;
|
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
|
# only used in test / dev
|
||||||
def index
|
def index
|
||||||
raise Discourse::InvalidAccess.new if Rails.env.production?
|
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
|
end
|
||||||
if theme.present?
|
|
||||||
|
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 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[:resolved_theme_ids] = [theme.id]
|
||||||
request.env[:skip_theme_ids_transformation] = true
|
request.env[:skip_theme_ids_transformation] = true
|
||||||
end
|
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
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
class ThemeJavascriptsController < ApplicationController
|
class ThemeJavascriptsController < ApplicationController
|
||||||
DISK_CACHE_PATH = "#{Rails.root}/tmp/javascript-cache"
|
DISK_CACHE_PATH = "#{Rails.root}/tmp/javascript-cache"
|
||||||
|
TESTS_DISK_CACHE_PATH = "#{Rails.root}/tmp/javascript-cache/tests"
|
||||||
|
|
||||||
skip_before_action(
|
skip_before_action(
|
||||||
:check_xhr,
|
:check_xhr,
|
||||||
|
@ -11,7 +12,7 @@ class ThemeJavascriptsController < ApplicationController
|
||||||
only: [:show, :show_tests]
|
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
|
def show
|
||||||
raise Discourse::NotFound unless last_modified.present?
|
raise Discourse::NotFound unless last_modified.present?
|
||||||
|
@ -35,26 +36,26 @@ class ThemeJavascriptsController < ApplicationController
|
||||||
end
|
end
|
||||||
|
|
||||||
def show_tests
|
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_by(id: params[:theme_id])
|
||||||
theme = Theme.find(theme_id)
|
raise Discourse::NotFound if theme.blank?
|
||||||
content = ThemeField
|
|
||||||
.where(
|
|
||||||
theme_id: theme_id,
|
|
||||||
target_id: Theme.targets[:tests_js]
|
|
||||||
)
|
|
||||||
.each(&:ensure_baked!)
|
|
||||||
.map(&:value_baked)
|
|
||||||
.join("\n")
|
|
||||||
|
|
||||||
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
|
@cache_file = "#{TESTS_DISK_CACHE_PATH}/#{digest}.js"
|
||||||
response.headers["Last-Modified"] = Time.zone.now.httpdate
|
return render body: nil, status: 304 if not_modified?
|
||||||
immutable_for(1.second)
|
|
||||||
|
|
||||||
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
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
@ -64,7 +65,13 @@ class ThemeJavascriptsController < ApplicationController
|
||||||
end
|
end
|
||||||
|
|
||||||
def last_modified
|
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
|
end
|
||||||
|
|
||||||
def not_modified?
|
def not_modified?
|
||||||
|
|
|
@ -2,23 +2,14 @@
|
||||||
|
|
||||||
module QunitHelper
|
module QunitHelper
|
||||||
def theme_tests
|
def theme_tests
|
||||||
theme_ids = request.env[:resolved_theme_ids]
|
theme = Theme.find_by(id: request.env[:resolved_theme_ids]&.first)
|
||||||
return "" if theme_ids.blank?
|
return "" if theme.blank?
|
||||||
|
|
||||||
skip_transformation = request.env[:skip_theme_ids_transformation]
|
_, digest = theme.baked_js_tests_with_digest
|
||||||
query = ThemeField
|
src = "#{GlobalSetting.cdn_url}" \
|
||||||
.joins(:theme)
|
"#{Discourse.base_path}" \
|
||||||
.where(
|
"/theme-javascripts/tests/#{theme.id}-#{digest}.js" \
|
||||||
target_id: Theme.targets[:tests_js],
|
"?__ws=#{Discourse.current_hostname}"
|
||||||
theme_id: skip_transformation ? theme_ids : Theme.transform_ids(theme_ids)
|
"<script src='#{src}'></script>".html_safe
|
||||||
)
|
|
||||||
.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
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -498,6 +498,16 @@ class Theme < ActiveRecord::Base
|
||||||
end
|
end
|
||||||
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
|
def build_settings_hash
|
||||||
hash = {}
|
hash = {}
|
||||||
self.settings.each do |setting|
|
self.settings.each do |setting|
|
||||||
|
@ -516,6 +526,7 @@ class Theme < ActiveRecord::Base
|
||||||
def clear_cached_settings!
|
def clear_cached_settings!
|
||||||
DB.after_commit do
|
DB.after_commit do
|
||||||
Discourse.cache.delete("settings_for_theme_#{self.id}")
|
Discourse.cache.delete("settings_for_theme_#{self.id}")
|
||||||
|
Discourse.cache.delete("default_settings_for_theme_#{self.id}")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -676,6 +687,23 @@ class Theme < ActiveRecord::Base
|
||||||
setting_row.save!
|
setting_row.save!
|
||||||
end
|
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
|
private
|
||||||
|
|
||||||
def to_scss_variable(name, value)
|
def to_scss_variable(name, value)
|
||||||
|
|
|
@ -3,13 +3,11 @@
|
||||||
<head>
|
<head>
|
||||||
<title>QUnit Test Runner</title>
|
<title>QUnit Test Runner</title>
|
||||||
<%= discourse_color_scheme_stylesheets %>
|
<%= discourse_color_scheme_stylesheets %>
|
||||||
<%= stylesheet_link_tag "test_helper" %>
|
<%= discourse_stylesheet_link_tag(:desktop, theme_ids: nil) %>
|
||||||
<%= javascript_include_tag "test_helper" %>
|
<%= discourse_stylesheet_link_tag(:test_helper, theme_ids: nil) %>
|
||||||
<%= theme_tests %>
|
<%= preload_script "discourse/tests/test_helper" %>
|
||||||
<%= theme_translations_lookup %>
|
<%= preload_script "discourse/tests/core_plugins_tests" %>
|
||||||
<%= theme_js_lookup %>
|
<%= preload_script "discourse/tests/test_starter" %>
|
||||||
<%= theme_lookup("head_tag") %>
|
|
||||||
<%= javascript_include_tag "test_starter" %>
|
|
||||||
<%= csrf_meta_tags %>
|
<%= csrf_meta_tags %>
|
||||||
<script src="<%= ExtraLocalesController.url('admin') %>"></script>
|
<script src="<%= ExtraLocalesController.url('admin') %>"></script>
|
||||||
<meta property="og:title" content="">
|
<meta property="og:title" content="">
|
||||||
|
|
|
@ -0,0 +1,49 @@
|
||||||
|
<!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 "locales/en" %>
|
||||||
|
<%= preload_script "discourse/tests/theme_test_vendor" %>
|
||||||
|
<%= preload_script "vendor" %>
|
||||||
|
<%= preload_script "pretty-text-bundle" %>
|
||||||
|
<%= preload_script "markdown-it-bundle" %>
|
||||||
|
<%= preload_script "application" %>
|
||||||
|
<%= preload_script "admin" %>
|
||||||
|
<%= preload_script "discourse/tests/theme_test_helper" %>
|
||||||
|
<%= theme_translations_lookup %>
|
||||||
|
<%= theme_js_lookup %>
|
||||||
|
<%= theme_lookup("head_tag") %>
|
||||||
|
<%= theme_tests %>
|
||||||
|
<%= preload_script_url ExtraLocalesController.url('admin') %>
|
||||||
|
<%= 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" %>
|
||||||
|
<%- 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>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<title>QUnit Test Runner</title>
|
<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 %>
|
<%= discourse_stylesheet_link_tag :wizard, theme_ids: nil %>
|
||||||
<%= javascript_include_tag "wizard/test/test_helper" %>
|
<%= preload_script "wizard/test/test_helper" %>
|
||||||
<%= csrf_meta_tags %>
|
<%= csrf_meta_tags %>
|
||||||
<script src="<%= ExtraLocalesController.url("wizard") %>"></script>
|
<script src="<%= ExtraLocalesController.url("wizard") %>"></script>
|
||||||
</head>
|
</head>
|
||||||
|
|
|
@ -126,11 +126,6 @@ module Discourse
|
||||||
|
|
||||||
config.assets.paths += %W(#{config.root}/config/locales #{config.root}/public/javascripts)
|
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
|
# Allows us to skip minifincation on some files
|
||||||
config.assets.skip_minification = []
|
config.assets.skip_minification = []
|
||||||
|
|
||||||
|
@ -166,6 +161,9 @@ module Discourse
|
||||||
confirm-new-email/bootstrap.js
|
confirm-new-email/bootstrap.js
|
||||||
onpopstate-handler.js
|
onpopstate-handler.js
|
||||||
embed-application.js
|
embed-application.js
|
||||||
|
discourse/tests/theme_test_helper.js
|
||||||
|
discourse/tests/theme_test_vendor.js
|
||||||
|
discourse/tests/test_starter.js
|
||||||
}
|
}
|
||||||
|
|
||||||
# Precompile all available locales
|
# 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 "stylesheets/:name.css" => "stylesheets#show", constraints: { name: /[-a-z0-9_]+/ }
|
||||||
get "color-scheme-stylesheet/:id(/:theme_id)" => "stylesheets#color_scheme", constraints: { format: :json }
|
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/: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/lookup-metadata" => "uploads#metadata"
|
||||||
post "uploads" => "uploads#create"
|
post "uploads" => "uploads#create"
|
||||||
|
@ -960,6 +960,7 @@ Discourse::Application.routes.draw do
|
||||||
get "/qunit" => "qunit#index"
|
get "/qunit" => "qunit#index"
|
||||||
get "/wizard/qunit" => "wizard#qunit"
|
get "/wizard/qunit" => "wizard#qunit"
|
||||||
end
|
end
|
||||||
|
get "/theme-qunit" => "qunit#theme"
|
||||||
|
|
||||||
post "/push_notifications/subscribe" => "push_notification#subscribe"
|
post "/push_notifications/subscribe" => "push_notification#subscribe"
|
||||||
post "/push_notifications/unsubscribe" => "push_notification#unsubscribe"
|
post "/push_notifications/unsubscribe" => "push_notification#unsubscribe"
|
||||||
|
|
|
@ -10,6 +10,7 @@ class ContentSecurityPolicy
|
||||||
def path_specific_extension(path_info)
|
def path_specific_extension(path_info)
|
||||||
{}.tap do |obj|
|
{}.tap do |obj|
|
||||||
for_qunit_route = !Rails.env.production? && ["/qunit", "/wizard/qunit"].include?(path_info)
|
for_qunit_route = !Rails.env.production? && ["/qunit", "/wizard/qunit"].include?(path_info)
|
||||||
|
for_qunit_route ||= "/theme-qunit" == path_info
|
||||||
obj[:script_src] = :unsafe_eval if for_qunit_route
|
obj[:script_src] = :unsafe_eval if for_qunit_route
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -329,26 +329,7 @@ task 'db:validate_indexes', [:arg] => ['db:ensure_post_migrations', 'environment
|
||||||
|
|
||||||
db = TemporaryDb.new
|
db = TemporaryDb.new
|
||||||
db.start
|
db.start
|
||||||
|
db.migrate
|
||||||
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)
|
|
||||||
|
|
||||||
ActiveRecord::Base.establish_connection(
|
ActiveRecord::Base.establish_connection(
|
||||||
adapter: 'postgresql',
|
adapter: 'postgresql',
|
||||||
|
|
|
@ -185,6 +185,15 @@ def dependencies
|
||||||
source: 'route-recognizer/dist/route-recognizer.js.map',
|
source: 'route-recognizer/dist/route-recognizer.js.map',
|
||||||
public_root: true
|
public_root: true
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
source: 'qunit/qunit/qunit.js'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
source: 'pretender/pretender.js'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
source: 'sinon/pkg/sinon.js'
|
||||||
|
},
|
||||||
|
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
|
@ -45,13 +45,14 @@ task "qunit:test", [:timeout, :qunit_path] do |_, args|
|
||||||
|
|
||||||
pid = Process.spawn(
|
pid = Process.spawn(
|
||||||
{
|
{
|
||||||
"RAILS_ENV" => "test",
|
"RAILS_ENV" => ENV["QUNIT_RAILS_ENV"] || "test",
|
||||||
"SKIP_ENFORCE_HOSTNAME" => "1",
|
"SKIP_ENFORCE_HOSTNAME" => "1",
|
||||||
"UNICORN_PID_PATH" => "#{Rails.root}/tmp/pids/unicorn_test_#{port}.pid", # So this can run alongside development
|
"UNICORN_PID_PATH" => "#{Rails.root}/tmp/pids/unicorn_test_#{port}.pid", # So this can run alongside development
|
||||||
"UNICORN_PORT" => port.to_s,
|
"UNICORN_PORT" => port.to_s,
|
||||||
"UNICORN_SIDEKIQS" => "0"
|
"UNICORN_SIDEKIQS" => "0"
|
||||||
},
|
},
|
||||||
"#{Rails.root}/bin/unicorn -c config/unicorn.conf.rb"
|
"#{Rails.root}/bin/unicorn -c config/unicorn.conf.rb",
|
||||||
|
pgroup: true
|
||||||
)
|
)
|
||||||
|
|
||||||
begin
|
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}"
|
cmd = "node #{test_path}/run-qunit.js http://localhost:#{port}#{qunit_path}"
|
||||||
options = { seed: (ENV["QUNIT_SEED"] || Random.new.seed), hidepassed: 1 }
|
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?
|
options[arg] = ENV[arg.upcase] if ENV[arg.upcase].present?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -108,7 +109,7 @@ task "qunit:test", [:timeout, :qunit_path] do |_, args|
|
||||||
|
|
||||||
ensure
|
ensure
|
||||||
# was having issues with HUP
|
# was having issues with HUP
|
||||||
Process.kill "KILL", pid
|
Process.kill "-KILL", pid
|
||||||
FileUtils.rm("#{Rails.root}/tmp/pids/unicorn_test_#{port}.pid")
|
FileUtils.rm("#{Rails.root}/tmp/pids/unicorn_test_#{port}.pid")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -98,22 +98,59 @@ task "themes:audit" => :environment do
|
||||||
end
|
end
|
||||||
|
|
||||||
desc "Run QUnit tests of a theme/component"
|
desc "Run QUnit tests of a theme/component"
|
||||||
task "themes:qunit", :theme_name_or_url do |t, args|
|
task "themes:qunit", :type, :value do |t, args|
|
||||||
name_or_url = args[:theme_name_or_url]
|
type = args[:type]
|
||||||
if name_or_url.blank?
|
value = args[:value]
|
||||||
raise "A theme name or URL must be provided."
|
if !%w(name url id).include?(type) || value.blank?
|
||||||
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
|
|
||||||
raise <<~MSG
|
raise <<~MSG
|
||||||
Cannot parse passed argument #{name_or_url.inspect}.
|
Wrong arguments type:#{type.inspect}, value:#{value.inspect}"
|
||||||
Usage:
|
Usage:
|
||||||
`bundle exec rake themes:unit[url=<theme_url>]`
|
`bundle exec rake themes:unit[url,<theme_url>]`
|
||||||
OR
|
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
|
MSG
|
||||||
end
|
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
|
end
|
||||||
|
|
|
@ -86,6 +86,7 @@ class TemporaryDb
|
||||||
while !`#{pg_ctl_path} -D '#{PG_TEMP_PATH}' status`.include?('server is running')
|
while !`#{pg_ctl_path} -D '#{PG_TEMP_PATH}' status`.include?('server is running')
|
||||||
sleep 0.1
|
sleep 0.1
|
||||||
end
|
end
|
||||||
|
@started = true
|
||||||
|
|
||||||
`createuser -h localhost -p #{pg_port} -s -D -w discourse 2> /dev/null`
|
`createuser -h localhost -p #{pg_port} -s -D -w discourse 2> /dev/null`
|
||||||
`createdb -h localhost -p #{pg_port} discourse`
|
`createdb -h localhost -p #{pg_port} discourse`
|
||||||
|
@ -94,7 +95,37 @@ class TemporaryDb
|
||||||
end
|
end
|
||||||
|
|
||||||
def stop
|
def stop
|
||||||
|
@started = false
|
||||||
`#{pg_ctl_path} -D '#{PG_TEMP_PATH}' stop`
|
`#{pg_ctl_path} -D '#{PG_TEMP_PATH}' stop`
|
||||||
end
|
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
|
end
|
||||||
|
|
|
@ -151,18 +151,6 @@ class ThemeJavascriptCompiler
|
||||||
class CompileError < StandardError
|
class CompileError < StandardError
|
||||||
end
|
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
|
attr_accessor :content
|
||||||
|
|
||||||
def initialize(theme_id, theme_name)
|
def initialize(theme_id, theme_name)
|
||||||
|
|
|
@ -865,4 +865,41 @@ HTML
|
||||||
end
|
end
|
||||||
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
|
end
|
||||||
|
|
|
@ -3,9 +3,11 @@
|
||||||
require 'rails_helper'
|
require 'rails_helper'
|
||||||
|
|
||||||
describe QunitController do
|
describe QunitController do
|
||||||
|
describe "#theme" do
|
||||||
let(:theme) { Fabricate(:theme, name: 'main-theme') }
|
let(:theme) { Fabricate(:theme, name: 'main-theme') }
|
||||||
let(:component) { Fabricate(:theme, component: true, name: 'enabled-component') }
|
let(:component) { Fabricate(:theme, component: true, name: 'enabled-component') }
|
||||||
let(:disabled_component) { Fabricate(:theme, component: true, enabled: false, name: 'disabled-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
|
before do
|
||||||
Theme.destroy_all
|
Theme.destroy_all
|
||||||
|
@ -29,47 +31,73 @@ describe QunitController do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context "non-admin users on production" do
|
||||||
|
before do
|
||||||
|
Rails.env.stubs(:production?).returns(true)
|
||||||
|
end
|
||||||
|
|
||||||
|
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
|
||||||
|
end
|
||||||
|
|
||||||
|
context "admin users" do
|
||||||
|
before do
|
||||||
|
sign_in(Fabricate(:admin))
|
||||||
|
end
|
||||||
|
|
||||||
context "when no theme is specified" do
|
context "when no theme is specified" do
|
||||||
it "includes tests of enabled theme + components" do
|
it "renders a list of themes and components that have tests" do
|
||||||
get '/qunit'
|
get '/theme-qunit'
|
||||||
js_urls = JavascriptCache.where(theme_id: [theme.id, component.id]).map(&:url)
|
expect(response.status).to eq(200)
|
||||||
expect(js_urls.size).to eq(2)
|
[theme, component, disabled_component].each do |t|
|
||||||
js_urls.each do |url|
|
expect(response.body).to include(t.name)
|
||||||
expect(response.body).to include(url)
|
expect(response.body).to include("/theme-qunit?id=#{t.id}")
|
||||||
end
|
end
|
||||||
[theme, component].each do |t|
|
expect(response.body).not_to include(theme_without_tests.name)
|
||||||
expect(response.body).to include("/theme-javascripts/tests/#{t.id}.js")
|
expect(response.body).not_to include("/theme-qunit?id=#{theme_without_tests.id}")
|
||||||
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)
|
|
||||||
end
|
|
||||||
expect(response.body).not_to include("/theme-javascripts/tests/#{disabled_component.id}.js")
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context "when a theme is specified" do
|
it "can specify theme by id" do
|
||||||
it "includes tests of the specified theme only" do
|
get "/theme-qunit?id=#{theme.id}"
|
||||||
[theme, disabled_component].each do |t|
|
expect(response.status).to eq(200)
|
||||||
get "/qunit?theme_name=#{t.name}"
|
expect(response.body).to include("/theme-javascripts/tests/#{theme.id}-")
|
||||||
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
|
end
|
||||||
expect(response.body).to include("/theme-javascripts/tests/#{t.id}.js")
|
|
||||||
|
|
||||||
excluded = Theme.pluck(:id) - [t.id]
|
it "can specify theme by name" do
|
||||||
js_urls = JavascriptCache.where(theme_id: excluded).map(&:url)
|
get "/theme-qunit?name=#{theme.name}"
|
||||||
expect(js_urls.size).to eq(2)
|
expect(response.status).to eq(200)
|
||||||
js_urls.each do |url|
|
expect(response.body).to include("/theme-javascripts/tests/#{theme.id}-")
|
||||||
expect(response.body).not_to include(url)
|
|
||||||
end
|
|
||||||
excluded.each do |id|
|
|
||||||
expect(response.body).not_to include("/theme-javascripts/tests/#{id}.js")
|
|
||||||
end
|
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/theme_test_helper.js")
|
||||||
|
expect(response.body).to include("/assets/discourse/tests/theme_test_vendor.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
|
||||||
end
|
end
|
||||||
|
|
|
@ -2,9 +2,19 @@
|
||||||
require 'rails_helper'
|
require 'rails_helper'
|
||||||
|
|
||||||
describe ThemeJavascriptsController do
|
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) { Fabricate(:theme) }
|
||||||
let(:theme_field) { ThemeField.create!(theme: theme, target_id: 0, name: "header", value: "<a>html</a>") }
|
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) }
|
let(:javascript_cache) { JavascriptCache.create!(content: 'console.log("hello");', theme_field: theme_field) }
|
||||||
|
before { clear_disk_cache }
|
||||||
|
after { clear_disk_cache }
|
||||||
|
|
||||||
describe '#show' do
|
describe '#show' do
|
||||||
def update_digest_and_get(digest)
|
def update_digest_and_get(digest)
|
||||||
|
@ -47,31 +57,83 @@ describe ThemeJavascriptsController do
|
||||||
get "/theme-javascripts/#{javascript_cache.digest}.js"
|
get "/theme-javascripts/#{javascript_cache.digest}.js"
|
||||||
expect(response.status).to eq(404)
|
expect(response.status).to eq(404)
|
||||||
end
|
end
|
||||||
|
|
||||||
def clear_disk_cache
|
|
||||||
if Dir.exist?(ThemeJavascriptsController::DISK_CACHE_PATH)
|
|
||||||
`rm #{ThemeJavascriptsController::DISK_CACHE_PATH}/*`
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "#show_tests" do
|
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
|
before do
|
||||||
ThemeField.create!(
|
ThemeField.create!(
|
||||||
theme: component,
|
theme: component,
|
||||||
target_id: Theme.targets[:settings],
|
target_id: Theme.targets[:settings],
|
||||||
name: "yaml",
|
name: "yaml",
|
||||||
value: "num_setting: 5"
|
value: "num_setting: 5"
|
||||||
)
|
)
|
||||||
component.reload
|
component.save!
|
||||||
component.update_setting(:num_setting, 643)
|
end
|
||||||
|
|
||||||
get "/theme-javascripts/tests/#{component.id}.js"
|
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("require(\"discourse/lib/theme-settings-store\").registerSettings(#{component.id}, {\"num_setting\":5}, { force: true });")
|
||||||
end
|
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
|
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