diff --git a/package-lock.json b/package-lock.json index 70d105d0..f777cefb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4117,11 +4117,6 @@ "integrity": "sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=", "dev": true }, - "maquette": { - "version": "2.5.4", - "resolved": "https://registry.npmjs.org/maquette/-/maquette-2.5.4.tgz", - "integrity": "sha512-E7bciJ2sJxYPufjXZKHlxnrvcdo5okqLmJcvz0sWb5TLVN/M+aKt/epCQNGHBzYYZnx+3b4Ja+49BzpNJKBQGQ==" - }, "marked": { "version": "0.3.6", "resolved": "https://registry.npmjs.org/marked/-/marked-0.3.6.tgz", @@ -7046,12 +7041,6 @@ "requires": { "glob": "7.1.2" } - }, - "typescript": { - "version": "2.5.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-2.5.3.tgz", - "integrity": "sha512-ptLSQs2S4QuS6/OD1eAKG+S5G8QQtrU5RT32JULdZQtM1L3WTi34Wsu48Yndzi8xsObRAB9RPt/KhA9wlpEF6w==", - "dev": true } } }, diff --git a/package.json b/package.json index f11ab825..817ef2fc 100644 --- a/package.json +++ b/package.json @@ -54,7 +54,6 @@ }, "dependencies": { "intersection-observer": "^0.4.2", - "maquette": "~2.5.1", "pepjs": "^0.4.2" } } diff --git a/src/NodeHandler.ts b/src/NodeHandler.ts index 34d4442b..b963430a 100644 --- a/src/NodeHandler.ts +++ b/src/NodeHandler.ts @@ -1,12 +1,11 @@ import { Evented } from '@dojo/core/Evented'; -import { VNodeProperties } from '@dojo/interfaces/vdom'; import Map from '@dojo/shim/Map'; import { NodeHandlerInterface } from './interfaces'; /** * Enum to identify the type of event. * Listening to 'Projector' will notify when projector is created or updated - * Listening to 'Widget' will notifiy when widget root is created or updated + * Listening to 'Widget' will notify when widget root is created or updated */ export enum NodeEventType { Projector = 'Projector', @@ -25,17 +24,12 @@ export class NodeHandler extends Evented implements NodeHandlerInterface { return this._nodeMap.has(key); } - public add(element: HTMLElement, properties: VNodeProperties): void { - const key = String(properties.key); + public add(element: HTMLElement, key: string): void { this._nodeMap.set(key, element); this.emit({ type: key }); } - public addRoot(element: HTMLElement, properties: VNodeProperties): void { - if (properties && properties.key) { - this.add(element, properties); - } - + public addRoot(element: HTMLElement, key?: string): void { this.emit({ type: NodeEventType.Widget }); } diff --git a/src/WidgetBase.ts b/src/WidgetBase.ts index e9f1c63b..d6b692cf 100644 --- a/src/WidgetBase.ts +++ b/src/WidgetBase.ts @@ -1,11 +1,8 @@ +import { EventTypedObject } from '@dojo/interfaces/core'; import { Evented } from '@dojo/core/Evented'; -import { VNodeProperties } from '@dojo/interfaces/vdom'; -import { ProjectionOptions } from './interfaces'; import Map from '@dojo/shim/Map'; -import '@dojo/shim/Promise'; // Imported for side-effects import WeakMap from '@dojo/shim/WeakMap'; -import { Handle } from '@dojo/interfaces/core'; -import { isWNode, v, isHNode } from './d'; +import { v } from './d'; import { auto } from './diff'; import { AfterRender, @@ -15,10 +12,8 @@ import { DiffPropertyReaction, DNode, Render, - VirtualDomNode, WidgetMetaBase, WidgetMetaConstructor, - WidgetBaseConstructor, WidgetBaseInterface, WidgetProperties } from './interfaces'; @@ -26,15 +21,6 @@ import RegistryHandler from './RegistryHandler'; import NodeHandler from './NodeHandler'; import { isWidgetBaseConstructor, WIDGET_BASE_TYPE } from './Registry'; -/** - * Widget cache wrapper for instance management - */ -interface WidgetCacheWrapper { - child: WidgetBaseInterface; - widgetConstructor: WidgetBaseConstructor; - used: boolean; -} - enum WidgetRenderState { IDLE = 1, PROPERTIES, @@ -53,6 +39,12 @@ interface ReactionFunctionConfig { reaction: DiffPropertyReaction; } +export interface WidgetAndElementEvent extends EventTypedObject<'properties:changed'> { + key: string; + element: HTMLElement; + target: WidgetBase; +} + export type BoundFunctionData = { boundFunc: (...args: any[]) => any, scope: any }; const decoratorMap = new Map>(); @@ -78,11 +70,6 @@ export class WidgetBase

extends E */ private _dirty: boolean; - /** - * cachedVNode from previous render - */ - private _cachedVNode?: VirtualDomNode | VirtualDomNode[]; - /** * internal widget properties */ @@ -98,10 +85,7 @@ export class WidgetBase

extends E */ private _coreProperties: CoreProperties = {} as CoreProperties; - /** - * cached children map for instance management - */ - private _cachedChildrenMap: Map | WidgetBaseConstructor, WidgetCacheWrapper[]>; + private _cachedDNode: DNode | DNode[]; /** * map of specific property diff functions @@ -130,14 +114,6 @@ export class WidgetBase

extends E private _nodeHandler: NodeHandler; - private _projectorAttachEvent: Handle; - - private _currentRootNode = 0; - - private _numRootNodes = 0; - - private _rootNodeKeys: object[]; - /** * @constructor */ @@ -147,7 +123,6 @@ export class WidgetBase

extends E this._children = []; this._decoratorCache = new Map(); this._properties =

{}; - this._cachedChildrenMap = new Map | WidgetBaseConstructor, WidgetCacheWrapper[]>(); this._diffPropertyFunctionMap = new Map(); this._bindFunctionPropertyMap = new WeakMap<(...args: any[]) => any, { boundFunc: (...args: any[]) => any, scope: any }>(); this._registry = new RegistryHandler(); @@ -156,7 +131,22 @@ export class WidgetBase

extends E this.own(this._nodeHandler); this._boundRenderFunc = this.render.bind(this); this._boundInvalidate = this.invalidate.bind(this); - + this.own(this.on({ + 'element-created': ({ key, element }: WidgetAndElementEvent) => { + this._nodeHandler.add(element, `${key}`); + this.onElementCreated(element, key); + }, + 'element-updated': ({ key, element }: WidgetAndElementEvent) => { + this._nodeHandler.add(element, `${key}`); + this.onElementUpdated(element, key); + }, + 'widget-created': ({ element }: WidgetAndElementEvent) => { + this._nodeHandler.addRoot(element, undefined); + }, + 'widget-updated': ({ element }: WidgetAndElementEvent) => { + this._nodeHandler.addRoot(element, undefined); + } + })); this.own(this._registry.on('invalidate', this._boundInvalidate)); } @@ -175,58 +165,7 @@ export class WidgetBase

extends E } /** - * vnode afterCreate callback that calls the onElementCreated lifecycle method. - */ - private _afterCreateCallback( - element: HTMLElement, - projectionOptions: ProjectionOptions, - vnodeSelector: string, - properties: VNodeProperties - ): void { - this._addElementToNodeHandler(element, projectionOptions, properties); - this.onElementCreated(element, String(properties.key)); - } - - /** - * vnode afterUpdate callback that calls the onElementUpdated lifecycle method. - */ - private _afterUpdateCallback( - element: HTMLElement, - projectionOptions: ProjectionOptions, - vnodeSelector: string, - properties: VNodeProperties - ): void { - this._addElementToNodeHandler(element, projectionOptions, properties); - this.onElementUpdated(element, String(properties.key)); - } - - private _addElementToNodeHandler(element: HTMLElement, projectionOptions: ProjectionOptions, properties: VNodeProperties) { - const isRootNode = !properties.key || this._rootNodeKeys.indexOf(properties.key) > -1; - const hasKey = !!properties.key; - let isLastRootNode = false; - - if (isRootNode) { - this._currentRootNode++; - isLastRootNode = (this._currentRootNode === this._numRootNodes); - - if (this._projectorAttachEvent === undefined) { - this._projectorAttachEvent = projectionOptions.nodeEvent.on('rendered', () => { - this._nodeHandler.addProjector(); - }); - this.own(this._projectorAttachEvent); - } - } - - if (isLastRootNode) { - this._nodeHandler.addRoot(element, properties); - } - else if (hasKey) { - this._nodeHandler.add(element, properties); - } - } - - /** - * Widget lifecycle method that is called whenever a dom node is created for a vnode. + * Widget lifecycle method that is called whenever a dom node is created for a HNode. * Override this method to access the dom nodes that were inserted into the dom. * @param element The dom node represented by the vdom node. * @param key The vdom node's key. @@ -236,9 +175,7 @@ export class WidgetBase

extends E } /** - * Widget lifecycle method that is called whenever a dom node that is associated with a vnode is updated. - * Note: this method is dependant on the Maquette afterUpdate callback which is called if a dom - * node might have been updated. Maquette does not guarantee the dom node was updated. + * Widget lifecycle method that is called whenever a dom node that is associated with a HNode is updated. * Override this method to access the dom node. * @param element The dom node represented by the vdom node. * @param key The vdom node's key. @@ -255,6 +192,10 @@ export class WidgetBase

extends E return [ ...this._changedPropertyKeys ]; } + public get coreProperties(): CoreProperties { + return this._coreProperties; + } + public __setCoreProperties__(coreProperties: CoreProperties): void { this._renderState = WidgetRenderState.PROPERTIES; const { baseRegistry } = coreProperties; @@ -337,70 +278,20 @@ export class WidgetBase

extends E } } - public __render__(): VirtualDomNode | VirtualDomNode[] { + public __render__(): DNode | DNode[] { this._renderState = WidgetRenderState.RENDER; - if (this._dirty === true || this._cachedVNode === undefined) { + if (this._dirty || this._cachedDNode === undefined) { this._dirty = false; const render = this._runBeforeRenders(); let dNode = render(); - dNode = this.runAfterRenders(dNode); - this._decorateNodes(dNode); - const widget = this._dNodeToVNode(dNode); - this._manageDetachedChildren(); + this._cachedDNode = this.runAfterRenders(dNode); this._nodeHandler.clear(); - this._cachedVNode = widget; - this._renderState = WidgetRenderState.IDLE; - return widget; } this._renderState = WidgetRenderState.IDLE; - return this._cachedVNode; - } - - private _decorateNodes(node: DNode | DNode[]) { - let nodes = Array.isArray(node) ? [ ...node ] : [ node ]; - - this._numRootNodes = nodes.length; - this._currentRootNode = 0; - const rootNodes: DNode[] = []; - this._rootNodeKeys = []; - - nodes.forEach(node => { - if (isHNode(node)) { - rootNodes.push(node); - node.properties = node.properties || {}; - if (node.properties.key) { - this._rootNodeKeys.push(node.properties.key); - } - } - }); - - while (nodes.length) { - const node = nodes.pop(); - if (isHNode(node) || isWNode(node)) { - node.properties = node.properties || {}; - if (isHNode(node)) { - if (rootNodes.indexOf(node) !== -1 || node.properties.key) { - node.properties.afterCreate = this._afterCreateCallback; - node.properties.afterUpdate = this._afterUpdateCallback; - } - if (node.properties.bind === undefined) { - ( node.properties).bind = this; - } - } - else { - if (!node.coreProperties) { - node.coreProperties = { - bind: this, - baseRegistry: this._coreProperties.baseRegistry - }; - } - } - nodes = [ ...nodes, ...node.children ]; - } - } + return this._cachedDNode; } - protected invalidate(): void { + public invalidate(): void { if (this._renderState === WidgetRenderState.IDLE) { this._dirty = true; this.emit({ @@ -585,111 +476,6 @@ export class WidgetBase

extends E } return dNode; } - - /** - * Process a structure of DNodes into VNodes, string or null. `null` results are filtered. - * - * @param dNode the dnode to process - * @returns a VNode, string or null - */ - private _dNodeToVNode(dNode: DNode): VirtualDomNode; - private _dNodeToVNode(dNode: DNode[]): VirtualDomNode[]; - private _dNodeToVNode(dNode: DNode | DNode[]): VirtualDomNode | VirtualDomNode[]; - private _dNodeToVNode(dNode: DNode | DNode[]): VirtualDomNode | VirtualDomNode[] { - if (typeof dNode === 'string' || dNode === null || dNode === undefined) { - return dNode; - } - - if (Array.isArray(dNode)) { - return dNode.map((node) => this._dNodeToVNode(node)); - } - - if (isWNode(dNode)) { - const { children, properties, coreProperties } = dNode; - const { key } = properties; - - let { widgetConstructor } = dNode; - let child: WidgetBaseInterface; - - if (!isWidgetBaseConstructor(widgetConstructor)) { - const item = this._registry.get(widgetConstructor); - if (item === null) { - return null; - } - widgetConstructor = item; - } - - const childrenMapKey = key || widgetConstructor; - let cachedChildren = this._cachedChildrenMap.get(childrenMapKey) || []; - let cachedChild: WidgetCacheWrapper | undefined; - - for (let i = 0; i < cachedChildren.length; i++) { - const cachedChildWrapper = cachedChildren[i]; - if (cachedChildWrapper.widgetConstructor === widgetConstructor && cachedChildWrapper.used === false) { - cachedChild = cachedChildWrapper; - break; - } - } - - if (cachedChild !== undefined) { - child = cachedChild.child; - cachedChild.used = true; - } - else { - child = new widgetConstructor(); - child.own(child.on('invalidated', this._boundInvalidate)); - cachedChildren = [...cachedChildren, { child, widgetConstructor, used: true }]; - this._cachedChildrenMap.set(childrenMapKey, cachedChildren); - this.own(child); - } - child.__setCoreProperties__(coreProperties); - child.__setProperties__(properties); - if (typeof childrenMapKey !== 'string' && cachedChildren.length > 1) { - const widgetName = ( childrenMapKey).name; - let errorMsg = 'It is recommended to provide a unique \'key\' property when using the same widget multiple times'; - - if (widgetName) { - errorMsg = `It is recommended to provide a unique 'key' property when using the same widget (${widgetName}) multiple times`; - } - - console.warn(errorMsg); - this.emit({ type: 'error', target: this, error: new Error(errorMsg) }); - } - - child.__setChildren__(children); - return child.__render__(); - } - - dNode.vNodes = []; - for (let i = 0; i < dNode.children.length; i++) { - const child = dNode.children[i]; - if (child === null || child === undefined) { - continue; - } - dNode.vNodes.push(this._dNodeToVNode(child)); - } - - return dNode.render(); - } - - /** - * Manage widget instances after render processing - */ - private _manageDetachedChildren(): void { - this._cachedChildrenMap.forEach((cachedChildren, key) => { - const filteredCacheChildren: WidgetCacheWrapper[] = []; - for (let i = 0; i < cachedChildren.length; i++) { - const cachedChild = cachedChildren[i]; - if (cachedChild.used === false) { - cachedChild.child.destroy(); - continue; - } - cachedChild.used = false; - filteredCacheChildren.push(cachedChild); - } - this._cachedChildrenMap.set(key, filteredCacheChildren); - }); - } } export default WidgetBase; diff --git a/src/animations/cssTransitions.ts b/src/animations/cssTransitions.ts index 98e6c45e..982db86b 100644 --- a/src/animations/cssTransitions.ts +++ b/src/animations/cssTransitions.ts @@ -1,11 +1,8 @@ +import { VirtualDomProperties } from './../interfaces'; + let browserSpecificTransitionEndEventName = ''; let browserSpecificAnimationEndEventName = ''; -export interface VNodeProperties { - enterAnimationActive?: string; - exitAnimationActive?: string; -} - function determineBrowserStyleNames(element: HTMLElement) { if ('WebkitTransition' in element.style) { browserSpecificTransitionEndEventName = 'webkitTransitionEnd'; @@ -47,7 +44,7 @@ function runAndCleanUp(element: HTMLElement, startAnimation: () => void, finishA element.addEventListener(browserSpecificTransitionEndEventName, transitionEnd); } -function exit(node: HTMLElement, properties: VNodeProperties, exitAnimation: string, removeNode: () => void) { +function exit(node: HTMLElement, properties: VirtualDomProperties, exitAnimation: string, removeNode: () => void) { const activeClass = properties.exitAnimationActive || `${exitAnimation}-active`; runAndCleanUp(node, () => { @@ -61,7 +58,7 @@ function exit(node: HTMLElement, properties: VNodeProperties, exitAnimation: str }); } -function enter(node: HTMLElement, properties: VNodeProperties, enterAnimation: string) { +function enter(node: HTMLElement, properties: VirtualDomProperties, enterAnimation: string) { const activeClass = properties.enterAnimationActive || `${enterAnimation}-active`; runAndCleanUp(node, () => { diff --git a/src/d.ts b/src/d.ts index c0d7bd73..1af19b4c 100644 --- a/src/d.ts +++ b/src/d.ts @@ -1,7 +1,5 @@ import { assign } from '@dojo/core/lang'; -import { VNode } from '@dojo/interfaces/vdom'; import Symbol from '@dojo/shim/Symbol'; -import { h, VNodeProperties } from 'maquette'; import { Constructor, DefaultWidgetBaseInterface, @@ -81,9 +79,9 @@ export function w(widgetConstructor: Constructor< * Wrapper function for calls to create hyperscript, lazily executes the hyperscript creation */ export function v(tag: string, properties: VirtualDomProperties, children?: DNode[]): HNode; -export function v(tag: string, children: DNode[]): HNode; +export function v(tag: string, children: undefined | DNode[]): HNode; export function v(tag: string): HNode; -export function v(tag: string, propertiesOrChildren: VirtualDomProperties | DNode[] = {}, children: DNode[] = []): HNode { +export function v(tag: string, propertiesOrChildren: VirtualDomProperties | DNode[] = {}, children: undefined | DNode[] = undefined): HNode { let properties: VirtualDomProperties = propertiesOrChildren; if (Array.isArray(propertiesOrChildren)) { @@ -101,9 +99,6 @@ export function v(tag: string, propertiesOrChildren: VirtualDomProperties | DNod tag, children, properties, - render(this: { tag: string, vNodes: VNode[], properties: VNodeProperties }) { - return h(this.tag, this.properties, this.vNodes); - }, type: HNODE }; } diff --git a/src/interfaces.d.ts b/src/interfaces.d.ts index d910b6db..3bcf1f4d 100644 --- a/src/interfaces.d.ts +++ b/src/interfaces.d.ts @@ -1,16 +1,8 @@ import { Destroyable } from '@dojo/core/Destroyable'; import { Evented } from '@dojo/core/Evented'; import { EventTargettedObject } from '@dojo/interfaces/core'; -import { VNode, VNodeProperties, ProjectionOptions as MaquetteProjectionOptions } from '@dojo/interfaces/vdom'; import Map from '@dojo/shim/Map'; -/** - * Extended Dojo 2 projection options - */ -export interface ProjectionOptions extends MaquetteProjectionOptions { - nodeEvent: Evented; -} - /** * Generic constructor type */ @@ -24,7 +16,7 @@ export interface TypedTargetEvent extends Event { } /* - These are the event handlers exposed by Maquette. + These are the event handlers. */ export type EventHandlerResult = boolean | void; @@ -68,58 +60,55 @@ export type ClassesFunction = () => { [index: string]: boolean | null | undefined; }; +export interface TransitionStrategy { + enter(element: Element, properties: VirtualDomProperties, enterAnimation: string): void; + exit(element: Element, properties: VirtualDomProperties, exitAnimation: string, removeElement: () => void): void; +} + +export interface ProjectorOptions { + readonly transitions?: TransitionStrategy; + styleApplyer?(domNode: HTMLElement, styleName: string, value: string): void; +} + +export interface ProjectionOptions extends ProjectorOptions { + readonly namespace?: string; + eventHandlerInterceptor?: (propertyName: string, eventHandler: Function, domNode: Node, properties: VirtualDomProperties) => Function | undefined; + afterRenderCallbacks: Function[]; + merge: boolean; +} + +export interface Projection { + readonly domNode: Element; + update(updatedDNode: DNode): void; +} + export interface VirtualDomProperties { /** * The animation to perform when this node is added to an already existing parent. * When this value is a string, you must pass a `projectionOptions.transitions` object when creating the * projector using [[createProjector]]. - * {@link http://maquettejs.org/docs/animations.html|More about animations}. * @param element - Element that was just added to the DOM. * @param properties - The properties object that was supplied to the [[h]] method */ - enterAnimation?: ((element: Element, properties?: VNodeProperties) => void) | string; + enterAnimation?: ((element: Element, properties?: VirtualDomProperties) => void) | string; /** * The animation to perform when this node is removed while its parent remains. * When this value is a string, you must pass a `projectionOptions.transitions` object when creating the projector using [[createProjector]]. - * {@link http://maquettejs.org/docs/animations.html|More about animations}. * @param element - Element that ought to be removed from the DOM. * @param removeElement - Function that removes the element from the DOM. * This argument is provided purely for convenience. * You may use this function to remove the element when the animation is done. - * @param properties - The properties object that was supplied to the [[h]] method that rendered this [[VNode]] the previous time. + * @param properties - The properties object that was supplied to the [[v]] method that rendered this [[HNode]] the previous time. */ - exitAnimation?: ((element: Element, removeElement: () => void, properties?: VNodeProperties) => void) | string; + exitAnimation?: ((element: Element, removeElement: () => void, properties?: VirtualDomProperties) => void) | string; /** * The animation to perform when the properties of this node change. * This also includes attributes, styles, css classes. This callback is also invoked when node contains only text and that text changes. - * {@link http://maquettejs.org/docs/animations.html|More about animations}. * @param element - Element that was modified in the DOM. * @param properties - The last properties object that was supplied to the [[h]] method * @param previousProperties - The previous properties object that was supplied to the [[h]] method */ - updateAnimation?: (element: Element, properties?: VNodeProperties, previousProperties?: VNodeProperties) => void; - /** - * Callback that is executed after this node is added to the DOM. Child nodes and properties have - * already been applied. - * @param element - The element that was added to the DOM. - * @param projectionOptions - The projection options that were used, see [[createProjector]]. - * @param vnodeSelector - The selector passed to the [[h]] function. - * @param properties - The properties passed to the [[h]] function. - * @param children - The children that were created. - */ - afterCreate?(element: Element, projectionOptions: ProjectionOptions, vnodeSelector: string, properties: VNodeProperties, - children: VNode[]): void; - /** - * Callback that is executed every time this node may have been updated. Child nodes and properties - * have already been updated. - * @param element - The element that may have been updated in the DOM. - * @param projectionOptions - The projection options that were used, see [[createProjector]]. - * @param vnodeSelector - The selector passed to the [[h]] function. - * @param properties - The properties passed to the [[h]] function. - * @param children - The children for this node. - */ - afterUpdate?(element: Element, projectionOptions: ProjectionOptions, vnodeSelector: string, properties: VNodeProperties, - children: VNode[]): void; + updateAnimation?: (element: Element, properties?: VirtualDomProperties, previousProperties?: VirtualDomProperties) => void; /** * Bind should not be defined. */ @@ -206,7 +195,7 @@ export interface VirtualDomProperties { /** * Puts a non-interactive string of html inside the DOM node. * - * Note: if you use innerHTML, maquette cannot protect you from XSS vulnerabilities and you must make sure that the innerHTML value is safe. + * Note: if you use innerHTML, cannot protect you from XSS vulnerabilities and you must make sure that the innerHTML value is safe. */ readonly innerHTML?: string; @@ -261,36 +250,22 @@ interface CoreProperties { bind: any; } -/** - * Virtual DOM Node type - */ -export type VirtualDomNode = VNode | string | null | undefined; - /** * Wrapper for v */ export interface HNode { - /** - * Array of processed VNode children. - */ - vNodes?: (VirtualDomNode[] | VirtualDomNode)[]; /** * Specified children */ - children: DNode[]; - - /** - * render function that wraps returns VNode - */ - render(options?: { bind?: T }): VNode; + children?: DNode[]; /** - * The properties used to create the VNode + * HNode properties */ properties: VirtualDomProperties; /** - * The tagname used to create the VNode + * The tag of the HNode */ tag: string; @@ -298,6 +273,11 @@ export interface HNode { * The type of node */ type: symbol; + + /** + * Text node string + */ + text?: string; } /** @@ -314,15 +294,10 @@ export interface WNode = HNode | WNode | string | null | undefined; +export type DNode = HNode | WNode | undefined | null | string; /** * Property Change record for specific property diff functions @@ -358,14 +333,14 @@ export type WidgetBaseConstructor< P extends WidgetProperties = WidgetProperties, C extends DNode = DNode> = Constructor>; -export interface DefaultWidgetBaseInterface extends WidgetBaseInterface> {} +export interface DefaultWidgetBaseInterface extends WidgetBaseInterface {} /** * The interface for WidgetBase */ export interface WidgetBaseInterface< P = WidgetProperties, - C extends DNode = DNode> extends Evented { + C extends DNode = DNode> extends Evented { /** * Widget properties @@ -402,7 +377,7 @@ export interface WidgetBaseInterface< /** * Main internal function for dealing with widget rendering */ - __render__(): VirtualDomNode | VirtualDomNode[]; + __render__(): DNode | DNode[]; } /** @@ -422,9 +397,9 @@ export interface WidgetMetaConstructor { export interface NodeHandlerInterface extends Evented { get(key: string | number): HTMLElement | undefined; has(key: string | number): boolean; - add(element: HTMLElement, properties: VNodeProperties): void; - addRoot(element: HTMLElement, properties: VNodeProperties): void; - addProjector(element: HTMLElement, properties: VNodeProperties): void; + add(element: HTMLElement, key: string): void; + addRoot(element: HTMLElement, key: string): void; + addProjector(element: HTMLElement, properties: VirtualDomProperties): void; clear(): void; } diff --git a/src/mixins/I18n.ts b/src/mixins/I18n.ts index e6e41f61..878c6289 100644 --- a/src/mixins/I18n.ts +++ b/src/mixins/I18n.ts @@ -7,8 +7,7 @@ import i18n, { Messages, observeLocale } from '@dojo/i18n/i18n'; -import { VNodeProperties } from '@dojo/interfaces/vdom'; -import { Constructor, DNode, WidgetProperties } from './../interfaces'; +import { Constructor, DNode, WidgetProperties, VirtualDomProperties } from './../interfaces'; import { WidgetBase } from './../WidgetBase'; import { afterRender } from './../decorators/afterRender'; import { isHNode } from './../d'; @@ -32,7 +31,7 @@ export interface I18nProperties extends WidgetProperties { * @private * An internal helper interface for defining locale and text direction attributes on widget nodes. */ -interface I18nVNodeProperties extends VNodeProperties { +interface I18nVirtualDomProperties extends VirtualDomProperties { dir: string | null; lang: string | null; } @@ -109,19 +108,19 @@ export function I18nMixin>>(Base: T): T & protected renderDecorator(result: DNode): DNode { if (isHNode(result)) { const { locale, rtl } = this.properties; - const vNodeProperties: I18nVNodeProperties = { + const properties: I18nVirtualDomProperties = { dir: null, lang: null }; if (typeof rtl === 'boolean') { - vNodeProperties['dir'] = rtl ? 'rtl' : 'ltr'; + properties['dir'] = rtl ? 'rtl' : 'ltr'; } if (locale) { - vNodeProperties['lang'] = locale; + properties['lang'] = locale; } - assign(result.properties, vNodeProperties); + assign(result.properties, properties); } return result; } diff --git a/src/mixins/Projector.ts b/src/mixins/Projector.ts index 178630b1..fbc6c003 100644 --- a/src/mixins/Projector.ts +++ b/src/mixins/Projector.ts @@ -3,17 +3,15 @@ import global from '@dojo/shim/global'; import { createHandle } from '@dojo/core/lang'; import { Handle } from '@dojo/interfaces/core'; import { Evented } from '@dojo/core/Evented'; -import { VNode } from '@dojo/interfaces/vdom'; -import { ProjectionOptions } from '../interfaces'; -import { dom, Projection } from 'maquette'; import 'pepjs'; import cssTransitions from '../animations/cssTransitions'; -import { Constructor, DNode } from './../interfaces'; +import { Constructor, DNode, Projection, ProjectionOptions } from './../interfaces'; import { WidgetBase } from './../WidgetBase'; import { afterRender } from './../decorators/afterRender'; import { isHNode, v } from './../d'; import { Registry } from './../Registry'; import eventHandlerInterceptor from '../util/eventHandlerInterceptor'; +import { dom } from './../vdom'; /** * Represents the attach state of the projector @@ -138,22 +136,6 @@ export interface ProjectorMixin

{ invalidate(): void; } -/** - * Internal function that maps existing DOM Elements to virtual DOM nodes. - * - * The function does not presume DOM will be there. It does assume that if a DOM `Element` exists that the `VNode`s are in - * the same DOM order as the `Element`s. If a DOM Element does not exist, it will set the `vnode.domNode` to `null` and - * not descend further into the `VNode` children which will cause the maquette projection to create the Element anew. - * @param vnode The virtual DOM node - * @param domNode The Element, if any, to set on the virtual DOM node - */ -function setDomNodes(vnode: VNode, domNode: Element | null = null) { - vnode.domNode = domNode; - if (vnode.children && domNode) { - vnode.children.forEach((child, i) => setDomNodes(child, domNode.children[i])); - } -} - export function ProjectorMixin>>(Base: T): T & Constructor> { class Projector extends Base { @@ -163,16 +145,14 @@ export function ProjectorMixin>>(Base: T) private _root: Element; private _async = true; private _attachHandle: Handle; - private _projectionOptions: ProjectionOptions; + private _projectionOptions: Partial; private _projection: Projection | undefined; private _scheduled: number | undefined; private _paused: boolean; private _boundDoRender: () => void; private _boundRender: Function; private _projectorChildren: DNode[] = []; - private _resetChildren = true; private _projectorProperties: this['properties'] = {} as this['properties']; - private _resetProperties = true; private _rootTagName: string; private _attachType: AttachType; @@ -184,8 +164,7 @@ export function ProjectorMixin>>(Base: T) this._projectionOptions = { transitions: cssTransitions, - eventHandlerInterceptor: eventHandlerInterceptor.bind(this), - nodeEvent + eventHandlerInterceptor: eventHandlerInterceptor.bind(this) }; this._boundDoRender = this._doRender.bind(this); @@ -239,12 +218,16 @@ export function ProjectorMixin>>(Base: T) } public scheduleRender() { - if (this.projectorState === ProjectorAttachState.Attached && !this._scheduled && !this._paused) { - if (this._async) { - this._scheduled = global.requestAnimationFrame(this._boundDoRender); - } - else { - this._boundDoRender(); + if (this.projectorState === ProjectorAttachState.Attached) { + this.__setProperties__(this._projectorProperties); + this.__setChildren__(this._projectorChildren); + if (!this._scheduled && !this._paused) { + if (this._async) { + this._scheduled = global.requestAnimationFrame(this._boundDoRender); + } + else { + this._boundDoRender(); + } } } } @@ -290,13 +273,21 @@ export function ProjectorMixin>>(Base: T) } public setChildren(children: DNode[]): void { - this._resetChildren = false; + this.__setChildren__(children); + this.scheduleRender(); + } + + public __setChildren__(children: DNode[]) { this._projectorChildren = [ ...children ]; - super.__setChildren__([ ...children ]); + super.__setChildren__(children); } public setProperties(properties: this['properties']): void { - this._resetProperties = false; + this.__setProperties__(properties); + this.scheduleRender(); + } + + public __setProperties__(properties: this['properties']): void { if (this._projectorProperties && this._projectorProperties.registry !== properties.registry) { if (this._projectorProperties.registry) { this._projectorProperties.registry.destroy(); @@ -325,8 +316,7 @@ export function ProjectorMixin>>(Base: T) this._rootTagName = 'span'; } - node = v(this._rootTagName); - node.children = Array.isArray(result) ? result : [ result ]; + node = v(this._rootTagName, {}, Array.isArray(result) ? result : [ result ]); } else if (isHNode(node) && !this._rootTagName) { this._rootTagName = node.tag; @@ -338,8 +328,7 @@ export function ProjectorMixin>>(Base: T) node.tag = this._rootTagName; } else { - node = v(this._rootTagName); - node.children = Array.isArray(result) ? result : [ result ]; + node = v(this._rootTagName, {}, Array.isArray(result) ? result : [ result ]); } } } @@ -347,29 +336,11 @@ export function ProjectorMixin>>(Base: T) return node; } - public __render__(): VNode { - if (this._resetChildren) { - this.setChildren(this._projectorChildren); - } - if (this._resetProperties) { - this.setProperties(this._projectorProperties); - } - this._resetChildren = true; - this._resetProperties = true; - return super.__render__() as VNode; - } - - public invalidate(): void { - super.invalidate(); - this.scheduleRender(); - } - private _doRender() { this._scheduled = undefined; if (this._projection) { this._projection.update(this._boundRender()); - this._projectionOptions.nodeEvent.emit({ type: 'rendered' }); } } @@ -398,21 +369,17 @@ export function ProjectorMixin>>(Base: T) switch (type) { case AttachType.Append: - this._projection = dom.append(this.root, this._boundRender(), this._projectionOptions); + this._projection = dom.append(this.root, this._boundRender(), this, this._projectionOptions); break; case AttachType.Merge: this._rootTagName = this._root.tagName.toLowerCase(); - const vnode: VNode = this._boundRender(); - setDomNodes(vnode, this.root); - this._projection = dom.merge(this.root, vnode, this._projectionOptions); + this._projection = dom.merge(this.root, this._boundRender(), this , this._projectionOptions); break; case AttachType.Replace: - this._projection = dom.replace(this.root, this._boundRender(), this._projectionOptions); + this._projection = dom.replace(this.root, this._boundRender(), this, this._projectionOptions); break; } - this._projectionOptions.nodeEvent.emit({ type: 'rendered' }); - return this._attachHandle; } } diff --git a/src/util/DomWrapper.ts b/src/util/DomWrapper.ts index 85101be3..c955317c 100644 --- a/src/util/DomWrapper.ts +++ b/src/util/DomWrapper.ts @@ -1,7 +1,7 @@ import { WidgetBase } from './../WidgetBase'; -import { Constructor, DNode, VirtualDomProperties, WidgetProperties } from './../interfaces'; +import { Constructor, DNode, HNode, VirtualDomProperties, WidgetProperties } from './../interfaces'; import { v } from './../d'; -import { VNode } from '@dojo/interfaces/vdom'; +import { InternalHNode } from './../vdom'; export interface DomWrapperOptions { onAttached?(): void; @@ -14,10 +14,10 @@ export type DomWrapper = Constructor>; export function DomWrapper(domNode: Element, options: DomWrapperOptions = {}): DomWrapper { return class extends WidgetBase { - public __render__(): VNode { - const vNode = super.__render__() as VNode; - vNode.domNode = domNode; - return vNode; + public __render__(): HNode { + const hNode = super.__render__() as InternalHNode; + hNode.domNode = domNode; + return hNode; } protected onElementCreated(element: Element, key: string) { diff --git a/src/util/eventHandlerInterceptor.ts b/src/util/eventHandlerInterceptor.ts index 200a26eb..36f7af6b 100644 --- a/src/util/eventHandlerInterceptor.ts +++ b/src/util/eventHandlerInterceptor.ts @@ -1,5 +1,5 @@ import { includes } from '@dojo/shim/array'; -import { VNodeProperties } from 'maquette'; +import { VirtualDomProperties } from './../interfaces'; import Projector from '../mixins/Projector'; export const eventHandlers = [ @@ -25,7 +25,13 @@ export const eventHandlers = [ 'onsubmit' ]; -export default function eventHandlerInterceptor(this: Projector, propertyName: string, eventHandler: Function, domNode: Element, properties: VNodeProperties) { +export default function eventHandlerInterceptor( + this: Projector, + propertyName: string, + eventHandler: Function, + domNode: Element, + properties: VirtualDomProperties +) { if (includes(eventHandlers, propertyName)) { return function(this: Node, ...args: any[]) { return eventHandler.apply(properties.bind || this, args); diff --git a/src/vdom.ts b/src/vdom.ts new file mode 100644 index 00000000..16ca6bd7 --- /dev/null +++ b/src/vdom.ts @@ -0,0 +1,804 @@ +import global from '@dojo/shim/global'; +import { + CoreProperties, + DNode, + HNode, + WNode, + ProjectionOptions, + Projection, + TransitionStrategy, + VirtualDomProperties +} from './interfaces'; +import { from as arrayFrom } from '@dojo/shim/array'; +import { isWNode, isHNode, HNODE } from './d'; +import { WidgetBase } from './WidgetBase'; +import { isWidgetBaseConstructor } from './Registry'; + +const NAMESPACE_W3 = 'http://www.w3.org/'; +const NAMESPACE_SVG = NAMESPACE_W3 + '2000/svg'; +const NAMESPACE_XLINK = NAMESPACE_W3 + '1999/xlink'; + +const emptyArray: (InternalWNode | InternalHNode)[] = []; + +export interface InternalWNode extends WNode { + + /** + * The instance of the widget + */ + instance: WidgetBase; + + /** + * The rendered DNodes from the instance + */ + rendered: InternalDNode[]; + + /** + * Core properties that are used by the widget core system + */ + coreProperties: CoreProperties; + + /** + * Children for the WNode + */ + children: InternalDNode[]; +} + +export interface InternalHNode extends HNode { + + /** + * Children for the HNode + */ + children?: InternalDNode[]; + + /** + * DOM element + */ + domNode?: Element | Text; +} + +export type InternalDNode = InternalHNode | InternalWNode; + +function extend(base: T, overrides: any): T { + const result = {} as any; + Object.keys(base).forEach(function(key) { + result[key] = (base as any)[key]; + }); + if (overrides) { + Object.keys(overrides).forEach((key) => { + result[key] = overrides[key]; + }); + } + return result; +} + +function same(dnode1: InternalDNode, dnode2: InternalDNode) { + if (isHNode(dnode1) && isHNode(dnode2)) { + if (dnode1.tag !== dnode2.tag) { + return false; + } + if (dnode1.properties.key !== dnode2.properties.key) { + return false; + } + return true; + } + else if (isWNode(dnode1) && isWNode(dnode2)) { + if (dnode1.widgetConstructor !== dnode2.widgetConstructor) { + return false; + } + if (dnode1.properties.key !== dnode2.properties.key) { + return false; + } + return true; + } + return false; +} + +const missingTransition = function() { + throw new Error('Provide a transitions object to the projectionOptions to do animations'); +}; + +const DEFAULT_PROJECTION_OPTIONS: ProjectionOptions = { + namespace: undefined, + eventHandlerInterceptor: undefined, + styleApplyer: function(domNode: HTMLElement, styleName: string, value: string) { + (domNode.style as any)[styleName] = value; + }, + transitions: { + enter: missingTransition, + exit: missingTransition + }, + afterRenderCallbacks: [], + merge: false +}; + +function applyDefaultProjectionOptions(projectorOptions?: Partial): ProjectionOptions { + projectorOptions = extend(DEFAULT_PROJECTION_OPTIONS, projectorOptions); + projectorOptions.afterRenderCallbacks = []; + return projectorOptions as ProjectionOptions; +} + +function checkStyleValue(styleValue: Object) { + if (typeof styleValue !== 'string') { + throw new Error('Style values must be strings'); + } +} + +function setProperties(domNode: Node, properties: VirtualDomProperties, projectionOptions: ProjectionOptions) { + const eventHandlerInterceptor = projectionOptions.eventHandlerInterceptor; + const propNames = Object.keys(properties); + const propCount = propNames.length; + for (let i = 0; i < propCount; i++) { + const propName = propNames[i]; + let propValue = properties[propName]; + if (propName === 'className') { + throw new Error('Property `className` is not supported, use `class`.'); + } + else if (propName === 'class') { + (propValue as string).split(/\s+/).forEach(token => (domNode as Element).classList.add(token)); + } + else if (propName === 'classes') { + const classNames = Object.keys(propValue); + const classNameCount = classNames.length; + for (let j = 0; j < classNameCount; j++) { + const className = classNames[j]; + if (propValue[className]) { + (domNode as Element).classList.add(className); + } + } + } + else if (propName === 'styles') { + const styleNames = Object.keys(propValue); + const styleCount = styleNames.length; + for (let j = 0; j < styleCount; j++) { + const styleName = styleNames[j]; + const styleValue = propValue[styleName]; + if (styleValue) { + checkStyleValue(styleValue); + projectionOptions.styleApplyer!(domNode as HTMLElement, styleName, styleValue); + } + } + } + else if (propName !== 'key' && propValue !== null && propValue !== undefined) { + const type = typeof propValue; + if (type === 'function') { + if (propName.lastIndexOf('on', 0) === 0) { + if (eventHandlerInterceptor) { + propValue = eventHandlerInterceptor(propName, propValue, domNode, properties); + } + if (propName === 'oninput') { + (function() { + // record the evt.target.value, because IE and Edge sometimes do a requestAnimationFrame between changing value and running oninput + const oldPropValue = propValue; + propValue = function(this: HTMLElement, evt: Event) { + oldPropValue.apply(this, [evt]); + (evt.target as any)['oninput-value'] = (evt.target as HTMLInputElement).value; // may be HTMLTextAreaElement as well + }; + } ()); + } + (domNode as any)[propName] = propValue; + } + } + else if (type === 'string' && propName !== 'value' && propName !== 'innerHTML') { + if (projectionOptions.namespace === NAMESPACE_SVG && propName === 'href') { + (domNode as Element).setAttributeNS(NAMESPACE_XLINK, propName, propValue); + } + else { + (domNode as Element).setAttribute(propName, propValue); + } + } + else { + (domNode as any)[propName] = propValue; + } + } + } +} + +function updateProperties( + domNode: Node, + previousProperties: VirtualDomProperties, + properties: VirtualDomProperties, + projectionOptions: ProjectionOptions +) { + let propertiesUpdated = false; + const propNames = Object.keys(properties); + const propCount = propNames.length; + for (let i = 0; i < propCount; i++) { + const propName = propNames[i]; + let propValue = properties[propName]; + const previousValue = previousProperties![propName]; + if (propName === 'class') { + if (previousValue !== propValue) { + throw new Error('`class` property may not be updated. Use the `classes` property for conditional css classes.'); + } + } + else if (propName === 'classes') { + const classList = (domNode as Element).classList; + const classNames = Object.keys(propValue); + const classNameCount = classNames.length; + for (let j = 0; j < classNameCount; j++) { + const className = classNames[j]; + const on = !!propValue[className]; + const previousOn = !!previousValue[className]; + if (on === previousOn) { + continue; + } + propertiesUpdated = true; + if (on) { + classList.add(className); + } + else { + classList.remove(className); + } + } + } + else if (propName === 'styles') { + const styleNames = Object.keys(propValue); + const styleCount = styleNames.length; + for (let j = 0; j < styleCount; j++) { + const styleName = styleNames[j]; + const newStyleValue = propValue[styleName]; + const oldStyleValue = previousValue[styleName]; + if (newStyleValue === oldStyleValue) { + continue; + } + propertiesUpdated = true; + if (newStyleValue) { + checkStyleValue(newStyleValue); + projectionOptions.styleApplyer!(domNode as HTMLElement, styleName, newStyleValue); + } + else { + projectionOptions.styleApplyer!(domNode as HTMLElement, styleName, ''); + } + } + } + else { + if (!propValue && typeof previousValue === 'string') { + propValue = ''; + } + if (propName === 'value') { + const domValue = (domNode as any)[propName]; + if ( + domValue !== propValue + && ((domNode as any)['oninput-value'] + ? domValue === (domNode as any)['oninput-value'] + : propValue !== previousValue + ) + ) { + (domNode as any)[propName] = propValue; + (domNode as any)['oninput-value'] = undefined; + } + if (propValue !== previousValue) { + propertiesUpdated = true; + } + } + else if (propValue !== previousValue) { + const type = typeof propValue; + if (type === 'function') { + throw new Error(`Functions may not be updated on subsequent renders (property: ${propName})`); + } + if (type === 'string' && propName !== 'innerHTML') { + if (projectionOptions.namespace === NAMESPACE_SVG && propName === 'href') { + (domNode as Element).setAttributeNS(NAMESPACE_XLINK, propName, propValue); + } + else if (propName === 'role' && propValue === '') { + (domNode as any).removeAttribute(propName); + } + else { + (domNode as Element).setAttribute(propName, propValue); + } + } + else { + if ((domNode as any)[propName] !== propValue) { // Comparison is here for side-effects in Edge with scrollLeft and scrollTop + (domNode as any)[propName] = propValue; + } + } + propertiesUpdated = true; + } + } + } + return propertiesUpdated; +} + +function findIndexOfChild(children: InternalDNode[], sameAs: InternalDNode, start: number) { + for (let i = start; i < children.length; i++) { + if (same(children[i], sameAs)) { + return i; + } + } + return -1; +} + +export function toTextHNode(data: any): InternalHNode { + return { + tag: '', + properties: {}, + children: undefined, + text: `${data}`, + domNode: undefined, + type: HNODE + }; +} + +export function filterAndDecorateChildren(children: undefined | DNode | DNode[], instance: WidgetBase): InternalDNode[] { + if (children === undefined) { + return emptyArray; + } + children = Array.isArray(children) ? children : [ children ]; + + for (let i = 0; i < children.length;) { + const child = children[i] as InternalDNode; + if (child === undefined || child === null) { + children.splice(i, 1); + continue; + } + else if (typeof child === 'string') { + children[i] = toTextHNode(child); + } + else { + if (isHNode(child)) { + if (child.properties.bind === undefined) { + (child.properties as any).bind = instance; + if (child.children && child.children.length > 0) { + filterAndDecorateChildren(child.children, instance); + } + } + } + else { + if (!child.coreProperties) { + child.coreProperties = { + bind: instance, + baseRegistry: instance.coreProperties.baseRegistry + }; + } + if (child.children && child.children.length > 0) { + filterAndDecorateChildren(child.children, instance); + } + } + } + i++; + } + return children as InternalDNode[]; +} + +function hasRenderChanged(previousRendered: InternalDNode[], rendered: InternalDNode | InternalDNode[]): boolean { + const arrayRender = Array.isArray(rendered); + if (arrayRender) { + return previousRendered !== rendered; + } + else { + return Array.isArray(previousRendered) && previousRendered[0] !== rendered; + } +} + +function nodeAdded(dnode: InternalDNode, transitions: TransitionStrategy) { + if (isHNode(dnode) && dnode.properties) { + const enterAnimation = dnode.properties.enterAnimation; + if (enterAnimation) { + if (typeof enterAnimation === 'function') { + enterAnimation(dnode.domNode as Element, dnode.properties); + } + else { + transitions.enter(dnode.domNode as Element, dnode.properties, enterAnimation as string); + } + } + } +} + +function nodeToRemove(dnode: InternalDNode, transitions: TransitionStrategy, projectionOptions: ProjectionOptions) { + if (isWNode(dnode)) { + projectionOptions.afterRenderCallbacks.push(dnode.instance.destroy.bind(dnode.instance)); + const rendered = dnode.rendered || emptyArray ; + for (let i = 0; i < rendered.length; i++) { + const child = rendered[i]; + if (isHNode(child)) { + child.domNode!.parentNode!.removeChild(child.domNode!); + } + else { + nodeToRemove(child, transitions, projectionOptions); + } + } + } + else { + const domNode = dnode.domNode; + const properties = dnode.properties; + const exitAnimation = properties.exitAnimation; + if (properties && exitAnimation) { + (domNode as HTMLElement).style.pointerEvents = 'none'; + const removeDomNode = function() { + domNode && domNode.parentNode && domNode.parentNode.removeChild(domNode); + }; + if (typeof exitAnimation === 'function') { + exitAnimation(domNode as Element, removeDomNode, properties); + return; + } + else { + transitions.exit(dnode.domNode as Element, properties, exitAnimation as string, removeDomNode); + return; + } + } + domNode && domNode.parentNode && domNode.parentNode.removeChild(domNode); + } +} + +function checkDistinguishable(childNodes: InternalDNode[], indexToCheck: number, parentDNode: InternalDNode, operation: string) { + const childNode = childNodes[indexToCheck]; + if (isHNode(childNode) && childNode.tag === '') { + return; // Text nodes need not be distinguishable + } + const properties = childNode.properties; + const key = properties && properties.key; + + if (!key) { + for (let i = 0; i < childNodes.length; i++) { + if (i !== indexToCheck) { + const node = childNodes[i]; + if (same(node, childNode)) { + if (isWNode(childNode)) { + const widgetName = (childNode.widgetConstructor as any).name; + let errorMsg = 'It is recommended to provide a unique \'key\' property when using the same widget multiple times as siblings'; + + if (widgetName) { + errorMsg = `It is recommended to provide a unique 'key' property when using the same widget (${widgetName}) multiple times as siblings`; + } + console.warn(errorMsg); + } + else { + if (operation === 'added') { + throw new Error((parentDNode as HNode).tag + ' had a ' + childNode.tag + ' child ' + + 'added, but there is now more than one. You must add unique key properties to make them distinguishable.'); + } + else { + throw new Error((parentDNode as HNode).tag + ' had a ' + childNode.tag + ' child ' + + 'removed, but there were more than one. You must add unique key properties to make them distinguishable.'); + } + } + } + } + } + } +} + +function updateChildren( + dnode: InternalDNode, + domNode: Node, + oldChildren: InternalDNode[], + newChildren: InternalDNode[], + parentInstance: WidgetBase, + projectionOptions: ProjectionOptions +) { + oldChildren = oldChildren || emptyArray; + newChildren = newChildren; + const oldChildrenLength = oldChildren.length; + const newChildrenLength = newChildren.length; + const transitions = projectionOptions.transitions!; + + let oldIndex = 0; + let newIndex = 0; + let i: number; + let textUpdated = false; + while (newIndex < newChildrenLength) { + const oldChild = (oldIndex < oldChildrenLength) ? oldChildren[oldIndex] : undefined; + const newChild = newChildren[newIndex]; + + if (oldChild !== undefined && same(oldChild, newChild)) { + textUpdated = updateDom(oldChild, newChild, projectionOptions, domNode, parentInstance) || textUpdated; + oldIndex++; + } + else { + const findOldIndex = findIndexOfChild(oldChildren, newChild, oldIndex + 1); + if (findOldIndex >= 0) { + for (i = oldIndex; i < findOldIndex; i++) { + nodeToRemove(oldChildren[i], transitions, projectionOptions); + checkDistinguishable(oldChildren, i, dnode, 'removed'); + } + textUpdated = updateDom(oldChildren[findOldIndex], newChild, projectionOptions, domNode, parentInstance) || textUpdated; + oldIndex = findOldIndex + 1; + } + else { + let insertBefore: Node | undefined = undefined; + let child: InternalDNode = oldChildren[oldIndex]; + if (child) { + while (insertBefore === undefined) { + if (isWNode(child)) { + child = child.rendered[0]; + } + else { + insertBefore = child.domNode; + } + } + } + + createDom(newChild, domNode, insertBefore, projectionOptions, parentInstance); + nodeAdded(newChild, transitions); + checkDistinguishable(newChildren, newIndex, dnode, 'added'); + } + } + newIndex++; + } + if (oldChildrenLength > oldIndex) { + // Remove child fragments + for (i = oldIndex; i < oldChildrenLength; i++) { + nodeToRemove(oldChildren[i], transitions, projectionOptions); + checkDistinguishable(oldChildren, i, dnode, 'removed'); + } + } + return textUpdated; +} + +function addChildren( + domNode: Node, + children: InternalDNode[] | undefined, + projectionOptions: ProjectionOptions, + parentInstance: WidgetBase, + insertBefore: undefined | Node = undefined, + childNodes?: Node[] +) { + if (children === undefined) { + return; + } + + if (projectionOptions.merge && childNodes === undefined) { + childNodes = arrayFrom(domNode.childNodes); + } + + for (let i = 0; i < children.length; i++) { + const child = children[i]; + + if (isHNode(child)) { + if (projectionOptions.merge && childNodes) { + let domElement: HTMLElement | undefined = undefined; + while (child.domNode === undefined && childNodes.length > 0) { + domElement = childNodes.shift() as HTMLElement; + if (domElement && domElement.tagName === (child.tag.toUpperCase() || undefined)) { + child.domNode = domElement; + } + } + } + createDom(child, domNode, insertBefore, projectionOptions, parentInstance); + } + else { + createDom(child, domNode, insertBefore, projectionOptions, parentInstance, childNodes); + } + } +} + +function initPropertiesAndChildren( + domNode: Node, + dnode: InternalHNode, + parentInstance: WidgetBase, + projectionOptions: ProjectionOptions +) { + addChildren(domNode, dnode.children, projectionOptions, parentInstance, undefined); + setProperties(domNode, dnode.properties, projectionOptions); + if (dnode.properties.key !== null && dnode.properties.key !== undefined) { + projectionOptions.afterRenderCallbacks.push(() => { + parentInstance.emit({ type: 'element-created', key: dnode.properties.key, element: domNode }); + }); + } +} + +function createDom( + dnode: InternalDNode, + parentNode: Node, + insertBefore: Node | undefined, + projectionOptions: ProjectionOptions, + parentInstance: WidgetBase, + childNodes?: Node[] +) { + let domNode: Node | undefined; + if (isWNode(dnode)) { + let { widgetConstructor } = dnode; + if (!isWidgetBaseConstructor(widgetConstructor)) { + const item = parentInstance.registry.get(widgetConstructor); + if (item === null) { + return; + } + widgetConstructor = item; + } + const instance = new widgetConstructor(); + parentInstance.own(instance); + instance.own(instance.on('invalidated', () => { + parentInstance.invalidate(); + })); + dnode.instance = instance; + instance.__setCoreProperties__(dnode.coreProperties); + instance.__setChildren__(dnode.children); + instance.__setProperties__(dnode.properties); + const rendered = instance.__render__(); + if (rendered) { + const filteredRendered = filterAndDecorateChildren(rendered, instance as WidgetBase); + dnode.rendered = filteredRendered; + addChildren(parentNode, filteredRendered, projectionOptions, instance as WidgetBase, insertBefore, childNodes); + } + projectionOptions.afterRenderCallbacks.push(() => { + parentInstance.emit({ type: 'widget-created' }); + }); + } + else { + const doc = parentNode.ownerDocument; + if (dnode.tag === '') { + if (dnode.domNode !== undefined) { + const newDomNode = dnode.domNode.ownerDocument.createTextNode(dnode.text!); + dnode.domNode.parentNode!.replaceChild(newDomNode, dnode.domNode); + dnode.domNode = newDomNode; + } + else { + domNode = dnode.domNode = doc.createTextNode(dnode.text!); + if (insertBefore !== undefined) { + parentNode.insertBefore(domNode, insertBefore); + } + else { + parentNode.appendChild(domNode); + } + } + } + else { + if (dnode.domNode === undefined) { + if (dnode.tag === 'svg') { + projectionOptions = extend(projectionOptions, { namespace: NAMESPACE_SVG }); + } + if (projectionOptions.namespace !== undefined) { + domNode = dnode.domNode = doc.createElementNS(projectionOptions.namespace, dnode.tag); + } + else { + domNode = dnode.domNode = (dnode.domNode || doc.createElement(dnode.tag)); + } + } + else { + domNode = dnode.domNode; + } + if (insertBefore !== undefined) { + parentNode.insertBefore(domNode, insertBefore); + } + else if (domNode!.parentNode !== parentNode) { + parentNode.appendChild(domNode); + } + initPropertiesAndChildren(domNode!, dnode, parentInstance, projectionOptions); + } + } +} + +function updateDom(previous: any, dnode: InternalDNode, projectionOptions: ProjectionOptions, parentNode: Node, parentInstance: WidgetBase) { + if (previous === dnode) { + return false; + } + if (isWNode(dnode)) { + const { instance, rendered: previousRendered } = previous; + if (instance && previousRendered) { + instance.__setCoreProperties__(dnode.coreProperties); + instance.__setChildren__(dnode.children); + instance.__setProperties__(dnode.properties); + dnode.instance = instance; + const rendered = instance.__render__(); + dnode.rendered = filterAndDecorateChildren(rendered, instance); + if (hasRenderChanged(previousRendered, rendered)) { + updateChildren(dnode, parentNode, previousRendered, dnode.rendered, instance, projectionOptions); + projectionOptions.afterRenderCallbacks.push(() => { + parentInstance.emit({ type: 'widget-updated' }); + }); + } + } + else { + createDom(dnode, parentNode, undefined, projectionOptions, parentInstance); + } + } + else { + const domNode = previous.domNode!; + let textUpdated = false; + let updated = false; + if (dnode.tag === '') { + if (dnode.text !== previous.text) { + const newDomNode = domNode.ownerDocument.createTextNode(dnode.text!); + domNode.parentNode!.replaceChild(newDomNode, domNode); + dnode.domNode = newDomNode; + textUpdated = true; + return textUpdated; + } + } + else { + if (dnode.tag.lastIndexOf('svg', 0) === 0) { + projectionOptions = extend(projectionOptions, { namespace: NAMESPACE_SVG }); + } + if (previous.children !== dnode.children) { + const children = filterAndDecorateChildren(dnode.children, parentInstance); + dnode.children = children; + updated = updateChildren(dnode, domNode, previous.children, children, parentInstance, projectionOptions) || updated; + } + updated = updateProperties(domNode, previous.properties, dnode.properties, projectionOptions) || updated; + + if (dnode.properties.key !== null && dnode.properties.key !== undefined) { + projectionOptions.afterRenderCallbacks.push(() => { + parentInstance.emit({ type: 'element-updated', key: dnode.properties.key, element: domNode }); + }); + } + } + if (updated && dnode.properties && dnode.properties.updateAnimation) { + dnode.properties.updateAnimation(domNode as Element, dnode.properties, previous.properties); + } + dnode.domNode = previous.domNode; + return textUpdated; + } +} + +function runAfterRenderCallbacks(projectionOptions: ProjectionOptions) { + if (global.requestIdleCallback) { + global.requestIdleCallback(() => { + while (projectionOptions.afterRenderCallbacks.length) { + const callback = projectionOptions.afterRenderCallbacks.shift(); + callback && callback(); + } + }); + } + else { + setTimeout(() => { + while (projectionOptions.afterRenderCallbacks.length) { + const callback = projectionOptions.afterRenderCallbacks.shift(); + callback && callback(); + } + }); + } +} + +function createProjection(dnode: InternalHNode, parentInstance: WidgetBase, projectionOptions: ProjectionOptions): Projection { + return { + update: function(updatedHNode: HNode) { + if (dnode.tag !== updatedHNode.tag) { + throw new Error('The tag for the root HNode may not be changed. (consider using dom.merge and add one extra level to the virtual DOM)'); + } + updatedHNode.children = filterAndDecorateChildren(updatedHNode.children, parentInstance); + updateDom(dnode, updatedHNode as InternalHNode, projectionOptions, dnode.domNode as Element, parentInstance); + projectionOptions.afterRenderCallbacks.push(() => { + parentInstance.emit({ type: 'widget-created' }); + }); + runAfterRenderCallbacks(projectionOptions); + dnode = updatedHNode as InternalHNode; + }, + domNode: dnode.domNode as Element + }; +} + +export const dom = { + create: function(hNode: HNode, instance: WidgetBase, projectionOptions?: Partial): Projection { + const finalProjectorOptions = applyDefaultProjectionOptions(projectionOptions); + const decoratedNode = filterAndDecorateChildren(hNode, instance)[0] as InternalHNode; + createDom(decoratedNode, document.createElement('div'), undefined, finalProjectorOptions, instance); + finalProjectorOptions.afterRenderCallbacks.push(() => { + instance.emit({ type: 'widget-created' }); + }); + runAfterRenderCallbacks(finalProjectorOptions); + return createProjection(decoratedNode, instance, finalProjectorOptions); + }, + append: function(parentNode: Element, hNode: HNode, instance: WidgetBase, projectionOptions?: Partial): Projection { + const finalProjectorOptions = applyDefaultProjectionOptions(projectionOptions); + const decoratedNode = filterAndDecorateChildren(hNode, instance)[0] as InternalHNode; + createDom(decoratedNode, parentNode, undefined, finalProjectorOptions, instance); + finalProjectorOptions.afterRenderCallbacks.push(() => { + instance.emit({ type: 'widget-created' }); + }); + runAfterRenderCallbacks(finalProjectorOptions); + return createProjection(decoratedNode, instance, finalProjectorOptions); + }, + merge: function(element: Element, hNode: HNode, instance: WidgetBase, projectionOptions?: Partial): Projection { + const finalProjectorOptions = applyDefaultProjectionOptions(projectionOptions); + finalProjectorOptions.merge = true; + const decoratedNode = filterAndDecorateChildren(hNode, instance)[0] as InternalHNode; + decoratedNode.domNode = element; + initPropertiesAndChildren(element, decoratedNode, instance, finalProjectorOptions); + finalProjectorOptions.afterRenderCallbacks.push(() => { + instance.emit({ type: 'widget-created' }); + }); + runAfterRenderCallbacks(finalProjectorOptions); + return createProjection(decoratedNode, instance, finalProjectorOptions); + }, + replace: function(element: Element, hNode: HNode, instance: WidgetBase, projectionOptions?: Partial): Projection { + const finalProjectorOptions = applyDefaultProjectionOptions(projectionOptions); + const decoratedNode = filterAndDecorateChildren(hNode, instance)[0] as InternalHNode; + createDom(decoratedNode, element.parentNode!, element, finalProjectorOptions, instance); + finalProjectorOptions.afterRenderCallbacks.push(() => { + instance.emit({ type: 'widget-created' }); + }); + runAfterRenderCallbacks(finalProjectorOptions); + element.parentNode!.removeChild(element); + return createProjection(decoratedNode, instance, finalProjectorOptions); + } +}; diff --git a/tests/functional/meta/Drag.html b/tests/functional/meta/Drag.html index 7280cf14..75c843e6 100644 --- a/tests/functional/meta/Drag.html +++ b/tests/functional/meta/Drag.html @@ -9,7 +9,6 @@ require.config({ packages: [ { name: '@dojo', location: '../../../../node_modules/@dojo' }, - { name: 'maquette', location: '../../../../node_modules/maquette/dist', main: 'maquette' }, { name: 'pepjs', location: '../../../../node_modules/pepjs/dist', main: 'pep' } ] }); @@ -17,4 +16,4 @@ require([ './Drag' ], function () {}); - \ No newline at end of file + diff --git a/tests/functional/support/registerCustomElement.html b/tests/functional/support/registerCustomElement.html index 308f1642..f8f7d64c 100644 --- a/tests/functional/support/registerCustomElement.html +++ b/tests/functional/support/registerCustomElement.html @@ -18,7 +18,6 @@ require.config({ packages: [ { name: '@dojo', location: '../../../../node_modules/@dojo' }, - { name: 'maquette', location: '../../../../node_modules/maquette/dist', main: 'maquette' }, { name: 'pepjs', location: '../../../../node_modules/pepjs/dist', main: 'pep' } ] }); diff --git a/tests/intern.ts b/tests/intern.ts index 1e3fe86b..31df2938 100644 --- a/tests/intern.ts +++ b/tests/intern.ts @@ -59,7 +59,6 @@ export const loaderOptions = { { name: 'cldr-data', location: 'node_modules/cldr-data' }, { name: 'cldrjs', location: 'node_modules/cldrjs' }, { name: 'globalize', location: 'node_modules/globalize', main: 'dist/globalize' }, - { name: 'maquette', location: 'node_modules/maquette/dist', main: 'maquette' }, { name: 'pepjs', location: 'node_modules/pepjs/dist', main: 'pep' }, { name: 'intersection-observer', location: 'node_modules/intersection-observer', main: 'intersection-observer' }, { name: 'grunt-dojo2', location: 'node_modules/grunt-dojo2'}, diff --git a/tests/support/createTestWidget.ts b/tests/support/createTestWidget.ts deleted file mode 100644 index ce1383a5..00000000 --- a/tests/support/createTestWidget.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { WidgetBase } from './../../src/WidgetBase'; -import { w } from './../../src/d'; -import { Registry } from './../../src/Registry'; -import { Constructor, VirtualDomNode, WidgetBaseInterface } from './../../src/interfaces'; - -interface TestWidget extends WidgetBaseInterface { - getWidgetUnderTest(): W; - getAttribute(key: string): any; - setProperties(properties: W['properties']): void; - setChildren(children: W['children']): void; - invalidate(): void; - renderResult: VirtualDomNode | VirtualDomNode[]; -} - -function createTestWidget( - component: Constructor, - properties: W['properties'] & { registry?: Registry }, - children?: W['children'] -): TestWidget { - const { registry, ...props } = properties as any; - const testWidget = new class extends WidgetBase implements TestWidget { - private _testProperties: W['properties'] = props; - private _testChildren: W['children'] = children || []; - private _renderResult: VirtualDomNode | VirtualDomNode[]; - - getAttribute(key: string): any { - return ( this)[key]; - } - - setProperties(properties: W['properties'] & { registry: Registry }): void { - const { registry, ...props } = properties as any; - this._testProperties = props; - this.__setCoreProperties__({ baseRegistry: registry, bind: this }); - this.invalidate(); - } - - setChildren(children: W['children']): void { - this._testChildren = children; - this.invalidate(); - } - - invalidate(): void { - super.invalidate(); - } - - getWidgetUnderTest(): W { - return ( this)._cachedChildrenMap.get(component)[0].child; - } - - render() { - return w(component, this._testProperties, this._testChildren); - } - - get renderResult(): VirtualDomNode | VirtualDomNode[] { - return this._renderResult; - } - }; - testWidget.__setCoreProperties__({ baseRegistry: registry, bind: testWidget }); - testWidget.__render__(); - return testWidget; -} - -export default createTestWidget; diff --git a/tests/support/loadJsdom.ts b/tests/support/loadJsdom.ts index 80b01287..1f391fd5 100644 --- a/tests/support/loadJsdom.ts +++ b/tests/support/loadJsdom.ts @@ -29,6 +29,12 @@ global.requestAnimationFrame = (cb: (...args: any[]) => {}) => { return true; }; +global.requestIdleCallback = (cb: (...args: any[]) => {}) => { + setImmediate(cb); + // return something at least! + return true; +}; + global.cancelAnimationFrame = () => {}; global.IntersectionObserver = () => {}; diff --git a/tests/support/util.ts b/tests/support/util.ts index 97e64a15..ab5da234 100644 --- a/tests/support/util.ts +++ b/tests/support/util.ts @@ -1,7 +1,9 @@ +import global from '@dojo/shim/global'; import Promise from '@dojo/shim/Promise'; import loadCldrData from '@dojo/i18n/cldr/load'; import { systemLocale } from '@dojo/i18n/i18n'; import likelySubtags from './likelySubtags'; +import { stub, SinonStub } from 'sinon'; /** * Load into Globalize.js all CLDR data for the specified locales. @@ -17,3 +19,42 @@ export function fetchCldrData(): Promise { loadCldrData(likelySubtags) ]); } + +export function createResolvers() { + let rAFStub: SinonStub; + let rICStub: SinonStub; + + function resolveRAF() { + for (let i = 0; i < rAFStub.callCount; i++) { + rAFStub.getCall(0).args[0](); + } + rAFStub.reset(); + } + + function resolveRIC() { + for (let i = 0; i < rICStub.callCount; i++) { + rICStub.getCall(0).args[0](); + } + rICStub.reset(); + } + + return { + resolve() { + resolveRAF(); + resolveRIC(); + }, + stub() { + rAFStub = stub(global, 'requestAnimationFrame').returns(1); + if (global.requestIdleCallback) { + rICStub = stub(global, 'requestIdleCallback').returns(1); + } + else { + rICStub = stub(global, 'setTimeout').returns(1); + } + }, + restore() { + rAFStub.restore(); + rICStub.restore(); + } + }; +} diff --git a/tests/unit/Container.ts b/tests/unit/Container.ts index a84b380d..f24bb44d 100644 --- a/tests/unit/Container.ts +++ b/tests/unit/Container.ts @@ -1,6 +1,6 @@ import * as registerSuite from 'intern!object'; import * as assert from 'intern/chai!assert'; -import { v , w } from '../../src/d'; +import { v } from '../../src/d'; import { WidgetBase } from '../../src/WidgetBase'; import { diffProperty } from './../../src/decorators/diffProperty'; import { always } from '../../src/diff'; @@ -8,8 +8,6 @@ import { Container } from './../../src/Container'; import { Registry } from './../../src/Registry'; import { Injector } from './../../src/Injector'; -import createTestWidget from './../support/createTestWidget'; - interface TestWidgetProperties { foo: string; boo: number; @@ -17,6 +15,7 @@ interface TestWidgetProperties { class TestWidget extends WidgetBase { render() { + assertRender(this.properties); return v('test', this.properties); } } @@ -90,31 +89,29 @@ registerSuite({ }; const TestWidgetContainer = Container('test-widget', 'test-state-1', { getProperties }); - const widget = createTestWidget(TestWidgetContainer, { foo: 'bar', registry }); + const widget = new TestWidgetContainer(); const renderResult: any = widget.__render__(); - assert.strictEqual(renderResult.vnodeSelector, 'test'); + + assert.strictEqual(renderResult.widgetConstructor, 'test-widget'); }, 'container always updates'() { @diffProperty('foo', always) class Child extends WidgetBase<{ foo: string }> {} - const ChildContainer = Container(Child, 'test-state-1', { getProperties }); + let invalidatedCount = 0; - class Parent extends WidgetBase { - render() { - const { foo } = this.properties; - return w(ChildContainer, { foo }); + class ContainerClass extends Container(Child, 'test-state-1', { getProperties }) { + invalidate() { + invalidatedCount++; + super.invalidate(); } - } - const widget = new Parent(); + const widget = new ContainerClass(); widget.__setCoreProperties__({ bind: widget, baseRegistry: registry }); widget.__setProperties__({ foo: 'bar'}); - const renderResult = widget.__render__(); - injector.set({ foo: 'bar' }); - const injectorUpdatedRenderResult = widget.__render__(); - assert.notStrictEqual(injectorUpdatedRenderResult, renderResult); - widget.__setProperties__({ foo: 'bar', bar: 'foo' }); - const updatedRenderResult = widget.__render__(); - assert.notStrictEqual(updatedRenderResult, injectorUpdatedRenderResult); + assert.strictEqual(invalidatedCount, 3); + widget.__setProperties__({ foo: 'bar'}); + assert.strictEqual(invalidatedCount, 4); + widget.__setProperties__({ foo: 'bar'}); + assert.strictEqual(invalidatedCount, 5); } }); diff --git a/tests/unit/NodeHandler.ts b/tests/unit/NodeHandler.ts index b5a32592..6d0f85b3 100644 --- a/tests/unit/NodeHandler.ts +++ b/tests/unit/NodeHandler.ts @@ -16,18 +16,18 @@ registerSuite({ element = document.createElement('div'); }, 'add populates nodehandler map'() { - nodeHandler.add(element, { key: 'foo' }); + nodeHandler.add(element, 'foo'); assert.isTrue(nodeHandler.has('foo')); }, 'has returns undefined when element does not exist'() { assert.isFalse(nodeHandler.has('foo')); }, 'get returns elements that have been added'() { - nodeHandler.add(element, { key: 'foo' }); + nodeHandler.add(element, 'foo'); assert.equal(nodeHandler.get('foo'), element); }, 'clear removes nodes from map'() { - nodeHandler.add(element, { key: 'foo' }); + nodeHandler.add(element, 'foo'); assert.isTrue(nodeHandler.has('foo')); nodeHandler.clear(); assert.isFalse(nodeHandler.has('foo')); @@ -43,21 +43,20 @@ registerSuite({ nodeHandler.on(NodeEventType.Projector, projectorStub); }, 'add emits event when element added'() { - nodeHandler.add(element, { key: 'foo' }); + nodeHandler.add(element, 'foo'); assert.isTrue(elementStub.calledOnce); assert.isTrue(widgetStub.notCalled); assert.isTrue(projectorStub.notCalled); }, - 'add root emits Widget and element event'() { - nodeHandler.addRoot(element, { key: 'foo' }); + 'add root emits Widget'() { + nodeHandler.addRoot(element, 'foo'); assert.isTrue(widgetStub.calledOnce); - assert.isTrue(elementStub.calledOnce); assert.isTrue(projectorStub.notCalled); }, 'add root without a key emits Widget event only'() { - nodeHandler.addRoot(element, {}); + nodeHandler.addRoot(element); assert.isTrue(widgetStub.calledOnce); assert.isTrue(elementStub.notCalled); diff --git a/tests/unit/WidgetBase.ts b/tests/unit/WidgetBase.ts index 23009429..8225e88e 100644 --- a/tests/unit/WidgetBase.ts +++ b/tests/unit/WidgetBase.ts @@ -1,18 +1,54 @@ -import * as registerSuite from 'intern!object'; +import { describe, it } from 'intern!bdd'; import * as assert from 'intern/chai!assert'; -import { stub, spy, SinonStub } from 'sinon'; -import { v, w } from '../../src/d'; -import { VNode } from '@dojo/interfaces/vdom'; -import { Constructor, DNode } from '../../src/interfaces'; -import { WidgetBase } from '../../src/WidgetBase'; -import Registry, { WIDGET_BASE_TYPE } from './../../src/Registry'; +import { spy } from 'sinon'; +import { WidgetBase } from './../../src/WidgetBase'; +import { v } from './../../src/d'; +import { WIDGET_BASE_TYPE } from './../../src/Registry'; +import { HNode, WidgetMetaConstructor, WidgetMetaBase } from './../../src/interfaces'; import { handleDecorator } from './../../src/decorators/handleDecorator'; -import createTestWidget from './../support/createTestWidget'; +import { diffProperty } from './../../src/decorators/diffProperty'; +import { Registry } from './../../src/Registry'; +import { Base } from './../../src/meta/Base'; +import { NodeEventType } from './../../src/NodeHandler'; + +interface TestProperties { + foo?: string; + bar?: boolean; + baz?: Function; + qux?: object; + quux?: any[]; + foobar?: number; +} + +class TestMeta extends Base { + public widgetEvent = false; + + constructor(options: any) { + super(options); + this.nodeHandler.on(NodeEventType.Widget, () => { + this.widgetEvent = true; + }); + } + + get(key: string | number) { + return this.getNode(key); + } +} -let consoleStub: SinonStub; +class BaseTestWidget extends WidgetBase { + public meta(metaType: WidgetMetaConstructor) { + return super.meta(metaType) as T; + } -const registry = new Registry(); + public render() { + return super.render(); + } + + callGetDecorator(decoratorKey: string) { + return this.getDecorator(decoratorKey); + } +} function testDecorator(func?: Function) { return handleDecorator((target, propertyKey) => { @@ -20,180 +56,146 @@ function testDecorator(func?: Function) { }); } -registerSuite({ - name: 'WidgetBase', - beforeEach() { - consoleStub = stub(console, 'warn'); - }, - afterEach() { - consoleStub.restore(); - }, - api() { - const widgetBase = new WidgetBase(); - assert(widgetBase); - assert.isFunction(widgetBase.__render__); - }, - children() { - const expectedChild = v('div'); - const widget = new WidgetBase(); - - assert.lengthOf(widget.children, 0); - widget.__setChildren__([expectedChild]); - assert.lengthOf(widget.children, 1); - assert.strictEqual(widget.children[0], expectedChild); - }, - 'invalidate': { - 'should emit event and mark as dirty when invalidating during idle'() { - let invalidateCalled = false; - let renderCount = 0; - class Foo extends WidgetBase { - render() { - renderCount++; - return v('div', [ 'hello world ' ]); - } +describe('WidgetBase', () => { - callInvalidate() { - this.invalidate(); - } - } + it('default render returns a `div` with the current widgets children', () => { + const widget = new BaseTestWidget(); + widget.__setChildren__([ 'child' ]); + const renderResult = widget.render() as HNode; + assert.strictEqual(renderResult.tag, 'div'); + assert.deepEqual(renderResult.properties, {}); + assert.lengthOf(renderResult.children, 1); + assert.strictEqual(renderResult.children![0], 'child'); + }); - const foo = new Foo(); - foo.on('invalidated', () => { - invalidateCalled = true; - }); - foo.__render__(); - assert.equal(renderCount, 1); - foo.callInvalidate(); - foo.__render__(); - assert.equal(renderCount, 2); - assert.isTrue(invalidateCalled); - }, - 'should not emit event but mark as dirty when invalidating during property diff'() { - let invalidateCalled = false; - let renderCount = 0; - class Foo extends WidgetBase<{ bar: string }> { - invalidate() { - super.invalidate(); - } + describe('__render__', () => { + + it('returns render result', () => { + class TestWidget extends BaseTestWidget { render() { - renderCount++; - return v('div', [ 'hello world ' ]); + return v('my-app', [ 'child' ]); } } + const widget = new TestWidget(); + const renderResult = widget.__render__() as HNode; + assert.strictEqual(renderResult.tag, 'my-app'); + assert.lengthOf(renderResult.children, 1); + assert.strictEqual(renderResult.children![0], 'child'); + }); - const foo = new Foo(); - foo.on('invalidated', () => { - invalidateCalled = true; - }); - foo.__render__(); - assert.equal(renderCount, 1); - foo.__setCoreProperties__({ bind: foo, baseRegistry: registry }); - foo.invalidate(); - foo.__setProperties__({ bar: '' }); - foo.__render__(); - assert.equal(renderCount, 2); - assert.isFalse(invalidateCalled); - }, - 'should not emit event or mark as dirty when invalidating during render'() { - let invalidateCalled = false; - let renderCount = 0; - class Foo extends WidgetBase { + it('returns cached DNode when widget is ', () => { + class TestWidget extends BaseTestWidget { render() { - this.invalidate(); - renderCount++; - return v('div', [ 'hello world ' ]); + return v('my-app', [ 'child' ]); } } + const widget = new TestWidget(); + const renderResult = widget.__render__(); + const secondRenderResult = widget.__render__(); + assert.strictEqual(secondRenderResult, renderResult); + widget.invalidate(); + const thirdRenderResult = widget.__render__(); + assert.notStrictEqual(thirdRenderResult, secondRenderResult); + }); - const foo = new Foo(); - foo.on('invalidated', () => { - invalidateCalled = true; + }); + + describe('__setProperties__', () => { + + it('diffs properties using `auto` strategy by default', () => { + const widget = new BaseTestWidget(); + const invalidateSpy = spy(widget, 'invalidate'); + + function baz() {} + const qux = { + foo: 'bar', + bar: 'foo' + }; + const quux = [ 1, 2, 3, 4 ]; + + widget.__setProperties__({ + foo: 'bar', + bar: true, + baz, + qux, + quux }); - foo.__render__(); - assert.equal(renderCount, 1); - foo.__render__(); - assert.equal(renderCount, 1); - assert.isFalse(invalidateCalled); - } - }, - 'Applies div as default tag'() { - const widget = new WidgetBase(); - const renderedWidget = ( widget).__render__(); - assert.deepEqual(renderedWidget.vnodeSelector, 'div'); - }, - setProperties: { - 'widgets function properties are bound to the parent by default'() { - class TestChildWidget extends WidgetBase { - render() { - this.properties.foo(); - return v('div'); - } - } - class TestWidget extends WidgetBase { - count: number; - constructor() { - super(); - this.count = 0; - } + assert.isTrue(invalidateSpy.calledOnce); + assert.deepEqual(widget.changedPropertyKeys, [ 'foo', 'bar', 'qux', 'quux' ]); - foo() { - this.count++; - } + widget.__setProperties__({ + foo: 'bar', + bar: true, + baz, + qux, + quux + }); - render(): DNode { - return w(TestChildWidget, { foo: this.foo, bar: Math.random() }); - } - } + assert.isTrue(invalidateSpy.calledOnce); + assert.deepEqual(widget.changedPropertyKeys, [ ]); - const testWidget: any = new TestWidget(); - testWidget.__render__(); - assert.strictEqual(testWidget.count, 1); - testWidget.invalidate(); - testWidget.__render__(); - assert.strictEqual(testWidget.count, 2); - }, - 'widget function properties do not get re-bound when nested'() { - class TestChildWidget extends WidgetBase { - render() { - this.properties.foo(); - return v('div'); - } - } + widget.__setProperties__({ + foo: 'bar', + bar: true, + baz, + quux + }); - class TestNestedWidget extends WidgetBase { - render(): DNode { - const { foo, bar } = this.properties; + assert.isTrue(invalidateSpy.calledTwice); + assert.deepEqual(widget.changedPropertyKeys, [ 'qux' ]); + }); - return w(TestChildWidget, { foo, bar }); - } + it('Supports custom diffProperty function', () => { + function customDiff(previousValue: any = 0, newValue: any) { + return { + changed: true, + value: previousValue + newValue + }; } + @diffProperty('foobar', customDiff) + class TestWidget extends BaseTestWidget {} + const widget = new TestWidget(); + const invalidateSpy = spy(widget, 'invalidate'); - class TestWidget extends WidgetBase { - count: number; + widget.__setProperties__({ foobar: 2 }); + assert.isTrue(invalidateSpy.calledOnce); + assert.deepEqual(widget.changedPropertyKeys, [ 'foobar' ]); + assert.strictEqual(widget.properties.foobar, 2); - foo(this: any) { - this.count++; - } + widget.__setProperties__({ foobar: 4 }); + assert.isTrue(invalidateSpy.calledTwice); + assert.deepEqual(widget.changedPropertyKeys, [ 'foobar' ]); + assert.strictEqual(widget.properties.foobar, 6); - constructor() { - super(); - this.count = 0; - } + widget.__setProperties__({ }); + assert.isTrue(invalidateSpy.calledThrice); + assert.deepEqual(widget.changedPropertyKeys, [ 'foobar' ]); + assert.isUndefined(widget.properties.foobar); + }); - render(): DNode { - return w(TestNestedWidget, { foo: this.foo, bar: Math.random() }); - } + it('Runs registered reactions when property is considered changed', () => { + + }); + + it('Automatically binds functions properties', () => { + class TestWidget extends BaseTestWidget { + public called = false; + } + + function baz(this: TestWidget) { + this.called = true; } - const testWidget: any = new TestWidget(); - testWidget.__render__(); - assert.strictEqual(testWidget.count, 1); - testWidget.invalidate(); - testWidget.__render__(); - assert.strictEqual(testWidget.count, 2); - }, - 'widget constructors are not bound'() { + const widget = new TestWidget(); + + widget.__setCoreProperties__({ bind: widget } as any); + widget.__setProperties__({ baz }); + widget.properties.baz && widget.properties.baz(); + assert.isTrue(widget.called); + }); + + it('Does not bind Widget constructor properties', () => { const widgetConstructorSpy: any = function(this: any) { this.functionIsBound = true; }; @@ -216,874 +218,185 @@ registerSuite({ testWidget.callWidgetSpy(); assert.isFalse(testWidget.functionIsBound); assert.isTrue(testWidget.properties.functionIsBound); - }, - 'functions are bound to the creating parent not the containing widget'() { - class Foo extends WidgetBase { - render() { - return v('div', {}, [ - this.properties.label, - ...this.children - ]); - } - } + }); + }); - class Bar extends WidgetBase { - render() { - return v('div', { onclick: this.properties.onClick }); - } - } - class App extends WidgetBase { + it('__setChildren__', () => { + const widget = new BaseTestWidget(); + const invalidateSpy = spy(widget, 'invalidate'); + widget.__setChildren__([]); + assert.isTrue(invalidateSpy.notCalled); + widget.__setChildren__([ 'child' ]); + assert.isTrue(invalidateSpy.calledOnce); + widget.__setChildren__([ 'child' ]); + assert.isTrue(invalidateSpy.calledTwice); + widget.__setChildren__([]); + assert.isTrue(invalidateSpy.calledThrice); + widget.__setChildren__([]); + assert.isTrue(invalidateSpy.calledThrice); + }); - public onClickCalled = false; + describe('__setCoreProperties__', () => { + it('new baseRegistry is added to RegistryHandler and triggers an invalidation', () => { + const baseRegistry = new Registry(); + baseRegistry.defineInjector('label', 'item' as any); + const widget = new BaseTestWidget(); + const invalidateSpy = spy(widget, 'invalidate'); + widget.__setCoreProperties__({ bind: widget, baseRegistry }); + assert.isTrue(invalidateSpy.calledOnce); + assert.strictEqual(widget.registry.getInjector('label'), 'item'); + }); + + it('The same baseRegistry does not causes an invalidation', () => { + const baseRegistry = new Registry(); + const widget = new BaseTestWidget(); + widget.__setCoreProperties__({ bind: widget, baseRegistry }); + const invalidateSpy = spy(widget, 'invalidate'); + widget.__setCoreProperties__({ bind: widget, baseRegistry }); + assert.isTrue(invalidateSpy.notCalled); + }); + + it('different baseRegistry replaces the RegistryHandlers baseRegistry and triggers an invalidation', () => { + const baseRegistry = new Registry(); + baseRegistry.defineInjector('label', 'item' as any); + const widget = new BaseTestWidget(); + widget.__setCoreProperties__({ bind: widget, baseRegistry: new Registry() }); + assert.isNull(widget.registry.getInjector('label')); + const invalidateSpy = spy(widget, 'invalidate'); + widget.__setCoreProperties__({ bind: widget, baseRegistry }); + assert.strictEqual(widget.registry.getInjector('label'), 'item'); + assert.isTrue(invalidateSpy.called); + }); + }); - _onClick() { - this.onClickCalled = true; - } + describe('meta', () => { + it('meta providers are cached', () => { + const widget = new BaseTestWidget(); + const meta = widget.meta(Base); + assert.strictEqual(meta, widget.meta(Base)); + }); + + it('elements are added to node handler on create', () => { + const element = {}; + const key = '1'; + const widget = new BaseTestWidget(); + const meta = widget.meta(TestMeta); + widget.emit({ type: 'element-created', element, key }); + assert.isTrue(meta.has(key)); + assert.strictEqual(meta.get(key), element); + }); + + it('elements are added to node handler on update', () => { + const element = {}; + const key = '1'; + const widget = new BaseTestWidget(); + const meta = widget.meta(TestMeta); + widget.emit({ type: 'element-updated', element, key }); + assert.isTrue(meta.has(key)); + assert.strictEqual(meta.get(key), element); + }); + + it('root added to node handler on widget create', () => { + const widget = new BaseTestWidget(); + const meta = widget.meta(TestMeta); + + widget.emit({ type: 'widget-created' }); + assert.isTrue(meta.widgetEvent); + }); + + it('root added to node handler on widget update', () => { + const widget = new BaseTestWidget(); + const meta = widget.meta(TestMeta); + + widget.emit({ type: 'widget-updated' }); + assert.isTrue(meta.widgetEvent); + }); + }); - render() { - return v('div', [ - w(Foo, { label: 'foo' }, [ - w(Bar, { - onClick: this._onClick - }) - ]) - ]); - } + describe('onElementCreated called on `element-created` event', () => { + class TestWidget extends BaseTestWidget { + onElementCreated(element: any, key: any) { + assert.strictEqual(element, 'element'); + assert.strictEqual(key, 'key'); } - - const widget = new App(); - const renderResult: any = widget.__render__(); - renderResult.children[0].children[1].properties.onclick(); - assert.isTrue(widget.onClickCalled); - } - }, - 'extendable'() { - let called = false; - - function PropertyLogger() { - return testDecorator(function(dNode: any) { - called = true; - return dNode; - }); } + const widget = new TestWidget(); + widget.emit({ type: 'element-created', key: 'key', element: 'element' }); + }); - @PropertyLogger() - class TestWidget extends WidgetBase { - public log() { - const testDecorators = this.getDecorator('test-decorator'); - testDecorators[0](); + describe('onElementUpdated called on `element-updated` event', () => { + class TestWidget extends BaseTestWidget { + onElementUpdated(element: any, key: any) { + assert.strictEqual(element, 'element'); + assert.strictEqual(key, 'key'); } } - - const widget = new TestWidget(); - widget.log(); - assert.strictEqual(called, true); - }, - changedPropertyKeys() { - class TestWidget extends WidgetBase {} const widget = new TestWidget(); - widget.__setProperties__({ foo: true }); - assert.deepEqual(widget.changedPropertyKeys, [ 'foo' ]); - widget.__setProperties__({ foo: true }); - assert.deepEqual(widget.changedPropertyKeys, []); - widget.__setProperties__({ foo: true }); - widget.__setProperties__({ foo: true, bar: true }); - assert.deepEqual(widget.changedPropertyKeys, [ 'bar' ]); - const changedPropertyKeys = widget.changedPropertyKeys; - changedPropertyKeys.push('bad key'); - assert.notDeepEqual(changedPropertyKeys, widget.changedPropertyKeys); - }, - 'decorate rendered DNodes': { - 'HNode': { - 'single root node'() { - class TestWidget extends WidgetBase { - render() { - return v('div'); - } - } - const widget = new TestWidget(); - const result = widget.__render__() as VNode; - assert.strictEqual(result.properties!.bind, widget); - assert.isFunction(result.properties!.afterCreate); - assert.isFunction(result.properties!.afterUpdate); - }, - 'single root node with key'() { - class TestWidget extends WidgetBase { - render() { - return v('div', { key: '1' }); - } - } - const widget = new TestWidget(); - const result = widget.__render__() as VNode; - assert.strictEqual(result.properties!.bind, widget); - assert.isFunction(result.properties!.afterCreate); - assert.isFunction(result.properties!.afterUpdate); - }, - 'single root node with children'() { - class TestWidget extends WidgetBase { - render() { - return v('div', { key: '1' }, [ - v('div'), - v('div', { key: '1' }) - ]); - } - } - const widget = new TestWidget(); - const result = widget.__render__() as VNode; - assert.strictEqual(result.properties!.bind, widget); - assert.isFunction(result.properties!.afterCreate); - assert.isFunction(result.properties!.afterUpdate); - - const childOne = result.children![0]; - assert.strictEqual(childOne.properties!.bind, widget); - assert.isUndefined(childOne.properties!.afterCreate); - assert.isUndefined(childOne.properties!.afterUpdate); - - const childTwo = result.children![1]; - assert.strictEqual(childTwo.properties!.bind, widget); - assert.isFunction(childTwo.properties!.afterCreate); - assert.isFunction(childTwo.properties!.afterUpdate); - }, - 'Array root with mixed keys'() { - class TestWidget extends WidgetBase { - render() { - return [ - v('div', { key: '1' }), - v('div'), - v('div', { key: '2' }) - ]; - - } - } - const widget = new TestWidget(); - const results = widget.__render__() as VNode[]; - for (let i = 0; i < results.length; i++) { - const result = results[i]; - assert.strictEqual(result.properties!.bind, widget); - assert.isFunction(result.properties!.afterCreate); - assert.isFunction(result.properties!.afterUpdate); - } - }, - 'Array root with children'() { - class TestWidget extends WidgetBase { - render() { - return [ - v('div', { key: '1' }, [ - v('div') - ]), - v('div', [ - v('div', { key: '1' }) - ]) - ]; - - } - } - const widget = new TestWidget(); - const results = widget.__render__() as VNode[]; - const resultOne = results[0]; - assert.strictEqual(resultOne.properties!.bind, widget); - assert.isFunction(resultOne.properties!.afterCreate); - assert.isFunction(resultOne.properties!.afterUpdate); - const resultOneChild = resultOne.children![0]; - assert.strictEqual(resultOneChild.properties!.bind, widget); - assert.isUndefined(resultOneChild.properties!.afterCreate); - assert.isUndefined(resultOneChild.properties!.afterUpdate); - const resultTwo = results[1]; - assert.strictEqual(resultTwo.properties!.bind, widget); - assert.isFunction(resultTwo.properties!.afterCreate); - assert.isFunction(resultTwo.properties!.afterUpdate); - const resultTwoChild = resultTwo.children![0]; - assert.strictEqual(resultTwoChild.properties!.bind, widget); - assert.isFunction(resultTwoChild.properties!.afterCreate); - assert.isFunction(resultTwoChild.properties!.afterUpdate); - } - }, - 'WNode'() { - class ChildWidget extends WidgetBase {} - const setCorePropertiesSpy = spy(ChildWidget.prototype, '__setCoreProperties__'); - class TestWidget extends WidgetBase { - render() { - return w(ChildWidget, {}); - } - } - const widget = new TestWidget(); - widget.__render__(); - assert.isTrue(setCorePropertiesSpy.calledOnce); - assert.deepEqual(setCorePropertiesSpy.firstCall.args[0], { - bind: widget, - baseRegistry: undefined - }); - } - }, - render: { - 'render with non widget children'() { - class TestWidget extends WidgetBase { - render() { - return v('div', [ - v('header') - ]); - } - } - - const widget: any = new TestWidget(); - const result = widget.__render__(); - assert.lengthOf(result.children, 1); - assert.strictEqual(result.children && result.children[0].vnodeSelector, 'header'); - }, - 'lazily defined widget in registry renders when ready'() { - class TestWidget extends WidgetBase { - render() { - return v('div', [ - w('my-header3', undefined) - ]); - } - } - - class TestHeaderWidget extends WidgetBase { - render() { - return v('header'); - } - } - const myWidget: any = createTestWidget(TestWidget, { registry }); - let result = myWidget.__render__(); - assert.lengthOf(result.children, 0); - registry.define('my-header3', TestHeaderWidget); - result = myWidget.__render__(); - assert.lengthOf(result.children, 1); - }, - 'lazily defined widget using a symbol in registry renders when ready'() { - const myHeader = Symbol(); - class TestWidget extends WidgetBase { - render() { - return v('div', [ - w(myHeader, undefined) - ]); - } - } - - class TestHeaderWidget extends WidgetBase { - render() { - return v('header'); - } - } - const myWidget: any = createTestWidget(TestWidget, { registry }); - let result = myWidget.__render__(); - assert.lengthOf(result.children, 0); - registry.define(myHeader, TestHeaderWidget); - result = myWidget.__render__(); - assert.lengthOf(result.children, 1); - }, - 'locally defined widget in registry eventually replaces global one'() { - class TestWidget extends WidgetBase { - private _defineItem = false; - constructor() { - super(); - } - render() { - if (this._defineItem) { - this.registry.define('my-header4', TestHeaderLocalWidget); - } - else { - this._defineItem = true; - } - return v('div', [ - w('my-header4', {}) - ]); - } - invalidate() { - super.invalidate(); - } - } - - class TestHeaderWidget extends WidgetBase { - render() { - return v('global-header'); - } - } - - class TestHeaderLocalWidget extends WidgetBase { - render() { - return v('local-header'); - } - } - registry.define('my-header4', TestHeaderWidget); - const myWidget = new TestWidget(); - myWidget.__setCoreProperties__({ bind: myWidget, baseRegistry: registry }); - let result = myWidget.__render__(); - assert.equal(result.children[0].vnodeSelector, 'global-header'); - myWidget.invalidate(); - result = myWidget.__render__(); - assert.equal(result.children[0].vnodeSelector, 'local-header'); - }, - 'async factories only initialise once'() { - let resolveFunction: any; - const loadFunction = () => { - return new Promise>((resolve) => { - resolveFunction = resolve; - }); - }; - registry.define('my-header', loadFunction); - - class TestWidget extends WidgetBase { - public invalidate() { - super.invalidate(); - } - render() { - return v('div', [ - w('my-header', undefined) - ]); - } - } - - class TestHeaderWidget extends WidgetBase { - render() { - return v('header'); - } - } - - let invalidateCount = 0; - - const myWidget = new TestWidget(); - myWidget.__setCoreProperties__({ bind: myWidget, baseRegistry: registry }); - myWidget.on('invalidated', () => { - invalidateCount++; - }); - - let result = myWidget.__render__(); - assert.lengthOf(result.children, 0); - - myWidget.invalidate(); - myWidget.__render__(); - myWidget.invalidate(); - myWidget.__render__(); - - resolveFunction(TestHeaderWidget); - - const promise = new Promise((resolve) => setTimeout(resolve, 100)); - return promise.then(() => { - assert.equal(invalidateCount, 3); - result = myWidget.__render__(); - assert.lengthOf(result.children, 1); - assert.strictEqual(result.children![0].vnodeSelector, 'header'); - }); - }, - 'render with async factory'() { - let resolveFunction: any; - const loadFunction = () => { - return new Promise>((resolve) => { - resolveFunction = resolve; - }); - }; - registry.define('my-header1', loadFunction); - - class TestWidget extends WidgetBase { - render() { - return v('div', [ - w('my-header1', {}) - ]); - } - } - - class TestHeaderWidget extends WidgetBase { - render() { - return v('header'); - } - } - - const myWidget = createTestWidget(TestWidget, { registry }); - - let result = myWidget.__render__(); - assert.lengthOf(result.children, 0); - - resolveFunction(TestHeaderWidget); - return new Promise((resolve) => { - myWidget.on('invalidated', () => { - result = myWidget.__render__(); - assert.lengthOf(result.children, 1); - assert.strictEqual(result.children![0].vnodeSelector, 'header'); - resolve(); - }); - }); - }, - 'render using scoped factory registry'() { - class TestHeaderWidget extends WidgetBase { - render() { - return v('header'); - } - } - - const registry = new Registry(); - registry.define('my-header', TestHeaderWidget); - - class TestWidget extends WidgetBase { - constructor() { - super(); - this.registry.define('my-header', TestHeaderWidget); - } - - render() { - return v('div', [ - w('my-header', undefined) - ]); - } - } - - const myWidget: any = new TestWidget(); - - let result = myWidget.__render__(); - assert.lengthOf(result.children, 1); - assert.strictEqual(result.children![0].vnodeSelector, 'header'); - }, - 'render with nested children'() { - class TestWidget extends WidgetBase { - render() { - return v('div', [ - v('header', [ - v('section') - ]) - ]); - } - } - - const widget: any = new TestWidget(); - const result = widget.__render__(); - assert.lengthOf(result.children, 1); - assert.strictEqual(result.children![0].vnodeSelector, 'header'); - assert.strictEqual(result.children![0].children![0].vnodeSelector, 'section'); - }, - 'render with a text node children'() { - class TestWidget extends WidgetBase { - render() { - return v('div', [ 'I am a text node' ]); - } - } - - const widget: any = new TestWidget(); - const result = widget.__render__(); - assert.isUndefined(result.children); - assert.equal(result.text, 'I am a text node'); - }, - 'render returns array'() { - class TestChildWidget extends WidgetBase { - render() { - return [ - v('div', [ 'text' ]), - v('span', { key: 'span' }) - ]; - } - } + widget.emit({ type: 'element-updated', key: 'key', element: 'element' }); + }); - class TestWidget extends WidgetBase { - render() { - return v('div', [ w(TestChildWidget, {}) ]); - } - } + describe('decorators', () => { + it('returns an empty array for decorators that do not exist', () => { + @testDecorator() + class TestWidget extends BaseTestWidget {} const widget = new TestWidget(); - const result: any = widget.__render__(); - assert.strictEqual(result.vnodeSelector, 'div'); - assert.lengthOf(result.children, 2); - assert.strictEqual(result.children![0].vnodeSelector, 'div'); - assert.strictEqual(result.children![0].text, 'text'); - assert.strictEqual(result.children![1].vnodeSelector, 'span'); - assert.strictEqual(result.children![1].properties.key, 'span'); - }, - 'instance gets passed to VNodeProperties as bind to widget and all children'() { - - class InnerWidget extends WidgetBase {} - class TestWidget extends WidgetBase { - render() { - return w(InnerWidget, {}, [ - v('header', [ - v('section') - ]) - ]); - } - } - - const widget: any = new TestWidget(); - const result = widget.__render__(); - assert.lengthOf(result.children, 1); - assert.notStrictEqual(result.properties!.bind, widget); - assert.instanceOf(result.properties!.bind, InnerWidget); - assert.strictEqual(result.children![0].properties!.bind, widget); - assert.instanceOf(result.children![0].properties!.bind, TestWidget); - assert.strictEqual(result.children![0].children![0].properties!.bind, widget); - assert.instanceOf(result.children![0].children![0].properties!.bind, TestWidget); - }, - 'render with multiple text node children'() { - class TestWidget extends WidgetBase { - render() { - return v('div', [ 'I am a text node', 'Second text node' ]); - } - } - - const widget: any = new TestWidget(); - const result = widget.__render__(); - assert.isUndefined(result.text); - assert.lengthOf(result.children, 2); - assert.strictEqual(result.children![0].text, 'I am a text node'); - assert.strictEqual(result.children![1].text, 'Second text node'); - }, - 'render with widget children'() { - let countWidgetCreated = 0; - let countWidgetDestroyed = 0; - - class TestChildWidget extends WidgetBase { - constructor() { - super(); - countWidgetCreated++; - } - - render() { - return v('footer', this.children); - } - - destroy() { - countWidgetDestroyed++; - return super.destroy(); - } - } - - class TestWidget extends WidgetBase { - render() { - const properties = this.properties.classes ? { classes: this.properties.classes } : {}; - - return v('div', [ - this.properties.hide ? null : w(TestChildWidget, properties), - this.properties.hide ? undefined : w(TestChildWidget, properties), - this.properties.hide ? null : w(TestChildWidget, properties), - this.properties.hide ? undefined : w(TestChildWidget, properties) - ]); - } - } - - const widget: any = new TestWidget(); - const firstRenderResult = widget.__render__(); - assert.strictEqual(countWidgetCreated, 4); - assert.strictEqual(countWidgetDestroyed, 0); - assert.lengthOf(firstRenderResult.children, 4); - const firstRenderChild: any = firstRenderResult.children && firstRenderResult.children[0]; - assert.strictEqual(firstRenderChild.vnodeSelector, 'footer'); - - widget.invalidate(); - - const secondRenderResult = widget.__render__(); - assert.strictEqual(countWidgetCreated, 4); - assert.strictEqual(countWidgetDestroyed, 0); - assert.lengthOf(secondRenderResult.children, 4); - const secondRenderChild: any = secondRenderResult.children && secondRenderResult.children[0]; - assert.strictEqual(secondRenderChild.vnodeSelector, 'footer'); - - widget.__setProperties__( { hide: true }); - widget.invalidate(); - - const thirdRenderResult = widget.__render__(); - assert.strictEqual(countWidgetCreated, 4); - assert.strictEqual(countWidgetDestroyed, 4); - assert.lengthOf(thirdRenderResult.children, 0); + const decorators = widget.callGetDecorator('unknown-decorator'); + assert.lengthOf(decorators, 0); + }); - widget.__setProperties__( { hide: false }); - widget.invalidate(); - - const lastRenderResult = widget.__render__(); - assert.strictEqual(countWidgetCreated, 8); - assert.strictEqual(countWidgetDestroyed, 4); - assert.lengthOf(lastRenderResult.children, 4); - const lastRenderChild: any = lastRenderResult.children && lastRenderResult.children[0]; - assert.strictEqual(lastRenderChild.vnodeSelector, 'footer'); - }, - 'render with multiple children of the same type without an id'() { - class TestWidgetOne extends WidgetBase {} - class TestWidgetTwo extends WidgetBase {} - const widgetName = ( TestWidgetTwo).name; - let warnMsg = 'It is recommended to provide a unique \'key\' property when using the same widget multiple times'; - - if (widgetName) { - warnMsg = `It is recommended to provide a unique 'key' property when using the same widget (${widgetName}) multiple times`; - } - - class TestWidget extends WidgetBase { - render() { - return v('div', [ - w(TestWidgetOne, {}), - w(TestWidgetTwo, {}), - w(TestWidgetTwo, {}) - ]); - } - } + it('decorators are cached', () => { + @testDecorator() + class TestWidget extends BaseTestWidget {} - const widget: any = new TestWidget(); - widget.__render__(); - assert.isTrue(consoleStub.calledOnce); - assert.isTrue(consoleStub.calledWith(warnMsg)); - widget.invalidate(); - widget.__render__(); - assert.isTrue(consoleStub.calledThrice); - assert.isTrue(consoleStub.calledWith(warnMsg)); - }, - '__render__ with updated array properties'() { - const properties = { - items: [ - 'a', 'b' - ] - }; - - const myWidget = new WidgetBase(); - myWidget.__setProperties__(properties); - assert.deepEqual(( myWidget.properties).items, [ 'a', 'b' ]); - properties.items.push('c'); - myWidget.__setProperties__(properties); - assert.deepEqual(( myWidget.properties).items , [ 'a', 'b', 'c' ]); - properties.items.push('d'); - myWidget.__setProperties__(properties); - assert.deepEqual(( myWidget.properties).items , [ 'a', 'b', 'c', 'd' ]); - }, - '__render__ with internally updated array state'() { - const properties = { - items: [ - 'a', 'b' - ] - }; - - const myWidget: any = new WidgetBase(); - myWidget.__setProperties__(properties); - myWidget.__render__(); - assert.deepEqual(( myWidget.properties).items, [ 'a', 'b' ]); - myWidget.__setProperties__( { items: [ 'a', 'b', 'c'] }); - myWidget.__render__(); - assert.deepEqual(( myWidget.properties).items , [ 'a', 'b', 'c' ]); - }, - '__render__() and invalidate()'() { - const widgetBase: any = new WidgetBase(); - widgetBase.__setProperties__({ id: 'foo', label: 'foo' }); - const result1 = widgetBase.__render__(); - const result2 = widgetBase.__render__(); - widgetBase.invalidate(); - const result3 = widgetBase.__render__(); - const result4 = widgetBase.__render__(); - assert.strictEqual(result1, result2); - assert.strictEqual(result3, result4); - assert.notStrictEqual(result1, result3); - assert.notStrictEqual(result2, result4); - assert.deepEqual(result1, result3); - assert.deepEqual(result2, result4); - assert.strictEqual(result1.vnodeSelector, 'div'); - }, - 'render multiple child widgets using the same factory'() { - let childWidgetInstantiatedCount = 0; - - class TestChildWidget extends WidgetBase { + const widget = new TestWidget(); + const decorators = widget.callGetDecorator('test-decorator'); + assert.lengthOf(decorators, 1); + const cachedDecorators = widget.callGetDecorator('test-decorator'); + assert.lengthOf(cachedDecorators, 1); + assert.strictEqual(cachedDecorators, decorators); + }); + + it('decorators applied to subclasses are not applied to base classes', () => { + @testDecorator() + class TestWidget extends BaseTestWidget {} + @testDecorator() + @testDecorator() + class TestWidget2 extends TestWidget {} + + const baseWidget = new TestWidget(); + const widget = new TestWidget2(); + + assert.equal(baseWidget.callGetDecorator('test-decorator').length, 1); + assert.equal(widget.callGetDecorator('test-decorator').length, 3); + }); + + it('decorator cache is populated when addDecorator is called after instantiation', () => { + class TestWidget extends BaseTestWidget { constructor() { super(); - childWidgetInstantiatedCount++; - } - } - - class TestWidget extends WidgetBase { - render() { - return v('div', [ - w(TestChildWidget, {}), - v('div', {}, [ - 'text', - w(TestChildWidget, {}, [ - w(TestChildWidget, {}) - ]), - v('div', {}, [ - w(TestChildWidget, {}) - ]) - ]), - w(TestChildWidget, {}) - ]); + this.addDecorator('test-decorator-one', function() {}); + this.addDecorator('test-decorator-two', function() {}); } } - const testWidget: any = new TestWidget(); - testWidget.__render__(); + const testWidget = new TestWidget(); - assert.equal(childWidgetInstantiatedCount, 5); - }, - 'support updating factories for children with an `key`'() { - let renderWidgetOne = true; - let widgetOneInstantiated = false; - let widgetTwoInstantiated = false; + assert.lengthOf(testWidget.callGetDecorator('test-decorator-one'), 1); + assert.lengthOf(testWidget.callGetDecorator('test-decorator-two'), 1); + }); - class WidgetOne extends WidgetBase { - constructor() { - super(); - widgetOneInstantiated = true; - } - } - class WidgetTwo extends WidgetBase { + it('addDecorator accepts an array of decorators', () => { + class TestWidget extends BaseTestWidget { constructor() { super(); - widgetTwoInstantiated = true; - } - } - class TestWidget extends WidgetBase { - render() { - return v('div', [ - renderWidgetOne ? w(WidgetOne, { key: '1' }) : w(WidgetTwo, { key: '1' }) - ]); + this.addDecorator('test-decorator', [ () => {}, () => {} ]); } } - const myWidget: any = new TestWidget(); - myWidget.__render__(); - assert.isTrue(widgetOneInstantiated); - renderWidgetOne = false; - myWidget.invalidate(); - myWidget.__render__(); - assert.isTrue(widgetTwoInstantiated); - } - }, - 'child invalidation invalidates parent'() { - let childInvalidate = () => {}; - let childInvalidateCalled = false; - let parentInvalidateCalled = false; - - class TestChildWidget extends WidgetBase { - constructor() { - super(); - childInvalidate = () => { - childInvalidateCalled = true; - this.invalidate(); - }; - } - } - - class Widget extends WidgetBase { - render(): any { - return v('div', [ - w(TestChildWidget, {}) - ]); - } - invalidate() { - super.invalidate(); - parentInvalidateCalled = true; - } - } - - const widget = new Widget(); - - ( widget).__render__(); - childInvalidate(); - assert.isTrue(childInvalidateCalled); - assert.isTrue(parentInvalidateCalled); - }, - 'setting children should mark the enclosing widget as dirty'() { - let foo = 0; - class FooWidget extends WidgetBase { - render() { - foo = this.properties.foo; - return v('div', []); - } - } - - class ContainerWidget extends WidgetBase { - render() { - return v('div', {}, this.children); - } - } - - class TestWidget extends WidgetBase { - private foo = 0; - - render() { - this.foo++; - return w(ContainerWidget, {}, [ - w(FooWidget, { foo: this.foo }) - ]); - } - } - - const widget: any = new TestWidget(); - widget.__render__(); - assert.equal(foo, 1); - widget.invalidate(); - widget.__render__(); - assert.equal(foo, 2); - }, - 'widget should not be marked as dirty if previous and current children are empty'() { - let foo = 0; - class FooWidget extends WidgetBase { - render() { - foo++; - return v('div'); - } - } - - class TestWidget extends WidgetBase { - render() { - return w(FooWidget, { key: '1' }); - } - } - - const widget: any = new TestWidget(); - widget.__render__(); - assert.equal(foo, 1); - widget.invalidate(); - widget.__render__(); - assert.equal(foo, 1); - }, - 'decorators are cached'() { - @testDecorator() - class TestWidget extends WidgetBase { - render() { - return v('div'); - } - - callGetDecorator(decoratorKey: string) { - return this.getDecorator(decoratorKey); - } - } - - const widget = new TestWidget(); - const decoratorSpy = spy(widget, '_buildDecoratorList'); - - widget.callGetDecorator('test-decorator'); - - // first call calls the method - assert.equal(decoratorSpy.callCount, 1); - widget.callGetDecorator('test-decorator'); - - // second call is cached - assert.equal(decoratorSpy.callCount, 1); - }, - 'decorators applied to subclasses are not applied to base classes'() { - @testDecorator() - class TestWidget extends WidgetBase { - getTestDecorators(): Function[] { - return this.getDecorator('test-decorator'); - } - } - - @testDecorator() - class TestWidget2 extends TestWidget { } - - const testWidget = new TestWidget(); - const testWidget2 = new TestWidget2(); - - assert.equal(testWidget.getTestDecorators().length, 1); - assert.equal(testWidget2.getTestDecorators().length, 2); - }, - 'decorator cache is populated when addDecorator is called after instantiation'() { - class TestWidget extends WidgetBase { - constructor() { - super(); - - this.addDecorator('beforeRender', function() {}); - this.addDecorator('afterRender', function() {}); - } - - getDecoratorCount(key: string): number { - return this.getDecorator(key).length; - } - } - - const testWidget = new TestWidget(); + const testWidget = new TestWidget(); - assert.equal(testWidget.getDecoratorCount('beforeRender'), 1, 'beforeRender = 0 (existing) + 1'); - assert.equal(testWidget.getDecoratorCount('afterRender'), 1, 'afterRender = 0 (existing) + 1'); - } + assert.lengthOf(testWidget.callGetDecorator('test-decorator'), 2); + }); + }); }); diff --git a/tests/unit/all.ts b/tests/unit/all.ts index b58b40d1..86877e0a 100644 --- a/tests/unit/all.ts +++ b/tests/unit/all.ts @@ -15,3 +15,4 @@ import './tsx'; import './tsxIntegration'; import './NodeHandler'; import './meta/all'; +import './vdom'; diff --git a/tests/unit/customElements.ts b/tests/unit/customElements.ts index e1e5d528..0a36884b 100644 --- a/tests/unit/customElements.ts +++ b/tests/unit/customElements.ts @@ -292,13 +292,7 @@ registerSuite({ parentNode: element } ]; - // so.. this is going to fail in maquette, since we don't have a DOM, but, - // it's ok because all of our code has already run by now - try { - initializeElement(element); - } - catch (e) { - } + initializeElement(element); assert.lengthOf(element.removedChildren(), 1); assert.lengthOf(element.getWidgetInstance().children, 1); diff --git a/tests/unit/d.ts b/tests/unit/d.ts index a1db8d3c..22aa7495 100644 --- a/tests/unit/d.ts +++ b/tests/unit/d.ts @@ -66,7 +66,7 @@ registerSuite({ assert.isTrue(isWNode(dNode)); assert.isFalse(isHNode(dNode)); }, - 'create WNode wrapper using constructor with VNode children'() { + 'create WNode wrapper using constructor with HNode children'() { const dNode = w(TestChildWidget, { myChildProperty: '' }, [ v('div') ]); assert.equal(dNode.type, WNODE); @@ -113,27 +113,14 @@ registerSuite({ v: { 'create HNode wrapper'() { const hNode = v('div'); - assert.isFunction(hNode.render); - assert.lengthOf(hNode.children, 0); + assert.isUndefined(hNode.children); assert.equal(hNode.tag, 'div'); assert.equal(hNode.type, HNODE); assert.isTrue(isHNode(hNode)); assert.isFalse(isWNode(hNode)); }, - 'create HNode wrapper with options'() { - const hNode = v('div', { innerHTML: 'Hello World' }); - assert.isFunction(hNode.render); - assert.lengthOf(hNode.children, 0); - const render = hNode.render(); - assert.equal(render.vnodeSelector, 'div'); - assert.equal(render.properties && render.properties.innerHTML, 'Hello World'); - assert.equal(hNode.type, HNODE); - assert.isTrue(isHNode(hNode)); - assert.isFalse(isWNode(hNode)); - }, 'create HNode wrapper with children'() { const hNode = v('div', {}, [ v('div'), v('div') ]); - assert.isFunction(hNode.render); assert.lengthOf(hNode.children, 2); assert.equal(hNode.type, HNODE); assert.isTrue(isHNode(hNode)); @@ -141,7 +128,6 @@ registerSuite({ }, 'create HNode wrapper with children as options param'() { const hNode = v('div', [ v('div'), v('div') ]); - assert.isFunction(hNode.render); assert.lengthOf(hNode.children, 2); assert.equal(hNode.type, HNODE); assert.isTrue(isHNode(hNode)); @@ -149,7 +135,6 @@ registerSuite({ }, 'create HNode wrapper with text node children'() { const hNode = v('div', {}, [ 'This Text Node', 'That Text Node' ]); - assert.isFunction(hNode.render); assert.lengthOf(hNode.children, 2); assert.equal(hNode.type, HNODE); assert.isTrue(isHNode(hNode)); diff --git a/tests/unit/decorators/beforeRender.ts b/tests/unit/decorators/beforeRender.ts index 502f734d..f1ceffae 100644 --- a/tests/unit/decorators/beforeRender.ts +++ b/tests/unit/decorators/beforeRender.ts @@ -58,30 +58,25 @@ registerSuite({ widget.__setChildren__([v('baz', { baz: 'qux' })]); widget.__setProperties__({ foo: 'bar' }); const qux: any = widget.__render__(); - assert.equal(qux.vnodeSelector, 'qux'); - assert.deepEqual(qux.properties.bind, widget); + assert.equal(qux.tag, 'qux'); assert.equal(qux.properties.bar, 'foo'); assert.equal(qux.properties.foo, 'bar'); assert.lengthOf(qux.children, 1); const bar = qux.children[0]; - assert.equal(bar.vnodeSelector, 'bar'); - assert.deepEqual(bar.properties.bind, widget); + assert.equal(bar.tag, 'bar'); assert.deepEqual(bar.properties.foo, 'bar'); assert.lengthOf(bar.children, 2); const foo = bar.children[0]; - assert.equal(foo.vnodeSelector, 'foo'); - assert.deepEqual(foo.properties.bind, widget); + assert.equal(foo.tag, 'foo'); assert.lengthOf(foo.children, 1); const baz1 = foo.children[0]; - assert.equal(baz1.vnodeSelector, 'baz'); - assert.deepEqual(baz1.properties.bind, widget); + assert.equal(baz1.tag, 'baz'); assert.deepEqual(baz1.properties.baz, 'qux'); - assert.lengthOf(baz1.children, 0); + assert.isUndefined(baz1.children); const baz2 = bar.children[1]; - assert.equal(baz2.vnodeSelector, 'baz'); - assert.deepEqual(baz2.properties.bind, widget); + assert.equal(baz2.tag, 'baz'); assert.deepEqual(baz2.properties.baz, 'qux'); - assert.lengthOf(baz2.children, 0); + assert.isUndefined(baz2.children); }, 'class level decorator'() { let beforeRenderCount = 0; @@ -109,8 +104,8 @@ registerSuite({ } const widget = new TestWidget(); - const vNode = widget.__render__(); - assert.strictEqual(vNode, 'first render'); + const renderResult = widget.__render__(); + assert.strictEqual(renderResult, 'first render'); assert.isTrue(consoleStub.calledOnce); assert.isTrue(consoleStub.calledWith('Render function not returned from beforeRender, using previous render')); } diff --git a/tests/unit/decorators/diffProperty.ts b/tests/unit/decorators/diffProperty.ts index f699c9a2..25070eb7 100644 --- a/tests/unit/decorators/diffProperty.ts +++ b/tests/unit/decorators/diffProperty.ts @@ -180,13 +180,13 @@ registerSuite({ class SubWidget extends TestWidget { } const widget = new SubWidget(); - const vnode = widget.__render__(); + const renderResult = widget.__render__(); widget.__setProperties__({ foo: 'bar' }); - assert.notStrictEqual(vnode, widget.__render__()); + assert.notStrictEqual(renderResult, widget.__render__()); }, 'multiple custom decorators on the same method cause the first matching decorator to win'() { const calls: string[] = []; @@ -236,13 +236,13 @@ registerSuite({ class TestWidget extends WidgetBase { } const widget = new TestWidget(); - const vnode = widget.__render__(); + const renderResult = widget.__render__(); widget.__setProperties__({ foo: '', id: '' }); - assert.strictEqual(vnode, widget.__render__()); + assert.strictEqual(renderResult, widget.__render__()); }, 'properties that are deleted dont get returned'() { diff --git a/tests/unit/decorators/inject.ts b/tests/unit/decorators/inject.ts index a063a07a..47746ce8 100644 --- a/tests/unit/decorators/inject.ts +++ b/tests/unit/decorators/inject.ts @@ -7,8 +7,6 @@ import { Registry } from './../../../src/Registry'; import { Injector } from './../../../src/Injector'; import { WidgetProperties } from './../../../src/interfaces'; -import createTestWidget from './../../support/createTestWidget'; - let injectorOne = new Injector({ foo: 'bar' }); let injectorTwo = new Injector({ bar: 'foo' }); let registry: Registry; @@ -29,9 +27,11 @@ registerSuite({ @inject({ name: 'inject-one', getProperties }) class TestWidget extends WidgetBase {} - const widget = createTestWidget(TestWidget, { registry }); + const widget = new TestWidget(); + widget.__setCoreProperties__({ bind: widget, baseRegistry: registry }); + widget.__setProperties__({}); - assert.strictEqual(widget.getWidgetUnderTest().properties.foo, 'bar'); + assert.strictEqual(widget.properties.foo, 'bar'); }, 'multiple injectors'() { function getPropertiesOne(payload: any, properties: WidgetProperties): WidgetProperties { @@ -44,9 +44,11 @@ registerSuite({ @inject({ name: 'inject-one', getProperties: getPropertiesOne }) @inject({ name: 'inject-two', getProperties: getPropertiesTwo }) class TestWidget extends WidgetBase {} - const widget = createTestWidget(TestWidget, { registry }); - assert.strictEqual(widget.getWidgetUnderTest().properties.foo, 'bar'); - assert.strictEqual(widget.getWidgetUnderTest().properties.bar, 'foo'); + const widget = new TestWidget(); + widget.__setCoreProperties__({ bind: widget, baseRegistry: registry }); + widget.__setProperties__({}); + assert.strictEqual(widget.properties.foo, 'bar'); + assert.strictEqual(widget.properties.bar, 'foo'); }, 'payload are only attached once'() { let invalidateCount = 0; @@ -56,9 +58,10 @@ registerSuite({ @inject({ name: 'inject-one', getProperties }) class TestWidget extends WidgetBase {} - const widget = createTestWidget(TestWidget, { registry }); - widget.getWidgetUnderTest().__setProperties__({}); - widget.getWidgetUnderTest().on('invalidated', () => { + const widget = new TestWidget(); + widget.__setCoreProperties__({ bind: widget, baseRegistry: registry }); + widget.__setProperties__({}); + widget.on('invalidated', () => { invalidateCount++; }); injectorOne.set({}); @@ -79,8 +82,10 @@ registerSuite({ inject({ name: 'inject-two', getProperties: getPropertiesTwo })(this); } } - const widget = createTestWidget(TestWidget, { registry }); - assert.strictEqual(widget.getWidgetUnderTest().properties.foo, 'bar'); - assert.strictEqual(widget.getWidgetUnderTest().properties.bar, 'foo'); + const widget = new TestWidget(); + widget.__setCoreProperties__({ bind: widget, baseRegistry: registry }); + widget.__setProperties__({}); + assert.strictEqual(widget.properties.foo, 'bar'); + assert.strictEqual(widget.properties.bar, 'foo'); } }); diff --git a/tests/unit/meta/Dimensions.ts b/tests/unit/meta/Dimensions.ts index 50175881..dcea434c 100644 --- a/tests/unit/meta/Dimensions.ts +++ b/tests/unit/meta/Dimensions.ts @@ -99,7 +99,7 @@ registerSuite({ const element = document.createElement('div'); const getRectSpy = spy(element, 'getBoundingClientRect'); - nodeHandler.add(element, { key: 'foo' }); + nodeHandler.add(element, 'foo'); resolveRAF(); assert.isTrue(invalidateStub.calledOnce); @@ -127,7 +127,7 @@ registerSuite({ }) }; - nodeHandler.add(element as any, { key: 'foo' }); + nodeHandler.add(element as any, 'foo'); const dimensions = new Dimensions({ invalidate: () => {}, diff --git a/tests/unit/meta/Drag.ts b/tests/unit/meta/Drag.ts index 8f696833..2351c173 100644 --- a/tests/unit/meta/Drag.ts +++ b/tests/unit/meta/Drag.ts @@ -1,22 +1,14 @@ -import global from '@dojo/shim/global'; import * as registerSuite from 'intern!object'; import * as assert from 'intern/chai!assert'; -import { stub, SinonStub } from 'sinon'; import sendEvent from '../../support/sendEvent'; +import { createResolvers } from './../../support/util'; import { v } from '../../../src/d'; import { ProjectorMixin } from '../../../src/main'; import Drag, { DragResults } from '../../../src/meta/Drag'; import { WidgetBase } from '../../../src/WidgetBase'; import { ThemeableMixin } from '../../../src/mixins/Themeable'; -let rAF: SinonStub; - -function resolveRAF() { - for (let i = 0; i < rAF.callCount; i++) { - rAF.getCall(i).args[0](); - } - rAF.reset(); -} +const resolvers = createResolvers(); const emptyResults: DragResults = { delta: { x: 0, y: 0 }, @@ -27,11 +19,11 @@ registerSuite({ name: 'support/meta/Drag', beforeEach() { - rAF = stub(global, 'requestAnimationFrame'); + resolvers.stub(); }, afterEach() { - rAF.restore(); + resolvers.restore(); }, 'standard rendering'() { @@ -54,7 +46,8 @@ registerSuite({ const widget = new TestWidget(); widget.append(div); - resolveRAF(); + resolvers.resolve(); + resolvers.resolve(); assert.deepEqual(dragResults, [ emptyResults, emptyResults ], 'should have been called twice, both empty results'); @@ -85,7 +78,8 @@ registerSuite({ const widget = new TestWidget(); widget.append(div); - resolveRAF(); + resolvers.resolve(); + resolvers.resolve(); assert.deepEqual(dragResults, [ emptyResults, emptyResults ], 'should have been called twice, both empty results'); @@ -117,7 +111,8 @@ registerSuite({ const widget = new TestWidget(); widget.append(div); - resolveRAF(); + resolvers.resolve(); + resolvers.resolve(); sendEvent(div.firstChild as Element, 'pointerdown', { eventInit: { @@ -135,7 +130,7 @@ registerSuite({ } }); - resolveRAF(); + resolvers.resolve(); sendEvent(div.firstChild as Element, 'pointermove', { eventInit: { @@ -151,7 +146,7 @@ registerSuite({ } }); - resolveRAF(); + resolvers.resolve(); sendEvent(div.firstChild as Element, 'pointerup', { eventInit: { @@ -167,7 +162,7 @@ registerSuite({ } }); - resolveRAF(); + resolvers.resolve(); assert.deepEqual(dragResults, [ emptyResults, @@ -215,7 +210,8 @@ registerSuite({ const widget = new TestWidget(); widget.append(div); - resolveRAF(); + resolvers.resolve(); + resolvers.resolve(); sendEvent(div.firstChild as Element, 'pointerdown', { eventInit: { @@ -233,7 +229,7 @@ registerSuite({ } }); - resolveRAF(); + resolvers.resolve(); sendEvent(div.firstChild as Element, 'pointermove', { eventInit: { @@ -277,7 +273,7 @@ registerSuite({ } }); - resolveRAF(); + resolvers.resolve(); sendEvent(div.firstChild as Element, 'pointerup', { eventInit: { @@ -293,7 +289,7 @@ registerSuite({ } }); - resolveRAF(); + resolvers.resolve(); assert.deepEqual(dragResults, [ emptyResults, @@ -341,7 +337,8 @@ registerSuite({ const widget = new TestWidget(); widget.append(div); - resolveRAF(); + resolvers.resolve(); + resolvers.resolve(); sendEvent(div.firstChild as Element, 'pointerdown', { eventInit: { @@ -359,7 +356,7 @@ registerSuite({ } }); - resolveRAF(); + resolvers.resolve(); sendEvent(div.firstChild as Element, 'pointermove', { eventInit: { @@ -417,7 +414,7 @@ registerSuite({ } }); - resolveRAF(); + resolvers.resolve(); assert.deepEqual(dragResults, [ emptyResults, @@ -461,7 +458,8 @@ registerSuite({ const widget = new TestWidget(); widget.append(div); - resolveRAF(); + resolvers.resolve(); + resolvers.resolve(); sendEvent(div.firstChild as Element, 'pointermove', { eventInit: { @@ -477,7 +475,7 @@ registerSuite({ } }); - resolveRAF(); + resolvers.resolve(); sendEvent(div.firstChild as Element, 'pointerup', { eventInit: { @@ -493,7 +491,7 @@ registerSuite({ } }); - resolveRAF(); + resolvers.resolve(); assert.deepEqual(dragResults, [ emptyResults, @@ -532,7 +530,8 @@ registerSuite({ const widget = new TestWidget(); widget.append(div); - resolveRAF(); + resolvers.resolve(); + resolvers.resolve(); sendEvent(div.firstChild!.firstChild as Element, 'pointerdown', { eventInit: { @@ -550,7 +549,7 @@ registerSuite({ } }); - resolveRAF(); + resolvers.resolve(); sendEvent(div.firstChild!.firstChild as Element, 'pointermove', { eventInit: { @@ -566,7 +565,7 @@ registerSuite({ } }); - resolveRAF(); + resolvers.resolve(); sendEvent(div.firstChild!.firstChild as Element, 'pointerup', { eventInit: { @@ -582,7 +581,7 @@ registerSuite({ } }); - resolveRAF(); + resolvers.resolve(); assert.deepEqual(dragResults, [ emptyResults, @@ -638,7 +637,8 @@ registerSuite({ const widget = new TestWidget(); widget.append(div); - resolveRAF(); + resolvers.resolve(); + resolvers.resolve(); sendEvent(div.firstChild!.firstChild as Element, 'pointerdown', { eventInit: { @@ -656,7 +656,7 @@ registerSuite({ } }); - resolveRAF(); + resolvers.resolve(); sendEvent(div.firstChild!.firstChild as Element, 'pointermove', { eventInit: { @@ -672,7 +672,7 @@ registerSuite({ } }); - resolveRAF(); + resolvers.resolve(); sendEvent(div.firstChild!.firstChild as Element, 'pointerup', { eventInit: { @@ -688,7 +688,7 @@ registerSuite({ } }); - resolveRAF(); + resolvers.resolve(); assert.deepEqual(dragResults, [ emptyResults, @@ -723,7 +723,8 @@ registerSuite({ const widget = new TestWidget(); widget.append(div); - resolveRAF(); + resolvers.resolve(); + resolvers.resolve(); sendEvent(div.firstChild as Element, 'pointerdown', { eventInit: { @@ -741,7 +742,7 @@ registerSuite({ } }); - resolveRAF(); + resolvers.resolve(); sendEvent(div.firstChild as Element, 'pointermove', { eventInit: { @@ -757,7 +758,7 @@ registerSuite({ } }); - resolveRAF(); + resolvers.resolve(); sendEvent(div.firstChild as Element, 'pointerup', { eventInit: { @@ -773,7 +774,7 @@ registerSuite({ } }); - resolveRAF(); + resolvers.resolve(); assert.deepEqual(dragResults, [ emptyResults, @@ -808,7 +809,8 @@ registerSuite({ const widget = new TestWidget(); widget.append(div); - resolveRAF(); + resolvers.resolve(); + resolvers.resolve(); sendEvent(div.firstChild as Element, 'pointerdown', { eventInit: { @@ -826,7 +828,7 @@ registerSuite({ } }); - resolveRAF(); + resolvers.resolve(); sendEvent(div.firstChild as Element, 'pointerdown', { eventInit: { @@ -844,7 +846,7 @@ registerSuite({ } }); - resolveRAF(); + resolvers.resolve(); sendEvent(div.firstChild as Element, 'pointermove', { eventInit: { @@ -860,7 +862,7 @@ registerSuite({ } }); - resolveRAF(); + resolvers.resolve(); sendEvent(div.firstChild as Element, 'pointerup', { eventInit: { @@ -876,7 +878,7 @@ registerSuite({ } }); - resolveRAF(); + resolvers.resolve(); assert.deepEqual(dragResults, [ emptyResults, @@ -919,7 +921,8 @@ registerSuite({ const widget = new TestWidget(); widget.append(div); - resolveRAF(); + resolvers.resolve(); + resolvers.resolve(); sendEvent(div.firstChild as Element, 'pointerdown', { eventInit: { @@ -937,7 +940,7 @@ registerSuite({ } }); - resolveRAF(); + resolvers.resolve(); sendEvent(div.firstChild as Element, 'pointermove', { eventInit: { @@ -953,11 +956,11 @@ registerSuite({ } }); - resolveRAF(); + resolvers.resolve(); widget.invalidate(); - resolveRAF(); + resolvers.resolve(); sendEvent(div.firstChild as Element, 'pointerup', { eventInit: { @@ -973,7 +976,7 @@ registerSuite({ } }); - resolveRAF(); + resolvers.resolve(); assert.deepEqual(dragResults, [ emptyResults, diff --git a/tests/unit/meta/Intersection.ts b/tests/unit/meta/Intersection.ts index 953de5d3..ac0d6469 100644 --- a/tests/unit/meta/Intersection.ts +++ b/tests/unit/meta/Intersection.ts @@ -57,7 +57,7 @@ registerSuite({ nodeHandler }); const element = document.createElement('div'); - nodeHandler.add(element, { key: 'root' }); + nodeHandler.add(element, 'root'); intersection.get('root'); const [ observer, callback ] = observers[0]; @@ -114,7 +114,7 @@ registerSuite({ assert.isTrue(onSpy.firstCall.calledWith('root')); const element = document.createElement('div'); - nodeHandler.add(element, { key: 'root' }); + nodeHandler.add(element, 'root'); assert.isTrue(invalidateStub.calledOnce); onSpy.reset(); intersection.get('root'); @@ -135,7 +135,7 @@ registerSuite({ assert.isTrue(onSpy.firstCall.calledWith('root')); const element = document.createElement('div'); - nodeHandler.add(element, { key: 'root' }); + nodeHandler.add(element, 'root'); assert.isTrue(invalidateStub.calledOnce); @@ -181,12 +181,12 @@ registerSuite({ assert.isTrue(onSpy.firstCall.calledWith('root')); const element = document.createElement('div'); - nodeHandler.add(element, { key: 'foo' }); + nodeHandler.add(element, 'foo'); assert.isTrue(invalidateStub.notCalled); const root = document.createElement('div'); - nodeHandler.add(root, { key: 'root' }); + nodeHandler.add(root, 'root'); assert.isTrue(invalidateStub.calledOnce); @@ -216,8 +216,8 @@ registerSuite({ }); const root = document.createElement('div'); - nodeHandler.add(root, { key: 'foo' }); - nodeHandler.add(root, { key: 'root' }); + nodeHandler.add(root, 'foo'); + nodeHandler.add(root, 'root'); intersection.get('foo'); assert.lengthOf(observers, 1); intersection.get('foo', { root: 'root'}); diff --git a/tests/unit/meta/Matches.ts b/tests/unit/meta/Matches.ts index 56182a48..28433564 100644 --- a/tests/unit/meta/Matches.ts +++ b/tests/unit/meta/Matches.ts @@ -1,9 +1,8 @@ import * as registerSuite from 'intern!object'; import * as assert from 'intern/chai!assert'; -import { stub, SinonStub } from 'sinon'; -import global from '@dojo/shim/global'; import sendEvent from '../../support/sendEvent'; +import { createResolvers } from './../../support/util'; import { v } from '../../../src/d'; import { ProjectorMixin } from '../../../src/main'; import { WidgetBase } from '../../../src/WidgetBase'; @@ -11,24 +10,17 @@ import { ThemeableMixin } from '../../../src/mixins/Themeable'; import Matches from '../../../src/meta/Matches'; -let rAF: SinonStub; - -function resolveRAF() { - for (let i = 0; i < rAF.callCount; i++) { - rAF.getCall(i).args[0](); - } - rAF.reset(); -} +const resolvers = createResolvers(); registerSuite({ name: 'support/meta/Matches', beforeEach() { - rAF = stub(global, 'requestAnimationFrame'); + resolvers.stub(); }, afterEach() { - rAF.restore(); + resolvers.restore(); }, 'node matches'() { @@ -55,7 +47,8 @@ registerSuite({ const widget = new TestWidget(); widget.append(div); - resolveRAF(); + resolvers.resolve(); + resolvers.resolve(); sendEvent(div.firstChild as Element, 'click'); @@ -89,7 +82,8 @@ registerSuite({ const widget = new TestWidget(); widget.append(div); - resolveRAF(); + resolvers.resolve(); + resolvers.resolve(); sendEvent(div.firstChild as Element, 'click'); @@ -127,7 +121,8 @@ registerSuite({ const widget = new TestWidget(); widget.append(div); - resolveRAF(); + resolvers.resolve(); + resolvers.resolve(); sendEvent(div.firstChild!.firstChild as Element, 'click', { eventInit: { @@ -173,7 +168,8 @@ registerSuite({ const widget = new TestWidget(); widget.append(div); - resolveRAF(); + resolvers.resolve(); + resolvers.resolve(); sendEvent(div.firstChild!.firstChild as Element, 'click', { eventInit: { @@ -181,7 +177,7 @@ registerSuite({ } }); - resolveRAF(); + resolvers.resolve(); sendEvent(div.firstChild!.firstChild as Element, 'click', { eventInit: { diff --git a/tests/unit/meta/meta.ts b/tests/unit/meta/meta.ts index 6ed9e1b7..b245c903 100644 --- a/tests/unit/meta/meta.ts +++ b/tests/unit/meta/meta.ts @@ -1,34 +1,27 @@ -import global from '@dojo/shim/global'; import * as registerSuite from 'intern!object'; import * as assert from 'intern/chai!assert'; import { Base as MetaBase } from '../../../src/meta/Base'; -import { stub, SinonStub, spy } from 'sinon'; +import { stub, spy } from 'sinon'; +import { createResolvers } from './../../support/util'; import NodeHandler, { NodeEventType } from '../../../src/NodeHandler'; import { v } from '../../../src/d'; import { ProjectorMixin } from '../../../src/main'; import { WidgetBase } from '../../../src/WidgetBase'; -let rAFStub: SinonStub; - -function resolveRAF() { - for (let i = 0; i < rAFStub.callCount; i++) { - rAFStub.getCall(i).args[0](); - } - rAFStub.reset(); -} +const resolvers = createResolvers(); registerSuite({ name: 'meta base', beforeEach() { - rAFStub = stub(global, 'requestAnimationFrame').returns(1); + resolvers.stub(); }, afterEach() { - rAFStub.restore(); + resolvers.restore(); }, 'has checks nodehandler for nodes'() { const nodeHandler = new NodeHandler(); const element = document.createElement('div'); - nodeHandler.add(element, { key: 'foo' }); + nodeHandler.add(element, 'foo'); const meta = new MetaBase({ invalidate: () => {}, nodeHandler @@ -41,7 +34,7 @@ registerSuite({ const nodeHandler = new NodeHandler(); const invalidate = stub(); const element = document.createElement('div'); - nodeHandler.add(element, { key: 'foo' }); + nodeHandler.add(element, 'foo'); class MyMeta extends MetaBase { callGetNode(key: string) { @@ -99,9 +92,9 @@ registerSuite({ const element = document.createElement('div'); - nodeHandler.add(element, { key: 'foo' }); + nodeHandler.add(element, 'foo'); - resolveRAF(); + resolvers.resolve(); assert.isTrue(invalidate.calledOnce); onSpy.reset(); @@ -149,7 +142,7 @@ registerSuite({ }); meta.callInvalidate(); - resolveRAF(); + resolvers.resolve(); assert.isTrue(invalidate.calledOnce); }, 'integration with single root node'() { @@ -181,24 +174,21 @@ registerSuite({ const onFoo = stub(); const onBar = stub(); const onWidget = stub(); - const onProjector = stub(); nodeHandler.on('foo', onFoo); nodeHandler.on('bar', onBar); nodeHandler.on(NodeEventType.Widget, onWidget); - nodeHandler.on(NodeEventType.Projector, onProjector); const div = document.createElement('div'); widget.append(div); + resolvers.resolve(); assert.isTrue(meta.has('foo'), '1'); assert.isTrue(meta.has('bar'), '2'); assert.isTrue(onFoo.calledOnce, '3'); assert.isTrue(onBar.calledOnce, '4'); assert.isTrue(onWidget.calledOnce, '5'); - assert.isTrue(onProjector.calledOnce, '6'); - assert.isTrue(onFoo.calledBefore(onWidget), '7'); - assert.isTrue(onFoo.calledBefore(onProjector), '8'); + assert.isTrue(onFoo.calledBefore(onWidget), '6'); }, 'integration with multiple root node'() { class MyMeta extends MetaBase { @@ -230,23 +220,20 @@ registerSuite({ const onFoo = stub(); const onBar = stub(); const onWidget = stub(); - const onProjector = stub(); nodeHandler.on('foo', onFoo); nodeHandler.on('bar', onBar); nodeHandler.on(NodeEventType.Widget, onWidget); - nodeHandler.on(NodeEventType.Projector, onProjector); const div = document.createElement('div'); widget.append(div); + resolvers.resolve(); assert.isTrue(meta.has('foo')); assert.isTrue(meta.has('bar')); assert.isTrue(onFoo.calledOnce); assert.isTrue(onBar.calledOnce); assert.isTrue(onWidget.calledOnce); - assert.isTrue(onProjector.calledOnce); assert.isTrue(onFoo.calledBefore(onWidget)); - assert.isTrue(onFoo.calledBefore(onProjector)); } }); diff --git a/tests/unit/mixins/I18n.ts b/tests/unit/mixins/I18n.ts index 88a2c8f3..af041005 100644 --- a/tests/unit/mixins/I18n.ts +++ b/tests/unit/mixins/I18n.ts @@ -1,5 +1,4 @@ import i18n, { invalidate, switchLocale, systemLocale } from '@dojo/i18n/i18n'; -import { VNode } from '@dojo/interfaces/vdom'; import * as registerSuite from 'intern!object'; import * as assert from 'intern/chai!assert'; import * as sinon from 'sinon'; @@ -115,16 +114,16 @@ registerSuite({ localized = new LocalizedExtended(); localized.__setProperties__({locale: 'ar-JO'}); - const result = localized.__render__(); + const result = localized.__render__(); assert.isOk(result); - assert.isNull(result.properties!['lang']); + assert.isUndefined(result.properties!['lang']); }, '`properties.locale` updates the widget node\'s `lang` property': { 'when non-empty'() { localized = new Localized(); localized.__setProperties__({locale: 'ar-JO'}); - const result = localized.__render__(); + const result = localized.__render__(); assert.isOk(result); assert.strictEqual(result.properties!['lang'], 'ar-JO'); }, diff --git a/tests/unit/mixins/Projector.ts b/tests/unit/mixins/Projector.ts index 907492a6..3a839066 100644 --- a/tests/unit/mixins/Projector.ts +++ b/tests/unit/mixins/Projector.ts @@ -1,6 +1,5 @@ import global from '@dojo/shim/global'; import has from '@dojo/has/has'; -import { VNode } from '@dojo/interfaces/vdom'; import * as registerSuite from 'intern!object'; import * as assert from 'intern/chai!assert'; import { spy, stub, SinonStub } from 'sinon'; @@ -9,6 +8,7 @@ import { ProjectorMixin, ProjectorAttachState } from '../../../src/mixins/Projec import { WidgetBase } from '../../../src/WidgetBase'; import { beforeRender } from './../../../src/decorators/beforeRender'; import { Registry } from './../../../src/Registry'; +import { HNode } from './../../../src/interfaces'; const Event = global.window.Event; @@ -57,196 +57,98 @@ registerSuite({ afterEach() { if (projector) { projector.destroy(); - projector = undefined; + projector = undefined as any; } rafStub.restore(); cancelRafStub.restore(); }, - 'attach to projector': { - 'append': { - 'standard'() { + 'render': { + 'string root node'() { + result = 'my string'; + projector = new MyWidget(); + + const renderedResult = projector.__render__() as HNode; + assert.strictEqual(renderedResult.tag, 'span'); + assert.strictEqual(renderedResult.children![0], 'my string'); + }, + 'string root node after an initial render'() { + result = v('h1', [ 'my string' ]); + projector = new MyWidget(); + + let renderedResult = projector.__render__() as HNode; + assert.strictEqual(renderedResult.tag, 'h1'); + assert.strictEqual(renderedResult.children![0], 'my string'); + + result = 'my string'; + renderedResult = projector.__render__() as HNode; + assert.strictEqual(renderedResult.tag, 'h1'); + assert.strictEqual(renderedResult.children![0], 'my string'); + }, + 'null root node'() { + result = null; + projector = new MyWidget(); + + const renderedResult = projector.__render__() as HNode; + assert.strictEqual(renderedResult.tag, 'span'); + assert.isNull(renderedResult.children![0]); + }, + 'null root node after an initial render'() { + result = v('h1', [ 'my string' ]); + projector = new MyWidget(); + + let renderedResult = projector.__render__() as HNode; + assert.strictEqual(renderedResult.tag, 'h1'); + assert.strictEqual(renderedResult.children![0], 'my string'); + projector.invalidate(); + result = null; + renderedResult = projector.__render__() as HNode; + assert.strictEqual(renderedResult.tag, 'h1'); + assert.isNull(renderedResult.children![0]); + }, + 'undefined root node'() { + result = undefined; + projector = new MyWidget(); + + const renderedResult = projector.__render__() as HNode; + assert.strictEqual(renderedResult.tag, 'span'); + assert.isUndefined(renderedResult.children![0]); + }, + 'undefined root node after an initial render'() { + result = v('h1', [ 'my string' ]); + projector = new MyWidget(); + + let renderedResult = projector.__render__() as HNode; + assert.strictEqual(renderedResult.tag, 'h1'); + assert.strictEqual(renderedResult.children![0], 'my string'); + projector.invalidate(); + result = undefined; + renderedResult = projector.__render__() as HNode; + assert.strictEqual(renderedResult.tag, 'h1'); + assert.isUndefined(renderedResult.children![0]); + }, + 'array root node'() { + result = [ v('h1', [ 'my string' ]) ]; + projector = new MyWidget(); + + const renderedResult = projector.__render__() as HNode; + assert.strictEqual(renderedResult.tag, 'span'); + assert.strictEqual(renderedResult.children, result); + } + }, + 'attach projector': { + 'append'() { const childNodeLength = document.body.childNodes.length; projector = new BaseTestWidget(); projector.setChildren([ v('h2', [ 'foo' ] ) ]); - projector.append(); - assert.strictEqual(document.body.childNodes.length, childNodeLength + 1, 'child should have been added'); - const child = document.body.lastChild; + const child = document.body.lastChild as HTMLElement; assert.strictEqual(child.innerHTML, '

foo

'); assert.strictEqual(child.tagName.toLowerCase(), 'div'); - assert.strictEqual(( child.firstChild).tagName.toLowerCase(), 'h2'); - }, - 'string root node'() { - result = 'my string'; - projector = new MyWidget(); - - projector.append(); - let vnode: VNode = projector.__render__() as VNode; - assert.equal(vnode.vnodeSelector, 'span'); - assert.equal(vnode.text, result); - assert.isUndefined(vnode.children); - - result = v('div', [ 'other text' ]); - projector.invalidate(); - vnode = projector.__render__() as VNode; - assert.equal(vnode.vnodeSelector, 'span'); - assert.isUndefined(vnode.text); - assert.lengthOf(vnode.children, 1); - assert.equal(vnode.children![0].vnodeSelector, 'div'); - assert.equal(vnode.children![0].text, 'other text'); - }, - 'string root node after an initial render'() { - result = v('div', [ 'my string' ]); - projector = new MyWidget(); - - projector.append(); - let vnode: VNode = projector.__render__() as VNode; - assert.equal(vnode.vnodeSelector, 'div'); - assert.equal(vnode.text, 'my string'); - assert.isUndefined(vnode.children); - - result = 'other text'; - projector.invalidate(); - vnode = projector.__render__() as VNode; - assert.equal(vnode.vnodeSelector, 'div'); - assert.equal(vnode.text, 'other text'); - assert.isUndefined(vnode.children); - }, - 'null root node'() { - result = null; - projector = new MyWidget(); - - projector.append(); - let vnode: VNode = projector.__render__() as VNode; - assert.equal(vnode.vnodeSelector, 'span'); - assert.isUndefined(vnode.text); - assert.lengthOf(vnode.children, 0); - - result = v('div', [ 'other text' ]); - projector.invalidate(); - vnode = projector.__render__() as VNode; - assert.equal(vnode.vnodeSelector, 'span'); - assert.isUndefined(vnode.text); - assert.lengthOf(vnode.children, 1); - assert.equal(vnode.children![0].vnodeSelector, 'div'); - assert.equal(vnode.children![0].text, 'other text'); - - }, - 'null root node after an initial render'() { - result = v('h2', [ 'my string' ]); - projector = new MyWidget(); - - projector.append(); - let vnode: VNode = projector.__render__() as VNode; - assert.equal(vnode.vnodeSelector, 'h2'); - assert.equal(vnode.text, 'my string'); - assert.isUndefined(vnode.children); - - result = null; - projector.invalidate(); - vnode = projector.__render__() as VNode; - assert.equal(vnode.vnodeSelector, 'h2'); - assert.isUndefined(vnode.text); - assert.lengthOf(vnode.children, 0); - }, - 'undefined root node'() { - result = undefined; - projector = new MyWidget(); - - projector.append(); - let vnode: VNode = projector.__render__() as VNode; - assert.equal(vnode.vnodeSelector, 'span'); - assert.isUndefined(vnode.text); - assert.lengthOf(vnode.children, 0); - - result = v('div', [ 'other text' ]); - projector.invalidate(); - vnode = projector.__render__() as VNode; - assert.equal(vnode.vnodeSelector, 'span'); - assert.isUndefined(vnode.text); - assert.lengthOf(vnode.children, 1); - assert.equal(vnode.children![0].vnodeSelector, 'div'); - assert.equal(vnode.children![0].text, 'other text'); - - }, - 'undefined root node after an initial render'() { - result = v('h2', [ 'my string' ]); - projector = new MyWidget(); - - projector.append(); - let vnode: VNode = projector.__render__() as VNode; - assert.equal(vnode.vnodeSelector, 'h2'); - assert.equal(vnode.text, 'my string'); - assert.isUndefined(vnode.children); - - result = undefined; - projector.invalidate(); - vnode = projector.__render__() as VNode; - assert.equal(vnode.vnodeSelector, 'h2'); - assert.isUndefined(vnode.text); - assert.lengthOf(vnode.children, 0); - }, - 'array root node'() { - result = [ v('h2', [ 'my string' ]) ]; - projector = new MyWidget(); - - projector.append(); - let vnode: any = projector.__render__() as VNode; - assert.equal(vnode.vnodeSelector, 'span'); - assert.lengthOf(vnode.children, 1); - assert.strictEqual(vnode.children[0].vnodeSelector, 'h2'); - assert.strictEqual(vnode.children[0].text, 'my string'); - } + assert.strictEqual((child.firstChild as HTMLElement).tagName.toLowerCase(), 'h2'); }, - 'sandbox': { - 'attaching'() { - const childNodeLength = document.body.childNodes.length; - projector = new BaseTestWidget(); - projector.setChildren([ v('h2', [ 'foo' ]) ]); - - projector.sandbox(); - - assert.strictEqual(document.body.childNodes.length, childNodeLength, 'No nodes should be added to body'); - assert.instanceOf(projector.root, global.window.DocumentFragment, 'the root should be a document fragment'); - const child = projector.root.firstChild as HTMLElement; - assert.strictEqual(child.innerHTML, '

foo

'); - assert.strictEqual(child.tagName.toLocaleLowerCase(), 'div'); - assert.strictEqual((child.firstChild as HTMLElement).tagName.toLocaleLowerCase(), 'h2'); - - projector.destroy(); - assert.strictEqual(projector.root, document.body, 'Root should be reverted to document.body'); - }, - 'operates synchronously'() { - let count = 0; - const projector = new class extends BaseTestWidget { - render () { - count++; - return v('div', [ String(count) ]); - } - }(); - - projector.sandbox(); - assert.strictEqual(count, 1, 'render should have been called once'); - assert.strictEqual(projector.root.firstChild!.textContent, '1', 'should have rendered "1"'); - projector.invalidate(); - assert.strictEqual(count, 2, 'render should have been called synchronously'); - assert.strictEqual(projector.root.firstChild!.textContent, '2', 'should have rendered "2"'); - projector.destroy(); - }, - 'accepts other documents'() { - const doc = { - createDocumentFragment: spy(() => document.createDocumentFragment()) - } as any; - const projector = new BaseTestWidget(); - projector.sandbox(doc); - assert.isTrue(doc.createDocumentFragment.called, 'createDocumentFragment should have been called'); - projector.destroy(); - } - }, - 'replace': { - 'standard'() { + 'replace'() { const projector = new class extends BaseTestWidget { render() { return v('body', this.children); @@ -254,149 +156,11 @@ registerSuite({ }(); projector.setChildren([ v('h2', [ 'foo' ] ) ]); - projector.replace(); - assert.strictEqual(document.body.childNodes.length, 1, 'child should have been added'); - const child = document.body.lastChild; + const child = document.body.lastChild as HTMLElement; assert.strictEqual(child.innerHTML, 'foo'); assert.strictEqual(child.tagName.toLowerCase(), 'h2'); - }, - 'string root node'() { - const root = document.createElement('my-app'); - document.body.appendChild(root); - result = 'my string'; - projector = new MyWidget(); - - projector.replace(root); - let vnode: VNode = projector.__render__() as VNode; - assert.equal(vnode.vnodeSelector, 'span'); - assert.equal(vnode.text, result); - assert.isUndefined(vnode.children); - - result = v('div', [ 'other text' ]); - projector.invalidate(); - vnode = projector.__render__() as VNode; - assert.equal(vnode.vnodeSelector, 'span'); - assert.isUndefined(vnode.text); - assert.lengthOf(vnode.children, 1); - assert.equal(vnode.children![0].vnodeSelector, 'div'); - assert.equal(vnode.children![0].text, 'other text'); - }, - 'string root node after an initial render'() { - const root = document.createElement('my-app'); - document.body.appendChild(root); - result = v('div', [ 'my string' ]); - projector = new MyWidget(); - - projector.replace(root); - let vnode: VNode = projector.__render__() as VNode; - assert.equal(vnode.vnodeSelector, 'div'); - assert.equal(vnode.text, 'my string'); - assert.isUndefined(vnode.children); - - result = 'other text'; - projector.invalidate(); - vnode = projector.__render__() as VNode; - assert.equal(vnode.vnodeSelector, 'div'); - assert.equal(vnode.text, 'other text'); - assert.isUndefined(vnode.children); - }, - 'null root node'() { - const root = document.createElement('my-app'); - document.body.appendChild(root); - result = null; - projector = new MyWidget(); - - projector.replace(root); - let vnode: VNode = projector.__render__() as VNode; - assert.equal(vnode.vnodeSelector, 'span'); - assert.isUndefined(vnode.text); - assert.lengthOf(vnode.children, 0); - - result = v('div', [ 'other text' ]); - projector.invalidate(); - vnode = projector.__render__() as VNode; - assert.equal(vnode.vnodeSelector, 'span'); - assert.isUndefined(vnode.text); - assert.lengthOf(vnode.children, 1); - assert.equal(vnode.children![0].vnodeSelector, 'div'); - assert.equal(vnode.children![0].text, 'other text'); - - }, - 'null root node after an initial render'() { - const root = document.createElement('my-app'); - document.body.appendChild(root); - result = v('h2', [ 'my string' ]); - projector = new MyWidget(); - - projector.replace(root); - let vnode: VNode = projector.__render__() as VNode; - assert.equal(vnode.vnodeSelector, 'h2'); - assert.equal(vnode.text, 'my string'); - assert.isUndefined(vnode.children); - - result = null; - projector.invalidate(); - vnode = projector.__render__() as VNode; - assert.equal(vnode.vnodeSelector, 'h2'); - assert.isUndefined(vnode.text); - assert.lengthOf(vnode.children, 0); - }, - 'undefined root node'() { - const root = document.createElement('my-app'); - document.body.appendChild(root); - result = undefined; - projector = new MyWidget(); - - projector.replace(root); - let vnode: VNode = projector.__render__() as VNode; - assert.equal(vnode.vnodeSelector, 'span'); - assert.isUndefined(vnode.text); - assert.lengthOf(vnode.children, 0); - - result = v('div', [ 'other text' ]); - projector.invalidate(); - vnode = projector.__render__() as VNode; - assert.equal(vnode.vnodeSelector, 'span'); - assert.isUndefined(vnode.text); - assert.lengthOf(vnode.children, 1); - assert.equal(vnode.children![0].vnodeSelector, 'div'); - assert.equal(vnode.children![0].text, 'other text'); - - }, - 'undefined root node after an initial render'() { - const root = document.createElement('my-app'); - document.body.appendChild(root); - result = v('h2', [ 'my string' ]); - projector = new MyWidget(); - - projector.replace(root); - let vnode: VNode = projector.__render__() as VNode; - assert.equal(vnode.vnodeSelector, 'h2'); - assert.equal(vnode.text, 'my string'); - assert.isUndefined(vnode.children); - - result = undefined; - projector.invalidate(); - vnode = projector.__render__() as VNode; - assert.equal(vnode.vnodeSelector, 'h2'); - assert.isUndefined(vnode.text); - assert.lengthOf(vnode.children, 0); - }, - 'array root node'() { - const root = document.createElement('my-app'); - document.body.appendChild(root); - result = [ v('h2', [ 'my string' ]) ]; - projector = new MyWidget(); - - projector.replace(root); - let vnode: any = projector.__render__() as VNode; - assert.equal(vnode.vnodeSelector, 'span'); - assert.lengthOf(vnode.children, 1); - assert.strictEqual(vnode.children[0].vnodeSelector, 'h2'); - assert.strictEqual(vnode.children[0].text, 'my string'); - } }, 'merge': { 'standard'() { @@ -405,220 +169,60 @@ registerSuite({ const projector = new BaseTestWidget(); projector.setChildren([ v('h2', [ 'foo' ] ) ]); - projector.merge(div); - assert.strictEqual(div.childNodes.length, 1, 'child should have been added'); - const child = div.lastChild; + const child = div.lastChild as HTMLElement; assert.strictEqual(child.innerHTML, 'foo'); assert.strictEqual(child.tagName.toLowerCase(), 'h2'); document.body.removeChild(div); - }, - 'string root node'() { - const root = document.createElement('my-app'); - document.body.appendChild(root); - result = 'my string'; - projector = new MyWidget(); - - projector.merge(root); - let vnode: VNode = projector.__render__() as VNode; - assert.equal(vnode.vnodeSelector, 'my-app'); - assert.equal(vnode.text, result); - assert.isUndefined(vnode.children); - - result = v('div', [ 'other text' ]); - projector.invalidate(); - vnode = projector.__render__() as VNode; - assert.equal(vnode.vnodeSelector, 'my-app'); - assert.equal(vnode.text, 'other text'); - }, - 'string root node after an initial render'() { - const root = document.createElement('my-app'); - document.body.appendChild(root); - result = v('div', [ 'my string' ]); - projector = new MyWidget(); - - projector.merge(root); - let vnode: VNode = projector.__render__() as VNode; - assert.equal(vnode.vnodeSelector, 'my-app'); - assert.equal(vnode.text, 'my string'); - assert.isUndefined(vnode.children); - - result = 'other text'; - projector.invalidate(); - vnode = projector.__render__() as VNode; - assert.equal(vnode.vnodeSelector, 'my-app'); - assert.equal(vnode.text, 'other text'); - assert.isUndefined(vnode.children); - }, - 'null root node'() { - const root = document.createElement('my-app'); - document.body.appendChild(root); - result = null; - projector = new MyWidget(); - - projector.merge(root); - let vnode: VNode = projector.__render__() as VNode; - assert.equal(vnode.vnodeSelector, 'my-app'); - assert.isUndefined(vnode.text); - assert.lengthOf(vnode.children, 0); - - result = v('div', [ 'other text' ]); - projector.invalidate(); - vnode = projector.__render__() as VNode; - assert.equal(vnode.vnodeSelector, 'my-app'); - assert.equal(vnode.text, 'other text'); - assert.isUndefined(vnode.children); - }, - 'null root node after an initial render'() { - const root = document.createElement('my-app'); - document.body.appendChild(root); - result = v('h2', [ 'my string' ]); - projector = new MyWidget(); - - projector.merge(root); - let vnode: VNode = projector.__render__() as VNode; - assert.equal(vnode.vnodeSelector, 'my-app'); - assert.equal(vnode.text, 'my string'); - assert.isUndefined(vnode.children); - - result = null; - projector.invalidate(); - vnode = projector.__render__() as VNode; - assert.equal(vnode.vnodeSelector, 'my-app'); - assert.isUndefined(vnode.text); - assert.lengthOf(vnode.children, 0); - }, - 'undefined root node'() { - const root = document.createElement('my-app'); - document.body.appendChild(root); - result = undefined; - projector = new MyWidget(); - - projector.merge(root); - let vnode: VNode = projector.__render__() as VNode; - assert.equal(vnode.vnodeSelector, 'my-app'); - assert.isUndefined(vnode.text); - assert.lengthOf(vnode.children, 0); - - result = v('div', [ 'other text' ]); - projector.invalidate(); - vnode = projector.__render__() as VNode; - assert.equal(vnode.vnodeSelector, 'my-app'); - assert.equal(vnode.text, 'other text'); - assert.isUndefined(vnode.children); - }, - 'undefined root node after an initial render'() { - const root = document.createElement('my-app'); - document.body.appendChild(root); - result = v('h2', [ 'my string' ]); - projector = new MyWidget(); - - projector.merge(root); - let vnode: VNode = projector.__render__() as VNode; - assert.equal(vnode.vnodeSelector, 'my-app'); - assert.equal(vnode.text, 'my string'); - assert.isUndefined(vnode.children); - - result = undefined; - projector.invalidate(); - vnode = projector.__render__() as VNode; - assert.equal(vnode.vnodeSelector, 'my-app'); - assert.isUndefined(vnode.text); - assert.lengthOf(vnode.children, 0); - }, - 'array root node'() { - const root = document.createElement('my-app'); - document.body.appendChild(root); - result = [ v('h2', [ 'my string' ]) ]; - projector = new MyWidget(); - - projector.merge(root); - let vnode: any = projector.__render__() as VNode; - assert.equal(vnode.vnodeSelector, 'my-app'); - assert.lengthOf(vnode.children, 1); - assert.strictEqual(vnode.children[0].vnodeSelector, 'h2'); - assert.strictEqual(vnode.children[0].text, 'my string'); - }, - 'pre rendered DOM used'() { - const iframe = document.createElement('iframe'); - document.body.appendChild(iframe); - iframe.contentDocument.write(` -
- - - -
`); - iframe.contentDocument.close(); - const root = iframe.contentDocument.body.firstChild as HTMLElement; - const childElementCount = root.childElementCount; - const select = root.childNodes[3] as HTMLSelectElement; - const button = root.childNodes[5] as HTMLButtonElement; - assert.strictEqual((root.childNodes[3] as HTMLSelectElement).value, 'bar', 'bar should be selected'); - const onchangeListener = spy(); - const onclickListener = spy(); - const projector = new class extends BaseTestWidget { - render() { - return v('div', { - classes: { foo: true, bar: true } - }, [ - v('label', { - for: 'baz' - }, [ 'Select Me:' ]), - v('select', { - type: 'text', - name: 'baz', - id: 'baz', - disabled: false, - onchange: onchangeListener - }, [ - v('option', { value: 'foo', selected: true }, [ 'label foo' ]), - v('option', { value: 'bar', selected: false }, [ 'label bar' ]), - v('option', { value: 'baz', selected: false }, [ 'label baz' ]) - ]), - v('button', { - type: 'button', - disabled: false, - onclick: onclickListener - }, [ 'Click Me!' ]) - ]); - } - }(); - projector.merge(root); - assert.strictEqual(root.className, 'foo bar', 'should have added bar class'); - assert.strictEqual(root.childElementCount, childElementCount, 'should have the same number of children'); - assert.strictEqual(select, root.childNodes[3], 'should have been reused'); - assert.strictEqual(button, root.childNodes[5], 'should have been reused'); - assert.isFalse(select.disabled, 'select should be enabled'); - assert.isFalse(button.disabled, 'button shound be enabled'); - - assert.strictEqual(select.value, 'foo', 'foo should be selected'); - assert.strictEqual(select.children.length, 3, 'should have 3 children'); - - assert.isFalse(onchangeListener.called, 'onchangeListener should not have been called'); - assert.isFalse(onclickListener.called, 'onclickListener should not have been called'); - - const changeEvent = document.createEvent('Event'); - changeEvent.initEvent('change', true, true); - select.onchange(changeEvent); // firefox doesn't like to dispatch this event, either due to trust issues or - // that firefox doesn't generally dispatch this event until the element is blurred - // which is different than other browsers. Either way this is not material to testing - // the functionality of this test, so calling the listener directly. - assert.isTrue(onchangeListener.called, 'onchangeListener should have been called'); - - const clickEvent = document.createEvent('CustomEvent'); - clickEvent.initEvent('click', true, true); - button.dispatchEvent(clickEvent); - assert.isTrue(onclickListener.called, 'onclickListener should have been called'); - - document.body.removeChild(iframe); } } }, + 'sandbox': { + 'attaching'() { + const childNodeLength = document.body.childNodes.length; + projector = new BaseTestWidget(); + projector.setChildren([ v('h2', [ 'foo' ]) ]); + + projector.sandbox(); + + assert.strictEqual(document.body.childNodes.length, childNodeLength, 'No nodes should be added to body'); + assert.instanceOf(projector.root, global.window.DocumentFragment, 'the root should be a document fragment'); + const child = projector.root.firstChild as HTMLElement; + assert.strictEqual(child.innerHTML, '

foo

'); + assert.strictEqual(child.tagName.toLocaleLowerCase(), 'div'); + assert.strictEqual((child.firstChild as HTMLElement).tagName.toLocaleLowerCase(), 'h2'); + + projector.destroy(); + assert.strictEqual(projector.root, document.body, 'Root should be reverted to document.body'); + }, + 'operates synchronously'() { + let count = 0; + const projector = new class extends BaseTestWidget { + render () { + count++; + return v('div', [ String(count) ]); + } + }(); + + projector.sandbox(); + assert.strictEqual(count, 1, 'render should have been called once'); + assert.strictEqual(projector.root.firstChild!.textContent, '1', 'should have rendered "1"'); + projector.invalidate(); + assert.strictEqual(count, 2, 'render should have been called synchronously'); + assert.strictEqual(projector.root.firstChild!.textContent, '2', 'should have rendered "2"'); + projector.destroy(); + }, + 'accepts other documents'() { + const doc = { + createDocumentFragment: spy(() => document.createDocumentFragment()) + } as any; + const projector = new BaseTestWidget(); + projector.sandbox(doc); + assert.isTrue(doc.createDocumentFragment.called, 'createDocumentFragment should have been called'); + projector.destroy(); + } + }, 'get root'() { const projector = new BaseTestWidget(); const root = document.createElement('div'); @@ -647,9 +251,9 @@ registerSuite({ 'resume'() { const projector = new BaseTestWidget(); spy(projector, 'scheduleRender'); - assert.isFalse(( projector.scheduleRender).called); + assert.isFalse((projector.scheduleRender as any).called); projector.resume(); - assert.isTrue(( projector.scheduleRender).called); + assert.isTrue((projector.scheduleRender as any).called); }, 'get projector state'() { const projector = new BaseTestWidget(); @@ -718,16 +322,16 @@ registerSuite({ }, 'destroy'() { const projector = new BaseTestWidget(); - const maquetteProjectorStopSpy = spy(projector, 'pause'); + const projectorStopSpy = spy(projector, 'pause'); projector.append(); projector.destroy(); - assert.isTrue(maquetteProjectorStopSpy.calledOnce); + assert.isTrue(projectorStopSpy.calledOnce); projector.destroy(); - assert.isTrue(maquetteProjectorStopSpy.calledOnce); + assert.isTrue(projectorStopSpy.calledOnce); }, 'setProperties guards against original property interface'() { interface Props { @@ -761,15 +365,6 @@ registerSuite({ projector.destroy(); assert.strictEqual(registryDestroyedCount, 3); }, - 'scheduleRender on called on invalidate when projector is dirty'() { - const projector = new BaseTestWidget(); - const scheduleRender = spy(projector, 'scheduleRender'); - projector.append(); - projector.setProperties({ key: 'hello' }); - assert.isTrue(scheduleRender.calledOnce); - projector.setProperties({ key: 'hello' }); - assert.isTrue(scheduleRender.calledOnce); - }, 'properties are reset to original state on render'() { const testProperties = { key: 'bar' @@ -789,22 +384,8 @@ registerSuite({ const projector = new TestWidget(); projector.setProperties(testProperties); projector.setChildren(testChildren); - projector.__render__(); + projector.scheduleRender(); projector.invalidate(); - projector.__render__(); - }, - 'invalidate on setting children'() { - const projector = new BaseTestWidget(); - const scheduleRender = spy(projector, 'scheduleRender'); - projector.append(); - projector.setChildren([]); - assert.isTrue(scheduleRender.notCalled); - projector.setChildren([ v('div') ]); - assert.isTrue(scheduleRender.calledOnce); - projector.setChildren([]); - assert.isTrue(scheduleRender.calledTwice); - projector.setChildren([]); - assert.isTrue(scheduleRender.calledTwice); }, 'invalidate before attached'() { const projector: any = new BaseTestWidget(); @@ -878,7 +459,7 @@ registerSuite({ dispatchEvent(domNode as HTMLElement, 'pointermove'); assert.instanceOf(domEvent, Event); }, - async '-active gets appended to enter/exit animations by default'(this: any) { + '-active gets appended to enter/exit animations by default'(this: any) { if (!has('host-browser')) { this.skip('This test can only be run in a browser'); } @@ -889,7 +470,7 @@ registerSuite({ root = document.body; render() { - return v('div', {}, children); + return v('div', { id: 'root' }, children); } } @@ -897,11 +478,11 @@ registerSuite({ projector.async = false; projector.append(); - children.push(v('div', { + children = [ v('div', { id: 'test-element', enterAnimation: 'fade-in', exitAnimation: 'fade-out' - })); + }) ]; projector.invalidate(); @@ -910,11 +491,11 @@ registerSuite({ assert.isTrue(domNode.classList.contains('fade-in')); assert.isTrue(domNode.classList.contains('fade-in-active')); - // manually fire the transition end events - sendAnimationEndEvents(domNode!); + sendAnimationEndEvents(domNode); children = []; projector.invalidate(); + projector.scheduleRender(); assert.isTrue(domNode.classList.contains('fade-out')); assert.isTrue(domNode.classList.contains('fade-out-active')); @@ -940,13 +521,13 @@ registerSuite({ projector.async = false; projector.append(); - children.push(v('div', { + children = [ v('div', { id: 'test-element', enterAnimation: 'fade-in', enterAnimationActive: 'active-fade-in', exitAnimation: 'fade-out', exitAnimationActive: 'active-fade-out' - })); + }) ]; projector.invalidate(); @@ -955,7 +536,6 @@ registerSuite({ assert.isTrue(domNode.classList.contains('fade-in')); assert.isTrue(domNode.classList.contains('active-fade-in')); - // manually fire the transition end events sendAnimationEndEvents(domNode); children = []; @@ -967,7 +547,7 @@ registerSuite({ domNode.parentElement!.removeChild(domNode); }, - async 'dom nodes get removed after exit animations'(this: any) { + 'dom nodes get removed after exit animations'(this: any) { if (!has('host-browser')) { this.skip('This test can only be run in a browser'); } @@ -990,7 +570,7 @@ registerSuite({ const projector = new TestProjector(); projector.async = false; - await projector.append(); + projector.append(); const domNode = document.getElementById('test-element')!; assert.isNotNull(domNode); diff --git a/tests/unit/mixins/Themeable.ts b/tests/unit/mixins/Themeable.ts index 8f57b166..ee766835 100644 --- a/tests/unit/mixins/Themeable.ts +++ b/tests/unit/mixins/Themeable.ts @@ -1,4 +1,3 @@ -import { VNode } from '@dojo/interfaces/vdom'; import * as registerSuite from 'intern!object'; import * as assert from 'intern/chai!assert'; import { @@ -11,7 +10,7 @@ import { import { Injector } from './../../../src/Injector'; import { WidgetBase } from '../../../src/WidgetBase'; import { Registry } from '../../../src/Registry'; -import { v, w } from '../../../src/d'; +import { v } from '../../../src/d'; import { stub, SinonStub } from 'sinon'; import * as baseThemeClasses1 from './../../support/styles/testWidget1.css'; @@ -22,7 +21,6 @@ import * as extraClasses2 from './../../support/styles/extraClasses2.css'; import testTheme1 from './../../support/styles/theme1.css'; import testTheme2 from './../../support/styles/theme2.css'; import testTheme3 from './../../support/styles/theme3.css'; -import createTestWidget from './../../support/createTestWidget'; ( baseThemeClasses1)[' _key'] = 'testPath1'; ( baseThemeClasses2)[' _key'] = 'testPath2'; @@ -386,9 +384,11 @@ registerSuite({ return v('div', { classes: this.classes(baseThemeClasses1.class1) }); } } - const themeableInstance = createTestWidget(InjectedTheme, { registry: testRegistry }); - const vNode: any = themeableInstance.__render__(); - assert.deepEqual(vNode.properties.classes, { theme1Class1: true }); + const themeableInstance = new InjectedTheme(); + themeableInstance.__setCoreProperties__({ bind: themeableInstance, baseRegistry: testRegistry }); + themeableInstance.__setProperties__({}); + const renderResult: any = themeableInstance.__render__(); + assert.deepEqual(renderResult.properties.classes, { theme1Class1: true }); }, 'theme will not be injected if a theme has been passed via a property'() { const injector = new Injector(testTheme1); @@ -398,9 +398,11 @@ registerSuite({ return v('div', { classes: this.classes(baseThemeClasses1.class1) }); } } - const themeableInstance = createTestWidget(InjectedTheme, { theme: testTheme2, registry: testRegistry }); - const vNode: any = themeableInstance.__render__(); - assert.deepEqual(vNode.properties.classes, { theme2Class1: true }); + const themeableInstance = new InjectedTheme(); + themeableInstance.__setCoreProperties__({ bind: themeableInstance, baseRegistry: testRegistry }); + themeableInstance.__setProperties__({ theme: testTheme2 }); + const renderResult: any = themeableInstance.__render__(); + assert.deepEqual(renderResult.properties.classes, { theme2Class1: true }); }, 'does not attempt to inject if the ThemeInjector has not been defined in the registry'() { class InjectedTheme extends TestWidget { @@ -410,10 +412,10 @@ registerSuite({ } const themeableInstance = new InjectedTheme(); - const vNode: any = themeableInstance.__render__(); - assert.deepEqual(vNode.properties.classes, { baseClass1: true }); + const renderResult: any = themeableInstance.__render__(); + assert.deepEqual(renderResult.properties.classes, { baseClass1: true }); }, - 'setting the theme invalidates all "Themeable" widgets and the new theme is used'() { + 'setting the theme invalidates and the new theme is used'() { const themeInjectorContext = registerThemeInjector(testTheme1, testRegistry); class InjectedTheme extends TestWidget { render() { @@ -421,31 +423,18 @@ registerSuite({ } } - class MultipleThemedWidgets extends WidgetBase { - render() { - return v('div', [ - w(InjectedTheme, { key: '1' }), - w(InjectedTheme, { key: '2' }) - ]); - } - } - - const testWidget = new MultipleThemedWidgets(); + const testWidget = new InjectedTheme(); testWidget.__setCoreProperties__({ bind: testWidget, baseRegistry: testRegistry }); - let vNode: any = testWidget.__render__(); - assert.lengthOf(vNode.children, 2); - assert.deepEqual(vNode.children[0].properties.classes, { theme1Class1: true }); - assert.deepEqual(vNode.children[1].properties.classes, { theme1Class1: true }); + let renderResult: any = testWidget.__render__(); + assert.deepEqual(renderResult.properties.classes, { baseClass1: true }); themeInjectorContext.set(testTheme2); - vNode = testWidget.__render__(); - assert.lengthOf(vNode.children, 2); - assert.deepEqual(vNode.children[0].properties.classes, { theme1Class1: false, theme2Class1: true }); - assert.deepEqual(vNode.children[1].properties.classes, { theme1Class1: false, theme2Class1: true }); + testWidget.__setProperties__({}); + renderResult = testWidget.__render__(); + assert.deepEqual(renderResult.properties.classes, { baseClass1: false, theme2Class1: true }); themeInjectorContext.set(testTheme1); - vNode = testWidget.__render__(); - assert.lengthOf(vNode.children, 2); - assert.deepEqual(vNode.children[0].properties.classes, { theme2Class1: false, theme1Class1: true }); - assert.deepEqual(vNode.children[1].properties.classes, { theme2Class1: false, theme1Class1: true }); + testWidget.__setProperties__({}); + renderResult = testWidget.__render__(); + assert.deepEqual(renderResult.properties.classes, { baseClass1: false, theme2Class1: false, theme1Class1: true }); } }, 'integration': { @@ -468,7 +457,7 @@ registerSuite({ const themeableWidget: any = new IntegrationTest(); themeableWidget.__setProperties__({ theme: testTheme1 }); - const result = themeableWidget.__render__(); + const result = themeableWidget.__render__(); assert.deepEqual(result.children![0].properties!.classes, { [ testTheme1.testPath1.class1 ]: true, [ fixedClassName ]: true @@ -476,7 +465,7 @@ registerSuite({ themeableWidget.__setProperties__({ theme: testTheme2 }); - const result2 = themeableWidget.__render__(); + const result2 = themeableWidget.__render__(); assert.deepEqual(result2.children![0].properties!.classes, { [ testTheme1.testPath1.class1 ]: false, [ testTheme2.testPath1.class1 ]: true, diff --git a/tests/unit/tsxIntegration.tsx b/tests/unit/tsxIntegration.tsx index 5640f163..d6253ba5 100644 --- a/tests/unit/tsxIntegration.tsx +++ b/tests/unit/tsxIntegration.tsx @@ -2,8 +2,7 @@ import * as registerSuite from 'intern!object'; import * as assert from 'intern/chai!assert'; import { WidgetBase } from '../../src/WidgetBase'; import { Registry } from '../../src/Registry'; -import { WidgetProperties } from '../../src/interfaces'; -import { VNode } from '@dojo/interfaces/vdom'; +import { WidgetProperties, WNode } from '../../src/interfaces'; import { tsx, fromRegistry } from './../../src/tsx'; const registry = new Registry(); @@ -40,21 +39,15 @@ registerSuite({ const bar = new Bar(); bar.__setCoreProperties__({ bind: bar, baseRegistry: registry }); bar.__setProperties__({ registry }); - const barRender = bar.__render__() as VNode; - const barChild = barRender.children![0]; - assert.equal(barRender.vnodeSelector, 'header'); - assert.equal(barChild.text, 'world'); + const barRender = bar.__render__() as WNode; + assert.deepEqual(barRender.properties, { hello: 'world' }); + assert.strictEqual(barRender.widgetConstructor, Foo); + assert.lengthOf(barRender.children, 0); const qux = new Qux(); qux.__setCoreProperties__({ bind: qux, baseRegistry: registry }); qux.__setProperties__({ registry }); - const firstQuxRender = qux.__render__(); - assert.equal(firstQuxRender, null); - - registry.define('LazyFoo', Foo); - const secondQuxRender = qux.__render__() as VNode; - const secondQuxChild = secondQuxRender.children![0]; - assert.equal(secondQuxRender.vnodeSelector, 'header'); - assert.equal(secondQuxChild.text, 'cool'); + const firstQuxRender = qux.__render__() as WNode; + assert.strictEqual(firstQuxRender.widgetConstructor, 'LazyFoo'); } }); diff --git a/tests/unit/util/DomWrapper.ts b/tests/unit/util/DomWrapper.ts index 21e5de0c..a84f06e6 100644 --- a/tests/unit/util/DomWrapper.ts +++ b/tests/unit/util/DomWrapper.ts @@ -1,32 +1,25 @@ import * as registerSuite from 'intern!object'; +import { createResolvers } from './../../support/util'; import { WidgetBase } from './../../../src/WidgetBase'; import { v, w } from './../../../src/d'; import { DomWrapper } from '../../../src/util/DomWrapper'; -import global from '@dojo/shim/global'; -import { stub } from 'sinon'; import * as assert from 'intern/chai!assert'; import ProjectorMixin from '../../../src/mixins/Projector'; import { ThemeableMixin, theme } from '../../../src/mixins/Themeable'; -let rAF: any; let projector: any; -function resolveRAF() { - for (let i = 0; i < rAF.callCount; i++) { - rAF.getCall(0).args[0](); - } - rAF.reset(); -} +const resolvers = createResolvers(); registerSuite({ name: 'DomWrapper', beforeEach() { - rAF = stub(global, 'requestAnimationFrame'); + resolvers.stub(); }, afterEach() { - rAF.restore(); + resolvers.restore(); projector && projector.destroy(); }, @@ -47,12 +40,12 @@ registerSuite({ projector = new Projector(); const root = document.createElement('div'); projector.append(root); - resolveRAF(); + resolvers.resolve(); + resolvers.resolve(); assert.equal(domNode.foo, 'blah'); assert.equal(domNode.getAttribute('original'), 'woop'); assert.equal(domNode.getAttribute('id'), 'foo'); assert.deepEqual(domNode.extra, { foo: 'bar' }); - }, 'supports events'() { const domNode: any = document.createElement('custom-element'); @@ -71,7 +64,8 @@ registerSuite({ const Projector = ProjectorMixin(Foo); projector = new Projector(); projector.append(root); - resolveRAF(); + resolvers.resolve(); + resolvers.resolve(); domNode.click(); assert.isTrue(clicked); }, @@ -98,7 +92,8 @@ registerSuite({ const Projector = ProjectorMixin(Foo); projector = new Projector(); projector.append(root); - resolveRAF(); + resolvers.resolve(); + resolvers.resolve(); assert.isTrue(domNode.classList.contains('classFoo')); assert.equal(domNode.style.color, 'red'); }, @@ -121,7 +116,8 @@ registerSuite({ const Projector = ProjectorMixin(Foo); projector = new Projector(); projector.append(root); - resolveRAF(); + resolvers.resolve(); + resolvers.resolve(); assert.isTrue(attached); } }); diff --git a/tests/unit/vdom.ts b/tests/unit/vdom.ts new file mode 100644 index 00000000..e729271e --- /dev/null +++ b/tests/unit/vdom.ts @@ -0,0 +1,1812 @@ +import { afterEach, beforeEach, describe, it } from 'intern!bdd'; +import * as assert from 'intern/chai!assert'; +import { match, spy, stub, SinonStub } from 'sinon'; +import { createResolvers } from './../support/util'; + +import { dom, InternalHNode, InternalWNode } from '../../src/vdom'; +import { v, w } from '../../src/d'; +import { HNode } from '../../src/interfaces'; +import { WidgetBase } from '../../src/WidgetBase'; +import { Registry } from '../../src/Registry'; +import eventHandlerInterceptor from '../../src/util/eventHandlerInterceptor'; + +let consoleStub: SinonStub; + +const resolvers = createResolvers(); + +const noopEventHandlerInterceptor = (propertyName: string, functionPropertyArgument: Function) => { + return function(this: Node) { + return functionPropertyArgument.apply(this, arguments); + }; +}; + +const projectorStub: any = { + on: stub(), + emit: stub() +}; + +class MainBar extends WidgetBase { + render() { + return v('span', { innerHTML: 'Bar' }); + } +} + +class MainFoo extends WidgetBase { + render() { + const { show } = this.properties; + return v('div', { classes: { myClass: true }, foo: 'bar' }, [ + v('h1', { classes: { myClass: true }, key: 'one' }, [ 'Hello Widget' ]), + show ? w(MainBar, { classes: { myClass: true }, key: 'first' }) : null, + show ? w(MainBar, { key: 'second' }) : null, + show ? null : v('div', { key: 'three' }, ['me']), + `text node`, + v('h1', { key: 'two', classes: { myClass: true }, innerHTML: 'span' }) + ]); + } +} + +class TestWidget extends WidgetBase { + render() { + return v('span', { classes: { myClass: true } }, [ + w(MainFoo, { show: this.properties.show }) + ]); + } +} + +describe('vdom', () => { + beforeEach(() => { + projectorStub.on.reset(); + projectorStub.emit.reset(); + consoleStub = stub(console, 'warn'); + resolvers.stub(); + }); + + afterEach(() => { + consoleStub.restore(); + resolvers.restore(); + }); + + describe('widgets', () => { + + it('should create elements for widgets', () => { + const widget = new TestWidget(); + widget.__setCoreProperties__({ bind: widget } as any); + widget.__setProperties__({ show: true }); + + const renderResult = widget.__render__() as HNode; + const projection = dom.create(renderResult, widget); + const span = projection.domNode as HTMLSpanElement; + assert.lengthOf(span.childNodes, 1); + const div = span.childNodes[0] as HTMLDivElement; + assert.lengthOf(div.childNodes, 5); + assert.strictEqual(div.getAttribute('foo'), 'bar'); + + const headerOne = div.childNodes[0] as HTMLHeadElement; + const spanOne = div.childNodes[1] as HTMLSpanElement; + const spanTwo = div.childNodes[2] as HTMLSpanElement; + const text = div.childNodes[3] as Text; + const headerTwo = div.childNodes[4] as HTMLHeadElement; + + assert.lengthOf(headerOne.childNodes, 1); + assert.strictEqual((headerOne.childNodes[0] as Text).data, 'Hello Widget'); + + assert.lengthOf(spanOne.childNodes, 1); + assert.strictEqual(spanOne.innerHTML, 'Bar'); + + assert.lengthOf(spanTwo.childNodes, 1); + assert.strictEqual(spanTwo.innerHTML, 'Bar'); + + assert.strictEqual(text.data, 'text node'); + + assert.lengthOf(headerTwo.childNodes, 1); + assert.strictEqual(headerTwo.innerHTML, 'span'); + }); + + it('should update elements for widget changes', () => { + const widget = new TestWidget(); + widget.__setCoreProperties__({ bind: widget } as any); + widget.__setProperties__({ show: true }); + + const renderResult = widget.__render__() as HNode; + const projection = dom.create(renderResult, widget); + const root = projection.domNode as HTMLSpanElement; + + assert.lengthOf(root.childNodes, 1); + let rootChild = root.childNodes[0] as HTMLDivElement; + assert.lengthOf(rootChild.childNodes, 5); + assert.strictEqual(rootChild.getAttribute('foo'), 'bar'); + + let headerOne = rootChild.childNodes[0] as HTMLHeadElement; + let spanOne = rootChild.childNodes[1] as HTMLSpanElement; + let spanTwo = rootChild.childNodes[2] as HTMLSpanElement; + let text = rootChild.childNodes[3] as Text; + let headerTwo = rootChild.childNodes[4] as HTMLHeadElement; + + assert.lengthOf(headerOne.childNodes, 1); + assert.strictEqual((headerOne.childNodes[0] as Text).data, 'Hello Widget'); + + assert.lengthOf(spanOne.childNodes, 1); + assert.strictEqual(spanOne.innerHTML, 'Bar'); + + assert.lengthOf(spanTwo.childNodes, 1); + assert.strictEqual(spanTwo.innerHTML, 'Bar'); + + assert.strictEqual(text.data, 'text node'); + + assert.lengthOf(headerTwo.childNodes, 1); + assert.strictEqual(headerTwo.innerHTML, 'span'); + + widget.__setProperties__({ show: false }); + projection.update(widget.__render__() as HNode); + + assert.lengthOf(root.childNodes, 1); + rootChild = root.childNodes[0] as HTMLDivElement; + assert.lengthOf(rootChild.childNodes, 4); + assert.strictEqual(rootChild.getAttribute('foo'), 'bar'); + + headerOne = rootChild.childNodes[0] as HTMLHeadElement; + let insertedDiv = rootChild.childNodes[1] as HTMLDivElement; + text = rootChild.childNodes[2] as Text; + headerTwo = rootChild.childNodes[3] as HTMLHeadElement; + + assert.lengthOf(headerOne.childNodes, 1); + assert.strictEqual((headerOne.childNodes[0] as Text).data, 'Hello Widget'); + + assert.lengthOf(insertedDiv.childNodes, 1); + assert.strictEqual((insertedDiv.childNodes[0] as Text).data, 'me'); + + assert.strictEqual(text.data, 'text node'); + + assert.lengthOf(headerTwo.childNodes, 1); + assert.strictEqual(headerTwo.innerHTML, 'span'); + + widget.__setProperties__({ show: true }); + projection.update(widget.__render__() as HNode); + + assert.lengthOf(root.childNodes, 1); + rootChild = root.childNodes[0] as HTMLDivElement; + assert.lengthOf(rootChild.childNodes, 5); + assert.strictEqual(rootChild.getAttribute('foo'), 'bar'); + + headerOne = rootChild.childNodes[0] as HTMLHeadElement; + spanOne = rootChild.childNodes[1] as HTMLSpanElement; + spanTwo = rootChild.childNodes[2] as HTMLSpanElement; + text = rootChild.childNodes[3] as Text; + headerTwo = rootChild.childNodes[4] as HTMLHeadElement; + + assert.lengthOf(headerOne.childNodes, 1); + assert.strictEqual((headerOne.childNodes[0] as Text).data, 'Hello Widget'); + + assert.lengthOf(spanOne.childNodes, 1); + assert.strictEqual(spanOne.innerHTML, 'Bar'); + + assert.lengthOf(spanTwo.childNodes, 1); + assert.strictEqual(spanTwo.innerHTML, 'Bar'); + + assert.strictEqual(text.data, 'text node'); + + assert.lengthOf(headerTwo.childNodes, 1); + assert.strictEqual(headerTwo.innerHTML, 'span'); + }); + + it('invalidates up the widget tree', () => { + class Foo extends WidgetBase { + private _text = 'first'; + + private _onClick() { + this._text = 'second'; + this.invalidate(); + } + + render() { + return v('div', { onclick: this._onClick }, [ this._text ]); + } + } + + class Bar extends WidgetBase { + render() { + return v('div', [ + w(Foo, {}) + ]); + } + } + + class Baz extends WidgetBase { + render() { + return v('div', [ + w(Bar, {}) + ]); + } + } + + const widget = new Baz(); + const projection = dom.create( + widget.__render__() as HNode, + widget, + { eventHandlerInterceptor: eventHandlerInterceptor.bind(widget) } + ); + + const root = projection.domNode as HTMLElement; + assert.lengthOf(root.childNodes, 1); + const barDiv = root.childNodes[0]; + assert.lengthOf(barDiv.childNodes, 1); + const fooDiv = barDiv.childNodes[0] as HTMLDivElement; + assert.lengthOf(fooDiv.childNodes, 1); + const fooTextNode = fooDiv.childNodes[0] as Text; + assert.strictEqual(fooTextNode.data, 'first'); + projection.update(widget.__render__() as HNode); + assert.lengthOf(root.childNodes, 1); + assert.strictEqual(root.childNodes[0], barDiv); + assert.lengthOf(barDiv.childNodes, 1); + assert.strictEqual(barDiv.childNodes[0], fooDiv); + assert.lengthOf(fooDiv.childNodes, 1); + assert.strictEqual(fooDiv.childNodes[0], fooTextNode); + assert.strictEqual(fooTextNode.data, 'first'); + fooDiv.onclick({} as any); + projection.update(widget.__render__() as HNode); + assert.lengthOf(root.childNodes, 1); + assert.strictEqual(root.childNodes[0], barDiv); + assert.lengthOf(barDiv.childNodes, 1); + assert.strictEqual(barDiv.childNodes[0], fooDiv); + assert.lengthOf(fooDiv.childNodes, 1); + assert.notStrictEqual(fooDiv.childNodes[0], fooTextNode); + const updatedFooTextNode = fooDiv.childNodes[0] as Text; + assert.strictEqual(updatedFooTextNode.data, 'second'); + }); + + it('DNodes are bound to the parent widget', () => { + class Foo extends WidgetBase { + render() { + return v('div', { onclick: this.properties.onClick }, this.children); + } + } + + class Bar extends WidgetBase { + render() { + return v('div', { onclick: this.properties.onClick }); + } + } + class App extends WidgetBase { + + public onClickCount = 0; + + _onClick() { + this.onClickCount++; + } + + render() { + return v('div', { onclick: this._onClick }, [ + w(Foo, { onClick: this._onClick }, [ + v('div', { onclick: this._onClick }, [ + w(Bar, { + onClick: this._onClick + }) + ]) + ]) + ]); + } + } + + const widget = new App(); + const projection: any = dom.create( + widget.__render__() as HNode, + widget, + { eventHandlerInterceptor: eventHandlerInterceptor.bind(widget) } + ); + projection.domNode.onclick(); + projection.domNode.childNodes[0].onclick(); + projection.domNode.childNodes[0].childNodes[0].onclick(); + projection.domNode.childNodes[0].childNodes[0].childNodes[0].onclick(); + assert.strictEqual(widget.onClickCount, 4); + }); + + it('supports widget registry items', () => { + const baseRegistry = new Registry(); + + class Foo extends WidgetBase { + render() { + return v('h1', [ this.properties.text ]); + } + } + class Bar extends WidgetBase { + render() { + return v('h2', [ this.properties.text ]); + } + } + + baseRegistry.define('foo', Foo); + baseRegistry.define('bar', Bar); + class Baz extends WidgetBase { + render() { + return v('div', [ + w('foo', { text: 'foo' }), + w('bar', { text: 'bar' }) + ]); + } + } + + const widget = new Baz(); + widget.__setCoreProperties__({ bind: widget, baseRegistry }); + const projection: any = dom.create(widget.__render__() as HNode, widget); + const root = projection.domNode; + const headerOne = root.childNodes[0]; + const headerOneText = headerOne.childNodes[0] as Text; + const headerTwo = root.childNodes[1]; + const headerTwoText = headerTwo.childNodes[0] as Text; + assert.strictEqual(headerOneText.data, 'foo'); + assert.strictEqual(headerTwoText.data, 'bar'); + }); + + it('should invalidate when a registry items is loaded', () => { + const baseRegistry = new Registry(); + + class Foo extends WidgetBase { + render() { + return v('h1', [ this.properties.text ]); + } + } + class Bar extends WidgetBase { + render() { + return v('h2', [ this.properties.text ]); + } + } + + class Baz extends WidgetBase { + render() { + return v('div', [ + w('foo', { text: 'foo' }), + w('bar', { text: 'bar' }) + ]); + } + } + + const widget = new Baz(); + widget.__setCoreProperties__({ bind: widget, baseRegistry }); + const projection: any = dom.create(widget.__render__() as HNode, widget); + const root = projection.domNode; + assert.lengthOf(root.childNodes, 0); + baseRegistry.define('foo', Foo); + baseRegistry.define('bar', Bar); + projection.update(widget.__render__() as HNode); + const headerOne = root.childNodes[0]; + const headerOneText = headerOne.childNodes[0] as Text; + const headerTwo = root.childNodes[1]; + const headerTwoText = headerTwo.childNodes[0] as Text; + assert.strictEqual(headerOneText.data, 'foo'); + assert.strictEqual(headerTwoText.data, 'bar'); + }); + + it('supports an array of DNodes', () => { + class Foo extends WidgetBase { + private myClass = false; + + render() { + this.myClass = !this.myClass; + + return [ + v('div', { classes: { myClass: this.myClass } }, [ '1' ]), + v('div', {}, [ '2' ]), + v('div', { classes: { myClass: this.myClass } }, [ '3' ]) + ]; + } + } + + class Bar extends WidgetBase { + render() { + return v('div', [ + w(Foo, {}) + ]); + } + } + + const widget = new Bar(); + const renderResult = widget.__render__() as HNode; + const projection: any = dom.create(renderResult, widget); + const root = projection.domNode; + assert.lengthOf(root.childNodes, 3); + const childOne = root.childNodes[0]; + assert.lengthOf(childOne.childNodes, 1); + const textNodeOne = childOne.childNodes[0] as Text; + assert.strictEqual(textNodeOne.data, '1'); + const childTwo = root.childNodes[1]; + assert.lengthOf(childTwo.childNodes, 1); + const textNodeTwo = childTwo.childNodes[0] as Text; + assert.strictEqual(textNodeTwo.data, '2'); + const childThree = root.childNodes[2]; + assert.lengthOf(childThree.childNodes, 1); + const textNodeThree = childThree.childNodes[0] as Text; + assert.strictEqual(textNodeThree.data, '3'); + + widget.invalidate(); + const secondRenderResult = widget.__render__() as HNode; + projection.update(secondRenderResult); + const firstWNode = secondRenderResult.children![0] as InternalWNode; + const secondWNode = secondRenderResult.children![0] as InternalWNode; + assert.strictEqual(firstWNode.rendered, secondWNode.rendered); + }); + + it('supports null and undefined return from render', () => { + class Foo extends WidgetBase { + render() { + return null; + } + } + + class Bar extends WidgetBase { + render() { + return undefined; + } + } + + class Baz extends WidgetBase { + render() { + return v('div', [ + w(Foo, {}), + w(Bar, {}) + ]); + } + } + + const widget = new Baz(); + const projection: any = dom.create(widget.__render__() as HNode, widget); + const root = projection.domNode; + assert.lengthOf(root.childNodes, 0); + }); + + it('supports null return from render and subsequent return on re-render', () => { + let fooInvalidate: any; + class Foo extends WidgetBase { + + private myClass = false; + + constructor() { + super(); + fooInvalidate = this.invalidate.bind(this); + } + + render() { + + if (!this.properties.show) { + return null; + } + this.myClass = !this.myClass; + return v('div', { key: '1', classes: { myClass: this.myClass }}, [ + 'content' + ]); + } + } + + class Baz extends WidgetBase { + + private _show = false; + + set show(value: boolean) { + this._show = value; + this.invalidate(); + } + + render() { + return v('div', [ + w(Foo, { show: this._show }) + ]); + } + } + + const widget = new Baz(); + const projection: any = dom.create(widget.__render__() as HNode, widget); + const root = projection.domNode; + assert.lengthOf(root.childNodes, 0); + widget.show = true; + projection.update(widget.__render__() as HNode); + assert.lengthOf(root.childNodes, 1); + const fooDiv = root.childNodes[0] as HTMLDivElement; + assert.lengthOf(fooDiv.classList, 1); + assert.lengthOf(fooDiv.childNodes, 1); + const fooDivContent = fooDiv.childNodes[0] as Text; + assert.strictEqual(fooDivContent.data, 'content'); + fooInvalidate(); + projection.update(widget.__render__() as HNode); + assert.lengthOf(fooDiv.classList, 0); + assert.lengthOf(fooDiv.childNodes, 1); + }); + + it('should destroy widgets when they are no longer required', () => { + let fooDestroyedCount = 0; + + class Foo extends WidgetBase { + destroy() { + fooDestroyedCount++; + return super.destroy(); + } + render() { + return null; + } + } + + class Bar extends WidgetBase { + private _count = 20; + + set count(value: number) { + this._count = value; + this.invalidate(); + + } + + render() { + const children: any[] = []; + for (let i = 0; i < this._count; i++) { + children.push(w(Foo, { key: i})); + } + + return v('div', children); + } + } + + const widget = new Bar(); + const projection = dom.create(widget.__render__() as HNode, widget); + resolvers.resolve(); + widget.count = 10; + projection.update(widget.__render__() as HNode); + resolvers.resolve(); + assert.strictEqual(fooDestroyedCount, 10); + fooDestroyedCount = 0; + widget.count = 10; + projection.update(widget.__render__() as HNode); + resolvers.resolve(); + assert.strictEqual(fooDestroyedCount, 0); + widget.count = 20; + projection.update(widget.__render__() as HNode); + resolvers.resolve(); + assert.strictEqual(fooDestroyedCount, 0); + widget.count = 0; + projection.update(widget.__render__() as HNode); + resolvers.resolve(); + assert.strictEqual(fooDestroyedCount, 20); + }); + + it('destroys existing widget and uses new widget when widget changes', () => { + let fooDestroyed = false; + let fooCreated = false; + let barCreated = false; + class Foo extends WidgetBase { + + constructor() { + super(); + fooCreated = true; + } + + destroy() { + fooDestroyed = true; + return super.destroy(); + + } + render() { + return v('div'); + } + } + + class Bar extends WidgetBase { + constructor() { + super(); + barCreated = true; + } + + render() { + return v('span'); + } + } + + class Baz extends WidgetBase { + private _foo = true; + + set foo(value: boolean) { + this._foo = value; + this.invalidate(); + } + + render() { + return v('div', [ + this._foo ? w(Foo, {}) : w(Bar, {}) + ]); + } + } + + const widget = new Baz(); + const projection = dom.create(widget.__render__() as HNode, widget); + resolvers.resolve(); + assert.isTrue(fooCreated); + widget.foo = false; + projection.update(widget.__render__() as HNode); + resolvers.resolve(); + assert.isTrue(fooDestroyed); + assert.isTrue(barCreated); + }); + + it('remove elements for embedded WNodes', () => { + class Foo extends WidgetBase { + render() { + return v('div', { id: 'foo' }); + } + } + + class Bar extends WidgetBase { + render() { + return w(Foo, {}); + } + } + + class Baz extends WidgetBase { + private _show = true; + + set show(value: boolean) { + this._show = value; + this.invalidate(); + } + + render() { + return v('div', [ + this._show ? w(Bar, {}) : null + ]); + } + } + + const widget = new Baz(); + const projection = dom.create(widget.__render__() as HNode, widget); + const root = projection.domNode; + const fooDiv = root.childNodes[0] as HTMLDivElement; + assert.strictEqual(fooDiv.getAttribute('id'), 'foo'); + widget.show = false; + projection.update(widget.__render__() as HNode); + assert.isNull(fooDiv.parentNode); + }); + + it('should warn in the console for siblings for the same widgets with no key when added or removed', () => { + class Foo extends WidgetBase { + render() { + return v('div', [ this.properties.text ]); + } + } + + const widgetName = (Foo as any).name; + let errorMsg = 'It is recommended to provide a unique \'key\' property when using the same widget multiple times as siblings'; + + if (widgetName) { + errorMsg = `It is recommended to provide a unique 'key' property when using the same widget (${widgetName}) multiple times as siblings`; + } + + class Baz extends WidgetBase { + + show = false; + + render() { + return v('div', [ + w(Foo, { text: '1' }), + this.show ? w(Foo, { text: '2' }) : null, + w(Foo, { text: '3' }), + v('div', [ + w(Foo, { text: '4' }) + ]) + ]); + } + } + + const widget = new Baz(); + const projection = dom.create(widget.__render__() as HNode, widget); + assert.isTrue(consoleStub.notCalled); + widget.invalidate(); + widget.show = true; + projection.update(widget.__render__() as HNode); + assert.isTrue(consoleStub.calledTwice); + assert.isTrue(consoleStub.calledWith(errorMsg)); + }); + + }); + + describe('create', () => { + + it('should create and update single text nodes', () => { + const projection = dom.create(v('div', [ 'text' ]), projectorStub); + assert.strictEqual(projection.domNode.outerHTML, '
text
'); + + projection.update(v('div', [ 'text2' ])); + assert.strictEqual(projection.domNode.outerHTML, '
text2
'); + + projection.update(v('div', [ 'text2', v('span', [ 'a' ]) ])); + assert.strictEqual(projection.domNode.outerHTML, '
text2a
'); + + projection.update(v('div', [ 'text2' ])); + assert.strictEqual(projection.domNode.outerHTML, '
text2
'); + + projection.update(v('div', [ 'text' ])); + assert.strictEqual(projection.domNode.outerHTML, '
text
'); + }); + + it('should work correctly with adjacent text nodes', () => { + const projection = dom.create(v('div', [ '', '1', '' ]), projectorStub); + assert.strictEqual(projection.domNode.outerHTML, '
1
'); + + projection.update(v('div', [ ' ', '' ])); + assert.strictEqual(projection.domNode.outerHTML, '
'); + + projection.update(v('div', [ '', '1', '' ])); + assert.strictEqual(projection.domNode.outerHTML, '
1
'); + }); + + it('should break update when vdom object references are equal', () => { + const hNode = v('div', [ 'text' ]); + const projection = dom.create(hNode, projectorStub); + assert.strictEqual(projection.domNode.outerHTML, '
text
'); + hNode.text = 'new'; + projection.update(hNode); + assert.strictEqual(projection.domNode.outerHTML, '
text
'); + }); + + it('should give a meaningful error when the root selector is changed', () => { + const projection = dom.create(v('div'), projectorStub); + assert.throws(() => { + projection.update(v('span')); + }, Error, 'may not be changed'); + }); + + it('should allow an existing dom node to be used', () => { + const node = document.createElement('div'); + (node as any).foo = 'foo'; + const childNode = document.createElement('span'); + (childNode as any).bar = 'bar'; + node.appendChild(childNode); + const appendChildSpy = spy(node, 'appendChild'); + + const childHNode = v('span', { id: 'b' }) as InternalHNode; + childHNode.domNode = childNode; + const hNode = v('div', { id: 'a' }, [ childHNode ]) as InternalHNode; + hNode.domNode = node; + + const projection = dom.create(hNode, projectorStub); + const root = projection.domNode as any; + assert.strictEqual(root.outerHTML, '
'); + assert.strictEqual(root.foo, 'foo'); + assert.strictEqual(root.children[0].bar, 'bar'); + assert.isFalse(appendChildSpy.called); + }); + + }); + + describe('properties', () => { + + it('updates attributes', () => { + const projection = dom.create(v('a', { href: '#1' }), projectorStub); + const link = projection.domNode as HTMLLinkElement; + assert.strictEqual(link.getAttribute('href'), '#1'); + + projection.update(v('a', { href: '#2' })); + assert.strictEqual(link.getAttribute('href'), '#2'); + + projection.update(v('a', { href: undefined })); + assert.strictEqual(link.getAttribute('href'), ''); + }); + + it('can add an attribute that was initially undefined', () => { + const projection = dom.create(v('a', { href: undefined }), projectorStub); + const link = projection.domNode as HTMLLinkElement; + assert.isNull(link.getAttribute('href')); + + projection.update(v('a', { href: '#2' })); + assert.strictEqual(link.getAttribute('href'), '#2'); + }); + + it('can remove disabled property when set to null or undefined', () => { + const projection = dom.create(v('a', { disabled: true }), projectorStub); + const link = projection.domNode as HTMLLinkElement; + + assert.isTrue(link.disabled); + // Unfortunately JSDom does not map the property value to the attribute as real browsers do + // expect(link.getAttribute('disabled')).to.equal(''); + + projection.update(v('a', { disabled: null as any })); + + // What Chrome would do: + // expect(link.disabled).to.equal(false); + // expect(link.getAttribute('disabled')).to.be.null; + + // What JSDom does: + assert.isNull(link.disabled); + }); + + it('updates properties', () => { + const projection = dom.create(v('a', { href: '#1', tabIndex: 1 }), projectorStub); + const link = projection.domNode as HTMLLinkElement; + assert.strictEqual(link.tabIndex, 1); + + projection.update(v('a', { href: '#1', tabIndex: 2 })); + assert.strictEqual(link.tabIndex, 2); + + projection.update(v('a', { href: '#1', tabIndex: undefined })); + assert.strictEqual(link.tabIndex, 0); + }); + + it('updates innerHTML', () => { + const projection = dom.create(v('p', { innerHTML: 'INNER' }), projectorStub); + const paragraph = projection.domNode; + assert.lengthOf(paragraph.childNodes, 1); + assert.strictEqual(paragraph.childNodes[0].textContent, 'INNER'); + projection.update(v('p', { innerHTML: 'UPDATED' })); + assert.lengthOf(paragraph.childNodes, 1); + assert.strictEqual(paragraph.childNodes[0].textContent, 'UPDATED'); + }); + + it('does not mess up scrolling in Edge', () => { + const projection = dom.create(v('div', { scrollTop: 0 }), projectorStub); + const div = projection.domNode as HTMLDivElement; + Object.defineProperty(div, 'scrollTop', { + get: () => 1, + set: stub().throws('Setting scrollTop would mess up scrolling') + }); // meaning: div.scrollTop = 1; + projection.update(v('div', { scrollTop: 1 })); + }); + + describe('classes', () => { + + it('adds and removes classes', () => { + const projection = dom.create(v('div', { classes: { a: true, b: false } }), projectorStub); + const div = projection.domNode as HTMLDivElement; + assert.strictEqual(div.className, 'a'); + + projection.update(v('div', { classes: { a: true, b: true } })); + assert.strictEqual(div.className, 'a b'); + + projection.update(v('div', { classes: { a: false, b: true } })); + assert.strictEqual(div.className, 'b'); + }); + + it('allows a constant class to be applied to make JSX workable', () => { + const projection = dom.create(v('div', { class: 'extra special' }), projectorStub); + assert.strictEqual(projection.domNode.outerHTML, '
'); + projection.update(v('div', { class: 'extra special' })); + assert.throws(() => { + projection.update(v('div', { class: '' })); + }, Error); + }); + + it('allows classes and class to be combined', () => { + const projection = dom.create(v('div', { + classes: { extra: true }, + class: 'special' } + ), projectorStub); + assert.strictEqual(projection.domNode.outerHTML, '
'); + projection.update(v('div', { classes: { extra: false }, class: 'special' })); + assert.strictEqual(projection.domNode.outerHTML, '
'); + }); + + it('helps to prevent mistakes when using className', () => { + assert.throws(() => { + dom.create(v('div', { className: 'special' }), projectorStub); + }, Error); + }); + + }); + + describe('styles', () => { + + it('should not allow non-string values', () => { + try { + dom.create(v('div', { styles: { height: 20 as any } }), projectorStub); + assert.fail(); + } catch (e) { + assert.isTrue(e.message.indexOf('strings') >= 0); + } + }); + + it('should add styles to the real DOM', () => { + const projection = dom.create(v('div', { styles: { height: '20px' } }), projectorStub); + assert.strictEqual(projection.domNode.outerHTML, '
'); + }); + + it('should update styles', () => { + const projection = dom.create(v('div', { styles: { height: '20px' } }), projectorStub); + projection.update(v('div', { styles: { height: '30px' } })); + assert.strictEqual(projection.domNode.outerHTML, '
'); + }); + + it('should remove styles', () => { + const projection = dom.create(v('div', { styles: { width: '30px', height: '20px' } }), projectorStub); + projection.update(v('div', { styles: { height: null, width: '30px' } })); + assert.strictEqual(projection.domNode.outerHTML, '
'); + }); + + it('should add styles', () => { + const projection = dom.create(v('div', { styles: { height: undefined } }), projectorStub); + projection.update(v('div', { styles: { height: '20px' } })); + assert.strictEqual(projection.domNode.outerHTML, '
'); + projection.update(v('div', { styles: { height: '20px' } })); + }); + + it('should use the provided styleApplyer', () => { + const styleApplyer = (domNode: any, styleName: string, value: string) => { + // Useless styleApplyer which transforms height to minHeight + domNode.style['min' + styleName.substr(0, 1).toUpperCase() + styleName.substr(1)] = value; + }; + const projection = dom.create(v('div', { styles: { height: '20px' } }), projectorStub, { styleApplyer: styleApplyer }); + assert.strictEqual(projection.domNode.outerHTML, '
'); + projection.update(v('div', { styles: { height: '30px' } })); + assert.strictEqual(projection.domNode.outerHTML, '
'); + }); + + }); + + describe('event handlers', () => { + + it('allows one to correct the value while being typed', () => { + let typedKeys = ''; + const handleInput = (evt: any) => { + typedKeys = evt.target.value.substr(0, 2); + }; + const renderFunction = () => v('input', { value: typedKeys, oninput: handleInput }); + const projection = dom.create(renderFunction(), projectorStub, { eventHandlerInterceptor: noopEventHandlerInterceptor }); + const inputElement = projection.domNode as HTMLInputElement; + assert.strictEqual(inputElement.value, typedKeys); + + inputElement.value = 'ab'; + inputElement.oninput({ target: inputElement } as any); + assert.strictEqual(typedKeys, 'ab'); + projection.update(renderFunction()); + assert.strictEqual(inputElement.value, 'ab'); + + inputElement.value = 'abc'; + inputElement.oninput({ target: inputElement } as any); + assert.strictEqual(typedKeys, 'ab'); + projection.update(renderFunction()); + assert.strictEqual(inputElement.value, 'ab'); + }); + + it('does not undo keystrokes, even if a browser runs an animationFrame between changing the value property and running oninput', () => { + // Crazy internet explorer behavior + let typedKeys = ''; + const handleInput = (evt: Event) => { + typedKeys = (evt.target as HTMLInputElement).value; + }; + + const renderFunction = () => v('input', { value: typedKeys, oninput: handleInput }); + + const projection = dom.create(renderFunction(), projectorStub, { eventHandlerInterceptor: noopEventHandlerInterceptor }); + const inputElement = (projection.domNode as HTMLInputElement); + assert.strictEqual(inputElement.value, typedKeys); + + // Normal behavior + inputElement.value = 'a'; + inputElement.oninput({ target: inputElement } as any); + assert.strictEqual(typedKeys, 'a'); + projection.update(renderFunction()); + + // Crazy behavior + inputElement.value = 'ab'; + projection.update(renderFunction()); + assert.strictEqual(typedKeys, 'a'); + assert.strictEqual(inputElement.value, 'ab'); + inputElement.oninput({ target: inputElement } as any); + assert.strictEqual(typedKeys, 'ab'); + projection.update(renderFunction()); + }); + + it('does not allow event handlers to be updated, for performance reasons', () => { + const handler1 = () => undefined as void; + const handler2 = () => undefined as void; + const projection = dom.create(v('button', { onclick: handler1 }), projectorStub); + assert.throws(() => { + projection.update(v('button', { onclick: handler2 })); + }); + }); + + }); + + it('updates the value property', () => { + let typedKeys = ''; + const handleInput = (evt: Event) => { + typedKeys = (evt.target as HTMLInputElement).value; + }; + + const renderFunction = () => v('input', { value: typedKeys, oninput: handleInput }); + const projection = dom.create(renderFunction(), projectorStub, { eventHandlerInterceptor: noopEventHandlerInterceptor }); + const inputElement = (projection.domNode as HTMLInputElement); + assert.strictEqual(inputElement.value, typedKeys); + typedKeys = 'value1'; + projection.update(renderFunction()); + assert.strictEqual(inputElement.value, typedKeys); + }); + + it('does not clear a value that was set by a testing tool (like Ranorex) which manipulates input.value directly', () => { + let typedKeys = ''; + const handleInput = (evt: Event) => { + typedKeys = (evt.target as HTMLInputElement).value; + }; + + const renderFunction = () => v('input', { value: typedKeys, oninput: handleInput }); + + const projection = dom.create(renderFunction(), projectorStub, { eventHandlerInterceptor: noopEventHandlerInterceptor }); + const inputElement = (projection.domNode as HTMLInputElement); + assert.strictEqual(inputElement.value, typedKeys); + + inputElement.value = 'value written by a testing tool without invoking the input event'; + + projection.update(renderFunction()); + assert.notStrictEqual(inputElement.value, typedKeys); + }); + + it('Can handle oninput event handlers which pro-actively change element.value to correct user input when typing faster than 60 keys per second', () => { + let model = ''; + const handleInput = (evt: Event) => { + const inputElement = evt.target as HTMLInputElement; + model = inputElement.value; + if (model.indexOf(',') > 0) { + model = model.replace(/,/g, '.'); + inputElement.value = model; + } + }; + + const renderFunction = () => v('input', { value: model, oninput: handleInput }); + const projection = dom.create(renderFunction(), projectorStub, { eventHandlerInterceptor: noopEventHandlerInterceptor }); + + const inputElement = (projection.domNode as HTMLInputElement); + assert.strictEqual(inputElement.value, model); + + inputElement.value = '4'; + inputElement.oninput({target: inputElement} as any as Event); + projection.update(renderFunction()); + + inputElement.value = '4,'; + inputElement.oninput({target: inputElement} as any as Event); + projection.update(renderFunction()); + + assert.strictEqual(inputElement.value, '4.'); + + model = ''; + projection.update(renderFunction()); + + assert.strictEqual(inputElement.value, ''); + }); + + it('removes the attribute when a role property is set to undefined', () => { + let role: string | undefined = 'button'; + const renderFunction = () => v('div', { role: role }); + + const projection = dom.create(renderFunction(), projectorStub, { eventHandlerInterceptor: noopEventHandlerInterceptor }); + const element = projection.domNode; + + assert.property(element.attributes, 'role'); + assert.strictEqual(element.getAttribute('role'), role); + + role = undefined; + projection.update(renderFunction()); + assert.notProperty(element.attributes, 'role'); + }); + + }); + + describe('children', () => { + + it('can remove child nodes', () => { + const projection = dom.create(v('div', [ + v('span', { key: 1 }), + v('span', { key: 2 }), + v('span', { key: 3 }) + ]), projectorStub); + + const div = projection.domNode; + assert.lengthOf(div.childNodes, 3); + const firstSpan = div.childNodes[0]; + const lastSpan = div.childNodes[2]; + + projection.update(v('div', [ + v('span', { key: 1 }), + v('span', { key: 3 }) + ])); + + assert.lengthOf(div.childNodes, 2); + assert.strictEqual(div.childNodes[0], firstSpan); + assert.strictEqual(div.childNodes[1], lastSpan); + + projection.update(v('div', [ + v('span', { key: 3 }) + ])); + + assert.lengthOf(div.childNodes, 1); + assert.strictEqual(div.childNodes[0], lastSpan); + + projection.update(v('div')); + assert.lengthOf(div.childNodes, 0); + }); + + it('can add child nodes', () => { + const projection = dom.create(v('div', [ + v('span', { key: 2 }), + v('span', { key: 4 }) + ]), projectorStub); + + const div = projection.domNode; + assert.lengthOf(div.childNodes, 2); + const firstSpan = div.childNodes[0]; + const lastSpan = div.childNodes[1]; + + projection.update(v('div', [ + v('span', { key: 1 }), + v('span', { key: 2 }), + v('span', { key: 3 }), + v('span', { key: 4 }), + v('span', { key: 5 }) + ])); + + assert.lengthOf(div.childNodes, 5); + assert.strictEqual(div.childNodes[1], firstSpan); + assert.strictEqual(div.childNodes[3], lastSpan); + }); + + it('can distinguish between string keys when adding', () => { + const projection = dom.create(v('div', [ + v('span', { key: 'one' }), + v('span', { key: 'three' }) + ]), projectorStub); + + const div = projection.domNode; + assert.lengthOf(div.childNodes, 2); + const firstSpan = div.childNodes[0]; + const secondSpan = div.childNodes[1]; + + projection.update(v('div', [ + v('span', { key: 'one' }), + v('span', { key: 'two' }), + v('span', { key: 'three' }) + ])); + + assert.lengthOf(div.childNodes, 3); + assert.strictEqual(div.childNodes[0], firstSpan); + assert.strictEqual(div.childNodes[2], secondSpan); + }); + + it('can distinguish between falsy keys when replacing', () => { + const projection = dom.create(v('div', [ + v('span', { key: false }), + v('span', { key: null as any }), + v('span', { key: '' }), + v('span', {}) + ]), projectorStub); + + const div = projection.domNode; + assert.lengthOf(div.childNodes, 4); + + const firstSpan = div.childNodes[0]; + const secondSpan = div.childNodes[1]; + const thirdSpan = div.childNodes[2]; + const fourthSpan = div.childNodes[3]; + + projection.update(v('div', [ + v('span', { key: 0 }) + ])); + + assert.lengthOf(div.childNodes, 1); + const newSpan = div.childNodes[0]; + + assert.notStrictEqual(newSpan, firstSpan); + assert.notStrictEqual(newSpan, secondSpan); + assert.notStrictEqual(newSpan, thirdSpan); + assert.notStrictEqual(newSpan, fourthSpan); + }); + + it('can distinguish between string keys when deleting', () => { + const projection = dom.create(v('div', [ + v('span', { key: 'one' }), + v('span', { key: 'two' }), + v('span', { key: 'three' }) + ]), projectorStub); + + const div = projection.domNode; + assert.lengthOf(div.childNodes, 3); + const firstSpan = div.childNodes[0]; + const thirdSpan = div.childNodes[2]; + + projection.update(v('div', [ + v('span', { key: 'one' }), + v('span', { key: 'three' }) + ])); + + assert.lengthOf(div.childNodes, 2); + assert.strictEqual(div.childNodes[0], firstSpan); + assert.strictEqual(div.childNodes[1], thirdSpan); + }); + + it('can distinguish between falsy keys when deleting', () => { + const projection = dom.create(v('div', [ + v('span', { key: 0 }), + v('span', { key: false }), + v('span', { key: null as any }) + ]), projectorStub); + + const div = projection.domNode; + assert.lengthOf(div.childNodes, 3); + const firstSpan = div.childNodes[0]; + const thirdSpan = div.childNodes[2]; + + projection.update(v('div', [ + v('span', { key: 0 }), + v('span', { key: null as any }) + ])); + + assert.lengthOf(div.childNodes, 2); + assert.strictEqual(div.childNodes[0], firstSpan); + assert.strictEqual(div.childNodes[1], thirdSpan); + }); + + it('does not reorder nodes based on keys', () => { + const projection = dom.create(v('div', [ + v('span', { key: 'a' }), + v('span', { key: 'b' }) + ]), projectorStub); + + const div = projection.domNode; + assert.lengthOf(div.childNodes, 2); + const firstSpan = div.childNodes[0]; + const lastSpan = div.childNodes[1]; + + projection.update(v('div', [ + v('span', { key: 'b' }), + v('span', { key: 'a' }) + ])); + + assert.lengthOf(div.childNodes, 2); + assert.strictEqual(div.childNodes[0], lastSpan); + assert.notStrictEqual(div.childNodes[1], firstSpan); + }); + + it('can insert text nodes', () => { + const projection = dom.create(v('div', [ + v('span', { key: 2 }), + v('span', { key: 4 }) + ]), projectorStub); + + const div = projection.domNode; + assert.lengthOf(div.childNodes, 2); + const firstSpan = div.childNodes[0]; + const lastSpan = div.childNodes[1]; + + projection.update(v('div', [ + v('span', { key: 2 }), + 'Text between', + v('span', { key: 4 }) + ])); + + assert.lengthOf(div.childNodes, 3); + + assert.strictEqual(div.childNodes[0], firstSpan); + assert.strictEqual(div.childNodes[2], lastSpan); + }); + + it('can update single text nodes', () => { + const projection = dom.create(v('span', [ '' ]), projectorStub); + const span = projection.domNode; + assert.lengthOf(span.childNodes, 1); + + projection.update(v('span', [ undefined ])); + assert.lengthOf(span.childNodes, 0); + + projection.update(v('span', [ 'f' ])); + assert.lengthOf(span.childNodes, 1); + + projection.update(v('span', [ undefined ])); + assert.lengthOf(span.childNodes, 0); + + projection.update(v('span', [ '' ])); + assert.lengthOf(span.childNodes, 1); + + projection.update(v('span', [ ' ' ])); + assert.lengthOf(span.childNodes, 1); + }); + + it('will throw an error when vdom is not sure which node is added', () => { + const projection = dom.create(v('div', [ + v('span', [ 'a' ]), + v('span', [ 'c' ]) + ]), projectorStub); + assert.throws(() => { + projection.update(v('div', [ + v('span', [ 'a' ]), + v('span', [ 'b' ]), + v('span', [ 'c' ]) + ])); + }); + }); + + it('will throw an error when vdom is not sure which node is removed', () => { + const projection = dom.create(v('div', [ + v('span', [ 'a' ]), + v('span', [ 'b' ]), + v('span', [ 'c' ]) + ]), projectorStub); + assert.throws(() => { + projection.update(v('div', [ + v('span', [ 'a' ]), + v('span', [ 'c' ]) + ])); + }); + }); + + it('allows a contentEditable tag to be altered', () => { + let text = 'initial value'; + const handleInput = (evt: any) => { + text = evt.currentTarget.innerHTML; + }; + const renderDNodes = () => v('div', { + contentEditable: true, + oninput: handleInput, + innerHTML: text + }); + const projection = dom.create(renderDNodes(), projectorStub); + + projection.domNode.removeChild(projection.domNode.childNodes[0]); + handleInput({ currentTarget: projection.domNode }); + projection.update(renderDNodes()); + + projection.domNode.innerHTML = 'changed value'; + handleInput({ currentTarget: projection.domNode }); + projection.update(renderDNodes()); + + assert.strictEqual(projection.domNode.innerHTML, 'changed value'); + }); + + describe('svg', () => { + + it('creates and updates svg dom nodes with the right namespace', () => { + const projection = dom.create(v('div', [ + v('svg', [ + v('circle', { cx: '2cm', cy: '2cm', r: '1cm', fill: 'red' }), + v('image', { href: '/image.jpeg' }) + ]), + v('span') + ]), projectorStub); + const svg = projection.domNode.childNodes[0]; + assert.strictEqual(svg.namespaceURI, 'http://www.w3.org/2000/svg'); + const circle = svg.childNodes[0]; + assert.strictEqual(circle.namespaceURI, 'http://www.w3.org/2000/svg'); + const image = svg.childNodes[1]; + assert.strictEqual(image.attributes[0].namespaceURI, 'http://www.w3.org/1999/xlink'); + const span = projection.domNode.childNodes[1]; + assert.strictEqual(span.namespaceURI, 'http://www.w3.org/1999/xhtml'); + + projection.update(v('div', [ + v('svg', [ + v('circle', { key: 'blue', cx: '2cm', cy: '2cm', r: '1cm', fill: 'blue' }), + v('image', { href: '/image2.jpeg' }) + ]), + v('span') + ])); + + const blueCircle = svg.childNodes[0]; + assert.strictEqual(blueCircle.namespaceURI, 'http://www.w3.org/2000/svg'); + }); + }); + + }); + + it('Supports merging DNodes onto existing HTML', () => { + const iframe = document.createElement('iframe'); + document.body.appendChild(iframe); + iframe.contentDocument.write(`
`); + iframe.contentDocument.close(); + const root = iframe.contentDocument.body.firstChild as HTMLElement; + const childElementCount = root.childElementCount; + const select = root.childNodes[1] as HTMLSelectElement; + const button = root.childNodes[2] as HTMLButtonElement; + assert.strictEqual(select.value, 'bar', 'bar should be selected'); + const onchangeListener = spy(); + const onclickListener = spy(); + class Foo extends WidgetBase { + render() { + return v('div', { + classes: { foo: true, bar: true } + }, [ + v('label', { + for: 'baz' + }, [ 'Select Me:' ]), + v('select', { + type: 'text', + name: 'baz', + id: 'baz', + disabled: false, + onchange: onchangeListener + }, [ + v('option', { value: 'foo', selected: true }, [ 'label foo' ]), + v('option', { value: 'bar', selected: false }, [ 'label bar' ]), + v('option', { value: 'baz', selected: false }, [ 'label baz' ]) + ]), + v('button', { + type: 'button', + disabled: false, + onclick: onclickListener + }, [ 'Click Me!' ]) + ]); + } + } + const widget = new Foo(); + dom.merge(root, widget.__render__() as HNode, widget); + assert.strictEqual(root.className, 'foo bar', 'should have added bar class'); + assert.strictEqual(root.childElementCount, childElementCount, 'should have the same number of children'); + assert.strictEqual(select, root.childNodes[1], 'should have been reused'); + assert.strictEqual(button, root.childNodes[2], 'should have been reused'); + assert.isFalse(select.disabled, 'select should be enabled'); + assert.isFalse(button.disabled, 'button should be enabled'); + + assert.strictEqual(select.value, 'foo', 'foo should be selected'); + assert.strictEqual(select.children.length, 3, 'should have 3 children'); + + assert.isFalse(onchangeListener.called, 'onchangeListener should not have been called'); + assert.isFalse(onclickListener.called, 'onclickListener should not have been called'); + + const changeEvent = document.createEvent('Event'); + changeEvent.initEvent('change', true, true); + select.onchange(changeEvent); // firefox doesn't like to dispatch this event, either due to trust issues or + // that firefox doesn't generally dispatch this event until the element is blurred + // which is different than other browsers. Either way this is not material to testing + // the functionality of this test, so calling the listener directly. + assert.isTrue(onchangeListener.called, 'onchangeListener should have been called'); + + const clickEvent = document.createEvent('CustomEvent'); + clickEvent.initEvent('click', true, true); + button.dispatchEvent(clickEvent); + assert.isTrue(onclickListener.called, 'onclickListener should have been called'); + + document.body.removeChild(iframe); + }); + + it('Supports merging DNodes with widgets onto existing HTML', () => { + const iframe = document.createElement('iframe'); + document.body.appendChild(iframe); + iframe.contentDocument.write(`
label
last node
`); + iframe.contentDocument.close(); + const root = iframe.contentDocument.body.firstChild as HTMLElement; + const childElementCount = root.childElementCount; + const label = root.childNodes[0] as HTMLLabelElement; + const select = root.childNodes[1] as HTMLSelectElement; + const button = root.childNodes[2] as HTMLButtonElement; + const span = root.childNodes[3] as HTMLElement; + const div = root.childNodes[4] as HTMLElement; + assert.strictEqual(select.value, 'bar', 'bar should be selected'); + const onchangeListener = spy(); + const onclickListener = spy(); + + class Button extends WidgetBase { + render() { + return [ + v('button', { type: 'button', disabled: false, onclick: onclickListener }, [ 'Click Me!' ]), + v('span', {}, [ 'label' ]) + ]; + } + } + class Foo extends WidgetBase { + render() { + return v('div', { + classes: { foo: true, bar: true } + }, [ + v('label', { + for: 'baz' + }, [ 'Select Me:' ]), + v('select', { + type: 'text', + name: 'baz', + id: 'baz', + disabled: false, + onchange: onchangeListener + }, [ + v('option', { value: 'foo', selected: true }, [ 'label foo' ]), + v('option', { value: 'bar', selected: false }, [ 'label bar' ]), + v('option', { value: 'baz', selected: false }, [ 'label baz' ]) + ]), + w(Button, {}), + v('div', [ 'last node']) + ]); + } + } + const widget = new Foo(); + dom.merge(root, widget.__render__() as HNode, widget); + assert.strictEqual(root.className, 'foo bar', 'should have added bar class'); + assert.strictEqual(root.childElementCount, childElementCount, 'should have the same number of children'); + assert.strictEqual(label, root.childNodes[0], 'should have been reused'); + assert.strictEqual(select, root.childNodes[1], 'should have been reused'); + assert.strictEqual(button, root.childNodes[2], 'should have been reused'); + assert.strictEqual(span, root.childNodes[3], 'should have been reused'); + assert.strictEqual(div, root.childNodes[4], 'should have been reused'); + assert.isFalse(select.disabled, 'select should be enabled'); + assert.isFalse(button.disabled, 'button should be enabled'); + + assert.strictEqual(select.value, 'foo', 'foo should be selected'); + assert.strictEqual(select.children.length, 3, 'should have 3 children'); + + assert.isFalse(onchangeListener.called, 'onchangeListener should not have been called'); + assert.isFalse(onclickListener.called, 'onclickListener should not have been called'); + + const changeEvent = document.createEvent('Event'); + changeEvent.initEvent('change', true, true); + select.onchange(changeEvent); // firefox doesn't like to dispatch this event, either due to trust issues or + // that firefox doesn't generally dispatch this event until the element is blurred + // which is different than other browsers. Either way this is not material to testing + // the functionality of this test, so calling the listener directly. + assert.isTrue(onchangeListener.called, 'onchangeListener should have been called'); + + const clickEvent = document.createEvent('CustomEvent'); + clickEvent.initEvent('click', true, true); + button.dispatchEvent(clickEvent); + assert.isTrue(onclickListener.called, 'onclickListener should have been called'); + + document.body.removeChild(iframe); + }); + + it('Skips unknown nodes when merging', () => { + const iframe = document.createElement('iframe'); + document.body.appendChild(iframe); + iframe.contentDocument.write(` +
+ + + + label +
last node
+
`); + iframe.contentDocument.close(); + const root = iframe.contentDocument.body.firstChild as HTMLElement; + const childElementCount = root.childElementCount; + const label = root.childNodes[1] as HTMLLabelElement; + const select = root.childNodes[3] as HTMLSelectElement; + const button = root.childNodes[5] as HTMLButtonElement; + const span = root.childNodes[7] as HTMLElement; + const div = root.childNodes[9] as HTMLElement; + assert.strictEqual(select.value, 'bar', 'bar should be selected'); + const onchangeListener = spy(); + const onclickListener = spy(); + + class Button extends WidgetBase { + render() { + return [ + v('button', { type: 'button', disabled: false, onclick: onclickListener }, [ 'Click Me!' ]), + v('span', {}, [ 'label' ]) + ]; + } + } + class Foo extends WidgetBase { + render() { + return v('div', { + classes: { foo: true, bar: true } + }, [ + v('label', { + for: 'baz' + }, [ 'Select Me:' ]), + v('select', { + type: 'text', + name: 'baz', + id: 'baz', + disabled: false, + onchange: onchangeListener + }, [ + v('option', { value: 'foo', selected: true }, [ 'label foo' ]), + v('option', { value: 'bar', selected: false }, [ 'label bar' ]), + v('option', { value: 'baz', selected: false }, [ 'label baz' ]) + ]), + w(Button, {}), + v('div', [ 'last node']) + ]); + } + } + const widget = new Foo(); + dom.merge(root, widget.__render__() as HNode, widget); + assert.strictEqual(root.className, 'foo bar', 'should have added bar class'); + assert.strictEqual(root.childElementCount, childElementCount, 'should have the same number of children'); + assert.strictEqual(label, root.childNodes[1], 'should have been reused'); + assert.strictEqual(select, root.childNodes[3], 'should have been reused'); + assert.strictEqual(button, root.childNodes[5], 'should have been reused'); + assert.strictEqual(span, root.childNodes[7], 'should have been reused'); + assert.strictEqual(div, root.childNodes[9], 'should have been reused'); + assert.isFalse(select.disabled, 'select should be enabled'); + assert.isFalse(button.disabled, 'button should be enabled'); + + assert.strictEqual(select.value, 'foo', 'foo should be selected'); + assert.strictEqual(select.children.length, 3, 'should have 3 children'); + + assert.isFalse(onchangeListener.called, 'onchangeListener should not have been called'); + assert.isFalse(onclickListener.called, 'onclickListener should not have been called'); + + const changeEvent = document.createEvent('Event'); + changeEvent.initEvent('change', true, true); + select.onchange(changeEvent); // firefox doesn't like to dispatch this event, either due to trust issues or + // that firefox doesn't generally dispatch this event until the element is blurred + // which is different than other browsers. Either way this is not material to testing + // the functionality of this test, so calling the listener directly. + assert.isTrue(onchangeListener.called, 'onchangeListener should have been called'); + + const clickEvent = document.createEvent('CustomEvent'); + clickEvent.initEvent('click', true, true); + button.dispatchEvent(clickEvent); + assert.isTrue(onclickListener.called, 'onclickListener should have been called'); + + document.body.removeChild(iframe); + }); + + describe('node callbacks', () => { + + it('element-created not emitted for new nodes without a key', () => { + dom.create(v('div'), projectorStub); + resolvers.resolve(); + assert.isTrue(projectorStub.emit.neverCalledWith({ type: 'element-created' })); + }); + + it('element-created emitted for new nodes with a key', () => { + const projection = dom.create(v('div', { key: '1' }), projectorStub); + resolvers.resolve(); + assert.isTrue(projectorStub.emit.calledWith({ type: 'element-created', element: projection.domNode, key: '1' })); + }); + + it('element-created emitted for new nodes with a key of 0', () => { + const projection = dom.create(v('div', { key: 0 }), projectorStub); + resolvers.resolve(); + assert.isTrue(projectorStub.emit.calledWith({ type: 'element-created', element: projection.domNode, key: 0 })); + }); + + it('element-updated not emitted for updated nodes without a key', () => { + const projection = dom.create(v('div'), projectorStub); + resolvers.resolve(); + projection.update(v('div')); + resolvers.resolve(); + assert.isTrue(projectorStub.emit.neverCalledWith({ type: 'element-updated' })); + }); + + it('element-updated emitted for updated nodes with a key', () => { + const projection = dom.create(v('div'), projectorStub); + resolvers.resolve(); + projection.update(v('div', { key: '1' })); + resolvers.resolve(); + assert.isTrue(projectorStub.emit.calledWith({ type: 'element-updated', element: projection.domNode, key: '1' })); + }); + + it('element-updated emitted for updated nodes with a key of 0', () => { + const projection = dom.create(v('div'), projectorStub); + resolvers.resolve(); + projection.update(v('div', { key: 0 })); + resolvers.resolve(); + assert.isTrue(projectorStub.emit.calledWith({ type: 'element-updated', element: projection.domNode, key: 0 })); + }); + + }); + + describe('animations', () => { + + describe('updateAnimation', () => { + + it('is invoked when a node contains only text and that text changes', () => { + const updateAnimation = stub(); + const projection = dom.create(v('div', { updateAnimation }, [ 'text' ]), projectorStub); + projection.update(v('div', { updateAnimation }, [ 'text2' ])); + assert.isTrue(updateAnimation.calledOnce); + assert.strictEqual(projection.domNode.outerHTML, '
text2
'); + }); + + it('is invoked when a node contains text and other nodes and the text changes', () => { + const updateAnimation = stub(); + const projection = dom.create(v('div', { updateAnimation }, [ + 'textBefore', + v('span'), + 'textAfter' + ]), projectorStub); + projection.update(v('div', { updateAnimation }, [ + 'textBefore', + v('span'), + 'newTextAfter' + ])); + assert.isTrue(updateAnimation.calledOnce); + updateAnimation.reset(); + + projection.update(v('div', { updateAnimation }, [ + 'textBefore', + v('span'), + 'newTextAfter' + ])); + assert.isTrue(updateAnimation.notCalled); + }); + + it('is invoked when a property changes', () => { + const updateAnimation = stub(); + const projection = dom.create(v('a', { updateAnimation, href: '#1' }), projectorStub); + projection.update(v('a', { updateAnimation, href: '#2' })); + assert.isTrue(updateAnimation.calledWith( + projection.domNode, + match({ href: '#2' }), + match({ href: '#1' }) + )); + }); + }); + + describe('enterAnimation', () => { + + it('is invoked when a new node is added to an existing parent node', () => { + const enterAnimation = stub(); + const projection = dom.create(v('div', []), projectorStub); + + projection.update(v('div', [ + v('span', { enterAnimation }) + ])); + + assert.isTrue(enterAnimation.calledWith(projection.domNode.childNodes[0], match({}))); + }); + }); + + describe('exitAnimation', () => { + + it('is invoked when a node is removed from an existing parent node', () => { + const exitAnimation = stub(); + const projection = dom.create(v('div', [ + v('span', { exitAnimation }) + ]), projectorStub); + + projection.update(v('div', [])); + + assert.isTrue(exitAnimation.calledWithExactly(projection.domNode.childNodes[0], match({}), match({}))); + + assert.lengthOf(projection.domNode.childNodes, 1); + exitAnimation.lastCall.callArg(1); // arg1: removeElement + assert.lengthOf(projection.domNode.childNodes, 0); + }); + + }); + + describe('transitionStrategy', () => { + + it('will be invoked when enterAnimation is provided as a string', () => { + const transitionStrategy = { enter: stub(), exit: stub() }; + const projection = dom.create(v('div'), projectorStub, { transitions: transitionStrategy }); + + projection.update(v('div', [ + v('span', { enterAnimation: 'fadeIn' }) + ])); + + assert.isTrue(transitionStrategy.enter.calledWithExactly( + projection.domNode.firstChild, + match({}), + 'fadeIn' + )); + }); + + it('will be invoked when exitAnimation is provided as a string', () => { + const transitionStrategy = { enter: stub(), exit: stub() }; + const projection = dom.create( + v('div', [ + v('span', { exitAnimation: 'fadeOut' }) + ]), + projectorStub, + { transitions: transitionStrategy } + ); + + projection.update(v('div', [])); + + assert.isTrue(transitionStrategy.exit.calledWithExactly( + projection.domNode.firstChild, + match({}), + 'fadeOut', + match({}) + )); + + transitionStrategy.exit.lastCall.callArg(3); + assert.lengthOf(projection.domNode.childNodes, 0); + }); + + it('will complain about a missing transitionStrategy', () => { + const projection = dom.create(v('div'), projectorStub, {}); + + assert.throws(() => { + projection.update(v('div', [ + v('span', { enterAnimation: 'fadeIn' }) + ])); + }); + }); + + }); + + }); + +});