derby
Version:
MVC framework making it easy to write realtime, collaborative applications that run in both Node.js and browsers.
344 lines (343 loc) • 14.5 kB
JavaScript
var __extends = (this && this.__extends) || (function () {
var extendStatics = function (d, b) {
extendStatics = Object.setPrototypeOf ||
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; };
return extendStatics(d, b);
};
return function (d, b) {
if (typeof b !== "function" && b !== null)
throw new TypeError("Class extends value " + String(b) + " is not a constructor or null");
extendStatics(d, b);
function __() { this.constructor = d; }
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
};
})();
var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) {
if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) {
if (ar || !(i in from)) {
if (!ar) ar = Array.prototype.slice.call(from, 0, i);
ar[i] = from[i];
}
}
return to.concat(ar || Array.prototype.slice.call(from));
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.ComponentHarness = exports.AppForHarness = exports.PageForHarness = void 0;
var events_1 = require("events");
var url_1 = require("url");
var qs = require("qs");
var racer_1 = require("racer");
var App_1 = require("../App");
var AppForServer_1 = require("../AppForServer");
var components_1 = require("../components");
var Derby_1 = require("../Derby");
var Page_1 = require("../Page");
var PageForHarness = /** @class */ (function (_super) {
__extends(PageForHarness, _super);
function PageForHarness() {
return _super !== null && _super.apply(this, arguments) || this;
}
return PageForHarness;
}(Page_1.PageForClient));
exports.PageForHarness = PageForHarness;
var derby = new Derby_1.DerbyForClient();
var AppForHarness = /** @class */ (function (_super) {
__extends(AppForHarness, _super);
function AppForHarness(harness) {
var _this = _super.call(this, derby, 'ComponentHarness_App', '', {}) || this;
_this.Page = PageForHarness;
_this._harness = harness;
_this._pages = [];
// required for calling funcitons from views
_this.proto = PageForHarness.prototype;
return _this;
}
AppForHarness.prototype.createPage = function () {
var page = new this.Page(this, this._harness.model);
this._pages.push(page);
return page;
};
// Load views by filename. The client version of this method is a no-op
AppForHarness.prototype.loadViews = function () {
var _a;
var args = [];
for (var _i = 0; _i < arguments.length; _i++) {
args[_i] = arguments[_i];
}
(_a = AppForServer_1.AppForServer.prototype.loadViews).call.apply(_a, __spreadArray([this], args, false));
};
// `_init()` does setup for loading views from files on the server and loading
// serialized views and data on the client
AppForHarness.prototype._init = function () {
this._initLoad();
};
// Register default compilers so that AppForHarness can load views & styles from
// the filesystem
AppForHarness.prototype._initLoad = function () {
AppForServer_1.AppForServer.prototype._initLoad.call(this);
};
return AppForHarness;
}(App_1.AppForClient));
exports.AppForHarness = AppForHarness;
/**
* Creates a `ComponentHarness`.
*
* If arguments are provided, then `#setup` is called with the arguments.
*/
var ComponentHarness = /** @class */ (function (_super) {
__extends(ComponentHarness, _super);
/**
* Creates a `ComponentHarness`.
*
* If arguments are provided, then `#setup` is called with the arguments.
*/
function ComponentHarness() {
var _this = _super.call(this) || this;
_this.app = new AppForHarness(_this);
_this.model = new racer_1.RootModel();
if (arguments.length > 0) {
// eslint-disable-next-line prefer-spread, prefer-rest-params
_this.setup.apply(_this, arguments);
}
return _this;
}
/** @typedef { {view: {is: string, source?: string}} } InlineComponent */
/**
* Sets up the harness with a HTML template, which should contain a `<view is="..."/>` for the
* component under test, and the components to register for the test.
*
* @param {string} source - HTML template for the harness page
* @param {...(Component | InlineComponent} components - components to register for the test
*
* @example
* var harness = new ComponentHarness().setup('<view is="dialog"/>', Dialog);
*/
ComponentHarness.prototype.setup = function (source) {
var _this = this;
var components = [];
for (var _i = 1; _i < arguments.length; _i++) {
components[_i - 1] = arguments[_i];
}
this.app.views.register('$harness', source);
// Remaining variable arguments are components
components.forEach(function (constructor) { return _this.app.component(constructor); });
return this;
};
/**
* Stubs out view names with empty view or the provided source.
*
* A view name is a colon-separated string of segments, as used in `<view is="...">`.
*
* @example
* var harness = new ComponentHarness('<view is="dialog"/>', Dialog).stub(
* 'icons:open-icon',
* 'icons:close-icon',
* {is: 'dialog:buttons', source: '<button>OK</button>'}
* );
*/
ComponentHarness.prototype.stub = function () {
var args = [];
for (var _i = 0; _i < arguments.length; _i++) {
args[_i] = arguments[_i];
}
for (var i = 0; i < args.length; i++) {
// eslint-disable-next-line prefer-rest-params
var arg = arguments[i];
if (typeof arg === 'string') {
this.app.views.register(arg, '');
}
else if (arg && arg.is) {
this.app.views.register(arg.is, arg.source || '');
}
else {
throw new Error('each argument must be the name of a view or an object with an `is` property');
}
}
return this;
};
/**
* Stubs out view names as components.
*
* This can be used to test the values being bound to ("passed into") child components.
*
* @example
* var harness = new ComponentHarness('<view is="dialog"/>', Dialog)
* .stubComponent('common:file-picker', {is: 'footer', as: 'stubFooter'});
*/
ComponentHarness.prototype.stubComponent = function () {
var args = [];
for (var _i = 0; _i < arguments.length; _i++) {
args[_i] = arguments[_i];
}
for (var i = 0; i < args.length; i++) {
// eslint-disable-next-line prefer-rest-params
var arg = arguments[i];
var options = (typeof arg === 'string') ? { is: arg } : arg;
var Stub = createStubComponent(options);
this.app.component(Stub);
}
return this;
};
/**
* @typedef {Object} RenderOptions
* @property {string} [url] - Optional URL for the render, used to populate `page.params`
*/
/**
* Renders the harness into a HTML string, as server-side rendering would do.
*
* @param {RenderOptions} [options]
* @returns { Page & {html: string} } - a `Page` that has a `html` property with the rendered HTML
* string
*/
ComponentHarness.prototype.renderHtml = function (options) {
return this._get(function (page) {
page.html = page.get('$harness');
}, options);
};
/**
* Renders the harness into a `DocumentFragment`, as client-side rendering would do.
*
* @param {RenderOptions} [options]
* @returns { Page & {fragment: DocumentFragment} } a `Page` that has a `fragment` property with the
* rendered `DocumentFragment`
*/
ComponentHarness.prototype.renderDom = function (options) {
return this._get(function (page) {
page.fragment = page.getFragment('$harness');
}, options);
};
ComponentHarness.prototype.attachTo = function (parentNode, node) {
return this._get(function (page) {
var view = page.getView('$harness');
var targetNode = node || parentNode.firstChild;
view.attachTo(parentNode, targetNode, page.context);
});
};
/**
* @param {(page: PageForHarness) => void} render
* @param {RenderOptions} [options]
*/
ComponentHarness.prototype._get = function (renderFn, options) {
options = options || {};
var url = options.url || '';
var page = this.app.createPage();
// Set `page.params`, which is usually created in tracks during `Page#render`:
// https://github.com/derbyjs/tracks/blob/master/lib/index.js
function setPageUrl(url) {
page.params = {
url: url,
query: qs.parse((0, url_1.parse)(url).query),
// @ts-expect-error 'body' does not exist in type 'Readonly<PageParams>'
body: {},
};
// Set "$render.params", "$render.query", "$render.url" based on `page.params`.
page._setRenderParams();
}
setPageUrl(url);
// Fake some methods from tracks/lib/History.js.
// JSDOM doesn't really support updating the window URL, but this should work for Derby code that
// pulls URL info from the model or page.
this.app.history = { push: setPageUrl, replace: setPageUrl, refresh: function () { } };
// The `#render` assertion in assertions.js wants to compare the results of HTML and DOM
// rendering, to make sure they match. However, component `create()` methods can modify the DOM
// immediately after initial rendering, which can break assertions.
//
// To get around this, we trigger a "pageRendered" event on the harness before `create()` methods
// get called. This is done by pausing the context, which prevents create() methods from getting
// called until the pause-count drops to 0.
page.context.pause();
renderFn(page);
this.emit('pageRendered', page);
page.context.unpause();
// HACK: Implement getting an instance as a side-effect of rendering. This
// code relies on the fact that while rendering, components are instantiated,
// and a reference is kept on page._components. Since we just created the
// page, we can reliably return the first component.
//
// The more standard means for getting a reference to a component controller
// would be to add a hooks in the view with `as=` or `on-init=`. However, we
// want the developer to pass this view in, so they can supply whatever
// harness context they like.
//
// This may need to be updated if the internal workings of Derby change.
page.component = page._components._1;
return page;
};
/**
* Instantiates a component and calls its `init()` if present, without rendering it.
*
* This can be used in place of `new MyComponent()` in old tests that were written prior to the
* component test framework being developed.
*
* @param Ctor - class (constructor) for the component to instantiate
* @param rootModel - a root model
* @returns a newly instantiated component, with its `init()` already called if present
*/
ComponentHarness.prototype.createNonRenderedComponent = function (Ctor, rootModel) {
// If the component doesn't already extend Component, then do so.
// This normally happens when calling `app.component(Ctor)` to register a component:
// https://github.com/derbyjs/derby/blob/2ababe7c805c59e51ddef0153cb8c5f6b66dd4ce/lib/App.js#L278-L279
(0, components_1.extendComponent)(Ctor);
// Mimic Derby's component creation process:
// createFactory: https://github.com/derbyjs/derby/blob/57d28637e8489244cc8438041e6c61d9468cd344/lib/components.js#L346
// Factory#init: https://github.com/derbyjs/derby/blob/57d28637e8489244cc8438041e6c61d9468cd344/lib/components.js#L370-L392
// new Page: https://github.com/derbyjs/derby/blob/57d28637e8489244cc8438041e6c61d9468cd344/lib/Page.js#L15
// Context#controller: https://github.com/derbyjs/derby-templates/blob/master/lib/contexts.js#L19-L25
return (0, components_1.createFactory)(Ctor).init(new Page_1.PageForClient(this.app, rootModel).context).controller;
};
ComponentHarness.createStubComponent = function (options) {
return createStubComponent(options);
};
return ComponentHarness;
}(events_1.EventEmitter));
exports.ComponentHarness = ComponentHarness;
function createStubComponent(options) {
var as = options.as || options.is;
var asArray = options.asArray;
var StubComponent = /** @class */ (function (_super) {
__extends(StubComponent, _super);
function StubComponent() {
return _super !== null && _super.apply(this, arguments) || this;
}
StubComponent.prototype.init = function () {
if (asArray) {
pageArrayInit.call(this);
}
else {
pageInit.call(this);
}
};
StubComponent.view = {
is: options.is,
file: options.file,
source: options.source,
dependencies: options.dependencies
};
return StubComponent;
}(components_1.Component));
function pageArrayInit() {
var _this = this;
var page = this.page;
if (page[asArray]) {
page[asArray].push(this);
}
else {
page[asArray] = [this];
}
this.on('destroy', function () {
var index = page[asArray].indexOf(_this);
if (index === -1)
return;
page[asArray].splice(index, 1);
});
}
function pageInit() {
var page = this.page;
page[as] = this;
this.on('destroy', function () {
page[as] = undefined;
});
}
return StubComponent;
}