UNPKG

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
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; }