diff --git a/src/CustomElements/CustomElements.js b/src/CustomElements/CustomElements.js new file mode 100644 index 0000000..04ce503 --- /dev/null +++ b/src/CustomElements/CustomElements.js @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2014 The Polymer Project Authors. All rights reserved. + * This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt + * The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt + * The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt + * Code distributed by Google as part of the polymer project is also + * subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt + */ +(function() { + +// Estblish polyfill scope. We do this here to store flags. Flags are not +// supported in the build. +window.CustomElements = window.CustomElements || {flags:{}}; + +// Flags. Convert url arguments to flags +var flags = {}; +if (!flags.noOpts) { + location.search.slice(1).split('&').forEach(function(o) { + o = o.split('='); + o[0] && (flags[o[0]] = o[1] || true); + }); +} + + +// Load. +var file = 'CustomElements.js'; + +var modules = [ + '../WeakMap/WeakMap.js', + '../MutationObserver/MutationObserver.js', + 'base.js', + 'traverse.js', + 'observe.js', + 'upgrade.js', + 'register.js', + 'boot.js' +]; + +var src = + document.querySelector('script[src*="' + file + '"]').getAttribute('src'); +var basePath = src.slice(0, src.indexOf(file)); + +modules.forEach(function(f) { + document.write(''); +}); + +// exports +CustomElements.flags = flags; + +})(); \ No newline at end of file diff --git a/src/CustomElements/base.js b/src/CustomElements/base.js new file mode 100644 index 0000000..a971763 --- /dev/null +++ b/src/CustomElements/base.js @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2014 The Polymer Project Authors. All rights reserved. + * This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt + * The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt + * The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt + * Code distributed by Google as part of the polymer project is also + * subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt + */ +window.CustomElements = window.CustomElements || {flags:{}}; + +(function(scope) { + +// imports +var flags = scope.flags; + +// world's simplest module initializer +var modules = []; +var addModule = function(module) { + modules.push(module); +}; + +var initializeModules = function() { + modules.forEach(function(module) { + module(scope); + }); +}; + +// exports +scope.addModule = addModule; +scope.initializeModules = initializeModules; +scope.hasNative = Boolean(document.registerElement); + +// NOTE: For consistent timing, use native custom elements only when not +// polyfilling other key related web components features. +scope.useNative = !flags.register && scope.hasNative && + !window.ShadowDOMPolyfill && (!window.HTMLImports || HTMLImports.useNative); + +})(CustomElements); \ No newline at end of file diff --git a/src/CustomElements/boot.js b/src/CustomElements/boot.js new file mode 100644 index 0000000..11273c1 --- /dev/null +++ b/src/CustomElements/boot.js @@ -0,0 +1,113 @@ +/* + * Copyright (c) 2014 The Polymer Project Authors. All rights reserved. + * This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt + * The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt + * The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt + * Code distributed by Google as part of the polymer project is also + * subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt + */ +(function(scope){ + +// imports +var useNative = scope.useNative; +var initializeModules = scope.initializeModules; + +// If native, setup stub api and bail. +// NOTE: we fire `WebComponentsReady` under native for api compatibility +if (useNative) { + // stub + var nop = function() {}; + + // exports + scope.watchShadow = nop; + scope.upgradeAll = nop; + scope.upgradeDocumentTree = nop; + scope.takeRecords = nop; + scope.instanceof = function(obj, base) { + return obj instanceof base; + }; + +} else { + // Initialize polyfill modules. Note, polyfill modules are loaded but not + // executed; this is a convenient way to control which modules run when + // the polyfill is required and allows the polyfill to load even when it's + // not needed. + initializeModules(); +} + +// imports +var upgradeDocumentTree = scope.upgradeDocumentTree; + +// ShadowDOM polyfill wraps elements but some elements like `document` +// cannot be wrapped so we help the polyfill by wrapping some elements. +if (!window.wrap) { + if (window.ShadowDOMPolyfill) { + window.wrap = ShadowDOMPolyfill.wrapIfNeeded; + window.unwrap = ShadowDOMPolyfill.unwrapIfNeeded; + } else { + window.wrap = window.unwrap = function(node) { + return node; + }; + } +} + +// bootstrap parsing +function bootstrap() { + // parse document + upgradeDocumentTree(wrap(document)); + // install upgrade hook if HTMLImports are available + if (window.HTMLImports) { + HTMLImports.__importsParsingHook = function(elt) { + upgradeDocumentTree(wrap(elt.import)); + //CustomElements.parser.parse(elt.import); + }; + } + // set internal 'ready' flag, now document.registerElement will trigger + // synchronous upgrades + CustomElements.ready = true; + // async to ensure *native* custom elements upgrade prior to this + // DOMContentLoaded can fire before elements upgrade (e.g. when there's + // an external script) + setTimeout(function() { + // capture blunt profiling data + CustomElements.readyTime = Date.now(); + if (window.HTMLImports) { + CustomElements.elapsed = CustomElements.readyTime - HTMLImports.readyTime; + } + // notify the system that we are bootstrapped + document.dispatchEvent( + new CustomEvent('WebComponentsReady', {bubbles: true}) + ); + }); +} + +// CustomEvent shim for IE +if (typeof window.CustomEvent !== 'function') { + window.CustomEvent = function(inType, params) { + params = params || {}; + var e = document.createEvent('CustomEvent'); + e.initCustomEvent(inType, Boolean(params.bubbles), Boolean(params.cancelable), params.detail); + return e; + }; + window.CustomEvent.prototype = window.Event.prototype; +} + +// When loading at readyState complete time (or via flag), boot custom elements +// immediately. +// If relevant, HTMLImports must already be loaded. +if (document.readyState === 'complete' || scope.flags.eager) { + bootstrap(); +// When loading at readyState interactive time, bootstrap only if HTMLImports +// are not pending. Also avoid IE as the semantics of this state are unreliable. +} else if (document.readyState === 'interactive' && !window.attachEvent && + (!window.HTMLImports || window.HTMLImports.ready)) { + bootstrap(); +// When loading at other readyStates, wait for the appropriate DOM event to +// bootstrap. +} else { + var loadEvent = window.HTMLImports && !HTMLImports.ready ? + 'HTMLImportsLoaded' : 'DOMContentLoaded'; + window.addEventListener(loadEvent, bootstrap); +} + +})(window.CustomElements); diff --git a/src/CustomElements/observe.js b/src/CustomElements/observe.js new file mode 100644 index 0000000..6530f7f --- /dev/null +++ b/src/CustomElements/observe.js @@ -0,0 +1,307 @@ +/* + * Copyright (c) 2014 The Polymer Project Authors. All rights reserved. + * This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt + * The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt + * The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt + * Code distributed by Google as part of the polymer project is also + * subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt + */ + +/** + * Implements custom element observation and attached/detached callbacks + * @module observe +*/ + +CustomElements.addModule(function(scope){ + +// imports +var flags = scope.flags; +var forSubtree = scope.forSubtree; +var forDocumentTree = scope.forDocumentTree; + +/* + Manage nodes attached to document trees +*/ + +// manage lifecycle on added node and it's subtree; upgrade the node and +// entire subtree if necessary and process attached for the node and entire +// subtree +function addedNode(node) { + return added(node) || addedSubtree(node); +} + +// manage lifecycle on added node; upgrade if necessary and process attached +function added(node) { + if (scope.upgrade(node)) { + return true; + } + attached(node); +} + +// manage lifecycle on added node's subtree only; allows the entire subtree +// to upgrade if necessary and process attached +function addedSubtree(node) { + forSubtree(node, function(e) { + if (added(e)) { + return true; + } + }); +} + +function attachedNode(node) { + attached(node); + // only check subtree if node is actually in document + if (inDocument(node)) { + forSubtree(node, function(e) { + attached(e); + }); + } +} + +// On platforms without MutationObserver, mutations may not be +// reliable and therefore attached/detached are not reliable. +// To make these callbacks less likely to fail, we defer all inserts and removes +// to give a chance for elements to be attached into dom. +// This ensures attachedCallback fires for elements that are created and +// immediately added to dom. +var hasPolyfillMutations = (!window.MutationObserver || + (window.MutationObserver === window.JsMutationObserver)); +scope.hasPolyfillMutations = hasPolyfillMutations; + +var isPendingMutations = false; +var pendingMutations = []; +function deferMutation(fn) { + pendingMutations.push(fn); + if (!isPendingMutations) { + isPendingMutations = true; + setTimeout(takeMutations); + } +} + +function takeMutations() { + isPendingMutations = false; + var $p = pendingMutations; + for (var i=0, l=$p.length, p; (i= 0) { + implementPrototype(element, HTMLElement); + } + return element; +} + +function cloneNode(deep) { + // call original clone + var n = domCloneNode.call(this, deep); + // upgrade the element and subtree + upgrade(n); + // return the clone + return n; +} + +// capture native createElement before we override it +var domCreateElement = document.createElement.bind(document); +var domCreateElementNS = document.createElementNS.bind(document); +// capture native cloneNode before we override it +var domCloneNode = Node.prototype.cloneNode; + +// Create a custom 'instanceof'. This is necessary when CustomElements +// are implemented via a mixin strategy, as for example on IE10. +var isInstance; +if (!Object.__proto__ && !useNative) { + isInstance = function(obj, ctor) { + var p = obj; + while (p) { + // NOTE: this is not technically correct since we're not checking if + // an object is an instance of a constructor; however, this should + // be good enough for the mixin strategy. + if (p === ctor.prototype) { + return true; + } + p = p.__proto__; + } + return false; + }; +} else { + isInstance = function(obj, base) { + return obj instanceof base; + }; +} + +// exports +document.registerElement = register; +document.createElement = createElement; // override +document.createElementNS = createElementNS; // override +Node.prototype.cloneNode = cloneNode; // override +scope.registry = registry; +scope.instanceof = isInstance; +scope.reservedTagList = reservedTagList; +scope.getRegisteredDefinition = getRegisteredDefinition; + +// bc +document.register = document.registerElement; + +}); diff --git a/src/CustomElements/traverse.js b/src/CustomElements/traverse.js new file mode 100644 index 0000000..8dbada5 --- /dev/null +++ b/src/CustomElements/traverse.js @@ -0,0 +1,92 @@ +/* + * Copyright (c) 2014 The Polymer Project Authors. All rights reserved. + * This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt + * The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt + * The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt + * Code distributed by Google as part of the polymer project is also + * subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt + */ + +// helper methods for traversing through element trees +CustomElements.addModule(function(scope){ + +// imports +var IMPORT_LINK_TYPE = window.HTMLImports ? HTMLImports.IMPORT_LINK_TYPE : 'none'; + +// walk the subtree rooted at node, including descent into shadow-roots, +// applying 'cb' to each element +function forSubtree(node, cb) { + //flags.dom && node.childNodes && node.childNodes.length && console.group('subTree: ', node); + findAllElements(node, function(e) { + if (cb(e)) { + return true; + } + forRoots(e, cb); + }); + forRoots(node, cb); + //flags.dom && node.childNodes && node.childNodes.length && console.groupEnd(); +} + + +// walk the subtree rooted at node, applying 'find(element, data)' function +// to each element +// if 'find' returns true for 'element', do not search element's subtree +function findAllElements(node, find, data) { + var e = node.firstElementChild; + if (!e) { + e = node.firstChild; + while (e && e.nodeType !== Node.ELEMENT_NODE) { + e = e.nextSibling; + } + } + while (e) { + if (find(e, data) !== true) { + findAllElements(e, find, data); + } + e = e.nextElementSibling; + } + return null; +} + +// walk all shadowRoots on a given node. +function forRoots(node, cb) { + var root = node.shadowRoot; + while(root) { + forSubtree(root, cb); + root = root.olderShadowRoot; + } +} + +/* +Note that the import tree can consume itself and therefore special care +must be taken to avoid recursion. +*/ +var processingDocuments; +function forDocumentTree(doc, cb) { + processingDocuments = []; + _forDocumentTree(doc, cb); + processingDocuments = null; +} + + +function _forDocumentTree(doc, cb) { + doc = wrap(doc); + if (processingDocuments.indexOf(doc) >= 0) { + return; + } + processingDocuments.push(doc); + var imports = doc.querySelectorAll('link[rel=' + IMPORT_LINK_TYPE + ']'); + for (var i=0, l=imports.length, n; (i'); +}); + +// exports +HTMLImports.flags = flags; + +})(); \ No newline at end of file diff --git a/src/HTMLImports/Loader.js b/src/HTMLImports/Loader.js new file mode 100644 index 0000000..e5c2a1c --- /dev/null +++ b/src/HTMLImports/Loader.js @@ -0,0 +1,135 @@ +/* + * Copyright (c) 2014 The Polymer Project Authors. All rights reserved. + * This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt + * The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt + * The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt + * Code distributed by Google as part of the polymer project is also + * subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt + */ +HTMLImports.addModule(function(scope) { + +// imports +var xhr = scope.xhr; +var flags = scope.flags; + +// This loader supports a dynamic list of urls +// and an oncomplete callback that is called when the loader is done. +// NOTE: The polyfill currently does *not* need this dynamism or the +// onComplete concept. Because of this, the loader could be simplified +// quite a bit. +var Loader = function(onLoad, onComplete) { + this.cache = {}; + this.onload = onLoad; + this.oncomplete = onComplete; + this.inflight = 0; + this.pending = {}; +}; + +Loader.prototype = { + + addNodes: function(nodes) { + // number of transactions to complete + this.inflight += nodes.length; + // commence transactions + for (var i=0, l=nodes.length, n; (i -1) { + body = atob(body); + } else { + body = decodeURIComponent(body); + } + setTimeout(function() { + this.receive(url, elt, null, body); + }.bind(this), 0); + } else { + var receiveXhr = function(err, resource, redirectedUrl) { + this.receive(url, elt, err, resource, redirectedUrl); + }.bind(this); + xhr.load(url, receiveXhr); + } + }, + + receive: function(url, elt, err, resource, redirectedUrl) { + this.cache[url] = resource; + var $p = this.pending[url]; + for (var i=0, l=$p.length, p; (i when we ensure the document is in a ready state +function whenDocumentReady(callback, doc) { + if (!isDocumentReady(doc)) { + var checkReady = function() { + if (doc.readyState === 'complete' || + doc.readyState === requiredReadyState) { + doc.removeEventListener(READY_EVENT, checkReady); + whenDocumentReady(callback, doc); + } + }; + doc.addEventListener(READY_EVENT, checkReady); + } else if (callback) { + callback(); + } +} + +function markTargetLoaded(event) { + event.target.__loaded = true; +} + +// call when we ensure all imports have loaded +function watchImportsLoad(callback, doc) { + var imports = doc.querySelectorAll('link[rel=import]'); + var loaded = 0, l = imports.length; + function checkDone(d) { + if ((loaded == l) && callback) { + callback(); + } + } + function loadedImport(e) { + markTargetLoaded(e); + loaded++; + checkDone(); + } + if (l) { + for (var i=0, imp; (i.content + // see https://code.google.com/p/chromium/issues/detail?id=249381. + elt.__resource = resource; + elt.__error = err; + if (isImportLink(elt)) { + var doc = this.documents[url]; + // if we've never seen a document at this url + if (doc === undefined) { + // generate an HTMLDocument from data + doc = err ? null : makeDocument(resource, redirectedUrl || url); + if (doc) { + doc.__importLink = elt; + // note, we cannot use MO to detect parsed nodes because + // SD polyfill does not report these as mutations. + this.bootDocument(doc); + } + // cache document + this.documents[url] = doc; + } + // don't store import record until we're actually loaded + // store document resource + elt.import = doc; + } + parser.parseNext(); + }, + + bootDocument: function(doc) { + this.loadSubtree(doc); + // observe documents for new elements being added + this.observer.observe(doc); + parser.parseNext(); + }, + + loadedAll: function() { + parser.parseNext(); + } + +}; + +// loader singleton to handle loading imports +var importLoader = new Loader(importer.loaded.bind(importer), + importer.loadedAll.bind(importer)); + +// observer singleton to handle observing elements in imports +// NOTE: the observer has a node added callback and this is set +// by the dynamic importer module. +importer.observer = new Observer(); + +function isImportLink(elt) { + return isLinkRel(elt, IMPORT_LINK_TYPE); +} + +function isLinkRel(elt, rel) { + return elt.localName === 'link' && elt.getAttribute('rel') === rel; +} + +function makeDocument(resource, url) { + // create a new HTML document + var doc = document.implementation.createHTMLDocument(IMPORT_LINK_TYPE); + // cache the new document's source url + doc._URL = url; + // establish a relative path via + var base = doc.createElement('base'); + base.setAttribute('href', url); + // add baseURI support to browsers (IE) that lack it. + if (!doc.baseURI) { + doc.baseURI = url; + } + // ensure UTF-8 charset + var meta = doc.createElement('meta'); + meta.setAttribute('charset', 'utf-8'); + + doc.head.appendChild(meta); + doc.head.appendChild(base); + // install html + doc.body.innerHTML = resource; + // TODO(sorvell): ideally this code is not aware of Template polyfill, + // but for now the polyfill needs help to bootstrap these templates + if (window.HTMLTemplateElement && HTMLTemplateElement.bootstrap) { + HTMLTemplateElement.bootstrap(doc); + } + return doc; +} + +// Polyfill document.baseURI for browsers without it. +if (!document.baseURI) { + var baseURIDescriptor = { + get: function() { + var base = document.querySelector('base'); + return base ? base.href : window.location.href; + }, + configurable: true + }; + + Object.defineProperty(document, 'baseURI', baseURIDescriptor); + Object.defineProperty(rootDocument, 'baseURI', baseURIDescriptor); +} + +// exports +scope.importer = importer; +scope.importLoader = importLoader; + +}); \ No newline at end of file diff --git a/src/HTMLImports/parser.js b/src/HTMLImports/parser.js new file mode 100644 index 0000000..034901c --- /dev/null +++ b/src/HTMLImports/parser.js @@ -0,0 +1,334 @@ +/* + * Copyright (c) 2014 The Polymer Project Authors. All rights reserved. + * This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt + * The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt + * The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt + * Code distributed by Google as part of the polymer project is also + * subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt + */ +HTMLImports.addModule(function(scope) { + +// imports +var path = scope.path; +var rootDocument = scope.rootDocument; +var flags = scope.flags; +var isIE = scope.isIE; +var IMPORT_LINK_TYPE = scope.IMPORT_LINK_TYPE; +var IMPORT_SELECTOR = 'link[rel=' + IMPORT_LINK_TYPE + ']'; + +// importParser +// highlander object to manage parsing of imports +// parses import related elements and ensures proper parse order +// parse order is enforced by crawling the tree and monitoring which elements +// have been parsed; +// elements can be dynamically added to imports. These are maintained in a +// separate queue and parsed after all other elements. +var importParser = { + + // parse selectors for main document elements + documentSelectors: IMPORT_SELECTOR, + + // parse selectors for import document elements + importsSelectors: [ + IMPORT_SELECTOR, + 'link[rel=stylesheet]', + 'style', + 'script:not([type])', + 'script[type="text/javascript"]' + ].join(','), + + map: { + link: 'parseLink', + script: 'parseScript', + style: 'parseStyle' + }, + + dynamicElements: [], + + // try to parse the next import in the tree + parseNext: function() { + var next = this.nextToParse(); + if (next) { + this.parse(next); + } + }, + + parse: function(elt) { + if (this.isParsed(elt)) { + flags.parse && console.log('[%s] is already parsed', elt.localName); + return; + } + var fn = this[this.map[elt.localName]]; + if (fn) { + this.markParsing(elt); + fn.call(this, elt); + } + }, + + // marks an element for dynamic parsing and attempts to parse the next element + parseDynamic: function(elt, quiet) { + this.dynamicElements.push(elt); + if (!quiet) { + this.parseNext(); + } + }, + + // only 1 element may be parsed at a time; parsing is async so each + // parsing implementation must inform the system that parsing is complete + // via markParsingComplete. + // To prompt the system to parse the next element, parseNext should then be + // called. + // Note, parseNext used to be included at the end of markParsingComplete, but + // we must not do this so that, for example, we can (1) mark parsing complete + // then (2) fire an import load event, and then (3) parse the next resource. + markParsing: function(elt) { + flags.parse && console.log('parsing', elt); + this.parsingElement = elt; + }, + + markParsingComplete: function(elt) { + elt.__importParsed = true; + this.markDynamicParsingComplete(elt); + if (elt.__importElement) { + elt.__importElement.__importParsed = true; + this.markDynamicParsingComplete(elt.__importElement); + } + this.parsingElement = null; + flags.parse && console.log('completed', elt); + }, + + markDynamicParsingComplete: function(elt) { + var i = this.dynamicElements.indexOf(elt); + if (i >= 0) { + this.dynamicElements.splice(i, 1); + } + }, + + parseImport: function(elt) { + // TODO(sorvell): consider if there's a better way to do this; + // expose an imports parsing hook; this is needed, for example, by the + // CustomElements polyfill. + if (HTMLImports.__importsParsingHook) { + HTMLImports.__importsParsingHook(elt); + } + if (elt.import) { + elt.import.__importParsed = true; + } + this.markParsingComplete(elt); + // fire load event + if (elt.__resource && !elt.__error) { + elt.dispatchEvent(new CustomEvent('load', {bubbles: false})); + } else { + elt.dispatchEvent(new CustomEvent('error', {bubbles: false})); + } + // TODO(sorvell): workaround for Safari addEventListener not working + // for elements not in the main document. + if (elt.__pending) { + var fn; + while (elt.__pending.length) { + fn = elt.__pending.shift(); + if (fn) { + fn({target: elt}); + } + } + } + this.parseNext(); + }, + + parseLink: function(linkElt) { + if (nodeIsImport(linkElt)) { + this.parseImport(linkElt); + } else { + // make href absolute + linkElt.href = linkElt.href; + this.parseGeneric(linkElt); + } + }, + + parseStyle: function(elt) { + // TODO(sorvell): style element load event can just not fire so clone styles + var src = elt; + elt = cloneStyle(elt); + elt.__importElement = src; + this.parseGeneric(elt); + }, + + parseGeneric: function(elt) { + this.trackElement(elt); + this.addElementToDocument(elt); + }, + + rootImportForElement: function(elt) { + var n = elt; + while (n.ownerDocument.__importLink) { + n = n.ownerDocument.__importLink; + } + return n; + }, + + addElementToDocument: function(elt) { + var port = this.rootImportForElement(elt.__importElement || elt); + var l = port.__insertedElements = port.__insertedElements || 0; + var refNode = port.nextElementSibling; + for (var i=0; i < l; i++) { + refNode = refNode && refNode.nextElementSibling; + } + port.parentNode.insertBefore(elt, refNode); + }, + + // tracks when a loadable element has loaded + trackElement: function(elt, callback) { + var self = this; + var done = function(e) { + if (callback) { + callback(e); + } + self.markParsingComplete(elt); + self.parseNext(); + }; + elt.addEventListener('load', done); + elt.addEventListener('error', done); + + // NOTE: IE does not fire "load" event for styles that have already loaded + // This is in violation of the spec, so we try our hardest to work around it + if (isIE && elt.localName === 'style') { + var fakeLoad = false; + // If there's not @import in the textContent, assume it has loaded + if (elt.textContent.indexOf('@import') == -1) { + fakeLoad = true; + // if we have a sheet, we have been parsed + } else if (elt.sheet) { + fakeLoad = true; + var csr = elt.sheet.cssRules; + var len = csr ? csr.length : 0; + // search the rules for @import's + for (var i = 0, r; (i < len) && (r = csr[i]); i++) { + if (r.type === CSSRule.IMPORT_RULE) { + // if every @import has resolved, fake the load + fakeLoad = fakeLoad && Boolean(r.styleSheet); + } + } + } + // dispatch a fake load event and continue parsing + if (fakeLoad) { + elt.dispatchEvent(new CustomEvent('load', {bubbles: false})); + } + } + }, + + // NOTE: execute scripts by injecting them and watching for the load/error + // event. Inline scripts are handled via dataURL's because browsers tend to + // provide correct parsing errors in this case. If this has any compatibility + // issues, we can switch to injecting the inline script with textContent. + parseScript: function(scriptElt) { + var script = document.createElement('script'); + script.__importElement = scriptElt; + script.src = scriptElt.src ? scriptElt.src : + generateScriptDataUrl(scriptElt); + // keep track of executing script to help polyfill `document.currentScript` + scope.currentScript = scriptElt; + this.trackElement(script, function(e) { + script.parentNode.removeChild(script); + scope.currentScript = null; + }); + this.addElementToDocument(script); + }, + + // determine the next element in the tree which should be parsed + // crawl the document tree to find the next unparsed element + // then process any dynamically added elements (these should process in 'add' + // order. + nextToParse: function() { + this._mayParse = []; + return !this.parsingElement && (this.nextToParseInDoc(rootDocument) || + this.nextToParseDynamic()); + }, + + nextToParseInDoc: function(doc, link) { + // use `marParse` list to avoid looping into the same document again + // since it could cause an iloop. + if (doc && this._mayParse.indexOf(doc) < 0) { + this._mayParse.push(doc); + var nodes = doc.querySelectorAll(this.parseSelectorsForNode(doc)); + for (var i=0, l=nodes.length, p=0, n; (i= 0); + }, + + hasResource: function(node) { + if (nodeIsImport(node) && (node.import === undefined)) { + return false; + } + return true; + } + +}; + +function nodeIsImport(elt) { + return (elt.localName === 'link') && (elt.rel === IMPORT_LINK_TYPE); +} + +function generateScriptDataUrl(script) { + var scriptContent = generateScriptContent(script); + return 'data:text/javascript;charset=utf-8,' + encodeURIComponent(scriptContent); +} + +function generateScriptContent(script) { + return script.textContent + generateSourceMapHint(script); +} + +// calculate source map hint +function generateSourceMapHint(script) { + var owner = script.ownerDocument; + owner.__importedScripts = owner.__importedScripts || 0; + var moniker = script.ownerDocument.baseURI; + var num = owner.__importedScripts ? '-' + owner.__importedScripts : ''; + owner.__importedScripts++; + return '\n//# sourceURL=' + moniker + num + '.js\n'; +} + +// style/stylesheet handling + +// clone style with proper path resolution for main document +// NOTE: styles are the only elements that require direct path fixup. +function cloneStyle(style) { + var clone = style.ownerDocument.createElement('style'); + clone.textContent = style.textContent; + path.resolveUrlsInStyle(clone); + return clone; +} + +// exports +scope.parser = importParser; +scope.IMPORT_SELECTOR = IMPORT_SELECTOR; + +}); diff --git a/src/HTMLImports/path.js b/src/HTMLImports/path.js new file mode 100644 index 0000000..93d1d6d --- /dev/null +++ b/src/HTMLImports/path.js @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2014 The Polymer Project Authors. All rights reserved. + * This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt + * The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt + * The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt + * Code distributed by Google as part of the polymer project is also + * subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt + */ +HTMLImports.addModule(function(scope) { + +var CSS_URL_REGEXP = /(url\()([^)]*)(\))/g; +var CSS_IMPORT_REGEXP = /(@import[\s]+(?!url\())([^;]*)(;)/g; + +// path fixup: style elements in imports must be made relative to the main +// document. We fixup url's in url() and @import. +var path = { + + resolveUrlsInStyle: function(style) { + var doc = style.ownerDocument; + var resolver = doc.createElement('a'); + style.textContent = this.resolveUrlsInCssText(style.textContent, resolver); + return style; + }, + + resolveUrlsInCssText: function(cssText, urlObj) { + var r = this.replaceUrls(cssText, urlObj, CSS_URL_REGEXP); + r = this.replaceUrls(r, urlObj, CSS_IMPORT_REGEXP); + return r; + }, + + replaceUrls: function(text, urlObj, regexp) { + return text.replace(regexp, function(m, pre, url, post) { + var urlPath = url.replace(/["']/g, ''); + urlObj.href = urlPath; + urlPath = urlObj.href; + return pre + '\'' + urlPath + '\'' + post; + }); + } + +}; + +// exports +scope.path = path; + +}); diff --git a/src/HTMLImports/xhr.js b/src/HTMLImports/xhr.js new file mode 100644 index 0000000..f66bb8b --- /dev/null +++ b/src/HTMLImports/xhr.js @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2014 The Polymer Project Authors. All rights reserved. + * This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt + * The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt + * The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt + * Code distributed by Google as part of the polymer project is also + * subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt + */ +HTMLImports.addModule(function(scope) { + +/* + xhr processor. +*/ +xhr = { + async: true, + + ok: function(request) { + return (request.status >= 200 && request.status < 300) + || (request.status === 304) + || (request.status === 0); + }, + + load: function(url, next, nextContext) { + var request = new XMLHttpRequest(); + if (scope.flags.debug || scope.flags.bust) { + url += '?' + Math.random(); + } + request.open('GET', url, xhr.async); + request.addEventListener('readystatechange', function(e) { + if (request.readyState === 4) { + // Servers redirecting an import can add a Location header to help us + // polyfill correctly. + var locationHeader = request.getResponseHeader("Location"); + var redirectedUrl = null; + if (locationHeader) { + var redirectedUrl = (locationHeader.substr( 0, 1 ) === "/") + ? location.origin + locationHeader // Location is a relative path + : locationHeader; // Full path + } + next.call(nextContext, !xhr.ok(request) && request, + request.response || request.responseText, redirectedUrl); + } + }); + request.send(); + return request; + }, + + loadDocument: function(url, next, nextContext) { + this.load(url, next, nextContext).responseType = 'document'; + } + +}; + +// exports +scope.xhr = xhr; + +}); diff --git a/src/MutationObserver/MutationObserver.js b/src/MutationObserver/MutationObserver.js new file mode 100644 index 0000000..be7dda9 --- /dev/null +++ b/src/MutationObserver/MutationObserver.js @@ -0,0 +1,546 @@ +/* + * Copyright 2012 The Polymer Authors. All rights reserved. + * Use of this source code is goverened by a BSD-style + * license that can be found in the LICENSE file. + */ + +(function(global) { + + var registrationsTable = new WeakMap(); + + // We use setImmediate or postMessage for our future callback. + var setImmediate = window.msSetImmediate; + + // Use post message to emulate setImmediate. + if (!setImmediate) { + var setImmediateQueue = []; + var sentinel = String(Math.random()); + window.addEventListener('message', function(e) { + if (e.data === sentinel) { + var queue = setImmediateQueue; + setImmediateQueue = []; + queue.forEach(function(func) { + func(); + }); + } + }); + setImmediate = function(func) { + setImmediateQueue.push(func); + window.postMessage(sentinel, '*'); + }; + } + + // This is used to ensure that we never schedule 2 callas to setImmediate + var isScheduled = false; + + // Keep track of observers that needs to be notified next time. + var scheduledObservers = []; + + /** + * Schedules |dispatchCallback| to be called in the future. + * @param {MutationObserver} observer + */ + function scheduleCallback(observer) { + scheduledObservers.push(observer); + if (!isScheduled) { + isScheduled = true; + setImmediate(dispatchCallbacks); + } + } + + function wrapIfNeeded(node) { + return window.ShadowDOMPolyfill && + window.ShadowDOMPolyfill.wrapIfNeeded(node) || + node; + } + + function dispatchCallbacks() { + // http://dom.spec.whatwg.org/#mutation-observers + + isScheduled = false; // Used to allow a new setImmediate call above. + + var observers = scheduledObservers; + scheduledObservers = []; + // Sort observers based on their creation UID (incremental). + observers.sort(function(o1, o2) { + return o1.uid_ - o2.uid_; + }); + + var anyNonEmpty = false; + observers.forEach(function(observer) { + + // 2.1, 2.2 + var queue = observer.takeRecords(); + // 2.3. Remove all transient registered observers whose observer is mo. + removeTransientObserversFor(observer); + + // 2.4 + if (queue.length) { + observer.callback_(queue, observer); + anyNonEmpty = true; + } + }); + + // 3. + if (anyNonEmpty) + dispatchCallbacks(); + } + + function removeTransientObserversFor(observer) { + observer.nodes_.forEach(function(node) { + var registrations = registrationsTable.get(node); + if (!registrations) + return; + registrations.forEach(function(registration) { + if (registration.observer === observer) + registration.removeTransientObservers(); + }); + }); + } + + /** + * This function is used for the "For each registered observer observer (with + * observer's options as options) in target's list of registered observers, + * run these substeps:" and the "For each ancestor ancestor of target, and for + * each registered observer observer (with options options) in ancestor's list + * of registered observers, run these substeps:" part of the algorithms. The + * |options.subtree| is checked to ensure that the callback is called + * correctly. + * + * @param {Node} target + * @param {function(MutationObserverInit):MutationRecord} callback + */ + function forEachAncestorAndObserverEnqueueRecord(target, callback) { + for (var node = target; node; node = node.parentNode) { + var registrations = registrationsTable.get(node); + + if (registrations) { + for (var j = 0; j < registrations.length; j++) { + var registration = registrations[j]; + var options = registration.options; + + // Only target ignores subtree. + if (node !== target && !options.subtree) + continue; + + var record = callback(options); + if (record) + registration.enqueue(record); + } + } + } + } + + var uidCounter = 0; + + /** + * The class that maps to the DOM MutationObserver interface. + * @param {Function} callback. + * @constructor + */ + function JsMutationObserver(callback) { + this.callback_ = callback; + this.nodes_ = []; + this.records_ = []; + this.uid_ = ++uidCounter; + } + + JsMutationObserver.prototype = { + observe: function(target, options) { + target = wrapIfNeeded(target); + + // 1.1 + if (!options.childList && !options.attributes && !options.characterData || + + // 1.2 + options.attributeOldValue && !options.attributes || + + // 1.3 + options.attributeFilter && options.attributeFilter.length && + !options.attributes || + + // 1.4 + options.characterDataOldValue && !options.characterData) { + + throw new SyntaxError(); + } + + var registrations = registrationsTable.get(target); + if (!registrations) + registrationsTable.set(target, registrations = []); + + // 2 + // If target's list of registered observers already includes a registered + // observer associated with the context object, replace that registered + // observer's options with options. + var registration; + for (var i = 0; i < registrations.length; i++) { + if (registrations[i].observer === this) { + registration = registrations[i]; + registration.removeListeners(); + registration.options = options; + break; + } + } + + // 3. + // Otherwise, add a new registered observer to target's list of registered + // observers with the context object as the observer and options as the + // options, and add target to context object's list of nodes on which it + // is registered. + if (!registration) { + registration = new Registration(this, target, options); + registrations.push(registration); + this.nodes_.push(target); + } + + registration.addListeners(); + }, + + disconnect: function() { + this.nodes_.forEach(function(node) { + var registrations = registrationsTable.get(node); + for (var i = 0; i < registrations.length; i++) { + var registration = registrations[i]; + if (registration.observer === this) { + registration.removeListeners(); + registrations.splice(i, 1); + // Each node can only have one registered observer associated with + // this observer. + break; + } + } + }, this); + this.records_ = []; + }, + + takeRecords: function() { + var copyOfRecords = this.records_; + this.records_ = []; + return copyOfRecords; + } + }; + + /** + * @param {string} type + * @param {Node} target + * @constructor + */ + function MutationRecord(type, target) { + this.type = type; + this.target = target; + this.addedNodes = []; + this.removedNodes = []; + this.previousSibling = null; + this.nextSibling = null; + this.attributeName = null; + this.attributeNamespace = null; + this.oldValue = null; + } + + function copyMutationRecord(original) { + var record = new MutationRecord(original.type, original.target); + record.addedNodes = original.addedNodes.slice(); + record.removedNodes = original.removedNodes.slice(); + record.previousSibling = original.previousSibling; + record.nextSibling = original.nextSibling; + record.attributeName = original.attributeName; + record.attributeNamespace = original.attributeNamespace; + record.oldValue = original.oldValue; + return record; + }; + + // We keep track of the two (possibly one) records used in a single mutation. + var currentRecord, recordWithOldValue; + + /** + * Creates a record without |oldValue| and caches it as |currentRecord| for + * later use. + * @param {string} oldValue + * @return {MutationRecord} + */ + function getRecord(type, target) { + return currentRecord = new MutationRecord(type, target); + } + + /** + * Gets or creates a record with |oldValue| based in the |currentRecord| + * @param {string} oldValue + * @return {MutationRecord} + */ + function getRecordWithOldValue(oldValue) { + if (recordWithOldValue) + return recordWithOldValue; + recordWithOldValue = copyMutationRecord(currentRecord); + recordWithOldValue.oldValue = oldValue; + return recordWithOldValue; + } + + function clearRecords() { + currentRecord = recordWithOldValue = undefined; + } + + /** + * @param {MutationRecord} record + * @return {boolean} Whether the record represents a record from the current + * mutation event. + */ + function recordRepresentsCurrentMutation(record) { + return record === recordWithOldValue || record === currentRecord; + } + + /** + * Selects which record, if any, to replace the last record in the queue. + * This returns |null| if no record should be replaced. + * + * @param {MutationRecord} lastRecord + * @param {MutationRecord} newRecord + * @param {MutationRecord} + */ + function selectRecord(lastRecord, newRecord) { + if (lastRecord === newRecord) + return lastRecord; + + // Check if the the record we are adding represents the same record. If + // so, we keep the one with the oldValue in it. + if (recordWithOldValue && recordRepresentsCurrentMutation(lastRecord)) + return recordWithOldValue; + + return null; + } + + /** + * Class used to represent a registered observer. + * @param {MutationObserver} observer + * @param {Node} target + * @param {MutationObserverInit} options + * @constructor + */ + function Registration(observer, target, options) { + this.observer = observer; + this.target = target; + this.options = options; + this.transientObservedNodes = []; + } + + Registration.prototype = { + enqueue: function(record) { + var records = this.observer.records_; + var length = records.length; + + // There are cases where we replace the last record with the new record. + // For example if the record represents the same mutation we need to use + // the one with the oldValue. If we get same record (this can happen as we + // walk up the tree) we ignore the new record. + if (records.length > 0) { + var lastRecord = records[length - 1]; + var recordToReplaceLast = selectRecord(lastRecord, record); + if (recordToReplaceLast) { + records[length - 1] = recordToReplaceLast; + return; + } + } else { + scheduleCallback(this.observer); + } + + records[length] = record; + }, + + addListeners: function() { + this.addListeners_(this.target); + }, + + addListeners_: function(node) { + var options = this.options; + if (options.attributes) + node.addEventListener('DOMAttrModified', this, true); + + if (options.characterData) + node.addEventListener('DOMCharacterDataModified', this, true); + + if (options.childList) + node.addEventListener('DOMNodeInserted', this, true); + + if (options.childList || options.subtree) + node.addEventListener('DOMNodeRemoved', this, true); + }, + + removeListeners: function() { + this.removeListeners_(this.target); + }, + + removeListeners_: function(node) { + var options = this.options; + if (options.attributes) + node.removeEventListener('DOMAttrModified', this, true); + + if (options.characterData) + node.removeEventListener('DOMCharacterDataModified', this, true); + + if (options.childList) + node.removeEventListener('DOMNodeInserted', this, true); + + if (options.childList || options.subtree) + node.removeEventListener('DOMNodeRemoved', this, true); + }, + + /** + * Adds a transient observer on node. The transient observer gets removed + * next time we deliver the change records. + * @param {Node} node + */ + addTransientObserver: function(node) { + // Don't add transient observers on the target itself. We already have all + // the required listeners set up on the target. + if (node === this.target) + return; + + this.addListeners_(node); + this.transientObservedNodes.push(node); + var registrations = registrationsTable.get(node); + if (!registrations) + registrationsTable.set(node, registrations = []); + + // We know that registrations does not contain this because we already + // checked if node === this.target. + registrations.push(this); + }, + + removeTransientObservers: function() { + var transientObservedNodes = this.transientObservedNodes; + this.transientObservedNodes = []; + + transientObservedNodes.forEach(function(node) { + // Transient observers are never added to the target. + this.removeListeners_(node); + + var registrations = registrationsTable.get(node); + for (var i = 0; i < registrations.length; i++) { + if (registrations[i] === this) { + registrations.splice(i, 1); + // Each node can only have one registered observer associated with + // this observer. + break; + } + } + }, this); + }, + + handleEvent: function(e) { + // Stop propagation since we are managing the propagation manually. + // This means that other mutation events on the page will not work + // correctly but that is by design. + e.stopImmediatePropagation(); + + switch (e.type) { + case 'DOMAttrModified': + // http://dom.spec.whatwg.org/#concept-mo-queue-attributes + + var name = e.attrName; + var namespace = e.relatedNode.namespaceURI; + var target = e.target; + + // 1. + var record = new getRecord('attributes', target); + record.attributeName = name; + record.attributeNamespace = namespace; + + // 2. + var oldValue = + e.attrChange === MutationEvent.ADDITION ? null : e.prevValue; + + forEachAncestorAndObserverEnqueueRecord(target, function(options) { + // 3.1, 4.2 + if (!options.attributes) + return; + + // 3.2, 4.3 + if (options.attributeFilter && options.attributeFilter.length && + options.attributeFilter.indexOf(name) === -1 && + options.attributeFilter.indexOf(namespace) === -1) { + return; + } + // 3.3, 4.4 + if (options.attributeOldValue) + return getRecordWithOldValue(oldValue); + + // 3.4, 4.5 + return record; + }); + + break; + + case 'DOMCharacterDataModified': + // http://dom.spec.whatwg.org/#concept-mo-queue-characterdata + var target = e.target; + + // 1. + var record = getRecord('characterData', target); + + // 2. + var oldValue = e.prevValue; + + + forEachAncestorAndObserverEnqueueRecord(target, function(options) { + // 3.1, 4.2 + if (!options.characterData) + return; + + // 3.2, 4.3 + if (options.characterDataOldValue) + return getRecordWithOldValue(oldValue); + + // 3.3, 4.4 + return record; + }); + + break; + + case 'DOMNodeRemoved': + this.addTransientObserver(e.target); + // Fall through. + case 'DOMNodeInserted': + // http://dom.spec.whatwg.org/#concept-mo-queue-childlist + var target = e.relatedNode; + var changedNode = e.target; + var addedNodes, removedNodes; + if (e.type === 'DOMNodeInserted') { + addedNodes = [changedNode]; + removedNodes = []; + } else { + + addedNodes = []; + removedNodes = [changedNode]; + } + var previousSibling = changedNode.previousSibling; + var nextSibling = changedNode.nextSibling; + + // 1. + var record = getRecord('childList', target); + record.addedNodes = addedNodes; + record.removedNodes = removedNodes; + record.previousSibling = previousSibling; + record.nextSibling = nextSibling; + + forEachAncestorAndObserverEnqueueRecord(target, function(options) { + // 2.1, 3.2 + if (!options.childList) + return; + + // 2.2, 3.3 + return record; + }); + + } + + clearRecords(); + } + }; + + global.JsMutationObserver = JsMutationObserver; + + if (!global.MutationObserver) + global.MutationObserver = JsMutationObserver; + + +})(this); diff --git a/src/ShadowCss/ShadowCSS.js b/src/ShadowCss/ShadowCSS.js new file mode 100644 index 0000000..9337061 --- /dev/null +++ b/src/ShadowCss/ShadowCSS.js @@ -0,0 +1,780 @@ +/* + * Copyright (c) 2014 The Polymer Project Authors. All rights reserved. + * This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt + * The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt + * The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt + * Code distributed by Google as part of the polymer project is also + * subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt + */ + +/* + This is a limited shim for ShadowDOM css styling. + https://dvcs.w3.org/hg/webcomponents/raw-file/tip/spec/shadow/index.html#styles + + The intention here is to support only the styling features which can be + relatively simply implemented. The goal is to allow users to avoid the + most obvious pitfalls and do so without compromising performance significantly. + For ShadowDOM styling that's not covered here, a set of best practices + can be provided that should allow users to accomplish more complex styling. + + The following is a list of specific ShadowDOM styling features and a brief + discussion of the approach used to shim. + + Shimmed features: + + * :host, :host-context: ShadowDOM allows styling of the shadowRoot's host + element using the :host rule. To shim this feature, the :host styles are + reformatted and prefixed with a given scope name and promoted to a + document level stylesheet. + For example, given a scope name of .foo, a rule like this: + + :host { + background: red; + } + } + + becomes: + + .foo { + background: red; + } + + * encapsultion: Styles defined within ShadowDOM, apply only to + dom inside the ShadowDOM. Polymer uses one of two techniques to imlement + this feature. + + By default, rules are prefixed with the host element tag name + as a descendant selector. This ensures styling does not leak out of the 'top' + of the element's ShadowDOM. For example, + + div { + font-weight: bold; + } + + becomes: + + x-foo div { + font-weight: bold; + } + + becomes: + + + Alternatively, if WebComponents.ShadowCSS.strictStyling is set to true then + selectors are scoped by adding an attribute selector suffix to each + simple selector that contains the host element tag name. Each element + in the element's ShadowDOM template is also given the scope attribute. + Thus, these rules match only elements that have the scope attribute. + For example, given a scope name of x-foo, a rule like this: + + div { + font-weight: bold; + } + + becomes: + + div[x-foo] { + font-weight: bold; + } + + Note that elements that are dynamically added to a scope must have the scope + selector added to them manually. + + * upper/lower bound encapsulation: Styles which are defined outside a + shadowRoot should not cross the ShadowDOM boundary and should not apply + inside a shadowRoot. + + This styling behavior is not emulated. Some possible ways to do this that + were rejected due to complexity and/or performance concerns include: (1) reset + every possible property for every possible selector for a given scope name; + (2) re-implement css in javascript. + + As an alternative, users should make sure to use selectors + specific to the scope in which they are working. + + * ::distributed: This behavior is not emulated. It's often not necessary + to style the contents of a specific insertion point and instead, descendants + of the host element can be styled selectively. Users can also create an + extra node around an insertion point and style that node's contents + via descendent selectors. For example, with a shadowRoot like this: + + + + + could become: + + +
+ +
+ + Note the use of @polyfill in the comment above a ShadowDOM specific style + declaration. This is a directive to the styling shim to use the selector + in comments in lieu of the next selector when running under polyfill. +*/ +(function(scope) { + +var ShadowCSS = { + strictStyling: false, + registry: {}, + // Shim styles for a given root associated with a name and extendsName + // 1. cache root styles by name + // 2. optionally tag root nodes with scope name + // 3. shim polyfill directives /* @polyfill */ and /* @polyfill-rule */ + // 4. shim :host and scoping + shimStyling: function(root, name, extendsName) { + var scopeStyles = this.prepareRoot(root, name, extendsName); + var typeExtension = this.isTypeExtension(extendsName); + var scopeSelector = this.makeScopeSelector(name, typeExtension); + // use caching to make working with styles nodes easier and to facilitate + // lookup of extendee + var cssText = stylesToCssText(scopeStyles, true); + cssText = this.scopeCssText(cssText, scopeSelector); + // cache shimmed css on root for user extensibility + if (root) { + root.shimmedStyle = cssText; + } + // add style to document + this.addCssToDocument(cssText, name); + }, + /* + * Shim a style element with the given selector. Returns cssText that can + * be included in the document via WebComponents.ShadowCSS.addCssToDocument(css). + */ + shimStyle: function(style, selector) { + return this.shimCssText(style.textContent, selector); + }, + /* + * Shim some cssText with the given selector. Returns cssText that can + * be included in the document via WebComponents.ShadowCSS.addCssToDocument(css). + */ + shimCssText: function(cssText, selector) { + cssText = this.insertDirectives(cssText); + return this.scopeCssText(cssText, selector); + }, + makeScopeSelector: function(name, typeExtension) { + if (name) { + return typeExtension ? '[is=' + name + ']' : name; + } + return ''; + }, + isTypeExtension: function(extendsName) { + return extendsName && extendsName.indexOf('-') < 0; + }, + prepareRoot: function(root, name, extendsName) { + var def = this.registerRoot(root, name, extendsName); + this.replaceTextInStyles(def.rootStyles, this.insertDirectives); + // remove existing style elements + this.removeStyles(root, def.rootStyles); + // apply strict attr + if (this.strictStyling) { + this.applyScopeToContent(root, name); + } + return def.scopeStyles; + }, + removeStyles: function(root, styles) { + for (var i=0, l=styles.length, s; (i .bar { } + * + * to + * + * scopeName.foo > .bar + */ + convertColonHost: function(cssText) { + return this.convertColonRule(cssText, cssColonHostRe, + this.colonHostPartReplacer); + }, + /* + * convert a rule like :host-context(.foo) > .bar { } + * + * to + * + * scopeName.foo > .bar, .foo scopeName > .bar { } + * + * and + * + * :host-context(.foo:host) .bar { ... } + * + * to + * + * scopeName.foo .bar { ... } + */ + convertColonHostContext: function(cssText) { + return this.convertColonRule(cssText, cssColonHostContextRe, + this.colonHostContextPartReplacer); + }, + convertColonRule: function(cssText, regExp, partReplacer) { + // p1 = :host, p2 = contents of (), p3 rest of rule + return cssText.replace(regExp, function(m, p1, p2, p3) { + p1 = polyfillHostNoCombinator; + if (p2) { + var parts = p2.split(','), r = []; + for (var i=0, l=parts.length, p; (i .zot becomes .foo[name].bar[name] > .zot[name] + applyStrictSelectorScope: function(selector, scopeSelector) { + scopeSelector = scopeSelector.replace(/\[is=([^\]]*)\]/g, '$1'); + var splits = [' ', '>', '+', '~'], + scoped = selector, + attrName = '[' + scopeSelector + ']'; + splits.forEach(function(sep) { + var parts = scoped.split(sep); + scoped = parts.map(function(p) { + // remove :host since it should be unnecessary + var t = p.trim().replace(polyfillHostRe, ''); + if (t && (splits.indexOf(t) < 0) && (t.indexOf(attrName) < 0)) { + p = t.replace(/([^:]*)(:*)(.*)/, '$1' + attrName + '$2$3') + } + return p; + }).join(sep); + }); + return scoped; + }, + insertPolyfillHostInCssText: function(selector) { + return selector.replace(colonHostContextRe, polyfillHostContext).replace( + colonHostRe, polyfillHost); + }, + propertiesFromRule: function(rule) { + var cssText = rule.style.cssText; + // TODO(sorvell): Safari cssom incorrectly removes quotes from the content + // property. (https://bugs.webkit.org/show_bug.cgi?id=118045) + // don't replace attr rules + if (rule.style.content && !rule.style.content.match(/['"]+|attr/)) { + cssText = cssText.replace(/content:[^;]*;/g, 'content: \'' + + rule.style.content + '\';'); + } + // TODO(sorvell): we can workaround this issue here, but we need a list + // of troublesome properties to fix https://github.com/Polymer/platform/issues/53 + // + // inherit rules can be omitted from cssText + // TODO(sorvell): remove when Blink bug is fixed: + // https://code.google.com/p/chromium/issues/detail?id=358273 + var style = rule.style; + for (var i in style) { + if (style[i] === 'initial') { + cssText += i + ': initial; '; + } + } + return cssText; + }, + replaceTextInStyles: function(styles, action) { + if (styles && action) { + if (!(styles instanceof Array)) { + styles = [styles]; + } + Array.prototype.forEach.call(styles, function(s) { + s.textContent = action.call(this, s.textContent); + }, this); + } + }, + addCssToDocument: function(cssText, name) { + if (cssText.match('@import')) { + addOwnSheet(cssText, name); + } else { + addCssToDocument(cssText); + } + } +}; + +var selectorRe = /([^{]*)({[\s\S]*?})/gim, + cssCommentRe = /\/\*[^*]*\*+([^/*][^*]*\*+)*\//gim, + // TODO(sorvell): remove either content or comment + cssCommentNextSelectorRe = /\/\*\s*@polyfill ([^*]*\*+([^/*][^*]*\*+)*\/)([^{]*?){/gim, + cssContentNextSelectorRe = /polyfill-next-selector[^}]*content\:[\s]*?['"](.*?)['"][;\s]*}([^{]*?){/gim, + // TODO(sorvell): remove either content or comment + cssCommentRuleRe = /\/\*\s@polyfill-rule([^*]*\*+([^/*][^*]*\*+)*)\//gim, + cssContentRuleRe = /(polyfill-rule)[^}]*(content\:[\s]*['"](.*?)['"])[;\s]*[^}]*}/gim, + // TODO(sorvell): remove either content or comment + cssCommentUnscopedRuleRe = /\/\*\s@polyfill-unscoped-rule([^*]*\*+([^/*][^*]*\*+)*)\//gim, + cssContentUnscopedRuleRe = /(polyfill-unscoped-rule)[^}]*(content\:[\s]*['"](.*?)['"])[;\s]*[^}]*}/gim, + cssPseudoRe = /::(x-[^\s{,(]*)/gim, + cssPartRe = /::part\(([^)]*)\)/gim, + // note: :host pre-processed to -shadowcsshost. + polyfillHost = '-shadowcsshost', + // note: :host-context pre-processed to -shadowcsshostcontext. + polyfillHostContext = '-shadowcsscontext', + parenSuffix = ')(?:\\((' + + '(?:\\([^)(]*\\)|[^)(]*)+?' + + ')\\))?([^,{]*)'; + cssColonHostRe = new RegExp('(' + polyfillHost + parenSuffix, 'gim'), + cssColonHostContextRe = new RegExp('(' + polyfillHostContext + parenSuffix, 'gim'), + selectorReSuffix = '([>\\s~+\[.,{:][\\s\\S]*)?$', + colonHostRe = /\:host/gim, + colonHostContextRe = /\:host-context/gim, + /* host name without combinator */ + polyfillHostNoCombinator = polyfillHost + '-no-combinator', + polyfillHostRe = new RegExp(polyfillHost, 'gim'), + polyfillHostContextRe = new RegExp(polyfillHostContext, 'gim'), + shadowDOMSelectorsRe = [ + /\^\^/g, + /\^/g, + /\/shadow\//g, + /\/shadow-deep\//g, + /::shadow/g, + /\/deep\//g, + /::content/g + ]; + +function stylesToCssText(styles, preserveComments) { + var cssText = ''; + Array.prototype.forEach.call(styles, function(s) { + cssText += s.textContent + '\n\n'; + }); + // strip comments for easier processing + if (!preserveComments) { + cssText = cssText.replace(cssCommentRe, ''); + } + return cssText; +} + +function cssTextToStyle(cssText) { + var style = document.createElement('style'); + style.textContent = cssText; + return style; +} + +function cssToRules(cssText) { + var style = cssTextToStyle(cssText); + document.head.appendChild(style); + var rules = []; + if (style.sheet) { + // TODO(sorvell): Firefox throws when accessing the rules of a stylesheet + // with an @import + // https://bugzilla.mozilla.org/show_bug.cgi?id=625013 + try { + rules = style.sheet.cssRules; + } catch(e) { + // + } + } else { + console.warn('sheet not found', style); + } + style.parentNode.removeChild(style); + return rules; +} + +var frame = document.createElement('iframe'); +frame.style.display = 'none'; + +function initFrame() { + frame.initialized = true; + document.body.appendChild(frame); + var doc = frame.contentDocument; + var base = doc.createElement('base'); + base.href = document.baseURI; + doc.head.appendChild(base); +} + +function inFrame(fn) { + if (!frame.initialized) { + initFrame(); + } + document.body.appendChild(frame); + fn(frame.contentDocument); + document.body.removeChild(frame); +} + +// TODO(sorvell): use an iframe if the cssText contains an @import to workaround +// https://code.google.com/p/chromium/issues/detail?id=345114 +var isChrome = navigator.userAgent.match('Chrome'); +function withCssRules(cssText, callback) { + if (!callback) { + return; + } + var rules; + if (cssText.match('@import') && isChrome) { + var style = cssTextToStyle(cssText); + inFrame(function(doc) { + doc.head.appendChild(style.impl); + rules = Array.prototype.slice.call(style.sheet.cssRules, 0); + callback(rules); + }); + } else { + rules = cssToRules(cssText); + callback(rules); + } +} + +function rulesToCss(cssRules) { + for (var i=0, css=[]; i < cssRules.length; i++) { + css.push(cssRules[i].cssText); + } + return css.join('\n\n'); +} + +function addCssToDocument(cssText) { + if (cssText) { + getSheet().appendChild(document.createTextNode(cssText)); + } +} + +function addOwnSheet(cssText, name) { + var style = cssTextToStyle(cssText); + style.setAttribute(name, ''); + style.setAttribute(SHIMMED_ATTRIBUTE, ''); + document.head.appendChild(style); +} + +var SHIM_ATTRIBUTE = 'shim-shadowdom'; +var SHIMMED_ATTRIBUTE = 'shim-shadowdom-css'; +var NO_SHIM_ATTRIBUTE = 'no-shim'; + +var sheet; +function getSheet() { + if (!sheet) { + sheet = document.createElement("style"); + sheet.setAttribute(SHIMMED_ATTRIBUTE, ''); + sheet[SHIMMED_ATTRIBUTE] = true; + } + return sheet; +} + +// add polyfill stylesheet to document +if (window.ShadowDOMPolyfill) { + addCssToDocument('style { display: none !important; }\n'); + var doc = wrap(document); + var head = doc.querySelector('head'); + head.insertBefore(getSheet(), head.childNodes[0]); + + // TODO(sorvell): monkey-patching HTMLImports is abusive; + // consider a better solution. + document.addEventListener('DOMContentLoaded', function() { + var urlResolver = scope.urlResolver; + + if (window.HTMLImports && !HTMLImports.useNative) { + var SHIM_SHEET_SELECTOR = 'link[rel=stylesheet]' + + '[' + SHIM_ATTRIBUTE + ']'; + var SHIM_STYLE_SELECTOR = 'style[' + SHIM_ATTRIBUTE + ']'; + HTMLImports.importer.documentPreloadSelectors += ',' + SHIM_SHEET_SELECTOR; + HTMLImports.importer.importsPreloadSelectors += ',' + SHIM_SHEET_SELECTOR; + + HTMLImports.parser.documentSelectors = [ + HTMLImports.parser.documentSelectors, + SHIM_SHEET_SELECTOR, + SHIM_STYLE_SELECTOR + ].join(','); + + var originalParseGeneric = HTMLImports.parser.parseGeneric; + + HTMLImports.parser.parseGeneric = function(elt) { + if (elt[SHIMMED_ATTRIBUTE]) { + return; + } + var style = elt.__importElement || elt; + if (!style.hasAttribute(SHIM_ATTRIBUTE)) { + originalParseGeneric.call(this, elt); + return; + } + if (elt.__resource) { + style = elt.ownerDocument.createElement('style'); + style.textContent = elt.__resource; + } + // relay on HTMLImports for path fixup + HTMLImports.path.resolveUrlsInStyle(style); + style.textContent = ShadowCSS.shimStyle(style); + style.removeAttribute(SHIM_ATTRIBUTE, ''); + style.setAttribute(SHIMMED_ATTRIBUTE, ''); + style[SHIMMED_ATTRIBUTE] = true; + // place in document + if (style.parentNode !== head) { + // replace links in head + if (elt.parentNode === head) { + head.replaceChild(style, elt); + } else { + this.addElementToDocument(style); + } + } + style.__importParsed = true; + this.markParsingComplete(elt); + this.parseNext(); + } + + var hasResource = HTMLImports.parser.hasResource; + HTMLImports.parser.hasResource = function(node) { + if (node.localName === 'link' && node.rel === 'stylesheet' && + node.hasAttribute(SHIM_ATTRIBUTE)) { + return (node.__resource); + } else { + return hasResource.call(this, node); + } + } + + } + }); +} + +// exports +scope.ShadowCSS = ShadowCSS; + +})(window.WebComponents); diff --git a/src/ShadowDOM/ArraySplice.js b/src/ShadowDOM/ArraySplice.js new file mode 100644 index 0000000..14157f9 --- /dev/null +++ b/src/ShadowDOM/ArraySplice.js @@ -0,0 +1,255 @@ +// Copyright 2013 The Polymer Authors. All rights reserved. +// Use of this source code is goverened by a BSD-style +// license that can be found in the LICENSE file. +(function(scope) { + 'use strict'; + + function newSplice(index, removed, addedCount) { + return { + index: index, + removed: removed, + addedCount: addedCount + }; + } + + var EDIT_LEAVE = 0; + var EDIT_UPDATE = 1; + var EDIT_ADD = 2; + var EDIT_DELETE = 3; + + function ArraySplice() {} + + ArraySplice.prototype = { + + // Note: This function is *based* on the computation of the Levenshtein + // "edit" distance. The one change is that "updates" are treated as two + // edits - not one. With Array splices, an update is really a delete + // followed by an add. By retaining this, we optimize for "keeping" the + // maximum array items in the original array. For example: + // + // 'xxxx123' -> '123yyyy' + // + // With 1-edit updates, the shortest path would be just to update all seven + // characters. With 2-edit updates, we delete 4, leave 3, and add 4. This + // leaves the substring '123' intact. + calcEditDistances: function(current, currentStart, currentEnd, + old, oldStart, oldEnd) { + // "Deletion" columns + var rowCount = oldEnd - oldStart + 1; + var columnCount = currentEnd - currentStart + 1; + var distances = new Array(rowCount); + + // "Addition" rows. Initialize null column. + for (var i = 0; i < rowCount; i++) { + distances[i] = new Array(columnCount); + distances[i][0] = i; + } + + // Initialize null row + for (var j = 0; j < columnCount; j++) + distances[0][j] = j; + + for (var i = 1; i < rowCount; i++) { + for (var j = 1; j < columnCount; j++) { + if (this.equals(current[currentStart + j - 1], old[oldStart + i - 1])) + distances[i][j] = distances[i - 1][j - 1]; + else { + var north = distances[i - 1][j] + 1; + var west = distances[i][j - 1] + 1; + distances[i][j] = north < west ? north : west; + } + } + } + + return distances; + }, + + // This starts at the final weight, and walks "backward" by finding + // the minimum previous weight recursively until the origin of the weight + // matrix. + spliceOperationsFromEditDistances: function(distances) { + var i = distances.length - 1; + var j = distances[0].length - 1; + var current = distances[i][j]; + var edits = []; + while (i > 0 || j > 0) { + if (i == 0) { + edits.push(EDIT_ADD); + j--; + continue; + } + if (j == 0) { + edits.push(EDIT_DELETE); + i--; + continue; + } + var northWest = distances[i - 1][j - 1]; + var west = distances[i - 1][j]; + var north = distances[i][j - 1]; + + var min; + if (west < north) + min = west < northWest ? west : northWest; + else + min = north < northWest ? north : northWest; + + if (min == northWest) { + if (northWest == current) { + edits.push(EDIT_LEAVE); + } else { + edits.push(EDIT_UPDATE); + current = northWest; + } + i--; + j--; + } else if (min == west) { + edits.push(EDIT_DELETE); + i--; + current = west; + } else { + edits.push(EDIT_ADD); + j--; + current = north; + } + } + + edits.reverse(); + return edits; + }, + + /** + * Splice Projection functions: + * + * A splice map is a representation of how a previous array of items + * was transformed into a new array of items. Conceptually it is a list of + * tuples of + * + * + * + * which are kept in ascending index order of. The tuple represents that at + * the |index|, |removed| sequence of items were removed, and counting forward + * from |index|, |addedCount| items were added. + */ + + /** + * Lacking individual splice mutation information, the minimal set of + * splices can be synthesized given the previous state and final state of an + * array. The basic approach is to calculate the edit distance matrix and + * choose the shortest path through it. + * + * Complexity: O(l * p) + * l: The length of the current array + * p: The length of the old array + */ + calcSplices: function(current, currentStart, currentEnd, + old, oldStart, oldEnd) { + var prefixCount = 0; + var suffixCount = 0; + + var minLength = Math.min(currentEnd - currentStart, oldEnd - oldStart); + if (currentStart == 0 && oldStart == 0) + prefixCount = this.sharedPrefix(current, old, minLength); + + if (currentEnd == current.length && oldEnd == old.length) + suffixCount = this.sharedSuffix(current, old, minLength - prefixCount); + + currentStart += prefixCount; + oldStart += prefixCount; + currentEnd -= suffixCount; + oldEnd -= suffixCount; + + if (currentEnd - currentStart == 0 && oldEnd - oldStart == 0) + return []; + + if (currentStart == currentEnd) { + var splice = newSplice(currentStart, [], 0); + while (oldStart < oldEnd) + splice.removed.push(old[oldStart++]); + + return [ splice ]; + } else if (oldStart == oldEnd) + return [ newSplice(currentStart, [], currentEnd - currentStart) ]; + + var ops = this.spliceOperationsFromEditDistances( + this.calcEditDistances(current, currentStart, currentEnd, + old, oldStart, oldEnd)); + + var splice = undefined; + var splices = []; + var index = currentStart; + var oldIndex = oldStart; + for (var i = 0; i < ops.length; i++) { + switch(ops[i]) { + case EDIT_LEAVE: + if (splice) { + splices.push(splice); + splice = undefined; + } + + index++; + oldIndex++; + break; + case EDIT_UPDATE: + if (!splice) + splice = newSplice(index, [], 0); + + splice.addedCount++; + index++; + + splice.removed.push(old[oldIndex]); + oldIndex++; + break; + case EDIT_ADD: + if (!splice) + splice = newSplice(index, [], 0); + + splice.addedCount++; + index++; + break; + case EDIT_DELETE: + if (!splice) + splice = newSplice(index, [], 0); + + splice.removed.push(old[oldIndex]); + oldIndex++; + break; + } + } + + if (splice) { + splices.push(splice); + } + return splices; + }, + + sharedPrefix: function(current, old, searchLength) { + for (var i = 0; i < searchLength; i++) + if (!this.equals(current[i], old[i])) + return i; + return searchLength; + }, + + sharedSuffix: function(current, old, searchLength) { + var index1 = current.length; + var index2 = old.length; + var count = 0; + while (count < searchLength && this.equals(current[--index1], old[--index2])) + count++; + + return count; + }, + + calculateSplices: function(current, previous) { + return this.calcSplices(current, 0, current.length, previous, 0, + previous.length); + }, + + equals: function(currentValue, previousValue) { + return currentValue === previousValue; + } + }; + + // exports + scope.ArraySplice = ArraySplice; + +})(window.ShadowDOMPolyfill); \ No newline at end of file diff --git a/src/ShadowDOM/MutationObserver.js b/src/ShadowDOM/MutationObserver.js new file mode 100644 index 0000000..8824a94 --- /dev/null +++ b/src/ShadowDOM/MutationObserver.js @@ -0,0 +1,381 @@ +/* + * Copyright 2013 The Polymer Authors. All rights reserved. + * Use of this source code is goverened by a BSD-style + * license that can be found in the LICENSE file. + */ + +(function(scope) { + 'use strict'; + + var setEndOfMicrotask = scope.setEndOfMicrotask + var wrapIfNeeded = scope.wrapIfNeeded + var wrappers = scope.wrappers; + + var registrationsTable = new WeakMap(); + var globalMutationObservers = []; + var isScheduled = false; + + function scheduleCallback(observer) { + if (observer.scheduled_) + return; + + observer.scheduled_ = true; + globalMutationObservers.push(observer); + + if (isScheduled) + return; + setEndOfMicrotask(notifyObservers); + isScheduled = true; + } + + // http://dom.spec.whatwg.org/#mutation-observers + function notifyObservers() { + isScheduled = false; + + while (globalMutationObservers.length) { + var notifyList = globalMutationObservers; + globalMutationObservers = []; + + // Deliver changes in birth order of the MutationObservers. + notifyList.sort(function(x, y) { return x.uid_ - y.uid_; }); + + for (var i = 0; i < notifyList.length; i++) { + var mo = notifyList[i]; + mo.scheduled_ = false; + var queue = mo.takeRecords(); + removeTransientObserversFor(mo); + if (queue.length) { + mo.callback_(queue, mo); + } + } + } + } + + + /** + * @param {string} type + * @param {Node} target + * @constructor + */ + function MutationRecord(type, target) { + this.type = type; + this.target = target; + this.addedNodes = new wrappers.NodeList(); + this.removedNodes = new wrappers.NodeList(); + this.previousSibling = null; + this.nextSibling = null; + this.attributeName = null; + this.attributeNamespace = null; + this.oldValue = null; + } + + /** + * Registers transient observers to ancestor and its ancesors for the node + * which was removed. + * @param {!Node} ancestor + * @param {!Node} node + */ + function registerTransientObservers(ancestor, node) { + for (; ancestor; ancestor = ancestor.parentNode) { + var registrations = registrationsTable.get(ancestor); + if (!registrations) + continue; + for (var i = 0; i < registrations.length; i++) { + var registration = registrations[i]; + if (registration.options.subtree) + registration.addTransientObserver(node); + } + } + } + + function removeTransientObserversFor(observer) { + for (var i = 0; i < observer.nodes_.length; i++) { + var node = observer.nodes_[i]; + var registrations = registrationsTable.get(node); + if (!registrations) + return; + for (var j = 0; j < registrations.length; j++) { + var registration = registrations[j]; + if (registration.observer === observer) + registration.removeTransientObservers(); + } + } + } + + // http://dom.spec.whatwg.org/#queue-a-mutation-record + function enqueueMutation(target, type, data) { + // 1. + var interestedObservers = Object.create(null); + var associatedStrings = Object.create(null); + + // 2. + for (var node = target; node; node = node.parentNode) { + // 3. + var registrations = registrationsTable.get(node); + if (!registrations) + continue; + for (var j = 0; j < registrations.length; j++) { + var registration = registrations[j]; + var options = registration.options; + // 1. + if (node !== target && !options.subtree) + continue; + + // 2. + if (type === 'attributes' && !options.attributes) + continue; + + // 3. If type is "attributes", options's attributeFilter is present, and + // either options's attributeFilter does not contain name or namespace + // is non-null, continue. + if (type === 'attributes' && options.attributeFilter && + (data.namespace !== null || + options.attributeFilter.indexOf(data.name) === -1)) { + continue; + } + + // 4. + if (type === 'characterData' && !options.characterData) + continue; + + // 5. + if (type === 'childList' && !options.childList) + continue; + + // 6. + var observer = registration.observer; + interestedObservers[observer.uid_] = observer; + + // 7. If either type is "attributes" and options's attributeOldValue is + // true, or type is "characterData" and options's characterDataOldValue + // is true, set the paired string of registered observer's observer in + // interested observers to oldValue. + if (type === 'attributes' && options.attributeOldValue || + type === 'characterData' && options.characterDataOldValue) { + associatedStrings[observer.uid_] = data.oldValue; + } + } + } + + // 4. + for (var uid in interestedObservers) { + var observer = interestedObservers[uid]; + var record = new MutationRecord(type, target); + + // 2. + if ('name' in data && 'namespace' in data) { + record.attributeName = data.name; + record.attributeNamespace = data.namespace; + } + + // 3. + if (data.addedNodes) + record.addedNodes = data.addedNodes; + + // 4. + if (data.removedNodes) + record.removedNodes = data.removedNodes; + + // 5. + if (data.previousSibling) + record.previousSibling = data.previousSibling; + + // 6. + if (data.nextSibling) + record.nextSibling = data.nextSibling; + + // 7. + if (associatedStrings[uid] !== undefined) + record.oldValue = associatedStrings[uid]; + + // 8. + scheduleCallback(observer); + observer.records_.push(record); + } + } + + var slice = Array.prototype.slice; + + /** + * @param {!Object} options + * @constructor + */ + function MutationObserverOptions(options) { + this.childList = !!options.childList; + this.subtree = !!options.subtree; + + // 1. If either options' attributeOldValue or attributeFilter is present + // and options' attributes is omitted, set options' attributes to true. + if (!('attributes' in options) && + ('attributeOldValue' in options || 'attributeFilter' in options)) { + this.attributes = true; + } else { + this.attributes = !!options.attributes; + } + + // 2. If options' characterDataOldValue is present and options' + // characterData is omitted, set options' characterData to true. + if ('characterDataOldValue' in options && !('characterData' in options)) + this.characterData = true; + else + this.characterData = !!options.characterData; + + // 3. & 4. + if (!this.attributes && + (options.attributeOldValue || 'attributeFilter' in options) || + // 5. + !this.characterData && options.characterDataOldValue) { + throw new TypeError(); + } + + this.characterData = !!options.characterData; + this.attributeOldValue = !!options.attributeOldValue; + this.characterDataOldValue = !!options.characterDataOldValue; + if ('attributeFilter' in options) { + if (options.attributeFilter == null || + typeof options.attributeFilter !== 'object') { + throw new TypeError(); + } + this.attributeFilter = slice.call(options.attributeFilter); + } else { + this.attributeFilter = null; + } + } + + var uidCounter = 0; + + /** + * The class that maps to the DOM MutationObserver interface. + * @param {Function} callback. + * @constructor + */ + function MutationObserver(callback) { + this.callback_ = callback; + this.nodes_ = []; + this.records_ = []; + this.uid_ = ++uidCounter; + this.scheduled_ = false; + } + + MutationObserver.prototype = { + constructor: MutationObserver, + + // http://dom.spec.whatwg.org/#dom-mutationobserver-observe + observe: function(target, options) { + target = wrapIfNeeded(target); + + var newOptions = new MutationObserverOptions(options); + + // 6. + var registration; + var registrations = registrationsTable.get(target); + if (!registrations) + registrationsTable.set(target, registrations = []); + + for (var i = 0; i < registrations.length; i++) { + if (registrations[i].observer === this) { + registration = registrations[i]; + // 6.1. + registration.removeTransientObservers(); + // 6.2. + registration.options = newOptions; + } + } + + // 7. + if (!registration) { + registration = new Registration(this, target, newOptions); + registrations.push(registration); + this.nodes_.push(target); + } + }, + + // http://dom.spec.whatwg.org/#dom-mutationobserver-disconnect + disconnect: function() { + this.nodes_.forEach(function(node) { + var registrations = registrationsTable.get(node); + for (var i = 0; i < registrations.length; i++) { + var registration = registrations[i]; + if (registration.observer === this) { + registrations.splice(i, 1); + // Each node can only have one registered observer associated with + // this observer. + break; + } + } + }, this); + this.records_ = []; + }, + + takeRecords: function() { + var copyOfRecords = this.records_; + this.records_ = []; + return copyOfRecords; + } + }; + + /** + * Class used to represent a registered observer. + * @param {MutationObserver} observer + * @param {Node} target + * @param {MutationObserverOptions} options + * @constructor + */ + function Registration(observer, target, options) { + this.observer = observer; + this.target = target; + this.options = options; + this.transientObservedNodes = []; + } + + Registration.prototype = { + /** + * Adds a transient observer on node. The transient observer gets removed + * next time we deliver the change records. + * @param {Node} node + */ + addTransientObserver: function(node) { + // Don't add transient observers on the target itself. We already have all + // the required listeners set up on the target. + if (node === this.target) + return; + + // Make sure we remove transient observers at the end of microtask, even + // if we didn't get any change records. + scheduleCallback(this.observer); + + this.transientObservedNodes.push(node); + var registrations = registrationsTable.get(node); + if (!registrations) + registrationsTable.set(node, registrations = []); + + // We know that registrations does not contain this because we already + // checked if node === this.target. + registrations.push(this); + }, + + removeTransientObservers: function() { + var transientObservedNodes = this.transientObservedNodes; + this.transientObservedNodes = []; + + for (var i = 0; i < transientObservedNodes.length; i++) { + var node = transientObservedNodes[i]; + var registrations = registrationsTable.get(node); + for (var j = 0; j < registrations.length; j++) { + if (registrations[j] === this) { + registrations.splice(j, 1); + // Each node can only have one registered observer associated with + // this observer. + break; + } + } + } + } + }; + + scope.enqueueMutation = enqueueMutation; + scope.registerTransientObservers = registerTransientObservers; + scope.wrappers.MutationObserver = MutationObserver; + scope.wrappers.MutationRecord = MutationRecord; + +})(window.ShadowDOMPolyfill); diff --git a/src/ShadowDOM/ShadowDOM.js b/src/ShadowDOM/ShadowDOM.js new file mode 100644 index 0000000..4f11515 --- /dev/null +++ b/src/ShadowDOM/ShadowDOM.js @@ -0,0 +1,72 @@ +// Copyright 2013 The Polymer Authors. All rights reserved. +// Use of this source code is goverened by a BSD-style +// license that can be found in the LICENSE file. + +(function() { + +var thisFile = 'ShadowDOM.js'; +var base = ''; +Array.prototype.forEach.call(document.querySelectorAll('script[src]'), function(s) { + var src = s.getAttribute('src'); + var re = new RegExp(thisFile + '[^\\\\]*'); + var match = src.match(re); + if (match) { + base = src.slice(0, -match[0].length); + } +}); + +[ + '../WeakMap/WeakMap.js', + 'wrappers.js', + 'ArraySplice.js', + 'microtask.js', + 'MutationObserver.js', + 'TreeScope.js', + 'wrappers/events.js', + 'wrappers/TouchEvent.js', + 'wrappers/NodeList.js', + 'wrappers/HTMLCollection.js', + 'wrappers/Node.js', + 'querySelector.js', + 'wrappers/node-interfaces.js', + 'wrappers/CharacterData.js', + 'wrappers/Text.js', + 'wrappers/DOMTokenList.js', + 'wrappers/Element.js', + 'wrappers/HTMLElement.js', + 'wrappers/HTMLCanvasElement.js', + 'wrappers/HTMLContentElement.js', + 'wrappers/HTMLFormElement.js', + 'wrappers/HTMLImageElement.js', + 'wrappers/HTMLShadowElement.js', + 'wrappers/HTMLTemplateElement.js', + 'wrappers/HTMLMediaElement.js', + 'wrappers/HTMLAudioElement.js', + 'wrappers/HTMLOptionElement.js', + 'wrappers/HTMLSelectElement.js', + 'wrappers/HTMLTableElement.js', + 'wrappers/HTMLTableSectionElement.js', + 'wrappers/HTMLTableRowElement.js', + 'wrappers/HTMLUnknownElement.js', + 'wrappers/SVGElement.js', + 'wrappers/SVGUseElement.js', + 'wrappers/SVGElementInstance.js', + 'wrappers/CanvasRenderingContext2D.js', + 'wrappers/WebGLRenderingContext.js', + 'wrappers/Range.js', + 'wrappers/generic.js', + 'wrappers/ShadowRoot.js', + 'ShadowRenderer.js', + 'wrappers/elements-with-form-property.js', + 'wrappers/Selection.js', + 'wrappers/Document.js', + 'wrappers/Window.js', + 'wrappers/DataTransfer.js', + 'wrappers/FormData.js', + 'wrappers/XMLHttpRequest.js', + 'wrappers/override-constructors.js' +].forEach(function(src) { + document.write(''); +}); + +})(); diff --git a/src/ShadowDOM/ShadowRenderer.js b/src/ShadowDOM/ShadowRenderer.js new file mode 100644 index 0000000..0fc088c --- /dev/null +++ b/src/ShadowDOM/ShadowRenderer.js @@ -0,0 +1,665 @@ +// Copyright 2013 The Polymer Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +(function(scope) { + 'use strict'; + + var Element = scope.wrappers.Element; + var HTMLContentElement = scope.wrappers.HTMLContentElement; + var HTMLShadowElement = scope.wrappers.HTMLShadowElement; + var Node = scope.wrappers.Node; + var ShadowRoot = scope.wrappers.ShadowRoot; + var assert = scope.assert; + var getTreeScope = scope.getTreeScope; + var mixin = scope.mixin; + var oneOf = scope.oneOf; + var unsafeUnwrap = scope.unsafeUnwrap; + var unwrap = scope.unwrap; + var wrap = scope.wrap; + var ArraySplice = scope.ArraySplice; + + /** + * Updates the fields of a wrapper to a snapshot of the logical DOM as needed. + * Up means parentNode + * Sideways means previous and next sibling. + * @param {!Node} wrapper + */ + function updateWrapperUpAndSideways(wrapper) { + wrapper.previousSibling_ = wrapper.previousSibling; + wrapper.nextSibling_ = wrapper.nextSibling; + wrapper.parentNode_ = wrapper.parentNode; + } + + /** + * Updates the fields of a wrapper to a snapshot of the logical DOM as needed. + * Down means first and last child + * @param {!Node} wrapper + */ + function updateWrapperDown(wrapper) { + wrapper.firstChild_ = wrapper.firstChild; + wrapper.lastChild_ = wrapper.lastChild; + } + + function updateAllChildNodes(parentNodeWrapper) { + assert(parentNodeWrapper instanceof Node); + for (var childWrapper = parentNodeWrapper.firstChild; + childWrapper; + childWrapper = childWrapper.nextSibling) { + updateWrapperUpAndSideways(childWrapper); + } + updateWrapperDown(parentNodeWrapper); + } + + function insertBefore(parentNodeWrapper, newChildWrapper, refChildWrapper) { + var parentNode = unwrap(parentNodeWrapper); + var newChild = unwrap(newChildWrapper); + var refChild = refChildWrapper ? unwrap(refChildWrapper) : null; + + remove(newChildWrapper); + updateWrapperUpAndSideways(newChildWrapper); + + if (!refChildWrapper) { + parentNodeWrapper.lastChild_ = parentNodeWrapper.lastChild; + if (parentNodeWrapper.lastChild === parentNodeWrapper.firstChild) + parentNodeWrapper.firstChild_ = parentNodeWrapper.firstChild; + + var lastChildWrapper = wrap(parentNode.lastChild); + if (lastChildWrapper) + lastChildWrapper.nextSibling_ = lastChildWrapper.nextSibling; + } else { + if (parentNodeWrapper.firstChild === refChildWrapper) + parentNodeWrapper.firstChild_ = refChildWrapper; + + refChildWrapper.previousSibling_ = refChildWrapper.previousSibling; + } + + scope.originalInsertBefore.call(parentNode, newChild, refChild); + } + + function remove(nodeWrapper) { + var node = unwrap(nodeWrapper) + var parentNode = node.parentNode; + if (!parentNode) + return; + + var parentNodeWrapper = wrap(parentNode); + updateWrapperUpAndSideways(nodeWrapper); + + if (nodeWrapper.previousSibling) + nodeWrapper.previousSibling.nextSibling_ = nodeWrapper; + if (nodeWrapper.nextSibling) + nodeWrapper.nextSibling.previousSibling_ = nodeWrapper; + + if (parentNodeWrapper.lastChild === nodeWrapper) + parentNodeWrapper.lastChild_ = nodeWrapper; + if (parentNodeWrapper.firstChild === nodeWrapper) + parentNodeWrapper.firstChild_ = nodeWrapper; + + scope.originalRemoveChild.call(parentNode, node); + } + + var distributedNodesTable = new WeakMap(); + var destinationInsertionPointsTable = new WeakMap(); + var rendererForHostTable = new WeakMap(); + + function resetDistributedNodes(insertionPoint) { + distributedNodesTable.set(insertionPoint, []); + } + + function getDistributedNodes(insertionPoint) { + var rv = distributedNodesTable.get(insertionPoint); + if (!rv) + distributedNodesTable.set(insertionPoint, rv = []); + return rv; + } + + function getChildNodesSnapshot(node) { + var result = [], i = 0; + for (var child = node.firstChild; child; child = child.nextSibling) { + result[i++] = child; + } + return result; + } + + var request = oneOf(window, [ + 'requestAnimationFrame', + 'mozRequestAnimationFrame', + 'webkitRequestAnimationFrame', + 'setTimeout' + ]); + + var pendingDirtyRenderers = []; + var renderTimer; + + function renderAllPending() { + // TODO(arv): Order these in document order. That way we do not have to + // render something twice. + for (var i = 0; i < pendingDirtyRenderers.length; i++) { + var renderer = pendingDirtyRenderers[i]; + var parentRenderer = renderer.parentRenderer; + if (parentRenderer && parentRenderer.dirty) + continue; + renderer.render(); + } + + pendingDirtyRenderers = []; + } + + function handleRequestAnimationFrame() { + renderTimer = null; + renderAllPending(); + } + + /** + * Returns existing shadow renderer for a host or creates it if it is needed. + * @params {!Element} host + * @return {!ShadowRenderer} + */ + function getRendererForHost(host) { + var renderer = rendererForHostTable.get(host); + if (!renderer) { + renderer = new ShadowRenderer(host); + rendererForHostTable.set(host, renderer); + } + return renderer; + } + + function getShadowRootAncestor(node) { + var root = getTreeScope(node).root; + if (root instanceof ShadowRoot) + return root; + return null; + } + + function getRendererForShadowRoot(shadowRoot) { + return getRendererForHost(shadowRoot.host); + } + + var spliceDiff = new ArraySplice(); + spliceDiff.equals = function(renderNode, rawNode) { + return unwrap(renderNode.node) === rawNode; + }; + + /** + * RenderNode is used as an in memory "render tree". When we render the + * composed tree we create a tree of RenderNodes, then we diff this against + * the real DOM tree and make minimal changes as needed. + */ + function RenderNode(node) { + this.skip = false; + this.node = node; + this.childNodes = []; + } + + RenderNode.prototype = { + append: function(node) { + var rv = new RenderNode(node); + this.childNodes.push(rv); + return rv; + }, + + sync: function(opt_added) { + if (this.skip) + return; + + var nodeWrapper = this.node; + // plain array of RenderNodes + var newChildren = this.childNodes; + // plain array of real nodes. + var oldChildren = getChildNodesSnapshot(unwrap(nodeWrapper)); + var added = opt_added || new WeakMap(); + + var splices = spliceDiff.calculateSplices(newChildren, oldChildren); + + var newIndex = 0, oldIndex = 0; + var lastIndex = 0; + for (var i = 0; i < splices.length; i++) { + var splice = splices[i]; + for (; lastIndex < splice.index; lastIndex++) { + oldIndex++; + newChildren[newIndex++].sync(added); + } + + var removedCount = splice.removed.length; + for (var j = 0; j < removedCount; j++) { + var wrapper = wrap(oldChildren[oldIndex++]); + if (!added.get(wrapper)) + remove(wrapper); + } + + var addedCount = splice.addedCount; + var refNode = oldChildren[oldIndex] && wrap(oldChildren[oldIndex]); + for (var j = 0; j < addedCount; j++) { + var newChildRenderNode = newChildren[newIndex++]; + var newChildWrapper = newChildRenderNode.node; + insertBefore(nodeWrapper, newChildWrapper, refNode); + + // Keep track of added so that we do not remove the node after it + // has been added. + added.set(newChildWrapper, true); + + newChildRenderNode.sync(added); + } + + lastIndex += addedCount; + } + + for (var i = lastIndex; i < newChildren.length; i++) { + newChildren[i].sync(added); + } + } + }; + + function ShadowRenderer(host) { + this.host = host; + this.dirty = false; + this.invalidateAttributes(); + this.associateNode(host); + } + + ShadowRenderer.prototype = { + + // http://dvcs.w3.org/hg/webcomponents/raw-file/tip/spec/shadow/index.html#rendering-shadow-trees + render: function(opt_renderNode) { + if (!this.dirty) + return; + + this.invalidateAttributes(); + + var host = this.host; + + this.distribution(host); + var renderNode = opt_renderNode || new RenderNode(host); + this.buildRenderTree(renderNode, host); + + var topMostRenderer = !opt_renderNode; + if (topMostRenderer) + renderNode.sync(); + + this.dirty = false; + }, + + get parentRenderer() { + return getTreeScope(this.host).renderer; + }, + + invalidate: function() { + if (!this.dirty) { + this.dirty = true; + var parentRenderer = this.parentRenderer; + if (parentRenderer) + parentRenderer.invalidate(); + pendingDirtyRenderers.push(this); + if (renderTimer) + return; + renderTimer = window[request](handleRequestAnimationFrame, 0); + } + }, + + // http://w3c.github.io/webcomponents/spec/shadow/#distribution-algorithms + distribution: function(root) { + this.resetAllSubtrees(root); + this.distributionResolution(root); + }, + + resetAll: function(node) { + if (isInsertionPoint(node)) + resetDistributedNodes(node); + else + resetDestinationInsertionPoints(node); + + this.resetAllSubtrees(node); + }, + + resetAllSubtrees: function(node) { + for (var child = node.firstChild; child; child = child.nextSibling) { + this.resetAll(child); + } + + if (node.shadowRoot) + this.resetAll(node.shadowRoot); + + if (node.olderShadowRoot) + this.resetAll(node.olderShadowRoot); + }, + + // http://w3c.github.io/webcomponents/spec/shadow/#distribution-results + distributionResolution: function(node) { + if (isShadowHost(node)) { + var shadowHost = node; + // 1.1 + var pool = poolPopulation(shadowHost); + + var shadowTrees = getShadowTrees(shadowHost); + + // 1.2 + for (var i = 0; i < shadowTrees.length; i++) { + // 1.2.1 + this.poolDistribution(shadowTrees[i], pool); + } + + // 1.3 + for (var i = shadowTrees.length - 1; i >= 0; i--) { + var shadowTree = shadowTrees[i]; + + // 1.3.1 + // TODO(arv): We should keep the shadow insertion points on the + // shadow root (or renderer) so we don't have to search the tree + // every time. + var shadow = getShadowInsertionPoint(shadowTree); + + // 1.3.2 + if (shadow) { + + // 1.3.2.1 + var olderShadowRoot = shadowTree.olderShadowRoot; + if (olderShadowRoot) { + // 1.3.2.1.1 + pool = poolPopulation(olderShadowRoot); + } + + // 1.3.2.2 + for (var j = 0; j < pool.length; j++) { + // 1.3.2.2.1 + destributeNodeInto(pool[j], shadow); + } + } + + // 1.3.3 + this.distributionResolution(shadowTree); + } + } + + for (var child = node.firstChild; child; child = child.nextSibling) { + this.distributionResolution(child); + } + }, + + // http://w3c.github.io/webcomponents/spec/shadow/#dfn-pool-distribution-algorithm + poolDistribution: function (node, pool) { + if (node instanceof HTMLShadowElement) + return; + + if (node instanceof HTMLContentElement) { + var content = node; + this.updateDependentAttributes(content.getAttribute('select')); + + var anyDistributed = false; + + // 1.1 + for (var i = 0; i < pool.length; i++) { + var node = pool[i]; + if (!node) + continue; + if (matches(node, content)) { + destributeNodeInto(node, content); + pool[i] = undefined; + anyDistributed = true; + } + } + + // 1.2 + // Fallback content + if (!anyDistributed) { + for (var child = content.firstChild; + child; + child = child.nextSibling) { + destributeNodeInto(child, content); + } + } + + return; + } + + for (var child = node.firstChild; child; child = child.nextSibling) { + this.poolDistribution(child, pool); + } + }, + + buildRenderTree: function(renderNode, node) { + var children = this.compose(node); + for (var i = 0; i < children.length; i++) { + var child = children[i]; + var childRenderNode = renderNode.append(child); + this.buildRenderTree(childRenderNode, child); + } + + if (isShadowHost(node)) { + var renderer = getRendererForHost(node); + renderer.dirty = false; + } + + }, + + compose: function(node) { + var children = []; + var p = node.shadowRoot || node; + for (var child = p.firstChild; child; child = child.nextSibling) { + if (isInsertionPoint(child)) { + this.associateNode(p); + var distributedNodes = getDistributedNodes(child); + for (var j = 0; j < distributedNodes.length; j++) { + var distributedNode = distributedNodes[j]; + if (isFinalDestination(child, distributedNode)) + children.push(distributedNode); + } + } else { + children.push(child); + } + } + return children; + }, + + /** + * Invalidates the attributes used to keep track of which attributes may + * cause the renderer to be invalidated. + */ + invalidateAttributes: function() { + this.attributes = Object.create(null); + }, + + /** + * Parses the selector and makes this renderer dependent on the attribute + * being used in the selector. + * @param {string} selector + */ + updateDependentAttributes: function(selector) { + if (!selector) + return; + + var attributes = this.attributes; + + // .class + if (/\.\w+/.test(selector)) + attributes['class'] = true; + + // #id + if (/#\w+/.test(selector)) + attributes['id'] = true; + + selector.replace(/\[\s*([^\s=\|~\]]+)/g, function(_, name) { + attributes[name] = true; + }); + + // Pseudo selectors have been removed from the spec. + }, + + dependsOnAttribute: function(name) { + return this.attributes[name]; + }, + + associateNode: function(node) { + unsafeUnwrap(node).polymerShadowRenderer_ = this; + } + }; + + // http://w3c.github.io/webcomponents/spec/shadow/#dfn-pool-population-algorithm + function poolPopulation(node) { + var pool = []; + for (var child = node.firstChild; child; child = child.nextSibling) { + if (isInsertionPoint(child)) { + pool.push.apply(pool, getDistributedNodes(child)); + } else { + pool.push(child); + } + } + return pool; + } + + function getShadowInsertionPoint(node) { + if (node instanceof HTMLShadowElement) + return node; + if (node instanceof HTMLContentElement) + return null; + for (var child = node.firstChild; child; child = child.nextSibling) { + var res = getShadowInsertionPoint(child); + if (res) + return res; + } + return null; + } + + function destributeNodeInto(child, insertionPoint) { + getDistributedNodes(insertionPoint).push(child); + var points = destinationInsertionPointsTable.get(child); + if (!points) + destinationInsertionPointsTable.set(child, [insertionPoint]); + else + points.push(insertionPoint); + } + + function getDestinationInsertionPoints(node) { + return destinationInsertionPointsTable.get(node); + } + + function resetDestinationInsertionPoints(node) { + // IE11 crashes when delete is used. + destinationInsertionPointsTable.set(node, undefined); + } + + // AllowedSelectors : + // TypeSelector + // * + // ClassSelector + // IDSelector + // AttributeSelector + // negation + var selectorStartCharRe = /^(:not\()?[*.#[a-zA-Z_|]/; + + function matches(node, contentElement) { + var select = contentElement.getAttribute('select'); + if (!select) + return true; + + // Here we know the select attribute is a non empty string. + select = select.trim(); + if (!select) + return true; + + if (!(node instanceof Element)) + return false; + + if (!selectorStartCharRe.test(select)) + return false; + + try { + return node.matches(select); + } catch (ex) { + // Invalid selector. + return false; + } + } + + function isFinalDestination(insertionPoint, node) { + var points = getDestinationInsertionPoints(node); + return points && points[points.length - 1] === insertionPoint; + } + + function isInsertionPoint(node) { + return node instanceof HTMLContentElement || + node instanceof HTMLShadowElement; + } + + function isShadowHost(shadowHost) { + return shadowHost.shadowRoot; + } + + // Returns the shadow trees as an array, with the youngest tree at the + // beginning of the array. + function getShadowTrees(host) { + var trees = []; + + for (var tree = host.shadowRoot; tree; tree = tree.olderShadowRoot) { + trees.push(tree); + } + return trees; + } + + function render(host) { + new ShadowRenderer(host).render(); + }; + + // Need to rerender shadow host when: + // + // - a direct child to the ShadowRoot is added or removed + // - a direct child to the host is added or removed + // - a new shadow root is created + // - a direct child to a content/shadow element is added or removed + // - a sibling to a content/shadow element is added or removed + // - content[select] is changed + // - an attribute in a direct child to a host is modified + + /** + * This gets called when a node was added or removed to it. + */ + Node.prototype.invalidateShadowRenderer = function(force) { + var renderer = unsafeUnwrap(this).polymerShadowRenderer_; + if (renderer) { + renderer.invalidate(); + return true; + } + + return false; + }; + + HTMLContentElement.prototype.getDistributedNodes = + HTMLShadowElement.prototype.getDistributedNodes = function() { + // TODO(arv): We should only rerender the dirty ancestor renderers (from + // the root and down). + renderAllPending(); + return getDistributedNodes(this); + }; + + Element.prototype.getDestinationInsertionPoints = function() { + renderAllPending(); + return getDestinationInsertionPoints(this) || []; + }; + + HTMLContentElement.prototype.nodeIsInserted_ = + HTMLShadowElement.prototype.nodeIsInserted_ = function() { + // Invalidate old renderer if any. + this.invalidateShadowRenderer(); + + var shadowRoot = getShadowRootAncestor(this); + var renderer; + if (shadowRoot) + renderer = getRendererForShadowRoot(shadowRoot); + unsafeUnwrap(this).polymerShadowRenderer_ = renderer; + if (renderer) + renderer.invalidate(); + }; + + scope.getRendererForHost = getRendererForHost; + scope.getShadowTrees = getShadowTrees; + scope.renderAllPending = renderAllPending; + + scope.getDestinationInsertionPoints = getDestinationInsertionPoints; + + // Exposed for testing + scope.visual = { + insertBefore: insertBefore, + remove: remove, + }; + +})(window.ShadowDOMPolyfill); diff --git a/src/ShadowDOM/TreeScope.js b/src/ShadowDOM/TreeScope.js new file mode 100644 index 0000000..5bdada9 --- /dev/null +++ b/src/ShadowDOM/TreeScope.js @@ -0,0 +1,81 @@ +/** + * Copyright 2014 The Polymer Authors. All rights reserved. + * Use of this source code is goverened by a BSD-style + * license that can be found in the LICENSE file. + */ + +(function(scope) { + 'use strict'; + + /** + * A tree scope represents the root of a tree. All nodes in a tree point to + * the same TreeScope object. The tree scope of a node get set the first time + * it is accessed or when a node is added or remove to a tree. + * + * The root is a Node that has no parent. + * + * The parent is another TreeScope. For ShadowRoots, it is the TreeScope of + * the host of the ShadowRoot. + * + * @param {!Node} root + * @param {TreeScope} parent + * @constructor + */ + function TreeScope(root, parent) { + /** @type {!Node} */ + this.root = root; + + /** @type {TreeScope} */ + this.parent = parent; + } + + TreeScope.prototype = { + get renderer() { + if (this.root instanceof scope.wrappers.ShadowRoot) { + return scope.getRendererForHost(this.root.host); + } + return null; + }, + + contains: function(treeScope) { + for (; treeScope; treeScope = treeScope.parent) { + if (treeScope === this) + return true; + } + return false; + } + }; + + function setTreeScope(node, treeScope) { + if (node.treeScope_ !== treeScope) { + node.treeScope_ = treeScope; + for (var sr = node.shadowRoot; sr; sr = sr.olderShadowRoot) { + sr.treeScope_.parent = treeScope; + } + for (var child = node.firstChild; child; child = child.nextSibling) { + setTreeScope(child, treeScope); + } + } + } + + function getTreeScope(node) { + if (node instanceof scope.wrappers.Window) { + debugger; + } + + if (node.treeScope_) + return node.treeScope_; + var parent = node.parentNode; + var treeScope; + if (parent) + treeScope = getTreeScope(parent); + else + treeScope = new TreeScope(node, null); + return node.treeScope_ = treeScope; + } + + scope.TreeScope = TreeScope; + scope.getTreeScope = getTreeScope; + scope.setTreeScope = setTreeScope; + +})(window.ShadowDOMPolyfill); diff --git a/src/ShadowDOM/microtask.js b/src/ShadowDOM/microtask.js new file mode 100644 index 0000000..c0e35b7 --- /dev/null +++ b/src/ShadowDOM/microtask.js @@ -0,0 +1,49 @@ +/* + * Copyright 2013 The Polymer Authors. All rights reserved. + * Use of this source code is goverened by a BSD-style + * license that can be found in the LICENSE file. + */ + +(function(context) { + 'use strict'; + + var OriginalMutationObserver = window.MutationObserver; + var callbacks = []; + var pending = false; + var timerFunc; + + function handle() { + pending = false; + var copies = callbacks.slice(0); + callbacks = []; + for (var i = 0; i < copies.length; i++) { + (0, copies[i])(); + } + } + + if (OriginalMutationObserver) { + var counter = 1; + var observer = new OriginalMutationObserver(handle); + var textNode = document.createTextNode(counter); + observer.observe(textNode, {characterData: true}); + + timerFunc = function() { + counter = (counter + 1) % 2; + textNode.data = counter; + }; + + } else { + timerFunc = window.setImmediate || window.setTimeout; + } + + function setEndOfMicrotask(func) { + callbacks.push(func); + if (pending) + return; + pending = true; + timerFunc(handle, 0); + } + + context.setEndOfMicrotask = setEndOfMicrotask; + +})(window.ShadowDOMPolyfill); diff --git a/src/ShadowDOM/querySelector.js b/src/ShadowDOM/querySelector.js new file mode 100644 index 0000000..2901f98 --- /dev/null +++ b/src/ShadowDOM/querySelector.js @@ -0,0 +1,270 @@ +// Copyright 2013 The Polymer Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +(function(scope) { + 'use strict'; + + var HTMLCollection = scope.wrappers.HTMLCollection; + var NodeList = scope.wrappers.NodeList; + var getTreeScope = scope.getTreeScope; + var unsafeUnwrap = scope.unsafeUnwrap; + var wrap = scope.wrap; + + var originalDocumentQuerySelector = document.querySelector; + var originalElementQuerySelector = document.documentElement.querySelector; + + var originalDocumentQuerySelectorAll = document.querySelectorAll; + var originalElementQuerySelectorAll = document.documentElement.querySelectorAll; + + var originalDocumentGetElementsByTagName = document.getElementsByTagName; + var originalElementGetElementsByTagName = document.documentElement.getElementsByTagName; + + var originalDocumentGetElementsByTagNameNS = document.getElementsByTagNameNS; + var originalElementGetElementsByTagNameNS = document.documentElement.getElementsByTagNameNS; + + var OriginalElement = window.Element; + var OriginalDocument = window.HTMLDocument || window.Document; + + function filterNodeList(list, index, result, deep) { + var wrappedItem = null; + var root = null; + for (var i = 0, length = list.length; i < length; i++) { + wrappedItem = wrap(list[i]); + if (!deep && (root = getTreeScope(wrappedItem).root)) { + if (root instanceof scope.wrappers.ShadowRoot) { + continue; + } + } + result[index++] = wrappedItem; + } + + return index; + } + + function shimSelector(selector) { + return String(selector).replace(/\/deep\//g, ' '); + } + + function findOne(node, selector) { + var m, el = node.firstElementChild; + while (el) { + if (el.matches(selector)) + return el; + m = findOne(el, selector); + if (m) + return m; + el = el.nextElementSibling; + } + return null; + } + + function matchesSelector(el, selector) { + return el.matches(selector); + } + + var XHTML_NS = 'http://www.w3.org/1999/xhtml'; + + function matchesTagName(el, localName, localNameLowerCase) { + var ln = el.localName; + return ln === localName || + ln === localNameLowerCase && el.namespaceURI === XHTML_NS; + } + + function matchesEveryThing() { + return true; + } + + function matchesLocalNameOnly(el, ns, localName) { + return el.localName === localName; + } + + function matchesNameSpace(el, ns) { + return el.namespaceURI === ns; + } + + function matchesLocalNameNS(el, ns, localName) { + return el.namespaceURI === ns && el.localName === localName; + } + + function findElements(node, index, result, p, arg0, arg1) { + var el = node.firstElementChild; + while (el) { + if (p(el, arg0, arg1)) + result[index++] = el; + index = findElements(el, index, result, p, arg0, arg1); + el = el.nextElementSibling; + } + return index; + } + + // find and findAll will only match Simple Selectors, + // Structural Pseudo Classes are not guarenteed to be correct + // http://www.w3.org/TR/css3-selectors/#simple-selectors + + function querySelectorAllFiltered(p, index, result, selector, deep) { + var target = unsafeUnwrap(this); + var list; + var root = getTreeScope(this).root; + if (root instanceof scope.wrappers.ShadowRoot) { + // We are in the shadow tree and the logical tree is + // going to be disconnected so we do a manual tree traversal + return findElements(this, index, result, p, selector, null); + } else if (target instanceof OriginalElement) { + list = originalElementQuerySelectorAll.call(target, selector); + } else if (target instanceof OriginalDocument) { + list = originalDocumentQuerySelectorAll.call(target, selector); + } else { + // When we get a ShadowRoot the logical tree is going to be disconnected + // so we do a manual tree traversal + return findElements(this, index, result, p, selector, null); + } + + return filterNodeList(list, index, result, deep); + } + + var SelectorsInterface = { + querySelector: function(selector) { + var shimmed = shimSelector(selector); + var deep = shimmed !== selector; + selector = shimmed; + + var target = unsafeUnwrap(this); + var wrappedItem; + var root = getTreeScope(this).root; + if (root instanceof scope.wrappers.ShadowRoot) { + // We are in the shadow tree and the logical tree is + // going to be disconnected so we do a manual tree traversal + return findOne(this, selector); + } else if (target instanceof OriginalElement) { + wrappedItem = wrap(originalElementQuerySelector.call(target, selector)); + } else if (target instanceof OriginalDocument) { + wrappedItem = wrap(originalDocumentQuerySelector.call(target, selector)); + } else { + // When we get a ShadowRoot the logical tree is going to be disconnected + // so we do a manual tree traversal + return findOne(this, selector); + } + + if (!wrappedItem) { + // When the original query returns nothing + // we return nothing (to be consistent with the other wrapped calls) + return wrappedItem; + } else if (!deep && (root = getTreeScope(wrappedItem).root)) { + if (root instanceof scope.wrappers.ShadowRoot) { + // When the original query returns an element in the ShadowDOM + // we must do a manual tree traversal + return findOne(this, selector); + } + } + + return wrappedItem; + }, + querySelectorAll: function(selector) { + var shimmed = shimSelector(selector); + var deep = shimmed !== selector; + selector = shimmed; + + var result = new NodeList(); + + result.length = querySelectorAllFiltered.call(this, + matchesSelector, + 0, + result, + selector, + deep); + + return result; + } + }; + + function getElementsByTagNameFiltered(p, index, result, localName, + lowercase) { + var target = unsafeUnwrap(this); + var list; + var root = getTreeScope(this).root; + if (root instanceof scope.wrappers.ShadowRoot) { + // We are in the shadow tree and the logical tree is + // going to be disconnected so we do a manual tree traversal + return findElements(this, index, result, p, localName, lowercase); + } else if (target instanceof OriginalElement) { + list = originalElementGetElementsByTagName.call(target, localName, + lowercase); + } else if (target instanceof OriginalDocument) { + list = originalDocumentGetElementsByTagName.call(target, localName, + lowercase); + } else { + // When we get a ShadowRoot the logical tree is going to be disconnected + // so we do a manual tree traversal + return findElements(this, index, result, p, localName, lowercase); + } + + return filterNodeList(list, index, result, false); + } + + function getElementsByTagNameNSFiltered(p, index, result, ns, localName) { + var target = unsafeUnwrap(this); + var list; + var root = getTreeScope(this).root; + if (root instanceof scope.wrappers.ShadowRoot) { + // We are in the shadow tree and the logical tree is + // going to be disconnected so we do a manual tree traversal + return findElements(this, index, result, p, ns, localName); + } else if (target instanceof OriginalElement) { + list = originalElementGetElementsByTagNameNS.call(target, ns, localName); + } else if (target instanceof OriginalDocument) { + list = originalDocumentGetElementsByTagNameNS.call(target, ns, localName); + } else { + // When we get a ShadowRoot the logical tree is going to be disconnected + // so we do a manual tree traversal + return findElements(this, index, result, p, ns, localName); + } + + return filterNodeList(list, index, result, false); + } + + var GetElementsByInterface = { + getElementsByTagName: function(localName) { + var result = new HTMLCollection(); + var match = localName === '*' ? matchesEveryThing : matchesTagName; + + result.length = getElementsByTagNameFiltered.call(this, + match, + 0, + result, + localName, + localName.toLowerCase()); + + return result; + }, + + getElementsByClassName: function(className) { + // TODO(arv): Check className? + return this.querySelectorAll('.' + className); + }, + + getElementsByTagNameNS: function(ns, localName) { + var result = new HTMLCollection(); + var match = null; + + if (ns === '*') { + match = localName === '*' ? matchesEveryThing : matchesLocalNameOnly; + } else { + match = localName === '*' ? matchesNameSpace : matchesLocalNameNS; + } + + result.length = getElementsByTagNameNSFiltered.call(this, + match, + 0, + result, + ns || null, + localName); + + return result; + } + }; + + scope.GetElementsByInterface = GetElementsByInterface; + scope.SelectorsInterface = SelectorsInterface; + +})(window.ShadowDOMPolyfill); diff --git a/src/ShadowDOM/wrappers.js b/src/ShadowDOM/wrappers.js new file mode 100644 index 0000000..41fba6c --- /dev/null +++ b/src/ShadowDOM/wrappers.js @@ -0,0 +1,425 @@ +// Copyright 2012 The Polymer Authors. All rights reserved. +// Use of this source code is goverened by a BSD-style +// license that can be found in the LICENSE file. + +window.ShadowDOMPolyfill = {}; + +(function(scope) { + 'use strict'; + + var constructorTable = new WeakMap(); + var nativePrototypeTable = new WeakMap(); + var wrappers = Object.create(null); + + function detectEval() { + // Don't test for eval if we're running in a Chrome App environment. + // We check for APIs set that only exist in a Chrome App context. + if (typeof chrome !== 'undefined' && chrome.app && chrome.app.runtime) { + return false; + } + + // Firefox OS Apps do not allow eval. This feature detection is very hacky + // but even if some other platform adds support for this function this code + // will continue to work. + if (navigator.getDeviceStorage) { + return false; + } + + try { + var f = new Function('return true;'); + return f(); + } catch (ex) { + return false; + } + } + + var hasEval = detectEval(); + + function assert(b) { + if (!b) + throw new Error('Assertion failed'); + }; + + var defineProperty = Object.defineProperty; + var getOwnPropertyNames = Object.getOwnPropertyNames; + var getOwnPropertyDescriptor = Object.getOwnPropertyDescriptor; + + function mixin(to, from) { + var names = getOwnPropertyNames(from); + for (var i = 0; i < names.length; i++) { + var name = names[i]; + defineProperty(to, name, getOwnPropertyDescriptor(from, name)); + } + return to; + }; + + function mixinStatics(to, from) { + var names = getOwnPropertyNames(from); + for (var i = 0; i < names.length; i++) { + var name = names[i]; + switch (name) { + case 'arguments': + case 'caller': + case 'length': + case 'name': + case 'prototype': + case 'toString': + continue; + } + defineProperty(to, name, getOwnPropertyDescriptor(from, name)); + } + return to; + }; + + function oneOf(object, propertyNames) { + for (var i = 0; i < propertyNames.length; i++) { + if (propertyNames[i] in object) + return propertyNames[i]; + } + } + + var nonEnumerableDataDescriptor = { + value: undefined, + configurable: true, + enumerable: false, + writable: true + }; + + function defineNonEnumerableDataProperty(object, name, value) { + nonEnumerableDataDescriptor.value = value; + defineProperty(object, name, nonEnumerableDataDescriptor); + } + + // Mozilla's old DOM bindings are bretty busted: + // https://bugzilla.mozilla.org/show_bug.cgi?id=855844 + // Make sure they are create before we start modifying things. + getOwnPropertyNames(window); + + function getWrapperConstructor(node) { + var nativePrototype = node.__proto__ || Object.getPrototypeOf(node); + var wrapperConstructor = constructorTable.get(nativePrototype); + if (wrapperConstructor) + return wrapperConstructor; + + var parentWrapperConstructor = getWrapperConstructor(nativePrototype); + + var GeneratedWrapper = createWrapperConstructor(parentWrapperConstructor); + registerInternal(nativePrototype, GeneratedWrapper, node); + + return GeneratedWrapper; + } + + function addForwardingProperties(nativePrototype, wrapperPrototype) { + installProperty(nativePrototype, wrapperPrototype, true); + } + + function registerInstanceProperties(wrapperPrototype, instanceObject) { + installProperty(instanceObject, wrapperPrototype, false); + } + + var isFirefox = /Firefox/.test(navigator.userAgent); + + // This is used as a fallback when getting the descriptor fails in + // installProperty. + var dummyDescriptor = { + get: function() {}, + set: function(v) {}, + configurable: true, + enumerable: true + }; + + function isEventHandlerName(name) { + return /^on[a-z]+$/.test(name); + } + + function isIdentifierName(name) { + return /^\w[a-zA-Z_0-9]*$/.test(name); + } + + // The name of the implementation property is intentionally hard to + // remember. Unfortunately, browsers are slower doing obj[expr] than + // obj.foo so we resort to repeat this ugly name. This ugly name is never + // used outside of this file though. + + function getGetter(name) { + return hasEval && isIdentifierName(name) ? + new Function('return this.__impl4cf1e782hg__.' + name) : + function() { return this.__impl4cf1e782hg__[name]; }; + } + + function getSetter(name) { + return hasEval && isIdentifierName(name) ? + new Function('v', 'this.__impl4cf1e782hg__.' + name + ' = v') : + function(v) { this.__impl4cf1e782hg__[name] = v; }; + } + + function getMethod(name) { + return hasEval && isIdentifierName(name) ? + new Function('return this.__impl4cf1e782hg__.' + name + + '.apply(this.__impl4cf1e782hg__, arguments)') : + function() { + return this.__impl4cf1e782hg__[name].apply( + this.__impl4cf1e782hg__, arguments); + }; + } + + function getDescriptor(source, name) { + try { + return Object.getOwnPropertyDescriptor(source, name); + } catch (ex) { + // JSC and V8 both use data properties instead of accessors which can + // cause getting the property desciptor to throw an exception. + // https://bugs.webkit.org/show_bug.cgi?id=49739 + return dummyDescriptor; + } + } + + // Safari 8 exposes WebIDL attributes as an invalid accessor property. Its + // descriptor has {get: undefined, set: undefined}. We therefore ignore the + // shape of the descriptor and make all properties read-write. + // https://bugs.webkit.org/show_bug.cgi?id=49739 + var isBrokenSafari = function() { + var descr = Object.getOwnPropertyDescriptor(Node.prototype, 'nodeType'); + return descr && !descr.get && !descr.set; + }(); + + function installProperty(source, target, allowMethod, opt_blacklist) { + var names = getOwnPropertyNames(source); + for (var i = 0; i < names.length; i++) { + var name = names[i]; + if (name === 'polymerBlackList_') + continue; + + if (name in target) + continue; + + if (source.polymerBlackList_ && source.polymerBlackList_[name]) + continue; + + if (isFirefox) { + // Tickle Firefox's old bindings. + source.__lookupGetter__(name); + } + var descriptor = getDescriptor(source, name); + var getter, setter; + if (allowMethod && typeof descriptor.value === 'function') { + target[name] = getMethod(name); + continue; + } + + var isEvent = isEventHandlerName(name); + if (isEvent) + getter = scope.getEventHandlerGetter(name); + else + getter = getGetter(name); + + if (descriptor.writable || descriptor.set || isBrokenSafari) { + if (isEvent) + setter = scope.getEventHandlerSetter(name); + else + setter = getSetter(name); + } + + defineProperty(target, name, { + get: getter, + set: setter, + configurable: descriptor.configurable, + enumerable: descriptor.enumerable + }); + } + } + + /** + * @param {Function} nativeConstructor + * @param {Function} wrapperConstructor + * @param {Object=} opt_instance If present, this is used to extract + * properties from an instance object. + */ + function register(nativeConstructor, wrapperConstructor, opt_instance) { + var nativePrototype = nativeConstructor.prototype; + registerInternal(nativePrototype, wrapperConstructor, opt_instance); + mixinStatics(wrapperConstructor, nativeConstructor); + } + + function registerInternal(nativePrototype, wrapperConstructor, opt_instance) { + var wrapperPrototype = wrapperConstructor.prototype; + assert(constructorTable.get(nativePrototype) === undefined); + + constructorTable.set(nativePrototype, wrapperConstructor); + nativePrototypeTable.set(wrapperPrototype, nativePrototype); + + addForwardingProperties(nativePrototype, wrapperPrototype); + if (opt_instance) + registerInstanceProperties(wrapperPrototype, opt_instance); + + defineNonEnumerableDataProperty( + wrapperPrototype, 'constructor', wrapperConstructor); + // Set it again. Some VMs optimizes objects that are used as prototypes. + wrapperConstructor.prototype = wrapperPrototype; + } + + function isWrapperFor(wrapperConstructor, nativeConstructor) { + return constructorTable.get(nativeConstructor.prototype) === + wrapperConstructor; + } + + /** + * Creates a generic wrapper constructor based on |object| and its + * constructor. + * @param {Node} object + * @return {Function} The generated constructor. + */ + function registerObject(object) { + var nativePrototype = Object.getPrototypeOf(object); + + var superWrapperConstructor = getWrapperConstructor(nativePrototype); + var GeneratedWrapper = createWrapperConstructor(superWrapperConstructor); + registerInternal(nativePrototype, GeneratedWrapper, object); + + return GeneratedWrapper; + } + + function createWrapperConstructor(superWrapperConstructor) { + function GeneratedWrapper(node) { + superWrapperConstructor.call(this, node); + } + var p = Object.create(superWrapperConstructor.prototype); + p.constructor = GeneratedWrapper; + GeneratedWrapper.prototype = p; + + return GeneratedWrapper; + } + + function isWrapper(object) { + return object && object.__impl4cf1e782hg__; + } + + function isNative(object) { + return !isWrapper(object); + } + + /** + * Wraps a node in a WrapperNode. If there already exists a wrapper for the + * |node| that wrapper is returned instead. + * @param {Node} node + * @return {WrapperNode} + */ + function wrap(impl) { + if (impl === null) + return null; + + assert(isNative(impl)); + return impl.__wrapper8e3dd93a60__ || + (impl.__wrapper8e3dd93a60__ = new (getWrapperConstructor(impl))(impl)); + } + + /** + * Unwraps a wrapper and returns the node it is wrapping. + * @param {WrapperNode} wrapper + * @return {Node} + */ + function unwrap(wrapper) { + if (wrapper === null) + return null; + assert(isWrapper(wrapper)); + return wrapper.__impl4cf1e782hg__; + } + + function unsafeUnwrap(wrapper) { + return wrapper.__impl4cf1e782hg__; + } + + function setWrapper(impl, wrapper) { + wrapper.__impl4cf1e782hg__ = impl; + impl.__wrapper8e3dd93a60__ = wrapper; + } + + /** + * Unwraps object if it is a wrapper. + * @param {Object} object + * @return {Object} The native implementation object. + */ + function unwrapIfNeeded(object) { + return object && isWrapper(object) ? unwrap(object) : object; + } + + /** + * Wraps object if it is not a wrapper. + * @param {Object} object + * @return {Object} The wrapper for object. + */ + function wrapIfNeeded(object) { + return object && !isWrapper(object) ? wrap(object) : object; + } + + /** + * Overrides the current wrapper (if any) for node. + * @param {Node} node + * @param {WrapperNode=} wrapper If left out the wrapper will be created as + * needed next time someone wraps the node. + */ + function rewrap(node, wrapper) { + if (wrapper === null) + return; + assert(isNative(node)); + assert(wrapper === undefined || isWrapper(wrapper)); + node.__wrapper8e3dd93a60__ = wrapper; + } + + var getterDescriptor = { + get: undefined, + configurable: true, + enumerable: true + }; + + function defineGetter(constructor, name, getter) { + getterDescriptor.get = getter; + defineProperty(constructor.prototype, name, getterDescriptor); + } + + function defineWrapGetter(constructor, name) { + defineGetter(constructor, name, function() { + return wrap(this.__impl4cf1e782hg__[name]); + }); + } + + /** + * Forwards existing methods on the native object to the wrapper methods. + * This does not wrap any of the arguments or the return value since the + * wrapper implementation already takes care of that. + * @param {Array.} constructors + * @parem {Array.} names + */ + function forwardMethodsToWrapper(constructors, names) { + constructors.forEach(function(constructor) { + names.forEach(function(name) { + constructor.prototype[name] = function() { + var w = wrapIfNeeded(this); + return w[name].apply(w, arguments); + }; + }); + }); + } + + scope.assert = assert; + scope.constructorTable = constructorTable; + scope.defineGetter = defineGetter; + scope.defineWrapGetter = defineWrapGetter; + scope.forwardMethodsToWrapper = forwardMethodsToWrapper; + scope.isWrapper = isWrapper; + scope.isWrapperFor = isWrapperFor; + scope.mixin = mixin; + scope.nativePrototypeTable = nativePrototypeTable; + scope.oneOf = oneOf; + scope.registerObject = registerObject; + scope.registerWrapper = register; + scope.rewrap = rewrap; + scope.setWrapper = setWrapper; + scope.unsafeUnwrap = unsafeUnwrap; + scope.unwrap = unwrap; + scope.unwrapIfNeeded = unwrapIfNeeded; + scope.wrap = wrap; + scope.wrapIfNeeded = wrapIfNeeded; + scope.wrappers = wrappers; + +})(window.ShadowDOMPolyfill); diff --git a/src/ShadowDOM/wrappers/CanvasRenderingContext2D.js b/src/ShadowDOM/wrappers/CanvasRenderingContext2D.js new file mode 100644 index 0000000..dcbfef6 --- /dev/null +++ b/src/ShadowDOM/wrappers/CanvasRenderingContext2D.js @@ -0,0 +1,42 @@ +// Copyright 2013 The Polymer Authors. All rights reserved. +// Use of this source code is goverened by a BSD-style +// license that can be found in the LICENSE file. + +(function(scope) { + 'use strict'; + + var mixin = scope.mixin; + var registerWrapper = scope.registerWrapper; + var setWrapper = scope.setWrapper; + var unsafeUnwrap = scope.unsafeUnwrap; + var unwrap = scope.unwrap; + var unwrapIfNeeded = scope.unwrapIfNeeded; + var wrap = scope.wrap; + + var OriginalCanvasRenderingContext2D = window.CanvasRenderingContext2D; + + function CanvasRenderingContext2D(impl) { + setWrapper(impl, this); + } + + mixin(CanvasRenderingContext2D.prototype, { + get canvas() { + return wrap(unsafeUnwrap(this).canvas); + }, + + drawImage: function() { + arguments[0] = unwrapIfNeeded(arguments[0]); + unsafeUnwrap(this).drawImage.apply(unsafeUnwrap(this), arguments); + }, + + createPattern: function() { + arguments[0] = unwrap(arguments[0]); + return unsafeUnwrap(this).createPattern.apply(unsafeUnwrap(this), arguments); + } + }); + + registerWrapper(OriginalCanvasRenderingContext2D, CanvasRenderingContext2D, + document.createElement('canvas').getContext('2d')); + + scope.wrappers.CanvasRenderingContext2D = CanvasRenderingContext2D; +})(window.ShadowDOMPolyfill); diff --git a/src/ShadowDOM/wrappers/CharacterData.js b/src/ShadowDOM/wrappers/CharacterData.js new file mode 100644 index 0000000..061da86 --- /dev/null +++ b/src/ShadowDOM/wrappers/CharacterData.js @@ -0,0 +1,46 @@ +// Copyright 2013 The Polymer Authors. All rights reserved. +// Use of this source code is goverened by a BSD-style +// license that can be found in the LICENSE file. + +(function(scope) { + 'use strict'; + + var ChildNodeInterface = scope.ChildNodeInterface; + var Node = scope.wrappers.Node; + var enqueueMutation = scope.enqueueMutation; + var mixin = scope.mixin; + var registerWrapper = scope.registerWrapper; + var unsafeUnwrap = scope.unsafeUnwrap; + + var OriginalCharacterData = window.CharacterData; + + function CharacterData(node) { + Node.call(this, node); + } + CharacterData.prototype = Object.create(Node.prototype); + mixin(CharacterData.prototype, { + get textContent() { + return this.data; + }, + set textContent(value) { + this.data = value; + }, + get data() { + return unsafeUnwrap(this).data; + }, + set data(value) { + var oldValue = unsafeUnwrap(this).data; + enqueueMutation(this, 'characterData', { + oldValue: oldValue + }); + unsafeUnwrap(this).data = value; + } + }); + + mixin(CharacterData.prototype, ChildNodeInterface); + + registerWrapper(OriginalCharacterData, CharacterData, + document.createTextNode('')); + + scope.wrappers.CharacterData = CharacterData; +})(window.ShadowDOMPolyfill); diff --git a/src/ShadowDOM/wrappers/DOMTokenList.js b/src/ShadowDOM/wrappers/DOMTokenList.js new file mode 100644 index 0000000..63ba681 --- /dev/null +++ b/src/ShadowDOM/wrappers/DOMTokenList.js @@ -0,0 +1,50 @@ +// Copyright 2014 The Polymer Authors. All rights reserved. +// Use of this source code is goverened by a BSD-style +// license that can be found in the LICENSE file. + +(function(scope) { + 'use strict'; + + var setWrapper = scope.setWrapper; + var unsafeUnwrap = scope.unsafeUnwrap; + + function invalidateClass(el) { + scope.invalidateRendererBasedOnAttribute(el, 'class'); + } + + function DOMTokenList(impl, ownerElement) { + setWrapper(impl, this); + this.ownerElement_ = ownerElement; + } + + DOMTokenList.prototype = { + constructor: DOMTokenList, + get length() { + return unsafeUnwrap(this).length; + }, + item: function(index) { + return unsafeUnwrap(this).item(index); + }, + contains: function(token) { + return unsafeUnwrap(this).contains(token); + }, + add: function() { + unsafeUnwrap(this).add.apply(unsafeUnwrap(this), arguments); + invalidateClass(this.ownerElement_); + }, + remove: function() { + unsafeUnwrap(this).remove.apply(unsafeUnwrap(this), arguments); + invalidateClass(this.ownerElement_); + }, + toggle: function(token) { + var rv = unsafeUnwrap(this).toggle.apply(unsafeUnwrap(this), arguments); + invalidateClass(this.ownerElement_); + return rv; + }, + toString: function() { + return unsafeUnwrap(this).toString(); + } + }; + + scope.wrappers.DOMTokenList = DOMTokenList; +})(window.ShadowDOMPolyfill); diff --git a/src/ShadowDOM/wrappers/DataTransfer.js b/src/ShadowDOM/wrappers/DataTransfer.js new file mode 100644 index 0000000..ce60e67 --- /dev/null +++ b/src/ShadowDOM/wrappers/DataTransfer.js @@ -0,0 +1,26 @@ +/** + * Copyright 2014 The Polymer Authors. All rights reserved. + * Use of this source code is goverened by a BSD-style + * license that can be found in the LICENSE file. + */ + +(function(scope) { + 'use strict'; + + var unwrap = scope.unwrap; + + // DataTransfer (Clipboard in old Blink/WebKit) has a single method that + // requires wrapping. Since it is only a method we do not need a real wrapper, + // we can just override the method. + + var OriginalDataTransfer = window.DataTransfer || window.Clipboard; + var OriginalDataTransferSetDragImage = + OriginalDataTransfer.prototype.setDragImage; + + if (OriginalDataTransferSetDragImage) { + OriginalDataTransfer.prototype.setDragImage = function(image, x, y) { + OriginalDataTransferSetDragImage.call(this, unwrap(image), x, y); + }; + } + +})(window.ShadowDOMPolyfill); diff --git a/src/ShadowDOM/wrappers/Document.js b/src/ShadowDOM/wrappers/Document.js new file mode 100644 index 0000000..6f0b5d6 --- /dev/null +++ b/src/ShadowDOM/wrappers/Document.js @@ -0,0 +1,333 @@ +// Copyright 2013 The Polymer Authors. All rights reserved. +// Use of this source code is goverened by a BSD-style +// license that can be found in the LICENSE file. + +(function(scope) { + 'use strict'; + + var GetElementsByInterface = scope.GetElementsByInterface; + var Node = scope.wrappers.Node; + var ParentNodeInterface = scope.ParentNodeInterface; + var Selection = scope.wrappers.Selection; + var SelectorsInterface = scope.SelectorsInterface; + var ShadowRoot = scope.wrappers.ShadowRoot; + var TreeScope = scope.TreeScope; + var cloneNode = scope.cloneNode; + var defineWrapGetter = scope.defineWrapGetter; + var elementFromPoint = scope.elementFromPoint; + var forwardMethodsToWrapper = scope.forwardMethodsToWrapper; + var matchesNames = scope.matchesNames; + var mixin = scope.mixin; + var registerWrapper = scope.registerWrapper; + var renderAllPending = scope.renderAllPending; + var rewrap = scope.rewrap; + var setWrapper = scope.setWrapper; + var unsafeUnwrap = scope.unsafeUnwrap; + var unwrap = scope.unwrap; + var wrap = scope.wrap; + var wrapEventTargetMethods = scope.wrapEventTargetMethods; + var wrapNodeList = scope.wrapNodeList; + + var implementationTable = new WeakMap(); + + function Document(node) { + Node.call(this, node); + this.treeScope_ = new TreeScope(this, null); + } + Document.prototype = Object.create(Node.prototype); + + defineWrapGetter(Document, 'documentElement'); + + // Conceptually both body and head can be in a shadow but suporting that seems + // overkill at this point. + defineWrapGetter(Document, 'body'); + defineWrapGetter(Document, 'head'); + + // document cannot be overridden so we override a bunch of its methods + // directly on the instance. + + function wrapMethod(name) { + var original = document[name]; + Document.prototype[name] = function() { + return wrap(original.apply(unsafeUnwrap(this), arguments)); + }; + } + + [ + 'createComment', + 'createDocumentFragment', + 'createElement', + 'createElementNS', + 'createEvent', + 'createEventNS', + 'createRange', + 'createTextNode', + 'getElementById' + ].forEach(wrapMethod); + + var originalAdoptNode = document.adoptNode; + + function adoptNodeNoRemove(node, doc) { + originalAdoptNode.call(unsafeUnwrap(doc), unwrap(node)); + adoptSubtree(node, doc); + } + + function adoptSubtree(node, doc) { + if (node.shadowRoot) + doc.adoptNode(node.shadowRoot); + if (node instanceof ShadowRoot) + adoptOlderShadowRoots(node, doc); + for (var child = node.firstChild; child; child = child.nextSibling) { + adoptSubtree(child, doc); + } + } + + function adoptOlderShadowRoots(shadowRoot, doc) { + var oldShadowRoot = shadowRoot.olderShadowRoot; + if (oldShadowRoot) + doc.adoptNode(oldShadowRoot); + } + + var originalGetSelection = document.getSelection; + + mixin(Document.prototype, { + adoptNode: function(node) { + if (node.parentNode) + node.parentNode.removeChild(node); + adoptNodeNoRemove(node, this); + return node; + }, + elementFromPoint: function(x, y) { + return elementFromPoint(this, this, x, y); + }, + importNode: function(node, deep) { + return cloneNode(node, deep, unsafeUnwrap(this)); + }, + getSelection: function() { + renderAllPending(); + return new Selection(originalGetSelection.call(unwrap(this))); + }, + getElementsByName: function(name) { + return SelectorsInterface.querySelectorAll.call(this, + '[name=' + JSON.stringify(String(name)) + ']'); + } + }); + + if (document.registerElement) { + var originalRegisterElement = document.registerElement; + Document.prototype.registerElement = function(tagName, object) { + var prototype, extendsOption; + if (object !== undefined) { + prototype = object.prototype; + extendsOption = object.extends; + } + + if (!prototype) + prototype = Object.create(HTMLElement.prototype); + + + // If we already used the object as a prototype for another custom + // element. + if (scope.nativePrototypeTable.get(prototype)) { + // TODO(arv): DOMException + throw new Error('NotSupportedError'); + } + + // Find first object on the prototype chain that already have a native + // prototype. Keep track of all the objects before that so we can create + // a similar structure for the native case. + var proto = Object.getPrototypeOf(prototype); + var nativePrototype; + var prototypes = []; + while (proto) { + nativePrototype = scope.nativePrototypeTable.get(proto); + if (nativePrototype) + break; + prototypes.push(proto); + proto = Object.getPrototypeOf(proto); + } + + if (!nativePrototype) { + // TODO(arv): DOMException + throw new Error('NotSupportedError'); + } + + // This works by creating a new prototype object that is empty, but has + // the native prototype as its proto. The original prototype object + // passed into register is used as the wrapper prototype. + + var newPrototype = Object.create(nativePrototype); + for (var i = prototypes.length - 1; i >= 0; i--) { + newPrototype = Object.create(newPrototype); + } + + // Add callbacks if present. + // Names are taken from: + // https://code.google.com/p/chromium/codesearch#chromium/src/third_party/WebKit/Source/bindings/v8/CustomElementConstructorBuilder.cpp&sq=package:chromium&type=cs&l=156 + // and not from the spec since the spec is out of date. + [ + 'createdCallback', + 'attachedCallback', + 'detachedCallback', + 'attributeChangedCallback', + ].forEach(function(name) { + var f = prototype[name]; + if (!f) + return; + newPrototype[name] = function() { + // if this element has been wrapped prior to registration, + // the wrapper is stale; in this case rewrap + if (!(wrap(this) instanceof CustomElementConstructor)) { + rewrap(this); + } + f.apply(wrap(this), arguments); + }; + }); + + var p = {prototype: newPrototype}; + if (extendsOption) + p.extends = extendsOption; + + function CustomElementConstructor(node) { + if (!node) { + if (extendsOption) { + return document.createElement(extendsOption, tagName); + } else { + return document.createElement(tagName); + } + } + setWrapper(node, this); + } + CustomElementConstructor.prototype = prototype; + CustomElementConstructor.prototype.constructor = CustomElementConstructor; + + scope.constructorTable.set(newPrototype, CustomElementConstructor); + scope.nativePrototypeTable.set(prototype, newPrototype); + + // registration is synchronous so do it last + var nativeConstructor = originalRegisterElement.call(unwrap(this), + tagName, p); + return CustomElementConstructor; + }; + + forwardMethodsToWrapper([ + window.HTMLDocument || window.Document, // Gecko adds these to HTMLDocument + ], [ + 'registerElement', + ]); + } + + // We also override some of the methods on document.body and document.head + // for convenience. + forwardMethodsToWrapper([ + window.HTMLBodyElement, + window.HTMLDocument || window.Document, // Gecko adds these to HTMLDocument + window.HTMLHeadElement, + window.HTMLHtmlElement, + ], [ + 'appendChild', + 'compareDocumentPosition', + 'contains', + 'getElementsByClassName', + 'getElementsByTagName', + 'getElementsByTagNameNS', + 'insertBefore', + 'querySelector', + 'querySelectorAll', + 'removeChild', + 'replaceChild', + ].concat(matchesNames)); + + forwardMethodsToWrapper([ + window.HTMLDocument || window.Document, // Gecko adds these to HTMLDocument + ], [ + 'adoptNode', + 'importNode', + 'contains', + 'createComment', + 'createDocumentFragment', + 'createElement', + 'createElementNS', + 'createEvent', + 'createEventNS', + 'createRange', + 'createTextNode', + 'elementFromPoint', + 'getElementById', + 'getElementsByName', + 'getSelection', + ]); + + mixin(Document.prototype, GetElementsByInterface); + mixin(Document.prototype, ParentNodeInterface); + mixin(Document.prototype, SelectorsInterface); + + mixin(Document.prototype, { + get implementation() { + var implementation = implementationTable.get(this); + if (implementation) + return implementation; + implementation = + new DOMImplementation(unwrap(this).implementation); + implementationTable.set(this, implementation); + return implementation; + }, + + get defaultView() { + return wrap(unwrap(this).defaultView); + } + }); + + registerWrapper(window.Document, Document, + document.implementation.createHTMLDocument('')); + + // Both WebKit and Gecko uses HTMLDocument for document. HTML5/DOM only has + // one Document interface and IE implements the standard correctly. + if (window.HTMLDocument) + registerWrapper(window.HTMLDocument, Document); + + wrapEventTargetMethods([ + window.HTMLBodyElement, + window.HTMLDocument || window.Document, // Gecko adds these to HTMLDocument + window.HTMLHeadElement, + ]); + + function DOMImplementation(impl) { + setWrapper(impl, this); + } + + function wrapImplMethod(constructor, name) { + var original = document.implementation[name]; + constructor.prototype[name] = function() { + return wrap(original.apply(unsafeUnwrap(this), arguments)); + }; + } + + function forwardImplMethod(constructor, name) { + var original = document.implementation[name]; + constructor.prototype[name] = function() { + return original.apply(unsafeUnwrap(this), arguments); + }; + } + + wrapImplMethod(DOMImplementation, 'createDocumentType'); + wrapImplMethod(DOMImplementation, 'createDocument'); + wrapImplMethod(DOMImplementation, 'createHTMLDocument'); + forwardImplMethod(DOMImplementation, 'hasFeature'); + + registerWrapper(window.DOMImplementation, DOMImplementation); + + forwardMethodsToWrapper([ + window.DOMImplementation, + ], [ + 'createDocumentType', + 'createDocument', + 'createHTMLDocument', + 'hasFeature', + ]); + + scope.adoptNodeNoRemove = adoptNodeNoRemove; + scope.wrappers.DOMImplementation = DOMImplementation; + scope.wrappers.Document = Document; + +})(window.ShadowDOMPolyfill); diff --git a/src/ShadowDOM/wrappers/Element.js b/src/ShadowDOM/wrappers/Element.js new file mode 100644 index 0000000..4a199b4 --- /dev/null +++ b/src/ShadowDOM/wrappers/Element.js @@ -0,0 +1,150 @@ +// Copyright 2013 The Polymer Authors. All rights reserved. +// Use of this source code is goverened by a BSD-style +// license that can be found in the LICENSE file. + +(function(scope) { + 'use strict'; + + var ChildNodeInterface = scope.ChildNodeInterface; + var GetElementsByInterface = scope.GetElementsByInterface; + var Node = scope.wrappers.Node; + var DOMTokenList = scope.wrappers.DOMTokenList; + var ParentNodeInterface = scope.ParentNodeInterface; + var SelectorsInterface = scope.SelectorsInterface; + var addWrapNodeListMethod = scope.addWrapNodeListMethod; + var enqueueMutation = scope.enqueueMutation; + var mixin = scope.mixin; + var oneOf = scope.oneOf; + var registerWrapper = scope.registerWrapper; + var unsafeUnwrap = scope.unsafeUnwrap; + var wrappers = scope.wrappers; + + var OriginalElement = window.Element; + + var matchesNames = [ + 'matches', // needs to come first. + 'mozMatchesSelector', + 'msMatchesSelector', + 'webkitMatchesSelector', + ].filter(function(name) { + return OriginalElement.prototype[name]; + }); + + var matchesName = matchesNames[0]; + + var originalMatches = OriginalElement.prototype[matchesName]; + + function invalidateRendererBasedOnAttribute(element, name) { + // Only invalidate if parent node is a shadow host. + var p = element.parentNode; + if (!p || !p.shadowRoot) + return; + + var renderer = scope.getRendererForHost(p); + if (renderer.dependsOnAttribute(name)) + renderer.invalidate(); + } + + function enqueAttributeChange(element, name, oldValue) { + // This is not fully spec compliant. We should use localName (which might + // have a different case than name) and the namespace (which requires us + // to get the Attr object). + enqueueMutation(element, 'attributes', { + name: name, + namespace: null, + oldValue: oldValue + }); + } + + var classListTable = new WeakMap(); + + function Element(node) { + Node.call(this, node); + } + Element.prototype = Object.create(Node.prototype); + mixin(Element.prototype, { + createShadowRoot: function() { + var newShadowRoot = new wrappers.ShadowRoot(this); + unsafeUnwrap(this).polymerShadowRoot_ = newShadowRoot; + + var renderer = scope.getRendererForHost(this); + renderer.invalidate(); + + return newShadowRoot; + }, + + get shadowRoot() { + return unsafeUnwrap(this).polymerShadowRoot_ || null; + }, + + // getDestinationInsertionPoints added in ShadowRenderer.js + + setAttribute: function(name, value) { + var oldValue = unsafeUnwrap(this).getAttribute(name); + unsafeUnwrap(this).setAttribute(name, value); + enqueAttributeChange(this, name, oldValue); + invalidateRendererBasedOnAttribute(this, name); + }, + + removeAttribute: function(name) { + var oldValue = unsafeUnwrap(this).getAttribute(name); + unsafeUnwrap(this).removeAttribute(name); + enqueAttributeChange(this, name, oldValue); + invalidateRendererBasedOnAttribute(this, name); + }, + + matches: function(selector) { + return originalMatches.call(unsafeUnwrap(this), selector); + }, + + get classList() { + var list = classListTable.get(this); + if (!list) { + classListTable.set(this, + list = new DOMTokenList(unsafeUnwrap(this).classList, this)); + } + return list; + }, + + get className() { + return unsafeUnwrap(this).className; + }, + + set className(v) { + this.setAttribute('class', v); + }, + + get id() { + return unsafeUnwrap(this).id; + }, + + set id(v) { + this.setAttribute('id', v); + } + }); + + matchesNames.forEach(function(name) { + if (name !== 'matches') { + Element.prototype[name] = function(selector) { + return this.matches(selector); + }; + } + }); + + if (OriginalElement.prototype.webkitCreateShadowRoot) { + Element.prototype.webkitCreateShadowRoot = + Element.prototype.createShadowRoot; + } + + mixin(Element.prototype, ChildNodeInterface); + mixin(Element.prototype, GetElementsByInterface); + mixin(Element.prototype, ParentNodeInterface); + mixin(Element.prototype, SelectorsInterface); + + registerWrapper(OriginalElement, Element, + document.createElementNS(null, 'x')); + + scope.invalidateRendererBasedOnAttribute = invalidateRendererBasedOnAttribute; + scope.matchesNames = matchesNames; + scope.wrappers.Element = Element; +})(window.ShadowDOMPolyfill); diff --git a/src/ShadowDOM/wrappers/FormData.js b/src/ShadowDOM/wrappers/FormData.js new file mode 100644 index 0000000..3082a20 --- /dev/null +++ b/src/ShadowDOM/wrappers/FormData.js @@ -0,0 +1,31 @@ +/** + * Copyright 2014 The Polymer Authors. All rights reserved. + * Use of this source code is goverened by a BSD-style + * license that can be found in the LICENSE file. + */ + +(function(scope) { + 'use strict'; + + var registerWrapper = scope.registerWrapper; + var setWrapper = scope.setWrapper; + var unwrap = scope.unwrap; + + var OriginalFormData = window.FormData; + if (!OriginalFormData) return; + + function FormData(formElement) { + var impl; + if (formElement instanceof OriginalFormData) { + impl = formElement; + } else { + impl = new OriginalFormData(formElement && unwrap(formElement)); + } + setWrapper(impl, this); + } + + registerWrapper(OriginalFormData, FormData, new OriginalFormData()); + + scope.wrappers.FormData = FormData; + +})(window.ShadowDOMPolyfill); diff --git a/src/ShadowDOM/wrappers/HTMLAudioElement.js b/src/ShadowDOM/wrappers/HTMLAudioElement.js new file mode 100644 index 0000000..f87b196 --- /dev/null +++ b/src/ShadowDOM/wrappers/HTMLAudioElement.js @@ -0,0 +1,44 @@ +// Copyright 2013 The Polymer Authors. All rights reserved. +// Use of this source code is goverened by a BSD-style +// license that can be found in the LICENSE file. + +(function(scope) { + 'use strict'; + + var HTMLMediaElement = scope.wrappers.HTMLMediaElement; + var registerWrapper = scope.registerWrapper; + var unwrap = scope.unwrap; + var rewrap = scope.rewrap; + + var OriginalHTMLAudioElement = window.HTMLAudioElement; + + if (!OriginalHTMLAudioElement) return; + + function HTMLAudioElement(node) { + HTMLMediaElement.call(this, node); + } + HTMLAudioElement.prototype = Object.create(HTMLMediaElement.prototype); + + registerWrapper(OriginalHTMLAudioElement, HTMLAudioElement, + document.createElement('audio')); + + function Audio(src) { + if (!(this instanceof Audio)) { + throw new TypeError( + 'DOM object constructor cannot be called as a function.'); + } + + var node = unwrap(document.createElement('audio')); + HTMLMediaElement.call(this, node); + rewrap(node, this); + + node.setAttribute('preload', 'auto'); + if (src !== undefined) + node.setAttribute('src', src); + } + + Audio.prototype = HTMLAudioElement.prototype; + + scope.wrappers.HTMLAudioElement = HTMLAudioElement; + scope.wrappers.Audio = Audio; +})(window.ShadowDOMPolyfill); diff --git a/src/ShadowDOM/wrappers/HTMLCanvasElement.js b/src/ShadowDOM/wrappers/HTMLCanvasElement.js new file mode 100644 index 0000000..7162bf1 --- /dev/null +++ b/src/ShadowDOM/wrappers/HTMLCanvasElement.js @@ -0,0 +1,32 @@ +// Copyright 2013 The Polymer Authors. All rights reserved. +// Use of this source code is goverened by a BSD-style +// license that can be found in the LICENSE file. + +(function(scope) { + 'use strict'; + + var HTMLElement = scope.wrappers.HTMLElement; + var mixin = scope.mixin; + var registerWrapper = scope.registerWrapper; + var unsafeUnwrap = scope.unsafeUnwrap; + var wrap = scope.wrap; + + var OriginalHTMLCanvasElement = window.HTMLCanvasElement; + + function HTMLCanvasElement(node) { + HTMLElement.call(this, node); + } + HTMLCanvasElement.prototype = Object.create(HTMLElement.prototype); + + mixin(HTMLCanvasElement.prototype, { + getContext: function() { + var context = unsafeUnwrap(this).getContext.apply(unsafeUnwrap(this), arguments); + return context && wrap(context); + } + }); + + registerWrapper(OriginalHTMLCanvasElement, HTMLCanvasElement, + document.createElement('canvas')); + + scope.wrappers.HTMLCanvasElement = HTMLCanvasElement; +})(window.ShadowDOMPolyfill); diff --git a/src/ShadowDOM/wrappers/HTMLCollection.js b/src/ShadowDOM/wrappers/HTMLCollection.js new file mode 100644 index 0000000..70b5c1a --- /dev/null +++ b/src/ShadowDOM/wrappers/HTMLCollection.js @@ -0,0 +1,15 @@ +/* + * Copyright 2014 The Polymer Authors. All rights reserved. + * Use of this source code is goverened by a BSD-style + * license that can be found in the LICENSE file. + */ + +(function(scope) { + 'use strict'; + + // TODO(arv): Implement. + + scope.wrapHTMLCollection = scope.wrapNodeList; + scope.wrappers.HTMLCollection = scope.wrappers.NodeList; + +})(window.ShadowDOMPolyfill); diff --git a/src/ShadowDOM/wrappers/HTMLContentElement.js b/src/ShadowDOM/wrappers/HTMLContentElement.js new file mode 100644 index 0000000..7d70007 --- /dev/null +++ b/src/ShadowDOM/wrappers/HTMLContentElement.js @@ -0,0 +1,41 @@ +// Copyright 2013 The Polymer Authors. All rights reserved. +// Use of this source code is goverened by a BSD-style +// license that can be found in the LICENSE file. + +(function(scope) { + 'use strict'; + + var HTMLElement = scope.wrappers.HTMLElement; + var mixin = scope.mixin; + var registerWrapper = scope.registerWrapper; + + var OriginalHTMLContentElement = window.HTMLContentElement; + + function HTMLContentElement(node) { + HTMLElement.call(this, node); + } + HTMLContentElement.prototype = Object.create(HTMLElement.prototype); + mixin(HTMLContentElement.prototype, { + constructor: HTMLContentElement, + + get select() { + return this.getAttribute('select'); + }, + set select(value) { + this.setAttribute('select', value); + }, + + setAttribute: function(n, v) { + HTMLElement.prototype.setAttribute.call(this, n, v); + if (String(n).toLowerCase() === 'select') + this.invalidateShadowRenderer(true); + } + + // getDistributedNodes is added in ShadowRenderer + }); + + if (OriginalHTMLContentElement) + registerWrapper(OriginalHTMLContentElement, HTMLContentElement); + + scope.wrappers.HTMLContentElement = HTMLContentElement; +})(window.ShadowDOMPolyfill); diff --git a/src/ShadowDOM/wrappers/HTMLElement.js b/src/ShadowDOM/wrappers/HTMLElement.js new file mode 100644 index 0000000..b88b710 --- /dev/null +++ b/src/ShadowDOM/wrappers/HTMLElement.js @@ -0,0 +1,327 @@ +// Copyright 2013 The Polymer Authors. All rights reserved. +// Use of this source code is goverened by a BSD-style +// license that can be found in the LICENSE file. + +(function(scope) { + 'use strict'; + + var Element = scope.wrappers.Element; + var defineGetter = scope.defineGetter; + var enqueueMutation = scope.enqueueMutation; + var mixin = scope.mixin; + var nodesWereAdded = scope.nodesWereAdded; + var nodesWereRemoved = scope.nodesWereRemoved; + var registerWrapper = scope.registerWrapper; + var snapshotNodeList = scope.snapshotNodeList; + var unsafeUnwrap = scope.unsafeUnwrap; + var unwrap = scope.unwrap; + var wrap = scope.wrap; + var wrappers = scope.wrappers; + + ///////////////////////////////////////////////////////////////////////////// + // innerHTML and outerHTML + + // http://www.whatwg.org/specs/web-apps/current-work/multipage/the-end.html#escapingString + var escapeAttrRegExp = /[&\u00A0"]/g; + var escapeDataRegExp = /[&\u00A0<>]/g; + + function escapeReplace(c) { + switch (c) { + case '&': + return '&'; + case '<': + return '<'; + case '>': + return '>'; + case '"': + return '"' + case '\u00A0': + return ' '; + } + } + + function escapeAttr(s) { + return s.replace(escapeAttrRegExp, escapeReplace); + } + + function escapeData(s) { + return s.replace(escapeDataRegExp, escapeReplace); + } + + function makeSet(arr) { + var set = {}; + for (var i = 0; i < arr.length; i++) { + set[arr[i]] = true; + } + return set; + } + + // http://www.whatwg.org/specs/web-apps/current-work/#void-elements + var voidElements = makeSet([ + 'area', + 'base', + 'br', + 'col', + 'command', + 'embed', + 'hr', + 'img', + 'input', + 'keygen', + 'link', + 'meta', + 'param', + 'source', + 'track', + 'wbr' + ]); + + var plaintextParents = makeSet([ + 'style', + 'script', + 'xmp', + 'iframe', + 'noembed', + 'noframes', + 'plaintext', + 'noscript' + ]); + + function getOuterHTML(node, parentNode) { + switch (node.nodeType) { + case Node.ELEMENT_NODE: + var tagName = node.tagName.toLowerCase(); + var s = '<' + tagName; + var attrs = node.attributes; + for (var i = 0, attr; attr = attrs[i]; i++) { + s += ' ' + attr.name + '="' + escapeAttr(attr.value) + '"'; + } + s += '>'; + if (voidElements[tagName]) + return s; + + return s + getInnerHTML(node) + ''; + + case Node.TEXT_NODE: + var data = node.data; + if (parentNode && plaintextParents[parentNode.localName]) + return data; + return escapeData(data); + + case Node.COMMENT_NODE: + return ''; + + default: + console.error(node); + throw new Error('not implemented'); + } + } + + function getInnerHTML(node) { + if (node instanceof wrappers.HTMLTemplateElement) + node = node.content; + + var s = ''; + for (var child = node.firstChild; child; child = child.nextSibling) { + s += getOuterHTML(child, node); + } + return s; + } + + function setInnerHTML(node, value, opt_tagName) { + var tagName = opt_tagName || 'div'; + node.textContent = ''; + var tempElement = unwrap(node.ownerDocument.createElement(tagName)); + tempElement.innerHTML = value; + var firstChild; + while (firstChild = tempElement.firstChild) { + node.appendChild(wrap(firstChild)); + } + } + + // IE11 does not have MSIE in the user agent string. + var oldIe = /MSIE/.test(navigator.userAgent); + + var OriginalHTMLElement = window.HTMLElement; + var OriginalHTMLTemplateElement = window.HTMLTemplateElement; + + function HTMLElement(node) { + Element.call(this, node); + } + HTMLElement.prototype = Object.create(Element.prototype); + mixin(HTMLElement.prototype, { + get innerHTML() { + return getInnerHTML(this); + }, + set innerHTML(value) { + // IE9 does not handle set innerHTML correctly on plaintextParents. It + // creates element children. For example + // + // scriptElement.innerHTML = 'test' + // + // Creates a single HTMLAnchorElement child. + if (oldIe && plaintextParents[this.localName]) { + this.textContent = value; + return; + } + + var removedNodes = snapshotNodeList(this.childNodes); + + if (this.invalidateShadowRenderer()) { + if (this instanceof wrappers.HTMLTemplateElement) + setInnerHTML(this.content, value); + else + setInnerHTML(this, value, this.tagName); + + // If we have a non native template element we need to handle this + // manually since setting impl.innerHTML would add the html as direct + // children and not be moved over to the content fragment. + } else if (!OriginalHTMLTemplateElement && + this instanceof wrappers.HTMLTemplateElement) { + setInnerHTML(this.content, value); + } else { + unsafeUnwrap(this).innerHTML = value; + } + + var addedNodes = snapshotNodeList(this.childNodes); + + enqueueMutation(this, 'childList', { + addedNodes: addedNodes, + removedNodes: removedNodes + }); + + nodesWereRemoved(removedNodes); + nodesWereAdded(addedNodes, this); + }, + + get outerHTML() { + return getOuterHTML(this, this.parentNode); + }, + set outerHTML(value) { + var p = this.parentNode; + if (p) { + p.invalidateShadowRenderer(); + var df = frag(p, value); + p.replaceChild(df, this); + } + }, + + insertAdjacentHTML: function(position, text) { + var contextElement, refNode; + switch (String(position).toLowerCase()) { + case 'beforebegin': + contextElement = this.parentNode; + refNode = this; + break; + case 'afterend': + contextElement = this.parentNode; + refNode = this.nextSibling; + break; + case 'afterbegin': + contextElement = this; + refNode = this.firstChild; + break; + case 'beforeend': + contextElement = this; + refNode = null; + break; + default: + return; + } + + var df = frag(contextElement, text); + contextElement.insertBefore(df, refNode); + }, + + get hidden() { + return this.hasAttribute('hidden'); + }, + set hidden(v) { + if (v) { + this.setAttribute('hidden', ''); + } else { + this.removeAttribute('hidden'); + } + } + }); + + function frag(contextElement, html) { + // TODO(arv): This does not work with SVG and other non HTML elements. + var p = unwrap(contextElement.cloneNode(false)); + p.innerHTML = html; + var df = unwrap(document.createDocumentFragment()); + var c; + while (c = p.firstChild) { + df.appendChild(c); + } + return wrap(df); + } + + function getter(name) { + return function() { + scope.renderAllPending(); + return unsafeUnwrap(this)[name]; + }; + } + + function getterRequiresRendering(name) { + defineGetter(HTMLElement, name, getter(name)); + } + + [ + 'clientHeight', + 'clientLeft', + 'clientTop', + 'clientWidth', + 'offsetHeight', + 'offsetLeft', + 'offsetTop', + 'offsetWidth', + 'scrollHeight', + 'scrollWidth', + ].forEach(getterRequiresRendering); + + function getterAndSetterRequiresRendering(name) { + Object.defineProperty(HTMLElement.prototype, name, { + get: getter(name), + set: function(v) { + scope.renderAllPending(); + unsafeUnwrap(this)[name] = v; + }, + configurable: true, + enumerable: true + }); + } + + [ + 'scrollLeft', + 'scrollTop', + ].forEach(getterAndSetterRequiresRendering); + + function methodRequiresRendering(name) { + Object.defineProperty(HTMLElement.prototype, name, { + value: function() { + scope.renderAllPending(); + return unsafeUnwrap(this)[name].apply(unsafeUnwrap(this), arguments); + }, + configurable: true, + enumerable: true + }); + } + + [ + 'getBoundingClientRect', + 'getClientRects', + 'scrollIntoView' + ].forEach(methodRequiresRendering); + + // HTMLElement is abstract so we use a subclass that has no members. + registerWrapper(OriginalHTMLElement, HTMLElement, + document.createElement('b')); + + scope.wrappers.HTMLElement = HTMLElement; + + // TODO: Find a better way to share these two with WrapperShadowRoot. + scope.getInnerHTML = getInnerHTML; + scope.setInnerHTML = setInnerHTML +})(window.ShadowDOMPolyfill); diff --git a/src/ShadowDOM/wrappers/HTMLFormElement.js b/src/ShadowDOM/wrappers/HTMLFormElement.js new file mode 100644 index 0000000..842066f --- /dev/null +++ b/src/ShadowDOM/wrappers/HTMLFormElement.js @@ -0,0 +1,35 @@ +/* + * Copyright 2014 The Polymer Authors. All rights reserved. + * Use of this source code is governed by a BSD-style + * license that can be found in the LICENSE file. + */ + +(function(scope) { + 'use strict'; + + var HTMLElement = scope.wrappers.HTMLElement; + var mixin = scope.mixin; + var registerWrapper = scope.registerWrapper; + var wrapHTMLCollection = scope.wrapHTMLCollection; + var unwrap = scope.unwrap; + + var OriginalHTMLFormElement = window.HTMLFormElement; + + function HTMLFormElement(node) { + HTMLElement.call(this, node); + } + HTMLFormElement.prototype = Object.create(HTMLElement.prototype); + mixin(HTMLFormElement.prototype, { + get elements() { + // Note: technically this should be an HTMLFormControlsCollection, but + // that inherits from HTMLCollection, so should be good enough. Spec: + // http://www.whatwg.org/specs/web-apps/current-work/multipage/common-dom-interfaces.html#htmlformcontrolscollection + return wrapHTMLCollection(unwrap(this).elements); + } + }); + + registerWrapper(OriginalHTMLFormElement, HTMLFormElement, + document.createElement('form')); + + scope.wrappers.HTMLFormElement = HTMLFormElement; +})(window.ShadowDOMPolyfill); diff --git a/src/ShadowDOM/wrappers/HTMLImageElement.js b/src/ShadowDOM/wrappers/HTMLImageElement.js new file mode 100644 index 0000000..b6be685 --- /dev/null +++ b/src/ShadowDOM/wrappers/HTMLImageElement.js @@ -0,0 +1,43 @@ +// Copyright 2013 The Polymer Authors. All rights reserved. +// Use of this source code is goverened by a BSD-style +// license that can be found in the LICENSE file. + +(function(scope) { + 'use strict'; + + var HTMLElement = scope.wrappers.HTMLElement; + var registerWrapper = scope.registerWrapper; + var unwrap = scope.unwrap; + var rewrap = scope.rewrap; + + var OriginalHTMLImageElement = window.HTMLImageElement; + + function HTMLImageElement(node) { + HTMLElement.call(this, node); + } + HTMLImageElement.prototype = Object.create(HTMLElement.prototype); + + registerWrapper(OriginalHTMLImageElement, HTMLImageElement, + document.createElement('img')); + + function Image(width, height) { + if (!(this instanceof Image)) { + throw new TypeError( + 'DOM object constructor cannot be called as a function.'); + } + + var node = unwrap(document.createElement('img')); + HTMLElement.call(this, node); + rewrap(node, this); + + if (width !== undefined) + node.width = width; + if (height !== undefined) + node.height = height; + } + + Image.prototype = HTMLImageElement.prototype; + + scope.wrappers.HTMLImageElement = HTMLImageElement; + scope.wrappers.Image = Image; +})(window.ShadowDOMPolyfill); diff --git a/src/ShadowDOM/wrappers/HTMLMediaElement.js b/src/ShadowDOM/wrappers/HTMLMediaElement.js new file mode 100644 index 0000000..0133d14 --- /dev/null +++ b/src/ShadowDOM/wrappers/HTMLMediaElement.js @@ -0,0 +1,24 @@ +// Copyright 2013 The Polymer Authors. All rights reserved. +// Use of this source code is goverened by a BSD-style +// license that can be found in the LICENSE file. + +(function(scope) { + 'use strict'; + + var HTMLElement = scope.wrappers.HTMLElement; + var registerWrapper = scope.registerWrapper; + + var OriginalHTMLMediaElement = window.HTMLMediaElement; + + if (!OriginalHTMLMediaElement) return; + + function HTMLMediaElement(node) { + HTMLElement.call(this, node); + } + HTMLMediaElement.prototype = Object.create(HTMLElement.prototype); + + registerWrapper(OriginalHTMLMediaElement, HTMLMediaElement, + document.createElement('audio')); + + scope.wrappers.HTMLMediaElement = HTMLMediaElement; +})(window.ShadowDOMPolyfill); diff --git a/src/ShadowDOM/wrappers/HTMLOptionElement.js b/src/ShadowDOM/wrappers/HTMLOptionElement.js new file mode 100644 index 0000000..ea7a751 --- /dev/null +++ b/src/ShadowDOM/wrappers/HTMLOptionElement.js @@ -0,0 +1,63 @@ +// Copyright 2013 The Polymer Authors. All rights reserved. +// Use of this source code is goverened by a BSD-style +// license that can be found in the LICENSE file. + +(function(scope) { + 'use strict'; + + var HTMLElement = scope.wrappers.HTMLElement; + var mixin = scope.mixin; + var registerWrapper = scope.registerWrapper; + var rewrap = scope.rewrap; + var unwrap = scope.unwrap; + var wrap = scope.wrap; + + var OriginalHTMLOptionElement = window.HTMLOptionElement; + + function trimText(s) { + return s.replace(/\s+/g, ' ').trim(); + } + + function HTMLOptionElement(node) { + HTMLElement.call(this, node); + } + HTMLOptionElement.prototype = Object.create(HTMLElement.prototype); + mixin(HTMLOptionElement.prototype, { + get text() { + return trimText(this.textContent); + }, + set text(value) { + this.textContent = trimText(String(value)); + }, + get form() { + return wrap(unwrap(this).form); + } + }); + + registerWrapper(OriginalHTMLOptionElement, HTMLOptionElement, + document.createElement('option')); + + function Option(text, value, defaultSelected, selected) { + if (!(this instanceof Option)) { + throw new TypeError( + 'DOM object constructor cannot be called as a function.'); + } + + var node = unwrap(document.createElement('option')); + HTMLElement.call(this, node); + rewrap(node, this); + + if (text !== undefined) + node.text = text; + if (value !== undefined) + node.setAttribute('value', value); + if (defaultSelected === true) + node.setAttribute('selected', ''); + node.selected = selected === true; + } + + Option.prototype = HTMLOptionElement.prototype; + + scope.wrappers.HTMLOptionElement = HTMLOptionElement; + scope.wrappers.Option = Option; +})(window.ShadowDOMPolyfill); diff --git a/src/ShadowDOM/wrappers/HTMLSelectElement.js b/src/ShadowDOM/wrappers/HTMLSelectElement.js new file mode 100644 index 0000000..72c32a2 --- /dev/null +++ b/src/ShadowDOM/wrappers/HTMLSelectElement.js @@ -0,0 +1,50 @@ +// Copyright 2014 The Polymer Authors. All rights reserved. +// Use of this source code is goverened by a BSD-style +// license that can be found in the LICENSE file. + +(function(scope) { + 'use strict'; + + var HTMLElement = scope.wrappers.HTMLElement; + var mixin = scope.mixin; + var registerWrapper = scope.registerWrapper; + var unwrap = scope.unwrap; + var wrap = scope.wrap; + + var OriginalHTMLSelectElement = window.HTMLSelectElement; + + function HTMLSelectElement(node) { + HTMLElement.call(this, node); + } + HTMLSelectElement.prototype = Object.create(HTMLElement.prototype); + mixin(HTMLSelectElement.prototype, { + add: function(element, before) { + if (typeof before === 'object') // also includes null + before = unwrap(before); + unwrap(this).add(unwrap(element), before); + }, + + remove: function(indexOrNode) { + // Spec only allows index but implementations allow index or node. + // remove() is also allowed which is same as remove(undefined) + if (indexOrNode === undefined) { + HTMLElement.prototype.remove.call(this); + return; + } + + if (typeof indexOrNode === 'object') + indexOrNode = unwrap(indexOrNode); + + unwrap(this).remove(indexOrNode); + }, + + get form() { + return wrap(unwrap(this).form); + } + }); + + registerWrapper(OriginalHTMLSelectElement, HTMLSelectElement, + document.createElement('select')); + + scope.wrappers.HTMLSelectElement = HTMLSelectElement; +})(window.ShadowDOMPolyfill); diff --git a/src/ShadowDOM/wrappers/HTMLShadowElement.js b/src/ShadowDOM/wrappers/HTMLShadowElement.js new file mode 100644 index 0000000..78c94e0 --- /dev/null +++ b/src/ShadowDOM/wrappers/HTMLShadowElement.js @@ -0,0 +1,27 @@ +// Copyright 2013 The Polymer Authors. All rights reserved. +// Use of this source code is goverened by a BSD-style +// license that can be found in the LICENSE file. + +(function(scope) { + 'use strict'; + + var HTMLElement = scope.wrappers.HTMLElement; + var mixin = scope.mixin; + var NodeList = scope.wrappers.NodeList; + var registerWrapper = scope.registerWrapper; + + var OriginalHTMLShadowElement = window.HTMLShadowElement; + + function HTMLShadowElement(node) { + HTMLElement.call(this, node); + } + HTMLShadowElement.prototype = Object.create(HTMLElement.prototype); + HTMLShadowElement.prototype.constructor = HTMLShadowElement; + + // getDistributedNodes is added in ShadowRenderer + + if (OriginalHTMLShadowElement) + registerWrapper(OriginalHTMLShadowElement, HTMLShadowElement); + + scope.wrappers.HTMLShadowElement = HTMLShadowElement; +})(window.ShadowDOMPolyfill); diff --git a/src/ShadowDOM/wrappers/HTMLTableElement.js b/src/ShadowDOM/wrappers/HTMLTableElement.js new file mode 100644 index 0000000..c1339b6 --- /dev/null +++ b/src/ShadowDOM/wrappers/HTMLTableElement.js @@ -0,0 +1,64 @@ +/* + * Copyright 2014 The Polymer Authors. All rights reserved. + * Use of this source code is goverened by a BSD-style + * license that can be found in the LICENSE file. + */ + +(function(scope) { + 'use strict'; + + var HTMLElement = scope.wrappers.HTMLElement; + var mixin = scope.mixin; + var registerWrapper = scope.registerWrapper; + var unwrap = scope.unwrap; + var wrap = scope.wrap; + var wrapHTMLCollection = scope.wrapHTMLCollection; + + var OriginalHTMLTableElement = window.HTMLTableElement; + + function HTMLTableElement(node) { + HTMLElement.call(this, node); + } + HTMLTableElement.prototype = Object.create(HTMLElement.prototype); + mixin(HTMLTableElement.prototype, { + get caption() { + return wrap(unwrap(this).caption); + }, + createCaption: function() { + return wrap(unwrap(this).createCaption()); + }, + + get tHead() { + return wrap(unwrap(this).tHead); + }, + createTHead: function() { + return wrap(unwrap(this).createTHead()); + }, + + createTFoot: function() { + return wrap(unwrap(this).createTFoot()); + }, + get tFoot() { + return wrap(unwrap(this).tFoot); + }, + + get tBodies() { + return wrapHTMLCollection(unwrap(this).tBodies); + }, + createTBody: function() { + return wrap(unwrap(this).createTBody()); + }, + + get rows() { + return wrapHTMLCollection(unwrap(this).rows); + }, + insertRow: function(index) { + return wrap(unwrap(this).insertRow(index)); + } + }); + + registerWrapper(OriginalHTMLTableElement, HTMLTableElement, + document.createElement('table')); + + scope.wrappers.HTMLTableElement = HTMLTableElement; +})(window.ShadowDOMPolyfill); diff --git a/src/ShadowDOM/wrappers/HTMLTableRowElement.js b/src/ShadowDOM/wrappers/HTMLTableRowElement.js new file mode 100644 index 0000000..ba06eb2 --- /dev/null +++ b/src/ShadowDOM/wrappers/HTMLTableRowElement.js @@ -0,0 +1,37 @@ +/* + * Copyright 2014 The Polymer Authors. All rights reserved. + * Use of this source code is goverened by a BSD-style + * license that can be found in the LICENSE file. + */ + +(function(scope) { + 'use strict'; + + var HTMLElement = scope.wrappers.HTMLElement; + var mixin = scope.mixin; + var registerWrapper = scope.registerWrapper; + var wrapHTMLCollection = scope.wrapHTMLCollection; + var unwrap = scope.unwrap; + var wrap = scope.wrap; + + var OriginalHTMLTableRowElement = window.HTMLTableRowElement; + + function HTMLTableRowElement(node) { + HTMLElement.call(this, node); + } + HTMLTableRowElement.prototype = Object.create(HTMLElement.prototype); + mixin(HTMLTableRowElement.prototype, { + get cells() { + return wrapHTMLCollection(unwrap(this).cells); + }, + + insertCell: function(index) { + return wrap(unwrap(this).insertCell(index)); + } + }); + + registerWrapper(OriginalHTMLTableRowElement, HTMLTableRowElement, + document.createElement('tr')); + + scope.wrappers.HTMLTableRowElement = HTMLTableRowElement; +})(window.ShadowDOMPolyfill); diff --git a/src/ShadowDOM/wrappers/HTMLTableSectionElement.js b/src/ShadowDOM/wrappers/HTMLTableSectionElement.js new file mode 100644 index 0000000..8e6fc5e --- /dev/null +++ b/src/ShadowDOM/wrappers/HTMLTableSectionElement.js @@ -0,0 +1,37 @@ +/* + * Copyright 2014 The Polymer Authors. All rights reserved. + * Use of this source code is goverened by a BSD-style + * license that can be found in the LICENSE file. + */ + +(function(scope) { + 'use strict'; + + var HTMLElement = scope.wrappers.HTMLElement; + var mixin = scope.mixin; + var registerWrapper = scope.registerWrapper; + var wrapHTMLCollection = scope.wrapHTMLCollection; + var unwrap = scope.unwrap; + var wrap = scope.wrap; + + var OriginalHTMLTableSectionElement = window.HTMLTableSectionElement; + + function HTMLTableSectionElement(node) { + HTMLElement.call(this, node); + } + HTMLTableSectionElement.prototype = Object.create(HTMLElement.prototype); + mixin(HTMLTableSectionElement.prototype, { + constructor: HTMLTableSectionElement, + get rows() { + return wrapHTMLCollection(unwrap(this).rows); + }, + insertRow: function(index) { + return wrap(unwrap(this).insertRow(index)); + } + }); + + registerWrapper(OriginalHTMLTableSectionElement, HTMLTableSectionElement, + document.createElement('thead')); + + scope.wrappers.HTMLTableSectionElement = HTMLTableSectionElement; +})(window.ShadowDOMPolyfill); diff --git a/src/ShadowDOM/wrappers/HTMLTemplateElement.js b/src/ShadowDOM/wrappers/HTMLTemplateElement.js new file mode 100644 index 0000000..7acdae3 --- /dev/null +++ b/src/ShadowDOM/wrappers/HTMLTemplateElement.js @@ -0,0 +1,73 @@ +// Copyright 2013 The Polymer Authors. All rights reserved. +// Use of this source code is goverened by a BSD-style +// license that can be found in the LICENSE file. + +(function(scope) { + 'use strict'; + + var HTMLElement = scope.wrappers.HTMLElement; + var mixin = scope.mixin; + var registerWrapper = scope.registerWrapper; + var unsafeUnwrap = scope.unsafeUnwrap; + var unwrap = scope.unwrap; + var wrap = scope.wrap; + + var contentTable = new WeakMap(); + var templateContentsOwnerTable = new WeakMap(); + + // http://dvcs.w3.org/hg/webcomponents/raw-file/tip/spec/templates/index.html#dfn-template-contents-owner + function getTemplateContentsOwner(doc) { + if (!doc.defaultView) + return doc; + var d = templateContentsOwnerTable.get(doc); + if (!d) { + // TODO(arv): This should either be a Document or HTMLDocument depending + // on doc. + d = doc.implementation.createHTMLDocument(''); + while (d.lastChild) { + d.removeChild(d.lastChild); + } + templateContentsOwnerTable.set(doc, d); + } + return d; + } + + function extractContent(templateElement) { + // templateElement is not a wrapper here. + var doc = getTemplateContentsOwner(templateElement.ownerDocument); + var df = unwrap(doc.createDocumentFragment()); + var child; + while (child = templateElement.firstChild) { + df.appendChild(child); + } + return df; + } + + var OriginalHTMLTemplateElement = window.HTMLTemplateElement; + + function HTMLTemplateElement(node) { + HTMLElement.call(this, node); + if (!OriginalHTMLTemplateElement) { + var content = extractContent(node); + contentTable.set(this, wrap(content)); + } + } + HTMLTemplateElement.prototype = Object.create(HTMLElement.prototype); + + mixin(HTMLTemplateElement.prototype, { + constructor: HTMLTemplateElement, + get content() { + if (OriginalHTMLTemplateElement) + return wrap(unsafeUnwrap(this).content); + return contentTable.get(this); + }, + + // TODO(arv): cloneNode needs to clone content. + + }); + + if (OriginalHTMLTemplateElement) + registerWrapper(OriginalHTMLTemplateElement, HTMLTemplateElement); + + scope.wrappers.HTMLTemplateElement = HTMLTemplateElement; +})(window.ShadowDOMPolyfill); diff --git a/src/ShadowDOM/wrappers/HTMLUnknownElement.js b/src/ShadowDOM/wrappers/HTMLUnknownElement.js new file mode 100644 index 0000000..79c16e8 --- /dev/null +++ b/src/ShadowDOM/wrappers/HTMLUnknownElement.js @@ -0,0 +1,31 @@ +// Copyright 2013 The Polymer Authors. All rights reserved. +// Use of this source code is goverened by a BSD-style +// license that can be found in the LICENSE file. + +(function(scope) { + 'use strict'; + + var HTMLContentElement = scope.wrappers.HTMLContentElement; + var HTMLElement = scope.wrappers.HTMLElement; + var HTMLShadowElement = scope.wrappers.HTMLShadowElement; + var HTMLTemplateElement = scope.wrappers.HTMLTemplateElement; + var mixin = scope.mixin; + var registerWrapper = scope.registerWrapper; + + var OriginalHTMLUnknownElement = window.HTMLUnknownElement; + + function HTMLUnknownElement(node) { + switch (node.localName) { + case 'content': + return new HTMLContentElement(node); + case 'shadow': + return new HTMLShadowElement(node); + case 'template': + return new HTMLTemplateElement(node); + } + HTMLElement.call(this, node); + } + HTMLUnknownElement.prototype = Object.create(HTMLElement.prototype); + registerWrapper(OriginalHTMLUnknownElement, HTMLUnknownElement); + scope.wrappers.HTMLUnknownElement = HTMLUnknownElement; +})(window.ShadowDOMPolyfill); diff --git a/src/ShadowDOM/wrappers/Node.js b/src/ShadowDOM/wrappers/Node.js new file mode 100644 index 0000000..e47e716 --- /dev/null +++ b/src/ShadowDOM/wrappers/Node.js @@ -0,0 +1,739 @@ +/** + * Copyright 2012 The Polymer Authors. All rights reserved. + * Use of this source code is goverened by a BSD-style + * license that can be found in the LICENSE file. + */ + +(function(scope) { + 'use strict'; + + var EventTarget = scope.wrappers.EventTarget; + var NodeList = scope.wrappers.NodeList; + var TreeScope = scope.TreeScope; + var assert = scope.assert; + var defineWrapGetter = scope.defineWrapGetter; + var enqueueMutation = scope.enqueueMutation; + var getTreeScope = scope.getTreeScope; + var isWrapper = scope.isWrapper; + var mixin = scope.mixin; + var registerTransientObservers = scope.registerTransientObservers; + var registerWrapper = scope.registerWrapper; + var setTreeScope = scope.setTreeScope; + var unsafeUnwrap = scope.unsafeUnwrap; + var unwrap = scope.unwrap; + var unwrapIfNeeded = scope.unwrapIfNeeded; + var wrap = scope.wrap; + var wrapIfNeeded = scope.wrapIfNeeded; + var wrappers = scope.wrappers; + + function assertIsNodeWrapper(node) { + assert(node instanceof Node); + } + + function createOneElementNodeList(node) { + var nodes = new NodeList(); + nodes[0] = node; + nodes.length = 1; + return nodes; + } + + var surpressMutations = false; + + /** + * Called before node is inserted into a node to enqueue its removal from its + * old parent. + * @param {!Node} node The node that is about to be removed. + * @param {!Node} parent The parent node that the node is being removed from. + * @param {!NodeList} nodes The collected nodes. + */ + function enqueueRemovalForInsertedNodes(node, parent, nodes) { + enqueueMutation(parent, 'childList', { + removedNodes: nodes, + previousSibling: node.previousSibling, + nextSibling: node.nextSibling + }); + } + + function enqueueRemovalForInsertedDocumentFragment(df, nodes) { + enqueueMutation(df, 'childList', { + removedNodes: nodes + }); + } + + /** + * Collects nodes from a DocumentFragment or a Node for removal followed + * by an insertion. + * + * This updates the internal pointers for node, previousNode and nextNode. + */ + function collectNodes(node, parentNode, previousNode, nextNode) { + if (node instanceof DocumentFragment) { + var nodes = collectNodesForDocumentFragment(node); + + // The extra loop is to work around bugs with DocumentFragments in IE. + surpressMutations = true; + for (var i = nodes.length - 1; i >= 0; i--) { + node.removeChild(nodes[i]); + nodes[i].parentNode_ = parentNode; + } + surpressMutations = false; + + for (var i = 0; i < nodes.length; i++) { + nodes[i].previousSibling_ = nodes[i - 1] || previousNode; + nodes[i].nextSibling_ = nodes[i + 1] || nextNode; + } + + if (previousNode) + previousNode.nextSibling_ = nodes[0]; + if (nextNode) + nextNode.previousSibling_ = nodes[nodes.length - 1]; + + return nodes; + } + + var nodes = createOneElementNodeList(node); + var oldParent = node.parentNode; + if (oldParent) { + // This will enqueue the mutation record for the removal as needed. + oldParent.removeChild(node); + } + + node.parentNode_ = parentNode; + node.previousSibling_ = previousNode; + node.nextSibling_ = nextNode; + if (previousNode) + previousNode.nextSibling_ = node; + if (nextNode) + nextNode.previousSibling_ = node; + + return nodes; + } + + function collectNodesNative(node) { + if (node instanceof DocumentFragment) + return collectNodesForDocumentFragment(node); + + var nodes = createOneElementNodeList(node); + var oldParent = node.parentNode; + if (oldParent) + enqueueRemovalForInsertedNodes(node, oldParent, nodes); + return nodes; + } + + function collectNodesForDocumentFragment(node) { + var nodes = new NodeList(); + var i = 0; + for (var child = node.firstChild; child; child = child.nextSibling) { + nodes[i++] = child; + } + nodes.length = i; + enqueueRemovalForInsertedDocumentFragment(node, nodes); + return nodes; + } + + function snapshotNodeList(nodeList) { + // NodeLists are not live at the moment so just return the same object. + return nodeList; + } + + // http://dom.spec.whatwg.org/#node-is-inserted + function nodeWasAdded(node, treeScope) { + setTreeScope(node, treeScope); + node.nodeIsInserted_(); + } + + function nodesWereAdded(nodes, parent) { + var treeScope = getTreeScope(parent); + for (var i = 0; i < nodes.length; i++) { + nodeWasAdded(nodes[i], treeScope); + } + } + + // http://dom.spec.whatwg.org/#node-is-removed + function nodeWasRemoved(node) { + setTreeScope(node, new TreeScope(node, null)); + } + + function nodesWereRemoved(nodes) { + for (var i = 0; i < nodes.length; i++) { + nodeWasRemoved(nodes[i]); + } + } + + function ensureSameOwnerDocument(parent, child) { + var ownerDoc = parent.nodeType === Node.DOCUMENT_NODE ? + parent : parent.ownerDocument; + if (ownerDoc !== child.ownerDocument) + ownerDoc.adoptNode(child); + } + + function adoptNodesIfNeeded(owner, nodes) { + if (!nodes.length) + return; + + var ownerDoc = owner.ownerDocument; + + // All nodes have the same ownerDocument when we get here. + if (ownerDoc === nodes[0].ownerDocument) + return; + + for (var i = 0; i < nodes.length; i++) { + scope.adoptNodeNoRemove(nodes[i], ownerDoc); + } + } + + function unwrapNodesForInsertion(owner, nodes) { + adoptNodesIfNeeded(owner, nodes); + var length = nodes.length; + + if (length === 1) + return unwrap(nodes[0]); + + var df = unwrap(owner.ownerDocument.createDocumentFragment()); + for (var i = 0; i < length; i++) { + df.appendChild(unwrap(nodes[i])); + } + return df; + } + + function clearChildNodes(wrapper) { + if (wrapper.firstChild_ !== undefined) { + var child = wrapper.firstChild_; + while (child) { + var tmp = child; + child = child.nextSibling_; + tmp.parentNode_ = tmp.previousSibling_ = tmp.nextSibling_ = undefined; + } + } + wrapper.firstChild_ = wrapper.lastChild_ = undefined; + } + + function removeAllChildNodes(wrapper) { + if (wrapper.invalidateShadowRenderer()) { + var childWrapper = wrapper.firstChild; + while (childWrapper) { + assert(childWrapper.parentNode === wrapper); + var nextSibling = childWrapper.nextSibling; + var childNode = unwrap(childWrapper); + var parentNode = childNode.parentNode; + if (parentNode) + originalRemoveChild.call(parentNode, childNode); + childWrapper.previousSibling_ = childWrapper.nextSibling_ = + childWrapper.parentNode_ = null; + childWrapper = nextSibling; + } + wrapper.firstChild_ = wrapper.lastChild_ = null; + } else { + var node = unwrap(wrapper); + var child = node.firstChild; + var nextSibling; + while (child) { + nextSibling = child.nextSibling; + originalRemoveChild.call(node, child); + child = nextSibling; + } + } + } + + function invalidateParent(node) { + var p = node.parentNode; + return p && p.invalidateShadowRenderer(); + } + + function cleanupNodes(nodes) { + for (var i = 0, n; i < nodes.length; i++) { + n = nodes[i]; + n.parentNode.removeChild(n); + } + } + + var originalImportNode = document.importNode; + var originalCloneNode = window.Node.prototype.cloneNode; + + function cloneNode(node, deep, opt_doc) { + var clone; + if (opt_doc) + clone = wrap(originalImportNode.call(opt_doc, unsafeUnwrap(node), false)); + else + clone = wrap(originalCloneNode.call(unsafeUnwrap(node), false)); + + if (deep) { + for (var child = node.firstChild; child; child = child.nextSibling) { + clone.appendChild(cloneNode(child, true, opt_doc)); + } + + if (node instanceof wrappers.HTMLTemplateElement) { + var cloneContent = clone.content; + for (var child = node.content.firstChild; + child; + child = child.nextSibling) { + cloneContent.appendChild(cloneNode(child, true, opt_doc)); + } + } + } + // TODO(arv): Some HTML elements also clone other data like value. + return clone; + } + + function contains(self, child) { + if (!child || getTreeScope(self) !== getTreeScope(child)) + return false; + + for (var node = child; node; node = node.parentNode) { + if (node === self) + return true; + } + return false; + } + + var OriginalNode = window.Node; + + /** + * This represents a wrapper of a native DOM node. + * @param {!Node} original The original DOM node, aka, the visual DOM node. + * @constructor + * @extends {EventTarget} + */ + function Node(original) { + assert(original instanceof OriginalNode); + + EventTarget.call(this, original); + + // These properties are used to override the visual references with the + // logical ones. If the value is undefined it means that the logical is the + // same as the visual. + + /** + * @type {Node|undefined} + * @private + */ + this.parentNode_ = undefined; + + /** + * @type {Node|undefined} + * @private + */ + this.firstChild_ = undefined; + + /** + * @type {Node|undefined} + * @private + */ + this.lastChild_ = undefined; + + /** + * @type {Node|undefined} + * @private + */ + this.nextSibling_ = undefined; + + /** + * @type {Node|undefined} + * @private + */ + this.previousSibling_ = undefined; + + this.treeScope_ = undefined; + } + + var OriginalDocumentFragment = window.DocumentFragment; + var originalAppendChild = OriginalNode.prototype.appendChild; + var originalCompareDocumentPosition = + OriginalNode.prototype.compareDocumentPosition; + var originalInsertBefore = OriginalNode.prototype.insertBefore; + var originalRemoveChild = OriginalNode.prototype.removeChild; + var originalReplaceChild = OriginalNode.prototype.replaceChild; + + var isIe = /Trident/.test(navigator.userAgent); + + var removeChildOriginalHelper = isIe ? + function(parent, child) { + try { + originalRemoveChild.call(parent, child); + } catch (ex) { + if (!(parent instanceof OriginalDocumentFragment)) + throw ex; + } + } : + function(parent, child) { + originalRemoveChild.call(parent, child); + }; + + Node.prototype = Object.create(EventTarget.prototype); + mixin(Node.prototype, { + appendChild: function(childWrapper) { + return this.insertBefore(childWrapper, null); + }, + + insertBefore: function(childWrapper, refWrapper) { + assertIsNodeWrapper(childWrapper); + + var refNode; + if (refWrapper) { + if (isWrapper(refWrapper)) { + refNode = unwrap(refWrapper); + } else { + refNode = refWrapper; + refWrapper = wrap(refNode); + } + } else { + refWrapper = null; + refNode = null; + } + + refWrapper && assert(refWrapper.parentNode === this); + + var nodes; + var previousNode = + refWrapper ? refWrapper.previousSibling : this.lastChild; + + var useNative = !this.invalidateShadowRenderer() && + !invalidateParent(childWrapper); + + if (useNative) + nodes = collectNodesNative(childWrapper); + else + nodes = collectNodes(childWrapper, this, previousNode, refWrapper); + + if (useNative) { + ensureSameOwnerDocument(this, childWrapper); + clearChildNodes(this); + originalInsertBefore.call(unsafeUnwrap(this), unwrap(childWrapper), refNode); + } else { + if (!previousNode) + this.firstChild_ = nodes[0]; + if (!refWrapper) { + this.lastChild_ = nodes[nodes.length - 1]; + if (this.firstChild_ === undefined) + this.firstChild_ = this.firstChild; + } + + var parentNode = refNode ? refNode.parentNode : unsafeUnwrap(this); + + // insertBefore refWrapper no matter what the parent is? + if (parentNode) { + originalInsertBefore.call(parentNode, + unwrapNodesForInsertion(this, nodes), refNode); + } else { + adoptNodesIfNeeded(this, nodes); + } + } + + enqueueMutation(this, 'childList', { + addedNodes: nodes, + nextSibling: refWrapper, + previousSibling: previousNode + }); + + nodesWereAdded(nodes, this); + + return childWrapper; + }, + + removeChild: function(childWrapper) { + assertIsNodeWrapper(childWrapper); + if (childWrapper.parentNode !== this) { + // IE has invalid DOM trees at times. + var found = false; + var childNodes = this.childNodes; + for (var ieChild = this.firstChild; ieChild; + ieChild = ieChild.nextSibling) { + if (ieChild === childWrapper) { + found = true; + break; + } + } + if (!found) { + // TODO(arv): DOMException + throw new Error('NotFoundError'); + } + } + + var childNode = unwrap(childWrapper); + var childWrapperNextSibling = childWrapper.nextSibling; + var childWrapperPreviousSibling = childWrapper.previousSibling; + + if (this.invalidateShadowRenderer()) { + // We need to remove the real node from the DOM before updating the + // pointers. This is so that that mutation event is dispatched before + // the pointers have changed. + var thisFirstChild = this.firstChild; + var thisLastChild = this.lastChild; + + var parentNode = childNode.parentNode; + if (parentNode) + removeChildOriginalHelper(parentNode, childNode); + + if (thisFirstChild === childWrapper) + this.firstChild_ = childWrapperNextSibling; + if (thisLastChild === childWrapper) + this.lastChild_ = childWrapperPreviousSibling; + if (childWrapperPreviousSibling) + childWrapperPreviousSibling.nextSibling_ = childWrapperNextSibling; + if (childWrapperNextSibling) { + childWrapperNextSibling.previousSibling_ = + childWrapperPreviousSibling; + } + + childWrapper.previousSibling_ = childWrapper.nextSibling_ = + childWrapper.parentNode_ = undefined; + } else { + clearChildNodes(this); + removeChildOriginalHelper(unsafeUnwrap(this), childNode); + } + + if (!surpressMutations) { + enqueueMutation(this, 'childList', { + removedNodes: createOneElementNodeList(childWrapper), + nextSibling: childWrapperNextSibling, + previousSibling: childWrapperPreviousSibling + }); + } + + registerTransientObservers(this, childWrapper); + + return childWrapper; + }, + + replaceChild: function(newChildWrapper, oldChildWrapper) { + assertIsNodeWrapper(newChildWrapper); + + var oldChildNode; + if (isWrapper(oldChildWrapper)) { + oldChildNode = unwrap(oldChildWrapper); + } else { + oldChildNode = oldChildWrapper; + oldChildWrapper = wrap(oldChildNode); + } + + if (oldChildWrapper.parentNode !== this) { + // TODO(arv): DOMException + throw new Error('NotFoundError'); + } + + var nextNode = oldChildWrapper.nextSibling; + var previousNode = oldChildWrapper.previousSibling; + var nodes; + + var useNative = !this.invalidateShadowRenderer() && + !invalidateParent(newChildWrapper); + + if (useNative) { + nodes = collectNodesNative(newChildWrapper); + } else { + if (nextNode === newChildWrapper) + nextNode = newChildWrapper.nextSibling; + nodes = collectNodes(newChildWrapper, this, previousNode, nextNode); + } + + if (!useNative) { + if (this.firstChild === oldChildWrapper) + this.firstChild_ = nodes[0]; + if (this.lastChild === oldChildWrapper) + this.lastChild_ = nodes[nodes.length - 1]; + + oldChildWrapper.previousSibling_ = oldChildWrapper.nextSibling_ = + oldChildWrapper.parentNode_ = undefined; + + // replaceChild no matter what the parent is? + if (oldChildNode.parentNode) { + originalReplaceChild.call( + oldChildNode.parentNode, + unwrapNodesForInsertion(this, nodes), + oldChildNode); + } + } else { + ensureSameOwnerDocument(this, newChildWrapper); + clearChildNodes(this); + originalReplaceChild.call(unsafeUnwrap(this), unwrap(newChildWrapper), + oldChildNode); + } + + enqueueMutation(this, 'childList', { + addedNodes: nodes, + removedNodes: createOneElementNodeList(oldChildWrapper), + nextSibling: nextNode, + previousSibling: previousNode + }); + + nodeWasRemoved(oldChildWrapper); + nodesWereAdded(nodes, this); + + return oldChildWrapper; + }, + + /** + * Called after a node was inserted. Subclasses override this to invalidate + * the renderer as needed. + * @private + */ + nodeIsInserted_: function() { + for (var child = this.firstChild; child; child = child.nextSibling) { + child.nodeIsInserted_(); + } + }, + + hasChildNodes: function() { + return this.firstChild !== null; + }, + + /** @type {Node} */ + get parentNode() { + // If the parentNode has not been overridden, use the original parentNode. + return this.parentNode_ !== undefined ? + this.parentNode_ : wrap(unsafeUnwrap(this).parentNode); + }, + + /** @type {Node} */ + get firstChild() { + return this.firstChild_ !== undefined ? + this.firstChild_ : wrap(unsafeUnwrap(this).firstChild); + }, + + /** @type {Node} */ + get lastChild() { + return this.lastChild_ !== undefined ? + this.lastChild_ : wrap(unsafeUnwrap(this).lastChild); + }, + + /** @type {Node} */ + get nextSibling() { + return this.nextSibling_ !== undefined ? + this.nextSibling_ : wrap(unsafeUnwrap(this).nextSibling); + }, + + /** @type {Node} */ + get previousSibling() { + return this.previousSibling_ !== undefined ? + this.previousSibling_ : wrap(unsafeUnwrap(this).previousSibling); + }, + + get parentElement() { + var p = this.parentNode; + while (p && p.nodeType !== Node.ELEMENT_NODE) { + p = p.parentNode; + } + return p; + }, + + get textContent() { + // TODO(arv): This should fallback to unsafeUnwrap(this).textContent if there + // are no shadow trees below or above the context node. + var s = ''; + for (var child = this.firstChild; child; child = child.nextSibling) { + if (child.nodeType != Node.COMMENT_NODE) { + s += child.textContent; + } + } + return s; + }, + set textContent(textContent) { + if (textContent == null) textContent = ''; + var removedNodes = snapshotNodeList(this.childNodes); + + if (this.invalidateShadowRenderer()) { + removeAllChildNodes(this); + if (textContent !== '') { + var textNode = unsafeUnwrap(this).ownerDocument.createTextNode(textContent); + this.appendChild(textNode); + } + } else { + clearChildNodes(this); + unsafeUnwrap(this).textContent = textContent; + } + + var addedNodes = snapshotNodeList(this.childNodes); + + enqueueMutation(this, 'childList', { + addedNodes: addedNodes, + removedNodes: removedNodes + }); + + nodesWereRemoved(removedNodes); + nodesWereAdded(addedNodes, this); + }, + + get childNodes() { + var wrapperList = new NodeList(); + var i = 0; + for (var child = this.firstChild; child; child = child.nextSibling) { + wrapperList[i++] = child; + } + wrapperList.length = i; + return wrapperList; + }, + + cloneNode: function(deep) { + return cloneNode(this, deep); + }, + + contains: function(child) { + return contains(this, wrapIfNeeded(child)); + }, + + compareDocumentPosition: function(otherNode) { + // This only wraps, it therefore only operates on the composed DOM and not + // the logical DOM. + return originalCompareDocumentPosition.call(unsafeUnwrap(this), + unwrapIfNeeded(otherNode)); + }, + + normalize: function() { + var nodes = snapshotNodeList(this.childNodes); + var remNodes = []; + var s = ''; + var modNode; + + for (var i = 0, n; i < nodes.length; i++) { + n = nodes[i]; + if (n.nodeType === Node.TEXT_NODE) { + if (!modNode && !n.data.length) + this.removeNode(n); + else if (!modNode) + modNode = n; + else { + s += n.data; + remNodes.push(n); + } + } else { + if (modNode && remNodes.length) { + modNode.data += s; + cleanupNodes(remNodes); + } + remNodes = []; + s = ''; + modNode = null; + if (n.childNodes.length) + n.normalize(); + } + } + + // handle case where >1 text nodes are the last children + if (modNode && remNodes.length) { + modNode.data += s; + cleanupNodes(remNodes); + } + } + }); + + defineWrapGetter(Node, 'ownerDocument'); + + // We use a DocumentFragment as a base and then delete the properties of + // DocumentFragment.prototype from the wrapper Node. Since delete makes + // objects slow in some JS engines we recreate the prototype object. + registerWrapper(OriginalNode, Node, document.createDocumentFragment()); + delete Node.prototype.querySelector; + delete Node.prototype.querySelectorAll; + Node.prototype = mixin(Object.create(EventTarget.prototype), Node.prototype); + + scope.cloneNode = cloneNode; + scope.nodeWasAdded = nodeWasAdded; + scope.nodeWasRemoved = nodeWasRemoved; + scope.nodesWereAdded = nodesWereAdded; + scope.nodesWereRemoved = nodesWereRemoved; + scope.originalInsertBefore = originalInsertBefore; + scope.originalRemoveChild = originalRemoveChild; + scope.snapshotNodeList = snapshotNodeList; + scope.wrappers.Node = Node; + +})(window.ShadowDOMPolyfill); diff --git a/src/ShadowDOM/wrappers/NodeList.js b/src/ShadowDOM/wrappers/NodeList.js new file mode 100644 index 0000000..8c5322a --- /dev/null +++ b/src/ShadowDOM/wrappers/NodeList.js @@ -0,0 +1,50 @@ +// Copyright 2012 The Polymer Authors. All rights reserved. +// Use of this source code is goverened by a BSD-style +// license that can be found in the LICENSE file. + +(function(scope) { + 'use strict'; + + var unsafeUnwrap = scope.unsafeUnwrap; + var wrap = scope.wrap; + + var nonEnumDescriptor = {enumerable: false}; + + function nonEnum(obj, prop) { + Object.defineProperty(obj, prop, nonEnumDescriptor); + } + + function NodeList() { + this.length = 0; + nonEnum(this, 'length'); + } + NodeList.prototype = { + item: function(index) { + return this[index]; + } + }; + nonEnum(NodeList.prototype, 'item'); + + function wrapNodeList(list) { + if (list == null) + return list; + var wrapperList = new NodeList(); + for (var i = 0, length = list.length; i < length; i++) { + wrapperList[i] = wrap(list[i]); + } + wrapperList.length = length; + return wrapperList; + } + + function addWrapNodeListMethod(wrapperConstructor, name) { + wrapperConstructor.prototype[name] = function() { + return wrapNodeList( + unsafeUnwrap(this)[name].apply(unsafeUnwrap(this), arguments)); + }; + } + + scope.wrappers.NodeList = NodeList; + scope.addWrapNodeListMethod = addWrapNodeListMethod; + scope.wrapNodeList = wrapNodeList; + +})(window.ShadowDOMPolyfill); diff --git a/src/ShadowDOM/wrappers/Range.js b/src/ShadowDOM/wrappers/Range.js new file mode 100644 index 0000000..7af9c21 --- /dev/null +++ b/src/ShadowDOM/wrappers/Range.js @@ -0,0 +1,97 @@ +// Copyright 2013 The Polymer Authors. All rights reserved. +// Use of this source code is goverened by a BSD-style +// license that can be found in the LICENSE file. + +(function(scope) { + 'use strict'; + + var registerWrapper = scope.registerWrapper; + var setWrapper = scope.setWrapper; + var unsafeUnwrap = scope.unsafeUnwrap; + var unwrap = scope.unwrap; + var unwrapIfNeeded = scope.unwrapIfNeeded; + var wrap = scope.wrap; + + var OriginalRange = window.Range; + + function Range(impl) { + setWrapper(impl, this); + } + Range.prototype = { + get startContainer() { + return wrap(unsafeUnwrap(this).startContainer); + }, + get endContainer() { + return wrap(unsafeUnwrap(this).endContainer); + }, + get commonAncestorContainer() { + return wrap(unsafeUnwrap(this).commonAncestorContainer); + }, + setStart: function(refNode,offset) { + unsafeUnwrap(this).setStart(unwrapIfNeeded(refNode), offset); + }, + setEnd: function(refNode,offset) { + unsafeUnwrap(this).setEnd(unwrapIfNeeded(refNode), offset); + }, + setStartBefore: function(refNode) { + unsafeUnwrap(this).setStartBefore(unwrapIfNeeded(refNode)); + }, + setStartAfter: function(refNode) { + unsafeUnwrap(this).setStartAfter(unwrapIfNeeded(refNode)); + }, + setEndBefore: function(refNode) { + unsafeUnwrap(this).setEndBefore(unwrapIfNeeded(refNode)); + }, + setEndAfter: function(refNode) { + unsafeUnwrap(this).setEndAfter(unwrapIfNeeded(refNode)); + }, + selectNode: function(refNode) { + unsafeUnwrap(this).selectNode(unwrapIfNeeded(refNode)); + }, + selectNodeContents: function(refNode) { + unsafeUnwrap(this).selectNodeContents(unwrapIfNeeded(refNode)); + }, + compareBoundaryPoints: function(how, sourceRange) { + return unsafeUnwrap(this).compareBoundaryPoints(how, unwrap(sourceRange)); + }, + extractContents: function() { + return wrap(unsafeUnwrap(this).extractContents()); + }, + cloneContents: function() { + return wrap(unsafeUnwrap(this).cloneContents()); + }, + insertNode: function(node) { + unsafeUnwrap(this).insertNode(unwrapIfNeeded(node)); + }, + surroundContents: function(newParent) { + unsafeUnwrap(this).surroundContents(unwrapIfNeeded(newParent)); + }, + cloneRange: function() { + return wrap(unsafeUnwrap(this).cloneRange()); + }, + isPointInRange: function(node, offset) { + return unsafeUnwrap(this).isPointInRange(unwrapIfNeeded(node), offset); + }, + comparePoint: function(node, offset) { + return unsafeUnwrap(this).comparePoint(unwrapIfNeeded(node), offset); + }, + intersectsNode: function(node) { + return unsafeUnwrap(this).intersectsNode(unwrapIfNeeded(node)); + }, + toString: function() { + return unsafeUnwrap(this).toString(); + } + }; + + // IE9 does not have createContextualFragment. + if (OriginalRange.prototype.createContextualFragment) { + Range.prototype.createContextualFragment = function(html) { + return wrap(unsafeUnwrap(this).createContextualFragment(html)); + }; + } + + registerWrapper(window.Range, Range, document.createRange()); + + scope.wrappers.Range = Range; + +})(window.ShadowDOMPolyfill); diff --git a/src/ShadowDOM/wrappers/SVGElement.js b/src/ShadowDOM/wrappers/SVGElement.js new file mode 100644 index 0000000..c92084d --- /dev/null +++ b/src/ShadowDOM/wrappers/SVGElement.js @@ -0,0 +1,28 @@ +// Copyright 2014 The Polymer Authors. All rights reserved. +// Use of this source code is goverened by a BSD-style +// license that can be found in the LICENSE file. + +(function(scope) { + 'use strict'; + + var Element = scope.wrappers.Element; + var HTMLElement = scope.wrappers.HTMLElement; + var registerObject = scope.registerObject; + + var SVG_NS = 'http://www.w3.org/2000/svg'; + var svgTitleElement = document.createElementNS(SVG_NS, 'title'); + var SVGTitleElement = registerObject(svgTitleElement); + var SVGElement = Object.getPrototypeOf(SVGTitleElement.prototype).constructor; + + // IE11 does not have classList for SVG elements. The spec says that classList + // is an accessor on Element, but IE11 puts classList on HTMLElement, leaving + // SVGElement without a classList property. We therefore move the accessor for + // IE11. + if (!('classList' in svgTitleElement)) { + var descr = Object.getOwnPropertyDescriptor(Element.prototype, 'classList'); + Object.defineProperty(HTMLElement.prototype, 'classList', descr); + delete Element.prototype.classList; + } + + scope.wrappers.SVGElement = SVGElement; +})(window.ShadowDOMPolyfill); diff --git a/src/ShadowDOM/wrappers/SVGElementInstance.js b/src/ShadowDOM/wrappers/SVGElementInstance.js new file mode 100644 index 0000000..75598a4 --- /dev/null +++ b/src/ShadowDOM/wrappers/SVGElementInstance.js @@ -0,0 +1,68 @@ +// Copyright 2014 The Polymer Authors. All rights reserved. +// Use of this source code is goverened by a BSD-style +// license that can be found in the LICENSE file. + +(function(scope) { + 'use strict'; + + var EventTarget = scope.wrappers.EventTarget; + var mixin = scope.mixin; + var registerWrapper = scope.registerWrapper; + var unsafeUnwrap = scope.unsafeUnwrap; + var wrap = scope.wrap; + + var OriginalSVGElementInstance = window.SVGElementInstance; + if (!OriginalSVGElementInstance) + return; + + function SVGElementInstance(impl) { + EventTarget.call(this, impl); + } + + SVGElementInstance.prototype = Object.create(EventTarget.prototype); + mixin(SVGElementInstance.prototype, { + /** @type {SVGElement} */ + get correspondingElement() { + return wrap(unsafeUnwrap(this).correspondingElement); + }, + + /** @type {SVGUseElement} */ + get correspondingUseElement() { + return wrap(unsafeUnwrap(this).correspondingUseElement); + }, + + /** @type {SVGElementInstance} */ + get parentNode() { + return wrap(unsafeUnwrap(this).parentNode); + }, + + /** @type {SVGElementInstanceList} */ + get childNodes() { + throw new Error('Not implemented'); + }, + + /** @type {SVGElementInstance} */ + get firstChild() { + return wrap(unsafeUnwrap(this).firstChild); + }, + + /** @type {SVGElementInstance} */ + get lastChild() { + return wrap(unsafeUnwrap(this).lastChild); + }, + + /** @type {SVGElementInstance} */ + get previousSibling() { + return wrap(unsafeUnwrap(this).previousSibling); + }, + + /** @type {SVGElementInstance} */ + get nextSibling() { + return wrap(unsafeUnwrap(this).nextSibling); + } + }); + + registerWrapper(OriginalSVGElementInstance, SVGElementInstance); + + scope.wrappers.SVGElementInstance = SVGElementInstance; +})(window.ShadowDOMPolyfill); diff --git a/src/ShadowDOM/wrappers/SVGUseElement.js b/src/ShadowDOM/wrappers/SVGUseElement.js new file mode 100644 index 0000000..7f91363 --- /dev/null +++ b/src/ShadowDOM/wrappers/SVGUseElement.js @@ -0,0 +1,46 @@ +// Copyright 2014 The Polymer Authors. All rights reserved. +// Use of this source code is goverened by a BSD-style +// license that can be found in the LICENSE file. + +(function(scope) { + 'use strict'; + + var mixin = scope.mixin; + var registerWrapper = scope.registerWrapper; + var unwrap = scope.unwrap; + var wrap = scope.wrap; + + var OriginalSVGUseElement = window.SVGUseElement; + + // IE uses SVGElement as parent interface, SVG2 (Blink & Gecko) uses + // SVGGraphicsElement. Use the element to get the right prototype. + + var SVG_NS = 'http://www.w3.org/2000/svg'; + var gWrapper = wrap(document.createElementNS(SVG_NS, 'g')); + var useElement = document.createElementNS(SVG_NS, 'use'); + var SVGGElement = gWrapper.constructor; + var parentInterfacePrototype = Object.getPrototypeOf(SVGGElement.prototype); + var parentInterface = parentInterfacePrototype.constructor; + + function SVGUseElement(impl) { + parentInterface.call(this, impl); + } + + SVGUseElement.prototype = Object.create(parentInterfacePrototype); + + // Firefox does not expose instanceRoot. + if ('instanceRoot' in useElement) { + mixin(SVGUseElement.prototype, { + get instanceRoot() { + return wrap(unwrap(this).instanceRoot); + }, + get animatedInstanceRoot() { + return wrap(unwrap(this).animatedInstanceRoot); + }, + }); + } + + registerWrapper(OriginalSVGUseElement, SVGUseElement, useElement); + + scope.wrappers.SVGUseElement = SVGUseElement; +})(window.ShadowDOMPolyfill); diff --git a/src/ShadowDOM/wrappers/Selection.js b/src/ShadowDOM/wrappers/Selection.js new file mode 100644 index 0000000..e35c437 --- /dev/null +++ b/src/ShadowDOM/wrappers/Selection.js @@ -0,0 +1,69 @@ +// Copyright 2014 The Polymer Authors. All rights reserved. +// Use of this source code is goverened by a BSD-style +// license that can be found in the LICENSE file. + +(function(scope) { + 'use strict'; + + var registerWrapper = scope.registerWrapper; + var setWrapper = scope.setWrapper; + var unsafeUnwrap = scope.unsafeUnwrap; + var unwrap = scope.unwrap; + var unwrapIfNeeded = scope.unwrapIfNeeded; + var wrap = scope.wrap; + + var OriginalSelection = window.Selection; + + function Selection(impl) { + setWrapper(impl, this); + } + Selection.prototype = { + get anchorNode() { + return wrap(unsafeUnwrap(this).anchorNode); + }, + get focusNode() { + return wrap(unsafeUnwrap(this).focusNode); + }, + addRange: function(range) { + unsafeUnwrap(this).addRange(unwrap(range)); + }, + collapse: function(node, index) { + unsafeUnwrap(this).collapse(unwrapIfNeeded(node), index); + }, + containsNode: function(node, allowPartial) { + return unsafeUnwrap(this).containsNode(unwrapIfNeeded(node), allowPartial); + }, + extend: function(node, offset) { + unsafeUnwrap(this).extend(unwrapIfNeeded(node), offset); + }, + getRangeAt: function(index) { + return wrap(unsafeUnwrap(this).getRangeAt(index)); + }, + removeRange: function(range) { + unsafeUnwrap(this).removeRange(unwrap(range)); + }, + selectAllChildren: function(node) { + unsafeUnwrap(this).selectAllChildren(unwrapIfNeeded(node)); + }, + toString: function() { + return unsafeUnwrap(this).toString(); + } + }; + + // WebKit extensions. Not implemented. + // readonly attribute Node baseNode; + // readonly attribute long baseOffset; + // readonly attribute Node extentNode; + // readonly attribute long extentOffset; + // [RaisesException] void setBaseAndExtent([Default=Undefined] optional Node baseNode, + // [Default=Undefined] optional long baseOffset, + // [Default=Undefined] optional Node extentNode, + // [Default=Undefined] optional long extentOffset); + // [RaisesException, ImplementedAs=collapse] void setPosition([Default=Undefined] optional Node node, + // [Default=Undefined] optional long offset); + + registerWrapper(window.Selection, Selection, window.getSelection()); + + scope.wrappers.Selection = Selection; + +})(window.ShadowDOMPolyfill); diff --git a/src/ShadowDOM/wrappers/ShadowRoot.js b/src/ShadowDOM/wrappers/ShadowRoot.js new file mode 100644 index 0000000..7e6fe2b --- /dev/null +++ b/src/ShadowDOM/wrappers/ShadowRoot.js @@ -0,0 +1,77 @@ +// Copyright 2013 The Polymer Authors. All rights reserved. +// Use of this source code is goverened by a BSD-style +// license that can be found in the LICENSE file. + +(function(scope) { + 'use strict'; + + var DocumentFragment = scope.wrappers.DocumentFragment; + var TreeScope = scope.TreeScope; + var elementFromPoint = scope.elementFromPoint; + var getInnerHTML = scope.getInnerHTML; + var getTreeScope = scope.getTreeScope; + var mixin = scope.mixin; + var rewrap = scope.rewrap; + var setInnerHTML = scope.setInnerHTML; + var unsafeUnwrap = scope.unsafeUnwrap; + var unwrap = scope.unwrap; + + var shadowHostTable = new WeakMap(); + var nextOlderShadowTreeTable = new WeakMap(); + + var spaceCharRe = /[ \t\n\r\f]/; + + function ShadowRoot(hostWrapper) { + var node = unwrap(unsafeUnwrap(hostWrapper).ownerDocument.createDocumentFragment()); + DocumentFragment.call(this, node); + + // createDocumentFragment associates the node with a wrapper + // DocumentFragment instance. Override that. + rewrap(node, this); + + var oldShadowRoot = hostWrapper.shadowRoot; + nextOlderShadowTreeTable.set(this, oldShadowRoot); + + this.treeScope_ = + new TreeScope(this, getTreeScope(oldShadowRoot || hostWrapper)); + + shadowHostTable.set(this, hostWrapper); + } + ShadowRoot.prototype = Object.create(DocumentFragment.prototype); + mixin(ShadowRoot.prototype, { + constructor: ShadowRoot, + + get innerHTML() { + return getInnerHTML(this); + }, + set innerHTML(value) { + setInnerHTML(this, value); + this.invalidateShadowRenderer(); + }, + + get olderShadowRoot() { + return nextOlderShadowTreeTable.get(this) || null; + }, + + get host() { + return shadowHostTable.get(this) || null; + }, + + invalidateShadowRenderer: function() { + return shadowHostTable.get(this).invalidateShadowRenderer(); + }, + + elementFromPoint: function(x, y) { + return elementFromPoint(this, this.ownerDocument, x, y); + }, + + getElementById: function(id) { + if (spaceCharRe.test(id)) + return null; + return this.querySelector('[id="' + id + '"]'); + } + }); + + scope.wrappers.ShadowRoot = ShadowRoot; + +})(window.ShadowDOMPolyfill); diff --git a/src/ShadowDOM/wrappers/Text.js b/src/ShadowDOM/wrappers/Text.js new file mode 100644 index 0000000..fefc501 --- /dev/null +++ b/src/ShadowDOM/wrappers/Text.js @@ -0,0 +1,42 @@ +// Copyright 2014 The Polymer Authors. All rights reserved. +// Use of this source code is goverened by a BSD-style +// license that can be found in the LICENSE file. + +(function(scope) { + 'use strict'; + + var CharacterData = scope.wrappers.CharacterData; + var enqueueMutation = scope.enqueueMutation; + var mixin = scope.mixin; + var registerWrapper = scope.registerWrapper; + + function toUInt32(x) { + return x >>> 0; + } + + var OriginalText = window.Text; + + function Text(node) { + CharacterData.call(this, node); + } + Text.prototype = Object.create(CharacterData.prototype); + mixin(Text.prototype, { + splitText: function(offset) { + offset = toUInt32(offset); + var s = this.data; + if (offset > s.length) + throw new Error('IndexSizeError'); + var head = s.slice(0, offset); + var tail = s.slice(offset); + this.data = head; + var newTextNode = this.ownerDocument.createTextNode(tail); + if (this.parentNode) + this.parentNode.insertBefore(newTextNode, this.nextSibling); + return newTextNode; + } + }); + + registerWrapper(OriginalText, Text, document.createTextNode('')); + + scope.wrappers.Text = Text; +})(window.ShadowDOMPolyfill); diff --git a/src/ShadowDOM/wrappers/TouchEvent.js b/src/ShadowDOM/wrappers/TouchEvent.js new file mode 100644 index 0000000..f3a4552 --- /dev/null +++ b/src/ShadowDOM/wrappers/TouchEvent.js @@ -0,0 +1,126 @@ +/* + * Copyright 2014 The Polymer Authors. All rights reserved. + * Use of this source code is goverened by a BSD-style + * license that can be found in the LICENSE file. + */ + +(function(scope) { + 'use strict'; + + var UIEvent = scope.wrappers.UIEvent; + var mixin = scope.mixin; + var registerWrapper = scope.registerWrapper; + var setWrapper = scope.setWrapper; + var unsafeUnwrap = scope.unsafeUnwrap; + var wrap = scope.wrap; + + // TouchEvent is WebKit/Blink only. + var OriginalTouchEvent = window.TouchEvent; + if (!OriginalTouchEvent) + return; + + var nativeEvent; + try { + nativeEvent = document.createEvent('TouchEvent'); + } catch (ex) { + // In Chrome creating a TouchEvent fails if the feature is not turned on + // which it isn't on desktop Chrome. + return; + } + + var nonEnumDescriptor = {enumerable: false}; + + function nonEnum(obj, prop) { + Object.defineProperty(obj, prop, nonEnumDescriptor); + } + + function Touch(impl) { + setWrapper(impl, this); + } + + Touch.prototype = { + get target() { + return wrap(unsafeUnwrap(this).target); + } + }; + + var descr = { + configurable: true, + enumerable: true, + get: null + }; + + [ + 'clientX', + 'clientY', + 'screenX', + 'screenY', + 'pageX', + 'pageY', + 'identifier', + 'webkitRadiusX', + 'webkitRadiusY', + 'webkitRotationAngle', + 'webkitForce' + ].forEach(function(name) { + descr.get = function() { + return unsafeUnwrap(this)[name]; + }; + Object.defineProperty(Touch.prototype, name, descr); + }); + + function TouchList() { + this.length = 0; + nonEnum(this, 'length'); + } + + TouchList.prototype = { + item: function(index) { + return this[index]; + } + }; + + function wrapTouchList(nativeTouchList) { + var list = new TouchList(); + for (var i = 0; i < nativeTouchList.length; i++) { + list[i] = new Touch(nativeTouchList[i]); + } + list.length = i; + return list; + } + + function TouchEvent(impl) { + UIEvent.call(this, impl); + } + + TouchEvent.prototype = Object.create(UIEvent.prototype); + + mixin(TouchEvent.prototype, { + get touches() { + return wrapTouchList(unsafeUnwrap(this).touches); + }, + + get targetTouches() { + return wrapTouchList(unsafeUnwrap(this).targetTouches); + }, + + get changedTouches() { + return wrapTouchList(unsafeUnwrap(this).changedTouches); + }, + + initTouchEvent: function() { + // The only way to use this is to reuse the TouchList from an existing + // TouchEvent. Since this is WebKit/Blink proprietary API we will not + // implement this until someone screams. + throw new Error('Not implemented'); + } + }); + + registerWrapper(OriginalTouchEvent, TouchEvent, nativeEvent); + + scope.wrappers.Touch = Touch; + scope.wrappers.TouchEvent = TouchEvent; + scope.wrappers.TouchList = TouchList; + +})(window.ShadowDOMPolyfill); + diff --git a/src/ShadowDOM/wrappers/WebGLRenderingContext.js b/src/ShadowDOM/wrappers/WebGLRenderingContext.js new file mode 100644 index 0000000..f009263 --- /dev/null +++ b/src/ShadowDOM/wrappers/WebGLRenderingContext.js @@ -0,0 +1,52 @@ +// Copyright 2013 The Polymer Authors. All rights reserved. +// Use of this source code is goverened by a BSD-style +// license that can be found in the LICENSE file. + +(function(scope) { + 'use strict'; + + var mixin = scope.mixin; + var registerWrapper = scope.registerWrapper; + var setWrapper = scope.setWrapper; + var unsafeUnwrap = scope.unsafeUnwrap; + var unwrapIfNeeded = scope.unwrapIfNeeded; + var wrap = scope.wrap; + + var OriginalWebGLRenderingContext = window.WebGLRenderingContext; + + // IE10 does not have WebGL. + if (!OriginalWebGLRenderingContext) + return; + + function WebGLRenderingContext(impl) { + setWrapper(impl, this); + } + + mixin(WebGLRenderingContext.prototype, { + get canvas() { + return wrap(unsafeUnwrap(this).canvas); + }, + + texImage2D: function() { + arguments[5] = unwrapIfNeeded(arguments[5]); + unsafeUnwrap(this).texImage2D.apply(unsafeUnwrap(this), arguments); + }, + + texSubImage2D: function() { + arguments[6] = unwrapIfNeeded(arguments[6]); + unsafeUnwrap(this).texSubImage2D.apply(unsafeUnwrap(this), arguments); + } + }); + + // Blink/WebKit has broken DOM bindings. Usually we would create an instance + // of the object and pass it into registerWrapper as a "blueprint" but + // creating WebGL contexts is expensive and might fail so we use a dummy + // object with dummy instance properties for these broken browsers. + var instanceProperties = /WebKit/.test(navigator.userAgent) ? + {drawingBufferHeight: null, drawingBufferWidth: null} : {}; + + registerWrapper(OriginalWebGLRenderingContext, WebGLRenderingContext, + instanceProperties); + + scope.wrappers.WebGLRenderingContext = WebGLRenderingContext; +})(window.ShadowDOMPolyfill); diff --git a/src/ShadowDOM/wrappers/Window.js b/src/ShadowDOM/wrappers/Window.js new file mode 100644 index 0000000..b3e96a4 --- /dev/null +++ b/src/ShadowDOM/wrappers/Window.js @@ -0,0 +1,88 @@ +// Copyright 2013 The Polymer Authors. All rights reserved. +// Use of this source code is goverened by a BSD-style +// license that can be found in the LICENSE file. + +(function(scope) { + 'use strict'; + + var EventTarget = scope.wrappers.EventTarget; + var Selection = scope.wrappers.Selection; + var mixin = scope.mixin; + var registerWrapper = scope.registerWrapper; + var renderAllPending = scope.renderAllPending; + var unwrap = scope.unwrap; + var unwrapIfNeeded = scope.unwrapIfNeeded; + var wrap = scope.wrap; + + var OriginalWindow = window.Window; + var originalGetComputedStyle = window.getComputedStyle; + var originalGetDefaultComputedStyle = window.getDefaultComputedStyle; + var originalGetSelection = window.getSelection; + + function Window(impl) { + EventTarget.call(this, impl); + } + Window.prototype = Object.create(EventTarget.prototype); + + OriginalWindow.prototype.getComputedStyle = function(el, pseudo) { + return wrap(this || window).getComputedStyle(unwrapIfNeeded(el), pseudo); + }; + + // Mozilla proprietary extension. + if (originalGetDefaultComputedStyle) { + OriginalWindow.prototype.getDefaultComputedStyle = function(el, pseudo) { + return wrap(this || window).getDefaultComputedStyle( + unwrapIfNeeded(el), pseudo); + }; + } + + OriginalWindow.prototype.getSelection = function() { + return wrap(this || window).getSelection(); + }; + + // Work around for https://bugzilla.mozilla.org/show_bug.cgi?id=943065 + delete window.getComputedStyle; + delete window.getDefaultComputedStyle; + delete window.getSelection; + + ['addEventListener', 'removeEventListener', 'dispatchEvent'].forEach( + function(name) { + OriginalWindow.prototype[name] = function() { + var w = wrap(this || window); + return w[name].apply(w, arguments); + }; + + // Work around for https://bugzilla.mozilla.org/show_bug.cgi?id=943065 + delete window[name]; + }); + + mixin(Window.prototype, { + getComputedStyle: function(el, pseudo) { + renderAllPending(); + return originalGetComputedStyle.call(unwrap(this), unwrapIfNeeded(el), + pseudo); + }, + getSelection: function() { + renderAllPending(); + return new Selection(originalGetSelection.call(unwrap(this))); + }, + + get document() { + return wrap(unwrap(this).document); + } + }); + + // Mozilla proprietary extension. + if (originalGetDefaultComputedStyle) { + Window.prototype.getDefaultComputedStyle = function(el, pseudo) { + renderAllPending(); + return originalGetDefaultComputedStyle.call(unwrap(this), + unwrapIfNeeded(el),pseudo); + }; + } + + registerWrapper(OriginalWindow, Window, window); + + scope.wrappers.Window = Window; + +})(window.ShadowDOMPolyfill); diff --git a/src/ShadowDOM/wrappers/XMLHttpRequest.js b/src/ShadowDOM/wrappers/XMLHttpRequest.js new file mode 100644 index 0000000..6c99b79 --- /dev/null +++ b/src/ShadowDOM/wrappers/XMLHttpRequest.js @@ -0,0 +1,19 @@ +/* + * Copyright 2014 The Polymer Authors. All rights reserved. + * Use of this source code is goverened by a BSD-style + * license that can be found in the LICENSE file. + */ + +(function(scope) { + 'use strict'; + + var unwrapIfNeeded = scope.unwrapIfNeeded; + var originalSend = XMLHttpRequest.prototype.send; + + // Since we only need to adjust XHR.send, we just patch it instead of wrapping + // the entire object. This happens when FormData is passed. + XMLHttpRequest.prototype.send = function(obj) { + return originalSend.call(this, unwrapIfNeeded(obj)); + }; + +})(window.ShadowDOMPolyfill); diff --git a/src/ShadowDOM/wrappers/elements-with-form-property.js b/src/ShadowDOM/wrappers/elements-with-form-property.js new file mode 100644 index 0000000..b5af480 --- /dev/null +++ b/src/ShadowDOM/wrappers/elements-with-form-property.js @@ -0,0 +1,54 @@ +// Copyright 2013 The Polymer Authors. All rights reserved. +// Use of this source code is goverened by a BSD-style +// license that can be found in the LICENSE file. + +(function(scope) { + 'use strict'; + + var HTMLElement = scope.wrappers.HTMLElement; + var assert = scope.assert; + var mixin = scope.mixin; + var registerWrapper = scope.registerWrapper; + var unwrap = scope.unwrap; + var wrap = scope.wrap; + + var elementsWithFormProperty = [ + 'HTMLButtonElement', + 'HTMLFieldSetElement', + 'HTMLInputElement', + 'HTMLKeygenElement', + 'HTMLLabelElement', + 'HTMLLegendElement', + 'HTMLObjectElement', + // HTMLOptionElement is handled in HTMLOptionElement.js + 'HTMLOutputElement', + // HTMLSelectElement is handled in HTMLSelectElement.js + 'HTMLTextAreaElement', + ]; + + function createWrapperConstructor(name) { + if (!window[name]) + return; + + // Ensure we are not overriding an already existing constructor. + assert(!scope.wrappers[name]); + + var GeneratedWrapper = function(node) { + // At this point all of them extend HTMLElement. + HTMLElement.call(this, node); + } + GeneratedWrapper.prototype = Object.create(HTMLElement.prototype); + mixin(GeneratedWrapper.prototype, { + get form() { + return wrap(unwrap(this).form); + }, + }); + + registerWrapper(window[name], GeneratedWrapper, + document.createElement(name.slice(4, -7))); + scope.wrappers[name] = GeneratedWrapper; + } + + elementsWithFormProperty.forEach(createWrapperConstructor); + +})(window.ShadowDOMPolyfill); diff --git a/src/ShadowDOM/wrappers/events.js b/src/ShadowDOM/wrappers/events.js new file mode 100644 index 0000000..5d3ff68 --- /dev/null +++ b/src/ShadowDOM/wrappers/events.js @@ -0,0 +1,920 @@ +// Copyright 2013 The Polymer Authors. All rights reserved. +// Use of this source code is goverened by a BSD-style +// license that can be found in the LICENSE file. + +(function(scope) { + 'use strict'; + + var forwardMethodsToWrapper = scope.forwardMethodsToWrapper; + var getTreeScope = scope.getTreeScope; + var mixin = scope.mixin; + var registerWrapper = scope.registerWrapper; + var setWrapper = scope.setWrapper; + var unsafeUnwrap = scope.unsafeUnwrap; + var unwrap = scope.unwrap; + var wrap = scope.wrap; + var wrappers = scope.wrappers; + + var wrappedFuns = new WeakMap(); + var listenersTable = new WeakMap(); + var handledEventsTable = new WeakMap(); + var currentlyDispatchingEvents = new WeakMap(); + var targetTable = new WeakMap(); + var currentTargetTable = new WeakMap(); + var relatedTargetTable = new WeakMap(); + var eventPhaseTable = new WeakMap(); + var stopPropagationTable = new WeakMap(); + var stopImmediatePropagationTable = new WeakMap(); + var eventHandlersTable = new WeakMap(); + var eventPathTable = new WeakMap(); + + function isShadowRoot(node) { + return node instanceof wrappers.ShadowRoot; + } + + function rootOfNode(node) { + return getTreeScope(node).root; + } + + // http://w3c.github.io/webcomponents/spec/shadow/#event-paths + function getEventPath(node, event) { + var path = []; + var current = node; + path.push(current); + while (current) { + // 4.1. + var destinationInsertionPoints = getDestinationInsertionPoints(current); + if (destinationInsertionPoints && destinationInsertionPoints.length > 0) { + // 4.1.1 + for (var i = 0; i < destinationInsertionPoints.length; i++) { + var insertionPoint = destinationInsertionPoints[i]; + // 4.1.1.1 + if (isShadowInsertionPoint(insertionPoint)) { + var shadowRoot = rootOfNode(insertionPoint); + // 4.1.1.1.2 + var olderShadowRoot = shadowRoot.olderShadowRoot; + if (olderShadowRoot) + path.push(olderShadowRoot); + } + + // 4.1.1.2 + path.push(insertionPoint); + } + + // 4.1.2 + current = destinationInsertionPoints[ + destinationInsertionPoints.length - 1]; + + // 4.2 + } else { + if (isShadowRoot(current)) { + if (inSameTree(node, current) && eventMustBeStopped(event)) { + // Stop this algorithm + break; + } + current = current.host; + path.push(current); + + // 4.2.2 + } else { + current = current.parentNode; + if (current) + path.push(current); + } + } + } + + return path; + } + + // http://w3c.github.io/webcomponents/spec/shadow/#dfn-events-always-stopped + function eventMustBeStopped(event) { + if (!event) + return false; + + switch (event.type) { + case 'abort': + case 'error': + case 'select': + case 'change': + case 'load': + case 'reset': + case 'resize': + case 'scroll': + case 'selectstart': + return true; + } + return false; + } + + // http://w3c.github.io/webcomponents/spec/shadow/#dfn-shadow-insertion-point + function isShadowInsertionPoint(node) { + return node instanceof HTMLShadowElement; + // and make sure that there are no shadow precing this? + // and that there is no content ancestor? + } + + function getDestinationInsertionPoints(node) { + return scope.getDestinationInsertionPoints(node); + } + + // http://w3c.github.io/webcomponents/spec/shadow/#event-retargeting + function eventRetargetting(path, currentTarget) { + if (path.length === 0) + return currentTarget; + + // The currentTarget might be the window object. Use its document for the + // purpose of finding the retargetted node. + if (currentTarget instanceof wrappers.Window) + currentTarget = currentTarget.document; + + var currentTargetTree = getTreeScope(currentTarget); + var originalTarget = path[0]; + var originalTargetTree = getTreeScope(originalTarget); + var relativeTargetTree = + lowestCommonInclusiveAncestor(currentTargetTree, originalTargetTree); + + for (var i = 0; i < path.length; i++) { + var node = path[i]; + if (getTreeScope(node) === relativeTargetTree) + return node; + } + + return path[path.length - 1]; + } + + function getTreeScopeAncestors(treeScope) { + var ancestors = []; + for (;treeScope; treeScope = treeScope.parent) { + ancestors.push(treeScope); + } + return ancestors; + } + + function lowestCommonInclusiveAncestor(tsA, tsB) { + var ancestorsA = getTreeScopeAncestors(tsA); + var ancestorsB = getTreeScopeAncestors(tsB); + + var result = null; + while (ancestorsA.length > 0 && ancestorsB.length > 0) { + var a = ancestorsA.pop(); + var b = ancestorsB.pop(); + if (a === b) + result = a; + else + break; + } + return result; + } + + function getTreeScopeRoot(ts) { + if (!ts.parent) + return ts; + return getTreeScopeRoot(ts.parent); + } + + function relatedTargetResolution(event, currentTarget, relatedTarget) { + // In case the current target is a window use its document for the purpose + // of retargetting the related target. + if (currentTarget instanceof wrappers.Window) + currentTarget = currentTarget.document; + + var currentTargetTree = getTreeScope(currentTarget); + var relatedTargetTree = getTreeScope(relatedTarget); + + var relatedTargetEventPath = getEventPath(relatedTarget, event); + + var lowestCommonAncestorTree; + + // 4 + var lowestCommonAncestorTree = + lowestCommonInclusiveAncestor(currentTargetTree, relatedTargetTree); + + // 5 + if (!lowestCommonAncestorTree) + lowestCommonAncestorTree = relatedTargetTree.root; + + // 6 + for (var commonAncestorTree = lowestCommonAncestorTree; + commonAncestorTree; + commonAncestorTree = commonAncestorTree.parent) { + // 6.1 + var adjustedRelatedTarget; + for (var i = 0; i < relatedTargetEventPath.length; i++) { + var node = relatedTargetEventPath[i]; + if (getTreeScope(node) === commonAncestorTree) + return node; + } + } + + return null; + } + + function inSameTree(a, b) { + return getTreeScope(a) === getTreeScope(b); + } + + var NONE = 0; + var CAPTURING_PHASE = 1; + var AT_TARGET = 2; + var BUBBLING_PHASE = 3; + + // pendingError is used to rethrow the first error we got during an event + // dispatch. The browser actually reports all errors but to do that we would + // need to rethrow the error asynchronously. + var pendingError; + + function dispatchOriginalEvent(originalEvent) { + // Make sure this event is only dispatched once. + if (handledEventsTable.get(originalEvent)) + return; + handledEventsTable.set(originalEvent, true); + dispatchEvent(wrap(originalEvent), wrap(originalEvent.target)); + if (pendingError) { + var err = pendingError; + pendingError = null; + throw err; + } + } + + + function isLoadLikeEvent(event) { + switch (event.type) { + // http://www.whatwg.org/specs/web-apps/current-work/multipage/webappapis.html#events-and-the-window-object + case 'load': + // http://www.whatwg.org/specs/web-apps/current-work/multipage/browsers.html#unloading-documents + case 'beforeunload': + case 'unload': + return true; + } + return false; + } + + function dispatchEvent(event, originalWrapperTarget) { + if (currentlyDispatchingEvents.get(event)) + throw new Error('InvalidStateError'); + + currentlyDispatchingEvents.set(event, true); + + // Render to ensure that the event path is correct. + scope.renderAllPending(); + var eventPath; + + // http://www.whatwg.org/specs/web-apps/current-work/multipage/webappapis.html#events-and-the-window-object + // All events dispatched on Nodes with a default view, except load events, + // should propagate to the Window. + + // http://www.whatwg.org/specs/web-apps/current-work/multipage/the-end.html#the-end + var overrideTarget; + var win; + + // Should really be not cancelable too but since Firefox has a bug there + // we skip that check. + // https://bugzilla.mozilla.org/show_bug.cgi?id=999456 + if (isLoadLikeEvent(event) && !event.bubbles) { + var doc = originalWrapperTarget; + if (doc instanceof wrappers.Document && (win = doc.defaultView)) { + overrideTarget = doc; + eventPath = []; + } + } + + if (!eventPath) { + if (originalWrapperTarget instanceof wrappers.Window) { + win = originalWrapperTarget; + eventPath = []; + } else { + eventPath = getEventPath(originalWrapperTarget, event); + + if (!isLoadLikeEvent(event)) { + var doc = eventPath[eventPath.length - 1]; + if (doc instanceof wrappers.Document) + win = doc.defaultView; + } + } + } + + eventPathTable.set(event, eventPath); + + if (dispatchCapturing(event, eventPath, win, overrideTarget)) { + if (dispatchAtTarget(event, eventPath, win, overrideTarget)) { + dispatchBubbling(event, eventPath, win, overrideTarget); + } + } + + eventPhaseTable.set(event, NONE); + currentTargetTable.delete(event, null); + currentlyDispatchingEvents.delete(event); + + return event.defaultPrevented; + } + + function dispatchCapturing(event, eventPath, win, overrideTarget) { + var phase = CAPTURING_PHASE; + + if (win) { + if (!invoke(win, event, phase, eventPath, overrideTarget)) + return false; + } + + for (var i = eventPath.length - 1; i > 0; i--) { + if (!invoke(eventPath[i], event, phase, eventPath, overrideTarget)) + return false; + } + + return true; + } + + function dispatchAtTarget(event, eventPath, win, overrideTarget) { + var phase = AT_TARGET; + var currentTarget = eventPath[0] || win; + return invoke(currentTarget, event, phase, eventPath, overrideTarget); + } + + function dispatchBubbling(event, eventPath, win, overrideTarget) { + var phase = BUBBLING_PHASE; + for (var i = 1; i < eventPath.length; i++) { + if (!invoke(eventPath[i], event, phase, eventPath, overrideTarget)) + return; + } + + if (win && eventPath.length > 0) { + invoke(win, event, phase, eventPath, overrideTarget); + } + } + + function invoke(currentTarget, event, phase, eventPath, overrideTarget) { + var listeners = listenersTable.get(currentTarget); + if (!listeners) + return true; + + var target = overrideTarget || eventRetargetting(eventPath, currentTarget); + + if (target === currentTarget) { + if (phase === CAPTURING_PHASE) + return true; + + if (phase === BUBBLING_PHASE) + phase = AT_TARGET; + + } else if (phase === BUBBLING_PHASE && !event.bubbles) { + return true; + } + + if ('relatedTarget' in event) { + var originalEvent = unwrap(event); + var unwrappedRelatedTarget = originalEvent.relatedTarget; + + // X-Tag sets relatedTarget on a CustomEvent. If they do that there is no + // way to have relatedTarget return the adjusted target but worse is that + // the originalEvent might not have a relatedTarget so we hit an assert + // when we try to wrap it. + if (unwrappedRelatedTarget) { + // In IE we can get objects that are not EventTargets at this point. + // Safari does not have an EventTarget interface so revert to checking + // for addEventListener as an approximation. + if (unwrappedRelatedTarget instanceof Object && + unwrappedRelatedTarget.addEventListener) { + var relatedTarget = wrap(unwrappedRelatedTarget); + + var adjusted = + relatedTargetResolution(event, currentTarget, relatedTarget); + if (adjusted === target) + return true; + } else { + adjusted = null; + } + relatedTargetTable.set(event, adjusted); + } + } + + eventPhaseTable.set(event, phase); + var type = event.type; + + var anyRemoved = false; + targetTable.set(event, target); + currentTargetTable.set(event, currentTarget); + + // Keep track of the invoke depth so that we only clean up the removed + // listeners if we are in the outermost invoke. + listeners.depth++; + + for (var i = 0, len = listeners.length; i < len; i++) { + var listener = listeners[i]; + if (listener.removed) { + anyRemoved = true; + continue; + } + + if (listener.type !== type || + !listener.capture && phase === CAPTURING_PHASE || + listener.capture && phase === BUBBLING_PHASE) { + continue; + } + + try { + if (typeof listener.handler === 'function') + listener.handler.call(currentTarget, event); + else + listener.handler.handleEvent(event); + + if (stopImmediatePropagationTable.get(event)) + return false; + + } catch (ex) { + if (!pendingError) + pendingError = ex; + } + } + + listeners.depth--; + + if (anyRemoved && listeners.depth === 0) { + var copy = listeners.slice(); + listeners.length = 0; + for (var i = 0; i < copy.length; i++) { + if (!copy[i].removed) + listeners.push(copy[i]); + } + } + + return !stopPropagationTable.get(event); + } + + function Listener(type, handler, capture) { + this.type = type; + this.handler = handler; + this.capture = Boolean(capture); + } + Listener.prototype = { + equals: function(that) { + return this.handler === that.handler && this.type === that.type && + this.capture === that.capture; + }, + get removed() { + return this.handler === null; + }, + remove: function() { + this.handler = null; + } + }; + + var OriginalEvent = window.Event; + OriginalEvent.prototype.polymerBlackList_ = { + returnValue: true, + // TODO(arv): keyLocation is part of KeyboardEvent but Firefox does not + // support constructable KeyboardEvent so we keep it here for now. + keyLocation: true + }; + + /** + * Creates a new Event wrapper or wraps an existin native Event object. + * @param {string|Event} type + * @param {Object=} options + * @constructor + */ + function Event(type, options) { + if (type instanceof OriginalEvent) { + var impl = type; + // In browsers that do not correctly support BeforeUnloadEvent we get to + // the generic Event wrapper but we still want to ensure we create a + // BeforeUnloadEvent. Since BeforeUnloadEvent calls super, we need to + // prevent reentrancty. + if (!OriginalBeforeUnloadEvent && impl.type === 'beforeunload' && + !(this instanceof BeforeUnloadEvent)) { + return new BeforeUnloadEvent(impl); + } + setWrapper(impl, this); + } else { + return wrap(constructEvent(OriginalEvent, 'Event', type, options)); + } + } + Event.prototype = { + get target() { + return targetTable.get(this); + }, + get currentTarget() { + return currentTargetTable.get(this); + }, + get eventPhase() { + return eventPhaseTable.get(this); + }, + get path() { + var eventPath = eventPathTable.get(this); + if (!eventPath) + return []; + // TODO(arv): Event path should contain window. + return eventPath.slice(); + }, + stopPropagation: function() { + stopPropagationTable.set(this, true); + }, + stopImmediatePropagation: function() { + stopPropagationTable.set(this, true); + stopImmediatePropagationTable.set(this, true); + } + }; + registerWrapper(OriginalEvent, Event, document.createEvent('Event')); + + function unwrapOptions(options) { + if (!options || !options.relatedTarget) + return options; + return Object.create(options, { + relatedTarget: {value: unwrap(options.relatedTarget)} + }); + } + + function registerGenericEvent(name, SuperEvent, prototype) { + var OriginalEvent = window[name]; + var GenericEvent = function(type, options) { + if (type instanceof OriginalEvent) + setWrapper(type, this); + else + return wrap(constructEvent(OriginalEvent, name, type, options)); + }; + GenericEvent.prototype = Object.create(SuperEvent.prototype); + if (prototype) + mixin(GenericEvent.prototype, prototype); + if (OriginalEvent) { + // - Old versions of Safari fails on new FocusEvent (and others?). + // - IE does not support event constructors. + // - createEvent('FocusEvent') throws in Firefox. + // => Try the best practice solution first and fallback to the old way + // if needed. + try { + registerWrapper(OriginalEvent, GenericEvent, new OriginalEvent('temp')); + } catch (ex) { + registerWrapper(OriginalEvent, GenericEvent, + document.createEvent(name)); + } + } + return GenericEvent; + } + + var UIEvent = registerGenericEvent('UIEvent', Event); + var CustomEvent = registerGenericEvent('CustomEvent', Event); + + var relatedTargetProto = { + get relatedTarget() { + var relatedTarget = relatedTargetTable.get(this); + // relatedTarget can be null. + if (relatedTarget !== undefined) + return relatedTarget; + return wrap(unwrap(this).relatedTarget); + } + }; + + function getInitFunction(name, relatedTargetIndex) { + return function() { + arguments[relatedTargetIndex] = unwrap(arguments[relatedTargetIndex]); + var impl = unwrap(this); + impl[name].apply(impl, arguments); + }; + } + + var mouseEventProto = mixin({ + initMouseEvent: getInitFunction('initMouseEvent', 14) + }, relatedTargetProto); + + var focusEventProto = mixin({ + initFocusEvent: getInitFunction('initFocusEvent', 5) + }, relatedTargetProto); + + var MouseEvent = registerGenericEvent('MouseEvent', UIEvent, mouseEventProto); + var FocusEvent = registerGenericEvent('FocusEvent', UIEvent, focusEventProto); + + // In case the browser does not support event constructors we polyfill that + // by calling `createEvent('Foo')` and `initFooEvent` where the arguments to + // `initFooEvent` are derived from the registered default event init dict. + var defaultInitDicts = Object.create(null); + + var supportsEventConstructors = (function() { + try { + new window.FocusEvent('focus'); + } catch (ex) { + return false; + } + return true; + })(); + + /** + * Constructs a new native event. + */ + function constructEvent(OriginalEvent, name, type, options) { + if (supportsEventConstructors) + return new OriginalEvent(type, unwrapOptions(options)); + + // Create the arguments from the default dictionary. + var event = unwrap(document.createEvent(name)); + var defaultDict = defaultInitDicts[name]; + var args = [type]; + Object.keys(defaultDict).forEach(function(key) { + var v = options != null && key in options ? + options[key] : defaultDict[key]; + if (key === 'relatedTarget') + v = unwrap(v); + args.push(v); + }); + event['init' + name].apply(event, args); + return event; + } + + if (!supportsEventConstructors) { + var configureEventConstructor = function(name, initDict, superName) { + if (superName) { + var superDict = defaultInitDicts[superName]; + initDict = mixin(mixin({}, superDict), initDict); + } + + defaultInitDicts[name] = initDict; + }; + + // The order of the default event init dictionary keys is important, the + // arguments to initFooEvent is derived from that. + configureEventConstructor('Event', {bubbles: false, cancelable: false}); + configureEventConstructor('CustomEvent', {detail: null}, 'Event'); + configureEventConstructor('UIEvent', {view: null, detail: 0}, 'Event'); + configureEventConstructor('MouseEvent', { + screenX: 0, + screenY: 0, + clientX: 0, + clientY: 0, + ctrlKey: false, + altKey: false, + shiftKey: false, + metaKey: false, + button: 0, + relatedTarget: null + }, 'UIEvent'); + configureEventConstructor('FocusEvent', {relatedTarget: null}, 'UIEvent'); + } + + // Safari 7 does not yet have BeforeUnloadEvent. + // https://bugs.webkit.org/show_bug.cgi?id=120849 + var OriginalBeforeUnloadEvent = window.BeforeUnloadEvent; + + function BeforeUnloadEvent(impl) { + Event.call(this, impl); + } + BeforeUnloadEvent.prototype = Object.create(Event.prototype); + mixin(BeforeUnloadEvent.prototype, { + get returnValue() { + return unsafeUnwrap(this).returnValue; + }, + set returnValue(v) { + unsafeUnwrap(this).returnValue = v; + } + }); + + if (OriginalBeforeUnloadEvent) + registerWrapper(OriginalBeforeUnloadEvent, BeforeUnloadEvent); + + function isValidListener(fun) { + if (typeof fun === 'function') + return true; + return fun && fun.handleEvent; + } + + function isMutationEvent(type) { + switch (type) { + case 'DOMAttrModified': + case 'DOMAttributeNameChanged': + case 'DOMCharacterDataModified': + case 'DOMElementNameChanged': + case 'DOMNodeInserted': + case 'DOMNodeInsertedIntoDocument': + case 'DOMNodeRemoved': + case 'DOMNodeRemovedFromDocument': + case 'DOMSubtreeModified': + return true; + } + return false; + } + + var OriginalEventTarget = window.EventTarget; + + /** + * This represents a wrapper for an EventTarget. + * @param {!EventTarget} impl The original event target. + * @constructor + */ + function EventTarget(impl) { + setWrapper(impl, this); + } + + // Node and Window have different internal type checks in WebKit so we cannot + // use the same method as the original function. + var methodNames = [ + 'addEventListener', + 'removeEventListener', + 'dispatchEvent' + ]; + + [Node, Window].forEach(function(constructor) { + var p = constructor.prototype; + methodNames.forEach(function(name) { + Object.defineProperty(p, name + '_', {value: p[name]}); + }); + }); + + function getTargetToListenAt(wrapper) { + if (wrapper instanceof wrappers.ShadowRoot) + wrapper = wrapper.host; + return unwrap(wrapper); + } + + EventTarget.prototype = { + addEventListener: function(type, fun, capture) { + if (!isValidListener(fun) || isMutationEvent(type)) + return; + + var listener = new Listener(type, fun, capture); + var listeners = listenersTable.get(this); + if (!listeners) { + listeners = []; + listeners.depth = 0; + listenersTable.set(this, listeners); + } else { + // Might have a duplicate. + for (var i = 0; i < listeners.length; i++) { + if (listener.equals(listeners[i])) + return; + } + } + + listeners.push(listener); + + var target = getTargetToListenAt(this); + target.addEventListener_(type, dispatchOriginalEvent, true); + }, + removeEventListener: function(type, fun, capture) { + capture = Boolean(capture); + var listeners = listenersTable.get(this); + if (!listeners) + return; + var count = 0, found = false; + for (var i = 0; i < listeners.length; i++) { + if (listeners[i].type === type && listeners[i].capture === capture) { + count++; + if (listeners[i].handler === fun) { + found = true; + listeners[i].remove(); + } + } + } + + if (found && count === 1) { + var target = getTargetToListenAt(this); + target.removeEventListener_(type, dispatchOriginalEvent, true); + } + }, + dispatchEvent: function(event) { + // We want to use the native dispatchEvent because it triggers the default + // actions (like checking a checkbox). However, if there are no listeners + // in the composed tree then there are no events that will trigger and + // listeners in the non composed tree that are part of the event path are + // not notified. + // + // If we find out that there are no listeners in the composed tree we add + // a temporary listener to the target which makes us get called back even + // in that case. + + var nativeEvent = unwrap(event); + var eventType = nativeEvent.type; + + // Allow dispatching the same event again. This is safe because if user + // code calls this during an existing dispatch of the same event the + // native dispatchEvent throws (that is required by the spec). + handledEventsTable.set(nativeEvent, false); + + // Force rendering since we prefer native dispatch and that works on the + // composed tree. + scope.renderAllPending(); + + var tempListener; + if (!hasListenerInAncestors(this, eventType)) { + tempListener = function() {}; + this.addEventListener(eventType, tempListener, true); + } + + try { + return unwrap(this).dispatchEvent_(nativeEvent); + } finally { + if (tempListener) + this.removeEventListener(eventType, tempListener, true); + } + } + }; + + function hasListener(node, type) { + var listeners = listenersTable.get(node); + if (listeners) { + for (var i = 0; i < listeners.length; i++) { + if (!listeners[i].removed && listeners[i].type === type) + return true; + } + } + return false; + } + + function hasListenerInAncestors(target, type) { + for (var node = unwrap(target); node; node = node.parentNode) { + if (hasListener(wrap(node), type)) + return true; + } + return false; + } + + if (OriginalEventTarget) + registerWrapper(OriginalEventTarget, EventTarget); + + function wrapEventTargetMethods(constructors) { + forwardMethodsToWrapper(constructors, methodNames); + } + + var originalElementFromPoint = document.elementFromPoint; + + function elementFromPoint(self, document, x, y) { + scope.renderAllPending(); + + var element = + wrap(originalElementFromPoint.call(unsafeUnwrap(document), x, y)); + if (!element) + return null; + var path = getEventPath(element, null); + + // scope the path to this TreeScope + var idx = path.lastIndexOf(self); + if (idx == -1) + return null; + else + path = path.slice(0, idx); + + // TODO(dfreedm): pass idx to eventRetargetting to avoid array copy + return eventRetargetting(path, self); + } + + /** + * Returns a function that is to be used as a getter for `onfoo` properties. + * @param {string} name + * @return {Function} + */ + function getEventHandlerGetter(name) { + return function() { + var inlineEventHandlers = eventHandlersTable.get(this); + return inlineEventHandlers && inlineEventHandlers[name] && + inlineEventHandlers[name].value || null; + }; + } + + /** + * Returns a function that is to be used as a setter for `onfoo` properties. + * @param {string} name + * @return {Function} + */ + function getEventHandlerSetter(name) { + var eventType = name.slice(2); + return function(value) { + var inlineEventHandlers = eventHandlersTable.get(this); + if (!inlineEventHandlers) { + inlineEventHandlers = Object.create(null); + eventHandlersTable.set(this, inlineEventHandlers); + } + + var old = inlineEventHandlers[name]; + if (old) + this.removeEventListener(eventType, old.wrapped, false); + + if (typeof value === 'function') { + var wrapped = function(e) { + var rv = value.call(this, e); + if (rv === false) + e.preventDefault(); + else if (name === 'onbeforeunload' && typeof rv === 'string') + e.returnValue = rv; + // mouseover uses true for preventDefault but preventDefault for + // mouseover is ignored by browsers these day. + }; + + this.addEventListener(eventType, wrapped, false); + inlineEventHandlers[name] = { + value: value, + wrapped: wrapped + }; + } + }; + } + + scope.elementFromPoint = elementFromPoint; + scope.getEventHandlerGetter = getEventHandlerGetter; + scope.getEventHandlerSetter = getEventHandlerSetter; + scope.wrapEventTargetMethods = wrapEventTargetMethods; + scope.wrappers.BeforeUnloadEvent = BeforeUnloadEvent; + scope.wrappers.CustomEvent = CustomEvent; + scope.wrappers.Event = Event; + scope.wrappers.EventTarget = EventTarget; + scope.wrappers.FocusEvent = FocusEvent; + scope.wrappers.MouseEvent = MouseEvent; + scope.wrappers.UIEvent = UIEvent; + +})(window.ShadowDOMPolyfill); diff --git a/src/ShadowDOM/wrappers/generic.js b/src/ShadowDOM/wrappers/generic.js new file mode 100644 index 0000000..9d7a35a --- /dev/null +++ b/src/ShadowDOM/wrappers/generic.js @@ -0,0 +1,24 @@ +// Copyright 2013 The Polymer Authors. All rights reserved. +// Use of this source code is goverened by a BSD-style +// license that can be found in the LICENSE file. + +(function(scope) { + 'use strict'; + + var GetElementsByInterface = scope.GetElementsByInterface; + var ParentNodeInterface = scope.ParentNodeInterface; + var SelectorsInterface = scope.SelectorsInterface; + var mixin = scope.mixin; + var registerObject = scope.registerObject; + + var DocumentFragment = registerObject(document.createDocumentFragment()); + mixin(DocumentFragment.prototype, ParentNodeInterface); + mixin(DocumentFragment.prototype, SelectorsInterface); + mixin(DocumentFragment.prototype, GetElementsByInterface); + + var Comment = registerObject(document.createComment('')); + + scope.wrappers.Comment = Comment; + scope.wrappers.DocumentFragment = DocumentFragment; + +})(window.ShadowDOMPolyfill); diff --git a/src/ShadowDOM/wrappers/node-interfaces.js b/src/ShadowDOM/wrappers/node-interfaces.js new file mode 100644 index 0000000..9726094 --- /dev/null +++ b/src/ShadowDOM/wrappers/node-interfaces.js @@ -0,0 +1,75 @@ +// Copyright 2013 The Polymer Authors. All rights reserved. +// Use of this source code is goverened by a BSD-style +// license that can be found in the LICENSE file. + +(function(scope) { + 'use strict'; + + var NodeList = scope.wrappers.NodeList; + + function forwardElement(node) { + while (node && node.nodeType !== Node.ELEMENT_NODE) { + node = node.nextSibling; + } + return node; + } + + function backwardsElement(node) { + while (node && node.nodeType !== Node.ELEMENT_NODE) { + node = node.previousSibling; + } + return node; + } + + var ParentNodeInterface = { + get firstElementChild() { + return forwardElement(this.firstChild); + }, + + get lastElementChild() { + return backwardsElement(this.lastChild); + }, + + get childElementCount() { + var count = 0; + for (var child = this.firstElementChild; + child; + child = child.nextElementSibling) { + count++; + } + return count; + }, + + get children() { + var wrapperList = new NodeList(); + var i = 0; + for (var child = this.firstElementChild; + child; + child = child.nextElementSibling) { + wrapperList[i++] = child; + } + wrapperList.length = i; + return wrapperList; + }, + + remove: function() { + var p = this.parentNode; + if (p) + p.removeChild(this); + } + }; + + var ChildNodeInterface = { + get nextElementSibling() { + return forwardElement(this.nextSibling); + }, + + get previousElementSibling() { + return backwardsElement(this.previousSibling); + } + }; + + scope.ChildNodeInterface = ChildNodeInterface; + scope.ParentNodeInterface = ParentNodeInterface; + +})(window.ShadowDOMPolyfill); diff --git a/src/ShadowDOM/wrappers/override-constructors.js b/src/ShadowDOM/wrappers/override-constructors.js new file mode 100644 index 0000000..079c3a1 --- /dev/null +++ b/src/ShadowDOM/wrappers/override-constructors.js @@ -0,0 +1,108 @@ +// Copyright 2013 The Polymer Authors. All rights reserved. +// Use of this source code is goverened by a BSD-style +// license that can be found in the LICENSE file. + +(function(scope) { + 'use strict'; + + var isWrapperFor = scope.isWrapperFor; + + // This is a list of the elements we currently override the global constructor + // for. + var elements = { + 'a': 'HTMLAnchorElement', + // Do not create an applet element by default since it shows a warning in + // IE. + // https://github.com/Polymer/polymer/issues/217 + // 'applet': 'HTMLAppletElement', + 'area': 'HTMLAreaElement', + 'audio': 'HTMLAudioElement', + 'base': 'HTMLBaseElement', + 'body': 'HTMLBodyElement', + 'br': 'HTMLBRElement', + 'button': 'HTMLButtonElement', + 'canvas': 'HTMLCanvasElement', + 'caption': 'HTMLTableCaptionElement', + 'col': 'HTMLTableColElement', + // 'command': 'HTMLCommandElement', // Not fully implemented in Gecko. + 'content': 'HTMLContentElement', + 'data': 'HTMLDataElement', + 'datalist': 'HTMLDataListElement', + 'del': 'HTMLModElement', + 'dir': 'HTMLDirectoryElement', + 'div': 'HTMLDivElement', + 'dl': 'HTMLDListElement', + 'embed': 'HTMLEmbedElement', + 'fieldset': 'HTMLFieldSetElement', + 'font': 'HTMLFontElement', + 'form': 'HTMLFormElement', + 'frame': 'HTMLFrameElement', + 'frameset': 'HTMLFrameSetElement', + 'h1': 'HTMLHeadingElement', + 'head': 'HTMLHeadElement', + 'hr': 'HTMLHRElement', + 'html': 'HTMLHtmlElement', + 'iframe': 'HTMLIFrameElement', + 'img': 'HTMLImageElement', + 'input': 'HTMLInputElement', + 'keygen': 'HTMLKeygenElement', + 'label': 'HTMLLabelElement', + 'legend': 'HTMLLegendElement', + 'li': 'HTMLLIElement', + 'link': 'HTMLLinkElement', + 'map': 'HTMLMapElement', + 'marquee': 'HTMLMarqueeElement', + 'menu': 'HTMLMenuElement', + 'menuitem': 'HTMLMenuItemElement', + 'meta': 'HTMLMetaElement', + 'meter': 'HTMLMeterElement', + 'object': 'HTMLObjectElement', + 'ol': 'HTMLOListElement', + 'optgroup': 'HTMLOptGroupElement', + 'option': 'HTMLOptionElement', + 'output': 'HTMLOutputElement', + 'p': 'HTMLParagraphElement', + 'param': 'HTMLParamElement', + 'pre': 'HTMLPreElement', + 'progress': 'HTMLProgressElement', + 'q': 'HTMLQuoteElement', + 'script': 'HTMLScriptElement', + 'select': 'HTMLSelectElement', + 'shadow': 'HTMLShadowElement', + 'source': 'HTMLSourceElement', + 'span': 'HTMLSpanElement', + 'style': 'HTMLStyleElement', + 'table': 'HTMLTableElement', + 'tbody': 'HTMLTableSectionElement', + // WebKit and Moz are wrong: + // https://bugs.webkit.org/show_bug.cgi?id=111469 + // https://bugzilla.mozilla.org/show_bug.cgi?id=848096 + // 'td': 'HTMLTableCellElement', + 'template': 'HTMLTemplateElement', + 'textarea': 'HTMLTextAreaElement', + 'thead': 'HTMLTableSectionElement', + 'time': 'HTMLTimeElement', + 'title': 'HTMLTitleElement', + 'tr': 'HTMLTableRowElement', + 'track': 'HTMLTrackElement', + 'ul': 'HTMLUListElement', + 'video': 'HTMLVideoElement', + }; + + function overrideConstructor(tagName) { + var nativeConstructorName = elements[tagName]; + var nativeConstructor = window[nativeConstructorName]; + if (!nativeConstructor) + return; + var element = document.createElement(tagName); + var wrapperConstructor = element.constructor; + window[nativeConstructorName] = wrapperConstructor; + } + + Object.keys(elements).forEach(overrideConstructor); + + Object.getOwnPropertyNames(scope.wrappers).forEach(function(name) { + window[name] = scope.wrappers[name] + }); + +})(window.ShadowDOMPolyfill); diff --git a/src/WeakMap/WeakMap.js b/src/WeakMap/WeakMap.js new file mode 100644 index 0000000..0540742 --- /dev/null +++ b/src/WeakMap/WeakMap.js @@ -0,0 +1,46 @@ +/* + * Copyright 2012 The Polymer Authors. All rights reserved. + * Use of this source code is governed by a BSD-style + * license that can be found in the LICENSE file. + */ + +if (typeof WeakMap === 'undefined') { + (function() { + var defineProperty = Object.defineProperty; + var counter = Date.now() % 1e9; + + var WeakMap = function() { + this.name = '__st' + (Math.random() * 1e9 >>> 0) + (counter++ + '__'); + }; + + WeakMap.prototype = { + set: function(key, value) { + var entry = key[this.name]; + if (entry && entry[0] === key) + entry[1] = value; + else + defineProperty(key, this.name, {value: [key, value], writable: true}); + return this; + }, + get: function(key) { + var entry; + return (entry = key[this.name]) && entry[0] === key ? + entry[1] : undefined; + }, + delete: function(key) { + var entry = key[this.name]; + if (!entry) return false; + var hasValue = entry[0] === key; + entry[0] = entry[1] = undefined; + return hasValue; + }, + has: function(key) { + var entry = key[this.name]; + if (!entry) return false; + return entry[0] === key; + } + }; + + window.WeakMap = WeakMap; + })(); +} diff --git a/src/WebComponents/dom.js b/src/WebComponents/dom.js new file mode 100644 index 0000000..00a5de5 --- /dev/null +++ b/src/WebComponents/dom.js @@ -0,0 +1,97 @@ +/* + * Copyright (c) 2014 The Polymer Project Authors. All rights reserved. + * This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt + * The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt + * The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt + * Code distributed by Google as part of the polymer project is also + * subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt + */ + +(function(scope) { + + 'use strict'; + + // polyfill performance.now + + if (!window.performance) { + var start = Date.now(); + // only at millisecond precision + window.performance = {now: function(){ return Date.now() - start }}; + } + + // polyfill for requestAnimationFrame + + if (!window.requestAnimationFrame) { + window.requestAnimationFrame = (function() { + var nativeRaf = window.webkitRequestAnimationFrame || + window.mozRequestAnimationFrame; + + return nativeRaf ? + function(callback) { + return nativeRaf(function() { + callback(performance.now()); + }); + } : + function( callback ){ + return window.setTimeout(callback, 1000 / 60); + }; + })(); + } + + if (!window.cancelAnimationFrame) { + window.cancelAnimationFrame = (function() { + return window.webkitCancelAnimationFrame || + window.mozCancelAnimationFrame || + function(id) { + clearTimeout(id); + }; + })(); + } + + // Make a stub for Polymer() for polyfill purposes; under the HTMLImports + // polyfill, scripts in the main document run before imports. That means + // if (1) polymer is imported and (2) Polymer() is called in the main document + // in a script after the import, 2 occurs before 1. We correct this here + // by specfiically patching Polymer(); this is not necessary under native + // HTMLImports. + var elementDeclarations = []; + + var polymerStub = function(name, dictionary) { + if ((typeof name !== 'string') && (arguments.length === 1)) { + Array.prototype.push.call(arguments, document._currentScript); + } + elementDeclarations.push(arguments); + }; + window.Polymer = polymerStub; + + // deliver queued delcarations + scope.consumeDeclarations = function(callback) { + scope.consumeDeclarations = function() { + throw 'Possible attempt to load Polymer twice'; + }; + if (callback) { + callback(elementDeclarations); + } + elementDeclarations = null; + }; + + function installPolymerWarning() { + if (window.Polymer === polymerStub) { + window.Polymer = function() { + throw new Error('You tried to use polymer without loading it first. To ' + + 'load polymer, '); + }; + } + } + + // Once DOMContent has loaded, any main document scripts that depend on + // Polymer() should have run. Calling Polymer() now is an error until + // polymer is imported. + if (HTMLImports.useNative) { + installPolymerWarning(); + } else { + addEventListener('DOMContentLoaded', installPolymerWarning); + } + +})(window.WebComponents); diff --git a/src/WebComponents/lang.js b/src/WebComponents/lang.js new file mode 100644 index 0000000..c55e1e2 --- /dev/null +++ b/src/WebComponents/lang.js @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2014 The Polymer Project Authors. All rights reserved. + * This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt + * The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt + * The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt + * Code distributed by Google as part of the polymer project is also + * subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt + */ + +(function(scope) { + +// Old versions of iOS do not have bind. + +if (!Function.prototype.bind) { + Function.prototype.bind = function(scope) { + var self = this; + var args = Array.prototype.slice.call(arguments, 1); + return function() { + var args2 = args.slice(); + args2.push.apply(args2, arguments); + return self.apply(scope, args2); + }; + }; +} + +})(window.WebComponents); diff --git a/src/WebComponents/patches-html-imports-csp.js b/src/WebComponents/patches-html-imports-csp.js new file mode 100644 index 0000000..1875cb3 --- /dev/null +++ b/src/WebComponents/patches-html-imports-csp.js @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2014 The Polymer Project Authors. All rights reserved. + * This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt + * The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt + * The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt + * Code distributed by Google as part of the polymer project is also + * subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt + */ + +(function(scope) { + +if (!scope) { + scope = window.HTMLImports = {flags:{}}; +} + +// Patches for running in a sandboxed iframe. +// The origin is set to null when we're running in a sandbox, so we +// ask the parent window to fetch the resources. + +var xhr = { + callbacks: {}, + load: function(url, next, nextContext) { + xhr.callbacks[url] = { + next: next, + nextContext: nextContext + }; + parent.postMessage({ + url: url, + bust: scope.flags.debug || scope.flags.bust + }, '*'); + }, + receive: function(url, err, resource) { + var cb = xhr.callbacks[url]; + if (cb) { + var next = cb.next; + var nextContext = cb.nextContext; + next.call(nextContext, err, resource, url); + } + } +}; + +xhr.loadDocument = xhr.load; + +window.addEventListener('message', function(e) { + xhr.receive(e.data.url, e.data.err, e.data.resource); +}); + +// exports + +scope.xhr = xhr; + +})(window.HTMLImports); diff --git a/src/WebComponents/shadowdom.js b/src/WebComponents/shadowdom.js new file mode 100644 index 0000000..3ff30c1 --- /dev/null +++ b/src/WebComponents/shadowdom.js @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2014 The Polymer Project Authors. All rights reserved. + * This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt + * The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt + * The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt + * Code distributed by Google as part of the polymer project is also + * subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt + */ + +(function(scope) { + /** + The ShadowDOM polyfill uses a wrapping strategy on dom elements. This is + 99% transparent but there are a few nodes (e.g. document) that cannot be + automatically wrapped. Therefore, rarely it's necessary to wrap nodes in + user code. Here we're choosing to make convenient globals `wrap` and + `unwrap` that can be used whether or not the polyfill is in use. + */ + // convenient global + if (window.ShadowDOMPolyfill) { + window.wrap = ShadowDOMPolyfill.wrapIfNeeded; + window.unwrap = ShadowDOMPolyfill.unwrapIfNeeded; + } else { + // so we can call wrap/unwrap without testing for ShadowDOMPolyfill + window.wrap = window.unwrap = function(n) { + return n; + }; + } + +})(window.WebComponents); diff --git a/src/WebComponents/unresolved.js b/src/WebComponents/unresolved.js new file mode 100644 index 0000000..8798d64 --- /dev/null +++ b/src/WebComponents/unresolved.js @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2014 The Polymer Project Authors. All rights reserved. + * This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt + * The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt + * The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt + * Code distributed by Google as part of the polymer project is also + * subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt + */ + +(function(scope) { + + // It's desireable to provide a default stylesheet + // that's convenient for styling unresolved elements, but + // it's cumbersome to have to include this manually in every page. + // It would make sense to put inside some HTMLImport but + // the HTMLImports polyfill does not allow loading of stylesheets + // that block rendering. Therefore this injection is tolerated here. + // + // NOTE: position: relative fixes IE's failure to inherit opacity + // when a child is not statically positioned. + var style = document.createElement('style'); + style.textContent = '' + + 'body {' + + 'transition: opacity ease-in 0.2s;' + + ' } \n' + + 'body[unresolved] {' + + 'opacity: 0; display: block; overflow: hidden; position: relative;' + + ' } \n' + ; + var head = document.querySelector('head'); + head.insertBefore(style, head.firstChild); + +})(window.WebComponents); diff --git a/tests/CustomElements/html/attributes.html b/tests/CustomElements/html/attributes.html new file mode 100644 index 0000000..db4ea63 --- /dev/null +++ b/tests/CustomElements/html/attributes.html @@ -0,0 +1,54 @@ + + + + + Custom Elements: attributes + + + + + + + + diff --git a/tests/CustomElements/html/imports.html b/tests/CustomElements/html/imports.html new file mode 100644 index 0000000..5f97c98 --- /dev/null +++ b/tests/CustomElements/html/imports.html @@ -0,0 +1,55 @@ + + + + + Custom Elements: imports integration + + + + + + + + + + diff --git a/tests/CustomElements/html/shadowdom.html b/tests/CustomElements/html/shadowdom.html new file mode 100644 index 0000000..0f9d785 --- /dev/null +++ b/tests/CustomElements/html/shadowdom.html @@ -0,0 +1,59 @@ + + + + + Custom Elements: shadowdom integration + + + + + + + + + + diff --git a/tests/CustomElements/html/upgrade-dcl.html b/tests/CustomElements/html/upgrade-dcl.html new file mode 100644 index 0000000..c1fc49d --- /dev/null +++ b/tests/CustomElements/html/upgrade-dcl.html @@ -0,0 +1,44 @@ + + + + + Custom Elements: upgrade order + + + + + + + + + + + diff --git a/tests/CustomElements/html/upgrade-order.html b/tests/CustomElements/html/upgrade-order.html new file mode 100644 index 0000000..8d45b27 --- /dev/null +++ b/tests/CustomElements/html/upgrade-order.html @@ -0,0 +1,69 @@ + + + + + Custom Elements: upgrade order + + + + + + + + + + + + + + + + + + + + diff --git a/tests/CustomElements/js/customElements.js b/tests/CustomElements/js/customElements.js new file mode 100644 index 0000000..4109958 --- /dev/null +++ b/tests/CustomElements/js/customElements.js @@ -0,0 +1,527 @@ +/* + * Copyright 2013 The Polymer Authors. All rights reserved. + * Use of this source code is governed by a BSD-style + * license that can be found in the LICENSE file. + */ + +suite('customElements', function() { + var work; + var assert = chai.assert; + var HTMLNS = 'http://www.w3.org/1999/xhtml'; + + setup(function() { + work = document.createElement('div'); + document.body.appendChild(work); + }); + + teardown(function() { + document.body.removeChild(work); + }); + + test('document.registerElement requires name argument', function() { + try { + document.registerElement(); + } catch(x) { + return; + } + assert.ok(false, 'document.registerElement failed to throw when given no arguments'); + }); + + test('document.registerElement requires name argument to contain a dash', function() { + try { + document.registerElement('xfoo', {prototype: Object.create(HTMLElement.prototype)}); + } catch(x) { + return; + } + assert.ok(false, 'document.registerElement failed to throw when given no arguments'); + }); + + // http://w3c.github.io/webcomponents/spec/custom/#extensions-to-document-interface-to-register + test('document.registerElement second argument is optional', function() { + try { + document.registerElement('x-no-proto'); + } catch(x) { + return; + } + assert.ok(true, 'document.registerElement failed to function without ElementRegistionOptions argument'); + }); + + test('document.registerElement requires name argument to not conflict with a reserved name', function() { + try { + document.registerElement('font-face', {prototype: Object.create(HTMLElement.prototype)}); + } catch(x) { + return; + } + assert.ok(false, 'Failed to execute \'registerElement\' on \'Document\': Registration failed for type \'font-face\'. The type name is invalid.'); + }); + + test('document.registerElement requires name argument to be unique', function() { + var proto = {prototype: Object.create(HTMLElement.prototype)}; + document.registerElement('x-duplicate', proto); + try { + document.registerElement('x-duplicate', proto); + } catch(x) { + return; + } + assert.ok(false, 'document.registerElement failed to throw when called multiple times with the same element name'); + }); + + test('document.registerElement create via new', function() { + // register x-foo + var XFoo = document.registerElement('x-foo', {prototype: Object.create(HTMLElement.prototype)}); + // create an instance via new + var xfoo = new XFoo(); + // test localName + assert.equal(xfoo.localName, 'x-foo'); + // attach content + work.appendChild(xfoo).textContent = '[x-foo]'; + // reacquire + var xfoo = work.querySelector('x-foo'); + // test textContent + assert.equal(xfoo.textContent, '[x-foo]'); + }); + + test('document.registerElement create via createElement', function() { + // register x-foo + var XFoo = document.registerElement('x-foo2', {prototype: Object.create(HTMLElement.prototype)}); + // create an instance via createElement + var xfoo = document.createElement('x-foo2'); + // test localName + assert.equal(xfoo.localName, 'x-foo2'); + // attach content + xfoo.textContent = '[x-foo2]'; + // test textContent + assert.equal(xfoo.textContent, '[x-foo2]'); + }); + + test('document.registerElement create via createElementNS', function() { + // create an instance via createElementNS + var xfoo = document.createElementNS(HTMLNS, 'x-foo2'); + // test localName + assert.equal(xfoo.localName, 'x-foo2'); + // attach content + xfoo.textContent = '[x-foo2]'; + // test textContent + assert.equal(xfoo.textContent, '[x-foo2]'); + }); + + test('document.registerElement treats names as case insensitive', function() { + var proto = {prototype: Object.create(HTMLElement.prototype)}; + proto.prototype.isXCase = true; + var XCase = document.registerElement('X-CASE', proto); + // createElement + var x = document.createElement('X-CASE'); + assert.equal(x.isXCase, true); + x = document.createElement('x-case'); + assert.equal(x.isXCase, true); + // createElementNS + // NOTE: createElementNS is case sensitive, disable tests + // x = document.createElementNS(HTMLNS, 'X-CASE'); + // assert.equal(x.isXCase, true); + // x = document.createElementNS(HTMLNS, 'x-case'); + // assert.equal(x.isXCase, true); + // upgrade + work.innerHTML = ''; + CustomElements.takeRecords(); + assert.equal(work.firstChild.isXCase, true); + assert.equal(work.firstChild.nextSibling.isXCase, true); + }); + + test('document.registerElement create multiple instances', function() { + var XFooPrototype = Object.create(HTMLElement.prototype); + XFooPrototype.bluate = function() { + this.color = 'lightblue'; + }; + var XFoo = document.registerElement('x-foo3', { + prototype: XFooPrototype + }); + // create an instance + var xfoo1 = new XFoo(); + // create another instance + var xfoo2 = new XFoo(); + // test textContent + xfoo1.textContent = '[x-foo1]'; + xfoo2.textContent = '[x-foo2]'; + assert.equal(xfoo1.textContent, '[x-foo1]'); + assert.equal(xfoo2.textContent, '[x-foo2]'); + // test bluate + xfoo1.bluate(); + assert.equal(xfoo1.color, 'lightblue'); + assert.isUndefined(xfoo2.color); + }); + + test('document.registerElement extend native element', function() { + // test native element extension + var XBarPrototype = Object.create(HTMLButtonElement.prototype); + var XBar = document.registerElement('x-bar', { + prototype: XBarPrototype, + extends: 'button' + }); + var xbar = new XBar(); + work.appendChild(xbar).textContent = 'x-bar'; + xbar = work.querySelector('button[is=x-bar]'); + assert(xbar); + assert.equal(xbar.textContent, 'x-bar'); + // test extension of native element extension + var XBarBarPrototype = Object.create(XBarPrototype); + var XBarBar = document.registerElement('x-barbar', { + prototype: XBarBarPrototype, + extends: 'button' + }); + var xbarbar = new XBarBar(); + work.appendChild(xbarbar).textContent = 'x-barbar'; + xbarbar = work.querySelector('button[is=x-barbar]'); + assert(xbarbar); + assert.equal(xbarbar.textContent, 'x-barbar'); + // test extension^3 + var XBarBarBarPrototype = Object.create(XBarBarPrototype); + var XBarBarBar = document.registerElement('x-barbarbar', { + prototype: XBarBarBarPrototype, + extends: 'button' + }); + var xbarbarbar = new XBarBarBar(); + work.appendChild(xbarbarbar).textContent = 'x-barbarbar'; + xbarbarbar = work.querySelector('button[is=x-barbarbar]'); + assert(xbarbarbar); + assert.equal(xbarbarbar.textContent, 'x-barbarbar'); + }); + + test('document.registerElement createdCallback in prototype', function() { + var XBooPrototype = Object.create(HTMLElement.prototype); + XBooPrototype.createdCallback = function() { + this.style.fontStyle = 'italic'; + } + var XBoo = document.registerElement('x-boo', { + prototype: XBooPrototype + }); + var xboo = new XBoo(); + assert.equal(xboo.style.fontStyle, 'italic'); + // + var XBooBooPrototype = Object.create(XBooPrototype); + XBooBooPrototype.createdCallback = function() { + XBoo.prototype.createdCallback.call(this); + this.style.fontSize = '32pt'; + }; + var XBooBoo = document.registerElement('x-booboo', { + prototype: XBooBooPrototype + }); + var xbooboo = new XBooBoo(); + assert.equal(xbooboo.style.fontStyle, 'italic'); + assert.equal(xbooboo.style.fontSize, '32pt'); + }); + + test('document.registerElement [created|attached|detached]Callbacks in prototype', function(done) { + var ready, inserted, removed; + var XBooPrototype = Object.create(HTMLElement.prototype); + XBooPrototype.createdCallback = function() { + ready = true; + } + XBooPrototype.attachedCallback = function() { + inserted = true; + } + XBooPrototype.detachedCallback = function() { + removed = true; + } + var XBoo = document.registerElement('x-boo-ir', { + prototype: XBooPrototype + }); + var xboo = new XBoo(); + assert(ready, 'ready must be true [XBoo]'); + assert(!inserted, 'inserted must be false [XBoo]'); + assert(!removed, 'removed must be false [XBoo]'); + work.appendChild(xboo); + CustomElements.takeRecords(); + assert(inserted, 'inserted must be true [XBoo]'); + work.removeChild(xboo); + CustomElements.takeRecords(); + assert(removed, 'removed must be true [XBoo]'); + // + ready = inserted = removed = false; + var XBooBooPrototype = Object.create(XBooPrototype); + XBooBooPrototype.createdCallback = function() { + XBoo.prototype.createdCallback.call(this); + }; + XBooBooPrototype.attachedCallback = function() { + XBoo.prototype.attachedCallback.call(this); + }; + XBooBooPrototype.detachedCallback = function() { + XBoo.prototype.detachedCallback.call(this); + }; + var XBooBoo = document.registerElement('x-booboo-ir', { + prototype: XBooBooPrototype + }); + var xbooboo = new XBooBoo(); + assert(ready, 'ready must be true [XBooBoo]'); + assert(!inserted, 'inserted must be false [XBooBoo]'); + assert(!removed, 'removed must be false [XBooBoo]'); + work.appendChild(xbooboo); + CustomElements.takeRecords(); + assert(inserted, 'inserted must be true [XBooBoo]'); + work.removeChild(xbooboo); + CustomElements.takeRecords(); + assert(removed, 'removed must be true [XBooBoo]'); + done(); + }); + + test('document.registerElement attributeChangedCallback in prototype', function(done) { + var XBooPrototype = Object.create(HTMLElement.prototype); + XBooPrototype.attributeChangedCallback = function(inName, inOldValue) { + if (inName == 'foo' && inOldValue=='bar' + && this.attributes.foo.value == 'zot') { + done(); + } + } + var XBoo = document.registerElement('x-boo-acp', { + prototype: XBooPrototype + }); + var xboo = new XBoo(); + xboo.setAttribute('foo', 'bar'); + xboo.setAttribute('foo', 'zot'); + }); + + test('document.registerElement attachedCallbacks in prototype', function(done) { + var inserted = 0; + var XBooPrototype = Object.create(HTMLElement.prototype); + XBooPrototype.attachedCallback = function() { + inserted++; + }; + var XBoo = document.registerElement('x-boo-at', { + prototype: XBooPrototype + }); + var xboo = new XBoo(); + assert.equal(inserted, 0, 'inserted must be 0'); + work.appendChild(xboo); + CustomElements.takeRecords(); + assert.equal(inserted, 1, 'inserted must be 1'); + work.removeChild(xboo); + CustomElements.takeRecords(); + assert(!xboo.parentNode); + work.appendChild(xboo); + CustomElements.takeRecords(); + assert.equal(inserted, 2, 'inserted must be 2'); + done(); + }); + + test('document.registerElement detachedCallbacks in prototype', function(done) { + var ready, inserted, removed; + var XBooPrototype = Object.create(HTMLElement.prototype); + XBooPrototype.detachedCallback = function() { + removed = true; + } + var XBoo = document.registerElement('x-boo-ir2', { + prototype: XBooPrototype + }); + var xboo = new XBoo(); + assert(!removed, 'removed must be false [XBoo]'); + work.appendChild(xboo); + CustomElements.takeRecords(); + work.removeChild(xboo); + CustomElements.takeRecords(); + assert(removed, 'removed must be true [XBoo]'); + // + ready = inserted = removed = false; + var XBooBooPrototype = Object.create(XBooPrototype); + XBooBooPrototype.detachedCallback = function() { + XBoo.prototype.detachedCallback.call(this); + }; + var XBooBoo = document.registerElement('x-booboo-ir2', { + prototype: XBooBooPrototype + }); + var xbooboo = new XBooBoo(); + assert(!removed, 'removed must be false [XBooBoo]'); + work.appendChild(xbooboo); + CustomElements.takeRecords(); + work.removeChild(xbooboo); + CustomElements.takeRecords(); + assert(removed, 'removed must be true [XBooBoo]'); + done(); + }); + + test('document.registerElement can use Functions as definitions', function() { + // function used as Custom Element defintion + function A$A() { + this.alive = true; + } + A$A.prototype = Object.create(HTMLElement.prototype); + // bind createdCallback to function body + A$A.prototype.createdCallback = A$A; + A$A = document.registerElement('a-a', A$A); + // test via new + var a = new A$A(); + assert.equal(a.alive, true); + // test via parser upgrade + work.innerHTML = ''; + CustomElements.takeRecords(); + assert.equal(work.firstElementChild.alive, true); + }); + + test('node.cloneNode upgrades', function(done) { + var XBooPrototype = Object.create(HTMLElement.prototype); + XBooPrototype.createdCallback = function() { + this.__ready__ = true; + }; + var XBoo = document.registerElement('x-boo-clone', { + prototype: XBooPrototype + }); + var xboo = new XBoo(); + work.appendChild(xboo); + CustomElements.takeRecords(); + var xboo2 = xboo.cloneNode(true); + assert(xboo2.__ready__, 'clone createdCallback must be called'); + done(); + }); + + test('entered left apply to view', function() { + var invocations = []; + var elementProto = Object.create(HTMLElement.prototype); + elementProto.createdCallback = function() { + invocations.push('created'); + } + elementProto.attachedCallback = function() { + invocations.push('entered'); + } + elementProto.detachedCallback = function() { + invocations.push('left'); + } + var tagName = 'x-entered-left-view'; + var CustomElement = document.registerElement(tagName, { prototype: elementProto }); + + var docB = document.implementation.createHTMLDocument(''); + docB.body.innerHTML = '<' + tagName + '>'; + CustomElements.upgradeDocumentTree(docB); + CustomElements.takeRecords(); + assert.deepEqual(invocations, ['created'], 'created but not entered view'); + + var element = docB.body.childNodes[0]; + // note, cannot use instanceof due to IE + assert.equal(element.__proto__, CustomElement.prototype, 'element is correct type'); + + work.appendChild(element) + CustomElements.takeRecords(); + assert.deepEqual(invocations, ['created', 'entered'], + 'created and entered view'); + + docB.body.appendChild(element); + CustomElements.takeRecords(); + assert.deepEqual(invocations, ['created', 'entered', 'left'], + 'created, entered then left view'); + }); + + test('attachedCallback ordering', function() { + var log = []; + var p = Object.create(HTMLElement.prototype); + p.attachedCallback = function() { + log.push(this.id); + }; + document.registerElement('x-boo-ordering', {prototype: p}); + + work.innerHTML = + '' + + '' + + '' + + '' + + '' + + '' + + ''; + + CustomElements.takeRecords(); + assert.deepEqual(['a', 'b', 'c', 'd', 'e'], log); + }); + + test('detachedCallback ordering', function() { + var log = []; + var p = Object.create(HTMLElement.prototype); + p.detachedCallback = function() { + log.push(this.id); + }; + document.registerElement('x-boo2-ordering', {prototype: p}); + + work.innerHTML = + '' + + '' + + '' + + '' + + '' + + '' + + ''; + + CustomElements.takeRecords(); + work.removeChild(work.firstElementChild); + CustomElements.takeRecords(); + assert.deepEqual(['a', 'b', 'c', 'd', 'e'], log); + }); + + test('instanceof', function() { + var p = Object.create(HTMLElement.prototype); + var PCtor = document.registerElement('x-instance', {prototype: p}); + var x = document.createElement('x-instance'); + assert.isTrue(CustomElements.instanceof(x, PCtor), 'instanceof failed for x-instance'); + x = document.createElementNS(HTMLNS, 'x-instance'); + assert.isTrue(CustomElements.instanceof(x, PCtor), 'instanceof failed for x-instance'); + + var p2 = Object.create(PCtor.prototype); + var P2Ctor = document.registerElement('x-instance2', {prototype: p2}); + var x2 = document.createElement('x-instance2'); + assert.isTrue(CustomElements.instanceof(x2, P2Ctor), 'instanceof failed for x-instance2'); + assert.isTrue(CustomElements.instanceof(x2, PCtor), 'instanceof failed for x-instance2'); + x2 = document.createElementNS(HTMLNS, 'x-instance2'); + assert.isTrue(CustomElements.instanceof(x2, P2Ctor), 'instanceof failed for x-instance2'); + assert.isTrue(CustomElements.instanceof(x2, PCtor), 'instanceof failed for x-instance2'); + }); + + + test('instanceof typeExtension', function() { + var p = Object.create(HTMLButtonElement.prototype); + var PCtor = document.registerElement('x-button-instance', {prototype: p, extends: 'button'}); + var x = document.createElement('button', 'x-button-instance'); + assert.isTrue(CustomElements.instanceof(x, PCtor), 'instanceof failed for x-button-instance'); + assert.isTrue(CustomElements.instanceof(x, HTMLButtonElement), 'instanceof failed for x-button-instance'); + x = document.createElementNS(HTMLNS, 'button', 'x-button-instance'); + assert.isTrue(CustomElements.instanceof(x, PCtor), 'instanceof failed for x-button-instance'); + assert.isTrue(CustomElements.instanceof(x, HTMLButtonElement), 'instanceof failed for x-button-instance'); + + var p2 = Object.create(PCtor.prototype); + var P2Ctor = document.registerElement('x-button-instance2', {prototype: p2, extends: 'button'}); + var x2 = document.createElement('button','x-button-instance2'); + assert.isTrue(CustomElements.instanceof(x2, P2Ctor), 'instanceof failed for x-button-instance2'); + assert.isTrue(CustomElements.instanceof(x2, PCtor), 'instanceof failed for x-button-instance2'); + assert.isTrue(CustomElements.instanceof(x2, HTMLButtonElement), 'instanceof failed for x-button-instance2'); + x2 = document.createElementNS(HTMLNS, 'button','x-button-instance2'); + assert.isTrue(CustomElements.instanceof(x2, P2Ctor), 'instanceof failed for x-button-instance2'); + assert.isTrue(CustomElements.instanceof(x2, PCtor), 'instanceof failed for x-button-instance2'); + assert.isTrue(CustomElements.instanceof(x2, HTMLButtonElement), 'instanceof failed for x-button-instance2'); + }); + + test('extends and prototype mismatch', function() { + var p = Object.create(HTMLElement.prototype); + var PCtor = document.registerElement('not-button', { + extends: 'button', + prototype: p + }); + + var e = document.createElement('button', 'not-button'); + + // NOTE: firefox has a hack for instanceof that uses element tagname mapping + // Work around by checking prototype manually + // + var ff = document.createElement('button'); ff.__proto__ = null; + if (ff instanceof HTMLButtonElement) { + // Base proto will be one below custom proto + var proto = e.__proto__.__proto__; + assert.isFalse(proto === HTMLButtonElement.prototype); + assert.isTrue(proto === HTMLElement.prototype); + } else { + assert.isFalse(CustomElements.instanceof(e, HTMLButtonElement)); + assert.isTrue(CustomElements.instanceof(e, HTMLElement)); + } + }); + +}); + +htmlSuite('customElements (html)', function() { + htmlTest('../html/attributes.html'); + htmlTest('../html/upgrade-order.html'); + htmlTest('../html/upgrade-dcl.html'); + htmlTest('../html/imports.html'); + htmlTest('../html/shadowdom.html'); +}); diff --git a/tests/CustomElements/js/documentRegister.js b/tests/CustomElements/js/documentRegister.js new file mode 100644 index 0000000..580355c --- /dev/null +++ b/tests/CustomElements/js/documentRegister.js @@ -0,0 +1,242 @@ +/* + * Copyright 2013 The Polymer Authors. All rights reserved. + * Use of this source code is governed by a BSD-style + * license that can be found in the LICENSE file. + */ + + +// Adapted from: +// https://code.google.com/p/chromium/codesearch#chromium/src/third_party/WebKit/LayoutTests/fast/dom/custom/document-register-type-extensions.html +var testForm = document.createElement('form'); + +function isFormControl(element) +{ + testForm.appendChild(element); + return element.form == testForm; +} + + +/* + * Work around IE's insertion of XML Namespace elements into .outerHTML of HTMLUnknownElements + * + * Clone the input node, insert it into a div, and then read back the outerHTML, which is now stripped of the XML * + * Namespace element + */ +var isIE = navigator.userAgent.indexOf('Trident') > -1; +function assertOuterHTML(element, expected) { + var outerHTML = element.outerHTML; + if (isIE) { + var div = document.createElement('div'); + div.appendChild(element.cloneNode(true)); + outerHTML = div.firstChild.outerHTML; + } + chai.assert.equal(outerHTML, expected); +} + +var hasProto = ({}.__proto__); +function assertInstanceOf(element, constructor) { + if (hasProto) { + chai.assert.instanceOf(element, constructor); + } +} + +function assertNotInstanceOf(element, constructor) { + if (hasProto) { + chai.assert.notInstanceOf(element, constructor); + } +} + +suite('register-type-extensions', function() { + var assert = chai.assert; + + var fooConstructor = document.registerElement('x-foo-x', { + prototype: Object.create(HTMLElement.prototype) }); + var fooOuterHTML = ''; + var barConstructor = document.registerElement('x-bar-x', { + prototype: Object.create(HTMLInputElement.prototype), + extends:'input'}); + var barOuterHTML = ''; + var bazConstructor = document.registerElement('x-baz', { + prototype: Object.create(fooConstructor.prototype) }); + var quxConstructor = document.registerElement('x-qux', { + prototype: Object.create(barConstructor.prototype), + extends:'input'}); + + test('cannot register twice', function() { + assert.throws(function() { + document.registerElement('x-foo-x', { + prototype: Object.create(HTMLDivElement.prototype) }); + }); + }); + + suite('generated constructors', function() { + test('custom tag', function() { + var fooNewed = new fooConstructor(); + assertOuterHTML(fooNewed, fooOuterHTML); + assertInstanceOf(fooNewed, fooConstructor); + assertInstanceOf(fooNewed, HTMLElement); + // This is part of the Blink tests, but not supported in Firefox with + // polyfill. Similar assertions are also commented out below. + // assertNotInstanceOf(fooNewed, HTMLUnknownElement); + + test('custom tag constructor', function() { + assert.equal('a', 'b'); + }); + }); + + test('type extension', function() { + var barNewed = new barConstructor(); + assertOuterHTML(barNewed, barOuterHTML); + assertInstanceOf(barNewed, barConstructor); + assertInstanceOf(barNewed, HTMLInputElement); + assert.ok(isFormControl(barNewed)); + }); + + test('custom tag deriving from custom tag', function() { + var bazNewed = new bazConstructor(); + var bazOuterHTML = ''; + assertOuterHTML(bazNewed, bazOuterHTML); + assertInstanceOf(bazNewed, bazConstructor); + assertInstanceOf(bazNewed, HTMLElement); + // assertNotInstanceOf(bazNewed, HTMLUnknownElement); + }); + + test('type extension deriving from custom tag', function() { + var quxNewed = new quxConstructor(); + var quxOuterHTML = ''; + assertInstanceOf(quxNewed, quxConstructor); + assertInstanceOf(quxNewed, barConstructor); + assertInstanceOf(quxNewed, HTMLInputElement); + assertOuterHTML(quxNewed, quxOuterHTML); + assert.ok(isFormControl(quxNewed)); + }); + }); + + suite('single-parameter createElement', function() { + test('custom tag', function() { + var fooCreated = document.createElement('x-foo-x'); + assertOuterHTML(fooCreated, fooOuterHTML); + assertInstanceOf(fooCreated, fooConstructor); + }); + + test('type extension', function() { + var barCreated = document.createElement('x-bar-x'); + assertOuterHTML(barCreated, ''); + assertNotInstanceOf(barCreated, barConstructor); + // assertNotInstanceOf(barCreated, HTMLUnknownElement); + assertInstanceOf(barCreated, HTMLElement); + }); + + test('custom tag deriving from custom tag', function() { + bazCreated = document.createElement('x-baz'); + assertOuterHTML(bazCreated, ''); + assertInstanceOf(bazCreated, bazConstructor); + // assertNotInstanceOf(bazCreated, HTMLUnknownElement); + }); + + test('type extension deriving from custom tag', function() { + quxCreated = document.createElement('x-qux'); + assertOuterHTML(quxCreated, ''); + assertNotInstanceOf(quxCreated, quxConstructor); + // assertNotInstanceOf(quxCreated, HTMLUnknownElement); + assertInstanceOf(quxCreated, HTMLElement); + }); + }); + + suite('createElement with type extensions', function() { + test('extension is custom tag', function() { + var divFooCreated = document.createElement('div', 'x-foo-x'); + assertOuterHTML(divFooCreated, '
'); + assertNotInstanceOf(divFooCreated, fooConstructor); + assertInstanceOf(divFooCreated, HTMLDivElement); + }); + + test('valid extension', function() { + var inputBarCreated = document.createElement('input', 'x-bar-x'); + assertOuterHTML(inputBarCreated, barOuterHTML); + assertInstanceOf(inputBarCreated, barConstructor); + assertNotInstanceOf(inputBarCreated, HTMLUnknownElement); + assert.ok(isFormControl(inputBarCreated)); + }); + + test('type extension of incorrect tag', function() { + var divBarCreated = document.createElement('div', 'x-bar-x'); + assertOuterHTML(divBarCreated, '
'); + assertNotInstanceOf(divBarCreated, barConstructor); + assertInstanceOf(divBarCreated, HTMLDivElement); + }); + + test('incorrect extension of custom tag', function() { + var fooBarCreated = document.createElement('x-foo-x', 'x-bar-x'); + assertOuterHTML(fooBarCreated, ''); + assertInstanceOf(fooBarCreated, fooConstructor); + }); + + test('incorrect extension of type extension', function() { + var barFooCreated = document.createElement('x-bar-x', 'x-foo-x'); + assertOuterHTML(barFooCreated, ''); + // assertNotInstanceOf(barFooCreated, HTMLUnknownElement); + assertInstanceOf(barFooCreated, HTMLElement); + }); + + test('null type extension', function() { + var fooCreatedNull = document.createElement('x-foo-x', null); + assertOuterHTML(fooCreatedNull, fooOuterHTML); + assertInstanceOf(fooCreatedNull, fooConstructor); + }); + + test('empty type extension', function() { + fooCreatedEmpty = document.createElement('x-foo-x', ''); + assertOuterHTML(fooCreatedEmpty, fooOuterHTML); + assertInstanceOf(fooCreatedEmpty, fooConstructor); + }); + + test('invalid tag name', function() { + assert.throws(function() { + document.createElement('@invalid', 'x-bar-x'); + }); + }); + }); + + suite('parser', function() { + function createElementFromHTML(html) { + var container = document.createElement('div'); + container.innerHTML = html; + if (window.CustomElements) { + window.CustomElements.upgradeAll(container); + } + return container.firstChild; + } + + test('custom tag', function() { + var fooParsed = createElementFromHTML(''); + assertInstanceOf(fooParsed, fooConstructor); + }); + + test('type extension', function() { + var barParsed = createElementFromHTML(''); + assertInstanceOf(barParsed, barConstructor); + assert.ok(isFormControl(barParsed)); + }); + + test('custom tag as type extension', function() { + var divFooParsed = createElementFromHTML('
'); + assertNotInstanceOf(divFooParsed, fooConstructor); + assertInstanceOf(divFooParsed, HTMLDivElement); + }); + + // Should we upgrade invalid tags to HTMLElement? + /*test('type extension as custom tag', function() { + var namedBarParsed = createElementFromHTML('') + assertNotInstanceOf(namedBarParsed, barConstructor); + assertNotInstanceOf(namedBarParsed, HTMLUnknownElement); + assertInstanceOf(namedBarParsed, HTMLElement); + });*/ + + test('type extension of incorrect tag', function() { + var divBarParsed = createElementFromHTML('
'); + assertNotInstanceOf(divBarParsed, barConstructor); + assertInstanceOf(divBarParsed, HTMLDivElement); + }); + }); +}); diff --git a/tests/CustomElements/js/karma-defer-tests.js b/tests/CustomElements/js/karma-defer-tests.js new file mode 100644 index 0000000..c011985 --- /dev/null +++ b/tests/CustomElements/js/karma-defer-tests.js @@ -0,0 +1,14 @@ +/* + * Copyright 2013 The Polymer Authors. All rights reserved. + * Use of this source code is governed by a BSD-style + * license that can be found in the LICENSE file. + */ + +// defer start of tests until after WebComponentsReady event +if (window.__karma__) { + window.__karma__.loaded = function() { + window.addEventListener('WebComponentsReady', function() { + window.__karma__.start(); + }); + }; +} diff --git a/tests/CustomElements/js/observe.js b/tests/CustomElements/js/observe.js new file mode 100644 index 0000000..5d33490 --- /dev/null +++ b/tests/CustomElements/js/observe.js @@ -0,0 +1,79 @@ +/* + * Copyright 2013 The Polymer Authors. All rights reserved. + * Use of this source code is governed by a BSD-style + * license that can be found in the LICENSE file. + */ + +suite('observe', function() { + var work; + var assert = chai.assert; + + setup(function() { + work = document.createElement('div'); + document.body.appendChild(work); + }); + + teardown(function() { + document.body.removeChild(work); + }); + + function registerTestComponent(inName, inValue) { + var proto = Object.create(HTMLElement.prototype); + proto.value = inValue || 'value'; + document.registerElement(inName, { + prototype: proto + }); + } + + function testElements(node, selector, value) { + Array.prototype.forEach.call(node.querySelectorAll(selector), function(n) { + assert.equal(n.value, value); + }); + } + + test('custom element automatically upgrades', function(done) { + work.innerHTML = ''; + var x = work.firstChild; + assert.isUndefined(x.value); + registerTestComponent('x-auto', 'auto'); + assert.equal(x.value, 'auto'); + done(); + }); + + test('custom element automatically upgrades in subtree', function(done) { + work.innerHTML = '
'; + var target = work.firstChild; + target.innerHTML = ''; + var x = target.firstChild; + assert.isUndefined(x.value); + registerTestComponent('x-auto-sub', 'auto-sub'); + assert.equal(x.value, 'auto-sub'); + done(); + }); + + test('custom elements automatically upgrade', function(done) { + registerTestComponent('x-auto1', 'auto1'); + registerTestComponent('x-auto2', 'auto2'); + work.innerHTML = '
' + + '
' + + '
'; + CustomElements.takeRecords(); + testElements(work, 'x-auto1', 'auto1'); + testElements(work, 'x-auto2', 'auto2'); + done(); + }); + + test('custom elements automatically upgrade in subtree', function(done) { + registerTestComponent('x-auto-sub1', 'auto-sub1'); + registerTestComponent('x-auto-sub2', 'auto-sub2'); + work.innerHTML = '
'; + var target = work.firstChild; + target.innerHTML = '
' + + '
' + + '
'; + CustomElements.takeRecords(); + testElements(target, 'x-auto-sub1', 'auto-sub1'); + testElements(target, 'x-auto-sub2', 'auto-sub2'); + done(); + }); +}); diff --git a/tests/CustomElements/js/upgrade.js b/tests/CustomElements/js/upgrade.js new file mode 100644 index 0000000..adac88a --- /dev/null +++ b/tests/CustomElements/js/upgrade.js @@ -0,0 +1,117 @@ +/* + * Copyright 2013 The Polymer Authors. All rights reserved. + * Use of this source code is governed by a BSD-style + * license that can be found in the LICENSE file. + */ + +suite('upgradeElements', function() { + var work; + var assert = chai.assert; + + setup(function() { + work = document.createElement('div'); + document.body.appendChild(work); + }); + + teardown(function() { + document.body.removeChild(work); + }); + + function registerTestComponent(inName, inValue) { + var proto = Object.create(HTMLElement.prototype); + proto.value = inValue || 'value'; + document.registerElement(inName, { + prototype: proto + }); + } + + test('CustomElements.upgrade upgrades custom element syntax', function() { + registerTestComponent('x-foo31', 'foo'); + work.innerHTML = 'Foo'; + var xfoo = work.firstChild; + CustomElements.upgradeAll(xfoo); + assert.equal(xfoo.value, 'foo'); + }); + + test('mutation observer upgrades custom element syntax', function(done) { + registerTestComponent('x-foo32', 'foo'); + work.innerHTML = 'Foo'; + CustomElements.takeRecords(); + var xfoo = work.firstChild; + assert.equal(xfoo.value, 'foo'); + done(); + }); + + test('document.register upgrades custom element syntax', function() { + work.innerHTML = 'Foo'; + registerTestComponent('x-foo33', 'foo'); + var xfoo = work.firstChild; + assert.equal(xfoo.value, 'foo'); + }); + + test('CustomElements.upgrade upgrades custom element syntax', function() { + registerTestComponent('x-zot', 'zot'); + registerTestComponent('x-zim', 'zim'); + work.innerHTML = ''; + var xzot = work.firstChild, xzim = xzot.firstChild; + CustomElements.upgradeAll(work); + assert.equal(xzot.value, 'zot'); + assert.equal(xzim.value, 'zim'); + }); + + test('CustomElements.upgrade upgrades native extendor', function() { + var XButtonProto = Object.create(HTMLButtonElement.prototype); + XButtonProto.test = 'xbutton'; + document.registerElement('x-button', { + extends: 'button', + prototype: XButtonProto + }); + + work.innerHTML = ''; + var xbutton = work.firstChild; + CustomElements.upgradeAll(xbutton); + assert.equal(xbutton.test, 'xbutton'); + }); + + + test('CustomElements.upgrade upgrades extendor of native extendor', function() { + var XInputProto = Object.create(HTMLInputElement.prototype); + XInputProto.xInput = 'xInput'; + var XInput = document.registerElement('x-input', { + extends: 'input', + prototype: XInputProto + }); + var XSpecialInputProto = Object.create(XInput.prototype); + XSpecialInputProto.xSpecialInput = 'xSpecialInput'; + var XSpecialInput = document.registerElement('x-special-input', { + extends: 'input', + prototype: XSpecialInputProto + }); + work.innerHTML = ''; + var x = work.firstChild; + CustomElements.upgradeAll(x); + assert.equal(x.xInput, 'xInput'); + assert.equal(x.xSpecialInput, 'xSpecialInput'); + }); + + + test('CustomElements.upgrade upgrades native extendor', function() { + var YButtonProto = Object.create(HTMLButtonElement.prototype); + YButtonProto.test = 'ybutton'; + document.registerElement('y-button', { + extends: 'button', + prototype: YButtonProto + }); + + work.innerHTML = '' + + '
' + + '
'; + CustomElements.upgradeAll(work); + var b$ = work.querySelectorAll('[is=y-button]'); + Array.prototype.forEach.call(b$, function(b, i) { + assert.equal(b.test, 'ybutton'); + assert.equal(b.textContent, i); + }); + }); + +}); \ No newline at end of file diff --git a/tests/CustomElements/runner.html b/tests/CustomElements/runner.html new file mode 100644 index 0000000..d2daf41 --- /dev/null +++ b/tests/CustomElements/runner.html @@ -0,0 +1,30 @@ + + +CustomElements Tests + + + + + + + + + + + +
+ + + + + diff --git a/tests/CustomElements/tests.js b/tests/CustomElements/tests.js new file mode 100644 index 0000000..a454b15 --- /dev/null +++ b/tests/CustomElements/tests.js @@ -0,0 +1,28 @@ +/* + * Copyright 2013 The Polymer Authors. All rights reserved. + * Use of this source code is governed by a BSD-style + * license that can be found in the LICENSE file. + */ + +(function() { + + var file = 'tests.js'; + var base; + + var src = + document.querySelector('script[src*="' + file + '"]').getAttribute('src'); + var base = src.slice(0, src.indexOf(file)); + + + var modules = [ + 'customElements.js', + 'upgrade.js', + 'observe.js', + 'documentRegister.js' + ]; + + modules.forEach(function(src) { + document.write(''); + }); + +})(); \ No newline at end of file diff --git a/tests/HTMLImports/html/HTMLImports.html b/tests/HTMLImports/html/HTMLImports.html new file mode 100644 index 0000000..33b4740 --- /dev/null +++ b/tests/HTMLImports/html/HTMLImports.html @@ -0,0 +1,51 @@ + + + + + HTML Imports Test + + + + + + + + + diff --git a/tests/HTMLImports/html/HTMLImportsLoaded-native.html b/tests/HTMLImports/html/HTMLImportsLoaded-native.html new file mode 100644 index 0000000..f474a18 --- /dev/null +++ b/tests/HTMLImports/html/HTMLImportsLoaded-native.html @@ -0,0 +1,36 @@ + + + + + HTMLImportsLoaded, native + + + + + + + + + + diff --git a/tests/HTMLImports/html/base/load-base.html b/tests/HTMLImports/html/base/load-base.html new file mode 100644 index 0000000..bb1bfdb --- /dev/null +++ b/tests/HTMLImports/html/base/load-base.html @@ -0,0 +1,59 @@ + + + + + base load test + + + + + + + + + + +
Test
+
Test
+
+ + + diff --git a/tests/HTMLImports/html/csp.html b/tests/HTMLImports/html/csp.html new file mode 100644 index 0000000..be3e53b --- /dev/null +++ b/tests/HTMLImports/html/csp.html @@ -0,0 +1,24 @@ + + + + + parser Test + + + + + + + + + + + diff --git a/tests/HTMLImports/html/csp.js b/tests/HTMLImports/html/csp.js new file mode 100644 index 0000000..fd4a5eb --- /dev/null +++ b/tests/HTMLImports/html/csp.js @@ -0,0 +1,15 @@ +/* + * @license + * Copyright (c) 2014 The Polymer Project Authors. All rights reserved. + * This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt + * The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt + * The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt + * Code distributed by Google as part of the polymer project is also + * subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt + */ + +addEventListener('HTMLImportsLoaded', function() { + chai.assert.ok(window.externalScriptParsed1, 'externalScriptParsed1'); + chai.assert.ok(window.externalScriptParsed2, 'externalScriptParsed2'); + done(); +}); \ No newline at end of file diff --git a/tests/HTMLImports/html/currentScript.html b/tests/HTMLImports/html/currentScript.html new file mode 100644 index 0000000..e34f65c --- /dev/null +++ b/tests/HTMLImports/html/currentScript.html @@ -0,0 +1,29 @@ + + + + + _currentScript Test + + + + + + + + + + diff --git a/tests/HTMLImports/html/dedupe.html b/tests/HTMLImports/html/dedupe.html new file mode 100644 index 0000000..f70a6ec --- /dev/null +++ b/tests/HTMLImports/html/dedupe.html @@ -0,0 +1,36 @@ + + + + + HTML dedupe Test + + + + + + + + + + diff --git a/tests/HTMLImports/html/dynamic-elements.html b/tests/HTMLImports/html/dynamic-elements.html new file mode 100644 index 0000000..9ebc2bf --- /dev/null +++ b/tests/HTMLImports/html/dynamic-elements.html @@ -0,0 +1,33 @@ + + + + + HTML Imports Dynamic Elements + + + + + + +
red?
+ + + + diff --git a/tests/HTMLImports/html/dynamic.html b/tests/HTMLImports/html/dynamic.html new file mode 100644 index 0000000..48c02a8 --- /dev/null +++ b/tests/HTMLImports/html/dynamic.html @@ -0,0 +1,44 @@ + + + + + HTML Imports Dynamic + + + + + + + + + diff --git a/tests/HTMLImports/html/encoding.html b/tests/HTMLImports/html/encoding.html new file mode 100644 index 0000000..65d5d78 --- /dev/null +++ b/tests/HTMLImports/html/encoding.html @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + diff --git a/tests/HTMLImports/html/imports/abs.html b/tests/HTMLImports/html/imports/abs.html new file mode 100644 index 0000000..7e6fca8 --- /dev/null +++ b/tests/HTMLImports/html/imports/abs.html @@ -0,0 +1,10 @@ + + \ No newline at end of file diff --git a/tests/HTMLImports/html/imports/csp-import-1.html b/tests/HTMLImports/html/imports/csp-import-1.html new file mode 100644 index 0000000..65a535a --- /dev/null +++ b/tests/HTMLImports/html/imports/csp-import-1.html @@ -0,0 +1,11 @@ + + + diff --git a/tests/HTMLImports/html/imports/csp-import-2.html b/tests/HTMLImports/html/imports/csp-import-2.html new file mode 100644 index 0000000..e619553 --- /dev/null +++ b/tests/HTMLImports/html/imports/csp-import-2.html @@ -0,0 +1,10 @@ + + \ No newline at end of file diff --git a/tests/HTMLImports/html/imports/csp-script-1.js b/tests/HTMLImports/html/imports/csp-script-1.js new file mode 100644 index 0000000..6a6b166 --- /dev/null +++ b/tests/HTMLImports/html/imports/csp-script-1.js @@ -0,0 +1,11 @@ +/* + * @license + * Copyright (c) 2014 The Polymer Project Authors. All rights reserved. + * This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt + * The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt + * The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt + * Code distributed by Google as part of the polymer project is also + * subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt + */ + +window.externalScriptParsed1 = new Date().getTime(); \ No newline at end of file diff --git a/tests/HTMLImports/html/imports/csp-script-2.js b/tests/HTMLImports/html/imports/csp-script-2.js new file mode 100644 index 0000000..12cb041 --- /dev/null +++ b/tests/HTMLImports/html/imports/csp-script-2.js @@ -0,0 +1,11 @@ +/* + * @license + * Copyright (c) 2014 The Polymer Project Authors. All rights reserved. + * This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt + * The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt + * The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt + * Code distributed by Google as part of the polymer project is also + * subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt + */ + +window.externalScriptParsed2 = new Date().getTime(); \ No newline at end of file diff --git a/tests/HTMLImports/html/imports/current-script.js b/tests/HTMLImports/html/imports/current-script.js new file mode 100644 index 0000000..c93714c --- /dev/null +++ b/tests/HTMLImports/html/imports/current-script.js @@ -0,0 +1,13 @@ +/* + * @license + * Copyright (c) 2014 The Polymer Project Authors. All rights reserved. + * This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt + * The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt + * The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt + * Code distributed by Google as part of the polymer project is also + * subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt + */ + +remoteCurrentScriptExecuted = window.remoteCurrentScriptExecuted || 0; +remoteCurrentScriptExecuted++; +chai.assert.ok(document._currentScript); \ No newline at end of file diff --git a/tests/HTMLImports/html/imports/dedupe.html b/tests/HTMLImports/html/imports/dedupe.html new file mode 100644 index 0000000..109b9bd --- /dev/null +++ b/tests/HTMLImports/html/imports/dedupe.html @@ -0,0 +1,13 @@ + + diff --git a/tests/HTMLImports/html/imports/dynamic-elements-import.html b/tests/HTMLImports/html/imports/dynamic-elements-import.html new file mode 100644 index 0000000..cce128a --- /dev/null +++ b/tests/HTMLImports/html/imports/dynamic-elements-import.html @@ -0,0 +1,28 @@ + + diff --git a/tests/HTMLImports/html/imports/encoding-import.html b/tests/HTMLImports/html/imports/encoding-import.html new file mode 100644 index 0000000..6a1a416 --- /dev/null +++ b/tests/HTMLImports/html/imports/encoding-import.html @@ -0,0 +1,15 @@ + + diff --git a/tests/HTMLImports/html/imports/external-script.js b/tests/HTMLImports/html/imports/external-script.js new file mode 100644 index 0000000..45d61c2 --- /dev/null +++ b/tests/HTMLImports/html/imports/external-script.js @@ -0,0 +1,11 @@ +/* + * @license + * Copyright (c) 2014 The Polymer Project Authors. All rights reserved. + * This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt + * The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt + * The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt + * Code distributed by Google as part of the polymer project is also + * subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt + */ + +window.externalScriptParsed = new Date().getTime(); diff --git a/tests/HTMLImports/html/imports/google.png b/tests/HTMLImports/html/imports/google.png new file mode 100644 index 0000000..1974f31 Binary files /dev/null and b/tests/HTMLImports/html/imports/google.png differ diff --git a/tests/HTMLImports/html/imports/import-1-1.html b/tests/HTMLImports/html/imports/import-1-1.html new file mode 100644 index 0000000..a4db731 --- /dev/null +++ b/tests/HTMLImports/html/imports/import-1-1.html @@ -0,0 +1,10 @@ + + \ No newline at end of file diff --git a/tests/HTMLImports/html/imports/import-1-2.html b/tests/HTMLImports/html/imports/import-1-2.html new file mode 100644 index 0000000..0dc4dc2 --- /dev/null +++ b/tests/HTMLImports/html/imports/import-1-2.html @@ -0,0 +1,13 @@ + + + + + \ No newline at end of file diff --git a/tests/HTMLImports/html/imports/import-1-3.html b/tests/HTMLImports/html/imports/import-1-3.html new file mode 100644 index 0000000..3c6c9a1 --- /dev/null +++ b/tests/HTMLImports/html/imports/import-1-3.html @@ -0,0 +1,10 @@ + + \ No newline at end of file diff --git a/tests/HTMLImports/html/imports/import-1.html b/tests/HTMLImports/html/imports/import-1.html new file mode 100644 index 0000000..fce66dc --- /dev/null +++ b/tests/HTMLImports/html/imports/import-1.html @@ -0,0 +1,12 @@ + + + + \ No newline at end of file diff --git a/tests/HTMLImports/html/imports/load-1.html b/tests/HTMLImports/html/imports/load-1.html new file mode 100644 index 0000000..40c7a81 --- /dev/null +++ b/tests/HTMLImports/html/imports/load-1.html @@ -0,0 +1,23 @@ + + + + \ No newline at end of file diff --git a/tests/HTMLImports/html/imports/load-2.html b/tests/HTMLImports/html/imports/load-2.html new file mode 100644 index 0000000..a3cb3e3 --- /dev/null +++ b/tests/HTMLImports/html/imports/load-2.html @@ -0,0 +1,13 @@ + + diff --git a/tests/HTMLImports/html/imports/load-a.html b/tests/HTMLImports/html/imports/load-a.html new file mode 100644 index 0000000..b79070d --- /dev/null +++ b/tests/HTMLImports/html/imports/load-a.html @@ -0,0 +1,14 @@ + + + \ No newline at end of file diff --git a/tests/HTMLImports/html/imports/load-b.html b/tests/HTMLImports/html/imports/load-b.html new file mode 100644 index 0000000..c616fd0 --- /dev/null +++ b/tests/HTMLImports/html/imports/load-b.html @@ -0,0 +1,15 @@ + + + + \ No newline at end of file diff --git a/tests/HTMLImports/html/imports/load-c.html b/tests/HTMLImports/html/imports/load-c.html new file mode 100644 index 0000000..2a8f2ac --- /dev/null +++ b/tests/HTMLImports/html/imports/load-c.html @@ -0,0 +1,15 @@ + + + + \ No newline at end of file diff --git a/tests/HTMLImports/html/imports/load-d.html b/tests/HTMLImports/html/imports/load-d.html new file mode 100644 index 0000000..8caa784 --- /dev/null +++ b/tests/HTMLImports/html/imports/load-d.html @@ -0,0 +1,14 @@ + + + \ No newline at end of file diff --git a/tests/HTMLImports/html/imports/parsed-import-1.html b/tests/HTMLImports/html/imports/parsed-import-1.html new file mode 100644 index 0000000..335348f --- /dev/null +++ b/tests/HTMLImports/html/imports/parsed-import-1.html @@ -0,0 +1,16 @@ + + + + diff --git a/tests/HTMLImports/html/imports/parsed-import-2.html b/tests/HTMLImports/html/imports/parsed-import-2.html new file mode 100644 index 0000000..ff96cd3 --- /dev/null +++ b/tests/HTMLImports/html/imports/parsed-import-2.html @@ -0,0 +1,10 @@ + + diff --git a/tests/HTMLImports/html/imports/script-1.html b/tests/HTMLImports/html/imports/script-1.html new file mode 100644 index 0000000..259f79b --- /dev/null +++ b/tests/HTMLImports/html/imports/script-1.html @@ -0,0 +1,16 @@ + +
me
+ + \ No newline at end of file diff --git a/tests/HTMLImports/html/imports/script-2.html b/tests/HTMLImports/html/imports/script-2.html new file mode 100644 index 0000000..234c1d6 --- /dev/null +++ b/tests/HTMLImports/html/imports/script-2.html @@ -0,0 +1,15 @@ + +
me2
+ + \ No newline at end of file diff --git a/tests/HTMLImports/html/imports/sheet1.css b/tests/HTMLImports/html/imports/sheet1.css new file mode 100644 index 0000000..4bdca7b --- /dev/null +++ b/tests/HTMLImports/html/imports/sheet1.css @@ -0,0 +1,17 @@ +/* + * @license + * Copyright (c) 2014 The Polymer Project Authors. All rights reserved. + * This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt + * The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt + * The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt + * Code distributed by Google as part of the polymer project is also + * subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt + */ + +.red { + background-color: red; +} + +.overridden { + background-color: green; +} \ No newline at end of file diff --git a/tests/HTMLImports/html/imports/sheet2.css b/tests/HTMLImports/html/imports/sheet2.css new file mode 100644 index 0000000..5585347 --- /dev/null +++ b/tests/HTMLImports/html/imports/sheet2.css @@ -0,0 +1,13 @@ +/* + * @license + * Copyright (c) 2014 The Polymer Project Authors. All rights reserved. + * This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt + * The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt + * The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt + * Code distributed by Google as part of the polymer project is also + * subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt + */ + +.blue { + background-color: blue; +} \ No newline at end of file diff --git a/tests/HTMLImports/html/imports/sheet3.css b/tests/HTMLImports/html/imports/sheet3.css new file mode 100644 index 0000000..663de4e --- /dev/null +++ b/tests/HTMLImports/html/imports/sheet3.css @@ -0,0 +1,13 @@ +/* + * @license + * Copyright (c) 2014 The Polymer Project Authors. All rights reserved. + * This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt + * The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt + * The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt + * Code distributed by Google as part of the polymer project is also + * subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt + */ + +.orange { + background-color: orange; +} \ No newline at end of file diff --git a/tests/HTMLImports/html/imports/style-elements-import.html b/tests/HTMLImports/html/imports/style-elements-import.html new file mode 100644 index 0000000..575fea3 --- /dev/null +++ b/tests/HTMLImports/html/imports/style-elements-import.html @@ -0,0 +1,19 @@ + + + \ No newline at end of file diff --git a/tests/HTMLImports/html/imports/style-links-import.html b/tests/HTMLImports/html/imports/style-links-import.html new file mode 100644 index 0000000..fee70a6 --- /dev/null +++ b/tests/HTMLImports/html/imports/style-links-import.html @@ -0,0 +1,12 @@ + + + + diff --git a/tests/HTMLImports/html/imports/style-paths-import.html b/tests/HTMLImports/html/imports/style-paths-import.html new file mode 100644 index 0000000..fb09c84 --- /dev/null +++ b/tests/HTMLImports/html/imports/style-paths-import.html @@ -0,0 +1,25 @@ + + + + \ No newline at end of file diff --git a/tests/HTMLImports/html/imports/styles.css b/tests/HTMLImports/html/imports/styles.css new file mode 100644 index 0000000..48b1fd6 --- /dev/null +++ b/tests/HTMLImports/html/imports/styles.css @@ -0,0 +1,21 @@ +/* + * @license + * Copyright (c) 2014 The Polymer Project Authors. All rights reserved. + * This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt + * The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt + * The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt + * Code distributed by Google as part of the polymer project is also + * subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt + */ + +/* + Document : styles + Created on : Apr 9, 2013, 5:55:32 PM + Author : sjmiles + Description: + Purpose of the stylesheet follows. +*/ +root { + display: block; +} + diff --git a/tests/HTMLImports/html/imports/template-import.html b/tests/HTMLImports/html/imports/template-import.html new file mode 100644 index 0000000..80181cd --- /dev/null +++ b/tests/HTMLImports/html/imports/template-import.html @@ -0,0 +1,14 @@ + + \ No newline at end of file diff --git a/tests/HTMLImports/html/load-404.html b/tests/HTMLImports/html/load-404.html new file mode 100644 index 0000000..f306fde --- /dev/null +++ b/tests/HTMLImports/html/load-404.html @@ -0,0 +1,51 @@ + + + + + load ready 404 test + + + + + + + + + + + + + diff --git a/tests/HTMLImports/html/load-loop.html b/tests/HTMLImports/html/load-loop.html new file mode 100644 index 0000000..126c95c --- /dev/null +++ b/tests/HTMLImports/html/load-loop.html @@ -0,0 +1,27 @@ + + + + + load loop Test + + + + + + + + + diff --git a/tests/HTMLImports/html/load.html b/tests/HTMLImports/html/load.html new file mode 100644 index 0000000..63fe490 --- /dev/null +++ b/tests/HTMLImports/html/load.html @@ -0,0 +1,55 @@ + + + + + load event Test + + + + + + + + + +
Test
+
Test
+
+ + + diff --git a/tests/HTMLImports/html/parser.html b/tests/HTMLImports/html/parser.html new file mode 100644 index 0000000..3e81c03 --- /dev/null +++ b/tests/HTMLImports/html/parser.html @@ -0,0 +1,29 @@ + + + + + parser Test + + + + + + + + + diff --git a/tests/HTMLImports/html/redirect/imports/load.html b/tests/HTMLImports/html/redirect/imports/load.html new file mode 100644 index 0000000..1d0ac0a --- /dev/null +++ b/tests/HTMLImports/html/redirect/imports/load.html @@ -0,0 +1,12 @@ + + \ No newline at end of file diff --git a/tests/HTMLImports/html/redirect/imports/redirect/googley.png b/tests/HTMLImports/html/redirect/imports/redirect/googley.png new file mode 100644 index 0000000..1974f31 Binary files /dev/null and b/tests/HTMLImports/html/redirect/imports/redirect/googley.png differ diff --git a/tests/HTMLImports/html/redirect/imports/redirect/load.html b/tests/HTMLImports/html/redirect/imports/redirect/load.html new file mode 100644 index 0000000..6c2ac5b --- /dev/null +++ b/tests/HTMLImports/html/redirect/imports/redirect/load.html @@ -0,0 +1,19 @@ + + + diff --git a/tests/HTMLImports/html/redirect/load-redirect.html b/tests/HTMLImports/html/redirect/load-redirect.html new file mode 100644 index 0000000..2e163c7 --- /dev/null +++ b/tests/HTMLImports/html/redirect/load-redirect.html @@ -0,0 +1,36 @@ + + + + + load redirect Test + + + + + + +
+ + + diff --git a/tests/HTMLImports/html/style-links.html b/tests/HTMLImports/html/style-links.html new file mode 100644 index 0000000..d2119d6 --- /dev/null +++ b/tests/HTMLImports/html/style-links.html @@ -0,0 +1,69 @@ + + + + + Style Links Test + + + + + + + + + +
red?
+
white?
+
orange?
+ +
black?
+ +
red?
+
black?
+ + + + + diff --git a/tests/HTMLImports/html/style-paths.html b/tests/HTMLImports/html/style-paths.html new file mode 100644 index 0000000..afc5d93 --- /dev/null +++ b/tests/HTMLImports/html/style-paths.html @@ -0,0 +1,54 @@ + + + + + + HTMLImports Path Tests + + + + + + +
red
+
+
Source Code Pro
+ + + diff --git a/tests/HTMLImports/html/template-script.html b/tests/HTMLImports/html/template-script.html new file mode 100644 index 0000000..5fa9aa0 --- /dev/null +++ b/tests/HTMLImports/html/template-script.html @@ -0,0 +1,32 @@ + + + + + template script test + + + + + + + + + diff --git a/tests/HTMLImports/runner.html b/tests/HTMLImports/runner.html new file mode 100644 index 0000000..a459fc6 --- /dev/null +++ b/tests/HTMLImports/runner.html @@ -0,0 +1,28 @@ + + +HTMLImports Tests + + + + + + + + + + + +
+ + + + + diff --git a/tests/HTMLImports/tests.js b/tests/HTMLImports/tests.js new file mode 100644 index 0000000..3c6eb13 --- /dev/null +++ b/tests/HTMLImports/tests.js @@ -0,0 +1,30 @@ +/* + * Copyright 2013 The Polymer Authors. All rights reserved. + * Use of this source code is governed by a BSD-style + * license that can be found in the LICENSE file. + */ +(function() { + +htmlSuite('HTMLImports', function() { + htmlTest('html/HTMLImports.html'); + htmlTest('html/parser.html'); + htmlTest('html/style-links.html'); + htmlTest('html/style-paths.html'); + htmlTest('html/load.html'); + htmlTest('html/load-404.html'); + htmlTest('html/load-loop.html'); + htmlTest('html/base/load-base.html'); + htmlTest('html/currentScript.html'); + htmlTest('html/dedupe.html'); + htmlTest('html/dynamic.html'); + htmlTest('html/csp.html'); + htmlTest('html/encoding.html'); + htmlTest('html/HTMLImportsLoaded-native.html'); + // NOTE: The MO polyfill does not function on disconnected documents + // like html imports so dynamic elements in imports are not supported. + if (!navigator.userAgent.match(/MSIE 10/)) { + htmlTest('html/dynamic-elements.html'); + } +}); + +})(); \ No newline at end of file diff --git a/tests/MutationObservers/attributes.js b/tests/MutationObservers/attributes.js new file mode 100644 index 0000000..73d697b --- /dev/null +++ b/tests/MutationObservers/attributes.js @@ -0,0 +1,276 @@ +/* + * Copyright 2012 The Polymer Authors. All rights reserved. + * Use of this source code is governed by a BSD-style + * license that can be found in the LICENSE file. + */ + +suite('JsMutationObserver attributes', function() { + + test('attr', function() { + var div = document.createElement('div'); + var observer = new JsMutationObserver(function() {}); + observer.observe(div, { + attributes: true + }); + div.setAttribute('a', 'A'); + div.setAttribute('a', 'B'); + + var records = observer.takeRecords(); + assert.strictEqual(records.length, 2); + + expectRecord(records[0], { + type: 'attributes', + target: div, + attributeName: 'a', + attributeNamespace: null + }); + expectRecord(records[1], { + type: 'attributes', + target: div, + attributeName: 'a', + attributeNamespace: null + }); + }); + + test('attr with oldValue', function() { + var div = document.createElement('div'); + var observer = new JsMutationObserver(function() {}); + observer.observe(div, { + attributes: true, + attributeOldValue: true + }); + div.setAttribute('a', 'A'); + div.setAttribute('a', 'B'); + + var records = observer.takeRecords(); + assert.strictEqual(records.length, 2); + + expectRecord(records[0], { + type: 'attributes', + target: div, + attributeName: 'a', + attributeNamespace: null, + oldValue: null + }); + expectRecord(records[1], { + type: 'attributes', + target: div, + attributeName: 'a', + attributeNamespace: null, + oldValue: 'A' + }); + }); + + test('attr change in subtree should not genereate a record', function() { + var div = document.createElement('div'); + var child = div.appendChild(document.createElement('div')); + + var observer = new JsMutationObserver(function() {}); + observer.observe(div, { + attributes: true + }); + child.setAttribute('a', 'A'); + child.setAttribute('a', 'B'); + + var records = observer.takeRecords(); + assert.strictEqual(records.length, 0); + }); + + test('attr change, subtree', function() { + var div = document.createElement('div'); + var child = div.appendChild(document.createElement('div')); + + var observer = new JsMutationObserver(function() {}); + observer.observe(div, { + attributes: true, + subtree: true + }); + child.setAttribute('a', 'A'); + child.setAttribute('a', 'B'); + + var records = observer.takeRecords(); + assert.strictEqual(records.length, 2); + + expectRecord(records[0], { + type: 'attributes', + target: child, + attributeName: 'a' + }); + expectRecord(records[1], { + type: 'attributes', + target: child, + attributeName: 'a' + }); + }); + + + test('multiple observers on same target', function() { + var div = document.createElement('div'); + var observer1 = new JsMutationObserver(function() {}); + observer1.observe(div, { + attributes: true, + attributeOldValue: true + }); + var observer2 = new JsMutationObserver(function() {}); + observer2.observe(div, { + attributes: true, + attributeFilter: ['b'] + }); + + div.setAttribute('a', 'A'); + div.setAttribute('a', 'A2'); + div.setAttribute('b', 'B'); + + var records = observer1.takeRecords(); + assert.strictEqual(records.length, 3); + + expectRecord(records[0], { + type: 'attributes', + target: div, + attributeName: 'a' + }); + expectRecord(records[1], { + type: 'attributes', + target: div, + attributeName: 'a', + oldValue: 'A' + }); + expectRecord(records[2], { + type: 'attributes', + target: div, + attributeName: 'b' + }); + + records = observer2.takeRecords(); + assert.strictEqual(records.length, 1); + + expectRecord(records[0], { + type: 'attributes', + target: div, + attributeName: 'b' + }); + }); + + test('observer observes on different target', function() { + var div = document.createElement('div'); + var child = div.appendChild(document.createElement('div')); + + var observer = new JsMutationObserver(function() {}); + observer.observe(child, { + attributes: true + }); + observer.observe(div, { + attributes: true, + subtree: true, + attributeOldValue: true + }); + + child.setAttribute('a', 'A'); + child.setAttribute('a', 'A2'); + child.setAttribute('b', 'B'); + + var records = observer.takeRecords(); + assert.strictEqual(records.length, 3); + + expectRecord(records[0], { + type: 'attributes', + target: child, + attributeName: 'a' + }); + expectRecord(records[1], { + type: 'attributes', + target: child, + attributeName: 'a', + oldValue: 'A' + }); + expectRecord(records[2], { + type: 'attributes', + target: child, + attributeName: 'b' + }); + }); + + test('observing on the same node should update the options', function() { + var div = document.createElement('div'); + var observer = new JsMutationObserver(function() {}); + observer.observe(div, { + attributes: true, + attributeFilter: ['a'] + }); + observer.observe(div, { + attributes: true, + attributeFilter: ['b'] + }); + + div.setAttribute('a', 'A'); + div.setAttribute('b', 'B'); + + var records = observer.takeRecords(); + assert.strictEqual(records.length, 1); + + expectRecord(records[0], { + type: 'attributes', + target: div, + attributeName: 'b' + }); + }); + + test('disconnect should stop all events and empty the records', function() { + var div = document.createElement('div'); + var observer = new JsMutationObserver(function() {}); + observer.observe(div, { + attributes: true, + }); + + div.setAttribute('a', 'A'); + + observer.disconnect(); + var records = observer.takeRecords(); + assert.strictEqual(records.length, 0); + + div.setAttribute('b', 'B'); + + records = observer.takeRecords(); + assert.strictEqual(records.length, 0); + }); + + test('disconnect should not affect other observers', function() { + var div = document.createElement('div'); + var observer1 = new JsMutationObserver(function() {}); + observer1.observe(div, { + attributes: true, + }); + var observer2 = new JsMutationObserver(function() {}); + observer2.observe(div, { + attributes: true, + }); + + div.setAttribute('a', 'A'); + + observer1.disconnect(); + var records1 = observer1.takeRecords(); + assert.strictEqual(records1.length, 0); + + var records2 = observer2.takeRecords(); + assert.strictEqual(records2.length, 1); + expectRecord(records2[0], { + type: 'attributes', + target: div, + attributeName: 'a' + }); + + div.setAttribute('b', 'B'); + + records1 = observer1.takeRecords(); + assert.strictEqual(records1.length, 0); + + records2 = observer2.takeRecords(); + assert.strictEqual(records2.length, 1); + expectRecord(records2[0], { + type: 'attributes', + target: div, + attributeName: 'b' + }); + }); + +}); \ No newline at end of file diff --git a/tests/MutationObservers/callback.js b/tests/MutationObservers/callback.js new file mode 100644 index 0000000..b3083c7 --- /dev/null +++ b/tests/MutationObservers/callback.js @@ -0,0 +1,72 @@ +/* + * Copyright 2012 The Polymer Authors. All rights reserved. + * Use of this source code is governed by a BSD-style + * license that can be found in the LICENSE file. + */ + +suite('JsMutationObserver callback', function() { + + test('One observer, two attribute changes', function(cont) { + var div = document.createElement('div'); + var observer = new JsMutationObserver(function(records) { + assert.strictEqual(records.length, 2); + + expectRecord(records[0], { + type: 'attributes', + target: div, + attributeName: 'a', + attributeNamespace: null + }); + expectRecord(records[1], { + type: 'attributes', + target: div, + attributeName: 'a', + attributeNamespace: null + }); + + cont(); + }); + + observer.observe(div, { + attributes: true + }); + + div.setAttribute('a', 'A'); + div.setAttribute('a', 'B'); + }); + + test('nested changes', function(cont) { + var div = document.createElement('div'); + var i = 0; + var observer = new JsMutationObserver(function(records) { + assert.strictEqual(records.length, 1); + + if (i === 0) { + expectRecord(records[0], { + type: 'attributes', + target: div, + attributeName: 'a', + attributeNamespace: null + }); + div.setAttribute('b', 'B'); + i++; + } else { + expectRecord(records[0], { + type: 'attributes', + target: div, + attributeName: 'b', + attributeNamespace: null + }); + + cont(); + } + }); + + observer.observe(div, { + attributes: true + }); + + div.setAttribute('a', 'A'); + }); + +}); \ No newline at end of file diff --git a/tests/MutationObservers/characterData.js b/tests/MutationObservers/characterData.js new file mode 100644 index 0000000..498dc05 --- /dev/null +++ b/tests/MutationObservers/characterData.js @@ -0,0 +1,106 @@ +/* + * Copyright 2012 The Polymer Authors. All rights reserved. + * Use of this source code is governed by a BSD-style + * license that can be found in the LICENSE file. + */ + +suite('JsMutationObserver characterData', function() { + + var testDiv; + + setup(function() { + testDiv = document.body.appendChild(document.createElement('div')); + }); + + teardown(function() { + document.body.removeChild(testDiv); + }); + + test('characterData', function() { + var text = document.createTextNode('abc'); + var observer = new JsMutationObserver(function() {}); + observer.observe(text, { + characterData: true + }); + text.data = 'def'; + text.data = 'ghi'; + + var records = observer.takeRecords(); + assert.strictEqual(records.length, 2); + + expectRecord(records[0], { + type: 'characterData', + target: text + }); + expectRecord(records[1], { + type: 'characterData', + target: text + }); + }); + + test('characterData with old value', function() { + var text = testDiv.appendChild(document.createTextNode('abc')); + var observer = new JsMutationObserver(function() {}); + observer.observe(text, { + characterData: true, + characterDataOldValue: true + }); + text.data = 'def'; + text.data = 'ghi'; + + var records = observer.takeRecords(); + assert.strictEqual(records.length, 2); + + expectRecord(records[0], { + type: 'characterData', + target: text, + oldValue: 'abc' + }); + expectRecord(records[1], { + type: 'characterData', + target: text, + oldValue: 'def' + }); + }); + + test('characterData change in subtree should not generate a record', + function() { + var div = document.createElement('div'); + var text = div.appendChild(document.createTextNode('abc')); + var observer = new JsMutationObserver(function() {}); + observer.observe(div, { + characterData: true + }); + text.data = 'def'; + text.data = 'ghi'; + + var records = observer.takeRecords(); + assert.strictEqual(records.length, 0); + }); + + test('characterData change in subtree', + function() { + var div = document.createElement('div'); + var text = div.appendChild(document.createTextNode('abc')); + var observer = new JsMutationObserver(function() {}); + observer.observe(div, { + characterData: true, + subtree: true + }); + text.data = 'def'; + text.data = 'ghi'; + + var records = observer.takeRecords(); + assert.strictEqual(records.length, 2); + + expectRecord(records[0], { + type: 'characterData', + target: text + }); + expectRecord(records[1], { + type: 'characterData', + target: text + }); + }); + +}); \ No newline at end of file diff --git a/tests/MutationObservers/childList.js b/tests/MutationObservers/childList.js new file mode 100644 index 0000000..8ff4e4f --- /dev/null +++ b/tests/MutationObservers/childList.js @@ -0,0 +1,377 @@ +/* + * Copyright 2012 The Polymer Authors. All rights reserved. + * Use of this source code is governed by a BSD-style + * license that can be found in the LICENSE file. + */ + +suite('JsMutationObserver childList', function() { + + var testDiv; + + teardown(function() { + document.body.removeChild(testDiv); + }); + + var addedNodes, removedNodes; + + setup(function() { + testDiv = document.body.appendChild(document.createElement('div')); + addedNodes = []; + removedNodes = []; + }); + + function mergeRecords(records) { + records.forEach(function(record) { + if (record.addedNodes) + addedNodes.push.apply(addedNodes, record.addedNodes); + if (record.removedNodes) + removedNodes.push.apply(removedNodes, record.removedNodes); + }); + } + + function assertAll(records, expectedProperties) { + records.forEach(function(record) { + for (var propertyName in expectedProperties) { + assert.strictEqual(record[propertyName], expectedProperties[propertyName]); + } + }); + } + + test('appendChild', function() { + var div = document.createElement('div'); + var observer = new JsMutationObserver(function() {}); + observer.observe(div, { + childList: true + }); + var a = document.createElement('a'); + var b = document.createElement('b'); + + div.appendChild(a); + div.appendChild(b); + + var records = observer.takeRecords(); + assert.strictEqual(records.length, 2); + + expectRecord(records[0], { + type: 'childList', + target: div, + addedNodes: [a] + }); + + expectRecord(records[1], { + type: 'childList', + target: div, + addedNodes: [b], + previousSibling: a + }); + }); + + test('insertBefore', function() { + var div = document.createElement('div'); + var a = document.createElement('a'); + var b = document.createElement('b'); + var c = document.createElement('c'); + div.appendChild(a); + + var observer = new JsMutationObserver(function() {}); + observer.observe(div, { + childList: true + }); + + div.insertBefore(b, a); + div.insertBefore(c, a); + + var records = observer.takeRecords(); + assert.strictEqual(records.length, 2); + + expectRecord(records[0], { + type: 'childList', + target: div, + addedNodes: [b], + nextSibling: a + }); + + expectRecord(records[1], { + type: 'childList', + target: div, + addedNodes: [c], + nextSibling: a, + previousSibling: b + }); + }); + + + test('removeChild', function() { + var div = testDiv.appendChild(document.createElement('div')); + var a = div.appendChild(document.createElement('a')); + var b = div.appendChild(document.createElement('b')); + var c = div.appendChild(document.createElement('c')); + + var observer = new JsMutationObserver(function() {}); + observer.observe(div, { + childList: true + }); + + div.removeChild(b); + div.removeChild(a); + + var records = observer.takeRecords(); + assert.strictEqual(records.length, 2); + + expectRecord(records[0], { + type: 'childList', + target: div, + removedNodes: [b], + nextSibling: c, + previousSibling: a + }); + + expectRecord(records[1], { + type: 'childList', + target: div, + removedNodes: [a], + nextSibling: c + }); + }); + + test('Direct children', function() { + var div = testDiv.appendChild(document.createElement('div')); + var observer = new JsMutationObserver(function() {}); + observer.observe(div, { + childList: true + }); + var a = document.createElement('a'); + var b = document.createElement('b'); + + div.appendChild(a); + div.insertBefore(b, a); + div.removeChild(b); + + var records = observer.takeRecords(); + assert.strictEqual(records.length, 3); + + expectRecord(records[0], { + type: 'childList', + target: div, + addedNodes: [a] + }); + + expectRecord(records[1], { + type: 'childList', + target: div, + nextSibling: a, + addedNodes: [b] + }); + + expectRecord(records[2], { + type: 'childList', + target: div, + nextSibling: a, + removedNodes: [b] + }); + }); + + test('subtree', function() { + var div = document.createElement('div'); + var child = div.appendChild(document.createElement('div')); + var observer = new JsMutationObserver(function() {}); + observer.observe(child, { + childList: true + }); + var a = document.createTextNode('a'); + var b = document.createTextNode('b'); + + child.appendChild(a); + child.insertBefore(b, a); + child.removeChild(b); + + var records = observer.takeRecords(); + assert.strictEqual(records.length, 3); + + expectRecord(records[0], { + type: 'childList', + target: child, + addedNodes: [a] + }); + + expectRecord(records[1], { + type: 'childList', + target: child, + nextSibling: a, + addedNodes: [b] + }); + + expectRecord(records[2], { + type: 'childList', + target: child, + nextSibling: a, + removedNodes: [b] + }); + }); + + test('both direct and subtree', function() { + var div = document.createElement('div'); + var child = div.appendChild(document.createElement('div')); + var observer = new JsMutationObserver(function() {}); + observer.observe(div, { + childList: true, + subtree: true + }); + observer.observe(child, { + childList: true + }); + + var a = document.createTextNode('a'); + var b = document.createTextNode('b'); + + child.appendChild(a); + div.appendChild(b); + + var records = observer.takeRecords(); + assert.strictEqual(records.length, 2); + + expectRecord(records[0], { + type: 'childList', + target: child, + addedNodes: [a] + }); + + expectRecord(records[1], { + type: 'childList', + target: div, + addedNodes: [b], + previousSibling: child + }); + }); + + test('Append multiple at once at the end', function() { + var div = testDiv.appendChild(document.createElement('div')); + var a = div.appendChild(document.createTextNode('a')); + + var observer = new JsMutationObserver(function() {}); + observer.observe(div, { + childList: true + }); + + var df = document.createDocumentFragment(); + var b = df.appendChild(document.createTextNode('b')); + var c = df.appendChild(document.createTextNode('c')); + var d = df.appendChild(document.createTextNode('d')); + + div.appendChild(df); + + var records = observer.takeRecords(); + mergeRecords(records); + + assertArrayEqual(addedNodes, [b, c, d]); + assertArrayEqual(removedNodes, []); + assertAll(records, { + type: 'childList', + target: div + }); + }); + + test('Append multiple at once at the front', function() { + var div = testDiv.appendChild(document.createElement('div')); + var a = div.appendChild(document.createTextNode('a')); + + var observer = new JsMutationObserver(function() {}); + observer.observe(div, { + childList: true + }); + + var df = document.createDocumentFragment(); + var b = df.appendChild(document.createTextNode('b')); + var c = df.appendChild(document.createTextNode('c')); + var d = df.appendChild(document.createTextNode('d')); + + div.insertBefore(df, a); + + var records = observer.takeRecords(); + mergeRecords(records); + + assertArrayEqual(addedNodes, [b, c, d]); + assertArrayEqual(removedNodes, []); + assertAll(records, { + type: 'childList', + target: div + }); + }); + + test('Append multiple at once in the middle', function() { + var div = testDiv.appendChild(document.createElement('div')); + var a = div.appendChild(document.createTextNode('a')); + var b = div.appendChild(document.createTextNode('b')); + + var observer = new JsMutationObserver(function() {}); + observer.observe(div, { + childList: true + }); + + var df = document.createDocumentFragment(); + var c = df.appendChild(document.createTextNode('c')); + var d = df.appendChild(document.createTextNode('d')); + + div.insertBefore(df, b); + + var records = observer.takeRecords(); + mergeRecords(records); + + assertArrayEqual(addedNodes, [c, d]); + assertArrayEqual(removedNodes, []); + assertAll(records, { + type: 'childList', + target: div + }); + }); + + test('Remove all children', function() { + var div = testDiv.appendChild(document.createElement('div')); + var a = div.appendChild(document.createTextNode('a')); + var b = div.appendChild(document.createTextNode('b')); + var c = div.appendChild(document.createTextNode('c')); + + var observer = new JsMutationObserver(function() {}); + observer.observe(div, { + childList: true + }); + + div.innerHTML = ''; + + var records = observer.takeRecords(); + mergeRecords(records); + + assertArrayEqual(addedNodes, []); + assertArrayEqual(removedNodes, [a, b, c]); + assertAll(records, { + type: 'childList', + target: div + }); + }); + + test('Replace all children using innerHTML', function() { + var div = testDiv.appendChild(document.createElement('div')); + var a = div.appendChild(document.createTextNode('a')); + var b = div.appendChild(document.createTextNode('b')); + + var observer = new JsMutationObserver(function() {}); + observer.observe(div, { + childList: true + }); + + div.innerHTML = ''; + var c = div.firstChild; + var d = div.lastChild; + + var records = observer.takeRecords(); + mergeRecords(records); + + assertArrayEqual(addedNodes, [c, d]); + assertArrayEqual(removedNodes, [a, b]); + assertAll(records, { + type: 'childList', + target: div + }); + }); + +}); \ No newline at end of file diff --git a/tests/MutationObservers/mixed.js b/tests/MutationObservers/mixed.js new file mode 100644 index 0000000..eae6a60 --- /dev/null +++ b/tests/MutationObservers/mixed.js @@ -0,0 +1,36 @@ +/* + * Copyright 2012 The Polymer Authors. All rights reserved. + * Use of this source code is governed by a BSD-style + * license that can be found in the LICENSE file. + */ + +suite('JsMutationObserver mixed types', function() { + + test('attr and characterData', function() { + var div = document.createElement('div'); + var text = div.appendChild(document.createTextNode('text')); + var observer = new JsMutationObserver(function() {}); + observer.observe(div, { + attributes: true, + characterData: true, + subtree: true + }); + div.setAttribute('a', 'A'); + div.firstChild.data = 'changed'; + + var records = observer.takeRecords(); + assert.strictEqual(records.length, 2); + + expectRecord(records[0], { + type: 'attributes', + target: div, + attributeName: 'a', + attributeNamespace: null + }); + expectRecord(records[1], { + type: 'characterData', + target: div.firstChild + }); + }); + +}); \ No newline at end of file diff --git a/tests/MutationObservers/runner.html b/tests/MutationObservers/runner.html new file mode 100644 index 0000000..983f3e7 --- /dev/null +++ b/tests/MutationObservers/runner.html @@ -0,0 +1,85 @@ + + + + + + + + + + + + + + + + + + + +
+ diff --git a/tests/MutationObservers/transient.js b/tests/MutationObservers/transient.js new file mode 100644 index 0000000..7dda83e --- /dev/null +++ b/tests/MutationObservers/transient.js @@ -0,0 +1,283 @@ +/* + * Copyright 2012 The Polymer Authors. All rights reserved. + * Use of this source code is governed by a BSD-style + * license that can be found in the LICENSE file. + */ + +suite('JsMutationObserver transient', function() { + + var testDiv; + + setup(function() { + testDiv = document.body.appendChild(document.createElement('div')); + }); + + teardown(function() { + document.body.removeChild(testDiv); + }); + + test('attr', function() { + var div = testDiv.appendChild(document.createElement('div')); + var child = div.appendChild(document.createElement('div')); + var observer = new JsMutationObserver(function() {}); + observer.observe(div, { + attributes: true, + subtree: true + }); + div.removeChild(child); + child.setAttribute('a', 'A'); + + var records = observer.takeRecords(); + assert.strictEqual(records.length, 1); + + expectRecord(records[0], { + type: 'attributes', + target: child, + attributeName: 'a', + attributeNamespace: null + }); + + child.setAttribute('b', 'B'); + + records = observer.takeRecords(); + assert.strictEqual(records.length, 1); + + expectRecord(records[0], { + type: 'attributes', + target: child, + attributeName: 'b', + attributeNamespace: null + }); + }); + + test('attr callback', function(cont) { + var div = testDiv.appendChild(document.createElement('div')); + var child = div.appendChild(document.createElement('div')); + var i = 0; + var observer = new JsMutationObserver(function(records) { + i++; + if (i > 1) + expect().fail(); + + assert.strictEqual(records.length, 1); + + expectRecord(records[0], { + type: 'attributes', + target: child, + attributeName: 'a', + attributeNamespace: null + }); + + // The transient observers are removed before the callback is called. + child.setAttribute('b', 'B'); + records = observer.takeRecords(); + assert.strictEqual(records.length, 0); + + cont(); + }); + + observer.observe(div, { + attributes: true, + subtree: true + }); + + div.removeChild(child); + child.setAttribute('a', 'A'); + }); + + test('attr, make sure transient gets removed', function(cont) { + var div = testDiv.appendChild(document.createElement('div')); + var child = div.appendChild(document.createElement('div')); + var i = 0; + var observer = new JsMutationObserver(function(records) { + i++; + if (i > 1) + expect().fail(); + + assert.strictEqual(records.length, 1); + + expectRecord(records[0], { + type: 'attributes', + target: child, + attributeName: 'a', + attributeNamespace: null + }); + + step2(); + }); + + observer.observe(div, { + attributes: true, + subtree: true + }); + + div.removeChild(child); + child.setAttribute('a', 'A'); + + function step2() { + var div2 = document.createElement('div'); + var observer2 = new JsMutationObserver(function(records) { + i++; + if (i > 2) + expect().fail(); + + assert.strictEqual(records.length, 1); + + expectRecord(records[0], { + type: 'attributes', + target: child, + attributeName: 'b', + attributeNamespace: null + }); + + cont(); + }); + + observer2.observe(div2, { + attributes: true, + subtree: true, + }); + + div2.appendChild(child); + child.setAttribute('b', 'B'); + } + }); + + test('characterData', function() { + var div = document.createElement('div'); + var child = div.appendChild(document.createTextNode('text')); + var observer = new JsMutationObserver(function() {}); + observer.observe(div, { + characterData: true, + subtree: true + }); + div.removeChild(child); + child.data = 'changed'; + + var records = observer.takeRecords(); + assert.strictEqual(records.length, 1); + + expectRecord(records[0], { + type: 'characterData', + target: child + }); + + child.data += ' again'; + + records = observer.takeRecords(); + assert.strictEqual(records.length, 1); + + expectRecord(records[0], { + type: 'characterData', + target: child + }); + }); + + test('characterData callback', function(cont) { + var div = document.createElement('div'); + var child = div.appendChild(document.createTextNode('text')); + var i = 0; + var observer = new JsMutationObserver(function(records) { + i++; + if (i > 1) + expect().fail(); + + assert.strictEqual(records.length, 1); + + expectRecord(records[0], { + type: 'characterData', + target: child + }); + + // The transient observers are removed before the callback is called. + child.data += ' again'; + records = observer.takeRecords(); + assert.strictEqual(records.length, 0); + + cont(); + }); + observer.observe(div, { + characterData: true, + subtree: true + }); + div.removeChild(child); + child.data = 'changed'; + }); + + test('childList', function() { + var div = testDiv.appendChild(document.createElement('div')); + var child = div.appendChild(document.createElement('div')); + var observer = new JsMutationObserver(function() {}); + observer.observe(div, { + childList: true, + subtree: true + }); + div.removeChild(child); + var grandChild = child.appendChild(document.createElement('span')); + + var records = observer.takeRecords(); + assert.strictEqual(records.length, 2); + + expectRecord(records[0], { + type: 'childList', + target: div, + removedNodes: [child] + }); + + expectRecord(records[1], { + type: 'childList', + target: child, + addedNodes: [grandChild] + }); + + child.removeChild(grandChild); + + records = observer.takeRecords(); + assert.strictEqual(records.length, 1); + + expectRecord(records[0], { + type: 'childList', + target: child, + removedNodes: [grandChild] + }); + }); + + test('childList callback', function(cont) { + var div = testDiv.appendChild(document.createElement('div')); + var child = div.appendChild(document.createElement('div')); + var i = 0; + var observer = new JsMutationObserver(function(records) { + i++; + if (i > 1) + expect().fail(); + + assert.strictEqual(records.length, 2); + + expectRecord(records[0], { + type: 'childList', + target: div, + removedNodes: [child] + }); + + expectRecord(records[1], { + type: 'childList', + target: child, + addedNodes: [grandChild] + }); + + // The transient observers are removed before the callback is called. + child.removeChild(grandChild); + + records = observer.takeRecords(); + assert.strictEqual(records.length, 0); + + cont(); + }); + observer.observe(div, { + childList: true, + subtree: true + }); + div.removeChild(child); + var grandChild = child.appendChild(document.createElement('span')); + }); +}); diff --git a/tests/ShadowCss/html/assets/google.png b/tests/ShadowCss/html/assets/google.png new file mode 100644 index 0000000..1974f31 Binary files /dev/null and b/tests/ShadowCss/html/assets/google.png differ diff --git a/tests/ShadowCss/html/before-content.html b/tests/ShadowCss/html/before-content.html new file mode 100644 index 0000000..9f6fa91 --- /dev/null +++ b/tests/ShadowCss/html/before-content.html @@ -0,0 +1,73 @@ + + + + + content in :before pseudo-class + + + + + + + + + + + + + + + diff --git a/tests/ShadowCss/html/colon-host.html b/tests/ShadowCss/html/colon-host.html new file mode 100644 index 0000000..5970a68 --- /dev/null +++ b/tests/ShadowCss/html/colon-host.html @@ -0,0 +1,280 @@ + + + + + + Using :host styling + + + + + + + + + + + + + + + + + + + + + + + +

Expected: red background

+ + +

Expected: red background

+ + +

Expected: green background

+ + +

Expected: black background

+ + +

Expected: black background

+ + +

Expected: red background with white text

+ + +

Expected: red background with white text

+ + +

Expected: red background with black text and orange border

+ + +

Expected: red background with black text and orange border and 20px padding

+ + +

Expected: 20px padding, background green

+ + +

Expected: 20px padding, background green

+
+ +
+

Expected: 20px padding

+
+ +
+ +

Expected: blue background

+ Test Link +
+

Expected: blue background

+
+ +
+
+ + + + diff --git a/tests/ShadowCss/html/combinators-import.css b/tests/ShadowCss/html/combinators-import.css new file mode 100644 index 0000000..3a96f50 --- /dev/null +++ b/tests/ShadowCss/html/combinators-import.css @@ -0,0 +1,13 @@ +/* + * @license + * Copyright (c) 2014 The Polymer Project Authors. All rights reserved. + * This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt + * The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt + * The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt + * Code distributed by Google as part of the polymer project is also + * subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt + */ + +x-foo ^ .foo2 { + background: green; +} diff --git a/tests/ShadowCss/html/combinators-import.html b/tests/ShadowCss/html/combinators-import.html new file mode 100644 index 0000000..97da1d6 --- /dev/null +++ b/tests/ShadowCss/html/combinators-import.html @@ -0,0 +1,10 @@ + + \ No newline at end of file diff --git a/tests/ShadowCss/html/combinators-shadow-import.css b/tests/ShadowCss/html/combinators-shadow-import.css new file mode 100644 index 0000000..664edd6 --- /dev/null +++ b/tests/ShadowCss/html/combinators-shadow-import.css @@ -0,0 +1,13 @@ +/* + * @license + * Copyright (c) 2014 The Polymer Project Authors. All rights reserved. + * This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt + * The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt + * The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt + * Code distributed by Google as part of the polymer project is also + * subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt + */ + +x-foo::shadow .foo2 { + background: green; +} diff --git a/tests/ShadowCss/html/combinators-shadow-import.html b/tests/ShadowCss/html/combinators-shadow-import.html new file mode 100644 index 0000000..e769754 --- /dev/null +++ b/tests/ShadowCss/html/combinators-shadow-import.html @@ -0,0 +1,10 @@ + + \ No newline at end of file diff --git a/tests/ShadowCss/html/combinators-shadow.css b/tests/ShadowCss/html/combinators-shadow.css new file mode 100644 index 0000000..9a3202e --- /dev/null +++ b/tests/ShadowCss/html/combinators-shadow.css @@ -0,0 +1,13 @@ +/* + * @license + * Copyright (c) 2014 The Polymer Project Authors. All rights reserved. + * This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt + * The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt + * The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt + * Code distributed by Google as part of the polymer project is also + * subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt + */ + +body /deep/ x-zot::shadow .zot-inner { + background: blue; +} \ No newline at end of file diff --git a/tests/ShadowCss/html/combinators-shadow.html b/tests/ShadowCss/html/combinators-shadow.html new file mode 100644 index 0000000..63023bc --- /dev/null +++ b/tests/ShadowCss/html/combinators-shadow.html @@ -0,0 +1,113 @@ + + + + + + Using combinators styling + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/ShadowCss/html/combinators.css b/tests/ShadowCss/html/combinators.css new file mode 100644 index 0000000..e89c5b9 --- /dev/null +++ b/tests/ShadowCss/html/combinators.css @@ -0,0 +1,13 @@ +/* + * @license + * Copyright (c) 2014 The Polymer Project Authors. All rights reserved. + * This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt + * The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt + * The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt + * Code distributed by Google as part of the polymer project is also + * subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt + */ + +body ^^ x-zot ^ .zot-inner { + background: blue; +} \ No newline at end of file diff --git a/tests/ShadowCss/html/combinators.html b/tests/ShadowCss/html/combinators.html new file mode 100644 index 0000000..a6f749e --- /dev/null +++ b/tests/ShadowCss/html/combinators.html @@ -0,0 +1,113 @@ + + + + + + Using combinators styling + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/ShadowCss/html/compressed.html b/tests/ShadowCss/html/compressed.html new file mode 100644 index 0000000..3366205 --- /dev/null +++ b/tests/ShadowCss/html/compressed.html @@ -0,0 +1,45 @@ + + + + + + Using compressed stylesheet + + + + + + + + + + + + + + + + + diff --git a/tests/ShadowCss/html/css-animation.html b/tests/ShadowCss/html/css-animation.html new file mode 100644 index 0000000..931b7a3 --- /dev/null +++ b/tests/ShadowCss/html/css-animation.html @@ -0,0 +1,74 @@ + + + + + polyfill rule + + + + + + + + +Animate! + + + + + + diff --git a/tests/ShadowCss/html/imports/sheet1.css b/tests/ShadowCss/html/imports/sheet1.css new file mode 100644 index 0000000..de04d15 --- /dev/null +++ b/tests/ShadowCss/html/imports/sheet1.css @@ -0,0 +1,13 @@ +/* + * @license + * Copyright (c) 2014 The Polymer Project Authors. All rights reserved. + * This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt + * The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt + * The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt + * Code distributed by Google as part of the polymer project is also + * subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt + */ + +.red { + background: red; +} \ No newline at end of file diff --git a/tests/ShadowCss/html/imports/sheet2.css b/tests/ShadowCss/html/imports/sheet2.css new file mode 100644 index 0000000..2df2088 --- /dev/null +++ b/tests/ShadowCss/html/imports/sheet2.css @@ -0,0 +1,13 @@ +/* + * @license + * Copyright (c) 2014 The Polymer Project Authors. All rights reserved. + * This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt + * The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt + * The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt + * Code distributed by Google as part of the polymer project is also + * subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt + */ + +.blue { + background: blue; +} \ No newline at end of file diff --git a/tests/ShadowCss/html/imports/style-import.html b/tests/ShadowCss/html/imports/style-import.html new file mode 100644 index 0000000..3cce469 --- /dev/null +++ b/tests/ShadowCss/html/imports/style-import.html @@ -0,0 +1,19 @@ + + + + + diff --git a/tests/ShadowCss/html/polyfill-directive.html b/tests/ShadowCss/html/polyfill-directive.html new file mode 100644 index 0000000..665043d --- /dev/null +++ b/tests/ShadowCss/html/polyfill-directive.html @@ -0,0 +1,142 @@ + + + + + polyfill directive + + + + + + + + +
Green?
+
Orange?
+
+ + +
Green?
+
Orange?
+
+ + +
Green?
+
Red?
+
+ + + + + + + + + + + + diff --git a/tests/ShadowCss/html/polyfill-rule.html b/tests/ShadowCss/html/polyfill-rule.html new file mode 100644 index 0000000..a316dfc --- /dev/null +++ b/tests/ShadowCss/html/polyfill-rule.html @@ -0,0 +1,77 @@ + + + + + polyfill rule + + + + + + + + +
red?
+
+
unscoped
+ + + + + + + diff --git a/tests/ShadowCss/html/pseudo-scoping-strict.html b/tests/ShadowCss/html/pseudo-scoping-strict.html new file mode 100644 index 0000000..5612f94 --- /dev/null +++ b/tests/ShadowCss/html/pseudo-scoping-strict.html @@ -0,0 +1,122 @@ + + + + + Psuedo scoped styling + + + + + + + + + + + +
10px
+ + + + + + + + + + + + diff --git a/tests/ShadowCss/html/pseudo-scoping.html b/tests/ShadowCss/html/pseudo-scoping.html new file mode 100644 index 0000000..a47c8a3 --- /dev/null +++ b/tests/ShadowCss/html/pseudo-scoping.html @@ -0,0 +1,102 @@ + + + + + Psuedo scoped styling + + + + + + + + + + +
10px
+ + + + + + + + + + diff --git a/tests/ShadowCss/html/register.js b/tests/ShadowCss/html/register.js new file mode 100644 index 0000000..1f7b21e --- /dev/null +++ b/tests/ShadowCss/html/register.js @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2014 The Polymer Project Authors. All rights reserved. + * This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt + * The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt + * The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt + * Code distributed by Google as part of the polymer project is also + * subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt + */ +(function(scope) { + + var extendsRegistry = {}; + + function register(name, extnds, proto, templates) { + extendsRegistry[name] = extnds; + var typeExtension = extnds && extnds.indexOf('-') < 0; + var names = calcExtendsNames(name); + if (window.ShadowDOMPolyfill) { + shim(templates, names); + } + + var config = { + prototype: Object.create(proto, { + createdCallback: { + value: function() { + for (var i=0, n; i < names.length; i++) { + n = names[i]; + var template = templateForName(n); + if (template) { + this.createShadowRoot().appendChild(document.importNode(template.content, true)); + } + } + } + } + }) + }; + if (typeExtension) { + config.extends = extnds; + } + var ctor = document.registerElement(name, config); + return ctor; + } + + function calcExtendsNames(name) { + var names = [], n = name; + while (n) { + names.push(n); + n = extendsRegistry[n]; + } + return names.reverse(); + } + + function templateForName(name) { + return document.querySelector('#' + name); + } + + function shim(templates, names) { + var n = names[names.length-1]; + var template = templateForName(n); + WebComponents.ShadowCSS.shimStyling(template ? template.content : null, n, extendsRegistry[n]); + } + + scope.register = register; + +})(window); + diff --git a/tests/ShadowCss/html/style-import.html b/tests/ShadowCss/html/style-import.html new file mode 100644 index 0000000..87a92db --- /dev/null +++ b/tests/ShadowCss/html/style-import.html @@ -0,0 +1,36 @@ + + + + + Imports style loading + + + + + + + +
red
+
blue
+
+ + + diff --git a/tests/ShadowCss/runner.html b/tests/ShadowCss/runner.html new file mode 100644 index 0000000..5d9eaed --- /dev/null +++ b/tests/ShadowCss/runner.html @@ -0,0 +1,30 @@ + + +ShadowCss Tests + + + + + + + + + + + +
+ + + + + diff --git a/tests/ShadowCss/tests.js b/tests/ShadowCss/tests.js new file mode 100644 index 0000000..b74f8c0 --- /dev/null +++ b/tests/ShadowCss/tests.js @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2014 The Polymer Project Authors. All rights reserved. + * This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt + * The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt + * The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt + * Code distributed by Google as part of the polymer project is also + * subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt + */ + +htmlSuite('ShadowCss', function() { + htmlTest('html/pseudo-scoping.html'); + htmlTest('html/pseudo-scoping.html?shadow'); + htmlTest('html/pseudo-scoping-strict.html'); + htmlTest('html/pseudo-scoping-strict.html?shadow'); + htmlTest('html/polyfill-directive.html'); + htmlTest('html/polyfill-rule.html'); + htmlTest('html/colon-host.html'); + htmlTest('html/colon-host.html?shadow'); + htmlTest('html/combinators.html?shadow'); + htmlTest('html/combinators-shadow.html'); + htmlTest('html/combinators-shadow.html?shadow'); + htmlTest('html/compressed.html'); + htmlTest('html/before-content.html'); + htmlTest('html/before-content.html?shadow'); + htmlTest('html/before-content.html'); + htmlTest('html/style-import.html'); + htmlTest('html/css-animation.html'); +}); diff --git a/tests/ShadowDOM/html/document-body-inner-html.html b/tests/ShadowDOM/html/document-body-inner-html.html new file mode 100644 index 0000000..3ffd0af --- /dev/null +++ b/tests/ShadowDOM/html/document-body-inner-html.html @@ -0,0 +1,31 @@ + + + + + + + diff --git a/tests/ShadowDOM/html/document-body-shadow-root.html b/tests/ShadowDOM/html/document-body-shadow-root.html new file mode 100644 index 0000000..0473ce9 --- /dev/null +++ b/tests/ShadowDOM/html/document-body-shadow-root.html @@ -0,0 +1,34 @@ + + + + + + diff --git a/tests/ShadowDOM/html/document-write.html b/tests/ShadowDOM/html/document-write.html new file mode 100644 index 0000000..dc138b8 --- /dev/null +++ b/tests/ShadowDOM/html/document-write.html @@ -0,0 +1,31 @@ + + + + + + + diff --git a/tests/ShadowDOM/html/head-then-body.html b/tests/ShadowDOM/html/head-then-body.html new file mode 100644 index 0000000..cce34d1 --- /dev/null +++ b/tests/ShadowDOM/html/head-then-body.html @@ -0,0 +1,38 @@ + + + + + + + + + + + diff --git a/tests/ShadowDOM/html/on-load-test.html b/tests/ShadowDOM/html/on-load-test.html new file mode 100644 index 0000000..4411083 --- /dev/null +++ b/tests/ShadowDOM/html/on-load-test.html @@ -0,0 +1,29 @@ + + + + + + + diff --git a/tests/ShadowDOM/html/on-unload-test.html b/tests/ShadowDOM/html/on-unload-test.html new file mode 100644 index 0000000..3c59458 --- /dev/null +++ b/tests/ShadowDOM/html/on-unload-test.html @@ -0,0 +1,44 @@ + + + + + + diff --git a/tests/ShadowDOM/js/ChildNodeInterface.js b/tests/ShadowDOM/js/ChildNodeInterface.js new file mode 100644 index 0000000..274f922 --- /dev/null +++ b/tests/ShadowDOM/js/ChildNodeInterface.js @@ -0,0 +1,66 @@ +/* + * Copyright 2013 The Polymer Authors. All rights reserved. + * Use of this source code is goverened by a BSD-style + * license that can be found in the LICENSE file. + */ + +suite('ChildNodeInterface', function() { + + function getTree() { + var tree = {}; + var div = tree.div = document.createElement('div'); + div.innerHTML = 'ace'; + var a = tree.a = div.firstChild; + var b = tree.b = a.nextSibling; + var c = tree.c = b.nextSibling; + var d = tree.d = c.nextSibling; + var e = tree.e = d.nextSibling; + + var sr = tree.sr = div.createShadowRoot(); + sr.innerHTML = 'fhik'; + var f = tree.f = sr.firstChild; + var g = tree.g = f.nextSibling; + var h = tree.h = g.nextSibling; + var content = tree.content = h.nextSibling; + var i = tree.i = content.nextSibling; + var j = tree.j = i.nextSibling; + var k = tree.k = j.nextSibling; + + div.offsetHeight; // trigger rendering + + return tree; + } + + test('nextElementSibling', function() { + var tree = getTree(); + + assert.equal(tree.b.nextElementSibling, tree.d); + assert.equal(tree.d.nextElementSibling, null); + assert.equal(tree.g.nextElementSibling, tree.content); + assert.equal(tree.content.nextElementSibling, tree.j); + assert.equal(tree.j.nextElementSibling, null); + }); + + test('previousElementSibling', function() { + var tree = getTree(); + + assert.equal(tree.b.previousElementSibling, null); + assert.equal(tree.d.previousElementSibling, tree.b); + assert.equal(tree.g.previousElementSibling, null); + assert.equal(tree.content.previousElementSibling, tree.g); + assert.equal(tree.j.previousElementSibling, tree.content); + }); + + test('remove', function() { + var div = document.createElement('div'); + div.innerHTML = ''; + var a = div.firstChild; + a.remove(); + assert.equal(div.firstChild, null); + assert.equal(a.parentNode, null); + + // no op. + div.remove(); + }); + +}); diff --git a/tests/ShadowDOM/js/Comment.js b/tests/ShadowDOM/js/Comment.js new file mode 100644 index 0000000..f63c6b1 --- /dev/null +++ b/tests/ShadowDOM/js/Comment.js @@ -0,0 +1,21 @@ +/* + * Copyright 2013 The Polymer Authors. All rights reserved. + * Use of this source code is goverened by a BSD-style + * license that can be found in the LICENSE file. + */ + +suite('Comment', function() { + + test('instanceof', function() { + var div = document.createElement('div'); + div.innerHTML = ''; + assert.instanceOf(div.firstChild, Comment); + }); + + test('instanceof', function() { + var div = document.createElement('div'); + div.innerHTML = ''; + assert.equal(Comment, div.firstChild.constructor); + }); + +}); diff --git a/tests/ShadowDOM/js/DOMTokenList.js b/tests/ShadowDOM/js/DOMTokenList.js new file mode 100644 index 0000000..0ee2262 --- /dev/null +++ b/tests/ShadowDOM/js/DOMTokenList.js @@ -0,0 +1,109 @@ +/* + * Copyright 2014 The Polymer Authors. All rights reserved. + * Use of this source code is goverened by a BSD-style + * license that can be found in the LICENSE file. + */ + +suite('DOMTokenList', function() { + + test('instanceof', function() { + var div = document.createElement('div'); + assert.instanceOf(div.classList, DOMTokenList); + }); + + test('constructor', function() { + var div = document.createElement('div'); + assert.equal(DOMTokenList, div.classList.constructor); + }); + + test('identity', function() { + var div = document.createElement('div'); + assert.equal(div.classList, div.classList); + }); + + test('length', function() { + var div = document.createElement('div'); + var classList = div.classList; + assert.equal(classList.length, 0); + div.className = 'a'; + assert.equal(classList.length, 1); + div.className = 'a b'; + assert.equal(classList.length, 2); + div.className = 'a b a'; + assert.equal(classList.length, 3); + }); + + test('item', function() { + var div = document.createElement('div'); + var classList = div.classList; + assert.isNull(classList.item(0)); + div.className = 'a'; + assert.equal(classList.item(0), 'a'); + assert.isNull(classList.item(1)); + div.className = 'a b'; + assert.equal(classList.item(0), 'a'); + assert.equal(classList.item(1), 'b'); + assert.isNull(classList.item(2)); + div.className = 'a b a'; + assert.equal(classList.item(0), 'a'); + assert.equal(classList.item(1), 'b'); + assert.equal(classList.item(2), 'a'); + assert.isNull(classList.item(3)); + }); + + test('contains', function() { + var div = document.createElement('div'); + var classList = div.classList; + assert.isFalse(classList.contains()); + assert.isFalse(classList.contains('a')); + div.className = 'a'; + assert.isTrue(classList.contains('a')); + div.className = 'a b'; + assert.isTrue(classList.contains('a')); + assert.isTrue(classList.contains('b')); + }); + + test('add', function() { + var div = document.createElement('div'); + var classList = div.classList; + classList.add('a'); + assert.equal(div.className, 'a'); + classList.add('b'); + assert.equal(div.className, 'a b'); + classList.add('a'); + assert.equal(div.className, 'a b'); + }); + + test('remove', function() { + var div = document.createElement('div'); + var classList = div.classList; + div.className = 'a b'; + classList.remove('a'); + assert.equal(div.className, 'b'); + classList.remove('a'); + assert.equal(div.className, 'b'); + classList.remove('b'); + assert.equal(div.className, ''); + }); + + test('toggle', function() { + var div = document.createElement('div'); + var classList = div.classList; + div.className = 'a b'; + classList.toggle('a'); + assert.equal(div.className, 'b'); + classList.toggle('a'); + assert.equal(div.className, 'b a'); + classList.toggle('b'); + assert.equal(div.className, 'a'); + }); + + test('toString', function() { + var div = document.createElement('div'); + var classList = div.classList; + div.className = 'a'; + assert.equal(classList.toString(), 'a'); + div.className = 'b a'; + assert.equal(classList.toString(), 'b a'); + }); +}); diff --git a/tests/ShadowDOM/js/Document.js b/tests/ShadowDOM/js/Document.js new file mode 100644 index 0000000..7e44fbb --- /dev/null +++ b/tests/ShadowDOM/js/Document.js @@ -0,0 +1,674 @@ +/* + * Copyright 2013 The Polymer Authors. All rights reserved. + * Use of this source code is goverened by a BSD-style + * license that can be found in the LICENSE file. + */ + +htmlSuite('Document', function() { + + var wrap = ShadowDOMPolyfill.wrap; + + var div; + teardown(function() { + if (div) { + if (div.parentNode) + div.parentNode.removeChild(div); + div = undefined; + } + }); + + function skipTest () {} + + test('Ensure Document has ParentNodeInterface', function() { + var doc = wrap(document).implementation.createHTMLDocument(''); + assert.equal(doc.firstElementChild.tagName, 'HTML'); + assert.equal(doc.lastElementChild.tagName, 'HTML'); + + var doc2 = document.implementation.createHTMLDocument(''); + assert.equal(doc2.firstElementChild.tagName, 'HTML'); + assert.equal(doc2.lastElementChild.tagName, 'HTML'); + }); + + test('document.documentElement', function() { + var doc = wrap(document); + assert.equal(doc.documentElement.ownerDocument, doc); + assert.equal(doc.documentElement.tagName, 'HTML'); + }); + + test('document.body', function() { + var doc = wrap(document); + assert.equal(doc.body.ownerDocument, doc); + assert.equal(doc.body.tagName, 'BODY'); + assert.equal(doc.body.parentNode, doc.documentElement); + }); + + test('document.head', function() { + var doc = wrap(document); + assert.equal(doc.head.ownerDocument, doc); + assert.equal(doc.head.tagName, 'HEAD'); + assert.equal(doc.head.parentNode, doc.documentElement); + }); + + test('getElementsByTagName', function() { + var elements = document.getElementsByTagName('body'); + assert.isTrue(elements instanceof HTMLCollection); + assert.equal(elements.length, 1); + assert.isTrue(elements[0] instanceof HTMLElement); + + var doc = wrap(document); + assert.equal(doc.body, elements[0]); + assert.equal(doc.body, elements.item(0)); + + var elements2 = doc.getElementsByTagName('body'); + assert.isTrue(elements2 instanceof HTMLCollection); + assert.equal(elements2.length, 1); + assert.isTrue(elements2[0] instanceof HTMLElement); + assert.equal(doc.body, elements2[0]); + assert.equal(doc.body, elements2.item(0)); + }); + + skipTest('getElementsByTagName', function() { + div = document.body.appendChild(document.createElement('div')); + div.innerHTML = ''; + var aa1 = div.firstChild; + var aa2 = div.lastChild; + + var sr = div.createShadowRoot(); + sr.innerHTML = ''; + var aa3 = sr.firstChild; + var aa4 = sr.lastChild; + + div.offsetHeight; + + var elements = document.getElementsByTagName('aa'); + assert.equal(elements.length, 2); + assert.equal(elements[0], aa1); + assert.equal(elements[1], aa2); + + var elements = sr.getElementsByTagName('aa'); + assert.equal(elements.length, 2); + assert.equal(elements[0], aa3); + assert.equal(elements[1], aa4); + + var z = document.getElementsByTagName('z'); + assert.equal(z.length, 0); + }); + + test('getElementsByTagNameNS', function() { + var div = document.createElement('div'); + var nsOne = 'http://one.com'; + var nsTwo = 'http://two.com'; + var aOne = div.appendChild(document.createElementNS(nsOne, 'a')); + var aTwo = div.appendChild(document.createElementNS(nsTwo, 'a')); + var aNull = div.appendChild(document.createElementNS(null, 'a')); + var bOne = div.appendChild(document.createElementNS(nsOne, 'b')); + var bTwo = div.appendChild(document.createElementNS(nsTwo, 'b')); + var bNull = div.appendChild(document.createElementNS(null, 'b')); + + var all = div.getElementsByTagNameNS(nsOne, 'a'); + assert.equal(all.length, 1); + assert.equal(all[0], aOne); + + var all = div.getElementsByTagNameNS(nsTwo, 'a'); + assert.equal(all.length, 1); + assert.equal(all[0], aTwo); + + var all = div.getElementsByTagNameNS(null, 'a'); + assert.equal(all.length, 1); + assert.equal(all[0], aNull); + + var all = div.getElementsByTagNameNS('', 'a'); + assert.equal(all.length, 1); + assert.equal(all[0], aNull); + + var all = div.getElementsByTagNameNS('*', 'a'); + assert.equal(all.length, 3); + assert.equal(all[0], aOne); + assert.equal(all[1], aTwo); + assert.equal(all[2], aNull); + + var all = div.getElementsByTagNameNS(nsOne, '*'); + assert.equal(all.length, 2); + assert.equal(all[0], aOne); + assert.equal(all[1], bOne); + + var all = div.getElementsByTagNameNS('*', '*'); + assert.equal(all.length, 6); + assert.equal(all[0], aOne); + assert.equal(all[1], aTwo); + assert.equal(all[2], aNull); + assert.equal(all[3], bOne); + assert.equal(all[4], bTwo); + assert.equal(all[5], bNull); + + var all = div.getElementsByTagNameNS('*', 'A'); + assert.equal(all.length, 0); + }); + + test('querySelectorAll', function() { + var elements = document.querySelectorAll('body'); + assert.isTrue(elements instanceof NodeList); + assert.equal(elements.length, 1); + assert.isTrue(elements[0] instanceof HTMLElement); + + var doc = wrap(document); + assert.equal(doc.body, elements[0]); + + var elements2 = doc.querySelectorAll('body'); + assert.isTrue(elements2 instanceof NodeList); + assert.equal(elements2.length, 1); + assert.isTrue(elements2[0] instanceof HTMLElement); + assert.equal(doc.body, elements2[0]); + }); + + skipTest('querySelectorAll', function() { + div = document.body.appendChild(document.createElement('div')); + div.innerHTML = ''; + var aa1 = div.firstChild; + var aa2 = div.lastChild; + + var sr = div.createShadowRoot(); + sr.innerHTML = ''; + var aa3 = sr.firstChild; + var aa4 = sr.lastChild; + + div.offsetHeight; + + var elements = document.querySelectorAll('aa'); + assert.equal(elements.length, 2); + assert.equal(elements[0], aa1); + assert.equal(elements[1], aa2); + + var elements = sr.querySelectorAll('aa'); + assert.equal(elements.length, 2); + assert.equal(elements[0], aa3); + assert.equal(elements[1], aa4); + + var z = document.querySelectorAll('z'); + assert.equal(z.length, 0); + }); + + test('querySelector', function() { + var z = document.querySelector('z'); + assert.equal(z, null); + }); + + test('querySelector deep', function() { + div = document.body.appendChild(document.createElement('div')); + div.innerHTML = ''; + var aa1 = div.firstChild; + var aa2 = div.lastChild; + + var sr = div.createShadowRoot(); + sr.innerHTML = ''; + var bb = sr.firstChild; + + div.offsetHeight; + + assert.equal(aa1, document.querySelector('div /deep/ aa')); + assert.equal(bb, document.querySelector('div /deep/ bb')); + }); + + test('querySelectorAll deep', function() { + div = document.body.appendChild(document.createElement('div')); + div.innerHTML = ''; + var aa1 = div.firstChild; + var aa2 = div.lastChild; + + var sr = div.createShadowRoot(); + sr.innerHTML = ''; + var bb = sr.firstChild; + + div.offsetHeight; + + var list = document.querySelectorAll('div /deep/ aa'); + assert.equal(2, list.length); + assert.equal(aa1, list[0]); + assert.equal(aa2, list[1]); + + list = document.querySelectorAll('div /deep/ bb'); + assert.equal(1, list.length); + assert.equal(bb, list[0]); + }); + + test('addEventListener', function() { + var calls = 0; + var doc = wrap(document); + document.addEventListener('click', function f(e) { + calls++; + assert.equal(this, doc); + assert.equal(e.target, doc.body); + assert.equal(e.currentTarget, this); + document.removeEventListener('click', f); + }); + doc.addEventListener('click', function f(e) { + calls++; + assert.equal(this, doc); + assert.equal(e.target, doc.body); + assert.equal(e.currentTarget, this); + doc.removeEventListener('click', f); + }); + + document.body.click(); + assert.equal(2, calls); + + document.body.click(); + assert.equal(2, calls); + }); + + test('adoptNode', function() { + var doc = wrap(document); + var doc2 = doc.implementation.createHTMLDocument(''); + var div = doc2.createElement('div'); + assert.equal(div.ownerDocument, doc2); + + var div2 = document.adoptNode(div); + assert.equal(div, div2); + assert.equal(div.ownerDocument, doc); + + var div3 = doc2.adoptNode(div); + assert.equal(div, div3); + assert.equal(div.ownerDocument, doc2); + }); + + test('adoptNode with shadowRoot', function() { + var doc = wrap(document); + var doc2 = doc.implementation.createHTMLDocument(''); + var div = doc2.createElement('div'); + var sr = div.createShadowRoot(); + sr.innerHTML = ''; + var a = sr.firstChild; + + var sr2 = div.createShadowRoot(); + sr2.innerHTML = ''; + var b = sr2.firstChild; + + var sr3 = a.createShadowRoot(); + sr3.innerHTML = ''; + var c = sr3.firstChild; + + assert.equal(div.ownerDocument, doc2); + assert.equal(sr.ownerDocument, doc2); + assert.equal(sr2.ownerDocument, doc2); + assert.equal(sr3.ownerDocument, doc2); + assert.equal(a.ownerDocument, doc2); + assert.equal(b.ownerDocument, doc2); + assert.equal(c.ownerDocument, doc2); + + doc.adoptNode(div); + + assert.equal(div.ownerDocument, doc); + assert.equal(sr.ownerDocument, doc); + assert.equal(sr2.ownerDocument, doc); + assert.equal(sr3.ownerDocument, doc); + assert.equal(a.ownerDocument, doc); + assert.equal(b.ownerDocument, doc); + assert.equal(c.ownerDocument, doc); + }); + + test('importNode', function() { + var doc = wrap(document); + var doc2 = doc.implementation.createHTMLDocument(''); + var div = doc2.createElement('div'); + div.innerHTML = 'test'; + assert.equal(div.ownerDocument, doc2); + + var div2 = document.importNode(div, true); + assert.equal(div.innerHTML, div2.innerHTML); + assert.equal(div2.ownerDocument, doc); + + var div3 = doc2.importNode(div2); + assert.equal(div3.innerHTML, ''); + assert.equal(div3.ownerDocument, doc2); + }); + + test('importNode with shadow root', function() { + var doc = wrap(document); + var doc2 = doc.implementation.createHTMLDocument(''); + var div = doc2.createElement('div'); + div.textContent = 'test'; + var sr = div.createShadowRoot(); + sr.textContent = 'shadow root'; + + div.offsetHeight; + + assert.equal(div.ownerDocument, doc2); + + var div2 = document.importNode(div, true); + assert.equal(div.innerHTML, div2.innerHTML); + assert.equal(div2.ownerDocument, doc); + + var div3 = doc2.importNode(div2); + assert.equal(div3.innerHTML, ''); + assert.equal(div3.ownerDocument, doc2); + }); + + test('elementFromPoint', function() { + div = document.body.appendChild(document.createElement('div')); + div.style.cssText = 'position: fixed; background: green; ' + + 'width: 10px; height: 10px; top: 0; left: 0;'; + + assert.equal(document.elementFromPoint(5, 5), div); + + var doc = wrap(document); + assert.equal(doc.elementFromPoint(5, 5), div); + }); + + test('elementFromPoint in shadow', function() { + div = document.body.appendChild(document.createElement('div')); + div.style.cssText = 'position: fixed; background: red; ' + + 'width: 10px; height: 10px; top: 0; left: 0;'; + var sr = div.createShadowRoot(); + sr.innerHTML = ''; + var a = sr.firstChild; + a.style.cssText = 'position: absolute; width: 100%; height: 100%; ' + + 'background: green'; + + assert.equal(document.elementFromPoint(5, 5), div); + + var doc = wrap(document); + assert.equal(doc.elementFromPoint(5, 5), div); + }); + + test('elementFromPoint null', function() { + assert.isNull(document.elementFromPoint(-9999, -9999)); + + var doc = wrap(document); + assert.isNull(doc.elementFromPoint(-9999, -9999)); + }); + + test('document.contains', function() { + assert.isTrue(document.contains(document.body)); + assert.isTrue(document.contains(document.querySelector('body'))); + + assert.isTrue(document.contains(document.head)); + assert.isTrue(document.contains(document.querySelector('head'))); + + assert.isTrue(document.contains(document.documentElement)); + assert.isTrue(document.contains(document.querySelector('html'))); + }); + + test('document.registerElement', function() { + if (!document.registerElement) + return; + + var aPrototype = Object.create(HTMLElement.prototype); + aPrototype.getName = function() { + return 'a'; + }; + + var A = document.registerElement('x-a', {prototype: aPrototype}); + + var a1 = document.createElement('x-a'); + assert.equal('x-a', a1.localName); + assert.equal(Object.getPrototypeOf(a1), aPrototype); + assert.instanceOf(a1, A); + assert.instanceOf(a1, HTMLElement); + assert.equal(a1.getName(), 'a'); + + var a2 = new A(); + assert.equal('x-a', a2.localName); + assert.equal(Object.getPrototypeOf(a2), aPrototype); + assert.instanceOf(a2, A); + assert.instanceOf(a2, HTMLElement); + assert.equal(a2.getName(), 'a'); + + ////////////////////////////////////////////////////////////////////// + + var bPrototype = Object.create(A.prototype); + bPrototype.getName = function() { + return 'b'; + }; + + var B = document.registerElement('x-b', {prototype: bPrototype}); + + var b1 = document.createElement('x-b'); + assert.equal('x-b', b1.localName); + assert.equal(Object.getPrototypeOf(b1), bPrototype); + assert.instanceOf(b1, B); + assert.instanceOf(b1, A); + assert.instanceOf(b1, HTMLElement); + assert.equal(b1.getName(), 'b'); + + var b2 = new B(); + assert.equal('x-b', b2.localName); + assert.equal(Object.getPrototypeOf(b2), bPrototype); + assert.instanceOf(b2, B); + assert.instanceOf(b2, A); + assert.instanceOf(b2, HTMLElement); + assert.equal(b2.getName(), 'b'); + }); + + test('document.registerElement type extension', function() { + if (!document.registerElement) + return; + + var aPrototype = Object.create(HTMLSpanElement.prototype); + aPrototype.getName = function() { + return 'a'; + }; + + var A = document.registerElement('x-a-span', + {prototype: aPrototype, extends: 'span'}); + + var a1 = document.createElement('span', 'x-a-span'); + assert.equal('span', a1.localName); + assert.equal('', a1.outerHTML); + assert.equal(Object.getPrototypeOf(a1), aPrototype); + assert.instanceOf(a1, A); + assert.instanceOf(a1, HTMLSpanElement); + assert.equal(a1.getName(), 'a'); + + var a2 = new A(); + assert.equal('span', a2.localName); + assert.equal('', a2.outerHTML); + assert.equal(Object.getPrototypeOf(a2), aPrototype); + assert.instanceOf(a2, A); + assert.instanceOf(a2, HTMLSpanElement); + assert.equal(a2.getName(), 'a'); + }); + + test('document.registerElement deeper', function() { + if (!document.registerElement) + return; + + function C() {} + C.prototype = { + __proto__: HTMLElement.prototype + }; + + function B() {} + B.prototype = { + __proto__: C.prototype + }; + + function A() {} + A.prototype = { + __proto__: B.prototype + }; + + A = document.registerElement('x-a5', A); + + var a1 = document.createElement('x-a5'); + assert.equal('x-a5', a1.localName); + assert.equal(a1.__proto__, A.prototype); + assert.equal(a1.__proto__.__proto__, B.prototype); + assert.equal(a1.__proto__.__proto__.__proto__, C.prototype); + assert.equal(a1.__proto__.__proto__.__proto__.__proto__, + HTMLElement.prototype); + + var a2 = new A(); + assert.equal('x-a5', a2.localName); + assert.equal(a2.__proto__, A.prototype); + assert.equal(a2.__proto__.__proto__, B.prototype); + assert.equal(a2.__proto__.__proto__.__proto__, C.prototype); + assert.equal(a2.__proto__.__proto__.__proto__.__proto__, + HTMLElement.prototype); + }); + + test('document.registerElement createdCallback', function() { + if (!document.registerElement) + return; + + var self; + var createdCalls = 0; + + function A() {} + A.prototype = { + __proto__: HTMLElement.prototype, + createdCallback: function() { + createdCalls++; + assert.isUndefined(a); + assert.instanceOf(this, A); + self = this; + } + }; + + A = document.registerElement('x-a2', A); + + var a = new A; + assert.equal(createdCalls, 1); + assert.equal(self, a); + }); + + test('document.registerElement createdCallback upgrade', function() { + if (!document.registerElement) + return; + + div = document.body.appendChild(document.createElement('div')); + div.innerHTML = ''; + + function A() {} + A.prototype = { + __proto__: HTMLElement.prototype, + createdCallback: function() { + assert.isTrue(this.isCustom); + assert.instanceOf(this, A); + }, + isCustom: true + }; + + A = document.registerElement('x-a2-1', A); + }); + + test('document.registerElement attachedCallback, detachedCallback', + function() { + if (!document.registerElement) + return; + + var attachedCalls = 0; + var detachedCalls = 0; + + function A() {} + A.prototype = { + __proto__: HTMLElement.prototype, + attachedCallback: function() { + attachedCalls++; + assert.instanceOf(this, A); + assert.equal(a, this); + }, + detachedCallback: function() { + detachedCalls++; + assert.instanceOf(this, A); + assert.equal(a, this); + } + }; + + A = document.registerElement('x-a3', A); + + var a = new A; + document.body.appendChild(a); + assert.equal(attachedCalls, 1); + document.body.removeChild(a); + assert.equal(detachedCalls, 1); + }); + + test('document.registerElement attributeChangedCallback', function() { + if (!document.registerElement) + return; + + var attributeChangedCalls = 0; + + function A() {} + A.prototype = { + __proto__: HTMLElement.prototype, + attributeChangedCallback: function(name, oldValue, newValue) { + attributeChangedCalls++; + assert.equal(name, 'foo'); + switch (attributeChangedCalls) { + case 1: + assert.isNull(oldValue); + assert.equal(newValue, 'bar'); + break; + case 2: + assert.equal(oldValue, 'bar'); + assert.equal(newValue, 'baz'); + break; + case 3: + assert.equal(oldValue, 'baz'); + assert.isNull(newValue); + break; + } + console.log(arguments); + } + }; + + A = document.registerElement('x-a4', A); + + var a = new A; + assert.equal(attributeChangedCalls, 0); + a.setAttribute('foo', 'bar'); + assert.equal(attributeChangedCalls, 1); + a.setAttribute('foo', 'baz'); + assert.equal(attributeChangedCalls, 2); + a.removeAttribute('foo'); + assert.equal(attributeChangedCalls, 3); + }); + + test('document.registerElement get reference, upgrade, rewrap', function() { + if (!document.registerElement) + return; + + div = document.body.appendChild(document.createElement('div')); + div.innerHTML = ''; + // get reference (creates wrapper) + div.firstChild; + + function A() {} + A.prototype = { + __proto__: HTMLElement.prototype, + isCustom: true + }; + + A = document.registerElement('x-a6', A); + // re-wrap after registration to update wrapper + ShadowDOMPolyfill.rewrap(ShadowDOMPolyfill.unwrap(div.firstChild)); + assert.isTrue(div.firstChild.isCustom); + }); + + test('document.registerElement optional option', function() { + if (!document.registerElement) + return; + + document.registerElement('x-a7'); + var a = document.createElement('x-a7'); + assert.equal(Object.getPrototypeOf(Object.getPrototypeOf(a)), + HTMLElement.prototype); + + document.registerElement('x-a8', {}); + var a2 = document.createElement('x-a8'); + assert.equal(Object.getPrototypeOf(Object.getPrototypeOf(a2)), + HTMLElement.prototype); + + document.registerElement('x-a-span-2', {extends: 'span'}); + var a3 = document.createElement('span', 'x-a-span-2'); + assert.equal(Object.getPrototypeOf(Object.getPrototypeOf(a3)), + HTMLElement.prototype); + a3.localName = 'span'; + assert.equal('', a3.outerHTML); + }); + + htmlTest('../html/document-write.html'); + + htmlTest('../html/head-then-body.html'); +}); diff --git a/tests/ShadowDOM/js/Element.js b/tests/ShadowDOM/js/Element.js new file mode 100644 index 0000000..01af583 --- /dev/null +++ b/tests/ShadowDOM/js/Element.js @@ -0,0 +1,387 @@ +/* + * Copyright 2013 The Polymer Authors. All rights reserved. + * Use of this source code is goverened by a BSD-style + * license that can be found in the LICENSE file. + */ + +suite('Element', function() { + + var wrap = ShadowDOMPolyfill.wrap; + var div; + + teardown(function() { + if (div && div.parentNode) + div.parentNode.removeChild(div); + div = null; + }); + + function skipTest () {} + + test('querySelector', function() { + var div = document.createElement('div'); + div.innerHTML = ''; + var b = div.firstChild.firstChild; + assert.equal(div.querySelector('b'), b); + + var sr = div.createShadowRoot(); + sr.innerHTML = ''; + var srb = sr.firstChild; + + div.offsetHeight; + + assert.equal(div.querySelector('b'), b); + assert.equal(sr.querySelector('b'), srb); + + var z = div.querySelector('z'); + assert.equal(z, null); + + var z = sr.querySelector('z'); + assert.equal(z, null); + }); + + test('querySelectorAll', function() { + var div = document.createElement('div'); + div.innerHTML = '01'; + var a0 = div.firstChild; + var a1 = div.lastChild; + + var as = div.querySelectorAll('a'); + assert.equal(as.length, 2); + assert.equal(as[0], a0); + assert.equal(as.item(0), a0); + assert.equal(as[1], a1); + assert.equal(as.item(1), a1); + }); + + skipTest('querySelectorAll', function() { + var div = document.createElement('div'); + div.innerHTML = '01'; + var a0 = div.firstChild; + var a1 = div.lastChild; + + var sr = div.createShadowRoot(); + sr.innerHTML = '34'; + var a3 = sr.firstChild; + var a4 = sr.lastChild; + + div.offsetHeight; + + var as = div.querySelectorAll('a'); + assert.equal(as.length, 2); + assert.equal(as[0], a0); + assert.equal(as[1], a1); + + var as = sr.querySelectorAll('a'); + assert.equal(as.length, 2); + assert.equal(as[0], a3); + assert.equal(as[1], a4); + + var z = div.querySelectorAll('z'); + assert.equal(z.length, 0); + + var z = sr.querySelectorAll('z'); + assert.equal(z.length, 0); + }); + + test('querySelector deep', function() { + var div = document.createElement('div'); + div.innerHTML = ''; + var aa1 = div.firstChild; + var aa2 = div.lastChild; + + var sr = div.createShadowRoot(); + sr.innerHTML = ''; + var bb = sr.firstChild; + + div.offsetHeight; + + assert.equal(aa1, div.querySelector('div /deep/ aa')); + assert.equal(bb, div.querySelector('div /deep/ bb')); + }); + + test('querySelectorAll deep', function() { + var div = document.createElement('div'); + div.innerHTML = ''; + var aa1 = div.firstChild; + var aa2 = div.lastChild; + + var sr = div.createShadowRoot(); + sr.innerHTML = ''; + var bb = sr.firstChild; + + div.offsetHeight; + + var list = div.querySelectorAll('div /deep/ aa'); + assert.equal(2, list.length); + assert.equal(aa1, list[0]); + assert.equal(aa2, list[1]); + + list = div.querySelectorAll('div /deep/ bb'); + assert.equal(1, list.length); + assert.equal(bb, list[0]); + }); + + skipTest('getElementsByTagName', function() { + var div = document.createElement('div'); + div.innerHTML = '01'; + var a0 = div.firstChild; + var a1 = div.lastChild; + + var as = div.getElementsByTagName('a'); + assert.equal(as.length, 2); + assert.equal(as[0], a0); + assert.equal(as.item(0), a0); + assert.equal(as[1], a1); + assert.equal(as.item(1), a1); + + var sr = div.createShadowRoot(); + sr.innerHTML = '34'; + var a3 = sr.firstChild; + var a4 = sr.lastChild; + + div.offsetHeight; + + var as = div.getElementsByTagName('a'); + assert.equal(as.length, 2); + assert.equal(as[0], a0); + assert.equal(as[1], a1); + + var as = sr.getElementsByTagName('a'); + assert.equal(as.length, 2); + assert.equal(as[0], a3); + assert.equal(as[1], a4); + + var z = div.getElementsByTagName('z'); + assert.equal(z.length, 0); + + var z = sr.getElementsByTagName('z'); + assert.equal(z.length, 0); + }); + + test('getElementsByTagName with colon', function() { + var div = document.createElement('div'); + div.innerHTML = '01'; + var a0 = div.firstChild; + var a1 = div.lastChild; + + var as = div.getElementsByTagName('a:b:c'); + assert.equal(as.length, 2); + assert.equal(as[0], a0); + assert.equal(as.item(0), a0); + assert.equal(as[1], a1); + assert.equal(as.item(1), a1); + }); + + test('getElementsByTagName with namespace', function() { + var div = document.createElement('div'); + div.innerHTML = '01'; + var a0 = div.firstChild; + var a1 = div.lastChild; + var a2 = document.createElementNS('NS', 'a'); + var a3 = document.createElementNS('NS', 'A'); + div.appendChild(a2); + div.appendChild(a3); + + var as = div.getElementsByTagName('a'); + assert.equal(as.length, 3); + assert.equal(as[0], a0); + assert.equal(as.item(0), a0); + assert.equal(as[1], a1); + assert.equal(as.item(1), a1); + assert.equal(as[2], a2); + assert.equal(as.item(2), a2); + + var as = div.getElementsByTagName('A'); + assert.equal(as.length, 3); + assert.equal(as[0], a0); + assert.equal(as.item(0), a0); + assert.equal(as[1], a1); + assert.equal(as.item(1), a1); + assert.equal(as[2], a3); + assert.equal(as.item(2), a3); + }); + + test('getElementsByTagNameNS', function() { + var div = document.createElement('div'); + div.innerHTML = '01'; + + var sr = div.createShadowRoot(); + sr.innerHTML = '34'; + + var z = div.getElementsByTagNameNS('NS', 'z'); + assert.equal(z.length, 0); + + var z = sr.getElementsByTagNameNS('NS', 'z'); + assert.equal(z.length, 0); + }); + + skipTest('getElementsByClassName', function() { + var div = document.createElement('div'); + div.innerHTML = '01'; + var a0 = div.firstChild; + var a1 = div.lastChild; + + var as = div.getElementsByClassName('a'); + assert.equal(as.length, 2); + assert.equal(as[0], a0); + assert.equal(as.item(0), a0); + assert.equal(as[1], a1); + assert.equal(as.item(1), a1); + + var sr = div.createShadowRoot(); + sr.innerHTML = '34'; + var a3 = sr.firstChild; + var a4 = sr.lastChild; + + div.offsetHeight; + + var as = div.getElementsByClassName('a'); + assert.equal(as.length, 2); + assert.equal(as[0], a0); + assert.equal(as[1], a1); + + var as = sr.getElementsByClassName('a'); + assert.equal(as.length, 2); + assert.equal(as[0], a3); + assert.equal(as[1], a4); + }); + + test('webkitCreateShadowRoot', function() { + var div = document.createElement('div'); + if (!div.webkitCreateShadowRoot) + return; + var sr = div.webkitCreateShadowRoot(); + assert.instanceOf(sr, ShadowRoot); + }); + + test('getDestinationInsertionPoints', function() { + var div = document.createElement('div'); + div.innerHTML = ''; + var a = div.firstChild; + var b = div.lastChild; + var sr = div.createShadowRoot(); + sr.innerHTML = ''; + var content = sr.firstChild; + + assertArrayEqual([content], a.getDestinationInsertionPoints()); + assertArrayEqual([content], b.getDestinationInsertionPoints()); + + var sr2 = div.createShadowRoot(); + sr2.innerHTML = ''; + var contentB = sr2.firstChild; + + assertArrayEqual([content], a.getDestinationInsertionPoints()); + assertArrayEqual([contentB], b.getDestinationInsertionPoints()); + }); + + test('getDestinationInsertionPoints redistribution', function() { + var div = document.createElement('div'); + div.innerHTML = ''; + var a = div.firstChild; + var b = div.lastChild; + var sr = div.createShadowRoot(); + sr.innerHTML = ''; + var c = sr.firstChild; + var content = c.firstChild; + var sr2 = c.createShadowRoot(); + sr2.innerHTML = ''; + var contentB = sr2.firstChild; + + assertArrayEqual([content], a.getDestinationInsertionPoints()); + assertArrayEqual([content, contentB], b.getDestinationInsertionPoints()); + }); + + test('getElementsByName', function() { + div = document.createElement('div'); + document.body.appendChild(div); + div.innerHTML = '01'; + var a0 = div.firstChild; + var a1 = div.lastChild; + + var as = document.getElementsByName('a'); + assert.instanceOf(as, NodeList); + assert.equal(as.length, 2); + assert.equal(as[0], a0); + assert.equal(as.item(0), a0); + assert.equal(as[1], a1); + assert.equal(as.item(1), a1); + + var doc = wrap(document); + as = doc.getElementsByName('a'); + assert.instanceOf(as, NodeList); + assert.equal(as.length, 2); + assert.equal(as[0], a0); + assert.equal(as.item(0), a0); + assert.equal(as[1], a1); + assert.equal(as.item(1), a1); + + a0.setAttribute('name', '"odd"'); + as = document.getElementsByName('"odd"'); + assert.instanceOf(as, NodeList); + assert.equal(as.length, 1); + assert.equal(as[0], a0); + assert.equal(as.item(0), a0); + + var sr = div.createShadowRoot(); + sr.innerHTML = ''; + as = document.getElementsByName('a'); + assert.instanceOf(as, NodeList); + assert.equal(as.length, 1); + assert.equal(as[0], a1); + assert.equal(as.item(0), a1); + }); + + test('sub shadow-root traversal', function() { + var div = document.createElement("DIV"); + var sr = div.createShadowRoot(); + sr.innerHTML = ""; + + var saal = sr.getElementsByTagName("aa"); + var sbbl = sr.getElementsByTagName("bb"); + assert.equal(saal.length, 1); + assert.equal(sbbl.length, 1); + + var saa = saal [0]; + var sbb = sbbl [0]; + var abbl = saa.getElementsByTagName("bb"); + assert.equal(abbl.length, 1); + + var abb = abbl [0]; + assert.instanceOf(abb, HTMLElement); + assert.equal(abb, sbb); + + var saal = sr.getElementsByTagNameNS("*", "aa"); + var sbbl = sr.getElementsByTagNameNS("*", "bb"); + assert.equal(saal.length, 1); + assert.equal(sbbl.length, 1); + + var saa = saal [0]; + var sbb = sbbl [0]; + var abbl = saa.getElementsByTagNameNS("*", "bb"); + assert.equal(abbl.length, 1); + + var abb = abbl [0]; + assert.instanceOf(abb, HTMLElement); + assert.equal(abb, sbb); + + var saal = sr.querySelectorAll("aa"); + var sbbl = sr.querySelectorAll("bb"); + assert.equal(saal.length, 1); + assert.equal(sbbl.length, 1); + + var saa = saal [0]; + var sbb = sbbl [0]; + var abbl = saa.querySelectorAll("bb"); + assert.equal(abbl.length, 1); + + var abb = abbl [0]; + assert.instanceOf(abb, HTMLElement); + assert.equal(abb, sbb); + + var saa = sr.querySelector("aa"); + var sbb = sr.querySelector("bb"); + var abb = saa.querySelector("bb"); + assert.instanceOf(abb, HTMLElement); + assert.equal(abb, sbb); + }); +}); diff --git a/tests/ShadowDOM/js/FormData.js b/tests/ShadowDOM/js/FormData.js new file mode 100644 index 0000000..b9be8f9 --- /dev/null +++ b/tests/ShadowDOM/js/FormData.js @@ -0,0 +1,35 @@ +/* + * Copyright 2014 The Polymer Authors. All rights reserved. + * Use of this source code is goverened by a BSD-style + * license that can be found in the LICENSE file. + */ + +suite('FormData', function() { + + var wrap = ShadowDOMPolyfill.wrap; + var unwrap = ShadowDOMPolyfill.unwrap; + + test('instanceof', function() { + var fd = new FormData(); + assert.instanceOf(fd, FormData); + }); + + test('constructor', function() { + var fd = new FormData(); + assert.equal(FormData, fd.constructor); + }); + + test('form element', function() { + var formElement = document.createElement('form'); + var fd = new FormData(formElement) + assert.instanceOf(fd, FormData); + }); + + test('wrap/unwrap', function() { + var fd = new FormData(); + var unwrapped = unwrap(fd); + var wrapped = wrap(unwrapped); + assert.equal(fd, wrapped); + }); + +}); diff --git a/tests/ShadowDOM/js/HTMLAudioElement.js b/tests/ShadowDOM/js/HTMLAudioElement.js new file mode 100644 index 0000000..f35cb4e --- /dev/null +++ b/tests/ShadowDOM/js/HTMLAudioElement.js @@ -0,0 +1,55 @@ +/* + * Copyright 2013 The Polymer Authors. All rights reserved. + * Use of this source code is goverened by a BSD-style + * license that can be found in the LICENSE file. + */ + +suite('HTMLAudioElement', function() { + + test('instanceof', function() { + var audio = document.createElement('audio'); + assert.instanceOf(audio, HTMLAudioElement); + assert.instanceOf(audio, Audio); + assert.instanceOf(audio, HTMLMediaElement); + assert.instanceOf(audio, HTMLElement); + }); + + test('constructor', function() { + var audio = document.createElement('audio'); + assert.equal(audio.constructor, HTMLAudioElement); + }); + + test('Audio', function() { + var audio = new Audio(); + assert.instanceOf(audio, HTMLAudioElement); + assert.instanceOf(audio, Audio); + assert.instanceOf(audio, HTMLMediaElement); + assert.instanceOf(audio, HTMLElement); + }); + + test('Audio arguments', function() { + var audio = new Audio(); + assert.isFalse(audio.hasAttribute('src')); + assert.equal(audio.getAttribute('preload'), 'auto'); + + var src = 'foo.wav'; + var audio = new Audio(src); + assert.equal(audio.getAttribute('src'), 'foo.wav'); + }); + + test('Audio called as function', function() { + assert.throws(Audio, TypeError); + }); + + test('Audio basics', function() { + var audio = new Audio(); + assert.equal('audio', audio.localName); + + var div = document.createElement('div'); + div.appendChild(audio); + + assert.equal(div.firstChild, audio); + assert.equal('
', div.outerHTML); + }); + +}); diff --git a/tests/ShadowDOM/js/HTMLBodyElement.js b/tests/ShadowDOM/js/HTMLBodyElement.js new file mode 100644 index 0000000..a8d4b49 --- /dev/null +++ b/tests/ShadowDOM/js/HTMLBodyElement.js @@ -0,0 +1,127 @@ +/* + * Copyright 2013 The Polymer Authors. All rights reserved. + * Use of this source code is goverened by a BSD-style + * license that can be found in the LICENSE file. + */ + +htmlSuite('HTMLBodyElement', function() { + + var wrap = ShadowDOMPolyfill.wrap; + + var div, div2; + + teardown(function() { + if (div && div.parentNode) + div.parentNode.removeChild(div); + if (div2 && div2.parentNode) + div2.parentNode.removeChild(div2); + div = div2 = undefined; + }); + + test('appendChild', function() { + div = document.createElement('div'); + document.body.appendChild(div); + assert.equal(wrap(document.body.lastChild), div); + }); + + test('appendChild (through wrapper)', function() { + var doc = wrap(document); + div = doc.createElement('div'); + doc.body.appendChild(div); + assert.equal(doc.body.lastChild, div); + }); + + test('insertBefore', function() { + div = document.createElement('div'); + document.body.appendChild(div); + div2 = document.createElement('div'); + document.body.insertBefore(div2, div); + assert.equal(wrap(document.body.lastChild), div); + assert.equal(div2.nextSibling, div); + assert.equal(div.previousSibling, div2); + }); + + test('insertBefore (through wrapper)', function() { + var doc = wrap(document); + div = doc.createElement('div'); + doc.body.appendChild(div); + div2 = doc.createElement('div'); + doc.body.insertBefore(div2, div); + assert.equal(doc.body.lastChild, div); + assert.equal(div2.nextSibling, div); + assert.equal(div.previousSibling, div2); + }); + + test('replaceChild', function() { + div = document.createElement('div'); + document.body.appendChild(div); + div2 = document.createElement('div'); + document.body.replaceChild(div2, div); + assert.equal(wrap(document.body.lastChild), div2); + assert.isNull(div.parentNode); + }); + + test('replaceChild (through wrapper)', function() { + var doc = wrap(document); + div = doc.createElement('div'); + doc.body.appendChild(div); + div2 = doc.createElement('div'); + doc.body.replaceChild(div2, div); + assert.equal(doc.body.lastChild, div2); + assert.isNull(div.parentNode); + }); + + test('removeChild', function() { + div = document.createElement('div'); + document.body.appendChild(div); + document.body.removeChild(div); + assert.isNull(div.parentNode); + }); + + test('removeChild (through wrapper)', function() { + var doc = wrap(document); + div = doc.createElement('div'); + doc.body.appendChild(div); + doc.body.removeChild(div); + assert.isNull(div.parentNode); + }); + + test('dispatchEvent', function() { + var calls = 0; + var doc = wrap(document); + var f; + document.body.addEventListener('x', f = function(e) { + calls++; + assert.equal(e.target, doc.body); + assert.equal(e.currentTarget, doc.body); + assert.equal(this, doc.body); + if (calls === 2) + document.body.removeEventListener('x', f); + }); + + document.body.dispatchEvent(new Event('x')); + doc.body.dispatchEvent(new Event('x')); + + assert.equal(calls, 2); + }); + + test('document.body.contains', function() { + var doc = wrap(document); + assert.isTrue(doc.body.contains(doc.body.firstChild)); + assert.isTrue(doc.body.contains(document.body.firstChild)); + assert.isTrue(document.body.contains(doc.body.firstChild)); + assert.isTrue(document.body.contains(document.body.firstChild)); + }); + + test('instanceof', function() { + assert.instanceOf(document.createElement('body'), HTMLBodyElement); + }); + + test('constructor', function() { + assert.equal(HTMLBodyElement, document.createElement('body').constructor); + }); + + htmlTest('../html/document-body-inner-html.html'); + + htmlTest('../html/document-body-shadow-root.html'); +}); diff --git a/tests/ShadowDOM/js/HTMLButtonElement.js b/tests/ShadowDOM/js/HTMLButtonElement.js new file mode 100644 index 0000000..8988a0f --- /dev/null +++ b/tests/ShadowDOM/js/HTMLButtonElement.js @@ -0,0 +1,25 @@ +/* + * Copyright 2013 The Polymer Authors. All rights reserved. + * Use of this source code is goverened by a BSD-style + * license that can be found in the LICENSE file. + */ + +suite('HTMLButtonElement', function() { + + test('form', function() { + var form = document.createElement('form'); + var button = document.createElement('button'); + form.appendChild(button); + assert.equal(button.form, form); + }); + + test('instanceof', function() { + assert.instanceOf(document.createElement('button'), HTMLButtonElement); + }); + + test('constructor', function() { + assert.equal(HTMLButtonElement, + document.createElement('button').constructor); + }); + +}); diff --git a/tests/ShadowDOM/js/HTMLCanvasElement.js b/tests/ShadowDOM/js/HTMLCanvasElement.js new file mode 100644 index 0000000..b6ae9db --- /dev/null +++ b/tests/ShadowDOM/js/HTMLCanvasElement.js @@ -0,0 +1,207 @@ +/* + * Copyright 2013 The Polymer Authors. All rights reserved. + * Use of this source code is goverened by a BSD-style + * license that can be found in the LICENSE file. + */ + +suite('HTMLCanvasElement', function() { + + var iconUrl = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAHklEQVQ4T2Nk+A+EFADGUQMYRsOAYTQMgHloGKQDAJXkH/HZpKBrAAAAAElFTkSuQmCC'; + + test('getContext null', function() { + var canvas = document.createElement('canvas'); + // IE10 returns undefined instead of null + assert.isTrue(canvas.getContext('unknown') == null); + }); + + test('getContext 2d', function() { + var canvas = document.createElement('canvas'); + var context = canvas.getContext('2d'); + assert.instanceOf(context, CanvasRenderingContext2D); + assert.equal(context.canvas, canvas); + }); + + test('getContext webgl', function() { + // IE10 does not have WebGL. + if (typeof WebGLRenderingContext === 'undefined') + return; + + var canvas = document.createElement('canvas'); + var context = null; + // Firefox throws exception if graphics card is not supported + try { + context = canvas.getContext('webgl'); + } + catch(ex) { + } + // Chrome returns null if the graphics card is not supported + assert.isTrue(context === null || context instanceof WebGLRenderingContext); + + if (context != null) + assert.equal(context.canvas, canvas); + }); + + test('context instance properties', function() { + var canvas = document.createElement('canvas'); + var context = canvas.getContext('2d'); + + assert.isString(context.fillStyle); + assert.isString(context.strokeStyle); + assert.isString(context.textBaseline); + assert.isString(context.textAlign); + assert.isString(context.font); + + // lineDashOffset is not available in Firefox 25 + // assert.isNumber(context.lineDashOffset); + + assert.isString(context.shadowColor); + assert.isNumber(context.shadowBlur); + assert.isNumber(context.shadowOffsetY); + assert.isNumber(context.shadowOffsetX); + assert.isNumber(context.miterLimit); + assert.isString(context.lineJoin); + assert.isString(context.lineCap); + assert.isNumber(context.lineWidth); + assert.isNumber(context.globalAlpha); + }); + + test('2d drawImage using new Image', function(done) { + var canvas = document.createElement('canvas'); + var context = canvas.getContext('2d'); + + // var img = new Image(); + var img = document.createElement('img'); + img.width = img.height = 32; + img.onload = function() { + context.drawImage(img, 0, 0); + done(); + }; + img.src = iconUrl; + }); + + test('2d drawImage', function(done) { + var canvas = document.createElement('canvas'); + var context = canvas.getContext('2d'); + + var img = document.createElement('img'); + img.onload = function() { + context.drawImage(img, 0, 0); + done(); + }; + img.src = iconUrl; + }); + + test('2d createPattern', function(done) { + var canvas = document.createElement('canvas'); + var context = canvas.getContext('2d'); + var img = document.createElement('img'); + img.onload = function() { + var pattern = context.createPattern(img, 'repeat'); + done(); + }; + img.src = iconUrl; + }); + + test('WebGL texImage2D', function(done) { + var canvas = document.createElement('canvas'); + var gl = null; + // Firefox throws exception if graphics card is not supported + try { + gl = canvas.getContext('webgl'); + } catch (ex) { + } + // IE10 does not have WebGL. + // Chrome returns null if the graphics card is not supported + if (!gl) { + done(); + return; + } + + var imageData = document.createElement('canvas').getContext('2d'). + createImageData(16, 16); + var arrayBufferView = new Uint8Array(16 * 16 * 4); + + var img = document.createElement('img'); + img.onload = function() { + var texture = gl.createTexture(); + gl.bindTexture(gl.TEXTURE_2D, texture); + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, img); + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, + imageData); + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, + 16, 16, 0, + gl.RGBA, gl.UNSIGNED_BYTE, arrayBufferView); + + done(); + }; + img.src = iconUrl; + }); + + test('WebGL context instance properties', function() { + var canvas = document.createElement('canvas'); + var gl = null; + // Firefox throws exception if graphics card is not supported + try { + gl = canvas.getContext('webgl'); + } catch (ex) { + } + // IE10 does not have WebGL. + // Chrome returns null if the graphics card is not supported + if (!gl) { + return; + } + + assert.isNumber(gl.drawingBufferHeight); + assert.isNumber(gl.drawingBufferWidth); + }); + + test('WebGL texSubImage2D', function(done) { + var canvas = document.createElement('canvas'); + var gl = null; + // Firefox throws exception if graphics card is not supported + try { + gl = canvas.getContext('webgl'); + } catch(ex) { + } + // IE10 does not have WebGL. + // Chrome returns null if the graphics card is not supported + if (!gl) { + done(); + return; + } + + var arrayBufferView = new Uint8Array(16 * 16 * 4); + + var img = document.createElement('img'); + img.onload = function() { + var texture = gl.createTexture(); + gl.bindTexture(gl.TEXTURE_2D, texture); + gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, gl.RGBA, gl.UNSIGNED_BYTE, img); + gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, + 16, 16, + gl.RGBA, gl.UNSIGNED_BYTE, arrayBufferView); + done(); + }; + img.src = iconUrl; + }); + + test('width', function() { + var canvas = document.createElement('canvas'); + assert.isNumber(canvas.width); + }); + + test('height', function() { + var canvas = document.createElement('canvas'); + assert.isNumber(canvas.height); + }); + + test('instanceof', function() { + assert.instanceOf(document.createElement('canvas'), HTMLCanvasElement); + }); + + test('constructor', function() { + assert.equal(HTMLCanvasElement, + document.createElement('canvas').constructor); + }); + +}); diff --git a/tests/ShadowDOM/js/HTMLContentElement.js b/tests/ShadowDOM/js/HTMLContentElement.js new file mode 100644 index 0000000..25b944c --- /dev/null +++ b/tests/ShadowDOM/js/HTMLContentElement.js @@ -0,0 +1,240 @@ +/* + * Copyright 2012 The Polymer Authors. All rights reserved. + * Use of this source code is goverened by a BSD-style + * license that can be found in the LICENSE file. + */ + +suite('HTMLContentElement', function() { + + var unwrap = ShadowDOMPolyfill.unwrap; + + test('select', function() { + var el = document.createElement('content'); + assert.equal(el.select, null); + + el.select = '.xxx'; + assert.equal(el.select, '.xxx'); + assert.isTrue(el.hasAttribute('select')); + assert.equal(el.getAttribute('select'), '.xxx'); + + el.select = '.xxx'; + assert.equal(el.select, '.xxx'); + assert.isTrue(el.hasAttribute('select')); + assert.equal(el.getAttribute('select'), '.xxx'); + }); + + test('getDistributedNodes', function() { + var host = document.createElement('div'); + host.innerHTML = 'ab'; + var a = host.firstChild; + var b = host.lastChild; + + var sr = host.createShadowRoot(); + sr.innerHTML = ''; + var content = sr.firstChild; + + assertArrayEqual(content.getDistributedNodes(), [a, b]); + + content.select = 'a'; + assertArrayEqual(content.getDistributedNodes(), [a]); + + content.select = 'b'; + assertArrayEqual(content.getDistributedNodes(), [b]); + }); + + test('getDistributedNodes add document fragment with content', function() { + var host = document.createElement('div'); + host.innerHTML = ' '; + var root = host.createShadowRoot(); + var df = document.createDocumentFragment(); + df.appendChild(document.createTextNode(' ')); + var content = df.appendChild(document.createElement('content')); + df.appendChild(document.createTextNode(' ')); + root.appendChild(df); + + assertArrayEqual(content.getDistributedNodes().length, 3); + }); + + test('getDistributedNodes add content deep inside tree', function() { + var host = document.createElement('div'); + host.innerHTML = ' '; + var root = host.createShadowRoot(); + var b = document.createElement('b'); + var content = b.appendChild(document.createElement('content')); + content.select = '*'; + root.appendChild(b); + + assert.equal(content.getDistributedNodes().length, 3); + assertArrayEqual(content.getDistributedNodes(), host.children); + }); + + test('getDistributedNodes add content deeper inside tree', function() { + var foo = document.createElement('div'); + var fooRoot = foo.createShadowRoot(); + fooRoot.innerHTML = '
' + + '
item1
item2
item3
' + + '
'; + + var bar = fooRoot.firstChild; + var barRoot = bar.createShadowRoot(); + barRoot.innerHTML = '
'; + + var zot = barRoot.firstChild; + var zotRoot = zot.createShadowRoot(); + zotRoot.innerHTML = ''; + var content = zotRoot.firstChild; + + assert.equal(content.getDistributedNodes().length, 3); + assertArrayEqual(content.getDistributedNodes(), fooRoot.firstChild.children); + }); + + test('Adding tree with content again', function() { + var host = document.createElement('div'); + host.innerHTML = '

Content

'; + + var t = document.createElement('template'); + t.innerHTML = '
[]
'; + + var sr = host.createShadowRoot(); + sr.appendChild(t.content.cloneNode(true)); + + host.offsetHeight; + assert.equal(unwrap(host).innerHTML, + '
[

Content

]
'); + }); + + test('adding a new content element to a shadow tree', function() { + var host = document.createElement('div'); + host.innerHTML = ''; + var a = host.firstChild; + var b = host.lastChild; + + var sr = host.createShadowRoot(); + sr.innerHTML = ''; + var c = sr.firstChild; + + host.offsetHeight; + assert.equal(unwrap(host).innerHTML, ''); + + var content = document.createElement('content'); + content.select = 'b'; + c.appendChild(content); + + host.offsetHeight; + assert.equal(unwrap(host).innerHTML, ''); + + c.removeChild(content); + host.offsetHeight; + assert.equal(unwrap(host).innerHTML, ''); + }); + + test('adding a new content element to a shadow tree 2', function() { + var host = document.createElement('div'); + host.innerHTML = ''; + var a = host.firstChild; + var b = host.lastChild; + + var sr = host.createShadowRoot(); + sr.innerHTML = ''; + var c = sr.firstChild; + + host.offsetHeight; + assert.equal(unwrap(host).innerHTML, ''); + + var d = document.createElement('d'); + var content = d.appendChild(document.createElement('content')); + content.select = 'b'; + c.appendChild(d); + + host.offsetHeight; + assert.equal(unwrap(host).innerHTML, ''); + + c.removeChild(d); + host.offsetHeight; + assert.equal(unwrap(host).innerHTML, ''); + }); + + test('restricting select further', function() { + var host = document.createElement('div'); + host.innerHTML = ''; + var a = host.firstChild; + var b = host.lastChild; + + var sr = host.createShadowRoot(); + sr.innerHTML = ''; + var c = sr.firstChild; + var content = c.firstChild; + + host.offsetHeight; + assert.equal(unwrap(host).innerHTML, ''); + + content.select = 'b'; + host.offsetHeight; + assert.equal(unwrap(host).innerHTML, ''); + }); + + test('Mutating content fallback', function() { + var host = document.createElement('div'); + host.innerHTML = ''; + var a = host.firstChild; + + var sr = host.createShadowRoot(); + sr.innerHTML = ''; + var content = sr.firstChild; + + host.offsetHeight; + assert.equal(unwrap(host).innerHTML, ''); + + content.textContent = 'fallback'; + host.offsetHeight; + assert.equal(unwrap(host).innerHTML, 'fallback'); + + var b = content.appendChild(document.createElement('b')); + host.offsetHeight; + assert.equal(unwrap(host).innerHTML, 'fallback'); + + content.removeChild(b); + host.offsetHeight; + assert.equal(unwrap(host).innerHTML, 'fallback'); + }); + + test('Mutating content fallback 2', function() { + var host = document.createElement('div'); + host.innerHTML = ''; + var a = host.firstChild; + + var sr = host.createShadowRoot(); + sr.innerHTML = ''; + var b = sr.firstChild; + var content = b.firstChild; + + host.offsetHeight; + assert.equal(unwrap(host).innerHTML, ''); + + content.textContent = 'fallback'; + host.offsetHeight; + assert.equal(unwrap(host).innerHTML, 'fallback'); + + var c = content.appendChild(document.createElement('c')); + host.offsetHeight; + assert.equal(unwrap(host).innerHTML, 'fallback'); + + content.removeChild(c); + host.offsetHeight; + assert.equal(unwrap(host).innerHTML, 'fallback'); + }); + + test('getDistributedNodes outside shadow dom', function() { + var content = document.createElement('content'); + assert.deepEqual(content.getDistributedNodes(), []); + }); + + test('instanceof', function() { + assert.instanceOf(document.createElement('content'), HTMLContentElement); + }); + + test('constructor', function() { + assert.equal(HTMLContentElement, + document.createElement('content').constructor); + }); +}); \ No newline at end of file diff --git a/tests/ShadowDOM/js/HTMLElement.js b/tests/ShadowDOM/js/HTMLElement.js new file mode 100644 index 0000000..e8c2859 --- /dev/null +++ b/tests/ShadowDOM/js/HTMLElement.js @@ -0,0 +1,113 @@ +/* + * Copyright 2013 The Polymer Authors. All rights reserved. + * Use of this source code is goverened by a BSD-style + * license that can be found in the LICENSE file. + */ + +suite('HTMLElement', function() { + + var div; + + setup(function() { + div = document.body.appendChild(document.createElement('div')); + div.style.cssText = + 'width: 100px; height: 100px; overflow: scroll;' + + 'position: absolute; top: 100px; left: 100px;' + + 'border: 10px solid red'; + var sr = div.createShadowRoot(); + var div2 = sr.appendChild(document.createElement('div')); + div2.style.cssText = 'width: 1000px; height: 1000px'; + }); + + teardown(function() { + if (div && div.parentNode) + div.parentNode.removeChild(div); + div = undefined; + }); + + test('scrollTop', function() { + assert.equal(div.scrollTop, 0); + div.scrollTop = 100; + assert.equal(div.scrollTop, 100); + }); + + test('scrollLeft', function() { + assert.equal(div.scrollLeft, 0); + div.scrollLeft = 100; + assert.equal(div.scrollLeft, 100); + }); + + test('scrollHeight', function() { + assert.equal(div.scrollHeight, 1000); + }); + + test('scrollWidth', function() { + assert.equal(div.scrollHeight, 1000); + }); + + test('clientHeight', function() { + div.style.overflow = 'hidden'; + assert.equal(div.clientHeight, 100); + }); + + test('clientLeft', function() { + div.style.overflow = 'hidden'; + assert.equal(div.clientLeft, 10); + }); + + test('clientTop', function() { + assert.equal(div.clientTop, 10); + }); + + test('clientWidth', function() { + div.style.overflow = 'hidden'; + assert.equal(div.clientWidth, 100); + }); + + test('offsetHeight', function() { + assert.equal(div.offsetHeight, 120); + }); + + test('offsetLeft', function() { + assert.equal(div.offsetLeft, 100); + }); + + test('offsetTop', function() { + assert.equal(div.offsetTop, 100); + }); + + test('offsetWidth', function() { + assert.equal(div.offsetWidth, 120); + }); + + test('script innerHTML', function() { + var script = document.createElement('script'); + var html = '{{y}}'; + script.innerHTML = html; + assert.equal(script.innerHTML, html); + }); + + test('script textContent', function() { + var script = document.createElement('script'); + var html = '{{y}}'; + script.innerHTML = html; + assert.equal(script.textContent, html); + }); + + test('comment innerHTML', function() { + var div = document.createElement('div'); + var comment = document.createComment('&\u00A0<>"'); + div.appendChild(comment); + assert.equal(div.innerHTML, ''); + }); + + test('hidden property', function() { + var div = document.createElement('div'); + assert.isFalse(div.hidden); + div.hidden = true; + assert.isTrue(div.hasAttribute('hidden')); + assert.equal(div.getAttribute('hidden'), ''); + div.hidden = false; + assert.isFalse(div.hasAttribute('hidden')); + }); +}); diff --git a/tests/ShadowDOM/js/HTMLFieldSetElement.js b/tests/ShadowDOM/js/HTMLFieldSetElement.js new file mode 100644 index 0000000..4cefe75 --- /dev/null +++ b/tests/ShadowDOM/js/HTMLFieldSetElement.js @@ -0,0 +1,25 @@ +/* + * Copyright 2013 The Polymer Authors. All rights reserved. + * Use of this source code is goverened by a BSD-style + * license that can be found in the LICENSE file. + */ + +suite('HTMLFieldSetElement', function() { + + test('form', function() { + var form = document.createElement('form'); + var fieldSet = document.createElement('fieldset'); + form.appendChild(fieldSet); + assert.equal(fieldSet.form, form); + }); + + test('instanceof', function() { + assert.instanceOf(document.createElement('fieldset'), HTMLFieldSetElement); + }); + + test('constructor', function() { + assert.equal(HTMLFieldSetElement, + document.createElement('fieldset').constructor); + }); + +}); diff --git a/tests/ShadowDOM/js/HTMLFormElement.js b/tests/ShadowDOM/js/HTMLFormElement.js new file mode 100644 index 0000000..cfb58ce --- /dev/null +++ b/tests/ShadowDOM/js/HTMLFormElement.js @@ -0,0 +1,26 @@ +/* + * Copyright 2014 The Polymer Authors. All rights reserved. + * Use of this source code is governed by a BSD-style + * license that can be found in the LICENSE file. + */ + +suite('HTMLFormElement', function() { + + test('elements', function() { + var form = document.createElement('form'); + var input = document.createElement('input'); + form.appendChild(input); + assert.instanceOf(form.elements, HTMLCollection); + assert.equal(form.elements.length, 1); + }); + + test('instanceof', function() { + assert.instanceOf(document.createElement('form'), HTMLFormElement); + }); + + test('constructor', function() { + assert.equal(HTMLFormElement, + document.createElement('form').constructor); + }); + +}); diff --git a/tests/ShadowDOM/js/HTMLHeadElement.js b/tests/ShadowDOM/js/HTMLHeadElement.js new file mode 100644 index 0000000..ebf0202 --- /dev/null +++ b/tests/ShadowDOM/js/HTMLHeadElement.js @@ -0,0 +1,106 @@ +/* + * Copyright 2013 The Polymer Authors. All rights reserved. + * Use of this source code is goverened by a BSD-style + * license that can be found in the LICENSE file. + */ + +suite('HTMLHeadElement', function() { + + var wrap = ShadowDOMPolyfill.wrap; + + var div, div2; + + teardown(function() { + if (div && div.parentNode) + div.parentNode.removeChild(div); + if (div2 && div2.parentNode) + div2.parentNode.removeChild(div2); + div = div2 = undefined; + }); + + test('appendChild', function() { + div = document.createElement('div'); + document.head.appendChild(div); + assert.equal(wrap(document.head.lastChild), div); + }); + + test('appendChild (through wrapper)', function() { + var doc = wrap(document); + div = doc.createElement('div'); + doc.body.appendChild(div); + assert.equal(doc.body.lastChild, div); + }); + + test('insertBefore', function() { + div = document.createElement('div'); + document.head.appendChild(div); + div2 = document.createElement('div'); + document.head.insertBefore(div2, div); + assert.equal(wrap(document.head.lastChild), div); + assert.equal(div2.nextSibling, div); + assert.equal(div.previousSibling, div2); + }); + + test('insertBefore (through wrapper)', function() { + var doc = wrap(document); + div = doc.createElement('div'); + doc.body.appendChild(div); + div2 = doc.createElement('div'); + doc.body.insertBefore(div2, div); + assert.equal(doc.body.lastChild, div); + assert.equal(div2.nextSibling, div); + assert.equal(div.previousSibling, div2); + }); + + test('replaceChild', function() { + div = document.createElement('div'); + document.head.appendChild(div); + div2 = document.createElement('div'); + document.head.replaceChild(div2, div); + assert.equal(wrap(document.head.lastChild), div2); + assert.isNull(div.parentNode); + }); + + test('replaceChild (through wrapper)', function() { + var doc = wrap(document); + div = doc.createElement('div'); + doc.body.appendChild(div); + div2 = doc.createElement('div'); + doc.body.replaceChild(div2, div); + assert.equal(doc.body.lastChild, div2); + assert.isNull(div.parentNode); + }); + + test('removeChild', function() { + div = document.createElement('div'); + document.head.appendChild(div); + document.head.removeChild(div); + assert.isNull(div.parentNode); + }); + + test('removeChild (through wrapper)', function() { + var doc = wrap(document); + div = doc.createElement('div'); + doc.body.appendChild(div); + doc.body.removeChild(div); + assert.isNull(div.parentNode); + }); + + test('document.head.contains', function() { + var doc = wrap(document); + assert.isTrue(doc.head.contains(doc.head.firstChild)); + assert.isTrue(doc.head.contains(document.head.firstChild)); + assert.isTrue(document.head.contains(doc.head.firstChild)); + assert.isTrue(document.head.contains(document.head.firstChild)); + }); + + test('instanceof', function() { + assert.instanceOf(document.createElement('head'), HTMLHeadElement); + }); + + test('constructor', function() { + assert.equal(HTMLHeadElement, + document.createElement('head').constructor); + }); + +}); diff --git a/tests/ShadowDOM/js/HTMLHtmlElement.js b/tests/ShadowDOM/js/HTMLHtmlElement.js new file mode 100644 index 0000000..023f46e --- /dev/null +++ b/tests/ShadowDOM/js/HTMLHtmlElement.js @@ -0,0 +1,63 @@ +/* + * Copyright 2013 The Polymer Authors. All rights reserved. + * Use of this source code is goverened by a BSD-style + * license that can be found in the LICENSE file. + */ + +suite('HTMLHtmlElement', function() { + + var wrap = ShadowDOMPolyfill.wrap; + + test('instanceof', function() { + var doc = wrap(document); + assert.instanceOf(doc.documentElement, HTMLHtmlElement); + assert.equal(wrap(document.documentElement), doc.documentElement); + }); + + test('constructor', function() { + assert.equal(HTMLHtmlElement, + document.createElement('html').constructor); + }); + + test('appendChild', function() { + var doc = wrap(document); + + var a = document.createComment('a'); + var b = document.createComment('b'); + + document.documentElement.appendChild(a); + assert.equal(doc.documentElement.lastChild, a); + + doc.documentElement.appendChild(b); + assert.equal(doc.documentElement.lastChild, b); + }); + + test('insertBefore', function() { + var comment = document.createComment('comment'); + var root = document.documentElement; + root.insertBefore(comment, root.firstChild); + assert.equal(wrap(root.firstChild), comment); + }); + + test('replaceChild', function() { + var comment = document.createComment('comment'); + var comment2 = document.createComment('comment2'); + var root = document.documentElement; + root.insertBefore(comment, root.firstChild); + assert.equal(wrap(root.firstChild), comment); + root.replaceChild(comment2, root.firstChild); + }); + + test('matches', function() { + // From jQuery. + var html = document.documentElement; + var matches = html.matchesSelector || + html.mozMatchesSelector || + html.webkitMatchesSelector || + html.msMatchesSelector; + + assert.isTrue(matches.call(document.body, 'body')); + assert.isTrue(matches.call(wrap(document.body), 'body')); + }); + +}); diff --git a/tests/ShadowDOM/js/HTMLImageElement.js b/tests/ShadowDOM/js/HTMLImageElement.js new file mode 100644 index 0000000..adac85b --- /dev/null +++ b/tests/ShadowDOM/js/HTMLImageElement.js @@ -0,0 +1,66 @@ +/* + * Copyright 2013 The Polymer Authors. All rights reserved. + * Use of this source code is goverened by a BSD-style + * license that can be found in the LICENSE file. + */ + +suite('HTMLImageElement', function() { + + var iconUrl = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAHklEQVQ4T2Nk+A+EFADGUQMYRsOAYTQMgHloGKQDAJXkH/HZpKBrAAAAAElFTkSuQmCC'; + + test('instanceof', function() { + var img = document.createElement('img'); + assert.instanceOf(img, HTMLImageElement); + assert.instanceOf(img, Image); + assert.instanceOf(img, HTMLElement); + }); + + test('constructor', function() { + assert.equal(HTMLImageElement, document.createElement('img').constructor); + assert.equal(HTMLImageElement, new Image().constructor); + }); + + test('Image', function() { + var img = new Image(); + assert.instanceOf(img, HTMLImageElement); + assert.instanceOf(img, Image); + assert.instanceOf(img, HTMLElement); + }); + + test('Image arguments', function() { + var img = new Image(32); + assert.equal(img.width, 32); + assert.equal(img.height, 0); + + var img = new Image(undefined, 32); + assert.equal(img.width, 0); + assert.equal(img.height, 32); + }); + + test('Image called as function', function() { + assert.throws(Image, TypeError); + }); + + test('Image basics', function() { + var img = new Image(); + assert.equal('img', img.localName); + + var div = document.createElement('div'); + div.appendChild(img); + + assert.equal(div.firstChild, img); + assert.equal('
', div.outerHTML); + + assert.equal(img.width, 0); + assert.equal(img.height, 0); + }); + + test('Image load', function(done) { + var img = new Image(); + img.addEventListener('load', function() { + done(); + }); + img.src = iconUrl; + }); + +}); diff --git a/tests/ShadowDOM/js/HTMLInputElement.js b/tests/ShadowDOM/js/HTMLInputElement.js new file mode 100644 index 0000000..8eb1b9e --- /dev/null +++ b/tests/ShadowDOM/js/HTMLInputElement.js @@ -0,0 +1,25 @@ +/* + * Copyright 2013 The Polymer Authors. All rights reserved. + * Use of this source code is goverened by a BSD-style + * license that can be found in the LICENSE file. + */ + +suite('HTMLInputElement', function() { + + test('form', function() { + var form = document.createElement('form'); + var input = document.createElement('input'); + form.appendChild(input); + assert.equal(input.form, form); + }); + + test('instanceof', function() { + assert.instanceOf(document.createElement('input'), HTMLInputElement); + }); + + test('constructor', function() { + assert.equal(HTMLInputElement, + document.createElement('input').constructor); + }); + +}); diff --git a/tests/ShadowDOM/js/HTMLKeygenElement.js b/tests/ShadowDOM/js/HTMLKeygenElement.js new file mode 100644 index 0000000..5b34059 --- /dev/null +++ b/tests/ShadowDOM/js/HTMLKeygenElement.js @@ -0,0 +1,28 @@ +/* + * Copyright 2013 The Polymer Authors. All rights reserved. + * Use of this source code is goverened by a BSD-style + * license that can be found in the LICENSE file. + */ + +suite('HTMLKeygenElement', function() { + // Not implemented in Firefox. + if (!window.HTMLKeygenElement) + return; + + test('form', function() { + var form = document.createElement('form'); + var keygen = document.createElement('keygen'); + form.appendChild(keygen); + assert.equal(keygen.form, form); + }); + + test('instanceof', function() { + assert.instanceOf(document.createElement('keygen'), HTMLKeygenElement); + }); + + test('constructor', function() { + assert.equal(HTMLKeygenElement, + document.createElement('keygen').constructor); + }); + +}); diff --git a/tests/ShadowDOM/js/HTMLLabelElement.js b/tests/ShadowDOM/js/HTMLLabelElement.js new file mode 100644 index 0000000..b55155e --- /dev/null +++ b/tests/ShadowDOM/js/HTMLLabelElement.js @@ -0,0 +1,25 @@ +/* + * Copyright 2013 The Polymer Authors. All rights reserved. + * Use of this source code is goverened by a BSD-style + * license that can be found in the LICENSE file. + */ + +suite('HTMLLabelElement', function() { + + test('form', function() { + var form = document.createElement('form'); + var label = document.createElement('label'); + form.appendChild(label); + assert.equal(label.form, form); + }); + + test('instanceof', function() { + assert.instanceOf(document.createElement('label'), HTMLLabelElement); + }); + + test('constructor', function() { + assert.equal(HTMLLabelElement, + document.createElement('label').constructor); + }); + +}); diff --git a/tests/ShadowDOM/js/HTMLLegendElement.js b/tests/ShadowDOM/js/HTMLLegendElement.js new file mode 100644 index 0000000..804a40b --- /dev/null +++ b/tests/ShadowDOM/js/HTMLLegendElement.js @@ -0,0 +1,28 @@ +/* + * Copyright 2013 The Polymer Authors. All rights reserved. + * Use of this source code is goverened by a BSD-style + * license that can be found in the LICENSE file. + */ + +suite('HTMLLegendElement', function() { + + test('form', function() { + var form = document.createElement('form'); + var fieldSet = document.createElement('fieldset'); + var legend = document.createElement('legend'); + form.appendChild(fieldSet); + fieldSet.appendChild(legend); + assert.equal(legend.form, form); + }); + + test('instanceof', function() { + assert.instanceOf(document.createElement('legend'), HTMLLegendElement); + }); + + test('constructor', function() { + assert.equal(HTMLLegendElement, + document.createElement('legend').constructor); + }); + + +}); diff --git a/tests/ShadowDOM/js/HTMLObjectElement.js b/tests/ShadowDOM/js/HTMLObjectElement.js new file mode 100644 index 0000000..f774554 --- /dev/null +++ b/tests/ShadowDOM/js/HTMLObjectElement.js @@ -0,0 +1,25 @@ +/* + * Copyright 2013 The Polymer Authors. All rights reserved. + * Use of this source code is goverened by a BSD-style + * license that can be found in the LICENSE file. + */ + +suite('HTMLObjectElement', function() { + + test('form', function() { + var form = document.createElement('form'); + var object = document.createElement('object'); + form.appendChild(object); + assert.equal(object.form, form); + }); + + test('instanceof', function() { + assert.instanceOf(document.createElement('object'), HTMLObjectElement); + }); + + test('constructor', function() { + assert.equal(HTMLObjectElement, + document.createElement('object').constructor); + }); + +}); diff --git a/tests/ShadowDOM/js/HTMLOptionElement.js b/tests/ShadowDOM/js/HTMLOptionElement.js new file mode 100644 index 0000000..66acf89 --- /dev/null +++ b/tests/ShadowDOM/js/HTMLOptionElement.js @@ -0,0 +1,86 @@ +/* + * Copyright 2013 The Polymer Authors. All rights reserved. + * Use of this source code is goverened by a BSD-style + * license that can be found in the LICENSE file. + */ + +suite('HTMLOptionElement', function() { + + test('form', function() { + var form = document.createElement('form'); + var select = document.createElement('select'); + var option = document.createElement('option'); + form.appendChild(select); + select.appendChild(option); + assert.equal(option.form, form); + }); + + test('instanceof', function() { + var option = document.createElement('option'); + assert.instanceOf(option, HTMLOptionElement); + assert.instanceOf(option, Option); + assert.instanceOf(option, HTMLElement); + }); + + test('constructor', function() { + assert.equal(HTMLOptionElement, + document.createElement('option').constructor); + assert.equal(HTMLOptionElement, new Option().constructor); + }); + + test('Option', function() { + var option = new Option(); + assert.instanceOf(option, HTMLOptionElement); + assert.instanceOf(option, Option); + assert.instanceOf(option, HTMLElement); + }); + + test('Option arguments', function() { + var option = new Option(); + assert.equal(option.text, ''); + assert.equal(option.value, ''); + assert.isFalse(option.defaultSelected); + assert.isFalse(option.selected); + + var option = new Option(' more text '); + assert.equal(option.text, 'more text'); + // on IE10, the value includes the surrounding spaces; trim to workaround + assert.equal(option.value.trim(), 'more text'); + assert.isFalse(option.defaultSelected); + assert.isFalse(option.selected); + + var option = new Option('text', 'value'); + assert.equal(option.text, 'text'); + assert.equal(option.value, 'value'); + assert.isFalse(option.defaultSelected); + assert.isFalse(option.selected); + + var option = new Option('text', 'value', true); + assert.equal(option.text, 'text'); + assert.equal(option.value, 'value'); + assert.isTrue(option.defaultSelected); + assert.isFalse(option.selected); + + var option = new Option('text', 'value', true, true); + assert.equal(option.text, 'text'); + assert.equal(option.value, 'value'); + assert.isTrue(option.defaultSelected); + assert.isTrue(option.selected); + }); + + test('Option called as function', function() { + assert.throws(Option, TypeError); + }); + + test('Option basics', function() { + var option = new Option(); + assert.equal('option', option.localName); + + var select = document.createElement('select'); + select.appendChild(option); + + assert.equal(select.firstChild, option); + assert.equal('', select.outerHTML); + }); + +}); diff --git a/tests/ShadowDOM/js/HTMLOutputElement.js b/tests/ShadowDOM/js/HTMLOutputElement.js new file mode 100644 index 0000000..fec297d --- /dev/null +++ b/tests/ShadowDOM/js/HTMLOutputElement.js @@ -0,0 +1,28 @@ +/* + * Copyright 2013 The Polymer Authors. All rights reserved. + * Use of this source code is goverened by a BSD-style + * license that can be found in the LICENSE file. + */ + +suite('HTMLOutputElement', function() { + // Not implemented in IE10. + if (!window.HTMLOutputElement) + return; + + test('form', function() { + var form = document.createElement('form'); + var output = document.createElement('output'); + form.appendChild(output); + assert.equal(output.form, form); + }); + + test('instanceof', function() { + assert.instanceOf(document.createElement('output'), HTMLOutputElement); + }); + + test('constructor', function() { + assert.equal(HTMLOutputElement, + document.createElement('output').constructor); + }); + +}); diff --git a/tests/ShadowDOM/js/HTMLSelectElement.js b/tests/ShadowDOM/js/HTMLSelectElement.js new file mode 100644 index 0000000..0ffaa94 --- /dev/null +++ b/tests/ShadowDOM/js/HTMLSelectElement.js @@ -0,0 +1,95 @@ +/* + * Copyright 2013 The Polymer Authors. All rights reserved. + * Use of this source code is goverened by a BSD-style + * license that can be found in the LICENSE file. + */ + +suite('HTMLSelectElement', function() { + + test('form', function() { + var form = document.createElement('form'); + var select = document.createElement('select'); + form.appendChild(select); + assert.equal(select.form, form); + }); + + test('add', function() { + var select = document.createElement('select'); + + var a = document.createElement('option'); + a.text = 'a'; + select.add(a); + assert.equal(select.firstChild, a); + + var b = document.createElement('option'); + b.text = 'b'; + select.add(b, a); + assert.equal(select.firstChild, b); + assert.equal(select.lastChild, a); + + // https://code.google.com/p/chromium/issues/detail?id=345345 + if (/WebKit/.test(navigator.userAgent)) + return; + + var c = document.createElement('option'); + c.text = 'c'; + select.add(c, 1); + assert.equal(select.firstChild, b); + assert.equal(b.nextSibling, c); + assert.equal(select.lastChild, a); + }); + + test('remove', function() { + var select = document.createElement('select'); + + var a = document.createElement('option'); + a.text = 'a'; + select.appendChild(a); + + var b = document.createElement('option'); + b.text = 'b'; + select.appendChild(b); + + var c = document.createElement('option'); + c.text = 'c'; + select.appendChild(c); + + select.remove(a); + assert.equal(select.firstChild, b); + assert.equal(select.lastChild, c); + + select.remove(1); + assert.equal(select.firstChild, b); + assert.equal(select.lastChild, b); + }); + + test('remove no args', function() { + var div = document.createElement('div'); + var select = div.appendChild(document.createElement('select')); + + var a = document.createElement('option'); + a.text = 'a'; + select.appendChild(a); + var b = document.createElement('option'); + b.text = 'b'; + select.appendChild(b); + + assert.equal(select.parentNode, div); + + select.remove(); + assert.equal(select.firstChild, a); + assert.equal(select.lastChild, b); + assert.equal(select.parentNode, null); + assert.equal(div.firstChild, null); + }); + + test('instanceof', function() { + assert.instanceOf(document.createElement('select'), HTMLSelectElement); + }); + + test('constructor', function() { + assert.equal(HTMLSelectElement, + document.createElement('select').constructor); + }); + +}); diff --git a/tests/ShadowDOM/js/HTMLShadowElement.js b/tests/ShadowDOM/js/HTMLShadowElement.js new file mode 100644 index 0000000..83c56e7 --- /dev/null +++ b/tests/ShadowDOM/js/HTMLShadowElement.js @@ -0,0 +1,118 @@ +/* + * Copyright 2013 The Polymer Authors. All rights reserved. + * Use of this source code is goverened by a BSD-style + * license that can be found in the LICENSE file. + */ + +suite('HTMLShadowElement', function() { + + var unwrap = ShadowDOMPolyfill.unwrap; + + test('instanceof HTMLShadowElement', function() { + var host = document.createElement('div'); + host.innerHTML = 'ab'; + var a = host.firstChild; + var b = host.lastChild; + + var sr = host.createShadowRoot(); + sr.innerHTML = 'abc'; + var shadow = sr.firstElementChild; + + host.offsetWidth; + assert.isTrue(shadow instanceof HTMLShadowElement); + + var sr2 = host.createShadowRoot(); + sr2.innerHTML = 'def'; + var shadow2 = sr2.firstElementChild; + + host.offsetWidth; + assert.isTrue(shadow instanceof HTMLShadowElement); + + assert.isTrue(shadow2 instanceof HTMLShadowElement); + + assert.equal(unwrap(host).innerHTML, 'daabcf'); + }); + + test('constructor', function() { + assert.equal(HTMLShadowElement, + document.createElement('shadow').constructor); + }); + + test('adding a new shadow element to a shadow tree', function() { + var host = document.createElement('div'); + host.innerHTML = ''; + var a = host.firstChild; + + var sr = host.createShadowRoot(); + sr.innerHTML = ''; + + var sr2 = host.createShadowRoot(); + sr2.innerHTML = ''; + var b = sr2.firstChild; + + host.offsetHeight; + assert.equal(unwrap(host).innerHTML, ''); + + var shadow = document.createElement('shadow'); + b.appendChild(shadow); + + host.offsetHeight; + assert.equal(unwrap(host).innerHTML, ''); + + b.removeChild(shadow); + host.offsetHeight; + assert.equal(unwrap(host).innerHTML, ''); + }); + + test('Mutating shadow fallback (fallback support has been removed)', function() { + var host = document.createElement('div'); + host.innerHTML = ''; + var a = host.firstChild; + + var sr = host.createShadowRoot(); + sr.innerHTML = ''; + var shadow = sr.firstChild; + + host.offsetHeight; + assert.equal(unwrap(host).innerHTML, ''); + + shadow.textContent = 'fallback'; + host.offsetHeight; + assert.equal(unwrap(host).innerHTML, ''); + + var b = shadow.appendChild(document.createElement('b')); + host.offsetHeight; + assert.equal(unwrap(host).innerHTML, ''); + + shadow.removeChild(b); + host.offsetHeight; + assert.equal(unwrap(host).innerHTML, ''); + }); + + test('Mutating shadow fallback 2 (fallback support has been removed)', + function() { + var host = document.createElement('div'); + host.innerHTML = ''; + var a = host.firstChild; + + var sr = host.createShadowRoot(); + sr.innerHTML = ''; + var b = sr.firstChild; + var shadow = b.firstChild; + + host.offsetHeight; + assert.equal(unwrap(host).innerHTML, ''); + + shadow.textContent = 'fallback'; + host.offsetHeight; + assert.equal(unwrap(host).innerHTML, ''); + + var c = shadow.appendChild(document.createElement('c')); + host.offsetHeight; + assert.equal(unwrap(host).innerHTML, ''); + + shadow.removeChild(c); + host.offsetHeight; + assert.equal(unwrap(host).innerHTML, ''); + }); +}); diff --git a/tests/ShadowDOM/js/HTMLTableElement.js b/tests/ShadowDOM/js/HTMLTableElement.js new file mode 100644 index 0000000..8acb306 --- /dev/null +++ b/tests/ShadowDOM/js/HTMLTableElement.js @@ -0,0 +1,85 @@ +/* + * Copyright 2014 The Polymer Authors. All rights reserved. + * Use of this source code is goverened by a BSD-style + * license that can be found in the LICENSE file. + */ + +suite('HTMLTableElement', function() { + + test('instanceof', function() { + var table = createTable(); + assert.instanceOf(table, HTMLTableElement); + }); + + test('constructor', function() { + assert.equal(HTMLTableElement, + document.createElement('table').constructor); + }); + + test('caption', function() { + var table = createTable(); + assert.equal(table.caption.localName, 'caption'); + }); + + test('createCaption', function() { + var table = createTable(); + var caption = table.createCaption(); + assert.equal(caption.localName, 'caption'); + assert.instanceOf(caption, HTMLElement); + }); + + test('tHead', function() { + var table = createTable(); + assert.equal(table.tHead.localName, 'thead'); + assert.instanceOf(table.tHead, HTMLTableSectionElement); + }); + + test('createTHead', function() { + var table = createTable(); + var thead = table.createTHead(); + assert.equal(thead.localName, 'thead'); + assert.instanceOf(thead, HTMLTableSectionElement); + }); + + test('tFoot', function() { + var table = createTable(); + assert.equal(table.tFoot.localName, 'tfoot'); + assert.instanceOf(table.tFoot, HTMLTableSectionElement); + }); + + test('createTFoot', function() { + var table = createTable(); + var tfoot = table.createTFoot(); + assert.equal(tfoot.localName, 'tfoot'); + assert.instanceOf(tfoot, HTMLTableSectionElement); + }); + + test('tBodies', function() { + var table = createTable(); + assert.instanceOf(table.tBodies, HTMLCollection); + assert.equal(table.tBodies.length, 2); + + assert.equal(table.tBodies[0], table.querySelector('tbody')); + }); + + test('createTBody', function() { + var table = createTable(); + var tbody = table.createTBody(); + assert.equal(tbody.localName, 'tbody'); + assert.instanceOf(tbody, HTMLTableSectionElement); + }); + + test('rows', function() { + var table = createTable(); + assert.instanceOf(table.rows, HTMLCollection); + assert.equal(table.rows.length, 8); + }); + + test('insertRow', function() { + var table = createTable(); + var tr = table.insertRow(1); + assert.instanceOf(tr, HTMLTableRowElement); + assert.equal(tr.localName, 'tr'); + }); + +}); diff --git a/tests/ShadowDOM/js/HTMLTableRowElement.js b/tests/ShadowDOM/js/HTMLTableRowElement.js new file mode 100644 index 0000000..a07d326 --- /dev/null +++ b/tests/ShadowDOM/js/HTMLTableRowElement.js @@ -0,0 +1,35 @@ +/* + * Copyright 2014 The Polymer Authors. All rights reserved. + * Use of this source code is goverened by a BSD-style + * license that can be found in the LICENSE file. + */ + +suite('HTMLTableRowElement', function() { + + test('instanceof', function() { + var table = createTable(); + var row = table.querySelector('tr'); + assert.instanceOf(row, HTMLTableRowElement); + }); + + test('constructor', function() { + var table = createTable(); + var row = table.querySelector('tr'); + assert.equal(HTMLTableRowElement, row.constructor); + }); + + test('cells', function() { + var table = createTable(); + var row = table.querySelector('tr'); + assert.instanceOf(row.cells, HTMLCollection); + assert.equal(row.cells.length, 3); + }); + + test('insertCell', function() { + var table = createTable(); + var row = table.querySelector('tr'); + var newCell = row.insertCell(0); + assert.equal(newCell.localName, 'td'); + }); + +}); diff --git a/tests/ShadowDOM/js/HTMLTableSectionElement.js b/tests/ShadowDOM/js/HTMLTableSectionElement.js new file mode 100644 index 0000000..3c56265 --- /dev/null +++ b/tests/ShadowDOM/js/HTMLTableSectionElement.js @@ -0,0 +1,58 @@ +/* + * Copyright 2014 The Polymer Authors. All rights reserved. + * Use of this source code is goverened by a BSD-style + * license that can be found in the LICENSE file. + */ + +suite('HTMLTableSectionElement', function() { + + test('instanceof', function() { + var table = createTable(); + var thead = table.querySelector('thead'); + assert.instanceOf(thead, HTMLTableSectionElement); + var tfoot = table.querySelector('tfoot'); + assert.instanceOf(tfoot, HTMLTableSectionElement); + }); + + test('constructor', function() { + var table = createTable(); + var thead = table.querySelector('thead'); + assert.equal(HTMLTableSectionElement, thead.constructor); + var tfoot = table.querySelector('tfoot'); + assert.equal(HTMLTableSectionElement, tfoot.constructor); + }); + + test('rows', function() { + var table = createTable(); + var thead = table.querySelector('thead'); + assert.instanceOf(thead.rows, HTMLCollection); + assert.equal(thead.rows.length, 2); + + var tbody = table.querySelector('tbody'); + assert.instanceOf(tbody.rows, HTMLCollection); + assert.equal(tbody.rows.length, 2); + + var tfoot = table.querySelector('tfoot'); + assert.instanceOf(tfoot.rows, HTMLCollection); + assert.equal(tfoot.rows.length, 2); + }); + + test('insertRow', function() { + var table = createTable(); + var thead = table.querySelector('thead'); + var tr = thead.insertRow(1); + assert.instanceOf(tr, HTMLTableRowElement); + assert.equal(tr.localName, 'tr'); + + var tbody = table.querySelector('tbody'); + tr = thead.insertRow(1); + assert.instanceOf(tr, HTMLTableRowElement); + assert.equal(tr.localName, 'tr'); + + var tfoot = table.querySelector('tfoot'); + tr = thead.insertRow(1); + assert.instanceOf(tr, HTMLTableRowElement); + assert.equal(tr.localName, 'tr'); + }); + +}); diff --git a/tests/ShadowDOM/js/HTMLTemplateElement.js b/tests/ShadowDOM/js/HTMLTemplateElement.js new file mode 100644 index 0000000..82a4a43 --- /dev/null +++ b/tests/ShadowDOM/js/HTMLTemplateElement.js @@ -0,0 +1,156 @@ +/* + * Copyright 2013 The Polymer Authors. All rights reserved. + * Use of this source code is goverened by a BSD-style + * license that can be found in the LICENSE file. + */ + +suite('HTML Template Element', function() { + + test('content', function() { + var div = document.createElement('div'); + div.innerHTML = ''; + var template = div.firstChild; + var content = template.content; + + assert.isNull(template.firstChild); + assert.equal(content.childNodes.length, 2); + assert.equal(content.firstChild.tagName, 'A'); + assert.equal(content.lastChild.tagName, 'B'); + }); + + test('document', function() { + var div = document.createElement('div'); + div.innerHTML = ''; + var templateA = div.firstChild; + var templateB= div.lastChild; + var contentA = templateA.content; + var contentB = templateB.content; + + assert.notEqual(templateA.ownerDocument, contentB.ownerDocument); + assert.equal(contentA.ownerDocument, contentB.ownerDocument); + }); + + test('get innerHTML', function() { + var div = document.createElement('div'); + div.innerHTML = ''; + var template = div.firstChild; + + assert.equal(template.innerHTML, ''); + + assert.equal(div.innerHTML, ''); + }); + + test('get outerHTML', function() { + var div = document.createElement('div'); + div.innerHTML = ''; + var template = div.firstChild; + + assert.equal(template.outerHTML, ''); + assert.equal(div.outerHTML, + '
'); + }); + + test('set innerHTML', function() { + var div = document.createElement('div'); + div.innerHTML = ''; + var template = div.firstChild; + template.innerHTML = 'ce'; + + assert.equal(template.innerHTML, 'ce'); + + expectStructure(template, { + parentNode: div + }) + + var content = template.content; + var c = content.firstChild; + var d = c.nextSibling; + var e = d.nextSibling; + + assert.equal(c.textContent, 'c'); + assert.equal(d.tagName, 'D'); + assert.equal(e.textContent, 'e'); + + expectStructure(content, { + firstChild: c, + lastChild: e + }); + expectStructure(c, { + parentNode: content, + nextSibling: d + }); + expectStructure(d, { + parentNode: content, + previousSibling: c, + nextSibling: e + }); + expectStructure(e, { + parentNode: content, + previousSibling: d + }); + }); + + test('Mutation events', function() { + var div = document.createElement('div'); + div.innerHTML = ''; + + var count = 0; + function handleEvent(e) { + count++; + } + + div.addEventListener('DOMAttrModified', handleEvent, true); + div.addEventListener('DOMAttributeNameChanged', handleEvent, true); + div.addEventListener('DOMCharacterDataModified', handleEvent, true); + div.addEventListener('DOMElementNameChanged', handleEvent, true); + div.addEventListener('DOMNodeInserted', handleEvent, true); + div.addEventListener('DOMNodeInsertedIntoDocument', handleEvent, true); + div.addEventListener('DOMNodeRemoved', handleEvent, true); + div.addEventListener('DOMNodeRemovedFromDocument', handleEvent, true); + div.addEventListener('DOMSubtreeModified', handleEvent, true); + + var template = div.firstChild; + assert.instanceOf(template.content, DocumentFragment); + assert.instanceOf(template.content.firstChild, Text); + assert.instanceOf(template.content.firstElementChild, HTMLAnchorElement); + + assert.equal(count, 0); + }); + + test('cloneNode', function() { + var div = document.createElement('div'); + div.innerHTML = ''; + var template = div.firstChild; + + var clone = template.cloneNode(true); + assert.equal(clone.outerHTML, ''); + + clone = div.cloneNode(true); + assert.equal(clone.outerHTML, + '
'); + }); + + test('importNode', function() { + var doc2 = document.implementation.createHTMLDocument(''); + var div = doc2.createElement('div'); + div.innerHTML = ''; + var template = div.firstChild; + + var clone = document.importNode(template, true); + assert.equal(clone.outerHTML, ''); + + clone = document.importNode(div, true); + assert.equal(clone.outerHTML, + '
'); + }); + + test('instanceOf', function() { + assert.instanceOf(document.createElement('template'), HTMLTemplateElement); + }); + + test('constructor', function() { + assert.equal(HTMLTemplateElement, + document.createElement('template').constructor); + }); + +}); \ No newline at end of file diff --git a/tests/ShadowDOM/js/HTMLTextAreaElement.js b/tests/ShadowDOM/js/HTMLTextAreaElement.js new file mode 100644 index 0000000..9063e80 --- /dev/null +++ b/tests/ShadowDOM/js/HTMLTextAreaElement.js @@ -0,0 +1,25 @@ +/* + * Copyright 2013 The Polymer Authors. All rights reserved. + * Use of this source code is goverened by a BSD-style + * license that can be found in the LICENSE file. + */ + +suite('HTMLTextAreaElement', function() { + + test('form', function() { + var form = document.createElement('form'); + var textArea = document.createElement('textarea'); + form.appendChild(textArea); + assert.equal(textArea.form, form); + }); + + test('instanceof', function() { + assert.instanceOf(document.createElement('textarea'), HTMLTextAreaElement); + }); + + test('constructor', function() { + assert.equal(HTMLTextAreaElement, + document.createElement('textarea').constructor); + }); + +}); diff --git a/tests/ShadowDOM/js/MutationObserver.js b/tests/ShadowDOM/js/MutationObserver.js new file mode 100644 index 0000000..8dc3edf --- /dev/null +++ b/tests/ShadowDOM/js/MutationObserver.js @@ -0,0 +1,295 @@ +/* + * Copyright 2013 The Polymer Authors. All rights reserved. + * Use of this source code is goverened by a BSD-style + * license that can be found in the LICENSE file. + */ + +suite('MutationObserver', function() { + + var wrap = ShadowDOMPolyfill.wrap; + var addedNodes = [], removedNodes = []; + var div; + + function newValue() { + return Date.now(); + } + + setup(function() { + addedNodes = []; + removedNodes = []; + }); + + teardown(function() { + addedNodes = undefined; + removedNodes = undefined; + if (div) { + if (div.parentNode) + div.parentNode.removeChild(div); + div = undefined; + } + }); + + function mergeRecords(records) { + records.forEach(function(record) { + if (record.addedNodes) + addedNodes.push.apply(addedNodes, record.addedNodes); + if (record.removedNodes) + removedNodes.push.apply(removedNodes, record.removedNodes); + }); + } + + test('target', function(done) { + if (!window.MutationObserver) { + done(); + return; + } + + var div = document.createElement('div'); + + var mo = new MutationObserver(function(records, observer) { + assert.equal(this, mo); + assert.equal(observer, mo); + assert.equal(records[0].type, 'attributes'); + assert.equal(records[0].target, div); + mo.disconnect(); + done(); + }); + mo.observe(div, { + attributes: true + }); + div.setAttribute('a', newValue()); + }); + + test('addedNodes', function(done) { + if (!window.MutationObserver) { + done(); + return; + } + + div = document.body.appendChild(document.createElement('div')); + + var mo = new MutationObserver(function(records, observer) { + mergeRecords(records); + assert.equal(records[0].type, 'childList'); + assert.equal(records[0].target, div); + assert.equal(addedNodes.length, 2); + assert.equal(addedNodes[0], a); + assert.equal(addedNodes[1], b); + mo.disconnect(); + done(); + }); + mo.observe(div, { + childList: true + }); + div.innerHTML = ''; + var a = div.firstChild; + var b = div.lastChild; + }); + + test('addedNodes siblings', function(done) { + if (!window.MutationObserver) { + done(); + return; + } + + div = document.body.appendChild(document.createElement('div')); + + var mo = new MutationObserver(function(records, observer) { + mergeRecords(records); + assert.equal(records.length, 1); + assert.equal(records[0].type, 'childList'); + assert.equal(records[0].target, div); + assert.equal(addedNodes.length, 1); + assert.equal(addedNodes[0], c); + assert.equal(records[0].previousSibling, a); + assert.equal(records[0].nextSibling, b); + mo.disconnect(); + done(); + }); + div.innerHTML = ''; + var a = div.firstChild; + var b = div.lastChild; + + mo.observe(div, { + childList: true + }); + + var c = document.createElement('c'); + div.insertBefore(c, b); + + }); + + test('removedNodes', function(done) { + if (!window.MutationObserver) { + done(); + return; + } + + div = document.body.appendChild(document.createElement('div')); + + var mo = new MutationObserver(function(records, observer) { + mergeRecords(records); + assert.equal(records[0].type, 'childList'); + assert.equal(records[0].target, div); + assert.equal(addedNodes.length, 2); + assert.equal(addedNodes[0], c); + assert.equal(addedNodes[1], d); + assert.equal(removedNodes.length, 2); + // The ordering of the removed nodes is different in IE11. + if (removedNodes[0] === a) { + assert.equal(removedNodes[1], b); + } else { + assert.equal(removedNodes[0], b); + assert.equal(removedNodes[1], a); + } + mo.disconnect(); + done(); + }); + + div.innerHTML = ''; + var a = div.firstChild; + var b = div.lastChild; + + mo.observe(div, { + childList: true + }); + + div.innerHTML = ''; + var c = div.firstChild; + var d = div.lastChild; + }); + + test('removedNodes siblings', function(done) { + if (!window.MutationObserver) { + done(); + return; + } + + div = document.body.appendChild(document.createElement('div')); + + var mo = new MutationObserver(function(records, observer) { + mergeRecords(records); + assert.equal(records.length, 1); + assert.equal(records[0].type, 'childList'); + assert.equal(removedNodes.length, 1); + assert.equal(records[0].previousSibling, a); + assert.equal(records[0].nextSibling, c); + mo.disconnect(); + done(); + }); + + div.innerHTML = ''; + var a = div.firstChild; + var b = a.nextSibling; + var c = div.lastChild; + + mo.observe(div, { + childList: true + }); + + div.removeChild(b); + }); + + test('observe document', function(done) { + if (!window.MutationObserver) { + done(); + return; + } + + var mo = new MutationObserver(function(records, observer) { + assert.equal(this, mo); + assert.equal(observer, mo); + assert.equal(records[0].type, 'attributes'); + assert.equal(records[0].target, wrap(document).body); + mo.disconnect(); + done(); + }); + mo.observe(document, { + attributes: true, + subtree: true + }); + + wrap(document).body.setAttribute('a', newValue()); + }); + + test('observe document.body', function(done) { + if (!window.MutationObserver) { + done(); + return; + } + + var mo = new MutationObserver(function(records, observer) { + assert.equal(this, mo); + assert.equal(observer, mo); + assert.equal(records[0].type, 'attributes'); + assert.equal(records[0].target, wrap(document).body); + mo.disconnect(); + done(); + }); + mo.observe(document.body, { + attributes: true + }); + + wrap(document.body).setAttribute('a', newValue()); + }); + + test('observe document.head', function(done) { + if (!window.MutationObserver) { + done(); + return; + } + + var mo = new MutationObserver(function(records, observer) { + assert.equal(this, mo); + assert.equal(observer, mo); + assert.equal(records[0].type, 'attributes'); + assert.equal(records[0].target, wrap(document).head); + mo.disconnect(); + done(); + }); + mo.observe(document.head, { + attributes: true + }); + + wrap(document.head).setAttribute('a', newValue()); + }); + + test('observe text node', function(done) { + if (!window.MutationObserver) { + done(); + return; + } + + div = document.body.appendChild(document.createElement('div')); + var a = document.createTextNode(''); + div.appendChild(a); + + var mo = new MutationObserver(function(records, observer) { + mergeRecords(records); + assert.equal(this, mo); + assert.equal(observer, mo); + assert.equal(records[0].type, 'childList'); + assert.equal(records[0].target, div); + + // IE11 is broken and reports the text node being removed twice. + if (!/Trident/.test(navigator.userAgent)) + assert.equal(removedNodes.length, 1); + assert.equal(removedNodes[0], a); + done(); + }); + mo.observe(div, {childList: true}); + + div.removeChild(a); + }); + + test('instanceof', function() { + var mo = new MutationObserver(function(records, observer) {}); + assert.instanceOf(mo, MutationObserver); + }); + + test('constructor', function() { + var mo = new MutationObserver(function(records, observer) {}); + assert.equal(MutationObserver, mo.constructor); + }); + +}); diff --git a/tests/ShadowDOM/js/MutationObserver/attributes.js b/tests/ShadowDOM/js/MutationObserver/attributes.js new file mode 100644 index 0000000..0bc5ae8 --- /dev/null +++ b/tests/ShadowDOM/js/MutationObserver/attributes.js @@ -0,0 +1,280 @@ +/* + * Copyright 2012 The Polymer Authors. All rights reserved. + * Use of this source code is governed by a BSD-style + * license that can be found in the LICENSE file. + */ + +suite('MutationObserver', function() { + + suite('attributes', function() { + + test('attr', function() { + var div = document.createElement('div'); + var observer = new MutationObserver(function() {}); + observer.observe(div, { + attributes: true + }); + div.setAttribute('a', 'A'); + div.setAttribute('a', 'B'); + + var records = observer.takeRecords(); + assert.equal(records.length, 2); + + expectMutationRecord(records[0], { + type: 'attributes', + target: div, + attributeName: 'a', + attributeNamespace: null + }); + expectMutationRecord(records[1], { + type: 'attributes', + target: div, + attributeName: 'a', + attributeNamespace: null + }); + }); + + test('attr with oldValue', function() { + var div = document.createElement('div'); + var observer = new MutationObserver(function() {}); + observer.observe(div, { + attributes: true, + attributeOldValue: true + }); + div.setAttribute('a', 'A'); + div.setAttribute('a', 'B'); + + var records = observer.takeRecords(); + assert.equal(records.length, 2); + + expectMutationRecord(records[0], { + type: 'attributes', + target: div, + attributeName: 'a', + attributeNamespace: null, + oldValue: null + }); + expectMutationRecord(records[1], { + type: 'attributes', + target: div, + attributeName: 'a', + attributeNamespace: null, + oldValue: 'A' + }); + }); + + test('attr change in subtree should not genereate a record', function() { + var div = document.createElement('div'); + var child = div.appendChild(document.createElement('div')); + + var observer = new MutationObserver(function() {}); + observer.observe(div, { + attributes: true + }); + child.setAttribute('a', 'A'); + child.setAttribute('a', 'B'); + + var records = observer.takeRecords(); + assert.equal(records.length, 0); + }); + + test('attr change, subtree', function() { + var div = document.createElement('div'); + var child = div.appendChild(document.createElement('div')); + + var observer = new MutationObserver(function() {}); + observer.observe(div, { + attributes: true, + subtree: true + }); + child.setAttribute('a', 'A'); + child.setAttribute('a', 'B'); + + var records = observer.takeRecords(); + assert.equal(records.length, 2); + + expectMutationRecord(records[0], { + type: 'attributes', + target: child, + attributeName: 'a' + }); + expectMutationRecord(records[1], { + type: 'attributes', + target: child, + attributeName: 'a' + }); + }); + + + test('multiple observers on same target', function() { + var div = document.createElement('div'); + var observer1 = new MutationObserver(function() {}); + observer1.observe(div, { + attributes: true, + attributeOldValue: true + }); + var observer2 = new MutationObserver(function() {}); + observer2.observe(div, { + attributes: true, + attributeFilter: ['b'] + }); + + div.setAttribute('a', 'A'); + div.setAttribute('a', 'A2'); + div.setAttribute('b', 'B'); + + var records = observer1.takeRecords(); + assert.equal(records.length, 3); + + expectMutationRecord(records[0], { + type: 'attributes', + target: div, + attributeName: 'a' + }); + expectMutationRecord(records[1], { + type: 'attributes', + target: div, + attributeName: 'a', + oldValue: 'A' + }); + expectMutationRecord(records[2], { + type: 'attributes', + target: div, + attributeName: 'b' + }); + + records = observer2.takeRecords(); + assert.equal(records.length, 1); + + expectMutationRecord(records[0], { + type: 'attributes', + target: div, + attributeName: 'b' + }); + }); + + test('observer observes on different target', function() { + var div = document.createElement('div'); + var child = div.appendChild(document.createElement('div')); + + var observer = new MutationObserver(function() {}); + observer.observe(child, { + attributes: true + }); + observer.observe(div, { + attributes: true, + subtree: true, + attributeOldValue: true + }); + + child.setAttribute('a', 'A'); + child.setAttribute('a', 'A2'); + child.setAttribute('b', 'B'); + + var records = observer.takeRecords(); + assert.equal(records.length, 3); + + expectMutationRecord(records[0], { + type: 'attributes', + target: child, + attributeName: 'a' + }); + expectMutationRecord(records[1], { + type: 'attributes', + target: child, + attributeName: 'a', + oldValue: 'A' + }); + expectMutationRecord(records[2], { + type: 'attributes', + target: child, + attributeName: 'b' + }); + }); + + test('observing on the same node should update the options', function() { + var div = document.createElement('div'); + var observer = new MutationObserver(function() {}); + observer.observe(div, { + attributes: true, + attributeFilter: ['a'] + }); + observer.observe(div, { + attributes: true, + attributeFilter: ['b'] + }); + + div.setAttribute('a', 'A'); + div.setAttribute('b', 'B'); + + var records = observer.takeRecords(); + assert.equal(records.length, 1); + + expectMutationRecord(records[0], { + type: 'attributes', + target: div, + attributeName: 'b' + }); + }); + + test('disconnect should stop all events and empty the records', function() { + var div = document.createElement('div'); + var observer = new MutationObserver(function() {}); + observer.observe(div, { + attributes: true, + }); + + div.setAttribute('a', 'A'); + + observer.disconnect(); + var records = observer.takeRecords(); + assert.equal(records.length, 0); + + div.setAttribute('b', 'B'); + + records = observer.takeRecords(); + assert.equal(records.length, 0); + }); + + test('disconnect should not affect other observers', function() { + var div = document.createElement('div'); + var observer1 = new MutationObserver(function() {}); + observer1.observe(div, { + attributes: true, + }); + var observer2 = new MutationObserver(function() {}); + observer2.observe(div, { + attributes: true, + }); + + div.setAttribute('a', 'A'); + + observer1.disconnect(); + var records1 = observer1.takeRecords(); + assert.equal(records1.length, 0); + + var records2 = observer2.takeRecords(); + assert.equal(records2.length, 1); + expectMutationRecord(records2[0], { + type: 'attributes', + target: div, + attributeName: 'a' + }); + + div.setAttribute('b', 'B'); + + records1 = observer1.takeRecords(); + assert.equal(records1.length, 0); + + records2 = observer2.takeRecords(); + assert.equal(records2.length, 1); + expectMutationRecord(records2[0], { + type: 'attributes', + target: div, + attributeName: 'b' + }); + }); + + }); + +}); \ No newline at end of file diff --git a/tests/ShadowDOM/js/MutationObserver/callback.js b/tests/ShadowDOM/js/MutationObserver/callback.js new file mode 100644 index 0000000..7f84844 --- /dev/null +++ b/tests/ShadowDOM/js/MutationObserver/callback.js @@ -0,0 +1,76 @@ +/* + * Copyright 2012 The Polymer Authors. All rights reserved. + * Use of this source code is governed by a BSD-style + * license that can be found in the LICENSE file. + */ + +suite('MutationObserver', function() { + + suite('callback', function() { + + test('One observer, two attribute changes', function(done) { + var div = document.createElement('div'); + var observer = new MutationObserver(function(records) { + assert.equal(records.length, 2); + + expectMutationRecord(records[0], { + type: 'attributes', + target: div, + attributeName: 'a', + attributeNamespace: null + }); + expectMutationRecord(records[1], { + type: 'attributes', + target: div, + attributeName: 'a', + attributeNamespace: null + }); + + done(); + }); + + observer.observe(div, { + attributes: true + }); + + div.setAttribute('a', 'A'); + div.setAttribute('a', 'B'); + }); + + test('nested changes', function(done) { + var div = document.createElement('div'); + var i = 0; + var observer = new MutationObserver(function(records) { + assert.equal(records.length, 1); + + if (i === 0) { + expectMutationRecord(records[0], { + type: 'attributes', + target: div, + attributeName: 'a', + attributeNamespace: null + }); + div.setAttribute('b', 'B'); + i++; + } else { + expectMutationRecord(records[0], { + type: 'attributes', + target: div, + attributeName: 'b', + attributeNamespace: null + }); + + done(); + } + }); + + observer.observe(div, { + attributes: true + }); + + div.setAttribute('a', 'A'); + }); + + }); + +}); \ No newline at end of file diff --git a/tests/ShadowDOM/js/MutationObserver/characterData.js b/tests/ShadowDOM/js/MutationObserver/characterData.js new file mode 100644 index 0000000..cc462b2 --- /dev/null +++ b/tests/ShadowDOM/js/MutationObserver/characterData.js @@ -0,0 +1,100 @@ +/* + * Copyright 2012 The Polymer Authors. All rights reserved. + * Use of this source code is governed by a BSD-style + * license that can be found in the LICENSE file. + */ + +suite('MutationObserver', function() { + + suite('characterData', function() { + + test('characterData', function() { + var text = document.createTextNode('abc'); + var observer = new MutationObserver(function() {}); + observer.observe(text, { + characterData: true + }); + text.data = 'def'; + text.data = 'ghi'; + + var records = observer.takeRecords(); + assert.equal(records.length, 2); + + expectMutationRecord(records[0], { + type: 'characterData', + target: text + }); + expectMutationRecord(records[1], { + type: 'characterData', + target: text + }); + }); + + test('characterData with old value', function() { + var text = document.createTextNode('abc'); + var observer = new MutationObserver(function() {}); + observer.observe(text, { + characterData: true, + characterDataOldValue: true + }); + text.data = 'def'; + text.data = 'ghi'; + + var records = observer.takeRecords(); + assert.equal(records.length, 2); + + expectMutationRecord(records[0], { + type: 'characterData', + target: text, + oldValue: 'abc' + }); + expectMutationRecord(records[1], { + type: 'characterData', + target: text, + oldValue: 'def' + }); + }); + + test('characterData change in subtree should not generate a record', + function() { + var div = document.createElement('div'); + var text = div.appendChild(document.createTextNode('abc')); + var observer = new MutationObserver(function() {}); + observer.observe(div, { + characterData: true + }); + text.data = 'def'; + text.data = 'ghi'; + + var records = observer.takeRecords(); + assert.equal(records.length, 0); + }); + + test('characterData change in subtree', + function() { + var div = document.createElement('div'); + var text = div.appendChild(document.createTextNode('abc')); + var observer = new MutationObserver(function() {}); + observer.observe(div, { + characterData: true, + subtree: true + }); + text.data = 'def'; + text.data = 'ghi'; + + var records = observer.takeRecords(); + assert.equal(records.length, 2); + + expectMutationRecord(records[0], { + type: 'characterData', + target: text + }); + expectMutationRecord(records[1], { + type: 'characterData', + target: text + }); + }); + + }); + +}); \ No newline at end of file diff --git a/tests/ShadowDOM/js/MutationObserver/childList.js b/tests/ShadowDOM/js/MutationObserver/childList.js new file mode 100644 index 0000000..fe54c68 --- /dev/null +++ b/tests/ShadowDOM/js/MutationObserver/childList.js @@ -0,0 +1,795 @@ +/* + * Copyright 2013 The Polymer Authors. All rights reserved. + * Use of this source code is goverened by a BSD-style + * license that can be found in the LICENSE file. + */ + +suite('MutationObserver', function() { + 'use strict'; + + suite('childList', function() { + + var NodeList = ShadowDOMPolyfill.wrappers.NodeList; + + function makeNodeList(/* ...args */) { + var nodeList = new NodeList; + for (var i = 0; i < arguments.length; i++) { + nodeList[i] = arguments[i]; + } + nodeList.length = i; + return nodeList; + } + + test('appendChild', function() { + var div = document.createElement('div'); + var observer = new MutationObserver(function() {}); + observer.observe(div, { + childList: true + }); + var a = document.createElement('a'); + var b = document.createElement('b'); + + div.appendChild(a); + div.appendChild(b); + + var records = observer.takeRecords(); + assert.equal(records.length, 2); + + expectMutationRecord(records[0], { + type: 'childList', + target: div, + addedNodes: [a] + }); + + expectMutationRecord(records[1], { + type: 'childList', + target: div, + addedNodes: [b], + previousSibling: a + }); + }); + + test('insertBefore', function() { + var div = document.createElement('div'); + var a = document.createElement('a'); + var b = document.createElement('b'); + var c = document.createElement('c'); + div.appendChild(a); + + var observer = new MutationObserver(function() {}); + observer.observe(div, { + childList: true + }); + + div.insertBefore(b, a); + div.insertBefore(c, a); + + var records = observer.takeRecords(); + assert.equal(records.length, 2); + + expectMutationRecord(records[0], { + type: 'childList', + target: div, + addedNodes: [b], + nextSibling: a + }); + + expectMutationRecord(records[1], { + type: 'childList', + target: div, + addedNodes: [c], + nextSibling: a, + previousSibling: b + }); + }); + + test('replaceChild', function() { + var div = document.createElement('div'); + var a = document.createElement('a'); + var b = document.createElement('b'); + div.appendChild(a); + + var observer = new MutationObserver(function() {}); + observer.observe(div, { + childList: true + }); + + div.replaceChild(b, a); + + var records = observer.takeRecords(); + assert.equal(records.length, 1); + + expectMutationRecord(records[0], { + type: 'childList', + target: div, + addedNodes: [b], + removedNodes: [a] + }); + }); + + test('removeChild', function() { + var div = document.createElement('div'); + var a = div.appendChild(document.createElement('a')); + var b = div.appendChild(document.createElement('b')); + var c = div.appendChild(document.createElement('c')); + + var observer = new MutationObserver(function() {}); + observer.observe(div, { + childList: true + }); + + div.removeChild(b); + div.removeChild(a); + + var records = observer.takeRecords(); + assert.equal(records.length, 2); + + expectMutationRecord(records[0], { + type: 'childList', + target: div, + removedNodes: [b], + nextSibling: c, + previousSibling: a + }); + + expectMutationRecord(records[1], { + type: 'childList', + target: div, + removedNodes: [a], + nextSibling: c + }); + }); + + test('Direct children', function() { + var div = document.createElement('div'); + var observer = new MutationObserver(function() {}); + observer.observe(div, { + childList: true + }); + var a = document.createElement('a'); + var b = document.createElement('b'); + + div.appendChild(a); + div.insertBefore(b, a); + div.removeChild(b); + + var records = observer.takeRecords(); + assert.equal(records.length, 3); + + expectMutationRecord(records[0], { + type: 'childList', + target: div, + addedNodes: [a] + }); + + expectMutationRecord(records[1], { + type: 'childList', + target: div, + nextSibling: a, + addedNodes: [b] + }); + + expectMutationRecord(records[2], { + type: 'childList', + target: div, + nextSibling: a, + removedNodes: [b] + }); + }); + + test('subtree', function() { + var div = document.createElement('div'); + var child = div.appendChild(document.createElement('div')); + var observer = new MutationObserver(function() {}); + observer.observe(child, { + childList: true + }); + var a = document.createTextNode('a'); + var b = document.createTextNode('b'); + + child.appendChild(a); + child.insertBefore(b, a); + child.removeChild(b); + + var records = observer.takeRecords(); + assert.equal(records.length, 3); + + expectMutationRecord(records[0], { + type: 'childList', + target: child, + addedNodes: [a] + }); + + expectMutationRecord(records[1], { + type: 'childList', + target: child, + nextSibling: a, + addedNodes: [b] + }); + + expectMutationRecord(records[2], { + type: 'childList', + target: child, + nextSibling: a, + removedNodes: [b] + }); + }); + + test('both direct and subtree', function() { + var div = document.createElement('div'); + var child = div.appendChild(document.createElement('div')); + var observer = new MutationObserver(function() {}); + observer.observe(div, { + childList: true, + subtree: true + }); + observer.observe(child, { + childList: true + }); + + var a = document.createTextNode('a'); + var b = document.createTextNode('b'); + + child.appendChild(a); + div.appendChild(b); + + var records = observer.takeRecords(); + assert.equal(records.length, 2); + + expectMutationRecord(records[0], { + type: 'childList', + target: child, + addedNodes: [a] + }); + + expectMutationRecord(records[1], { + type: 'childList', + target: div, + addedNodes: [b], + previousSibling: child + }); + }); + + test('Append multiple at once at the end', function() { + var div = document.createElement('div'); + var a = div.appendChild(document.createTextNode('a')); + + var observer = new MutationObserver(function() {}); + observer.observe(div, { + childList: true + }); + + var df = document.createDocumentFragment(); + var b = df.appendChild(document.createTextNode('b')); + var c = df.appendChild(document.createTextNode('c')); + var d = df.appendChild(document.createTextNode('d')); + + div.appendChild(df); + + var records = observer.takeRecords(); + assert.equal(records.length, 1); + expectMutationRecord(records[0], { + type: 'childList', + target: div, + addedNodes: makeNodeList(b, c, d), + removedNodes: makeNodeList(), + previousSibling: a, + nextSibling: null + }); + }); + + test('Append multiple at once at the front', function() { + var div = document.createElement('div'); + var a = div.appendChild(document.createTextNode('a')); + + var observer = new MutationObserver(function() {}); + observer.observe(div, { + childList: true + }); + + var df = document.createDocumentFragment(); + var b = df.appendChild(document.createTextNode('b')); + var c = df.appendChild(document.createTextNode('c')); + var d = df.appendChild(document.createTextNode('d')); + + div.insertBefore(df, a); + + var records = observer.takeRecords(); + assert.equal(records.length, 1); + expectMutationRecord(records[0], { + type: 'childList', + target: div, + addedNodes: makeNodeList(b, c, d), + removedNodes: makeNodeList(), + previousSibling: null, + nextSibling: a + }); + }); + + test('Append multiple at once in the middle', function() { + var div = document.createElement('div'); + var a = div.appendChild(document.createTextNode('a')); + var b = div.appendChild(document.createTextNode('b')); + + var observer = new MutationObserver(function() {}); + observer.observe(div, { + childList: true + }); + + var df = document.createDocumentFragment(); + var c = df.appendChild(document.createTextNode('c')); + var d = df.appendChild(document.createTextNode('d')); + + div.insertBefore(df, b); + + var records = observer.takeRecords(); + assert.equal(records.length, 1); + expectMutationRecord(records[0], { + type: 'childList', + target: div, + addedNodes: makeNodeList(c, d), + removedNodes: makeNodeList(), + previousSibling: a, + nextSibling: b + }); + }); + + test('Remove all children using innerHTML', function() { + var div = document.createElement('div'); + var a = div.appendChild(document.createTextNode('a')); + var b = div.appendChild(document.createTextNode('b')); + var c = div.appendChild(document.createTextNode('c')); + + var observer = new MutationObserver(function() {}); + observer.observe(div, { + childList: true + }); + + div.innerHTML = ''; + + var records = observer.takeRecords(); + assert.equal(records.length, 1); + expectMutationRecord(records[0], { + type: 'childList', + target: div, + addedNodes: makeNodeList(), + removedNodes: makeNodeList(a, b, c), + previousSibling: null, + nextSibling: null + }); + }); + + test('Replace all children using innerHTML', function() { + var div = document.createElement('div'); + var a = div.appendChild(document.createTextNode('a')); + var b = div.appendChild(document.createTextNode('b')); + + var observer = new MutationObserver(function() {}); + observer.observe(div, { + childList: true + }); + + div.innerHTML = ''; + var c = div.firstChild; + var d = div.lastChild; + + var records = observer.takeRecords(); + assert.equal(records.length, 1); + expectMutationRecord(records[0], { + type: 'childList', + target: div, + addedNodes: makeNodeList(c, d), + removedNodes: makeNodeList(a, b), + previousSibling: null, + nextSibling: null + }); + }); + + test('Remove all children using textContent', function() { + var div = document.createElement('div'); + var a = div.appendChild(document.createTextNode('a')); + var b = div.appendChild(document.createTextNode('b')); + var c = div.appendChild(document.createTextNode('c')); + + var observer = new MutationObserver(function() {}); + observer.observe(div, { + childList: true + }); + + div.textContent = ''; + + var records = observer.takeRecords(); + assert.equal(records.length, 1); + expectMutationRecord(records[0], { + type: 'childList', + target: div, + addedNodes: makeNodeList(), + removedNodes: makeNodeList(a, b, c), + previousSibling: null, + nextSibling: null + }); + }); + + test('Replace all children using textContent', function() { + var div = document.createElement('div'); + var a = div.appendChild(document.createTextNode('a')); + var b = div.appendChild(document.createTextNode('b')); + + var observer = new MutationObserver(function() {}); + observer.observe(div, { + childList: true + }); + + div.textContent = 'text'; + var text = div.firstChild; + + var records = observer.takeRecords(); + assert.equal(records.length, 1); + expectMutationRecord(records[0], { + type: 'childList', + target: div, + addedNodes: makeNodeList(text), + removedNodes: makeNodeList(a, b), + previousSibling: null, + nextSibling: null + }); + }); + + test('appendChild removal', function() { + var a = document.createElement('a'); + var b = document.createElement('b'); + var c = document.createElement('c'); + + a.appendChild(c); + + var observerA = new MutationObserver(function() {}); + observerA.observe(a, { + childList: true + }); + + var observerB = new MutationObserver(function() {}); + observerB.observe(b, { + childList: true + }); + + b.appendChild(c); + + var recordsA = observerA.takeRecords(); + + assert.equal(recordsA.length, 1); + expectMutationRecord(recordsA[0], { + type: 'childList', + target: a, + removedNodes: [c] + }); + + var recordsB = observerB.takeRecords(); + assert.equal(recordsB.length, 1); + expectMutationRecord(recordsB[0], { + type: 'childList', + target: b, + addedNodes: [c] + }); + }); + + test('insertBefore removal', function() { + var a = document.createElement('a'); + var b = document.createElement('b'); + var c = document.createElement('c'); + var d = document.createElement('d'); + var e = document.createElement('e'); + + a.appendChild(c); + a.appendChild(d); + b.appendChild(e); + + var observerA = new MutationObserver(function() {}); + observerA.observe(a, { + childList: true + }); + + var observerB = new MutationObserver(function() {}); + observerB.observe(b, { + childList: true + }); + + b.insertBefore(d, e); + + var recordsA = observerA.takeRecords(); + + assert.equal(recordsA.length, 1); + expectMutationRecord(recordsA[0], { + type: 'childList', + target: a, + removedNodes: [d], + previousSibling: c + }); + + var recordsB = observerB.takeRecords(); + assert.equal(recordsB.length, 1); + expectMutationRecord(recordsB[0], { + type: 'childList', + target: b, + addedNodes: [d], + nextSibling: e + }); + }); + + test('insertBefore removal document fragment', function() { + var df = document.createDocumentFragment(); + var a = df.appendChild(document.createElement('a')); + var b = df.appendChild(document.createElement('b')); + var c = df.appendChild(document.createElement('c')); + + var d = document.createElement('d'); + var e = d.appendChild(document.createElement('e')); + var f = d.appendChild(document.createElement('f')); + + var observerDf = new MutationObserver(function() {}); + observerDf.observe(df, { + childList: true + }); + + var observerD = new MutationObserver(function() {}); + observerD.observe(d, { + childList: true + }); + + d.insertBefore(df, f); + + var recordsDf = observerDf.takeRecords(); + + assert.equal(recordsDf.length, 1); + expectMutationRecord(recordsDf[0], { + type: 'childList', + target: df, + removedNodes: [a, b, c] + }); + + var recordsD = observerD.takeRecords(); + assert.equal(recordsD.length, 1); + expectMutationRecord(recordsD[0], { + type: 'childList', + target: d, + addedNodes: [a, b, c], + previousSibling: e, + nextSibling: f + }); + }); + + + test('insertBefore removal document fragment (with shadow roots)', function() { + var df = document.createDocumentFragment(); + var a = df.appendChild(document.createElement('a')); + var b = df.appendChild(document.createElement('b')); + var c = df.appendChild(document.createElement('c')); + + var d = document.createElement('d'); + var sr = d.createShadowRoot(); + var e = sr.appendChild(document.createElement('e')); + var f = sr.appendChild(document.createElement('f')); + + var observerDf = new MutationObserver(function() {}); + observerDf.observe(df, { + childList: true + }); + + var observerSr = new MutationObserver(function() {}); + observerSr.observe(sr, { + childList: true + }); + + sr.insertBefore(df, f); + + var recordsDf = observerDf.takeRecords(); + + assert.equal(recordsDf.length, 1); + expectMutationRecord(recordsDf[0], { + type: 'childList', + target: df, + removedNodes: [a, b, c] + }); + + var recordsSr = observerSr.takeRecords(); + assert.equal(recordsSr.length, 1); + expectMutationRecord(recordsSr[0], { + type: 'childList', + target: sr, + addedNodes: [a, b, c], + previousSibling: e, + nextSibling: f + }); + }); + + test('Check old siblings', function() { + var a = document.createElement('a'); + a.innerHTML = ''; + var b = a.firstChild; + var c = a.lastChild; + + var d = document.createElement('d'); + d.innerHTML = ''; + var e = d.firstChild; + var f = e.nextSibling; + var g = d.lastChild; + + var observer = new MutationObserver(function() {}); + observer.observe(a, { + childList: true + }); + + var observer2 = new MutationObserver(function() {}); + observer2.observe(d, { + childList: true + }); + + a.insertBefore(f, c); + + var records = observer.takeRecords(); + assert.equal(records.length, 1); + expectMutationRecord(records[0], { + type: 'childList', + target: a, + addedNodes: [f], + previousSibling: b, + nextSibling: c + }); + + records = observer2.takeRecords(); + assert.equal(records.length, 1); + expectMutationRecord(records[0], { + type: 'childList', + target: d, + removedNodes: [f], + previousSibling: e, + nextSibling: g + }); + }); + + test('insertAdjacentHTML beforebegin', function() { + var a = document.createElement('a'); + a.innerHTML = ''; + var b = a.firstChild; + var c = a.lastChild; + + var observer = new MutationObserver(function() {}); + observer.observe(a, { + childList: true + }); + + c.insertAdjacentHTML('beforebegin', ''); + + assert.equal(a.innerHTML, ''); + var d = b.nextSibling; + var e = d.nextSibling; + + var records = observer.takeRecords(); + assert.equal(records.length, 1); + expectMutationRecord(records[0], { + type: 'childList', + target: a, + addedNodes: [d, e], + previousSibling: b, + nextSibling: c + }); + }); + + test('insertAdjacentHTML afterbegin', function() { + var a = document.createElement('a'); + a.innerHTML = ''; + var b = a.firstChild; + var c = a.lastChild; + + var observer = new MutationObserver(function() {}); + observer.observe(a, { + childList: true + }); + + a.insertAdjacentHTML('afterbegin', ''); + + assert.equal(a.innerHTML, ''); + var d = a.firstChild; + var e = d.nextSibling; + + var records = observer.takeRecords(); + assert.equal(records.length, 1); + expectMutationRecord(records[0], { + type: 'childList', + target: a, + addedNodes: [d, e], + previousSibling: null, + nextSibling: b + }); + }); + + test('insertAdjacentHTML beforeend', function() { + var a = document.createElement('a'); + a.innerHTML = ''; + var b = a.firstChild; + var c = a.lastChild; + + var observer = new MutationObserver(function() {}); + observer.observe(a, { + childList: true + }); + + a.insertAdjacentHTML('beforeend', ''); + + assert.equal(a.innerHTML, ''); + var d = c.nextSibling; + var e = d.nextSibling; + + var records = observer.takeRecords(); + assert.equal(records.length, 1); + expectMutationRecord(records[0], { + type: 'childList', + target: a, + addedNodes: [d, e], + previousSibling: c, + nextSibling: null + }); + }); + + test('insertAdjacentHTML afterend', function() { + var a = document.createElement('a'); + a.innerHTML = ''; + var b = a.firstChild; + var c = a.lastChild; + + var observer = new MutationObserver(function() {}); + observer.observe(a, { + childList: true + }); + + b.insertAdjacentHTML('afterend', ''); + + assert.equal(a.innerHTML, ''); + var d = b.nextSibling; + var e = d.nextSibling; + + var records = observer.takeRecords(); + assert.equal(records.length, 1); + expectMutationRecord(records[0], { + type: 'childList', + target: a, + addedNodes: [d, e], + previousSibling: b, + nextSibling: c + }); + }); + + test('outerHTML', function() { + var a = document.createElement('a'); + a.innerHTML = ''; + var b = a.firstChild; + var c = b.nextSibling; + var d = a.lastChild; + + var sr = a.createShadowRoot(); + a.offsetHeight; + + var observer = new MutationObserver(function() {}); + observer.observe(a, { + childList: true + }); + + c.outerHTML = ''; + assert.equal(a.innerHTML, ''); + var e = b.nextSibling; + var f = e.nextSibling; + + var records = observer.takeRecords(); + assert.equal(records.length, 1); + expectMutationRecord(records[0], { + type: 'childList', + target: a, + addedNodes: [e, f], + removedNodes: [c], + previousSibling: b, + nextSibling: d + }); + }); + + }); + +}); diff --git a/tests/ShadowDOM/js/MutationObserver/mixed.js b/tests/ShadowDOM/js/MutationObserver/mixed.js new file mode 100644 index 0000000..afc75a9 --- /dev/null +++ b/tests/ShadowDOM/js/MutationObserver/mixed.js @@ -0,0 +1,40 @@ +/* + * Copyright 2012 The Polymer Authors. All rights reserved. + * Use of this source code is governed by a BSD-style + * license that can be found in the LICENSE file. + */ + +suite('MutationObserver', function() { + + suite('mixed', function() { + + test('attr and characterData', function() { + var div = document.createElement('div'); + var text = div.appendChild(document.createTextNode('text')); + var observer = new MutationObserver(function() {}); + observer.observe(div, { + attributes: true, + characterData: true, + subtree: true + }); + div.setAttribute('a', 'A'); + div.firstChild.data = 'changed'; + + var records = observer.takeRecords(); + assert.equal(records.length, 2); + + expectMutationRecord(records[0], { + type: 'attributes', + target: div, + attributeName: 'a', + attributeNamespace: null + }); + expectMutationRecord(records[1], { + type: 'characterData', + target: div.firstChild + }); + }); + + }); + +}); \ No newline at end of file diff --git a/tests/ShadowDOM/js/MutationObserver/options.js b/tests/ShadowDOM/js/MutationObserver/options.js new file mode 100644 index 0000000..bea4c5d --- /dev/null +++ b/tests/ShadowDOM/js/MutationObserver/options.js @@ -0,0 +1,110 @@ +/* + * Copyright 2012 The Polymer Authors. All rights reserved. + * Use of this source code is governed by a BSD-style + * license that can be found in the LICENSE file. + */ + +suite('MutationObserver', function() { + + suite('options', function() { + + test('attributeOldValue and attributes', function() { + var div = document.createElement('div'); + var observer = new MutationObserver(function() {}); + + assert.throws(function() { + observer.observe(div, { + attributeOldValue: true, + attributes: false + }); + }, TypeError); + + observer.observe(div, { + attributeOldValue: true, + }); + + observer.observe(div, { + attributeOldValue: false, + attributes: false + }); + + observer.observe(div, { + attributeOldValue: false, + attributes: true + }); + + observer.observe(div, { + attributeOldValue: true, + attributes: true + }); + }); + + test('attributeFilter and attributes', function() { + var div = document.createElement('div'); + var observer = new MutationObserver(function() {}); + + assert.throws(function() { + observer.observe(div, { + attributeFilter: ['name'], + attributes: false + }); + }, TypeError); + + observer.observe(div, { + attributeFilter: ['name'], + }); + + assert.throws(function() { + observer.observe(div, { + attributeFilter: null, + }); + }, TypeError); + + observer.observe(div, { + attributeFilter: ['name'], + attributes: true + }); + + observer.observe(div, { + attributes: false + }); + + observer.observe(div, { + attributes: true + }); + }); + + test('characterDataOldValue and characterData', function() { + var div = document.createElement('div'); + var observer = new MutationObserver(function() {}); + + assert.throws(function() { + observer.observe(div, { + characterDataOldValue: true, + characterData: false + }); + }, TypeError); + + observer.observe(div, { + characterDataOldValue: true + }); + + observer.observe(div, { + characterDataOldValue: false, + characterData: false + }); + + observer.observe(div, { + characterDataOldValue: false, + characterData: true + }); + + observer.observe(div, { + characterDataOldValue: true, + characterData: true + }); + }); + + }); + +}); \ No newline at end of file diff --git a/tests/ShadowDOM/js/MutationObserver/shadow-root.js b/tests/ShadowDOM/js/MutationObserver/shadow-root.js new file mode 100644 index 0000000..0fc68c1 --- /dev/null +++ b/tests/ShadowDOM/js/MutationObserver/shadow-root.js @@ -0,0 +1,105 @@ +/* + * Copyright 2013 The Polymer Authors. All rights reserved. + * Use of this source code is governed by a BSD-style + * license that can be found in the LICENSE file. + */ + +suite('MutationObserver', function() { + + suite('Shadow Root', function() { + + var unwrap = ShadowDOMPolyfill.unwrap; + + test('Make no notifications due to render', function() { + var a = document.createElement('a'); + a.innerHTML = ''; + var sr = a.createShadowRoot(); + sr.innerHTML = ''; + + var observer = new MutationObserver(function() {}); + + observer.observe(a, { + childList: true, + attributes: true, + characterData: true, + subtree: true + }); + observer.observe(sr, { + childList: true, + attributes: true, + characterData: true, + subtree: true + }); + + a.offsetHeight; + + var records = observer.takeRecords(); + assert.equal(records.length, 0); + + assert.equal(unwrap(a).outerHTML, ''); + }); + + test('Observe ShadowRoot', function() { + var a = document.createElement('a'); + a.innerHTML = ''; + var sr = a.createShadowRoot(); + sr.innerHTML = ''; + var c = sr.firstChild; + var d = c.nextSibling; + var content = d.firstChild; + + var observer = new MutationObserver(function() {}); + + observer.observe(sr, { + childList: true, + subtree: true + }); + + a.offsetHeight; + + var records = observer.takeRecords(); + assert.equal(records.length, 0); + assert.equal(unwrap(a).outerHTML, ''); + + var e = document.createElement('e'); + e.innerHTML = ''; + var f = e.firstChild; + var g = f.nextSibling + var h = e.lastChild; + + var observer2 = new MutationObserver(function() {}); + observer2.observe(e, { + childList: true, + subtree: true + }); + + d.insertBefore(g, content); + + assert.equal(unwrap(a).outerHTML, ''); + a.offsetHeight; + assert.equal(unwrap(a).outerHTML, ''); + + records = observer.takeRecords(); + assert.equal(records.length, 1); + expectMutationRecord(records[0], { + type: 'childList', + target: d, + addedNodes: [g], + previousSibling: null, + nextSibling: content + }); + + records = observer2.takeRecords(); + assert.equal(records.length, 1); + expectMutationRecord(records[0], { + type: 'childList', + target: e, + removedNodes: [g], + previousSibling: f, + nextSibling: h + }); + }); + + }); + +}); \ No newline at end of file diff --git a/tests/ShadowDOM/js/MutationObserver/transient.js b/tests/ShadowDOM/js/MutationObserver/transient.js new file mode 100644 index 0000000..ffc718a --- /dev/null +++ b/tests/ShadowDOM/js/MutationObserver/transient.js @@ -0,0 +1,306 @@ +/* + * Copyright 2012 The Polymer Authors. All rights reserved. + * Use of this source code is governed by a BSD-style + * license that can be found in the LICENSE file. + */ + +suite('MutationObserver', function() { + + suite('transient', function() { + + test('attr', function() { + var div = document.createElement('div'); + var child = div.appendChild(document.createElement('div')); + var observer = new MutationObserver(function() {}); + observer.observe(div, { + attributes: true, + subtree: true + }); + div.removeChild(child); + child.setAttribute('a', 'A'); + + var records = observer.takeRecords(); + assert.equal(records.length, 1); + + expectMutationRecord(records[0], { + type: 'attributes', + target: child, + attributeName: 'a', + attributeNamespace: null + }); + + child.setAttribute('b', 'B'); + + records = observer.takeRecords(); + assert.equal(records.length, 1); + + expectMutationRecord(records[0], { + type: 'attributes', + target: child, + attributeName: 'b', + attributeNamespace: null + }); + }); + + test('attr callback', function(cont) { + var div = document.createElement('div'); + var child = div.appendChild(document.createElement('div')); + var i = 0; + var observer = new MutationObserver(function(records) { + i++; + if (i > 1) + expect().fail(); + + assert.equal(records.length, 1); + + expectMutationRecord(records[0], { + type: 'attributes', + target: child, + attributeName: 'a', + attributeNamespace: null + }); + + // The transient observers are removed before the callback is called. + child.setAttribute('b', 'B'); + records = observer.takeRecords(); + assert.equal(records.length, 0); + + cont(); + }); + + observer.observe(div, { + attributes: true, + subtree: true + }); + + div.removeChild(child); + child.setAttribute('a', 'A'); + }); + + test('attr, make sure transient gets removed', function(cont) { + var div = document.createElement('div'); + var child = div.appendChild(document.createElement('div')); + var i = 0; + var observer = new MutationObserver(function(records) { + i++; + if (i > 1) + expect().fail(); + + assert.equal(records.length, 1); + + expectMutationRecord(records[0], { + type: 'attributes', + target: child, + attributeName: 'a', + attributeNamespace: null + }); + + step2(); + }); + + observer.observe(div, { + attributes: true, + subtree: true + }); + + div.removeChild(child); + child.setAttribute('a', 'A'); + + function step2() { + var div2 = document.createElement('div'); + var observer2 = new MutationObserver(function(records) { + i++; + if (i > 2) + expect().fail(); + + assert.equal(records.length, 1); + + expectMutationRecord(records[0], { + type: 'attributes', + target: child, + attributeName: 'b', + attributeNamespace: null + }); + + cont(); + }); + + observer2.observe(div2, { + attributes: true, + subtree: true, + }); + + div2.appendChild(child); + child.setAttribute('b', 'B'); + } + }); + + test('characterData', function() { + var div = document.createElement('div'); + var child = div.appendChild(document.createTextNode('text')); + var observer = new MutationObserver(function() {}); + observer.observe(div, { + characterData: true, + subtree: true + }); + div.removeChild(child); + child.data = 'changed'; + + var records = observer.takeRecords(); + assert.equal(records.length, 1); + + expectMutationRecord(records[0], { + type: 'characterData', + target: child + }); + + child.data += ' again'; + + records = observer.takeRecords(); + assert.equal(records.length, 1); + + expectMutationRecord(records[0], { + type: 'characterData', + target: child + }); + }); + + test('characterData callback', function(cont) { + var div = document.createElement('div'); + var child = div.appendChild(document.createTextNode('text')); + var i = 0; + var observer = new MutationObserver(function(records) { + i++; + if (i > 1) + expect().fail(); + + assert.equal(records.length, 1); + + expectMutationRecord(records[0], { + type: 'characterData', + target: child + }); + + // The transient observers are removed before the callback is called. + child.data += ' again'; + records = observer.takeRecords(); + assert.equal(records.length, 0); + + cont(); + }); + observer.observe(div, { + characterData: true, + subtree: true + }); + div.removeChild(child); + child.data = 'changed'; + }); + + test('childList', function() { + var div = document.createElement('div'); + var child = div.appendChild(document.createElement('div')); + var observer = new MutationObserver(function() {}); + observer.observe(div, { + childList: true, + subtree: true + }); + div.removeChild(child); + var grandChild = child.appendChild(document.createElement('span')); + + var records = observer.takeRecords(); + assert.equal(records.length, 2); + + expectMutationRecord(records[0], { + type: 'childList', + target: div, + removedNodes: [child] + }); + + expectMutationRecord(records[1], { + type: 'childList', + target: child, + addedNodes: [grandChild] + }); + + child.removeChild(grandChild); + + records = observer.takeRecords(); + assert.equal(records.length, 1); + + expectMutationRecord(records[0], { + type: 'childList', + target: child, + removedNodes: [grandChild] + }); + }); + + test('childList callback', function(cont) { + var div = document.createElement('div'); + var child = div.appendChild(document.createElement('div')); + var i = 0; + var observer = new MutationObserver(function(records) { + i++; + if (i > 1) + expect().fail(); + + assert.equal(records.length, 2); + + expectMutationRecord(records[0], { + type: 'childList', + target: div, + removedNodes: [child] + }); + + expectMutationRecord(records[1], { + type: 'childList', + target: child, + addedNodes: [grandChild] + }); + + // The transient observers are removed before the callback is called. + child.removeChild(grandChild); + + records = observer.takeRecords(); + assert.equal(records.length, 0); + + cont(); + }); + observer.observe(div, { + childList: true, + subtree: true + }); + div.removeChild(child); + var grandChild = child.appendChild(document.createElement('span')); + }); + + // https://dom.spec.whatwg.org/#notify-mutation-observers + test('removed at end of microtask', function(done) { + var div = document.createElement('div'); + var child = div.appendChild(document.createTextNode('text')); + var observer = new MutationObserver(function() {}); + observer.observe(div, { + characterData: true, + subtree: true + }); + div.removeChild(child); + + records = observer.takeRecords(); + assert.equal(records.length, 0); + + // Run after all other end-of-microtask things, like observers, have + // had their chance to run. Use `setTimeout(4)` to keep the test isolated + // from details of the MutationObserver system and to have it run late + // enough on browsers without true microtasks. + setTimeout(function() { + child.data = 'changed'; + + records = observer.takeRecords(); + assert.equal(records.length, 0); + + done(); + }, 4); + }); + + }); + +}); \ No newline at end of file diff --git a/tests/ShadowDOM/js/Node.js b/tests/ShadowDOM/js/Node.js new file mode 100644 index 0000000..8a8e9c6 --- /dev/null +++ b/tests/ShadowDOM/js/Node.js @@ -0,0 +1,436 @@ +/* + * Copyright 2013 The Polymer Authors. All rights reserved. + * Use of this source code is goverened by a BSD-style + * license that can be found in the LICENSE file. + */ + +suite('Node', function() { + + var wrap = ShadowDOMPolyfill.wrap; + var unwrap = ShadowDOMPolyfill.unwrap; + + var DOCUMENT_POSITION_DISCONNECTED = Node.DOCUMENT_POSITION_DISCONNECTED; + var DOCUMENT_POSITION_PRECEDING = Node.DOCUMENT_POSITION_PRECEDING; + var DOCUMENT_POSITION_FOLLOWING = Node.DOCUMENT_POSITION_FOLLOWING; + var DOCUMENT_POSITION_CONTAINS = Node.DOCUMENT_POSITION_CONTAINS; + var DOCUMENT_POSITION_CONTAINED_BY = Node.DOCUMENT_POSITION_CONTAINED_BY; + var DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC = Node.DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC; + + suite('compareDocumentPosition', function() { + + test('between wrappers', function() { + var div = document.createElement('div'); + div.innerHTML = ''; + var a = div.firstChild; + var b = a.firstChild; + var c = a.lastChild; + + assert.equal(div.compareDocumentPosition(div), 0); + assert.equal(div.compareDocumentPosition(a), + DOCUMENT_POSITION_CONTAINED_BY | DOCUMENT_POSITION_FOLLOWING); + assert.equal(div.compareDocumentPosition(b), + DOCUMENT_POSITION_CONTAINED_BY | DOCUMENT_POSITION_FOLLOWING); + assert.equal(div.compareDocumentPosition(c), + DOCUMENT_POSITION_CONTAINED_BY | DOCUMENT_POSITION_FOLLOWING); + + assert.equal(a.compareDocumentPosition(div), + DOCUMENT_POSITION_CONTAINS | DOCUMENT_POSITION_PRECEDING); + assert.equal(a.compareDocumentPosition(a), 0); + assert.equal(a.compareDocumentPosition(b), + DOCUMENT_POSITION_CONTAINED_BY | DOCUMENT_POSITION_FOLLOWING); + assert.equal(a.compareDocumentPosition(c), + DOCUMENT_POSITION_CONTAINED_BY | DOCUMENT_POSITION_FOLLOWING); + + assert.equal(b.compareDocumentPosition(div), + DOCUMENT_POSITION_CONTAINS | DOCUMENT_POSITION_PRECEDING); + assert.equal(b.compareDocumentPosition(a), + DOCUMENT_POSITION_CONTAINS | DOCUMENT_POSITION_PRECEDING); + assert.equal(b.compareDocumentPosition(b), 0); + assert.equal(b.compareDocumentPosition(c), + DOCUMENT_POSITION_FOLLOWING); + + assert.equal(c.compareDocumentPosition(div), + DOCUMENT_POSITION_CONTAINS | DOCUMENT_POSITION_PRECEDING); + assert.equal(c.compareDocumentPosition(a), + DOCUMENT_POSITION_CONTAINS | DOCUMENT_POSITION_PRECEDING); + assert.equal(c.compareDocumentPosition(b), + DOCUMENT_POSITION_PRECEDING); + assert.equal(c.compareDocumentPosition(c), 0); + + // WebKit uses DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC. + assert.notEqual(document.compareDocumentPosition(div) & + DOCUMENT_POSITION_DISCONNECTED, 0) + }); + + var doc = wrap(document); + test('with document', function() { + assert.equal(doc.compareDocumentPosition(doc), 0); + assert.equal(doc.compareDocumentPosition(document), 0); + assert.equal(document.compareDocumentPosition(document), 0); + assert.equal(document.compareDocumentPosition(doc), 0); + }); + test('with document.body', function() { + assert.equal(doc.body.compareDocumentPosition(doc.body), 0); + assert.equal(doc.body.compareDocumentPosition(document.body), 0); + assert.equal(document.body.compareDocumentPosition(document.body), 0); + assert.equal(document.body.compareDocumentPosition(doc.body), 0); + }); + test('with document.head', function() { + assert.equal(doc.head.compareDocumentPosition(doc.head), 0); + assert.equal(doc.head.compareDocumentPosition(document.head), 0); + assert.equal(document.head.compareDocumentPosition(document.head), 0); + assert.equal(document.head.compareDocumentPosition(doc.head), 0); + }); + test('with document.documentElement', function() { + assert.equal(doc.documentElement.compareDocumentPosition( + doc.documentElement), 0); + assert.equal(doc.documentElement.compareDocumentPosition( + document.documentElement), 0); + assert.equal(document.documentElement.compareDocumentPosition( + document.documentElement), 0); + assert.equal(document.documentElement.compareDocumentPosition( + doc.documentElement), 0); + }); + }); + + test('ownerDocument with template and shadow root', function() { + var div = document.createElement('div'); + div.innerHTML = ''; + + var content1 = div.firstChild.content; + var host = content1.firstChild; + + div.innerHTML = ''; + var content2 = div.firstChild.content; + var x = content2.firstChild; + + var sr = host.createShadowRoot(); + sr.appendChild(content2); + + assert.equal(x.parentNode, sr); + assert.equal(x.ownerDocument, sr.ownerDocument); + assert.equal(sr.ownerDocument, host.ownerDocument); + + var doc = wrap(document); + doc.body.appendChild(host); + assert.equal(host.ownerDocument, doc); + assert.equal(sr.ownerDocument, doc); + assert.equal(x.ownerDocument, doc); + + doc.body.removeChild(host); + }); + + test('ownerDocument when appending to document', function() { + var doc1 = document.implementation.createHTMLDocument(''); + var comment = doc1.createComment(''); + doc1.appendChild(comment); + assert.equal(doc1, comment.ownerDocument); + + var doc2 = document.implementation.createHTMLDocument(''); + doc2.appendChild(comment); + assert.equal(doc2, comment.ownerDocument); + }); + + test('removeChild resets pointers', function() { + var host = document.createElement('div'); + host.innerHTML = ''; + var a = host.firstChild; + var sr = host.createShadowRoot(); + + host.offsetHeight; + + host.removeChild(a); + + expectStructure(a, {}); + + var div = document.createElement('div'); + div.appendChild(a); + + expectStructure(div, { + firstChild: a, + lastChild: a + }); + + expectStructure(a, { + parentNode: div + }); + }); + + test('replaceChild resets pointers', function() { + var host = document.createElement('div'); + host.innerHTML = ''; + var a = host.firstChild; + var sr = host.createShadowRoot(); + + host.offsetHeight; + + var b = document.createElement('b'); + + host.replaceChild(b, a); + + expectStructure(a, {}); + + expectStructure(b, { + parentNode: host + }); + + var div = document.createElement('div'); + div.appendChild(a); + + expectStructure(div, { + firstChild: a, + lastChild: a + }); + + expectStructure(a, { + parentNode: div + }); + }); + + test('appendChild resets pointers', function() { + var host1 = document.createElement('div'); + host1.innerHTML = ''; + var a = host1.firstChild; + var sr1 = host1.createShadowRoot(); + + var host2 = document.createElement('div'); + host2.innerHTML = ''; + var b = host2.firstChild; + var sr2 = host2.createShadowRoot(); + + host1.offsetHeight; + host2.offsetHeight; + + host1.appendChild(b); + + expectStructure(host1, { + firstChild: a, + lastChild: b + }); + + expectStructure(a, { + parentNode: host1, + nextSibling: b + }); + + expectStructure(b, { + parentNode: host1, + previousSibling: a + }); + + expectStructure(host2, {}); + }); + + test('hasChildNodes without a shadow root', function() { + var div = document.createElement('div'); + + assert.isFalse(div.hasChildNodes(), 'should be false with no children'); + + div.innerHTML = ''; + assert.isTrue(div.hasChildNodes(), 'should be true with a single child'); + + div.innerHTML = '
    '; + assert.isTrue(div.hasChildNodes(), 'should be true with multiple children'); + }); + + test('parentElement', function() { + var a = document.createElement('a'); + a.textContent = 'text'; + var textNode = a.firstChild; + assert.equal(textNode.parentElement, a); + assert.isNull(a.parentElement); + + var doc = wrap(document); + var body = doc.body; + var documentElement = doc.documentElement; + assert.equal(body.parentElement, documentElement); + assert.isNull(documentElement.parentElement); + }); + + test('contains', function() { + var div = document.createElement('div'); + assert.isTrue(div.contains(div)); + + div.textContent = 'a'; + var textNode = div.firstChild; + assert.isTrue(textNode.contains(textNode)); + assert.isTrue(div.contains(textNode)); + assert.isFalse(textNode.contains(div)); + + var doc = div.ownerDocument; + assert.isTrue(doc.contains(doc)); + assert.isFalse(doc.contains(div)); + assert.isFalse(doc.contains(textNode)); + + assert.isFalse(div.contains(null)); + assert.isFalse(div.contains()); + }); + + test('instanceof', function() { + var div = document.createElement('div'); + assert.instanceOf(div, HTMLElement); + assert.instanceOf(div, Element); + assert.instanceOf(div, EventTarget); + }); + + test('cloneNode(false)', function() { + var doc = wrap(document); + var a = document.createElement('a'); + a.href = 'http://domain.com/'; + a.textContent = 'text'; + var textNode = a.firstChild; + + var aClone = a.cloneNode(false); + + assert.equal(aClone.tagName, 'A'); + assert.equal(aClone.href, 'http://domain.com/'); + expectStructure(aClone, {}); + }); + + test('cloneNode(true)', function() { + var doc = wrap(document); + var a = document.createElement('a'); + a.href = 'http://domain.com/'; + a.textContent = 'text'; + var textNode = a.firstChild; + + var aClone = a.cloneNode(true); + var textNodeClone = aClone.firstChild; + + assert.equal(aClone.tagName, 'A'); + assert.equal(aClone.href, 'http://domain.com/'); + expectStructure(aClone, { + firstChild: textNodeClone, + lastChild: textNodeClone + }); + expectStructure(textNodeClone, { + parentNode: aClone + }); + }); + + test('cloneNode with shadowRoot', function() { + var div = document.createElement('div'); + var a = div.appendChild(document.createElement('a')); + var sr = a.createShadowRoot(); + sr.innerHTML = ''; + div.offsetHeight; + assert.equal(unwrap(div).innerHTML, ''); + + var clone = div.cloneNode(true); + assert.equal(clone.innerHTML, ''); + clone.offsetHeight; + // shadow roots are not cloned. + assert.equal(unwrap(clone).innerHTML, ''); + }); + + test('insertBefore', function() { + var parent = document.createElement('div'); + var c1 = document.createElement('div'); + var c2 = document.createElement('div'); + var c3 = document.createElement('div'); + parent.insertBefore(c3); + parent.insertBefore(c2, c3); + parent.insertBefore(c1, c2); + + assert.equal(parent.firstChild, c1); + assert.equal(c1.nextElementSibling, c2); + assert.equal(c2.nextElementSibling, c3); + }); + + test('textContent of comment', function() { + var comment = document.createComment('abc'); + assert.equal(comment.textContent, 'abc'); + }); + + test('textContent ignores comments', function() { + var div = document.createElement('div'); + div.innerHTML = 'abef'; + assert.equal(div.textContent, 'abef'); + }); + + test('null textContent', function() { + var div = document.createElement('div'); + var root = div.createShadowRoot(); + div.textContent = null; + assert.equal(div.textContent, ''); + }); + + test('normalize', function() { + var div = document.createElement('div'); + div.appendChild(document.createTextNode('foo\n')); + var span = document.createElement('span'); + span.appendChild(document.createTextNode('buzz')); + span.appendChild(document.createTextNode('quux')); + div.appendChild(span); + div.appendChild(document.createTextNode('bar\n')); + assert.equal(div.textContent, 'foo\nbuzzquuxbar\n'); + + div.normalize(); + + assert.equal(div.textContent, 'foo\nbuzzquuxbar\n'); + assert.equal(div.childNodes.length, 3); + assert.equal(div.firstChild.textContent, 'foo\n'); + assert.equal(div.firstChild.nextSibling, span); + assert.equal(span.childNodes.length, 1); + assert.equal(span.firstChild.textContent, 'buzzquux'); + assert.equal(span.nextSibling, div.lastChild); + assert.equal(div.lastChild.textContent, 'bar\n'); + }); + + test('normalize with shadowroot', function() { + var div = document.createElement('div'); + div.appendChild(document.createTextNode('foo\n')); + var sr = div.createShadowRoot(); + sr.appendChild(document.createTextNode('buzz')); + sr.appendChild(document.createTextNode('quux')); + div.appendChild(document.createTextNode('bar\n')); + assert.equal(div.textContent, 'foo\nbar\n'); + assert.equal(sr.textContent, 'buzzquux'); + + div.normalize(); + + assert.equal(div.textContent, 'foo\nbar\n'); + assert.equal(sr.textContent, 'buzzquux'); + assert.equal(div.childNodes.length, 1); + assert.equal(div.firstChild.textContent, 'foo\nbar\n'); + assert.equal(sr.childNodes.length, 2); + assert.equal(sr.firstChild.textContent, 'buzz'); + assert.equal(sr.firstChild.nextSibling.textContent, 'quux'); + }); + + test('normalize - issue 441', function() { + var div = document.createElement('div'); + div.appendChild(document.createTextNode('a')); + div.appendChild(document.createTextNode('b')); + div.appendChild(document.createElement('span')); + div.appendChild(document.createTextNode('c')); + div.appendChild(document.createTextNode('d')); + + div.normalize(); + + assert.equal(div.textContent, 'abcd'); + assert.equal(div.childNodes.length, 3); + }); + + test('appendChild last and first', function() { + var a = document.createElement('a'); + a.innerHTML = ''; + var b = a.firstChild; + var sr = a.createShadowRoot(); + + var c = document.createElement('c'); + c.innerHTML = ''; + var d = c.firstChild; + c.appendChild(b); + + var cs = c.childNodes; + assert.equal(cs.length, 2); + assert.equal(cs[0], d); + assert.equal(cs[1], b); + + c.removeChild(b); + cs = c.childNodes; + assert.equal(cs.length, 1); + assert.equal(cs[0], d); + }); +}); diff --git a/tests/ShadowDOM/js/ParentNodeInterface.js b/tests/ShadowDOM/js/ParentNodeInterface.js new file mode 100644 index 0000000..35ac61c --- /dev/null +++ b/tests/ShadowDOM/js/ParentNodeInterface.js @@ -0,0 +1,82 @@ +/* + * Copyright 2013 The Polymer Authors. All rights reserved. + * Use of this source code is goverened by a BSD-style + * license that can be found in the LICENSE file. + */ + +suite('ParentNodeInterface', function() { + + test('childElementCount', function() { + var div = document.createElement('div'); + div.innerHTML = 'ac'; + assert.equal(div.childElementCount, 1); + div.appendChild(document.createElement('d')); + assert.equal(div.childElementCount, 2); + div.appendChild(document.createTextNode('e')); + assert.equal(div.childElementCount, 2); + + var sr = div.createShadowRoot(); + sr.innerHTML = 'fg'; + + div.offsetHeight; // trigger rendering + + assert.equal(sr.childElementCount, 1); + assert.equal(div.childElementCount, 2); + }); + + test('children', function() { + var div = document.createElement('div'); + div.innerHTML = 'ac'; + var b = div.firstChild.nextSibling; + + assertArrayEqual(div.children, [b]); + var d = div.appendChild(document.createElement('d')); + assertArrayEqual(div.children, [b, d]); + div.appendChild(document.createTextNode('e')); + assertArrayEqual(div.children, [b, d]); + + var sr = div.createShadowRoot(); + sr.innerHTML = 'fg'; + var content = sr.firstChild.nextSibling; + + div.offsetHeight; // trigger rendering + + assertArrayEqual(sr.children, [content]); + assertArrayEqual(div.children, [b, d]); + }); + + test('firstElementChild', function() { + var div = document.createElement('div'); + div.innerHTML = 'ac'; + var b = div.firstChild.nextSibling; + + assert.equal(div.firstElementChild, b); + + var sr = div.createShadowRoot(); + sr.innerHTML = 'fg'; + var content = sr.firstChild.nextSibling; + + div.offsetHeight; // trigger rendering + + assert.equal(sr.firstElementChild, content); + assert.equal(div.firstElementChild, b); + }); + + test('lastElementChild', function() { + var div = document.createElement('div'); + div.innerHTML = 'ac'; + var b = div.firstChild.nextSibling; + + assert.equal(div.lastElementChild, b); + + var sr = div.createShadowRoot(); + sr.innerHTML = 'fg'; + var content = sr.firstChild.nextSibling; + + div.offsetHeight; // trigger rendering + + assert.equal(sr.lastElementChild, content); + assert.equal(div.lastElementChild, b); + }); + +}); diff --git a/tests/ShadowDOM/js/Range.js b/tests/ShadowDOM/js/Range.js new file mode 100644 index 0000000..80c3779 --- /dev/null +++ b/tests/ShadowDOM/js/Range.js @@ -0,0 +1,82 @@ +/* + * Copyright 2013 The Polymer Authors. All rights reserved. + * Use of this source code is goverened by a BSD-style + * license that can be found in the LICENSE file. + */ + +suite('Range', function() { + + var wrap = ShadowDOMPolyfill.wrap; + + var div; + + teardown(function() { + if (div && div.parentNode) + div.parentNode.removeChild(div); + div = undefined; + }); + + test('instanceof', function() { + var range = document.createRange(); + assert.instanceOf(range, Range); + + var range2 = wrap(document).createRange(); + assert.instanceOf(range2, Range); + }); + + test('constructor', function() { + var range = document.createRange(); + assert.equal(Range, range.constructor); + }); + + test('createContextualFragment', function() { + // IE9 does not support createContextualFragment. + if (!Range.prototype.createContextualFragment) + return; + + var range = document.createRange(); + var container = document.body || document.head; + + range.selectNode(container); + + var fragment = range.createContextualFragment(''); + + assert.instanceOf(fragment, DocumentFragment); + assert.equal(fragment.firstChild.localName, 'b'); + assert.equal(fragment.childNodes.length, 1); + }); + + test('WebIDL attributes', function() { + var range = document.createRange(); + + assert.isTrue('collapsed' in range); + assert.isFalse(range.hasOwnProperty('collapsed')); + + assert.isTrue('commonAncestorContainer' in range); + assert.isFalse(range.hasOwnProperty('commonAncestorContainer')); + + assert.isTrue('endContainer' in range); + assert.isFalse(range.hasOwnProperty('endContainer')); + + assert.isTrue('endOffset' in range); + assert.isFalse(range.hasOwnProperty('endOffset')); + + assert.isTrue('startContainer' in range); + assert.isFalse(range.hasOwnProperty('startContainer')); + + assert.isTrue('startOffset' in range); + assert.isFalse(range.hasOwnProperty('startOffset')); + }); + + test('toString', function() { + var range = document.createRange(); + div = document.createElement('div'); + document.body.appendChild(div); + div.innerHTML = 'abc'; + var a = div.firstChild; + var b = a.nextSibling; + range.selectNode(b); + assert.equal(range.toString(), 'b'); + }); + +}); diff --git a/tests/ShadowDOM/js/SVGElement.js b/tests/ShadowDOM/js/SVGElement.js new file mode 100644 index 0000000..fb1dd46 --- /dev/null +++ b/tests/ShadowDOM/js/SVGElement.js @@ -0,0 +1,71 @@ +/* + * Copyright 2014 The Polymer Authors. All rights reserved. + * Use of this source code is goverened by a BSD-style + * license that can be found in the LICENSE file. + */ + +suite('SVGElement', function() { + + var SVG_NS = 'http://www.w3.org/2000/svg'; + + test('Basics', function() { + var el = document.createElementNS(SVG_NS, 'svg'); + + assert.equal(el.localName, 'svg'); + assert.equal(el.tagName, 'svg'); + assert.equal(el.namespaceURI, SVG_NS); + assert.instanceOf(el, SVGElement); + assert.instanceOf(el, Element); + assert.instanceOf(el, Node); + assert.instanceOf(el, EventTarget); + assert.notInstanceOf(el, HTMLElement); + }); + + test('Basics innerHTML', function() { + var div = document.createElement('div'); + div.innerHTML = ''; + var el = div.firstChild; + + assert.equal(el.localName, 'svg'); + assert.equal(el.tagName, 'svg'); + assert.equal(el.namespaceURI, SVG_NS); + assert.instanceOf(el, SVGElement); + assert.instanceOf(el, Element); + assert.instanceOf(el, Node); + assert.instanceOf(el, EventTarget); + assert.notInstanceOf(el, HTMLElement); + }); + + test('template', function() { + var el = document.createElementNS(SVG_NS, 'template'); + + assert.equal(el.localName, 'template'); + assert.equal(el.tagName, 'template'); + assert.equal(el.namespaceURI, SVG_NS); + + // IE does not create an SVGElement if the local name is not a known SVG + // element. + // Safari 7 has the same issue but nightly WebKit works as expected. + // assert.instanceOf(el, SVGElement); + + assert.instanceOf(el, Element); + assert.instanceOf(el, Node); + assert.instanceOf(el, EventTarget); + assert.notInstanceOf(el, HTMLElement); + }); + + test('classList', function() { + var el = document.createElementNS(SVG_NS, 'svg'); + el.setAttribute('class', 'a b'); + + assert.isTrue(el.classList === undefined || + (el.classList instanceof DOMTokenList)); + + if (el.classList !== undefined) { + assert.equal(el.classList.length, 2); + assert.isTrue(el.classList.contains('a')); + assert.isTrue(el.classList.contains('b')); + } + }); + +}); diff --git a/tests/ShadowDOM/js/SVGElementInstance.js b/tests/ShadowDOM/js/SVGElementInstance.js new file mode 100644 index 0000000..22156b9 --- /dev/null +++ b/tests/ShadowDOM/js/SVGElementInstance.js @@ -0,0 +1,118 @@ +/* + * Copyright 2014 The Polymer Authors. All rights reserved. + * Use of this source code is goverened by a BSD-style + * license that can be found in the LICENSE file. + */ + +suite('SVGElementInstance', function() { + + var div; + + teardown(function() { + if (div) { + if (div.parentNode) + div.parentNode.removeChild(div); + div = undefined; + } + }); + + var svgCode = '\ + \ + \ + \ + \ + \ + \ + \ + '; + + function getInstanceRoot() { + div = document.body.appendChild(document.createElement('div')); + div.innerHTML = svgCode; + var svg = div.firstElementChild; + var useElement = svg.firstElementChild.nextElementSibling; + return useElement.instanceRoot; + } + + test('instanceof SVGUseElement', function() { + div = document.body.appendChild(document.createElement('div')); + div.innerHTML = svgCode; + var svg = div.firstElementChild; + var useElement = svg.firstElementChild.nextElementSibling; + assert.instanceOf(useElement, SVGUseElement); + assert.instanceOf(useElement, SVGElement); + assert.instanceOf(useElement, Element); + assert.instanceOf(useElement, EventTarget); + }); + + test('instanceof SVGElementInstance', function() { + // Firefox does not implement SVGElementInstance. + if (/Firefox/.test(navigator.userAgent)) + return; + + var instanceRoot = getInstanceRoot(); + + // Safari 6 seems to return null here in some cases. + if (!instanceRoot) + return; + + assert.instanceOf(instanceRoot, SVGElementInstance); + assert.instanceOf(instanceRoot, EventTarget); + }); + + test('correspondingUseElement', function() { + // Firefox does not implement SVGElementInstance. + if (/Firefox/.test(navigator.userAgent)) + return; + + div = document.body.appendChild(document.createElement('div')); + div.innerHTML = svgCode; + var svg = div.firstElementChild; + var useElement = svg.firstElementChild.nextElementSibling; + var instanceRoot = useElement.instanceRoot; + + // Safari 6 seems to return null here in some cases. + if (!instanceRoot) + return; + + assert.equal(useElement, instanceRoot.correspondingUseElement); + }); + + test('correspondingElement', function() { + // Firefox does not implement SVGElementInstance. + if (/Firefox/.test(navigator.userAgent)) + return; + + var instanceRoot = getInstanceRoot(); + + // Safari 6 seems to return null here in some cases. + if (!instanceRoot) + return; + + assert.equal('g', instanceRoot.correspondingElement.localName); + }); + + test('tree', function() { + // Firefox does not implement SVGElementInstance. + if (/Firefox/.test(navigator.userAgent)) + return; + + var instanceRoot = getInstanceRoot(); + + // Safari 6 seems to return null here in some cases. + if (!instanceRoot) + return; + + assert.equal('line', instanceRoot.firstChild.correspondingElement.localName); + assert.equal('line', instanceRoot.lastChild.correspondingElement.localName); + + // IE always returns new wrappers for all the accessors. + if (/Trident/.test(navigator.userAgent)) + return; + + assert.equal(instanceRoot.firstChild, instanceRoot.lastChild); + assert.equal(instanceRoot, instanceRoot.firstChild.parentNode); + assert.isNull(instanceRoot.parentNode); + }); + +}); diff --git a/tests/ShadowDOM/js/Selection.js b/tests/ShadowDOM/js/Selection.js new file mode 100644 index 0000000..5644292 --- /dev/null +++ b/tests/ShadowDOM/js/Selection.js @@ -0,0 +1,174 @@ +/* + * Copyright 2014 The Polymer Authors. All rights reserved. + * Use of this source code is goverened by a BSD-style + * license that can be found in the LICENSE file. + */ + +suite('Selection', function() { + + var wrap = ShadowDOMPolyfill.wrap; + var div, a, b, c; + + teardown(function() { + if (div && div.parentNode) + div.parentNode.removeChild(div); + div = a = b = c = undefined; + }); + + setup(function() { + div = document.createElement('div'); + div.innerHTML = 'abc'; + a = div.firstChild; + b = a.nextSibling; + c = div.lastChild; + document.body.appendChild(div); + }); + + + test('document.getSelection()', function() { + var selection = document.getSelection(); + assert.instanceOf(selection, Selection); + + var doc = wrap(document); + selection = doc.getSelection(); + assert.instanceOf(selection, Selection); + }); + + test('window.getSelection()', function() { + var selection = window.getSelection(); + assert.instanceOf(selection, Selection); + + var win = wrap(window); + selection = win.getSelection(); + assert.instanceOf(selection, Selection); + }); + + test('constructor', function() { + var selection = window.getSelection(); + assert.equal(Selection, selection.constructor); + }); + + test('getSelection()', function() { + var selection = getSelection(); + assert.instanceOf(selection, Selection); + }); + + test('basics', function() { + var selection = window.getSelection(); + selection.selectAllChildren(div); + + assert.equal(selection.toString(), 'abc'); + + assert.isFalse(selection.isCollapsed); + assert.equal(selection.rangeCount, 1); + + // https://code.google.com/p/chromium/issues/detail?id=336821 + if (/WebKit/.test(navigator.userAgent)) + return; + + assert.equal(selection.anchorNode, div); + assert.equal(selection.anchorOffset, 0); + + assert.equal(selection.focusNode, div); + assert.equal(selection.focusOffset, 3); + }); + + test('getRangeAt', function() { + var selection = window.getSelection(); + selection.selectAllChildren(div); + var range = selection.getRangeAt(0); + assert.instanceOf(range, Range); + }); + + test('collapse', function() { + var selection = window.getSelection(); + + for (var i = 0; i < 4; i++) { + selection.selectAllChildren(div); + selection.collapse(div, i); + + assert.isTrue(selection.isCollapsed); + assert.equal(selection.toString(), ''); + + // https://code.google.com/p/chromium/issues/detail?id=336821 + if (/WebKit/.test(navigator.userAgent)) + continue; + + assert.equal(selection.anchorNode, div); + assert.equal(selection.anchorOffset, i); + + assert.equal(selection.focusNode, div); + assert.equal(selection.focusOffset, i); + } + }); + + test('extend', function() { + // IE does not have extend. + if (/Trident/.test(navigator.userAgent)) + return; + + var selection = window.getSelection(); + + for (var i = 0; i < 4; i++) { + selection.selectAllChildren(div); + selection.extend(div, i); + + assert.equal(selection.isCollapsed, i === 0); + assert.equal(selection.toString(), 'abc'.slice(0, i)); + + // https://code.google.com/p/chromium/issues/detail?id=336821 + if (/WebKit/.test(navigator.userAgent)) + continue; + + assert.equal(selection.anchorNode, div); + assert.equal(selection.anchorOffset, 0); + + assert.equal(selection.focusNode, div); + assert.equal(selection.focusOffset, i); + } + }); + + test('addRange', function() { + var selection = window.getSelection(); + var range = document.createRange(); + range.selectNode(b); + selection.addRange(range); + + // Uncertain why this fails in Blink. The same test passes without the + // shadow dom polyfill in Blink. + if (/WebKit/.test(navigator.userAgent)) + return; + + assert.equal(selection.toString(), 'b'); + }); + + test('removeRange', function() { + // Not implemented in Blink. + if (/WebKit/.test(navigator.userAgent)) + return; + + var selection = window.getSelection(); + selection.selectAllChildren(div); + var range = selection.getRangeAt(0); + selection.removeRange(range); + assert.equal(selection.toString(), ''); + }); + + test('containsNode', function() { + // IE does not have containsNode. + if (/Trident/.test(navigator.userAgent)) + return; + + var selection = window.getSelection(); + selection.selectAllChildren(div); + + assert.isFalse(selection.containsNode(div)); + assert.isFalse(selection.containsNode(document)); + assert.isFalse(selection.containsNode(document.body)); + + assert.isTrue(selection.containsNode(a, true)); + assert.isTrue(selection.containsNode(b, true)); + assert.isTrue(selection.containsNode(c, true)); + }); + +}); diff --git a/tests/ShadowDOM/js/ShadowRoot.js b/tests/ShadowDOM/js/ShadowRoot.js new file mode 100644 index 0000000..ce28ef6 --- /dev/null +++ b/tests/ShadowDOM/js/ShadowRoot.js @@ -0,0 +1,102 @@ +// Copyright 2013 The Polymer Authors. All rights reserved. +// Use of this source code is goverened by a BSD-style +// license that can be found in the LICENSE file. + +suite('ShadowRoot', function() { + + var div; + teardown(function() { + if (div) { + if (div.parentNode) + div.parentNode.removeChild(div); + div = undefined; + } + }); + + test('elementFromPoint', function() { + div = document.body.appendChild(document.createElement('div')); + div.style.cssText = 'position: fixed; background: red; ' + + 'width: 10px; height: 10px; top: 0; left: 0;'; + var sr = div.createShadowRoot(); + sr.innerHTML = ''; + var a = sr.firstChild; + a.style.cssText = 'position: absolute; width: 100%; height: 100%; ' + + 'background: green'; + + assert.equal(sr.elementFromPoint(5, 5), a); + + var sr2 = a.createShadowRoot(); + assert.equal(sr.elementFromPoint(5, 5), a); + assert.equal(sr2.elementFromPoint(5, 5), null); + }); + + test('getElementById', function() { + var div = document.createElement('div'); + var sr = div.createShadowRoot(); + sr.innerHTML = ''; + var a = sr.firstChild; + var b = sr.lastChild; + + assert.equal(sr.getElementById('a'), a); + assert.equal(sr.getElementById('b'), b); + }); + + test('getElementById with a non CSS ID', function() { + var div = document.createElement('div'); + var sr = div.createShadowRoot(); + sr.innerHTML = ''; + var a = sr.firstChild; + var b = sr.lastChild; + + assert.equal(sr.getElementById(1), a); + assert.equal(sr.getElementById(2), b); + }); + + test('getElementById with a non ID', function() { + var div = document.createElement('div'); + var sr = div.createShadowRoot(); + sr.innerHTML = ''; + var a = sr.firstChild; + + assert.isNull(sr.getElementById('a b')); + }); + + test('olderShadowRoot', function() { + var host = document.createElement('div'); + host.innerHTML = 'ab'; + var a = host.firstChild; + var b = host.lastChild; + + var sr = host.createShadowRoot(); + sr.innerHTML = 'a'; + + host.offsetWidth; + assert.isNull(sr.olderShadowRoot); + + var sr2 = host.createShadowRoot(); + sr2.innerHTML = 'b'; + + host.offsetWidth; + assert.equal(sr2.olderShadowRoot, sr); + }); + + test('host', function() { + var host = document.createElement('div'); + var sr = host.createShadowRoot(); + assert.equal(host, sr.host); + + var sr2 = host.createShadowRoot(); + assert.equal(host, sr2.host); + }); + + test('instanceof', function() { + var sr = document.createElement('div').createShadowRoot(); + assert.instanceOf(sr, ShadowRoot); + }); + + test('constructor', function() { + var sr = document.createElement('div').createShadowRoot(); + assert.equal(ShadowRoot, sr.constructor); + }); + +}); diff --git a/tests/ShadowDOM/js/Text.js b/tests/ShadowDOM/js/Text.js new file mode 100644 index 0000000..b97eea2 --- /dev/null +++ b/tests/ShadowDOM/js/Text.js @@ -0,0 +1,66 @@ +/* + * Copyright 2013 The Polymer Authors. All rights reserved. + * Use of this source code is goverened by a BSD-style + * license that can be found in the LICENSE file. + */ + +suite('Text', function() { + + test('instanceof', function() { + var div = document.createElement('div'); + div.textContent = 'abc'; + assert.instanceOf(div.firstChild, Text); + }); + + test('constructor', function() { + var div = document.createElement('div'); + div.textContent = 'abc'; + assert.equal(Text, div.firstChild.constructor); + }); + + test('splitText', function() { + var t = document.createTextNode('abcd'); + var t2 = t.splitText(3); + assert.equal(t.data, 'abc'); + assert.equal(t2.data, 'd'); + + t = document.createTextNode('abcd'); + t2 = t.splitText(0); + assert.equal(t.data, ''); + assert.equal(t2.data, 'abcd'); + + t = document.createTextNode('abcd'); + t2 = t.splitText(4); + assert.equal(t.data, 'abcd'); + assert.equal(t2.data, ''); + }); + + test('splitText with too large offset', function() { + var t = document.createTextNode('abcd'); + assert.throws(function() { + t.splitText(5); + }); + }); + + test('splitText negative offset', function() { + var t = document.createTextNode('abcd'); + assert.throws(function() { + t.splitText(-1); + }); + }); + + test('splitText siblings', function() { + var div = document.createElement('div'); + div.innerHTML = 'abcd'; + var t = div.firstChild; + var b = div.lastChild; + + var t2 = t.splitText(3); + assert.equal(t.data, 'abc'); + assert.equal(t2.data, 'd'); + + assert.equal(t.nextSibling, t2); + assert.equal(t2.nextSibling, b); + }); + +}); diff --git a/tests/ShadowDOM/js/TouchEvent.js b/tests/ShadowDOM/js/TouchEvent.js new file mode 100644 index 0000000..795fddb --- /dev/null +++ b/tests/ShadowDOM/js/TouchEvent.js @@ -0,0 +1,118 @@ +/* + * Copyright 2014 The Polymer Authors. All rights reserved. + * Use of this source code is goverened by a BSD-style + * license that can be found in the LICENSE file. + */ + +htmlSuite('Events', function() { + + var unwrap = ShadowDOMPolyfill.unwrap; + var wrap = ShadowDOMPolyfill.wrap; + + try { + document.createEvent('TouchEvent'); + } catch (ex) { + // Touch events are not supported + return; + } + + function createMockTouch(nativeTarget) { + return { + clientX: 1, + clientY: 2, + screenX: 3, + screenY: 4, + pageX: 5, + pageY: 6, + identifier: 7, + webkitRadiusX: 8, + webkitRadiusY: 9, + webkitRotationAngle: 10, + webkitForce: 11, + target: nativeTarget + }; + } + + test('TouchEvent', function() { + var e = document.createEvent('TouchEvent'); + assert.instanceOf(e, TouchEvent); + assert.instanceOf(e, UIEvent); + assert.instanceOf(e, Event); + }); + + test('constructor', function() { + var e = document.createEvent('TouchEvent'); + assert.equal(TouchEvent, e.constructor); + }); + + test('Touch', function() { + // There is no way to create a native Touch object so we use a mock impl. + + var target = document.createElement('div'); + var impl = createMockTouch(unwrap(target)); + var touch = new Touch(impl); + + assert.equal(touch.clientX, 1); + assert.equal(touch.clientY, 2); + assert.equal(touch.screenX, 3); + assert.equal(touch.screenY, 4); + assert.equal(touch.pageX, 5); + assert.equal(touch.pageY, 6); + assert.equal(touch.identifier, 7); + assert.equal(touch.webkitRadiusX, 8); + assert.equal(touch.webkitRadiusY, 9); + assert.equal(touch.webkitRotationAngle, 10); + assert.equal(touch.webkitForce, 11); + assert.equal(touch.target, target); + }); + + test('TouchList', function() { + + function createMockTouchList(elements) { + var arr = []; + for (var i = 0; i < elements.length; i++) { + arr[i] = createMockTouch(unwrap(elements[i])); + } + return arr; + } + + var a = document.createElement('a'); + var b = document.createElement('b'); + var c = document.createElement('c'); + var d = document.createElement('d'); + var e = document.createElement('e'); + var f = document.createElement('f'); + + var mockEvent = { + __proto__: unwrap(document.createEvent('TouchEvent')).__proto__, + touches: createMockTouchList([a]), + targetTouches: createMockTouchList([b, c]), + changedTouches: createMockTouchList([d, e, f]) + }; + + var event = wrap(mockEvent); + + assert.instanceOf(event.touches, TouchList); + assert.instanceOf(event.targetTouches, TouchList); + assert.instanceOf(event.changedTouches, TouchList); + + assert.equal(event.touches.length, 1); + assert.equal(event.targetTouches.length, 2); + assert.equal(event.changedTouches.length, 3); + + assert.instanceOf(event.touches[0], Touch); + assert.instanceOf(event.targetTouches[0], Touch); + assert.instanceOf(event.targetTouches[1], Touch); + assert.instanceOf(event.changedTouches[0], Touch); + assert.instanceOf(event.changedTouches[1], Touch); + assert.instanceOf(event.changedTouches[2], Touch); + + assert.equal(event.touches[0].target, a); + assert.equal(event.targetTouches[0].target, b); + assert.equal(event.targetTouches[1].target, c); + assert.equal(event.changedTouches[0].target, d); + assert.equal(event.changedTouches[1].target, e); + assert.equal(event.changedTouches[2].target, f); + }); + +}); diff --git a/tests/ShadowDOM/js/TreeScope.js b/tests/ShadowDOM/js/TreeScope.js new file mode 100644 index 0000000..b6025e0 --- /dev/null +++ b/tests/ShadowDOM/js/TreeScope.js @@ -0,0 +1,86 @@ +/** + * Copyright 2014 The Polymer Authors. All rights reserved. + * Use of this source code is goverened by a BSD-style + * license that can be found in the LICENSE file. + */ + +suite('TreeScope', function() { + + var getTreeScope = ShadowDOMPolyfill.getTreeScope; + + test('Basic', function() { + var div = document.createElement('div'); + + var ts = getTreeScope(div); + assert.equal(ts.root, div); + + div.innerHTML = ''; + var a = div.firstChild; + var b = a.firstChild; + + assert.equal(getTreeScope(a), ts); + assert.equal(getTreeScope(b), ts); + }); + + test('ShadowRoot', function() { + var div = document.createElement('div'); + + var ts = getTreeScope(div); + assert.equal(ts.root, div); + + div.innerHTML = ''; + var a = div.firstChild; + var b = a.firstChild; + + var sr = a.createShadowRoot(); + + var srTs = getTreeScope(sr); + assert.equal(srTs.root, sr); + assert.equal(srTs.parent, ts); + + sr.innerHTML = ''; + var c = sr.firstChild; + var d = c.firstChild; + + assert.equal(getTreeScope(c), srTs); + assert.equal(getTreeScope(d), srTs); + }); + + test('change parent in shadow', function() { + var div = document.createElement('div'); + div.innerHTML = ''; + var a = div.firstChild; + + var sr = a.createShadowRoot(); + sr.innerHTML = ''; + var b = sr.firstChild; + + var sr2 = b.createShadowRoot(); + sr2.innerHTML = ''; + var c = sr2.firstChild; + + var sr3 = a.createShadowRoot(); + sr3.innerHTML = ''; + var d = sr3.firstChild; + + var ts1 = getTreeScope(a); + var ts2 = getTreeScope(b); + var ts3 = getTreeScope(c); + var ts4 = getTreeScope(d); + + assert.equal(ts1.parent, null); + assert.equal(ts2.parent, ts1); + assert.equal(ts3.parent, ts2); + assert.equal(ts4.parent, ts2); + + var div2 = document.createElement('div'); + div2.appendChild(a); + + var ts5 = getTreeScope(a); + assert.notEqual(ts1, ts5); + assert.equal(ts2.parent, ts5); + assert.equal(ts3.parent, ts2); + assert.equal(ts4.parent, ts5); + }); + +}); diff --git a/tests/ShadowDOM/js/Window.js b/tests/ShadowDOM/js/Window.js new file mode 100644 index 0000000..67853e5 --- /dev/null +++ b/tests/ShadowDOM/js/Window.js @@ -0,0 +1,80 @@ +/* + * Copyright 2013 The Polymer Authors. All rights reserved. + * Use of this source code is goverened by a BSD-style + * license that can be found in the LICENSE file. + */ + +suite('Window', function() { + + var wrap = ShadowDOMPolyfill.wrap; + + test('addEventListener', function() { + var calls = 0; + var doc = wrap(document); + var win = wrap(window); + window.addEventListener('click', function f(e) { + calls++; + assert.equal(this, win); + assert.equal(e.target, doc.body); + assert.equal(e.currentTarget, this); + window.removeEventListener('click', f); + }); + win.addEventListener('click', function f(e) { + calls++; + assert.equal(this, win); + assert.equal(e.target, doc.body); + assert.equal(e.currentTarget, this); + win.removeEventListener('click', f); + }); + + addEventListener('click', function f(e) { + calls++; + assert.equal(this, win); + assert.equal(e.target, doc.body); + assert.equal(e.currentTarget, this); + removeEventListener('click', f); + }); + + document.body.click(); + assert.equal(3, calls); + + document.body.click(); + assert.equal(3, calls); + }); + + test('getComputedStyle', function() { + var div = document.createElement('div'); + var cs = window.getComputedStyle(div); + assert.isTrue(cs != null); + + div = document.createElement('div'); + cs = wrap(window).getComputedStyle(div); + assert.isTrue(cs != null); + + div = document.createElement('div'); + cs = getComputedStyle(div); + assert.isTrue(cs != null); + }); + + test('getComputedStyleShadow', function() { + var host = document.createElement('div'); + var root = host.createShadowRoot(); + var elt = document.createElement('div'); + root.appendChild(elt); + document.body.appendChild(host); + elt.style.padding = '4px'; + assert.equal(getComputedStyle(elt).paddingLeft, '4px'); + document.body.removeChild(host); + }); + + test('instanceof', function() { + var win = wrap(window); + assert.instanceOf(win, Window); + }); + + test('constructor', function() { + var win = wrap(window); + assert.equal(Window, win.constructor); + }); + +}); diff --git a/tests/ShadowDOM/js/XMLHttpRequest.js b/tests/ShadowDOM/js/XMLHttpRequest.js new file mode 100644 index 0000000..7ade23c --- /dev/null +++ b/tests/ShadowDOM/js/XMLHttpRequest.js @@ -0,0 +1,23 @@ +/* + * Copyright 2014 The Polymer Authors. All rights reserved. + * Use of this source code is goverened by a BSD-style + * license that can be found in the LICENSE file. + */ + +suite('XMLHttpRequest', function() { + + var wrap = ShadowDOMPolyfill.wrap; + var unwrap = ShadowDOMPolyfill.unwrap; + + test('instanceof', function() { + var xhr = new XMLHttpRequest(); + assert.instanceOf(xhr, XMLHttpRequest); + }); + + test('send', function() { + var xhr = new XMLHttpRequest(); + xhr.open('POST', location.href); + xhr.send(new FormData()); + }); + +}); diff --git a/tests/ShadowDOM/js/build-json.js b/tests/ShadowDOM/js/build-json.js new file mode 100644 index 0000000..7632085 --- /dev/null +++ b/tests/ShadowDOM/js/build-json.js @@ -0,0 +1,43 @@ +/* + * Copyright 2014 The Polymer Authors. All rights reserved. + * Use of this source code is goverened by a BSD-style + * license that can be found in the LICENSE file. + */ + +suite('build.json', function() { + + teardown(function() { + delete document.write; + }); + + test('Ensure lists match', function(done) { + var xhrJson = new XMLHttpRequest; + // karma serves the test runner at /context.html, need to adjust xhr request url to match + var requestBase = window.__karma__ ? '/base/ShadowDOM/' : '../'; + xhrJson.open('GET', requestBase + 'build.json'); + xhrJson.onload = function() { + var buildJson = JSON.parse(xhrJson.responseText); + + var xhrJs = new XMLHttpRequest; + xhrJs.open('GET', requestBase + 'shadowdom.js'); + xhrJs.onload = function() { + var sources = []; + + document.write = function(s) { + var path = + s.slice((' + + +

    Reload this page and you should see a dialog saying "OK". diff --git a/tests/ShadowDOM/manual/setDragImage.html b/tests/ShadowDOM/manual/setDragImage.html new file mode 100644 index 0000000..85f21bd --- /dev/null +++ b/tests/ShadowDOM/manual/setDragImage.html @@ -0,0 +1,28 @@ + + + + +

    Drag the div below the image. When dragging the image should be used as the +drag image. + +

    + +

    Drag me + + diff --git a/tests/ShadowDOM/runner.html b/tests/ShadowDOM/runner.html new file mode 100644 index 0000000..c0c3012 --- /dev/null +++ b/tests/ShadowDOM/runner.html @@ -0,0 +1,25 @@ + + +ShadowDOM Tests + + + + + + + + + + + + + + +

    + diff --git a/tests/ShadowDOM/runner.min.html b/tests/ShadowDOM/runner.min.html new file mode 100644 index 0000000..c930518 --- /dev/null +++ b/tests/ShadowDOM/runner.min.html @@ -0,0 +1,18 @@ + + + + + + + + + +
    + + diff --git a/tests/ShadowDOM/tests.js b/tests/ShadowDOM/tests.js new file mode 100644 index 0000000..1fa13f4 --- /dev/null +++ b/tests/ShadowDOM/tests.js @@ -0,0 +1,142 @@ +/* + * Copyright 2013 The Polymer Authors. All rights reserved. + * Use of this source code is governed by a BSD-style + * license that can be found in the LICENSE file. + */ + +var assert = chai.assert; + +var thisFile = 'tests.js'; +var base; +(function() { + var s$ = document.querySelectorAll('script[src]'); + Array.prototype.forEach.call(s$, function(s) { + var src = s.getAttribute('src'); + var re = new RegExp(thisFile + '[^\\\\]*'); + var match = src.match(re); + if (match) { + base = src.slice(0, -match[0].length); + } + }); +})(); + +function expectStructure(nodeOrWrapper, nonNullFields) { + assert(nodeOrWrapper); + assert.strictEqual(nodeOrWrapper.parentNode, nonNullFields.parentNode || null); + assert.strictEqual(nodeOrWrapper.previousSibling, + nonNullFields.previousSibling || null); + assert.strictEqual(nodeOrWrapper.nextSibling, nonNullFields.nextSibling || null); + assert.strictEqual(nodeOrWrapper.firstChild, nonNullFields.firstChild || null); + assert.strictEqual(nodeOrWrapper.lastChild, nonNullFields.lastChild || null); +} + +function unwrapAndExpectStructure(node, nonNullFields) { + for (var p in nonNullFields) { + nonNullFields[p] = ShadowDOMPolyfill.unwrap(nonNullFields[p]); + } + expectStructure(ShadowDOMPolyfill.unwrap(node), nonNullFields); +} + +function assertArrayEqual(a, b, msg) { + for (var i = 0; i < a.length; i++) { + assert.equal(a[i], b[i], msg); + } + assert.equal(a.length, b.length, msg); +} + +function expectMutationRecord(record, expected) { + assert.equal(record.type, + expected.type === undefined ? null : expected.type); + assert.equal(record.target, + expected.target === undefined ? null : expected.target); + assertArrayEqual(record.addedNodes, + expected.addedNodes === undefined ? [] : expected.addedNodes); + assertArrayEqual(record.removedNodes, + expected.removedNodes === undefined ? [] : expected.removedNodes); + assert.equal(record.previousSibling, + expected.previousSibling === undefined ? + null : expected.previousSibling); + assert.equal(record.nextSibling, + expected.nextSibling === undefined ? null : expected.nextSibling); + assert.equal(record.attributeName, + expected.attributeName === undefined ? null : expected.attributeName); + assert.equal(record.attributeNamespace, + expected.attributeNamespace === undefined ? + null : expected.attributeNamespace); + assert.equal(record.oldValue, + expected.oldValue === undefined ? null : expected.oldValue); +} + +mocha.setup({ + ui: 'tdd', + globals: ['console', 'getInterface'] +}) + +var modules = [ + 'ChildNodeInterface.js', + 'Comment.js', + 'DOMTokenList.js', + 'Document.js', + 'Element.js', + 'FormData.js', + 'HTMLAudioElement.js', + 'HTMLBodyElement.js', + 'HTMLButtonElement.js', + 'HTMLCanvasElement.js', + 'HTMLContentElement.js', + 'HTMLElement.js', + 'HTMLFieldSetElement.js', + 'HTMLFormElement.js', + 'HTMLHeadElement.js', + 'HTMLHtmlElement.js', + 'HTMLImageElement.js', + 'HTMLInputElement.js', + 'HTMLKeygenElement.js', + 'HTMLLabelElement.js', + 'HTMLLegendElement.js', + 'HTMLObjectElement.js', + 'HTMLOptionElement.js', + 'HTMLOutputElement.js', + 'HTMLSelectElement.js', + 'HTMLShadowElement.js', + 'HTMLTableElement.js', + 'HTMLTableRowElement.js', + 'HTMLTableSectionElement.js', + 'HTMLTemplateElement.js', + 'HTMLTextAreaElement.js', + 'MutationObserver.js', + 'MutationObserver/attributes.js', + 'MutationObserver/callback.js', + 'MutationObserver/characterData.js', + 'MutationObserver/childList.js', + 'MutationObserver/mixed.js', + 'MutationObserver/options.js', + 'MutationObserver/shadow-root.js', + 'MutationObserver/transient.js', + 'Node.js', + 'ParentNodeInterface.js', + 'Range.js', + 'SVGElement.js', + 'SVGElementInstance.js', + 'Selection.js', + 'ShadowRoot.js', + 'Text.js', + 'TouchEvent.js', + 'TreeScope.js', + 'Window.js', + 'XMLHttpRequest.js', + //'build-json.js', + 'createTable.js', + 'custom-element.js', + 'events.js', + 'microtask.js', + 'paralleltrees.js', + 'reprojection.js', + 'rerender.js', + 'test.js', + 'wrappers.js', +]; + +modules.forEach(function(inSrc) { + document.write(''); +}); diff --git a/tests/WeakMap/runner.html b/tests/WeakMap/runner.html new file mode 100644 index 0000000..0f04d6f --- /dev/null +++ b/tests/WeakMap/runner.html @@ -0,0 +1,75 @@ + + +WeakMap tests + + + + + + + + + + + +
    + + + + + diff --git a/tests/WebComponents/html/ce-import.html b/tests/WebComponents/html/ce-import.html new file mode 100644 index 0000000..2a39092 --- /dev/null +++ b/tests/WebComponents/html/ce-import.html @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + diff --git a/tests/WebComponents/html/ce-upgrade-order.html b/tests/WebComponents/html/ce-upgrade-order.html new file mode 100644 index 0000000..e58fce0 --- /dev/null +++ b/tests/WebComponents/html/ce-upgrade-order.html @@ -0,0 +1,41 @@ + + + + + + Custom Element Upgrade Order + + + + + + + + + + + diff --git a/tests/WebComponents/html/ce-upgradedocumenttree.html b/tests/WebComponents/html/ce-upgradedocumenttree.html new file mode 100644 index 0000000..4701abc --- /dev/null +++ b/tests/WebComponents/html/ce-upgradedocumenttree.html @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + diff --git a/tests/WebComponents/html/dev-loader-swizzled.html b/tests/WebComponents/html/dev-loader-swizzled.html new file mode 100644 index 0000000..a65a15d --- /dev/null +++ b/tests/WebComponents/html/dev-loader-swizzled.html @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + diff --git a/tests/WebComponents/html/dev-loader.html b/tests/WebComponents/html/dev-loader.html new file mode 100644 index 0000000..1b548f2 --- /dev/null +++ b/tests/WebComponents/html/dev-loader.html @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + diff --git a/tests/WebComponents/html/element-import-a.html b/tests/WebComponents/html/element-import-a.html new file mode 100644 index 0000000..539fd2b --- /dev/null +++ b/tests/WebComponents/html/element-import-a.html @@ -0,0 +1,10 @@ + + + \ No newline at end of file diff --git a/tests/WebComponents/html/element-import-b.html b/tests/WebComponents/html/element-import-b.html new file mode 100644 index 0000000..151696a --- /dev/null +++ b/tests/WebComponents/html/element-import-b.html @@ -0,0 +1,10 @@ + + + \ No newline at end of file diff --git a/tests/WebComponents/html/element-import.html b/tests/WebComponents/html/element-import.html new file mode 100644 index 0000000..fd50ec1 --- /dev/null +++ b/tests/WebComponents/html/element-import.html @@ -0,0 +1,20 @@ + + + diff --git a/tests/WebComponents/html/html-import-sandbox-iframe.html b/tests/WebComponents/html/html-import-sandbox-iframe.html new file mode 100644 index 0000000..6b6ca29 --- /dev/null +++ b/tests/WebComponents/html/html-import-sandbox-iframe.html @@ -0,0 +1,26 @@ + + + + + + + + + + + + diff --git a/tests/WebComponents/html/html-import-sandbox.html b/tests/WebComponents/html/html-import-sandbox.html new file mode 100644 index 0000000..ef4a0a5 --- /dev/null +++ b/tests/WebComponents/html/html-import-sandbox.html @@ -0,0 +1,32 @@ + + + + + + + + + + + + diff --git a/tests/WebComponents/html/import-upgrade-order.html b/tests/WebComponents/html/import-upgrade-order.html new file mode 100644 index 0000000..9740ebc --- /dev/null +++ b/tests/WebComponents/html/import-upgrade-order.html @@ -0,0 +1,9 @@ + + diff --git a/tests/WebComponents/html/includes/import-file.html b/tests/WebComponents/html/includes/import-file.html new file mode 100644 index 0000000..82e57c7 --- /dev/null +++ b/tests/WebComponents/html/includes/import-file.html @@ -0,0 +1,12 @@ + + \ No newline at end of file diff --git a/tests/WebComponents/html/includes/strawkit.js b/tests/WebComponents/html/includes/strawkit.js new file mode 100644 index 0000000..c50494a --- /dev/null +++ b/tests/WebComponents/html/includes/strawkit.js @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2014 The Polymer Project Authors. All rights reserved. + * This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt + * The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt + * The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt + * Code distributed by Google as part of the polymer project is also + * subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt + */ +Polymer = { + register: function(inElement, inPrototype) { + if (inElement === window) { + return; + } + inPrototype.readyCallback = function() { + var template = inElement.querySelector('template'); + if (template) { + var root = this.createShadowRoot(); + root.appendChild(Platform.templateContent(template).cloneNode(true)); + } + inPrototype.created.call(this); + }; + inElement.register({ + prototype: inPrototype + }); + } +}; diff --git a/tests/WebComponents/html/jquery-shadowdom-polyfill.html b/tests/WebComponents/html/jquery-shadowdom-polyfill.html new file mode 100644 index 0000000..ed90b52 --- /dev/null +++ b/tests/WebComponents/html/jquery-shadowdom-polyfill.html @@ -0,0 +1,30 @@ + + + + + + ShadowDOM Polyfill + jQuery + + + + + + +
    asdf
    + + + diff --git a/tests/WebComponents/html/loader-forcepoly.html b/tests/WebComponents/html/loader-forcepoly.html new file mode 100644 index 0000000..13c6f6e --- /dev/null +++ b/tests/WebComponents/html/loader-forcepoly.html @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + diff --git a/tests/WebComponents/html/smoke.html b/tests/WebComponents/html/smoke.html new file mode 100644 index 0000000..9969d2e --- /dev/null +++ b/tests/WebComponents/html/smoke.html @@ -0,0 +1,66 @@ + + + + + + + + + + + + + plain + + + + diff --git a/tests/WebComponents/html/strawkit.html b/tests/WebComponents/html/strawkit.html new file mode 100644 index 0000000..890d584 --- /dev/null +++ b/tests/WebComponents/html/strawkit.html @@ -0,0 +1,51 @@ + + + + + Strawkit Test + + + + + + + + + + + + + + + diff --git a/tests/WebComponents/html/web-components.html b/tests/WebComponents/html/web-components.html new file mode 100644 index 0000000..0110c06 --- /dev/null +++ b/tests/WebComponents/html/web-components.html @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + diff --git a/tests/WebComponents/runner.html b/tests/WebComponents/runner.html new file mode 100644 index 0000000..8862386 --- /dev/null +++ b/tests/WebComponents/runner.html @@ -0,0 +1,30 @@ + + +WebComponents Tests + + + + + + + + + + + +
    + + + + + diff --git a/tests/WebComponents/tests.js b/tests/WebComponents/tests.js new file mode 100644 index 0000000..bd9ecec --- /dev/null +++ b/tests/WebComponents/tests.js @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2014 The Polymer Project Authors. All rights reserved. + * This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt + * The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt + * The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt + * Code distributed by Google as part of the polymer project is also + * subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt + */ + +htmlSuite('loader and build', function() { + htmlTest('html/dev-loader.html'); + htmlTest('html/dev-loader-swizzled.html'); + htmlTest('html/loader-forcepoly.html'); +}); + +htmlSuite('integration', function() { + htmlTest('html/web-components.html'); + htmlTest('html/smoke.html'); + htmlTest('html/smoke.html?shadow'); + htmlTest('html/ce-import.html'); + htmlTest('html/ce-upgradedocumenttree.html'); + htmlTest('html/ce-import.html?shadow'); + htmlTest('html/ce-upgrade-order.html'); +}); + +htmlSuite('Library Cooperation', function() { + htmlTest('html/jquery-shadowdom-polyfill.html'); +}); diff --git a/tests/runner.html b/tests/runner.html new file mode 100644 index 0000000..566c95a --- /dev/null +++ b/tests/runner.html @@ -0,0 +1,34 @@ + + +All WebComponents Tests + + + + + + + + + + + +
    + + + + + + + diff --git a/tests/tools/chai/.bower.json b/tests/tools/chai/.bower.json new file mode 100644 index 0000000..413db6e --- /dev/null +++ b/tests/tools/chai/.bower.json @@ -0,0 +1,37 @@ +{ + "name": "chai", + "version": "1.9.1", + "description": "BDD/TDD assertion library for node.js and the browser. Test framework agnostic.", + "license": "MIT", + "keywords": [ + "test", + "assertion", + "assert", + "testing", + "chai" + ], + "main": "chai.js", + "ignore": [ + "build", + "components", + "lib", + "node_modules", + "support", + "test", + "index.js", + "Makefile", + ".*" + ], + "dependencies": {}, + "devDependencies": {}, + "homepage": "https://github.com/chaijs/chai", + "_release": "1.9.1", + "_resolution": { + "type": "version", + "tag": "1.9.1", + "commit": "4180251dd45560f189192e28d3c0ba011f6d8178" + }, + "_source": "git://github.com/chaijs/chai.git", + "_target": ">=1.8.1", + "_originalSource": "chai" +} \ No newline at end of file diff --git a/tests/tools/chai/CONTRIBUTING.md b/tests/tools/chai/CONTRIBUTING.md new file mode 100644 index 0000000..cf6c5dc --- /dev/null +++ b/tests/tools/chai/CONTRIBUTING.md @@ -0,0 +1,173 @@ +# Chai Contribution Guidelines + +We like to encourage you to contribute to the Chai.js repository. This should be as easy as possible for you but there are a few things to consider when contributing. The following guidelines for contribution should be followed if you want to submit a pull request or open an issue. + +Following these guidelines helps to communicate that you respect the time of the developers managing and developing this open source project. In return, they should reciprocate that respect in addressing your issue or assessing patches and features. + +#### Table of Contents + +- [TLDR;](#tldr) +- [Contributing](#contributing) + - [Bug Reports](#bugs) + - [Feature Requests](#features) + - [Pull Requests](#pull-requests) +- [Support](#support) + - [Resources](#resources) + - [Core Contributors](#contributors) + + +## TLDR; + +- Creating an Issue or Pull Request requires a [GitHub](http://github.com) account. +- Issue reports should be **clear**, **concise** and **reproducible**. Check to see if your issue has already been resolved in the [master]() branch or already reported in Chai's [GitHub Issue Tracker](https://github.com/chaijs/chai/issues). +- Pull Requests must adhere to strict [coding style guidelines](https://github.com/chaijs/chai/wiki/Chai-Coding-Style-Guide). +- In general, avoid submitting PRs for new Assertions without asking core contributors first. More than likely it would be better implemented as a plugin. +- Additional support is available via the [Google Group](http://groups.google.com/group/chaijs) or on irc.freenode.net#chaijs. +- **IMPORTANT**: By submitting a patch, you agree to allow the project owner to license your work under the same license as that used by the project. + + + + +## Contributing + +The issue tracker is the preferred channel for [bug reports](#bugs), +[feature requests](#features) and [submitting pull +requests](#pull-requests), but please respect the following restrictions: + +* Please **do not** use the issue tracker for personal support requests (use + [Google Group](https://groups.google.com/forum/#!forum/chaijs) or IRC). +* Please **do not** derail or troll issues. Keep the discussion on topic and + respect the opinions of others + + +### Bug Reports + +A bug is a **demonstrable problem** that is caused by the code in the repository. + +Guidelines for bug reports: + +1. **Use the GitHub issue search** — check if the issue has already been reported. +2. **Check if the issue has been fixed** — try to reproduce it using the latest `master` or development branch in the repository. +3. **Isolate the problem** — create a test case to demonstrate your issue. Provide either a repo, gist, or code sample to demonstrate you problem. + +A good bug report shouldn't leave others needing to chase you up for more information. Please try to be as detailed as possible in your report. What is your environment? What steps will reproduce the issue? What browser(s) and/or Node.js versions experience the problem? What would you expect to be the outcome? All these details will help people to fix any potential bugs. + +Example: + +> Short and descriptive example bug report title +> +> A summary of the issue and the browser/OS environment in which it occurs. If suitable, include the steps required to reproduce the bug. +> +> 1. This is the first step +> 2. This is the second step +> 3. Further steps, etc. +> +> `` - a link to the reduced test case OR +> ```js +> expect(a).to.equal('a'); +> // code sample +> ``` +> +> Any other information you want to share that is relevant to the issue being reported. This might include the lines of code that you have identified as causing the bug, and potential solutions (and your opinions on their merits). + + +### Feature Requests + +Feature requests are welcome. But take a moment to find out whether your idea fits with the scope and aims of the project. It's up to *you* to make a strong case to convince the project's developers of the merits of this feature. Please provide as much detail and context as possible. + +Furthermore, since Chai.js has a [robust plugin API](http://chaijs.com/guide/plugins/), we encourage you to publish **new Assertions** as plugins. If your feature is an enhancement to an **existing Assertion**, please propose your changes as an issue prior to opening a pull request. If the core Chai.js contributors feel your plugin would be better suited as a core assertion, they will invite you to open a PR in [chaijs/chai](https://github.com/chaijs/chai). + + +### Pull Requests + +- PRs for new core-assertions are advised against. +- PRs for core-assertion bug fixes are always welcome. +- PRs for enhancing the interfaces are always welcome. +- PRs that increase test coverage are always welcome. +- PRs are scrutinized for coding-style. + +Good pull requests - patches, improvements, new features - are a fantastic help. They should remain focused in scope and avoid containing unrelated commits. + +**Please ask first** before embarking on any significant pull request (e.g. implementing features, refactoring code), otherwise you risk spending a lot of time working on something that the project's developers might not want to merge into the project. + +Please adhere to the coding conventions used throughout a project (indentation, accurate comments, etc.) and any other requirements (such as test coverage). Please review the [Chai.js Coding Style Guide](https://github.com/chaijs/chai/wiki/Chai-Coding-Style-Guide). + +Follow this process if you'd like your work considered for inclusion in the project: + +1. [Fork](http://help.github.com/fork-a-repo/) the project, clone your fork, and configure the remotes: + +```bash +# Clone your fork of the repo into the current directory +git clone https://github.com// +# Navigate to the newly cloned directory +cd +# Assign the original repo to a remote called "upstream" +git remote add upstream https://github.com// +``` + +2. If you cloned a while ago, get the latest changes from upstream: + +```bash +git checkout +git pull upstream +``` + +3. Create a new topic branch (off the main project development branch) to contain your feature, change, or fix: + +```bash +git checkout -b +``` + +4. Commit your changes in logical chunks. Use Git's [interactive rebase](https://help.github.com/articles/interactive-rebase) feature to tidy up your commits before making them public. + +5. Locally merge (or rebase) the upstream development branch into your topic branch: + +```bash +git pull [--rebase] upstream +``` + +6. Push your topic branch up to your fork: + +```bash +git push origin +``` + +7. [Open a Pull Request](https://help.github.com/articles/using-pull-requests/) with a clear title and description. + +**IMPORTANT**: By submitting a patch, you agree to allow the project owner to license your work under the same license as that used by the project. + + +## Support + + +### Resources + +For most of the documentation you are going to want to visit [ChaiJS.com](http://chaijs.com). + +- [Getting Started Guide](http://chaijs.com/guide/) +- [API Reference](http://chaijs.com/api/) +- [Plugins](http://chaijs.com/plugins/) + +Alternatively, the [wiki](https://github.com/chaijs/chai/wiki) might be what you are looking for. + +- [Chai Coding Style Guide](https://github.com/chaijs/chai/wiki/Chai-Coding-Style-Guide) +- [Third-party Resources](https://github.com/chaijs/chai/wiki/Third-Party-Resources) + +Or finally, you may find a core-contributor or like-minded developer in any of our support channels. + +- IRC: irc.freenode.org #chaijs +- [Mailing List / Google Group](https://groups.google.com/forum/#!forum/chaijs) + + +### Core Contributors + +Feel free to reach out to any of the core-contributors with you questions or concerns. We will do our best to respond in a timely manner. + +- Jake Luer + - GH: [@logicalparadox](https://github.com/logicalparadox) + - TW: [@jakeluer](http://twitter.com/jakeluer) + - IRC: logicalparadox +- Veselin Todorov + - GH: [@vesln](https://github.com/vesln/) + - TW: [@vesln](http://twitter.com/vesln) + - IRC: vesln diff --git a/tests/tools/chai/History.md b/tests/tools/chai/History.md new file mode 100644 index 0000000..32b6ef9 --- /dev/null +++ b/tests/tools/chai/History.md @@ -0,0 +1,895 @@ + +1.9.1 / 2014-03-19 +================== + + * deps update + * util: [getActual] select actual logic now allows undefined for actual. Closes #183 + * docs: [config] make public, express param type + * Merge pull request #251 from romario333/threshold3 + * Fix issue #166 - configurable threshold in objDisplay. + * Move configuration options to config.js. + * Merge pull request #233 from Empeeric/master + * Merge pull request #244 from leider/fix_for_contains + * Merge pull request #247 from didoarellano/typo-fixes + * Fix typos + * Merge pull request #245 from lfac-pt/patch-1 + * Update `exports.version` to 1.9.0 + * aborting loop on finding + * declaring variable only once + * additional test finds incomplete implementation + * simplified code + * fixing #239 (without changing chai.js) + * ssfi as it should be + * Merge pull request #228 from duncanbeevers/deep_members + * Deep equality check for collection membership + +1.9.0 / 2014-01-29 +================== + + * docs: add contributing.md #238 + * assert: .throws() returns thrown error. Closes #185 + * Merge pull request #232 from laconbass/assert-throws + * assert: .fail() parameter mismatch. Closes #206 + * Merge branch 'karma-fixes' + * Add karma phantomjs launcher + * Use latest karma and sauce launcher + * Karma tweaks + * Merge pull request #230 from jkroso/include + * Merge pull request #237 from chaijs/coverage + * Add coverage to npmignore + * Remove lib-cov from test-travisci dependents + * Remove the not longer needed lcov reporter + * Test coverage with istanbul + * Remove jscoverage + * Remove coveralls + * Merge pull request #226 from duncanbeevers/add_has + * Avoid error instantiation if possible on assert.throws + * Merge pull request #231 from duncanbeevers/update_copyright_year + * Update Copyright notices to 2014 + * handle negation correctly + * add failing test case + * support `{a:1,b:2}.should.include({a:1})` + * Merge pull request #224 from vbardales/master + * Add `has` to language chains + * Merge pull request #219 from demands/overwrite_chainable + * return error on throw method to chain on error properties, possibly different from message + * util: store chainable behavior in a __methods object on ctx + * util: code style fix + * util: add overwriteChainableMethod utility (for #215) + * Merge pull request #217 from demands/test_cleanup + * test: make it possible to run utilities tests with --watch + * makefile: change location of karma-runner bin script + * Merge pull request #202 from andreineculau/patch-2 + * test: add tests for throwing custom errors + * Merge pull request #201 from andreineculau/patch-1 + * test: updated for the new assertion errors + * core: improve message for assertion errors (throw assertion) + +1.8.1 / 2013-10-10 +================== + + * pkg: update deep-eql version + +1.8.0 / 2013-09-18 +================== + + * test: [sauce] add a few more browsers + * Merge branch 'refactor/deep-equal' + * util: remove embedded deep equal utility + * util: replace embedded deep equal with external module + * Merge branch 'feature/karma' + * docs: add sauce badge to readme [ci skip] + * test: [sauce] use karma@canary to prevent timeouts + * travis: only run on node 0.10 + * test: [karma] use karma phantomjs runner + * Merge pull request #181 from tricknotes/fix-highlight + * Fix highlight for example code + +1.7.2 / 2013-06-27 +================== + + * coverage: add coveralls badge + * test: [coveralls] add coveralls api integration. testing travis-ci integration + * Merge branch 'master' of github.com:chaijs/chai + * Merge branch 'feature/bower' + * Merge pull request #180 from tricknotes/modify-method-title + * Merge pull request #179 from tricknotes/highlight-code-example + * Modify method title to include argument name + * Fix to highlight code example + * bower: granular ignores + +1.7.1 / 2013-06-24 +================== + + * Merge branch 'feature/bower'. #175 + * bower: add json file + * build: browser + +1.7.0 / 2013-06-17 +================== + + * error: remove internal assertion error constructor + * core: [assertion-error] replace internal assertion error with dep + * deps: add chaijs/assertion-error@1.0.0 + * docs: fix typo in source file. #174 + * Merge pull request #174 from piecioshka/master + * typo + * Merge branch 'master' of github.com:chaijs/chai + * pkg: lock mocha/mocha-phantomjs versions (for now) + * Merge pull request #173 from chaijs/inspect-fix + * Fix `utils.inspect` with custom object-returning inspect()s. + * Merge pull request #171 from Bartvds/master + * replaced tabs with 2 spaces + * added assert.notOk() + * Merge pull request #169 from katsgeorgeek/topics/master + * Fix comparison objects. + +1.6.1 / 2013-06-05 +================== + + * Merge pull request #168 from katsgeorgeek/topics/master + * Add test for different RegExp flags. + * Add test for regexp comparison. + * Downgrade mocha version for fix running Phantom tests. + * Fix comparison equality of two regexps. + * Merge pull request #161 from brandonpayton/master + * Fix documented name for assert interfaces isDefined method + +1.6.0 / 2013-04-29 +================== + + * build: browser + * assert: [(not)include] throw on incompatible haystack. Closes #142 + * assert: [notInclude] add assert.notInclude. Closes #158 + * browser build + * makefile: force browser build on browser-test + * makefile: use component for browser build + * core: [assertions] remove extraneous comments + * Merge branch 'master' of github.com:chaijs/chai + * test: [assert] deep equal ordering + * Merge pull request #153 from NickHeiner/array-assertions + * giving members a no-flag assertion + * Code review comments - changing syntax + * Code review comments + * Adding members and memberEquals assertions for checking for subsets and set equality. Implements chaijs/chai#148. + * Merge pull request #140 from RubenVerborgh/function-prototype + * Restore the `call` and `apply` methods of Function when adding a chainable method. + * readme: 2013 + * notes: migration notes for deep equal changes + * test: for ever err() there must be a passing version + +1.5.0 / 2013-02-03 +================== + + * docs: add Release Notes for non-gitlog summary of changes. + * lib: update copyright to 2013 + * Merge branch 'refactor/travis' + * makefile: remove test-component for full test run + * pkg: script test now runs make test so travis will test browser + * browser: build + * tests: refactor some tests to support new objDisplay output + * test: [bootstrap] normalize boostrap across all test scenarios + * assertions: refactor some assertions to use objDisplay instead of inspect + * util: [objDisplay] normalize output of functions + * makefile: refactor for full build scenarios + * component: fix build bug where missing util:type file + * assertions: [throw] code cleanup + * Merge branch 'refactor/typeDetection' + * browser: build + * makefile: chai.js is .PHONY so it builds every time + * test: [expect] add arguments type detection test + * core/assertions: [type] (a/an) refactor to use type detection utility + * util: add cross-browser type detection utility + * Merge branch 'feature/component' + * browser: build + * component: add component.json file + * makefile: refactor for fine grain control of testing scenarios + * test: add mochaPhantomJS support and component test file + * deps: add component and mocha-phantomjs for browser testing + * ignore: update ignore files for component support + * travis: run for all branches + * Merge branch 'feature/showDiff' + * test: [Assertion] configruable showDiff flag. Closes #132 + * lib: [Assertion] add configurable showDiff flag. #132 + * Merge branch 'feature/saucelabs' + * Merge branch 'master' into feature/saucelabs + * browser: build + * support: add mocha cloud runner, client, and html test page + * test: [saucelabs] add auth placeholder + * deps: add mocha-cloud + * Merge pull request #136 from whatthejeff/message_fix + * Merge pull request #138 from timnew/master + * Fix issue #137, test message existence by using message!=null rather than using message + * Fixed backwards negation messages. + * Merge pull request #133 from RubenVerborgh/throw + * Functions throwing strings can reliably be tested. + * Merge pull request #131 from RubenVerborgh/proto + * Cache whether __proto__ is supported. + * Use __proto__ if available. + * Determine the property names to exclude beforehand. + * Merge pull request #126 from RubenVerborgh/eqls + * Add alias eqls for eql. + * Use inherited enumerable properties in deep equality comparison. + * Show inherited properties when inspecting an object. + * Add new getProperties and getEnumerableProperties utils. + * showDiff: force true for equal and eql + +1.4.2 / 2012-12-21 +================== + + * browser build: (object diff support when used with mocha) #106 + * test: [display] array test for mocha object diff + * browser: no longer need different AssertionError constructor + +1.4.1 / 2012-12-21 +================== + + * showDiff: force diff for equal and eql. #106 + * test: [expect] type null. #122 + * Merge pull request #115 from eshao/fix-assert-Throw + * FIX: assert.Throw checks error type/message + * TST: assert.Throw should check error type/message + +1.4.0 / 2012-11-29 +================== + + * pre-release browser build + * clean up index.js to not check for cov, revert package.json to use index.js + * convert tests to use new bootstrap + * refactor testing bootstrap + * use spaces (not tabs). Clean up #114 + * Merge pull request #114 from trantorLiu/master + * Add most() (alias: lte) and least() (alias: gte) to the API with new chainers "at" and "of". + * Change `main` to ./lib/chai. Fixes #28. + * Merge pull request #104 from connec/deep_equals_circular_references_ + * Merge pull request #109 from nnarhinen/patch-1 + * Check for 'actual' type + * Added support for circular references when checking deep (in)equality. + +1.3.0 / 2012-10-01 +================== + + * browser build w/ folio >= 0.3.4. Closes #99 + * add back buffer test for deep equal + * do not write flags to assertion.prototype + * remove buffer test from expect + * browser build + * improve documentation of custom error messages + * Merge branch 'master' of git://github.com/Liffft/chai into Liffft-master + * browser build + * improved buffer deep equal checking + * mocha is npm test command + * Cleaning up the js style… + * expect tests now include message pass-through + * packaging up browser-side changes… + * Increasing Throws error message verbosity + * Should syntax: piping message through + * Make globalShould test work in browser too. + * Add a setter for `Object.prototype.should`. Closes #86. + +1.2.0 / 2012-08-07 +================== + + * Merge branch 'feature/errmsg' + * browser build + * comment updates for utilities + * tweak objDislay to only kick in if object inspection is too long + * Merge branch 'master' into feature/errmsg + * add display sample for error message refactor + * first draft of error message refactor. #93 + * add `closeTo` assertion to `assert` interface. Closes #89. + * update folio build for better require.js handling. Closes #85 + * Merge pull request #92 from paulmillr/topics/add-dom-checks + * Add check for DOM objects. + * browser build + * Merge branch 'master' of github.com:chaijs/chai + * bug - getActual not defaulting to assertion subject + * Merge pull request #88 from pwnall/master + * Don't inspect() assertion arguments if the assertion passes. + +1.1.1 / 2012-07-09 +================== + + * improve commonjs support on browser build + * Merge pull request #83 from tkazec/equals + * Document .equals + * Add .equals as an alias of .equal + * remove unused browser prefix/suffix + * Merge branch 'feature/folio-build' + * browser build + * using folio to compile + * clean up makefile + * early folio 0.3.x support + +1.1.0 / 2012-06-26 +================== + + * browser build + * Disable "Assertion.includeStack is false" test in IE. + * Use `utils.getName` for all function inspections. + * Merge pull request #80 from kilianc/closeTo + * fixes #79 + * browser build + * expand docs to indicate change of subject for chaining. Closes #78 + * add `that` chain noop + * Merge branch 'bug/74' + * comments on how to property use `length` as chain. Closes #74 + * tests for length as chainable property. #74 + * add support for `length` as chainable prop/method. + * Merge branch 'bug/77' + * tests for getPathValue when working with nested arrays. Closes #77 + * add getPathValue support for nested arrays + * browser build + * fix bug for missing browser utils + * compile tool aware of new folder layout + * Merge branch 'refactor/1dot1' + * move core assertions to own file and refactor all using utils + * rearrange folder structure + +1.0.4 / 2012-06-03 +================== + + * Merge pull request #68 from fizker/itself + * Added itself chain. + * simplify error inspections for cross browser compatibility + * fix safari `addChainableMethod` errors. Closes #69 + +1.0.3 / 2012-05-27 +================== + + * Point Travis badge to the right place. + * Make error message for eql/deep.equal more clear. + * Fix .not.deep.equal. + * contributors list + +1.0.2 / 2012-05-26 +================== + + * Merge pull request #67 from chaijs/chaining-and-flags + * Browser build. + * Use `addChainableMethod` to get away from `__proto__` manipulation. + * New `addChainableMethod` utility. + * Replace `getAllFlags` with `transferFlags` utility. + * browser build + * test - get all flags + * utility - get all flags + * Add .mailmap to .npmignore. + * Add a .mailmap file to fix my name in shortlogs. + +1.0.1 / 2012-05-18 +================== + + * browser build + * Fixing "an" vs. "a" grammar in type assertions. + * Uniformize `assert` interface inline docs. + * Don't use `instanceof` for `assert.isArray`. + * Add `deep` flag for equality and property value. + * Merge pull request #64 from chaijs/assertion-docs + * Uniformize assertion inline docs. + * Add npm-debug.log to .gitignore. + * no reserved words as actuals. #62 + +1.0.0 / 2012-05-15 +================== + + * readme cleanup + * browser build + * utility comments + * removed docs + * update to package.json + * docs build + * comments / docs updates + * plugins app cleanup + * Merge pull request #61 from joliss/doc + * Fix and improve documentation of assert.equal and friends + * browser build + * doc checkpoint - texture + * Update chai-jquery link + * Use defined return value of Assertion extension functions + * Update utility docs + +1.0.0-rc3 / 2012-05-09 +================== + + * Merge branch 'feature/rc3' + * docs update + * browser build + * assert test conformity for minor refactor api + * assert minor refactor + * update util tests for new add/overwrite prop/method format + * added chai.Assertion.add/overwrite prop/method for plugin toolbox + * add/overwrite prop/method don't make assumptions about context + * doc test suite + * docs don't need coverage + * refactor all simple chains into one forEach loop, for clean documentation + * updated npm ignore + * remove old docs + * docs checkpoint - guide styled + * Merge pull request #59 from joliss/doc + * Document how to run the test suite + * don't need to rebuild docs to view + * dep update + * docs checkpoint - api section + * comment updates for docs + * new doc site checkpoint - plugin directory! + * Merge pull request #57 from kossnocorp/patch-1 + * Fix typo: devDependancies → devDependencies + * Using message flag in `getMessage` util instead of old `msg` property. + * Adding self to package.json contributors. + * `getMessage` shouldn't choke on null/omitted messages. + * `return this` not necessary in example. + * `return this` not necessary in example. + * Sinon–Chai has a dash + * updated plugins list for docs + +1.0.0-rc2 / 2012-05-06 +================== + + * Merge branch 'feature/test-cov' + * browser build + * missing assert tests for ownProperty + * appropriate assert equivalent for expect.to.have.property(key, val) + * reset AssertionError to include full stack + * test for plugin utilities + * overwrite Property and Method now ensure chain + * version notes in readme + +1.0.0-rc1 / 2012-05-04 +================== + + * browser build (rc1) + * assert match/notMatch tests + * assert interface - notMatch, ownProperty, notOwnProperty, ownPropertyVal, ownPropertyNotVal + * cleaner should interface export. + * added chai.Assertion.prototype._obj (getter) for quick access to object flag + * moved almostEqual / almostDeepEqual to stats plugin + * added mocha.opts + * Add test for `utils.addMethod` + * Fix a typo + * Add test for `utils.overwriteMethod` + * Fix a typo + * Browser build + * Add undefined assertion + * Add null assertion + * Fix an issue with `mocha --watch` + * travis no longer tests on node 0.4.x + * removing unnecissary carbon dep + * Merge branch 'feature/plugins-app' + * docs build + * templates for docs express app for plugin directory + * express app for plugin and static serving + * added web server deps + * Merge pull request #54 from josher19/master + * Remove old test.assert code + * Use util.inspect instead of inspect for deepAlmostEqual and almostEqual + * browser build + * Added almostEqual and deepAlmostEqual to assert test suite. + * bug - context determinants for utils + * dec=0 means rounding, so assert.deepAlmostEqual({pi: 3.1416}, {pi: 3}, 0) is true + * wrong travis link + * readme updates for version information + * travis tests 0.5.x branch as well + * [bug] util `addProperty` not correctly exporting + * read me version notes + * browser build 1.0.0alpha1 + * not using reserved words in internal assertions. #52 + * version tick + * clean up redundant tests + * Merge branch 'refs/heads/0.6.x' + * update version tag in package 1.0.0alpha1 + * browser build + * added utility tests to browser specs + * beginning utility testing + * updated utility comments + * utility - overwriteMethod + * utility - overwriteProperty + * utility - addMethod + * utility - addProperty + * missing ; + * contributors list update + * Merge branch 'refs/heads/0.6.x-docs' into 0.6.x + * Added guide link to docs. WIP + * Include/contain are now both properties and methods + * Add an alias annotation + * Remove usless function wrapper + * Fix a typo + * A/an are now both properties and methods + * [docs] new site homepage layout / color checkpoint + * Ignore IE-specific error properties. + * Fixing order of error message test. + * New cross-browser `getName` util. + * Fixing up `AssertionError` inheritance. + * backup docs + * Add doctypes + * [bug] was still using `constructor.name` in `throw` assertion + * [bug] flag Object.create(null) instead of new Object + * [test] browser build + * [refactor] all usage of Assertion.prototype.assert now uses template tags and flags + * [refactor] remove Assertion.prototype.inspect for testable object inspection + * [refactor] object to test is now stored in flag, with ssfi and custom message + * [bug] flag util - don't return on `set` + * [docs] comments for getMessage utility + * [feature] getMessage + * [feature] testing utilities + * [refactor] flag doesn't require `call` + * Make order of source files well-defined + * Added support for throw(errorInstance). + * Use a foolproof method of grabbing an error's name. + * Removed constructor.name check from throw. + * disabled stackTrack configuration tests until api is stable again + * first version of line displayed error for node js (unstable) + * refactor core Assertion to use flag utility for negation + * added flag utility + * tests for assert interface negatives. Closed #42 + * added assertion negatives that were missing. #42 + * Support for expected and actual parameters in assert-style error object + * chai as promised - readme + * Added assert.fail. Closes #40 + * better error message for assert.operator. Closes #39 + * [refactor] Assertion#property to use getPathValue property + * added getPathValue utility helper + * removed todo about browser build + * version notes + * version bumb 0.6.0 + * browser build + * [refactor] browser compile function to replace with `require('./error')' with 'require('./browser/error')' + * [feature] browser uses different error.js + * [refactor] error without chai.fail + * Assertion & interfaces use new utils helper export + * [refactor] primary export for new plugin util usage + * added util index.js helper + * added 2012 to copyright headers + * Added DeepEqual assertions + +0.5.3 / 2012-04-21 +================== + + * Merge branch 'refs/heads/jgonera-oldbrowsers' + * browser build + * fixed reserved names for old browsers in interface/assert + * fixed reserved names for old browsers in interface/should + * fixed: chai.js no longer contains fail() + * fixed reserved names for old browsers in Assertion + * Merge pull request #49 from joliss/build-order + * Make order of source files well-defined + * Merge pull request #43 from zzen/patch-1 + * Support for expected and actual parameters in assert-style error object + * chai as promised - readme + +0.5.2 / 2012-03-21 +================== + + * browser build + * Merge branch 'feature/assert-fail' + * Added assert.fail. Closes #40 + * Merge branch 'bug/operator-msg' + * better error message for assert.operator. Closes #39 + * version notes + +0.5.1 / 2012-03-14 +================== + + * chai.fail no longer exists + * Merge branch 'feature/assertdefined' + * Added asset#isDefined. Closes #37. + * dev docs update for Assertion#assert + +0.5.0 / 2012-03-07 +================== + + * [bug] on inspect of reg on n 0.4.12 + * Merge branch 'bug/33-throws' + * Merge pull request #35 from logicalparadox/empty-object + * browser build + * updated #throw docs + * Assertion#throw `should` tests updated + * Assertion#throw `expect` tests + * Should interface supports multiple throw parameters + * Update Assertion#throw to support strings and type checks. + * Add more tests for `empty` in `should`. + * Add more tests for `empty` in `expect`. + * Merge branch 'master' into empty-object + * don't switch act/exp + * Merge pull request #34 from logicalparadox/assert-operator + * Update the compiled verison. + * Add `assert.operator`. + * Notes on messages. #22 + * browser build + * have been test + * below tests + * Merge branch 'feature/actexp' + * browser build + * remove unnecessary fail export + * full support for actual/expected where relevant + * Assertion.assert support expected value + * clean up error + * Update the compiled version. + * Add object & sane arguments support to `Assertion#empty`. + +0.4.2 / 2012-02-28 +================== + + * fix for `process` not available in browser when used via browserify. Closes #28 + * Merge pull request #31 from joliss/doc + * Document that "should" works in browsers other than IE + * Merge pull request #30 from logicalparadox/assert-tests + * Update the browser version of chai. + * Update `assert.doesNotThrow` test in order to check the use case when type is a string. + * Add test for `assert.ifError`. + * Falsey -> falsy. + * Full coverage for `assert.throws` and `assert.doesNotThrow`. + * Add test for `assert.doesNotThrow`. + * Add test for `assert.throws`. + * Add test for `assert.length`. + * Add test for `assert.include`. + * Add test for `assert.isBoolean`. + * Fix the implementation of `assert.isNumber`. + * Add test for `assert.isNumber`. + * Add test for `assert.isString`. + * Add test for `assert.isArray`. + * Add test for `assert.isUndefined`. + * Add test for `assert.isNotNull`. + * Fix `assert.isNotNull` implementation. + * Fix `assert.isNull` implementation. + * Add test for `assert.isNull`. + * Add test for `assert.notDeepEqual`. + * Add test for `assert.deepEqual`. + * Add test for `assert.notStrictEqual`. + * Add test for `assert.strictEqual`. + * Add test for `assert.notEqual`. + +0.4.1 / 2012-02-26 +================== + + * Merge pull request #27 from logicalparadox/type-fix + * Update the browser version. + * Add should tests for type checks. + * Add function type check test. + * Add more type checks tests. + * Add test for `new Number` type check. + * Fix type of actual checks. + +0.4.0 / 2012-02-25 +================== + + * docs and readme for upcoming 0.4.0 + * docs generated + * putting coverage and tests for docs in docs/out/support + * make docs + * makefile copy necessary resources for tests in docs + * rename configuration test + * Merge pull request #21 from logicalparadox/close-to + * Update the browser version. + * Update `closeTo()` docs. + * Add `Assertion.closeTo()` method. + * Add `.closeTo()` should test. + * Add `.closeTo()` expect test. + * Merge pull request #20 from logicalparadox/satisfy + * Update the browser version. + * `..` -> `()` in `.satisfy()` should test. + * Update example for `.satisfy()`. + * Update the compiled browser version. + * Add `Assertion.satisfy()` method. + * Add `.satisfy()` should test. + * Add `.satisfy()` expect test. + * Merge pull request #19 from logicalparadox/respond-to + * Update the compiled browser version. + * Add `respondTo` Assertion. + * Add `respondTo` should test. + * Add `respondTo` expect test. + * Merge branch 'feature/coverage' + * mocha coverage support + * doc contributors + * README contributors + +0.3.4 / 2012-02-23 +================== + + * inline comment typos for #15 + * Merge branch 'refs/heads/jeffbski-configErrorStackCompat' + * includeStack documentation for all interfaces + * suite name more generic + * Update test to be compatible with browsers that do not support err.stack + * udpated compiled chai.js and added to browser tests + * Allow inclusion of stack trace for Assert error messages to be configurable + * docs sharing buttons + * sinon-chai link + * doc updates + * read me updates include plugins + +0.3.3 / 2012-02-12 +================== + + * Merge pull request #14 from jfirebaugh/configurable_properties + * Make Assertion.prototype properties configurable + +0.3.2 / 2012-02-10 +================== + + * codex version + * docs + * docs cleanup + +0.3.1 / 2012-02-07 +================== + + * node 0.4.x compat + +0.3.0 / 2012-02-07 +================== + + * Merge branch 'feature/03x' + * browser build + * remove html/json/headers testign + * regex error.message testing + * tests for using plugins + * Merge pull request #11 from domenic/master + * Make `chai.use` a no-op if the function has already been used. + +0.2.4 / 2012-02-02 +================== + + * added in past tense switch for `been` + +0.2.3 / 2012-02-01 +================== + + * try that again + +0.2.2 / 2012-02-01 +================== + + * added `been` (past of `be`) alias + +0.2.1 / 2012-01-29 +================== + + * added Throw, with a capital T, as an alias to `throw` (#7) + +0.2.0 / 2012-01-26 +================== + + * update gitignore for vim *.swp + * Merge branch 'feature/plugins' + * browser build + * interfaces now work with use + * simple .use function. See #9. + * readme notice on browser compat + +0.1.7 / 2012-01-25 +================== + + * added assert tests to browser test runner + * browser update + * `should` interface patch for primitives support in FF + * fix isObject() Thanks @milewise + * travis only on branch `master` + * add instanceof alias `instanceOf`. #6 + * some tests for assert module + +0.1.6 / 2012-01-02 +================== + + * commenting for assert interface + * updated codex dep + +0.1.5 / 2012-01-02 +================== + + * browser tests pass + * type in should.not.equal + * test for should (not) exist + * added should.exist and should.not.exist + * browser uses tdd + * convert tests to tdd + +0.1.4 / 2011-12-26 +================== + + * browser lib update for new assert interface compatiblitiy + * inspect typos + * added strict equal + negatives and ifError + * interface assert had doesNotThrow + * added should tests to browser + * new expect empty tests + * should test browser compat + * Fix typo for instanceof docs. Closes #3 [ci skip] + +0.1.3 / 2011-12-18 +================== + + * much cleaner reporting string on error. + +0.1.2 / 2011-12-18 +================== + + * [docs] for upcoming 0.1.2 + * browser version built with pre/suffix … all tests passing + * make / compile now use prefix/suffix correctly + * code clean + * prefix/suffix to wrap browser output to prevent conflicts with other `require` methods. + * Merge branch 'feature/should4xcompatibility' + * compile for browser tests.. all pass + * added header/status/html/json + * throw tests + * should.throw & should.not.throw shortcuts + * improved `throw` type detection and messaging + * contain is now `include` … keys modifier is now `contain` + * removed object() test + * removed #respondTo + * Merge branch 'bug/2' + * replaced __defineGetter__ with defineProperty for all uses + * [docs] change mp tracking code + * docs site updated with assert (TDD) interface + * updated doc comments for assert interface + +0.1.1 / 2011-12-16 +================== + + * docs ready for upcoming 0.1.1 + * readme image fixed [ci skip] + * more readme tweaks [ci skip] + * réadmet image fixed [ci skip] + * documentation + * codex locked in version 0.0.5 + * more comments to assertions for docs + * assertions fully commented, browser library updated + * adding codex as doc dependancy + * prepping for docs + * assertion component completely commented for documentation + * added exist test + * var expect outside of browser if check + * added keywords to package.json + +0.1.0 / 2011-12-15 +================== + + * failing on purpose successful .. back to normal + * testing travis failure + * assert#arguments getter + * readme typo + * updated README + * added travis and npmignore + * copyright notices … think i got them all + * moved expect interface to own file for consistency + * assert ui deepEqual + * browser tests expect (all working) + * browser version built + * chai.fail (should ui) + * expect tests browser compatible + * tests for should and expect (all pass) + * moved fail to primary export + * should compatibility testing + * within, greaterThan, object, keys, + * Aliases + * Assertion#property now correctly works with negate and undefined values + * error message language matches should + * Assertion#respondTo + * Assertion now uses inspect util + * git ignore node modules + * should is exported + * AssertionError __proto__ from Error.prototype + * add should interface for should.js compatibility + * moved eql to until folder and added inspect from (joyent/node) + * added mocha for testing + * browser build for current api + * multiple .property assertions + * added deep equal from node + +0.0.2 / 2011-12-07 +================== + + * cleaner output on error + * improved exists detection + * package remnant artifact + * empty deep equal + * test browser build + * assertion cleanup + * client compile script + * makefile + * most of the basic assertions + * allow no parameters to assertion error + * name change + * assertion error instance + * main exports: assert() & expect() + * initialize diff --git a/tests/tools/chai/README.md b/tests/tools/chai/README.md new file mode 100644 index 0000000..45d7bec --- /dev/null +++ b/tests/tools/chai/README.md @@ -0,0 +1,99 @@ +[![Chai Documentation](http://chaijs.com/public/img/chai-logo.png)](http://chaijs.com) + +Chai is a BDD / TDD assertion library for [node](http://nodejs.org) and the browser that +can be delightfully paired with any javascript testing framework. + +For more information or to download plugins, view the [documentation](http://chaijs.com). + +[![Build Status](https://travis-ci.org/chaijs/chai.png?branch=master)](https://travis-ci.org/chaijs/chai) + +[![Selenium Test Status](https://saucelabs.com/browser-matrix/chaijs.svg)](https://saucelabs.com/u/chaijs) + +### Plugins + +Chai offers a robust Plugin architecture for extending Chai's assertions and interfaces. + +- Need a plugin? View the [official plugin list](http://chaijs.com/plugins). +- Have a plugin and want it listed? Open a Pull Request at [chaijs/chai-docs:plugin.js](https://github.com/chaijs/chai-docs/blob/master/plugins.js#L1-L12). +- Want to build a plugin? Read the [plugin api documentation](http://chaijs.com/guide/plugins/). + +### Related Projects + +- [chaijs / assertion-error](https://github.com/chaijs/assertion-error): Custom `Error` constructor thrown upon an assertion failing. +- [chaijs / deep-eql](https://github.com/chaijs/deep-eql): Improved deep equality testing for Node.js and the browser. + +### Contributors + + project : chai + repo age : 2 years, 3 months ago + commits : 756 + active : 170 days + files : 57 + authors : + 540 Jake Luer 71.4% + 79 Veselin Todorov 10.4% + 43 Domenic Denicola 5.7% + 6 Ruben Verborgh 0.8% + 5 George Kats 0.7% + 5 Jo Liss 0.7% + 5 Juliusz Gonera 0.7% + 5 Scott Nonnenberg 0.7% + 5 leider 0.7% + 4 John Firebaugh 0.5% + 4 Max Edmands 0.5% + 4 Nick Heiner 0.5% + 4 josher19 0.5% + 3 Andrei Neculau 0.4% + 3 Duncan Beevers 0.4% + 3 Jake Rosoman 0.4% + 3 Jeff Barczewski 0.4% + 3 Ryunosuke SATO 0.4% + 3 Veselin 0.4% + 2 Bartvds 0.3% + 2 Edwin Shao 0.3% + 2 Jakub Nešetřil 0.3% + 2 Roman Masek 0.3% + 2 Teddy Cross 0.3% + 1 Anand Patil 0.1% + 1 Benjamin Horsleben 0.1% + 1 Brandon Payton 0.1% + 1 Chris Connelly 0.1% + 1 Chun-Yi 0.1% + 1 DD 0.1% + 1 Dido Arellano 0.1% + 1 Jeff Welch 0.1% + 1 Kilian Ciuffolo 0.1% + 1 Luís Cardoso 0.1% + 1 Niklas Närhinen 0.1% + 1 Paul Miller 0.1% + 1 Refael Ackermann 0.1% + 1 Sasha Koss 0.1% + 1 Victor Costan 0.1% + 1 Vinay Pulim 0.1% + 1 Virginie BARDALES 0.1% + 1 laconbass 0.1% + 1 piecioshka 0.1% + +## License + +(The MIT License) + +Copyright (c) 2011-2014 Jake Luer + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/tests/tools/chai/ReleaseNotes.md b/tests/tools/chai/ReleaseNotes.md new file mode 100644 index 0000000..fb2a1dc --- /dev/null +++ b/tests/tools/chai/ReleaseNotes.md @@ -0,0 +1,482 @@ +# Release Notes + +## 1.9.1 / 2014-03-19 + +The following changes are required if you are upgrading from the previous version: + +- **Users:** + - Migrate configuration options to new interface. (see notes) +- **Plugin Developers:** + - No changes required +- **Core Contributors:** + - Refresh `node_modules` folder for updated dependencies. + +### Configuration + +There have been requests for changes and additions to the configuration mechanisms +and their impact in the Chai architecture. As such, we have decoupled the +configuration from the `Assertion` constructor. This not only allows for centralized +configuration, but will allow us to shift the responsibility from the `Assertion` +constructor to the `assert` interface in future releases. + +These changes have been implemented in a non-breaking way, but a depretiation +warning will be presented to users until they migrate. The old config method will +be removed in either `v1.11.0` or `v2.0.0`, whichever comes first. + +#### Quick Migration + +```js +// change this: +chai.Assertion.includeStack = true; +chai.Assertion.showDiff = false; + +// ... to this: +chai.config.includeStack = true; +chai.config.showDiff = false; +``` + +#### All Config Options + +##### config.includeStack + +- **@param** _{Boolean}_ +- **@default** `false` + +User configurable property, influences whether stack trace is included in +Assertion error message. Default of `false` suppresses stack trace in the error +message. + +##### config.showDiff + +- **@param** _{Boolean}_ +- **@default** `true` + +User configurable property, influences whether or not the `showDiff` flag +should be included in the thrown AssertionErrors. `false` will always be `false`; +`true` will be true when the assertion has requested a diff be shown. + +##### config.truncateThreshold **(NEW)** + +- **@param** _{Number}_ +- **@default** `40` + +User configurable property, sets length threshold for actual and expected values +in assertion errors. If this threshold is exceeded, the value is truncated. + +Set it to zero if you want to disable truncating altogether. + +```js +chai.config.truncateThreshold = 0; // disable truncating +``` + +### Community Contributions + +- [#228](https://github.com/chaijs/chai/pull/228) Deep equality check for memebers. [@duncanbeevers](https://github.com/duncanbeevers) +- [#247](https://github.com/chaijs/chai/pull/247) Proofreading. [@didorellano](https://github.com/didoarellano) +- [#244](https://github.com/chaijs/chai/pull/244) Fix `contain`/`include` 1.9.0 regression. [@leider](https://github.com/leider) +- [#233](https://github.com/chaijs/chai/pull/233) Improvements to `ssfi` for `assert` interface. [@refack](https://github.com/refack) +- [#251](https://github.com/chaijs/chai/pull/251) New config option: object display threshold. [@romario333](https://github.com/romario333) + +Thank you to all who took time to contribute! + +### Other Bug Fixes + +- [#183](https://github.com/chaijs/chai/issues/183) Allow `undefined` for actual. (internal api) +- Update Karam(+plugins)/Istanbul to most recent versions. + +## 1.9.0 / 2014-01-29 + +The following changes are required if you are upgrading from the previous version: + +- **Users:** + - No changes required +- **Plugin Developers:** + - Review [#219](https://github.com/chaijs/chai/pull/219). +- **Core Contributors:** + - Refresh `node_modules` folder for updated dependencies. + +### Community Contributions + +- [#202](https://github.com/chaijs/chai/pull/201) Improve error message for .throw(). [@andreineculau](https://github.com/andreineculau) +- [#217](https://github.com/chaijs/chai/pull/217) Chai tests can be run with `--watch`. [@demands](https://github.com/demands) +- [#219](https://github.com/chaijs/chai/pull/219) Add overwriteChainableMethod utility. [@demands](https://github.com/demands) +- [#224](https://github.com/chaijs/chai/pull/224) Return error on throw method to chain on error properties. [@vbardales](https://github.com/vbardales) +- [#226](https://github.com/chaijs/chai/pull/226) Add `has` to language chains. [@duncanbeevers](https://github.com/duncanbeevers) +- [#230](https://github.com/chaijs/chai/pull/230) Support `{a:1,b:2}.should.include({a:1})` [@jkroso](https://github.com/jkroso) +- [#231](https://github.com/chaijs/chai/pull/231) Update Copyright notices to 2014 [@duncanbeevers](https://github.com/duncanbeevers) +- [#232](https://github.com/chaijs/chai/pull/232) Avoid error instantiation if possible on assert.throws. [@laconbass](https://github.com/laconbass) + +Thank you to all who took time to contribute! + +### Other Bug Fixes + +- [#225](https://github.com/chaijs/chai/pull/225) Improved AMD wrapper provided by upstream `component(1)`. +- [#185](https://github.com/chaijs/chai/issues/185) `assert.throws()` returns thrown error for further assertions. +- [#237](https://github.com/chaijs/chai/pull/237) Remove coveralls/jscoverage, include istanbul coverage report in travis test. +- Update Karma and Sauce runner versions for consistent CI results. No more karma@canary. + +## 1.8.1 / 2013-10-10 + +The following changes are required if you are upgrading from the previous version: + +- **Users:** + - Refresh `node_modules` folder for updated dependencies. +- **Plugin Developers:** + - No changes required +- **Core Contributors:** + - Refresh `node_modules` folder for updated dependencies. + +### Browserify + +This is a small patch that updates the dependency tree so browserify users can install +chai. (Remove conditional requires) + +## 1.8.0 / 2013-09-18 + +The following changes are required if you are upgrading from the previous version: + +- **Users:** + - See `deep.equal` notes. +- **Plugin Developers:** + - No changes required +- **Core Contributors:** + - Refresh `node_modules` folder for updated dependencies. + +### Deep Equals + +This version of Chai focused on a overhaul to the deep equal utility. The code for this +tool has been removed from the core lib and can now be found at: +[chai / deep-eql](https://github.com/chaijs/deep-eql). As stated in previous releases, +this is part of a larger initiative to provide transparency, independent testing, and coverage for +some of the more complicated internal tools. + +For the most part `.deep.equal` will behave the same as it has. However, in order to provide a +consistent ruleset across all types being tested, the following changes have been made and _might_ +require changes to your tests. + +**1.** Strict equality for non-traversable nodes according to [egal](http://wiki.ecmascript.org/doku.php?id=harmony:egal). + +_Previously:_ Non-traversable equal via `===`. + +```js +expect(NaN).to.deep.equal(NaN); +expect(-0).to.not.deep.equal(+0); +``` + +**2.** Arguments are not Arrays (and all types must be equal): + +_Previously:_ Some crazy nonsense that led to empty arrays deep equaling empty objects deep equaling dates. + +```js +expect(arguments).to.not.deep.equal([]); +expect(Array.prototype.slice.call(arguments)).to.deep.equal([]); +``` + +- [#156](https://github.com/chaijs/chai/issues/156) Empty object is eql to empty array +- [#192](https://github.com/chaijs/chai/issues/192) empty object is eql to a Date object +- [#194](https://github.com/chaijs/chai/issues/194) refactor deep-equal utility + +### CI and Browser Testing + +Chai now runs the browser CI suite using [Karma](http://karma-runner.github.io/) directed at +[SauceLabs](https://saucelabs.com/). This means we get to know where our browser support stands... +and we get a cool badge: + +[![Selenium Test Status](https://saucelabs.com/browser-matrix/logicalparadox.svg)](https://saucelabs.com/u/logicalparadox) + +Look for the list of browsers/versions to expand over the coming releases. + +- [#195](https://github.com/chaijs/chai/issues/195) karma test framework + +## 1.7.2 / 2013-06-27 + +The following changes are required if you are upgrading from the previous version: + +- **Users:** + - No changes required. +- **Plugin Developers:** + - No changes required +- **Core Contributors:** + - Refresh `node_modules` folder for updated dependencies. + +### Coverage Reporting + +Coverage reporting has always been available for core-developers but the data has never been published +for our end users. In our ongoing effort to improve accountability this data will now be published via +the [coveralls.io](https://coveralls.io/) service. A badge has been added to the README and the full report +can be viewed online at the [chai coveralls project](https://coveralls.io/r/chaijs/chai). Furthermore, PRs +will receive automated messages indicating how their PR impacts test coverage. This service is tied to TravisCI. + +### Other Fixes + +- [#175](https://github.com/chaijs/chai/issues/175) Add `bower.json`. (Fix ignore all) + +## 1.7.1 / 2013-06-24 + +The following changes are required if you are upgrading from the previous version: + +- **Users:** + - No changes required. +- **Plugin Developers:** + - No changes required +- **Core Contributors:** + - Refresh `node_modules` folder for updated dependencies. + +### Official Bower Support + +Support has been added for the Bower Package Manager ([bower.io])(http://bower.io/). Though +Chai could be installed via Bower in the past, this update adds official support via the `bower.json` +specification file. + +- [#175](https://github.com/chaijs/chai/issues/175) Add `bower.json`. + +## 1.7.0 / 2013-06-17 + +The following changes are required if you are upgrading from the previous version: + +- **Users:** + - No changes required. +- **Plugin Developers:** + - Review AssertionError update notice. +- **Core Contributors:** + - Refresh `node_modules` folder for updated dependencies. + +### AssertionError Update Notice + +Chai now uses [chaijs/assertion-error](https://github.com/chaijs/assertion-error) instead an internal +constructor. This will allow for further iteration/experimentation of the AssertionError constructor +independant of Chai. Future plans include stack parsing for callsite support. + +This update constructor has a different constructor param signature that conforms more with the standard +`Error` object. If your plugin throws and `AssertionError` directly you will need to update your plugin +with the new signature. + +```js +var AssertionError = require('chai').AssertionError; + +/** + * previous + * + * @param {Object} options + */ + +throw new AssertionError({ + message: 'An assertion error occurred' + , actual: actual + , expect: expect + , startStackFunction: arguments.callee + , showStack: true +}); + +/** + * new + * + * @param {String} message + * @param {Object} options + * @param {Function} start stack function + */ + +throw new AssertionError('An assertion error occurred', { + actual: actual + , expect: expect + , showStack: true +}, arguments.callee); + +// other signatures +throw new AssertionError('An assertion error occurred'); +throw new AssertionError('An assertion error occurred', null, arguments.callee); +``` + +#### External Dependencies + +This is the first non-developement dependency for Chai. As Chai continues to evolve we will begin adding +more; the next will likely be improved type detection and deep equality. With Chai's userbase continually growing +there is an higher need for accountability and documentation. External dependencies will allow us to iterate and +test on features independent from our interfaces. + +Note: The browser packaged version `chai.js` will ALWAYS contain all dependencies needed to run Chai. + +### Community Contributions + +- [#169](https://github.com/chaijs/chai/pull/169) Fix deep equal comparison for Date/Regexp types. [@katsgeorgeek](https://github.com/katsgeorgeek) +- [#171](https://github.com/chaijs/chai/pull/171) Add `assert.notOk()`. [@Bartvds](https://github.com/Bartvds) +- [#173](https://github.com/chaijs/chai/pull/173) Fix `inspect` utility. [@domenic](https://github.com/domenic) + +Thank you to all who took the time to contribute! + +## 1.6.1 / 2013-06-05 + +The following changes are required if you are upgrading from the previous version: + +- **Users:** + - No changes required. +- **Plugin Developers:** + - No changes required. +- **Core Contributors:** + - Refresh `node_modules` folder for updated developement dependencies. + +### Deep Equality + +Regular Expressions are now tested as part of all deep equality assertions. In previous versions +they silently passed for all scenarios. Thanks to [@katsgeorgeek](https://github.com/katsgeorgeek) for the contribution. + +### Community Contributions + +- [#161](https://github.com/chaijs/chai/pull/161) Fix documented name for assert interface's isDefined method. [@brandonpayton](https://github.com/brandonpayton) +- [#168](https://github.com/chaijs/chai/pull/168) Fix comparison equality of two regexps for when using deep equality. [@katsgeorgeek](https://github.com/katsgeorgeek) + +Thank you to all who took the time to contribute! + +### Additional Notes + +- Mocha has been locked at version `1.8.x` to ensure `mocha-phantomjs` compatibility. + +## 1.6.0 / 2013-04-29 + +The following changes are required if you are upgrading from the previous version: + +- **Users:** + - No changes required. +- **Plugin Developers:** + - No changes required. +- **Core Contributors:** + - Refresh `node_modules` folder for updated developement dependencies. + +### New Assertions + +#### Array Members Inclusion + +Asserts that the target is a superset of `set`, or that the target and `set` have the same members. +Order is not taken into account. Thanks to [@NickHeiner](https://github.com/NickHeiner) for the contribution. + +```js +// (expect/should) full set +expect([4, 2]).to.have.members([2, 4]); +expect([5, 2]).to.not.have.members([5, 2, 1]); + +// (expect/should) inclusion +expect([1, 2, 3]).to.include.members([3, 2]); +expect([1, 2, 3]).to.not.include.members([3, 2, 8]); + +// (assert) full set +assert.sameMembers([ 1, 2, 3 ], [ 2, 1, 3 ], 'same members'); + +// (assert) inclusion +assert.includeMembers([ 1, 2, 3 ], [ 2, 1 ], 'include members'); + +``` + +#### Non-inclusion for Assert Interface + +Most `assert` functions have a negative version, like `instanceOf()` has a corresponding `notInstaceOf()`. +However `include()` did not have a corresponding `notInclude()`. This has been added. + +```js +assert.notInclude([ 1, 2, 3 ], 8); +assert.notInclude('foobar', 'baz'); +``` + +### Community Contributions + +- [#140](https://github.com/chaijs/chai/pull/140) Restore `call`/`apply` methods for plugin interface. [@RubenVerborgh](https://github.com/RubenVerborgh) +- [#148](https://github.com/chaijs/chai/issues/148)/[#153](https://github.com/chaijs/chai/pull/153) Add `members` and `include.members` assertions. [#NickHeiner](https://github.com/NickHeiner) + +Thank you to all who took time to contribute! + +### Other Bug Fixes + +- [#142](https://github.com/chaijs/chai/issues/142) `assert#include` will no longer silently pass on wrong-type haystack. +- [#158](https://github.com/chaijs/chai/issues/158) `assert#notInclude` has been added. +- Travis-CI now tests Node.js `v0.10.x`. Support for `v0.6.x` has been removed. `v0.8.x` is still tested as before. + +## 1.5.0 / 2013-02-03 + +### Migration Requirements + +The following changes are required if you are upgrading from the previous version: + +- **Users:** + - _Update [2013-02-04]:_ Some users may notice a small subset of deep equality assertions will no longer pass. This is the result of + [#120](https://github.com/chaijs/chai/issues/120), an improvement to our deep equality algorithm. Users will need to revise their assertions + to be more granular should this occur. Further information: [#139](https://github.com/chaijs/chai/issues/139). +- **Plugin Developers:** + - No changes required. +- **Core Contributors:** + - Refresh `node_modules` folder for updated developement dependencies. + +### Community Contributions + +- [#126](https://github.com/chaijs/chai/pull/126): Add `eqls` alias for `eql`. [@RubenVerborgh](https://github.com/RubenVerborgh) +- [#127](https://github.com/chaijs/chai/issues/127): Performance refactor for chainable methods. [@RubenVerborgh](https://github.com/RubenVerborgh) +- [#133](https://github.com/chaijs/chai/pull/133): Assertion `.throw` support for primitives. [@RubenVerborgh](https://github.com/RubenVerborgh) +- [#137](https://github.com/chaijs/chai/issues/137): Assertion `.throw` support for empty messages. [@timnew](https://github.com/timnew) +- [#136](https://github.com/chaijs/chai/pull/136): Fix backward negation messages when using `.above()` and `.below()`. [@whatthejeff](https://github.com/whatthejeff) + +Thank you to all who took time to contribute! + +### Other Bug Fixes + +- Improve type detection of `.a()`/`.an()` to work in cross-browser scenarios. +- [#116](https://github.com/chaijs/chai/issues/116): `.throw()` has cleaner display of errors when WebKit browsers. +- [#120](https://github.com/chaijs/chai/issues/120): `.eql()` now works to compare dom nodes in browsers. + + +### Usage Updates + +#### For Users + +**1. Component Support:** Chai now included the proper configuration to be installed as a +[component](https://github.com/component/component). Component users are encouraged to consult +[chaijs.com](http://chaijs.com) for the latest version number as using the master branch +does not gaurantee stability. + +```js +// relevant component.json + devDependencies: { + "chaijs/chai": "1.5.0" + } +``` + +Alternatively, bleeding-edge is available: + + $ component install chaijs/chai + +**2. Configurable showDiff:** Some test runners (such as [mocha](http://visionmedia.github.com/mocha/)) +include support for showing the diff of strings and objects when an equality error occurs. Chai has +already included support for this, however some users may not prefer this display behavior. To revert to +no diff display, the following configuration is available: + +```js +chai.Assertion.showDiff = false; // diff output disabled +chai.Assertion.showDiff = true; // default, diff output enabled +``` + +#### For Plugin Developers + +**1. New Utility - type**: The new utility `.type()` is available as a better implementation of `typeof` +that can be used cross-browser. It handles the inconsistencies of Array, `null`, and `undefined` detection. + +- **@param** _{Mixed}_ object to detect type of +- **@return** _{String}_ object type + +```js +chai.use(function (c, utils) { + // some examples + utils.type({}); // 'object' + utils.type(null); // `null' + utils.type(undefined); // `undefined` + utils.type([]); // `array` +}); +``` + +#### For Core Contributors + +**1. Browser Testing**: Browser testing of the `./chai.js` file is now available in the command line +via PhantomJS. `make test` and Travis-CI will now also rebuild and test `./chai.js`. Consequently, all +pull requests will now be browser tested in this way. + +_Note: Contributors opening pull requests should still NOT include the browser build._ + +**2. SauceLabs Testing**: Early SauceLab support has been enabled with the file `./support/mocha-cloud.js`. +Those interested in trying it out should create a free [Open Sauce](https://saucelabs.com/signup/plan) account +and include their credentials in `./test/auth/sauce.json`. diff --git a/tests/tools/chai/bower.json b/tests/tools/chai/bower.json new file mode 100644 index 0000000..8d7b3d5 --- /dev/null +++ b/tests/tools/chai/bower.json @@ -0,0 +1,27 @@ +{ + "name": "chai" + , "version": "1.9.1" + , "description": "BDD/TDD assertion library for node.js and the browser. Test framework agnostic." + , "license": "MIT" + , "keywords": [ + "test" + , "assertion" + , "assert" + , "testing" + , "chai" + ] + , "main": "chai.js" + , "ignore": [ + "build" + , "components" + , "lib" + , "node_modules" + , "support" + , "test" + , "index.js" + , "Makefile" + , ".*" + ] + , "dependencies": {} + , "devDependencies": {} +} diff --git a/tests/tools/chai/chai.js b/tests/tools/chai/chai.js new file mode 100644 index 0000000..a75da85 --- /dev/null +++ b/tests/tools/chai/chai.js @@ -0,0 +1,4782 @@ +;(function(){ + +/** + * Require the given path. + * + * @param {String} path + * @return {Object} exports + * @api public + */ + +function require(path, parent, orig) { + var resolved = require.resolve(path); + + // lookup failed + if (null == resolved) { + orig = orig || path; + parent = parent || 'root'; + var err = new Error('Failed to require "' + orig + '" from "' + parent + '"'); + err.path = orig; + err.parent = parent; + err.require = true; + throw err; + } + + var module = require.modules[resolved]; + + // perform real require() + // by invoking the module's + // registered function + if (!module._resolving && !module.exports) { + var mod = {}; + mod.exports = {}; + mod.client = mod.component = true; + module._resolving = true; + module.call(this, mod.exports, require.relative(resolved), mod); + delete module._resolving; + module.exports = mod.exports; + } + + return module.exports; +} + +/** + * Registered modules. + */ + +require.modules = {}; + +/** + * Registered aliases. + */ + +require.aliases = {}; + +/** + * Resolve `path`. + * + * Lookup: + * + * - PATH/index.js + * - PATH.js + * - PATH + * + * @param {String} path + * @return {String} path or null + * @api private + */ + +require.resolve = function(path) { + if (path.charAt(0) === '/') path = path.slice(1); + + var paths = [ + path, + path + '.js', + path + '.json', + path + '/index.js', + path + '/index.json' + ]; + + for (var i = 0; i < paths.length; i++) { + var path = paths[i]; + if (require.modules.hasOwnProperty(path)) return path; + if (require.aliases.hasOwnProperty(path)) return require.aliases[path]; + } +}; + +/** + * Normalize `path` relative to the current path. + * + * @param {String} curr + * @param {String} path + * @return {String} + * @api private + */ + +require.normalize = function(curr, path) { + var segs = []; + + if ('.' != path.charAt(0)) return path; + + curr = curr.split('/'); + path = path.split('/'); + + for (var i = 0; i < path.length; ++i) { + if ('..' == path[i]) { + curr.pop(); + } else if ('.' != path[i] && '' != path[i]) { + segs.push(path[i]); + } + } + + return curr.concat(segs).join('/'); +}; + +/** + * Register module at `path` with callback `definition`. + * + * @param {String} path + * @param {Function} definition + * @api private + */ + +require.register = function(path, definition) { + require.modules[path] = definition; +}; + +/** + * Alias a module definition. + * + * @param {String} from + * @param {String} to + * @api private + */ + +require.alias = function(from, to) { + if (!require.modules.hasOwnProperty(from)) { + throw new Error('Failed to alias "' + from + '", it does not exist'); + } + require.aliases[to] = from; +}; + +/** + * Return a require function relative to the `parent` path. + * + * @param {String} parent + * @return {Function} + * @api private + */ + +require.relative = function(parent) { + var p = require.normalize(parent, '..'); + + /** + * lastIndexOf helper. + */ + + function lastIndexOf(arr, obj) { + var i = arr.length; + while (i--) { + if (arr[i] === obj) return i; + } + return -1; + } + + /** + * The relative require() itself. + */ + + function localRequire(path) { + var resolved = localRequire.resolve(path); + return require(resolved, parent, path); + } + + /** + * Resolve relative to the parent. + */ + + localRequire.resolve = function(path) { + var c = path.charAt(0); + if ('/' == c) return path.slice(1); + if ('.' == c) return require.normalize(p, path); + + // resolve deps by returning + // the dep in the nearest "deps" + // directory + var segs = parent.split('/'); + var i = lastIndexOf(segs, 'deps') + 1; + if (!i) i = 0; + path = segs.slice(0, i + 1).join('/') + '/deps/' + path; + return path; + }; + + /** + * Check if module is defined at `path`. + */ + + localRequire.exists = function(path) { + return require.modules.hasOwnProperty(localRequire.resolve(path)); + }; + + return localRequire; +}; +require.register("chaijs-assertion-error/index.js", function(exports, require, module){ +/*! + * assertion-error + * Copyright(c) 2013 Jake Luer + * MIT Licensed + */ + +/*! + * Return a function that will copy properties from + * one object to another excluding any originally + * listed. Returned function will create a new `{}`. + * + * @param {String} excluded properties ... + * @return {Function} + */ + +function exclude () { + var excludes = [].slice.call(arguments); + + function excludeProps (res, obj) { + Object.keys(obj).forEach(function (key) { + if (!~excludes.indexOf(key)) res[key] = obj[key]; + }); + } + + return function extendExclude () { + var args = [].slice.call(arguments) + , i = 0 + , res = {}; + + for (; i < args.length; i++) { + excludeProps(res, args[i]); + } + + return res; + }; +}; + +/*! + * Primary Exports + */ + +module.exports = AssertionError; + +/** + * ### AssertionError + * + * An extension of the JavaScript `Error` constructor for + * assertion and validation scenarios. + * + * @param {String} message + * @param {Object} properties to include (optional) + * @param {callee} start stack function (optional) + */ + +function AssertionError (message, _props, ssf) { + var extend = exclude('name', 'message', 'stack', 'constructor', 'toJSON') + , props = extend(_props || {}); + + // default values + this.message = message || 'Unspecified AssertionError'; + this.showDiff = false; + + // copy from properties + for (var key in props) { + this[key] = props[key]; + } + + // capture stack trace + ssf = ssf || arguments.callee; + if (ssf && Error.captureStackTrace) { + Error.captureStackTrace(this, ssf); + } +} + +/*! + * Inherit from Error.prototype + */ + +AssertionError.prototype = Object.create(Error.prototype); + +/*! + * Statically set name + */ + +AssertionError.prototype.name = 'AssertionError'; + +/*! + * Ensure correct constructor + */ + +AssertionError.prototype.constructor = AssertionError; + +/** + * Allow errors to be converted to JSON for static transfer. + * + * @param {Boolean} include stack (default: `true`) + * @return {Object} object that can be `JSON.stringify` + */ + +AssertionError.prototype.toJSON = function (stack) { + var extend = exclude('constructor', 'toJSON', 'stack') + , props = extend({ name: this.name }, this); + + // include stack if exists and not turned off + if (false !== stack && this.stack) { + props.stack = this.stack; + } + + return props; +}; + +}); +require.register("chaijs-type-detect/lib/type.js", function(exports, require, module){ +/*! + * type-detect + * Copyright(c) 2013 jake luer + * MIT Licensed + */ + +/*! + * Primary Exports + */ + +var exports = module.exports = getType; + +/*! + * Detectable javascript natives + */ + +var natives = { + '[object Array]': 'array' + , '[object RegExp]': 'regexp' + , '[object Function]': 'function' + , '[object Arguments]': 'arguments' + , '[object Date]': 'date' +}; + +/** + * ### typeOf (obj) + * + * Use several different techniques to determine + * the type of object being tested. + * + * + * @param {Mixed} object + * @return {String} object type + * @api public + */ + +function getType (obj) { + var str = Object.prototype.toString.call(obj); + if (natives[str]) return natives[str]; + if (obj === null) return 'null'; + if (obj === undefined) return 'undefined'; + if (obj === Object(obj)) return 'object'; + return typeof obj; +} + +exports.Library = Library; + +/** + * ### Library + * + * Create a repository for custom type detection. + * + * ```js + * var lib = new type.Library; + * ``` + * + */ + +function Library () { + this.tests = {}; +} + +/** + * #### .of (obj) + * + * Expose replacement `typeof` detection to the library. + * + * ```js + * if ('string' === lib.of('hello world')) { + * // ... + * } + * ``` + * + * @param {Mixed} object to test + * @return {String} type + */ + +Library.prototype.of = getType; + +/** + * #### .define (type, test) + * + * Add a test to for the `.test()` assertion. + * + * Can be defined as a regular expression: + * + * ```js + * lib.define('int', /^[0-9]+$/); + * ``` + * + * ... or as a function: + * + * ```js + * lib.define('bln', function (obj) { + * if ('boolean' === lib.of(obj)) return true; + * var blns = [ 'yes', 'no', 'true', 'false', 1, 0 ]; + * if ('string' === lib.of(obj)) obj = obj.toLowerCase(); + * return !! ~blns.indexOf(obj); + * }); + * ``` + * + * @param {String} type + * @param {RegExp|Function} test + * @api public + */ + +Library.prototype.define = function (type, test) { + if (arguments.length === 1) return this.tests[type]; + this.tests[type] = test; + return this; +}; + +/** + * #### .test (obj, test) + * + * Assert that an object is of type. Will first + * check natives, and if that does not pass it will + * use the user defined custom tests. + * + * ```js + * assert(lib.test('1', 'int')); + * assert(lib.test('yes', 'bln')); + * ``` + * + * @param {Mixed} object + * @param {String} type + * @return {Boolean} result + * @api public + */ + +Library.prototype.test = function (obj, type) { + if (type === getType(obj)) return true; + var test = this.tests[type]; + + if (test && 'regexp' === getType(test)) { + return test.test(obj); + } else if (test && 'function' === getType(test)) { + return test(obj); + } else { + throw new ReferenceError('Type test "' + type + '" not defined or invalid.'); + } +}; + +}); +require.register("chaijs-deep-eql/lib/eql.js", function(exports, require, module){ +/*! + * deep-eql + * Copyright(c) 2013 Jake Luer + * MIT Licensed + */ + +/*! + * Module dependencies + */ + +var type = require('type-detect'); + +/*! + * Buffer.isBuffer browser shim + */ + +var Buffer; +try { Buffer = require('buffer').Buffer; } +catch(ex) { + Buffer = {}; + Buffer.isBuffer = function() { return false; } +} + +/*! + * Primary Export + */ + +module.exports = deepEqual; + +/** + * Assert super-strict (egal) equality between + * two objects of any type. + * + * @param {Mixed} a + * @param {Mixed} b + * @param {Array} memoised (optional) + * @return {Boolean} equal match + */ + +function deepEqual(a, b, m) { + if (sameValue(a, b)) { + return true; + } else if ('date' === type(a)) { + return dateEqual(a, b); + } else if ('regexp' === type(a)) { + return regexpEqual(a, b); + } else if (Buffer.isBuffer(a)) { + return bufferEqual(a, b); + } else if ('arguments' === type(a)) { + return argumentsEqual(a, b, m); + } else if (!typeEqual(a, b)) { + return false; + } else if (('object' !== type(a) && 'object' !== type(b)) + && ('array' !== type(a) && 'array' !== type(b))) { + return sameValue(a, b); + } else { + return objectEqual(a, b, m); + } +} + +/*! + * Strict (egal) equality test. Ensures that NaN always + * equals NaN and `-0` does not equal `+0`. + * + * @param {Mixed} a + * @param {Mixed} b + * @return {Boolean} equal match + */ + +function sameValue(a, b) { + if (a === b) return a !== 0 || 1 / a === 1 / b; + return a !== a && b !== b; +} + +/*! + * Compare the types of two given objects and + * return if they are equal. Note that an Array + * has a type of `array` (not `object`) and arguments + * have a type of `arguments` (not `array`/`object`). + * + * @param {Mixed} a + * @param {Mixed} b + * @return {Boolean} result + */ + +function typeEqual(a, b) { + return type(a) === type(b); +} + +/*! + * Compare two Date objects by asserting that + * the time values are equal using `saveValue`. + * + * @param {Date} a + * @param {Date} b + * @return {Boolean} result + */ + +function dateEqual(a, b) { + if ('date' !== type(b)) return false; + return sameValue(a.getTime(), b.getTime()); +} + +/*! + * Compare two regular expressions by converting them + * to string and checking for `sameValue`. + * + * @param {RegExp} a + * @param {RegExp} b + * @return {Boolean} result + */ + +function regexpEqual(a, b) { + if ('regexp' !== type(b)) return false; + return sameValue(a.toString(), b.toString()); +} + +/*! + * Assert deep equality of two `arguments` objects. + * Unfortunately, these must be sliced to arrays + * prior to test to ensure no bad behavior. + * + * @param {Arguments} a + * @param {Arguments} b + * @param {Array} memoize (optional) + * @return {Boolean} result + */ + +function argumentsEqual(a, b, m) { + if ('arguments' !== type(b)) return false; + a = [].slice.call(a); + b = [].slice.call(b); + return deepEqual(a, b, m); +} + +/*! + * Get enumerable properties of a given object. + * + * @param {Object} a + * @return {Array} property names + */ + +function enumerable(a) { + var res = []; + for (var key in a) res.push(key); + return res; +} + +/*! + * Simple equality for flat iterable objects + * such as Arrays or Node.js buffers. + * + * @param {Iterable} a + * @param {Iterable} b + * @return {Boolean} result + */ + +function iterableEqual(a, b) { + if (a.length !== b.length) return false; + + var i = 0; + var match = true; + + for (; i < a.length; i++) { + if (a[i] !== b[i]) { + match = false; + break; + } + } + + return match; +} + +/*! + * Extension to `iterableEqual` specifically + * for Node.js Buffers. + * + * @param {Buffer} a + * @param {Mixed} b + * @return {Boolean} result + */ + +function bufferEqual(a, b) { + if (!Buffer.isBuffer(b)) return false; + return iterableEqual(a, b); +} + +/*! + * Block for `objectEqual` ensuring non-existing + * values don't get in. + * + * @param {Mixed} object + * @return {Boolean} result + */ + +function isValue(a) { + return a !== null && a !== undefined; +} + +/*! + * Recursively check the equality of two objects. + * Once basic sameness has been established it will + * defer to `deepEqual` for each enumerable key + * in the object. + * + * @param {Mixed} a + * @param {Mixed} b + * @return {Boolean} result + */ + +function objectEqual(a, b, m) { + if (!isValue(a) || !isValue(b)) { + return false; + } + + if (a.prototype !== b.prototype) { + return false; + } + + var i; + if (m) { + for (i = 0; i < m.length; i++) { + if ((m[i][0] === a && m[i][1] === b) + || (m[i][0] === b && m[i][1] === a)) { + return true; + } + } + } else { + m = []; + } + + try { + var ka = enumerable(a); + var kb = enumerable(b); + } catch (ex) { + return false; + } + + ka.sort(); + kb.sort(); + + if (!iterableEqual(ka, kb)) { + return false; + } + + m.push([ a, b ]); + + var key; + for (i = ka.length - 1; i >= 0; i--) { + key = ka[i]; + if (!deepEqual(a[key], b[key], m)) { + return false; + } + } + + return true; +} + +}); +require.register("chai/index.js", function(exports, require, module){ +module.exports = require('./lib/chai'); + +}); +require.register("chai/lib/chai.js", function(exports, require, module){ +/*! + * chai + * Copyright(c) 2011-2014 Jake Luer + * MIT Licensed + */ + +var used = [] + , exports = module.exports = {}; + +/*! + * Chai version + */ + +exports.version = '1.9.1'; + +/*! + * Assertion Error + */ + +exports.AssertionError = require('assertion-error'); + +/*! + * Utils for plugins (not exported) + */ + +var util = require('./chai/utils'); + +/** + * # .use(function) + * + * Provides a way to extend the internals of Chai + * + * @param {Function} + * @returns {this} for chaining + * @api public + */ + +exports.use = function (fn) { + if (!~used.indexOf(fn)) { + fn(this, util); + used.push(fn); + } + + return this; +}; + +/*! + * Configuration + */ + +var config = require('./chai/config'); +exports.config = config; + +/*! + * Primary `Assertion` prototype + */ + +var assertion = require('./chai/assertion'); +exports.use(assertion); + +/*! + * Core Assertions + */ + +var core = require('./chai/core/assertions'); +exports.use(core); + +/*! + * Expect interface + */ + +var expect = require('./chai/interface/expect'); +exports.use(expect); + +/*! + * Should interface + */ + +var should = require('./chai/interface/should'); +exports.use(should); + +/*! + * Assert interface + */ + +var assert = require('./chai/interface/assert'); +exports.use(assert); + +}); +require.register("chai/lib/chai/assertion.js", function(exports, require, module){ +/*! + * chai + * http://chaijs.com + * Copyright(c) 2011-2014 Jake Luer + * MIT Licensed + */ + +var config = require('./config'); + +module.exports = function (_chai, util) { + /*! + * Module dependencies. + */ + + var AssertionError = _chai.AssertionError + , flag = util.flag; + + /*! + * Module export. + */ + + _chai.Assertion = Assertion; + + /*! + * Assertion Constructor + * + * Creates object for chaining. + * + * @api private + */ + + function Assertion (obj, msg, stack) { + flag(this, 'ssfi', stack || arguments.callee); + flag(this, 'object', obj); + flag(this, 'message', msg); + } + + Object.defineProperty(Assertion, 'includeStack', { + get: function() { + console.warn('Assertion.includeStack is deprecated, use chai.config.includeStack instead.'); + return config.includeStack; + }, + set: function(value) { + console.warn('Assertion.includeStack is deprecated, use chai.config.includeStack instead.'); + config.includeStack = value; + } + }); + + Object.defineProperty(Assertion, 'showDiff', { + get: function() { + console.warn('Assertion.showDiff is deprecated, use chai.config.showDiff instead.'); + return config.showDiff; + }, + set: function(value) { + console.warn('Assertion.showDiff is deprecated, use chai.config.showDiff instead.'); + config.showDiff = value; + } + }); + + Assertion.addProperty = function (name, fn) { + util.addProperty(this.prototype, name, fn); + }; + + Assertion.addMethod = function (name, fn) { + util.addMethod(this.prototype, name, fn); + }; + + Assertion.addChainableMethod = function (name, fn, chainingBehavior) { + util.addChainableMethod(this.prototype, name, fn, chainingBehavior); + }; + + Assertion.overwriteProperty = function (name, fn) { + util.overwriteProperty(this.prototype, name, fn); + }; + + Assertion.overwriteMethod = function (name, fn) { + util.overwriteMethod(this.prototype, name, fn); + }; + + Assertion.overwriteChainableMethod = function (name, fn, chainingBehavior) { + util.overwriteChainableMethod(this.prototype, name, fn, chainingBehavior); + }; + + /*! + * ### .assert(expression, message, negateMessage, expected, actual) + * + * Executes an expression and check expectations. Throws AssertionError for reporting if test doesn't pass. + * + * @name assert + * @param {Philosophical} expression to be tested + * @param {String} message to display if fails + * @param {String} negatedMessage to display if negated expression fails + * @param {Mixed} expected value (remember to check for negation) + * @param {Mixed} actual (optional) will default to `this.obj` + * @api private + */ + + Assertion.prototype.assert = function (expr, msg, negateMsg, expected, _actual, showDiff) { + var ok = util.test(this, arguments); + if (true !== showDiff) showDiff = false; + if (true !== config.showDiff) showDiff = false; + + if (!ok) { + var msg = util.getMessage(this, arguments) + , actual = util.getActual(this, arguments); + throw new AssertionError(msg, { + actual: actual + , expected: expected + , showDiff: showDiff + }, (config.includeStack) ? this.assert : flag(this, 'ssfi')); + } + }; + + /*! + * ### ._obj + * + * Quick reference to stored `actual` value for plugin developers. + * + * @api private + */ + + Object.defineProperty(Assertion.prototype, '_obj', + { get: function () { + return flag(this, 'object'); + } + , set: function (val) { + flag(this, 'object', val); + } + }); +}; + +}); +require.register("chai/lib/chai/config.js", function(exports, require, module){ +module.exports = { + + /** + * ### config.includeStack + * + * User configurable property, influences whether stack trace + * is included in Assertion error message. Default of false + * suppresses stack trace in the error message. + * + * chai.config.includeStack = true; // enable stack on error + * + * @param {Boolean} + * @api public + */ + + includeStack: false, + + /** + * ### config.showDiff + * + * User configurable property, influences whether or not + * the `showDiff` flag should be included in the thrown + * AssertionErrors. `false` will always be `false`; `true` + * will be true when the assertion has requested a diff + * be shown. + * + * @param {Boolean} + * @api public + */ + + showDiff: true, + + /** + * ### config.truncateThreshold + * + * User configurable property, sets length threshold for actual and + * expected values in assertion errors. If this threshold is exceeded, + * the value is truncated. + * + * Set it to zero if you want to disable truncating altogether. + * + * chai.config.truncateThreshold = 0; // disable truncating + * + * @param {Number} + * @api public + */ + + truncateThreshold: 40 + +}; + +}); +require.register("chai/lib/chai/core/assertions.js", function(exports, require, module){ +/*! + * chai + * http://chaijs.com + * Copyright(c) 2011-2014 Jake Luer + * MIT Licensed + */ + +module.exports = function (chai, _) { + var Assertion = chai.Assertion + , toString = Object.prototype.toString + , flag = _.flag; + + /** + * ### Language Chains + * + * The following are provided as chainable getters to + * improve the readability of your assertions. They + * do not provide testing capabilities unless they + * have been overwritten by a plugin. + * + * **Chains** + * + * - to + * - be + * - been + * - is + * - that + * - and + * - has + * - have + * - with + * - at + * - of + * - same + * + * @name language chains + * @api public + */ + + [ 'to', 'be', 'been' + , 'is', 'and', 'has', 'have' + , 'with', 'that', 'at' + , 'of', 'same' ].forEach(function (chain) { + Assertion.addProperty(chain, function () { + return this; + }); + }); + + /** + * ### .not + * + * Negates any of assertions following in the chain. + * + * expect(foo).to.not.equal('bar'); + * expect(goodFn).to.not.throw(Error); + * expect({ foo: 'baz' }).to.have.property('foo') + * .and.not.equal('bar'); + * + * @name not + * @api public + */ + + Assertion.addProperty('not', function () { + flag(this, 'negate', true); + }); + + /** + * ### .deep + * + * Sets the `deep` flag, later used by the `equal` and + * `property` assertions. + * + * expect(foo).to.deep.equal({ bar: 'baz' }); + * expect({ foo: { bar: { baz: 'quux' } } }) + * .to.have.deep.property('foo.bar.baz', 'quux'); + * + * @name deep + * @api public + */ + + Assertion.addProperty('deep', function () { + flag(this, 'deep', true); + }); + + /** + * ### .a(type) + * + * The `a` and `an` assertions are aliases that can be + * used either as language chains or to assert a value's + * type. + * + * // typeof + * expect('test').to.be.a('string'); + * expect({ foo: 'bar' }).to.be.an('object'); + * expect(null).to.be.a('null'); + * expect(undefined).to.be.an('undefined'); + * + * // language chain + * expect(foo).to.be.an.instanceof(Foo); + * + * @name a + * @alias an + * @param {String} type + * @param {String} message _optional_ + * @api public + */ + + function an (type, msg) { + if (msg) flag(this, 'message', msg); + type = type.toLowerCase(); + var obj = flag(this, 'object') + , article = ~[ 'a', 'e', 'i', 'o', 'u' ].indexOf(type.charAt(0)) ? 'an ' : 'a '; + + this.assert( + type === _.type(obj) + , 'expected #{this} to be ' + article + type + , 'expected #{this} not to be ' + article + type + ); + } + + Assertion.addChainableMethod('an', an); + Assertion.addChainableMethod('a', an); + + /** + * ### .include(value) + * + * The `include` and `contain` assertions can be used as either property + * based language chains or as methods to assert the inclusion of an object + * in an array or a substring in a string. When used as language chains, + * they toggle the `contain` flag for the `keys` assertion. + * + * expect([1,2,3]).to.include(2); + * expect('foobar').to.contain('foo'); + * expect({ foo: 'bar', hello: 'universe' }).to.include.keys('foo'); + * + * @name include + * @alias contain + * @param {Object|String|Number} obj + * @param {String} message _optional_ + * @api public + */ + + function includeChainingBehavior () { + flag(this, 'contains', true); + } + + function include (val, msg) { + if (msg) flag(this, 'message', msg); + var obj = flag(this, 'object'); + var expected = false; + if (_.type(obj) === 'array' && _.type(val) === 'object') { + for (var i in obj) { + if (_.eql(obj[i], val)) { + expected = true; + break; + } + } + } else if (_.type(val) === 'object') { + if (!flag(this, 'negate')) { + for (var k in val) new Assertion(obj).property(k, val[k]); + return; + } + var subset = {} + for (var k in val) subset[k] = obj[k] + expected = _.eql(subset, val); + } else { + expected = obj && ~obj.indexOf(val) + } + this.assert( + expected + , 'expected #{this} to include ' + _.inspect(val) + , 'expected #{this} to not include ' + _.inspect(val)); + } + + Assertion.addChainableMethod('include', include, includeChainingBehavior); + Assertion.addChainableMethod('contain', include, includeChainingBehavior); + + /** + * ### .ok + * + * Asserts that the target is truthy. + * + * expect('everthing').to.be.ok; + * expect(1).to.be.ok; + * expect(false).to.not.be.ok; + * expect(undefined).to.not.be.ok; + * expect(null).to.not.be.ok; + * + * @name ok + * @api public + */ + + Assertion.addProperty('ok', function () { + this.assert( + flag(this, 'object') + , 'expected #{this} to be truthy' + , 'expected #{this} to be falsy'); + }); + + /** + * ### .true + * + * Asserts that the target is `true`. + * + * expect(true).to.be.true; + * expect(1).to.not.be.true; + * + * @name true + * @api public + */ + + Assertion.addProperty('true', function () { + this.assert( + true === flag(this, 'object') + , 'expected #{this} to be true' + , 'expected #{this} to be false' + , this.negate ? false : true + ); + }); + + /** + * ### .false + * + * Asserts that the target is `false`. + * + * expect(false).to.be.false; + * expect(0).to.not.be.false; + * + * @name false + * @api public + */ + + Assertion.addProperty('false', function () { + this.assert( + false === flag(this, 'object') + , 'expected #{this} to be false' + , 'expected #{this} to be true' + , this.negate ? true : false + ); + }); + + /** + * ### .null + * + * Asserts that the target is `null`. + * + * expect(null).to.be.null; + * expect(undefined).not.to.be.null; + * + * @name null + * @api public + */ + + Assertion.addProperty('null', function () { + this.assert( + null === flag(this, 'object') + , 'expected #{this} to be null' + , 'expected #{this} not to be null' + ); + }); + + /** + * ### .undefined + * + * Asserts that the target is `undefined`. + * + * expect(undefined).to.be.undefined; + * expect(null).to.not.be.undefined; + * + * @name undefined + * @api public + */ + + Assertion.addProperty('undefined', function () { + this.assert( + undefined === flag(this, 'object') + , 'expected #{this} to be undefined' + , 'expected #{this} not to be undefined' + ); + }); + + /** + * ### .exist + * + * Asserts that the target is neither `null` nor `undefined`. + * + * var foo = 'hi' + * , bar = null + * , baz; + * + * expect(foo).to.exist; + * expect(bar).to.not.exist; + * expect(baz).to.not.exist; + * + * @name exist + * @api public + */ + + Assertion.addProperty('exist', function () { + this.assert( + null != flag(this, 'object') + , 'expected #{this} to exist' + , 'expected #{this} to not exist' + ); + }); + + + /** + * ### .empty + * + * Asserts that the target's length is `0`. For arrays, it checks + * the `length` property. For objects, it gets the count of + * enumerable keys. + * + * expect([]).to.be.empty; + * expect('').to.be.empty; + * expect({}).to.be.empty; + * + * @name empty + * @api public + */ + + Assertion.addProperty('empty', function () { + var obj = flag(this, 'object') + , expected = obj; + + if (Array.isArray(obj) || 'string' === typeof object) { + expected = obj.length; + } else if (typeof obj === 'object') { + expected = Object.keys(obj).length; + } + + this.assert( + !expected + , 'expected #{this} to be empty' + , 'expected #{this} not to be empty' + ); + }); + + /** + * ### .arguments + * + * Asserts that the target is an arguments object. + * + * function test () { + * expect(arguments).to.be.arguments; + * } + * + * @name arguments + * @alias Arguments + * @api public + */ + + function checkArguments () { + var obj = flag(this, 'object') + , type = Object.prototype.toString.call(obj); + this.assert( + '[object Arguments]' === type + , 'expected #{this} to be arguments but got ' + type + , 'expected #{this} to not be arguments' + ); + } + + Assertion.addProperty('arguments', checkArguments); + Assertion.addProperty('Arguments', checkArguments); + + /** + * ### .equal(value) + * + * Asserts that the target is strictly equal (`===`) to `value`. + * Alternately, if the `deep` flag is set, asserts that + * the target is deeply equal to `value`. + * + * expect('hello').to.equal('hello'); + * expect(42).to.equal(42); + * expect(1).to.not.equal(true); + * expect({ foo: 'bar' }).to.not.equal({ foo: 'bar' }); + * expect({ foo: 'bar' }).to.deep.equal({ foo: 'bar' }); + * + * @name equal + * @alias equals + * @alias eq + * @alias deep.equal + * @param {Mixed} value + * @param {String} message _optional_ + * @api public + */ + + function assertEqual (val, msg) { + if (msg) flag(this, 'message', msg); + var obj = flag(this, 'object'); + if (flag(this, 'deep')) { + return this.eql(val); + } else { + this.assert( + val === obj + , 'expected #{this} to equal #{exp}' + , 'expected #{this} to not equal #{exp}' + , val + , this._obj + , true + ); + } + } + + Assertion.addMethod('equal', assertEqual); + Assertion.addMethod('equals', assertEqual); + Assertion.addMethod('eq', assertEqual); + + /** + * ### .eql(value) + * + * Asserts that the target is deeply equal to `value`. + * + * expect({ foo: 'bar' }).to.eql({ foo: 'bar' }); + * expect([ 1, 2, 3 ]).to.eql([ 1, 2, 3 ]); + * + * @name eql + * @alias eqls + * @param {Mixed} value + * @param {String} message _optional_ + * @api public + */ + + function assertEql(obj, msg) { + if (msg) flag(this, 'message', msg); + this.assert( + _.eql(obj, flag(this, 'object')) + , 'expected #{this} to deeply equal #{exp}' + , 'expected #{this} to not deeply equal #{exp}' + , obj + , this._obj + , true + ); + } + + Assertion.addMethod('eql', assertEql); + Assertion.addMethod('eqls', assertEql); + + /** + * ### .above(value) + * + * Asserts that the target is greater than `value`. + * + * expect(10).to.be.above(5); + * + * Can also be used in conjunction with `length` to + * assert a minimum length. The benefit being a + * more informative error message than if the length + * was supplied directly. + * + * expect('foo').to.have.length.above(2); + * expect([ 1, 2, 3 ]).to.have.length.above(2); + * + * @name above + * @alias gt + * @alias greaterThan + * @param {Number} value + * @param {String} message _optional_ + * @api public + */ + + function assertAbove (n, msg) { + if (msg) flag(this, 'message', msg); + var obj = flag(this, 'object'); + if (flag(this, 'doLength')) { + new Assertion(obj, msg).to.have.property('length'); + var len = obj.length; + this.assert( + len > n + , 'expected #{this} to have a length above #{exp} but got #{act}' + , 'expected #{this} to not have a length above #{exp}' + , n + , len + ); + } else { + this.assert( + obj > n + , 'expected #{this} to be above ' + n + , 'expected #{this} to be at most ' + n + ); + } + } + + Assertion.addMethod('above', assertAbove); + Assertion.addMethod('gt', assertAbove); + Assertion.addMethod('greaterThan', assertAbove); + + /** + * ### .least(value) + * + * Asserts that the target is greater than or equal to `value`. + * + * expect(10).to.be.at.least(10); + * + * Can also be used in conjunction with `length` to + * assert a minimum length. The benefit being a + * more informative error message than if the length + * was supplied directly. + * + * expect('foo').to.have.length.of.at.least(2); + * expect([ 1, 2, 3 ]).to.have.length.of.at.least(3); + * + * @name least + * @alias gte + * @param {Number} value + * @param {String} message _optional_ + * @api public + */ + + function assertLeast (n, msg) { + if (msg) flag(this, 'message', msg); + var obj = flag(this, 'object'); + if (flag(this, 'doLength')) { + new Assertion(obj, msg).to.have.property('length'); + var len = obj.length; + this.assert( + len >= n + , 'expected #{this} to have a length at least #{exp} but got #{act}' + , 'expected #{this} to have a length below #{exp}' + , n + , len + ); + } else { + this.assert( + obj >= n + , 'expected #{this} to be at least ' + n + , 'expected #{this} to be below ' + n + ); + } + } + + Assertion.addMethod('least', assertLeast); + Assertion.addMethod('gte', assertLeast); + + /** + * ### .below(value) + * + * Asserts that the target is less than `value`. + * + * expect(5).to.be.below(10); + * + * Can also be used in conjunction with `length` to + * assert a maximum length. The benefit being a + * more informative error message than if the length + * was supplied directly. + * + * expect('foo').to.have.length.below(4); + * expect([ 1, 2, 3 ]).to.have.length.below(4); + * + * @name below + * @alias lt + * @alias lessThan + * @param {Number} value + * @param {String} message _optional_ + * @api public + */ + + function assertBelow (n, msg) { + if (msg) flag(this, 'message', msg); + var obj = flag(this, 'object'); + if (flag(this, 'doLength')) { + new Assertion(obj, msg).to.have.property('length'); + var len = obj.length; + this.assert( + len < n + , 'expected #{this} to have a length below #{exp} but got #{act}' + , 'expected #{this} to not have a length below #{exp}' + , n + , len + ); + } else { + this.assert( + obj < n + , 'expected #{this} to be below ' + n + , 'expected #{this} to be at least ' + n + ); + } + } + + Assertion.addMethod('below', assertBelow); + Assertion.addMethod('lt', assertBelow); + Assertion.addMethod('lessThan', assertBelow); + + /** + * ### .most(value) + * + * Asserts that the target is less than or equal to `value`. + * + * expect(5).to.be.at.most(5); + * + * Can also be used in conjunction with `length` to + * assert a maximum length. The benefit being a + * more informative error message than if the length + * was supplied directly. + * + * expect('foo').to.have.length.of.at.most(4); + * expect([ 1, 2, 3 ]).to.have.length.of.at.most(3); + * + * @name most + * @alias lte + * @param {Number} value + * @param {String} message _optional_ + * @api public + */ + + function assertMost (n, msg) { + if (msg) flag(this, 'message', msg); + var obj = flag(this, 'object'); + if (flag(this, 'doLength')) { + new Assertion(obj, msg).to.have.property('length'); + var len = obj.length; + this.assert( + len <= n + , 'expected #{this} to have a length at most #{exp} but got #{act}' + , 'expected #{this} to have a length above #{exp}' + , n + , len + ); + } else { + this.assert( + obj <= n + , 'expected #{this} to be at most ' + n + , 'expected #{this} to be above ' + n + ); + } + } + + Assertion.addMethod('most', assertMost); + Assertion.addMethod('lte', assertMost); + + /** + * ### .within(start, finish) + * + * Asserts that the target is within a range. + * + * expect(7).to.be.within(5,10); + * + * Can also be used in conjunction with `length` to + * assert a length range. The benefit being a + * more informative error message than if the length + * was supplied directly. + * + * expect('foo').to.have.length.within(2,4); + * expect([ 1, 2, 3 ]).to.have.length.within(2,4); + * + * @name within + * @param {Number} start lowerbound inclusive + * @param {Number} finish upperbound inclusive + * @param {String} message _optional_ + * @api public + */ + + Assertion.addMethod('within', function (start, finish, msg) { + if (msg) flag(this, 'message', msg); + var obj = flag(this, 'object') + , range = start + '..' + finish; + if (flag(this, 'doLength')) { + new Assertion(obj, msg).to.have.property('length'); + var len = obj.length; + this.assert( + len >= start && len <= finish + , 'expected #{this} to have a length within ' + range + , 'expected #{this} to not have a length within ' + range + ); + } else { + this.assert( + obj >= start && obj <= finish + , 'expected #{this} to be within ' + range + , 'expected #{this} to not be within ' + range + ); + } + }); + + /** + * ### .instanceof(constructor) + * + * Asserts that the target is an instance of `constructor`. + * + * var Tea = function (name) { this.name = name; } + * , Chai = new Tea('chai'); + * + * expect(Chai).to.be.an.instanceof(Tea); + * expect([ 1, 2, 3 ]).to.be.instanceof(Array); + * + * @name instanceof + * @param {Constructor} constructor + * @param {String} message _optional_ + * @alias instanceOf + * @api public + */ + + function assertInstanceOf (constructor, msg) { + if (msg) flag(this, 'message', msg); + var name = _.getName(constructor); + this.assert( + flag(this, 'object') instanceof constructor + , 'expected #{this} to be an instance of ' + name + , 'expected #{this} to not be an instance of ' + name + ); + }; + + Assertion.addMethod('instanceof', assertInstanceOf); + Assertion.addMethod('instanceOf', assertInstanceOf); + + /** + * ### .property(name, [value]) + * + * Asserts that the target has a property `name`, optionally asserting that + * the value of that property is strictly equal to `value`. + * If the `deep` flag is set, you can use dot- and bracket-notation for deep + * references into objects and arrays. + * + * // simple referencing + * var obj = { foo: 'bar' }; + * expect(obj).to.have.property('foo'); + * expect(obj).to.have.property('foo', 'bar'); + * + * // deep referencing + * var deepObj = { + * green: { tea: 'matcha' } + * , teas: [ 'chai', 'matcha', { tea: 'konacha' } ] + * }; + + * expect(deepObj).to.have.deep.property('green.tea', 'matcha'); + * expect(deepObj).to.have.deep.property('teas[1]', 'matcha'); + * expect(deepObj).to.have.deep.property('teas[2].tea', 'konacha'); + * + * You can also use an array as the starting point of a `deep.property` + * assertion, or traverse nested arrays. + * + * var arr = [ + * [ 'chai', 'matcha', 'konacha' ] + * , [ { tea: 'chai' } + * , { tea: 'matcha' } + * , { tea: 'konacha' } ] + * ]; + * + * expect(arr).to.have.deep.property('[0][1]', 'matcha'); + * expect(arr).to.have.deep.property('[1][2].tea', 'konacha'); + * + * Furthermore, `property` changes the subject of the assertion + * to be the value of that property from the original object. This + * permits for further chainable assertions on that property. + * + * expect(obj).to.have.property('foo') + * .that.is.a('string'); + * expect(deepObj).to.have.property('green') + * .that.is.an('object') + * .that.deep.equals({ tea: 'matcha' }); + * expect(deepObj).to.have.property('teas') + * .that.is.an('array') + * .with.deep.property('[2]') + * .that.deep.equals({ tea: 'konacha' }); + * + * @name property + * @alias deep.property + * @param {String} name + * @param {Mixed} value (optional) + * @param {String} message _optional_ + * @returns value of property for chaining + * @api public + */ + + Assertion.addMethod('property', function (name, val, msg) { + if (msg) flag(this, 'message', msg); + + var descriptor = flag(this, 'deep') ? 'deep property ' : 'property ' + , negate = flag(this, 'negate') + , obj = flag(this, 'object') + , value = flag(this, 'deep') + ? _.getPathValue(name, obj) + : obj[name]; + + if (negate && undefined !== val) { + if (undefined === value) { + msg = (msg != null) ? msg + ': ' : ''; + throw new Error(msg + _.inspect(obj) + ' has no ' + descriptor + _.inspect(name)); + } + } else { + this.assert( + undefined !== value + , 'expected #{this} to have a ' + descriptor + _.inspect(name) + , 'expected #{this} to not have ' + descriptor + _.inspect(name)); + } + + if (undefined !== val) { + this.assert( + val === value + , 'expected #{this} to have a ' + descriptor + _.inspect(name) + ' of #{exp}, but got #{act}' + , 'expected #{this} to not have a ' + descriptor + _.inspect(name) + ' of #{act}' + , val + , value + ); + } + + flag(this, 'object', value); + }); + + + /** + * ### .ownProperty(name) + * + * Asserts that the target has an own property `name`. + * + * expect('test').to.have.ownProperty('length'); + * + * @name ownProperty + * @alias haveOwnProperty + * @param {String} name + * @param {String} message _optional_ + * @api public + */ + + function assertOwnProperty (name, msg) { + if (msg) flag(this, 'message', msg); + var obj = flag(this, 'object'); + this.assert( + obj.hasOwnProperty(name) + , 'expected #{this} to have own property ' + _.inspect(name) + , 'expected #{this} to not have own property ' + _.inspect(name) + ); + } + + Assertion.addMethod('ownProperty', assertOwnProperty); + Assertion.addMethod('haveOwnProperty', assertOwnProperty); + + /** + * ### .length(value) + * + * Asserts that the target's `length` property has + * the expected value. + * + * expect([ 1, 2, 3]).to.have.length(3); + * expect('foobar').to.have.length(6); + * + * Can also be used as a chain precursor to a value + * comparison for the length property. + * + * expect('foo').to.have.length.above(2); + * expect([ 1, 2, 3 ]).to.have.length.above(2); + * expect('foo').to.have.length.below(4); + * expect([ 1, 2, 3 ]).to.have.length.below(4); + * expect('foo').to.have.length.within(2,4); + * expect([ 1, 2, 3 ]).to.have.length.within(2,4); + * + * @name length + * @alias lengthOf + * @param {Number} length + * @param {String} message _optional_ + * @api public + */ + + function assertLengthChain () { + flag(this, 'doLength', true); + } + + function assertLength (n, msg) { + if (msg) flag(this, 'message', msg); + var obj = flag(this, 'object'); + new Assertion(obj, msg).to.have.property('length'); + var len = obj.length; + + this.assert( + len == n + , 'expected #{this} to have a length of #{exp} but got #{act}' + , 'expected #{this} to not have a length of #{act}' + , n + , len + ); + } + + Assertion.addChainableMethod('length', assertLength, assertLengthChain); + Assertion.addMethod('lengthOf', assertLength, assertLengthChain); + + /** + * ### .match(regexp) + * + * Asserts that the target matches a regular expression. + * + * expect('foobar').to.match(/^foo/); + * + * @name match + * @param {RegExp} RegularExpression + * @param {String} message _optional_ + * @api public + */ + + Assertion.addMethod('match', function (re, msg) { + if (msg) flag(this, 'message', msg); + var obj = flag(this, 'object'); + this.assert( + re.exec(obj) + , 'expected #{this} to match ' + re + , 'expected #{this} not to match ' + re + ); + }); + + /** + * ### .string(string) + * + * Asserts that the string target contains another string. + * + * expect('foobar').to.have.string('bar'); + * + * @name string + * @param {String} string + * @param {String} message _optional_ + * @api public + */ + + Assertion.addMethod('string', function (str, msg) { + if (msg) flag(this, 'message', msg); + var obj = flag(this, 'object'); + new Assertion(obj, msg).is.a('string'); + + this.assert( + ~obj.indexOf(str) + , 'expected #{this} to contain ' + _.inspect(str) + , 'expected #{this} to not contain ' + _.inspect(str) + ); + }); + + + /** + * ### .keys(key1, [key2], [...]) + * + * Asserts that the target has exactly the given keys, or + * asserts the inclusion of some keys when using the + * `include` or `contain` modifiers. + * + * expect({ foo: 1, bar: 2 }).to.have.keys(['foo', 'bar']); + * expect({ foo: 1, bar: 2, baz: 3 }).to.contain.keys('foo', 'bar'); + * + * @name keys + * @alias key + * @param {String...|Array} keys + * @api public + */ + + function assertKeys (keys) { + var obj = flag(this, 'object') + , str + , ok = true; + + keys = keys instanceof Array + ? keys + : Array.prototype.slice.call(arguments); + + if (!keys.length) throw new Error('keys required'); + + var actual = Object.keys(obj) + , len = keys.length; + + // Inclusion + ok = keys.every(function(key){ + return ~actual.indexOf(key); + }); + + // Strict + if (!flag(this, 'negate') && !flag(this, 'contains')) { + ok = ok && keys.length == actual.length; + } + + // Key string + if (len > 1) { + keys = keys.map(function(key){ + return _.inspect(key); + }); + var last = keys.pop(); + str = keys.join(', ') + ', and ' + last; + } else { + str = _.inspect(keys[0]); + } + + // Form + str = (len > 1 ? 'keys ' : 'key ') + str; + + // Have / include + str = (flag(this, 'contains') ? 'contain ' : 'have ') + str; + + // Assertion + this.assert( + ok + , 'expected #{this} to ' + str + , 'expected #{this} to not ' + str + ); + } + + Assertion.addMethod('keys', assertKeys); + Assertion.addMethod('key', assertKeys); + + /** + * ### .throw(constructor) + * + * Asserts that the function target will throw a specific error, or specific type of error + * (as determined using `instanceof`), optionally with a RegExp or string inclusion test + * for the error's message. + * + * var err = new ReferenceError('This is a bad function.'); + * var fn = function () { throw err; } + * expect(fn).to.throw(ReferenceError); + * expect(fn).to.throw(Error); + * expect(fn).to.throw(/bad function/); + * expect(fn).to.not.throw('good function'); + * expect(fn).to.throw(ReferenceError, /bad function/); + * expect(fn).to.throw(err); + * expect(fn).to.not.throw(new RangeError('Out of range.')); + * + * Please note that when a throw expectation is negated, it will check each + * parameter independently, starting with error constructor type. The appropriate way + * to check for the existence of a type of error but for a message that does not match + * is to use `and`. + * + * expect(fn).to.throw(ReferenceError) + * .and.not.throw(/good function/); + * + * @name throw + * @alias throws + * @alias Throw + * @param {ErrorConstructor} constructor + * @param {String|RegExp} expected error message + * @param {String} message _optional_ + * @see https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Error#Error_types + * @returns error for chaining (null if no error) + * @api public + */ + + function assertThrows (constructor, errMsg, msg) { + if (msg) flag(this, 'message', msg); + var obj = flag(this, 'object'); + new Assertion(obj, msg).is.a('function'); + + var thrown = false + , desiredError = null + , name = null + , thrownError = null; + + if (arguments.length === 0) { + errMsg = null; + constructor = null; + } else if (constructor && (constructor instanceof RegExp || 'string' === typeof constructor)) { + errMsg = constructor; + constructor = null; + } else if (constructor && constructor instanceof Error) { + desiredError = constructor; + constructor = null; + errMsg = null; + } else if (typeof constructor === 'function') { + name = constructor.prototype.name || constructor.name; + if (name === 'Error' && constructor !== Error) { + name = (new constructor()).name; + } + } else { + constructor = null; + } + + try { + obj(); + } catch (err) { + // first, check desired error + if (desiredError) { + this.assert( + err === desiredError + , 'expected #{this} to throw #{exp} but #{act} was thrown' + , 'expected #{this} to not throw #{exp}' + , (desiredError instanceof Error ? desiredError.toString() : desiredError) + , (err instanceof Error ? err.toString() : err) + ); + + flag(this, 'object', err); + return this; + } + + // next, check constructor + if (constructor) { + this.assert( + err instanceof constructor + , 'expected #{this} to throw #{exp} but #{act} was thrown' + , 'expected #{this} to not throw #{exp} but #{act} was thrown' + , name + , (err instanceof Error ? err.toString() : err) + ); + + if (!errMsg) { + flag(this, 'object', err); + return this; + } + } + + // next, check message + var message = 'object' === _.type(err) && "message" in err + ? err.message + : '' + err; + + if ((message != null) && errMsg && errMsg instanceof RegExp) { + this.assert( + errMsg.exec(message) + , 'expected #{this} to throw error matching #{exp} but got #{act}' + , 'expected #{this} to throw error not matching #{exp}' + , errMsg + , message + ); + + flag(this, 'object', err); + return this; + } else if ((message != null) && errMsg && 'string' === typeof errMsg) { + this.assert( + ~message.indexOf(errMsg) + , 'expected #{this} to throw error including #{exp} but got #{act}' + , 'expected #{this} to throw error not including #{act}' + , errMsg + , message + ); + + flag(this, 'object', err); + return this; + } else { + thrown = true; + thrownError = err; + } + } + + var actuallyGot = '' + , expectedThrown = name !== null + ? name + : desiredError + ? '#{exp}' //_.inspect(desiredError) + : 'an error'; + + if (thrown) { + actuallyGot = ' but #{act} was thrown' + } + + this.assert( + thrown === true + , 'expected #{this} to throw ' + expectedThrown + actuallyGot + , 'expected #{this} to not throw ' + expectedThrown + actuallyGot + , (desiredError instanceof Error ? desiredError.toString() : desiredError) + , (thrownError instanceof Error ? thrownError.toString() : thrownError) + ); + + flag(this, 'object', thrownError); + }; + + Assertion.addMethod('throw', assertThrows); + Assertion.addMethod('throws', assertThrows); + Assertion.addMethod('Throw', assertThrows); + + /** + * ### .respondTo(method) + * + * Asserts that the object or class target will respond to a method. + * + * Klass.prototype.bar = function(){}; + * expect(Klass).to.respondTo('bar'); + * expect(obj).to.respondTo('bar'); + * + * To check if a constructor will respond to a static function, + * set the `itself` flag. + * + * Klass.baz = function(){}; + * expect(Klass).itself.to.respondTo('baz'); + * + * @name respondTo + * @param {String} method + * @param {String} message _optional_ + * @api public + */ + + Assertion.addMethod('respondTo', function (method, msg) { + if (msg) flag(this, 'message', msg); + var obj = flag(this, 'object') + , itself = flag(this, 'itself') + , context = ('function' === _.type(obj) && !itself) + ? obj.prototype[method] + : obj[method]; + + this.assert( + 'function' === typeof context + , 'expected #{this} to respond to ' + _.inspect(method) + , 'expected #{this} to not respond to ' + _.inspect(method) + ); + }); + + /** + * ### .itself + * + * Sets the `itself` flag, later used by the `respondTo` assertion. + * + * function Foo() {} + * Foo.bar = function() {} + * Foo.prototype.baz = function() {} + * + * expect(Foo).itself.to.respondTo('bar'); + * expect(Foo).itself.not.to.respondTo('baz'); + * + * @name itself + * @api public + */ + + Assertion.addProperty('itself', function () { + flag(this, 'itself', true); + }); + + /** + * ### .satisfy(method) + * + * Asserts that the target passes a given truth test. + * + * expect(1).to.satisfy(function(num) { return num > 0; }); + * + * @name satisfy + * @param {Function} matcher + * @param {String} message _optional_ + * @api public + */ + + Assertion.addMethod('satisfy', function (matcher, msg) { + if (msg) flag(this, 'message', msg); + var obj = flag(this, 'object'); + this.assert( + matcher(obj) + , 'expected #{this} to satisfy ' + _.objDisplay(matcher) + , 'expected #{this} to not satisfy' + _.objDisplay(matcher) + , this.negate ? false : true + , matcher(obj) + ); + }); + + /** + * ### .closeTo(expected, delta) + * + * Asserts that the target is equal `expected`, to within a +/- `delta` range. + * + * expect(1.5).to.be.closeTo(1, 0.5); + * + * @name closeTo + * @param {Number} expected + * @param {Number} delta + * @param {String} message _optional_ + * @api public + */ + + Assertion.addMethod('closeTo', function (expected, delta, msg) { + if (msg) flag(this, 'message', msg); + var obj = flag(this, 'object'); + this.assert( + Math.abs(obj - expected) <= delta + , 'expected #{this} to be close to ' + expected + ' +/- ' + delta + , 'expected #{this} not to be close to ' + expected + ' +/- ' + delta + ); + }); + + function isSubsetOf(subset, superset, cmp) { + return subset.every(function(elem) { + if (!cmp) return superset.indexOf(elem) !== -1; + + return superset.some(function(elem2) { + return cmp(elem, elem2); + }); + }) + } + + /** + * ### .members(set) + * + * Asserts that the target is a superset of `set`, + * or that the target and `set` have the same strictly-equal (===) members. + * Alternately, if the `deep` flag is set, set members are compared for deep + * equality. + * + * expect([1, 2, 3]).to.include.members([3, 2]); + * expect([1, 2, 3]).to.not.include.members([3, 2, 8]); + * + * expect([4, 2]).to.have.members([2, 4]); + * expect([5, 2]).to.not.have.members([5, 2, 1]); + * + * expect([{ id: 1 }]).to.deep.include.members([{ id: 1 }]); + * + * @name members + * @param {Array} set + * @param {String} message _optional_ + * @api public + */ + + Assertion.addMethod('members', function (subset, msg) { + if (msg) flag(this, 'message', msg); + var obj = flag(this, 'object'); + + new Assertion(obj).to.be.an('array'); + new Assertion(subset).to.be.an('array'); + + var cmp = flag(this, 'deep') ? _.eql : undefined; + + if (flag(this, 'contains')) { + return this.assert( + isSubsetOf(subset, obj, cmp) + , 'expected #{this} to be a superset of #{act}' + , 'expected #{this} to not be a superset of #{act}' + , obj + , subset + ); + } + + this.assert( + isSubsetOf(obj, subset, cmp) && isSubsetOf(subset, obj, cmp) + , 'expected #{this} to have the same members as #{act}' + , 'expected #{this} to not have the same members as #{act}' + , obj + , subset + ); + }); +}; + +}); +require.register("chai/lib/chai/interface/assert.js", function(exports, require, module){ +/*! + * chai + * Copyright(c) 2011-2014 Jake Luer + * MIT Licensed + */ + + +module.exports = function (chai, util) { + + /*! + * Chai dependencies. + */ + + var Assertion = chai.Assertion + , flag = util.flag; + + /*! + * Module export. + */ + + /** + * ### assert(expression, message) + * + * Write your own test expressions. + * + * assert('foo' !== 'bar', 'foo is not bar'); + * assert(Array.isArray([]), 'empty arrays are arrays'); + * + * @param {Mixed} expression to test for truthiness + * @param {String} message to display on error + * @name assert + * @api public + */ + + var assert = chai.assert = function (express, errmsg) { + var test = new Assertion(null, null, chai.assert); + test.assert( + express + , errmsg + , '[ negation message unavailable ]' + ); + }; + + /** + * ### .fail(actual, expected, [message], [operator]) + * + * Throw a failure. Node.js `assert` module-compatible. + * + * @name fail + * @param {Mixed} actual + * @param {Mixed} expected + * @param {String} message + * @param {String} operator + * @api public + */ + + assert.fail = function (actual, expected, message, operator) { + message = message || 'assert.fail()'; + throw new chai.AssertionError(message, { + actual: actual + , expected: expected + , operator: operator + }, assert.fail); + }; + + /** + * ### .ok(object, [message]) + * + * Asserts that `object` is truthy. + * + * assert.ok('everything', 'everything is ok'); + * assert.ok(false, 'this will fail'); + * + * @name ok + * @param {Mixed} object to test + * @param {String} message + * @api public + */ + + assert.ok = function (val, msg) { + new Assertion(val, msg).is.ok; + }; + + /** + * ### .notOk(object, [message]) + * + * Asserts that `object` is falsy. + * + * assert.notOk('everything', 'this will fail'); + * assert.notOk(false, 'this will pass'); + * + * @name notOk + * @param {Mixed} object to test + * @param {String} message + * @api public + */ + + assert.notOk = function (val, msg) { + new Assertion(val, msg).is.not.ok; + }; + + /** + * ### .equal(actual, expected, [message]) + * + * Asserts non-strict equality (`==`) of `actual` and `expected`. + * + * assert.equal(3, '3', '== coerces values to strings'); + * + * @name equal + * @param {Mixed} actual + * @param {Mixed} expected + * @param {String} message + * @api public + */ + + assert.equal = function (act, exp, msg) { + var test = new Assertion(act, msg, assert.equal); + + test.assert( + exp == flag(test, 'object') + , 'expected #{this} to equal #{exp}' + , 'expected #{this} to not equal #{act}' + , exp + , act + ); + }; + + /** + * ### .notEqual(actual, expected, [message]) + * + * Asserts non-strict inequality (`!=`) of `actual` and `expected`. + * + * assert.notEqual(3, 4, 'these numbers are not equal'); + * + * @name notEqual + * @param {Mixed} actual + * @param {Mixed} expected + * @param {String} message + * @api public + */ + + assert.notEqual = function (act, exp, msg) { + var test = new Assertion(act, msg, assert.notEqual); + + test.assert( + exp != flag(test, 'object') + , 'expected #{this} to not equal #{exp}' + , 'expected #{this} to equal #{act}' + , exp + , act + ); + }; + + /** + * ### .strictEqual(actual, expected, [message]) + * + * Asserts strict equality (`===`) of `actual` and `expected`. + * + * assert.strictEqual(true, true, 'these booleans are strictly equal'); + * + * @name strictEqual + * @param {Mixed} actual + * @param {Mixed} expected + * @param {String} message + * @api public + */ + + assert.strictEqual = function (act, exp, msg) { + new Assertion(act, msg).to.equal(exp); + }; + + /** + * ### .notStrictEqual(actual, expected, [message]) + * + * Asserts strict inequality (`!==`) of `actual` and `expected`. + * + * assert.notStrictEqual(3, '3', 'no coercion for strict equality'); + * + * @name notStrictEqual + * @param {Mixed} actual + * @param {Mixed} expected + * @param {String} message + * @api public + */ + + assert.notStrictEqual = function (act, exp, msg) { + new Assertion(act, msg).to.not.equal(exp); + }; + + /** + * ### .deepEqual(actual, expected, [message]) + * + * Asserts that `actual` is deeply equal to `expected`. + * + * assert.deepEqual({ tea: 'green' }, { tea: 'green' }); + * + * @name deepEqual + * @param {Mixed} actual + * @param {Mixed} expected + * @param {String} message + * @api public + */ + + assert.deepEqual = function (act, exp, msg) { + new Assertion(act, msg).to.eql(exp); + }; + + /** + * ### .notDeepEqual(actual, expected, [message]) + * + * Assert that `actual` is not deeply equal to `expected`. + * + * assert.notDeepEqual({ tea: 'green' }, { tea: 'jasmine' }); + * + * @name notDeepEqual + * @param {Mixed} actual + * @param {Mixed} expected + * @param {String} message + * @api public + */ + + assert.notDeepEqual = function (act, exp, msg) { + new Assertion(act, msg).to.not.eql(exp); + }; + + /** + * ### .isTrue(value, [message]) + * + * Asserts that `value` is true. + * + * var teaServed = true; + * assert.isTrue(teaServed, 'the tea has been served'); + * + * @name isTrue + * @param {Mixed} value + * @param {String} message + * @api public + */ + + assert.isTrue = function (val, msg) { + new Assertion(val, msg).is['true']; + }; + + /** + * ### .isFalse(value, [message]) + * + * Asserts that `value` is false. + * + * var teaServed = false; + * assert.isFalse(teaServed, 'no tea yet? hmm...'); + * + * @name isFalse + * @param {Mixed} value + * @param {String} message + * @api public + */ + + assert.isFalse = function (val, msg) { + new Assertion(val, msg).is['false']; + }; + + /** + * ### .isNull(value, [message]) + * + * Asserts that `value` is null. + * + * assert.isNull(err, 'there was no error'); + * + * @name isNull + * @param {Mixed} value + * @param {String} message + * @api public + */ + + assert.isNull = function (val, msg) { + new Assertion(val, msg).to.equal(null); + }; + + /** + * ### .isNotNull(value, [message]) + * + * Asserts that `value` is not null. + * + * var tea = 'tasty chai'; + * assert.isNotNull(tea, 'great, time for tea!'); + * + * @name isNotNull + * @param {Mixed} value + * @param {String} message + * @api public + */ + + assert.isNotNull = function (val, msg) { + new Assertion(val, msg).to.not.equal(null); + }; + + /** + * ### .isUndefined(value, [message]) + * + * Asserts that `value` is `undefined`. + * + * var tea; + * assert.isUndefined(tea, 'no tea defined'); + * + * @name isUndefined + * @param {Mixed} value + * @param {String} message + * @api public + */ + + assert.isUndefined = function (val, msg) { + new Assertion(val, msg).to.equal(undefined); + }; + + /** + * ### .isDefined(value, [message]) + * + * Asserts that `value` is not `undefined`. + * + * var tea = 'cup of chai'; + * assert.isDefined(tea, 'tea has been defined'); + * + * @name isDefined + * @param {Mixed} value + * @param {String} message + * @api public + */ + + assert.isDefined = function (val, msg) { + new Assertion(val, msg).to.not.equal(undefined); + }; + + /** + * ### .isFunction(value, [message]) + * + * Asserts that `value` is a function. + * + * function serveTea() { return 'cup of tea'; }; + * assert.isFunction(serveTea, 'great, we can have tea now'); + * + * @name isFunction + * @param {Mixed} value + * @param {String} message + * @api public + */ + + assert.isFunction = function (val, msg) { + new Assertion(val, msg).to.be.a('function'); + }; + + /** + * ### .isNotFunction(value, [message]) + * + * Asserts that `value` is _not_ a function. + * + * var serveTea = [ 'heat', 'pour', 'sip' ]; + * assert.isNotFunction(serveTea, 'great, we have listed the steps'); + * + * @name isNotFunction + * @param {Mixed} value + * @param {String} message + * @api public + */ + + assert.isNotFunction = function (val, msg) { + new Assertion(val, msg).to.not.be.a('function'); + }; + + /** + * ### .isObject(value, [message]) + * + * Asserts that `value` is an object (as revealed by + * `Object.prototype.toString`). + * + * var selection = { name: 'Chai', serve: 'with spices' }; + * assert.isObject(selection, 'tea selection is an object'); + * + * @name isObject + * @param {Mixed} value + * @param {String} message + * @api public + */ + + assert.isObject = function (val, msg) { + new Assertion(val, msg).to.be.a('object'); + }; + + /** + * ### .isNotObject(value, [message]) + * + * Asserts that `value` is _not_ an object. + * + * var selection = 'chai' + * assert.isNotObject(selection, 'tea selection is not an object'); + * assert.isNotObject(null, 'null is not an object'); + * + * @name isNotObject + * @param {Mixed} value + * @param {String} message + * @api public + */ + + assert.isNotObject = function (val, msg) { + new Assertion(val, msg).to.not.be.a('object'); + }; + + /** + * ### .isArray(value, [message]) + * + * Asserts that `value` is an array. + * + * var menu = [ 'green', 'chai', 'oolong' ]; + * assert.isArray(menu, 'what kind of tea do we want?'); + * + * @name isArray + * @param {Mixed} value + * @param {String} message + * @api public + */ + + assert.isArray = function (val, msg) { + new Assertion(val, msg).to.be.an('array'); + }; + + /** + * ### .isNotArray(value, [message]) + * + * Asserts that `value` is _not_ an array. + * + * var menu = 'green|chai|oolong'; + * assert.isNotArray(menu, 'what kind of tea do we want?'); + * + * @name isNotArray + * @param {Mixed} value + * @param {String} message + * @api public + */ + + assert.isNotArray = function (val, msg) { + new Assertion(val, msg).to.not.be.an('array'); + }; + + /** + * ### .isString(value, [message]) + * + * Asserts that `value` is a string. + * + * var teaOrder = 'chai'; + * assert.isString(teaOrder, 'order placed'); + * + * @name isString + * @param {Mixed} value + * @param {String} message + * @api public + */ + + assert.isString = function (val, msg) { + new Assertion(val, msg).to.be.a('string'); + }; + + /** + * ### .isNotString(value, [message]) + * + * Asserts that `value` is _not_ a string. + * + * var teaOrder = 4; + * assert.isNotString(teaOrder, 'order placed'); + * + * @name isNotString + * @param {Mixed} value + * @param {String} message + * @api public + */ + + assert.isNotString = function (val, msg) { + new Assertion(val, msg).to.not.be.a('string'); + }; + + /** + * ### .isNumber(value, [message]) + * + * Asserts that `value` is a number. + * + * var cups = 2; + * assert.isNumber(cups, 'how many cups'); + * + * @name isNumber + * @param {Number} value + * @param {String} message + * @api public + */ + + assert.isNumber = function (val, msg) { + new Assertion(val, msg).to.be.a('number'); + }; + + /** + * ### .isNotNumber(value, [message]) + * + * Asserts that `value` is _not_ a number. + * + * var cups = '2 cups please'; + * assert.isNotNumber(cups, 'how many cups'); + * + * @name isNotNumber + * @param {Mixed} value + * @param {String} message + * @api public + */ + + assert.isNotNumber = function (val, msg) { + new Assertion(val, msg).to.not.be.a('number'); + }; + + /** + * ### .isBoolean(value, [message]) + * + * Asserts that `value` is a boolean. + * + * var teaReady = true + * , teaServed = false; + * + * assert.isBoolean(teaReady, 'is the tea ready'); + * assert.isBoolean(teaServed, 'has tea been served'); + * + * @name isBoolean + * @param {Mixed} value + * @param {String} message + * @api public + */ + + assert.isBoolean = function (val, msg) { + new Assertion(val, msg).to.be.a('boolean'); + }; + + /** + * ### .isNotBoolean(value, [message]) + * + * Asserts that `value` is _not_ a boolean. + * + * var teaReady = 'yep' + * , teaServed = 'nope'; + * + * assert.isNotBoolean(teaReady, 'is the tea ready'); + * assert.isNotBoolean(teaServed, 'has tea been served'); + * + * @name isNotBoolean + * @param {Mixed} value + * @param {String} message + * @api public + */ + + assert.isNotBoolean = function (val, msg) { + new Assertion(val, msg).to.not.be.a('boolean'); + }; + + /** + * ### .typeOf(value, name, [message]) + * + * Asserts that `value`'s type is `name`, as determined by + * `Object.prototype.toString`. + * + * assert.typeOf({ tea: 'chai' }, 'object', 'we have an object'); + * assert.typeOf(['chai', 'jasmine'], 'array', 'we have an array'); + * assert.typeOf('tea', 'string', 'we have a string'); + * assert.typeOf(/tea/, 'regexp', 'we have a regular expression'); + * assert.typeOf(null, 'null', 'we have a null'); + * assert.typeOf(undefined, 'undefined', 'we have an undefined'); + * + * @name typeOf + * @param {Mixed} value + * @param {String} name + * @param {String} message + * @api public + */ + + assert.typeOf = function (val, type, msg) { + new Assertion(val, msg).to.be.a(type); + }; + + /** + * ### .notTypeOf(value, name, [message]) + * + * Asserts that `value`'s type is _not_ `name`, as determined by + * `Object.prototype.toString`. + * + * assert.notTypeOf('tea', 'number', 'strings are not numbers'); + * + * @name notTypeOf + * @param {Mixed} value + * @param {String} typeof name + * @param {String} message + * @api public + */ + + assert.notTypeOf = function (val, type, msg) { + new Assertion(val, msg).to.not.be.a(type); + }; + + /** + * ### .instanceOf(object, constructor, [message]) + * + * Asserts that `value` is an instance of `constructor`. + * + * var Tea = function (name) { this.name = name; } + * , chai = new Tea('chai'); + * + * assert.instanceOf(chai, Tea, 'chai is an instance of tea'); + * + * @name instanceOf + * @param {Object} object + * @param {Constructor} constructor + * @param {String} message + * @api public + */ + + assert.instanceOf = function (val, type, msg) { + new Assertion(val, msg).to.be.instanceOf(type); + }; + + /** + * ### .notInstanceOf(object, constructor, [message]) + * + * Asserts `value` is not an instance of `constructor`. + * + * var Tea = function (name) { this.name = name; } + * , chai = new String('chai'); + * + * assert.notInstanceOf(chai, Tea, 'chai is not an instance of tea'); + * + * @name notInstanceOf + * @param {Object} object + * @param {Constructor} constructor + * @param {String} message + * @api public + */ + + assert.notInstanceOf = function (val, type, msg) { + new Assertion(val, msg).to.not.be.instanceOf(type); + }; + + /** + * ### .include(haystack, needle, [message]) + * + * Asserts that `haystack` includes `needle`. Works + * for strings and arrays. + * + * assert.include('foobar', 'bar', 'foobar contains string "bar"'); + * assert.include([ 1, 2, 3 ], 3, 'array contains value'); + * + * @name include + * @param {Array|String} haystack + * @param {Mixed} needle + * @param {String} message + * @api public + */ + + assert.include = function (exp, inc, msg) { + new Assertion(exp, msg, assert.include).include(inc); + }; + + /** + * ### .notInclude(haystack, needle, [message]) + * + * Asserts that `haystack` does not include `needle`. Works + * for strings and arrays. + *i + * assert.notInclude('foobar', 'baz', 'string not include substring'); + * assert.notInclude([ 1, 2, 3 ], 4, 'array not include contain value'); + * + * @name notInclude + * @param {Array|String} haystack + * @param {Mixed} needle + * @param {String} message + * @api public + */ + + assert.notInclude = function (exp, inc, msg) { + new Assertion(exp, msg, assert.notInclude).not.include(inc); + }; + + /** + * ### .match(value, regexp, [message]) + * + * Asserts that `value` matches the regular expression `regexp`. + * + * assert.match('foobar', /^foo/, 'regexp matches'); + * + * @name match + * @param {Mixed} value + * @param {RegExp} regexp + * @param {String} message + * @api public + */ + + assert.match = function (exp, re, msg) { + new Assertion(exp, msg).to.match(re); + }; + + /** + * ### .notMatch(value, regexp, [message]) + * + * Asserts that `value` does not match the regular expression `regexp`. + * + * assert.notMatch('foobar', /^foo/, 'regexp does not match'); + * + * @name notMatch + * @param {Mixed} value + * @param {RegExp} regexp + * @param {String} message + * @api public + */ + + assert.notMatch = function (exp, re, msg) { + new Assertion(exp, msg).to.not.match(re); + }; + + /** + * ### .property(object, property, [message]) + * + * Asserts that `object` has a property named by `property`. + * + * assert.property({ tea: { green: 'matcha' }}, 'tea'); + * + * @name property + * @param {Object} object + * @param {String} property + * @param {String} message + * @api public + */ + + assert.property = function (obj, prop, msg) { + new Assertion(obj, msg).to.have.property(prop); + }; + + /** + * ### .notProperty(object, property, [message]) + * + * Asserts that `object` does _not_ have a property named by `property`. + * + * assert.notProperty({ tea: { green: 'matcha' }}, 'coffee'); + * + * @name notProperty + * @param {Object} object + * @param {String} property + * @param {String} message + * @api public + */ + + assert.notProperty = function (obj, prop, msg) { + new Assertion(obj, msg).to.not.have.property(prop); + }; + + /** + * ### .deepProperty(object, property, [message]) + * + * Asserts that `object` has a property named by `property`, which can be a + * string using dot- and bracket-notation for deep reference. + * + * assert.deepProperty({ tea: { green: 'matcha' }}, 'tea.green'); + * + * @name deepProperty + * @param {Object} object + * @param {String} property + * @param {String} message + * @api public + */ + + assert.deepProperty = function (obj, prop, msg) { + new Assertion(obj, msg).to.have.deep.property(prop); + }; + + /** + * ### .notDeepProperty(object, property, [message]) + * + * Asserts that `object` does _not_ have a property named by `property`, which + * can be a string using dot- and bracket-notation for deep reference. + * + * assert.notDeepProperty({ tea: { green: 'matcha' }}, 'tea.oolong'); + * + * @name notDeepProperty + * @param {Object} object + * @param {String} property + * @param {String} message + * @api public + */ + + assert.notDeepProperty = function (obj, prop, msg) { + new Assertion(obj, msg).to.not.have.deep.property(prop); + }; + + /** + * ### .propertyVal(object, property, value, [message]) + * + * Asserts that `object` has a property named by `property` with value given + * by `value`. + * + * assert.propertyVal({ tea: 'is good' }, 'tea', 'is good'); + * + * @name propertyVal + * @param {Object} object + * @param {String} property + * @param {Mixed} value + * @param {String} message + * @api public + */ + + assert.propertyVal = function (obj, prop, val, msg) { + new Assertion(obj, msg).to.have.property(prop, val); + }; + + /** + * ### .propertyNotVal(object, property, value, [message]) + * + * Asserts that `object` has a property named by `property`, but with a value + * different from that given by `value`. + * + * assert.propertyNotVal({ tea: 'is good' }, 'tea', 'is bad'); + * + * @name propertyNotVal + * @param {Object} object + * @param {String} property + * @param {Mixed} value + * @param {String} message + * @api public + */ + + assert.propertyNotVal = function (obj, prop, val, msg) { + new Assertion(obj, msg).to.not.have.property(prop, val); + }; + + /** + * ### .deepPropertyVal(object, property, value, [message]) + * + * Asserts that `object` has a property named by `property` with value given + * by `value`. `property` can use dot- and bracket-notation for deep + * reference. + * + * assert.deepPropertyVal({ tea: { green: 'matcha' }}, 'tea.green', 'matcha'); + * + * @name deepPropertyVal + * @param {Object} object + * @param {String} property + * @param {Mixed} value + * @param {String} message + * @api public + */ + + assert.deepPropertyVal = function (obj, prop, val, msg) { + new Assertion(obj, msg).to.have.deep.property(prop, val); + }; + + /** + * ### .deepPropertyNotVal(object, property, value, [message]) + * + * Asserts that `object` has a property named by `property`, but with a value + * different from that given by `value`. `property` can use dot- and + * bracket-notation for deep reference. + * + * assert.deepPropertyNotVal({ tea: { green: 'matcha' }}, 'tea.green', 'konacha'); + * + * @name deepPropertyNotVal + * @param {Object} object + * @param {String} property + * @param {Mixed} value + * @param {String} message + * @api public + */ + + assert.deepPropertyNotVal = function (obj, prop, val, msg) { + new Assertion(obj, msg).to.not.have.deep.property(prop, val); + }; + + /** + * ### .lengthOf(object, length, [message]) + * + * Asserts that `object` has a `length` property with the expected value. + * + * assert.lengthOf([1,2,3], 3, 'array has length of 3'); + * assert.lengthOf('foobar', 5, 'string has length of 6'); + * + * @name lengthOf + * @param {Mixed} object + * @param {Number} length + * @param {String} message + * @api public + */ + + assert.lengthOf = function (exp, len, msg) { + new Assertion(exp, msg).to.have.length(len); + }; + + /** + * ### .throws(function, [constructor/string/regexp], [string/regexp], [message]) + * + * Asserts that `function` will throw an error that is an instance of + * `constructor`, or alternately that it will throw an error with message + * matching `regexp`. + * + * assert.throw(fn, 'function throws a reference error'); + * assert.throw(fn, /function throws a reference error/); + * assert.throw(fn, ReferenceError); + * assert.throw(fn, ReferenceError, 'function throws a reference error'); + * assert.throw(fn, ReferenceError, /function throws a reference error/); + * + * @name throws + * @alias throw + * @alias Throw + * @param {Function} function + * @param {ErrorConstructor} constructor + * @param {RegExp} regexp + * @param {String} message + * @see https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Error#Error_types + * @api public + */ + + assert.Throw = function (fn, errt, errs, msg) { + if ('string' === typeof errt || errt instanceof RegExp) { + errs = errt; + errt = null; + } + + var assertErr = new Assertion(fn, msg).to.Throw(errt, errs); + return flag(assertErr, 'object'); + }; + + /** + * ### .doesNotThrow(function, [constructor/regexp], [message]) + * + * Asserts that `function` will _not_ throw an error that is an instance of + * `constructor`, or alternately that it will not throw an error with message + * matching `regexp`. + * + * assert.doesNotThrow(fn, Error, 'function does not throw'); + * + * @name doesNotThrow + * @param {Function} function + * @param {ErrorConstructor} constructor + * @param {RegExp} regexp + * @param {String} message + * @see https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Error#Error_types + * @api public + */ + + assert.doesNotThrow = function (fn, type, msg) { + if ('string' === typeof type) { + msg = type; + type = null; + } + + new Assertion(fn, msg).to.not.Throw(type); + }; + + /** + * ### .operator(val1, operator, val2, [message]) + * + * Compares two values using `operator`. + * + * assert.operator(1, '<', 2, 'everything is ok'); + * assert.operator(1, '>', 2, 'this will fail'); + * + * @name operator + * @param {Mixed} val1 + * @param {String} operator + * @param {Mixed} val2 + * @param {String} message + * @api public + */ + + assert.operator = function (val, operator, val2, msg) { + if (!~['==', '===', '>', '>=', '<', '<=', '!=', '!=='].indexOf(operator)) { + throw new Error('Invalid operator "' + operator + '"'); + } + var test = new Assertion(eval(val + operator + val2), msg); + test.assert( + true === flag(test, 'object') + , 'expected ' + util.inspect(val) + ' to be ' + operator + ' ' + util.inspect(val2) + , 'expected ' + util.inspect(val) + ' to not be ' + operator + ' ' + util.inspect(val2) ); + }; + + /** + * ### .closeTo(actual, expected, delta, [message]) + * + * Asserts that the target is equal `expected`, to within a +/- `delta` range. + * + * assert.closeTo(1.5, 1, 0.5, 'numbers are close'); + * + * @name closeTo + * @param {Number} actual + * @param {Number} expected + * @param {Number} delta + * @param {String} message + * @api public + */ + + assert.closeTo = function (act, exp, delta, msg) { + new Assertion(act, msg).to.be.closeTo(exp, delta); + }; + + /** + * ### .sameMembers(set1, set2, [message]) + * + * Asserts that `set1` and `set2` have the same members. + * Order is not taken into account. + * + * assert.sameMembers([ 1, 2, 3 ], [ 2, 1, 3 ], 'same members'); + * + * @name sameMembers + * @param {Array} superset + * @param {Array} subset + * @param {String} message + * @api public + */ + + assert.sameMembers = function (set1, set2, msg) { + new Assertion(set1, msg).to.have.same.members(set2); + } + + /** + * ### .includeMembers(superset, subset, [message]) + * + * Asserts that `subset` is included in `superset`. + * Order is not taken into account. + * + * assert.includeMembers([ 1, 2, 3 ], [ 2, 1 ], 'include members'); + * + * @name includeMembers + * @param {Array} superset + * @param {Array} subset + * @param {String} message + * @api public + */ + + assert.includeMembers = function (superset, subset, msg) { + new Assertion(superset, msg).to.include.members(subset); + } + + /*! + * Undocumented / untested + */ + + assert.ifError = function (val, msg) { + new Assertion(val, msg).to.not.be.ok; + }; + + /*! + * Aliases. + */ + + (function alias(name, as){ + assert[as] = assert[name]; + return alias; + }) + ('Throw', 'throw') + ('Throw', 'throws'); +}; + +}); +require.register("chai/lib/chai/interface/expect.js", function(exports, require, module){ +/*! + * chai + * Copyright(c) 2011-2014 Jake Luer + * MIT Licensed + */ + +module.exports = function (chai, util) { + chai.expect = function (val, message) { + return new chai.Assertion(val, message); + }; +}; + + +}); +require.register("chai/lib/chai/interface/should.js", function(exports, require, module){ +/*! + * chai + * Copyright(c) 2011-2014 Jake Luer + * MIT Licensed + */ + +module.exports = function (chai, util) { + var Assertion = chai.Assertion; + + function loadShould () { + // explicitly define this method as function as to have it's name to include as `ssfi` + function shouldGetter() { + if (this instanceof String || this instanceof Number) { + return new Assertion(this.constructor(this), null, shouldGetter); + } else if (this instanceof Boolean) { + return new Assertion(this == true, null, shouldGetter); + } + return new Assertion(this, null, shouldGetter); + } + function shouldSetter(value) { + // See https://github.com/chaijs/chai/issues/86: this makes + // `whatever.should = someValue` actually set `someValue`, which is + // especially useful for `global.should = require('chai').should()`. + // + // Note that we have to use [[DefineProperty]] instead of [[Put]] + // since otherwise we would trigger this very setter! + Object.defineProperty(this, 'should', { + value: value, + enumerable: true, + configurable: true, + writable: true + }); + } + // modify Object.prototype to have `should` + Object.defineProperty(Object.prototype, 'should', { + set: shouldSetter + , get: shouldGetter + , configurable: true + }); + + var should = {}; + + should.equal = function (val1, val2, msg) { + new Assertion(val1, msg).to.equal(val2); + }; + + should.Throw = function (fn, errt, errs, msg) { + new Assertion(fn, msg).to.Throw(errt, errs); + }; + + should.exist = function (val, msg) { + new Assertion(val, msg).to.exist; + } + + // negation + should.not = {} + + should.not.equal = function (val1, val2, msg) { + new Assertion(val1, msg).to.not.equal(val2); + }; + + should.not.Throw = function (fn, errt, errs, msg) { + new Assertion(fn, msg).to.not.Throw(errt, errs); + }; + + should.not.exist = function (val, msg) { + new Assertion(val, msg).to.not.exist; + } + + should['throw'] = should['Throw']; + should.not['throw'] = should.not['Throw']; + + return should; + }; + + chai.should = loadShould; + chai.Should = loadShould; +}; + +}); +require.register("chai/lib/chai/utils/addChainableMethod.js", function(exports, require, module){ +/*! + * Chai - addChainingMethod utility + * Copyright(c) 2012-2014 Jake Luer + * MIT Licensed + */ + +/*! + * Module dependencies + */ + +var transferFlags = require('./transferFlags'); +var flag = require('./flag'); +var config = require('../config'); + +/*! + * Module variables + */ + +// Check whether `__proto__` is supported +var hasProtoSupport = '__proto__' in Object; + +// Without `__proto__` support, this module will need to add properties to a function. +// However, some Function.prototype methods cannot be overwritten, +// and there seems no easy cross-platform way to detect them (@see chaijs/chai/issues/69). +var excludeNames = /^(?:length|name|arguments|caller)$/; + +// Cache `Function` properties +var call = Function.prototype.call, + apply = Function.prototype.apply; + +/** + * ### addChainableMethod (ctx, name, method, chainingBehavior) + * + * Adds a method to an object, such that the method can also be chained. + * + * utils.addChainableMethod(chai.Assertion.prototype, 'foo', function (str) { + * var obj = utils.flag(this, 'object'); + * new chai.Assertion(obj).to.be.equal(str); + * }); + * + * Can also be accessed directly from `chai.Assertion`. + * + * chai.Assertion.addChainableMethod('foo', fn, chainingBehavior); + * + * The result can then be used as both a method assertion, executing both `method` and + * `chainingBehavior`, or as a language chain, which only executes `chainingBehavior`. + * + * expect(fooStr).to.be.foo('bar'); + * expect(fooStr).to.be.foo.equal('foo'); + * + * @param {Object} ctx object to which the method is added + * @param {String} name of method to add + * @param {Function} method function to be used for `name`, when called + * @param {Function} chainingBehavior function to be called every time the property is accessed + * @name addChainableMethod + * @api public + */ + +module.exports = function (ctx, name, method, chainingBehavior) { + if (typeof chainingBehavior !== 'function') { + chainingBehavior = function () { }; + } + + var chainableBehavior = { + method: method + , chainingBehavior: chainingBehavior + }; + + // save the methods so we can overwrite them later, if we need to. + if (!ctx.__methods) { + ctx.__methods = {}; + } + ctx.__methods[name] = chainableBehavior; + + Object.defineProperty(ctx, name, + { get: function () { + chainableBehavior.chainingBehavior.call(this); + + var assert = function assert() { + var old_ssfi = flag(this, 'ssfi'); + if (old_ssfi && config.includeStack === false) + flag(this, 'ssfi', assert); + var result = chainableBehavior.method.apply(this, arguments); + return result === undefined ? this : result; + }; + + // Use `__proto__` if available + if (hasProtoSupport) { + // Inherit all properties from the object by replacing the `Function` prototype + var prototype = assert.__proto__ = Object.create(this); + // Restore the `call` and `apply` methods from `Function` + prototype.call = call; + prototype.apply = apply; + } + // Otherwise, redefine all properties (slow!) + else { + var asserterNames = Object.getOwnPropertyNames(ctx); + asserterNames.forEach(function (asserterName) { + if (!excludeNames.test(asserterName)) { + var pd = Object.getOwnPropertyDescriptor(ctx, asserterName); + Object.defineProperty(assert, asserterName, pd); + } + }); + } + + transferFlags(this, assert); + return assert; + } + , configurable: true + }); +}; + +}); +require.register("chai/lib/chai/utils/addMethod.js", function(exports, require, module){ +/*! + * Chai - addMethod utility + * Copyright(c) 2012-2014 Jake Luer + * MIT Licensed + */ + +var config = require('../config'); + +/** + * ### .addMethod (ctx, name, method) + * + * Adds a method to the prototype of an object. + * + * utils.addMethod(chai.Assertion.prototype, 'foo', function (str) { + * var obj = utils.flag(this, 'object'); + * new chai.Assertion(obj).to.be.equal(str); + * }); + * + * Can also be accessed directly from `chai.Assertion`. + * + * chai.Assertion.addMethod('foo', fn); + * + * Then can be used as any other assertion. + * + * expect(fooStr).to.be.foo('bar'); + * + * @param {Object} ctx object to which the method is added + * @param {String} name of method to add + * @param {Function} method function to be used for name + * @name addMethod + * @api public + */ +var flag = require('./flag'); + +module.exports = function (ctx, name, method) { + ctx[name] = function () { + var old_ssfi = flag(this, 'ssfi'); + if (old_ssfi && config.includeStack === false) + flag(this, 'ssfi', ctx[name]); + var result = method.apply(this, arguments); + return result === undefined ? this : result; + }; +}; + +}); +require.register("chai/lib/chai/utils/addProperty.js", function(exports, require, module){ +/*! + * Chai - addProperty utility + * Copyright(c) 2012-2014 Jake Luer + * MIT Licensed + */ + +/** + * ### addProperty (ctx, name, getter) + * + * Adds a property to the prototype of an object. + * + * utils.addProperty(chai.Assertion.prototype, 'foo', function () { + * var obj = utils.flag(this, 'object'); + * new chai.Assertion(obj).to.be.instanceof(Foo); + * }); + * + * Can also be accessed directly from `chai.Assertion`. + * + * chai.Assertion.addProperty('foo', fn); + * + * Then can be used as any other assertion. + * + * expect(myFoo).to.be.foo; + * + * @param {Object} ctx object to which the property is added + * @param {String} name of property to add + * @param {Function} getter function to be used for name + * @name addProperty + * @api public + */ + +module.exports = function (ctx, name, getter) { + Object.defineProperty(ctx, name, + { get: function () { + var result = getter.call(this); + return result === undefined ? this : result; + } + , configurable: true + }); +}; + +}); +require.register("chai/lib/chai/utils/flag.js", function(exports, require, module){ +/*! + * Chai - flag utility + * Copyright(c) 2012-2014 Jake Luer + * MIT Licensed + */ + +/** + * ### flag(object ,key, [value]) + * + * Get or set a flag value on an object. If a + * value is provided it will be set, else it will + * return the currently set value or `undefined` if + * the value is not set. + * + * utils.flag(this, 'foo', 'bar'); // setter + * utils.flag(this, 'foo'); // getter, returns `bar` + * + * @param {Object} object (constructed Assertion + * @param {String} key + * @param {Mixed} value (optional) + * @name flag + * @api private + */ + +module.exports = function (obj, key, value) { + var flags = obj.__flags || (obj.__flags = Object.create(null)); + if (arguments.length === 3) { + flags[key] = value; + } else { + return flags[key]; + } +}; + +}); +require.register("chai/lib/chai/utils/getActual.js", function(exports, require, module){ +/*! + * Chai - getActual utility + * Copyright(c) 2012-2014 Jake Luer + * MIT Licensed + */ + +/** + * # getActual(object, [actual]) + * + * Returns the `actual` value for an Assertion + * + * @param {Object} object (constructed Assertion) + * @param {Arguments} chai.Assertion.prototype.assert arguments + */ + +module.exports = function (obj, args) { + return args.length > 4 ? args[4] : obj._obj; +}; + +}); +require.register("chai/lib/chai/utils/getEnumerableProperties.js", function(exports, require, module){ +/*! + * Chai - getEnumerableProperties utility + * Copyright(c) 2012-2014 Jake Luer + * MIT Licensed + */ + +/** + * ### .getEnumerableProperties(object) + * + * This allows the retrieval of enumerable property names of an object, + * inherited or not. + * + * @param {Object} object + * @returns {Array} + * @name getEnumerableProperties + * @api public + */ + +module.exports = function getEnumerableProperties(object) { + var result = []; + for (var name in object) { + result.push(name); + } + return result; +}; + +}); +require.register("chai/lib/chai/utils/getMessage.js", function(exports, require, module){ +/*! + * Chai - message composition utility + * Copyright(c) 2012-2014 Jake Luer + * MIT Licensed + */ + +/*! + * Module dependancies + */ + +var flag = require('./flag') + , getActual = require('./getActual') + , inspect = require('./inspect') + , objDisplay = require('./objDisplay'); + +/** + * ### .getMessage(object, message, negateMessage) + * + * Construct the error message based on flags + * and template tags. Template tags will return + * a stringified inspection of the object referenced. + * + * Message template tags: + * - `#{this}` current asserted object + * - `#{act}` actual value + * - `#{exp}` expected value + * + * @param {Object} object (constructed Assertion) + * @param {Arguments} chai.Assertion.prototype.assert arguments + * @name getMessage + * @api public + */ + +module.exports = function (obj, args) { + var negate = flag(obj, 'negate') + , val = flag(obj, 'object') + , expected = args[3] + , actual = getActual(obj, args) + , msg = negate ? args[2] : args[1] + , flagMsg = flag(obj, 'message'); + + msg = msg || ''; + msg = msg + .replace(/#{this}/g, objDisplay(val)) + .replace(/#{act}/g, objDisplay(actual)) + .replace(/#{exp}/g, objDisplay(expected)); + + return flagMsg ? flagMsg + ': ' + msg : msg; +}; + +}); +require.register("chai/lib/chai/utils/getName.js", function(exports, require, module){ +/*! + * Chai - getName utility + * Copyright(c) 2012-2014 Jake Luer + * MIT Licensed + */ + +/** + * # getName(func) + * + * Gets the name of a function, in a cross-browser way. + * + * @param {Function} a function (usually a constructor) + */ + +module.exports = function (func) { + if (func.name) return func.name; + + var match = /^\s?function ([^(]*)\(/.exec(func); + return match && match[1] ? match[1] : ""; +}; + +}); +require.register("chai/lib/chai/utils/getPathValue.js", function(exports, require, module){ +/*! + * Chai - getPathValue utility + * Copyright(c) 2012-2014 Jake Luer + * @see https://github.com/logicalparadox/filtr + * MIT Licensed + */ + +/** + * ### .getPathValue(path, object) + * + * This allows the retrieval of values in an + * object given a string path. + * + * var obj = { + * prop1: { + * arr: ['a', 'b', 'c'] + * , str: 'Hello' + * } + * , prop2: { + * arr: [ { nested: 'Universe' } ] + * , str: 'Hello again!' + * } + * } + * + * The following would be the results. + * + * getPathValue('prop1.str', obj); // Hello + * getPathValue('prop1.att[2]', obj); // b + * getPathValue('prop2.arr[0].nested', obj); // Universe + * + * @param {String} path + * @param {Object} object + * @returns {Object} value or `undefined` + * @name getPathValue + * @api public + */ + +var getPathValue = module.exports = function (path, obj) { + var parsed = parsePath(path); + return _getPathValue(parsed, obj); +}; + +/*! + * ## parsePath(path) + * + * Helper function used to parse string object + * paths. Use in conjunction with `_getPathValue`. + * + * var parsed = parsePath('myobject.property.subprop'); + * + * ### Paths: + * + * * Can be as near infinitely deep and nested + * * Arrays are also valid using the formal `myobject.document[3].property`. + * + * @param {String} path + * @returns {Object} parsed + * @api private + */ + +function parsePath (path) { + var str = path.replace(/\[/g, '.[') + , parts = str.match(/(\\\.|[^.]+?)+/g); + return parts.map(function (value) { + var re = /\[(\d+)\]$/ + , mArr = re.exec(value) + if (mArr) return { i: parseFloat(mArr[1]) }; + else return { p: value }; + }); +}; + +/*! + * ## _getPathValue(parsed, obj) + * + * Helper companion function for `.parsePath` that returns + * the value located at the parsed address. + * + * var value = getPathValue(parsed, obj); + * + * @param {Object} parsed definition from `parsePath`. + * @param {Object} object to search against + * @returns {Object|Undefined} value + * @api private + */ + +function _getPathValue (parsed, obj) { + var tmp = obj + , res; + for (var i = 0, l = parsed.length; i < l; i++) { + var part = parsed[i]; + if (tmp) { + if ('undefined' !== typeof part.p) + tmp = tmp[part.p]; + else if ('undefined' !== typeof part.i) + tmp = tmp[part.i]; + if (i == (l - 1)) res = tmp; + } else { + res = undefined; + } + } + return res; +}; + +}); +require.register("chai/lib/chai/utils/getProperties.js", function(exports, require, module){ +/*! + * Chai - getProperties utility + * Copyright(c) 2012-2014 Jake Luer + * MIT Licensed + */ + +/** + * ### .getProperties(object) + * + * This allows the retrieval of property names of an object, enumerable or not, + * inherited or not. + * + * @param {Object} object + * @returns {Array} + * @name getProperties + * @api public + */ + +module.exports = function getProperties(object) { + var result = Object.getOwnPropertyNames(subject); + + function addProperty(property) { + if (result.indexOf(property) === -1) { + result.push(property); + } + } + + var proto = Object.getPrototypeOf(subject); + while (proto !== null) { + Object.getOwnPropertyNames(proto).forEach(addProperty); + proto = Object.getPrototypeOf(proto); + } + + return result; +}; + +}); +require.register("chai/lib/chai/utils/index.js", function(exports, require, module){ +/*! + * chai + * Copyright(c) 2011 Jake Luer + * MIT Licensed + */ + +/*! + * Main exports + */ + +var exports = module.exports = {}; + +/*! + * test utility + */ + +exports.test = require('./test'); + +/*! + * type utility + */ + +exports.type = require('./type'); + +/*! + * message utility + */ + +exports.getMessage = require('./getMessage'); + +/*! + * actual utility + */ + +exports.getActual = require('./getActual'); + +/*! + * Inspect util + */ + +exports.inspect = require('./inspect'); + +/*! + * Object Display util + */ + +exports.objDisplay = require('./objDisplay'); + +/*! + * Flag utility + */ + +exports.flag = require('./flag'); + +/*! + * Flag transferring utility + */ + +exports.transferFlags = require('./transferFlags'); + +/*! + * Deep equal utility + */ + +exports.eql = require('deep-eql'); + +/*! + * Deep path value + */ + +exports.getPathValue = require('./getPathValue'); + +/*! + * Function name + */ + +exports.getName = require('./getName'); + +/*! + * add Property + */ + +exports.addProperty = require('./addProperty'); + +/*! + * add Method + */ + +exports.addMethod = require('./addMethod'); + +/*! + * overwrite Property + */ + +exports.overwriteProperty = require('./overwriteProperty'); + +/*! + * overwrite Method + */ + +exports.overwriteMethod = require('./overwriteMethod'); + +/*! + * Add a chainable method + */ + +exports.addChainableMethod = require('./addChainableMethod'); + +/*! + * Overwrite chainable method + */ + +exports.overwriteChainableMethod = require('./overwriteChainableMethod'); + + +}); +require.register("chai/lib/chai/utils/inspect.js", function(exports, require, module){ +// This is (almost) directly from Node.js utils +// https://github.com/joyent/node/blob/f8c335d0caf47f16d31413f89aa28eda3878e3aa/lib/util.js + +var getName = require('./getName'); +var getProperties = require('./getProperties'); +var getEnumerableProperties = require('./getEnumerableProperties'); + +module.exports = inspect; + +/** + * Echos the value of a value. Trys to print the value out + * in the best way possible given the different types. + * + * @param {Object} obj The object to print out. + * @param {Boolean} showHidden Flag that shows hidden (not enumerable) + * properties of objects. + * @param {Number} depth Depth in which to descend in object. Default is 2. + * @param {Boolean} colors Flag to turn on ANSI escape codes to color the + * output. Default is false (no coloring). + */ +function inspect(obj, showHidden, depth, colors) { + var ctx = { + showHidden: showHidden, + seen: [], + stylize: function (str) { return str; } + }; + return formatValue(ctx, obj, (typeof depth === 'undefined' ? 2 : depth)); +} + +// https://gist.github.com/1044128/ +var getOuterHTML = function(element) { + if ('outerHTML' in element) return element.outerHTML; + var ns = "http://www.w3.org/1999/xhtml"; + var container = document.createElementNS(ns, '_'); + var elemProto = (window.HTMLElement || window.Element).prototype; + var xmlSerializer = new XMLSerializer(); + var html; + if (document.xmlVersion) { + return xmlSerializer.serializeToString(element); + } else { + container.appendChild(element.cloneNode(false)); + html = container.innerHTML.replace('><', '>' + element.innerHTML + '<'); + container.innerHTML = ''; + return html; + } +}; + +// Returns true if object is a DOM element. +var isDOMElement = function (object) { + if (typeof HTMLElement === 'object') { + return object instanceof HTMLElement; + } else { + return object && + typeof object === 'object' && + object.nodeType === 1 && + typeof object.nodeName === 'string'; + } +}; + +function formatValue(ctx, value, recurseTimes) { + // Provide a hook for user-specified inspect functions. + // Check that value is an object with an inspect function on it + if (value && typeof value.inspect === 'function' && + // Filter out the util module, it's inspect function is special + value.inspect !== exports.inspect && + // Also filter out any prototype objects using the circular check. + !(value.constructor && value.constructor.prototype === value)) { + var ret = value.inspect(recurseTimes); + if (typeof ret !== 'string') { + ret = formatValue(ctx, ret, recurseTimes); + } + return ret; + } + + // Primitive types cannot have properties + var primitive = formatPrimitive(ctx, value); + if (primitive) { + return primitive; + } + + // If it's DOM elem, get outer HTML. + if (isDOMElement(value)) { + return getOuterHTML(value); + } + + // Look up the keys of the object. + var visibleKeys = getEnumerableProperties(value); + var keys = ctx.showHidden ? getProperties(value) : visibleKeys; + + // Some type of object without properties can be shortcutted. + // In IE, errors have a single `stack` property, or if they are vanilla `Error`, + // a `stack` plus `description` property; ignore those for consistency. + if (keys.length === 0 || (isError(value) && ( + (keys.length === 1 && keys[0] === 'stack') || + (keys.length === 2 && keys[0] === 'description' && keys[1] === 'stack') + ))) { + if (typeof value === 'function') { + var name = getName(value); + var nameSuffix = name ? ': ' + name : ''; + return ctx.stylize('[Function' + nameSuffix + ']', 'special'); + } + if (isRegExp(value)) { + return ctx.stylize(RegExp.prototype.toString.call(value), 'regexp'); + } + if (isDate(value)) { + return ctx.stylize(Date.prototype.toUTCString.call(value), 'date'); + } + if (isError(value)) { + return formatError(value); + } + } + + var base = '', array = false, braces = ['{', '}']; + + // Make Array say that they are Array + if (isArray(value)) { + array = true; + braces = ['[', ']']; + } + + // Make functions say that they are functions + if (typeof value === 'function') { + var name = getName(value); + var nameSuffix = name ? ': ' + name : ''; + base = ' [Function' + nameSuffix + ']'; + } + + // Make RegExps say that they are RegExps + if (isRegExp(value)) { + base = ' ' + RegExp.prototype.toString.call(value); + } + + // Make dates with properties first say the date + if (isDate(value)) { + base = ' ' + Date.prototype.toUTCString.call(value); + } + + // Make error with message first say the error + if (isError(value)) { + return formatError(value); + } + + if (keys.length === 0 && (!array || value.length == 0)) { + return braces[0] + base + braces[1]; + } + + if (recurseTimes < 0) { + if (isRegExp(value)) { + return ctx.stylize(RegExp.prototype.toString.call(value), 'regexp'); + } else { + return ctx.stylize('[Object]', 'special'); + } + } + + ctx.seen.push(value); + + var output; + if (array) { + output = formatArray(ctx, value, recurseTimes, visibleKeys, keys); + } else { + output = keys.map(function(key) { + return formatProperty(ctx, value, recurseTimes, visibleKeys, key, array); + }); + } + + ctx.seen.pop(); + + return reduceToSingleString(output, base, braces); +} + + +function formatPrimitive(ctx, value) { + switch (typeof value) { + case 'undefined': + return ctx.stylize('undefined', 'undefined'); + + case 'string': + var simple = '\'' + JSON.stringify(value).replace(/^"|"$/g, '') + .replace(/'/g, "\\'") + .replace(/\\"/g, '"') + '\''; + return ctx.stylize(simple, 'string'); + + case 'number': + return ctx.stylize('' + value, 'number'); + + case 'boolean': + return ctx.stylize('' + value, 'boolean'); + } + // For some reason typeof null is "object", so special case here. + if (value === null) { + return ctx.stylize('null', 'null'); + } +} + + +function formatError(value) { + return '[' + Error.prototype.toString.call(value) + ']'; +} + + +function formatArray(ctx, value, recurseTimes, visibleKeys, keys) { + var output = []; + for (var i = 0, l = value.length; i < l; ++i) { + if (Object.prototype.hasOwnProperty.call(value, String(i))) { + output.push(formatProperty(ctx, value, recurseTimes, visibleKeys, + String(i), true)); + } else { + output.push(''); + } + } + keys.forEach(function(key) { + if (!key.match(/^\d+$/)) { + output.push(formatProperty(ctx, value, recurseTimes, visibleKeys, + key, true)); + } + }); + return output; +} + + +function formatProperty(ctx, value, recurseTimes, visibleKeys, key, array) { + var name, str; + if (value.__lookupGetter__) { + if (value.__lookupGetter__(key)) { + if (value.__lookupSetter__(key)) { + str = ctx.stylize('[Getter/Setter]', 'special'); + } else { + str = ctx.stylize('[Getter]', 'special'); + } + } else { + if (value.__lookupSetter__(key)) { + str = ctx.stylize('[Setter]', 'special'); + } + } + } + if (visibleKeys.indexOf(key) < 0) { + name = '[' + key + ']'; + } + if (!str) { + if (ctx.seen.indexOf(value[key]) < 0) { + if (recurseTimes === null) { + str = formatValue(ctx, value[key], null); + } else { + str = formatValue(ctx, value[key], recurseTimes - 1); + } + if (str.indexOf('\n') > -1) { + if (array) { + str = str.split('\n').map(function(line) { + return ' ' + line; + }).join('\n').substr(2); + } else { + str = '\n' + str.split('\n').map(function(line) { + return ' ' + line; + }).join('\n'); + } + } + } else { + str = ctx.stylize('[Circular]', 'special'); + } + } + if (typeof name === 'undefined') { + if (array && key.match(/^\d+$/)) { + return str; + } + name = JSON.stringify('' + key); + if (name.match(/^"([a-zA-Z_][a-zA-Z_0-9]*)"$/)) { + name = name.substr(1, name.length - 2); + name = ctx.stylize(name, 'name'); + } else { + name = name.replace(/'/g, "\\'") + .replace(/\\"/g, '"') + .replace(/(^"|"$)/g, "'"); + name = ctx.stylize(name, 'string'); + } + } + + return name + ': ' + str; +} + + +function reduceToSingleString(output, base, braces) { + var numLinesEst = 0; + var length = output.reduce(function(prev, cur) { + numLinesEst++; + if (cur.indexOf('\n') >= 0) numLinesEst++; + return prev + cur.length + 1; + }, 0); + + if (length > 60) { + return braces[0] + + (base === '' ? '' : base + '\n ') + + ' ' + + output.join(',\n ') + + ' ' + + braces[1]; + } + + return braces[0] + base + ' ' + output.join(', ') + ' ' + braces[1]; +} + +function isArray(ar) { + return Array.isArray(ar) || + (typeof ar === 'object' && objectToString(ar) === '[object Array]'); +} + +function isRegExp(re) { + return typeof re === 'object' && objectToString(re) === '[object RegExp]'; +} + +function isDate(d) { + return typeof d === 'object' && objectToString(d) === '[object Date]'; +} + +function isError(e) { + return typeof e === 'object' && objectToString(e) === '[object Error]'; +} + +function objectToString(o) { + return Object.prototype.toString.call(o); +} + +}); +require.register("chai/lib/chai/utils/objDisplay.js", function(exports, require, module){ +/*! + * Chai - flag utility + * Copyright(c) 2012-2014 Jake Luer + * MIT Licensed + */ + +/*! + * Module dependancies + */ + +var inspect = require('./inspect'); +var config = require('../config'); + +/** + * ### .objDisplay (object) + * + * Determines if an object or an array matches + * criteria to be inspected in-line for error + * messages or should be truncated. + * + * @param {Mixed} javascript object to inspect + * @name objDisplay + * @api public + */ + +module.exports = function (obj) { + var str = inspect(obj) + , type = Object.prototype.toString.call(obj); + + if (config.truncateThreshold && str.length >= config.truncateThreshold) { + if (type === '[object Function]') { + return !obj.name || obj.name === '' + ? '[Function]' + : '[Function: ' + obj.name + ']'; + } else if (type === '[object Array]') { + return '[ Array(' + obj.length + ') ]'; + } else if (type === '[object Object]') { + var keys = Object.keys(obj) + , kstr = keys.length > 2 + ? keys.splice(0, 2).join(', ') + ', ...' + : keys.join(', '); + return '{ Object (' + kstr + ') }'; + } else { + return str; + } + } else { + return str; + } +}; + +}); +require.register("chai/lib/chai/utils/overwriteMethod.js", function(exports, require, module){ +/*! + * Chai - overwriteMethod utility + * Copyright(c) 2012-2014 Jake Luer + * MIT Licensed + */ + +/** + * ### overwriteMethod (ctx, name, fn) + * + * Overwites an already existing method and provides + * access to previous function. Must return function + * to be used for name. + * + * utils.overwriteMethod(chai.Assertion.prototype, 'equal', function (_super) { + * return function (str) { + * var obj = utils.flag(this, 'object'); + * if (obj instanceof Foo) { + * new chai.Assertion(obj.value).to.equal(str); + * } else { + * _super.apply(this, arguments); + * } + * } + * }); + * + * Can also be accessed directly from `chai.Assertion`. + * + * chai.Assertion.overwriteMethod('foo', fn); + * + * Then can be used as any other assertion. + * + * expect(myFoo).to.equal('bar'); + * + * @param {Object} ctx object whose method is to be overwritten + * @param {String} name of method to overwrite + * @param {Function} method function that returns a function to be used for name + * @name overwriteMethod + * @api public + */ + +module.exports = function (ctx, name, method) { + var _method = ctx[name] + , _super = function () { return this; }; + + if (_method && 'function' === typeof _method) + _super = _method; + + ctx[name] = function () { + var result = method(_super).apply(this, arguments); + return result === undefined ? this : result; + } +}; + +}); +require.register("chai/lib/chai/utils/overwriteProperty.js", function(exports, require, module){ +/*! + * Chai - overwriteProperty utility + * Copyright(c) 2012-2014 Jake Luer + * MIT Licensed + */ + +/** + * ### overwriteProperty (ctx, name, fn) + * + * Overwites an already existing property getter and provides + * access to previous value. Must return function to use as getter. + * + * utils.overwriteProperty(chai.Assertion.prototype, 'ok', function (_super) { + * return function () { + * var obj = utils.flag(this, 'object'); + * if (obj instanceof Foo) { + * new chai.Assertion(obj.name).to.equal('bar'); + * } else { + * _super.call(this); + * } + * } + * }); + * + * + * Can also be accessed directly from `chai.Assertion`. + * + * chai.Assertion.overwriteProperty('foo', fn); + * + * Then can be used as any other assertion. + * + * expect(myFoo).to.be.ok; + * + * @param {Object} ctx object whose property is to be overwritten + * @param {String} name of property to overwrite + * @param {Function} getter function that returns a getter function to be used for name + * @name overwriteProperty + * @api public + */ + +module.exports = function (ctx, name, getter) { + var _get = Object.getOwnPropertyDescriptor(ctx, name) + , _super = function () {}; + + if (_get && 'function' === typeof _get.get) + _super = _get.get + + Object.defineProperty(ctx, name, + { get: function () { + var result = getter(_super).call(this); + return result === undefined ? this : result; + } + , configurable: true + }); +}; + +}); +require.register("chai/lib/chai/utils/overwriteChainableMethod.js", function(exports, require, module){ +/*! + * Chai - overwriteChainableMethod utility + * Copyright(c) 2012-2014 Jake Luer + * MIT Licensed + */ + +/** + * ### overwriteChainableMethod (ctx, name, fn) + * + * Overwites an already existing chainable method + * and provides access to the previous function or + * property. Must return functions to be used for + * name. + * + * utils.overwriteChainableMethod(chai.Assertion.prototype, 'length', + * function (_super) { + * } + * , function (_super) { + * } + * ); + * + * Can also be accessed directly from `chai.Assertion`. + * + * chai.Assertion.overwriteChainableMethod('foo', fn, fn); + * + * Then can be used as any other assertion. + * + * expect(myFoo).to.have.length(3); + * expect(myFoo).to.have.length.above(3); + * + * @param {Object} ctx object whose method / property is to be overwritten + * @param {String} name of method / property to overwrite + * @param {Function} method function that returns a function to be used for name + * @param {Function} chainingBehavior function that returns a function to be used for property + * @name overwriteChainableMethod + * @api public + */ + +module.exports = function (ctx, name, method, chainingBehavior) { + var chainableBehavior = ctx.__methods[name]; + + var _chainingBehavior = chainableBehavior.chainingBehavior; + chainableBehavior.chainingBehavior = function () { + var result = chainingBehavior(_chainingBehavior).call(this); + return result === undefined ? this : result; + }; + + var _method = chainableBehavior.method; + chainableBehavior.method = function () { + var result = method(_method).apply(this, arguments); + return result === undefined ? this : result; + }; +}; + +}); +require.register("chai/lib/chai/utils/test.js", function(exports, require, module){ +/*! + * Chai - test utility + * Copyright(c) 2012-2014 Jake Luer + * MIT Licensed + */ + +/*! + * Module dependancies + */ + +var flag = require('./flag'); + +/** + * # test(object, expression) + * + * Test and object for expression. + * + * @param {Object} object (constructed Assertion) + * @param {Arguments} chai.Assertion.prototype.assert arguments + */ + +module.exports = function (obj, args) { + var negate = flag(obj, 'negate') + , expr = args[0]; + return negate ? !expr : expr; +}; + +}); +require.register("chai/lib/chai/utils/transferFlags.js", function(exports, require, module){ +/*! + * Chai - transferFlags utility + * Copyright(c) 2012-2014 Jake Luer + * MIT Licensed + */ + +/** + * ### transferFlags(assertion, object, includeAll = true) + * + * Transfer all the flags for `assertion` to `object`. If + * `includeAll` is set to `false`, then the base Chai + * assertion flags (namely `object`, `ssfi`, and `message`) + * will not be transferred. + * + * + * var newAssertion = new Assertion(); + * utils.transferFlags(assertion, newAssertion); + * + * var anotherAsseriton = new Assertion(myObj); + * utils.transferFlags(assertion, anotherAssertion, false); + * + * @param {Assertion} assertion the assertion to transfer the flags from + * @param {Object} object the object to transfer the flags too; usually a new assertion + * @param {Boolean} includeAll + * @name getAllFlags + * @api private + */ + +module.exports = function (assertion, object, includeAll) { + var flags = assertion.__flags || (assertion.__flags = Object.create(null)); + + if (!object.__flags) { + object.__flags = Object.create(null); + } + + includeAll = arguments.length === 3 ? includeAll : true; + + for (var flag in flags) { + if (includeAll || + (flag !== 'object' && flag !== 'ssfi' && flag != 'message')) { + object.__flags[flag] = flags[flag]; + } + } +}; + +}); +require.register("chai/lib/chai/utils/type.js", function(exports, require, module){ +/*! + * Chai - type utility + * Copyright(c) 2012-2014 Jake Luer + * MIT Licensed + */ + +/*! + * Detectable javascript natives + */ + +var natives = { + '[object Arguments]': 'arguments' + , '[object Array]': 'array' + , '[object Date]': 'date' + , '[object Function]': 'function' + , '[object Number]': 'number' + , '[object RegExp]': 'regexp' + , '[object String]': 'string' +}; + +/** + * ### type(object) + * + * Better implementation of `typeof` detection that can + * be used cross-browser. Handles the inconsistencies of + * Array, `null`, and `undefined` detection. + * + * utils.type({}) // 'object' + * utils.type(null) // `null' + * utils.type(undefined) // `undefined` + * utils.type([]) // `array` + * + * @param {Mixed} object to detect type of + * @name type + * @api private + */ + +module.exports = function (obj) { + var str = Object.prototype.toString.call(obj); + if (natives[str]) return natives[str]; + if (obj === null) return 'null'; + if (obj === undefined) return 'undefined'; + if (obj === Object(obj)) return 'object'; + return typeof obj; +}; + +}); + + + + +require.alias("chaijs-assertion-error/index.js", "chai/deps/assertion-error/index.js"); +require.alias("chaijs-assertion-error/index.js", "chai/deps/assertion-error/index.js"); +require.alias("chaijs-assertion-error/index.js", "assertion-error/index.js"); +require.alias("chaijs-assertion-error/index.js", "chaijs-assertion-error/index.js"); +require.alias("chaijs-deep-eql/lib/eql.js", "chai/deps/deep-eql/lib/eql.js"); +require.alias("chaijs-deep-eql/lib/eql.js", "chai/deps/deep-eql/index.js"); +require.alias("chaijs-deep-eql/lib/eql.js", "deep-eql/index.js"); +require.alias("chaijs-type-detect/lib/type.js", "chaijs-deep-eql/deps/type-detect/lib/type.js"); +require.alias("chaijs-type-detect/lib/type.js", "chaijs-deep-eql/deps/type-detect/index.js"); +require.alias("chaijs-type-detect/lib/type.js", "chaijs-type-detect/index.js"); +require.alias("chaijs-deep-eql/lib/eql.js", "chaijs-deep-eql/index.js"); +require.alias("chai/index.js", "chai/index.js");if (typeof exports == "object") { + module.exports = require("chai"); +} else if (typeof define == "function" && define.amd) { + define([], function(){ return require("chai"); }); +} else { + this["chai"] = require("chai"); +}})(); \ No newline at end of file diff --git a/tests/tools/chai/component.json b/tests/tools/chai/component.json new file mode 100644 index 0000000..33a867c --- /dev/null +++ b/tests/tools/chai/component.json @@ -0,0 +1,49 @@ +{ + "name": "chai" + , "repo": "chaijs/chai" + , "version": "1.9.1" + , "description": "BDD/TDD assertion library for node.js and the browser. Test framework agnostic." + , "license": "MIT" + , "keywords": [ + "test" + , "assertion" + , "assert" + , "testing" + , "chai" + ] + , "main": "index.js" + , "scripts": [ + "index.js" + , "lib/chai.js" + , "lib/chai/assertion.js" + , "lib/chai/config.js" + , "lib/chai/core/assertions.js" + , "lib/chai/interface/assert.js" + , "lib/chai/interface/expect.js" + , "lib/chai/interface/should.js" + , "lib/chai/utils/addChainableMethod.js" + , "lib/chai/utils/addMethod.js" + , "lib/chai/utils/addProperty.js" + , "lib/chai/utils/flag.js" + , "lib/chai/utils/getActual.js" + , "lib/chai/utils/getEnumerableProperties.js" + , "lib/chai/utils/getMessage.js" + , "lib/chai/utils/getName.js" + , "lib/chai/utils/getPathValue.js" + , "lib/chai/utils/getProperties.js" + , "lib/chai/utils/index.js" + , "lib/chai/utils/inspect.js" + , "lib/chai/utils/objDisplay.js" + , "lib/chai/utils/overwriteMethod.js" + , "lib/chai/utils/overwriteProperty.js" + , "lib/chai/utils/overwriteChainableMethod.js" + , "lib/chai/utils/test.js" + , "lib/chai/utils/transferFlags.js" + , "lib/chai/utils/type.js" + ] + , "dependencies": { + "chaijs/assertion-error": "1.0.0" + , "chaijs/deep-eql": "0.1.3" + } + , "development": {} +} diff --git a/tests/tools/chai/karma.conf.js b/tests/tools/chai/karma.conf.js new file mode 100644 index 0000000..4a4a0cc --- /dev/null +++ b/tests/tools/chai/karma.conf.js @@ -0,0 +1,38 @@ +/* + * @license + * Copyright (c) 2014 The Polymer Project Authors. All rights reserved. + * This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt + * The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt + * The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt + * Code distributed by Google as part of the polymer project is also + * subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt + */ + +module.exports = function(config) { + config.set({ + frameworks: [ 'mocha' ] + , files: [ + 'build/build.js' + , 'test/bootstrap/karma.js' + , 'test/*.js' + ] + , reporters: [ 'progress' ] + , colors: true + , logLevel: config.LOG_INFO + , autoWatch: false + , browsers: [ 'PhantomJS' ] + , browserDisconnectTimeout: 10000 + , browserDisconnectTolerance: 2 + , browserNoActivityTimeout: 20000 + , singleRun: true + }); + + switch (process.env.CHAI_TEST_ENV) { + case 'sauce': + require('./karma.sauce')(config); + break; + default: + // ... + break; + }; +}; diff --git a/tests/tools/chai/karma.sauce.js b/tests/tools/chai/karma.sauce.js new file mode 100644 index 0000000..c116b87 --- /dev/null +++ b/tests/tools/chai/karma.sauce.js @@ -0,0 +1,51 @@ +/* + * @license + * Copyright (c) 2014 The Polymer Project Authors. All rights reserved. + * This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt + * The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt + * The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt + * Code distributed by Google as part of the polymer project is also + * subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt + */ + +var version = require('./package.json').version; +var ts = new Date().getTime(); + +module.exports = function(config) { + var auth; + + try { + auth = require('./test/auth/index'); + } catch(ex) { + auth = {}; + auth.SAUCE_USERNAME = process.env.SAUCE_USERNAME || null; + auth.SAUCE_ACCESS_KEY = process.env.SAUCE_ACCESS_KEY || null; + } + + if (!auth.SAUCE_USERNAME || !auth.SAUCE_ACCESS_KEY) return; + if (process.env.SKIP_SAUCE) return; + + var branch = process.env.TRAVIS_BRANCH || 'local' + var browserConfig = require('./sauce.browsers'); + var browsers = Object.keys(browserConfig); + var tags = [ 'chaijs_' + version, auth.SAUCE_USERNAME + '@' + branch ]; + var tunnel = process.env.TRAVIS_JOB_NUMBER || ts; + + if (process.env.TRAVIS_JOB_NUMBER) { + tags.push('travis@' + process.env.TRAVIS_JOB_NUMBER); + } + + config.browsers = config.browsers.concat(browsers); + config.customLaunchers = browserConfig; + config.reporters.push('saucelabs'); + config.transports = [ 'xhr-polling' ]; + + config.sauceLabs = { + username: auth.SAUCE_USERNAME + , accessKey: auth.SAUCE_ACCESS_KEY + , startConnect: true + , tags: tags + , testName: 'ChaiJS' + , tunnelIdentifier: tunnel + }; +}; diff --git a/tests/tools/chai/package.json b/tests/tools/chai/package.json new file mode 100644 index 0000000..14fadc0 --- /dev/null +++ b/tests/tools/chai/package.json @@ -0,0 +1,42 @@ +{ + "author": "Jake Luer ", + "name": "chai", + "description": "BDD/TDD assertion library for node.js and the browser. Test framework agnostic.", + "keywords": [ "test", "assertion", "assert", "testing", "chai" ], + "homepage": "http://chaijs.com", + "license": "MIT", + "contributors": [ + "Jake Luer ", + "Domenic Denicola (http://domenicdenicola.com)", + "Veselin Todorov ", + "John Firebaugh " + ], + "version": "1.9.1", + "repository": { + "type": "git", + "url": "https://github.com/chaijs/chai" + }, + "bugs": { + "url": "https://github.com/chaijs/chai/issues" + }, + "main": "./index", + "scripts": { + "test": "make test" + }, + "engines": { + "node": ">= 0.4.0" + }, + "dependencies": { + "assertion-error": "1.0.0" + , "deep-eql": "0.1.3" + }, + "devDependencies": { + "component": "*" + , "karma": "0.12.x" + , "karma-mocha": "*" + , "karma-sauce-launcher": "0.2.x" + , "karma-phantomjs-launcher": "0.1.1" + , "mocha": "1.17.x" + , "istanbul": "0.2.x" + } +} diff --git a/tests/tools/chai/sauce.browsers.js b/tests/tools/chai/sauce.browsers.js new file mode 100644 index 0000000..d1a1d82 --- /dev/null +++ b/tests/tools/chai/sauce.browsers.js @@ -0,0 +1,138 @@ +/* + * @license + * Copyright (c) 2014 The Polymer Project Authors. All rights reserved. + * This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt + * The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt + * The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt + * Code distributed by Google as part of the polymer project is also + * subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt + */ + + +/*! + * Chrome + */ + +exports['SL_Chrome'] = { + base: 'SauceLabs' + , browserName: 'chrome' +}; + +/*! + * Firefox + */ + +/*! + * TODO: Karma doesn't seem to like this, though sauce boots its up + * + +exports['SL_Firefox_23'] = { + base: 'SauceLabs' + , browserName: 'firefox' + , platform: 'Windows XP' + , version: '23' +}; + +*/ + +exports['SL_Firefox_22'] = { + base: 'SauceLabs' + , browserName: 'firefox' + , platform: 'Windows 7' + , version: '22' +}; + +/*! + * Opera + */ + +exports['SL_Opera_12'] = { + base: 'SauceLabs' + , browserName: 'opera' + , platform: 'Windows 7' + , version: '12' +}; + +exports['SL_Opera_11'] = { + base: 'SauceLabs' + , browserName: 'opera' + , platform: 'Windows 7' + , version: '11' +}; + +/*! + * Internet Explorer + */ + +exports['SL_IE_10'] = { + base: 'SauceLabs' + , browserName: 'internet explorer' + , platform: 'Windows 2012' + , version: '10' +}; + +/*! + * Safari + */ + +exports['SL_Safari_6'] = { + base: 'SauceLabs' + , browserName: 'safari' + , platform: 'Mac 10.8' + , version: '6' +}; + +exports['SL_Safari_5'] = { + base: 'SauceLabs' + , browserName: 'safari' + , platform: 'Mac 10.6' + , version: '5' +}; + +/*! + * iPhone + */ + +/*! + * TODO: These take forever to boot or shut down. Causes timeout. + * + +exports['SL_iPhone_6'] = { + base: 'SauceLabs' + , browserName: 'iphone' + , platform: 'Mac 10.8' + , version: '6' +}; + +exports['SL_iPhone_5-1'] = { + base: 'SauceLabs' + , browserName: 'iphone' + , platform: 'Mac 10.8' + , version: '5.1' +}; + +exports['SL_iPhone_5'] = { + base: 'SauceLabs' + , browserName: 'iphone' + , platform: 'Mac 10.6' + , version: '5' +}; + +*/ + +/*! + * Android + */ + +/*! + * TODO: fails because of error serialization + * + +exports['SL_Android_4'] = { + base: 'SauceLabs' + , browserName: 'android' + , platform: 'Linux' + , version: '4' +}; + +*/ diff --git a/tests/tools/htmltest.js b/tests/tools/htmltest.js new file mode 100644 index 0000000..e5e1e02 --- /dev/null +++ b/tests/tools/htmltest.js @@ -0,0 +1,39 @@ +/* + * Copyright 2013 The Polymer Authors. All rights reserved. + * Use of this source code is governed by a BSD-style + * license that can be found in the LICENSE file. + */ + +// if standalone +if (window.top === window) { + // if standalone + var failed = false; + window.done = function() { + window.onerror = null; + if (!failed) { + var d = document.createElement('pre'); + d.style.cssText = 'padding: 6px; background-color: lightgreen;'; + d.textContent = 'Passed'; + document.body.appendChild(d); + } + }; + window.onerror = function(x) { + failed = true; + var d = document.createElement('pre'); + d.style.cssText = 'padding: 6px; background-color: #FFE0E0;'; + d.textContent = 'FAILED: ' + x; + document.body.appendChild(d); + }; +} else +// if part of a test suite +{ + window.done = function() { + window.onerror = null; + parent.postMessage('ok', '*'); + }; + + window.onerror = function(x) { + parent.postMessage({error: x}, '*'); + }; +} + diff --git a/tests/tools/mocha-htmltest.js b/tests/tools/mocha-htmltest.js new file mode 100644 index 0000000..5e7a274 --- /dev/null +++ b/tests/tools/mocha-htmltest.js @@ -0,0 +1,83 @@ +/* + * Copyright 2013 The Polymer Authors. All rights reserved. + * Use of this source code is governed by a BSD-style + * license that can be found in the LICENSE file. + */ + +(function() { + var thisFile = 'lib/mocha-htmltest.js'; + var base = ''; + + mocha.htmlbase = function(htmlbase) { + base = htmlbase; + }; + + (function() { + var s$ = document.querySelectorAll('script[src]'); + Array.prototype.forEach.call(s$, function(s) { + var src = s.getAttribute('src'); + var re = new RegExp(thisFile + '[^\\\\]*'); + var match = src.match(re); + if (match) { + base = src.slice(0, -match[0].length); + } + }); + })(); + + var next, iframe; + + var listener = function(event) { + if (event.data === 'ok') { + next(); + } else if (event.data && event.data.error) { + // errors cannot be cloned via postMessage according to spec, so we re-errorify them + throw new Error(event.data.error); + } + }; + + function htmlSetup() { + window.addEventListener("message", listener); + iframe = document.createElement('iframe'); + iframe.style.cssText = 'position: absolute; left: -9000em; width:768px; height: 1024px'; + document.body.appendChild(iframe); + } + + function htmlTeardown() { + window.removeEventListener('message', listener); + document.body.removeChild(iframe); + } + + function htmlTest(src) { + var basePath = calcBase(); + test(src, function(done) { + next = done; + var url = basePath + src; + var delimiter = url.indexOf('?') < 0 ? '?' : '&'; + var docSearch = location.search.slice(1); + iframe.src = url + delimiter + Math.random() + '&' + docSearch; + }); + }; + + function htmlSuite(inName, inFn) { + suite(inName, function() { + setup(htmlSetup); + teardown(htmlTeardown); + inFn(); + }); + }; + + function calcBase() { + var b = base; + var script = document._currentScript || + document.scripts[document.scripts.length - 1]; + if (script) { + var parts = script.src.split('/'); + parts.pop(); + b = parts.join('/') + '/'; + } + return b; + } + + window.htmlTest = htmlTest; + window.htmlSuite = htmlSuite; +})(); diff --git a/tests/tools/mocha/.bower.json b/tests/tools/mocha/.bower.json new file mode 100644 index 0000000..ac412a0 --- /dev/null +++ b/tests/tools/mocha/.bower.json @@ -0,0 +1,33 @@ +{ + "name": "mocha", + "version": "1.20.1", + "main": [ + "mocha.js", + "mocha.css" + ], + "ignore": [ + "bin", + "editors", + "images", + "lib", + "support", + "test", + ".gitignore", + ".npmignore", + ".travis.yml", + "component.json", + "index.js", + "Makefile", + "package.json" + ], + "homepage": "https://github.com/visionmedia/mocha", + "_release": "1.20.1", + "_resolution": { + "type": "version", + "tag": "1.20.1", + "commit": "869b5ad681b923a75c5e6fb3fcbab9a865b4115e" + }, + "_source": "git://github.com/visionmedia/mocha.git", + "_target": ">=1.13.0", + "_originalSource": "mocha" +} \ No newline at end of file diff --git a/tests/tools/mocha/History.md b/tests/tools/mocha/History.md new file mode 100644 index 0000000..7ab50bd --- /dev/null +++ b/tests/tools/mocha/History.md @@ -0,0 +1,692 @@ +1.20.1 / 2014-06-03 +================== + + * update: should dependency to ~4.0.0 (#1231) + +1.20.0 / 2014-05-28 +================== + + * add: filenames to suite objects (#1222) + +1.19.0 / 2014-05-17 +================== + + * add: browser script option to package.json + * add: export file in Mocha.Test objects (#1174) + * add: add docs for wrapped node flags + * fix: mocha.run() to return error status in browser (#1216) + * fix: clean() to show failure details (#1205) + * fix: regex that generates html for new keyword (#1201) + * fix: sibling suites have inherited but separate contexts (#1164) + + +1.18.2 / 2014-03-18 +================== + + * fix: html runner was prevented from using #mocha as the default root el (#1162) + +1.18.1 / 2014-03-18 +================== + + * fix: named before/after hooks in bdd, tdd, qunit interfaces (#1161) + +1.18.0 / 2014-03-13 +================== + + * add: promise support (#329) + * add: named before/after hooks (#966) + +1.17.1 / 2014-01-22 +================== + + * fix: expected messages in should.js (should.js#168) + * fix: expect errno global in node versions < v0.9.11 (#1111) + * fix: unreliable checkGlobals optimization (#1110) + +1.17.0 / 2014-01-09 +================== + + * add: able to require globals (describe, it, etc.) through mocha (#1077) + * fix: abort previous run on --watch change (#1100) + * fix: reset context for each --watch triggered run (#1099) + * fix: error when cli can't resolve path or pattern (#799) + * fix: canonicalize objects before stringifying and diffing them (#1079) + * fix: make CR call behave like carriage return for non tty (#1087) + + +1.16.2 / 2013-12-23 +================== + + * fix: couple issues with ie 8 (#1082, #1081) + * fix: issue running the xunit reporter in browsers (#1068) + * fix: issue with firefox < 3.5 (#725) + + +1.16.1 / 2013-12-19 +================== + + * fix: recompiled for missed changes from the last release + + +1.16.0 / 2013-12-19 +================== + + * add: Runnable.globals(arr) for per test global whitelist (#1046) + * add: mocha.throwError(err) for assertion libs to call (#985) + * remove: --watch's spinner (#806) + * fix: duplicate test output for multi-line specs in spec reporter (#1006) + * fix: gracefully exit on SIGINT (#1063) + * fix expose the specified ui only in the browser (#984) + * fix: ensure process exit code is preserved when using --no-exit (#1059) + * fix: return true from window.onerror handler (#868) + * fix: xunit reporter to use process.stdout.write (#1068) + * fix: utils.clean(str) indentation (#761) + * fix: xunit reporter returning test duration a NaN (#1039) + +1.15.1 / 2013-12-03 +================== + + * fix: recompiled for missed changes from the last release + +1.15.0 / 2013-12-02 +================== + + * add: `--no-exit` to prevent `process.exit()` (#1018) + * fix: using inline diffs (#1044) + * fix: show pending test details in xunit reporter (#1051) + * fix: faster global leak detection (#1024) + * fix: yui compression (#1035) + * fix: wrapping long lines in test results (#1030, #1031) + * fix: handle errors in hooks (#1043) + +1.14.0 / 2013-11-02 +================== + + * add: unified diff (#862) + * add: set MOCHA_COLORS env var to use colors (#965) + * add: able to override tests links in html reporters (#776) + * remove: teamcity reporter (#954) + * update: commander dependency to 2.0.0 (#1010) + * fix: mocha --ui will try to require the ui if not built in, as --reporter does (#1022) + * fix: send cursor commands only if isatty (#184, #1003) + * fix: include assertion message in base reporter (#993, #991) + * fix: consistent return of it, it.only, and describe, describe.only (#840) + +1.13.0 / 2013-09-15 +================== + + * add: sort test files with --sort (#813) + * update: diff depedency to 1.0.7 + * update: glob dependency to 3.2.3 (#927) + * fix: diffs show whitespace differences (#976) + * fix: improve global leaks (#783) + * fix: firefox window.getInterface leak + * fix: accessing iframe via window[iframeIndex] leak + * fix: faster global leak checking + * fix: reporter pending css selector (#970) + +1.12.1 / 2013-08-29 +================== + + * remove test.js from .gitignore + * update included version of ms.js + +1.12.0 / 2013-07-01 +================== + + * add: prevent diffs for differing types. Closes #900 + * add `Mocha.process` hack for phantomjs + * fix: use compilers with requires + * fix regexps in diffs. Closes #890 + * fix xunit NaN on failure. Closes #894 + * fix: strip tab indentation in `clean` utility method + * fix: textmate bundle installation + +1.11.0 / 2013-06-12 +================== + + * add --prof support + * add --harmony support + * add --harmony-generators support + * add "Uncaught " prefix to uncaught exceptions + * add web workers support + * add `suite.skip()` + * change to output # of pending / passing even on failures. Closes #872 + * fix: prevent hooks from being called if we are bailing + * fix `this.timeout(0)` + +1.10.0 / 2013-05-21 +================== + + * add add better globbing support for windows via `glob` module + * add support to pass through flags such as --debug-brk=1234. Closes #852 + * add test.only, test.skip to qunit interface + * change to always use word-based diffs for now. Closes #733 + * change `mocha init` tests.html to index.html + * fix `process` global leak in the browser + * fix: use resolve() instead of join() for --require + * fix: filterLeaks() condition to not consider indices in global object as leaks + * fix: restrict mocha.css styling to #mocha id + * fix: save timer references to avoid Sinon interfering in the browser build. + +1.9.0 / 2013-04-03 +================== + + * add improved setImmediate implementation + * replace --ignore-leaks with --check-leaks + * change default of ignoreLeaks to true. Closes #791 + * remove scrolling for HTML reporter + * fix retina support + * fix tmbundle, restrict to js scope + +1.8.2 / 2013-03-11 +================== + + * add `setImmediate` support for 0.10.x + * fix mocha -w spinner on windows + +1.8.1 / 2013-01-09 +================== + + * fix .bail() arity check causing it to default to true + +1.8.0 / 2013-01-08 +================== + + * add Mocha() options bail support + * add `Mocha#bail()` method + * add instanceof check back for inheriting from Error + * add component.json + * add diff.js to browser build + * update growl + * fix TAP reporter failures comment :D + +1.7.4 / 2012-12-06 +================== + + * add total number of passes and failures to TAP + * remove .bind() calls. re #680 + * fix indexOf. Closes #680 + +1.7.3 / 2012-11-30 +================== + + * fix uncaught error support for the browser + * revert uncaught "fix" which breaks node + +1.7.2 / 2012-11-28 +================== + + * fix uncaught errors to expose the original error message + +1.7.0 / 2012-11-07 +================== + + * add `--async-only` support to prevent false positives for missing `done()` + * add sorting by filename in code coverage + * add HTML 5 doctype to browser template. + * add play button to html reporter to rerun a single test + * add `this.timeout(ms)` as Suite#timeout(ms). Closes #599 + * update growl dependency to 1.6.x + * fix encoding of test-case ?grep. Closes #637 + * fix unicode chars on windows + * fix dom globals in Opera/IE. Closes #243 + * fix markdown reporter a tags + * fix `this.timeout("5s")` support + +1.6.0 / 2012-10-02 +================== + + * add object diffs when `err.showDiff` is present + * add hiding of empty suites when pass/failures are toggled + * add faster `.length` checks to `checkGlobals()` before performing the filter + +1.5.0 / 2012-09-21 +================== + + * add `ms()` to `.slow()` and `.timeout()` + * add `Mocha#checkLeaks()` to re-enable global leak checks + * add `this.slow()` option [aheckmann] + * add tab, CR, LF to error diffs for now + * add faster `.checkGlobals()` solution [guille] + * remove `fn.call()` from reduce util + * remove `fn.call()` from filter util + * fix forEach. Closes #582 + * fix relaying of signals [TooTallNate] + * fix TAP reporter grep number + +1.4.2 / 2012-09-01 +================== + + * add support to multiple `Mocha#globals()` calls, and strings + * add `mocha.reporter()` constructor support [jfirebaugh] + * add `mocha.timeout()` + * move query-string parser to utils.js + * move highlight code to utils.js + * fix third-party reporter support [exogen] + * fix client-side API to match node-side [jfirebaugh] + * fix mocha in iframe [joliss] + +1.4.1 / 2012-08-28 +================== + + * add missing `Markdown` export + * fix `Mocha#grep()`, escape regexp strings + * fix reference error when `devicePixelRatio` is not defined. Closes #549 + +1.4.0 / 2012-08-22 +================== + + * add mkdir -p to `mocha init`. Closes #539 + * add `.only()`. Closes #524 + * add `.skip()`. Closes #524 + * change str.trim() to use utils.trim(). Closes #533 + * fix HTML progress indicator retina display + * fix url-encoding of click-to-grep HTML functionality + +1.3.2 / 2012-08-01 +================== + + * fix exports double-execution regression. Closes #531 + +1.3.1 / 2012-08-01 +================== + + * add passes/failures toggling to HTML reporter + * add pending state to `xit()` and `xdescribe()` [Brian Moore] + * add the @charset "UTF-8"; to fix #522 with FireFox. [Jonathan Creamer] + * add border-bottom to #stats links + * add check for runnable in `Runner#uncaught()`. Closes #494 + * add 0.4 and 0.6 back to travis.yml + * add `-E, --growl-errors` to growl on failures only + * add prefixes to debug() names. Closes #497 + * add `Mocha#invert()` to js api + * change dot reporter to use sexy unicode dots + * fix error when clicking pending test in HTML reporter + * fix `make tm` + +1.3.0 / 2012-07-05 +================== + + * add window scrolling to `HTML` reporter + * add v8 `--trace-*` option support + * add support for custom reports via `--reporter MODULE` + * add `--invert` switch to invert `--grep` matches + * fix export of `Nyan` reporter. Closes #495 + * fix escaping of `HTML` suite titles. Closes #486 + * fix `done()` called multiple times with an error test + * change `--grep` - regexp escape the input + +1.2.2 / 2012-06-28 +================== + + * Added 0.8.0 support + +1.2.1 / 2012-06-25 +================== + + * Added `this.test.error(err)` support to after each hooks. Closes #287 + * Added: export top-level suite on global mocha object (mocha.suite). Closes #448 + * Fixed `js` code block format error in markdown reporter + * Fixed deprecation warning when using `path.existsSync` + * Fixed --globals with wildcard + * Fixed chars in nyan when his head moves back + * Remove `--growl` from test/mocha.opts. Closes #289 + +1.2.0 / 2012-06-17 +================== + + * Added `nyan` reporter [Atsuya Takagi] + * Added `mocha init ` to copy client files + * Added "specify" synonym for "it" [domenic] + * Added global leak wildcard support [nathanbowser] + * Fixed runner emitter leak. closes #432 + * Fixed omission of .js extension. Closes #454 + +1.1.0 / 2012-05-30 +================== + + * Added: check each `mocha(1)` arg for directories to walk + * Added `--recursive` [tricknotes] + * Added `context` for BDD [hokaccha] + * Added styling for new clickable titles + * Added clickable suite titles to HTML reporter + * Added warning when strings are thrown as errors + * Changed: green arrows again in HTML reporter styling + * Changed ul/li elements instead of divs for better copy-and-pasting [joliss] + * Fixed issue #325 - add better grep support to js api + * Fixed: save timer references to avoid Sinon interfering. + +1.0.3 / 2012-04-30 +================== + + * Fixed string diff newlines + * Fixed: removed mocha.css target. Closes #401 + +1.0.2 / 2012-04-25 +================== + + * Added HTML reporter duration. Closes #47 + * Fixed: one postMessage event listener [exogen] + * Fixed: allow --globals to be used multiple times. Closes #100 [brendannee] + * Fixed #158: removes jquery include from browser tests + * Fixed grep. Closes #372 [brendannee] + * Fixed #166 - When grepping don't display the empty suites + * Removed test/browser/style.css. Closes #385 + +1.0.1 / 2012-04-04 +================== + + * Fixed `.timeout()` in hooks + * Fixed: allow callback for `mocha.run()` in client version + * Fixed browser hook error display. Closes #361 + +1.0.0 / 2012-03-24 +================== + + * Added js API. Closes #265 + * Added: initial run of tests with `--watch`. Closes #345 + * Added: mark `location` as a global on the CS. Closes #311 + * Added `markdown` reporter (github flavour) + * Added: scrolling menu to coverage.html. Closes #335 + * Added source line to html report for Safari [Tyson Tate] + * Added "min" reporter, useful for `--watch` [Jakub Nešetřil] + * Added support for arbitrary compilers via . Closes #338 [Ian Young] + * Added Teamcity export to lib/reporters/index [Michael Riley] + * Fixed chopping of first char in error reporting. Closes #334 [reported by topfunky] + * Fixed terrible FF / Opera stack traces + +0.14.1 / 2012-03-06 +================== + + * Added lib-cov to _.npmignore_ + * Added reporter to `mocha.run([reporter])` as argument + * Added some margin-top to the HTML reporter + * Removed jQuery dependency + * Fixed `--watch`: purge require cache. Closes #266 + +0.14.0 / 2012-03-01 +================== + + * Added string diff support for terminal reporters + +0.13.0 / 2012-02-23 +================== + + * Added preliminary test coverage support. Closes #5 + * Added `HTMLCov` reporter + * Added `JSONCov` reporter [kunklejr] + * Added `xdescribe()` and `xit()` to the BDD interface. Closes #263 (docs * Changed: make json reporter output pretty json + * Fixed node-inspector support, swapped `--debug` for `debug` to match node. +needed) +Closes #247 + +0.12.1 / 2012-02-14 +================== + + * Added `npm docs mocha` support [TooTallNate] + * Added a `Context` object used for hook and test-case this. Closes #253 + * Fixed `Suite#clone()` `.ctx` reference. Closes #262 + +0.12.0 / 2012-02-02 +================== + + * Added .coffee `--watch` support. Closes #242 + * Added support to `--require` files relative to the CWD. Closes #241 + * Added quick n dirty syntax highlighting. Closes #248 + * Changed: made HTML progress indicator smaller + * Fixed xunit errors attribute [dhendo] + +0.10.2 / 2012-01-21 +================== + + * Fixed suite count in reporter stats. Closes #222 + * Fixed `done()` after timeout error reporting [Phil Sung] + * Changed the 0-based errors to 1 + +0.10.1 / 2012-01-17 +================== + + * Added support for node 0.7.x + * Fixed absolute path support. Closes #215 [kompiro] + * Fixed `--no-colors` option [Jussi Virtanen] + * Fixed Arial CSS typo in the correct file + +0.10.0 / 2012-01-13 +================== + + * Added `-b, --bail` to exit on first exception [guillermo] + * Added support for `-gc` / `--expose-gc` [TooTallNate] + * Added `qunit`-inspired interface + * Added MIT LICENSE. Closes #194 + * Added: `--watch` all .js in the CWD. Closes #139 + * Fixed `self.test` reference in runner. Closes #189 + * Fixed double reporting of uncaught exceptions after timeout. Closes #195 + +0.8.2 / 2012-01-05 +================== + + * Added test-case context support. Closes #113 + * Fixed exit status. Closes #187 + * Update commander. Closes #190 + +0.8.1 / 2011-12-30 +================== + + * Fixed reporting of uncaught exceptions. Closes #183 + * Fixed error message defaulting [indutny] + * Changed mocha(1) from bash to node for windows [Nathan Rajlich] + +0.8.0 / 2011-12-28 +================== + + * Added `XUnit` reporter [FeeFighters/visionmedia] + * Added `say(1)` notification support [Maciej Małecki] + * Changed: fail when done() is invoked with a non-Error. Closes #171 + * Fixed `err.stack`, defaulting to message. Closes #180 + * Fixed: `make tm` mkdir -p the dest. Closes #137 + * Fixed mocha(1) --help bin name + * Fixed `-d` for `--debug` support + +0.7.1 / 2011-12-22 +================== + + * Removed `mocha-debug(1)`, use `mocha --debug` + * Fixed CWD relative requires + * Fixed growl issue on windows [Raynos] + * Fixed: platform specific line endings [TooTallNate] + * Fixed: escape strings in HTML reporter. Closes #164 + +0.7.0 / 2011-12-18 +================== + + * Added support for IE{7,8} [guille] + * Changed: better browser nextTick implementation [guille] + +0.6.0 / 2011-12-18 +================== + + * Added setZeroTimeout timeout for browser (nicer stack traces). Closes #153 + * Added "view source" on hover for HTML reporter to make it obvious + * Changed: replace custom growl with growl lib + * Fixed duplicate reporting for HTML reporter. Closes #154 + * Fixed silent hook errors in the HTML reporter. Closes #150 + +0.5.0 / 2011-12-14 +================== + + * Added: push node_modules directory onto module.paths for relative require Closes #93 + * Added teamcity reporter [blindsey] + * Fixed: recover from uncaught exceptions for tests. Closes #94 + * Fixed: only emit "test end" for uncaught within test, not hook + +0.4.0 / 2011-12-14 +================== + + * Added support for test-specific timeouts via `this.timeout(0)`. Closes #134 + * Added guillermo's client-side EventEmitter. Closes #132 + * Added progress indicator to the HTML reporter + * Fixed slow browser tests. Closes #135 + * Fixed "suite" color for light terminals + * Fixed `require()` leak spotted by [guillermo] + +0.3.6 / 2011-12-09 +================== + + * Removed suite merging (for now) + +0.3.5 / 2011-12-08 +================== + + * Added support for `window.onerror` [guillermo] + * Fixed: clear timeout on uncaught exceptions. Closes #131 [guillermo] + * Added `mocha.css` to PHONY list. + * Added `mocha.js` to PHONY list. + +0.3.4 / 2011-12-08 +================== + + * Added: allow `done()` to be called with non-Error + * Added: return Runner from `mocha.run()`. Closes #126 + * Fixed: run afterEach even on failures. Closes #125 + * Fixed clobbering of current runnable. Closes #121 + +0.3.3 / 2011-12-08 +================== + + * Fixed hook timeouts. Closes #120 + * Fixed uncaught exceptions in hooks + +0.3.2 / 2011-12-05 +================== + + * Fixed weird reporting when `err.message` is not present + +0.3.1 / 2011-12-04 +================== + + * Fixed hook event emitter leak. Closes #117 + * Fixed: export `Spec` constructor. Closes #116 + +0.3.0 / 2011-12-04 +================== + + * Added `-w, --watch`. Closes #72 + * Added `--ignore-leaks` to ignore global leak checking + * Added browser `?grep=pattern` support + * Added `--globals ` to specify accepted globals. Closes #99 + * Fixed `mocha-debug(1)` on some systems. Closes #232 + * Fixed growl total, use `runner.total` + +0.2.0 / 2011-11-30 +================== + + * Added `--globals ` to specify accepted globals. Closes #99 + * Fixed funky highlighting of messages. Closes #97 + * Fixed `mocha-debug(1)`. Closes #232 + * Fixed growl total, use runner.total + +0.1.0 / 2011-11-29 +================== + + * Added `suiteSetup` and `suiteTeardown` to TDD interface [David Henderson] + * Added growl icons. Closes #84 + * Fixed coffee-script support + +0.0.8 / 2011-11-25 +================== + + * Fixed: use `Runner#total` for accurate reporting + +0.0.7 / 2011-11-25 +================== + + * Added `Hook` + * Added `Runnable` + * Changed: `Test` is `Runnable` + * Fixed global leak reporting in hooks + * Fixed: > 2 calls to done() only report the error once + * Fixed: clear timer on failure. Closes #80 + +0.0.6 / 2011-11-25 +================== + + * Fixed return on immediate async error. Closes #80 + +0.0.5 / 2011-11-24 +================== + + * Fixed: make mocha.opts whitespace less picky [kkaefer] + +0.0.4 / 2011-11-24 +================== + + * Added `--interfaces` + * Added `--reporters` + * Added `-c, --colors`. Closes #69 + * Fixed hook timeouts + +0.0.3 / 2011-11-23 +================== + + * Added `-C, --no-colors` to explicitly disable + * Added coffee-script support + +0.0.2 / 2011-11-22 +================== + + * Fixed global leak detection due to Safari bind() change + * Fixed: escape html entities in Doc reporter + * Fixed: escape html entities in HTML reporter + * Fixed pending test support for HTML reporter. Closes #66 + +0.0.1 / 2011-11-22 +================== + + * Added `--timeout` second shorthand support, ex `--timeout 3s`. + * Fixed "test end" event for uncaughtExceptions. Closes #61 + +0.0.1-alpha6 / 2011-11-19 +================== + + * Added travis CI support (needs enabling when public) + * Added preliminary browser support + * Added `make mocha.css` target. Closes #45 + * Added stack trace to TAP errors. Closes #52 + * Renamed tearDown to teardown. Closes #49 + * Fixed: cascading hooksc. Closes #30 + * Fixed some colors for non-tty + * Fixed errors thrown in sync test-cases due to nextTick + * Fixed Base.window.width... again give precedence to 0.6.x + +0.0.1-alpha5 / 2011-11-17 +================== + + * Added `doc` reporter. Closes #33 + * Added suite merging. Closes #28 + * Added TextMate bundle and `make tm`. Closes #20 + +0.0.1-alpha4 / 2011-11-15 +================== + + * Fixed getWindowSize() for 0.4.x + +0.0.1-alpha3 / 2011-11-15 +================== + + * Added `-s, --slow ` to specify "slow" test threshold + * Added `mocha-debug(1)` + * Added `mocha.opts` support. Closes #31 + * Added: default [files] to _test/*.js_ + * Added protection against multiple calls to `done()`. Closes #35 + * Changed: bright yellow for slow Dot reporter tests + +0.0.1-alpha1 / 2011-11-08 +================== + + * Missed this one :) + +0.0.1-alpha1 / 2011-11-08 +================== + + * Initial release diff --git a/tests/tools/mocha/LICENSE b/tests/tools/mocha/LICENSE new file mode 100644 index 0000000..1c5d7fa --- /dev/null +++ b/tests/tools/mocha/LICENSE @@ -0,0 +1,22 @@ +(The MIT License) + +Copyright (c) 2011-2014 TJ Holowaychuk + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/tests/tools/mocha/Readme.md b/tests/tools/mocha/Readme.md new file mode 100644 index 0000000..9340cac --- /dev/null +++ b/tests/tools/mocha/Readme.md @@ -0,0 +1,172 @@ + [![Build Status](https://secure.travis-ci.org/visionmedia/mocha.png)](http://travis-ci.org/visionmedia/mocha) + + [![Mocha test framework](http://f.cl.ly/items/3l1k0n2A1U3M1I1L210p/Screen%20Shot%202012-02-24%20at%202.21.43%20PM.png)](http://visionmedia.github.io/mocha) + + Mocha is a simple, flexible, fun JavaScript test framework for node.js and the browser. For more information view the [documentation](http://visionmedia.github.io/mocha). + +## Contributors + +``` + + project : mocha + repo age : 2 years, 4 months ago + commits : 1314 + active : 372 days + files : 141 + authors : + 582 TJ Holowaychuk 44.3% + 389 Tj Holowaychuk 29.6% + 46 Travis Jeffery 3.5% + 31 Guillermo Rauch 2.4% + 13 Attila Domokos 1.0% + 10 John Firebaugh 0.8% + 8 Jo Liss 0.6% + 7 Nathan Rajlich 0.5% + 6 Mike Pennisi 0.5% + 6 James Carr 0.5% + 6 Brendan Nee 0.5% + 5 Aaron Heckmann 0.4% + 5 Ryunosuke SATO 0.4% + 4 hokaccha 0.3% + 4 Joshua Krall 0.3% + 4 Xavier Antoviaque 0.3% + 3 Jesse Dailey 0.2% + 3 Forbes Lindesay 0.2% + 3 Sindre Sorhus 0.2% + 3 Cory Thomas 0.2% + 3 Fredrik Enestad 0.2% + 3 Ben Lindsey 0.2% + 3 Tyson Tate 0.2% + 3 Mathieu Desvé 0.2% + 3 Valentin Agachi 0.2% + 3 Wil Moore III 0.2% + 3 Merrick Christensen 0.2% + 3 eiji.ienaga 0.2% + 3 fool2fish 0.2% + 3 Nathan Bowser 0.2% + 3 Paul Miller 0.2% + 2 Juzer Ali 0.2% + 2 Pete Hawkins 0.2% + 2 Jonas Westerlund 0.2% + 2 Arian Stolwijk 0.2% + 2 Quang Van 0.2% + 2 Glen Mailer 0.2% + 2 Justin DuJardin 0.2% + 2 FARKAS Máté 0.2% + 2 Raynos 0.2% + 2 Michael Riley 0.2% + 2 Michael Schoonmaker 0.2% + 2 Domenic Denicola 0.2% + 2 Simon Gaeremynck 0.2% + 2 Konstantin Käfer 0.2% + 2 domenic 0.2% + 2 Paul Armstrong 0.2% + 2 fcrisci 0.2% + 2 Alexander Early 0.2% + 2 Shawn Krisman 0.2% + 2 Brian Beck 0.2% + 2 Nathan Alderson 0.2% + 2 David Henderson 0.2% + 2 Timo Tijhof 0.2% + 2 Ian Storm Taylor 0.2% + 2 travis jeffery 0.2% + 1 Matt Smith 0.1% + 1 Matthew Shanley 0.1% + 1 Nathan Black 0.1% + 1 Phil Sung 0.1% + 1 R56 0.1% + 1 Refael Ackermann 0.1% + 1 Richard Dingwall 0.1% + 1 Romain Prieto 0.1% + 1 Roman Neuhauser 0.1% + 1 Roman Shtylman 0.1% + 1 Russ Bradberry 0.1% + 1 Russell Munson 0.1% + 1 Rustem Mustafin 0.1% + 1 Salehen Shovon Rahman 0.1% + 1 Sasha Koss 0.1% + 1 Seiya Konno 0.1% + 1 Simon Goumaz 0.1% + 1 Standa Opichal 0.1% + 1 Stephen Mathieson 0.1% + 1 Steve Mason 0.1% + 1 Tapiwa Kelvin 0.1% + 1 Teddy Zeenny 0.1% + 1 Tim Ehat 0.1% + 1 Vadim Nikitin 0.1% + 1 Victor Costan 0.1% + 1 Will Langstroth 0.1% + 1 Yanis Wang 0.1% + 1 Yuest Wang 0.1% + 1 abrkn 0.1% + 1 airportyh 0.1% + 1 badunk 0.1% + 1 fengmk2 0.1% + 1 grasGendarme 0.1% + 1 lodr 0.1% + 1 tgautier@yahoo.com 0.1% + 1 traleig1 0.1% + 1 vlad 0.1% + 1 yuitest 0.1% + 1 Adam Crabtree 0.1% + 1 Andreas Brekken 0.1% + 1 Andreas Lind Petersen 0.1% + 1 Andrew Nesbitt 0.1% + 1 Andrey Popp 0.1% + 1 Arnaud Brousseau 0.1% + 1 Atsuya Takagi 0.1% + 1 Austin Birch 0.1% + 1 Bjørge Næss 0.1% + 1 Brian Lalor 0.1% + 1 Brian M. Carlson 0.1% + 1 Brian Moore 0.1% + 1 Bryan Donovan 0.1% + 1 Casey Foster 0.1% + 1 ChrisWren 0.1% + 1 Corey Butler 0.1% + 1 Daniel Stockman 0.1% + 1 Dave McKenna 0.1% + 1 Di Wu 0.1% + 1 Dmitry Shirokov 0.1% + 1 Fedor Indutny 0.1% + 1 Florian Margaine 0.1% + 1 Frederico Silva 0.1% + 1 Fredrik Lindin 0.1% + 1 Gareth Murphy 0.1% + 1 Gavin Mogan 0.1% + 1 Glen Huang 0.1% + 1 Greg Perkins 0.1% + 1 Harry Brundage 0.1% + 1 Herman Junge 0.1% + 1 Ian Young 0.1% + 1 Ivan 0.1% + 1 JP Bochi 0.1% + 1 Jaakko Salonen 0.1% + 1 Jakub Nešetřil 0.1% + 1 James Bowes 0.1% + 1 James Lal 0.1% + 1 Jason Barry 0.1% + 1 Javier Aranda 0.1% + 1 Jeff Kunkle 0.1% + 1 Jeremy Martin 0.1% + 1 Jimmy Cuadra 0.1% + 1 Jonathan Creamer 0.1% + 1 Jussi Virtanen 0.1% + 1 Katie Gengler 0.1% + 1 Kazuhito Hokamura 0.1% + 1 Kirill Korolyov 0.1% + 1 Koen Punt 0.1% + 1 Laszlo Bacsi 0.1% + 1 Liam Newman 0.1% + 1 László Bácsi 0.1% + 1 Maciej Małecki 0.1% + 1 Mal Graty 0.1% + 1 Marc Kuo 0.1% + 1 Matt Robenolt 0.1% +``` + +## Links + + - [Google Group](http://groups.google.com/group/mochajs) + - [Wiki](https://github.com/visionmedia/mocha/wiki) + - Mocha [Extensions and reporters](https://github.com/visionmedia/mocha/wiki) diff --git a/tests/tools/mocha/bower.json b/tests/tools/mocha/bower.json new file mode 100644 index 0000000..b6e3fd7 --- /dev/null +++ b/tests/tools/mocha/bower.json @@ -0,0 +1,23 @@ +{ + "name": "mocha", + "version": "1.20.1", + "main": [ + "mocha.js", + "mocha.css" + ], + "ignore": [ + "bin", + "editors", + "images", + "lib", + "support", + "test", + ".gitignore", + ".npmignore", + ".travis.yml", + "component.json", + "index.js", + "Makefile", + "package.json" + ] +} diff --git a/tests/tools/mocha/media/logo.svg b/tests/tools/mocha/media/logo.svg new file mode 100644 index 0000000..8770b08 --- /dev/null +++ b/tests/tools/mocha/media/logo.svg @@ -0,0 +1,8 @@ + + + + + +mocha + diff --git a/tests/tools/mocha/mocha.css b/tests/tools/mocha/mocha.css new file mode 100644 index 0000000..42b9798 --- /dev/null +++ b/tests/tools/mocha/mocha.css @@ -0,0 +1,270 @@ +@charset "utf-8"; + +body { + margin:0; +} + +#mocha { + font: 20px/1.5 "Helvetica Neue", Helvetica, Arial, sans-serif; + margin: 60px 50px; +} + +#mocha ul, +#mocha li { + margin: 0; + padding: 0; +} + +#mocha ul { + list-style: none; +} + +#mocha h1, +#mocha h2 { + margin: 0; +} + +#mocha h1 { + margin-top: 15px; + font-size: 1em; + font-weight: 200; +} + +#mocha h1 a { + text-decoration: none; + color: inherit; +} + +#mocha h1 a:hover { + text-decoration: underline; +} + +#mocha .suite .suite h1 { + margin-top: 0; + font-size: .8em; +} + +#mocha .hidden { + display: none; +} + +#mocha h2 { + font-size: 12px; + font-weight: normal; + cursor: pointer; +} + +#mocha .suite { + margin-left: 15px; +} + +#mocha .test { + margin-left: 15px; + overflow: hidden; +} + +#mocha .test.pending:hover h2::after { + content: '(pending)'; + font-family: arial, sans-serif; +} + +#mocha .test.pass.medium .duration { + background: #c09853; +} + +#mocha .test.pass.slow .duration { + background: #b94a48; +} + +#mocha .test.pass::before { + content: '✓'; + font-size: 12px; + display: block; + float: left; + margin-right: 5px; + color: #00d6b2; +} + +#mocha .test.pass .duration { + font-size: 9px; + margin-left: 5px; + padding: 2px 5px; + color: #fff; + -webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.2); + -moz-box-shadow: inset 0 1px 1px rgba(0,0,0,.2); + box-shadow: inset 0 1px 1px rgba(0,0,0,.2); + -webkit-border-radius: 5px; + -moz-border-radius: 5px; + -ms-border-radius: 5px; + -o-border-radius: 5px; + border-radius: 5px; +} + +#mocha .test.pass.fast .duration { + display: none; +} + +#mocha .test.pending { + color: #0b97c4; +} + +#mocha .test.pending::before { + content: '◦'; + color: #0b97c4; +} + +#mocha .test.fail { + color: #c00; +} + +#mocha .test.fail pre { + color: black; +} + +#mocha .test.fail::before { + content: '✖'; + font-size: 12px; + display: block; + float: left; + margin-right: 5px; + color: #c00; +} + +#mocha .test pre.error { + color: #c00; + max-height: 300px; + overflow: auto; +} + +/** + * (1): approximate for browsers not supporting calc + * (2): 42 = 2*15 + 2*10 + 2*1 (padding + margin + border) + * ^^ seriously + */ +#mocha .test pre { + display: block; + float: left; + clear: left; + font: 12px/1.5 monaco, monospace; + margin: 5px; + padding: 15px; + border: 1px solid #eee; + max-width: 85%; /*(1)*/ + max-width: calc(100% - 42px); /*(2)*/ + word-wrap: break-word; + border-bottom-color: #ddd; + -webkit-border-radius: 3px; + -webkit-box-shadow: 0 1px 3px #eee; + -moz-border-radius: 3px; + -moz-box-shadow: 0 1px 3px #eee; + border-radius: 3px; +} + +#mocha .test h2 { + position: relative; +} + +#mocha .test a.replay { + position: absolute; + top: 3px; + right: 0; + text-decoration: none; + vertical-align: middle; + display: block; + width: 15px; + height: 15px; + line-height: 15px; + text-align: center; + background: #eee; + font-size: 15px; + -moz-border-radius: 15px; + border-radius: 15px; + -webkit-transition: opacity 200ms; + -moz-transition: opacity 200ms; + transition: opacity 200ms; + opacity: 0.3; + color: #888; +} + +#mocha .test:hover a.replay { + opacity: 1; +} + +#mocha-report.pass .test.fail { + display: none; +} + +#mocha-report.fail .test.pass { + display: none; +} + +#mocha-report.pending .test.pass, +#mocha-report.pending .test.fail { + display: none; +} +#mocha-report.pending .test.pass.pending { + display: block; +} + +#mocha-error { + color: #c00; + font-size: 1.5em; + font-weight: 100; + letter-spacing: 1px; +} + +#mocha-stats { + position: fixed; + top: 15px; + right: 10px; + font-size: 12px; + margin: 0; + color: #888; + z-index: 1; +} + +#mocha-stats .progress { + float: right; + padding-top: 0; +} + +#mocha-stats em { + color: black; +} + +#mocha-stats a { + text-decoration: none; + color: inherit; +} + +#mocha-stats a:hover { + border-bottom: 1px solid #eee; +} + +#mocha-stats li { + display: inline-block; + margin: 0 5px; + list-style: none; + padding-top: 11px; +} + +#mocha-stats canvas { + width: 40px; + height: 40px; +} + +#mocha code .comment { color: #ddd; } +#mocha code .init { color: #2f6fad; } +#mocha code .string { color: #5890ad; } +#mocha code .keyword { color: #8a6343; } +#mocha code .number { color: #2f6fad; } + +@media screen and (max-device-width: 480px) { + #mocha { + margin: 60px 0px; + } + + #mocha #stats { + position: absolute; + } +} diff --git a/tests/tools/mocha/mocha.js b/tests/tools/mocha/mocha.js new file mode 100644 index 0000000..19edc87 --- /dev/null +++ b/tests/tools/mocha/mocha.js @@ -0,0 +1,5842 @@ +;(function(){ + +// CommonJS require() + +function require(p){ + var path = require.resolve(p) + , mod = require.modules[path]; + if (!mod) throw new Error('failed to require "' + p + '"'); + if (!mod.exports) { + mod.exports = {}; + mod.call(mod.exports, mod, mod.exports, require.relative(path)); + } + return mod.exports; + } + +require.modules = {}; + +require.resolve = function (path){ + var orig = path + , reg = path + '.js' + , index = path + '/index.js'; + return require.modules[reg] && reg + || require.modules[index] && index + || orig; + }; + +require.register = function (path, fn){ + require.modules[path] = fn; + }; + +require.relative = function (parent) { + return function(p){ + if ('.' != p.charAt(0)) return require(p); + + var path = parent.split('/') + , segs = p.split('/'); + path.pop(); + + for (var i = 0; i < segs.length; i++) { + var seg = segs[i]; + if ('..' == seg) path.pop(); + else if ('.' != seg) path.push(seg); + } + + return require(path.join('/')); + }; + }; + + +require.register("browser/debug.js", function(module, exports, require){ + +module.exports = function(type){ + return function(){ + } +}; + +}); // module: browser/debug.js + +require.register("browser/diff.js", function(module, exports, require){ +/* See LICENSE file for terms of use */ + +/* + * Text diff implementation. + * + * This library supports the following APIS: + * JsDiff.diffChars: Character by character diff + * JsDiff.diffWords: Word (as defined by \b regex) diff which ignores whitespace + * JsDiff.diffLines: Line based diff + * + * JsDiff.diffCss: Diff targeted at CSS content + * + * These methods are based on the implementation proposed in + * "An O(ND) Difference Algorithm and its Variations" (Myers, 1986). + * http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.4.6927 + */ +var JsDiff = (function() { + /*jshint maxparams: 5*/ + function clonePath(path) { + return { newPos: path.newPos, components: path.components.slice(0) }; + } + function removeEmpty(array) { + var ret = []; + for (var i = 0; i < array.length; i++) { + if (array[i]) { + ret.push(array[i]); + } + } + return ret; + } + function escapeHTML(s) { + var n = s; + n = n.replace(/&/g, '&'); + n = n.replace(//g, '>'); + n = n.replace(/"/g, '"'); + + return n; + } + + var Diff = function(ignoreWhitespace) { + this.ignoreWhitespace = ignoreWhitespace; + }; + Diff.prototype = { + diff: function(oldString, newString) { + // Handle the identity case (this is due to unrolling editLength == 0 + if (newString === oldString) { + return [{ value: newString }]; + } + if (!newString) { + return [{ value: oldString, removed: true }]; + } + if (!oldString) { + return [{ value: newString, added: true }]; + } + + newString = this.tokenize(newString); + oldString = this.tokenize(oldString); + + var newLen = newString.length, oldLen = oldString.length; + var maxEditLength = newLen + oldLen; + var bestPath = [{ newPos: -1, components: [] }]; + + // Seed editLength = 0 + var oldPos = this.extractCommon(bestPath[0], newString, oldString, 0); + if (bestPath[0].newPos+1 >= newLen && oldPos+1 >= oldLen) { + return bestPath[0].components; + } + + for (var editLength = 1; editLength <= maxEditLength; editLength++) { + for (var diagonalPath = -1*editLength; diagonalPath <= editLength; diagonalPath+=2) { + var basePath; + var addPath = bestPath[diagonalPath-1], + removePath = bestPath[diagonalPath+1]; + oldPos = (removePath ? removePath.newPos : 0) - diagonalPath; + if (addPath) { + // No one else is going to attempt to use this value, clear it + bestPath[diagonalPath-1] = undefined; + } + + var canAdd = addPath && addPath.newPos+1 < newLen; + var canRemove = removePath && 0 <= oldPos && oldPos < oldLen; + if (!canAdd && !canRemove) { + bestPath[diagonalPath] = undefined; + continue; + } + + // Select the diagonal that we want to branch from. We select the prior + // path whose position in the new string is the farthest from the origin + // and does not pass the bounds of the diff graph + if (!canAdd || (canRemove && addPath.newPos < removePath.newPos)) { + basePath = clonePath(removePath); + this.pushComponent(basePath.components, oldString[oldPos], undefined, true); + } else { + basePath = clonePath(addPath); + basePath.newPos++; + this.pushComponent(basePath.components, newString[basePath.newPos], true, undefined); + } + + var oldPos = this.extractCommon(basePath, newString, oldString, diagonalPath); + + if (basePath.newPos+1 >= newLen && oldPos+1 >= oldLen) { + return basePath.components; + } else { + bestPath[diagonalPath] = basePath; + } + } + } + }, + + pushComponent: function(components, value, added, removed) { + var last = components[components.length-1]; + if (last && last.added === added && last.removed === removed) { + // We need to clone here as the component clone operation is just + // as shallow array clone + components[components.length-1] = + {value: this.join(last.value, value), added: added, removed: removed }; + } else { + components.push({value: value, added: added, removed: removed }); + } + }, + extractCommon: function(basePath, newString, oldString, diagonalPath) { + var newLen = newString.length, + oldLen = oldString.length, + newPos = basePath.newPos, + oldPos = newPos - diagonalPath; + while (newPos+1 < newLen && oldPos+1 < oldLen && this.equals(newString[newPos+1], oldString[oldPos+1])) { + newPos++; + oldPos++; + + this.pushComponent(basePath.components, newString[newPos], undefined, undefined); + } + basePath.newPos = newPos; + return oldPos; + }, + + equals: function(left, right) { + var reWhitespace = /\S/; + if (this.ignoreWhitespace && !reWhitespace.test(left) && !reWhitespace.test(right)) { + return true; + } else { + return left === right; + } + }, + join: function(left, right) { + return left + right; + }, + tokenize: function(value) { + return value; + } + }; + + var CharDiff = new Diff(); + + var WordDiff = new Diff(true); + var WordWithSpaceDiff = new Diff(); + WordDiff.tokenize = WordWithSpaceDiff.tokenize = function(value) { + return removeEmpty(value.split(/(\s+|\b)/)); + }; + + var CssDiff = new Diff(true); + CssDiff.tokenize = function(value) { + return removeEmpty(value.split(/([{}:;,]|\s+)/)); + }; + + var LineDiff = new Diff(); + LineDiff.tokenize = function(value) { + return value.split(/^/m); + }; + + return { + Diff: Diff, + + diffChars: function(oldStr, newStr) { return CharDiff.diff(oldStr, newStr); }, + diffWords: function(oldStr, newStr) { return WordDiff.diff(oldStr, newStr); }, + diffWordsWithSpace: function(oldStr, newStr) { return WordWithSpaceDiff.diff(oldStr, newStr); }, + diffLines: function(oldStr, newStr) { return LineDiff.diff(oldStr, newStr); }, + + diffCss: function(oldStr, newStr) { return CssDiff.diff(oldStr, newStr); }, + + createPatch: function(fileName, oldStr, newStr, oldHeader, newHeader) { + var ret = []; + + ret.push('Index: ' + fileName); + ret.push('==================================================================='); + ret.push('--- ' + fileName + (typeof oldHeader === 'undefined' ? '' : '\t' + oldHeader)); + ret.push('+++ ' + fileName + (typeof newHeader === 'undefined' ? '' : '\t' + newHeader)); + + var diff = LineDiff.diff(oldStr, newStr); + if (!diff[diff.length-1].value) { + diff.pop(); // Remove trailing newline add + } + diff.push({value: '', lines: []}); // Append an empty value to make cleanup easier + + function contextLines(lines) { + return lines.map(function(entry) { return ' ' + entry; }); + } + function eofNL(curRange, i, current) { + var last = diff[diff.length-2], + isLast = i === diff.length-2, + isLastOfType = i === diff.length-3 && (current.added !== last.added || current.removed !== last.removed); + + // Figure out if this is the last line for the given file and missing NL + if (!/\n$/.test(current.value) && (isLast || isLastOfType)) { + curRange.push('\\ No newline at end of file'); + } + } + + var oldRangeStart = 0, newRangeStart = 0, curRange = [], + oldLine = 1, newLine = 1; + for (var i = 0; i < diff.length; i++) { + var current = diff[i], + lines = current.lines || current.value.replace(/\n$/, '').split('\n'); + current.lines = lines; + + if (current.added || current.removed) { + if (!oldRangeStart) { + var prev = diff[i-1]; + oldRangeStart = oldLine; + newRangeStart = newLine; + + if (prev) { + curRange = contextLines(prev.lines.slice(-4)); + oldRangeStart -= curRange.length; + newRangeStart -= curRange.length; + } + } + curRange.push.apply(curRange, lines.map(function(entry) { return (current.added?'+':'-') + entry; })); + eofNL(curRange, i, current); + + if (current.added) { + newLine += lines.length; + } else { + oldLine += lines.length; + } + } else { + if (oldRangeStart) { + // Close out any changes that have been output (or join overlapping) + if (lines.length <= 8 && i < diff.length-2) { + // Overlapping + curRange.push.apply(curRange, contextLines(lines)); + } else { + // end the range and output + var contextSize = Math.min(lines.length, 4); + ret.push( + '@@ -' + oldRangeStart + ',' + (oldLine-oldRangeStart+contextSize) + + ' +' + newRangeStart + ',' + (newLine-newRangeStart+contextSize) + + ' @@'); + ret.push.apply(ret, curRange); + ret.push.apply(ret, contextLines(lines.slice(0, contextSize))); + if (lines.length <= 4) { + eofNL(ret, i, current); + } + + oldRangeStart = 0; newRangeStart = 0; curRange = []; + } + } + oldLine += lines.length; + newLine += lines.length; + } + } + + return ret.join('\n') + '\n'; + }, + + applyPatch: function(oldStr, uniDiff) { + var diffstr = uniDiff.split('\n'); + var diff = []; + var remEOFNL = false, + addEOFNL = false; + + for (var i = (diffstr[0][0]==='I'?4:0); i < diffstr.length; i++) { + if(diffstr[i][0] === '@') { + var meh = diffstr[i].split(/@@ -(\d+),(\d+) \+(\d+),(\d+) @@/); + diff.unshift({ + start:meh[3], + oldlength:meh[2], + oldlines:[], + newlength:meh[4], + newlines:[] + }); + } else if(diffstr[i][0] === '+') { + diff[0].newlines.push(diffstr[i].substr(1)); + } else if(diffstr[i][0] === '-') { + diff[0].oldlines.push(diffstr[i].substr(1)); + } else if(diffstr[i][0] === ' ') { + diff[0].newlines.push(diffstr[i].substr(1)); + diff[0].oldlines.push(diffstr[i].substr(1)); + } else if(diffstr[i][0] === '\\') { + if (diffstr[i-1][0] === '+') { + remEOFNL = true; + } else if(diffstr[i-1][0] === '-') { + addEOFNL = true; + } + } + } + + var str = oldStr.split('\n'); + for (var i = diff.length - 1; i >= 0; i--) { + var d = diff[i]; + for (var j = 0; j < d.oldlength; j++) { + if(str[d.start-1+j] !== d.oldlines[j]) { + return false; + } + } + Array.prototype.splice.apply(str,[d.start-1,+d.oldlength].concat(d.newlines)); + } + + if (remEOFNL) { + while (!str[str.length-1]) { + str.pop(); + } + } else if (addEOFNL) { + str.push(''); + } + return str.join('\n'); + }, + + convertChangesToXML: function(changes){ + var ret = []; + for ( var i = 0; i < changes.length; i++) { + var change = changes[i]; + if (change.added) { + ret.push(''); + } else if (change.removed) { + ret.push(''); + } + + ret.push(escapeHTML(change.value)); + + if (change.added) { + ret.push(''); + } else if (change.removed) { + ret.push(''); + } + } + return ret.join(''); + }, + + // See: http://code.google.com/p/google-diff-match-patch/wiki/API + convertChangesToDMP: function(changes){ + var ret = [], change; + for ( var i = 0; i < changes.length; i++) { + change = changes[i]; + ret.push([(change.added ? 1 : change.removed ? -1 : 0), change.value]); + } + return ret; + } + }; +})(); + +if (typeof module !== 'undefined') { + module.exports = JsDiff; +} + +}); // module: browser/diff.js + +require.register("browser/events.js", function(module, exports, require){ + +/** + * Module exports. + */ + +exports.EventEmitter = EventEmitter; + +/** + * Check if `obj` is an array. + */ + +function isArray(obj) { + return '[object Array]' == {}.toString.call(obj); +} + +/** + * Event emitter constructor. + * + * @api public + */ + +function EventEmitter(){}; + +/** + * Adds a listener. + * + * @api public + */ + +EventEmitter.prototype.on = function (name, fn) { + if (!this.$events) { + this.$events = {}; + } + + if (!this.$events[name]) { + this.$events[name] = fn; + } else if (isArray(this.$events[name])) { + this.$events[name].push(fn); + } else { + this.$events[name] = [this.$events[name], fn]; + } + + return this; +}; + +EventEmitter.prototype.addListener = EventEmitter.prototype.on; + +/** + * Adds a volatile listener. + * + * @api public + */ + +EventEmitter.prototype.once = function (name, fn) { + var self = this; + + function on () { + self.removeListener(name, on); + fn.apply(this, arguments); + }; + + on.listener = fn; + this.on(name, on); + + return this; +}; + +/** + * Removes a listener. + * + * @api public + */ + +EventEmitter.prototype.removeListener = function (name, fn) { + if (this.$events && this.$events[name]) { + var list = this.$events[name]; + + if (isArray(list)) { + var pos = -1; + + for (var i = 0, l = list.length; i < l; i++) { + if (list[i] === fn || (list[i].listener && list[i].listener === fn)) { + pos = i; + break; + } + } + + if (pos < 0) { + return this; + } + + list.splice(pos, 1); + + if (!list.length) { + delete this.$events[name]; + } + } else if (list === fn || (list.listener && list.listener === fn)) { + delete this.$events[name]; + } + } + + return this; +}; + +/** + * Removes all listeners for an event. + * + * @api public + */ + +EventEmitter.prototype.removeAllListeners = function (name) { + if (name === undefined) { + this.$events = {}; + return this; + } + + if (this.$events && this.$events[name]) { + this.$events[name] = null; + } + + return this; +}; + +/** + * Gets all listeners for a certain event. + * + * @api public + */ + +EventEmitter.prototype.listeners = function (name) { + if (!this.$events) { + this.$events = {}; + } + + if (!this.$events[name]) { + this.$events[name] = []; + } + + if (!isArray(this.$events[name])) { + this.$events[name] = [this.$events[name]]; + } + + return this.$events[name]; +}; + +/** + * Emits an event. + * + * @api public + */ + +EventEmitter.prototype.emit = function (name) { + if (!this.$events) { + return false; + } + + var handler = this.$events[name]; + + if (!handler) { + return false; + } + + var args = [].slice.call(arguments, 1); + + if ('function' == typeof handler) { + handler.apply(this, args); + } else if (isArray(handler)) { + var listeners = handler.slice(); + + for (var i = 0, l = listeners.length; i < l; i++) { + listeners[i].apply(this, args); + } + } else { + return false; + } + + return true; +}; +}); // module: browser/events.js + +require.register("browser/fs.js", function(module, exports, require){ + +}); // module: browser/fs.js + +require.register("browser/path.js", function(module, exports, require){ + +}); // module: browser/path.js + +require.register("browser/progress.js", function(module, exports, require){ +/** + * Expose `Progress`. + */ + +module.exports = Progress; + +/** + * Initialize a new `Progress` indicator. + */ + +function Progress() { + this.percent = 0; + this.size(0); + this.fontSize(11); + this.font('helvetica, arial, sans-serif'); +} + +/** + * Set progress size to `n`. + * + * @param {Number} n + * @return {Progress} for chaining + * @api public + */ + +Progress.prototype.size = function(n){ + this._size = n; + return this; +}; + +/** + * Set text to `str`. + * + * @param {String} str + * @return {Progress} for chaining + * @api public + */ + +Progress.prototype.text = function(str){ + this._text = str; + return this; +}; + +/** + * Set font size to `n`. + * + * @param {Number} n + * @return {Progress} for chaining + * @api public + */ + +Progress.prototype.fontSize = function(n){ + this._fontSize = n; + return this; +}; + +/** + * Set font `family`. + * + * @param {String} family + * @return {Progress} for chaining + */ + +Progress.prototype.font = function(family){ + this._font = family; + return this; +}; + +/** + * Update percentage to `n`. + * + * @param {Number} n + * @return {Progress} for chaining + */ + +Progress.prototype.update = function(n){ + this.percent = n; + return this; +}; + +/** + * Draw on `ctx`. + * + * @param {CanvasRenderingContext2d} ctx + * @return {Progress} for chaining + */ + +Progress.prototype.draw = function(ctx){ + try { + var percent = Math.min(this.percent, 100) + , size = this._size + , half = size / 2 + , x = half + , y = half + , rad = half - 1 + , fontSize = this._fontSize; + + ctx.font = fontSize + 'px ' + this._font; + + var angle = Math.PI * 2 * (percent / 100); + ctx.clearRect(0, 0, size, size); + + // outer circle + ctx.strokeStyle = '#9f9f9f'; + ctx.beginPath(); + ctx.arc(x, y, rad, 0, angle, false); + ctx.stroke(); + + // inner circle + ctx.strokeStyle = '#eee'; + ctx.beginPath(); + ctx.arc(x, y, rad - 1, 0, angle, true); + ctx.stroke(); + + // text + var text = this._text || (percent | 0) + '%' + , w = ctx.measureText(text).width; + + ctx.fillText( + text + , x - w / 2 + 1 + , y + fontSize / 2 - 1); + } catch (ex) {} //don't fail if we can't render progress + return this; +}; + +}); // module: browser/progress.js + +require.register("browser/tty.js", function(module, exports, require){ + +exports.isatty = function(){ + return true; +}; + +exports.getWindowSize = function(){ + if ('innerHeight' in global) { + return [global.innerHeight, global.innerWidth]; + } else { + // In a Web Worker, the DOM Window is not available. + return [640, 480]; + } +}; + +}); // module: browser/tty.js + +require.register("context.js", function(module, exports, require){ + +/** + * Expose `Context`. + */ + +module.exports = Context; + +/** + * Initialize a new `Context`. + * + * @api private + */ + +function Context(){} + +/** + * Set or get the context `Runnable` to `runnable`. + * + * @param {Runnable} runnable + * @return {Context} + * @api private + */ + +Context.prototype.runnable = function(runnable){ + if (0 == arguments.length) return this._runnable; + this.test = this._runnable = runnable; + return this; +}; + +/** + * Set test timeout `ms`. + * + * @param {Number} ms + * @return {Context} self + * @api private + */ + +Context.prototype.timeout = function(ms){ + this.runnable().timeout(ms); + return this; +}; + +/** + * Set test slowness threshold `ms`. + * + * @param {Number} ms + * @return {Context} self + * @api private + */ + +Context.prototype.slow = function(ms){ + this.runnable().slow(ms); + return this; +}; + +/** + * Inspect the context void of `._runnable`. + * + * @return {String} + * @api private + */ + +Context.prototype.inspect = function(){ + return JSON.stringify(this, function(key, val){ + if ('_runnable' == key) return; + if ('test' == key) return; + return val; + }, 2); +}; + +}); // module: context.js + +require.register("hook.js", function(module, exports, require){ + +/** + * Module dependencies. + */ + +var Runnable = require('./runnable'); + +/** + * Expose `Hook`. + */ + +module.exports = Hook; + +/** + * Initialize a new `Hook` with the given `title` and callback `fn`. + * + * @param {String} title + * @param {Function} fn + * @api private + */ + +function Hook(title, fn) { + Runnable.call(this, title, fn); + this.type = 'hook'; +} + +/** + * Inherit from `Runnable.prototype`. + */ + +function F(){}; +F.prototype = Runnable.prototype; +Hook.prototype = new F; +Hook.prototype.constructor = Hook; + + +/** + * Get or set the test `err`. + * + * @param {Error} err + * @return {Error} + * @api public + */ + +Hook.prototype.error = function(err){ + if (0 == arguments.length) { + var err = this._error; + this._error = null; + return err; + } + + this._error = err; +}; + +}); // module: hook.js + +require.register("interfaces/bdd.js", function(module, exports, require){ + +/** + * Module dependencies. + */ + +var Suite = require('../suite') + , Test = require('../test') + , utils = require('../utils'); + +/** + * BDD-style interface: + * + * describe('Array', function(){ + * describe('#indexOf()', function(){ + * it('should return -1 when not present', function(){ + * + * }); + * + * it('should return the index when present', function(){ + * + * }); + * }); + * }); + * + */ + +module.exports = function(suite){ + var suites = [suite]; + + suite.on('pre-require', function(context, file, mocha){ + + /** + * Execute before running tests. + */ + + context.before = function(name, fn){ + suites[0].beforeAll(name, fn); + }; + + /** + * Execute after running tests. + */ + + context.after = function(name, fn){ + suites[0].afterAll(name, fn); + }; + + /** + * Execute before each test case. + */ + + context.beforeEach = function(name, fn){ + suites[0].beforeEach(name, fn); + }; + + /** + * Execute after each test case. + */ + + context.afterEach = function(name, fn){ + suites[0].afterEach(name, fn); + }; + + /** + * Describe a "suite" with the given `title` + * and callback `fn` containing nested suites + * and/or tests. + */ + + context.describe = context.context = function(title, fn){ + var suite = Suite.create(suites[0], title); + suites.unshift(suite); + fn.call(suite); + suites.shift(); + return suite; + }; + + /** + * Pending describe. + */ + + context.xdescribe = + context.xcontext = + context.describe.skip = function(title, fn){ + var suite = Suite.create(suites[0], title); + suite.pending = true; + suites.unshift(suite); + fn.call(suite); + suites.shift(); + }; + + /** + * Exclusive suite. + */ + + context.describe.only = function(title, fn){ + var suite = context.describe(title, fn); + mocha.grep(suite.fullTitle()); + return suite; + }; + + /** + * Describe a specification or test-case + * with the given `title` and callback `fn` + * acting as a thunk. + */ + + context.it = context.specify = function(title, fn){ + var suite = suites[0]; + if (suite.pending) var fn = null; + var test = new Test(title, fn); + suite.addTest(test); + return test; + }; + + /** + * Exclusive test-case. + */ + + context.it.only = function(title, fn){ + var test = context.it(title, fn); + var reString = '^' + utils.escapeRegexp(test.fullTitle()) + '$'; + mocha.grep(new RegExp(reString)); + return test; + }; + + /** + * Pending test case. + */ + + context.xit = + context.xspecify = + context.it.skip = function(title){ + context.it(title); + }; + }); +}; + +}); // module: interfaces/bdd.js + +require.register("interfaces/exports.js", function(module, exports, require){ + +/** + * Module dependencies. + */ + +var Suite = require('../suite') + , Test = require('../test'); + +/** + * TDD-style interface: + * + * exports.Array = { + * '#indexOf()': { + * 'should return -1 when the value is not present': function(){ + * + * }, + * + * 'should return the correct index when the value is present': function(){ + * + * } + * } + * }; + * + */ + +module.exports = function(suite){ + var suites = [suite]; + + suite.on('require', visit); + + function visit(obj) { + var suite; + for (var key in obj) { + if ('function' == typeof obj[key]) { + var fn = obj[key]; + switch (key) { + case 'before': + suites[0].beforeAll(fn); + break; + case 'after': + suites[0].afterAll(fn); + break; + case 'beforeEach': + suites[0].beforeEach(fn); + break; + case 'afterEach': + suites[0].afterEach(fn); + break; + default: + suites[0].addTest(new Test(key, fn)); + } + } else { + var suite = Suite.create(suites[0], key); + suites.unshift(suite); + visit(obj[key]); + suites.shift(); + } + } + } +}; + +}); // module: interfaces/exports.js + +require.register("interfaces/index.js", function(module, exports, require){ + +exports.bdd = require('./bdd'); +exports.tdd = require('./tdd'); +exports.qunit = require('./qunit'); +exports.exports = require('./exports'); + +}); // module: interfaces/index.js + +require.register("interfaces/qunit.js", function(module, exports, require){ + +/** + * Module dependencies. + */ + +var Suite = require('../suite') + , Test = require('../test') + , utils = require('../utils'); + +/** + * QUnit-style interface: + * + * suite('Array'); + * + * test('#length', function(){ + * var arr = [1,2,3]; + * ok(arr.length == 3); + * }); + * + * test('#indexOf()', function(){ + * var arr = [1,2,3]; + * ok(arr.indexOf(1) == 0); + * ok(arr.indexOf(2) == 1); + * ok(arr.indexOf(3) == 2); + * }); + * + * suite('String'); + * + * test('#length', function(){ + * ok('foo'.length == 3); + * }); + * + */ + +module.exports = function(suite){ + var suites = [suite]; + + suite.on('pre-require', function(context, file, mocha){ + + /** + * Execute before running tests. + */ + + context.before = function(name, fn){ + suites[0].beforeAll(name, fn); + }; + + /** + * Execute after running tests. + */ + + context.after = function(name, fn){ + suites[0].afterAll(name, fn); + }; + + /** + * Execute before each test case. + */ + + context.beforeEach = function(name, fn){ + suites[0].beforeEach(name, fn); + }; + + /** + * Execute after each test case. + */ + + context.afterEach = function(name, fn){ + suites[0].afterEach(name, fn); + }; + + /** + * Describe a "suite" with the given `title`. + */ + + context.suite = function(title){ + if (suites.length > 1) suites.shift(); + var suite = Suite.create(suites[0], title); + suites.unshift(suite); + return suite; + }; + + /** + * Exclusive test-case. + */ + + context.suite.only = function(title, fn){ + var suite = context.suite(title, fn); + mocha.grep(suite.fullTitle()); + }; + + /** + * Describe a specification or test-case + * with the given `title` and callback `fn` + * acting as a thunk. + */ + + context.test = function(title, fn){ + var test = new Test(title, fn); + suites[0].addTest(test); + return test; + }; + + /** + * Exclusive test-case. + */ + + context.test.only = function(title, fn){ + var test = context.test(title, fn); + var reString = '^' + utils.escapeRegexp(test.fullTitle()) + '$'; + mocha.grep(new RegExp(reString)); + }; + + /** + * Pending test case. + */ + + context.test.skip = function(title){ + context.test(title); + }; + }); +}; + +}); // module: interfaces/qunit.js + +require.register("interfaces/tdd.js", function(module, exports, require){ + +/** + * Module dependencies. + */ + +var Suite = require('../suite') + , Test = require('../test') + , utils = require('../utils');; + +/** + * TDD-style interface: + * + * suite('Array', function(){ + * suite('#indexOf()', function(){ + * suiteSetup(function(){ + * + * }); + * + * test('should return -1 when not present', function(){ + * + * }); + * + * test('should return the index when present', function(){ + * + * }); + * + * suiteTeardown(function(){ + * + * }); + * }); + * }); + * + */ + +module.exports = function(suite){ + var suites = [suite]; + + suite.on('pre-require', function(context, file, mocha){ + + /** + * Execute before each test case. + */ + + context.setup = function(name, fn){ + suites[0].beforeEach(name, fn); + }; + + /** + * Execute after each test case. + */ + + context.teardown = function(name, fn){ + suites[0].afterEach(name, fn); + }; + + /** + * Execute before the suite. + */ + + context.suiteSetup = function(name, fn){ + suites[0].beforeAll(name, fn); + }; + + /** + * Execute after the suite. + */ + + context.suiteTeardown = function(name, fn){ + suites[0].afterAll(name, fn); + }; + + /** + * Describe a "suite" with the given `title` + * and callback `fn` containing nested suites + * and/or tests. + */ + + context.suite = function(title, fn){ + var suite = Suite.create(suites[0], title); + suites.unshift(suite); + fn.call(suite); + suites.shift(); + return suite; + }; + + /** + * Pending suite. + */ + context.suite.skip = function(title, fn) { + var suite = Suite.create(suites[0], title); + suite.pending = true; + suites.unshift(suite); + fn.call(suite); + suites.shift(); + }; + + /** + * Exclusive test-case. + */ + + context.suite.only = function(title, fn){ + var suite = context.suite(title, fn); + mocha.grep(suite.fullTitle()); + }; + + /** + * Describe a specification or test-case + * with the given `title` and callback `fn` + * acting as a thunk. + */ + + context.test = function(title, fn){ + var suite = suites[0]; + if (suite.pending) var fn = null; + var test = new Test(title, fn); + suite.addTest(test); + return test; + }; + + /** + * Exclusive test-case. + */ + + context.test.only = function(title, fn){ + var test = context.test(title, fn); + var reString = '^' + utils.escapeRegexp(test.fullTitle()) + '$'; + mocha.grep(new RegExp(reString)); + }; + + /** + * Pending test case. + */ + + context.test.skip = function(title){ + context.test(title); + }; + }); +}; + +}); // module: interfaces/tdd.js + +require.register("mocha.js", function(module, exports, require){ +/*! + * mocha + * Copyright(c) 2011 TJ Holowaychuk + * MIT Licensed + */ + +/** + * Module dependencies. + */ + +var path = require('browser/path') + , utils = require('./utils'); + +/** + * Expose `Mocha`. + */ + +exports = module.exports = Mocha; + +/** + * Expose internals. + */ + +exports.utils = utils; +exports.interfaces = require('./interfaces'); +exports.reporters = require('./reporters'); +exports.Runnable = require('./runnable'); +exports.Context = require('./context'); +exports.Runner = require('./runner'); +exports.Suite = require('./suite'); +exports.Hook = require('./hook'); +exports.Test = require('./test'); + +/** + * Return image `name` path. + * + * @param {String} name + * @return {String} + * @api private + */ + +function image(name) { + return __dirname + '/../images/' + name + '.png'; +} + +/** + * Setup mocha with `options`. + * + * Options: + * + * - `ui` name "bdd", "tdd", "exports" etc + * - `reporter` reporter instance, defaults to `mocha.reporters.Dot` + * - `globals` array of accepted globals + * - `timeout` timeout in milliseconds + * - `bail` bail on the first test failure + * - `slow` milliseconds to wait before considering a test slow + * - `ignoreLeaks` ignore global leaks + * - `grep` string or regexp to filter tests with + * + * @param {Object} options + * @api public + */ + +function Mocha(options) { + options = options || {}; + this.files = []; + this.options = options; + this.grep(options.grep); + this.suite = new exports.Suite('', new exports.Context); + this.ui(options.ui); + this.bail(options.bail); + this.reporter(options.reporter); + if (null != options.timeout) this.timeout(options.timeout); + this.useColors(options.useColors) + if (options.slow) this.slow(options.slow); + + this.suite.on('pre-require', function (context) { + exports.afterEach = context.afterEach || context.teardown; + exports.after = context.after || context.suiteTeardown; + exports.beforeEach = context.beforeEach || context.setup; + exports.before = context.before || context.suiteSetup; + exports.describe = context.describe || context.suite; + exports.it = context.it || context.test; + exports.setup = context.setup || context.beforeEach; + exports.suiteSetup = context.suiteSetup || context.before; + exports.suiteTeardown = context.suiteTeardown || context.after; + exports.suite = context.suite || context.describe; + exports.teardown = context.teardown || context.afterEach; + exports.test = context.test || context.it; + }); +} + +/** + * Enable or disable bailing on the first failure. + * + * @param {Boolean} [bail] + * @api public + */ + +Mocha.prototype.bail = function(bail){ + if (0 == arguments.length) bail = true; + this.suite.bail(bail); + return this; +}; + +/** + * Add test `file`. + * + * @param {String} file + * @api public + */ + +Mocha.prototype.addFile = function(file){ + this.files.push(file); + return this; +}; + +/** + * Set reporter to `reporter`, defaults to "dot". + * + * @param {String|Function} reporter name or constructor + * @api public + */ + +Mocha.prototype.reporter = function(reporter){ + if ('function' == typeof reporter) { + this._reporter = reporter; + } else { + reporter = reporter || 'dot'; + var _reporter; + try { _reporter = require('./reporters/' + reporter); } catch (err) {}; + if (!_reporter) try { _reporter = require(reporter); } catch (err) {}; + if (!_reporter && reporter === 'teamcity') + console.warn('The Teamcity reporter was moved to a package named ' + + 'mocha-teamcity-reporter ' + + '(https://npmjs.org/package/mocha-teamcity-reporter).'); + if (!_reporter) throw new Error('invalid reporter "' + reporter + '"'); + this._reporter = _reporter; + } + return this; +}; + +/** + * Set test UI `name`, defaults to "bdd". + * + * @param {String} bdd + * @api public + */ + +Mocha.prototype.ui = function(name){ + name = name || 'bdd'; + this._ui = exports.interfaces[name]; + if (!this._ui) try { this._ui = require(name); } catch (err) {}; + if (!this._ui) throw new Error('invalid interface "' + name + '"'); + this._ui = this._ui(this.suite); + return this; +}; + +/** + * Load registered files. + * + * @api private + */ + +Mocha.prototype.loadFiles = function(fn){ + var self = this; + var suite = this.suite; + var pending = this.files.length; + this.files.forEach(function(file){ + file = path.resolve(file); + suite.emit('pre-require', global, file, self); + suite.emit('require', require(file), file, self); + suite.emit('post-require', global, file, self); + --pending || (fn && fn()); + }); +}; + +/** + * Enable growl support. + * + * @api private + */ + +Mocha.prototype._growl = function(runner, reporter) { + var notify = require('growl'); + + runner.on('end', function(){ + var stats = reporter.stats; + if (stats.failures) { + var msg = stats.failures + ' of ' + runner.total + ' tests failed'; + notify(msg, { name: 'mocha', title: 'Failed', image: image('error') }); + } else { + notify(stats.passes + ' tests passed in ' + stats.duration + 'ms', { + name: 'mocha' + , title: 'Passed' + , image: image('ok') + }); + } + }); +}; + +/** + * Add regexp to grep, if `re` is a string it is escaped. + * + * @param {RegExp|String} re + * @return {Mocha} + * @api public + */ + +Mocha.prototype.grep = function(re){ + this.options.grep = 'string' == typeof re + ? new RegExp(utils.escapeRegexp(re)) + : re; + return this; +}; + +/** + * Invert `.grep()` matches. + * + * @return {Mocha} + * @api public + */ + +Mocha.prototype.invert = function(){ + this.options.invert = true; + return this; +}; + +/** + * Ignore global leaks. + * + * @param {Boolean} ignore + * @return {Mocha} + * @api public + */ + +Mocha.prototype.ignoreLeaks = function(ignore){ + this.options.ignoreLeaks = !!ignore; + return this; +}; + +/** + * Enable global leak checking. + * + * @return {Mocha} + * @api public + */ + +Mocha.prototype.checkLeaks = function(){ + this.options.ignoreLeaks = false; + return this; +}; + +/** + * Enable growl support. + * + * @return {Mocha} + * @api public + */ + +Mocha.prototype.growl = function(){ + this.options.growl = true; + return this; +}; + +/** + * Ignore `globals` array or string. + * + * @param {Array|String} globals + * @return {Mocha} + * @api public + */ + +Mocha.prototype.globals = function(globals){ + this.options.globals = (this.options.globals || []).concat(globals); + return this; +}; + +/** + * Emit color output. + * + * @param {Boolean} colors + * @return {Mocha} + * @api public + */ + +Mocha.prototype.useColors = function(colors){ + this.options.useColors = arguments.length && colors != undefined + ? colors + : true; + return this; +}; + +/** + * Use inline diffs rather than +/-. + * + * @param {Boolean} inlineDiffs + * @return {Mocha} + * @api public + */ + +Mocha.prototype.useInlineDiffs = function(inlineDiffs) { + this.options.useInlineDiffs = arguments.length && inlineDiffs != undefined + ? inlineDiffs + : false; + return this; +}; + +/** + * Set the timeout in milliseconds. + * + * @param {Number} timeout + * @return {Mocha} + * @api public + */ + +Mocha.prototype.timeout = function(timeout){ + this.suite.timeout(timeout); + return this; +}; + +/** + * Set slowness threshold in milliseconds. + * + * @param {Number} slow + * @return {Mocha} + * @api public + */ + +Mocha.prototype.slow = function(slow){ + this.suite.slow(slow); + return this; +}; + +/** + * Makes all tests async (accepting a callback) + * + * @return {Mocha} + * @api public + */ + +Mocha.prototype.asyncOnly = function(){ + this.options.asyncOnly = true; + return this; +}; + +/** + * Run tests and invoke `fn()` when complete. + * + * @param {Function} fn + * @return {Runner} + * @api public + */ + +Mocha.prototype.run = function(fn){ + if (this.files.length) this.loadFiles(); + var suite = this.suite; + var options = this.options; + options.files = this.files; + var runner = new exports.Runner(suite); + var reporter = new this._reporter(runner, options); + runner.ignoreLeaks = false !== options.ignoreLeaks; + runner.asyncOnly = options.asyncOnly; + if (options.grep) runner.grep(options.grep, options.invert); + if (options.globals) runner.globals(options.globals); + if (options.growl) this._growl(runner, reporter); + exports.reporters.Base.useColors = options.useColors; + exports.reporters.Base.inlineDiffs = options.useInlineDiffs; + return runner.run(fn); +}; + +}); // module: mocha.js + +require.register("ms.js", function(module, exports, require){ +/** + * Helpers. + */ + +var s = 1000; +var m = s * 60; +var h = m * 60; +var d = h * 24; +var y = d * 365.25; + +/** + * Parse or format the given `val`. + * + * Options: + * + * - `long` verbose formatting [false] + * + * @param {String|Number} val + * @param {Object} options + * @return {String|Number} + * @api public + */ + +module.exports = function(val, options){ + options = options || {}; + if ('string' == typeof val) return parse(val); + return options.long ? longFormat(val) : shortFormat(val); +}; + +/** + * Parse the given `str` and return milliseconds. + * + * @param {String} str + * @return {Number} + * @api private + */ + +function parse(str) { + var match = /^((?:\d+)?\.?\d+) *(ms|seconds?|s|minutes?|m|hours?|h|days?|d|years?|y)?$/i.exec(str); + if (!match) return; + var n = parseFloat(match[1]); + var type = (match[2] || 'ms').toLowerCase(); + switch (type) { + case 'years': + case 'year': + case 'y': + return n * y; + case 'days': + case 'day': + case 'd': + return n * d; + case 'hours': + case 'hour': + case 'h': + return n * h; + case 'minutes': + case 'minute': + case 'm': + return n * m; + case 'seconds': + case 'second': + case 's': + return n * s; + case 'ms': + return n; + } +} + +/** + * Short format for `ms`. + * + * @param {Number} ms + * @return {String} + * @api private + */ + +function shortFormat(ms) { + if (ms >= d) return Math.round(ms / d) + 'd'; + if (ms >= h) return Math.round(ms / h) + 'h'; + if (ms >= m) return Math.round(ms / m) + 'm'; + if (ms >= s) return Math.round(ms / s) + 's'; + return ms + 'ms'; +} + +/** + * Long format for `ms`. + * + * @param {Number} ms + * @return {String} + * @api private + */ + +function longFormat(ms) { + return plural(ms, d, 'day') + || plural(ms, h, 'hour') + || plural(ms, m, 'minute') + || plural(ms, s, 'second') + || ms + ' ms'; +} + +/** + * Pluralization helper. + */ + +function plural(ms, n, name) { + if (ms < n) return; + if (ms < n * 1.5) return Math.floor(ms / n) + ' ' + name; + return Math.ceil(ms / n) + ' ' + name + 's'; +} + +}); // module: ms.js + +require.register("reporters/base.js", function(module, exports, require){ + +/** + * Module dependencies. + */ + +var tty = require('browser/tty') + , diff = require('browser/diff') + , ms = require('../ms') + , utils = require('../utils'); + +/** + * Save timer references to avoid Sinon interfering (see GH-237). + */ + +var Date = global.Date + , setTimeout = global.setTimeout + , setInterval = global.setInterval + , clearTimeout = global.clearTimeout + , clearInterval = global.clearInterval; + +/** + * Check if both stdio streams are associated with a tty. + */ + +var isatty = tty.isatty(1) && tty.isatty(2); + +/** + * Expose `Base`. + */ + +exports = module.exports = Base; + +/** + * Enable coloring by default. + */ + +exports.useColors = isatty || (process.env.MOCHA_COLORS !== undefined); + +/** + * Inline diffs instead of +/- + */ + +exports.inlineDiffs = false; + +/** + * Default color map. + */ + +exports.colors = { + 'pass': 90 + , 'fail': 31 + , 'bright pass': 92 + , 'bright fail': 91 + , 'bright yellow': 93 + , 'pending': 36 + , 'suite': 0 + , 'error title': 0 + , 'error message': 31 + , 'error stack': 90 + , 'checkmark': 32 + , 'fast': 90 + , 'medium': 33 + , 'slow': 31 + , 'green': 32 + , 'light': 90 + , 'diff gutter': 90 + , 'diff added': 42 + , 'diff removed': 41 +}; + +/** + * Default symbol map. + */ + +exports.symbols = { + ok: '✓', + err: '✖', + dot: '․' +}; + +// With node.js on Windows: use symbols available in terminal default fonts +if ('win32' == process.platform) { + exports.symbols.ok = '\u221A'; + exports.symbols.err = '\u00D7'; + exports.symbols.dot = '.'; +} + +/** + * Color `str` with the given `type`, + * allowing colors to be disabled, + * as well as user-defined color + * schemes. + * + * @param {String} type + * @param {String} str + * @return {String} + * @api private + */ + +var color = exports.color = function(type, str) { + if (!exports.useColors) return str; + return '\u001b[' + exports.colors[type] + 'm' + str + '\u001b[0m'; +}; + +/** + * Expose term window size, with some + * defaults for when stderr is not a tty. + */ + +exports.window = { + width: isatty + ? process.stdout.getWindowSize + ? process.stdout.getWindowSize(1)[0] + : tty.getWindowSize()[1] + : 75 +}; + +/** + * Expose some basic cursor interactions + * that are common among reporters. + */ + +exports.cursor = { + hide: function(){ + isatty && process.stdout.write('\u001b[?25l'); + }, + + show: function(){ + isatty && process.stdout.write('\u001b[?25h'); + }, + + deleteLine: function(){ + isatty && process.stdout.write('\u001b[2K'); + }, + + beginningOfLine: function(){ + isatty && process.stdout.write('\u001b[0G'); + }, + + CR: function(){ + if (isatty) { + exports.cursor.deleteLine(); + exports.cursor.beginningOfLine(); + } else { + process.stdout.write('\r'); + } + } +}; + +/** + * Outut the given `failures` as a list. + * + * @param {Array} failures + * @api public + */ + +exports.list = function(failures){ + console.error(); + failures.forEach(function(test, i){ + // format + var fmt = color('error title', ' %s) %s:\n') + + color('error message', ' %s') + + color('error stack', '\n%s\n'); + + // msg + var err = test.err + , message = err.message || '' + , stack = err.stack || message + , index = stack.indexOf(message) + message.length + , msg = stack.slice(0, index) + , actual = err.actual + , expected = err.expected + , escape = true; + + // uncaught + if (err.uncaught) { + msg = 'Uncaught ' + msg; + } + + // explicitly show diff + if (err.showDiff && sameType(actual, expected)) { + escape = false; + err.actual = actual = stringify(canonicalize(actual)); + err.expected = expected = stringify(canonicalize(expected)); + } + + // actual / expected diff + if ('string' == typeof actual && 'string' == typeof expected) { + fmt = color('error title', ' %s) %s:\n%s') + color('error stack', '\n%s\n'); + var match = message.match(/^([^:]+): expected/); + msg = '\n ' + color('error message', match ? match[1] : msg); + + if (exports.inlineDiffs) { + msg += inlineDiff(err, escape); + } else { + msg += unifiedDiff(err, escape); + } + } + + // indent stack trace without msg + stack = stack.slice(index ? index + 1 : index) + .replace(/^/gm, ' '); + + console.error(fmt, (i + 1), test.fullTitle(), msg, stack); + }); +}; + +/** + * Initialize a new `Base` reporter. + * + * All other reporters generally + * inherit from this reporter, providing + * stats such as test duration, number + * of tests passed / failed etc. + * + * @param {Runner} runner + * @api public + */ + +function Base(runner) { + var self = this + , stats = this.stats = { suites: 0, tests: 0, passes: 0, pending: 0, failures: 0 } + , failures = this.failures = []; + + if (!runner) return; + this.runner = runner; + + runner.stats = stats; + + runner.on('start', function(){ + stats.start = new Date; + }); + + runner.on('suite', function(suite){ + stats.suites = stats.suites || 0; + suite.root || stats.suites++; + }); + + runner.on('test end', function(test){ + stats.tests = stats.tests || 0; + stats.tests++; + }); + + runner.on('pass', function(test){ + stats.passes = stats.passes || 0; + + var medium = test.slow() / 2; + test.speed = test.duration > test.slow() + ? 'slow' + : test.duration > medium + ? 'medium' + : 'fast'; + + stats.passes++; + }); + + runner.on('fail', function(test, err){ + stats.failures = stats.failures || 0; + stats.failures++; + test.err = err; + failures.push(test); + }); + + runner.on('end', function(){ + stats.end = new Date; + stats.duration = new Date - stats.start; + }); + + runner.on('pending', function(){ + stats.pending++; + }); +} + +/** + * Output common epilogue used by many of + * the bundled reporters. + * + * @api public + */ + +Base.prototype.epilogue = function(){ + var stats = this.stats; + var tests; + var fmt; + + console.log(); + + // passes + fmt = color('bright pass', ' ') + + color('green', ' %d passing') + + color('light', ' (%s)'); + + console.log(fmt, + stats.passes || 0, + ms(stats.duration)); + + // pending + if (stats.pending) { + fmt = color('pending', ' ') + + color('pending', ' %d pending'); + + console.log(fmt, stats.pending); + } + + // failures + if (stats.failures) { + fmt = color('fail', ' %d failing'); + + console.error(fmt, + stats.failures); + + Base.list(this.failures); + console.error(); + } + + console.log(); +}; + +/** + * Pad the given `str` to `len`. + * + * @param {String} str + * @param {String} len + * @return {String} + * @api private + */ + +function pad(str, len) { + str = String(str); + return Array(len - str.length + 1).join(' ') + str; +} + + +/** + * Returns an inline diff between 2 strings with coloured ANSI output + * + * @param {Error} Error with actual/expected + * @return {String} Diff + * @api private + */ + +function inlineDiff(err, escape) { + var msg = errorDiff(err, 'WordsWithSpace', escape); + + // linenos + var lines = msg.split('\n'); + if (lines.length > 4) { + var width = String(lines.length).length; + msg = lines.map(function(str, i){ + return pad(++i, width) + ' |' + ' ' + str; + }).join('\n'); + } + + // legend + msg = '\n' + + color('diff removed', 'actual') + + ' ' + + color('diff added', 'expected') + + '\n\n' + + msg + + '\n'; + + // indent + msg = msg.replace(/^/gm, ' '); + return msg; +} + +/** + * Returns a unified diff between 2 strings + * + * @param {Error} Error with actual/expected + * @return {String} Diff + * @api private + */ + +function unifiedDiff(err, escape) { + var indent = ' '; + function cleanUp(line) { + if (escape) { + line = escapeInvisibles(line); + } + if (line[0] === '+') return indent + colorLines('diff added', line); + if (line[0] === '-') return indent + colorLines('diff removed', line); + if (line.match(/\@\@/)) return null; + if (line.match(/\\ No newline/)) return null; + else return indent + line; + } + function notBlank(line) { + return line != null; + } + msg = diff.createPatch('string', err.actual, err.expected); + var lines = msg.split('\n').splice(4); + return '\n ' + + colorLines('diff added', '+ expected') + ' ' + + colorLines('diff removed', '- actual') + + '\n\n' + + lines.map(cleanUp).filter(notBlank).join('\n'); +} + +/** + * Return a character diff for `err`. + * + * @param {Error} err + * @return {String} + * @api private + */ + +function errorDiff(err, type, escape) { + var actual = escape ? escapeInvisibles(err.actual) : err.actual; + var expected = escape ? escapeInvisibles(err.expected) : err.expected; + return diff['diff' + type](actual, expected).map(function(str){ + if (str.added) return colorLines('diff added', str.value); + if (str.removed) return colorLines('diff removed', str.value); + return str.value; + }).join(''); +} + +/** + * Returns a string with all invisible characters in plain text + * + * @param {String} line + * @return {String} + * @api private + */ +function escapeInvisibles(line) { + return line.replace(/\t/g, '') + .replace(/\r/g, '') + .replace(/\n/g, '\n'); +} + +/** + * Color lines for `str`, using the color `name`. + * + * @param {String} name + * @param {String} str + * @return {String} + * @api private + */ + +function colorLines(name, str) { + return str.split('\n').map(function(str){ + return color(name, str); + }).join('\n'); +} + +/** + * Stringify `obj`. + * + * @param {Object} obj + * @return {String} + * @api private + */ + +function stringify(obj) { + if (obj instanceof RegExp) return obj.toString(); + return JSON.stringify(obj, null, 2); +} + +/** + * Return a new object that has the keys in sorted order. + * @param {Object} obj + * @return {Object} + * @api private + */ + + function canonicalize(obj, stack) { + stack = stack || []; + + if (utils.indexOf(stack, obj) !== -1) return obj; + + var canonicalizedObj; + + if ('[object Array]' == {}.toString.call(obj)) { + stack.push(obj); + canonicalizedObj = utils.map(obj, function(item) { + return canonicalize(item, stack); + }); + stack.pop(); + } else if (typeof obj === 'object' && obj !== null) { + stack.push(obj); + canonicalizedObj = {}; + utils.forEach(utils.keys(obj).sort(), function(key) { + canonicalizedObj[key] = canonicalize(obj[key], stack); + }); + stack.pop(); + } else { + canonicalizedObj = obj; + } + + return canonicalizedObj; + } + +/** + * Check that a / b have the same type. + * + * @param {Object} a + * @param {Object} b + * @return {Boolean} + * @api private + */ + +function sameType(a, b) { + a = Object.prototype.toString.call(a); + b = Object.prototype.toString.call(b); + return a == b; +} + + +}); // module: reporters/base.js + +require.register("reporters/doc.js", function(module, exports, require){ + +/** + * Module dependencies. + */ + +var Base = require('./base') + , utils = require('../utils'); + +/** + * Expose `Doc`. + */ + +exports = module.exports = Doc; + +/** + * Initialize a new `Doc` reporter. + * + * @param {Runner} runner + * @api public + */ + +function Doc(runner) { + Base.call(this, runner); + + var self = this + , stats = this.stats + , total = runner.total + , indents = 2; + + function indent() { + return Array(indents).join(' '); + } + + runner.on('suite', function(suite){ + if (suite.root) return; + ++indents; + console.log('%s
    ', indent()); + ++indents; + console.log('%s

    %s

    ', indent(), utils.escape(suite.title)); + console.log('%s
    ', indent()); + }); + + runner.on('suite end', function(suite){ + if (suite.root) return; + console.log('%s
    ', indent()); + --indents; + console.log('%s
    ', indent()); + --indents; + }); + + runner.on('pass', function(test){ + console.log('%s
    %s
    ', indent(), utils.escape(test.title)); + var code = utils.escape(utils.clean(test.fn.toString())); + console.log('%s
    %s
    ', indent(), code); + }); +} + +}); // module: reporters/doc.js + +require.register("reporters/dot.js", function(module, exports, require){ + +/** + * Module dependencies. + */ + +var Base = require('./base') + , color = Base.color; + +/** + * Expose `Dot`. + */ + +exports = module.exports = Dot; + +/** + * Initialize a new `Dot` matrix test reporter. + * + * @param {Runner} runner + * @api public + */ + +function Dot(runner) { + Base.call(this, runner); + + var self = this + , stats = this.stats + , width = Base.window.width * .75 | 0 + , n = 0; + + runner.on('start', function(){ + process.stdout.write('\n '); + }); + + runner.on('pending', function(test){ + process.stdout.write(color('pending', Base.symbols.dot)); + }); + + runner.on('pass', function(test){ + if (++n % width == 0) process.stdout.write('\n '); + if ('slow' == test.speed) { + process.stdout.write(color('bright yellow', Base.symbols.dot)); + } else { + process.stdout.write(color(test.speed, Base.symbols.dot)); + } + }); + + runner.on('fail', function(test, err){ + if (++n % width == 0) process.stdout.write('\n '); + process.stdout.write(color('fail', Base.symbols.dot)); + }); + + runner.on('end', function(){ + console.log(); + self.epilogue(); + }); +} + +/** + * Inherit from `Base.prototype`. + */ + +function F(){}; +F.prototype = Base.prototype; +Dot.prototype = new F; +Dot.prototype.constructor = Dot; + +}); // module: reporters/dot.js + +require.register("reporters/html-cov.js", function(module, exports, require){ + +/** + * Module dependencies. + */ + +var JSONCov = require('./json-cov') + , fs = require('browser/fs'); + +/** + * Expose `HTMLCov`. + */ + +exports = module.exports = HTMLCov; + +/** + * Initialize a new `JsCoverage` reporter. + * + * @param {Runner} runner + * @api public + */ + +function HTMLCov(runner) { + var jade = require('jade') + , file = __dirname + '/templates/coverage.jade' + , str = fs.readFileSync(file, 'utf8') + , fn = jade.compile(str, { filename: file }) + , self = this; + + JSONCov.call(this, runner, false); + + runner.on('end', function(){ + process.stdout.write(fn({ + cov: self.cov + , coverageClass: coverageClass + })); + }); +} + +/** + * Return coverage class for `n`. + * + * @return {String} + * @api private + */ + +function coverageClass(n) { + if (n >= 75) return 'high'; + if (n >= 50) return 'medium'; + if (n >= 25) return 'low'; + return 'terrible'; +} +}); // module: reporters/html-cov.js + +require.register("reporters/html.js", function(module, exports, require){ + +/** + * Module dependencies. + */ + +var Base = require('./base') + , utils = require('../utils') + , Progress = require('../browser/progress') + , escape = utils.escape; + +/** + * Save timer references to avoid Sinon interfering (see GH-237). + */ + +var Date = global.Date + , setTimeout = global.setTimeout + , setInterval = global.setInterval + , clearTimeout = global.clearTimeout + , clearInterval = global.clearInterval; + +/** + * Expose `HTML`. + */ + +exports = module.exports = HTML; + +/** + * Stats template. + */ + +var statsTemplate = ''; + +/** + * Initialize a new `HTML` reporter. + * + * @param {Runner} runner + * @api public + */ + +function HTML(runner) { + Base.call(this, runner); + + var self = this + , stats = this.stats + , total = runner.total + , stat = fragment(statsTemplate) + , items = stat.getElementsByTagName('li') + , passes = items[1].getElementsByTagName('em')[0] + , passesLink = items[1].getElementsByTagName('a')[0] + , failures = items[2].getElementsByTagName('em')[0] + , failuresLink = items[2].getElementsByTagName('a')[0] + , duration = items[3].getElementsByTagName('em')[0] + , canvas = stat.getElementsByTagName('canvas')[0] + , report = fragment('
      ') + , stack = [report] + , progress + , ctx + , root = document.getElementById('mocha'); + + if (canvas.getContext) { + var ratio = window.devicePixelRatio || 1; + canvas.style.width = canvas.width; + canvas.style.height = canvas.height; + canvas.width *= ratio; + canvas.height *= ratio; + ctx = canvas.getContext('2d'); + ctx.scale(ratio, ratio); + progress = new Progress; + } + + if (!root) return error('#mocha div missing, add it to your document'); + + // pass toggle + on(passesLink, 'click', function(){ + unhide(); + var name = /pass/.test(report.className) ? '' : ' pass'; + report.className = report.className.replace(/fail|pass/g, '') + name; + if (report.className.trim()) hideSuitesWithout('test pass'); + }); + + // failure toggle + on(failuresLink, 'click', function(){ + unhide(); + var name = /fail/.test(report.className) ? '' : ' fail'; + report.className = report.className.replace(/fail|pass/g, '') + name; + if (report.className.trim()) hideSuitesWithout('test fail'); + }); + + root.appendChild(stat); + root.appendChild(report); + + if (progress) progress.size(40); + + runner.on('suite', function(suite){ + if (suite.root) return; + + // suite + var url = self.suiteURL(suite); + var el = fragment('
    • %s

    • ', url, escape(suite.title)); + + // container + stack[0].appendChild(el); + stack.unshift(document.createElement('ul')); + el.appendChild(stack[0]); + }); + + runner.on('suite end', function(suite){ + if (suite.root) return; + stack.shift(); + }); + + runner.on('fail', function(test, err){ + if ('hook' == test.type) runner.emit('test end', test); + }); + + runner.on('test end', function(test){ + // TODO: add to stats + var percent = stats.tests / this.total * 100 | 0; + if (progress) progress.update(percent).draw(ctx); + + // update stats + var ms = new Date - stats.start; + text(passes, stats.passes); + text(failures, stats.failures); + text(duration, (ms / 1000).toFixed(2)); + + // test + if ('passed' == test.state) { + var url = self.testURL(test); + var el = fragment('
    • %e%ems

    • ', test.speed, test.title, test.duration, url); + } else if (test.pending) { + var el = fragment('
    • %e

    • ', test.title); + } else { + var el = fragment('
    • %e

    • ', test.title, encodeURIComponent(test.fullTitle())); + var str = test.err.stack || test.err.toString(); + + // FF / Opera do not add the message + if (!~str.indexOf(test.err.message)) { + str = test.err.message + '\n' + str; + } + + // <=IE7 stringifies to [Object Error]. Since it can be overloaded, we + // check for the result of the stringifying. + if ('[object Error]' == str) str = test.err.message; + + // Safari doesn't give you a stack. Let's at least provide a source line. + if (!test.err.stack && test.err.sourceURL && test.err.line !== undefined) { + str += "\n(" + test.err.sourceURL + ":" + test.err.line + ")"; + } + + el.appendChild(fragment('
      %e
      ', str)); + } + + // toggle code + // TODO: defer + if (!test.pending) { + var h2 = el.getElementsByTagName('h2')[0]; + + on(h2, 'click', function(){ + pre.style.display = 'none' == pre.style.display + ? 'block' + : 'none'; + }); + + var pre = fragment('
      %e
      ', utils.clean(test.fn.toString())); + el.appendChild(pre); + pre.style.display = 'none'; + } + + // Don't call .appendChild if #mocha-report was already .shift()'ed off the stack. + if (stack[0]) stack[0].appendChild(el); + }); +} + +/** + * Provide suite URL + * + * @param {Object} [suite] + */ + +HTML.prototype.suiteURL = function(suite){ + return '?grep=' + encodeURIComponent(suite.fullTitle()); +}; + +/** + * Provide test URL + * + * @param {Object} [test] + */ + +HTML.prototype.testURL = function(test){ + return '?grep=' + encodeURIComponent(test.fullTitle()); +}; + +/** + * Display error `msg`. + */ + +function error(msg) { + document.body.appendChild(fragment('
      %s
      ', msg)); +} + +/** + * Return a DOM fragment from `html`. + */ + +function fragment(html) { + var args = arguments + , div = document.createElement('div') + , i = 1; + + div.innerHTML = html.replace(/%([se])/g, function(_, type){ + switch (type) { + case 's': return String(args[i++]); + case 'e': return escape(args[i++]); + } + }); + + return div.firstChild; +} + +/** + * Check for suites that do not have elements + * with `classname`, and hide them. + */ + +function hideSuitesWithout(classname) { + var suites = document.getElementsByClassName('suite'); + for (var i = 0; i < suites.length; i++) { + var els = suites[i].getElementsByClassName(classname); + if (0 == els.length) suites[i].className += ' hidden'; + } +} + +/** + * Unhide .hidden suites. + */ + +function unhide() { + var els = document.getElementsByClassName('suite hidden'); + for (var i = 0; i < els.length; ++i) { + els[i].className = els[i].className.replace('suite hidden', 'suite'); + } +} + +/** + * Set `el` text to `str`. + */ + +function text(el, str) { + if (el.textContent) { + el.textContent = str; + } else { + el.innerText = str; + } +} + +/** + * Listen on `event` with callback `fn`. + */ + +function on(el, event, fn) { + if (el.addEventListener) { + el.addEventListener(event, fn, false); + } else { + el.attachEvent('on' + event, fn); + } +} + +}); // module: reporters/html.js + +require.register("reporters/index.js", function(module, exports, require){ + +exports.Base = require('./base'); +exports.Dot = require('./dot'); +exports.Doc = require('./doc'); +exports.TAP = require('./tap'); +exports.JSON = require('./json'); +exports.HTML = require('./html'); +exports.List = require('./list'); +exports.Min = require('./min'); +exports.Spec = require('./spec'); +exports.Nyan = require('./nyan'); +exports.XUnit = require('./xunit'); +exports.Markdown = require('./markdown'); +exports.Progress = require('./progress'); +exports.Landing = require('./landing'); +exports.JSONCov = require('./json-cov'); +exports.HTMLCov = require('./html-cov'); +exports.JSONStream = require('./json-stream'); + +}); // module: reporters/index.js + +require.register("reporters/json-cov.js", function(module, exports, require){ + +/** + * Module dependencies. + */ + +var Base = require('./base'); + +/** + * Expose `JSONCov`. + */ + +exports = module.exports = JSONCov; + +/** + * Initialize a new `JsCoverage` reporter. + * + * @param {Runner} runner + * @param {Boolean} output + * @api public + */ + +function JSONCov(runner, output) { + var self = this + , output = 1 == arguments.length ? true : output; + + Base.call(this, runner); + + var tests = [] + , failures = [] + , passes = []; + + runner.on('test end', function(test){ + tests.push(test); + }); + + runner.on('pass', function(test){ + passes.push(test); + }); + + runner.on('fail', function(test){ + failures.push(test); + }); + + runner.on('end', function(){ + var cov = global._$jscoverage || {}; + var result = self.cov = map(cov); + result.stats = self.stats; + result.tests = tests.map(clean); + result.failures = failures.map(clean); + result.passes = passes.map(clean); + if (!output) return; + process.stdout.write(JSON.stringify(result, null, 2 )); + }); +} + +/** + * Map jscoverage data to a JSON structure + * suitable for reporting. + * + * @param {Object} cov + * @return {Object} + * @api private + */ + +function map(cov) { + var ret = { + instrumentation: 'node-jscoverage' + , sloc: 0 + , hits: 0 + , misses: 0 + , coverage: 0 + , files: [] + }; + + for (var filename in cov) { + var data = coverage(filename, cov[filename]); + ret.files.push(data); + ret.hits += data.hits; + ret.misses += data.misses; + ret.sloc += data.sloc; + } + + ret.files.sort(function(a, b) { + return a.filename.localeCompare(b.filename); + }); + + if (ret.sloc > 0) { + ret.coverage = (ret.hits / ret.sloc) * 100; + } + + return ret; +}; + +/** + * Map jscoverage data for a single source file + * to a JSON structure suitable for reporting. + * + * @param {String} filename name of the source file + * @param {Object} data jscoverage coverage data + * @return {Object} + * @api private + */ + +function coverage(filename, data) { + var ret = { + filename: filename, + coverage: 0, + hits: 0, + misses: 0, + sloc: 0, + source: {} + }; + + data.source.forEach(function(line, num){ + num++; + + if (data[num] === 0) { + ret.misses++; + ret.sloc++; + } else if (data[num] !== undefined) { + ret.hits++; + ret.sloc++; + } + + ret.source[num] = { + source: line + , coverage: data[num] === undefined + ? '' + : data[num] + }; + }); + + ret.coverage = ret.hits / ret.sloc * 100; + + return ret; +} + +/** + * Return a plain-object representation of `test` + * free of cyclic properties etc. + * + * @param {Object} test + * @return {Object} + * @api private + */ + +function clean(test) { + return { + title: test.title + , fullTitle: test.fullTitle() + , duration: test.duration + } +} + +}); // module: reporters/json-cov.js + +require.register("reporters/json-stream.js", function(module, exports, require){ + +/** + * Module dependencies. + */ + +var Base = require('./base') + , color = Base.color; + +/** + * Expose `List`. + */ + +exports = module.exports = List; + +/** + * Initialize a new `List` test reporter. + * + * @param {Runner} runner + * @api public + */ + +function List(runner) { + Base.call(this, runner); + + var self = this + , stats = this.stats + , total = runner.total; + + runner.on('start', function(){ + console.log(JSON.stringify(['start', { total: total }])); + }); + + runner.on('pass', function(test){ + console.log(JSON.stringify(['pass', clean(test)])); + }); + + runner.on('fail', function(test, err){ + console.log(JSON.stringify(['fail', clean(test)])); + }); + + runner.on('end', function(){ + process.stdout.write(JSON.stringify(['end', self.stats])); + }); +} + +/** + * Return a plain-object representation of `test` + * free of cyclic properties etc. + * + * @param {Object} test + * @return {Object} + * @api private + */ + +function clean(test) { + return { + title: test.title + , fullTitle: test.fullTitle() + , duration: test.duration + } +} +}); // module: reporters/json-stream.js + +require.register("reporters/json.js", function(module, exports, require){ + +/** + * Module dependencies. + */ + +var Base = require('./base') + , cursor = Base.cursor + , color = Base.color; + +/** + * Expose `JSON`. + */ + +exports = module.exports = JSONReporter; + +/** + * Initialize a new `JSON` reporter. + * + * @param {Runner} runner + * @api public + */ + +function JSONReporter(runner) { + var self = this; + Base.call(this, runner); + + var tests = [] + , failures = [] + , passes = []; + + runner.on('test end', function(test){ + tests.push(test); + }); + + runner.on('pass', function(test){ + passes.push(test); + }); + + runner.on('fail', function(test){ + failures.push(test); + }); + + runner.on('end', function(){ + var obj = { + stats: self.stats + , tests: tests.map(clean) + , failures: failures.map(clean) + , passes: passes.map(clean) + }; + + process.stdout.write(JSON.stringify(obj, null, 2)); + }); +} + +/** + * Return a plain-object representation of `test` + * free of cyclic properties etc. + * + * @param {Object} test + * @return {Object} + * @api private + */ + +function clean(test) { + return { + title: test.title + , fullTitle: test.fullTitle() + , duration: test.duration + } +} +}); // module: reporters/json.js + +require.register("reporters/landing.js", function(module, exports, require){ + +/** + * Module dependencies. + */ + +var Base = require('./base') + , cursor = Base.cursor + , color = Base.color; + +/** + * Expose `Landing`. + */ + +exports = module.exports = Landing; + +/** + * Airplane color. + */ + +Base.colors.plane = 0; + +/** + * Airplane crash color. + */ + +Base.colors['plane crash'] = 31; + +/** + * Runway color. + */ + +Base.colors.runway = 90; + +/** + * Initialize a new `Landing` reporter. + * + * @param {Runner} runner + * @api public + */ + +function Landing(runner) { + Base.call(this, runner); + + var self = this + , stats = this.stats + , width = Base.window.width * .75 | 0 + , total = runner.total + , stream = process.stdout + , plane = color('plane', '✈') + , crashed = -1 + , n = 0; + + function runway() { + var buf = Array(width).join('-'); + return ' ' + color('runway', buf); + } + + runner.on('start', function(){ + stream.write('\n '); + cursor.hide(); + }); + + runner.on('test end', function(test){ + // check if the plane crashed + var col = -1 == crashed + ? width * ++n / total | 0 + : crashed; + + // show the crash + if ('failed' == test.state) { + plane = color('plane crash', '✈'); + crashed = col; + } + + // render landing strip + stream.write('\u001b[4F\n\n'); + stream.write(runway()); + stream.write('\n '); + stream.write(color('runway', Array(col).join('⋅'))); + stream.write(plane) + stream.write(color('runway', Array(width - col).join('⋅') + '\n')); + stream.write(runway()); + stream.write('\u001b[0m'); + }); + + runner.on('end', function(){ + cursor.show(); + console.log(); + self.epilogue(); + }); +} + +/** + * Inherit from `Base.prototype`. + */ + +function F(){}; +F.prototype = Base.prototype; +Landing.prototype = new F; +Landing.prototype.constructor = Landing; + +}); // module: reporters/landing.js + +require.register("reporters/list.js", function(module, exports, require){ + +/** + * Module dependencies. + */ + +var Base = require('./base') + , cursor = Base.cursor + , color = Base.color; + +/** + * Expose `List`. + */ + +exports = module.exports = List; + +/** + * Initialize a new `List` test reporter. + * + * @param {Runner} runner + * @api public + */ + +function List(runner) { + Base.call(this, runner); + + var self = this + , stats = this.stats + , n = 0; + + runner.on('start', function(){ + console.log(); + }); + + runner.on('test', function(test){ + process.stdout.write(color('pass', ' ' + test.fullTitle() + ': ')); + }); + + runner.on('pending', function(test){ + var fmt = color('checkmark', ' -') + + color('pending', ' %s'); + console.log(fmt, test.fullTitle()); + }); + + runner.on('pass', function(test){ + var fmt = color('checkmark', ' '+Base.symbols.dot) + + color('pass', ' %s: ') + + color(test.speed, '%dms'); + cursor.CR(); + console.log(fmt, test.fullTitle(), test.duration); + }); + + runner.on('fail', function(test, err){ + cursor.CR(); + console.log(color('fail', ' %d) %s'), ++n, test.fullTitle()); + }); + + runner.on('end', self.epilogue.bind(self)); +} + +/** + * Inherit from `Base.prototype`. + */ + +function F(){}; +F.prototype = Base.prototype; +List.prototype = new F; +List.prototype.constructor = List; + + +}); // module: reporters/list.js + +require.register("reporters/markdown.js", function(module, exports, require){ +/** + * Module dependencies. + */ + +var Base = require('./base') + , utils = require('../utils'); + +/** + * Expose `Markdown`. + */ + +exports = module.exports = Markdown; + +/** + * Initialize a new `Markdown` reporter. + * + * @param {Runner} runner + * @api public + */ + +function Markdown(runner) { + Base.call(this, runner); + + var self = this + , stats = this.stats + , level = 0 + , buf = ''; + + function title(str) { + return Array(level).join('#') + ' ' + str; + } + + function indent() { + return Array(level).join(' '); + } + + function mapTOC(suite, obj) { + var ret = obj; + obj = obj[suite.title] = obj[suite.title] || { suite: suite }; + suite.suites.forEach(function(suite){ + mapTOC(suite, obj); + }); + return ret; + } + + function stringifyTOC(obj, level) { + ++level; + var buf = ''; + var link; + for (var key in obj) { + if ('suite' == key) continue; + if (key) link = ' - [' + key + '](#' + utils.slug(obj[key].suite.fullTitle()) + ')\n'; + if (key) buf += Array(level).join(' ') + link; + buf += stringifyTOC(obj[key], level); + } + --level; + return buf; + } + + function generateTOC(suite) { + var obj = mapTOC(suite, {}); + return stringifyTOC(obj, 0); + } + + generateTOC(runner.suite); + + runner.on('suite', function(suite){ + ++level; + var slug = utils.slug(suite.fullTitle()); + buf += '' + '\n'; + buf += title(suite.title) + '\n'; + }); + + runner.on('suite end', function(suite){ + --level; + }); + + runner.on('pass', function(test){ + var code = utils.clean(test.fn.toString()); + buf += test.title + '.\n'; + buf += '\n```js\n'; + buf += code + '\n'; + buf += '```\n\n'; + }); + + runner.on('end', function(){ + process.stdout.write('# TOC\n'); + process.stdout.write(generateTOC(runner.suite)); + process.stdout.write(buf); + }); +} +}); // module: reporters/markdown.js + +require.register("reporters/min.js", function(module, exports, require){ + +/** + * Module dependencies. + */ + +var Base = require('./base'); + +/** + * Expose `Min`. + */ + +exports = module.exports = Min; + +/** + * Initialize a new `Min` minimal test reporter (best used with --watch). + * + * @param {Runner} runner + * @api public + */ + +function Min(runner) { + Base.call(this, runner); + + runner.on('start', function(){ + // clear screen + process.stdout.write('\u001b[2J'); + // set cursor position + process.stdout.write('\u001b[1;3H'); + }); + + runner.on('end', this.epilogue.bind(this)); +} + +/** + * Inherit from `Base.prototype`. + */ + +function F(){}; +F.prototype = Base.prototype; +Min.prototype = new F; +Min.prototype.constructor = Min; + + +}); // module: reporters/min.js + +require.register("reporters/nyan.js", function(module, exports, require){ +/** + * Module dependencies. + */ + +var Base = require('./base') + , color = Base.color; + +/** + * Expose `Dot`. + */ + +exports = module.exports = NyanCat; + +/** + * Initialize a new `Dot` matrix test reporter. + * + * @param {Runner} runner + * @api public + */ + +function NyanCat(runner) { + Base.call(this, runner); + var self = this + , stats = this.stats + , width = Base.window.width * .75 | 0 + , rainbowColors = this.rainbowColors = self.generateColors() + , colorIndex = this.colorIndex = 0 + , numerOfLines = this.numberOfLines = 4 + , trajectories = this.trajectories = [[], [], [], []] + , nyanCatWidth = this.nyanCatWidth = 11 + , trajectoryWidthMax = this.trajectoryWidthMax = (width - nyanCatWidth) + , scoreboardWidth = this.scoreboardWidth = 5 + , tick = this.tick = 0 + , n = 0; + + runner.on('start', function(){ + Base.cursor.hide(); + self.draw(); + }); + + runner.on('pending', function(test){ + self.draw(); + }); + + runner.on('pass', function(test){ + self.draw(); + }); + + runner.on('fail', function(test, err){ + self.draw(); + }); + + runner.on('end', function(){ + Base.cursor.show(); + for (var i = 0; i < self.numberOfLines; i++) write('\n'); + self.epilogue(); + }); +} + +/** + * Draw the nyan cat + * + * @api private + */ + +NyanCat.prototype.draw = function(){ + this.appendRainbow(); + this.drawScoreboard(); + this.drawRainbow(); + this.drawNyanCat(); + this.tick = !this.tick; +}; + +/** + * Draw the "scoreboard" showing the number + * of passes, failures and pending tests. + * + * @api private + */ + +NyanCat.prototype.drawScoreboard = function(){ + var stats = this.stats; + var colors = Base.colors; + + function draw(color, n) { + write(' '); + write('\u001b[' + color + 'm' + n + '\u001b[0m'); + write('\n'); + } + + draw(colors.green, stats.passes); + draw(colors.fail, stats.failures); + draw(colors.pending, stats.pending); + write('\n'); + + this.cursorUp(this.numberOfLines); +}; + +/** + * Append the rainbow. + * + * @api private + */ + +NyanCat.prototype.appendRainbow = function(){ + var segment = this.tick ? '_' : '-'; + var rainbowified = this.rainbowify(segment); + + for (var index = 0; index < this.numberOfLines; index++) { + var trajectory = this.trajectories[index]; + if (trajectory.length >= this.trajectoryWidthMax) trajectory.shift(); + trajectory.push(rainbowified); + } +}; + +/** + * Draw the rainbow. + * + * @api private + */ + +NyanCat.prototype.drawRainbow = function(){ + var self = this; + + this.trajectories.forEach(function(line, index) { + write('\u001b[' + self.scoreboardWidth + 'C'); + write(line.join('')); + write('\n'); + }); + + this.cursorUp(this.numberOfLines); +}; + +/** + * Draw the nyan cat + * + * @api private + */ + +NyanCat.prototype.drawNyanCat = function() { + var self = this; + var startWidth = this.scoreboardWidth + this.trajectories[0].length; + var color = '\u001b[' + startWidth + 'C'; + var padding = ''; + + write(color); + write('_,------,'); + write('\n'); + + write(color); + padding = self.tick ? ' ' : ' '; + write('_|' + padding + '/\\_/\\ '); + write('\n'); + + write(color); + padding = self.tick ? '_' : '__'; + var tail = self.tick ? '~' : '^'; + var face; + write(tail + '|' + padding + this.face() + ' '); + write('\n'); + + write(color); + padding = self.tick ? ' ' : ' '; + write(padding + '"" "" '); + write('\n'); + + this.cursorUp(this.numberOfLines); +}; + +/** + * Draw nyan cat face. + * + * @return {String} + * @api private + */ + +NyanCat.prototype.face = function() { + var stats = this.stats; + if (stats.failures) { + return '( x .x)'; + } else if (stats.pending) { + return '( o .o)'; + } else if(stats.passes) { + return '( ^ .^)'; + } else { + return '( - .-)'; + } +} + +/** + * Move cursor up `n`. + * + * @param {Number} n + * @api private + */ + +NyanCat.prototype.cursorUp = function(n) { + write('\u001b[' + n + 'A'); +}; + +/** + * Move cursor down `n`. + * + * @param {Number} n + * @api private + */ + +NyanCat.prototype.cursorDown = function(n) { + write('\u001b[' + n + 'B'); +}; + +/** + * Generate rainbow colors. + * + * @return {Array} + * @api private + */ + +NyanCat.prototype.generateColors = function(){ + var colors = []; + + for (var i = 0; i < (6 * 7); i++) { + var pi3 = Math.floor(Math.PI / 3); + var n = (i * (1.0 / 6)); + var r = Math.floor(3 * Math.sin(n) + 3); + var g = Math.floor(3 * Math.sin(n + 2 * pi3) + 3); + var b = Math.floor(3 * Math.sin(n + 4 * pi3) + 3); + colors.push(36 * r + 6 * g + b + 16); + } + + return colors; +}; + +/** + * Apply rainbow to the given `str`. + * + * @param {String} str + * @return {String} + * @api private + */ + +NyanCat.prototype.rainbowify = function(str){ + var color = this.rainbowColors[this.colorIndex % this.rainbowColors.length]; + this.colorIndex += 1; + return '\u001b[38;5;' + color + 'm' + str + '\u001b[0m'; +}; + +/** + * Stdout helper. + */ + +function write(string) { + process.stdout.write(string); +} + +/** + * Inherit from `Base.prototype`. + */ + +function F(){}; +F.prototype = Base.prototype; +NyanCat.prototype = new F; +NyanCat.prototype.constructor = NyanCat; + + +}); // module: reporters/nyan.js + +require.register("reporters/progress.js", function(module, exports, require){ + +/** + * Module dependencies. + */ + +var Base = require('./base') + , cursor = Base.cursor + , color = Base.color; + +/** + * Expose `Progress`. + */ + +exports = module.exports = Progress; + +/** + * General progress bar color. + */ + +Base.colors.progress = 90; + +/** + * Initialize a new `Progress` bar test reporter. + * + * @param {Runner} runner + * @param {Object} options + * @api public + */ + +function Progress(runner, options) { + Base.call(this, runner); + + var self = this + , options = options || {} + , stats = this.stats + , width = Base.window.width * .50 | 0 + , total = runner.total + , complete = 0 + , max = Math.max; + + // default chars + options.open = options.open || '['; + options.complete = options.complete || '▬'; + options.incomplete = options.incomplete || Base.symbols.dot; + options.close = options.close || ']'; + options.verbose = false; + + // tests started + runner.on('start', function(){ + console.log(); + cursor.hide(); + }); + + // tests complete + runner.on('test end', function(){ + complete++; + var incomplete = total - complete + , percent = complete / total + , n = width * percent | 0 + , i = width - n; + + cursor.CR(); + process.stdout.write('\u001b[J'); + process.stdout.write(color('progress', ' ' + options.open)); + process.stdout.write(Array(n).join(options.complete)); + process.stdout.write(Array(i).join(options.incomplete)); + process.stdout.write(color('progress', options.close)); + if (options.verbose) { + process.stdout.write(color('progress', ' ' + complete + ' of ' + total)); + } + }); + + // tests are complete, output some stats + // and the failures if any + runner.on('end', function(){ + cursor.show(); + console.log(); + self.epilogue(); + }); +} + +/** + * Inherit from `Base.prototype`. + */ + +function F(){}; +F.prototype = Base.prototype; +Progress.prototype = new F; +Progress.prototype.constructor = Progress; + + +}); // module: reporters/progress.js + +require.register("reporters/spec.js", function(module, exports, require){ + +/** + * Module dependencies. + */ + +var Base = require('./base') + , cursor = Base.cursor + , color = Base.color; + +/** + * Expose `Spec`. + */ + +exports = module.exports = Spec; + +/** + * Initialize a new `Spec` test reporter. + * + * @param {Runner} runner + * @api public + */ + +function Spec(runner) { + Base.call(this, runner); + + var self = this + , stats = this.stats + , indents = 0 + , n = 0; + + function indent() { + return Array(indents).join(' ') + } + + runner.on('start', function(){ + console.log(); + }); + + runner.on('suite', function(suite){ + ++indents; + console.log(color('suite', '%s%s'), indent(), suite.title); + }); + + runner.on('suite end', function(suite){ + --indents; + if (1 == indents) console.log(); + }); + + runner.on('pending', function(test){ + var fmt = indent() + color('pending', ' - %s'); + console.log(fmt, test.title); + }); + + runner.on('pass', function(test){ + if ('fast' == test.speed) { + var fmt = indent() + + color('checkmark', ' ' + Base.symbols.ok) + + color('pass', ' %s '); + cursor.CR(); + console.log(fmt, test.title); + } else { + var fmt = indent() + + color('checkmark', ' ' + Base.symbols.ok) + + color('pass', ' %s ') + + color(test.speed, '(%dms)'); + cursor.CR(); + console.log(fmt, test.title, test.duration); + } + }); + + runner.on('fail', function(test, err){ + cursor.CR(); + console.log(indent() + color('fail', ' %d) %s'), ++n, test.title); + }); + + runner.on('end', self.epilogue.bind(self)); +} + +/** + * Inherit from `Base.prototype`. + */ + +function F(){}; +F.prototype = Base.prototype; +Spec.prototype = new F; +Spec.prototype.constructor = Spec; + + +}); // module: reporters/spec.js + +require.register("reporters/tap.js", function(module, exports, require){ + +/** + * Module dependencies. + */ + +var Base = require('./base') + , cursor = Base.cursor + , color = Base.color; + +/** + * Expose `TAP`. + */ + +exports = module.exports = TAP; + +/** + * Initialize a new `TAP` reporter. + * + * @param {Runner} runner + * @api public + */ + +function TAP(runner) { + Base.call(this, runner); + + var self = this + , stats = this.stats + , n = 1 + , passes = 0 + , failures = 0; + + runner.on('start', function(){ + var total = runner.grepTotal(runner.suite); + console.log('%d..%d', 1, total); + }); + + runner.on('test end', function(){ + ++n; + }); + + runner.on('pending', function(test){ + console.log('ok %d %s # SKIP -', n, title(test)); + }); + + runner.on('pass', function(test){ + passes++; + console.log('ok %d %s', n, title(test)); + }); + + runner.on('fail', function(test, err){ + failures++; + console.log('not ok %d %s', n, title(test)); + if (err.stack) console.log(err.stack.replace(/^/gm, ' ')); + }); + + runner.on('end', function(){ + console.log('# tests ' + (passes + failures)); + console.log('# pass ' + passes); + console.log('# fail ' + failures); + }); +} + +/** + * Return a TAP-safe title of `test` + * + * @param {Object} test + * @return {String} + * @api private + */ + +function title(test) { + return test.fullTitle().replace(/#/g, ''); +} + +}); // module: reporters/tap.js + +require.register("reporters/xunit.js", function(module, exports, require){ + +/** + * Module dependencies. + */ + +var Base = require('./base') + , utils = require('../utils') + , escape = utils.escape; + +/** + * Save timer references to avoid Sinon interfering (see GH-237). + */ + +var Date = global.Date + , setTimeout = global.setTimeout + , setInterval = global.setInterval + , clearTimeout = global.clearTimeout + , clearInterval = global.clearInterval; + +/** + * Expose `XUnit`. + */ + +exports = module.exports = XUnit; + +/** + * Initialize a new `XUnit` reporter. + * + * @param {Runner} runner + * @api public + */ + +function XUnit(runner) { + Base.call(this, runner); + var stats = this.stats + , tests = [] + , self = this; + + runner.on('pending', function(test){ + tests.push(test); + }); + + runner.on('pass', function(test){ + tests.push(test); + }); + + runner.on('fail', function(test){ + tests.push(test); + }); + + runner.on('end', function(){ + console.log(tag('testsuite', { + name: 'Mocha Tests' + , tests: stats.tests + , failures: stats.failures + , errors: stats.failures + , skipped: stats.tests - stats.failures - stats.passes + , timestamp: (new Date).toUTCString() + , time: (stats.duration / 1000) || 0 + }, false)); + + tests.forEach(test); + console.log(''); + }); +} + +/** + * Inherit from `Base.prototype`. + */ + +function F(){}; +F.prototype = Base.prototype; +XUnit.prototype = new F; +XUnit.prototype.constructor = XUnit; + + +/** + * Output tag for the given `test.` + */ + +function test(test) { + var attrs = { + classname: test.parent.fullTitle() + , name: test.title + , time: (test.duration / 1000) || 0 + }; + + if ('failed' == test.state) { + var err = test.err; + attrs.message = escape(err.message); + console.log(tag('testcase', attrs, false, tag('failure', attrs, false, cdata(err.stack)))); + } else if (test.pending) { + console.log(tag('testcase', attrs, false, tag('skipped', {}, true))); + } else { + console.log(tag('testcase', attrs, true) ); + } +} + +/** + * HTML tag helper. + */ + +function tag(name, attrs, close, content) { + var end = close ? '/>' : '>' + , pairs = [] + , tag; + + for (var key in attrs) { + pairs.push(key + '="' + escape(attrs[key]) + '"'); + } + + tag = '<' + name + (pairs.length ? ' ' + pairs.join(' ') : '') + end; + if (content) tag += content + ''; +} + +}); // module: reporters/xunit.js + +require.register("runnable.js", function(module, exports, require){ + +/** + * Module dependencies. + */ + +var EventEmitter = require('browser/events').EventEmitter + , debug = require('browser/debug')('mocha:runnable') + , milliseconds = require('./ms'); + +/** + * Save timer references to avoid Sinon interfering (see GH-237). + */ + +var Date = global.Date + , setTimeout = global.setTimeout + , setInterval = global.setInterval + , clearTimeout = global.clearTimeout + , clearInterval = global.clearInterval; + +/** + * Object#toString(). + */ + +var toString = Object.prototype.toString; + +/** + * Expose `Runnable`. + */ + +module.exports = Runnable; + +/** + * Initialize a new `Runnable` with the given `title` and callback `fn`. + * + * @param {String} title + * @param {Function} fn + * @api private + */ + +function Runnable(title, fn) { + this.title = title; + this.fn = fn; + this.async = fn && fn.length; + this.sync = ! this.async; + this._timeout = 2000; + this._slow = 75; + this.timedOut = false; +} + +/** + * Inherit from `EventEmitter.prototype`. + */ + +function F(){}; +F.prototype = EventEmitter.prototype; +Runnable.prototype = new F; +Runnable.prototype.constructor = Runnable; + + +/** + * Set & get timeout `ms`. + * + * @param {Number|String} ms + * @return {Runnable|Number} ms or self + * @api private + */ + +Runnable.prototype.timeout = function(ms){ + if (0 == arguments.length) return this._timeout; + if ('string' == typeof ms) ms = milliseconds(ms); + debug('timeout %d', ms); + this._timeout = ms; + if (this.timer) this.resetTimeout(); + return this; +}; + +/** + * Set & get slow `ms`. + * + * @param {Number|String} ms + * @return {Runnable|Number} ms or self + * @api private + */ + +Runnable.prototype.slow = function(ms){ + if (0 === arguments.length) return this._slow; + if ('string' == typeof ms) ms = milliseconds(ms); + debug('timeout %d', ms); + this._slow = ms; + return this; +}; + +/** + * Return the full title generated by recursively + * concatenating the parent's full title. + * + * @return {String} + * @api public + */ + +Runnable.prototype.fullTitle = function(){ + return this.parent.fullTitle() + ' ' + this.title; +}; + +/** + * Clear the timeout. + * + * @api private + */ + +Runnable.prototype.clearTimeout = function(){ + clearTimeout(this.timer); +}; + +/** + * Inspect the runnable void of private properties. + * + * @return {String} + * @api private + */ + +Runnable.prototype.inspect = function(){ + return JSON.stringify(this, function(key, val){ + if ('_' == key[0]) return; + if ('parent' == key) return '#'; + if ('ctx' == key) return '#'; + return val; + }, 2); +}; + +/** + * Reset the timeout. + * + * @api private + */ + +Runnable.prototype.resetTimeout = function(){ + var self = this; + var ms = this.timeout() || 1e9; + + this.clearTimeout(); + this.timer = setTimeout(function(){ + self.callback(new Error('timeout of ' + ms + 'ms exceeded')); + self.timedOut = true; + }, ms); +}; + +/** + * Whitelist these globals for this test run + * + * @api private + */ +Runnable.prototype.globals = function(arr){ + var self = this; + this._allowedGlobals = arr; +}; + +/** + * Run the test and invoke `fn(err)`. + * + * @param {Function} fn + * @api private + */ + +Runnable.prototype.run = function(fn){ + var self = this + , ms = this.timeout() + , start = new Date + , ctx = this.ctx + , finished + , emitted; + + if (ctx) ctx.runnable(this); + + // called multiple times + function multiple(err) { + if (emitted) return; + emitted = true; + self.emit('error', err || new Error('done() called multiple times')); + } + + // finished + function done(err) { + if (self.timedOut) return; + if (finished) return multiple(err); + self.clearTimeout(); + self.duration = new Date - start; + finished = true; + fn(err); + } + + // for .resetTimeout() + this.callback = done; + + // explicit async with `done` argument + if (this.async) { + this.resetTimeout(); + + try { + this.fn.call(ctx, function(err){ + if (err instanceof Error || toString.call(err) === "[object Error]") return done(err); + if (null != err) return done(new Error('done() invoked with non-Error: ' + err)); + done(); + }); + } catch (err) { + done(err); + } + return; + } + + if (this.asyncOnly) { + return done(new Error('--async-only option in use without declaring `done()`')); + } + + // sync or promise-returning + try { + if (this.pending) { + done(); + } else { + callFn(this.fn); + } + } catch (err) { + done(err); + } + + function callFn(fn) { + var result = fn.call(ctx); + if (result && typeof result.then === 'function') { + self.resetTimeout(); + result.then(function(){ done() }, done); + } else { + done(); + } + } +}; + +}); // module: runnable.js + +require.register("runner.js", function(module, exports, require){ +/** + * Module dependencies. + */ + +var EventEmitter = require('browser/events').EventEmitter + , debug = require('browser/debug')('mocha:runner') + , Test = require('./test') + , utils = require('./utils') + , filter = utils.filter + , keys = utils.keys; + +/** + * Non-enumerable globals. + */ + +var globals = [ + 'setTimeout', + 'clearTimeout', + 'setInterval', + 'clearInterval', + 'XMLHttpRequest', + 'Date' +]; + +/** + * Expose `Runner`. + */ + +module.exports = Runner; + +/** + * Initialize a `Runner` for the given `suite`. + * + * Events: + * + * - `start` execution started + * - `end` execution complete + * - `suite` (suite) test suite execution started + * - `suite end` (suite) all tests (and sub-suites) have finished + * - `test` (test) test execution started + * - `test end` (test) test completed + * - `hook` (hook) hook execution started + * - `hook end` (hook) hook complete + * - `pass` (test) test passed + * - `fail` (test, err) test failed + * - `pending` (test) test pending + * + * @api public + */ + +function Runner(suite) { + var self = this; + this._globals = []; + this._abort = false; + this.suite = suite; + this.total = suite.total(); + this.failures = 0; + this.on('test end', function(test){ self.checkGlobals(test); }); + this.on('hook end', function(hook){ self.checkGlobals(hook); }); + this.grep(/.*/); + this.globals(this.globalProps().concat(extraGlobals())); +} + +/** + * Wrapper for setImmediate, process.nextTick, or browser polyfill. + * + * @param {Function} fn + * @api private + */ + +Runner.immediately = global.setImmediate || process.nextTick; + +/** + * Inherit from `EventEmitter.prototype`. + */ + +function F(){}; +F.prototype = EventEmitter.prototype; +Runner.prototype = new F; +Runner.prototype.constructor = Runner; + + +/** + * Run tests with full titles matching `re`. Updates runner.total + * with number of tests matched. + * + * @param {RegExp} re + * @param {Boolean} invert + * @return {Runner} for chaining + * @api public + */ + +Runner.prototype.grep = function(re, invert){ + debug('grep %s', re); + this._grep = re; + this._invert = invert; + this.total = this.grepTotal(this.suite); + return this; +}; + +/** + * Returns the number of tests matching the grep search for the + * given suite. + * + * @param {Suite} suite + * @return {Number} + * @api public + */ + +Runner.prototype.grepTotal = function(suite) { + var self = this; + var total = 0; + + suite.eachTest(function(test){ + var match = self._grep.test(test.fullTitle()); + if (self._invert) match = !match; + if (match) total++; + }); + + return total; +}; + +/** + * Return a list of global properties. + * + * @return {Array} + * @api private + */ + +Runner.prototype.globalProps = function() { + var props = utils.keys(global); + + // non-enumerables + for (var i = 0; i < globals.length; ++i) { + if (~utils.indexOf(props, globals[i])) continue; + props.push(globals[i]); + } + + return props; +}; + +/** + * Allow the given `arr` of globals. + * + * @param {Array} arr + * @return {Runner} for chaining + * @api public + */ + +Runner.prototype.globals = function(arr){ + if (0 == arguments.length) return this._globals; + debug('globals %j', arr); + this._globals = this._globals.concat(arr); + return this; +}; + +/** + * Check for global variable leaks. + * + * @api private + */ + +Runner.prototype.checkGlobals = function(test){ + if (this.ignoreLeaks) return; + var ok = this._globals; + + var globals = this.globalProps(); + var isNode = process.kill; + var leaks; + + if (test) { + ok = ok.concat(test._allowedGlobals || []); + } + + if(this.prevGlobalsLength == globals.length) return; + this.prevGlobalsLength = globals.length; + + leaks = filterLeaks(ok, globals); + this._globals = this._globals.concat(leaks); + + if (leaks.length > 1) { + this.fail(test, new Error('global leaks detected: ' + leaks.join(', ') + '')); + } else if (leaks.length) { + this.fail(test, new Error('global leak detected: ' + leaks[0])); + } +}; + +/** + * Fail the given `test`. + * + * @param {Test} test + * @param {Error} err + * @api private + */ + +Runner.prototype.fail = function(test, err){ + ++this.failures; + test.state = 'failed'; + + if ('string' == typeof err) { + err = new Error('the string "' + err + '" was thrown, throw an Error :)'); + } + + this.emit('fail', test, err); +}; + +/** + * Fail the given `hook` with `err`. + * + * Hook failures work in the following pattern: + * - If bail, then exit + * - Failed `before` hook skips all tests in a suite and subsuites, + * but jumps to corresponding `after` hook + * - Failed `before each` hook skips remaining tests in a + * suite and jumps to corresponding `after each` hook, + * which is run only once + * - Failed `after` hook does not alter + * execution order + * - Failed `after each` hook skips remaining tests in a + * suite and subsuites, but executes other `after each` + * hooks + * + * @param {Hook} hook + * @param {Error} err + * @api private + */ + +Runner.prototype.failHook = function(hook, err){ + this.fail(hook, err); + if (this.suite.bail()) { + this.emit('end'); + } +}; + +/** + * Run hook `name` callbacks and then invoke `fn()`. + * + * @param {String} name + * @param {Function} function + * @api private + */ + +Runner.prototype.hook = function(name, fn){ + var suite = this.suite + , hooks = suite['_' + name] + , self = this + , timer; + + function next(i) { + var hook = hooks[i]; + if (!hook) return fn(); + if (self.failures && suite.bail()) return fn(); + self.currentRunnable = hook; + + hook.ctx.currentTest = self.test; + + self.emit('hook', hook); + + hook.on('error', function(err){ + self.failHook(hook, err); + }); + + hook.run(function(err){ + hook.removeAllListeners('error'); + var testError = hook.error(); + if (testError) self.fail(self.test, testError); + if (err) { + self.failHook(hook, err); + + // stop executing hooks, notify callee of hook err + return fn(err); + } + self.emit('hook end', hook); + delete hook.ctx.currentTest; + next(++i); + }); + } + + Runner.immediately(function(){ + next(0); + }); +}; + +/** + * Run hook `name` for the given array of `suites` + * in order, and callback `fn(err, errSuite)`. + * + * @param {String} name + * @param {Array} suites + * @param {Function} fn + * @api private + */ + +Runner.prototype.hooks = function(name, suites, fn){ + var self = this + , orig = this.suite; + + function next(suite) { + self.suite = suite; + + if (!suite) { + self.suite = orig; + return fn(); + } + + self.hook(name, function(err){ + if (err) { + var errSuite = self.suite; + self.suite = orig; + return fn(err, errSuite); + } + + next(suites.pop()); + }); + } + + next(suites.pop()); +}; + +/** + * Run hooks from the top level down. + * + * @param {String} name + * @param {Function} fn + * @api private + */ + +Runner.prototype.hookUp = function(name, fn){ + var suites = [this.suite].concat(this.parents()).reverse(); + this.hooks(name, suites, fn); +}; + +/** + * Run hooks from the bottom up. + * + * @param {String} name + * @param {Function} fn + * @api private + */ + +Runner.prototype.hookDown = function(name, fn){ + var suites = [this.suite].concat(this.parents()); + this.hooks(name, suites, fn); +}; + +/** + * Return an array of parent Suites from + * closest to furthest. + * + * @return {Array} + * @api private + */ + +Runner.prototype.parents = function(){ + var suite = this.suite + , suites = []; + while (suite = suite.parent) suites.push(suite); + return suites; +}; + +/** + * Run the current test and callback `fn(err)`. + * + * @param {Function} fn + * @api private + */ + +Runner.prototype.runTest = function(fn){ + var test = this.test + , self = this; + + if (this.asyncOnly) test.asyncOnly = true; + + try { + test.on('error', function(err){ + self.fail(test, err); + }); + test.run(fn); + } catch (err) { + fn(err); + } +}; + +/** + * Run tests in the given `suite` and invoke + * the callback `fn()` when complete. + * + * @param {Suite} suite + * @param {Function} fn + * @api private + */ + +Runner.prototype.runTests = function(suite, fn){ + var self = this + , tests = suite.tests.slice() + , test; + + + function hookErr(err, errSuite, after) { + // before/after Each hook for errSuite failed: + var orig = self.suite; + + // for failed 'after each' hook start from errSuite parent, + // otherwise start from errSuite itself + self.suite = after ? errSuite.parent : errSuite; + + if (self.suite) { + // call hookUp afterEach + self.hookUp('afterEach', function(err2, errSuite2) { + self.suite = orig; + // some hooks may fail even now + if (err2) return hookErr(err2, errSuite2, true); + // report error suite + fn(errSuite); + }); + } else { + // there is no need calling other 'after each' hooks + self.suite = orig; + fn(errSuite); + } + } + + function next(err, errSuite) { + // if we bail after first err + if (self.failures && suite._bail) return fn(); + + if (self._abort) return fn(); + + if (err) return hookErr(err, errSuite, true); + + // next test + test = tests.shift(); + + // all done + if (!test) return fn(); + + // grep + var match = self._grep.test(test.fullTitle()); + if (self._invert) match = !match; + if (!match) return next(); + + // pending + if (test.pending) { + self.emit('pending', test); + self.emit('test end', test); + return next(); + } + + // execute test and hook(s) + self.emit('test', self.test = test); + self.hookDown('beforeEach', function(err, errSuite){ + + if (err) return hookErr(err, errSuite, false); + + self.currentRunnable = self.test; + self.runTest(function(err){ + test = self.test; + + if (err) { + self.fail(test, err); + self.emit('test end', test); + return self.hookUp('afterEach', next); + } + + test.state = 'passed'; + self.emit('pass', test); + self.emit('test end', test); + self.hookUp('afterEach', next); + }); + }); + } + + this.next = next; + next(); +}; + +/** + * Run the given `suite` and invoke the + * callback `fn()` when complete. + * + * @param {Suite} suite + * @param {Function} fn + * @api private + */ + +Runner.prototype.runSuite = function(suite, fn){ + var total = this.grepTotal(suite) + , self = this + , i = 0; + + debug('run suite %s', suite.fullTitle()); + + if (!total) return fn(); + + this.emit('suite', this.suite = suite); + + function next(errSuite) { + if (errSuite) { + // current suite failed on a hook from errSuite + if (errSuite == suite) { + // if errSuite is current suite + // continue to the next sibling suite + return done(); + } else { + // errSuite is among the parents of current suite + // stop execution of errSuite and all sub-suites + return done(errSuite); + } + } + + if (self._abort) return done(); + + var curr = suite.suites[i++]; + if (!curr) return done(); + self.runSuite(curr, next); + } + + function done(errSuite) { + self.suite = suite; + self.hook('afterAll', function(){ + self.emit('suite end', suite); + fn(errSuite); + }); + } + + this.hook('beforeAll', function(err){ + if (err) return done(); + self.runTests(suite, next); + }); +}; + +/** + * Handle uncaught exceptions. + * + * @param {Error} err + * @api private + */ + +Runner.prototype.uncaught = function(err){ + debug('uncaught exception %s', err.message); + var runnable = this.currentRunnable; + if (!runnable || 'failed' == runnable.state) return; + runnable.clearTimeout(); + err.uncaught = true; + this.fail(runnable, err); + + // recover from test + if ('test' == runnable.type) { + this.emit('test end', runnable); + this.hookUp('afterEach', this.next); + return; + } + + // bail on hooks + this.emit('end'); +}; + +/** + * Run the root suite and invoke `fn(failures)` + * on completion. + * + * @param {Function} fn + * @return {Runner} for chaining + * @api public + */ + +Runner.prototype.run = function(fn){ + var self = this + , fn = fn || function(){}; + + function uncaught(err){ + self.uncaught(err); + } + + debug('start'); + + // callback + this.on('end', function(){ + debug('end'); + process.removeListener('uncaughtException', uncaught); + fn(self.failures); + }); + + // run suites + this.emit('start'); + this.runSuite(this.suite, function(){ + debug('finished running'); + self.emit('end'); + }); + + // uncaught exception + process.on('uncaughtException', uncaught); + + return this; +}; + +/** + * Cleanly abort execution + * + * @return {Runner} for chaining + * @api public + */ +Runner.prototype.abort = function(){ + debug('aborting'); + this._abort = true; +} + +/** + * Filter leaks with the given globals flagged as `ok`. + * + * @param {Array} ok + * @param {Array} globals + * @return {Array} + * @api private + */ + +function filterLeaks(ok, globals) { + return filter(globals, function(key){ + // Firefox and Chrome exposes iframes as index inside the window object + if (/^d+/.test(key)) return false; + + // in firefox + // if runner runs in an iframe, this iframe's window.getInterface method not init at first + // it is assigned in some seconds + if (global.navigator && /^getInterface/.test(key)) return false; + + // an iframe could be approached by window[iframeIndex] + // in ie6,7,8 and opera, iframeIndex is enumerable, this could cause leak + if (global.navigator && /^\d+/.test(key)) return false; + + // Opera and IE expose global variables for HTML element IDs (issue #243) + if (/^mocha-/.test(key)) return false; + + var matched = filter(ok, function(ok){ + if (~ok.indexOf('*')) return 0 == key.indexOf(ok.split('*')[0]); + return key == ok; + }); + return matched.length == 0 && (!global.navigator || 'onerror' !== key); + }); +} + +/** + * Array of globals dependent on the environment. + * + * @return {Array} + * @api private + */ + + function extraGlobals() { + if (typeof(process) === 'object' && + typeof(process.version) === 'string') { + + var nodeVersion = process.version.split('.').reduce(function(a, v) { + return a << 8 | v; + }); + + // 'errno' was renamed to process._errno in v0.9.11. + + if (nodeVersion < 0x00090B) { + return ['errno']; + } + } + + return []; + } + +}); // module: runner.js + +require.register("suite.js", function(module, exports, require){ + +/** + * Module dependencies. + */ + +var EventEmitter = require('browser/events').EventEmitter + , debug = require('browser/debug')('mocha:suite') + , milliseconds = require('./ms') + , utils = require('./utils') + , Hook = require('./hook'); + +/** + * Expose `Suite`. + */ + +exports = module.exports = Suite; + +/** + * Create a new `Suite` with the given `title` + * and parent `Suite`. When a suite with the + * same title is already present, that suite + * is returned to provide nicer reporter + * and more flexible meta-testing. + * + * @param {Suite} parent + * @param {String} title + * @return {Suite} + * @api public + */ + +exports.create = function(parent, title){ + var suite = new Suite(title, parent.ctx); + suite.parent = parent; + if (parent.pending) suite.pending = true; + title = suite.fullTitle(); + parent.addSuite(suite); + return suite; +}; + +/** + * Initialize a new `Suite` with the given + * `title` and `ctx`. + * + * @param {String} title + * @param {Context} ctx + * @api private + */ + +function Suite(title, parentContext) { + this.title = title; + var context = function () {}; + context.prototype = parentContext; + this.ctx = new context(); + this.suites = []; + this.tests = []; + this.pending = false; + this._beforeEach = []; + this._beforeAll = []; + this._afterEach = []; + this._afterAll = []; + this.root = !title; + this._timeout = 2000; + this._slow = 75; + this._bail = false; +} + +/** + * Inherit from `EventEmitter.prototype`. + */ + +function F(){}; +F.prototype = EventEmitter.prototype; +Suite.prototype = new F; +Suite.prototype.constructor = Suite; + + +/** + * Return a clone of this `Suite`. + * + * @return {Suite} + * @api private + */ + +Suite.prototype.clone = function(){ + var suite = new Suite(this.title); + debug('clone'); + suite.ctx = this.ctx; + suite.timeout(this.timeout()); + suite.slow(this.slow()); + suite.bail(this.bail()); + return suite; +}; + +/** + * Set timeout `ms` or short-hand such as "2s". + * + * @param {Number|String} ms + * @return {Suite|Number} for chaining + * @api private + */ + +Suite.prototype.timeout = function(ms){ + if (0 == arguments.length) return this._timeout; + if ('string' == typeof ms) ms = milliseconds(ms); + debug('timeout %d', ms); + this._timeout = parseInt(ms, 10); + return this; +}; + +/** + * Set slow `ms` or short-hand such as "2s". + * + * @param {Number|String} ms + * @return {Suite|Number} for chaining + * @api private + */ + +Suite.prototype.slow = function(ms){ + if (0 === arguments.length) return this._slow; + if ('string' == typeof ms) ms = milliseconds(ms); + debug('slow %d', ms); + this._slow = ms; + return this; +}; + +/** + * Sets whether to bail after first error. + * + * @parma {Boolean} bail + * @return {Suite|Number} for chaining + * @api private + */ + +Suite.prototype.bail = function(bail){ + if (0 == arguments.length) return this._bail; + debug('bail %s', bail); + this._bail = bail; + return this; +}; + +/** + * Run `fn(test[, done])` before running tests. + * + * @param {Function} fn + * @return {Suite} for chaining + * @api private + */ + +Suite.prototype.beforeAll = function(title, fn){ + if (this.pending) return this; + if ('function' === typeof title) { + fn = title; + title = fn.name; + } + title = '"before all" hook' + (title ? ': ' + title : ''); + + var hook = new Hook(title, fn); + hook.parent = this; + hook.timeout(this.timeout()); + hook.slow(this.slow()); + hook.ctx = this.ctx; + this._beforeAll.push(hook); + this.emit('beforeAll', hook); + return this; +}; + +/** + * Run `fn(test[, done])` after running tests. + * + * @param {Function} fn + * @return {Suite} for chaining + * @api private + */ + +Suite.prototype.afterAll = function(title, fn){ + if (this.pending) return this; + if ('function' === typeof title) { + fn = title; + title = fn.name; + } + title = '"after all" hook' + (title ? ': ' + title : ''); + + var hook = new Hook(title, fn); + hook.parent = this; + hook.timeout(this.timeout()); + hook.slow(this.slow()); + hook.ctx = this.ctx; + this._afterAll.push(hook); + this.emit('afterAll', hook); + return this; +}; + +/** + * Run `fn(test[, done])` before each test case. + * + * @param {Function} fn + * @return {Suite} for chaining + * @api private + */ + +Suite.prototype.beforeEach = function(title, fn){ + if (this.pending) return this; + if ('function' === typeof title) { + fn = title; + title = fn.name; + } + title = '"before each" hook' + (title ? ': ' + title : ''); + + var hook = new Hook(title, fn); + hook.parent = this; + hook.timeout(this.timeout()); + hook.slow(this.slow()); + hook.ctx = this.ctx; + this._beforeEach.push(hook); + this.emit('beforeEach', hook); + return this; +}; + +/** + * Run `fn(test[, done])` after each test case. + * + * @param {Function} fn + * @return {Suite} for chaining + * @api private + */ + +Suite.prototype.afterEach = function(title, fn){ + if (this.pending) return this; + if ('function' === typeof title) { + fn = title; + title = fn.name; + } + title = '"after each" hook' + (title ? ': ' + title : ''); + + var hook = new Hook(title, fn); + hook.parent = this; + hook.timeout(this.timeout()); + hook.slow(this.slow()); + hook.ctx = this.ctx; + this._afterEach.push(hook); + this.emit('afterEach', hook); + return this; +}; + +/** + * Add a test `suite`. + * + * @param {Suite} suite + * @return {Suite} for chaining + * @api private + */ + +Suite.prototype.addSuite = function(suite){ + suite.parent = this; + suite.timeout(this.timeout()); + suite.slow(this.slow()); + suite.bail(this.bail()); + this.suites.push(suite); + this.emit('suite', suite); + return this; +}; + +/** + * Add a `test` to this suite. + * + * @param {Test} test + * @return {Suite} for chaining + * @api private + */ + +Suite.prototype.addTest = function(test){ + test.parent = this; + test.timeout(this.timeout()); + test.slow(this.slow()); + test.ctx = this.ctx; + this.tests.push(test); + this.emit('test', test); + return this; +}; + +/** + * Return the full title generated by recursively + * concatenating the parent's full title. + * + * @return {String} + * @api public + */ + +Suite.prototype.fullTitle = function(){ + if (this.parent) { + var full = this.parent.fullTitle(); + if (full) return full + ' ' + this.title; + } + return this.title; +}; + +/** + * Return the total number of tests. + * + * @return {Number} + * @api public + */ + +Suite.prototype.total = function(){ + return utils.reduce(this.suites, function(sum, suite){ + return sum + suite.total(); + }, 0) + this.tests.length; +}; + +/** + * Iterates through each suite recursively to find + * all tests. Applies a function in the format + * `fn(test)`. + * + * @param {Function} fn + * @return {Suite} + * @api private + */ + +Suite.prototype.eachTest = function(fn){ + utils.forEach(this.tests, fn); + utils.forEach(this.suites, function(suite){ + suite.eachTest(fn); + }); + return this; +}; + +}); // module: suite.js + +require.register("test.js", function(module, exports, require){ + +/** + * Module dependencies. + */ + +var Runnable = require('./runnable'); + +/** + * Expose `Test`. + */ + +module.exports = Test; + +/** + * Initialize a new `Test` with the given `title` and callback `fn`. + * + * @param {String} title + * @param {Function} fn + * @api private + */ + +function Test(title, fn) { + Runnable.call(this, title, fn); + this.pending = !fn; + this.type = 'test'; +} + +/** + * Inherit from `Runnable.prototype`. + */ + +function F(){}; +F.prototype = Runnable.prototype; +Test.prototype = new F; +Test.prototype.constructor = Test; + + +}); // module: test.js + +require.register("utils.js", function(module, exports, require){ +/** + * Module dependencies. + */ + +var fs = require('browser/fs') + , path = require('browser/path') + , join = path.join + , debug = require('browser/debug')('mocha:watch'); + +/** + * Ignored directories. + */ + +var ignore = ['node_modules', '.git']; + +/** + * Escape special characters in the given string of html. + * + * @param {String} html + * @return {String} + * @api private + */ + +exports.escape = function(html){ + return String(html) + .replace(/&/g, '&') + .replace(/"/g, '"') + .replace(//g, '>'); +}; + +/** + * Array#forEach (<=IE8) + * + * @param {Array} array + * @param {Function} fn + * @param {Object} scope + * @api private + */ + +exports.forEach = function(arr, fn, scope){ + for (var i = 0, l = arr.length; i < l; i++) + fn.call(scope, arr[i], i); +}; + +/** + * Array#map (<=IE8) + * + * @param {Array} array + * @param {Function} fn + * @param {Object} scope + * @api private + */ + +exports.map = function(arr, fn, scope){ + var result = []; + for (var i = 0, l = arr.length; i < l; i++) + result.push(fn.call(scope, arr[i], i)); + return result; +}; + +/** + * Array#indexOf (<=IE8) + * + * @parma {Array} arr + * @param {Object} obj to find index of + * @param {Number} start + * @api private + */ + +exports.indexOf = function(arr, obj, start){ + for (var i = start || 0, l = arr.length; i < l; i++) { + if (arr[i] === obj) + return i; + } + return -1; +}; + +/** + * Array#reduce (<=IE8) + * + * @param {Array} array + * @param {Function} fn + * @param {Object} initial value + * @api private + */ + +exports.reduce = function(arr, fn, val){ + var rval = val; + + for (var i = 0, l = arr.length; i < l; i++) { + rval = fn(rval, arr[i], i, arr); + } + + return rval; +}; + +/** + * Array#filter (<=IE8) + * + * @param {Array} array + * @param {Function} fn + * @api private + */ + +exports.filter = function(arr, fn){ + var ret = []; + + for (var i = 0, l = arr.length; i < l; i++) { + var val = arr[i]; + if (fn(val, i, arr)) ret.push(val); + } + + return ret; +}; + +/** + * Object.keys (<=IE8) + * + * @param {Object} obj + * @return {Array} keys + * @api private + */ + +exports.keys = Object.keys || function(obj) { + var keys = [] + , has = Object.prototype.hasOwnProperty // for `window` on <=IE8 + + for (var key in obj) { + if (has.call(obj, key)) { + keys.push(key); + } + } + + return keys; +}; + +/** + * Watch the given `files` for changes + * and invoke `fn(file)` on modification. + * + * @param {Array} files + * @param {Function} fn + * @api private + */ + +exports.watch = function(files, fn){ + var options = { interval: 100 }; + files.forEach(function(file){ + debug('file %s', file); + fs.watchFile(file, options, function(curr, prev){ + if (prev.mtime < curr.mtime) fn(file); + }); + }); +}; + +/** + * Ignored files. + */ + +function ignored(path){ + return !~ignore.indexOf(path); +} + +/** + * Lookup files in the given `dir`. + * + * @return {Array} + * @api private + */ + +exports.files = function(dir, ret){ + ret = ret || []; + + fs.readdirSync(dir) + .filter(ignored) + .forEach(function(path){ + path = join(dir, path); + if (fs.statSync(path).isDirectory()) { + exports.files(path, ret); + } else if (path.match(/\.(js|coffee|litcoffee|coffee.md)$/)) { + ret.push(path); + } + }); + + return ret; +}; + +/** + * Compute a slug from the given `str`. + * + * @param {String} str + * @return {String} + * @api private + */ + +exports.slug = function(str){ + return str + .toLowerCase() + .replace(/ +/g, '-') + .replace(/[^-\w]/g, ''); +}; + +/** + * Strip the function definition from `str`, + * and re-indent for pre whitespace. + */ + +exports.clean = function(str) { + str = str + .replace(/\r\n?|[\n\u2028\u2029]/g, "\n").replace(/^\uFEFF/, '') + .replace(/^function *\(.*\) *{/, '') + .replace(/\s+\}$/, ''); + + var spaces = str.match(/^\n?( *)/)[1].length + , tabs = str.match(/^\n?(\t*)/)[1].length + , re = new RegExp('^\n?' + (tabs ? '\t' : ' ') + '{' + (tabs ? tabs : spaces) + '}', 'gm'); + + str = str.replace(re, ''); + + return exports.trim(str); +}; + +/** + * Escape regular expression characters in `str`. + * + * @param {String} str + * @return {String} + * @api private + */ + +exports.escapeRegexp = function(str){ + return str.replace(/[-\\^$*+?.()|[\]{}]/g, "\\$&"); +}; + +/** + * Trim the given `str`. + * + * @param {String} str + * @return {String} + * @api private + */ + +exports.trim = function(str){ + return str.replace(/^\s+|\s+$/g, ''); +}; + +/** + * Parse the given `qs`. + * + * @param {String} qs + * @return {Object} + * @api private + */ + +exports.parseQuery = function(qs){ + return exports.reduce(qs.replace('?', '').split('&'), function(obj, pair){ + var i = pair.indexOf('=') + , key = pair.slice(0, i) + , val = pair.slice(++i); + + obj[key] = decodeURIComponent(val); + return obj; + }, {}); +}; + +/** + * Highlight the given string of `js`. + * + * @param {String} js + * @return {String} + * @api private + */ + +function highlight(js) { + return js + .replace(//g, '>') + .replace(/\/\/(.*)/gm, '//$1') + .replace(/('.*?')/gm, '$1') + .replace(/(\d+\.\d+)/gm, '$1') + .replace(/(\d+)/gm, '$1') + .replace(/\bnew *(\w+)/gm, 'new $1') + .replace(/\b(function|new|throw|return|var|if|else)\b/gm, '$1') +} + +/** + * Highlight the contents of tag `name`. + * + * @param {String} name + * @api private + */ + +exports.highlightTags = function(name) { + var code = document.getElementsByTagName(name); + for (var i = 0, len = code.length; i < len; ++i) { + code[i].innerHTML = highlight(code[i].innerHTML); + } +}; + +}); // module: utils.js +// The global object is "self" in Web Workers. +global = (function() { return this; })(); + +/** + * Save timer references to avoid Sinon interfering (see GH-237). + */ + +var Date = global.Date; +var setTimeout = global.setTimeout; +var setInterval = global.setInterval; +var clearTimeout = global.clearTimeout; +var clearInterval = global.clearInterval; + +/** + * Node shims. + * + * These are meant only to allow + * mocha.js to run untouched, not + * to allow running node code in + * the browser. + */ + +var process = {}; +process.exit = function(status){}; +process.stdout = {}; + +var uncaughtExceptionHandlers = []; + +/** + * Remove uncaughtException listener. + */ + +process.removeListener = function(e, fn){ + if ('uncaughtException' == e) { + global.onerror = function() {}; + var i = Mocha.utils.indexOf(uncaughtExceptionHandlers, fn); + if (i != -1) { uncaughtExceptionHandlers.splice(i, 1); } + } +}; + +/** + * Implements uncaughtException listener. + */ + +process.on = function(e, fn){ + if ('uncaughtException' == e) { + global.onerror = function(err, url, line){ + fn(new Error(err + ' (' + url + ':' + line + ')')); + return true; + }; + uncaughtExceptionHandlers.push(fn); + } +}; + +/** + * Expose mocha. + */ + +var Mocha = global.Mocha = require('mocha'), + mocha = global.mocha = new Mocha({ reporter: 'html' }); + +// The BDD UI is registered by default, but no UI will be functional in the +// browser without an explicit call to the overridden `mocha.ui` (see below). +// Ensure that this default UI does not expose its methods to the global scope. +mocha.suite.removeAllListeners('pre-require'); + +var immediateQueue = [] + , immediateTimeout; + +function timeslice() { + var immediateStart = new Date().getTime(); + while (immediateQueue.length && (new Date().getTime() - immediateStart) < 100) { + immediateQueue.shift()(); + } + if (immediateQueue.length) { + immediateTimeout = setTimeout(timeslice, 0); + } else { + immediateTimeout = null; + } +} + +/** + * High-performance override of Runner.immediately. + */ + +Mocha.Runner.immediately = function(callback) { + immediateQueue.push(callback); + if (!immediateTimeout) { + immediateTimeout = setTimeout(timeslice, 0); + } +}; + +/** + * Function to allow assertion libraries to throw errors directly into mocha. + * This is useful when running tests in a browser because window.onerror will + * only receive the 'message' attribute of the Error. + */ +mocha.throwError = function(err) { + Mocha.utils.forEach(uncaughtExceptionHandlers, function (fn) { + fn(err); + }); + throw err; +}; + +/** + * Override ui to ensure that the ui functions are initialized. + * Normally this would happen in Mocha.prototype.loadFiles. + */ + +mocha.ui = function(ui){ + Mocha.prototype.ui.call(this, ui); + this.suite.emit('pre-require', global, null, this); + return this; +}; + +/** + * Setup mocha with the given setting options. + */ + +mocha.setup = function(opts){ + if ('string' == typeof opts) opts = { ui: opts }; + for (var opt in opts) this[opt](opts[opt]); + return this; +}; + +/** + * Run mocha, returning the Runner. + */ + +mocha.run = function(fn){ + var options = mocha.options; + mocha.globals('location'); + + var query = Mocha.utils.parseQuery(global.location.search || ''); + if (query.grep) mocha.grep(query.grep); + if (query.invert) mocha.invert(); + + return Mocha.prototype.run.call(mocha, function(){ + // The DOM Document is not available in Web Workers. + if (global.document) { + Mocha.utils.highlightTags('code'); + } + if (fn) fn(); + }); +}; + +/** + * Expose the process shim. + */ + +Mocha.process = process; +})(); \ No newline at end of file diff --git a/webcomponents.js b/webcomponents.js new file mode 100644 index 0000000..e270ef9 --- /dev/null +++ b/webcomponents.js @@ -0,0 +1,88 @@ +/* + * Copyright 2013 The Polymer Authors. All rights reserved. + * Use of this source code is governed by a BSD-style + * license that can be found in the LICENSE file. + */ + +(function() { + + // Establish scope. + window.WebComponents = window.WebComponents || {flags:{}}; + + // loading script + var file = 'webcomponents.js'; + var script = document.querySelector('script[src*="' + file + '"]'); + + // Flags. Convert url arguments to flags + var flags = {}; + if (!flags.noOpts) { + // from url + location.search.slice(1).split('&').forEach(function(o) { + o = o.split('='); + o[0] && (flags[o[0]] = o[1] || true); + }); + // from script + for (var i=0, a; (a=script.attributes[i]); i++) { + if (a.name !== 'src') { + flags[a.name] = a.value || true; + } + } + // log flags + if (flags.log) { + var parts = flags.log.split(','); + flags.log = {}; + parts.forEach(function(f) { + flags.log[f] = true; + }); + } + } + + // Determine default settings. + // If any of these flags match 'native', then force native ShadowDOM; any + // other truthy value, or failure to detect native + // ShadowDOM, results in polyfill + flags.shadow = (flags.shadow || flags.shadowdom || flags.polyfill); + if (flags.shadow === 'native') { + flags.shadow = false; + } else { + flags.shadow = flags.shadow || !HTMLElement.prototype.createShadowRoot; + } + + // Load. + var ShadowDOMNative = [ + 'WebComponents/shadowdom.js' + ]; + + var ShadowDOMPolyfill = [ + 'ShadowDOM/ShadowDOM.js', + 'WebComponents/shadowdom.js', + 'ShadowCSS/ShadowCSS.js' + ]; + + // select ShadowDOM impl + var ShadowDOM = flags.shadow ? ShadowDOMPolyfill : ShadowDOMNative; + + // construct full dependency list + var modules = [].concat( + ShadowDOM, + [ + 'HTMLImports/HTMLImports.js', + 'CustomElements/CustomElements.js', + 'WebComponents/lang.js', + // these scripts are loaded here due to polyfill timing issues + 'WebComponents/dom.js', + 'WebComponents/unresolved.js' + ] + ); + + var src = script.getAttribute('src'); + var path = src.slice(0, src.indexOf(file)); + + modules.forEach(function(f) { + document.write(''); + }); + + // exports + WebComponents.flags = flags; + +})();