From 2ae6907b32487db706d2ff3777c155e95808a560 Mon Sep 17 00:00:00 2001 From: Kyle Simpson Date: Sun, 26 Mar 2017 09:50:52 -0500 Subject: [PATCH] more work on mock dom loading, per #113 --- mock-dom-resource-loading.js | 226 ++++++++++++++++++++++++++++++----- 1 file changed, 198 insertions(+), 28 deletions(-) diff --git a/mock-dom-resource-loading.js b/mock-dom-resource-loading.js index a660dff..55906c4 100644 --- a/mock-dom-resource-loading.js +++ b/mock-dom-resource-loading.js @@ -13,15 +13,33 @@ // ************************************** function createMockDOM(opts) { - opts = opts || {}; + opts = opts ? JSON.parse( JSON.stringify( opts ) ) : {}; if (!("relList" in opts)) opts.relList = true; if (!("scriptAsync" in opts)) opts.scriptAsync = true; if (!("linkPreload" in opts)) opts.linkPreload = true; if (!("baseURI" in opts)) opts.baseURI = ""; + if (!("log" in opts)) opts.log = function log(status) { console.log( JSON.stringify( status ) ); } + if (!("error" in opts)) opts.error = function error(err) { throw err; }; + if (!("resources" in opts)) opts.resources = []; + + // setup Element prototype + Element.prototype.getElementsByTagName = getElementsByTagName; + Element.prototype.appendChild = appendChild; + Element.prototype.removeChild = removeChild; + Element.prototype.setAttribute = setAttribute; + Element.prototype.getAttribute = getAttribute; + Element.prototype.addEventListener = addEventListener; + Element.prototype.removeEventListener = removeEventListener; + Element.prototype.dispatchEvent = dispatchEvent; + + var loadQueue = []; + var silent = true; var documentElement = createElement( "document" ); documentElement.head = createElement( "head" ); documentElement.body = createElement( "body" ); + documentElement.appendChild( documentElement.head ); + documentElement.appendChild( documentElement.body ); documentElement.baseURI = opts.baseURI; documentElement.createElement = createElement; @@ -33,14 +51,13 @@ mockDOM.document = documentElement; mockDOM.performance = performanceAPI; - // setup Element prototype - Element.prototype.getElementsByTagName = getElementsByTagName; - Element.prototype.appendChild = appendChild; - Element.prototype.removeChild = removeChild; - Element.prototype.setAttribute = setAttribute; - Element.prototype.getAttribute = getAttribute; - Element.prototype.addEventListener = addEventListener; - Element.prototype.removeEventListener = removeEventListener; + silent = false; + + // notify: internal IDs for built-ins + opts.log( {window: mockDOM._internal_id} ); + opts.log( {document: documentElement._internal_id} ); + opts.log( {head: documentElement.head._internal_id} ); + opts.log( {body: documentElement.body._internal_id} ); return mockDOM; @@ -54,9 +71,17 @@ var element = new Element(); element.tagName = tagName.toUpperCase(); + !silent && opts.log( {createElement: tagName, internal_id: element._internal_id} ); + if (tagName == "script") { element.async = !!opts.scriptAsync; } + if (tagName == "link") { + element.href = ""; + } + if (/^(?:script|img)$/.test( tagName )) { + element.src = ""; + } return element; } @@ -73,27 +98,55 @@ } // ************ - this.tagNameTagNameNodeLists = {}; + this._internal_id = Math.floor( Math.random() * 1E6 ); + this._tagNameNodeLists = {}; this._eventHandlers = {}; } // Element#getElementsByTagName(..) function getElementsByTagName(tagName) { - this.tagNameTagNameNodeLists[tagName] = this.tagNameTagNameNodeLists[tagName] || []; - return this.tagNameTagNameNodeLists[tagName]; + this._tagNameNodeLists[tagName] = this._tagNameNodeLists[tagName] || []; + return this._tagNameNodeLists[tagName]; } // Element#appendChild(..) function appendChild(childElement) { + !silent && opts.log( {appendChild: childElement._internal_id, internal_id: this._internal_id} ); + this.childNodes.push( childElement ); childElement.parentNode = this; updateTagNameNodeLists( childElement ); - if (childElement.tagName == "link" && childElement.rel == "preload") { - // TODO: simulate resource preloading + if (childElement.tagName.toLowerCase() == "link" && childElement.rel == "preload") { + var resource = findMatchingOptResource( childElement.href ); + + if (resource) { + fakePreload( resource, childElement ); + } + else { + opts.error( new Error( "appendChild: Preload resource not found (" + childElement.href + "; " + childElement._internal_id + ")" ) ); + } + } + else if (/^(?:script|link|img)$/i.test( childElement.tagName )) { + var url = (/^(?:script|img)$/i.test( childElement.tagName )) ? + childElement.src : + childElement.href; + var resource = findMatchingOptResource( url ); + + if (resource) { + // track load-order queue (for ordered-async on scripts)? + if (opts.scriptAsync && childElement.tagName.toLowerCase() == "script" && childElement.async === false) { + loadQueue.push( {url: url, element: childElement} ); + } + + fakeLoad( resource, childElement ); + } + else { + opts.error( new Error( "appendChild: Load resource not found (" + url + "; " + childElement._internal_id + ")" ) ); + } } - else if (/^(?:script|link|img)$/.test( childElement.tagName )) { - // TODO: simulate resource loading + else { + !silent && opts.error( new Error( "appendChild: Unrecognized tag (" + childElement.tagName + "; " + childElement._internal_id + ")" ) ); } return childElement; @@ -101,6 +154,8 @@ // Element#removeChild(..) function removeChild(childElement) { + opts.log( {removeChild: childElement._internal_id, internal_id: this._internal_id} ); + var idx = this.childNodes.indexOf( childElement ); if (idx != -1) { this.childNodes.splice( idx, 1 ); @@ -113,6 +168,8 @@ // Element#setAttribute(..) function setAttribute(attrName,attrValue) { + opts.log( {setAttribute: attrName + " | " + attrValue, internal_id: this._internal_id} ); + this[attrName] = attrValue; } @@ -123,21 +180,36 @@ // Element#addEventListener(..) function addEventListener(evtName,handler) { - this._evtHandlers[evtName] = this._evtHandlers[evtName] || []; - this._evtHandlers[evtName].push( handler ); + opts.log( {addEventListener: evtName, internal_id: this._internal_id} ); + + this._eventHandlers[evtName] = this._eventHandlers[evtName] || []; + this._eventHandlers[evtName].push( handler ); } // Element#removeEventListener(..) function removeEventListener(evtName,handler) { - if (this._evtHandlers[evtName]) { - var idx = this._evtHandlers[evtName].indexOf( handler ); - this._evtHandlers[evtName].splice( idx, 1 ); + opts.log( {removeEventListener: evtName, internal_id: this._internal_id} ); + + if (this._eventHandlers[evtName]) { + var idx = this._eventHandlers[evtName].indexOf( handler ); + this._eventHandlers[evtName].splice( idx, 1 ); + } + } + + // Element#dispatchEvent(..) + function dispatchEvent(evt) { + opts.log( {dispatchEvent: evt.type, internal_id: this._internal_id} ); + + if (this._eventHandlers[evt.type]) { + for (var i = 0; i < this._eventHandlers[evt.type].length; i++) { + try { this._eventHandlers[evt.type][i].call( this, evt ); } catch (err) {} + } } } // Element#relList.supports(..) - function supports(feature) { - if (feature == "preload" && opts.linkPreload && this._parent.tagName == "link") { + function supports(rel) { + if (rel == "preload" && opts.linkPreload && this._parent.tagName.toLowerCase() == "link") { return true; } return false; @@ -156,9 +228,9 @@ // recursively walk up the element tree while (el != null) { - el.tagNameTagNameNodeLists[element.tagName] = el.tagNameTagNameNodeLists[element.tagName] || []; - if (!~el.tagNameTagNameNodeLists[element.tagName].indexOf( element )) { - el.tagNameTagNameNodeLists[element.tagName].push( element ); + el._tagNameNodeLists[element.tagName] = el._tagNameNodeLists[element.tagName] || []; + if (!~el._tagNameNodeLists[element.tagName].indexOf( element )) { + el._tagNameNodeLists[element.tagName].push( element ); } el = el.parentNode; } @@ -170,11 +242,109 @@ // recursively walk up the element tree while (el != null) { var idx; - if (el.tagNameTagNameNodeLists[element.tagName] && (idx = el.tagNameTagNameNodeLists[element.tagName].indexOf( element )) != -1) { - el.tagNameTagNameNodeLists[element.tagName].splice( idx, 1 ); + if (el._tagNameNodeLists[element.tagName] && (idx = el._tagNameNodeLists[element.tagName].indexOf( element )) != -1) { + el._tagNameNodeLists[element.tagName].splice( idx, 1 ); } el = el.parentNode; } } + + function findMatchingOptResource(url) { + for (var i = 0; i < opts.resources.length; i++) { + if (opts.resources[i].url == url) { + return opts.resources[i]; + } + } + } + + function fakePreload(resource,element) { + setTimeout( function preload(){ + if (resource.preload === true) { + var evt = createEvent( "load", element ); + } + else { + var evt = createEvent( "error", element ); + } + + element.dispatchEvent( evt ); + }, resource.preloadDelay || 0 ); + } + + function fakeLoad(resource,element) { + if (resource.load === true) { + var evt = createEvent( "load", element ); + } + else { + var evt = createEvent( "error", element ); + } + + setTimeout( function load(){ + // simulating ordered-async for