From daf8160bf249a0b90219a74ae4d576d2c24d9d82 Mon Sep 17 00:00:00 2001 From: Alec Embke Date: Wed, 20 Aug 2014 14:16:16 -0700 Subject: [PATCH] added jsonp transport --- archive/v0.1.21/mithril-tests.js | 3020 ++++++++++++++++++++++++++++++ mithril.js | 114 +- tests/mithril-tests.js | 127 ++ tests/mock.js | 17 + 4 files changed, 3261 insertions(+), 17 deletions(-) create mode 100644 archive/v0.1.21/mithril-tests.js diff --git a/archive/v0.1.21/mithril-tests.js b/archive/v0.1.21/mithril-tests.js new file mode 100644 index 00000000..8582a8be --- /dev/null +++ b/archive/v0.1.21/mithril-tests.js @@ -0,0 +1,3020 @@ +Mithril = m = new function app(window, undefined) { + var type = {}.toString + var parser = /(?:(^|#|\.)([^#\.\[\]]+))|(\[.+?\])/g, attrParser = /\[(.+?)(?:=("|'|)(.*?)\2)?\]/ + var voidElements = /AREA|BASE|BR|COL|COMMAND|EMBED|HR|IMG|INPUT|KEYGEN|LINK|META|PARAM|SOURCE|TR‌​ACK|WBR/ + + function m() { + var args = arguments + var hasAttrs = args[1] != null && type.call(args[1]) == "[object Object]" && !("tag" in args[1]) && !("subtree" in args[1]) + var attrs = hasAttrs ? args[1] : {} + var classAttrName = "class" in attrs ? "class" : "className" + var cell = {tag: "div", attrs: {}} + var match, classes = [] + while (match = parser.exec(args[0])) { + if (match[1] == "") cell.tag = match[2] + else if (match[1] == "#") cell.attrs.id = match[2] + else if (match[1] == ".") classes.push(match[2]) + else if (match[3][0] == "[") { + var pair = attrParser.exec(match[3]) + cell.attrs[pair[1]] = pair[3] || (pair[2] ? "" :true) + } + } + if (classes.length > 0) cell.attrs[classAttrName] = classes.join(" ") + + cell.children = hasAttrs ? args[2] : args[1] + + for (var attrName in attrs) { + if (attrName == classAttrName) cell.attrs[attrName] = (cell.attrs[attrName] || "") + " " + attrs[attrName] + else cell.attrs[attrName] = attrs[attrName] + } + return cell + } + function build(parentElement, parentTag, parentCache, parentIndex, data, cached, shouldReattach, index, editable, namespace, configs) { + //`build` is a recursive function that manages creation/diffing/removal of DOM elements based on comparison between `data` and `cached` + + //`parentElement` is a DOM element used for W3C DOM API calls + //`parentTag` is only used for handling a corner case for textarea values + //`parentCache` is used to remove nodes in some multi-node cases + //`parentIndex` and `index` are used to figure out the offset of nodes. They're artifacts from before arrays started being flattened and are likely refactorable + //`data` and `cached` are, respectively, the new and old nodes being diffed + //`shouldReattach` is a flag indicating whether a parent node was recreated (if so, and if this node is reused, then this node must reattach itself to the new parent) + //`editable` is a flag that indicates whether an ancestor is contenteditable + //`namespace` indicates the closest HTML namespace as it cascades down from an ancestor + //`configs` is a list of config functions to run after the topmost `build` call finishes running + + //there's logic that relies on the assumption that null and undefined data are equivalent to empty strings + //- this prevents lifecycle surprises from procedural helpers that mix implicit and explicit return statements + //- it simplifies diffing code + if (data == null) data = "" + if (data.subtree === "retain") return cached + + var cachedType = type.call(cached), dataType = type.call(data) + if (cached == null || cachedType != dataType) { + if (cached != null) { + if (parentCache && parentCache.nodes) { + var offset = index - parentIndex + var end = offset + (dataType == "[object Array]" ? data : cached.nodes).length + clear(parentCache.nodes.slice(offset, end), parentCache.slice(offset, end)) + } + else if (cached.nodes) clear(cached.nodes, cached) + } + cached = new data.constructor + cached.nodes = [] + } + + if (dataType == "[object Array]") { + data = flatten(data) + var nodes = [], intact = cached.length === data.length, subArrayCount = 0 + + //key algorithm: sort elements without recreating them if keys are present + //1) create a map of all existing keys, and mark all for deletion + //2) add new keys to map and mark them for addition + //3) if key exists in new list, change action from deletion to a move + //4) for each key, handle its corresponding action as marked in previous steps + //5) copy unkeyed items into their respective gaps + var DELETION = 1, INSERTION = 2 , MOVE = 3 + var existing = {}, unkeyed = [], shouldMaintainIdentities = false + for (var i = 0; i < cached.length; i++) { + if (cached[i] && cached[i].attrs && cached[i].attrs.key != null) { + shouldMaintainIdentities = true + existing[cached[i].attrs.key] = {action: DELETION, index: i} + } + } + if (shouldMaintainIdentities) { + for (var i = 0; i < data.length; i++) { + if (data[i] && data[i].attrs) { + if (data[i].attrs.key != null) { + var key = data[i].attrs.key + if (!existing[key]) existing[key] = {action: INSERTION, index: i} + else existing[key] = {action: MOVE, index: i, from: existing[key].index, element: parentElement.childNodes[existing[key].index]} + } + else unkeyed.push({index: i, element: parentElement.childNodes[i]}) + } + } + var actions = Object.keys(existing).map(function(key) {return existing[key]}) + var changes = actions.sort(function(a, b) {return a.action - b.action || a.index - b.index}) + var newCached = cached.slice() + + for (var i = 0, change; change = changes[i]; i++) { + if (change.action == DELETION) { + clear(cached[change.index].nodes, cached[change.index]) + newCached.splice(change.index, 1) + } + if (change.action == INSERTION) { + var dummy = window.document.createElement("div") + dummy.key = data[change.index].attrs.key + parentElement.insertBefore(dummy, parentElement.childNodes[change.index]) + newCached.splice(change.index, 0, {attrs: {key: data[change.index].attrs.key}, nodes: [dummy]}) + } + + if (change.action == MOVE) { + if (parentElement.childNodes[change.index] !== change.element && change.element !== null) { + parentElement.insertBefore(change.element, parentElement.childNodes[change.index]) + } + newCached[change.index] = cached[change.from] + } + } + for (var i = 0; i < unkeyed.length; i++) { + var change = unkeyed[i] + parentElement.insertBefore(change.element, parentElement.childNodes[change.index]) + newCached[change.index] = cached[change.index] + } + cached = newCached + cached.nodes = [] + for (var i = 0, child; child = parentElement.childNodes[i]; i++) cached.nodes.push(child) + } + //end key algorithm + + for (var i = 0, cacheCount = 0; i < data.length; i++) { + var item = build(parentElement, parentTag, cached, index, data[i], cached[cacheCount], shouldReattach, index + subArrayCount || subArrayCount, editable, namespace, configs) + if (item === undefined) continue + if (!item.nodes.intact) intact = false + var isArray = type.call(item) == "[object Array]" + subArrayCount += isArray ? item.length : 1 + cached[cacheCount++] = item + } + if (!intact) { + for (var i = 0; i < data.length; i++) { + if (cached[i] != null) nodes = nodes.concat(cached[i].nodes) + } + for (var i = 0, node; node = cached.nodes[i]; i++) { + if (node.parentNode != null && nodes.indexOf(node) < 0) clear([node], [cached[i]]) + } + for (var i = cached.nodes.length, node; node = nodes[i]; i++) { + if (node.parentNode == null) parentElement.appendChild(node) + } + if (data.length < cached.length) cached.length = data.length + cached.nodes = nodes + } + } + else if (data != null && dataType == "[object Object]") { + //if an element is different enough from the one in cache, recreate it + if (data.tag != cached.tag || Object.keys(data.attrs).join() != Object.keys(cached.attrs).join() || data.attrs.id != cached.attrs.id) { + clear(cached.nodes) + if (cached.configContext && typeof cached.configContext.onunload == "function") cached.configContext.onunload() + } + if (typeof data.tag != "string") return + + var node, isNew = cached.nodes.length === 0 + if (data.attrs.xmlns) namespace = data.attrs.xmlns + else if (data.tag === "svg") namespace = "http://www.w3.org/2000/svg" + else if (data.tag === "math") namespace = "http://www.w3.org/1998/Math/MathML" + if (isNew) { + node = namespace === undefined ? window.document.createElement(data.tag) : window.document.createElementNS(namespace, data.tag) + cached = { + tag: data.tag, + //process children before attrs so that select.value works correctly + children: build(node, data.tag, undefined, undefined, data.children, cached.children, true, 0, data.attrs.contenteditable ? node : editable, namespace, configs), + attrs: setAttributes(node, data.tag, data.attrs, {}, namespace), + nodes: [node] + } + parentElement.insertBefore(node, parentElement.childNodes[index] || null) + } + else { + node = cached.nodes[0] + setAttributes(node, data.tag, data.attrs, cached.attrs, namespace) + cached.children = build(node, data.tag, undefined, undefined, data.children, cached.children, false, 0, data.attrs.contenteditable ? node : editable, namespace, configs) + cached.nodes.intact = true + if (shouldReattach === true && node != null) parentElement.insertBefore(node, parentElement.childNodes[index] || null) + } + //schedule configs to be called. They are called after `build` finishes running + if (typeof data.attrs["config"] === "function") { + configs.push(data.attrs["config"].bind(window, node, !isNew, cached.configContext = cached.configContext || {}, cached)) + } + } + else if (typeof dataType != "function") { + //handle text nodes + var nodes + if (cached.nodes.length === 0) { + if (data.$trusted) { + nodes = injectHTML(parentElement, index, data) + } + else { + nodes = [window.document.createTextNode(data)] + if (!parentElement.nodeName.match(voidElements)) parentElement.insertBefore(nodes[0], parentElement.childNodes[index] || null) + } + cached = "string number boolean".indexOf(typeof data) > -1 ? new data.constructor(data) : data + cached.nodes = nodes + } + else if (cached.valueOf() !== data.valueOf() || shouldReattach === true) { + nodes = cached.nodes + if (!editable || editable !== window.document.activeElement) { + if (data.$trusted) { + clear(nodes, cached) + nodes = injectHTML(parentElement, index, data) + } + else { + //corner case: replacing the nodeValue of a text node that is a child of a textarea/contenteditable doesn't work + if (parentTag === "textarea") parentElement.value = data + else if (editable) editable.innerHTML = data + else { + if (nodes[0].nodeType == 1 || nodes.length > 1) { //was a trusted string + clear(cached.nodes, cached) + nodes = [window.document.createTextNode(data)] + } + parentElement.insertBefore(nodes[0], parentElement.childNodes[index] || null) + nodes[0].nodeValue = data + } + } + } + cached = new data.constructor(data) + cached.nodes = nodes + } + else cached.nodes.intact = true + } + + return cached + } + function setAttributes(node, tag, dataAttrs, cachedAttrs, namespace) { + var groups = {} + for (var attrName in dataAttrs) { + var dataAttr = dataAttrs[attrName] + var cachedAttr = cachedAttrs[attrName] + if (!(attrName in cachedAttrs) || (cachedAttr !== dataAttr) || node === window.document.activeElement) { + cachedAttrs[attrName] = dataAttr + if (attrName === "config") continue + else if (typeof dataAttr == "function" && attrName.indexOf("on") == 0) { + node[attrName] = autoredraw(dataAttr, node) + } + else if (attrName === "style" && typeof dataAttr == "object") { + for (var rule in dataAttr) { + if (cachedAttr == null || cachedAttr[rule] !== dataAttr[rule]) node.style[rule] = dataAttr[rule] + } + for (var rule in cachedAttr) { + if (!(rule in dataAttr)) node.style[rule] = "" + } + } + else if (namespace != null) { + if (attrName === "href") node.setAttributeNS("http://www.w3.org/1999/xlink", "href", dataAttr) + else if (attrName === "className") node.setAttribute("class", dataAttr) + else node.setAttribute(attrName, dataAttr) + } + else if (attrName === "value" && tag === "input") { + if (node.value !== dataAttr) node.value = dataAttr + } + else if (attrName in node && !(attrName == "list" || attrName == "style")) { + node[attrName] = dataAttr + } + else node.setAttribute(attrName, dataAttr) + } + } + return cachedAttrs + } + function clear(nodes, cached) { + for (var i = nodes.length - 1; i > -1; i--) { + if (nodes[i] && nodes[i].parentNode) { + nodes[i].parentNode.removeChild(nodes[i]) + cached = [].concat(cached) + if (cached[i]) unload(cached[i]) + } + } + if (nodes.length != 0) nodes.length = 0 + } + function unload(cached) { + if (cached.configContext && typeof cached.configContext.onunload == "function") cached.configContext.onunload() + if (cached.children) { + if (type.call(cached.children) == "[object Array]") for (var i = 0; i < cached.children.length; i++) unload(cached.children[i]) + else if (cached.children.tag) unload(cached.children) + } + } + function injectHTML(parentElement, index, data) { + var nextSibling = parentElement.childNodes[index] + if (nextSibling) { + var isElement = nextSibling.nodeType != 1 + var placeholder = window.document.createElement("span") + if (isElement) { + parentElement.insertBefore(placeholder, nextSibling) + placeholder.insertAdjacentHTML("beforebegin", data) + parentElement.removeChild(placeholder) + } + else nextSibling.insertAdjacentHTML("beforebegin", data) + } + else parentElement.insertAdjacentHTML("beforeend", data) + var nodes = [] + while (parentElement.childNodes[index] !== nextSibling) { + nodes.push(parentElement.childNodes[index]) + index++ + } + return nodes + } + function flatten(data) { + var flattened = [] + for (var i = 0; i < data.length; i++) { + var item = data[i] + if (type.call(item) == "[object Array]") flattened.push.apply(flattened, flatten(item)) + else flattened.push(item) + } + return flattened + } + function autoredraw(callback, object, group) { + return function(e) { + e = e || event + m.redraw.strategy("diff") + m.startComputation() + try {return callback.call(object, e)} + finally { + if (!lastRedrawId) lastRedrawId = -1; + m.endComputation() + } + } + } + + var html + var documentNode = { + insertAdjacentHTML: function(_, data) { + window.document.write(data) + window.document.close() + }, + appendChild: function(node) { + if (html === undefined) html = window.document.createElement("html") + if (node.nodeName == "HTML") html = node + else html.appendChild(node) + if (window.document.documentElement && window.document.documentElement !== html) { + window.document.replaceChild(html, window.document.documentElement) + } + else window.document.appendChild(html) + }, + insertBefore: function(node) { + this.appendChild(node) + }, + childNodes: [] + } + var nodeCache = [], cellCache = {} + m.render = function(root, cell, forceRecreation) { + var configs = [] + if (!root) throw new Error("Please ensure the DOM element exists before rendering a template into it.") + var id = getCellCacheKey(root) + var node = root == window.document || root == window.document.documentElement ? documentNode : root + if (cellCache[id] === undefined) clear(node.childNodes) + if (forceRecreation === true) reset(root) + cellCache[id] = build(node, null, undefined, undefined, cell, cellCache[id], false, 0, null, undefined, configs) + for (var i = 0; i < configs.length; i++) configs[i]() + } + function getCellCacheKey(element) { + var index = nodeCache.indexOf(element) + return index < 0 ? nodeCache.push(element) - 1 : index + } + + m.trust = function(value) { + value = new String(value) + value.$trusted = true + return value + } + + function _prop(store) { + var prop = function() { + if (arguments.length) store = arguments[0] + return store + } + + prop.toJSON = function() { + return store + } + + return prop + } + + m.prop = function (store) { + if ((typeof store === 'object' || typeof store === 'function') && + typeof store.then === 'function') { + var prop = _prop() + newPromisedProp(prop, store).then(prop) + + return prop + } + + return _prop(store) + } + + var roots = [], modules = [], controllers = [], lastRedrawId = 0, computePostRedrawHook = null, prevented = false + m.module = function(root, module) { + var index = roots.indexOf(root) + if (index < 0) index = roots.length + var isPrevented = false + if (controllers[index] && typeof controllers[index].onunload == "function") { + var event = { + preventDefault: function() {isPrevented = true} + } + controllers[index].onunload(event) + } + if (!isPrevented) { + m.redraw.strategy("all") + m.startComputation() + roots[index] = root + modules[index] = module + controllers[index] = new module.controller + m.endComputation() + } + } + m.redraw = function(force) { + var cancel = window.cancelAnimationFrame || window.clearTimeout + var defer = window.requestAnimationFrame || window.setTimeout + if (lastRedrawId && force !== true) { + cancel(lastRedrawId) + lastRedrawId = defer(redraw, 0) + } + else { + redraw() + lastRedrawId = defer(function() {lastRedrawId = null}, 0) + } + } + m.redraw.strategy = m.prop() + function redraw() { + var mode = m.redraw.strategy() + for (var i = 0; i < roots.length; i++) { + if (controllers[i] && mode != "none") m.render(roots[i], modules[i].view(controllers[i]), mode == "all") + } + if (computePostRedrawHook) { + computePostRedrawHook() + computePostRedrawHook = null + } + lastRedrawId = null + m.redraw.strategy("diff") + } + + var pendingRequests = 0 + m.startComputation = function() {pendingRequests++} + m.endComputation = function() { + pendingRequests = Math.max(pendingRequests - 1, 0) + if (pendingRequests == 0) m.redraw() + } + + m.withAttr = function(prop, withAttrCallback) { + return function(e) { + e = e || event + var currentTarget = e.currentTarget || this + withAttrCallback(prop in currentTarget ? currentTarget[prop] : currentTarget.getAttribute(prop)) + } + } + + //routing + var modes = {pathname: "", hash: "#", search: "?"} + var redirect = function() {}, routeParams = {}, currentRoute + m.route = function() { + if (arguments.length === 0) return currentRoute + else if (arguments.length === 3 && typeof arguments[1] == "string") { + var root = arguments[0], defaultRoute = arguments[1], router = arguments[2] + redirect = function(source) { + var path = currentRoute = normalizeRoute(source) + if (!routeByValue(root, router, path)) { + m.route(defaultRoute, true) + } + } + var listener = m.route.mode == "hash" ? "onhashchange" : "onpopstate" + window[listener] = function() { + if (currentRoute != normalizeRoute(window.location[m.route.mode])) { + redirect(window.location[m.route.mode]) + } + } + computePostRedrawHook = setScroll + window[listener]() + } + else if (arguments[0].addEventListener) { + var element = arguments[0] + var isInitialized = arguments[1] + if (element.href.indexOf(modes[m.route.mode]) < 0) { + element.href = window.location.pathname + modes[m.route.mode] + element.pathname + } + if (!isInitialized) { + element.removeEventListener("click", routeUnobtrusive) + element.addEventListener("click", routeUnobtrusive) + } + } + else if (typeof arguments[0] == "string") { + currentRoute = arguments[0] + var querystring = typeof arguments[1] == "object" ? buildQueryString(arguments[1]) : null + if (querystring) currentRoute += (currentRoute.indexOf("?") === -1 ? "?" : "&") + querystring + + var shouldReplaceHistoryEntry = (arguments.length == 3 ? arguments[2] : arguments[1]) === true + + if (window.history.pushState) { + computePostRedrawHook = function() { + window.history[shouldReplaceHistoryEntry ? "replaceState" : "pushState"](null, window.document.title, modes[m.route.mode] + currentRoute) + setScroll() + } + redirect(modes[m.route.mode] + currentRoute) + } + else window.location[m.route.mode] = currentRoute + } + } + m.route.param = function(key) {return routeParams[key]} + m.route.mode = "search" + function normalizeRoute(route) {return route.slice(modes[m.route.mode].length)} + function routeByValue(root, router, path) { + routeParams = {} + + var queryStart = path.indexOf("?") + if (queryStart !== -1) { + routeParams = parseQueryString(path.substr(queryStart + 1, path.length)) + path = path.substr(0, queryStart) + } + + for (var route in router) { + if (route == path) { + m.module(root, router[route]) + return true + } + + var matcher = new RegExp("^" + route.replace(/:[^\/]+?\.{3}/g, "(.*?)").replace(/:[^\/]+/g, "([^\\/]+)") + "\/?$") + + if (matcher.test(path)) { + path.replace(matcher, function() { + var keys = route.match(/:[^\/]+/g) || [] + var values = [].slice.call(arguments, 1, -2) + for (var i = 0; i < keys.length; i++) routeParams[keys[i].replace(/:|\./g, "")] = decodeURIComponent(values[i]) + m.module(root, router[route]) + }) + return true + } + } + } + function routeUnobtrusive(e) { + e = e || event + if (e.ctrlKey || e.metaKey || e.which == 2) return + e.preventDefault() + m.route(e.currentTarget[m.route.mode].slice(modes[m.route.mode].length)) + } + function setScroll() { + if (m.route.mode != "hash" && window.location.hash) window.location.hash = window.location.hash + else window.scrollTo(0, 0) + } + function buildQueryString(object, prefix) { + var str = [] + for(var prop in object) { + var key = prefix ? prefix + "[" + prop + "]" : prop, value = object[prop] + str.push(typeof value == "object" ? buildQueryString(value, key) : encodeURIComponent(key) + "=" + encodeURIComponent(value)) + } + return str.join("&") + } + function parseQueryString(str) { + var pairs = str.split("&"), params = {} + for (var i = 0; i < pairs.length; i++) { + var pair = pairs[i].split("=") + params[decodeSpace(pair[0])] = pair[1] ? decodeSpace(pair[1]) : (pair.length === 1 ? true : "") + } + return params + } + function decodeSpace(string) { + return decodeURIComponent(string.replace(/\+/g, " ")) + } + function reset(root) { + var cacheKey = getCellCacheKey(root) + clear(root.childNodes, cellCache[cacheKey]) + cellCache[cacheKey] = undefined + } + + function newPromisedProp(prop, promise) { + prop.then = function () { + var newProp = m.prop() + return newPromisedProp(newProp, + promise.then.apply(promise, arguments).then(newProp)) + } + prop.promise = prop + prop.resolve = function (val) { + prop(val) + promise = promise.resolve.apply(promise, arguments) + return prop + } + prop.reject = function () { + promise = promise.reject.apply(promise, arguments) + return prop + } + + return prop + } + m.deferred = function () { + return newPromisedProp(m.prop(), new Deferred()) + } + // Promiz.mithril.js | Zolmeister | MIT + function Deferred(fn, er) { + // states + // 0: pending + // 1: resolving + // 2: rejecting + // 3: resolved + // 4: rejected + var self = this, + state = 0, + val = 0, + next = []; + + self['promise'] = self + + self['resolve'] = function (v) { + if (!state) { + val = v + state = 1 + + fire() + } + return this + } + + self['reject'] = function (v) { + if (!state) { + val = v + state = 2 + + fire() + } + return this + } + + self['then'] = function (fn, er) { + var d = new Deferred(fn, er) + if (state == 3) { + d.resolve(val) + } + else if (state == 4) { + d.reject(val) + } + else { + next.push(d) + } + return d + } + + var finish = function (type) { + state = type || 4 + next.map(function (p) { + state == 3 && p.resolve(val) || p.reject(val) + }) + } + + // ref : reference to 'then' function + // cb, ec, cn : successCallback, failureCallback, notThennableCallback + function thennable (ref, cb, ec, cn) { + if ((typeof val == 'object' || typeof val == 'function') && typeof ref == 'function') { + try { + + // cnt protects against abuse calls from spec checker + var cnt = 0 + ref.call(val, function (v) { + if (cnt++) return + val = v + cb() + }, function (v) { + if (cnt++) return + val = v + ec() + }) + } catch (e) { + val = e + ec() + } + } else { + cn() + } + }; + + function fire() { + + // check if it's a thenable + var ref; + try { + ref = val && val.then + } catch (e) { + val = e + state = 2 + return fire() + } + thennable(ref, function () { + state = 1 + fire() + }, function () { + state = 2 + fire() + }, function () { + try { + if (state == 1 && typeof fn == 'function') { + val = fn(val) + } + + else if (state == 2 && typeof er == 'function') { + val = er(val) + state = 1 + } + } catch (e) { + val = e + return finish() + } + + if (val == self) { + val = TypeError() + finish() + } else thennable(ref, function () { + finish(3) + }, finish, function () { + finish(state == 1 && 3) + }) + + }) + } + } + + m.sync = function(args) { + var method = "resolve" + function synchronizer(pos, resolved) { + return function(value) { + results[pos] = value + if (!resolved) method = "reject" + if (--outstanding == 0) { + deferred.promise(results) + deferred[method](results) + } + return value + } + } + + var deferred = m.deferred() + var outstanding = args.length + var results = new Array(outstanding) + if (args.length > 0) { + for (var i = 0; i < args.length; i++) { + args[i].then(synchronizer(i, true), synchronizer(i, false)) + } + } + else deferred.resolve() + + return deferred.promise + } + function identity(value) {return value} + + function serializeArray(array, prefix){ + var idx, out = []; + for(idx in array){ + var formatted = (prefix ? prefix : "") + "[]"; + if(prefix && typeof array[idx] === "object") + formatted = formatted.replace(/\[\]$/i, "[" + idx + "]"); + if(typeof array[idx] === "object" && JSON.stringify(array[idx]) === "{}"){ + continue; + } + if(array[idx] instanceof Array) + out.push(serializeArray(array[idx], formatted)); + else if(typeof array[idx] === "object") + out.push(serializeObject(array[idx], formatted)); + else + out.push(encodeURIComponent(formatted) + "=" + encodeURIComponent(array[idx])); + } + return out.join("&"); + } + + function serializeObject(obj, prefix) { + var key, out = []; + for(key in obj){ + var formatted = prefix ? prefix + "[" + key + "]" : key; + if(obj[key] instanceof Array){ + if(obj[key].length < 1) + continue; + out.push(serializeArray(obj[key], formatted)); + }else if(typeof obj[key] === "object"){ + if(JSON.stringify(obj[key]) === "{}") + continue; + out.push(serializeObject(obj[key], formatted)); + }else{ + out.push(encodeURIComponent(formatted) + "=" + encodeURIComponent(obj[key])); + } + }; + return out.join("&"); + } + + function ajax(options) { + if (options.dataType && options.dataType.toLowerCase() === "jsonp") { + var callbackKey = "mithril_callback_" + new Date().getTime() + "_" + (Math.round(Math.random() * 1e16)).toString(36); + var script = window.document.createElement("script"); + + window[callbackKey] = function(resp){ + delete window[callbackKey]; + window.document.body.removeChild(script); + options.onload({ type: "load", target: { + responseText: resp + } }); + }; + + script.onerror = function(e){ + delete window[callbackKey]; + window.document.body.removeChild(script); + + options.onerror({ type: "error", target: { + status: 500, + responseText: JSON.stringify({ error: "Error making jsonp request" }) + } }); + + e.preventDefault(); + e.stopPropagation(); + }; + + script.onload = function(e){ + e.preventDefault(); + e.stopPropagation(); + }; + + + script.src = options.url + + (options.url.indexOf("?") > 0 ? "&" : "?") + + (options.callbackKey ? options.callbackKey : "callback") + + "=" + callbackKey + + "&" + serializeObject(options.data || {}); + window.document.body.appendChild(script); + }else{ + var xhr = new window.XMLHttpRequest + xhr.open(options.method, options.url, true, options.user, options.password) + xhr.onreadystatechange = function() { + if (xhr.readyState === 4) { + if (xhr.status >= 200 && xhr.status < 300) options.onload({type: "load", target: xhr}) + else options.onerror({type: "error", target: xhr}) + } + } + if (options.serialize == JSON.stringify && options.method != "GET") { + xhr.setRequestHeader("Content-Type", "application/json; charset=utf-8"); + } + if (typeof options.config == "function") { + var maybeXhr = options.config(xhr, options) + if (maybeXhr != null) xhr = maybeXhr + } + + xhr.send(options.method == "GET" ? "" : options.data) + return xhr + } + } + function bindData(xhrOptions, data, serialize) { + if (data && Object.keys(data).length > 0) { + if (xhrOptions.method == "GET") { + xhrOptions.url = xhrOptions.url + (xhrOptions.url.indexOf("?") < 0 ? "?" : "&") + buildQueryString(data) + } + else xhrOptions.data = serialize(data) + } + return xhrOptions + } + function parameterizeUrl(url, data) { + var tokens = url.match(/:[a-z]\w+/gi) + if (tokens && data) { + for (var i = 0; i < tokens.length; i++) { + var key = tokens[i].slice(1) + url = url.replace(tokens[i], data[key]) + delete data[key] + } + } + return url + } + + m.request = function(xhrOptions) { + if (xhrOptions.background !== true) m.startComputation() + var deferred = m.deferred() + var serialize = xhrOptions.serialize = xhrOptions.dataType && xhrOptions.dataType.toLowerCase() === "jsonp" + ? identity : xhrOptions.serialize || JSON.stringify + var deserialize = xhrOptions.deserialize = xhrOptions.dataType && xhrOptions.dataType.toLowerCase() === "jsonp" + ? identity : xhrOptions.deserialize || JSON.parse + var extract = xhrOptions.extract || function(xhr) { + return xhr.responseText.length === 0 && deserialize === JSON.parse ? null : xhr.responseText + } + xhrOptions.url = parameterizeUrl(xhrOptions.url, xhrOptions.data) + xhrOptions = bindData(xhrOptions, xhrOptions.data, serialize) + xhrOptions.onload = xhrOptions.onerror = function(e) { + try { + e = e || event + var unwrap = (e.type == "load" ? xhrOptions.unwrapSuccess : xhrOptions.unwrapError) || identity + var response = unwrap(deserialize(extract(e.target, xhrOptions))) + if (e.type == "load") { + if (type.call(response) == "[object Array]" && xhrOptions.type) { + for (var i = 0; i < response.length; i++) response[i] = new xhrOptions.type(response[i]) + } + else if (xhrOptions.type) response = new xhrOptions.type(response) + } + deferred[e.type == "load" ? "resolve" : "reject"](response) + } + catch (e) { + if (e instanceof SyntaxError) throw new SyntaxError("Could not parse HTTP response. See http://lhorie.github.io/mithril/mithril.request.html#using-variable-data-formats") + else if (type.call(e) == "[object Error]" && e.constructor !== Error) throw e + else deferred.reject(e) + } + if (xhrOptions.background !== true) m.endComputation() + } + ajax(xhrOptions) + return deferred.promise + } + + //testing API + m.deps = function(mock) {return window = mock} + //for internal testing only, do not use `m.deps.factory` + m.deps.factory = app + + return m +}(typeof window != "undefined" ? window : {}) + +if (typeof module != "undefined" && module !== null) module.exports = m +if (typeof define == "function" && define.amd) define(function() {return m}) + +;;; + +function test(condition) { + var duration = 0 + var start = 0 + var result = true + test.total++ + + if (typeof performance != "undefined") { + start = performance.now() + } + try { + if (!condition()) throw new Error() + } + catch (e) { + result = false + console.error(e) + test.failures.push(condition) + } + if (typeof performance != "undefined") { + duration = performance.now() - start + } + + test_obj = { + name: "" + test.total, + result: result, + duration: duration + } + + if (typeof window != "undefined") { + if (!result) { + window.global_test_results.tests.push(test_obj) + } + + window.global_test_results.duration += duration + if (result) { + window.global_test_results.passed++ + } else { + window.global_test_results.failed++ + } + } +} +test.total = 0 +test.failures = [] +test.print = function(print) { + for (var i = 0; i < test.failures.length; i++) { + print(test.failures[i].toString()) + } + print("tests: " + test.total + "\nfailures: " + test.failures.length) + + if (test.failures.length > 0) { + throw new Error(test.failures.length + " tests did not pass") + } +} + +var mock = {} +mock.window = new function() { + var window = {} + window.document = {} + window.document.childNodes = [] + window.document.createElement = function(tag) { + return { + style: {}, + childNodes: [], + nodeType: 1, + nodeName: tag.toUpperCase(), + appendChild: window.document.appendChild, + removeChild: window.document.removeChild, + replaceChild: window.document.replaceChild, + insertBefore: function(node, reference) { + node.parentNode = this + var referenceIndex = this.childNodes.indexOf(reference) + if (referenceIndex < 0) this.childNodes.push(node) + else { + var index = this.childNodes.indexOf(node) + if (index > -1) this.childNodes.splice(index, 1) + this.childNodes.splice(referenceIndex, 0, node) + } + }, + insertAdjacentHTML: function(position, html) { + //todo: accept markup + if (position == "beforebegin") { + this.parentNode.insertBefore(window.document.createTextNode(html), this) + } + else if (position == "beforeend") { + this.appendChild(window.document.createTextNode(html)) + } + }, + setAttribute: function(name, value) { + this[name] = value.toString() + }, + setAttributeNS: function(namespace, name, value) { + this.namespaceURI = namespace + this[name] = value.toString() + }, + getAttribute: function(name, value) { + return this[name] + } + } + } + window.document.createElementNS = function(namespace, tag) { + var element = window.document.createElement(tag) + element.namespaceURI = namespace + return element + } + window.document.createTextNode = function(text) { + return {nodeValue: text.toString()} + } + window.document.documentElement = window.document.createElement("html") + window.document.replaceChild = function(newChild, oldChild) { + var index = this.childNodes.indexOf(oldChild) + if (index > -1) this.childNodes.splice(index, 1, newChild) + else this.childNodes.push(newChild) + newChild.parentNode = this + oldChild.parentNode = null + } + window.document.appendChild = function(child) { + var index = this.childNodes.indexOf(child) + if (index > -1) this.childNodes.splice(index, 1) + this.childNodes.push(child) + child.parentNode = this + } + window.document.removeChild = function(child) { + var index = this.childNodes.indexOf(child) + this.childNodes.splice(index, 1) + child.parentNode = null + } + window.document.getElementsByTagName = function(name){ + name = name.toLowerCase(); + var out = []; + + var traverse = function(node){ + if(node.childNodes && node.childNodes.length > 0){ + node.childNodes.forEach(function(curr){ + if(curr.nodeName.toLowerCase() === name) + out.push(curr); + traverse(curr); + }); + } + }; + + traverse(window.document); + return out; + } + window.scrollTo = function() {} + window.cancelAnimationFrame = function() {} + window.requestAnimationFrame = function(callback) { + window.requestAnimationFrame.$callback = callback + return window.requestAnimationFrame.$id++ + } + window.requestAnimationFrame.$id = 1 + window.requestAnimationFrame.$resolve = function() { + if (window.requestAnimationFrame.$callback) window.requestAnimationFrame.$callback() + window.requestAnimationFrame.$callback = null + } + window.XMLHttpRequest = new function() { + var request = function() { + this.$headers = {} + this.setRequestHeader = function(key, value) { + this.$headers[key] = value + } + this.open = function(method, url) { + this.method = method + this.url = url + } + this.send = function() { + this.responseText = JSON.stringify(this) + this.readyState = 4 + this.status = 200 + request.$instances.push(this) + } + } + request.$instances = [] + return request + } + window.location = {search: "", pathname: "", hash: ""}, + window.history = {} + window.history.pushState = function(data, title, url) { + window.location.pathname = window.location.search = window.location.hash = url + }, + window.history.replaceState = function(data, title, url) { + window.location.pathname = window.location.search = window.location.hash = url + } + return window +} +function testMithril(mock) { + m.deps(mock) + + //m + test(function() {return m("div").tag === "div"}) + test(function() {return m(".foo").tag === "div"}) + test(function() {return m(".foo").attrs.className === "foo"}) + test(function() {return m("[title=bar]").tag === "div"}) + test(function() {return m("[title=bar]").attrs.title === "bar"}) + test(function() {return m("[title=\'bar\']").attrs.title === "bar"}) + test(function() {return m("[title=\"bar\"]").attrs.title === "bar"}) + test(function() {return m("div", "test").children === "test"}) + test(function() {return m("div", ["test"]).children[0] === "test"}) + test(function() {return m("div", {title: "bar"}, "test").attrs.title === "bar"}) + test(function() {return m("div", {title: "bar"}, "test").children === "test"}) + test(function() {return m("div", {title: "bar"}, ["test"]).children[0] === "test"}) + test(function() {return m("div", {title: "bar"}, m("div")).children.tag === "div"}) + test(function() {return m("div", {title: "bar"}, [m("div")]).children[0].tag === "div"}) + test(function() {return m("div", ["a", "b"]).children.length === 2}) + test(function() {return m("div", [m("div")]).children[0].tag === "div"}) + test(function() {return m("div", m("div")).children.tag === "div"}) //yes, this is expected behavior: see method signature + test(function() {return m("div", [undefined]).tag === "div"}) + test(function() {return m("div", [{foo: "bar"}])}) //as long as it doesn't throw errors, it's fine + test(function() {return m("svg", [m("g")])}) + test(function() {return m("svg", [m("a[href='http://google.com']")])}) + test(function() {return m(".foo", {class: "bar"}).attrs.class == "foo bar"}) + test(function() {return m(".foo", {className: "bar"}).attrs.className == "foo bar"}) + + //m.module + test(function() { + mock.requestAnimationFrame.$resolve() + + var root1 = mock.document.createElement("div") + m.module(root1, { + controller: function() {this.value = "test1"}, + view: function(ctrl) {return ctrl.value} + }) + + var root2 = mock.document.createElement("div") + m.module(root2, { + controller: function() {this.value = "test2"}, + view: function(ctrl) {return ctrl.value} + }) + + mock.requestAnimationFrame.$resolve() + + return root1.childNodes[0].nodeValue === "test1" && root2.childNodes[0].nodeValue === "test2" + }) + + //m.withAttr + test(function() { + var value + var handler = m.withAttr("test", function(data) {value = data}) + handler({currentTarget: {test: "foo"}}) + return value === "foo" + }) + + //m.trust + test(function() {return m.trust("test").valueOf() === "test"}) + + //m.render + test(function() { + var root = mock.document.createElement("div") + m.render(root, "test") + return root.childNodes[0].nodeValue === "test" + }) + test(function() { + var root = mock.document.createElement("div") + m.render(root, m("div", {class: "a"})) + var elementBefore = root.childNodes[0] + m.render(root, m("div", {class: "b"})) + var elementAfter = root.childNodes[0] + return elementBefore === elementAfter + }) + test(function() { + var root = mock.document.createElement("div") + m.render(root, m(".a")) + var elementBefore = root.childNodes[0] + m.render(root, m(".b")) + var elementAfter = root.childNodes[0] + return elementBefore === elementAfter + }) + test(function() { + var root = mock.document.createElement("div") + m.render(root, m("div", {id: "a"})) + var elementBefore = root.childNodes[0] + m.render(root, m("div", {title: "b"})) + var elementAfter = root.childNodes[0] + return elementBefore !== elementAfter + }) + test(function() { + var root = mock.document.createElement("div") + m.render(root, m("#a")) + var elementBefore = root.childNodes[0] + m.render(root, m("[title=b]")) + var elementAfter = root.childNodes[0] + return elementBefore !== elementAfter + }) + test(function() { + var root = mock.document.createElement("div") + m.render(root, m("#a")) + var elementBefore = root.childNodes[0] + m.render(root, "test") + var elementAfter = root.childNodes[0] + return elementBefore !== elementAfter + }) + test(function() { + var root = mock.document.createElement("div") + m.render(root, m("div", [undefined])) + return root.childNodes[0].childNodes[0].nodeValue === "" + }) + test(function() { + var root = mock.document.createElement("div") + m.render(root, m("svg", [m("g")])) + var g = root.childNodes[0].childNodes[0] + return g.nodeName === "G" && g.namespaceURI == "http://www.w3.org/2000/svg" + }) + test(function() { + var root = mock.document.createElement("div") + m.render(root, m("svg", [m("a[href='http://google.com']")])) + return root.childNodes[0].childNodes[0].nodeName === "A" + }) + test(function() { + var root = mock.document.createElement("div") + m.render(root, m("div.classname", [m("a", {href: "/first"})])) + m.render(root, m("div", [m("a", {href: "/second"})])) + return root.childNodes[0].childNodes.length == 1 + }) + test(function() { + var root = mock.document.createElement("div") + m.render(root, m("ul", [m("li")])) + m.render(root, m("ul", [m("li"), undefined])) + return root.childNodes[0].childNodes[1].nodeValue === "" + }) + test(function() { + var root = mock.document.createElement("div") + m.render(root, m("ul", [m("li"), m("li")])) + m.render(root, m("ul", [m("li"), undefined])) + return root.childNodes[0].childNodes.length == 2 && root.childNodes[0].childNodes[1].nodeValue === "" + }) + test(function() { + var root = mock.document.createElement("div") + m.render(root, m("ul", [m("li")])) + m.render(root, m("ul", [undefined])) + return root.childNodes[0].childNodes[0].nodeValue === "" + }) + test(function() { + var root = mock.document.createElement("div") + m.render(root, m("ul", [m("li")])) + m.render(root, m("ul", [{}])) + return root.childNodes[0].childNodes.length === 0 + }) + test(function() { + var root = mock.document.createElement("div") + m.render(root, m("ul", [m("li", [m("a")])])) + m.render(root, m("ul", [{subtree: "retain"}])) + return root.childNodes[0].childNodes[0].childNodes[0].nodeName === "A" + }) + test(function() { + //https://github.com/lhorie/mithril.js/issues/43 + var root = mock.document.createElement("div") + m.render(root, m("a", {config: m.route}, "test")) + m.render(root, m("a", {config: m.route}, "test")) + return root.childNodes[0].childNodes[0].nodeValue === "test" + }) + test(function() { + //https://github.com/lhorie/mithril.js/issues/29 + var root = mock.document.createElement("div") + var list = [false, false] + m.render(root, list.reverse().map(function(flag, index) { + return m("input[type=checkbox]", {onclick: m.withAttr("checked", function(value) {list[index] = value}), checked: flag}) + })) + + mock.document.activeElement = root.childNodes[0] + root.childNodes[0].checked = true + root.childNodes[0].onclick({currentTarget: {checked: true}}) + + m.render(root, list.reverse().map(function(flag, index) { + return m("input[type=checkbox]", {onclick: m.withAttr("checked", function(value) {list[index] = value}), checked: flag}) + })) + + mock.document.activeElement = null + + return root.childNodes[0].checked === false && root.childNodes[1].checked === true + }) + test(function() { + //https://github.com/lhorie/mithril.js/issues/44 (1) + var root = mock.document.createElement("div") + m.render(root, m("#foo", [null, m("#bar")])) + m.render(root, m("#foo", ["test", m("#bar")])) + return root.childNodes[0].childNodes[0].nodeValue === "test" + }) + test(function() { + //https://github.com/lhorie/mithril.js/issues/44 (2) + var root = mock.document.createElement("div") + m.render(root, m("#foo", [null, m("#bar")])) + m.render(root, m("#foo", [m("div"), m("#bar")])) + return root.childNodes[0].childNodes[0].nodeName === "DIV" + }) + test(function() { + //https://github.com/lhorie/mithril.js/issues/44 (3) + var root = mock.document.createElement("div") + m.render(root, m("#foo", ["test", m("#bar")])) + m.render(root, m("#foo", [m("div"), m("#bar")])) + return root.childNodes[0].childNodes[0].nodeName === "DIV" + }) + test(function() { + //https://github.com/lhorie/mithril.js/issues/44 (4) + var root = mock.document.createElement("div") + m.render(root, m("#foo", [m("div"), m("#bar")])) + m.render(root, m("#foo", ["test", m("#bar")])) + return root.childNodes[0].childNodes[0].nodeValue === "test" + }) + test(function() { + //https://github.com/lhorie/mithril.js/issues/44 (5) + var root = mock.document.createElement("div") + m.render(root, m("#foo", [m("#bar")])) + m.render(root, m("#foo", [m("#bar"), [m("#baz")]])) + return root.childNodes[0].childNodes[1].id === "baz" + }) + test(function() { + //https://github.com/lhorie/mithril.js/issues/48 + var root = mock.document + m.render(root, m("html", [m("#foo")])) + var result = root.childNodes[0].childNodes[0].id === "foo" + root.childNodes = [mock.document.createElement("html")] + return result + }) + test(function() { + //https://github.com/lhorie/mithril.js/issues/49 + var root = mock.document.createElement("div") + m.render(root, m("a", "test")) + m.render(root, m("a.foo", "test")) + return root.childNodes[0].childNodes[0].nodeValue === "test" + }) + test(function() { + //https://github.com/lhorie/mithril.js/issues/49 + var root = mock.document.createElement("div") + m.render(root, m("a.foo", "test")) + m.render(root, m("a", "test")) + return root.childNodes[0].childNodes[0].nodeValue === "test" + }) + test(function() { + //https://github.com/lhorie/mithril.js/issues/49 + var root = mock.document.createElement("div") + m.render(root, m("a.foo", "test")) + m.render(root, m("a", "test1")) + return root.childNodes[0].childNodes[0].nodeValue === "test1" + }) + test(function() { + //https://github.com/lhorie/mithril.js/issues/49 + var root = mock.document.createElement("div") + m.render(root, m("a", "test")) + m.render(root, m("a", "test1")) + return root.childNodes[0].childNodes[0].nodeValue === "test1" + }) + test(function() { + //https://github.com/lhorie/mithril.js/issues/50 + var root = mock.document.createElement("div") + m.render(root, m("#foo", [[m("div", "a"), m("div", "b")], m("#bar")])) + return root.childNodes[0].childNodes[1].childNodes[0].nodeValue === "b" + }) + test(function() { + //https://github.com/lhorie/mithril.js/issues/50 + var root = mock.document.createElement("div") + m.render(root, m("#foo", [[m("div", "a"), m("div", "b")], m("#bar")])) + m.render(root, m("#foo", [[m("div", "a"), m("div", "b"), m("div", "c")], m("#bar")])) + return root.childNodes[0].childNodes[2].childNodes[0].nodeValue === "c" + }) + test(function() { + //https://github.com/lhorie/mithril.js/issues/50 + var root = mock.document.createElement("div") + m.render(root, m("#foo", [[m("div", "a"), m("div", "b")], [m("div", "c"), m("div", "d")], m("#bar")])) + return root.childNodes[0].childNodes[3].childNodes[0].nodeValue === "d" && root.childNodes[0].childNodes[4].id === "bar" + }) + test(function() { + //https://github.com/lhorie/mithril.js/issues/50 + var root = mock.document.createElement("div") + m.render(root, m("#foo", [[m("div", "a"), m("div", "b")], "test"])) + return root.childNodes[0].childNodes[1].childNodes[0].nodeValue === "b" && root.childNodes[0].childNodes[2].nodeValue === "test" + }) + test(function() { + //https://github.com/lhorie/mithril.js/issues/50 + var root = mock.document.createElement("div") + m.render(root, m("#foo", [["a", "b"], "test"])) + return root.childNodes[0].childNodes[1].nodeValue === "b" && root.childNodes[0].childNodes[2].nodeValue === "test" + }) + test(function() { + //https://github.com/lhorie/mithril.js/issues/51 + var root = mock.document.createElement("div") + m.render(root, m("main", [m("button"), m("article", [m("section"), m("nav")])])) + m.render(root, m("main", [m("button"), m("article", [m("span"), m("nav")])])) + return root.childNodes[0].childNodes[1].childNodes[0].nodeName === "SPAN" + }) + test(function() { + //https://github.com/lhorie/mithril.js/issues/51 + var root = mock.document.createElement("div") + m.render(root, m("main", [m("button"), m("article", [m("section"), m("nav")])])) + m.render(root, m("main", [m("button"), m("article", ["test", m("nav")])])) + return root.childNodes[0].childNodes[1].childNodes[0].nodeValue === "test" + }) + test(function() { + //https://github.com/lhorie/mithril.js/issues/51 + var root = mock.document.createElement("div") + m.render(root, m("main", [m("button"), m("article", [m("section"), m("nav")])])) + m.render(root, m("main", [m("button"), m("article", [m.trust("test"), m("nav")])])) + return root.childNodes[0].childNodes[1].childNodes[0].nodeValue === "test" + }) + test(function() { + //https://github.com/lhorie/mithril.js/issues/55 + var root = mock.document.createElement("div") + m.render(root, m("#a")) + var elementBefore = root.childNodes[0] + m.render(root, m("#b")) + var elementAfter = root.childNodes[0] + return elementBefore !== elementAfter + }) + test(function() { + //https://github.com/lhorie/mithril.js/issues/56 + var root = mock.document.createElement("div") + m.render(root, [null, "foo"]) + m.render(root, ["bar"]) + return root.childNodes.length == 1 + }) + test(function() { + //https://github.com/lhorie/mithril.js/issues/56 + var root = mock.document.createElement("div") + m.render(root, m("div", "foo")) + return root.childNodes.length == 1 + }) + test(function() { + var root = mock.document.createElement("div") + m.render(root, m("div", [m("button"), m("ul")])) + var valueBefore = root.childNodes[0].childNodes[0].nodeName + m.render(root, m("div", [undefined, m("ul")])) + var valueAfter = root.childNodes[0].childNodes[0].nodeValue + return valueBefore === "BUTTON" && valueAfter === "" + }) + test(function() { + var root = mock.document.createElement("div") + m.render(root, m("div", [m("ul"), undefined])) + var valueBefore1 = root.childNodes[0].childNodes[0].nodeName + var valueBefore2 = root.childNodes[0].childNodes[1].nodeValue + m.render(root, m("div", [undefined, m("ul")])) + var valueAfter1 = root.childNodes[0].childNodes[0].nodeValue + var valueAfter2 = root.childNodes[0].childNodes[1].nodeName + return valueBefore1 === "UL" && valueAfter1 === "" && valueBefore2 === "" && valueAfter2 === "UL" + }) + test(function() { + //https://github.com/lhorie/mithril.js/issues/79 + var root = mock.document.createElement("div") + m.render(root, m("div", {style: {background: "red"}})) + var valueBefore = root.childNodes[0].style.background + m.render(root, m("div", {style: {}})) + var valueAfter = root.childNodes[0].style.background + return valueBefore === "red" && valueAfter === "" + }) + test(function() { + var root = mock.document.createElement("div") + m.render(root, m("div[style='background:red']")) + return root.childNodes[0].style === "background:red" + }) + test(function() { + var root = mock.document.createElement("div") + m.render(root, m("div", {style: {background: "red"}})) + var valueBefore = root.childNodes[0].style.background + m.render(root, m("div", {})) + var valueAfter = root.childNodes[0].style.background + return valueBefore === "red" && valueAfter === undefined + }) + test(function() { + var root = mock.document.createElement("div") + var module = {}, unloaded = false + module.controller = function() { + this.onunload = function() {unloaded = true} + } + module.view = function() {} + m.module(root, module) + m.module(root, {controller: function() {}, view: function() {}}) + return unloaded === true + }) + test(function() { + //https://github.com/lhorie/mithril.js/issues/87 + var root = mock.document.createElement("div") + m.render(root, m("div", [[m("a"), m("a")], m("button")])) + m.render(root, m("div", [[m("a")], m("button")])) + return root.childNodes[0].childNodes.length == 2 && root.childNodes[0].childNodes[1].nodeName == "BUTTON" + }) + test(function() { + //https://github.com/lhorie/mithril.js/issues/87 + var root = mock.document.createElement("div") + m.render(root, m("div", [m("a"), m("b"), m("button")])) + m.render(root, m("div", [m("a"), m("button")])) + return root.childNodes[0].childNodes.length == 2 && root.childNodes[0].childNodes[1].nodeName == "BUTTON" + }) + test(function() { + //https://github.com/lhorie/mithril.js/issues/99 + var root = mock.document.createElement("div") + m.render(root, m("div", [m("img"), m("h1")])) + m.render(root, m("div", [m("a")])) + return root.childNodes[0].childNodes.length == 1 && root.childNodes[0].childNodes[0].nodeName == "A" + }) + test(function() { + //https://github.com/lhorie/mithril.js/issues/120 + var root = mock.document.createElement("div") + m.render(root, m("div", ["a", "b", "c", "d"])) + m.render(root, m("div", [["d", "e"]])) + var children = root.childNodes[0].childNodes + return children.length == 2 && children[0].nodeValue == "d" && children[1].nodeValue == "e" + }) + test(function() { + //https://github.com/lhorie/mithril.js/issues/120 + var root = mock.document.createElement("div") + m.render(root, m("div", [["a", "b", "c", "d"]])) + m.render(root, m("div", ["d", "e"])) + var children = root.childNodes[0].childNodes + return children.length == 2 && children[0].nodeValue == "d" && children[1].nodeValue == "e" + }) + test(function() { + //https://github.com/lhorie/mithril.js/issues/120 + var root = mock.document.createElement("div") + m.render(root, m("div", ["x", [["a"], "b", "c", "d"]])) + m.render(root, m("div", ["d", ["e"]])) + var children = root.childNodes[0].childNodes + return children.length == 2 && children[0].nodeValue == "d" && children[1].nodeValue == "e" + }) + test(function() { + //https://github.com/lhorie/mithril.js/issues/120 + var root = mock.document.createElement("div") + m.render(root, m("div", ["b"])) + m.render(root, m("div", [["e"]])) + var children = root.childNodes[0].childNodes + return children.length == 1 && children[0].nodeValue == "e" + }) + test(function() { + //https://github.com/lhorie/mithril.js/issues/120 + var root = mock.document.createElement("div") + m.render(root, m("div", ["a", ["b"]])) + m.render(root, m("div", ["d", [["e"]]])) + var children = root.childNodes[0].childNodes + return children.length == 2 && children[0].nodeValue == "d" && children[1].nodeValue == "e" + }) + test(function() { + //https://github.com/lhorie/mithril.js/issues/120 + var root = mock.document.createElement("div") + m.render(root, m("div", ["a", [["b"]]])) + m.render(root, m("div", ["d", ["e"]])) + var children = root.childNodes[0].childNodes + return children.length == 2 && children[0].nodeValue == "d" && children[1].nodeValue == "e" + }) + test(function() { + //https://github.com/lhorie/mithril.js/issues/120 + var root = mock.document.createElement("div") + m.render(root, m("div", ["a", [["b"], "c"]])) + m.render(root, m("div", ["d", [[["e"]], "x"]])) + var children = root.childNodes[0].childNodes + return children.length == 3 && children[0].nodeValue == "d" && children[1].nodeValue == "e" + }) + test(function() { + var root = mock.document.createElement("div") + + var success = false + m.render(root, m("div", {config: function(elem, isInitialized, ctx) {ctx.data = 1}})) + m.render(root, m("div", {config: function(elem, isInitialized, ctx) {success = ctx.data === 1}})) + return success + }) + test(function() { + var root = mock.document.createElement("div") + + var index = 0; + var success = true; + var statefulConfig = function(elem, isInitialized, ctx) {ctx.data = index++} + var node = m("div", {config: statefulConfig}); + m.render(root, [node, node]); + + index = 0; + var checkConfig = function(elem, isInitialized, ctx) { + success = success && (ctx.data === index++) + } + node = m("div", {config: checkConfig}); + m.render(root, [node, node]); + return success; + }) + test(function() { + var root = mock.document.createElement("div") + var parent + m.render(root, m("div", m("a", { + config: function(el) {parent = el.parentNode.parentNode} + }))); + return parent === root + }) + test(function() { + var root = mock.document.createElement("div") + var count = 0 + m.render(root, m("div", m("a", { + config: function(el) { + var island = mock.document.createElement("div") + count++ + if (count > 2) throw "too much recursion..." + m.render(island, m("div")) + } + }))); + return count == 1 + }) + test(function() { + //https://github.com/lhorie/mithril.js/issues/129 + var root = mock.document.createElement("div") + m.render(root, m("div", [["foo", "bar"], ["foo", "bar"], ["foo", "bar"]])); + m.render(root, m("div", ["asdf", "asdf2", "asdf3"])); + return true + }) + test(function() { + //https://github.com/lhorie/mithril.js/issues/98 + //insert at beginning + var root = mock.document.createElement("div") + m.render(root, [m("a", {key: 1}), m("a", {key: 2}), m("a", {key: 3})]) + var firstBefore = root.childNodes[0] + m.render(root, [m("a", {key: 4}), m("a", {key: 1}), m("a", {key: 2}), m("a", {key: 3})]) + var firstAfter = root.childNodes[1] + return firstBefore == firstAfter && root.childNodes[0].key == 4 && root.childNodes.length == 4 + }) + test(function() { + //https://github.com/lhorie/mithril.js/issues/98 + var root = mock.document.createElement("div") + m.render(root, [m("a", {key: 1}), m("a", {key: 2}), m("a", {key: 3})]) + var firstBefore = root.childNodes[0] + m.render(root, [m("a", {key: 4}), m("a", {key: 1}), m("a", {key: 2})]) + var firstAfter = root.childNodes[1] + return firstBefore == firstAfter && root.childNodes[0].key == 4 && root.childNodes.length == 3 + }) + test(function() { + //https://github.com/lhorie/mithril.js/issues/98 + var root = mock.document.createElement("div") + m.render(root, [m("a", {key: 1}), m("a", {key: 2}), m("a", {key: 3})]) + var firstBefore = root.childNodes[1] + m.render(root, [m("a", {key: 2}), m("a", {key: 3}), m("a", {key: 4})]) + var firstAfter = root.childNodes[0] + return firstBefore == firstAfter && root.childNodes[0].key === "2" && root.childNodes.length === 3 + }) + test(function() { + //https://github.com/lhorie/mithril.js/issues/98 + var root = mock.document.createElement("div") + m.render(root, [m("a", {key: 1}), m("a", {key: 2}), m("a", {key: 3}), m("a", {key: 4}), m("a", {key: 5})]) + var firstBefore = root.childNodes[0] + var secondBefore = root.childNodes[1] + var fourthBefore = root.childNodes[3] + m.render(root, [m("a", {key: 4}), m("a", {key: 10}), m("a", {key: 1}), m("a", {key: 2})]) + var firstAfter = root.childNodes[2] + var secondAfter = root.childNodes[3] + var fourthAfter = root.childNodes[0] + return firstBefore === firstAfter && secondBefore === secondAfter && fourthBefore === fourthAfter && root.childNodes[1].key == "10" && root.childNodes.length === 4 + }) + test(function() { + //https://github.com/lhorie/mithril.js/issues/98 + var root = mock.document.createElement("div") + m.render(root, [m("a", {key: 1}), m("a", {key: 2}), m("a", {key: 3}), m("a", {key: 4}), m("a", {key: 5})]) + var firstBefore = root.childNodes[0] + var secondBefore = root.childNodes[1] + var fourthBefore = root.childNodes[3] + m.render(root, [m("a", {key: 4}), m("a", {key: 10}), m("a", {key: 2}), m("a", {key: 1}), m("a", {key: 6}), m("a", {key: 7})]) + var firstAfter = root.childNodes[3] + var secondAfter = root.childNodes[2] + var fourthAfter = root.childNodes[0] + return firstBefore === firstAfter && secondBefore === secondAfter && fourthBefore === fourthAfter && root.childNodes[1].key == "10" && root.childNodes[4].key == "6" && root.childNodes[5].key == "7" && root.childNodes.length === 6 + }) + test(function() { + //https://github.com/lhorie/mithril.js/issues/149 + var root = mock.document.createElement("div") + m.render(root, [m("a", {key: 1}), m("a", {key: 2}), m("a"), m("a", {key: 4}), m("a", {key: 5})]) + var firstBefore = root.childNodes[0] + var secondBefore = root.childNodes[1] + var thirdBefore = root.childNodes[2] + var fourthBefore = root.childNodes[3] + var fifthBefore = root.childNodes[4] + m.render(root, [m("a", {key: 4}), m("a", {key: 5}), m("a"), m("a", {key: 1}), m("a", {key: 2})]) + var firstAfter = root.childNodes[3] + var secondAfter = root.childNodes[4] + var thirdAfter = root.childNodes[2] + var fourthAfter = root.childNodes[0] + var fifthAfter = root.childNodes[1] + return firstBefore === firstAfter && secondBefore === secondAfter && thirdBefore === thirdAfter && fourthBefore === fourthAfter && fifthBefore === fifthAfter + }) + test(function() { + //https://github.com/lhorie/mithril.js/issues/134 + var root = mock.document.createElement("div") + m.render(root, m("div", {contenteditable: true}, "test")) + mock.document.activeElement = root.childNodes[0] + m.render(root, m("div", {contenteditable: true}, "test1")) + m.render(root, m("div", {contenteditable: false}, "test2")) + return root.childNodes[0].childNodes[0].nodeValue === "test2" + }) + test(function() { + //https://github.com/lhorie/mithril.js/issues/136 + var root = mock.document.createElement("div") + m.render(root, m("textarea", ["test"])) + m.render(root, m("textarea", ["test1"])) + return root.childNodes[0].value === "test1" + }) + test(function() { + var root = mock.document.createElement("div") + var unloaded = 0 + m.render(root, [ + m("div", { + key: 1, + config: function(el, init, ctx) { + ctx.onunload = function() { + unloaded++ + } + } + }) + ]) + m.render(root, [ + m("div", {key: 2}), + m("div", { + key: 1, + config: function(el, init, ctx) { + ctx.onunload = function() { + unloaded++ + } + } + }) + ]) + return unloaded == 0 + }) + test(function() { + var root = mock.document.createElement("div") + var unloadedParent = 0 + var unloadedChild = 0 + var configParent = function(el, init, ctx) { + ctx.onunload = function() { + unloadedParent++ + } + } + var configChild = function(el, init, ctx) { + ctx.onunload = function() { + unloadedChild++ + } + } + var unloaded = 0 + m.render(root, m("div", {config: configParent}, m("a", {config: configChild}))) + m.render(root, m("main", {config: configParent}, m("a", {config: configChild}))) + return unloadedParent === 1 && unloadedChild === 0 + }) + test(function() { + var root = mock.document.createElement("div") + var unloadedParent = 0 + var unloadedChild = 0 + var configParent = function(el, init, ctx) { + ctx.onunload = function() { + unloadedParent++ + } + } + var configChild = function(el, init, ctx) { + ctx.onunload = function() { + unloadedChild++ + } + } + var unloaded = 0 + m.render(root, m("div", {config: configParent}, m("a", {config: configChild}))) + m.render(root, m("main", {config: configParent}, m("b", {config: configChild}))) + return unloadedParent === 1 && unloadedChild === 1 + }) + test(function() { + //https://github.com/lhorie/mithril.js/issues/150 + var root = mock.document.createElement("div") + m.render(root, [m("a"), m("div")]) + m.render(root, [[], m("div")]) + return root.childNodes.length == 1 && root.childNodes[0].nodeName == "DIV" + }) + test(function() { + //https://github.com/lhorie/mithril.js/issues/156 + var root = mock.document.createElement("div") + m.render(root, m("div", [ + ["a", "b", "c", "d"].map(function() { + return [m("div"), " "] + }), + m("span") + ])) + return root.childNodes[0].childNodes[8].nodeName == "SPAN" + }) + test(function() { + //https://github.com/lhorie/mithril.js/issues/157 + var root = mock.document.createElement("div") + m.render(root, m("ul", [m("li", {key: 0}), m("li", {key: 2}), m("li", {key: 4})])) + m.render(root, m("ul", [m("li", {key: 0}), m("li", {key: 1}), m("li", {key: 2}), m("li", {key: 3}), m("li", {key: 4}), m("li", {key: 5})])) + return root.childNodes[0].childNodes.map(function(n) {return n.key}).join("") == "012345" + }) + test(function() { + //https://github.com/lhorie/mithril.js/issues/157 + var root = mock.document.createElement("div") + m.render(root, m("input", {value: "a"})) + m.render(root, m("input", {value: "aa"})) + return root.childNodes[0].childNodes.length == 0 + }) + test(function() { + //https://github.com/lhorie/mithril.js/issues/157 + var root = mock.document.createElement("div") + m.render(root, m("br", {class: "a"})) + m.render(root, m("br", {class: "aa"})) + return root.childNodes[0].childNodes.length == 0 + }) + test(function() { + //https://github.com/lhorie/mithril.js/issues/194 + var root = mock.document.createElement("div") + m.render(root, m("ul", [m("li", {key: 0}), m("li", {key: 1}), m("li", {key: 2}), m("li", {key: 3}), m("li", {key: 4}), m("li", {key: 5})])) + m.render(root, m("ul", [m("li", {key: 0}), m("li", {key: 1}), m("li", {key: 2}), m("li", {key: 4}), m("li", {key: 5})])) + return root.childNodes[0].childNodes.map(function(n) {return n.key}).join("") == "01245" + }) + test(function() { + //https://github.com/lhorie/mithril.js/issues/206 + var root = mock.document.createElement("div") + m.render(root, m("div", undefined)) + m.render(root, m("div", [m("div")])) + return root.childNodes[0].childNodes.length == 1 + }) + test(function() { + //https://github.com/lhorie/mithril.js/issues/206 + var root = mock.document.createElement("div") + m.render(root, m("div", null)) + m.render(root, m("div", [m("div")])) + return root.childNodes[0].childNodes.length == 1 + }) + test(function() { + //https://github.com/lhorie/mithril.js/issues/200 + var root = mock.document.createElement("div") + + var unloaded1 = false + function unloadable1(element, isInit, context) { + context.onunload = function() { + unloaded1 = true + } + } + m.render(root, [ m("div", {config: unloadable1}) ]) + m.render(root, [ ]) + + var unloaded2 = false + function unloadable2(element, isInit, context) { + context.onunload = function() { + unloaded2 = true + } + } + m.render(root, [ m("div", {config: unloadable2}) ]) + m.render(root, [ ]) + + return unloaded1 === true && unloaded2 === true + }) + //end m.render + + //m.redraw + test(function() { + mock.requestAnimationFrame.$resolve() //setup + var controller + var root = mock.document.createElement("div") + m.module(root, { + controller: function() {controller = this}, + view: function(ctrl) {return ctrl.value} + }) + mock.requestAnimationFrame.$resolve() + var valueBefore = root.childNodes[0].nodeValue + controller.value = "foo" + m.redraw() + mock.requestAnimationFrame.$resolve() + return valueBefore === "" && root.childNodes[0].nodeValue === "foo" + }) + test(function() { + mock.requestAnimationFrame.$resolve() //setup + var count = 0 + var root = mock.document.createElement("div") + m.module(root, { + controller: function() {}, + view: function(ctrl) { + count++ + } + }) + mock.requestAnimationFrame.$resolve() //teardown + m.redraw() //should run synchronously + + m.redraw() //rest should run asynchronously since they're spamming + m.redraw() + m.redraw() + mock.requestAnimationFrame.$resolve() //teardown + return count === 3 + }) + test(function() { + mock.requestAnimationFrame.$resolve() //setup + var count = 0 + var root = mock.document.createElement("div") + m.module(root, { + controller: function() {}, + view: function(ctrl) { + count++ + } + }) + mock.requestAnimationFrame.$resolve() //teardown + m.redraw(true) //should run synchronously + + m.redraw(true) //forced to run synchronously + m.redraw(true) + m.redraw(true) + mock.requestAnimationFrame.$resolve() //teardown + return count === 5 + }) + + //m.route + test(function() { + mock.requestAnimationFrame.$resolve() //setup + mock.location.search = "?" + + var root = mock.document.createElement("div") + m.route.mode = "search" + m.route(root, "/test1", { + "/test1": {controller: function() {}, view: function() {return "foo"}} + }) + mock.requestAnimationFrame.$resolve() //teardown + return mock.location.search == "?/test1" && root.childNodes[0].nodeValue === "foo" + }) + test(function() { + mock.requestAnimationFrame.$resolve() //setup + mock.location.pathname = "/" + + var root = mock.document.createElement("div") + m.route.mode = "pathname" + m.route(root, "/test2", { + "/test2": {controller: function() {}, view: function() {return "foo"}} + }) + mock.requestAnimationFrame.$resolve() //teardown + return mock.location.pathname == "/test2" && root.childNodes[0].nodeValue === "foo" + }) + test(function() { + mock.requestAnimationFrame.$resolve() //setup + mock.location.hash = "#" + + var root = mock.document.createElement("div") + m.route.mode = "hash" + m.route(root, "/test3", { + "/test3": {controller: function() {}, view: function() {return "foo"}} + }) + mock.requestAnimationFrame.$resolve() //teardown + return mock.location.hash == "#/test3" && root.childNodes[0].nodeValue === "foo" + }) + test(function() { + mock.requestAnimationFrame.$resolve() //setup + mock.location.search = "?" + + var root = mock.document.createElement("div") + m.route.mode = "search" + m.route(root, "/test4/foo", { + "/test4/:test": {controller: function() {}, view: function() {return m.route.param("test")}} + }) + mock.requestAnimationFrame.$resolve() //teardown + return mock.location.search == "?/test4/foo" && root.childNodes[0].nodeValue === "foo" + }) + test(function() { + mock.requestAnimationFrame.$resolve() //setup + mock.location.search = "?" + + var module = {controller: function() {}, view: function() {return m.route.param("test")}} + + var root = mock.document.createElement("div") + m.route.mode = "search" + m.route(root, "/test5/foo", { + "/": module, + "/test5/:test": module + }) + var paramValueBefore = m.route.param("test") + mock.requestAnimationFrame.$resolve() + m.route("/") + var paramValueAfter = m.route.param("test") + mock.requestAnimationFrame.$resolve() //teardown + return mock.location.search == "?/" && paramValueBefore === "foo" && paramValueAfter === undefined + }) + test(function() { + mock.requestAnimationFrame.$resolve() //setup + mock.location.search = "?" + + var module = {controller: function() {}, view: function() {return m.route.param("a1")}} + + var root = mock.document.createElement("div") + m.route.mode = "search" + m.route(root, "/test6/foo", { + "/": module, + "/test6/:a1": module + }) + var paramValueBefore = m.route.param("a1") + mock.requestAnimationFrame.$resolve() + m.route("/") + var paramValueAfter = m.route.param("a1") + mock.requestAnimationFrame.$resolve() //teardown + return mock.location.search == "?/" && paramValueBefore === "foo" && paramValueAfter === undefined + }) + test(function() { + //https://github.com/lhorie/mithril.js/issues/61 + mock.requestAnimationFrame.$resolve() //setup + mock.location.search = "?" + + var module = {controller: function() {}, view: function() {return m.route.param("a1")}} + + var root = mock.document.createElement("div") + m.route.mode = "search" + m.route(root, "/test7/foo", { + "/": module, + "/test7/:a1": module + }) + var routeValueBefore = m.route() + mock.requestAnimationFrame.$resolve() + m.route("/") + var routeValueAfter = m.route() + mock.requestAnimationFrame.$resolve() //teardown + return routeValueBefore === "/test7/foo" && routeValueAfter === "/" + }) + test(function() { + mock.requestAnimationFrame.$resolve() //setup + mock.location.search = "?" + + var root = mock.document.createElement("div") + m.route.mode = "search" + m.route(root, "/test8/foo/SEP/bar/baz", { + "/test8/:test/SEP/:path...": { + controller: function() {}, + view: function() { + return m.route.param("test") + "_" + m.route.param("path") + } + } + }) + mock.requestAnimationFrame.$resolve() //teardown + return mock.location.search == "?/test8/foo/SEP/bar/baz" && root.childNodes[0].nodeValue === "foo_bar/baz" + }) + test(function() { + mock.requestAnimationFrame.$resolve() //setup + mock.location.search = "?" + + var root = mock.document.createElement("div") + m.route.mode = "search" + m.route(root, "/test9/foo/bar/SEP/baz", { + "/test9/:test.../SEP/:path": { + controller: function() {}, + view: function() { + return m.route.param("test") + "_" + m.route.param("path") + } + } + }) + mock.requestAnimationFrame.$resolve() //teardown + return mock.location.search == "?/test9/foo/bar/SEP/baz" && root.childNodes[0].nodeValue === "foo/bar_baz" + }) + test(function() { + mock.requestAnimationFrame.$resolve() //setup + mock.location.search = "?" + + var root = mock.document.createElement("div") + m.route.mode = "search" + m.route(root, "/test10/foo%20bar", { + "/test10/:test": { + controller: function() {}, + view: function() { + return m.route.param("test") + } + } + }) + mock.requestAnimationFrame.$resolve() //teardown + return root.childNodes[0].nodeValue === "foo bar" + }) + test(function() { + mock.requestAnimationFrame.$resolve() //setup + mock.location.search = "?" + + var root = mock.document.createElement("div") + m.route.mode = "search" + m.route(root, "/", { + "/": {controller: function() {}, view: function() {return "foo"}}, + "/test11": {controller: function() {}, view: function() {return "bar"}} + }) + mock.requestAnimationFrame.$resolve() + m.route("/test11/") + mock.requestAnimationFrame.$resolve() //teardown + return mock.location.search == "?/test11/" && root.childNodes[0].nodeValue === "bar" + }) + test(function() { + mock.requestAnimationFrame.$resolve() //setup + mock.location.search = "?" + + var root = mock.document.createElement("div") + m.route.mode = "search" + m.route(root, "/", { + "/": {controller: function() {}, view: function() {}}, + "/test12": {controller: function() {}, view: function() {}} + }) + mock.requestAnimationFrame.$resolve() + m.route("/test12?a=foo&b=bar") + mock.requestAnimationFrame.$resolve() //teardown + return mock.location.search == "?/test12?a=foo&b=bar" && m.route.param("a") == "foo" && m.route.param("b") == "bar" + }) + test(function() { + mock.requestAnimationFrame.$resolve() //setup + mock.location.search = "?" + + var root = mock.document.createElement("div") + m.route.mode = "search" + m.route(root, "/", { + "/": {controller: function() {}, view: function() {return "bar"}}, + "/test13/:test": {controller: function() {}, view: function() {return m.route.param("test")}} + }) + mock.requestAnimationFrame.$resolve() + m.route("/test13/foo?test=bar") + mock.requestAnimationFrame.$resolve() //teardown + return mock.location.search == "?/test13/foo?test=bar" && root.childNodes[0].nodeValue === "foo" + }) + test(function() { + mock.requestAnimationFrame.$resolve() //setup + mock.location.search = "?" + + var root = mock.document.createElement("div") + m.route.mode = "search" + m.route(root, "/", { + "/": {controller: function() {}, view: function() {return "bar"}}, + "/test14": {controller: function() {}, view: function() {return "foo"}} + }) + mock.requestAnimationFrame.$resolve() + m.route("/test14?test&test2=") + mock.requestAnimationFrame.$resolve() //teardown + return mock.location.search == "?/test14?test&test2=" && m.route.param("test") === true && m.route.param("test2") === "" + }) + test(function() { + mock.requestAnimationFrame.$resolve() //setup + mock.location.search = "?" + + var root = mock.document.createElement("div") + m.route.mode = "search" + m.route(root, "/", { + "/": {controller: function() {}, view: function() {}}, + "/test12": {controller: function() {}, view: function() {}} + }) + mock.requestAnimationFrame.$resolve() + m.route("/test12", {a: "foo", b: "bar"}) + mock.requestAnimationFrame.$resolve() //teardown + return mock.location.search == "?/test12?a=foo&b=bar" && m.route.param("a") == "foo" && m.route.param("b") == "bar" + }) + test(function() { + mock.requestAnimationFrame.$resolve() //setup + mock.location.search = "?" + + var root = mock.document.createElement("div") + var route1, route2 + m.route.mode = "search" + m.route(root, "/", { + "/": {controller: function() {route1 = m.route()}, view: function() {}}, + "/test13": {controller: function() {route2 = m.route()}, view: function() {}} + }) + mock.requestAnimationFrame.$resolve() + m.route("/test13") + mock.requestAnimationFrame.$resolve() //teardown + return route1 == "/" && route2 == "/test13" + }) + test(function() { + mock.requestAnimationFrame.$resolve() //setup + mock.location.search = "?" + + var root = mock.document.createElement("div") + var unloaded = 0 + m.route.mode = "search" + m.route(root, "/", { + "/": { + controller: function() {}, + view: function() { + return m("div", { + config: function(el, init, ctx) { + ctx.onunload = function() { + unloaded++ + } + } + }) + } + }, + "/test14": {controller: function() {}, view: function() {}} + }) + mock.requestAnimationFrame.$resolve() + m.route("/test14") + mock.requestAnimationFrame.$resolve() //teardown + return unloaded == 1 + }) + test(function() { + mock.requestAnimationFrame.$resolve() //setup + mock.location.search = "?" + + var root = mock.document.createElement("div") + var unloaded = 0 + m.route.mode = "search" + m.route(root, "/", { + "/": { + controller: function() {}, + view: function() { + return [ + m("div"), + m("div", { + config: function(el, init, ctx) { + ctx.onunload = function() { + unloaded++ + } + } + }) + ] + } + }, + "/test15": { + controller: function() {}, + view: function() { + return [m("div")] + } + } + }) + mock.requestAnimationFrame.$resolve() + m.route("/test15") + mock.requestAnimationFrame.$resolve() //teardown + return unloaded == 1 + }) + test(function() { + mock.requestAnimationFrame.$resolve() //setup + mock.location.search = "?" + + var root = mock.document.createElement("div") + var unloaded = 0 + m.route.mode = "search" + m.route(root, "/", { + "/": { + controller: function() {}, + view: function() { + return m("div", { + config: function(el, init, ctx) { + ctx.onunload = function() { + unloaded++ + } + } + }) + } + }, + "/test16": { + controller: function() {}, + view: function() { + return m("a") + } + } + }) + mock.requestAnimationFrame.$resolve() + m.route("/test16") + mock.requestAnimationFrame.$resolve() //teardown + return unloaded == 1 + }) + test(function() { + mock.requestAnimationFrame.$resolve() //setup + mock.location.search = "?" + + var root = mock.document.createElement("div") + var unloaded = 0 + m.route.mode = "search" + m.route(root, "/", { + "/": { + controller: function() {}, + view: function() { + return [ + m("div", { + config: function(el, init, ctx) { + ctx.onunload = function() { + unloaded++ + } + } + }) + ] + } + }, + "/test17": { + controller: function() {}, + view: function() { + return m("a") + } + } + }) + mock.requestAnimationFrame.$resolve() + m.route("/test17") + mock.requestAnimationFrame.$resolve() //teardown + return unloaded == 1 + }) + test(function() { + mock.requestAnimationFrame.$resolve() //setup + mock.location.search = "?" + + var root = mock.document.createElement("div") + var unloaded = 0 + m.route.mode = "search" + m.route(root, "/", { + "/": { + controller: function() {}, + view: function() { + return m("div", { + config: function(el, init, ctx) { + ctx.onunload = function() { + unloaded++ + } + } + }) + } + }, + "/test18": { + controller: function() {}, + view: function() { + return [m("a")] + } + } + }) + mock.requestAnimationFrame.$resolve() + m.route("/test18") + mock.requestAnimationFrame.$resolve() //teardown + return unloaded == 1 + }) + test(function() { + mock.requestAnimationFrame.$resolve() //setup + mock.location.search = "?" + + var root = mock.document.createElement("div") + var unloaded = 0 + m.route.mode = "search" + m.route(root, "/", { + "/": { + controller: function() {}, + view: function() { + return [ + m("div", { + key: 1, + config: function(el, init, ctx) { + ctx.onunload = function() { + unloaded++ + } + } + }) + ] + } + }, + "/test20": { + controller: function() {}, + view: function() { + return [ + m("div", { + key: 2, + config: function(el, init, ctx) { + ctx.onunload = function() { + unloaded++ + } + } + }) + ] + } + } + }) + mock.requestAnimationFrame.$resolve() + m.route("/test20") + mock.requestAnimationFrame.$resolve() //teardown + return unloaded == 1 + }) + test(function() { + mock.requestAnimationFrame.$resolve() //setup + mock.location.search = "?" + + var root = mock.document.createElement("div") + var unloaded = 0 + m.route.mode = "search" + m.route(root, "/", { + "/": { + controller: function() {}, + view: function() { + return [ + m("div", { + key: 1, + config: function(el, init, ctx) { + ctx.onunload = function() { + unloaded++ + } + } + }) + ] + } + }, + "/test21": { + controller: function() {}, + view: function() { + return [ + m("div", { + config: function(el, init, ctx) { + ctx.onunload = function() { + unloaded++ + } + } + }) + ] + } + } + }) + mock.requestAnimationFrame.$resolve() + m.route("/test21") + mock.requestAnimationFrame.$resolve() //teardown + return unloaded == 1 + }) + test(function() { + mock.requestAnimationFrame.$resolve() //setup + mock.location.search = "?" + + var root = mock.document.createElement("div") + m.route.mode = "search" + m.route(root, "/foo", { + "/foo": { + controller: function() {}, + view: function() { + return m("div", "foo"); + } + }, + "/bar": { + controller: function() {}, + view: function() { + return m("div", "bar"); + } + }, + }) + mock.requestAnimationFrame.$resolve() + var foo = root.childNodes[0].childNodes[0].nodeValue; + m.route("/bar") + mock.requestAnimationFrame.$resolve() //teardown + var bar = root.childNodes[0].childNodes[0].nodeValue; + return (foo === "foo" && bar === "bar") + }) + test(function() { + mock.requestAnimationFrame.$resolve() //setup + mock.location.search = "?" + + var root = mock.document.createElement("div") + var unloaded = 0 + var config = function(el, init, ctx) { + ctx.onunload = function() { + unloaded++ + } + } + m.route.mode = "search" + m.route(root, "/foo1", { + "/foo1": { + controller: function() {}, + view: function() { + return m("div", m("a", {config: config}, "foo")); + } + }, + "/bar1": { + controller: function() {}, + view: function() { + return m("main", m("a", {config: config}, "foo")); + } + }, + }) + mock.requestAnimationFrame.$resolve() + m.route("/bar1") + mock.requestAnimationFrame.$resolve() //teardown + return unloaded == 1 + }) + test(function() { + mock.requestAnimationFrame.$resolve() //setup + mock.location.search = "?" + + var root = mock.document.createElement("div") + var strategy + m.route.mode = "search" + m.route(root, "/foo1", { + "/foo1": { + controller: function() { + strategy = m.redraw.strategy() + m.redraw.strategy("none") + }, + view: function() { + return m("div"); + } + } + }) + mock.requestAnimationFrame.$resolve() //teardown + return strategy == "all" && root.childNodes.length == 0 + }) + test(function() { + mock.requestAnimationFrame.$resolve() //setup + mock.location.search = "?" + + var root = mock.document.createElement("div") + var strategy, count = 0 + var config = function(el, init) {if (!init) count++} + m.route.mode = "search" + m.route(root, "/foo1", { + "/foo1": { + controller: function() {}, + view: function() { + return m("div", {config: config}); + } + }, + "/bar1": { + controller: function() { + strategy = m.redraw.strategy() + m.redraw.strategy("redraw") + }, + view: function() { + return m("div", {config: config}); + } + }, + }) + mock.requestAnimationFrame.$resolve() + m.route("/bar1") + mock.requestAnimationFrame.$resolve() //teardown + return strategy == "all" && count == 1 + }) + test(function() { + mock.requestAnimationFrame.$resolve() //setup + mock.location.search = "?" + + var root = mock.document.createElement("div") + var strategy + m.route.mode = "search" + m.route(root, "/foo1", { + "/foo1": { + controller: function() {this.number = 1}, + view: function(ctrl) { + return m("div", {onclick: function() { + strategy = m.redraw.strategy() + ctrl.number++ + m.redraw.strategy("none") + }}, ctrl.number); + } + } + }) + root.childNodes[0].onclick({}) + return strategy == "diff" && root.childNodes[0].childNodes[0].nodeValue == "1" + }) + test(function() { + mock.requestAnimationFrame.$resolve() //setup + mock.location.search = "?" + + var root = mock.document.createElement("div") + var count = 0 + var config = function(el, init ) {if (!init) count++} + m.route.mode = "search" + m.route(root, "/foo1", { + "/foo1": { + controller: function() {}, + view: function(ctrl) { + return m("div", {config: config, onclick: function() { + m.redraw.strategy("all") + }}); + } + } + }) + root.childNodes[0].onclick({}) + mock.requestAnimationFrame.$resolve() //teardown + return count == 2 + }) + test(function() { + mock.requestAnimationFrame.$resolve() //setup + mock.location.search = "?" + + var root = mock.document.createElement("div") + var value + m.route(root, "/foo+bar", { + "/:arg": { + controller: function() {value = m.route.param("arg")}, + view: function(ctrl) { + return "" + } + } + }) + return value == "foo+bar" + }) + //end m.route + + //m.prop + test(function() { + var prop = m.prop("test") + return prop() === "test" + }) + test(function() { + var prop = m.prop("test") + prop("foo") + return prop() === "foo" + }) + test(function() { + var prop = m.prop("test") + return JSON.stringify(prop) === '"test"' + }) + test(function() { + var obj = {prop: m.prop("test")} + return JSON.stringify(obj) === '{"prop":"test"}' + }) + test(function() { + var defer = m.deferred() + var prop = m.prop(defer.promise) + defer.resolve("test") + + return prop() === "test" + }) + test(function() { + var defer = m.deferred() + var prop = m.prop(defer.promise).then(function () { + return "test2" + }) + defer.resolve("test") + + return prop() === "test2" + }) + + //m.request + test(function() { + var prop = m.request({method: "GET", url: "test"}) + mock.XMLHttpRequest.$instances.pop().onreadystatechange() + return prop().method === "GET" && prop().url === "test" + }) + test(function() { + var prop = m.request({method: "GET", url: "test"}).then(function(value) {return "foo"}) + mock.XMLHttpRequest.$instances.pop().onreadystatechange() + return prop() === "foo" + }) + test(function() { + var prop = m.request({method: "POST", url: "http://domain.com:80", data: {}}).then(function(value) {return value}) + mock.XMLHttpRequest.$instances.pop().onreadystatechange() + return prop().url === "http://domain.com:80" + }) + test(function() { + var prop = m.request({method: "POST", url: "http://domain.com:80/:test1", data: {test1: "foo"}}).then(function(value) {return value}) + mock.XMLHttpRequest.$instances.pop().onreadystatechange() + return prop().url === "http://domain.com:80/foo" + }) + test(function() { + var error = m.prop("no error") + var prop = m.request({method: "GET", url: "test", deserialize: function() {throw new Error("error occurred")}}).then(null, error) + mock.XMLHttpRequest.$instances.pop().onreadystatechange() + return prop().message === "error occurred" && error().message === "error occurred" + }) + test(function() { + var error = m.prop("no error"), exception + var prop = m.request({method: "GET", url: "test", deserialize: function() {throw new TypeError("error occurred")}}).then(null, error) + try {mock.XMLHttpRequest.$instances.pop().onreadystatechange()} + catch (e) {exception = e} + m.endComputation() + return prop() === undefined && error() === "no error" && exception.message == "error occurred" + }) + test(function() { + var error = m.prop("no error") + var prop = m.request({method: "POST", url: "test"}).then(null, error) + var xhr = mock.XMLHttpRequest.$instances.pop() + xhr.onreadystatechange() + return xhr.$headers["Content-Type"] == "application/json; charset=utf-8" + }) + + // m.request over jsonp + test(function(){ + // script tags cannot be appended directly on the document + var body = mock.document.createElement("body"); + mock.document.body = body; + mock.document.appendChild(body); + + var _window = mock; + var error = m.prop("no error"); + var req = m.request({ + url: "/test", + dataType: "jsonp", + background: true + }).then(null, error); + var callbackKeys = []; + Object.keys(_window).forEach(function(globalKey){ + if(globalKey.indexOf("mithril_callback") > -1) + callbackKeys.push(globalKey); + }); + var foundScriptTag = false, scriptTag = null; + mock.document.getElementsByTagName("script").forEach(function(script){ + if(!scriptTag && script.src.indexOf(callbackKeys[0]) > -1) + scriptTag = script; + }); + if(scriptTag){ + foundScriptTag = true; + delete _window[callbackKeys[0]]; + mock.document.body.removeChild(scriptTag); + } + mock.document.removeChild(body); + return callbackKeys.length === 1 && foundScriptTag; + }) + test(function(){ + var body = mock.document.createElement("body"); + mock.document.body = body; + mock.document.appendChild(body); + + var _window = mock; + var error = m.prop(false); + var req = m.request({ + url: "/test", + dataType: "jsonp", + background: true, + callbackKey: "jsonpCallback" + }).then(null, error); + var callbackKeys = []; + Object.keys(_window).forEach(function(globalKey){ + if(globalKey.indexOf("mithril_callback") > -1) + callbackKeys.push(globalKey); + }); + var scriptTag = null; + mock.document.getElementsByTagName("script").forEach(function(script){ + if(!scriptTag && script.src.indexOf(callbackKeys[0]) > -1) + scriptTag = script; + }); + var correctCallback = false; + if(scriptTag){ + correctCallback = scriptTag.src.indexOf("jsonpCallback") > -1; + delete _window[callbackKeys[0]]; + mock.document.body.removeChild(scriptTag); + } + mock.document.removeChild(body); + return correctCallback; + }) + test(function(){ + var body = mock.document.createElement("body"); + mock.document.body = body; + mock.document.appendChild(body); + + var _window = mock; + var req = m.request({ + url: "/test", + dataType: "jsonp", + background: true + }); + var callbackKeys = []; + Object.keys(_window).forEach(function(globalKey){ + if(globalKey.indexOf("mithril_callback") > -1) + callbackKeys.push(globalKey); + }); + var scriptTag = null; + mock.document.getElementsByTagName("script").forEach(function(script){ + if(!scriptTag && script.src.indexOf(callbackKeys[0]) > -1) + scriptTag = script; + }); + var out = { foo: "bar" }; + if(scriptTag && callbackKeys.length > 0){ + _window[callbackKeys[0]](out); + mock.document.body.removeChild(scriptTag); + delete _window[callbackKeys[0]]; + } + mock.document.removeChild(body); + return JSON.stringify(out) === JSON.stringify(req()); + }) + test(function(){ + var body = mock.document.createElement("body"); + mock.document.body = body; + mock.document.appendChild(body); + + var _window = mock; + var error = m.prop(false); + var req = m.request({ + url: "/test", + dataType: "jsonp", + data: { foo: "bar" }, + background: true + }); + var callbackKeys = []; + Object.keys(_window).forEach(function(globalKey){ + if(globalKey.indexOf("mithril_callback") > -1) + callbackKeys.push(globalKey); + }); + var scriptTag = null; + mock.document.getElementsByTagName("script").forEach(function(script){ + if(!scriptTag && script.src.indexOf(callbackKeys[0]) > -1) + scriptTag = script; + }); + var correctData = false; + if(scriptTag){ + correctData = scriptTag.src.indexOf("foo=bar") > -1; + mock.document.body.removeChild(scriptTag); + delete _window[callbackKeys[0]]; + } + mock.document.removeChild(body); + return correctData; + }) + + //m.deferred + test(function() { + var value + var deferred = m.deferred() + deferred.promise.then(function(data) {value = data}) + deferred.resolve("test") + return value === "test" + }) + test(function() { + var value + var deferred = m.deferred() + deferred.promise.then(function(data) {return "foo"}).then(function(data) {value = data}) + deferred.resolve("test") + return value === "foo" + }) + test(function() { + var value + var deferred = m.deferred() + deferred.promise.then(null, function(data) {value = data}) + deferred.reject("test") + return value === "test" + }) + test(function() { + var value + var deferred = m.deferred() + deferred.promise.then(null, function(data) {return "foo"}).then(function(data) {value = data}) + deferred.reject("test") + return value === "foo" + }) + test(function() { + var value1, value2 + var deferred = m.deferred() + deferred.promise.then(function(data) {throw new Error}).then(function(data) {value1 = 1}, function(data) {value2 = data}) + deferred.resolve("test") + return value1 === undefined && value2 instanceof Error + }) + test(function() { + var deferred1 = m.deferred() + var deferred2 = m.deferred() + var value1, value2 + deferred1.promise.then(function(data) { + value1 = data + return deferred2.promise + }).then(function(data) { + value2 = data + }) + deferred1.resolve(1) + deferred2.resolve(2) + return value1 === 1 && value2 === 2 + }) + test(function() { + //https://github.com/lhorie/mithril.js/issues/80 + var deferred = m.deferred(), value + deferred.resolve(1) + deferred.promise.then(function(data) { + value = data + }) + return value === 1 + }) + test(function() { + //https://github.com/lhorie/mithril.js/issues/80 + var deferred = m.deferred(), value + deferred.reject(1) + deferred.promise.then(null, function(data) { + value = data + }) + return value === 1 + }) + test(function() { + //https://github.com/lhorie/mithril.js/issues/80 + var deferred = m.deferred(), value + deferred.resolve(1) + deferred.resolve(2) + deferred.promise.then(function(data) { + value = data + }) + return value === 1 + }) + test(function() { + //https://github.com/lhorie/mithril.js/issues/80 + var deferred = m.deferred(), value + deferred.promise.then(function(data) { + value = data + }) + deferred.resolve(1) + deferred.resolve(2) + return value === 1 + }) + test(function() { + //https://github.com/lhorie/mithril.js/issues/80 + var deferred = m.deferred(), value1, value2 + deferred.promise.then(function(data) { + value1 = data + }, function(data) { + value2 = data + }) + deferred.resolve(1) + deferred.reject(2) + return value1 === 1 && value2 === undefined + }) + test(function() { + //https://github.com/lhorie/mithril.js/issues/80 + var deferred = m.deferred(), value1, value2 + deferred.promise.then(function() { + value1 = data + }, function(data) { + value2 = data + }) + deferred.reject(1) + deferred.resolve(2) + return value1 === undefined && value2 === 1 + }) + test(function() { + //https://github.com/lhorie/mithril.js/issues/80 + var deferred = m.deferred(), value + deferred.promise.then(null, function(data) { + value = data + }) + deferred.reject(1) + deferred.reject(2) + return value === 1 + }) + test(function() { + //https://github.com/lhorie/mithril.js/issues/85 + var deferred = m.deferred(), value + deferred.resolve() + deferred.promise.then(function(data) { + value = 1 + }) + return value === 1 + }) + test(function() { + //https://github.com/lhorie/mithril.js/issues/85 + var deferred = m.deferred(), value + deferred.reject() + deferred.promise.then(null, function(data) { + value = 1 + }) + return value === 1 + }) + test(function() { + var deferred = m.deferred(), value + deferred.resolve(1) + return deferred.promise() === 1 + }) + test(function() { + var deferred = m.deferred(), value + var promise = deferred.promise.then(function(data) {return data + 1}) + deferred.resolve(1) + return promise() === 2 + }) + test(function() { + var deferred = m.deferred(), value + deferred.reject(1) + return deferred.promise() === undefined + }) + + //m.sync + test(function() { + var value + var deferred1 = m.deferred() + var deferred2 = m.deferred() + m.sync([deferred1.promise, deferred2.promise]).then(function(data) {value = data}) + deferred1.resolve("test") + deferred2.resolve("foo") + return value[0] === "test" && value[1] === "foo" + }) + test(function() { + var value + var deferred1 = m.deferred() + var deferred2 = m.deferred() + m.sync([deferred1.promise, deferred2.promise]).then(function(data) {value = data}) + deferred2.resolve("foo") + deferred1.resolve("test") + return value[0] === "test" && value[1] === "foo" + }) + test(function() { + var value = 1 + m.sync([]).then(function() {value = 2}) + return value == 2 + }) + + //m.startComputation/m.endComputation + test(function() { + mock.requestAnimationFrame.$resolve() + + var controller + var root = mock.document.createElement("div") + m.module(root, { + controller: function() {controller = this}, + view: function(ctrl) {return ctrl.value} + }) + + mock.requestAnimationFrame.$resolve() + + m.startComputation() + controller.value = "foo" + m.endComputation() + mock.requestAnimationFrame.$resolve() + + return root.childNodes[0].nodeValue === "foo" + }) + + //console.log presence + test(function() { + return m.deps.factory.toString().indexOf("console") < 0 + }) + + // config context + test(function() { + var root = mock.document.createElement("div") + + var success = false; + m.render(root, m("div", {config: function(elem, isInitialized, ctx) {ctx.data=1}})); + m.render(root, m("div", {config: function(elem, isInitialized, ctx) {success = ctx.data===1}})); + return success; + }) + + // more complex config context + test(function() { + var root = mock.document.createElement("div") + + var idx = 0; + var success = true; + var statefulConfig = function(elem, isInitialized, ctx) {ctx.data=idx++} + var node = m("div", {config: statefulConfig}); + m.render(root, [node, node]); + + idx = 0; + var checkConfig = function(elem, isInitialized, ctx) { + success = success && (ctx.data === idx++) + } + node = m("div", {config: checkConfig}); + m.render(root, [node, node]); + return success; + }) + +} + +//test reporting for saucelabs +if (typeof window != "undefined") { + window.global_test_results = { + tests: [], + duration: 0, + passed: 0, + failed: 0 + }; +} + +//mock +testMithril(mock.window); + +test.print(function(value) {console.log(value)}) diff --git a/mithril.js b/mithril.js index 8af1bc28..b55d4c76 100644 --- a/mithril.js +++ b/mithril.js @@ -740,24 +740,102 @@ Mithril = m = new function app(window, undefined) { } function identity(value) {return value} - function ajax(options) { - var xhr = new window.XMLHttpRequest - xhr.open(options.method, options.url, true, options.user, options.password) - xhr.onreadystatechange = function() { - if (xhr.readyState === 4) { - if (xhr.status >= 200 && xhr.status < 300) options.onload({type: "load", target: xhr}) - else options.onerror({type: "error", target: xhr}) + function serializeArray(array, prefix){ + var idx, out = []; + for(idx in array){ + var formatted = (prefix ? prefix : "") + "[]"; + if(prefix && typeof array[idx] === "object") + formatted = formatted.replace(/\[\]$/i, "[" + idx + "]"); + if(typeof array[idx] === "object" && JSON.stringify(array[idx]) === "{}"){ + continue; } + if(array[idx] instanceof Array) + out.push(serializeArray(array[idx], formatted)); + else if(typeof array[idx] === "object") + out.push(serializeObject(array[idx], formatted)); + else + out.push(encodeURIComponent(formatted) + "=" + encodeURIComponent(array[idx])); } - if (options.serialize == JSON.stringify && options.method != "GET") { - xhr.setRequestHeader("Content-Type", "application/json; charset=utf-8"); + return out.join("&"); + } + + function serializeObject(obj, prefix) { + var key, out = []; + for(key in obj){ + var formatted = prefix ? prefix + "[" + key + "]" : key; + if(obj[key] instanceof Array){ + if(obj[key].length < 1) + continue; + out.push(serializeArray(obj[key], formatted)); + }else if(typeof obj[key] === "object"){ + if(JSON.stringify(obj[key]) === "{}") + continue; + out.push(serializeObject(obj[key], formatted)); + }else{ + out.push(encodeURIComponent(formatted) + "=" + encodeURIComponent(obj[key])); + } + }; + return out.join("&"); + } + + function ajax(options) { + if (options.dataType && options.dataType.toLowerCase() === "jsonp") { + var callbackKey = "mithril_callback_" + new Date().getTime() + "_" + (Math.round(Math.random() * 1e16)).toString(36); + var script = window.document.createElement("script"); + + window[callbackKey] = function(resp){ + delete window[callbackKey]; + window.document.body.removeChild(script); + options.onload({ type: "load", target: { + responseText: resp + } }); + }; + + script.onerror = function(e){ + delete window[callbackKey]; + window.document.body.removeChild(script); + + options.onerror({ type: "error", target: { + status: 500, + responseText: JSON.stringify({ error: "Error making jsonp request" }) + } }); + + e.preventDefault(); + e.stopPropagation(); + }; + + script.onload = function(e){ + e.preventDefault(); + e.stopPropagation(); + }; + + + script.src = options.url + + (options.url.indexOf("?") > 0 ? "&" : "?") + + (options.callbackKey ? options.callbackKey : "callback") + + "=" + callbackKey + + "&" + serializeObject(options.data || {}); + window.document.body.appendChild(script); + }else{ + var xhr = new window.XMLHttpRequest + xhr.open(options.method, options.url, true, options.user, options.password) + xhr.onreadystatechange = function() { + if (xhr.readyState === 4) { + if (xhr.status >= 200 && xhr.status < 300) options.onload({type: "load", target: xhr}) + else options.onerror({type: "error", target: xhr}) + } + } + if (options.serialize == JSON.stringify && options.method != "GET") { + xhr.setRequestHeader("Content-Type", "application/json; charset=utf-8"); + } + if (typeof options.config == "function") { + var maybeXhr = options.config(xhr, options) + if (maybeXhr != null) xhr = maybeXhr + } + + xhr.send(options.method == "GET" ? "" : options.data) + return xhr } - if (typeof options.config == "function") { - var maybeXhr = options.config(xhr, options) - if (maybeXhr != null) xhr = maybeXhr - } - xhr.send(options.method == "GET" ? "" : options.data) - return xhr } function bindData(xhrOptions, data, serialize) { if (data && Object.keys(data).length > 0) { @@ -783,8 +861,10 @@ Mithril = m = new function app(window, undefined) { m.request = function(xhrOptions) { if (xhrOptions.background !== true) m.startComputation() var deferred = m.deferred() - var serialize = xhrOptions.serialize = xhrOptions.serialize || JSON.stringify - var deserialize = xhrOptions.deserialize = xhrOptions.deserialize || JSON.parse + var serialize = xhrOptions.serialize = xhrOptions.dataType && xhrOptions.dataType.toLowerCase() === "jsonp" + ? identity : xhrOptions.serialize || JSON.stringify + var deserialize = xhrOptions.deserialize = xhrOptions.dataType && xhrOptions.dataType.toLowerCase() === "jsonp" + ? identity : xhrOptions.deserialize || JSON.parse var extract = xhrOptions.extract || function(xhr) { return xhr.responseText.length === 0 && deserialize === JSON.parse ? null : xhr.responseText } diff --git a/tests/mithril-tests.js b/tests/mithril-tests.js index 7d7687c2..71ebeb0b 100644 --- a/tests/mithril-tests.js +++ b/tests/mithril-tests.js @@ -1545,6 +1545,133 @@ function testMithril(mock) { xhr.onreadystatechange() return xhr.$headers["Content-Type"] == "application/json; charset=utf-8" }) + + // m.request over jsonp + test(function(){ + // script tags cannot be appended directly on the document + var body = mock.document.createElement("body"); + mock.document.body = body; + mock.document.appendChild(body); + + var _window = mock; + var error = m.prop("no error"); + var req = m.request({ + url: "/test", + dataType: "jsonp", + background: true + }).then(null, error); + var callbackKeys = []; + Object.keys(_window).forEach(function(globalKey){ + if(globalKey.indexOf("mithril_callback") > -1) + callbackKeys.push(globalKey); + }); + var foundScriptTag = false, scriptTag = null; + mock.document.getElementsByTagName("script").forEach(function(script){ + if(!scriptTag && script.src.indexOf(callbackKeys[0]) > -1) + scriptTag = script; + }); + if(scriptTag){ + foundScriptTag = true; + delete _window[callbackKeys[0]]; + mock.document.body.removeChild(scriptTag); + } + mock.document.removeChild(body); + return callbackKeys.length === 1 && foundScriptTag; + }) + test(function(){ + var body = mock.document.createElement("body"); + mock.document.body = body; + mock.document.appendChild(body); + + var _window = mock; + var error = m.prop(false); + var req = m.request({ + url: "/test", + dataType: "jsonp", + background: true, + callbackKey: "jsonpCallback" + }).then(null, error); + var callbackKeys = []; + Object.keys(_window).forEach(function(globalKey){ + if(globalKey.indexOf("mithril_callback") > -1) + callbackKeys.push(globalKey); + }); + var scriptTag = null; + mock.document.getElementsByTagName("script").forEach(function(script){ + if(!scriptTag && script.src.indexOf(callbackKeys[0]) > -1) + scriptTag = script; + }); + var correctCallback = false; + if(scriptTag){ + correctCallback = scriptTag.src.indexOf("jsonpCallback") > -1; + delete _window[callbackKeys[0]]; + mock.document.body.removeChild(scriptTag); + } + mock.document.removeChild(body); + return correctCallback; + }) + test(function(){ + var body = mock.document.createElement("body"); + mock.document.body = body; + mock.document.appendChild(body); + + var _window = mock; + var req = m.request({ + url: "/test", + dataType: "jsonp", + background: true + }); + var callbackKeys = []; + Object.keys(_window).forEach(function(globalKey){ + if(globalKey.indexOf("mithril_callback") > -1) + callbackKeys.push(globalKey); + }); + var scriptTag = null; + mock.document.getElementsByTagName("script").forEach(function(script){ + if(!scriptTag && script.src.indexOf(callbackKeys[0]) > -1) + scriptTag = script; + }); + var out = { foo: "bar" }; + if(scriptTag && callbackKeys.length > 0){ + _window[callbackKeys[0]](out); + mock.document.body.removeChild(scriptTag); + delete _window[callbackKeys[0]]; + } + mock.document.removeChild(body); + return JSON.stringify(out) === JSON.stringify(req()); + }) + test(function(){ + var body = mock.document.createElement("body"); + mock.document.body = body; + mock.document.appendChild(body); + + var _window = mock; + var error = m.prop(false); + var req = m.request({ + url: "/test", + dataType: "jsonp", + data: { foo: "bar" }, + background: true + }); + var callbackKeys = []; + Object.keys(_window).forEach(function(globalKey){ + if(globalKey.indexOf("mithril_callback") > -1) + callbackKeys.push(globalKey); + }); + var scriptTag = null; + mock.document.getElementsByTagName("script").forEach(function(script){ + if(!scriptTag && script.src.indexOf(callbackKeys[0]) > -1) + scriptTag = script; + }); + var correctData = false; + if(scriptTag){ + correctData = scriptTag.src.indexOf("foo=bar") > -1; + mock.document.body.removeChild(scriptTag); + delete _window[callbackKeys[0]]; + } + mock.document.removeChild(body); + return correctData; + }) //m.deferred test(function() { diff --git a/tests/mock.js b/tests/mock.js index fe458649..fae3574e 100644 --- a/tests/mock.js +++ b/tests/mock.js @@ -70,6 +70,23 @@ mock.window = new function() { this.childNodes.splice(index, 1) child.parentNode = null } + window.document.getElementsByTagName = function(name){ + name = name.toLowerCase(); + var out = []; + + var traverse = function(node){ + if(node.childNodes && node.childNodes.length > 0){ + node.childNodes.forEach(function(curr){ + if(curr.nodeName.toLowerCase() === name) + out.push(curr); + traverse(curr); + }); + } + }; + + traverse(window.document); + return out; + } window.scrollTo = function() {} window.cancelAnimationFrame = function() {} window.requestAnimationFrame = function(callback) {