UNPKG

siesta-lite

Version:

Stress-free JavaScript unit testing and functional testing tool, works in NodeJS and browsers

590 lines (394 loc) 19.5 kB
/* Siesta 5.6.1 Copyright(c) 2009-2022 Bryntum AB https://bryntum.com/contact https://bryntum.com/products/siesta/license */ Role('Scope.Provider.Role.WithDOM', { requires : [ 'getDocument', 'create', 'getPreload', 'isAlreadySetUp', 'setViewportSize' ], has : { useStrictMode : true, sourceURL : null, crossOriginFailed : false, innerHtmlHead : null, innerHtmlBody : null, minViewportSize : null, parentWindow : function () { return window }, scopeId : function () { return Math.round(Math.random() * 1e10) }, failOnResourceLoadError : false, failOnPromiseRejection : true, // init function attachToOnError : function () { // returns the value of the attribute // the "handler" argument is no longer used, its now being taken from the __ONERROR__ handler every time return function (window, scopeId, handler, preventDefault, failOnResourceLoadError, failOnPromiseRejection) { handler = (window.opener || window.parent).Scope.Provider.__ONERROR__[ scopeId ] if (failOnResourceLoadError && ("ErrorEvent" in window)) { // if (window.ErrorEvent.__SIESTA_HOOK_INSTALLED__) return // http://stackoverflow.com/questions/8504673/how-to-detect-on-page-404-errors-using-javascript window.addEventListener('error', handler, true) // window.ErrorEvent.__SIESTA_HOOK_INSTALLED__ = true } else { var prevHandler = window.onerror if (prevHandler && prevHandler.__SP_MANAGED__) return // this, "managed" handler is basically a wrapper around the current value in the "__ONERROR__" hash window.onerror = function (message, url, lineNumber) { // prevent recursive calls if other authors politely has not overwrite the handler and call it if (handler.__CALLING__) return handler.__CALLING__ = true prevHandler && prevHandler.apply(this, arguments) handler.apply(this, arguments) handler.__CALLING__ = false // in FF/IE need to return `true` to prevent default action if (preventDefault !== false) return window.WebKitPoint ? false : true } window.onerror.__SP_MANAGED__ = true } if (failOnPromiseRejection && window.Promise && ('onunhandledrejection' in window)) { window.addEventListener('unhandledrejection', function (e) { handler.apply(this, [ null, null, null, null, null, e ]) }, true) } } }, // this is a "cached" onerror handler - a handler which was provided before the scope // has started the creation process - should be installed ASAP in the creation process // to allow catching of the exceptions in the scope with `sourceURL` cachedOnError : null }, override : { cleanup : function () { var onErrorHandler = this.cachedOnError this.cachedOnError = null // can throw exceptions for cross-domain case try { var scope = this.scope if (scope.ErrorEvent /*&& scope.ErrorEvent.__SIESTA_HOOK_INSTALLED__*/) scope.removeEventListener('error', onErrorHandler) scope.onerror = null } catch (e) { } this.SUPERARG(arguments) this.scope = null } }, methods : { cleanupHandlers : function () { var scopeProvider = this.parentWindow.Scope.Provider var scopeId = this.scopeId delete scopeProvider.__ONLOAD__[ scopeId ] delete scopeProvider.__ONERROR__[ scopeId ] delete scopeProvider.__FAILED_PRELOAD__[ scopeId ] }, getHead : function () { return this.getDocument().getElementsByTagName('head')[ 0 ] }, installOnErrorHandler : function (handler) { if (this.crossOriginFailed) return if (!this.isAlreadySetUp()) throw "Scope should be already set up" this.attachToOnError(this.scope, this.scopeId, handler, false, this.failOnResourceLoadError, this.failOnPromiseRejection) }, addOnErrorHandler : function (handler, preventDefault) { if (this.crossOriginFailed) return handler.__SP_MANAGED__ = true this.cachedOnError = handler var scopeId = this.scopeId this.parentWindow.Scope.Provider.__ONERROR__[ scopeId ] = handler var attachToOnError = ';(' + this.attachToOnError.toString() + ')(window, ' + scopeId + ', (window.opener || window.parent).Scope.Provider.__ONERROR__[ ' + scopeId + ' ], ' + preventDefault + ', ' + this.failOnResourceLoadError + ', ' + this.failOnPromiseRejection + ');' if (this.isAlreadySetUp()) this.runCode(attachToOnError) else { // this is a fallback - run the "attachToOnError" from inside of scope this.getPreload().unshift({ type : 'js', content : attachToOnError, unordered : true }) } }, addSeedingToPreload : function () { var preload = this.getPreload() if (this.seedingCode) preload.unshift({ type : 'js', content : this.seedingCode }) if (this.seedingScript) preload.push({ type : 'js', url : this.seedingScript, isEcmaModule : this.seedingScriptIsEcmaModule }) }, setup : function (callback) { var isIE = 'v' == '\v' || Boolean(this.parentWindow.msWriteProfilerMark) // var isOpera = Object.prototype.toString.call(this.parentWindow.opera) == '[object Opera]' var hasInlineScript = false Joose.A.each(this.getPreload(), function (preloadDesc) { // IE will execute the inline scripts ASAP, this might be not what we want (inline script might be need executed only after some url script) // its however ok in some cases (like adding `onerror` handler // such inline scripts should be marked with `unordered` - true if (preloadDesc.type == 'js' && preloadDesc.content && !preloadDesc.unordered) { hasInlineScript = true return false } }) var me = this var cont = function (e) { callback && callback(me, me.parentWindow.Scope.Provider.__FAILED_PRELOAD__[ me.scopeId ], e) } this.parentWindow.Scope.Provider.__FAILED_PRELOAD__[ this.scopeId ] = {} if (this.sourceURL || isIE && hasInlineScript) { this.addSeedingToPreload() this.setupIncrementally(cont) } else { // for sane browsers just add the seeding code and seeding script to preloads if (!isIE) this.addSeedingToPreload() // seeding scripts are included only for sane browsers (not IE) this.setupWithDocWrite(cont, isIE) } }, setupWithDocWrite : function (callback, needToSeed) { var html = [] var me = this Joose.A.each(this.getPreload(), function (preloadDesc) { if (preloadDesc.type == 'js') html.push( me.getScriptTagString(preloadDesc.url, preloadDesc.content, preloadDesc.isEcmaModule) ) else if (preloadDesc.type == 'css') html.push( me.getLinkTagString(preloadDesc.url, preloadDesc.content) ) else throw "Incorrect preload descriptor " + preloadDesc }) // no need to wait for DOM ready - we'll overwrite it anyway this.create() var scopeId = this.scopeId this.parentWindow.Scope.Provider.__ONLOAD__[ scopeId ] = function () { var cont = function () { callback && callback() } // sane browsers - seeding code and script has been already added if (!needToSeed) { cont(); return } // our beloved IE - manually seeding the scope if (me.seedingCode) me.runCode(me.seedingCode, null) if (me.seedingScript) me.runScript(me.seedingScript, cont, me.seedingScriptIsEcmaModule) else cont() } var doc = this.getDocument() doc.open() doc.write([ this.useStrictMode ? '<!DOCTYPE html>' : '', '<html style="width: 100%; height: 100%; margin : 0; padding : 0;">', '<head>', this.innerHtmlHead || '', html.join(''), '</head>', // delay here is for IE9 - the "onerror" handlers of the <script> tags are fired _after_ <body> onload otherwise '<body style="margin : 0; padding : 0; width: 100%; height: 100%" onload="setTimeout(function () { (window.opener || window.parent).Scope.Provider.__ONLOAD__[' + scopeId + ']() }, 0)">', this.innerHtmlBody || '', '</body>', '</html>' ].join('')) doc.close() // Chrome (Webkit?) will clear the `onerror` after "doc.open()/.close()" so need to re-install it if (me.cachedOnError) me.installOnErrorHandler(me.cachedOnError) }, setupIncrementally : function (callback) { // here the "onerror" should be included early in the "preloads" this.create(function (me, e) { if (e) { callback && callback(e) return } if (!me.sourceURL) { var doc = me.getDocument() if (me.innerHtmlHead) { var head = me.getHead() // IE9 throws exception when accessing innerHTML of the <head> - its read only try { head.innerHTML = me.innerHtmlHead } catch (e) { var div = doc.createElement('div') div.innerHTML = me.innerHtmlHead for (var i = 0; i < div.children.length; i++) head.appendChild(div.children[ i ]) } } if (me.innerHtmlBody) doc.body.innerHTML = me.innerHtmlBody } var loadScripts = function (preloads, callback) { var cont = function () { // cleanup can happen in the middle of setup if (me.scope) loadScripts(preloads, callback) } if (!preloads.length) callback && callback() else { var preloadDesc = preloads.shift() if (preloadDesc.url) me.runScript(preloadDesc.url, cont, preloadDesc.isEcmaModule) else if (preloadDesc.type == 'js') me.runCode(preloadDesc.content, cont, preloadDesc.isEcmaModule) else { me.addStyleTag(preloadDesc.content) cont() } } } // cleanup can happen in the middle of setup if (me.scope) loadScripts(me.getPreload().slice(), callback) }) }, getScriptTagString : function (url, text, isEcmaModule) { var type = isEcmaModule ? 'module' : 'text/javascript', res = '<script type="' + type + '"' + (isEcmaModule ? ' crossorigin="anonymous"' : '') var onerror = '(window.opener || window.parent).Scope.Provider.__FAILED_PRELOAD__[ scopeId ][ url ] = true' onerror = onerror.replace(/scopeId/, "'" + this.scopeId + "'").replace(/url/, "'" + url + "'") if (url) res += ' src="' + url + '" onerror="' + onerror + '"></script>' else res += '>' + text.replace(/<\/script>/gi, '\\x3C/script>') + '</script>' return res }, getLinkTagString : function (url, text) { if (url) return '<link href="' + url + '" rel="stylesheet" type="text/css" />' if (text) return '<style>' + text + '</style>' }, loadCSS : function (url, callback) { var doc = this.getDocument() var link = doc.createElement('link') link.type = 'text/css' link.rel = 'stylesheet' link.href = url var hasOnLoad = false if ('onload' in link) { hasOnLoad = true link.onload = function () { link.onload = null if (callback) callback() callback = null link = null } } this.getHead().appendChild(link) if (!hasOnLoad) { var hasContinued = false var cont = function () { // just in case some crazy JS engine calls `onerror` even after node removal if (hasContinued) return hasContinued = true clearTimeout(forcedTimeout) if (callback) callback() doc.body.removeChild(img) } var forcedTimeout = setTimeout(cont, 30000) var img = doc.createElement('img') img.onerror = cont doc.body.appendChild(img) img.src = url } }, runCode : function (text, callback, isEcmaModule) { if (this.crossOriginFailed) { callback && callback() return } this.getHead().appendChild(this.createScriptTag(text, null, null, null, isEcmaModule)) callback && callback() }, runScript : function (url, callback, isEcmaModule) { if (this.crossOriginFailed) { callback && callback() return } var scopeId = this.scopeId if (this.isCSS(url)) this.loadCSS(url, callback) else { var onerror = function () { this.onerror = null var doc = this.ownerDocument var win = doc.defaultView || doc.parentWindow ;(win.opener || win.parent).Scope.Provider.__FAILED_PRELOAD__[ scopeId ][ url ] = true callback() } this.getHead().appendChild(this.createScriptTag(null, url, callback, onerror, isEcmaModule)) } }, createScriptTag : function (text, url, callback, errback, isEcmaModule) { var node = this.getDocument().createElement("script") node.setAttribute("type", isEcmaModule ? 'module' : 'text/javascript') isEcmaModule && node.setAttribute("crossorigin", "anonymous") if (url) node.setAttribute("src", url) if (text) node.text = text if (callback) node.onload = node.onreadystatechange = function () { if (!node.readyState || node.readyState == "loaded" || node.readyState == "complete" || node.readyState == 4 && node.status == 200) { node.onload = node.onreadystatechange = null //surely for IE6.. if ('v' == '\v') setTimeout(callback, 0) else callback() } } if (errback) node.onerror = errback return node }, addStyleTag : function (text) { var document = this.getDocument() var node = document.createElement('style') node.setAttribute("type", "text/css") var head = document.getElementsByTagName('head')[0] head.appendChild(node) if (node.styleSheet) { // IE node.styleSheet.cssText = text } else { // the world node.appendChild(document.createTextNode(text)) } } } }) /** Name ==== Scope.Provider.Role.WithDOM - role for scope provider, which uses `script` tag for running the code. SYNOPSIS ======== Class('Scope.Provider.IFrame', { isa : Scope.Provider, does : Scope.Provider.Role.WithDOM, ... }) DESCRIPTION =========== `Scope.Provider.Role.WithDOM` requires the implementation of the `getDocument` method, which should return the document into which the `script` tags will be created. In return, this role provides the implementation of `runCode` and `runScript`. GETTING HELP ============ This extension is supported via github issues tracker: <http://github.com/SamuraiJack/Scope-Provider/issues> For general Joose questions you can also visit [#joose](http://webchat.freenode.net/?randomnick=1&channels=joose&prompt=1) on irc.freenode.org or the forum at: <http://joose.it/forum> SEE ALSO ======== Web page of this module: <http://github.com/SamuraiJack/Scope-Provider/> General documentation for Joose: <http://joose.github.com/Joose> BUGS ==== All complex software has bugs lurking in it, and this module is no exception. Please report any bugs through the web interface at <http://github.com/SamuraiJack/Scope-Provider/issues> AUTHORS ======= Nickolay Platonov <nplatonov@cpan.org> COPYRIGHT AND LICENSE ===================== This software is Copyright (c) 2010 by Nickolay Platonov <nplatonov@cpan.org>. This is free software, licensed under: The GNU Lesser General Public License, Version 3, June 2007 */